""" 🚀 SMART ComfyUI Workflow to HTML Generator v2.0 Professional-grade HTML interface generator with modern UI/UX """ import gradio as gr import json import requests import logging from typing import Dict, List, Tuple, Optional from datetime import datetime from pathlib import Path import traceback # Configure logging with UTF-8 encoding for Windows import sys import io # Force UTF-8 output for Windows console if sys.platform == "win32": sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s", handlers=[ logging.FileHandler("generator.log", encoding="utf-8"), logging.StreamHandler(sys.stdout), ], ) logger = logging.getLogger(__name__) class SmartWorkflowGenerator: """Enhanced workflow generator with professional features""" VERSION = "2.0.0" # Predefined dropdown options matching ComfyUI DROPDOWN_OPTIONS = { "megapixel": [ "0.1", "0.15", "0.2", "0.25", "0.3", "0.35", "0.4", "0.45", "0.5", "0.55", "0.6", "0.65", "0.7", "0.75", "0.8", "0.85", "0.9", "0.95", "1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "2.0", "2.5", "3.0", "4.0", ], "divisible_by": ["8", "16", "32", "64"], "aspect_ratio": [ "1:1 (Perfect Square)", "2:3 (Classic Portrait)", "3:4 (Golden Ratio)", "3:5 (Elegant Vertical)", "4:5 (Artistic Frame)", "5:7 (Balanced Portrait)", "5:8 (Tall Portrait)", "7:9 (Modern Portrait)", "9:16 (Slim Vertical)", "9:19 (Tall Slim)", "9:21 (Ultra Tall)", "9:32 (Skyline)", "3:2 (Golden Landscape)", "4:3 (Classic Landscape)", "5:3 (Wide Horizon)", "5:4 (Balanced Frame)", "7:5 (Elegant Landscape)", "8:5 (Cinematic View)", "9:7 (Artful Horizon)", "16:9 (Panorama)", "19:9 (Cinematic Ultrawide)", "21:9 (Epic Ultrawide)", "32:9 (Super Ultrawide)", ], "sampler_name": [ "euler", "euler_cfg_pp", "euler_ancestral", "euler_ancestral_cfg_pp", "heun", "heunpp2", "dpm_2", "dpm_2_ancestral", "lms", "dpm_fast", "dpm_adaptive", "dpmpp_2s_ancestral", "dpmpp_2s_ancestral_cfg_pp", "dpmpp_sde", "dpmpp_sde_gpu", "dpmpp_2m", "dpmpp_2m_cfg_pp", "dpmpp_2m_sde", "dpmpp_2m_sde_gpu", "dpmpp_3m_sde", "dpmpp_3m_sde_gpu", "ddpm", "lcm", "ipndm", "ipndm_v", "deis", "ddim", "uni_pc", "uni_pc_bh2", "res_momentumized", "res_multistep", "restart", ], "scheduler": [ "normal", "karras", "exponential", "sgm_uniform", "simple", "ddim_uniform", "beta", "linear_quadratic", "lcm", "turbo", "align_your_steps", "gits", ], "type": [ "flux", "flux2", "sd3", "sd15", "sd21", "sdxl", "stable_diffusion", "lumina2", "hunyuan_video", "auraflow", "ltxv", ], "device": ["default", "cpu", "cuda", "mps"], "format": [ "video/h264-mp4", "video/h265-mp4", "video/av1-mp4", "video/vp9-webm", "image/gif", "image/webp", ], "upscale_method": ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"], "keep_proportion": ["crop", "pad", "disabled"], "crop_position": ["center", "top", "bottom", "left", "right"], "pix_fmt": ["yuv420p", "yuv422p", "yuv444p"], } # Hardcoded upload fields for nodes that don't have proper API flags UPLOAD_FIELDS = { # Video Helper Suite nodes "VHS_LoadAudioUpload": {"audio": "audio"}, "VHS_LoadVideo": {"video": "video"}, "VHS_LoadVideoPath": {"video": "video"}, "VHS_LoadImagePath": {"image": "image"}, # Add more as needed } def __init__(self): self.workflow = None self.parameters = [] self.nodes_by_category = {} self.node_defs = {} self.server_url = "http://127.0.0.1:8188" logger.info(f"🚀 Generator v{self.VERSION} initialized") def fetch_node_defs(self, server_url: str): """Fetch ALL node definitions from ComfyUI API""" try: logger.info(f"📡 Connecting to ComfyUI: {server_url}") response = requests.get(f"{server_url}/object_info", timeout=10) if response.status_code == 200: self.node_defs = response.json() logger.info(f"✅ Fetched {len(self.node_defs)} node types from API") # Debug: Save API response for inspection try: with open("api_debug.json", "w", encoding="utf-8") as f: json.dump(self.node_defs, f, indent=2, ensure_ascii=False) logger.info("📝 Saved API response to api_debug.json") except Exception as e: logger.warning(f"Could not save API debug: {e}") # Debug: Check for specific nodes if "TextEncodeAceStepAudio1.5" in self.node_defs: node_info = self.node_defs["TextEncodeAceStepAudio1.5"] logger.info(f"🔍 Found TextEncodeAceStepAudio1.5 in API") logger.info(f" Inputs: {list(node_info.get('input', {}).keys())}") else: logger.warning(f"âš ī¸ TextEncodeAceStepAudio1.5 NOT found in API") return ( True, f"✅ API Connected: {len(self.node_defs)} node types fetched", ) logger.warning(f"âš ī¸ HTTP {response.status_code}") return False, f"âš ī¸ HTTP {response.status_code}" except requests.exceptions.ConnectionError: logger.error(f"❌ Connection refused: {server_url}") return ( False, f"❌ Could not connect to ComfyUI at {server_url}. Is ComfyUI running?", ) except requests.exceptions.Timeout: logger.error(f"❌ Connection timeout: {server_url}") return False, f"❌ Connection timeout. ComfyUI is not responding." except Exception as e: logger.error(f"❌ API Error: {str(e)}") return False, f"❌ Error: {str(e)}" def get_dropdown_options_from_api( self, class_type: str, input_name: str ) -> Tuple[Optional[List], bool, Optional[str]]: """Extract dropdown options from ComfyUI API with enhanced parsing. Returns: (options_list, has_upload, upload_type)""" if class_type not in self.node_defs: logger.debug(f"🔍 Node {class_type} not found in API definitions") return None, False node_def = self.node_defs[class_type] # Debug: Log the node structure for specific nodes if "TextEncode" in class_type or "Audio" in class_type or "Ace" in class_type: logger.info(f"🔍 Checking {class_type}.{input_name}") if "input" in node_def: logger.info(f" Input sections: {list(node_def['input'].keys())}") def extract_options(input_def, section_name): """Extract options from input definition with multiple format support. Returns: (options_list, has_upload, upload_type)""" if not isinstance(input_def, list): return None, False, None if len(input_def) == 0: return None, False, None _has_img_upload_tmp = False _has_aud_upload_tmp = False # Check for image_upload flag in metadata (second element) if len(input_def) > 1 and isinstance(input_def[1], dict): _has_img_upload_tmp = input_def[1].get("image_upload", False) _has_aud_upload_tmp = input_def[1].get("audio_upload", False) if _has_img_upload_tmp: logger.info(f"đŸ–ŧī¸ {class_type}.{input_name} has image_upload support") if _has_aud_upload_tmp: logger.info(f"🔊 {class_type}.{input_name} has audio_upload support") # Format 1: COMBO widget - ["COMBO", {"options": [...]}] if ( isinstance(input_def[0], str) and len(input_def) > 1 and isinstance(input_def[1], dict) and "options" in input_def[1] ): options = input_def[1]["options"] if isinstance(options, list) and len(options) > 0: logger.info( f"📋 {class_type}.{input_name} ({section_name}): {len(options)} options [COMBO]" ) has_upload = _has_img_upload_tmp or _has_aud_upload_tmp upload_type = ( "audio" if _has_aud_upload_tmp else ("image" if _has_img_upload_tmp else None) ) return options, has_upload, upload_type # Format 2: [["option1", "option2", ...], {}] - list of options in first element if isinstance(input_def[0], list) and len(input_def[0]) > 0: options = input_def[0] if all(isinstance(opt, str) for opt in options): logger.info( f"📋 {class_type}.{input_name} ({section_name}): {len(options)} options [list]" ) has_upload = _has_img_upload_tmp or _has_aud_upload_tmp upload_type = ( "audio" if _has_aud_upload_tmp else ("image" if _has_img_upload_tmp else None) ) return options, has_upload, upload_type # Format 3: [{"options": [...]}, {}] - nested options dict in first element if isinstance(input_def[0], dict) and "options" in input_def[0]: options = input_def[0]["options"] if isinstance(options, list) and len(options) > 0: logger.info( f"📋 {class_type}.{input_name} ({section_name}): {len(options)} options [nested]" ) has_upload = _has_img_upload_tmp or _has_aud_upload_tmp upload_type = ( "audio" if _has_aud_upload_tmp else ("image" if _has_img_upload_tmp else None) ) return options, has_upload, upload_type # Format 4: Direct list of strings ["option1", "option2"] if all(isinstance(opt, str) for opt in input_def): logger.info( f"📋 {class_type}.{input_name} ({section_name}): {len(input_def)} options [direct]" ) has_upload = _has_img_upload_tmp or _has_aud_upload_tmp upload_type = ( "audio" if has_audio_upload else ("image" if _has_img_upload_tmp else None) ) return input_def, has_upload, upload_type has_upload = _has_img_upload_tmp or _has_aud_upload_tmp upload_type = ( "audio" if _has_aud_upload_tmp else ("image" if _has_img_upload_tmp else None) ) return None, has_upload, upload_type # Check required inputs if "input" in node_def: inputs = node_def["input"] if "required" in inputs and input_name in inputs["required"]: input_def = inputs["required"][input_name] options, has_upload, upload_type = extract_options( input_def, "required" ) if options or has_upload: return options, has_upload, upload_type # Check optional inputs if "optional" in inputs and input_name in inputs["optional"]: input_def = inputs["optional"][input_name] options, has_upload, upload_type = extract_options( input_def, "optional" ) if options or has_upload: return options, has_upload, upload_type # Check hidden inputs (some nodes put dropdowns here) if "hidden" in inputs and input_name in inputs["hidden"]: input_def = inputs["hidden"][input_name] options, has_upload, upload_type = extract_options(input_def, "hidden") if options or has_upload: return options, has_upload, upload_type return None, False, None def get_input_description(self, class_type: str, input_name: str) -> str: """Get parameter description from API for tooltips""" if class_type not in self.node_defs: return "" node_def = self.node_defs[class_type] # Try to extract description if available if "input" in node_def: for section in ["required", "optional"]: if ( section in node_def["input"] and input_name in node_def["input"][section] ): input_def = node_def["input"][section][input_name] if isinstance(input_def, list) and len(input_def) > 1: if isinstance(input_def[1], dict) and "tooltip" in input_def[1]: return input_def[1]["tooltip"] return "" def load_workflow(self, file, server_url="http://127.0.0.1:8188") -> Tuple: """Load and analyze workflow with full API integration""" try: logger.info("=" * 60) logger.info( f"📂 Loading workflow: {file.name if hasattr(file, 'name') else 'uploaded file'}" ) logger.info(f"🌐 ComfyUI Server: {server_url}") logger.info("=" * 60) self.server_url = server_url # STEP 1: Fetch API definitions logger.info("STEP 1: Fetching ComfyUI API...") api_success, api_msg = self.fetch_node_defs(server_url) # STEP 2: Load workflow logger.info("STEP 2: Loading workflow JSON...") if hasattr(file, "name"): with open(file.name, "r", encoding="utf-8") as f: self.workflow = json.load(f) else: if file is None: return None, "❌ No file uploaded", [] self.workflow = json.loads( file.decode("utf-8") if isinstance(file, bytes) else file ) logger.info(f"✅ Loaded workflow with {len(self.workflow)} nodes") # STEP 3: Extract parameters logger.info("STEP 3: Analyzing parameters...") self.parameters = [] self.nodes_by_category = {} for node_id, node_data in self.workflow.items(): class_type = node_data.get("class_type", "") inputs = node_data.get("inputs", {}) title = node_data.get("_meta", {}).get("title", class_type) logger.debug(f"🔍 Node {node_id}: {class_type} ({title})") for input_name, input_value in inputs.items(): # Skip connections if isinstance(input_value, list): continue # Get API options and description api_options, has_upload, upload_type = ( self.get_dropdown_options_from_api(class_type, input_name) ) description = self.get_input_description(class_type, input_name) # Log for debugging specific nodes if ( "TextEncode" in class_type or "Audio" in class_type or "language" in input_name.lower() or has_upload ): logger.info( f"🔍 Processing: {class_type}.{input_name} - Options: {api_options is not None} ({len(api_options) if api_options else 0} items), Upload: {has_upload} ({upload_type})" ) # Fallback to hardcoded if not api_options: api_options = self.get_dropdown_options(input_name) # Detect type - pass upload_type instead of has_image_upload param_type, detected_upload_type = self.smart_detect_type( input_name, input_value, class_type, api_options, has_upload, upload_type, ) category = self.categorize(class_type, input_name, title) # Use detected upload type if available, otherwise keep API value final_upload_type = ( detected_upload_type if detected_upload_type else upload_type ) param = { "node_id": node_id, "node_title": title, "node_class": class_type, "input_name": input_name, "value": input_value, "type": param_type, "options": api_options, "category": category, "description": description, "has_upload": has_upload or (detected_upload_type is not None), "upload_type": final_upload_type, } self.parameters.append(param) if category not in self.nodes_by_category: self.nodes_by_category[category] = set() self.nodes_by_category[category].add(node_id) # Build checkbox options options = [] for i, param in enumerate(self.parameters): opts_info = ( f" [{len(param['options'])} opts]" if param["options"] else "" ) label = f"{param['category']} | {param['node_title']} → {param['input_name']} ({param['type']}{opts_info})" options.append((label, i)) logger.info("=" * 60) logger.info(f"✅ COMPLETE: {len(self.parameters)} parameters extracted") logger.info(f"📂 {len(self.nodes_by_category)} categories detected") logger.info("=" * 60) info = api_msg + "\n" info += f"✅ Loaded {len(self.parameters)} parameters from {len(self.workflow)} nodes\n" info += f"📂 {len(self.nodes_by_category)} categories detected" return ( gr.update(choices=options, value=list(range(len(options)))), info, list(range(len(options))), ) except Exception as e: error_msg = f"❌ Error: {str(e)}\n{traceback.format_exc()}" logger.error(error_msg) return gr.update(choices=[], value=[]), error_msg, [] def smart_detect_type( self, input_name: str, value, class_type: str, api_options: Optional[List], has_upload: bool = False, upload_type: Optional[str] = None, ): """Enhanced type detection with API priority - returns (param_type, upload_type)""" name_lower = input_name.lower() # Priority #0: Upload fields (image or audio) from API flags if has_upload: if upload_type == "audio": return "audio_upload", "audio" else: return "image_upload", "image" # Priority #0.5: Hardcoded upload fields for nodes without proper API flags if class_type in self.UPLOAD_FIELDS: if input_name in self.UPLOAD_FIELDS[class_type]: field_type = self.UPLOAD_FIELDS[class_type][input_name] return f"{field_type}_upload", field_type # Priority #1: API dropdown if api_options and len(api_options) > 0: return "dropdown", None # Check hardcoded dropdown if input_name in self.DROPDOWN_OPTIONS or name_lower in self.DROPDOWN_OPTIONS: return "dropdown", None # Type detection from value if isinstance(value, bool): return "toggle", None elif isinstance(value, int): if "seed" in name_lower and value > 1000: return "seed", None return "number", None elif isinstance(value, float): return ("slider_small" if value <= 2.0 else "slider"), None elif isinstance(value, str): try: float_val = float(value) return ("slider_small" if float_val <= 2.0 else "slider"), None except: pass if len(value) > 100 or "\n" in value: return "textarea", None if any( ext in value for ext in [".safetensors", ".ckpt", ".gguf", ".pth", ".pt"] ): return "file_selector", None if "/" in value or "\\" in value: return "path", None return "text", None def get_dropdown_options(self, input_name: str) -> Optional[List[str]]: """Fallback to hardcoded dropdown options""" if input_name in self.DROPDOWN_OPTIONS: return self.DROPDOWN_OPTIONS[input_name] name_lower = input_name.lower() if name_lower in self.DROPDOWN_OPTIONS: return self.DROPDOWN_OPTIONS[name_lower] for key in self.DROPDOWN_OPTIONS.keys(): if key in name_lower: return self.DROPDOWN_OPTIONS[key] return None def categorize(self, class_type: str, input_name: str, title: str): """Smart categorization with emoji icons""" class_lower = class_type.lower() input_lower = input_name.lower() title_lower = title.lower() if any(w in class_lower for w in ["loader", "load"]): if ( "model" in class_lower or "checkpoint" in class_lower or "gguf" in class_lower ): return "🤖 Models" if "vae" in class_lower: return "🎨 VAE" if "clip" in class_lower: return "📝 CLIP" if "lora" in class_lower: return "✨ LoRA" return "📂 Loaders" if any( w in class_lower for w in ["sampler", "ksampler", "sample", "scheduler"] ): return "âš™ī¸ Sampling" if "resolution" in class_lower or "resolution" in title_lower: return "📐 Resolution" if any(w in class_lower for w in ["condition", "encode"]): if "text" in class_lower or "prompt" in input_lower: return "📝 Prompts" return "đŸŽ¯ Conditioning" if "seed" in input_lower or "noise" in input_lower: return "🎲 Seeds" if any(w in input_lower for w in ["steps", "cfg", "denoise"]): return "⚡ Generation" if any(w in class_lower for w in ["image", "resize", "scale", "upscale"]): return "đŸ–ŧī¸ Images" if any(w in class_lower for w in ["video", "frame", "fps"]): return "đŸŽŦ Video" if "batch" in class_lower or "batch" in title_lower: return f"đŸ“Ļ Batch" if any(w in class_lower for w in ["audio", "sound", "tts"]): return "🔊 Audio" if any(w in class_lower for w in ["save", "output", "preview", "combine"]): return "💾 Output" return f"🔧 {title}" def generate_html( self, selected_indices, app_name, server_url, output_path, enable_features ): """Generate enhanced HTML with modern features""" if not selected_indices: return "❌ Please select at least one parameter", None selected = [ self.parameters[i] for i in selected_indices if i < len(self.parameters) ] # Group by category grouped = {} for param in selected: cat = param["category"] if cat not in grouped: grouped[cat] = [] grouped[cat].append(param) # Parse enabled features features = { "dark_mode": "Dark Mode" in enable_features, "presets": "Preset System" in enable_features, "gallery": "Enhanced Gallery" in enable_features, "progress": "Progress Tracking" in enable_features, "tooltips": "Tooltips" in enable_features, "keyboard": "Keyboard Shortcuts" in enable_features, "autosave": "Auto-save Parameters" in enable_features, "batch": "Batch Generation" in enable_features, } html = self.build_enhanced_html( app_name, server_url, output_path, grouped, self.workflow, features ) filename = f"{app_name.replace(' ', '_').replace('/', '_')}.html" filepath = Path(filename) with open(filepath, "w", encoding="utf-8") as f: f.write(html) logger.info(f"✅ Generated: {filename} ({len(html)} bytes)") result = f"✅ Generated: {filename}\n" result += f"📊 Parameters: {len(selected)}\n" result += f"📂 Categories: {len(grouped)}\n" result += f"🎨 Features: {', '.join([k for k, v in features.items() if v])}\n" result += f"đŸ“Ļ Size: {len(html) // 1024}KB\n" result += f"📁 Location: {str(filepath.absolute())}" return result, str(filepath.absolute()) def build_enhanced_html( self, app_name, server_url, output_path, grouped, workflow, features ): """Build modern, feature-rich HTML""" # Generate parameter inputs HTML inputs_html = "" param_map = [] bypass_toggles = [] for category, params in sorted(grouped.items()): cat_id = ( category.replace(" ", "_") .replace(":", "_") .replace("🤖", "") .replace("📝", "") .replace("âš™ī¸", "") .replace("🎨", "") .replace("✨", "") .replace("📂", "") .replace("📐", "") .replace("đŸŽ¯", "") .replace("🎲", "") .replace("⚡", "") .replace("đŸ–ŧī¸", "") .replace("đŸŽŦ", "") .replace("đŸ“Ļ", "") .replace("🔊", "") .replace("💾", "") .replace("🔧", "") .strip() ) bypass_id = f"bypass_{cat_id}" cat_nodes = set(p["node_id"] for p in params) bypass_toggles.append({"id": bypass_id, "nodes": list(cat_nodes)}) inputs_html += f'''

