Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| import logging | |
| import shutil | |
| import tempfile | |
| from functools import partial | |
| from pathlib import Path | |
| import gradio as gr | |
| from png23mf import ( | |
| flip_image_horizontally, | |
| generate_3mf_file, | |
| generate_qrcode, | |
| overlay_images_core, | |
| ) | |
| logger = logging.getLogger(__name__) | |
| temp_dir = Path(tempfile.gettempdir()) | |
| def calculate_zoom_value( | |
| *, | |
| base_img, | |
| overlay_img, | |
| current_zoom, | |
| resolution, | |
| ): | |
| """Calculate zoom to fit overlay on base image.""" | |
| logger.debug(f"Entering calculate_zoom_value with arguments: {locals()}") | |
| if base_img is None or overlay_img is None: | |
| result = current_zoom | |
| logger.debug(f"Exiting calculate_zoom_value with result: {result!r}") | |
| return result | |
| base = base_img.convert(mode="RGBA") | |
| w_base, h_base = base.size | |
| if max(w_base, h_base) > resolution: | |
| scale_factor = resolution / max(w_base, h_base) | |
| w_base = int(w_base * scale_factor) | |
| h_base = int(h_base * scale_factor) | |
| base_longer = max(w_base, h_base) | |
| w_overlay, h_overlay = overlay_img.size | |
| overlay_longer = max(w_overlay, h_overlay) | |
| if overlay_longer > base_longer: | |
| zoom = round(base_longer / overlay_longer, 2) | |
| zoom = max(0.01, min(10.0, zoom)) | |
| result = zoom | |
| logger.debug(f"Exiting calculate_zoom_value with result: {result!r}") | |
| return result | |
| else: | |
| result = current_zoom | |
| logger.debug(f"Exiting calculate_zoom_value with result: {result!r}") | |
| return result | |
| def update_width_from_height( | |
| width, | |
| height, | |
| use_aspect_ratio, | |
| base_img, | |
| overlay_img, | |
| ): | |
| """Calculate new width based on height and aspect ratio.""" | |
| logger.debug( | |
| f"Entering update_width_from_height with arguments: {locals()}" | |
| ) | |
| if use_aspect_ratio: | |
| if base_img is not None: | |
| w_base, h_base = base_img.size | |
| elif overlay_img is not None: | |
| w_base, h_base = overlay_img.size | |
| else: | |
| result = width | |
| logger.debug( | |
| f"Exiting update_width_from_height with result: {result!r}" | |
| ) | |
| return result | |
| new_width = round(height * w_base / h_base, 2) | |
| if abs(new_width - width) > 1e-6: | |
| result = new_width | |
| logger.debug( | |
| f"Exiting update_width_from_height with result: {result!r}" | |
| ) | |
| return result | |
| result = width | |
| logger.debug(f"Exiting update_width_from_height with result: {result!r}") | |
| return result | |
| def update_height_from_width( | |
| width, | |
| height, | |
| use_aspect_ratio, | |
| base_img, | |
| overlay_img, | |
| ): | |
| """Calculate new height based on width and aspect ratio.""" | |
| logger.debug( | |
| f"Entering update_height_from_width with arguments: {locals()}" | |
| ) | |
| if use_aspect_ratio: | |
| if base_img is not None: | |
| w_base, h_base = base_img.size | |
| elif overlay_img is not None: | |
| w_base, h_base = overlay_img.size | |
| else: | |
| result = height | |
| logger.debug( | |
| f"Exiting update_height_from_width with result: {result!r}" | |
| ) | |
| return result | |
| new_height = round(width * h_base / w_base, 2) | |
| if abs(new_height - height) > 1e-6: | |
| result = new_height | |
| logger.debug( | |
| f"Exiting update_height_from_width with result: {result!r}" | |
| ) | |
| return result | |
| result = height | |
| logger.debug(f"Exiting update_height_from_width with result: {result!r}") | |
| return result | |
| def overlay_front_images( | |
| base_img, | |
| front_img, | |
| x, | |
| y, | |
| rot, | |
| zoom, | |
| indexed_colors, | |
| use_common_colors, | |
| morph_size, | |
| resolution, | |
| ): | |
| """Overlay an image onto a base image and extract masks.""" | |
| base_rgb, masks, mask_colors = overlay_images_core( | |
| base_img=base_img, | |
| overlay_img=front_img, | |
| x=x, | |
| y=y, | |
| rot=rot, | |
| zoom=zoom, | |
| indexed_colors=indexed_colors, | |
| use_common_colors=use_common_colors, | |
| morph_size=morph_size, | |
| resolution=resolution, | |
| ) | |
| if base_rgb is None: | |
| return None, None, None, None, gr.update(interactive=False) | |
| return base_rgb, masks, masks, mask_colors, gr.update(interactive=True) | |
| def overlay_back_images( | |
| base_img, | |
| back_img, | |
| x, | |
| y, | |
| rot, | |
| zoom, | |
| indexed_colors, | |
| use_common_colors, | |
| morph_size, | |
| resolution, | |
| ): | |
| """Overlay an image onto a base image and extract masks.""" | |
| flipped_base_img = ( | |
| flip_image_horizontally(image=base_img) | |
| if base_img is not None | |
| else None | |
| ) | |
| base_rgb, masks, mask_colors = overlay_images_core( | |
| base_img=flipped_base_img, | |
| overlay_img=back_img, | |
| x=x, | |
| y=y, | |
| rot=rot, | |
| zoom=zoom, | |
| indexed_colors=indexed_colors, | |
| use_common_colors=use_common_colors, | |
| morph_size=morph_size, | |
| resolution=resolution, | |
| ) | |
| if base_rgb is None: | |
| return None, None, None, None, gr.update(interactive=False) | |
| return base_rgb, masks, masks, mask_colors, gr.update(interactive=True) | |
| def update_slider_ranges(base_img, overlay_img, resolution): | |
| if base_img is None: | |
| if overlay_img is not None: | |
| w, h = overlay_img.size | |
| else: | |
| w, h = resolution, resolution | |
| else: | |
| w, h = base_img.size | |
| max_dim = resolution | |
| if max(w, h) > max_dim: | |
| scale_factor = max_dim / max(w, h) | |
| w = int(w * scale_factor) | |
| h = int(h * scale_factor) | |
| default_width = round(100, 2) | |
| default_height = round(default_width * h / w, 2) | |
| if overlay_img is not None: | |
| zoom_val = calculate_zoom_value( | |
| base_img=base_img, | |
| overlay_img=overlay_img, | |
| current_zoom=1.0, | |
| resolution=resolution, | |
| ) | |
| else: | |
| zoom_val = 1.0 | |
| return ( | |
| gr.update(minimum=-w, maximum=w, step=1, value=0), | |
| gr.update(minimum=-h, maximum=h, step=1, value=0), | |
| gr.update(maximum=180, step=1, value=0), | |
| gr.update(maximum=10.0, step=0.01, value=zoom_val), | |
| gr.update(value=4), | |
| gr.update(value=default_width), | |
| gr.update(value=default_height), | |
| ) | |
| def get_user_dir(req: gr.Request) -> str: | |
| session_hash = ( | |
| str(req.session_hash) if req.session_hash is not None else "unknown" | |
| ) | |
| user_dir = temp_dir / session_hash | |
| user_dir.mkdir(parents=True, exist_ok=True) | |
| return str(user_dir) | |
| def delete_user_dir(req: gr.Request) -> None: | |
| session_hash = ( | |
| str(req.session_hash) if req.session_hash is not None else "unknown" | |
| ) | |
| user_dir = temp_dir / session_hash | |
| if user_dir.exists(): | |
| shutil.rmtree(user_dir) | |
| def switch_to_model_tab(): | |
| return gr.update(selected="model") | |
| generate_3mf_with_frames = partial(generate_3mf_file, animate_frames=18) | |
| def generate_qr_code_from_text(text: str): | |
| """Generate a QR code image from input text.""" | |
| logger.debug( | |
| f"Entering generate_qr_code_from_text with arguments: {locals()}" | |
| ) | |
| image = generate_qrcode(fill_color="random", text=text) | |
| result = image | |
| logger.debug(f"Exiting generate_qrcode with result: {result!r}") | |
| return result | |
| with gr.Blocks() as demo: | |
| gr.Markdown(value="# PNG to scad/3mf") | |
| with gr.Row(): | |
| with gr.Column(): | |
| with gr.Tabs() as left_tabs: | |
| with gr.Tab("Front Image"): | |
| with gr.Row(): | |
| front_overlay_img = gr.Image( | |
| label="Front Image", | |
| type="pil", | |
| image_mode="RGBA", | |
| ) | |
| with gr.Row(): | |
| qr_text_front = gr.Textbox( | |
| label="Generate QR-code", | |
| placeholder="Enter text", | |
| submit_btn="▣ QRCode", | |
| ) | |
| with gr.Column(): | |
| x_slider = gr.Slider( | |
| label="X (px)", | |
| minimum=-500, | |
| maximum=500, | |
| step=1, | |
| value=0, | |
| interactive=False, | |
| info="Overlay image X coordinate relative to base", | |
| ) | |
| y_slider = gr.Slider( | |
| label="Y (px)", | |
| minimum=-500, | |
| maximum=500, | |
| step=1, | |
| value=0, | |
| interactive=False, | |
| info="Overlay image Y coordinate relative to base", | |
| ) | |
| rot_slider = gr.Slider( | |
| label="Rotation (°)", | |
| minimum=-180, | |
| maximum=180, | |
| step=1, | |
| value=0, | |
| interactive=False, | |
| info="Rotate overlay image", | |
| ) | |
| zoom_slider = gr.Slider( | |
| label="Zoom", | |
| minimum=0.01, | |
| maximum=10.0, | |
| step=0.01, | |
| value=1.0, | |
| interactive=False, | |
| info="Make overlay image bigger or smaller", | |
| ) | |
| indexed_slider = gr.Slider( | |
| label="Indexed Colors", | |
| minimum=2, | |
| maximum=255, | |
| step=1, | |
| value=4, | |
| info="Overlay image colors will be quantized", | |
| ) | |
| use_common_colors_checkbox = gr.Checkbox( | |
| label="Use most common colors for quantization", | |
| value=True, | |
| interactive=True, | |
| info="Best for the logo's.", | |
| ) | |
| morphology_slider = gr.Slider( | |
| label="Morphology Size", | |
| minimum=1, | |
| maximum=20, | |
| step=1, | |
| value=1, | |
| info=( | |
| "Size of morphological opening/closing " | |
| "to remove small features" | |
| ), | |
| ) | |
| with gr.Tab("Back Image"): | |
| with gr.Row(): | |
| back_overlay_img = gr.Image( | |
| label="Back Image", | |
| type="pil", | |
| image_mode="RGBA", | |
| ) | |
| with gr.Row(): | |
| qr_text_back = gr.Textbox( | |
| label="Generate QR-code", | |
| placeholder="Enter text", | |
| submit_btn="▣ QRCode", | |
| ) | |
| with gr.Column(): | |
| x_slider_back = gr.Slider( | |
| label="X (px)", | |
| minimum=-500, | |
| maximum=500, | |
| step=1, | |
| value=0, | |
| interactive=False, | |
| info="Overlay image X coordinate relative to base", | |
| ) | |
| y_slider_back = gr.Slider( | |
| label="Y (px)", | |
| minimum=-500, | |
| maximum=500, | |
| step=1, | |
| value=0, | |
| interactive=False, | |
| info="Overlay image Y coordinate relative to base", | |
| ) | |
| rot_slider_back = gr.Slider( | |
| label="Rotation (°)", | |
| minimum=-180, | |
| maximum=180, | |
| step=1, | |
| value=0, | |
| interactive=False, | |
| info="Rotate overlay image", | |
| ) | |
| zoom_slider_back = gr.Slider( | |
| label="Zoom", | |
| minimum=0.01, | |
| maximum=10.0, | |
| step=0.01, | |
| value=1.0, | |
| interactive=False, | |
| info="Make overlay image bigger or smaller", | |
| ) | |
| indexed_slider_back = gr.Slider( | |
| label="Indexed Colors", | |
| minimum=2, | |
| maximum=255, | |
| step=1, | |
| value=4, | |
| info="Overlay image colors will be quantized", | |
| ) | |
| use_common_colors_checkbox_back = gr.Checkbox( | |
| label="Use most common colors for quantization", | |
| value=True, | |
| interactive=True, | |
| info="Best for the logo's.", | |
| ) | |
| morphology_slider_back = gr.Slider( | |
| label="Morphology Size", | |
| minimum=1, | |
| maximum=20, | |
| step=1, | |
| value=1, | |
| info=( | |
| "Size of morphological opening/closing " | |
| "to remove small features" | |
| ), | |
| ) | |
| with gr.Tab("Base Image"): | |
| gr.Markdown( | |
| value="**Tip:** The base image must be " | |
| "black and white. Black is treated as transparent" | |
| "and White will be extruded." | |
| ) | |
| with gr.Row(): | |
| base_img = gr.Image( | |
| label="Base Image", | |
| type="pil", | |
| image_mode="RGB", | |
| ) | |
| gr.Markdown(value="**Examples**") | |
| base_examples_dir = ( | |
| Path(__file__).parent.parent / "examples" / "base" | |
| ) | |
| gr.Examples( | |
| examples=str(base_examples_dir), | |
| inputs=[base_img], | |
| label="Base", | |
| ) | |
| front_examples_dir = ( | |
| Path(__file__).parent.parent / "examples" / "front" | |
| ) | |
| gr.Examples( | |
| examples=str(front_examples_dir), | |
| inputs=[front_overlay_img], | |
| label="Front", | |
| ) | |
| back_examples_dir = ( | |
| Path(__file__).parent.parent / "examples" / "back" | |
| ) | |
| gr.Examples( | |
| examples=str(back_examples_dir), | |
| inputs=[back_overlay_img], | |
| label="Back", | |
| ) | |
| with gr.Tab("Settings"): | |
| with gr.Column(): | |
| resolution_dropdown = gr.Dropdown( | |
| label="Resolution (px)", | |
| choices=[512, 1024, 2048], | |
| value=512, | |
| interactive=True, | |
| ) | |
| model_width = gr.Number( | |
| label="Width (mm)", | |
| minimum=0, | |
| step=0.01, | |
| value=100, | |
| interactive=True, | |
| ) | |
| model_height = gr.Number( | |
| label="Height (mm)", | |
| minimum=0, | |
| step=0.01, | |
| value=100, | |
| interactive=True, | |
| ) | |
| use_aspect_ratio = gr.Checkbox( | |
| label="Use image aspect ratio (base/front)", | |
| value=True, | |
| interactive=True, | |
| ) | |
| base_thickness = gr.Number( | |
| label="Base Thickness (mm)", | |
| minimum=0, | |
| step=0.1, | |
| value=1.0, | |
| ) | |
| overlay_thickness = gr.Number( | |
| label="Overlay Thickness (mm)", | |
| minimum=0, | |
| step=0.1, | |
| value=0.5, | |
| ) | |
| with gr.Column(): | |
| with gr.Tabs() as right_tabs: | |
| with gr.Tab("Preview", id="preview"): | |
| front_out = gr.Image(label="Front Preview") | |
| back_out = gr.Image(label="Back Preview") | |
| with gr.Tab("Masks", id="masks"): | |
| mask_gallery_front = gr.Gallery(label="Front Masks") | |
| mask_gallery_back = gr.Gallery(label="Back Masks") | |
| with gr.Tab("Model", id="model"): | |
| animate_gif = gr.Image( | |
| label="Animation GIF", | |
| type="filepath", | |
| interactive=False, | |
| ) | |
| scad_file = gr.File(label="scad/3mf archive") | |
| generate_button = gr.Button( | |
| "Generate Model", | |
| variant="primary", | |
| interactive=False, | |
| ) | |
| masks_state_front = gr.State() | |
| mask_colors_state_front = gr.State() | |
| masks_state_back = gr.State() | |
| mask_colors_state_back = gr.State() | |
| user_dir = gr.Text(visible=False) | |
| model_height.input( | |
| fn=update_width_from_height, | |
| inputs=[ | |
| model_width, | |
| model_height, | |
| use_aspect_ratio, | |
| base_img, | |
| front_overlay_img, | |
| ], | |
| outputs=[model_width], | |
| ) | |
| model_width.input( | |
| fn=update_height_from_width, | |
| inputs=[ | |
| model_width, | |
| model_height, | |
| use_aspect_ratio, | |
| base_img, | |
| front_overlay_img, | |
| ], | |
| outputs=[model_height], | |
| ) | |
| qr_text_front.submit( | |
| fn=generate_qr_code_from_text, | |
| inputs=[qr_text_front], | |
| outputs=[front_overlay_img], | |
| ) | |
| qr_text_back.submit( | |
| fn=generate_qr_code_from_text, | |
| inputs=[qr_text_back], | |
| outputs=[back_overlay_img], | |
| ) | |
| front_overlay_img.change( | |
| fn=overlay_front_images, | |
| inputs=[ | |
| base_img, | |
| front_overlay_img, | |
| x_slider, | |
| y_slider, | |
| rot_slider, | |
| zoom_slider, | |
| indexed_slider, | |
| use_common_colors_checkbox, | |
| morphology_slider, | |
| resolution_dropdown, | |
| ], | |
| outputs=[ | |
| front_out, | |
| mask_gallery_front, | |
| masks_state_front, | |
| mask_colors_state_front, | |
| generate_button, | |
| ], | |
| ) | |
| back_overlay_img.change( | |
| fn=overlay_back_images, | |
| inputs=[ | |
| base_img, | |
| back_overlay_img, | |
| x_slider_back, | |
| y_slider_back, | |
| rot_slider_back, | |
| zoom_slider_back, | |
| indexed_slider_back, | |
| use_common_colors_checkbox_back, | |
| morphology_slider_back, | |
| resolution_dropdown, | |
| ], | |
| outputs=[ | |
| back_out, | |
| mask_gallery_back, | |
| masks_state_back, | |
| mask_colors_state_back, | |
| generate_button, | |
| ], | |
| ) | |
| for comp in [ | |
| x_slider, | |
| y_slider, | |
| rot_slider, | |
| zoom_slider, | |
| indexed_slider, | |
| use_common_colors_checkbox, | |
| morphology_slider, | |
| resolution_dropdown, | |
| ]: | |
| comp.change( | |
| fn=overlay_front_images, | |
| inputs=[ | |
| base_img, | |
| front_overlay_img, | |
| x_slider, | |
| y_slider, | |
| rot_slider, | |
| zoom_slider, | |
| indexed_slider, | |
| use_common_colors_checkbox, | |
| morphology_slider, | |
| resolution_dropdown, | |
| ], | |
| outputs=[ | |
| front_out, | |
| mask_gallery_front, | |
| masks_state_front, | |
| mask_colors_state_front, | |
| generate_button, | |
| ], | |
| ) | |
| for comp in [ | |
| x_slider_back, | |
| y_slider_back, | |
| rot_slider_back, | |
| zoom_slider_back, | |
| indexed_slider_back, | |
| use_common_colors_checkbox_back, | |
| morphology_slider_back, | |
| resolution_dropdown, | |
| ]: | |
| comp.change( | |
| fn=overlay_back_images, | |
| inputs=[ | |
| base_img, | |
| back_overlay_img, | |
| x_slider_back, | |
| y_slider_back, | |
| rot_slider_back, | |
| zoom_slider_back, | |
| indexed_slider_back, | |
| use_common_colors_checkbox_back, | |
| morphology_slider_back, | |
| resolution_dropdown, | |
| ], | |
| outputs=[ | |
| back_out, | |
| mask_gallery_back, | |
| masks_state_back, | |
| mask_colors_state_back, | |
| generate_button, | |
| ], | |
| ) | |
| base_img.change( | |
| fn=overlay_front_images, | |
| inputs=[ | |
| base_img, | |
| front_overlay_img, | |
| x_slider, | |
| y_slider, | |
| rot_slider, | |
| zoom_slider, | |
| indexed_slider, | |
| use_common_colors_checkbox, | |
| morphology_slider, | |
| resolution_dropdown, | |
| ], | |
| outputs=[ | |
| front_out, | |
| mask_gallery_front, | |
| masks_state_front, | |
| mask_colors_state_front, | |
| generate_button, | |
| ], | |
| ) | |
| base_img.change( | |
| fn=overlay_back_images, | |
| inputs=[ | |
| base_img, | |
| back_overlay_img, | |
| x_slider_back, | |
| y_slider_back, | |
| rot_slider_back, | |
| zoom_slider_back, | |
| indexed_slider_back, | |
| use_common_colors_checkbox_back, | |
| morphology_slider_back, | |
| resolution_dropdown, | |
| ], | |
| outputs=[ | |
| back_out, | |
| mask_gallery_back, | |
| masks_state_back, | |
| mask_colors_state_back, | |
| generate_button, | |
| ], | |
| ) | |
| base_img.change( | |
| fn=update_slider_ranges, | |
| inputs=[base_img, front_overlay_img, resolution_dropdown], | |
| outputs=[ | |
| x_slider, | |
| y_slider, | |
| rot_slider, | |
| zoom_slider, | |
| indexed_slider, | |
| model_width, | |
| model_height, | |
| ], | |
| ) | |
| base_img.change( | |
| fn=update_slider_ranges, | |
| inputs=[base_img, back_overlay_img, resolution_dropdown], | |
| outputs=[ | |
| x_slider_back, | |
| y_slider_back, | |
| rot_slider_back, | |
| zoom_slider_back, | |
| indexed_slider_back, | |
| model_width, | |
| model_height, | |
| ], | |
| ) | |
| front_overlay_img.change( | |
| fn=update_height_from_width, | |
| inputs=[ | |
| model_width, | |
| model_height, | |
| use_aspect_ratio, | |
| base_img, | |
| front_overlay_img, | |
| ], | |
| outputs=[model_height], | |
| ) | |
| back_overlay_img.change( | |
| fn=update_height_from_width, | |
| inputs=[ | |
| model_width, | |
| model_height, | |
| use_aspect_ratio, | |
| base_img, | |
| back_overlay_img, | |
| ], | |
| outputs=[model_height], | |
| ) | |
| front_overlay_img.change( | |
| fn=lambda img, base, current_zoom, res: ( | |
| gr.update(interactive=bool(img)), | |
| gr.update(interactive=bool(img)), | |
| gr.update(interactive=bool(img)), | |
| gr.update( | |
| interactive=bool(img), | |
| value=calculate_zoom_value( | |
| base_img=base, | |
| overlay_img=img, | |
| current_zoom=current_zoom, | |
| resolution=res, | |
| ) | |
| if img | |
| else current_zoom, | |
| ), | |
| ), | |
| inputs=[front_overlay_img, base_img, zoom_slider, resolution_dropdown], | |
| outputs=[x_slider, y_slider, rot_slider, zoom_slider], | |
| ) | |
| back_overlay_img.change( | |
| fn=lambda img, base, current_zoom, res: ( | |
| gr.update(interactive=bool(img)), | |
| gr.update(interactive=bool(img)), | |
| gr.update(interactive=bool(img)), | |
| gr.update( | |
| interactive=bool(img), | |
| value=calculate_zoom_value( | |
| base_img=base, | |
| overlay_img=img, | |
| current_zoom=current_zoom, | |
| resolution=res, | |
| ) | |
| if img | |
| else current_zoom, | |
| ), | |
| ), | |
| inputs=[ | |
| back_overlay_img, | |
| base_img, | |
| zoom_slider_back, | |
| resolution_dropdown, | |
| ], | |
| outputs=[ | |
| x_slider_back, | |
| y_slider_back, | |
| rot_slider_back, | |
| zoom_slider_back, | |
| ], | |
| ) | |
| resolution_dropdown.change( | |
| fn=update_slider_ranges, | |
| inputs=[base_img, front_overlay_img, resolution_dropdown], | |
| outputs=[ | |
| x_slider, | |
| y_slider, | |
| rot_slider, | |
| zoom_slider, | |
| indexed_slider, | |
| model_width, | |
| model_height, | |
| ], | |
| ) | |
| resolution_dropdown.change( | |
| fn=update_slider_ranges, | |
| inputs=[base_img, back_overlay_img, resolution_dropdown], | |
| outputs=[ | |
| x_slider_back, | |
| y_slider_back, | |
| rot_slider_back, | |
| zoom_slider_back, | |
| indexed_slider_back, | |
| model_width, | |
| model_height, | |
| ], | |
| ) | |
| demo.load(get_user_dir, outputs=user_dir) | |
| demo.unload(delete_user_dir) | |
| generate_button.click( | |
| fn=switch_to_model_tab, | |
| inputs=[], | |
| outputs=right_tabs, | |
| ).then( | |
| fn=generate_3mf_with_frames, | |
| inputs=[ | |
| base_img, | |
| model_width, | |
| model_height, | |
| base_thickness, | |
| overlay_thickness, | |
| resolution_dropdown, | |
| masks_state_front, | |
| mask_colors_state_front, | |
| masks_state_back, | |
| mask_colors_state_back, | |
| user_dir, | |
| ], | |
| outputs=[scad_file, animate_gif], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(share=False, server_name="0.0.0.0") | |