diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index 7e0cd0626b..c8bf560a20 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -393,8 +393,6 @@ async def get_models(request: Request, user=Depends(get_verified_user)): model_node_id = None for node in image_config.COMFYUI_WORKFLOW_NODES: - # Support both old ('model') and new ('ClassName::key') type formats. - # A node is a model-loader if its key ends with '_name' (e.g. ckpt_name, unet_name). node_type = node.get('type', '') node_key = node.get('key', '') is_model_node = node_type == 'model' or node_key.endswith('_name') diff --git a/backend/open_webui/utils/images/comfyui.py b/backend/open_webui/utils/images/comfyui.py index 17a1a5ecec..af2582bb68 100644 --- a/backend/open_webui/utils/images/comfyui.py +++ b/backend/open_webui/utils/images/comfyui.py @@ -170,7 +170,6 @@ def _apply_workflow_nodes(workflow, nodes, model, payload): node_type = node.type or '' node_key = node.key or '' - # --- Legacy semantic type names (backward compat) -------------------- if node_type == 'model': for node_id in node.node_ids: workflow[node_id]['inputs'][node_key] = model @@ -205,17 +204,13 @@ def _apply_workflow_nodes(workflow, nodes, model, payload): for node_id in node.node_ids: workflow[node_id]['inputs'][node_key] = seed - # --- New dynamic format: 'ClassName::inputKey' ----------------------- elif '::' in node_type: - # Derive semantic meaning from the key name for node_id in node.node_ids: if node_id not in workflow: continue if node_key in ('ckpt_name', 'unet_name'): - # Model selection workflow[node_id]['inputs'][node_key] = model elif node_key in ('text', 'prompt', 'positive'): - # Positive prompt text workflow[node_id]['inputs'][node_key] = payload.prompt elif node_key in ('width',): workflow[node_id]['inputs'][node_key] = payload.width @@ -239,13 +234,10 @@ def _apply_workflow_nodes(workflow, nodes, model, payload): else: workflow[node_id]['inputs'][node_key] = img elif node.value is not None: - # Custom static override workflow[node_id]['inputs'][node_key] = node.value elif hasattr(payload, 'extra_params') and payload.extra_params and node_key in payload.extra_params: - # API-provided dynamic override workflow[node_id]['inputs'][node_key] = payload.extra_params[node_key] - # --- Generic static value passthrough -------------------------------- else: for node_id in node.node_ids: workflow[node_id]['inputs'][node_key] = node.value diff --git a/src/lib/components/admin/Settings/Images.svelte b/src/lib/components/admin/Settings/Images.svelte index 5654395be1..44d9068975 100644 --- a/src/lib/components/admin/Settings/Images.svelte +++ b/src/lib/components/admin/Settings/Images.svelte @@ -39,20 +39,10 @@ let showComfyUIWorkflowEditor = false; - // Dynamic workflow node config — populated automatically by parseAndPopulateWorkflowNodes() let workflowNodesConfig: { type: string; key: string; node_ids: string; class_type: string }[] = []; let lastKnownWorkflowString: string | null = null; - /** - * Scans every node in the parsed ComfyUI API workflow object and builds a configurable - * row for each primitive input (string / number / boolean). Array-valued inputs are - * wires/links and are intentionally skipped. - * - * @param workflow - Parsed JSON object (ComfyUI API format). - * @param savedNodes - Previously-saved node configs used for reconciliation on load. - * @param showToast - Whether to surface success/warning toasts to the user. - */ function parseAndPopulateWorkflowNodes( workflow: Record, savedNodes: { type: string; key: string; node_ids: string[] | string }[] = [], @@ -64,9 +54,6 @@ return false; } - // Each entry is keyed by "nodeId::class_type::inputKey" so that nodes - // sharing the same class and input (e.g. positive vs negative CLIPTextEncode) - // are always kept as separate rows rather than merged together. const nodeGroups = new Map< string, { type: string; key: string; node_ids: string[]; class_type: string } @@ -83,17 +70,13 @@ const val = node.inputs[inputKey]; const valType = typeof val; - // Only expose primitive (non-link) inputs if (valType !== 'string' && valType !== 'number' && valType !== 'boolean') continue; discoveredPrimitiveCount++; - // Unique key per node+input — never merges across different node IDs const entryKey = `${nodeId}::${node.class_type}::${inputKey}`; - // The "type" stored in COMFYUI_WORKFLOW_NODES uses class_type::inputKey const semanticType = `${node.class_type}::${inputKey}`; - // Check if this entry had saved node IDs the user configured const saved = savedNodes.find( (s) => s.type === semanticType && @@ -104,10 +87,6 @@ ); if (!nodeGroups.has(entryKey)) { - // Keys that map to payload.prompt in _apply_workflow_nodes are ambiguous - // when multiple nodes share the same key (e.g. positive vs negative - // CLIPTextEncode both have key 'text'). Leave node_ids empty for - // these so the admin explicitly picks which nodes receive the prompt. const ambiguousKeys = new Set(['text', 'prompt', 'positive']); const autoAssign = !ambiguousKeys.has(inputKey); @@ -127,7 +106,6 @@ return false; } - // Convert map → array, joining node_ids to comma-separated string for the UI input fields workflowNodesConfig = Array.from(nodeGroups.values()).map((n) => ({ ...n, node_ids: n.node_ids.join(',') @@ -152,15 +130,9 @@ return true; } - /** - * Reads config.COMFYUI_WORKFLOW, validates it, and triggers node auto-detection. - * @param showToast - Surface toasts to the user. - * @param isNewImport - When true, ignore previously-saved node IDs (fresh import). - */ const parseWorkflowAndUpdateNodes = (showToast = false, isNewImport = false) => { const wfString: string = config.COMFYUI_WORKFLOW ?? ''; - // Skip if nothing changed (unless it's an explicit new import) if (showToast && wfString === lastKnownWorkflowString && !isNewImport) return; if (wfString.trim() === '') { @@ -197,7 +169,6 @@ let showComfyUIEditWorkflowEditor = false; - // Dynamic edit-workflow node config let editWorkflowNodesConfig: { type: string; key: string; @@ -206,9 +177,6 @@ }[] = []; let lastKnownEditWorkflowString: string | null = null; - /** - * Same as parseWorkflowAndUpdateNodes but operates on the edit-workflow config. - */ const parseEditWorkflowAndUpdateNodes = (showToast = false, isNewImport = false) => { const wfString: string = config.IMAGES_EDIT_COMFYUI_WORKFLOW ?? ''; @@ -238,7 +206,6 @@ } const reconcileWith = isNewImport ? [] : (config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES ?? []); - // Re-use the same parsing function, writing to editWorkflowNodesConfig const savedNodes = reconcileWith; if (!parsed || typeof parsed !== 'object') { editWorkflowNodesConfig = []; @@ -257,12 +224,10 @@ const valType = typeof val; if (valType !== 'string' && valType !== 'number' && valType !== 'boolean') continue; - // Unique key per node+input — never merges across different node IDs const entryKey = `${nodeId}::${node.class_type}::${inputKey}`; const semanticType = `${node.class_type}::${inputKey}`; if (!nodeGroups.has(entryKey)) { - // Same ambiguous-key logic as parseAndPopulateWorkflowNodes const ambiguousKeys = new Set(['text', 'prompt', 'positive']); const autoAssign = !ambiguousKeys.has(inputKey); @@ -370,7 +335,6 @@ const saveHandler = async () => { loading = true; - // Serialize dynamic workflow node configs before saving if (config?.COMFYUI_WORKFLOW) { if (!validateJSON(config?.COMFYUI_WORKFLOW)) { toast.error($i18n.t('Invalid JSON format for ComfyUI Workflow.')); @@ -424,14 +388,12 @@ getModels(); } - // Pretty-print stored workflow JSON for the code editor if (config.COMFYUI_WORKFLOW) { try { config.COMFYUI_WORKFLOW = JSON.stringify(JSON.parse(config.COMFYUI_WORKFLOW), null, 2); } catch (e) { console.error(e); } - // Auto-parse on load, reconciling with any saved node configs parseWorkflowAndUpdateNodes(false, false); } @@ -724,7 +686,6 @@ const reader = new FileReader(); reader.onload = (ev) => { config.COMFYUI_WORKFLOW = ev.target.result as string; - // Auto-detect nodes from fresh import parseWorkflowAndUpdateNodes(true, true); (e.target as HTMLInputElement).value = ''; }; @@ -774,7 +735,6 @@ lang="json" onChange={(e) => { config.COMFYUI_WORKFLOW = e; - // Re-detect nodes as the user edits JSON parseWorkflowAndUpdateNodes(false, false); }} onSave={() => { @@ -1048,7 +1008,6 @@ const reader = new FileReader(); reader.onload = (ev) => { config.IMAGES_EDIT_COMFYUI_WORKFLOW = ev.target.result as string; - // Auto-detect nodes from fresh import parseEditWorkflowAndUpdateNodes(true, true); (e.target as HTMLInputElement).value = ''; }; @@ -1097,7 +1056,6 @@ lang="json" onChange={(e) => { config.IMAGES_EDIT_COMFYUI_WORKFLOW = e; - // Re-detect nodes as the user edits JSON parseEditWorkflowAndUpdateNodes(false, false); }} onSave={() => { diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index a01be4d9f3..d6a76f999e 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -43,7 +43,6 @@ "{{NAMES}} reacted with {{REACTION}}": "", "{{user}}'s Chats": "", "{{webUIName}} Backend Required": "", - "*Prompt node ID(s) are required for image generation": "", "1 file": "", "1 group": "", "1 hour before": "", @@ -310,6 +309,7 @@ "Auto Redirect": "", "Auto-Copy Response to Clipboard": "", "Auto-Create Groups": "", + "Auto-detected": "", "Auto-Playback Response": "", "Autocomplete Generation": "", "Autocomplete Generation Input Max Length": "", @@ -1091,6 +1091,7 @@ "Error accessing Google Drive: {{error}}": "", "Error accessing media devices.": "", "Error deleting model: {{error}}": "", + "Error occurred during workflow parsing.": "", "Error starting recording.": "", "Error unloading model: {{error}}": "", "Error uploading file: {{error}}": "", @@ -1466,6 +1467,8 @@ "Invalid JSON format in {{NAME}}": "", "Invalid JSON format in Additional Config": "", "Invalid JSON format in MinerU Parameters": "", + "Invalid JSON syntax in ComfyUI Workflow.": "", + "Invalid workflow data provided for parsing.": "", "is typing...": "", "Italic": "", "January": "", @@ -1604,8 +1607,6 @@ "Map LDAP groups to Open WebUI groups.": "", "Map OAuth claims to Open WebUI groups.": "", "Map OAuth claims to Open WebUI roles.": "", - "Map workflow node inputs used for image edits.": "", - "Map workflow node inputs used for image generation.": "", "Mapped Source": "", "March": "", "Mark all as read": "", @@ -1803,6 +1804,7 @@ "No chats found": "", "No chats found for this user.": "", "No chats found.": "", + "No configurable inputs detected. Upload a workflow in API format.": "", "No content": "", "No content found": "", "No content to speak": "", @@ -1867,7 +1869,9 @@ "No valves to update": "", "No webhooks yet": "", "Node Ids": "", + "Node IDs are auto-detected from your workflow. Adjust them if needed.": "", "None": "", + "Not a valid ComfyUI API Workflow JSON format. Make sure to export as API format from ComfyUI.": "", "Not configured": "", "Not factually correct": "", "Not helpful": "", @@ -2948,6 +2952,9 @@ "Width in pixels to compress images to. Leave empty for no compression.": "", "Wikipedia": "", "Won": "", + "Workflow parsed, but no configurable primitive inputs were found. Ensure you exported in API format.": "", + "Workflow parsed. {{count}} configurable input(s) found. Please review the Node IDs._one": "", + "Workflow parsed. {{count}} configurable input(s) found. Please review the Node IDs._other": "", "Working Directory": "", "Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text.": "", "Workspace": "",