{category}

â–ŧ
''' for param in params: param_id = f"param_{param['node_id']}_{param['input_name']}" param_map.append( { "id": param_id, "node_id": param["node_id"], "input_name": param["input_name"], "type": param["type"], "has_upload": param.get("has_upload", False), "upload_type": param.get("upload_type", None), } ) tooltip_html = "" if features["tooltips"] and param["description"]: tooltip_html = f'â„šī¸' label_html = f'' input_html = self.generate_input_html(param, param_id) inputs_html += f"""
{label_html} {input_html}
""" inputs_html += """
""" # Build feature sections conditionally preset_section = "" if features["presets"]: preset_section = """
""" batch_section = "" if features["batch"]: batch_section = """
""" progress_section = "" if features["progress"]: progress_section = """
0%

Initializing...

""" theme_toggle = "" if features["dark_mode"]: theme_toggle = '' shortcuts_toggle = "" if features["keyboard"]: shortcuts_toggle = '' gallery_section = "" if features["gallery"]: gallery_section = """ """ shortcuts_help = "" if features["keyboard"]: shortcuts_help = """

Keyboard Shortcuts

Ctrl + Enter - Generate

Ctrl + R - Reset

Ctrl + S - Save Preset

? - Toggle this help

""" # Generate complete HTML html = f''' {app_name}

