Spaces:
Running
on
Zero
Running
on
Zero
File size: 36,427 Bytes
9d4ae64 83aa561 0531be0 6c28396 8df4053 e0eea98 83aa561 8bf0664 e0eea98 2dd4885 83aa561 91a801a c0fb813 c1be18e 91a801a 1799f4b 91a801a 1799f4b 91a801a 1799f4b 91a801a 8df4053 91a801a e0eea98 91a801a 17cbe46 91a801a 74517ab 91a801a 74517ab 91a801a afc20d4 91a801a 9b2f647 1799f4b 9b2f647 91a801a e0eea98 91a801a 1799f4b 91a801a 1799f4b 91a801a 1799f4b 91a801a 1799f4b 039620a 1799f4b 039620a 1799f4b 039620a 1799f4b 91a801a 039620a 91a801a c214ec0 8388e80 1799f4b e0eea98 313e62a e0eea98 313e62a e0eea98 1799f4b e0eea98 1799f4b 313e62a 2c24bbf 91a801a 1799f4b 91a801a 9d4ae64 580e68f 1799f4b 580e68f 1799f4b 580e68f 1799f4b 9d4ae64 1799f4b 91a801a f896384 91a801a 643e04c cd8bafc 643e04c cd8bafc aa240c3 1799f4b aa240c3 cd8bafc 91a801a c417fc7 9d4ae64 c417fc7 91a801a 1799f4b 9d4ae64 91a801a 1799f4b 91a801a 1799f4b 643e04c 91a801a 1799f4b 91a801a 1799f4b 91a801a 1799f4b 91a801a 1799f4b 91a801a 1799f4b 91a801a 9d4ae64 91a801a cd8bafc 91a801a cd8bafc 1799f4b 7121f00 cd8bafc 91a801a 1799f4b 91a801a b20cbc9 91a801a b20cbc9 91a801a cd8bafc 91a801a cd8bafc 91a801a cd8bafc 2c24bbf 3b90904 cd8bafc e0eea98 91a801a 1799f4b 91a801a 3b90904 bfb9b80 89f9d38 210cb7f 91a801a 0531be0 91a801a 1799f4b 91a801a 1799f4b 91a801a 1799f4b 91a801a 210cb7f 91a801a 210cb7f 91a801a 1799f4b 91a801a 1799f4b 91a801a c8df84a 91a801a 83aa561 e6d37b7 83aa561 91a801a 1799f4b 2c24bbf c214ec0 b7dfb35 c214ec0 e0eea98 3296634 7f776d6 3b90904 7f776d6 1799f4b 3b90904 7f776d6 1799f4b 3b90904 7f776d6 1799f4b 7f776d6 1799f4b 7f776d6 3b90904 7f776d6 3b90904 1799f4b 7f776d6 1799f4b ca96a6b 3b90904 1799f4b ca96a6b 1799f4b ca96a6b 7f776d6 043831b a27912a 1799f4b a27912a 1799f4b a27912a 91a801a 1799f4b d3ab058 1799f4b d3ab058 2c24bbf 91a801a 1799f4b 91a801a 3b90904 1799f4b 3b90904 1799f4b 3b90904 1799f4b 97dd22d 83aa561 1799f4b 83aa561 91a801a 1799f4b 3b90904 2c24bbf 91a801a 1799f4b 91a801a 83aa561 1799f4b 91a801a 1799f4b e605b0c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 |
import json
import string
import uuid
import os
import logging
import zipfile
import importlib
import wandb
from contextlib import redirect_stdout, redirect_stderr
import spaces
USE_WANDB = "WANDB_API_KEY" in os.environ
if USE_WANDB:
wandb.login(key=os.environ["WANDB_API_KEY"])
else:
print("Warning: WANDB_API_KEY not set. Skipping wandb logging.")
import gradio as gr
import pandas as pd
import time
import sys
from datetime import datetime
import re
# --- Configuration ---
DEFAULT_MATERIALS_CSV = "default_materials.csv"
GRADIO_OUTPUT_BASE_DIR = "output"
os.makedirs(GRADIO_OUTPUT_BASE_DIR, exist_ok=True)
REQUIRED_SCRIPT_COLS = ["Brand", " Name", " TD", " Color"]
DISPLAY_COL_MAP = {
"Brand": "Brand",
" Name": "Name",
" TD": "TD",
" Color": "Color (Hex)",
}
def exc_text(exc: BaseException) -> str:
txt = str(exc).strip()
if txt:
return txt
if exc.args:
return " ".join(str(a) for a in exc.args).strip()
return exc.__class__.__name__
def ensure_required_cols(df, *, in_display_space):
target_cols = (
DISPLAY_COL_MAP if in_display_space else {k: k for k in REQUIRED_SCRIPT_COLS}
)
df_fixed = df.copy()
for col_script, col_display in target_cols.items():
if col_display not in df_fixed.columns:
if "TD" in col_display:
default = 0.0
elif "Color" in col_display:
default = "#000000"
elif "Owned" in col_display:
default = "false"
else:
default = ""
df_fixed[col_display] = default
return df_fixed[list(target_cols.values())]
def rgba_to_hex(col: str) -> str:
if not isinstance(col, str):
return col
col = col.strip()
if col.startswith("#"):
return col.upper()
m = re.match(
r"rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*[\d.]+)?\s*\)",
col,
)
if not m:
return col
r, g, b = (int(float(x)) for x in m.groups()[:3])
return "#{:02X}{:02X}{:02X}".format(r, g, b)
def zip_dir_no_compress(src_dir: str, dest_zip: str) -> str:
t0 = time.time()
with zipfile.ZipFile(dest_zip, "w",
compression=zipfile.ZIP_STORED,
allowZip64=True) as zf:
for root, _, files in os.walk(src_dir):
for fname in files:
fpath = os.path.join(root, fname)
zf.write(fpath, os.path.relpath(fpath, src_dir))
print(f"Zipping finished in {time.time() - t0:.1f}s")
return dest_zip
def get_script_args_info(exclude_args=None):
if exclude_args is None:
exclude_args = []
all_args_info = [
{
"name": "--iterations",
"type": "number",
"default": 4000,
"help": "Number of optimization iterations",
},
{
"name": "--layer_height",
"type": "number",
"default": 0.04,
"step": 0.01,
"help": "Layer thickness in mm",
},
{
"name": "--max_layers",
"type": "number",
"default": 75,
"precision": 0,
"help": "Maximum number of layers",
},
{
"name": "--learning_rate",
"type": "number",
"default": 0.015,
"step": 0.001,
"help": "Learning rate for optimization",
},
{
"name": "--background_height",
"type": "number",
"default": 0.4,
"step": 0.01,
"help": "Height of the background in mm",
},
{
"name": "--background_color",
"type": "colorpicker",
"default": "#000000",
"help": "Background color",
},
{
"name": "--stl_output_size",
"type": "number",
"default": 100,
"precision": 0,
"help": "Size of the longest dimension of the output STL file in mm",
},
{
"name": "--nozzle_diameter",
"type": "number",
"default": 0.4,
"step": 0.1,
"help": "Diameter of the printer nozzle in mm",
},
{
"name": "--pruning_max_colors",
"type": "number",
"default": 100,
"precision": 0,
"help": "Max number of colors allowed after pruning",
},
{
"name": "--pruning_max_swaps",
"type": "number",
"default": 50,
"precision": 0,
"help": "Max number of swaps allowed after pruning",
},
{
"name": "--pruning_max_layer",
"type": "number",
"default": 75,
"precision": 0,
"help": "Max number of layers allowed after pruning",
},
{
"name": "--warmup_fraction",
"type": "slider",
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"help": "Fraction of iterations for keeping the tau at the initial value",
},
{
"name": "--learning_rate_warmup_fraction",
"type": "slider",
"default": 0.01,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"help": "Fraction of iterations that the learning rate is increasing (warmup)",
},
{
"name": "--early_stopping",
"type": "number",
"default": 5000,
"precision": 0,
"help": "Number of steps without improvement before stopping",
},
{
"name": "--fast_pruning_percent",
"type": "slider",
"default": 0.05,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"help": "Percentage of increment search for fast pruning.",
},
{
"name": "--random_seed",
"type": "number",
"default": 0,
"precision": 0,
"help": "Specify the random seed, or use 0 for automatic generation",
},
{
"name": "--num_init_rounds",
"type": "number",
"default": 8,
"precision": 0,
"help": "Number of rounds to choose the starting height map from.",
},
]
return [arg for arg in all_args_info if arg["name"] not in exclude_args]
# initial data that will be used if no CSV exists
initial_filament_data = {
"Brand": ["Generic", "Generic", "Generic", "Generic", "Generic", "Generic"],
" Name": ["PLA Black", "PLA Grey", "PLA White", "PLA Red", "PLA Green", "PLA Blue"],
" TD": [5.0, 5.0, 5.0, 5.0, 5.0],
" Color": ["#000000", "#808080", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF"],
" Owned": ["true", "true", "true", "true", "true", "true"],
}
def normalize_filament_df(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df.columns = [c.strip() for c in df.columns]
rename_map = {
"Name": " Name",
"TD": " TD",
"Color": " Color",
"Owned": " Owned",
}
for src, dst in rename_map.items():
if src in df.columns and dst not in df.columns:
df.rename(columns={src: dst}, inplace=True)
if " TD" in df.columns:
df[" TD"] = pd.to_numeric(df[" TD"], errors="coerce").fillna(0.0)
else:
df[" TD"] = 0.0
if " Color" in df.columns:
df[" Color"] = df[" Color"].astype(str)
else:
df[" Color"] = "#000000"
if " Owned" not in df.columns:
df[" Owned"] = "false"
else:
df[" Owned"] = df[" Owned"].astype(str)
if "Brand" not in df.columns:
df["Brand"] = ""
ordered_cols = ["Brand", " Name", " TD", " Color", " Owned"]
df = df[[c for c in ordered_cols if c in df.columns]]
return df
# load CSV if present
if os.path.exists(DEFAULT_MATERIALS_CSV):
try:
loaded_df = pd.read_csv(DEFAULT_MATERIALS_CSV, index_col=False)
loaded_df = normalize_filament_df(loaded_df)
initial_df = loaded_df.copy()
initial_filament_data = {
"Brand": initial_df["Brand"].tolist(),
" Name": initial_df[" Name"].tolist(),
" TD": initial_df[" TD"].tolist(),
" Color": initial_df[" Color"].tolist(),
}
if " Owned" in initial_df.columns:
initial_filament_data[" Owned"] = initial_df[" Owned"].astype(str).tolist()
else:
initial_filament_data[" Owned"] = ["false"] * len(initial_df)
except Exception as e:
print(f"Warning: Could not load {DEFAULT_MATERIALS_CSV}: {e}. Using default.")
initial_df = pd.DataFrame(initial_filament_data)
else:
initial_df = pd.DataFrame(initial_filament_data)
initial_df.to_csv(DEFAULT_MATERIALS_CSV, index=False)
def run_autoforge_process(cmd, log_path):
from joblib import parallel_backend
cli_args = cmd[1:]
autoforge_main = importlib.import_module("autoforge.__main__")
exit_code = 0
with open(log_path, "w", buffering=1, encoding="utf-8") as log_f, \
redirect_stdout(log_f), redirect_stderr(log_f), parallel_backend("threading", n_jobs=-1):
try:
sys.argv = ["autoforge"] + cli_args
autoforge_main.main()
except SystemExit as e:
exit_code = e.code
except Exception as e:
log_f.write(f"\nERROR: {e}\n")
exit_code = -1
return exit_code
def create_empty_error_outputs(log_message=""):
return (
log_message,
None,
gr.update(visible=False, interactive=False),
)
def load_filaments_from_json_upload(file_obj):
if file_obj is None:
current_script_df = filament_df_state.value
if current_script_df is not None and not current_script_df.empty:
return current_script_df.rename(
columns={" Name": "Name", " TD": "TD", " Color": "Color (Hex)"}
)
return initial_df.copy().rename(
columns={" Name": "Name", " TD": "TD", " Color": "Color (Hex)"}
)
try:
with open(file_obj.name, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict) and "Filaments" in data:
data = data["Filaments"]
df_loaded = pd.DataFrame(data)
df_loaded.columns = [c.strip() for c in df_loaded.columns]
rename_map = {
"Name": " Name",
"Transmissivity": " TD",
"Color": " Color",
"Owned": " Owned",
}
df_loaded.rename(
columns={k: v for k, v in rename_map.items() if k in df_loaded.columns},
inplace=True,
)
df_loaded = normalize_filament_df(df_loaded)
filament_df_state.value = df_loaded.copy()
return df_loaded.rename(
columns={" Name": "Name", " TD": "TD", " Color": "Color (Hex)"}
)
except Exception as e:
gr.Error(f"Error loading JSON: {e}")
return filament_table.value
with gr.Blocks(theme=gr.themes.Soft()) as demo:
gr.Markdown("# [Autoforge](https://github.com/hvoss-techfak/AutoForge) Web UI")
filament_df_state = gr.State(initial_df.copy())
current_run_output_dir = gr.State(None)
with gr.Tabs():
with gr.TabItem("Filament Management"):
gr.Markdown(
'Manage your filament list here. This list will be used by Autoforge during the optimization process.'
)
gr.Markdown(
'If you have Hueforge, you can export your filaments under "Filaments -> Export" in the Hueforge software. Please make sure to select "CSV" instead of "JSON" during the export dialog.'
)
gr.Markdown(
'If you want to load your personal library of Hueforge filaments, you can also simply paste this path into your explorer address bar: %APPDATA%\\HueForge\\Filaments\\ and import your "personal_library.json" using the "Load Filaments Json" button.'
)
gr.Markdown(
'To remove a filament simply right-click on any of the fields and select "Delete Row"'
)
gr.Markdown(
'Hint: If you have an AMS 3d printer try giving it your entire filament library and then set "pruning_max_colors" under "Autoforge Parameters" in the second tab to your number of AMS slots.'
' Autoforge will automatically select the best matching colors for your image.'
)
with gr.Row():
load_csv_button = gr.UploadButton(
"Load Filaments CSV", file_types=[".csv"]
)
load_json_button = gr.UploadButton(
"Load Filaments JSON", file_types=[".json"]
)
save_csv_button = gr.Button("Save Current Filaments to CSV")
filament_table = gr.DataFrame(
value=ensure_required_cols(
initial_df.copy().rename(
columns={" Name": "Name", " TD": "TD", " Color": "Color (Hex)"}
),
in_display_space=True,
),
headers=["Brand", "Name", "TD", "Color (Hex)"],
datatype=["str", "str", "number", "str"],
interactive=True,
label="Filaments",
)
gr.Markdown("## Add New Filament")
with gr.Row():
new_brand = gr.Textbox(label="Brand")
new_name = gr.Textbox(label="Name")
with gr.Row():
new_td = gr.Number(
label="TD (Transmission/Opacity)",
value=1.0,
minimum=0,
maximum=100,
step=0.1,
)
new_color_hex = gr.ColorPicker(label="Color", value="#FF0000")
add_filament_button = gr.Button("Add Filament to Table")
download_csv_trigger = gr.File(
label="Download Filament CSV", visible=False, interactive=False
)
def update_filament_df_state_from_table(display_df):
display_df = ensure_required_cols(display_df, in_display_space=True)
if "Color (Hex)" in display_df.columns:
display_df["Color (Hex)"] = display_df["Color (Hex)"].apply(
rgba_to_hex
)
script_df = display_df.rename(
columns={"Name": " Name", "TD": " TD", "Color (Hex)": " Color"}
)
script_df = ensure_required_cols(script_df, in_display_space=False)
filament_df_state.value = script_df
def add_filament_to_table(current_display_df, brand, name, td, color_hex):
if not brand or not name:
gr.Warning("Brand and Name cannot be empty.")
return current_display_df
color_hex = rgba_to_hex(color_hex)
new_row = pd.DataFrame(
[{"Brand": brand, "Name": name, "TD": td, "Color (Hex)": color_hex}]
)
updated_display_df = pd.concat(
[current_display_df, new_row], ignore_index=True
)
update_filament_df_state_from_table(updated_display_df)
return updated_display_df
def load_filaments_from_csv_upload(file_obj):
if file_obj is None:
current_script_df = filament_df_state.value
if current_script_df is not None and not current_script_df.empty:
return current_script_df.rename(
columns={
" Name": "Name",
" TD": "TD",
" Color": "Color (Hex)",
}
)
return initial_df.copy().rename(
columns={" Name": "Name", " TD": "TD", " Color": "Color (Hex)"}
)
try:
loaded_script_df = pd.read_csv(file_obj.name, index_col=False)
loaded_script_df = normalize_filament_df(loaded_script_df)
expected_cols = ["Brand", " Name", " TD", " Color"]
if not all(col in loaded_script_df.columns for col in expected_cols):
gr.Error(
f"CSV must contain columns: {', '.join(expected_cols)}. Found: {loaded_script_df.columns.tolist()}"
)
current_script_df = filament_df_state.value
if (
current_script_df is not None
and not current_script_df.empty
):
return current_script_df.rename(
columns={
" Name": "Name",
" TD": "TD",
" Color": "Color (Hex)",
}
)
return initial_df.copy().rename(
columns={
" Name": "Name",
" TD": "TD",
" Color": "Color (Hex)",
}
)
filament_df_state.value = loaded_script_df.copy()
return loaded_script_df.rename(
columns={" Name": "Name", " TD": "TD", " Color": "Color (Hex)"}
)
except Exception as e:
gr.Error(f"Error loading CSV: {e}")
current_script_df = filament_df_state.value
if current_script_df is not None and not current_script_df.empty:
return current_script_df.rename(
columns={
" Name": "Name",
" TD": "TD",
" Color": "Color (Hex)",
}
)
return initial_df.copy().rename(
columns={" Name": "Name", " TD": "TD", " Color": "Color (Hex)"}
)
def save_filaments_to_file_for_download(current_script_df_from_state):
if (
current_script_df_from_state is None
or current_script_df_from_state.empty
):
gr.Warning("Filament table is empty. Nothing to save.")
return None
df_to_save = current_script_df_from_state.copy()
required_cols = ["Brand", " Name", " TD", " Color"]
if not all(col in df_to_save.columns for col in required_cols):
gr.Error(
f"Cannot save. DataFrame missing required script columns. Expected: {required_cols}. Found: {df_to_save.columns.tolist()}"
)
return None
temp_dir = os.path.join(GRADIO_OUTPUT_BASE_DIR, "_temp_downloads")
os.makedirs(temp_dir, exist_ok=True)
temp_filament_csv_path = os.path.join(
temp_dir,
f"filaments_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
)
try:
df_to_save.to_csv(temp_filament_csv_path, index=False)
gr.Info("Filaments prepared for download.")
return gr.File(
value=temp_filament_csv_path,
label="Download Filament CSV",
interactive=True,
visible=True,
)
except Exception as e:
gr.Error(f"Error saving CSV for download: {e}")
return None
filament_table.change(
update_filament_df_state_from_table,
inputs=[filament_table],
outputs=None,
queue=False,
)
add_filament_button.click(
add_filament_to_table,
inputs=[filament_table, new_brand, new_name, new_td, new_color_hex],
outputs=[filament_table],
)
load_csv_button.upload(
load_filaments_from_csv_upload,
inputs=[load_csv_button],
outputs=[filament_table],
)
load_json_button.upload(
load_filaments_from_json_upload,
inputs=[load_json_button],
outputs=[filament_table],
)
save_csv_button.click(
save_filaments_to_file_for_download,
inputs=[filament_df_state],
outputs=[download_csv_trigger],
)
with gr.TabItem("Run Autoforge"):
accordion_params_dict = {}
accordion_params_ordered_names = []
gr.Markdown(
'Here you can upload an image, adjust the parameters and run the Autoforge process. The filaments from the "Filament Management" Tab are automatically used.'
)
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### Input Image (Required)")
input_image_component = gr.Image(
type="pil",
image_mode="RGBA",
label="Upload Image",
sources=["upload"],
interactive=True,
)
with gr.Column(scale=2):
gr.Markdown("### Preview")
with gr.Accordion("Progress & Output", open=True):
final_image_preview = gr.Image(
label="Model Preview",
type="filepath",
interactive=False,
)
with gr.Row():
with gr.Accordion("Autoforge Parameters", open=False):
args_for_accordion = get_script_args_info(
exclude_args=["--input_image"]
)
for arg in args_for_accordion:
label, info, default_val = (
f"{arg['name']}",
arg["help"],
arg.get("default"),
)
if arg["type"] == "number":
accordion_params_dict[arg["name"]] = gr.Number(
label=label,
value=default_val,
info=info,
minimum=arg.get("min"),
maximum=arg.get("max"),
step=arg.get(
"step",
0.001 if isinstance(default_val, float) else 1,
),
precision=arg.get("precision", None),
)
elif arg["type"] == "slider":
accordion_params_dict[arg["name"]] = gr.Slider(
label=label,
value=default_val,
info=info,
minimum=arg.get("min", 0),
maximum=arg.get("max", 1),
step=arg.get("step", 0.01),
)
elif arg["type"] == "checkbox":
accordion_params_dict[arg["name"]] = gr.Checkbox(
label=label, value=default_val, info=info
)
elif arg["type"] == "colorpicker":
accordion_params_dict[arg["name"]] = gr.ColorPicker(
label=label, value=default_val, info=info
)
else:
accordion_params_dict[arg["name"]] = gr.Textbox(
label=label, value=str(default_val), info=info
)
accordion_params_ordered_names.append(arg["name"])
run_button = gr.Button(
"Run Autoforge Process",
variant="primary",
elem_id="run_button_full_width",
)
progress_output = gr.Textbox(
label="Console Output",
lines=15,
autoscroll=True,
show_copy_button=False,
)
with gr.Row():
download_results = gr.File(
label="Download Results (zip)",
file_count="single",
interactive=True,
visible=False,
)
@spaces.GPU(duration=150)
def execute_autoforge_script(
current_filaments_df_state_val, input_image, *accordion_param_values
):
log_output = []
if input_image is None:
gr.Error("Input Image is required! Please upload an image.")
return create_empty_error_outputs("Error: Input Image is required!")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + "_" + str(uuid.uuid4())
run_output_dir_val = os.path.join(GRADIO_OUTPUT_BASE_DIR, f"run_{timestamp}")
os.makedirs(run_output_dir_val, exist_ok=True)
current_run_output_dir.value = run_output_dir_val
if (
current_filaments_df_state_val is None
or current_filaments_df_state_val.empty
):
gr.Error("Filament table is empty. Please add filaments.")
return create_empty_error_outputs("Error: Filament table is empty.")
temp_filament_csv = os.path.join(run_output_dir_val, "materials.csv")
df_to_save = current_filaments_df_state_val.copy()
required_cols = ["Brand", " Name", " TD", " Color"]
missing_cols = [col for col in required_cols if col not in df_to_save.columns]
if missing_cols:
err_msg = (
f"Error: Filament data is missing columns: {', '.join(missing_cols)}."
)
gr.Error(err_msg)
return create_empty_error_outputs(err_msg)
try:
df_to_save.to_csv(temp_filament_csv, index=False)
except Exception as e:
err_msg = f"Error saving temporary filament CSV: {e}"
gr.Error(err_msg)
return create_empty_error_outputs(err_msg)
command = ["autoforge"]
command.extend(["--csv_file", temp_filament_csv])
command.extend(["--output_folder", run_output_dir_val])
command.extend(["--disable_visualization_for_gradio", "1"])
try:
script_input_image_path = os.path.join(
run_output_dir_val, "input_image.png"
)
input_image.save(script_input_image_path, format="PNG")
command.extend(["--input_image", script_input_image_path])
except Exception as e:
err_msg = f"Error handling input image: {e}"
gr.Error(err_msg)
return create_empty_error_outputs(err_msg)
param_dict = dict(zip(accordion_params_ordered_names, accordion_param_values))
for arg_name, arg_widget_val in param_dict.items():
if arg_widget_val is None or arg_widget_val == "":
arg_info_list = [
item for item in get_script_args_info() if item["name"] == arg_name
]
if (
arg_info_list
and arg_info_list[0]["type"] == "checkbox"
and arg_widget_val is False
):
continue
else:
continue
if arg_name == "--background_color":
arg_widget_val = rgba_to_hex(arg_widget_val)
if isinstance(arg_widget_val, bool):
if arg_widget_val:
command.append(arg_name)
else:
command.extend([arg_name, str(arg_widget_val)])
log_output = [
"Starting Autoforge process at ",
f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n",
f"Output directory: {run_output_dir_val}\n",
f"Command: {' '.join(command)}\n\n",
]
yield create_empty_error_outputs("".join(log_output))
log_file = os.path.join(run_output_dir_val, "autoforge_live.log")
open(log_file, "w", encoding="utf-8").close()
import threading
class Worker(threading.Thread):
def __init__(self, cmd, log_path):
super().__init__(daemon=True)
self.cmd, self.log_path = cmd, log_path
self.returncode = None
self.exc = None
def run(self):
try:
self.returncode = run_autoforge_process(self.cmd, self.log_path)
except Exception as e:
self.exc = e
with open(self.log_path, "a", encoding="utf-8") as lf:
lf.write(
"\nERROR: {}. This usually means there was no GPU or the process took too long.\n".format(
exc_text(e)
)
)
self.returncode = -1
try:
worker = Worker(command, log_file)
worker.start()
preview_mtime = 0
last_push = 0
file_pos = 0
def _maybe_new_preview():
nonlocal preview_mtime
src = os.path.join(run_output_dir_val, "vis_temp.png")
if not os.path.exists(src):
return gr.update()
mtime = os.path.getmtime(src)
if mtime <= preview_mtime:
return gr.update()
preview_mtime = mtime
return src
while worker.is_alive() or file_pos < os.path.getsize(log_file):
with open(log_file, "r", encoding="utf-8") as lf:
lf.seek(file_pos)
new_txt = lf.read()
file_pos = lf.tell()
log_output.append(new_txt)
now = time.time()
if now - last_push >= 1.0:
current_preview = _maybe_new_preview()
yield (
"".join(log_output),
current_preview,
gr.update(),
)
last_push = now
time.sleep(0.05)
worker.join()
except RuntimeError as e:
log_output.append(repr(e))
gr.Error(str(e))
with open(log_file, "r", encoding="utf-8") as lf:
lf.seek(file_pos)
new_txt = lf.read()
file_pos = lf.tell()
log_output.append(new_txt)
yield (
"".join(log_output),
gr.update(),
gr.update(),
)
return create_empty_error_outputs(str(e))
if getattr(worker, "exc", None) is not None:
err_msg = f"GPU run failed: {worker.exc}"
log_output.append(f"\n{err_msg}\n")
gr.Error(err_msg)
yield (
"".join(log_output),
gr.update(),
gr.update(),
)
return
with open(log_file, "r", encoding="utf-8") as lf:
lf.seek(file_pos)
log_output.append(lf.read())
return_code = worker.returncode
files_to_offer = [
p
for p in [
os.path.join(run_output_dir_val, "final_model.png"),
os.path.join(run_output_dir_val, "final_model.stl"),
os.path.join(run_output_dir_val, "swap_instructions.txt"),
os.path.join(run_output_dir_val, "project_file.hfp"),
]
if os.path.exists(p)
]
png_path = os.path.join(run_output_dir_val, "final_model.png")
out_png = png_path if os.path.exists(png_path) else None
if return_code != 0:
err_msg = (
f"Autoforge exited with code {return_code}\n"
"See the console output above for details."
)
log_output.append(f"\n{err_msg}\n")
gr.Error(err_msg)
yield (
"".join(log_output),
out_png if out_png else gr.update(),
gr.update(),
)
return
log_output.append("\nAutoforge process completed successfully!")
zip_path = None
if files_to_offer:
zip_path = os.path.join(run_output_dir_val, "autoforge_results.zip")
log_output.append(f"\nZipping results to {os.path.basename(zip_path)}...")
try:
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_STORED) as zf:
for f in files_to_offer:
zf.write(f, os.path.basename(f))
log_output.append(" done.")
except Exception as e:
log_output.append(f"\nError creating zip file: {e}")
zip_path = None
if USE_WANDB:
run = None
try:
run = wandb.init(
project="autoforge",
name=f"run_{timestamp}",
notes="Autoforge Web UI run",
tags=["autoforge", "gradio"],
)
wlogs = {"input_image": wandb.Image(script_input_image_path)}
if out_png:
wlogs["output_image"] = wandb.Image(out_png)
material_csv = pd.read_csv(temp_filament_csv)
table = wandb.Table(dataframe=material_csv)
wlogs["materials"] = table
from wandb import Html
log_text = "".join(log_output).replace("\r", "\n")
def clean_log_strict(text: str) -> str:
allowed = set(string.printable) | {"\n", "\t"}
return "".join(ch for ch in text if ch in allowed)
log_text_cleaned = clean_log_strict(log_text)
wlogs["log"] = Html(f"<pre>{log_text_cleaned}</pre>")
wandb.log(wlogs)
except Exception as e:
print(e)
finally:
if run is not None:
run.finish()
yield (
"".join(log_output),
out_png,
gr.update(
value=zip_path,
visible=bool(zip_path),
interactive=bool(zip_path),
),
)
run_inputs = [filament_df_state, input_image_component] + [
accordion_params_dict[name] for name in accordion_params_ordered_names
]
run_outputs = [
progress_output,
final_image_preview,
download_results,
]
run_button.click(execute_autoforge_script, inputs=run_inputs, outputs=run_outputs)
css = """ #run_button_full_width { width: 100%; } """
if __name__ == "__main__":
if not os.path.exists(DEFAULT_MATERIALS_CSV):
print(f"Creating default filament file: {DEFAULT_MATERIALS_CSV}")
try:
initial_df.to_csv(DEFAULT_MATERIALS_CSV, index=False)
except Exception as e:
print(f"Could not write default {DEFAULT_MATERIALS_CSV}: {e}")
print("To run the UI, execute: python app.py")
demo.queue(default_concurrency_limit=1).launch(share=False)
|