{app_name}

{theme_toggle} {shortcuts_toggle}
{inputs_html}
{shortcuts_help} ''' return html def generate_input_html(self, param, param_id): """Generate appropriate HTML input for parameter type""" value = param["value"] ptype = param["type"] options = param.get("options") upload_type = param.get("upload_type", "image") # Safety check: if we have options but type isn't dropdown, fix it if ( options and len(options) > 0 and ptype not in ["dropdown", "toggle", "image_upload", "audio_upload"] ): ptype = "dropdown" if ptype in ["image_upload", "audio_upload"]: # Image/audio upload widget with dropdown for existing files # Check if current value exists in options value_exists = options and str(value) in [str(opt) for opt in options] options_html = "".join( [ f'' for opt in (options or []) ] ) # Warning if value doesn't exist in options warning_html = "" if value and not value_exists: warning_html = f'
âš ī¸ Warning: Selected file "{value}" not found in input folder
' return f'''
{ f"""""" if options else "" }
{warning_html}
''' elif ptype == "dropdown" and options: options_html = "".join( [ f'' for opt in options ] ) return f'' elif ptype == "toggle": checked = "checked" if value else "" return f''' ''' elif ptype == "seed": return f'''
''' elif ptype in ["slider", "slider_small"]: min_val = 0 max_val = 100 if ptype == "slider" else 2 step = 0.1 if isinstance(value, float) else 1 return f'''
{value}
''' elif ptype == "number": return f'' elif ptype == "textarea": return f'' else: # text, path, file_selector return f'' # Gradio Interface def create_interface(): """Create enhanced Gradio interface""" generator = SmartWorkflowGenerator() with gr.Blocks( title="ComfyUI HTML Generator v2.0", theme=gr.themes.Soft() ) as interface: gr.Markdown(f""" # 🚀 ComfyUI Workflow to HTML Generator v{generator.VERSION} ### Create professional, standalone HTML interfaces for your ComfyUI workflows **Features:** Modern UI, Dark Mode, Presets, Gallery, Progress Tracking, and more! """) with gr.Row(): with gr.Column(scale=2): workflow_file = gr.File( label="📁 Upload ComfyUI Workflow JSON", file_types=[".json"] ) server_url = gr.Textbox( label="🌐 ComfyUI Server URL", value="http://127.0.0.1:8188", placeholder="http://127.0.0.1:8188", ) load_btn = gr.Button( "🔍 Load & Analyze Workflow", variant="primary", size="lg" ) with gr.Column(scale=1): info_box = gr.Textbox(label="â„šī¸ Status", lines=5, interactive=False) param_selector = gr.CheckboxGroup( label="✅ Select Parameters to Include", choices=[], value=[], interactive=True, ) param_choices_state = gr.State([]) with gr.Row(): select_all_btn = gr.Button("✅ Select All") select_none_btn = gr.Button("âŦœ Select None") with gr.Row(): app_name = gr.Textbox( label="📝 App Name", value="My ComfyUI Generator", placeholder="Enter a name for your HTML app", ) output_path = gr.Textbox( label="📂 Output Directory (in ComfyUI)", value="output", placeholder="output", ) feature_selector = gr.CheckboxGroup( label="🎨 Enable Features", choices=[ "Dark Mode", "Preset System", "Enhanced Gallery", "Progress Tracking", "Tooltips", "Keyboard Shortcuts", "Auto-save Parameters", "Batch Generation", ], value=[ "Dark Mode", "Preset System", "Enhanced Gallery", "Progress Tracking", "Tooltips", "Keyboard Shortcuts", "Auto-save Parameters", "Batch Generation", ], ) generate_btn = gr.Button("🎨 Generate HTML", variant="primary", size="lg") result_text = gr.Textbox(label="✅ Result", lines=10, interactive=False) # Event handlers load_btn.click( fn=generator.load_workflow, inputs=[workflow_file, server_url], outputs=[param_selector, info_box, param_choices_state], ) select_all_btn.click( fn=lambda all_vals: gr.update(value=all_vals), inputs=[param_choices_state], outputs=[param_selector], ) select_none_btn.click( fn=lambda: gr.update(value=[]), inputs=[], outputs=[param_selector] ) def generate_with_error_handling( selected_indices, app_name, server_url, output_path, enable_features ): try: result, filepath = generator.generate_html( selected_indices, app_name, server_url, output_path, enable_features ) return result except Exception as e: import traceback error_msg = f"❌ Error: {str(e)}\n{traceback.format_exc()}" return error_msg generate_btn.click( fn=generate_with_error_handling, inputs=[ param_selector, app_name, server_url, output_path, feature_selector, ], outputs=[result_text], ) gr.Markdown(""" --- ### 📚 Quick Guide: 1. **Upload** your ComfyUI workflow JSON file 2. **Load** to analyze parameters (make sure ComfyUI is running!) 3. **Select** which parameters to include 4. **Choose** features you want 5. **Generate** your professional HTML interface! ### âŒ¨ī¸ Pro Tips: - Generated HTML works completely standalone - Uses Bootstrap 5 + modern CSS - Dark mode auto-saves preference - Presets stored in browser localStorage - Gallery shows last 50 generations - Keyboard shortcuts: Ctrl+Enter (generate), Ctrl+R (reset), Ctrl+S (save preset) **Made with â¤ī¸ for the ComfyUI community** """) return interface if __name__ == "__main__": interface = create_interface() interface.queue() interface.launch( server_name="0.0.0.0", server_port=7860, share=False, inbrowser=True )