From ae779bb071759521e9fdab252e80096b659a684b Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Thu, 30 Jul 2026 14:47:09 +0800 Subject: [PATCH 01/20] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E7=94=9F=E6=88=90?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=88=A0=E9=99=A4=E8=AE=B0=E5=BD=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 286 ++++++++++++++++++++++++++++++++++++ static/css/canvas.css | 6 + static/css/smart-canvas.css | 6 + static/js/canvas.js | 55 ++++++- static/js/i18n/canvas.js | 9 ++ static/js/smart-canvas.js | 53 ++++++- 6 files changed, 413 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index d16baba2c..f23db0fac 100644 --- a/main.py +++ b/main.py @@ -2690,6 +2690,12 @@ class GenerateRequest(BaseModel): class DeleteHistoryRequest(BaseModel): timestamp: float +class DeleteCanvasLogRequest(BaseModel): + log_id: str + delete_unreferenced_media: bool = False + reset_referencing_nodes: bool = False + base_updated_at: int = 0 + class TokenRequest(BaseModel): token: str @@ -6627,6 +6633,208 @@ def output_file_from_url(url): return path return None +def collect_local_media_urls(value: Any) -> List[str]: + urls = [] + if isinstance(value, str): + text = value.strip() + if text.startswith(("/assets/", "/output/", "/api/storage-files/")): + urls.append(text) + elif isinstance(value, dict): + for item in value.values(): + urls.extend(collect_local_media_urls(item)) + elif isinstance(value, (list, tuple)): + for item in value: + urls.extend(collect_local_media_urls(item)) + return urls + +def local_media_path_from_url(url: str) -> Optional[str]: + try: + return output_file_from_url(url) + except (HTTPException, OSError, ValueError): + return None + +def generated_media_path_from_url(url: str) -> Optional[str]: + path = local_media_path_from_url(url) + if not path or not os.path.isfile(path): + return None + path = os.path.realpath(path) + for root in (OUTPUT_OUTPUT_DIR, OUTPUT_DIR): + root = os.path.realpath(root) + try: + if os.path.commonpath([root, path]) == root: + return path + except ValueError: + continue + return None + +def json_references_media_path(value: Any, target_path: str) -> bool: + target = os.path.normcase(os.path.realpath(target_path)) + if isinstance(value, str): + resolved = local_media_path_from_url(value.strip()) + return bool(resolved and os.path.normcase(os.path.realpath(resolved)) == target) + if isinstance(value, dict): + return any(json_references_media_path(item, target) for item in value.values()) + if isinstance(value, (list, tuple)): + return any(json_references_media_path(item, target) for item in value) + return False + +def persisted_json_references_media_path(target_path: str) -> bool: + candidates = [ASSET_LIBRARY_PATH] + for root in (CANVAS_DIR, CONVERSATION_DIR): + if os.path.isdir(root): + for current, _, files in os.walk(root): + candidates.extend( + os.path.join(current, name) + for name in files + if name.lower().endswith(".json") + ) + seen = set() + for path in candidates: + path = os.path.abspath(path) + if path in seen or not os.path.isfile(path): + continue + seen.add(path) + try: + with open(path, "r", encoding="utf-8-sig") as handle: + value = json.load(handle) + except (OSError, UnicodeError, json.JSONDecodeError): + return True + if json_references_media_path(value, target_path): + return True + return False + +def prune_generation_history_for_media(paths: List[str]) -> int: + if not paths or not os.path.isfile(HISTORY_FILE): + return 0 + try: + with HISTORY_LOCK: + with open(HISTORY_FILE, "r", encoding="utf-8-sig") as handle: + history = json.load(handle) + if not isinstance(history, list): + return 0 + kept = [ + record for record in history + if not any(json_references_media_path(record, path) for path in paths) + ] + removed = len(history) - len(kept) + if removed: + with open(HISTORY_FILE, "w", encoding="utf-8") as handle: + json.dump(kept, handle, ensure_ascii=False, indent=4) + return removed + except (OSError, UnicodeError, json.JSONDecodeError): + return 0 + +def smart_owned_result_items(images: List[Any], paths: List[str]) -> List[Any]: + return [ + item for item in images + if isinstance(item, dict) + and item.get("loopInputPreview") is not True + and ( + item.get("generatedResult") is True + or any(json_references_media_path(item, path) for path in paths) + ) + ] + +def expand_canvas_generated_media_paths(canvas: Dict[str, Any], paths: List[str]) -> List[str]: + expanded = list(paths) + for node in list(canvas.get("nodes") or []): + node_type = str(node.get("type") or "").strip().lower() + images = list(node.get("images") or []) + if node_type == "output": + owned_items = images + elif node_type == "smart-image": + owned_items = smart_owned_result_items(images, paths) + else: + owned_items = [] + if not any(json_references_media_path(item, path) for item in owned_items for path in paths): + continue + for item in owned_items: + for url in collect_local_media_urls(item): + candidate = generated_media_path_from_url(url) + if candidate and candidate not in expanded: + expanded.append(candidate) + return expanded + +def reset_canvas_result_nodes_for_media(canvas: Dict[str, Any], paths: List[str]) -> List[str]: + reset_ids = [] + updated_nodes = [] + for node in list(canvas.get("nodes") or []): + node = dict(node) + node_type = str(node.get("type") or "").strip().lower() + changed = False + if isinstance(node.get("generatedOutputs"), list): + outputs = list(node.get("generatedOutputs") or []) + kept_outputs = [ + item for item in outputs + if not any(json_references_media_path(item, path) for path in paths) + ] + if len(kept_outputs) != len(outputs): + node["generatedOutputs"] = kept_outputs + changed = True + if node_type in {"smart-image", "output"} and isinstance(node.get("images"), list): + images = list(node.get("images") or []) + owned_items = smart_owned_result_items(images, paths) if node_type == "smart-image" else images + owns_target = any( + json_references_media_path(item, path) for item in owned_items for path in paths + ) + kept_images = [ + item for item in images + if not ( + owns_target + and item in owned_items + and any(json_references_media_path(item, path) for path in paths) + ) + ] + if len(kept_images) != len(images): + node["images"] = kept_images + changed = True + if node_type == "output": + node["_pending"] = [] + node["imageComparisons"] = {} + if node_type == "smart-image": + node["pending"] = 0 + node["running"] = False + node["queued"] = False + for key in ( + "jimengPending", "pendingTasks", "runStartedAt", "runFinishedAt", + "runElapsedMs", "runTimerHidden", "outputKind", "w", "h", + ): + node.pop(key, None) + elif node_type == "image" and any(json_references_media_path(node.get("url"), path) for path in paths): + node["url"] = "" + node["mediaKind"] = "image" + node["name"] = "空白图片" + changed = True + if changed and node.get("id"): + reset_ids.append(str(node["id"])) + updated_nodes.append(node) + + if reset_ids: + canvas["nodes"] = updated_nodes + return reset_ids + +def delete_media_preview_cache(path: str) -> int: + try: + stat = os.stat(path) + except OSError: + return 0 + source = os.path.realpath(path) + removed = 0 + for width in range(0, 4097): + keys = [hashlib.sha1(f"{source}|{stat.st_mtime_ns}|{stat.st_size}|{width}|jpg".encode("utf-8", "ignore")).hexdigest() + ".jpg"] + if 64 <= width <= 2048: + preview_key = hashlib.sha1(f"{source}|{stat.st_mtime_ns}|{stat.st_size}|{width}".encode("utf-8", "ignore")).hexdigest() + keys.extend((preview_key + ".webp", preview_key + ".png")) + for name in keys: + cache_path = os.path.join(MEDIA_PREVIEW_DIR, name) + try: + if os.path.isfile(cache_path): + os.remove(cache_path) + removed += 1 + except OSError: + pass + return removed + def image_has_alpha(img: Image.Image) -> bool: if img.mode in ("RGBA", "LA"): return True @@ -16569,6 +16777,84 @@ async def update_canvas(canvas_id: str, payload: CanvasSaveRequest): await manager.broadcast_canvas_updated(canvas_id, int(canvas.get("updated_at") or now_ms()), payload.client_id) return {"canvas": canvas} +@app.post("/api/canvases/{canvas_id}/logs/delete") +async def delete_canvas_log(canvas_id: str, payload: DeleteCanvasLogRequest): + log_id = str(payload.log_id or "").strip() + if not log_id: + raise HTTPException(status_code=400, detail="缺少日志 ID") + + def remove_log_record(): + with CANVAS_LOCK: + canvas = load_canvas(canvas_id) + current_updated_at = int(canvas.get("updated_at") or 0) + if payload.base_updated_at and current_updated_at and int(payload.base_updated_at) < current_updated_at: + raise HTTPException(status_code=409, detail={ + "message": "画布已被其他页面更新,请刷新后重试。", + "canvas": canvas, + "updated_at": current_updated_at, + }) + logs = list(canvas.get("logs") or []) + target = next((item for item in logs if str(item.get("id") or "") == log_id), None) + if not target: + raise HTTPException(status_code=404, detail="生成日志不存在") + + candidate_paths = [] + if payload.delete_unreferenced_media: + for url in collect_local_media_urls(target.get("outputs") or []): + path = generated_media_path_from_url(url) + if path and path not in candidate_paths: + candidate_paths.append(path) + + reset_node_ids = [] + if payload.reset_referencing_nodes and candidate_paths: + candidate_paths = expand_canvas_generated_media_paths(canvas, candidate_paths) + reset_node_ids = reset_canvas_result_nodes_for_media(canvas, candidate_paths) + + canvas["logs"] = [item for item in logs if str(item.get("id") or "") != log_id] + save_canvas(canvas) + return canvas, candidate_paths, reset_node_ids + + canvas, candidate_paths, reset_node_ids = await asyncio.to_thread(remove_log_record) + + def cleanup_unreferenced_media(): + removed_files = [] + skipped_referenced = [] + removed_previews = 0 + deletable_paths = [] + for path in candidate_paths: + if persisted_json_references_media_path(path): + skipped_referenced.append(os.path.basename(path)) + continue + deletable_paths.append(path) + prune_generation_history_for_media(deletable_paths) + for path in deletable_paths: + try: + removed_previews += delete_media_preview_cache(path) + os.remove(path) + removed_files.append(os.path.basename(path)) + except OSError: + skipped_referenced.append(os.path.basename(path)) + return removed_files, skipped_referenced, removed_previews + + removed_files = [] + skipped_referenced = [] + removed_previews = 0 + if payload.delete_unreferenced_media: + def locked_cleanup(): + with CANVAS_LOCK: + return cleanup_unreferenced_media() + removed_files, skipped_referenced, removed_previews = await asyncio.to_thread(locked_cleanup) + + await manager.broadcast_canvas_updated(canvas_id, int(canvas.get("updated_at") or now_ms())) + return { + "ok": True, + "canvas": canvas, + "removed_files": removed_files, + "removed_previews": removed_previews, + "reset_node_ids": reset_node_ids, + "skipped_referenced": skipped_referenced, + } + @app.delete("/api/canvases/{canvas_id}") async def delete_canvas(canvas_id: str): canvas = load_canvas_any(canvas_id) diff --git a/static/css/canvas.css b/static/css/canvas.css index 6a8164a34..de9932336 100644 --- a/static/css/canvas.css +++ b/static/css/canvas.css @@ -900,6 +900,12 @@ body.theme-dark .minimap-arrange-btn:hover { background:rgba(30,41,59,.96); colo .log-field-value { margin-top:3px; color:var(--text); font-size:11px; font-weight:800; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .log-prompt { width:100%; max-width:560px; color:var(--faint); font-size:10.5px; line-height:1.35; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; cursor:copy; } .log-prompt.copied { color:#059669; font-weight:900; } +.log-actions { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; } +.log-actions button { min-height:28px; padding:0 10px; border-radius:9px; display:inline-flex; align-items:center; gap:5px; border:1px solid var(--line); background:var(--card-solid); color:var(--muted); font-size:10px; font-weight:850; } +.log-actions button:hover { border-color:var(--text); color:var(--text); } +.log-actions button.danger { color:#dc2626; border-color:rgba(239,68,68,.28); background:rgba(239,68,68,.08); } +.log-actions button.danger:hover { border-color:#dc2626; background:rgba(239,68,68,.14); } +.log-actions button i,.log-actions button svg { width:13px; height:13px; } .log-subline { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-bottom:5px; color:var(--faint); font-size:10.5px; font-weight:800; } .log-subline span { max-width:180px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .log-error { margin:0 0 7px; padding:7px 8px; border-radius:10px; background:rgba(239,68,68,.1); border:1px solid rgba(239,68,68,.22); color:#dc2626; font-size:11px; line-height:1.35; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; cursor:copy; } diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index 6cc1ef466..3547f4b2b 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -1115,6 +1115,12 @@ input[type=range].smart-range::-webkit-slider-thumb { -webkit-appearance:none; w .log-error.copied { color:#059669; border-color:rgba(5,150,105,.35); background:rgba(5,150,105,.1); font-weight:900; } .log-prompt { width:100%; max-width:560px; color:var(--faint); font-size:10.5px; line-height:1.35; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; cursor:copy; } .log-prompt.copied { color:#059669; font-weight:900; } +.log-actions { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; } +.log-actions button { min-height:28px; padding:0 10px; border-radius:9px; display:inline-flex; align-items:center; gap:5px; border:1px solid var(--line); background:var(--card); color:var(--muted); font-size:10px; font-weight:850; } +.log-actions button:hover { border-color:var(--text); color:var(--text); } +.log-actions button.danger { color:#dc2626; border-color:rgba(239,68,68,.28); background:rgba(239,68,68,.08); } +.log-actions button.danger:hover { border-color:#dc2626; background:rgba(239,68,68,.14); } +.log-actions button i,.log-actions button svg { width:13px; height:13px; } .log-thumbs { display:flex; gap:6px; align-items:flex-start; justify-content:flex-end; max-width:126px; flex-wrap:wrap; } .log-thumbs img,.log-thumbs video { width:38px; height:38px; object-fit:cover; border-radius:8px; border:1px solid var(--line); background:#0f172a; cursor:pointer; } .asset-panel { position:absolute; right:22px; top:66px; bottom:168px; z-index:55; width:300px; max-width:calc(100vw - 44px); min-height:0; display:flex; flex-direction:column; gap:10px; padding:12px; border-radius:18px; background:var(--panel); border:1px solid var(--line); box-shadow:0 22px 58px var(--shadow); backdrop-filter:blur(20px); overflow:hidden; transform:translateX(16px); opacity:0; visibility:hidden; pointer-events:none; transition:opacity .16s ease, transform .16s ease, visibility .16s ease; } diff --git a/static/js/canvas.js b/static/js/canvas.js index 15e57547b..4d346cf1b 100644 --- a/static/js/canvas.js +++ b/static/js/canvas.js @@ -12092,6 +12092,48 @@ function logTaskLabel(log){ } return log?.model || '-'; } +async function deleteCanvasLogEntry(logId, deleteMedia=false){ + if(!canvas || !logId) return; + const confirmText = deleteMedia ? tr('canvas.deleteLogMediaConfirm') : tr('canvas.deleteLogConfirm'); + if(!confirm(confirmText)) return; + try { + if(localCanvasDirty || saveTimer){ + clearTimeout(saveTimer); + saveTimer = null; + await saveCanvas(); + } + const res = await fetch(`/api/canvases/${encodeURIComponent(canvas.id)}/logs/delete`, { + method:'POST', + headers:{'Content-Type':'application/json'}, + body:JSON.stringify({ + log_id:logId, + delete_unreferenced_media:deleteMedia, + reset_referencing_nodes:deleteMedia, + base_updated_at:Number(canvas.updated_at || lastCanvasUpdatedAt || 0) + }) + }); + const data = await res.json().catch(() => ({})); + if(!res.ok) throw new Error(data.detail || tr('canvas.logDeleteFailed')); + canvas.logs = data.canvas?.logs || (canvas.logs || []).filter(item => item.id !== logId); + if(data.canvas?.nodes){ + canvas.nodes = data.canvas.nodes; + canvas.connections = data.canvas.connections || []; + nodes = canvas.nodes; + connections = canvas.connections; + render(); + } + canvas.updated_at = Number(data.canvas?.updated_at || canvas.updated_at || Date.now()); + lastCanvasUpdatedAt = canvas.updated_at; + renderCanvasLog(); + const notes = [tr('canvas.logDeleted')]; + if(data.removed_files?.length) notes.push(tr('canvas.logMediaRemoved').replace('{n}', data.removed_files.length)); + if(data.reset_node_ids?.length) notes.push(tr('canvas.logNodesReset').replace('{n}', data.reset_node_ids.length)); + if(data.skipped_referenced?.length) notes.push(tr('canvas.logMediaReferenced').replace('{n}', data.skipped_referenced.length)); + setStatus(notes.join(' · ')); + } catch(err) { + setStatus(err?.message || tr('canvas.logDeleteFailed')); + } +} function addGenerationLog({run, outputs=[], runMs=0, error=''}) { if(!canvas) return; canvas.logs = canvas.logs || []; @@ -12140,7 +12182,7 @@ function renderCanvasLog(){ idText ? `ID ${idText}` : '', backendText, ].filter(Boolean); - return `
+ return `
${escapeHtml(log.status === 'failed' ? tr('canvas.failed') : tr('canvas.success'))} @@ -12151,6 +12193,10 @@ function renderCanvasLog(){
${subParts.map(part => `${escapeHtml(part)}`).join('')}
${log.error ? `
${escapeHtml(log.error)}
` : ''}
${escapeHtml(log.prompt || tr('canvas.noPromptMeta'))}
+
+ + +
${thumbs}
`; @@ -12180,6 +12226,13 @@ function renderCanvasLog(){ }; bindCanvasLogCopy('[data-prompt]', 'prompt'); bindCanvasLogCopy('[data-error]', 'error'); + list.querySelectorAll('[data-log-delete]').forEach(button => { + button.onclick = e => { + e.stopPropagation(); + const logId = button.closest('[data-canvas-log-id]')?.dataset.canvasLogId || ''; + deleteCanvasLogEntry(logId, button.dataset.logDelete === 'media'); + }; + }); refreshIcons(); } async function importWorkflowAssetUrl(url, name='workflow'){ diff --git a/static/js/i18n/canvas.js b/static/js/i18n/canvas.js index 03f04617a..89e23938f 100644 --- a/static/js/i18n/canvas.js +++ b/static/js/i18n/canvas.js @@ -72,6 +72,15 @@ "canvas.logs": { zh: "日志", en: "Logs" }, "canvas.generationLogs": { zh: "生成日志", en: "Generation Logs" }, "canvas.noLogs": { zh: "还没有生成日志", en: "No generation logs yet" }, + "canvas.deleteLog": { zh: "删记录", en: "Delete record" }, + "canvas.deleteLogAndMedia": { zh: "彻底删除记录和原图", en: "Delete record and original media" }, + "canvas.deleteLogConfirm": { zh: "确认删除这条生成记录?原图会保留。", en: "Delete this generation record? Media files will be kept." }, + "canvas.deleteLogMediaConfirm": { zh: "确认删除这条记录和原图?结果节点会保留并回退到生成前状态,提示词、参考图、设置和连线不会删除。", en: "Delete this record and original media? The result node will be kept and reset to its pre-generation state, preserving prompts, references, settings, and links." }, + "canvas.logDeleted": { zh: "生成记录已删除", en: "Generation record deleted" }, + "canvas.logDeleteFailed": { zh: "删除生成记录失败", en: "Failed to delete generation record" }, + "canvas.logMediaRemoved": { zh: "已清理 {n} 个原图", en: "Removed {n} media files" }, + "canvas.logNodesReset": { zh: "已回退 {n} 个结果节点", en: "Reset {n} result nodes" }, + "canvas.logMediaReferenced": { zh: "{n} 个原图仍被其他位置引用,已保留", en: "Kept {n} media files that are still referenced" }, "canvas.success": { zh: "成功", en: "Success" }, "canvas.failed": { zh: "失败", en: "Failed" }, "canvas.selectCanvas": { zh: "选择画布", en: "Select Canvas" }, diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 508f8dfd5..fad660b4f 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -6887,6 +6887,46 @@ function openSmartLogLightbox(url, kind='image'){ function smartLogPreviewNode(url, kind='image'){ openSmartLogLightbox(url, kind); } +async function deleteCanvasLogEntry(logId, deleteMedia=false){ + if(!canvas || !canvasId || !logId) return; + const confirmText = deleteMedia ? tr('canvas.deleteLogMediaConfirm') : tr('canvas.deleteLogConfirm'); + if(!confirm(confirmText)) return; + try { + if(saveTimer){ + clearTimeout(saveTimer); + saveTimer = null; + await saveCanvas(); + } + const res = await fetch(`/api/canvases/${encodeURIComponent(canvasId)}/logs/delete`, { + method:'POST', + headers:{'Content-Type':'application/json'}, + body:JSON.stringify({ + log_id:logId, + delete_unreferenced_media:deleteMedia, + reset_referencing_nodes:deleteMedia, + base_updated_at:Number(canvas.updated_at || 0) + }) + }); + const data = await res.json().catch(() => ({})); + if(!res.ok) throw new Error(data.detail || tr('canvas.logDeleteFailed')); + canvas.logs = data.canvas?.logs || (canvas.logs || []).filter(item => item.id !== logId); + if(data.canvas?.nodes){ + canvas.nodes = data.canvas.nodes; + canvas.connections = data.canvas.connections || []; + nodes = canvas.nodes; + render(); + } + canvas.updated_at = Number(data.canvas?.updated_at || canvas.updated_at || Date.now()); + renderSmartCanvasLog(); + const notes = [tr('canvas.logDeleted')]; + if(data.removed_files?.length) notes.push(tr('canvas.logMediaRemoved').replace('{n}', data.removed_files.length)); + if(data.reset_node_ids?.length) notes.push(tr('canvas.logNodesReset').replace('{n}', data.reset_node_ids.length)); + if(data.skipped_referenced?.length) notes.push(tr('canvas.logMediaReferenced').replace('{n}', data.skipped_referenced.length)); + toast(notes.join(' · ')); + } catch(err) { + toast(err?.message || tr('canvas.logDeleteFailed')); + } +} function renderSmartCanvasLog(){ const logs = canvas?.logs || []; smartLogList.innerHTML = logs.length ? logs.map(log => { @@ -6910,7 +6950,7 @@ function renderSmartCanvasLog(){ taskId ? `ID ${taskId}` : '', backend ].filter(Boolean); - return `
+ return `
${escapeHtml(log.status === 'failed' ? tr('canvas.failed') : tr('canvas.success'))} @@ -6921,6 +6961,10 @@ function renderSmartCanvasLog(){
${subParts.map(part => `${escapeHtml(part)}`).join('')}
${log.error ? `
${escapeHtml(log.error)}
` : ''}
${escapeHtml(log.prompt || tr('canvas.noPromptMeta'))}
+
+ + +
${thumbs}
`; @@ -6950,6 +6994,13 @@ function renderSmartCanvasLog(){ }; bindLogCopy('[data-prompt]', 'prompt'); bindLogCopy('[data-error]', 'error'); + smartLogList.querySelectorAll('[data-log-delete]').forEach(button => { + button.onclick = e => { + e.stopPropagation(); + const logId = button.closest('[data-canvas-log-id]')?.dataset.canvasLogId || ''; + deleteCanvasLogEntry(logId, button.dataset.logDelete === 'media'); + }; + }); refreshIcons(); } function openSmartCanvasLog(){ From c351a39922131b6da325fb8a8ee91e4b0bb52ff8 Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Thu, 30 Jul 2026 16:28:43 +0800 Subject: [PATCH 02/20] =?UTF-8?q?=E5=A2=9E=E5=8A=A0gPRCServerCLI=E5=92=8C?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E8=8A=82=E7=82=B9=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 49 ++ CLI/macos/drawthings/README.md | 37 + CLI/macos/drawthings/requirements-Dt-gRPC.txt | 6 + draw_things_grpc.py | 528 ++++++++++++ main.py | 135 ++- static/api-settings.html | 22 +- static/css/api-settings.css | 8 +- static/js/api-settings.js | 99 ++- static/js/canvas.js | 271 +++++- static/js/i18n/smart-canvas.js | 2 + static/js/smart-canvas.js | 804 +++++++++++++++--- 11 files changed, 1786 insertions(+), 175 deletions(-) create mode 100644 .gitignore create mode 100644 CLI/macos/drawthings/README.md create mode 100644 CLI/macos/drawthings/requirements-Dt-gRPC.txt create mode 100644 draw_things_grpc.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..7c4dac3fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Local secrets and runtime state +API/.env +history.json + +# Python caches and test artifacts +**/__pycache__/ +**/__pycache__/.pytest_cache/ +*.py[cod] + +# Local smoke tests +/CLI/macos/drawthings_hint_smoke_test.py +/CLI/macos/drawthings_img2img_smoke_test.py + +# Generated image/output directories +output/ +assets/output/ +data/media_previews/ + +# Local project data and assets +/assets/ +/data/ + +# Local environment files +.env +.env.local +.venv/ +venv/ +*.egg-info/ + +# IDE and editor configurations +.vscode/ +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +Thumbs.db + +# Build artifacts +dist/ +build/ +*.log + +# Node.js dependencies (if applicable) +node_modules/ + +# Project revision plan list +IMPLEMENTATION_PLAN.md diff --git a/CLI/macos/drawthings/README.md b/CLI/macos/drawthings/README.md new file mode 100644 index 000000000..ae8d47b22 --- /dev/null +++ b/CLI/macos/drawthings/README.md @@ -0,0 +1,37 @@ +# Draw Things gRPCServerCLI + +这是 Infinite-Canvas 的 Draw Things gRPCServerCLI 独立依赖说明,仅适用于 macOS Apple Silicon(M 芯片)。Draw Things gRPCServerCLI 需要由用户单独启动,Infinite-Canvas 只负责连接已运行的服务。 + +## 安装依赖 + +请在 Infinite-Canvas 项目根目录执行。建议使用 conda 或 Miniforge 创建的独立环境,不要把这些依赖追加到项目根目录的 `requirements.txt`。 + +```bash +python -m pip install -r CLI/macos/drawthings/requirements-Dt-gRPC.txt +``` + +也可以明确使用当前环境的 Python: + +```bash +/Users/hanqingren/miniforge3/bin/python -m pip install -r CLI/macos/drawthings/requirements-Dt-gRPC.txt +``` + +## 启动服务 + +请先在 Draw Things 中启动 gRPCServerCLI。默认连接地址为 `127.0.0.1:7859`,TLS 默认开启;如果启动服务时使用了自定义主机或端口,请在 Infinite-Canvas 的 Draw Things gRPC 设置中填写对应的 `主机:端口`。 + +## 项目设置 + +在 Infinite-Canvas 的 API 设置中添加 Draw Things gRPCServerCLI provider 后,再选择实际连接到 gRPCServerCLI 的模型。模型列表由后端实时读取,不在画布节点中固定保存模型名称。 + +该连接目前用于图片生成、单图图生图和 Hint 多图编辑,不提供聊天模型能力。Hint 最多支持四张图片,每张图片可在画布节点中独立设置控制类型。 + +## 本地 smoke test + +`drawthings_*_smoke_test.py` 仅用于本机联调,已经被 Git 忽略,不会同步到 GitHub。运行测试前需要确保: + +1. gRPCServerCLI 已经启动。 +2. 当前环境已安装上面的独立依赖。 +3. 测试使用的模型名称与 Draw Things 当前实时模型列表中的文件名一致。 + +测试输出图片也会被 Git 忽略。 diff --git a/CLI/macos/drawthings/requirements-Dt-gRPC.txt b/CLI/macos/drawthings/requirements-Dt-gRPC.txt new file mode 100644 index 000000000..608843632 --- /dev/null +++ b/CLI/macos/drawthings/requirements-Dt-gRPC.txt @@ -0,0 +1,6 @@ +grpcio>=1.71.0 +flatbuffers>=25.2.10 +protobuf>=5.29.0 +fpzip +numpy +Pillow diff --git a/draw_things_grpc.py b/draw_things_grpc.py new file mode 100644 index 000000000..353cd0894 --- /dev/null +++ b/draw_things_grpc.py @@ -0,0 +1,528 @@ +"""Minimal Draw Things gRPC client used by the Infinite-Canvas image path.""" + +from __future__ import annotations + +import base64 +import io +import os +import secrets +import struct +import sys +from pathlib import Path +from urllib.parse import urlsplit + + +REPO_ROOT = Path(__file__).resolve().parents[1] +PLUGIN_SRC = REPO_ROOT / "draw-things-comfyui" / "src" +if str(PLUGIN_SRC) not in sys.path: + sys.path.insert(0, str(PLUGIN_SRC)) + + +DEFAULT_HOST = "127.0.0.1" +DEFAULT_PORT = 7859 +DEFAULT_SIZE = (1024, 1024) + + +def draw_things_model_supports_editing(model: str) -> bool: + normalized = str(model or "").strip().lower().replace("-", "_") + if "klein" in normalized: + return True + return "qwen" in normalized and "edit" in normalized + + +def _settings(endpoint: str = "") -> tuple[str, int, bool, str]: + host = str(os.getenv("DRAW_THINGS_GRPC_HOST", DEFAULT_HOST)).strip() or DEFAULT_HOST + try: + port = int(os.getenv("DRAW_THINGS_GRPC_PORT", str(DEFAULT_PORT))) + except ValueError: + port = DEFAULT_PORT + custom_endpoint = str(endpoint or "").strip() + if custom_endpoint: + # A provider-specific host:port takes precedence over environment + # defaults, while grpc:// and https:// prefixes remain accepted. + parsed = urlsplit( + custom_endpoint + if "://" in custom_endpoint + else f"//{custom_endpoint}" + ) + if not parsed.hostname: + raise ValueError( + "Draw Things gRPCServerCLI 地址无效,请填写主机:端口,例如 127.0.0.1:7859。" + ) + host = parsed.hostname + if parsed.port is not None: + port = parsed.port + # gRPCServerCLI enables TLS by default. Plaintext remains available only + # when a user explicitly sets DRAW_THINGS_GRPC_TLS=false. + use_tls = str(os.getenv("DRAW_THINGS_GRPC_TLS", "true")).strip().lower() not in { + "0", + "false", + "no", + "off", + } + shared_secret = str(os.getenv("DRAW_THINGS_GRPC_SHARED_SECRET", "")).strip() + return host, port, use_tls, shared_secret + + +def _parse_size(size: str) -> tuple[int, int]: + import re + + match = re.fullmatch(r"\s*(\d+)\s*[xX*]\s*(\d+)\s*", str(size or "")) + if not match: + # The upstream Infinite-Canvas API defaults to 1024x1024. Normal + # canvas requests carry an explicit size from the node's settings. + return DEFAULT_SIZE + width = max(64, min(2048, int(match.group(1)) // 64 * 64)) + height = max(64, min(2048, int(match.group(2)) // 64 * 64)) + return width, height + + +def _build_configuration( + model: str, + width: int, + height: int, + seed: int | None = None, + strength: float | None = None, + batch_size: int = 1, +) -> bytes: + import flatbuffers + from generated import config_generated + + model_name = str(model or "").lower() + is_klein = "klein" in model_name + is_z_image = "z_image" in model_name or "zimage" in model_name + config = config_generated.GenerationConfigurationT() + config.model = model + config.startWidth = width // 64 + config.startHeight = height // 64 + default_steps = "4" + config.steps = int(os.getenv("DRAW_THINGS_GRPC_STEPS", default_steps)) + default_guidance = "1.0" if (is_klein or is_z_image) else "3.5" + config.guidanceScale = float( + os.getenv("DRAW_THINGS_GRPC_GUIDANCE", default_guidance) + ) + if seed is not None: + # The canvas dice control supplies an explicit seed for this request. + config.seed = int(seed) % 4294967295 + else: + configured_seed = os.getenv("DRAW_THINGS_GRPC_SEED") + if configured_seed is None or not configured_seed.strip(): + # Generate a fresh seed per request so batch generations do not + # repeat the same image when no canvas seed was supplied. + config.seed = secrets.randbelow(4294967295) + else: + # An explicit environment value intentionally enables deterministic + # generation for debugging and reproduction. + config.seed = int(configured_seed) % 4294967295 + config.batchCount = 1 + config.batchSize = max(1, min(8, int(batch_size or 1))) + if strength is not None: + # Draw Things uses strength for image-to-image denoising. Keep it in + # the same [0, 1] range exposed by the ComfyUI plugin. + config.strength = max(0.0, min(1.0, float(strength))) + if is_klein: + # FLUX.2 Klein's known-good Draw Things setup is 4-step DDIM Trailing + # with CFG 1, ScaleAlike seeds, shift 3, and no resolution shift. + config.sampler = 16 # SamplerType.DDIMTrailing + config.seedMode = 2 # SeedMode.ScaleAlike + config.shift = 3.0 + config.resolutionDependentShift = False + config.speedUpWithGuidanceEmbed = True + config.guidanceEmbed = 3.5 + elif is_z_image: + # Z Image Turbo's official Draw Things setup uses UniPC Trailing, + # ScaleAlike seeds, shift 3, and resolution-independent shift. + config.sampler = 17 # SamplerType.UniPCTrailing + config.seedMode = 2 # SeedMode.ScaleAlike + config.shift = 3.0 + config.resolutionDependentShift = False + + builder = flatbuffers.Builder(0) + builder.Finish(config.Pack(builder)) + return bytes(builder.Output()) + + +def _reference_image_bytes(reference: object) -> bytes: + """Read one Infinite-Canvas reference image from a local source.""" + value = reference + if isinstance(reference, dict): + value = ( + reference.get("url") + or reference.get("path") + or reference.get("file") + or reference.get("data") + ) + if isinstance(value, bytes): + return value + source = str(value or "").strip() + if not source: + raise ValueError("参考图缺少本地 url、path、file 或 data 字段。") + if source.startswith("data:"): + try: + _, encoded = source.split(",", 1) + return base64.b64decode(encoded) + except (ValueError, base64.binascii.Error) as exc: + raise ValueError("参考图 data URL 无法解码。") from exc + if source.startswith("http://") or source.startswith("https://"): + raise ValueError("阶段 3 只读取本地参考图,不下载远程 URL。") + if source.startswith("file://"): + source = urlsplit(source).path + path = Path(source).expanduser() + if not path.is_absolute(): + candidates = ( + Path.cwd() / path, + Path(__file__).resolve().parent / path, + REPO_ROOT / path, + ) + path = next((candidate for candidate in candidates if candidate.is_file()), candidates[0]) + try: + return path.read_bytes() + except OSError as exc: + raise ValueError(f"参考图无法读取:{path}") from exc + + +def _resize_crop_reference(image, width: int, height: int): + """Match draw-things-comfyui's resize-then-center-crop behavior.""" + from PIL import Image + + image = image.convert("RGB") + if image.size == (width, height): + return image + source_width, source_height = image.size + scale = max(width / source_width, height / source_height) + resized = image.resize( + (max(width, int(source_width * scale)), max(height, int(source_height * scale))), + Image.Resampling.BILINEAR, + ) + left = max(0, (resized.width - width) // 2) + top = max(0, (resized.height - height) // 2) + return resized.crop((left, top, left + width, top + height)) + + +def _encode_image_for_request(reference: object, width: int, height: int) -> bytes: + """Encode a local image as Draw Things' RGB NHWC FP16 tensor.""" + import numpy as np + from PIL import Image + + raw = _reference_image_bytes(reference) + with Image.open(io.BytesIO(raw)) as source: + image = _resize_crop_reference(source, width, height) + pixels = np.asarray(image, dtype=np.float32) / 255.0 * 2.0 - 1.0 + + # This is the same 68-byte CCV header and HWC FP16 payload used by the + # draw-things-comfyui plugin. It is an image input, not a HintProto. + encoded = bytearray(68 + width * height * 3 * 2) + struct.pack_into( + "<9I", + encoded, + 0, + 0, + 0x1, # CCV_TENSOR_CPU_MEMORY + 0x2, # CCV_TENSOR_FORMAT_NHWC + 0x20000, # CCV_16F + 0, + 1, + height, + width, + 3, + ) + encoded[68:] = pixels.astype(np.float16, copy=False).tobytes(order="C") + return bytes(encoded) + + +def _parse_strength(value: object, default: float = 0.75) -> float: + try: + strength = float(value) + except (TypeError, ValueError): + strength = default + return max(0.0, min(1.0, strength)) + + +def _parse_hint_weight(value: object, default: float = 1.0) -> float: + """Normalize one Hint tensor weight to the range accepted by Draw Things.""" + try: + weight = float(value) + except (TypeError, ValueError): + weight = default + if weight < 0: + raise ValueError("Draw Things Hint 权重不能小于 0。") + return weight + + +def _build_hint_protos( + references: list[object], + width: int, + height: int, + hint_type: str = "shuffle", + weights: list[object] | None = None, +): + """Build one HintProto from one or more local images. + + Draw Things expects all images belonging to one control type inside the + same HintProto. This mirrors draw-things-comfyui's request construction + and keeps ordinary request.image input separate from Hint inputs. + """ + from generated import imageService_pb2 + + normalized_type = str(hint_type or "").strip().lower() + if not normalized_type: + raise ValueError("Draw Things Hint 类型不能为空。") + if not references: + raise ValueError("Draw Things Hint 至少需要一张参考图。") + if weights is not None and len(weights) not in {0, len(references)}: + raise ValueError("Hint 权重数量必须与参考图数量一致。") + + tensor_weights = [] + for index, reference in enumerate(references): + reference_weight = None + if isinstance(reference, dict): + reference_weight = reference.get("weight") + if weights: + reference_weight = weights[index] + tensor_weights.append( + ( + _encode_image_for_request(reference, width, height), + _parse_hint_weight(reference_weight), + ) + ) + + hint = imageService_pb2.HintProto(hintType=normalized_type) + hint.tensors.extend( + imageService_pb2.TensorAndWeight(tensor=tensor, weight=weight) + for tensor, weight in tensor_weights + ) + return [hint] + + +def _decode_response_image(response_image: bytes) -> bytes: + import fpzip + import numpy as np + from PIL import Image + + header = np.frombuffer(response_image, dtype=np.uint32, count=17) + height, width, channels = (int(value) for value in header[6:9]) + sample_count = width * height * channels + payload = response_image[68:] + if int(header[0]) == 1012247: + values = fpzip.decompress(payload, order="C").astype(np.float16).reshape(-1) + values = values[:sample_count] + else: + values = np.frombuffer(payload, dtype=np.float16, count=sample_count) + if values.size != sample_count: + raise ValueError( + f"Draw Things returned an invalid image payload: " + f"expected {sample_count} values, got {values.size}" + ) + pixels = np.clip((values + 1) * 127.5, 0, 255).astype(np.uint8) + mode = "RGBA" if channels == 4 else "RGB" + image = Image.frombytes(mode, (width, height), pixels.tobytes()) + from io import BytesIO + + output = BytesIO() + image.save(output, format="PNG") + return output.getvalue() + + +def _channel(target: str, use_tls: bool): + import grpc + from credentials import credentials + + options = [ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ] + if use_tls: + return grpc.aio.secure_channel(target, credentials, options=options) + return grpc.aio.insecure_channel(target, options=options) + + +def _model_files(echo_reply) -> list[str]: + import json + + try: + raw_models = bytes(echo_reply.override.models or b"") + models = json.loads(raw_models.decode("utf-8")) if raw_models else [] + except (AttributeError, UnicodeDecodeError, json.JSONDecodeError): + models = [] + + # Older/newer server builds may expose the model browser as EchoReply.files + # instead of MetadataOverride.models. Keep both forms compatible. + if not models: + models = list(getattr(echo_reply, "files", ()) or ()) + + files = [] + for model in models if isinstance(models, list) else []: + if isinstance(model, str): + filename = model.strip() + elif isinstance(model, dict): + filename = str(model.get("file") or "").strip() + else: + filename = "" + if filename and filename not in files: + files.append(filename) + return files + + +async def list_draw_things_models(endpoint: str = "") -> dict: + """Read the live model list exposed by gRPCServerCLI's model browser.""" + import grpc + from generated import imageService_pb2, imageService_pb2_grpc + + host, port, use_tls, shared_secret = _settings(endpoint) + target = f"{host}:{port}" + try: + async with _channel(target, use_tls) as channel: + stub = imageService_pb2_grpc.ImageGenerationServiceStub(channel) + echo_request = imageService_pb2.EchoRequest(name="Infinite-Canvas") + if shared_secret: + echo_request.sharedSecret = shared_secret + reply = await stub.Echo(echo_request, timeout=10) + return { + "connected": True, + "models": _model_files(reply), + "host": host, + "port": port, + } + except grpc.aio.AioRpcError as exc: + return { + "connected": False, + "models": [], + "host": host, + "port": port, + "error": ( + "无法连接 Draw Things gRPCServerCLI。请确认服务已由用户手动启动," + f"并检查地址 {host}:{port}、TLS 和 shared secret 配置。" + f" gRPC={exc.code().name}: {exc.details()}" + ), + } + + +async def generate_draw_things_image( + prompt: str, + size: str, + model: str = "", + reference_images: list[dict] | None = None, + endpoint: str = "", + seed: int | None = None, + strength: float | None = None, + hint_images: list[dict] | None = None, + hint_type: str = "shuffle", + hint_weights: list[object] | None = None, + batch_size: int = 1, +) -> tuple[dict, dict]: + """Generate one image and return the project's standard image item shape.""" + import grpc + from generated import imageService_pb2, imageService_pb2_grpc + + references = [item for item in (reference_images or []) if item] + if len(references) > 1: + raise RuntimeError( + "当前 Draw Things 模型不支持多图图像编辑,请只保留一张输入图,或切换到 Klein/Qwen Edit 模型。" + ) + hints = [item for item in (hint_images or []) if item] + if references and hints: + raise RuntimeError( + "Draw Things gRPC 请求不能同时使用普通图生图 image 和 Hint 输入。" + ) + + host, port, use_tls, shared_secret = _settings(endpoint) + target = f"{host}:{port}" + selected_model = str(model or "").strip() + if not selected_model: + raise RuntimeError( + "Draw Things gRPCServerCLI 未选择模型。请连接服务后从实时模型列表中选择。" + ) + width, height = _parse_size(size) + input_image = None + image_strength = None + if references: + # Ordinary image-to-image is carried by request.image. It must remain + # separate from HintProto, which is reserved for later control inputs. + input_image = _encode_image_for_request(references[0], width, height) + image_strength = _parse_strength( + strength + if strength is not None + else os.getenv("DRAW_THINGS_GRPC_STRENGTH", "0.75") + ) + request_hints = _build_hint_protos( + hints, + width, + height, + hint_type=hint_type, + weights=hint_weights, + ) if hints else [] + + try: + async with _channel(target, use_tls) as channel: + stub = imageService_pb2_grpc.ImageGenerationServiceStub(channel) + echo_request = imageService_pb2.EchoRequest(name="Infinite-Canvas") + if shared_secret: + echo_request.sharedSecret = shared_secret + echo_reply = await stub.Echo(echo_request, timeout=10) + available_models = _model_files(echo_reply) + if available_models and selected_model not in available_models: + raise RuntimeError( + f"Draw Things 模型不可用:{selected_model}。" + "请从当前 gRPCServerCLI 模型列表中重新选择。" + ) + + request = imageService_pb2.ImageGenerationRequest( + image=input_image or b"", + scaleFactor=1, + hints=request_hints, + prompt=str(prompt or ""), + negativePrompt="", + configuration=_build_configuration( + selected_model, + width, + height, + seed, + strength=image_strength, + batch_size=batch_size, + ), + user="Infinite-Canvas", + device=imageService_pb2.LAPTOP, + ) + if shared_secret: + request.sharedSecret = shared_secret + + generated = [] + async for response in stub.GenerateImage(request, timeout=1800): + generated.extend(response.generatedImages) + if not generated: + raise RuntimeError("Draw Things gRPCServerCLI 未返回图片。") + + generated_items = [] + for generated_image in generated: + png = _decode_response_image(generated_image) + generated_items.append({ + "b64_json": base64.b64encode(png).decode("ascii"), + "mime_type": "image/png", + }) + image_item = { + "type": "b64", + "value": generated_items[0]["b64_json"], + "mime_type": "image/png", + } + return image_item, { + "provider": "drawthings", + "model": selected_model, + "width": width, + "height": height, + "image_to_image": bool(input_image), + "strength": image_strength, + "hint_type": str(hint_type or "").strip().lower() if hints else "", + "hint_count": len(hints), + "hint_weights": [ + float(tensor.weight) + for hint in request_hints + for tensor in hint.tensors + ], + "images": generated_items, + } + except grpc.aio.AioRpcError as exc: + raise RuntimeError( + "无法连接 Draw Things gRPCServerCLI。请确认服务已由用户手动启动," + f"并检查地址 {host}:{port}、TLS 和 shared secret 配置。" + f" gRPC={exc.code().name}: {exc.details()}" + ) from exc diff --git a/main.py b/main.py index f23db0fac..49bca1a7a 100644 --- a/main.py +++ b/main.py @@ -28,7 +28,7 @@ import html import xml.etree.ElementTree as ET from typing import List, Dict, Any, Optional, Tuple -from threading import Lock, Thread +from threading import Lock, RLock, Thread import httpx from PIL import Image, ImageOps from io import BytesIO @@ -301,7 +301,7 @@ def apply_storage_settings(dirs=None): HISTORY_LOCK = Lock() GLOBAL_CONFIG_LOCK = Lock() CONVERSATION_LOCK = Lock() -CANVAS_LOCK = Lock() +CANVAS_LOCK = RLock() LOAD_LOCK = Lock() RUNNINGHUB_WORKFLOW_LOCK = Lock() NEXT_TASK_ID = 1 @@ -314,7 +314,7 @@ def apply_storage_settings(dirs=None): } PROVIDER_ID_RE = re.compile(r"^[a-zA-Z0-9_-]{2,40}$") -SUPPORTED_PROVIDER_PROTOCOLS = {"openai", "apimart", "gemini", "gemini-cli", "volcengine", "runninghub", "jimeng", "codex", "tudou"} +SUPPORTED_PROVIDER_PROTOCOLS = {"openai", "apimart", "gemini", "gemini-cli", "volcengine", "runninghub", "jimeng", "codex", "tudou", "grpc"} SUPPORTED_IMAGE_REQUEST_MODES = {"openai", "openai-json", "openai-video-proxy", "openai-responses"} RUNNINGHUB_DEFAULT_BASE_URL = "https://www.runninghub.cn" RUNNINGHUB_OPENAPI_BASE_URL = "https://www.runninghub.cn/openapi/v2" @@ -1270,11 +1270,12 @@ def normalize_provider(item): raise HTTPException(status_code=400, detail=f"API 平台 ID 不合法:{provider_id or '(empty)'}") name = re.sub(r"\s+", " ", str(item.get("name") or provider_id).strip())[:60] or provider_id base_url = str(item.get("base_url") or "").strip().rstrip("/") - if base_url and not re.match(r"^https?://", base_url): - raise HTTPException(status_code=400, detail=f"{name} 的 Base URL 需要以 http:// 或 https:// 开头") protocol = str(item.get("protocol") or "openai").strip().lower() if protocol not in SUPPORTED_PROVIDER_PROTOCOLS: protocol = "openai" + is_drawthings = provider_id == "drawthings" or protocol == "grpc" + if base_url and not is_drawthings and not re.match(r"^https?://", base_url): + raise HTTPException(status_code=400, detail=f"{name} 的 Base URL 需要以 http:// 或 https:// 开头") image_request_mode = detect_image_request_mode(base_url, item.get("image_models") or []) or normalize_image_request_mode(item.get("image_request_mode")) # Migrate existing Tudou entries by official host so their special request shapes # remain scoped to this provider instead of leaking into generic OpenAI handling. @@ -1295,6 +1296,10 @@ def normalize_provider(item): base_url = "" if protocol in {"codex", "gemini-cli"}: base_url = "" + if provider_id == "drawthings" or protocol == "grpc": + provider_id = "drawthings" + name = name or "Draw Things gRPCServerCLI" + protocol = "grpc" if provider_id == "runninghub": protocol = "runninghub" base_url = base_url or RUNNINGHUB_DEFAULT_BASE_URL @@ -2733,6 +2738,8 @@ class OnlineImageRequest(BaseModel): resolution: str = "" quality: str = "auto" n: int = 1 + batch_size: int = 1 + seed: Optional[int] = None reference_images: List[AIReference] = [] operation: str = "" resolution_type: str = "" @@ -4616,6 +4623,9 @@ def is_codex_provider(provider): def is_gemini_cli_provider(provider): return provider_protocol(provider) == "gemini-cli" +def is_draw_things_provider(provider): + return provider_protocol(provider) == "grpc" or str((provider or {}).get("id") or "").strip().lower() == "drawthings" + def codex_env_value(key): return os.getenv(key, "") or read_api_env_value(key) @@ -11043,10 +11053,58 @@ async def generate_runninghub_video(payload, provider): local_urls = [await save_remote_video_to_output(url, prefix="rh_video_") for url in urls] return {"videos": local_urls, "task_id": task_id, "raw": result} -async def generate_ai_image(prompt, size, quality, model, reference_images=None, provider_id="comfly", aspect_ratio="", resolution=""): +async def generate_ai_image(prompt, size, quality, model, reference_images=None, provider_id="comfly", aspect_ratio="", resolution="", seed=None, batch_size=1): provider = get_api_provider(provider_id) if is_tudou_provider(provider): model = tudou_image_model_for_request(model) + if is_draw_things_provider(provider): + from draw_things_grpc import draw_things_model_supports_editing + + drawthings_references = [] + for reference in (reference_images or []): + item = dict(reference) if isinstance(reference, dict) else {"url": reference} + url = str(item.get("url") or "").strip() + # Canvas references normally use /output or /assets URLs. Resolve + # those to local files before passing them to the gRPC client; + # other providers keep their existing reference handling. + local_path = local_media_path_from_url(url) + if local_path: + drawthings_references.append({ + "path": local_path, + "name": item.get("name") or os.path.basename(local_path), + "weight": item.get("weight", 1.0), + }) + elif url: + drawthings_references.append(item) + editing_model = draw_things_model_supports_editing(model) + if not editing_model and len(drawthings_references) > 1: + raise HTTPException( + status_code=400, + detail="当前 Draw Things 模型只支持文生图或单图图生图,不能输入多张参考图。", + ) + try: + from draw_things_grpc import generate_draw_things_image + # The provider's saved host:port overrides environment defaults. + request_options = { + "endpoint": provider.get("base_url") or "", + "seed": seed, + "batch_size": batch_size, + } + if editing_model: + request_options.update( + hint_images=drawthings_references, + hint_type="shuffle" if drawthings_references else "", + ) + else: + request_options.update( + reference_images=drawthings_references, + ) + image_item, raw = await generate_draw_things_image( + prompt, size, model, **request_options + ) + except Exception as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + return image_item, raw if provider["id"] == "modelscope": return await generate_modelscope_provider_image(prompt, size, model, reference_images, provider) if is_codex_provider(provider): @@ -13005,6 +13063,18 @@ async def ai_models(): async def api_providers(): return {"providers": public_api_providers()} +@app.get("/api/drawthings/models") +async def drawthings_models(): + provider = next((item for item in load_api_providers() if item.get("id") == "drawthings"), None) + if not provider: + return {"connected": False, "models": [], "message": "Draw Things provider 尚未添加"} + result = await fetch_models_from_upstream(provider.get("base_url") or "", "", "grpc", "openai") + return { + "connected": bool(result.get("ok")), + "models": result.get("image_models") or [], + "message": result.get("message") or "", + } + @app.put("/api/providers") async def save_providers(payload: List[ApiProviderPayload]): providers = [] @@ -13100,6 +13170,8 @@ def protocol_from_payload(payload): return "runninghub" if provider_id == "jimeng": return "jimeng" + if provider_id == "drawthings": + return "grpc" base_url = str(getattr(payload, "base_url", "") or "").strip().lower() if "runninghub.cn" in base_url or "runninghub.ai" in base_url: return "runninghub" @@ -13110,6 +13182,8 @@ def api_key_from_payload(payload, protocol: str = ""): explicit = str(getattr(payload, "api_key", "") or "").strip() provider_id = str(getattr(payload, "provider_id", "") or "").strip().lower() protocol = str(protocol or protocol_from_payload(payload) or "").strip().lower() + if protocol == "grpc": + return "" if explicit: return explicit if provider_id: @@ -13362,6 +13436,25 @@ async def test_provider_connection(payload: TestConnectionPayload): "protocol": "runninghub", "raw": payload_models.get("raw"), } + if protocol == "grpc": + try: + from draw_things_grpc import list_draw_things_models + result = await list_draw_things_models(payload.base_url) + except Exception as exc: + result = {"connected": False, "models": [], "error": str(exc)} + models = result.get("models") or [] + return { + "ok": bool(result.get("connected")), + "protocol": "grpc", + "status": 200 if result.get("connected") else 0, + "message": "Draw Things gRPCServerCLI 已连接" if result.get("connected") else (result.get("error") or "Draw Things gRPCServerCLI 未连接"), + "model_count": len(models), + "image_models": models, + "chat_models": [], + "video_models": [], + "all": models, + "raw": result, + } base_url = (payload.base_url or "").strip().rstrip("/") if not base_url: raise HTTPException(status_code=400, detail="请先填写请求地址") @@ -13584,6 +13677,25 @@ async def fetch_models_from_upstream(base_url: str, api_key: str, protocol: str payload = gemini_cli_models_payload(raw={"status": status}) payload["message"] = status.get("message") or payload["message"] return payload + if protocol == "grpc": + try: + from draw_things_grpc import list_draw_things_models + result = await list_draw_things_models(base_url) + except Exception as exc: + result = {"connected": False, "models": [], "error": str(exc)} + models = result.get("models") or [] + return { + "ok": bool(result.get("connected")), + "protocol": "grpc", + "status": 200 if result.get("connected") else 0, + "message": "Draw Things gRPCServerCLI 已连接" if result.get("connected") else (result.get("error") or "Draw Things gRPCServerCLI 未连接"), + "total": len(models), + "image_models": models, + "chat_models": [], + "video_models": [], + "all": models, + "raw": result, + } if protocol == "jimeng": return { "total": len(JIMENG_DEFAULT_IMAGE_MODELS) + len(JIMENG_DEFAULT_VIDEO_MODELS), @@ -13716,6 +13828,8 @@ async def fetch_upstream_models(provider_id: str): return await fetch_models_from_upstream("", "", "codex", provider.get("image_request_mode") or "openai") if is_gemini_cli_provider(provider): return await fetch_models_from_upstream("", "", "gemini-cli", provider.get("image_request_mode") or "openai") + if is_draw_things_provider(provider): + return await fetch_models_from_upstream(provider.get("base_url") or "", "", "grpc", provider.get("image_request_mode") or "openai") api_key = os.getenv(runninghub_wallet_key_env(), "") if provider["id"] == "runninghub" else "" if not api_key: api_key = provider_env_key_value(provider["id"]) @@ -13731,7 +13845,10 @@ async def build_online_image_result(payload: OnlineImageRequest): refs = [ref.dict() for ref in payload.reference_images if ref.url] image_refs = image_references(refs) count = max(1, min(8, int(payload.n or 1))) - operation = str(payload.operation or "").strip().lower() + batch_size = max(1, min(8, int(payload.batch_size or 1))) if is_draw_things_provider(provider) else 1 + if batch_size > 1: + count = 1 + operation = str(getattr(payload, "operation", "") or "").strip().lower() if operation == "upscale": if not is_jimeng_provider(provider): raise HTTPException(status_code=400, detail="图片放大目前仅支持即梦(Dreamina)平台") @@ -13744,7 +13861,7 @@ async def generate_one(): else: image_data, raw_item = await generate_ai_image( payload.prompt, request_size, payload.quality, model, image_refs, provider["id"], - payload.aspect_ratio, payload.resolution, + payload.aspect_ratio, payload.resolution, payload.seed, batch_size=batch_size, ) try: image_items = extract_images(raw_item) if isinstance(raw_item, dict) else [image_data] @@ -13788,7 +13905,7 @@ async def generate_one(): "provider_name": provider.get("name") or provider["id"], "task_id": extract_task_id(raw) if isinstance(raw, dict) else None, "request_id": raw.get("id") if isinstance(raw, dict) else None, - "params": {"provider_id": provider["id"], "model": model, "size": request_size, "requested_size": payload.size, "quality": payload.quality, "n": count, "reference_images": refs}, + "params": {"provider_id": provider["id"], "model": model, "size": request_size, "requested_size": payload.size, "aspect_ratio": payload.aspect_ratio, "resolution": payload.resolution, "quality": payload.quality, "n": count, "batch_size": batch_size, "reference_images": refs}, "raw_usage": raw.get("usage") if isinstance(raw, dict) else None, } save_to_history(result) diff --git a/static/api-settings.html b/static/api-settings.html index 61418521e..9560dd6ce 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -46,6 +46,7 @@ +
需要先安装 CLI文件夹中的依赖。
@@ -80,7 +81,7 @@
-
+
生图模型
@@ -426,7 +428,7 @@
- +
@@ -566,6 +568,6 @@
- + diff --git a/static/css/api-settings.css b/static/css/api-settings.css index 69203812d..7bbe4d86f 100644 --- a/static/css/api-settings.css +++ b/static/css/api-settings.css @@ -375,7 +375,8 @@ body.show-jimeng .api-key-field, body.show-codex .api-base-url-field, body.show-codex .api-key-field, body.show-gemini-cli .api-base-url-field, -body.show-gemini-cli .api-key-field { display:none !important; } +body.show-gemini-cli .api-key-field, +body.show-drawthings .api-key-field { display:none !important; } .jimeng-cli-panel { grid-column:1/-1; display:none; flex-direction:column; gap:12px; margin-top:2px; padding:14px; border:1px solid var(--line); border-radius:14px; background:var(--soft); } body.show-jimeng .jimeng-cli-panel:not([hidden]) { display:flex; } body.show-codex .jimeng-cli-panel:not([hidden]) { display:flex; } @@ -618,7 +619,10 @@ body.show-codex .image-edit-route-wrap, body.show-gemini-cli #probeAsyncBtn, body.show-gemini-cli .advanced-endpoints, body.show-gemini-cli .image-request-mode-wrap, -body.show-gemini-cli .image-edit-route-wrap { display:none !important; } +body.show-gemini-cli .image-edit-route-wrap, +body.show-drawthings #probeAsyncBtn, +body.show-drawthings .protocol-selector-wrap, +body.show-drawthings .image-request-mode-wrap { display:none !important; } .protocol-selector-wrap, .image-request-mode-wrap, .image-edit-route-wrap { height:36px; padding:0 4px; flex-shrink:0; background:var(--panel); } diff --git a/static/js/api-settings.js b/static/js/api-settings.js index afab37e8b..9ef20d374 100644 --- a/static/js/api-settings.js +++ b/static/js/api-settings.js @@ -64,6 +64,8 @@ let rhWorkflowEditorZoom = document.getElementById('rhWorkflowEditorZoom'); const imageModelList = document.getElementById('imageModelList'); const chatModelList = document.getElementById('chatModelList'); const videoModelList = document.getElementById('videoModelList'); +const chatModelsBlock = document.getElementById('chatModelsBlock'); +const baseUrlLabel = document.getElementById('baseUrlLabel'); const msLoraBlock = document.getElementById('msLoraBlock'); const msLoraList = document.getElementById('msLoraList'); const recommendApiOverlay = document.getElementById('recommendApiOverlay'); @@ -93,11 +95,13 @@ const CODEX_DEFAULT_CHAT_MODELS = ['gpt-5.5']; const GEMINI_CLI_DEFAULT_IMAGE_MODELS = ['auto']; const GEMINI_CLI_DEFAULT_CHAT_MODELS = ['auto']; const CLI_PROTOCOLS = new Set(['jimeng', 'codex', 'gemini-cli']); -const API_PROTOCOLS = ['openai', 'apimart', 'gemini', 'tudou', 'volcengine', 'runninghub', 'jimeng', 'codex', 'gemini-cli']; +const DRAW_THINGS_DEFAULT_ENDPOINT = '127.0.0.1:7859'; +const API_PROTOCOLS = ['openai', 'apimart', 'gemini', 'tudou', 'volcengine', 'runninghub', 'jimeng', 'codex', 'gemini-cli', 'grpc']; const CLI_PROVIDER_PRESETS = { jimeng:{id:'jimeng', name:'即梦 CLI', protocol:'jimeng'}, codex:{id:'codex', name:'GPT CLI', protocol:'codex'}, - 'gemini-cli':{id:'gemini-cli', name:'Antigravity CLI', protocol:'gemini-cli'} + 'gemini-cli':{id:'gemini-cli', name:'Antigravity CLI', protocol:'gemini-cli'}, + drawthings:{id:'drawthings', name:'Draw Things gRPCServerCLI', protocol:'grpc'} }; const ONBOARDING_GUIDES = { modelscope:{ @@ -417,7 +421,7 @@ function deriveIdFromName(name, existingId){ function updateIdPreview(){ const item = provider(); if(!item) return; - const isBuiltin = item.id === 'comfly' || item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || item.id === 'jimeng'; + const isBuiltin = item.id === 'comfly' || item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || item.id === 'jimeng' || item.id === 'drawthings'; const idPreview = document.getElementById('idPreview'); if(!idPreview) return; if(isBuiltin){ @@ -806,19 +810,21 @@ function syncEditor(){ ? 'runninghub' : item.id === 'volcengine' ? 'volcengine' + : item.id === 'drawthings' + ? 'grpc' : (protocolInput?.value || 'openai'); - item.base_url = CLI_PROTOCOLS.has(selectedProtocol) ? '' : baseInput.value.trim(); + item.base_url = selectedProtocol === 'grpc' ? (baseInput.value.trim() || DRAW_THINGS_DEFAULT_ENDPOINT) : CLI_PROTOCOLS.has(selectedProtocol) ? '' : baseInput.value.trim(); // 固定平台不从协议下拉读取 item.protocol = selectedProtocol; item.image_request_mode = normalizeImageRequestMode( - item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || CLI_PROTOCOLS.has(selectedProtocol) + item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || CLI_PROTOCOLS.has(selectedProtocol) || selectedProtocol === 'grpc' ? 'openai' : lockedApi ? lockedApi.image_request_mode : (imageRequestModeInput?.value || item.image_request_mode) ); item.image_edit_route = normalizeImageEditRoute( - item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || CLI_PROTOCOLS.has(selectedProtocol) + item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || CLI_PROTOCOLS.has(selectedProtocol) || selectedProtocol === 'grpc' ? 'general' : (imageEditRouteInput?.value || item.image_edit_route) ); @@ -863,6 +869,7 @@ function updateProtocolFromInput(){ document.body.classList.toggle('show-jimeng', item.protocol === 'jimeng'); document.body.classList.toggle('show-codex', item.protocol === 'codex'); document.body.classList.toggle('show-gemini-cli', item.protocol === 'gemini-cli'); + document.body.classList.toggle('show-drawthings', item.protocol === 'grpc' || item.id === 'drawthings'); clearVerifyResult(); // 协议会改变整个表单(如即梦 CLI 账户面板、默认模型、Key 占位)。renderEditor 是唯一切换这些的入口, // 这里复跑一次让面板立即出现;保存并恢复 Key 输入框,避免推荐流程里先填的 Key 被 renderEditor 清空。 @@ -2504,17 +2511,17 @@ function renderEditor(){ if(lockedApi) applyLockedRecommendedProtocol(item); if(protocolInput){ protocolInput.value = item.id === 'runninghub' ? 'runninghub' : item.id === 'volcengine' ? 'volcengine' : (item.protocol || 'openai'); - protocolInput.disabled = FIXED_PROTOCOL_PROVIDER_IDS.has(item.id) || Boolean(lockedApi); + protocolInput.disabled = FIXED_PROTOCOL_PROVIDER_IDS.has(item.id) || item.id === 'drawthings' || Boolean(lockedApi); protocolInput.title = lockedApi ? '推荐平台使用固定协议' : (protocolInput.disabled ? '内置平台使用固定协议' : ''); } if(imageRequestModeInput){ imageRequestModeInput.value = normalizeImageRequestMode(item.image_request_mode); - imageRequestModeInput.disabled = Boolean(lockedApi) || item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || CLI_PROTOCOLS.has(String(protocolInput?.value || item.protocol || '').toLowerCase()); + imageRequestModeInput.disabled = Boolean(lockedApi) || item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || CLI_PROTOCOLS.has(String(protocolInput?.value || item.protocol || '').toLowerCase()) || String(protocolInput?.value || item.protocol || '').toLowerCase() === 'grpc'; imageRequestModeInput.title = lockedApi ? '推荐平台使用固定图片协议' : ''; } if(imageEditRouteInput){ imageEditRouteInput.value = normalizeImageEditRoute(item.image_edit_route); - imageEditRouteInput.disabled = item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || CLI_PROTOCOLS.has(String(protocolInput?.value || item.protocol || '').toLowerCase()); + imageEditRouteInput.disabled = item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || CLI_PROTOCOLS.has(String(protocolInput?.value || item.protocol || '').toLowerCase()) || String(protocolInput?.value || item.protocol || '').toLowerCase() === 'grpc'; } keyInput.value = ''; keyInput.placeholder = item.has_key ? `${tr('api.keepCurrentKey')} ${item.key_preview || ''}` : tr('api.enterKey'); @@ -2526,6 +2533,7 @@ function renderEditor(){ const isJimeng = String(protocolInput?.value || item.protocol || '').toLowerCase() === 'jimeng'; const isCodex = String(protocolInput?.value || item.protocol || '').toLowerCase() === 'codex'; const isGeminiCli = String(protocolInput?.value || item.protocol || '').toLowerCase() === 'gemini-cli'; + const isDrawThings = item.id === 'drawthings' || String(protocolInput?.value || item.protocol || '').toLowerCase() === 'grpc'; if(isRunningHub){ ensureRunningHubLists(item); if(rhFreeKeyInput){ @@ -2578,6 +2586,29 @@ function renderEditor(){ keyInput.placeholder = 'Antigravity CLI 使用本机 agy 登录态,无需 API Key'; keyHint.textContent = '请先安装 Antigravity CLI,并在终端执行 agy 完成登录'; } + if(isDrawThings){ + item.base_url = item.base_url || DRAW_THINGS_DEFAULT_ENDPOINT; + item.protocol = 'grpc'; + baseInput.value = item.base_url; + baseInput.placeholder = DRAW_THINGS_DEFAULT_ENDPOINT; + if(baseUrlLabel) baseUrlLabel.textContent = 'gRPCServerCLI 地址(主机:端口)'; + keyInput.placeholder = 'Draw Things gRPCServerCLI 默认使用本机 TLS 加密连接,无需 API Key'; + keyHint.textContent = '请先手动启动 Draw Things gRPCServerCLI,模型列表会通过 Echo() 读取'; + item.image_models = Array.isArray(item.image_models) ? item.image_models : []; + item.chat_models = []; + item.video_models = []; + } else if(baseUrlLabel){ + baseUrlLabel.textContent = tr('api.baseUrl') || '请求地址'; + } + if(chatModelsBlock){ + chatModelsBlock.hidden = isDrawThings; + chatModelsBlock.style.display = isDrawThings ? 'none' : ''; + } + const pickerChatTab = document.getElementById('pickerChatTab'); + if(pickerChatTab){ + pickerChatTab.hidden = isDrawThings; + pickerChatTab.style.display = isDrawThings ? 'none' : ''; + } document.body.classList.toggle('show-ms', isModelScope); document.body.classList.toggle('show-runninghub', isRunningHub); document.body.classList.toggle('show-volcengine', isVolcengine); @@ -2585,6 +2616,7 @@ function renderEditor(){ document.body.classList.toggle('show-jimeng', isJimeng); document.body.classList.toggle('show-codex', isCodex); document.body.classList.toggle('show-gemini-cli', isGeminiCli); + document.body.classList.toggle('show-drawthings', isDrawThings); updateApimartDomesticHint(item); renderProviderOnboarding(item); renderRecommendApi(); @@ -3226,8 +3258,10 @@ async function fetchModels(){ const baseUrl = baseInput.value.trim(); const apiKey = currentProviderApiKey(item); const isJimeng = (protocolInput?.value || '') === 'jimeng'; - const isCliProtocol = CLI_PROTOCOLS.has(String(protocolInput?.value || item.protocol || '').toLowerCase()); - if(!baseUrl && !isJimeng && !isCliProtocol){ alert('请先填写请求地址'); return; } + const currentProtocol = String(protocolInput?.value || item.protocol || '').toLowerCase(); + const isCliProtocol = CLI_PROTOCOLS.has(currentProtocol); + const isDrawThings = currentProtocol === 'grpc' || item.id === 'drawthings'; + if(!baseUrl && !isJimeng && !isCliProtocol && !isDrawThings){ alert('请先填写请求地址'); return; } if(btn){ btn.disabled = true; btn.querySelector('span').textContent = tr('api.fetchingModels') || '拉取中...'; } setStatus(tr('api.fetchingModels') || '正在从上游拉取模型列表...'); try { @@ -3255,7 +3289,9 @@ async function fetchModels(){ // 启用「选择模型」按钮,并 statusbar 显示已拉取数量 const openBtn = document.getElementById('openPickerBtn'); if(openBtn){ openBtn.disabled = false; openBtn.style.opacity = '1'; } - const extra = (runninghubContext || detectedProtocol === 'runninghub' || item.id === 'runninghub') + const extra = (isDrawThings || detectedProtocol === 'grpc') + ? ' · Draw Things gRPCServerCLI' + : (runninghubContext || detectedProtocol === 'runninghub' || item.id === 'runninghub') ? ` · RunningHub OpenAPI${runninghubModelSourceNote(data)}` : (detectedProtocol === 'volcengine' || isVolcengineProvider(item)) ? ' · 已识别方舟协议,火山聊天建议改填 ep-... 接入点' : ''; const imageModeExtra = normalizeImageRequestMode(imageRequestModeInput?.value || item.image_request_mode) === 'openai-json' ? ' · 图片接口已设为 OpenAI JSON' : ''; @@ -3276,8 +3312,9 @@ let pickerVisibleIds = []; function openModelPicker(){ const item = provider(); if(!item || !lastFetchedAll.length){ alert('没有拉取到模型'); return; } - const existing = { image: new Set(item.image_models||[]), chat: new Set(item.chat_models||[]), video: new Set(item.video_models||[]) }; - const allIds = new Set([...lastFetchedAll, ...(item.image_models||[]), ...(item.chat_models||[]), ...(item.video_models||[])]); + const isDrawThings = item.id === 'drawthings' || String(item.protocol || '').toLowerCase() === 'grpc'; + const existing = { image: new Set(item.image_models||[]), chat: new Set(isDrawThings ? [] : (item.chat_models||[])), video: new Set(isDrawThings ? [] : (item.video_models||[])) }; + const allIds = new Set([...lastFetchedAll, ...(item.image_models||[]), ...(isDrawThings ? [] : (item.chat_models||[])), ...(isDrawThings ? [] : (item.video_models||[]))]); pickerState = { category: {}, selected: {} }; allIds.forEach(id => { // 类别归属:用户已配置 > 关键字建议 > 默认 chat @@ -3287,7 +3324,7 @@ function openModelPicker(){ else if(existing.chat.has(id)) cat = 'chat'; else if(lastFetchedSuggestion?.image?.has(id)) cat = 'image'; else if(lastFetchedSuggestion?.video?.has(id)) cat = 'video'; - else cat = 'chat'; + else cat = isDrawThings ? 'image' : 'chat'; pickerState.category[id] = cat; // 默认勾选状态:已在用户配置里的 = 勾选;新拉的 = 不勾选(让用户主动选) pickerState.selected[id] = existing.image.has(id) || existing.chat.has(id) || existing.video.has(id); @@ -3368,6 +3405,7 @@ function selectPickerCat(cat){ } function applyModelPicker(){ const item = provider(); if(!item) return; + const isDrawThings = item.id === 'drawthings' || String(item.protocol || '').toLowerCase() === 'grpc'; const image = [], chat = [], video = []; const modelNames = {}; Object.entries(pickerState.selected).forEach(([id, sel]) => { @@ -3380,8 +3418,8 @@ function applyModelPicker(){ if(label && label !== id) modelNames[id] = label; }); item.image_models = image; - item.chat_models = chat; - item.video_models = video; + item.chat_models = isDrawThings ? [] : chat; + item.video_models = isDrawThings ? [] : video; item.model_names = modelNames; renderModels('image'); renderModels('chat'); renderModels('video'); renderMsLoras(); @@ -3570,9 +3608,13 @@ async function addCliProvider(kind){ } item.id = preset.id; item.name = item.name || preset.name; - item.base_url = ''; + item.base_url = preset.protocol === 'grpc' ? (item.base_url || DRAW_THINGS_DEFAULT_ENDPOINT) : ''; item.protocol = preset.protocol; - if(preset.protocol === 'jimeng'){ + if(preset.protocol === 'grpc'){ + item.image_models = unique(item.image_models || []); + item.chat_models = []; + item.video_models = []; + } else if(preset.protocol === 'jimeng'){ item.image_models = unique([...(item.image_models || []).filter(model => !JIMENG_LEGACY_IMAGE_MODELS.has(String(model || '').trim())), ...JIMENG_DEFAULT_IMAGE_MODELS]); item.video_models = unique([...(item.video_models || []).filter(model => !JIMENG_LEGACY_VIDEO_MODELS.has(String(model || '').trim())), ...JIMENG_DEFAULT_VIDEO_MODELS]); item.chat_models = unique(item.chat_models || []); @@ -3588,7 +3630,7 @@ async function addCliProvider(kind){ selectedId = item.id; renderEditor(); if(protocolInput) protocolInput.value = preset.protocol; - setStatus(`${preset.name} 已添加,使用本机登录态,无需填写 API Key。`); + setStatus(preset.protocol === 'grpc' ? `${preset.name} 已添加,请先启动 gRPCServerCLI。` : `${preset.name} 已添加,使用本机登录态,无需填写 API Key。`); } } function deleteProvider(){ @@ -3734,23 +3776,32 @@ async function saveProviders(){ providers.forEach(item => { item.id = normalizeId(item.id); applyLockedRecommendedProtocol(item); - item.protocol = item.id === 'runninghub' + item.protocol = item.id === 'drawthings' + ? 'grpc' + : item.id === 'runninghub' ? 'runninghub' : item.id === 'volcengine' ? 'volcengine' : API_PROTOCOLS.includes(String(item.protocol || '').toLowerCase()) ? String(item.protocol).toLowerCase() : 'openai'; const isCliProtocol = CLI_PROTOCOLS.has(item.protocol); + const isDrawThings = item.protocol === 'grpc' || item.id === 'drawthings'; item.image_request_mode = normalizeImageRequestMode( - item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || isCliProtocol + item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || isCliProtocol || isDrawThings ? 'openai' : item.image_request_mode ); item.image_edit_route = normalizeImageEditRoute( - item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || isCliProtocol + item.id === 'modelscope' || item.id === 'runninghub' || item.id === 'volcengine' || isCliProtocol || isDrawThings ? 'general' : item.image_edit_route ); if(isCliProtocol) applyCliProtocolDefaults(item, item.protocol); + if(isDrawThings){ + item.base_url = item.base_url || DRAW_THINGS_DEFAULT_ENDPOINT; + item.image_models = unique(item.image_models || []); + item.chat_models = []; + item.video_models = []; + } if(item.id === 'runninghub'){ item.base_url = item.base_url || RH_DEFAULT_BASE_URL; item.image_models = unique(item.image_models || []); @@ -3794,7 +3845,7 @@ async function saveProviders(){ id:item.id, name:item.name, base_url:item.base_url, - protocol:(item.id === 'modelscope') ? 'openai' : item.id === 'runninghub' ? 'runninghub' : item.id === 'volcengine' ? 'volcengine' : (item.protocol || 'openai'), + protocol:(item.id === 'drawthings') ? 'grpc' : (item.id === 'modelscope') ? 'openai' : item.id === 'runninghub' ? 'runninghub' : item.id === 'volcengine' ? 'volcengine' : (item.protocol || 'openai'), image_request_mode:item.image_request_mode || 'openai', image_edit_route:item.image_edit_route || 'general', image_generation_endpoint:item.image_generation_endpoint || '', diff --git a/static/js/canvas.js b/static/js/canvas.js index 4d346cf1b..c045f21e9 100644 --- a/static/js/canvas.js +++ b/static/js/canvas.js @@ -690,6 +690,97 @@ function providerImageModels(providerId){ const provider = apiProviders.find(p => p.id === providerId); return uniqueModels(provider?.image_models || []); } +function isDrawThingsProvider(provider){ + return String(provider?.id || '').trim().toLowerCase() === 'drawthings' + || String(provider?.protocol || '').trim().toLowerCase() === 'grpc'; +} +// Keep Draw Things seeds in the same uint32 range as the canvas/ComfyUI seed controls. +function drawThingsRandomSeed(){ return Math.floor(Math.random() * 4294967295) + 1; } +function normalizeDrawThingsSeed(value){ + const num = Number(value); + if(!Number.isFinite(num)) return drawThingsRandomSeed(); + return Math.max(1, Math.min(4294967295, Math.floor(num))); +} +// 普通画布只在随机模式下显示本次实际提交的 seed;固定模式必须保留用户输入,供指纹缓存复用。 +function applyDrawThingsSeedToGenerator(gen, seeds=[]){ + if(!gen || gen.drawThingsSeedRandom === false || !Array.isArray(seeds) || !seeds.length) return; + gen.drawThingsSeed = normalizeDrawThingsSeed(seeds[seeds.length - 1]); +} +// 统一把请求对象按 key 排序,避免对象属性插入顺序不同造成错误的指纹变化。 +function stableGenerationFingerprintValue(value){ + if(Array.isArray(value)) return value.map(stableGenerationFingerprintValue); + if(value && typeof value === 'object'){ + return Object.keys(value).sort().reduce((result, key) => { + result[key] = stableGenerationFingerprintValue(value[key]); + return result; + }, {}); + } + return value; +} +// 只要存在固定 seed,就必须把完整请求和 seed 一起验证;随机模式本身不命中缓存, +// 但生成完成后会按实际提交的 seed 暂存结果,方便关闭骰子后复用刚生成的图片。 +function generationRequestFingerprint(payload, count, fixedSeedState){ + if(!fixedSeedState || fixedSeedState.random) return ''; + return JSON.stringify(stableGenerationFingerprintValue({ + request:payload || {}, + count:Number(count) || 1, + seed:fixedSeedState + })); +} +function drawThingsSeedResultCacheKey(payload, seed){ + return generationRequestFingerprint(payload, 1, { + provider:'drawthings', + seed:normalizeDrawThingsSeed(seed) + }); +} +function generationSeedField(field){ + const name = `${field?.id || ''} ${field?.input || ''} ${field?.name || ''} ${field?.fieldName || ''}`.toLowerCase(); + return /(^|[^a-z])(seed|noise[_ -]?seed|random[_ -]?seed)([^a-z]|$)|种子|噪声/.test(name); +} +// ComfyUI/RunningHub 的动态参数都通过同一个 helper 判断是否是固定 seed。 +function fixedGenerationSeedState({fields=[], valueFor, randomEnabled, randomActive}){ + const values = {}; + let hasSeed = false; + let random = false; + (fields || []).forEach(field => { + if(!generationSeedField(field)) return; + hasSeed = true; + const id = field.id || field.fieldName || field.input || String(Object.keys(values).length); + if(randomEnabled?.(field) && randomActive?.(field)) random = true; + values[id] = valueFor ? valueFor(field) : undefined; + }); + if(!hasSeed || random) return null; + return {fields:values}; +} +function generationCachedOutputsForNode(node, out, key){ + const cache = node?._fixedSeedGenerationCache; + const outputs = Array.isArray(cache?.outputs) ? cache.outputs.filter(Boolean) : []; + if(!key || cache?.key !== key || !outputs.length) return []; + // 删除过输出图片后不能继续复用已不存在的结果。 + const available = out + ? outputs.every(url => outputHasUrl(out, url)) + : outputs.every(url => (node.generatedOutputs || []).some(item => outputUrlValue(item) === url)); + return available ? outputs : []; +} +function rememberGenerationOutputs(node, key, outputs){ + if(!node || !key) return; + const urls = (outputs || []).map(outputUrlValue).filter(Boolean); + if(urls.length) node._fixedSeedGenerationCache = {key, outputs:urls}; +} +async function refreshDrawThingsModels(){ + const provider = apiProviders.find(p => isDrawThingsProvider(p)); + if(!provider) return; + try { + const data = await fetch('/api/drawthings/models').then(r => r.json()); + provider.image_models = Array.isArray(data.models) ? uniqueModels(data.models) : []; + provider.drawthings_connected = Boolean(data.connected); + provider.drawthings_status = data.message || ''; + } catch(err){ + provider.image_models = []; + provider.drawthings_connected = false; + provider.drawthings_status = String(err?.message || err || '连接失败'); + } +} function sanitizeImageNodeProviderModel(node){ if(!node || node.type !== 'generator') return; node.apiProvider = resolveImageProviderId(node.apiProvider || ''); @@ -9838,6 +9929,7 @@ async function runRhNode(nodeId, opts={}){ const pendingId = uid('p'); const run = runSnapshot(node, media.prompt || 'RunningHub', media.refs); run.taskLabel = 'RunningHub'; + let fixedSeedCacheKey = ''; if(out) out._pending = [...(out._pending || []), makePendingForRun(pendingId, run, node, {refs:media.refs, cascadeTargetId})]; if(!opts.cascade) node.running = true; refreshRunNodes(node, out); @@ -9848,6 +9940,26 @@ async function runRhNode(nodeId, opts={}){ const body = mode === 'workflow' ? {workflowId:node.workflowId.trim(), nodeInfoList, useWallet:rhUseWallet(node), ...workflowExtras} : {webappId:node.webappId.trim(), nodeInfoList, instanceType:node.instanceType || '', useWallet:rhUseWallet(node)}; + const fields = rhActiveFields(node); + const fixedSeedState = fixedGenerationSeedState({ + fields, + valueFor:field => nodeInfoList.find(item => rhParamKey(item.nodeId, item.fieldName) === rhParamKey(field.nodeId, field.fieldName))?.fieldValue, + randomEnabled:rhRandomEnabled, + randomActive:field => rhRandomActive(node, rhParamKey(field.nodeId, field.fieldName)) + }); + fixedSeedCacheKey = generationRequestFingerprint({engine:'runninghub', mode, body}, 1, fixedSeedState); + const cached = fixedSeedCacheKey ? generationCachedOutputsForNode(node, out, fixedSeedCacheKey) : []; + if(cached.length){ + // RunningHub 的固定 seed 也按完整 nodeInfoList 指纹复用,随机字段开启时不会进入这里。 + const cachedMeta = collectRunMeta(out, pendingId); + if(out) out._pending = (out._pending || []).filter(p => p.id !== pendingId); + appendOutputImages(out, cached, media.refs[0], [cachedMeta]); + mergeGeneratedOutputs(node, cached, Boolean(opts.cascade)); + node.runStatus = 'done'; node.runError = ''; node.running = false; + refreshRunNodes(node, out); + scheduleSave(); + return; + } const submit = await cascadeFetch(endpoint, { method:'POST', headers:{'Content-Type':'application/json'}, @@ -9879,6 +9991,7 @@ async function runRhNode(nodeId, opts={}){ if(!result) throw new Error(tr('canvas.rhTimeout')); const outputs = result.urls || []; if(!outputs.length) throw new Error(tr('canvas.rhOutputsEmpty')); + rememberGenerationOutputs(node, fixedSeedCacheKey, outputs); const meta = collectRunMeta(out, pendingId); if(out) out._pending = (out._pending || []).filter(p => p.id !== pendingId); appendOutputImages(out, outputs, media.refs[0], [meta]); @@ -10443,8 +10556,44 @@ async function runGenerator(genId, opts={}){ size:await generatorSizeForRun(gen, refs), reference_images:refs.slice(0, CANVAS_REFERENCE_IMAGE_MAX) }; + const drawThingsSelected = isDrawThingsProvider(providerById(payload.provider_id)); + const submittedSeeds = []; const quality = normalizedImageQuality(gen.quality); if(quality) payload.quality = quality; + const requestPayload = () => { + if(!drawThingsSelected) return payload; + // Match the existing dice behavior: each batch request gets a new seed + // while disabling the dice keeps the value entered in the node. + const seed = gen.drawThingsSeedRandom !== false + ? drawThingsRandomSeed() + : normalizeDrawThingsSeed(gen.drawThingsSeed); + submittedSeeds.push(seed); + return { + ...payload, + seed + }; + }; + const fixedSeedState = drawThingsSelected && gen.drawThingsSeedRandom === false + ? {provider:'drawthings', seed:normalizeDrawThingsSeed(gen.drawThingsSeed)} + : null; + const fixedSeedCacheKey = generationRequestFingerprint(payload, count, fixedSeedState); + // 将缓存键带进结果元数据,批量任务完成后可以准确归并到本次请求。 + if(fixedSeedCacheKey) run.fixedSeedCacheKey = fixedSeedCacheKey; + const cachedFixedSeedOutputs = fixedSeedCacheKey + ? generationCachedOutputsForNode(gen, out, fixedSeedCacheKey) + : []; + if(cachedFixedSeedOutputs.length){ + // 固定 seed 且完整请求未变化时复用当前结果,避免再次提交相同的 gRPC 任务。 + setStatus('检测到 seed 值(种子数)相同,跳过生成'); + mergeGeneratedOutputs(gen, cachedFixedSeedOutputs, Boolean(opts.cascade)); + gen.runStatus = 'done'; + gen.runError = ''; + gen.running = false; + refreshRunNodes(gen, out); + scheduleSave(); + return; + } + const runCacheKey = fixedSeedCacheKey; let pendingIds = []; const startedAt = nowMs(); if(!opts.cascade){ @@ -10454,16 +10603,27 @@ async function runGenerator(genId, opts={}){ setTimeout(() => { gen.running = false; refreshRunNodes(gen, out); }, 2000); } try { - const taskInfos = await Promise.all(Array.from({length:count}, () => createCanvasImageTask(payload, {cascadeTargetId}))); + const taskInfos = await Promise.all(Array.from({length:count}, () => createCanvasImageTask(requestPayload(), {cascadeTargetId}))); + applyDrawThingsSeedToGenerator(gen, submittedSeeds); + const randomSeedCacheKeys = drawThingsSelected && gen.drawThingsSeedRandom !== false + ? submittedSeeds.map(seed => drawThingsSeedResultCacheKey(payload, seed)) + : []; if(!out){ let outputs = []; + const taskOutputs = []; for(const task of taskInfos){ const result = await waitCanvasImageTaskResult(task.task_id, {cascadeTargetId}); - outputs.push(...(result.images || [])); + const images = result.images || []; + taskOutputs.push(images); + outputs.push(...images); run.request = requestMetaFromResult(result); } if(!outputs.length) throw new Error(tr('canvas.generationFailed')); mergeGeneratedOutputs(gen, outputs, Boolean(opts.cascade)); + if(runCacheKey) rememberGenerationOutputs(gen, runCacheKey, outputs); + else taskOutputs.forEach((images, index) => { + rememberGenerationOutputs(gen, randomSeedCacheKeys[index], images); + }); addGenerationLog({run, outputs, runMs:nowMs() - startedAt}); gen.runStatus = 'done'; gen.runError = ''; @@ -10475,13 +10635,18 @@ async function runGenerator(genId, opts={}){ pendingIds = taskInfos.map(() => uid('p')); if(out) out._pending = [ ...(out._pending || []), - ...taskInfos.map((task, index) => makePendingForRun(pendingIds[index], run, gen, {refs, requestSize:payload.size, cascadeTargetId}, { - canvasTaskId:task.task_id, - canvasTaskType:'online-image', - providerId:payload.provider_id, - model:payload.model, - appendGenerated:Boolean(opts.cascade) - })) + ...taskInfos.map((task, index) => { + const taskCacheKey = runCacheKey || randomSeedCacheKeys[index] || ''; + const taskRun = taskCacheKey ? {...run, fixedSeedCacheKey:taskCacheKey} : run; + return makePendingForRun(pendingIds[index], taskRun, gen, {refs, requestSize:payload.size, cascadeTargetId}, { + canvasTaskId:task.task_id, + canvasTaskType:'online-image', + providerId:payload.provider_id, + model:payload.model, + appendGenerated:Boolean(opts.cascade), + fixedSeedCacheKey:taskCacheKey + }); + }) ]; refreshRunNodes(gen, out); scheduleSave(); @@ -10539,19 +10704,57 @@ async function runGeneratorLegacy(genId, opts={}){ size:requestSize, reference_images:refs.slice(0, CANVAS_REFERENCE_IMAGE_MAX) }; + const drawThingsSelected = isDrawThingsProvider(providerById(payload.provider_id)); + const submittedSeeds = []; + const requestPayload = () => { + if(!drawThingsSelected) return payload; + // The legacy endpoint also honors the generator's seed control. + const seed = gen.drawThingsSeedRandom !== false + ? drawThingsRandomSeed() + : normalizeDrawThingsSeed(gen.drawThingsSeed); + submittedSeeds.push(seed); + return { + ...payload, + seed + }; + }; const quality = normalizedImageQuality(gen.quality); if(quality) payload.quality = quality; + const fixedSeedState = isDrawThingsProvider(providerById(payload.provider_id)) && gen.drawThingsSeedRandom === false + ? {provider:'drawthings', seed:normalizeDrawThingsSeed(gen.drawThingsSeed)} + : null; + const fixedSeedCacheKey = generationRequestFingerprint(payload, count, fixedSeedState); + const cached = fixedSeedCacheKey ? generationCachedOutputsForNode(gen, out, fixedSeedCacheKey) : []; + if(cached.length){ + setStatus('检测到 seed 值(种子数)相同,跳过生成'); + const cachedMetas = collectRunMetas(out, pendingIds); + if(out) out._pending = (out._pending || []).filter(p => !pendingIds.includes(p.id)); + appendOutputImages(out, cached, refs[0], cachedMetas); + mergeGeneratedOutputs(gen, cached, Boolean(opts.cascade)); + gen.runStatus = 'done'; gen.runError = ''; gen.running = false; + refreshRunNodes(gen, out); + scheduleSave(); + return; + } const results = await Promise.all(Array.from({length:count}, () => fetch('/api/online-image', { method:'POST', headers:{'Content-Type':'application/json'}, - body:JSON.stringify(payload) + body:JSON.stringify(requestPayload()) }).then(async r => { if(!r.ok) throw new Error(await responseErrorMessage(r, tr('canvas.generationFailed'))); return r.json(); }))); + applyDrawThingsSeedToGenerator(gen, submittedSeeds); const images = results.flatMap(result => result.images || []); + const randomSeedCacheKeys = drawThingsSelected && gen.drawThingsSeedRandom !== false + ? submittedSeeds.map(seed => drawThingsSeedResultCacheKey(payload, seed)) + : []; const metas = collectRunMetas(out, pendingIds); run.request = results[0] ? requestMetaFromResult(results[0]) : {}; if(out) out._pending = (out._pending||[]).filter(p => !pendingIds.includes(p.id)); appendOutputImages(out, images, refs[0], metas); mergeGeneratedOutputs(gen, images, Boolean(opts.cascade)); + if(fixedSeedCacheKey) rememberGenerationOutputs(gen, fixedSeedCacheKey, images); + else results.forEach((result, index) => { + rememberGenerationOutputs(gen, randomSeedCacheKeys[index], result.images || []); + }); addGenerationLog({run, outputs:images, runMs:Math.max(...metas.map(m => m.runMs || 0), 0)}); gen.runStatus = 'done'; gen.runError = ''; refreshRunNodes(gen, out); @@ -11254,6 +11457,21 @@ async function runLTXDirectorNode(nodeId, opts={}){ [LTX_DIRECTOR_WF_NODE]:directorInputs, [LTX_DIRECTOR_SEED_NODE]:{noise_seed:Number(node.noiseSeed ?? 12)} }; + const fixedSeedCacheKey = generationRequestFingerprint({engine:'comfy', workflow:LTX_DIRECTOR_WORKFLOW, prompt:globalPrompt, params}, 1, { + provider:'comfy', + fields:{noise_seed:Number(node.noiseSeed ?? 12)} + }); + const cached = generationCachedOutputsForNode(node, out, fixedSeedCacheKey); + if(cached.length){ + const cachedMeta = collectRunMeta(out, pendingId); + if(out) out._pending = (out._pending || []).filter(p => p.id !== pendingId); + appendOutputImages(out, cached, refs[0], [cachedMeta]); + mergeGeneratedOutputs(node, cached, Boolean(opts.cascade)); + node.runStatus = 'done'; node.runError = ''; node.running = false; + refreshRunNodes(node, out); + scheduleSave(); + return; + } const result = await runQueuedComfyGenerate({ prompt:globalPrompt, workflow_json:LTX_DIRECTOR_WORKFLOW, @@ -11265,6 +11483,7 @@ async function runLTXDirectorNode(nodeId, opts={}){ if(result.error) throw new Error(result.error); const outputs = comfyResultOutputs(result); if(!outputs.length) throw new Error(tr('canvas.ltxNoOutput')); + rememberGenerationOutputs(node, fixedSeedCacheKey, outputs); const meta = collectRunMeta(out, pendingId); if(out) out._pending = (out._pending || []).filter(p => p.id !== pendingId); appendOutputImages(out, outputs, refs[0], [meta]); @@ -11315,6 +11534,7 @@ async function runComfyNode(nodeId, opts={}){ const pendingId = uid('p'); const run = runSnapshot(node, prompt, refs); run.taskLabel = comfyRunLabel(node); + let fixedSeedCacheKey = ''; const requestSize = mode === 'text' ? {width:Number(node.width || 1024), height:Number(node.height || 1024)} : null; if(out) out._pending = [...(out._pending||[]), makePendingForRun(pendingId, run, node, {refs, requestSize, cascadeTargetId})]; if(!opts.cascade){ @@ -11396,6 +11616,25 @@ async function runComfyNode(nodeId, opts={}){ } params[f.node][f.input] = comfyParamValue(node, f); }); + const fixedSeedState = fixedGenerationSeedState({ + fields:settingFields, + valueFor:f => comfyParamValue(node, f), + randomEnabled:comfyRandomEnabled, + randomActive:f => comfyRandomActive(node, f.id) + }); + fixedSeedCacheKey = generationRequestFingerprint({engine:'comfy', mode, workflow:workflowName, prompt, refs:allRefs, params}, 1, fixedSeedState); + const cached = fixedSeedCacheKey ? generationCachedOutputsForNode(node, out, fixedSeedCacheKey) : []; + if(cached.length){ + // Comfy 固定 seed 与其它 provider 使用同一套完整请求指纹,命中时不再重复提交。 + const cachedMeta = collectRunMeta(out, pendingId); + if(out) out._pending = (out._pending || []).filter(p => p.id !== pendingId); + appendOutputImages(out, cached, refs[0], [cachedMeta]); + mergeGeneratedOutputs(node, cached, Boolean(opts.cascade)); + node.runStatus = 'done'; node.runError = ''; node.running = false; + refreshRunNodes(node, out); + scheduleSave(); + return; + } const result = await runQueuedComfyGenerate({ prompt, workflow_json:workflowName, @@ -11407,6 +11646,7 @@ async function runComfyNode(nodeId, opts={}){ if(result.error) throw new Error(actionFailed('canvas.comfyCustom', result.error)); images = comfyResultOutputs(result); if(!images.length) throw new Error(noReturnedImage('canvas.comfyCustom')); + rememberGenerationOutputs(node, fixedSeedCacheKey, images); } else { run.taskLabel = tr('canvas.comfyEdit'); const names = []; @@ -12390,6 +12630,7 @@ function completeRecoverPendingOutput(out, pending, result){ const gen = nodes.find(n => n.id === meta.run?.node?.id); if(gen){ mergeGeneratedOutputs(gen, images, Boolean(pending.appendGenerated)); + rememberGenerationOutputs(gen, pending.fixedSeedCacheKey, images); gen.runStatus = 'done'; gen.runError = ''; gen.running = false; @@ -12508,6 +12749,15 @@ function completeCanvasImageTask(taskId, result){ const gen = nodes.find(n => n.id === meta.run?.node?.id); if(gen){ mergeGeneratedOutputs(gen, images, Boolean(pending.appendGenerated)); + // 批量任务逐个完成;只有本次固定 seed 的同组任务全部结束后才写入缓存。 + const sameRunPending = (out._pending || []).some(item => item.fixedSeedCacheKey === pending.fixedSeedCacheKey); + if(!sameRunPending && pending.fixedSeedCacheKey){ + const cachedImages = (out.images || []) + .filter(item => item?.fixedSeedCacheKey === pending.fixedSeedCacheKey) + .map(outputUrlValue) + .filter(Boolean); + rememberGenerationOutputs(gen, pending.fixedSeedCacheKey, cachedImages); + } gen.runStatus = 'done'; gen.runError = ''; gen.running = false; @@ -12632,6 +12882,7 @@ function appendOutputImages(out, images, compareRef, metas=[], layout=null){ if(source.kind || source.mediaKind) item.kind = source.kind || source.mediaKind; if(meta.kind) item.kind = meta.kind; if(meta.grid) item.grid = meta.grid; + if(meta.run?.fixedSeedCacheKey) item.fixedSeedCacheKey = meta.run.fixedSeedCacheKey; return item; })]; if(compareRef?.url){ diff --git a/static/js/i18n/smart-canvas.js b/static/js/i18n/smart-canvas.js index 418b6a5b1..2dc84d75d 100644 --- a/static/js/i18n/smart-canvas.js +++ b/static/js/i18n/smart-canvas.js @@ -168,6 +168,7 @@ "smart.audioDurationRange": { zh: "音频时长 {sec}s 不在 2–15s 范围,已忽略该音频", en: "Audio duration {sec}s is outside 2–15s and was ignored" }, "smart.diceOn": { zh: "随机已开启,点击关闭", en: "Random ON. Click to disable." }, "smart.diceOff": { zh: "随机已关闭,点击开启", en: "Random OFF. Click to enable." }, + "smart.drawThingsSeed": { zh: "种子", en: "Seed" }, "smart.inputThumbs": { zh: "输入图", en: "input" }, "smart.inputCount": { zh: "{n} 输入图", en: "{n} input images" }, "smart.inputNum": { zh: "输入 {n}", en: "Input {n}" }, @@ -234,6 +235,7 @@ "smart.errComfyEmpty": { zh: "ComfyUI 返回图片为空", en: "ComfyUI returned empty images" }, "smart.errEnhanceNeedRefs": { zh: "图片增强需要参考图", en: "Enhance requires a reference image" }, "smart.errEditNeedRefs": { zh: "图片编辑需要参考图", en: "Edit requires reference images" }, + "smart.errDrawThingsMultiImage": { zh: "当前 Draw Things 模型不支持多图图像编辑,请只保留一张输入图,或切换到 Klein/Qwen Edit 模型", en: "The selected Draw Things model does not support multi-image editing. Keep one input image or switch to a Klein/Qwen Edit model" }, "smart.errRunFailed": { zh: "生成失败", en: "Generation failed" }, "smart.errRunTimeout": { zh: "生成超时", en: "Generation timed out" }, "smart.refMapHeader": { zh: "下面是参考图编号:", en: "Reference image map:" }, diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index fad660b4f..64bc94f37 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -306,6 +306,9 @@ let settings = { model:'', ratio:'square', resolution:'4k', + // Draw Things 的 seed 只在选择 gRPC provider 时使用,其他 provider 不携带该字段。 + drawThingsSeed:0, + drawThingsSeedRandom:true, customRatio:'', customRatioWidth:'', customRatioHeight:'', @@ -703,6 +706,28 @@ function isGptImageAutoSizeModel(model){ function defaultSmartApiResolution(model){ return isGptImageAutoSizeModel(model) ? '4k' : '1k'; } +// 生成结果按图片保存一份不可变的运行快照,避免后续修改节点参数覆盖历史结果。 +function snapshotImageGenerationMeta(meta){ + if(!meta || typeof meta !== 'object') return null; + return { + prompt:meta.prompt || '', + displayPrompt:meta.displayPrompt || meta.promptText || meta.prompt || '', + promptHtml:meta.promptHtml || '', + promptText:meta.promptText || meta.displayPrompt || meta.prompt || '', + promptRefs:cloneSmartSettings(meta.promptRefs || []), + inputRefs:cloneSmartSettings(meta.inputRefs || meta.promptRefs || []), + sourceNodeId:meta.sourceNodeId || '', + settings:cloneSmartSettings(meta.settings || {}), + createdAt:Number(meta.createdAt || Date.now()) + }; +} +function attachImageGenerationMeta(images=[], meta=null){ + const snapshot = snapshotImageGenerationMeta(meta); + if(!snapshot) return images || []; + return (images || []).map(img => img?.url + ? {...img, generationMeta:snapshotImageGenerationMeta(snapshot)} + : img); +} function mediaItemForStorage(item){ if(!item || typeof item !== 'object') return item; const clean = {...item}; @@ -1136,7 +1161,8 @@ function stripOutpaintDisplaySettings(settingsObj, node=null){ return clean; } function smartSettingsForNode(node){ - const nodeSettings = stripOutpaintDisplaySettings(node?.runSettings || {}, node); + const imageMeta = selectedImageGenerationMetaForNode(node); + const nodeSettings = stripOutpaintDisplaySettings(imageMeta?.settings || node?.runSettings || {}, node); const recentSettings = Object.keys(nodeSettings).length ? {} : recentSmartSettingsForMode(); const base = { ...cloneSmartSettings(canvasDefaultSmartSettings || initialSmartSettings), @@ -1146,6 +1172,17 @@ function smartSettingsForNode(node){ normalizeSmartVideoModeSettings(base, true); return withOutpaintDisplaySettings(node, base); } +function selectedImageGenerationMetaForNode(node){ + if(!node || selectedImage.index < 0 || !selectedImage.nodeId) return null; + const owner = nodes.find(item => item.id === selectedImage.nodeId); + if(!owner || !owner.images?.[Number(selectedImage.index)]) return null; + if(owner.id !== node.id){ + const belongsToGroup = node.type === 'smart-group' + && smartGroupImageRefs(node).some(ref => ref.nodeId === owner.id && Number(ref.index) === Number(selectedImage.index)); + if(!belongsToGroup) return null; + } + return owner.images[Number(selectedImage.index)]?.generationMeta || null; +} function activeSettingsSubject(){ const active = activeComposerSubject?.id ? (nodes.find(n => n.id === activeComposerSubject.id) || activeComposerSubject) @@ -1165,6 +1202,15 @@ function persistActiveSmartSettings(){ subject.runSettings = settingsForStorage(settings); rememberRecentSmartSettings(settings, subject); } +function restoreSmartSettingsAfterRun(runNode, previousSettings){ + const selected = selectedNode(); + const activeSubject = isSmartRunnableNode(selected) ? selected : activeSettingsSubject(); + if(activeSubject?.id === runNode?.id || !activeSubject){ + settings = previousSettings; + return; + } + settings = smartSettingsForNode(activeSubject); +} function rememberCanvasListProject(projectId){ const pid = projectId || 'default'; try { localStorage.setItem(CANVAS_LIST_PROJECT_KEY, pid); } catch(e){} @@ -1640,6 +1686,7 @@ function mediaLayoutSize(img){ } function copyMediaSizeFields(source, target={}){ if(!source || typeof source !== 'object') return target; + if(source.batchCopyId) target.batchCopyId = source.batchCopyId; ['natural_w','natural_h','width','height','w','h','layout_w','layout_h'].forEach(key => { const n = Number(source[key]); if(Number.isFinite(n) && n > 0) target[key] = n; @@ -2380,6 +2427,155 @@ function providerImageModels(providerId){ if(providerId === 'volcengine') return volcengineProvider().image_models || []; return (apiProviders || []).find(p => p.id === providerId)?.image_models || []; } +// Draw Things 的 provider 由用户在 API 设置中添加,使用 id/protocol 双重判断以兼容已有配置。 +function isDrawThingsProvider(providerId){ + // 运行时配置尚未刷新时也要识别已保存的 Draw Things provider。 + if(String(providerId || '').trim().toLowerCase() === 'drawthings') return true; + const provider = apiProviderById(providerId); + return String(provider?.id || '').trim().toLowerCase() === 'drawthings' + || String(provider?.protocol || '').trim().toLowerCase() === 'grpc'; +} +function drawThingsModelSupportsEditing(model){ + const normalized = String(model || '').trim().toLowerCase().replace(/-/g, '_'); + if(normalized.includes('klein')) return true; + return normalized.includes('qwen') && normalized.includes('edit'); +} +function drawThingsReferenceImagesForRequest(node, refs, runSettings){ + const references = (refs || []).filter(ref => ref?.url); + // 非编辑模型也支持标准单图图生图;当前节点上一轮输出因此必须保留为 + // 参考图,不能再被当成“编辑模型自引用”过滤掉。多图限制在提交前统一提示。 + return references; +} +function drawThingsReferenceError(runSettings, refs){ + if(!isDrawThingsProvider(runSettings?.provider_id)) return ''; + if(drawThingsModelSupportsEditing(runSettings?.model)) return ''; + return (refs || []).filter(ref => ref?.url).length > 1 + ? tr('smart.errDrawThingsMultiImage') + : ''; +} +function drawThingsRandomSeed(){ + if(globalThis.crypto?.getRandomValues){ + const values = new Uint32Array(1); + globalThis.crypto.getRandomValues(values); + return Math.max(1, values[0] % 4294967295); + } + return Math.floor(Math.random() * 4294967295) + 1; +} +function drawThingsUniqueRandomSeed(usedSeeds){ + let seed = drawThingsRandomSeed(); + while(usedSeeds.has(seed)) seed = drawThingsRandomSeed(); + usedSeeds.add(seed); + return seed; +} +function normalizeDrawThingsSeed(value){ + const num = Number(value); + if(!Number.isFinite(num)) return drawThingsRandomSeed(); + return Math.max(1, Math.min(4294967295, Math.floor(num))); +} +// 统一生成完整请求指纹;固定 seed 才允许复用,随机 seed 明确跳过缓存。 +const fixedSeedGenerationCache = new Map(); +function stableGenerationFingerprintValue(value){ + if(Array.isArray(value)) return value.map(stableGenerationFingerprintValue); + if(value && typeof value === 'object'){ + return Object.keys(value).sort().reduce((result, key) => { + result[key] = stableGenerationFingerprintValue(value[key]); + return result; + }, {}); + } + return value; +} +function generationRequestFingerprint(payload, count, fixedSeedState){ + if(!fixedSeedState || fixedSeedState.random) return ''; + return JSON.stringify(stableGenerationFingerprintValue({ + request:payload || {}, + count:Number(count) || 1, + seed:fixedSeedState + })); +} +// 随机模式完成后也暂存本次结果;关闭骰子时可直接复用刚才这张图,而不是重复提交一次。 +function drawThingsSeedResultCacheKey(payload, seed){ + return generationRequestFingerprint(payload, 1, { + provider:'drawthings', + seed:normalizeDrawThingsSeed(seed) + }); +} +function drawThingsCachePayload(payload, options={}){ + const selfReferenceNodeId = String(options.selfReferenceNodeId || ''); + const selfReferenceUrls = new Set((options.selfReferenceUrls || []).map(drawThingsCacheUrlKey).filter(Boolean)); + if(!Array.isArray(payload?.reference_images)) return payload; + const referenceImages = payload.reference_images.filter(ref => { + const sameNode = selfReferenceNodeId && String(ref?.nodeId || '') === selfReferenceNodeId; + const sameImage = selfReferenceUrls.has(drawThingsCacheUrlKey(ref?.url)); + return !sameNode && !sameImage; + }); + const stableReferences = referenceImages.map(ref => ({ + url:drawThingsCacheUrlKey(ref?.url), + ...(Number.isFinite(Number(ref?.weight)) ? {weight:Number(ref.weight)} : {}) + })); + return {...payload, reference_images:stableReferences}; +} +function drawThingsCacheUrlKey(url){ + const value = String(url || '').trim(); + if(!value) return ''; + try { + const parsed = new URL(value, globalThis.location?.href || 'http://localhost/'); + parsed.search = ''; + parsed.hash = ''; + return parsed.href; + } catch(_err){ + return value.split(/[?#]/, 1)[0]; + } +} +function drawThingsCacheOptionsForNode(node){ + return { + selfReferenceNodeId:node?.id, + selfReferenceUrls:(node?.images || []).map(img => img?.url).filter(Boolean) + }; +} +function smartApiImageGenerationCount(runSettings){ + return Math.max(1, Math.min(8, Number(runSettings?.count || 1))); +} +function smartApiImageTaskCount(runSettings){ + if(isDrawThingsProvider(runSettings?.provider_id)){ + return runSettings?.drawThingsSeedRandom !== false + ? smartApiImageGenerationCount(runSettings) + : 1; + } + return smartApiImageGenerationCount(runSettings); +} +function repeatFixedSeedBatchImages(urls, count){ + const requestedCount = Math.max(1, Number(count) || 1); + if(requestedCount <= 1) return urls || []; + const source = resultMediaUrls(urls)[0]; + if(!source) return urls || []; + return Array.from({length:requestedCount}, (_, index) => ({ + ...(typeof source === 'string' ? {url:source} : source), + batchCopyId:`fixed-seed-${uid('batch-copy')}-${index + 1}` + })); +} +function generationSeedField(field){ + const name = `${field?.id || ''} ${field?.input || ''} ${field?.name || ''} ${field?.fieldName || ''}`.toLowerCase(); + return /(^|[^a-z])(seed|noise[_ -]?seed|random[_ -]?seed)([^a-z]|$)|种子|噪声/.test(name); +} +function fixedGenerationSeedState({fields=[], valueFor, randomEnabled, randomActive}){ + const values = {}; + let hasSeed = false; + let random = false; + (fields || []).forEach(field => { + if(!generationSeedField(field)) return; + hasSeed = true; + const id = field.id || field.fieldName || field.input || String(Object.keys(values).length); + if(randomEnabled?.(field) && randomActive?.(field)) random = true; + values[id] = valueFor ? valueFor(field) : undefined; + }); + if(!hasSeed || random) return null; + return {fields:values}; +} +function rememberFixedSeedGenerationResult(cacheKey, urls){ + const outputs = resultMediaUrls(urls).filter(Boolean); + if(cacheKey && outputs.length) fixedSeedGenerationCache.set(cacheKey, outputs); +} +// 即梦 image_upscale 支持的放大分辨率(与后端 JIMENG_UPSCALE_RESOLUTIONS 保持一致) const JIMENG_UPSCALE_RESOLUTIONS = ['2k', '4k', '8k']; function isJimengProviderId(providerId){ const id = String(providerId || '').trim().toLowerCase(); @@ -2771,6 +2967,12 @@ function renderApiParams(){ if(!settings.provider_id || !providers.some(p => p.id === settings.provider_id)) settings.provider_id = providers[0]?.id || ''; const models = filterJimengImageModels(providerImageModels(settings.provider_id)); if(!settings.model || !models.includes(settings.model)) settings.model = models[0] || ''; + if(isDrawThingsProvider(settings.provider_id)){ + if(!Number.isFinite(Number(settings.drawThingsSeed)) || Number(settings.drawThingsSeed) < 1){ + settings.drawThingsSeed = drawThingsRandomSeed(); + } + if(settings.drawThingsSeedRandom === undefined) settings.drawThingsSeedRandom = true; + } // 切换平台/模型时保留用户已选的分辨率(记忆),normalizeApiSizeSettings 只会修正非法的 auto。 normalizeApiSizeSettings(''); const outpaintLocked = settings.outpaintResolutionLocked === true; @@ -2780,6 +2982,7 @@ function renderApiParams(){ ${renderSizePickerControl('', true)} ${renderQualityControl()} ${renderCountVisualControl()} + ${renderDrawThingsSeedControl()} ${isJimengProviderId(settings.provider_id) ? renderJimengUpscaleControl() : ''} `; } @@ -3316,6 +3519,19 @@ function renderCountVisualControl(){
`; } +function renderDrawThingsSeedControl(){ + if(!isDrawThingsProvider(settings.provider_id) || settings.apiKind === 'video') return ''; + const seed = normalizeDrawThingsSeed(settings.drawThingsSeed); + settings.drawThingsSeed = seed; + const active = settings.drawThingsSeedRandom !== false; + return `
+
+ ${escapeHtml(tr('smart.drawThingsSeed'))} + + +
+
`; +} function renderCountControl(){ return ``; } @@ -3998,6 +4214,27 @@ function bindDynamicParams(){ if(input.dataset.param === 'videoDuration' && event?.type === 'change') renderDynamicParams(); }; }); + // 智能画布的 Draw Things seed 沿用普通画布的随机开关语义,固定 seed 时批量任务共用输入值。 + dynamicParams.querySelectorAll('[data-drawthings-seed]').forEach(input => { + input.onclick = event => event.stopPropagation(); + input.oninput = input.onchange = event => { + event?.stopPropagation?.(); + settings.drawThingsSeed = normalizeDrawThingsSeed(input.value); + input.value = String(settings.drawThingsSeed); + persistActiveSmartSettings(); + scheduleSave(); + }; + }); + dynamicParams.querySelectorAll('[data-drawthings-seed-random]').forEach(btn => { + btn.onclick = event => { + event.preventDefault(); + event.stopPropagation(); + settings.drawThingsSeedRandom = !(settings.drawThingsSeedRandom !== false); + persistActiveSmartSettings(); + renderDynamicParams(); + scheduleSave(); + }; + }); dynamicParams.querySelectorAll('[data-toggle-param]').forEach(btn => { btn.onclick = event => { event.preventDefault(); @@ -5003,16 +5240,17 @@ let connectionLayerRaf = 0; function mergeSmartImageLists(localImgs, remoteImgs){ const out = []; const seen = new Set(); + const keyFor = img => `${img?.url || ''}|${img?.batchCopyId || ''}`; (localImgs || []).forEach(img => { - const u = img && img.url; - if(u && seen.has(u)) return; - if(u) seen.add(u); + const key = keyFor(img); + if(img?.url && seen.has(key)) return; + if(img?.url) seen.add(key); out.push(img); }); (remoteImgs || []).forEach(img => { - const u = img && img.url; - if(!u || seen.has(u)) return; - seen.add(u); + const key = keyFor(img); + if(!img?.url || seen.has(key)) return; + seen.add(key); out.push(img); }); return out; @@ -6364,7 +6602,7 @@ function imageForDisplay(img){ originalLocalUrl:img.originalLocalUrl || localUrl }; } -function resultMediaUrls(result){ +function resultMediaUrls(result, preserveDuplicates=false){ const urls = []; const add = value => { if(!value) return; @@ -6381,6 +6619,7 @@ function resultMediaUrls(result){ const url = value.url || value.path || value.src || value.uri; if(url){ const item = {url, kind:value.kind || value.type || value.mediaKind || '', name:value.name || value.filename || ''}; + if(value.batchCopyId) item.batchCopyId = value.batchCopyId; ['natural_w','natural_h','width','height','w','h','layout_w','layout_h'].forEach(key => { const n = Number(value[key]); if(Number.isFinite(n) && n > 0) item[key] = n; @@ -6394,14 +6633,25 @@ function resultMediaUrls(result){ }; add(result); ['image_items','media_items','items','outputs','videos','audios','texts','files','images','urls','data','result','output','url'].forEach(key => add(result?.[key])); - const seen = new Set(); + const seenUrls = new Set(); + const seenCopies = new Set(); return urls.map(item => { const url = typeof item === 'string' ? item : item?.url || item?.path || ''; if(!url) return null; return typeof item === 'object' ? {...item, url} : url; }).filter(item => { const url = typeof item === 'string' ? item : item?.url || ''; - return url && !seen.has(url) && seen.add(url); + const batchCopyId = preserveDuplicates && typeof item === 'object' ? item?.batchCopyId || '' : ''; + if(!url) return false; + if(batchCopyId){ + if(seenCopies.has(batchCopyId)) return false; + seenCopies.add(batchCopyId); + seenUrls.add(url); + return true; + } + if(seenUrls.has(url)) return false; + seenUrls.add(url); + return true; }); } function mediaKindForUrls(urls, fallback='image'){ @@ -11547,9 +11797,9 @@ function updateComposer(){ if(!node) setPromptText(''); return; } - // composer 只绑定节点本身:图片只是素材/结果,不携带提示词或参数状态。 + // 默认仍按节点工作;选中某张生成图时,composer 会切换到该图的历史快照。 const subject = node; - const composerKey = `${node.id}:node`; + const composerKey = `${node.id}:node:${selectedImage.nodeId || ''}:${Number(selectedImage.index ?? -1)}`; const switchedNode = lastComposerNodeId !== composerKey; if(switchedNode) savePromptDraftForCurrent(); lastComposerNodeId = composerKey; @@ -11557,7 +11807,9 @@ function updateComposer(){ const hasPromptInput = promptInputNodesFor(node).length > 0; if(switchedNode){ settings = smartSettingsForNode(subject); - loadPromptDraft(subject); + const selectedMeta = selectedImageGenerationMetaForNode(subject); + if(selectedMeta) loadNodePromptDraftToInput(subject); + else loadPromptDraft(subject); } setPromptInputLocked(false); syncCascadeRunButton(node); @@ -13186,14 +13438,14 @@ function buildPromptRequest(node, overrideDefaultImages=null, consumeDefault=fal return { prompt:`${tr('smart.refMapHeader')}\n${mapText}\n\n${tr('smart.refUserNeed')}\n${body}`, displayPrompt, - refs:refs.map((img, index) => ({url:img.url, name:img.name || `图${index + 1}`, kind:img.kind || mediaKindForItem(img), asset_uris:img.asset_uris || {}, role:`image_${index + 1}`})), + refs:refs.map((img, index) => ({url:img.url, name:img.name || `图${index + 1}`, mediaInstanceId:img.mediaInstanceId || '', nodeId:img.nodeId || '', imageIndex:Number.isFinite(Number(img.imageIndex)) ? Number(img.imageIndex) : index, kind:img.kind || mediaKindForItem(img), asset_uris:img.asset_uris || {}, role:`image_${index + 1}`})), mentioned:true }; } return { prompt:body, displayPrompt, - refs:refs.map((img, index) => ({url:img.url, name:img.name || `图${index + 1}`, kind:img.kind || mediaKindForItem(img), asset_uris:img.asset_uris || {}, role:`image_${index + 1}`})), + refs:refs.map((img, index) => ({url:img.url, name:img.name || `图${index + 1}`, mediaInstanceId:img.mediaInstanceId || '', nodeId:img.nodeId || '', imageIndex:Number.isFinite(Number(img.imageIndex)) ? Number(img.imageIndex) : index, kind:img.kind || mediaKindForItem(img), asset_uris:img.asset_uris || {}, role:`image_${index + 1}`})), mentioned:false }; } @@ -13390,11 +13642,11 @@ function finalizePendingNode(pendingNode, urls, meta, kind='image'){ if(!pendingNode) return; pendingNode = liveSmartNode(pendingNode); const ext = kind === 'video' ? 'mp4' : kind === 'audio' ? 'mp3' : kind === 'text' ? 'txt' : 'png'; - const imgs = cleanHistoryImages(urls.map((item, i) => { + const imgs = attachImageGenerationMeta(cleanHistoryImages(urls.map((item, i) => { const url = typeof item === 'string' ? item : item?.url || ''; const itemKind = (typeof item === 'object' && item.kind) || kind; return copyMediaSizeFields(item, {url, name:(typeof item === 'object' && item.name) || `output-${i + 1}.${ext}`, kind:itemKind, generatedResult:true}); - }).filter(img => img.url)); + }).filter(img => img.url)), meta); pendingNode.images = imgs; markSmartNodeComplete(pendingNode, meta); pendingNode.outputKind = kind; @@ -13773,7 +14025,8 @@ function cleanHistoryImages(images=[]){ return nonPreviewOutputImages(images) .map(img => stripImageGenerationMeta({...img})) .filter(img => { - const key = `${img.kind || ''}|${img.url || ''}`; + const copyKey = img.batchCopyId ? `|${img.batchCopyId}` : ''; + const key = `${img.kind || ''}|${img.url || ''}${copyKey}`; if(seen.has(key)) return false; seen.add(key); return true; @@ -13844,7 +14097,7 @@ function replaceOutputsToNodeWithHistory(node, additions, kind='image', meta=nul node = liveSmartNode(node); const beforeRight = (Number(node.x) || 0) + nodeRect(node).width; const existing = cleanHistoryImages(node.images || []); - const next = cleanHistoryImages(additions); + const next = attachImageGenerationMeta(cleanHistoryImages(additions), meta); if(!next.length) return []; const history = existing.length ? ensureHistoryGroupForNode(node) : historyGroupForNode(node); if(history){ @@ -13875,9 +14128,9 @@ function appendOutputsToNode(node, additions, kind='image', options={}){ node = liveSmartNode(node); const beforeRight = (Number(node.x) || 0) + nodeRect(node).width; const existing = cleanHistoryImages(node.images || []); - const seen = new Set(existing.map(img => `${img.kind || ''}|${img.url || ''}`)); - const next = cleanHistoryImages(additions).filter(img => { - const key = `${img.kind || ''}|${img.url || ''}`; + const seen = new Set(existing.map(img => `${img.kind || ''}|${img.url || ''}|${img.batchCopyId || ''}`)); + const next = attachImageGenerationMeta(cleanHistoryImages(additions), options.meta).filter(img => { + const key = `${img.kind || ''}|${img.url || ''}|${img.batchCopyId || ''}`; if(seen.has(key)) return false; seen.add(key); return true; @@ -13894,7 +14147,7 @@ function appendOutputsToNode(node, additions, kind='image', options={}){ if(!skipShift) pushRightSideNodes(node, afterRight - beforeRight + 36); return next; } -function appendLoopOutputsToNode(node, additions, kind='image', ctx=smartLoopContext){ +function appendLoopOutputsToNode(node, additions, kind='image', ctx=smartLoopContext, meta=null){ if(!node || !additions?.length) return []; const runState = ctx?.runState; if(runState && !runState.loopAppendInitialized) runState.loopAppendInitialized = new Set(); @@ -13913,7 +14166,7 @@ function appendLoopOutputsToNode(node, additions, kind='image', ctx=smartLoopCon } node.images = []; } - return appendOutputsToNode(node, additions, kind, {skipShift:true}); + return appendOutputsToNode(node, additions, kind, {skipShift:true, meta}); } function syncCascadeRunButton(node=selectedNode()){ if(!cascadeRunBtn) return; @@ -13930,7 +14183,20 @@ function syncCascadeRunButton(node=selectedNode()){ refreshIcons(); } function loadNodePromptDraftToInput(node){ - if(node?.promptDraftHtml) { + const selectedMeta = selectedImageGenerationMetaForNode(node); + const promptHtml = selectedMeta?.promptHtml; + const promptText = selectedMeta?.promptText || selectedMeta?.displayPrompt || selectedMeta?.prompt || ''; + const promptRefs = selectedMeta?.promptRefs || []; + if(promptHtml){ + const hasToken = String(promptHtml || '').includes('mention-image-token'); + promptInput.innerHTML = hasToken + ? promptHtml + : (promptHtmlWithMentionTokens(promptText, promptRefs) || promptHtml); + } else if(selectedMeta){ + const rebuiltSelected = promptHtmlWithMentionTokens(promptText, promptRefs); + if(rebuiltSelected) promptInput.innerHTML = rebuiltSelected; + else setPromptText(promptText); + } else if(node?.promptDraftHtml) { const hasToken = String(node.promptDraftHtml || '').includes('mention-image-token'); promptInput.innerHTML = hasToken ? node.promptDraftHtml @@ -14000,35 +14266,54 @@ async function generateUrlsForCurrentSettings(node, prompt, refs, runSettings=se const activeSettings = runSettings || settings; if(activeSettings.engine === 'comfy') return generateComfyUrlsWithSettings(activeSettings, prompt, refs); if(activeSettings.engine === 'runninghub' && runningHubSelectedModel(activeSettings)){ - const taskResult = await runApiGeneration(prompt, refs, runningHubModelApiSettings(activeSettings)); + const taskResult = await runApiGeneration(prompt, refs, runningHubModelApiSettings(activeSettings), drawThingsCacheOptionsForNode(node)); const taskIds = Array.isArray(taskResult?.taskIds) ? taskResult.taskIds : []; if(taskIds.length){ - const settled = await Promise.all(taskIds.map(taskId => pollSmartCanvasTask(taskId))); - const urls = settled.flatMap(result => resultMediaUrls(result?.image_items?.length ? result.image_items : (result?.images?.length ? result.images : result))).filter(Boolean); - return {urls, kind:mediaKindForUrls(urls, 'image')}; + const settled = await Promise.all(taskIds.map(async taskId => { + const result = await pollSmartCanvasTask(taskId); + const sourceImages = resultMediaUrls(result?.image_items?.length ? result.image_items : (result?.images?.length ? result.images : result)); + rememberFixedSeedGenerationResult(taskResult.randomSeedCacheKeys?.[taskId], sourceImages); + return repeatFixedSeedBatchImages(sourceImages, taskResult.repeatCount || 1); + })); + const urls = settled.flat().filter(Boolean); + return {urls, kind:mediaKindForUrls(urls, 'image'), seeds:taskResult.seeds || []}; } const urls = resultMediaUrls(taskResult); - return {urls, kind:mediaKindForUrls(urls, 'image')}; + return {urls, kind:mediaKindForUrls(urls, 'image'), seeds:taskResult.seeds || []}; } if(isApiLikeEngine(activeSettings.engine) && activeSettings.apiKind === 'video'){ return {urls:await runApiVideoGeneration(prompt, refs, activeSettings), kind:'video'}; } if(isApiLikeEngine(activeSettings.engine)){ - const taskResult = await runApiGeneration(prompt, refs, activeSettings); + const taskResult = await runApiGeneration(prompt, refs, activeSettings, drawThingsCacheOptionsForNode(node)); + if(taskResult?.cachedImages?.length){ + return {urls:taskResult.cachedImages, kind:'image', seeds:taskResult.seeds || [], cached:true, cacheKey:taskResult.cacheKey || ''}; + } const taskIds = Array.isArray(taskResult?.taskIds) ? taskResult.taskIds : []; if(taskIds.length){ - const settled = await Promise.all(taskIds.map(taskId => pollSmartCanvasTask(taskId))); - const urls = settled.flatMap(result => resultMediaUrls(result?.image_items?.length ? result.image_items : (result?.images?.length ? result.images : result))).filter(Boolean); - return {urls, kind:mediaKindForUrls(urls, 'image')}; + const settled = await Promise.all(taskIds.map(async taskId => { + const result = await pollSmartCanvasTask(taskId); + const sourceImages = resultMediaUrls(result?.image_items?.length ? result.image_items : (result?.images?.length ? result.images : result)); + rememberFixedSeedGenerationResult(taskResult.randomSeedCacheKeys?.[taskId], sourceImages); + return repeatFixedSeedBatchImages(sourceImages, taskResult.repeatCount || 1); + })); + const urls = settled.flat().filter(Boolean); + const sourceUrls = settled.flat().filter(Boolean); + rememberFixedSeedGenerationResult(taskResult.cacheKey, resultMediaUrls(sourceUrls)); + return {urls, kind:mediaKindForUrls(urls, 'image'), seeds:taskResult.seeds || []}; } const urls = resultMediaUrls(taskResult); - return {urls, kind:mediaKindForUrls(urls, 'image')}; + return {urls, kind:mediaKindForUrls(urls, 'image'), seeds:taskResult.seeds || []}; } - const urls = activeSettings.engine === 'runninghub' + const runningHubResult = activeSettings.engine === 'runninghub' ? await runRunningHubGeneration(prompt, refs, activeSettings) : activeSettings.engine === 'modelscope' ? await runModelscopeGeneration(prompt, refs, activeSettings) : []; + if(activeSettings.engine === 'runninghub' && !Array.isArray(runningHubResult)){ + return {urls:runningHubResult.urls || [], kind:'image', cached:Boolean(runningHubResult.cached), cacheKey:runningHubResult.cacheKey || ''}; + } + const urls = runningHubResult; return {urls, kind:mediaKindForUrls(urls, 'image')}; } async function generateComfyUrlsWithSettings(runSettings, prompt, refs){ @@ -14081,10 +14366,23 @@ async function generateComfyUrlsWithSettings(runSettings, prompt, refs){ values[field.id] = runSettings.comfyParams?.[field.id] ?? field.default; } }); - const result = await runQueuedSmartComfyGenerate({prompt, workflow_json:workflowName, params:comfyParamsFromWorkflowValues(wf.config || {fields:[]}, values), type:'workflow-custom', client_id:smartClientId}); + const params = comfyParamsFromWorkflowValues(wf.config || {fields:[]}, values); + const fixedSeedState = fixedGenerationSeedState({ + fields:fields.filter(f => comfyFieldKind(f) === 'setting'), + valueFor:field => values[field.id], + randomEnabled:comfyRandomEnabledField, + randomActive:field => smartComfyRandomActiveFor(runSettings, field.id) + }); + const cacheKey = generationRequestFingerprint({engine:'comfy', mode, workflow:workflowName, prompt, refs:allRefs, params}, 1, fixedSeedState); + if(cacheKey){ + const cached = fixedSeedGenerationCache.get(cacheKey) || []; + if(cached.length) return {urls:cached.slice(), kind:'image', cached:true, cacheKey}; + } + const result = await runQueuedSmartComfyGenerate({prompt, workflow_json:workflowName, params, type:'workflow-custom', client_id:smartClientId}); const urls = resultMediaUrls(result); const fallbackKind = result.videos?.length ? 'video' : result.audios?.length ? 'audio' : result.texts?.length ? 'text' : 'image'; - return {urls, kind:mediaKindForUrls(urls, fallbackKind)}; + rememberFixedSeedGenerationResult(cacheKey, urls); + return {urls, kind:mediaKindForUrls(urls, fallbackKind), cacheKey}; } async function runCascadeStepIntoNode(sourceNode, targetNode, inputRefs, ctx=smartLoopContext){ const outputNode = targetNode || sourceNode; @@ -14095,10 +14393,18 @@ async function runCascadeStepIntoNode(sourceNode, targetNode, inputRefs, ctx=sma settings = runSettings; const outpaintSize = validOutpaintSize(requestNode); const selfRefs = sourceNode?.type === 'smart-loop' ? [] : selfReferenceImagesForNode(sourceNode, false, ctx).filter(img => img?.url); - const sourceRefs = (selfRefs.length ? selfRefs : defaultReferenceImagesFor(requestNode, false, ctx)).filter(img => img?.url); + const defaultRefs = defaultReferenceImagesFor(requestNode, false, ctx); + const candidateRefs = selfRefs.length && drawThingsModelSupportsEditing(runSettings.model) ? selfRefs : defaultRefs; + const sourceRefs = drawThingsReferenceImagesForRequest(requestNode, candidateRefs, runSettings); + const fallbackRefs = drawThingsReferenceImagesForRequest(requestNode, inputRefs, runSettings); const refsForRequest = sourceRefs.length ? sourceRefs - : (inputRefs && inputRefs.length ? inputRefs : null); + : (fallbackRefs.length ? fallbackRefs : null); + const referenceError = drawThingsReferenceError(runSettings, refsForRequest); + if(referenceError){ + settings = previousSettings; + throw new Error(referenceError); + } const request = buildPromptRequestForNode( requestNode, refsForRequest, @@ -14146,19 +14452,24 @@ async function runCascadeStepIntoNode(sourceNode, targetNode, inputRefs, ctx=sma render(); settings = previousSettings; try { - const result = await generateUrlsForCurrentSettings(outputNode, prompt, request.refs || [], runSettings); + const result = await generateUrlsForCurrentSettings(requestNode, prompt, request.refs || [], runSettings); + const appliedSeed = applyDrawThingsSeedResult(result, runSettings, previousSettings, meta, outputNode); + // 级联节点完成后会恢复这份旧快照;保留本次真实提交的 seed,避免它覆盖新值。 + if(appliedSeed && targetPromptState.runSettings && isDrawThingsProvider(targetPromptState.runSettings.provider_id || runSettings.provider_id)){ + targetPromptState.runSettings.drawThingsSeed = appliedSeed; + } if(!result.urls?.length) throw new Error(result.kind === 'video' ? tr('smart.errNoOutVideos') : tr('smart.errNoOutImages')); if(outpaintSize) delete requestNode.outpaintSize; - addSmartGenerationLog({run:{...runLog, kind:result.kind || logKind}, outputs:result.urls, runMs:nowMs() - runLogStart}); + if(!result.cached) addSmartGenerationLog({run:{...runLog, kind:result.kind || logKind}, outputs:result.urls, runMs:nowMs() - runLogStart}); const ext = result.kind === 'video' ? 'mp4' : result.kind === 'audio' ? 'mp3' : result.kind === 'text' ? 'txt' : 'png'; const additions = result.urls.map((item, i) => { const url = typeof item === 'string' ? item : item?.url || ''; return stripImageGenerationMeta(copyMediaSizeFields(item, {url, name:(typeof item === 'object' && item.name) || `output-${i + 1}.${ext}`, kind:(typeof item === 'object' && item.kind) || result.kind, generatedResult:true})); }).filter(item => item.url); if(ctx?.appendLoopOutputs) { - appendLoopOutputsToNode(outputNode, additions, result.kind, ctx); + appendLoopOutputsToNode(outputNode, additions, result.kind, ctx, meta); } else { - replaceOutputsToNodeWithHistory(outputNode, additions, result.kind, null, {skipShift:Boolean(ctx?.nodeId)}); + replaceOutputsToNodeWithHistory(outputNode, additions, result.kind, meta, {skipShift:Boolean(ctx?.nodeId)}); } outputNode.runPrompt = targetPromptState.runPrompt; outputNode.runModelPrompt = targetPromptState.runModelPrompt; @@ -14215,7 +14526,7 @@ async function runLoopRoundIntoSlot(loopNode, rootNode, outputSlot, loopIndex, c const runLog = smartRunSnapshot(rootNode, prompt, request.refs || [], logKind); const runLogStart = nowMs(); const expectedCount = isApiLikeEngine(runSettings.engine) && runSettings.apiKind !== 'video' - ? Math.max(1, Math.min(8, Number(runSettings.count || 1))) + ? smartApiImageGenerationCount(runSettings) : 1; outputSlot.queued = false; outputSlot.running = true; @@ -14233,7 +14544,12 @@ async function runLoopRoundIntoSlot(loopNode, rootNode, outputSlot, loopIndex, c settings = previousSettings; let result; if(isApiLikeEngine(runSettings.engine) && runSettings.apiKind !== 'video'){ - const taskResult = await runApiGeneration(prompt, request.refs || [], runSettings); + const taskResult = await runApiGeneration(prompt, request.refs || [], runSettings, drawThingsCacheOptionsForNode(rootNode)); + applyDrawThingsSeedResult(taskResult, runSettings, previousSettings, meta, outputSlot); + if(taskResult?.cachedImages?.length){ + // 循环节点也遵循固定 seed 的请求缓存,避免每一轮重复提交相同任务。 + result = {urls:taskResult.cachedImages, kind:'image', cached:true}; + } else { const taskIds = Array.isArray(taskResult?.taskIds) ? taskResult.taskIds : []; if(!taskIds.length) throw new Error(tr('smart.errRunFailed')); const existing = cleanHistoryImages(outputSlot.images || []); @@ -14247,26 +14563,42 @@ async function runLoopRoundIntoSlot(loopNode, rootNode, outputSlot, loopIndex, c delete history.h; outputSlot.images = []; } - outputSlot.pendingTasks = taskIds.map(taskId => ({taskId, kind:'image', providerId:taskResult.providerId, model:taskResult.model})); - outputSlot.pending = Math.max(taskIds.length, Number(outputSlot.pending || 0) || taskIds.length); + outputSlot.pendingTasks = taskIds.map(taskId => ({ + taskId, + kind:'image', + providerId:taskResult.providerId, + model:taskResult.model, + seedCacheKey:taskResult.cacheKey || taskResult.randomSeedCacheKeys?.[taskId] || '', + repeatCount:taskResult.repeatCount || 1, + imageMeta:snapshotImageGenerationMeta(meta) + })); + outputSlot.pending = taskIds.length; outputSlot.running = false; render(); scheduleSave(); await saveCanvas(); - await resumeSmartPendingNode(outputSlot, {run:runLog, runLogStart}); + await resumeSmartPendingNode(outputSlot, {run:runLog, runLogStart, imageMeta:meta}); if(outputSlot.jimengPending || smartRecoverableImageTask(outputSlot)){ outputSlot.queued = false; return []; } result = {urls:(outputSlot.images || []).map(img => img?.url ? img : null).filter(Boolean), kind:'image'}; + } } else { result = await generateUrlsForCurrentSettings(outputSlot, prompt, request.refs || [], runSettings); } if(!result.urls?.length) throw new Error(result.kind === 'video' ? tr('smart.errNoOutVideos') : tr('smart.errNoOutImages')); let additions; - if(isApiLikeEngine(runSettings.engine) && runSettings.apiKind !== 'video'){ + const cachedAlreadyDisplayed = result.cached + && result.urls.every(item => { + const url = typeof item === 'string' ? item : item?.url || ''; + return url && (outputSlot.images || []).some(img => img?.url === url); + }); + if(isApiLikeEngine(runSettings.engine) && runSettings.apiKind !== 'video' && !result.cached){ additions = (outputSlot.images || []).map(img => stripImageGenerationMeta({...img})).filter(img => img?.url); if(meta) attachRunMeta(outputSlot, meta); + } else if(cachedAlreadyDisplayed){ + additions = []; } else { const ext = result.kind === 'video' ? 'mp4' : result.kind === 'audio' ? 'mp3' : result.kind === 'text' ? 'txt' : 'png'; additions = result.urls.map((item, i) => { @@ -14284,7 +14616,7 @@ async function runLoopRoundIntoSlot(loopNode, rootNode, outputSlot, loopIndex, c runPath.states[edgeKey] = 'done'; scheduleConnectionLayerRefresh(); } - addSmartGenerationLog({run:{...runLog, kind:result.kind || logKind}, outputs:result.urls, runMs:nowMs() - runLogStart}); + if(!result.cached) addSmartGenerationLog({run:{...runLog, kind:result.kind || logKind}, outputs:result.urls, runMs:nowMs() - runLogStart}); return rememberRoundOutputs(ctx, outputSlot, additions); } catch(e) { if(handleJimengPendingSignal(outputSlot, e)){ @@ -14596,39 +14928,46 @@ async function runGeneration(){ const prompt = request.prompt.trim(); if(!node) return; if(smartNodeInFlight(node)) return; - const refs = request.refs; + const rawRefs = request.refs; const previousSettings = cloneSmartSettings(settings); - const runSettings = smartSettingsForNode(node); + const runSettings = cloneSmartSettings(smartSettingsForNode(node)); settings = {...settings, ...cloneSmartSettings(runSettings || {})}; - if(!prompt && smartRunNeedsPrompt(settings)){ - settings = previousSettings; + const refs = drawThingsReferenceImagesForRequest(node, rawRefs, runSettings); + const referenceError = drawThingsReferenceError(runSettings, refs); + if(referenceError){ + restoreSmartSettingsAfterRun(node, previousSettings); + toast(referenceError); + return; + } + if(!prompt && smartRunNeedsPrompt(runSettings)){ + restoreSmartSettingsAfterRun(node, previousSettings); toast(tr('smart.toastNeedPrompt')); return; } const outpaintSize = node?.outpaintSize && Number(node.outpaintSize.width) > 0 && Number(node.outpaintSize.height) > 0 ? {width:Math.round(Number(node.outpaintSize.width)), height:Math.round(Number(node.outpaintSize.height))} : null; - if(outpaintSize && isApiLikeEngine(settings.engine) && settings.apiKind !== 'video'){ - settings = { - ...settings, + if(outpaintSize && isApiLikeEngine(runSettings.engine) && runSettings.apiKind !== 'video'){ + Object.assign(runSettings, { resolution:'custom', ratio:'', customWidth:outpaintSize.width, customHeight:outpaintSize.height, customSize:`${outpaintSize.width}x${outpaintSize.height}` - }; + }); + settings = {...settings, ...cloneSmartSettings(runSettings)}; } const meta = snapshotRunMeta(prompt, node.id, request.displayPrompt, refs); - const logKind = isApiLikeEngine(settings.engine) && settings.apiKind === 'video' ? 'video' : 'image'; + const logKind = isApiLikeEngine(runSettings.engine) && runSettings.apiKind === 'video' ? 'video' : 'image'; const runLog = smartRunSnapshot(node, prompt, refs, logKind); - rememberRecentSmartSettings(settings, node); + rememberRecentSmartSettings(runSettings, node); const runLogStart = nowMs(); - const expectedCount = settings.engine === 'runninghub' + const expectedCount = runSettings.engine === 'runninghub' ? 1 - : settings.engine === 'comfy' - ? (settings.comfyMode === 'text' || settings.comfyMode === 'enhance' || settings.comfyMode === 'edit' || settings.comfyMode === 'custom' ? 1 : 1) - : Math.max(1, Math.min(8, Number(settings.count || 1))); - const apiConcurrentRun = isApiLikeEngine(settings.engine) || settings.engine === 'runninghub' || settings.engine === 'modelscope' || settings.engine === 'comfy'; + : runSettings.engine === 'comfy' + ? (runSettings.comfyMode === 'text' || runSettings.comfyMode === 'enhance' || runSettings.comfyMode === 'edit' || runSettings.comfyMode === 'custom' ? 1 : 1) + : smartApiImageGenerationCount(runSettings); + const apiConcurrentRun = isApiLikeEngine(runSettings.engine) || runSettings.engine === 'runninghub' || runSettings.engine === 'modelscope' || runSettings.engine === 'comfy'; const nodeHasImages = isSmartGroupNode(node) ? imagesForNode(node).some(img => img?.url) : (node.images || []).some(img => img?.url); const workflowModeRun = smartImageUsesWorkflowInput(node, smartLoopContext); const sourceVisualState = isSmartImageNode(node) && nodeHasImages && !workflowModeRun ? { @@ -14670,57 +15009,98 @@ async function runGeneration(){ } render(); try { - if(settings.engine === 'comfy'){ - await runComfyGeneration(pendingNode, prompt, refs, pendingNode, pendingMeta); + if(runSettings.engine === 'comfy'){ + await runComfyGeneration(pendingNode, prompt, refs, pendingNode, pendingMeta, runSettings); if(sourceVisualState) restoreSourceVisualState(node, sourceVisualState); addSmartGenerationLog({run:runLog, outputs:pendingNode.images || [], runMs:nowMs() - runLogStart}); - settings = previousSettings; + restoreSmartSettingsAfterRun(node, previousSettings); return; } - if(isApiLikeEngine(settings.engine) && settings.apiKind === 'video'){ - const outVideos = await runApiVideoGeneration(prompt, refs); + if(isApiLikeEngine(runSettings.engine) && runSettings.apiKind === 'video'){ + const outVideos = await runApiVideoGeneration(prompt, refs, runSettings); if(!outVideos.length) throw new Error(tr('smart.errNoOutVideos')); finalizePendingNode(pendingNode, outVideos, pendingMeta, 'video'); if(sourceVisualState) restoreSourceVisualState(node, sourceVisualState); addSmartGenerationLog({run:runLog, outputs:outVideos, runMs:nowMs() - runLogStart}); clearPromptInput({preserveDraft:true}); - settings = previousSettings; + restoreSmartSettingsAfterRun(node, previousSettings); + scheduleSave(); + return; + } + const rhModelMode = runSettings.engine === 'runninghub' && Boolean(runningHubSelectedModel(runSettings)); + const rawOutImages = rhModelMode + ? await runApiGeneration(prompt, refs, runningHubModelApiSettings(runSettings), drawThingsCacheOptionsForNode(node)) + : runSettings.engine === 'runninghub' + ? await runRunningHubGeneration(prompt, refs, runSettings) + : runSettings.engine === 'modelscope' + ? await runModelscopeGeneration(prompt, refs, runSettings) + : await runApiGeneration(prompt, refs, runSettings, drawThingsCacheOptionsForNode(node)); + const outImages = rawOutImages; + applyDrawThingsSeedResult(outImages, runSettings, previousSettings, pendingMeta, pendingNode); + if(outImages?.cachedImages?.length){ + // 固定 seed 命中缓存时仍更新当前节点的结果快照,但不新增生成日志或后端任务。 + finalizePendingNode(pendingNode, outImages.cachedImages, pendingMeta); + if(sourceVisualState) restoreSourceVisualState(node, sourceVisualState); + clearPromptInput({preserveDraft:true}); + restoreSmartSettingsAfterRun(node, previousSettings); + scheduleSave(); + return; + } + if(runSettings.engine === 'runninghub' && !rhModelMode && outImages?.cached){ + // RunningHub 命中固定 seed 指纹时只恢复结果,不新增一次生成日志。 + finalizePendingNode(pendingNode, outImages.urls || [], pendingMeta); + if(sourceVisualState) restoreSourceVisualState(node, sourceVisualState); + clearPromptInput({preserveDraft:true}); + restoreSmartSettingsAfterRun(node, previousSettings); scheduleSave(); return; } - const rhModelMode = settings.engine === 'runninghub' && Boolean(runningHubSelectedModel(settings)); - const outImages = rhModelMode - ? await runApiGeneration(prompt, refs, runningHubModelApiSettings(settings)) - : settings.engine === 'runninghub' - ? await runRunningHubGeneration(prompt, refs) - : settings.engine === 'modelscope' - ? await runModelscopeGeneration(prompt, refs) - : await runApiGeneration(prompt, refs); - if(isApiLikeEngine(settings.engine) || rhModelMode){ + if(isApiLikeEngine(runSettings.engine) || rhModelMode){ const taskIds = Array.isArray(outImages?.taskIds) ? outImages.taskIds : []; if(!taskIds.length) throw new Error(tr('smart.errRunFailed')); - pendingNode.pendingTasks = taskIds.map(taskId => ({taskId, kind:'image', providerId:outImages.providerId, model:outImages.model})); - pendingNode.pending = Math.max(taskIds.length, Number(pendingNode.pending || 0) || taskIds.length); + pendingNode.pendingTasks = taskIds.map(taskId => ({ + taskId, + kind:'image', + providerId:outImages.providerId, + model:outImages.model, + seedCacheKey:outImages.cacheKey || outImages.randomSeedCacheKeys?.[taskId] || '', + repeatCount:outImages.repeatCount || 1, + imageMeta:snapshotImageGenerationMeta(pendingMeta) + })); + pendingNode.pending = taskIds.length; pendingNode.runStartedAt = nowMs(); pendingNode.runTimerHidden = false; pendingNode.running = false; render(); scheduleSave(); await saveCanvas(); - await resumeSmartPendingNode(pendingNode, {run:runLog, runLogStart}); + await resumeSmartPendingNode(pendingNode, {run:runLog, runLogStart, imageMeta:pendingMeta}); if(pendingNode.jimengPending || smartRecoverableImageTask(pendingNode)){ if(sourceVisualState) restoreSourceVisualState(node, sourceVisualState); clearPromptInput({preserveDraft:true}); - settings = previousSettings; + restoreSmartSettingsAfterRun(node, previousSettings); scheduleSave(); return; } if(!(pendingNode.images || []).length) throw new Error(tr('smart.errNoOutImages')); + rememberFixedSeedGenerationResult(outImages.cacheKey, pendingNode.images || []); if(outpaintSize) delete node.outpaintSize; if(sourceVisualState) restoreSourceVisualState(node, sourceVisualState); addSmartGenerationLog({run:runLog, outputs:pendingNode.images || [], runMs:nowMs() - runLogStart}); clearPromptInput({preserveDraft:true}); - settings = previousSettings; + restoreSmartSettingsAfterRun(node, previousSettings); + scheduleSave(); + return; + } + if(runSettings.engine === 'runninghub' && !rhModelMode){ + const urls = outImages?.urls || []; + if(!urls.length) throw new Error(tr('smart.errNoOutImages')); + if(outpaintSize) delete node.outpaintSize; + finalizePendingNode(pendingNode, urls, pendingMeta); + if(sourceVisualState) restoreSourceVisualState(node, sourceVisualState); + addSmartGenerationLog({run:runLog, outputs:urls, runMs:nowMs() - runLogStart}); + clearPromptInput({preserveDraft:true}); + restoreSmartSettingsAfterRun(node, previousSettings); scheduleSave(); return; } @@ -14730,10 +15110,10 @@ async function runGeneration(){ if(sourceVisualState) restoreSourceVisualState(node, sourceVisualState); addSmartGenerationLog({run:runLog, outputs:outImages, runMs:nowMs() - runLogStart}); clearPromptInput({preserveDraft:true}); - settings = previousSettings; + restoreSmartSettingsAfterRun(node, previousSettings); scheduleSave(); } catch(e) { - settings = previousSettings; + restoreSmartSettingsAfterRun(node, previousSettings); if(handleJimengPendingSignal(pendingNode, e)){ if(sourceVisualState) restoreSourceVisualState(node, sourceVisualState); delete pendingNode._runMetaTargetId; @@ -14814,15 +15194,118 @@ function comfyFieldKind(field){ if(field?.type === 'textarea' || /prompt|text|提示词|正向|负向/.test(key)) return 'prompt'; return 'setting'; } -async function runApiGeneration(prompt, refs, runSettings=settings){ +function applyDrawThingsSeedResult(taskResult, runSettings=settings, restoreSettings=null, meta=null, targetNode=null){ + if(!isDrawThingsProvider(runSettings?.provider_id)) return 0; + const seeds = Array.isArray(taskResult?.seeds) ? taskResult.seeds : []; + if(!seeds.length) return 0; + const randomMode = runSettings.drawThingsSeedRandom !== false; + // 批量任务各自使用独立 seed;历史快照必须记录本批次最后一次实际提交的 seed。 + const actualSeed = normalizeDrawThingsSeed(seeds[seeds.length - 1]); + if(meta?.settings){ + meta.settings.drawThingsSeed = actualSeed; + // 快照同时保存开关状态,避免随机结果被误读成固定 seed 设置。 + meta.settings.drawThingsSeedRandom = randomMode; + } + + // 固定模式的 seed 是用户输入,不能被任务结果回写;否则后续指纹会被悄悄改变。 + // 随机模式则把本次实际 seed 显示出来,但保留随机开关,下一次仍会重新取 seed。 + if(!randomMode) return normalizeDrawThingsSeed(runSettings.drawThingsSeed); + runSettings.drawThingsSeed = actualSeed; + if(restoreSettings && isDrawThingsProvider(restoreSettings.provider_id)) restoreSettings.drawThingsSeed = actualSeed; + + // 优先写入本次任务的目标节点;级联运行时当前选中节点可能已经变化,不能依赖全局选择状态。 + const subjects = [targetNode].filter(Boolean); + const seen = new Set(); + subjects.forEach(subject => { + if(seen.has(subject.id) || !isDrawThingsProvider(subject.runSettings?.provider_id || runSettings.provider_id)) return; + seen.add(subject.id); + subject.runSettings = settingsForStorage({...settingsForStorage(subject.runSettings || {}), drawThingsSeed:actualSeed, drawThingsSeedRandom:true}); + }); + + // 只有用户仍选中本次运行目标时才刷新输入框,避免后台任务覆盖其他节点的参数。 + const activeSubject = activeSettingsSubject(); + if(targetNode?.id && activeSubject?.id === targetNode.id + && isDrawThingsProvider(settings?.provider_id) + && String(settings.provider_id) === String(runSettings.provider_id)){ + settings.drawThingsSeed = actualSeed; + dynamicParams?.querySelectorAll('[data-drawthings-seed]').forEach(input => { + input.value = String(actualSeed); + }); + // 强制重建控件,确保后续 composer/render 不会从缓存的旧 DOM 值恢复。 + renderDynamicParams({force:true}); + persistActiveSmartSettings(); + scheduleSave(); + } + return actualSeed; +} +async function runApiGeneration(prompt, refs, runSettings=settings, cacheOptions={}){ if(!runSettings.provider_id || !runSettings.model) throw new Error(tr('smart.errNoApiModel')); - const count = Math.max(1, Math.min(8, Number(runSettings.count || 1))); - const payload = {prompt, provider_id:runSettings.provider_id, model:runSettings.model, size:sizeForRun(runSettings), quality:runSettings.quality || 'auto', n:1, reference_images:imageRefsOnly(refs).slice(0, SMART_REFERENCE_IMAGE_MAX)}; - const tasks = await Promise.all(Array.from({length:count}, () => fetch('/api/canvas-image-tasks', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)}).then(async r => { + const requestedCount = smartApiImageGenerationCount(runSettings); + const drawThingsSelected = isDrawThingsProvider(runSettings.provider_id); + const count = smartApiImageTaskCount(runSettings); + const drawThingsBatchSize = 1; + const payload = {prompt, provider_id:runSettings.provider_id, model:runSettings.model, size:sizeForRun(runSettings), aspect_ratio:runSettings.ratio === 'custom' ? (runSettings.customRatio || '') : (runSettings.ratio || ''), resolution:runSettings.resolution || '', quality:runSettings.quality || 'auto', n:1, batch_size:drawThingsBatchSize, reference_images:imageRefsOnly(refs).slice(0, SMART_REFERENCE_IMAGE_MAX)}; + const cachePayload = drawThingsSelected ? drawThingsCachePayload(payload, cacheOptions) : payload; + const fixedSeedState = drawThingsSelected && runSettings.drawThingsSeedRandom === false + ? {provider:'drawthings', seed:normalizeDrawThingsSeed(runSettings.drawThingsSeed)} + : null; + const fixedSeedCacheKey = generationRequestFingerprint(cachePayload, 1, fixedSeedState); + if(fixedSeedCacheKey){ + const cached = fixedSeedGenerationCache.get(fixedSeedCacheKey) || []; + if(cached.length){ + // 固定 seed 的完全相同请求直接复用结果,不再创建新的后端任务。 + toast('检测到 seed 值(种子数)相同,跳过生成'); + return { + cachedImages:repeatFixedSeedBatchImages(cached, requestedCount), + count:requestedCount, + taskCount:count, + repeatCount:fixedSeedState ? requestedCount : 1, + providerId:payload.provider_id, + model:payload.model, + // 缓存命中时沿用指纹中的固定 seed,避免引用不存在的临时变量。 + seeds:Array.from({length:requestedCount}, () => fixedSeedState.seed), + cacheKey:fixedSeedCacheKey, + cached:true + }; + } + } + const seeds = []; + const usedSeeds = new Set(); + const tasks = await Promise.all(Array.from({length:count}, () => { + const requestPayload = {...payload}; + if(drawThingsSelected){ + const seed = runSettings.drawThingsSeedRandom !== false + ? drawThingsUniqueRandomSeed(usedSeeds) + : normalizeDrawThingsSeed(runSettings.drawThingsSeed); + seeds.push(seed); + requestPayload.seed = seed; + } + return fetch('/api/canvas-image-tasks', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(requestPayload)}).then(async r => { if(!r.ok) throw new Error(await r.text()); return r.json(); - }))); - return {taskIds:tasks.map(task => task.task_id).filter(Boolean), count, providerId:payload.provider_id, model:payload.model}; + }); + })); + const taskIds = tasks.map(task => task.task_id).filter(Boolean); + const randomSeedCacheKeys = {}; + if(drawThingsSelected && runSettings.drawThingsSeedRandom !== false){ + tasks.forEach((task, index) => { + if(task?.task_id && seeds[index] !== undefined){ + randomSeedCacheKeys[task.task_id] = drawThingsSeedResultCacheKey(cachePayload, seeds[index]); + } + }); + } + // 返回实际提交的 seed,调用层据此更新前端显示和本次生成的参数快照。 + return { + taskIds, + count:requestedCount, + taskCount:count, + repeatCount:fixedSeedState ? requestedCount : 1, + providerId:payload.provider_id, + model:payload.model, + seeds, + cacheKey:fixedSeedCacheKey, + randomSeedCacheKeys + }; } async function runRunningHubGeneration(prompt, refs, runSettings=settings){ const ref = selectedRunningHubRef(runSettings); @@ -14838,6 +15321,17 @@ async function runRunningHubGeneration(prompt, refs, runSettings=settings){ const body = mode === 'workflow' ? {workflowId:ref.id, nodeInfoList, useWallet:runSettings.rhPayment === 'wallet', ...workflowExtras} : {webappId:ref.id, nodeInfoList, instanceType:runSettings.rhInstanceType || '', useWallet:runSettings.rhPayment === 'wallet'}; + const fixedSeedState = fixedGenerationSeedState({ + fields, + valueFor:field => nodeInfoList.find(item => rhParamKey(item.nodeId, item.fieldName) === rhParamKey(field.nodeId, field.fieldName))?.fieldValue, + randomEnabled:rhRandomEnabled, + randomActive:field => smartRhRandomActiveFor(runSettings, rhParamKey(field.nodeId, field.fieldName)) + }); + const cacheKey = generationRequestFingerprint({engine:'runninghub', mode, body}, 1, fixedSeedState); + if(cacheKey){ + const cached = fixedSeedGenerationCache.get(cacheKey) || []; + if(cached.length) return {urls:cached.slice(), cached:true, cacheKey}; + } const submit = await fetch(endpoint, { method:'POST', headers:{'Content-Type':'application/json'}, @@ -14860,7 +15354,8 @@ async function runRunningHubGeneration(prompt, refs, runSettings=settings){ if(data.status === 'SUCCESS'){ const urls = resultMediaUrls(data.image_items?.length ? data.image_items : (data.urls || [])); if(!urls.length) throw new Error(tr('smart.rhOutputsEmpty')); - return urls; + rememberFixedSeedGenerationResult(cacheKey, urls); + return {urls, cacheKey}; } if(data.status === 'FAILED') throw new Error(data.failReason || tr('smart.rhFailed')); } @@ -14968,14 +15463,44 @@ async function urlToBase64(url){ }); } function sleep(ms){ return new Promise(resolve => setTimeout(resolve, ms)); } -async function runComfyGeneration(node, prompt, refs, pendingNode, meta){ +// 探测媒体时长(秒);用于即梦音频 2–15s 校验。加载失败/超时返回 null(放行,不误伤)。 +function probeMediaDuration(url, kind='audio'){ + return new Promise(resolve => { + if(!url){ resolve(null); return; } + let done = false; + const finish = value => { if(done) return; done = true; try { el.src = ''; } catch(e){} resolve(value); }; + let el; + try { + el = document.createElement(kind === 'video' ? 'video' : 'audio'); + } catch(e){ resolve(null); return; } + el.preload = 'metadata'; + el.onloadedmetadata = () => { const d = Number(el.duration); finish(Number.isFinite(d) && d > 0 ? d : null); }; + el.onerror = () => finish(null); + setTimeout(() => finish(null), 8000); + try { el.src = url; } catch(e){ finish(null); } + }); +} +async function runSmartComfyUpscale(imageUrl, resolution){ + if(!imageUrl) throw new Error(tr('smart.errRunFailed')); + const inputName = await comfyNameForRef({url:imageUrl, name:'smart-upscale-input.png'}); + return runQueuedSmartComfyGenerate({ + workflow_json:'upscale.json', + params:{ + "15":{image:inputName}, + "172":{seed:Math.floor(Math.random() * 4294967295), resolution:Number(resolution || 2048)} + }, + type:'enhance', + client_id:smartClientId + }); +} +async function runComfyGeneration(node, prompt, refs, pendingNode, meta, runSettings=settings){ const allRefs = refs || []; refs = imageRefsOnly(allRefs); - const mode = settings.comfyMode || 'text'; - if(mode === 'text') return runComfyText(node, prompt, pendingNode, meta); - if(mode === 'enhance') return runComfyEnhance(node, refs, pendingNode, meta); - if(mode === 'edit') return runComfyEdit(node, prompt, refs, pendingNode, meta); - const workflowName = settings.comfyWorkflow || comfyWorkflows[0]?.name || ''; + const mode = runSettings.comfyMode || 'text'; + if(mode === 'text') return runComfyText(node, prompt, pendingNode, meta, runSettings); + if(mode === 'enhance') return runComfyEnhance(node, refs, pendingNode, meta, runSettings); + if(mode === 'edit') return runComfyEdit(node, prompt, refs, pendingNode, meta, runSettings); + const workflowName = runSettings.comfyWorkflow || comfyWorkflows[0]?.name || ''; if(!workflowName) throw new Error(tr('smart.errNeedWorkflow')); const wf = await fetch(`/api/workflows/${encodeURIComponent(workflowName)}`).then(async r => { if(!r.ok) throw new Error(await r.text()); @@ -14995,13 +15520,29 @@ async function runComfyGeneration(node, prompt, refs, pendingNode, meta){ await assignMediaFields(fields.filter(f => comfyFieldKind(f) === 'video'), videoRefsOnly(allRefs)); await assignMediaFields(fields.filter(f => comfyFieldKind(f) === 'audio'), audioRefsOnly(allRefs)); fields.filter(f => comfyFieldKind(f) === 'setting').forEach(field => { - if(comfyRandomEnabledField(field) && smartComfyRandomActive(field.id)){ + if(comfyRandomEnabledField(field) && smartComfyRandomActiveFor(runSettings, field.id)){ values[field.id] = smartComfyRandomValue(field); } else { - values[field.id] = settings.comfyParams?.[field.id] ?? field.default; + values[field.id] = runSettings.comfyParams?.[field.id] ?? field.default; } }); - const result = await runQueuedSmartComfyGenerate({prompt, workflow_json:workflowName, params:comfyParamsFromWorkflowValues(wf.config || {fields:[]}, values), type:'workflow-custom', client_id:smartClientId}); + const params = comfyParamsFromWorkflowValues(wf.config || {fields:[]}, values); + const fixedSeedState = fixedGenerationSeedState({ + fields:fields.filter(f => comfyFieldKind(f) === 'setting'), + valueFor:field => values[field.id], + randomEnabled:comfyRandomEnabledField, + randomActive:field => smartComfyRandomActive(field.id) + }); + const cacheKey = generationRequestFingerprint({engine:'comfy', mode, workflow:workflowName, prompt, refs:allRefs, params}, 1, fixedSeedState); + const cached = cacheKey ? fixedSeedGenerationCache.get(cacheKey) || [] : []; + if(cached.length){ + // 直接运行路径与级联路径共用完整 seed 指纹,固定输入命中时不再提交 Comfy 任务。 + if(pendingNode) finalizePendingNode(pendingNode, cached, meta, 'image'); + clearPromptInput({preserveDraft:true}); + scheduleSave(); + return; + } + const result = await runQueuedSmartComfyGenerate({prompt, workflow_json:workflowName, params, type:'workflow-custom', client_id:smartClientId}); const urls = resultMediaUrls(result); if(!urls.length) throw new Error(tr('smart.errComfyNoImages')); const kind = mediaKindForUrls(urls, result.videos?.length ? 'video' : result.audios?.length ? 'audio' : result.texts?.length ? 'text' : 'image'); @@ -15009,6 +15550,7 @@ async function runComfyGeneration(node, prompt, refs, pendingNode, meta){ const out = urls.map((url, i) => ({url, name:`comfy-${i + 1}.${ext}`, kind})).filter(x => x.url); if(!out.length) throw new Error(tr('smart.errComfyEmpty')); const outputUrls = out.map(o => o.url); + rememberFixedSeedGenerationResult(cacheKey, outputUrls); if(pendingNode){ finalizePendingNode(pendingNode, outputUrls, meta, kind); } else { @@ -15019,8 +15561,8 @@ async function runComfyGeneration(node, prompt, refs, pendingNode, meta){ clearPromptInput({preserveDraft:true}); scheduleSave(); } -async function runComfyText(node, prompt, pendingNode, meta){ - const data = await runQueuedSmartComfyGenerate({prompt, width:Number(settings.width || 1024), height:Number(settings.height || 1024), workflow_json:'Z-Image.json', type:'zimage', client_id:smartClientId}); +async function runComfyText(node, prompt, pendingNode, meta, runSettings=settings){ + const data = await runQueuedSmartComfyGenerate({prompt, width:Number(runSettings.width || 1024), height:Number(runSettings.height || 1024), workflow_json:'Z-Image.json', type:'zimage', client_id:smartClientId}); const out = data.outputs || data.images || []; if(!out.length) throw new Error(tr('smart.errComfyNoImages')); if(pendingNode){ @@ -15033,11 +15575,16 @@ async function runComfyText(node, prompt, pendingNode, meta){ clearPromptInput({preserveDraft:true}); scheduleSave(); } -async function runComfyEnhance(node, refs, pendingNode, meta){ +async function runComfyEnhance(node, refs, pendingNode, meta, runSettings=settings){ if(!refs.length) throw new Error(tr('smart.errEnhanceNeedRefs')); const inputName = await comfyNameForRef(refs[0]); - const data = await runQueuedSmartComfyGenerate({workflow_json:'Z-Image-Enhance.json', type:'enhance', params:{"15":{image:inputName},"204":{value:Number(settings.enhanceStrength ?? 0.5)}}, client_id:smartClientId}); - const out = data.outputs || data.images || []; + const data = await runQueuedSmartComfyGenerate({workflow_json:'Z-Image-Enhance.json', type:'enhance', params:{"15":{image:inputName},"204":{value:Number(runSettings.enhanceStrength ?? 0.5)}}, client_id:smartClientId}); + //修复超分勾选 + let out = data.outputs || data.images || []; + if(runSettings.enhanceUpscale && out[0]){ + const upscale = await runSmartComfyUpscale(out[0], runSettings.enhanceUpscaleRes || 2048); + out = upscale.outputs || upscale.images || []; + } if(!out.length) throw new Error(tr('smart.errComfyNoImages')); if(pendingNode){ finalizePendingNode(pendingNode, out, meta); @@ -15048,12 +15595,17 @@ async function runComfyEnhance(node, refs, pendingNode, meta){ } scheduleSave(); } -async function runComfyEdit(node, prompt, refs, pendingNode, meta){ +async function runComfyEdit(node, prompt, refs, pendingNode, meta, runSettings=settings){ if(!refs.length) throw new Error(tr('smart.errEditNeedRefs')); const names = []; for(const ref of refs.slice(0, 3)) names.push(await comfyNameForRef(ref)); const data = await runQueuedSmartComfyGenerate({prompt, workflow_json:'Flux2-Klein.json', type:'klein', params:{"168":{text:prompt},"158":{noise_seed:Math.floor(Math.random()*1000000)},"278":{image:names[0] || ""},"270":{image:names[1] || ""},"292":{image:names[2] || ""},"313":{value:Boolean(names[1])},"314":{value:Boolean(names[2])}}, client_id:smartClientId}); - const out = data.outputs || data.images || []; + //修复超分勾选 + let out = data.outputs || data.images || []; + if(runSettings.editUpscale && out[0]){ + const upscale = await runSmartComfyUpscale(out[0], runSettings.editUpscaleRes || 2048); + out = upscale.outputs || upscale.images || []; + } if(!out.length) throw new Error(tr('smart.errComfyNoImages')); if(pendingNode){ finalizePendingNode(pendingNode, out, meta); @@ -15252,7 +15804,10 @@ async function querySmartImageTaskNow(nodeId, localTaskId){ if(data.status === 'succeeded'){ task.failed = false; task.querying = false; - finalizeSmartPendingTask(node, task.taskId, resultMediaUrls(data.image_items?.length ? data.image_items : (data.images?.length ? data.images : data)), task.kind || 'image'); + const sourceImages = resultMediaUrls(data.image_items?.length ? data.image_items : (data.images?.length ? data.images : data)); + rememberFixedSeedGenerationResult(task.seedCacheKey, sourceImages); + const images = repeatFixedSeedBatchImages(sourceImages, task.repeatCount || 1); + finalizeSmartPendingTask(node, task.taskId, images, task.kind || 'image'); render(); scheduleSave(); return; @@ -15334,20 +15889,20 @@ async function pollSmartCanvasTask(taskId){ activeSmartTaskPolls.delete(taskId); } } -function finalizeSmartPendingTask(node, taskId, images, kind='image'){ +function finalizeSmartPendingTask(node, taskId, images, kind='image', meta=null){ if(!node || !taskId) return; node.pendingTasks = smartPendingTasks(node).filter(task => task.taskId !== taskId); node.pending = Math.max(0, Number(node.pending || 0) - 1); const ext = kind === 'video' ? 'mp4' : kind === 'audio' ? 'mp3' : kind === 'text' ? 'txt' : 'png'; - const mediaItems = resultMediaUrls(images); + const mediaItems = resultMediaUrls(images, true); const existing = cleanHistoryImages(node.images || []); - const seen = new Set(existing.map(img => `${img.kind || ''}|${img.url || ''}`)); - const additions = cleanHistoryImages((mediaItems || []).map((item, i) => { + const seen = new Set(existing.map(img => `${img.kind || ''}|${img.url || ''}|${img.batchCopyId || ''}`)); + const additions = attachImageGenerationMeta(cleanHistoryImages((mediaItems || []).map((item, i) => { const url = typeof item === 'string' ? item : item?.url || ''; const itemKind = (typeof item === 'object' && item.kind) || kind; return stripImageGenerationMeta(copyMediaSizeFields(item, {url, name:(typeof item === 'object' && item.name) || `output-${i + 1}.${ext}`, kind:itemKind, generatedResult:true})); - }).filter(item => item.url)).filter(item => { - const key = `${item.kind || ''}|${item.url || ''}`; + }).filter(item => item.url)), meta).filter(item => { + const key = `${item.kind || ''}|${item.url || ''}|${item.batchCopyId || ''}`; if(seen.has(key)) return false; seen.add(key); return true; @@ -15381,7 +15936,7 @@ async function resumeSmartPendingNode(node, logContext={}){ error:message }); }; - node.pending = Math.max(tasks.length, Number(node.pending || 0) || tasks.length); + node.pending = tasks.length; node.running = false; render(); const failures = []; @@ -15389,7 +15944,16 @@ async function resumeSmartPendingNode(node, logContext={}){ if(task.failed && task.recoverTaskId) return; try { const result = await pollSmartCanvasTask(task.taskId); - finalizeSmartPendingTask(node, task.taskId, resultMediaUrls(result?.image_items?.length ? result.image_items : (result?.images?.length ? result.images : result)), task.kind || 'image'); + const sourceImages = resultMediaUrls(result?.image_items?.length ? result.image_items : (result?.images?.length ? result.images : result)); + rememberFixedSeedGenerationResult(task.seedCacheKey, sourceImages); + const images = repeatFixedSeedBatchImages(sourceImages, task.repeatCount || 1); + finalizeSmartPendingTask( + node, + task.taskId, + images, + task.kind || 'image', + task.imageMeta || logContext.imageMeta || null + ); render(); scheduleSave(); } catch(e) { From 9ef77abddb12fbeeae6a96c31423416a7270be52 Mon Sep 17 00:00:00 2001 From: Rayen21 <19681670+Rayen21@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:52:33 +0800 Subject: [PATCH 03/20] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E4=BB=8B=E7=BB=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed local smoke test instructions from README. --- CLI/macos/drawthings/README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/CLI/macos/drawthings/README.md b/CLI/macos/drawthings/README.md index ae8d47b22..0dc289c6f 100644 --- a/CLI/macos/drawthings/README.md +++ b/CLI/macos/drawthings/README.md @@ -26,12 +26,6 @@ python -m pip install -r CLI/macos/drawthings/requirements-Dt-gRPC.txt 该连接目前用于图片生成、单图图生图和 Hint 多图编辑,不提供聊天模型能力。Hint 最多支持四张图片,每张图片可在画布节点中独立设置控制类型。 -## 本地 smoke test -`drawthings_*_smoke_test.py` 仅用于本机联调,已经被 Git 忽略,不会同步到 GitHub。运行测试前需要确保: - -1. gRPCServerCLI 已经启动。 -2. 当前环境已安装上面的独立依赖。 -3. 测试使用的模型名称与 Draw Things 当前实时模型列表中的文件名一致。 - -测试输出图片也会被 Git 忽略。 +image +image From 517eeb25a6eb683d897a6db17b11252074da38bd Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Thu, 30 Jul 2026 17:00:45 +0800 Subject: [PATCH 04/20] =?UTF-8?q?=E8=A1=A5=E5=85=85Draw=20Things=20Mac?= =?UTF-8?q?=E8=8A=AF=E7=89=87=E6=8F=90=E7=A4=BA=E5=9B=BD=E9=99=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/js/api-settings.js | 2 ++ static/js/i18n/api-settings.js | 1 + 2 files changed, 3 insertions(+) diff --git a/static/js/api-settings.js b/static/js/api-settings.js index 9ef20d374..3190819f9 100644 --- a/static/js/api-settings.js +++ b/static/js/api-settings.js @@ -2589,6 +2589,8 @@ function renderEditor(){ if(isDrawThings){ item.base_url = item.base_url || DRAW_THINGS_DEFAULT_ENDPOINT; item.protocol = 'grpc'; + // Draw Things gRPCServerCLI only provides image generation on macOS M chips. + editorTitle.textContent = `${item.name || item.id}${tr('api.drawThingsMacOnly')}`; baseInput.value = item.base_url; baseInput.placeholder = DRAW_THINGS_DEFAULT_ENDPOINT; if(baseUrlLabel) baseUrlLabel.textContent = 'gRPCServerCLI 地址(主机:端口)'; diff --git a/static/js/i18n/api-settings.js b/static/js/i18n/api-settings.js index 9fd039ce1..c405cba49 100644 --- a/static/js/i18n/api-settings.js +++ b/static/js/i18n/api-settings.js @@ -23,6 +23,7 @@ "api.protocolGemini": { zh: "Gemini 协议", en: "Gemini Protocol" }, "api.protocolTudou": { zh: "土豆 API 协议", en: "Tudou API Protocol" }, "api.protocolVolcengine": { zh: "火山引擎协议", en: "Volcengine Protocol" }, + "api.drawThingsMacOnly": { zh: "(只适用于 Mac M 芯片)", en: " (Mac M-series chips only)" }, "api.key": { zh: "API Key", en: "API Key" }, "api.msNotes": { zh: "ModelScope 使用提示", en: "ModelScope Notes" }, "api.msChinaEndpoint": { zh: "国内默认请求地址:", en: "China endpoint: " }, From e1954e511dc7623f2439ee05f751c874d4cff15a Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Thu, 30 Jul 2026 22:39:07 +0800 Subject: [PATCH 05/20] feat: add Draw Things LoRA support --- static/css/smart-canvas.css | 10 +++ static/js/smart-canvas.js | 129 +++++++++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/static/css/smart-canvas.css b/static/css/smart-canvas.css index 3547f4b2b..48997c26b 100644 --- a/static/css/smart-canvas.css +++ b/static/css/smart-canvas.css @@ -898,6 +898,16 @@ video.node-img { object-fit:cover; background:#0f172a; } .smart-control:hover .smart-popover, .smart-control:focus-within .smart-popover, .smart-control.pinned .smart-popover, .smart-control.interacting .smart-popover { opacity:1; visibility:visible; pointer-events:auto; transform:translate(-50%, 0); } .smart-popover-title { color:var(--faint); font-size:9.5px; line-height:1.2; font-weight:600; letter-spacing:.06em; text-transform:uppercase; margin:0 2px 6px; } .size-picker-control { z-index:52; } +.drawthings-lora-control { z-index:56; } +.drawthings-lora-control:hover, +.drawthings-lora-control:focus-within, +.drawthings-lora-control.pinned, +.drawthings-lora-control.interacting { z-index:80; } +.drawthings-lora-control .smart-pill { max-width:220px; } +.drawthings-lora-summary { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +.drawthings-lora-weight-label { display:flex; align-items:center; justify-content:space-between; gap:8px; margin:6px 2px 0; color:var(--faint); font-size:9.5px; line-height:1.2; font-weight:600; letter-spacing:.06em; } +.drawthings-lora-weight-label input { width:64px; flex:0 0 64px; text-align:right; } +.drawthings-lora-warning { margin:6px 2px 0; color:var(--faint); font-size:9.5px; line-height:1.2; font-weight:600; letter-spacing:.06em; } .size-picker-control .smart-popover { transform:translate(-50%, 4px); } .size-picker-control.pinned .smart-popover, .size-picker-control.interacting .smart-popover, diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 64bc94f37..6c7aa7e15 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -309,6 +309,8 @@ let settings = { // Draw Things 的 seed 只在选择 gRPC provider 时使用,其他 provider 不携带该字段。 drawThingsSeed:0, drawThingsSeedRandom:true, + drawThingsLoraFile:'', + drawThingsLoraWeight:1, customRatio:'', customRatioWidth:'', customRatioHeight:'', @@ -2427,6 +2429,70 @@ function providerImageModels(providerId){ if(providerId === 'volcengine') return volcengineProvider().image_models || []; return (apiProviders || []).find(p => p.id === providerId)?.image_models || []; } +function drawThingsProvider(providerId=settings.provider_id){ + return (apiProviders || []).find(provider => + isDrawThingsProvider(provider.id) && + (!providerId || String(provider.id) === String(providerId)) + ) || null; +} +function drawThingsLoras(providerId=settings.provider_id){ + const provider = drawThingsProvider(providerId); + return Array.isArray(provider?.drawthings_loras) ? provider.drawthings_loras : []; +} +function drawThingsModelMetadata(providerId=settings.provider_id){ + const provider = drawThingsProvider(providerId); + return Array.isArray(provider?.drawthings_model_metadata) ? provider.drawthings_model_metadata : []; +} +function drawThingsMetadataFamily(item){ + const value = typeof item === 'string' + ? item + : ['version', 'prefix', 'name', 'file'].map(key => item?.[key] || '').join(' '); + const compact = String(value).toLowerCase().replace(/[._-]/g, ''); + if(compact.includes('qwenimageedit') || compact.includes('qwenedit')) return 'qwenedit'; + if(compact.includes('zimage')) return 'z_image'; + if(compact.includes('klein') || compact.includes('flux2klein')) return 'klein'; + return ''; +} +function selectedDrawThingsLoras(runSettings=settings){ + const file = String(runSettings?.drawThingsLoraFile || '').trim(); + if(!file) return []; + let weight = Number(runSettings?.drawThingsLoraWeight); + if(!Number.isFinite(weight)) weight = 1; + return [{file, weight:Math.max(-5, Math.min(5, weight))}]; +} +function selectedDrawThingsLora(runSettings=settings){ + const file = String(runSettings?.drawThingsLoraFile || '').trim(); + return drawThingsLoras(runSettings?.provider_id).find(item => String(item?.file || '').trim() === file) || null; +} +function drawThingsLoraCompatibility(runSettings=settings){ + const selected = selectedDrawThingsLora(runSettings); + if(!selected) return ''; + const modelMetadata = drawThingsModelMetadata(runSettings?.provider_id).find(item => + String(item?.file || '').trim() === String(runSettings?.model || '').trim() + ); + const currentFamily = drawThingsMetadataFamily(modelMetadata) || drawThingsMetadataFamily(runSettings?.model); + const loraFamily = drawThingsMetadataFamily(selected); + if(!currentFamily || !loraFamily || currentFamily === loraFamily) return ''; + return trf('smart.drawThingsLoraIncompatible', {model:currentFamily, lora:loraFamily}); +} +async function refreshSmartDrawThingsModels(){ + const provider = drawThingsProvider(); + if(!provider) return; + try { + const data = await fetch('/api/drawthings/models').then(response => response.json()); + provider.image_models = Array.isArray(data.models) ? [...new Set(data.models.filter(Boolean))] : []; + provider.drawthings_model_metadata = Array.isArray(data.model_metadata) ? data.model_metadata : []; + provider.drawthings_loras = Array.isArray(data.loras) ? data.loras : []; + provider.drawthings_connected = Boolean(data.connected); + provider.drawthings_status = data.message || ''; + } catch(error){ + provider.drawthings_model_metadata = []; + provider.drawthings_loras = []; + provider.drawthings_connected = false; + provider.drawthings_status = String(error?.message || error || '连接失败'); + } + if(isDrawThingsProvider(settings.provider_id)) renderDynamicParams(); +} // Draw Things 的 provider 由用户在 API 设置中添加,使用 id/protocol 双重判断以兼容已有配置。 function isDrawThingsProvider(providerId){ // 运行时配置尚未刷新时也要识别已保存的 Draw Things provider。 @@ -2983,6 +3049,7 @@ function renderApiParams(){ ${renderQualityControl()} ${renderCountVisualControl()} ${renderDrawThingsSeedControl()} + ${renderDrawThingsLoraControl()} ${isJimengProviderId(settings.provider_id) ? renderJimengUpscaleControl() : ''} `; } @@ -3532,6 +3599,40 @@ function renderDrawThingsSeedControl(){ `; } +function renderDrawThingsLoraControl(){ + if(!isDrawThingsProvider(settings.provider_id) || settings.apiKind === 'video') return ''; + const loras = drawThingsLoras(settings.provider_id); + const selectedFile = String(settings.drawThingsLoraFile || '').trim(); + const selected = loras.find(item => String(item?.file || '').trim() === selectedFile); + const rawWeight = Number(settings.drawThingsLoraWeight); + const weight = Number.isFinite(rawWeight) ? Math.max(-5, Math.min(5, rawWeight)) : 1; + const selectedLabel = selected ? String(selected.name || selected.file || '').trim() : ''; + const summary = selectedLabel + ? `${tr('smart.drawThingsLora')} · ${selectedLabel} · ${weight}` + : tr('smart.drawThingsLora'); + const compatibility = selected ? drawThingsLoraCompatibility(settings) : ''; + const options = [ + ``, + ...loras.map(item => { + const file = String(item?.file || '').trim(); + if(!file) return ''; + const label = String(item?.name || file).trim(); + return ``; + }), + ].join(''); + return `
+ +
+
${escapeHtml(tr('smart.drawThingsLora'))}
+ + + ${compatibility ? `
${escapeHtml(compatibility)}
` : ''} + ${!loras.length ? `
${escapeHtml(tr('smart.drawThingsLoraEmpty'))}
` : ''} +
+
`; +} function renderCountControl(){ return ``; } @@ -4235,6 +4336,28 @@ function bindDynamicParams(){ scheduleSave(); }; }); + dynamicParams.querySelectorAll('[data-drawthings-lora-file]').forEach(input => { + input.onclick = event => event.stopPropagation(); + input.onchange = event => { + event?.stopPropagation?.(); + settings.drawThingsLoraFile = String(input.value || ''); + persistActiveSmartSettings(); + renderDynamicParams(); + scheduleSave(); + }; + }); + dynamicParams.querySelectorAll('[data-drawthings-lora-weight]').forEach(input => { + input.onclick = event => event.stopPropagation(); + input.oninput = input.onchange = event => { + event?.stopPropagation?.(); + let value = Number(input.value); + if(!Number.isFinite(value)) value = 1; + value = Math.max(-5, Math.min(5, value)); + settings.drawThingsLoraWeight = value; + persistActiveSmartSettings(); + scheduleSave(); + }; + }); dynamicParams.querySelectorAll('[data-toggle-param]').forEach(btn => { btn.onclick = event => { event.preventDefault(); @@ -4394,6 +4517,7 @@ async function loadConfig(){ // 提供商配置已就绪即先渲染参数面板,避免等工作流/RunningHub 预取完成后参数才「突然刷新出来」。 sanitizeSmartApiSelection(settings); updateProviderModels(); + if(apiProviders.some(provider => isDrawThingsProvider(provider.id))) void refreshSmartDrawThingsModels(); const wf = await fetch('/api/workflows').then(r => r.json()).catch(() => ({workflows:[]})); comfyWorkflows = Array.isArray(wf.workflows) ? wf.workflows : []; runningHubWorkflowCache = {}; @@ -15244,7 +15368,10 @@ async function runApiGeneration(prompt, refs, runSettings=settings, cacheOptions const drawThingsSelected = isDrawThingsProvider(runSettings.provider_id); const count = smartApiImageTaskCount(runSettings); const drawThingsBatchSize = 1; - const payload = {prompt, provider_id:runSettings.provider_id, model:runSettings.model, size:sizeForRun(runSettings), aspect_ratio:runSettings.ratio === 'custom' ? (runSettings.customRatio || '') : (runSettings.ratio || ''), resolution:runSettings.resolution || '', quality:runSettings.quality || 'auto', n:1, batch_size:drawThingsBatchSize, reference_images:imageRefsOnly(refs).slice(0, SMART_REFERENCE_IMAGE_MAX)}; + const loras = drawThingsSelected ? selectedDrawThingsLoras(runSettings) : []; + const loraCompatibility = drawThingsSelected ? drawThingsLoraCompatibility(runSettings) : ''; + if(loraCompatibility) throw new Error(loraCompatibility); + const payload = {prompt, provider_id:runSettings.provider_id, model:runSettings.model, size:sizeForRun(runSettings), aspect_ratio:runSettings.ratio === 'custom' ? (runSettings.customRatio || '') : (runSettings.ratio || ''), resolution:runSettings.resolution || '', quality:runSettings.quality || 'auto', n:1, batch_size:drawThingsBatchSize, reference_images:imageRefsOnly(refs).slice(0, SMART_REFERENCE_IMAGE_MAX), loras}; const cachePayload = drawThingsSelected ? drawThingsCachePayload(payload, cacheOptions) : payload; const fixedSeedState = drawThingsSelected && runSettings.drawThingsSeedRandom === false ? {provider:'drawthings', seed:normalizeDrawThingsSeed(runSettings.drawThingsSeed)} From 168c6aa8d3d19dcc219ce6112e24fef0e0909ee4 Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Mon, 3 Aug 2026 16:53:35 +0800 Subject: [PATCH 06/20] =?UTF-8?q?=E5=AF=B9=E9=BD=90=20Draw=20Things=20?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- draw_things_grpc.py | 166 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 149 insertions(+), 17 deletions(-) diff --git a/draw_things_grpc.py b/draw_things_grpc.py index 353cd0894..e87394755 100644 --- a/draw_things_grpc.py +++ b/draw_things_grpc.py @@ -84,23 +84,27 @@ def _build_configuration( seed: int | None = None, strength: float | None = None, batch_size: int = 1, + loras: list[dict] | None = None, ) -> bytes: import flatbuffers from generated import config_generated model_name = str(model or "").lower() + is_klein_9b = "flux_2_klein_9b" in model_name + is_klein_4b = "flux_2_klein_4b" in model_name is_klein = "klein" in model_name is_z_image = "z_image" in model_name or "zimage" in model_name config = config_generated.GenerationConfigurationT() config.model = model config.startWidth = width // 64 config.startHeight = height // 64 - default_steps = "4" + default_steps = "8" if (is_z_image or is_klein_9b) else "4" config.steps = int(os.getenv("DRAW_THINGS_GRPC_STEPS", default_steps)) default_guidance = "1.0" if (is_klein or is_z_image) else "3.5" config.guidanceScale = float( os.getenv("DRAW_THINGS_GRPC_GUIDANCE", default_guidance) ) + config.strength = 1.0 if seed is not None: # The canvas dice control supplies an explicit seed for this request. config.seed = int(seed) % 4294967295 @@ -116,26 +120,46 @@ def _build_configuration( config.seed = int(configured_seed) % 4294967295 config.batchCount = 1 config.batchSize = max(1, min(8, int(batch_size or 1))) + config.loras = [] + for item in loras or []: + if not isinstance(item, dict): + continue + file_name = str(item.get("file") or "").strip() + if not file_name: + continue + try: + weight = float(item.get("weight", 1.0)) + except (TypeError, ValueError): + weight = 1.0 + lora = config_generated.LoRAT() + lora.file = file_name + lora.weight = max(-5.0, min(5.0, weight)) + lora.mode = 0 + config.loras.append(lora) if strength is not None: # Draw Things uses strength for image-to-image denoising. Keep it in # the same [0, 1] range exposed by the ComfyUI plugin. config.strength = max(0.0, min(1.0, float(strength))) if is_klein: - # FLUX.2 Klein's known-good Draw Things setup is 4-step DDIM Trailing - # with CFG 1, ScaleAlike seeds, shift 3, and no resolution shift. + # FLUX.2 Klein uses model-specific Draw Things presets. config.sampler = 16 # SamplerType.DDIMTrailing config.seedMode = 2 # SeedMode.ScaleAlike config.shift = 3.0 config.resolutionDependentShift = False + if is_klein_9b: + config.maskBlur = 1.5 + elif is_klein_4b: + config.maskBlur = 2.5 config.speedUpWithGuidanceEmbed = True config.guidanceEmbed = 3.5 elif is_z_image: - # Z Image Turbo's official Draw Things setup uses UniPC Trailing, - # ScaleAlike seeds, shift 3, and resolution-independent shift. + # Z Image Turbo uses the official eight-step Draw Things setup. config.sampler = 17 # SamplerType.UniPCTrailing config.seedMode = 2 # SeedMode.ScaleAlike config.shift = 3.0 config.resolutionDependentShift = False + config.speedUpWithGuidanceEmbed = False + config.guidanceEmbed = 0.0 builder = flatbuffers.Builder(0) builder.Finish(config.Pack(builder)) @@ -199,6 +223,27 @@ def _resize_crop_reference(image, width: int, height: int): return resized.crop((left, top, left + width, top + height)) +def _resize_crop_mask(image, width: int, height: int): + """Resize a mask without losing alpha or introducing soft edges.""" + from PIL import Image + + if "A" in image.getbands(): + background = Image.new("RGBA", image.size, (0, 0, 0, 255)) + image = Image.alpha_composite(background, image.convert("RGBA")) + image = image.convert("L") + if image.size == (width, height): + return image + source_width, source_height = image.size + scale = max(width / source_width, height / source_height) + resized = image.resize( + (max(width, int(source_width * scale)), max(height, int(source_height * scale))), + Image.Resampling.NEAREST, + ) + left = max(0, (resized.width - width) // 2) + top = max(0, (resized.height - height) // 2) + return resized.crop((left, top, left + width, top + height)) + + def _encode_image_for_request(reference: object, width: int, height: int) -> bytes: """Encode a local image as Draw Things' RGB NHWC FP16 tensor.""" import numpy as np @@ -230,6 +275,38 @@ def _encode_image_for_request(reference: object, width: int, height: int) -> byt return bytes(encoded) +def _encode_mask_for_request(reference: object, width: int, height: int) -> bytes: + """Encode a black/white mask as Draw Things' NCHW 8-bit tensor.""" + from PIL import Image + + raw = _reference_image_bytes(reference) + with Image.open(io.BytesIO(raw)) as source: + image = _resize_crop_mask(source, width, height) + + # Draw Things uses 0 for retained pixels and 2 for pixels redrawn with + # the configured img2img strength. This matches the ComfyUI node's mask + # conversion and keeps the mask separate from request.image. + encoded = bytearray(68 + width * height) + struct.pack_into( + "<9I", + encoded, + 0, + 0, + 0x1, # CCV_TENSOR_CPU_MEMORY + 0x01, # CCV_TENSOR_FORMAT_NCHW + 0x1000, # CCV_8U + 0, + height, + width, + 0, + 0, + ) + for y in range(height): + for x in range(width): + encoded[68 + y * width + x] = 2 if image.getpixel((x, y)) >= 50 else 0 + return bytes(encoded) + + def _parse_strength(value: object, default: float = 0.75) -> float: try: strength = float(value) @@ -337,13 +414,7 @@ def _channel(target: str, use_tls: bool): def _model_files(echo_reply) -> list[str]: - import json - - try: - raw_models = bytes(echo_reply.override.models or b"") - models = json.loads(raw_models.decode("utf-8")) if raw_models else [] - except (AttributeError, UnicodeDecodeError, json.JSONDecodeError): - models = [] + models = _metadata_items(echo_reply, "models") # Older/newer server builds may expose the model browser as EchoReply.files # instead of MetadataOverride.models. Keep both forms compatible. @@ -363,6 +434,46 @@ def _model_files(echo_reply) -> list[str]: return files +def _metadata_items(echo_reply, field: str) -> list[object]: + """Decode a Draw Things MetadataOverride JSON field.""" + import json + + try: + raw_value = bytes(getattr(echo_reply.override, field, b"") or b"") + if not raw_value: + return [] + try: + decoded = json.loads(raw_value.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError): + decoded = json.loads( + base64.b64decode(raw_value.strip(), validate=True).decode("utf-8") + ) + except (AttributeError, UnicodeDecodeError, json.JSONDecodeError, TypeError, ValueError): + decoded = [] + return decoded if isinstance(decoded, list) else [] + + +def _metadata_files(items: list[object]) -> list[dict]: + """Keep the metadata shape used by the Draw Things model browser.""" + normalized = [] + seen = set() + for item in items: + if isinstance(item, str): + file_name = item.strip() + value = {"file": file_name, "name": file_name} + elif isinstance(item, dict): + file_name = str(item.get("file") or "").strip() + value = dict(item) + value["file"] = file_name + value.setdefault("name", file_name) + else: + continue + if file_name and file_name not in seen: + seen.add(file_name) + normalized.append(value) + return normalized + + async def list_draw_things_models(endpoint: str = "") -> dict: """Read the live model list exposed by gRPCServerCLI's model browser.""" import grpc @@ -377,9 +488,13 @@ async def list_draw_things_models(endpoint: str = "") -> dict: if shared_secret: echo_request.sharedSecret = shared_secret reply = await stub.Echo(echo_request, timeout=10) + model_metadata = _metadata_files(_metadata_items(reply, "models")) + loras = _metadata_files(_metadata_items(reply, "loras")) return { "connected": True, "models": _model_files(reply), + "model_metadata": model_metadata, + "loras": loras, "host": host, "port": port, } @@ -387,6 +502,8 @@ async def list_draw_things_models(endpoint: str = "") -> dict: return { "connected": False, "models": [], + "model_metadata": [], + "loras": [], "host": host, "port": port, "error": ( @@ -406,9 +523,11 @@ async def generate_draw_things_image( seed: int | None = None, strength: float | None = None, hint_images: list[dict] | None = None, + mask_images: list[dict] | None = None, hint_type: str = "shuffle", hint_weights: list[object] | None = None, batch_size: int = 1, + loras: list[dict] | None = None, ) -> tuple[dict, dict]: """Generate one image and return the project's standard image item shape.""" import grpc @@ -419,6 +538,11 @@ async def generate_draw_things_image( raise RuntimeError( "当前 Draw Things 模型不支持多图图像编辑,请只保留一张输入图,或切换到 Klein/Qwen Edit 模型。" ) + masks = [item for item in (mask_images or []) if item] + if len(masks) > 1: + raise RuntimeError("当前 Draw Things 请求只支持一张遮罩图。") + if masks and not references: + raise RuntimeError("Draw Things 遮罩需要与一张输入图一起使用。") hints = [item for item in (hint_images or []) if item] if references and hints: raise RuntimeError( @@ -439,11 +563,16 @@ async def generate_draw_things_image( # Ordinary image-to-image is carried by request.image. It must remain # separate from HintProto, which is reserved for later control inputs. input_image = _encode_image_for_request(references[0], width, height) - image_strength = _parse_strength( - strength - if strength is not None - else os.getenv("DRAW_THINGS_GRPC_STRENGTH", "0.75") - ) + if strength is not None: + image_strength = _parse_strength(strength) + else: + configured_strength = os.getenv("DRAW_THINGS_GRPC_STRENGTH") + if configured_strength is None or not configured_strength.strip(): + # Infinite-Canvas always supplies a prompt for image edits; + # use Draw Things' full-strength edit configuration. + configured_strength = "1.0" + image_strength = _parse_strength(configured_strength) + mask_image = _encode_mask_for_request(masks[0], width, height) if masks else None request_hints = _build_hint_protos( hints, width, @@ -469,6 +598,7 @@ async def generate_draw_things_image( request = imageService_pb2.ImageGenerationRequest( image=input_image or b"", scaleFactor=1, + mask=mask_image or b"", hints=request_hints, prompt=str(prompt or ""), negativePrompt="", @@ -479,6 +609,7 @@ async def generate_draw_things_image( seed, strength=image_strength, batch_size=batch_size, + loras=loras, ), user="Infinite-Canvas", device=imageService_pb2.LAPTOP, @@ -510,6 +641,7 @@ async def generate_draw_things_image( "width": width, "height": height, "image_to_image": bool(input_image), + "masked": bool(mask_image), "strength": image_strength, "hint_type": str(hint_type or "").strip().lower() if hints else "", "hint_count": len(hints), From 318c1db75a1506aa567f2bc1251b4933578c7f9d Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Mon, 3 Aug 2026 17:00:13 +0800 Subject: [PATCH 07/20] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E7=94=BB=E5=B8=83=E4=B8=8E=20Draw=20Things=20=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + ...5\244\215\346\235\203\351\231\220.command" | 0 ...5\212\250\346\234\215\345\212\241.command" | 0 main.py | 69 ++++++++++++++++-- static/angle.html | 18 ++--- static/api-settings.html | 14 ++-- static/asset-manager.html | 10 +-- static/canvas-list.html | 16 ++--- static/canvas.html | 18 ++--- static/comfyui-settings.html | 14 ++-- static/enhance.html | 16 ++--- static/gpt-chat.html | 12 ++-- static/index.html | 28 ++++---- static/js/canvas.js | 2 +- static/js/i18n/smart-canvas.js | 6 ++ static/js/smart-canvas.js | 71 +++++++++++++++++-- static/klein.html | 16 ++--- static/online.html | 16 ++--- static/smart-canvas.html | 12 ++-- static/zimage.html | 14 ++-- ...43\205\345\215\263\346\242\246CLI.command" | 0 ...75\225\345\215\263\346\242\246CLI.command" | 0 22 files changed, 239 insertions(+), 116 deletions(-) mode change 100644 => 100755 "mac-\344\277\256\345\244\215\346\235\203\351\231\220.command" mode change 100644 => 100755 "mac-\345\220\257\345\212\250\346\234\215\345\212\241.command" mode change 100644 => 100755 main.py mode change 100644 => 100755 "\345\256\211\350\243\205\345\215\263\346\242\246CLI.command" mode change 100644 => 100755 "\347\231\273\345\275\225\345\215\263\346\242\246CLI.command" diff --git a/.gitignore b/.gitignore index 7c4dac3fc..9fb58e19c 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ node_modules/ # Project revision plan list IMPLEMENTATION_PLAN.md + +# Local feature backups and patch records +/bak/ diff --git "a/mac-\344\277\256\345\244\215\346\235\203\351\231\220.command" "b/mac-\344\277\256\345\244\215\346\235\203\351\231\220.command" old mode 100644 new mode 100755 diff --git "a/mac-\345\220\257\345\212\250\346\234\215\345\212\241.command" "b/mac-\345\220\257\345\212\250\346\234\215\345\212\241.command" old mode 100644 new mode 100755 diff --git a/main.py b/main.py old mode 100644 new mode 100755 index c2ca719d8..958361b43 --- a/main.py +++ b/main.py @@ -2735,6 +2735,8 @@ class OnlineImageRequest(BaseModel): batch_size: int = 1 seed: Optional[int] = None reference_images: List[AIReference] = [] + mask_images: List[AIReference] = [] + loras: Optional[List[Dict[str, Any]]] = None operation: str = "" resolution_type: str = "" @@ -11241,7 +11243,7 @@ async def generate_runninghub_video(payload, provider): local_urls = [await save_remote_video_to_output(url, prefix="rh_video_") for url in urls] return {"videos": local_urls, "task_id": task_id, "raw": result} -async def generate_ai_image(prompt, size, quality, model, reference_images=None, provider_id="comfly", aspect_ratio="", resolution="", seed=None, batch_size=1): +async def generate_ai_image(prompt, size, quality, model, reference_images=None, provider_id="comfly", aspect_ratio="", resolution="", seed=None, batch_size=1, loras=None, mask_images=None): provider = get_api_provider(provider_id) if is_tudou_provider(provider): model = tudou_image_model_for_request(model) @@ -11249,27 +11251,60 @@ async def generate_ai_image(prompt, size, quality, model, reference_images=None, from draw_things_grpc import draw_things_model_supports_editing drawthings_references = [] + drawthings_masks = [] + seen_sources = set() for reference in (reference_images or []): item = dict(reference) if isinstance(reference, dict) else {"url": reference} url = str(item.get("url") or "").strip() + is_mask = ( + str(item.get("role") or "").strip().lower() == "mask" + or bool(re.search(r"(?:^|_)mask\.(?:png|jpe?g|webp)$", str(item.get("name") or "").strip(), re.IGNORECASE)) + ) # Canvas references normally use /output or /assets URLs. Resolve # those to local files before passing them to the gRPC client; # other providers keep their existing reference handling. local_path = local_media_path_from_url(url) + target = drawthings_masks if is_mask else drawthings_references + if local_path: + target.append({ + "path": local_path, + "name": item.get("name") or os.path.basename(local_path), + "weight": item.get("weight", 1.0), + }) + elif url: + target.append(item) + if url: + seen_sources.add(url) + for reference in (mask_images or []): + item = dict(reference) if isinstance(reference, dict) else {"url": reference} + url = str(item.get("url") or "").strip() + if url and url in seen_sources: + continue + local_path = local_media_path_from_url(url) if local_path: - drawthings_references.append({ + drawthings_masks.append({ "path": local_path, "name": item.get("name") or os.path.basename(local_path), "weight": item.get("weight", 1.0), }) elif url: - drawthings_references.append(item) + drawthings_masks.append(item) editing_model = draw_things_model_supports_editing(model) if not editing_model and len(drawthings_references) > 1: raise HTTPException( status_code=400, detail="当前 Draw Things 模型只支持文生图或单图图生图,不能输入多张参考图。", ) + if len(drawthings_masks) > 1: + raise HTTPException( + status_code=400, + detail="当前 Draw Things 请求只支持一张遮罩图。", + ) + if drawthings_masks and not drawthings_references: + raise HTTPException( + status_code=400, + detail="遮罩需要与一张输入图一起使用。", + ) try: from draw_things_grpc import generate_draw_things_image # The provider's saved host:port overrides environment defaults. @@ -11277,8 +11312,16 @@ async def generate_ai_image(prompt, size, quality, model, reference_images=None, "endpoint": provider.get("base_url") or "", "seed": seed, "batch_size": batch_size, + "loras": loras or [], } - if editing_model: + if drawthings_masks: + # Draw Things masks belong to ImageGenerationRequest.mask and + # must not be counted as a second ordinary reference image. + request_options.update( + reference_images=drawthings_references, + mask_images=drawthings_masks, + ) + elif editing_model: request_options.update( hint_images=drawthings_references, hint_type="shuffle" if drawthings_references else "", @@ -13257,11 +13300,19 @@ async def api_providers(): async def drawthings_models(): provider = next((item for item in load_api_providers() if item.get("id") == "drawthings"), None) if not provider: - return {"connected": False, "models": [], "message": "Draw Things provider 尚未添加"} + return { + "connected": False, + "models": [], + "model_metadata": [], + "loras": [], + "message": "Draw Things provider 尚未添加", + } result = await fetch_models_from_upstream(provider.get("base_url") or "", "", "grpc", "openai") return { "connected": bool(result.get("ok")), "models": result.get("image_models") or [], + "model_metadata": result.get("model_metadata") or [], + "loras": result.get("loras") or [], "message": result.get("message") or "", } @@ -13643,6 +13694,8 @@ async def test_provider_connection(payload: TestConnectionPayload): "chat_models": [], "video_models": [], "all": models, + "model_metadata": result.get("model_metadata") or [], + "loras": result.get("loras") or [], "raw": result, } base_url = (payload.base_url or "").strip().rstrip("/") @@ -13906,6 +13959,8 @@ async def fetch_models_from_upstream(base_url: str, api_key: str, protocol: str "chat_models": [], "video_models": [], "all": models, + "model_metadata": result.get("model_metadata") or [], + "loras": result.get("loras") or [], "raw": result, } if protocol == "jimeng": @@ -14055,6 +14110,7 @@ async def build_online_image_result(payload: OnlineImageRequest): model = selected_model(payload.model, default_model) request_size = snap_size_to_multiple(payload.size, 16) refs = [ref.dict() for ref in payload.reference_images if ref.url] + mask_refs = [ref.dict() for ref in payload.mask_images if ref.url] image_refs = image_references(refs) count = max(1, min(8, int(payload.n or 1))) batch_size = max(1, min(8, int(payload.batch_size or 1))) if is_draw_things_provider(provider) else 1 @@ -14074,6 +14130,7 @@ async def generate_one(): image_data, raw_item = await generate_ai_image( payload.prompt, request_size, payload.quality, model, image_refs, provider["id"], payload.aspect_ratio, payload.resolution, payload.seed, batch_size=batch_size, + loras=payload.loras, mask_images=mask_refs, ) try: image_items = extract_images(raw_item) if isinstance(raw_item, dict) else [image_data] @@ -14117,7 +14174,7 @@ async def generate_one(): "provider_name": provider.get("name") or provider["id"], "task_id": extract_task_id(raw) if isinstance(raw, dict) else None, "request_id": raw.get("id") if isinstance(raw, dict) else None, - "params": {"provider_id": provider["id"], "model": model, "size": request_size, "requested_size": payload.size, "aspect_ratio": payload.aspect_ratio, "resolution": payload.resolution, "quality": payload.quality, "n": count, "batch_size": batch_size, "reference_images": refs}, + "params": {"provider_id": provider["id"], "model": model, "size": request_size, "requested_size": payload.size, "aspect_ratio": payload.aspect_ratio, "resolution": payload.resolution, "quality": payload.quality, "n": count, "batch_size": batch_size, "reference_images": refs, "mask_images": mask_refs}, "raw_usage": raw.get("usage") if isinstance(raw, dict) else None, } save_to_history(result) diff --git a/static/angle.html b/static/angle.html index 25ab1506d..7f2a7c314 100644 --- a/static/angle.html +++ b/static/angle.html @@ -17,21 +17,21 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/api-settings.html b/static/api-settings.html index 50798da32..da2474839 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -568,6 +568,6 @@
- + diff --git a/static/asset-manager.html b/static/asset-manager.html index a8acebda6..043505a6b 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index f3988b537..f639bcbdb 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index a3b132e7c..2b4d8709c 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -351,7 +351,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index 6f34f08c7..2eacc0248 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/enhance.html b/static/enhance.html index 004ec1bc1..54caa5b03 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index 1b0969f29..6f6521a5e 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/online.html b/static/online.html index cbdd16161..39ee13f48 100644 --- a/static/online.html +++ b/static/online.html @@ -16,14 +16,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + - + diff --git "a/\345\256\211\350\243\205\345\215\263\346\242\246CLI.command" "b/\345\256\211\350\243\205\345\215\263\346\242\246CLI.command" old mode 100644 new mode 100755 diff --git "a/\347\231\273\345\275\225\345\215\263\346\242\246CLI.command" "b/\347\231\273\345\275\225\345\215\263\346\242\246CLI.command" old mode 100644 new mode 100755 From 715e3c15c1aca07a31e9715eabe2a686d27399dd Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Mon, 3 Aug 2026 17:11:02 +0800 Subject: [PATCH 08/20] =?UTF-8?q?=E5=81=9C=E6=AD=A2=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=BF=BD=E7=95=A5=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 52 ---------------------------------------------------- 1 file changed, 52 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 9fb58e19c..000000000 --- a/.gitignore +++ /dev/null @@ -1,52 +0,0 @@ -# Local secrets and runtime state -API/.env -history.json - -# Python caches and test artifacts -**/__pycache__/ -**/__pycache__/.pytest_cache/ -*.py[cod] - -# Local smoke tests -/CLI/macos/drawthings_hint_smoke_test.py -/CLI/macos/drawthings_img2img_smoke_test.py - -# Generated image/output directories -output/ -assets/output/ -data/media_previews/ - -# Local project data and assets -/assets/ -/data/ - -# Local environment files -.env -.env.local -.venv/ -venv/ -*.egg-info/ - -# IDE and editor configurations -.vscode/ -.idea/ -*.swp -*.swo - -# OS generated files -.DS_Store -Thumbs.db - -# Build artifacts -dist/ -build/ -*.log - -# Node.js dependencies (if applicable) -node_modules/ - -# Project revision plan list -IMPLEMENTATION_PLAN.md - -# Local feature backups and patch records -/bak/ From cf4d943490a03cb819f88c1ee935f95e357ae274 Mon Sep 17 00:00:00 2001 From: Rayen21 <19681670+Rayen21@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:30:41 +0800 Subject: [PATCH 09/20] =?UTF-8?q?=E6=9B=B4=E6=96=B0gPRCServerCLI=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新gPRCServerCLI安装说明 --- CLI/macos/drawthings/README.md | 36 +++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/CLI/macos/drawthings/README.md b/CLI/macos/drawthings/README.md index 0dc289c6f..ca0ae2900 100644 --- a/CLI/macos/drawthings/README.md +++ b/CLI/macos/drawthings/README.md @@ -2,6 +2,27 @@ 这是 Infinite-Canvas 的 Draw Things gRPCServerCLI 独立依赖说明,仅适用于 macOS Apple Silicon(M 芯片)。Draw Things gRPCServerCLI 需要由用户单独启动,Infinite-Canvas 只负责连接已运行的服务。 +## 安装gPRCServerCLI +以下是drawthings作者编译好的gPRCServerCLI执行文件下载地址 +https://github.com/drawthingsai/draw-things-community/releases +参考drawthings作者GitHub仓库中的安装方法安装: +``` +Self-host gRPCServerCLI from Packaged Binaries + +We provide pre-built self-hosted gRPCServerCLI binaries through this repository. Latest version should be available at Releases. + +These pre-built binaries provide a quick way to host Draw Things gRPC Server on your Mac or Linux systems without download the Draw Things app. Draw Things app then can connect to these self-hosted servers through Server-Offload feature within your network. + +macOS + +On macOS, simply download the gRPCServerCLI-macOS on your macOS systems. You can put it under /usr/local/bin or anywhere you feel comfortable, and launch it with: + +gRPCServerCLI-macOS /the-path-to-host-the-models +If you have Draw Things app installed, you can simply refer the model path by doing: + +gRPCServerCLI-macOS ~/Library/Containers/com.liuliu.draw-things/Data/Documents/Models +``` + ## 安装依赖 请在 Infinite-Canvas 项目根目录执行。建议使用 conda 或 Miniforge 创建的独立环境,不要把这些依赖追加到项目根目录的 `requirements.txt`。 @@ -18,11 +39,20 @@ python -m pip install -r CLI/macos/drawthings/requirements-Dt-gRPC.txt ## 启动服务 -请先在 Draw Things 中启动 gRPCServerCLI。默认连接地址为 `127.0.0.1:7859`,TLS 默认开启;如果启动服务时使用了自定义主机或端口,请在 Infinite-Canvas 的 Draw Things gRPC 设置中填写对应的 `主机:端口`。 +### 方法1(推荐) +直接启动gPRCServerCLI(drawthings的模型路径没改动的话直接执行以下命令)不用开启drawthings本身 +``` +gRPCServerCLI ~/Library/Containers/com.liuliu.draw-things/Data/Documents/Models --model-browser +``` +默认连接地址为 `127.0.0.1:7859`,TLS 默认开启 +注:(如果启动服务时使用了自定义主机或端口,请在 Infinite-Canvas 的 Draw Things gRPC 设置中填写对应的 `主机:端口`。) +### 方法2 +先在 Draw Things 中启动 gRPCServerCLI。 -## 项目设置 +## 大雄画布中的API设置 -在 Infinite-Canvas 的 API 设置中添加 Draw Things gRPCServerCLI provider 后,再选择实际连接到 gRPCServerCLI 的模型。模型列表由后端实时读取,不在画布节点中固定保存模型名称。 +在 Infinite-Canvas 的 API 设置中添加 Draw Things gRPCServerCLI provider 后,再选择实际连接到 gRPCServerCLI 的本地路径和端口号。 +模型列表由后端实时读取,不在画布节点中固定保存模型名称。 该连接目前用于图片生成、单图图生图和 Hint 多图编辑,不提供聊天模型能力。Hint 最多支持四张图片,每张图片可在画布节点中独立设置控制类型。 From 056429e22b65d2f6b00af089c489fca33a71d118 Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Mon, 3 Aug 2026 21:11:49 +0800 Subject: [PATCH 10/20] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20Draw=20Things=20gRPC?= =?UTF-8?q?=20=E8=B7=AF=E5=BE=84=E4=B8=8E=E9=81=AE=E7=BD=A9=E5=8F=82?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLI/__init__.py | 1 + CLI/macos/__init__.py | 1 + CLI/macos/drawthings/README.md | 4 + CLI/macos/drawthings/__init__.py | 1 + CLI/macos/drawthings/credentials.py | 35 + .../macos/drawthings/draw_things_grpc.py | 34 +- CLI/macos/drawthings/generated/__init__.py | 0 .../drawthings/generated/config_generated.py | 1782 +++++++++++++++++ .../drawthings/generated/config_generated.pyi | 534 +++++ .../drawthings/generated/imageService_pb2.py | 98 + .../drawthings/generated/imageService_pb2.pyi | 296 +++ .../generated/imageService_pb2_grpc.py | 312 +++ main.py | 8 +- 13 files changed, 3082 insertions(+), 24 deletions(-) create mode 100644 CLI/__init__.py create mode 100644 CLI/macos/__init__.py create mode 100644 CLI/macos/drawthings/__init__.py create mode 100644 CLI/macos/drawthings/credentials.py rename draw_things_grpc.py => CLI/macos/drawthings/draw_things_grpc.py (96%) create mode 100644 CLI/macos/drawthings/generated/__init__.py create mode 100644 CLI/macos/drawthings/generated/config_generated.py create mode 100644 CLI/macos/drawthings/generated/config_generated.pyi create mode 100644 CLI/macos/drawthings/generated/imageService_pb2.py create mode 100644 CLI/macos/drawthings/generated/imageService_pb2.pyi create mode 100644 CLI/macos/drawthings/generated/imageService_pb2_grpc.py diff --git a/CLI/__init__.py b/CLI/__init__.py new file mode 100644 index 000000000..7631c1404 --- /dev/null +++ b/CLI/__init__.py @@ -0,0 +1 @@ +"""Project command-line integrations.""" diff --git a/CLI/macos/__init__.py b/CLI/macos/__init__.py new file mode 100644 index 000000000..f0c42bf17 --- /dev/null +++ b/CLI/macos/__init__.py @@ -0,0 +1 @@ +"""macOS-specific integrations.""" diff --git a/CLI/macos/drawthings/README.md b/CLI/macos/drawthings/README.md index ca0ae2900..cfcd48ecc 100644 --- a/CLI/macos/drawthings/README.md +++ b/CLI/macos/drawthings/README.md @@ -2,6 +2,10 @@ 这是 Infinite-Canvas 的 Draw Things gRPCServerCLI 独立依赖说明,仅适用于 macOS Apple Silicon(M 芯片)。Draw Things gRPCServerCLI 需要由用户单独启动,Infinite-Canvas 只负责连接已运行的服务。 +Draw Things gRPCServerCLI 借助了 +https://github.com/drawthingsai/draw-things-comfyui.git 这个comfyui插件代码的能力。 + +当前版本已将 Draw Things gRPC 所需的协议文件和客户端代码放在本目录中,不再运行时调用或依赖 `draw-things-comfyui` 插件,也不需要启动 ComfyUI。只需在画布所使用的 Python 环境中安装下面列出的 Python 依赖。 ## 安装gPRCServerCLI 以下是drawthings作者编译好的gPRCServerCLI执行文件下载地址 https://github.com/drawthingsai/draw-things-community/releases diff --git a/CLI/macos/drawthings/__init__.py b/CLI/macos/drawthings/__init__.py new file mode 100644 index 000000000..963b861f5 --- /dev/null +++ b/CLI/macos/drawthings/__init__.py @@ -0,0 +1 @@ +"""Draw Things gRPCServerCLI integration for Infinite-Canvas.""" diff --git a/CLI/macos/drawthings/credentials.py b/CLI/macos/drawthings/credentials.py new file mode 100644 index 000000000..8cc3b9dcc --- /dev/null +++ b/CLI/macos/drawthings/credentials.py @@ -0,0 +1,35 @@ +import grpc + +_cert = b'''-----BEGIN CERTIFICATE----- +MIIFHTCCAwWgAwIBAgIUWxJuoygy7Hsb9bcSfggNGLGZJW4wDQYJKoZIhvcNAQEL +BQAwHjEcMBoGA1UEAwwTRHJhdyBUaGluZ3MgUm9vdCBDQTAeFw0yNDEwMTUxNzI3 +NTJaFw0zNDEwMTMxNzI3NTJaMB4xHDAaBgNVBAMME0RyYXcgVGhpbmdzIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDe/RKAuabH2pEadZj6 +JRTOaEIMYXsAI7ZIG+LSAEkyK/QZAMdLq+wBq6uJDIEvTXMyyhNgkI3oUnS2PJqi +y9lzGAh1s2y6MDG17BFboyriW0y6BKd42amX/g9A40ZC1cBs2NI9e0zjy/vhHLw1 +EHK1XDLsIYAZvqQLJR3zRslHTHN6BysNWNmO/s1myLHQzbjyg4+/JHqma5Xatz0W +I5Wi6zxu/G1IdWeO6tlWWBSArDbhru+rb2U9p9/jKGW7fOom7sH9oBpj7q+xcrr5 +h2Aoam4xRqxc3SG7TRc1inEki86/FoWCARSqGo2t7q/brkwwGbeZsuwKhIuhWGzW +CJKp0NvD11HyCqsJsLMTx9PXzEsCDFsios+zI6zu1aIVomO5h8d59oxMGEvNozIc +gSHJI3pCiHmJt0o9xoRi0UGiB6PP3k4ZzxTV30wt0oMOzS8dgMdl1u0zpAc2aEGG +4cdWQaDP2UgZlNQyzGbGUC2Q2ln1ghTlEBAs23/yDZyEbtWj+Qo1Isk80CXISs8/ +H4cdM9Xw/Rt5fGxSaNzHJZJ9gK8YFI0z7IDiQp9nWkMqyDhGjhT4ZR847Nz52gcK +zuqmSK6B7ksumilchQ8hq79VAAvZqQoyVIvLvkbb6pXZbH0qTK5yk0YQVJ49JU1L +XnB4Iu8IuDxTLmtW2WoCjUZaqQIDAQABo1MwUTAdBgNVHQ4EFgQUhmFk2qHWAU6/ +3u6FyCnk2vaV0fswHwYDVR0jBBgwFoAUhmFk2qHWAU6/3u6FyCnk2vaV0fswDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAdvryE1xbhpyDjtP+I95Z +tgmlkmIWTPoHL5WO20SWtWjryHTs0XGXkohqSFKBqYTOVTyRCTtUTF4nWoNfBhlz +aOExf64UgvYHO4NxcPNjUH2Yx/AKFWBeHx50jfjz/zTSqhAHv8rlYDt6rlLs1aFm +rNj3DObqmTfDoI8qkdLK8bekjhul6PusmezhW+qa/DMvDRy3moUugpXwzvyG5GRW +C3+nNbBdCdblUyiEgFu5htH6hSSu2IX5t/ryoKNjAAfUxMKcNFdYCnzWiHKOlrmp +wYL4YhVQZZYmis8ZIFOQ+BKVQHJcqE5bdrbNbCpurMNODEuDDB/VkbGHEVFVgB0n +x+ZtaGnfTeJJ6h7IIl+Gnpx0u9k+2pu78cEQ+6ZYKaGUoOKccxgipsSXWL75qHl9 +7/scB3imqRq0Q7/jKP6mvcB3/5irQwVmczsFwELLP0LJdsCZMcQQQsSCGuskzcAJ +iiiGzRVTfYFUu2hJ5JIgewg+NEzMCwzR5yyWacBcrrDxQTymTNW9NWahHxvdZJHd +zRd4Y3HNLPikGg37mCYIPWtUxJCU7/lZleNSqlMBhDdbIZcAqaHOQlYJQSZaTMwK +kWF1y/C6TdCKWyXhAEV8zp/0q4b6vC1ynn/GfopROPXceLbGA+BLG9JEQ1AiGae3 +ejQ40oILyZjEclMPGLYjqoQ= +-----END CERTIFICATE----- +''' + +credentials = grpc.ssl_channel_credentials(_cert) diff --git a/draw_things_grpc.py b/CLI/macos/drawthings/draw_things_grpc.py similarity index 96% rename from draw_things_grpc.py rename to CLI/macos/drawthings/draw_things_grpc.py index e87394755..387c1ec6f 100644 --- a/draw_things_grpc.py +++ b/CLI/macos/drawthings/draw_things_grpc.py @@ -7,17 +7,10 @@ import os import secrets import struct -import sys from pathlib import Path from urllib.parse import urlsplit - -REPO_ROOT = Path(__file__).resolve().parents[1] -PLUGIN_SRC = REPO_ROOT / "draw-things-comfyui" / "src" -if str(PLUGIN_SRC) not in sys.path: - sys.path.insert(0, str(PLUGIN_SRC)) - - +PROJECT_ROOT = Path(__file__).resolve().parents[3] DEFAULT_HOST = "127.0.0.1" DEFAULT_PORT = 7859 DEFAULT_SIZE = (1024, 1024) @@ -87,7 +80,7 @@ def _build_configuration( loras: list[dict] | None = None, ) -> bytes: import flatbuffers - from generated import config_generated + from .generated import config_generated model_name = str(model or "").lower() is_klein_9b = "flux_2_klein_9b" in model_name @@ -147,7 +140,7 @@ def _build_configuration( config.shift = 3.0 config.resolutionDependentShift = False if is_klein_9b: - config.maskBlur = 1.5 + config.maskBlur = 2.5 elif is_klein_4b: config.maskBlur = 2.5 config.speedUpWithGuidanceEmbed = True @@ -158,6 +151,7 @@ def _build_configuration( config.seedMode = 2 # SeedMode.ScaleAlike config.shift = 3.0 config.resolutionDependentShift = False + config.maskBlur = 2.5 config.speedUpWithGuidanceEmbed = False config.guidanceEmbed = 0.0 @@ -196,7 +190,7 @@ def _reference_image_bytes(reference: object) -> bytes: candidates = ( Path.cwd() / path, Path(__file__).resolve().parent / path, - REPO_ROOT / path, + PROJECT_ROOT / path, ) path = next((candidate for candidate in candidates if candidate.is_file()), candidates[0]) try: @@ -206,7 +200,7 @@ def _reference_image_bytes(reference: object) -> bytes: def _resize_crop_reference(image, width: int, height: int): - """Match draw-things-comfyui's resize-then-center-crop behavior.""" + """Match Draw Things' resize-then-center-crop behavior.""" from PIL import Image image = image.convert("RGB") @@ -254,8 +248,8 @@ def _encode_image_for_request(reference: object, width: int, height: int) -> byt image = _resize_crop_reference(source, width, height) pixels = np.asarray(image, dtype=np.float32) / 255.0 * 2.0 - 1.0 - # This is the same 68-byte CCV header and HWC FP16 payload used by the - # draw-things-comfyui plugin. It is an image input, not a HintProto. + # This is Draw Things' 68-byte CCV header and HWC FP16 image payload. + # It is an image input, not a HintProto. encoded = bytearray(68 + width * height * 3 * 2) struct.pack_into( "<9I", @@ -336,10 +330,10 @@ def _build_hint_protos( """Build one HintProto from one or more local images. Draw Things expects all images belonging to one control type inside the - same HintProto. This mirrors draw-things-comfyui's request construction - and keeps ordinary request.image input separate from Hint inputs. + same HintProto. Keep ordinary request.image input separate from Hint + inputs while constructing the request from the local protocol types. """ - from generated import imageService_pb2 + from .generated import imageService_pb2 normalized_type = str(hint_type or "").strip().lower() if not normalized_type: @@ -402,7 +396,7 @@ def _decode_response_image(response_image: bytes) -> bytes: def _channel(target: str, use_tls: bool): import grpc - from credentials import credentials + from .credentials import credentials options = [ ("grpc.max_send_message_length", -1), @@ -477,7 +471,7 @@ def _metadata_files(items: list[object]) -> list[dict]: async def list_draw_things_models(endpoint: str = "") -> dict: """Read the live model list exposed by gRPCServerCLI's model browser.""" import grpc - from generated import imageService_pb2, imageService_pb2_grpc + from .generated import imageService_pb2, imageService_pb2_grpc host, port, use_tls, shared_secret = _settings(endpoint) target = f"{host}:{port}" @@ -531,7 +525,7 @@ async def generate_draw_things_image( ) -> tuple[dict, dict]: """Generate one image and return the project's standard image item shape.""" import grpc - from generated import imageService_pb2, imageService_pb2_grpc + from .generated import imageService_pb2, imageService_pb2_grpc references = [item for item in (reference_images or []) if item] if len(references) > 1: diff --git a/CLI/macos/drawthings/generated/__init__.py b/CLI/macos/drawthings/generated/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/CLI/macos/drawthings/generated/config_generated.py b/CLI/macos/drawthings/generated/config_generated.py new file mode 100644 index 000000000..cb21cb084 --- /dev/null +++ b/CLI/macos/drawthings/generated/config_generated.py @@ -0,0 +1,1782 @@ +# automatically generated by the FlatBuffers compiler, do not modify + +# namespace: + +import flatbuffers +from flatbuffers.compat import import_numpy +from typing import Any +from typing import Optional +np = import_numpy() + +class SamplerType(object): + DPMPP2MKarras = 0 + EulerA = 1 + DDIM = 2 + PLMS = 3 + DPMPPSDEKarras = 4 + UniPC = 5 + LCM = 6 + EulerASubstep = 7 + DPMPPSDESubstep = 8 + TCD = 9 + EulerATrailing = 10 + DPMPPSDETrailing = 11 + DPMPP2MAYS = 12 + EulerAAYS = 13 + DPMPPSDEAYS = 14 + DPMPP2MTrailing = 15 + DDIMTrailing = 16 + UniPCTrailing = 17 + UniPCAYS = 18 + TCDTrailing = 19 + + +class SeedMode(object): + Legacy = 0 + TorchCpuCompatible = 1 + ScaleAlike = 2 + NvidiaGpuCompatible = 3 + + +class ControlMode(object): + Balanced = 0 + Prompt = 1 + Control = 2 + + +class ControlInputType(object): + Unspecified = 0 + Custom = 1 + Depth = 2 + Canny = 3 + Scribble = 4 + Pose = 5 + Normalbae = 6 + Color = 7 + Lineart = 8 + Softedge = 9 + Seg = 10 + Inpaint = 11 + Ip2p = 12 + Shuffle = 13 + Mlsd = 14 + Tile = 15 + Blur = 16 + Lowquality = 17 + Gray = 18 + + +class LoRAMode(object): + All = 0 + Base = 1 + Refiner = 2 + + +class CompressionMethod(object): + Disabled = 0 + H264 = 1 + H265 = 2 + Jpeg = 3 + + +class ColorCalibration(object): + Disabled = 0 + Lab = 1 + + +class Control(object): + __slots__ = ['_tab'] + + @classmethod + def GetRootAs(cls, buf, offset: int = 0): + n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) + x = Control() + x.Init(buf, n + offset) + return x + + @classmethod + def GetRootAsControl(cls, buf, offset=0): + """This method is deprecated. Please switch to GetRootAs.""" + return cls.GetRootAs(buf, offset) + # Control + def Init(self, buf: bytes, pos: int): + self._tab = flatbuffers.table.Table(buf, pos) + + # Control + def File(self) -> Optional[str]: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # Control + def Weight(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 1.0 + + # Control + def GuidanceStart(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 0.0 + + # Control + def GuidanceEnd(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 1.0 + + # Control + def NoPrompt(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # Control + def GlobalAveragePooling(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return True + + # Control + def DownSamplingRate(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 1.0 + + # Control + def ControlMode(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int8Flags, o + self._tab.Pos) + return 0 + + # Control + def TargetBlocks(self, j: int): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20)) + if o != 0: + a = self._tab.Vector(o) + return self._tab.String(a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 4)) + return "" + + # Control + def TargetBlocksLength(self) -> int: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20)) + if o != 0: + return self._tab.VectorLen(o) + return 0 + + # Control + def TargetBlocksIsNone(self) -> bool: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20)) + return o == 0 + + # Control + def InputOverride(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(22)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int8Flags, o + self._tab.Pos) + return 0 + +def ControlStart(builder: flatbuffers.Builder): + builder.StartObject(10) + +def ControlAddFile(builder: flatbuffers.Builder, file: int): + builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(file), 0) + +def ControlAddWeight(builder: flatbuffers.Builder, weight: float): + builder.PrependFloat32Slot(1, weight, 1.0) + +def ControlAddGuidanceStart(builder: flatbuffers.Builder, guidanceStart: float): + builder.PrependFloat32Slot(2, guidanceStart, 0.0) + +def ControlAddGuidanceEnd(builder: flatbuffers.Builder, guidanceEnd: float): + builder.PrependFloat32Slot(3, guidanceEnd, 1.0) + +def ControlAddNoPrompt(builder: flatbuffers.Builder, noPrompt: bool): + builder.PrependBoolSlot(4, noPrompt, 0) + +def ControlAddGlobalAveragePooling(builder: flatbuffers.Builder, globalAveragePooling: bool): + builder.PrependBoolSlot(5, globalAveragePooling, 1) + +def ControlAddDownSamplingRate(builder: flatbuffers.Builder, downSamplingRate: float): + builder.PrependFloat32Slot(6, downSamplingRate, 1.0) + +def ControlAddControlMode(builder: flatbuffers.Builder, controlMode: int): + builder.PrependInt8Slot(7, controlMode, 0) + +def ControlAddTargetBlocks(builder: flatbuffers.Builder, targetBlocks: int): + builder.PrependUOffsetTRelativeSlot(8, flatbuffers.number_types.UOffsetTFlags.py_type(targetBlocks), 0) + +def ControlStartTargetBlocksVector(builder, numElems: int) -> int: + return builder.StartVector(4, numElems, 4) + +def ControlAddInputOverride(builder: flatbuffers.Builder, inputOverride: int): + builder.PrependInt8Slot(9, inputOverride, 0) + +def ControlEnd(builder: flatbuffers.Builder) -> int: + return builder.EndObject() + + +try: + from typing import List +except: + pass + +class ControlT(object): + + # ControlT + def __init__( + self, + file = None, + weight = 1.0, + guidanceStart = 0.0, + guidanceEnd = 1.0, + noPrompt = False, + globalAveragePooling = True, + downSamplingRate = 1.0, + controlMode = 0, + targetBlocks = None, + inputOverride = 0, + ): + self.file = file # type: Optional[str] + self.weight = weight # type: float + self.guidanceStart = guidanceStart # type: float + self.guidanceEnd = guidanceEnd # type: float + self.noPrompt = noPrompt # type: bool + self.globalAveragePooling = globalAveragePooling # type: bool + self.downSamplingRate = downSamplingRate # type: float + self.controlMode = controlMode # type: int + self.targetBlocks = targetBlocks # type: Optional[List[Optional[str]]] + self.inputOverride = inputOverride # type: int + + @classmethod + def InitFromBuf(cls, buf, pos): + control = Control() + control.Init(buf, pos) + return cls.InitFromObj(control) + + @classmethod + def InitFromPackedBuf(cls, buf, pos=0): + n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, pos) + return cls.InitFromBuf(buf, pos+n) + + @classmethod + def InitFromObj(cls, control): + x = ControlT() + x._UnPack(control) + return x + + # ControlT + def _UnPack(self, control): + if control is None: + return + self.file = control.File() + self.weight = control.Weight() + self.guidanceStart = control.GuidanceStart() + self.guidanceEnd = control.GuidanceEnd() + self.noPrompt = control.NoPrompt() + self.globalAveragePooling = control.GlobalAveragePooling() + self.downSamplingRate = control.DownSamplingRate() + self.controlMode = control.ControlMode() + if not control.TargetBlocksIsNone(): + self.targetBlocks = [] + for i in range(control.TargetBlocksLength()): + self.targetBlocks.append(control.TargetBlocks(i)) + self.inputOverride = control.InputOverride() + + # ControlT + def Pack(self, builder): + if self.file is not None: + file = builder.CreateString(self.file) + if self.targetBlocks is not None: + targetBlockslist = [] + for i in range(len(self.targetBlocks)): + targetBlockslist.append(builder.CreateString(self.targetBlocks[i])) + ControlStartTargetBlocksVector(builder, len(self.targetBlocks)) + for i in reversed(range(len(self.targetBlocks))): + builder.PrependUOffsetTRelative(targetBlockslist[i]) + targetBlocks = builder.EndVector() + ControlStart(builder) + if self.file is not None: + ControlAddFile(builder, file) + ControlAddWeight(builder, self.weight) + ControlAddGuidanceStart(builder, self.guidanceStart) + ControlAddGuidanceEnd(builder, self.guidanceEnd) + ControlAddNoPrompt(builder, self.noPrompt) + ControlAddGlobalAveragePooling(builder, self.globalAveragePooling) + ControlAddDownSamplingRate(builder, self.downSamplingRate) + ControlAddControlMode(builder, self.controlMode) + if self.targetBlocks is not None: + ControlAddTargetBlocks(builder, targetBlocks) + ControlAddInputOverride(builder, self.inputOverride) + control = ControlEnd(builder) + return control + + +class LoRA(object): + __slots__ = ['_tab'] + + @classmethod + def GetRootAs(cls, buf, offset: int = 0): + n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) + x = LoRA() + x.Init(buf, n + offset) + return x + + @classmethod + def GetRootAsLoRA(cls, buf, offset=0): + """This method is deprecated. Please switch to GetRootAs.""" + return cls.GetRootAs(buf, offset) + # LoRA + def Init(self, buf: bytes, pos: int): + self._tab = flatbuffers.table.Table(buf, pos) + + # LoRA + def File(self) -> Optional[str]: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # LoRA + def Weight(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 0.6 + + # LoRA + def Mode(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int8Flags, o + self._tab.Pos) + return 0 + +def LoRAStart(builder: flatbuffers.Builder): + builder.StartObject(3) + +def LoRAAddFile(builder: flatbuffers.Builder, file: int): + builder.PrependUOffsetTRelativeSlot(0, flatbuffers.number_types.UOffsetTFlags.py_type(file), 0) + +def LoRAAddWeight(builder: flatbuffers.Builder, weight: float): + builder.PrependFloat32Slot(1, weight, 0.6) + +def LoRAAddMode(builder: flatbuffers.Builder, mode: int): + builder.PrependInt8Slot(2, mode, 0) + +def LoRAEnd(builder: flatbuffers.Builder) -> int: + return builder.EndObject() + + + +class LoRAT(object): + + # LoRAT + def __init__( + self, + file = None, + weight = 0.6, + mode = 0, + ): + self.file = file # type: Optional[str] + self.weight = weight # type: float + self.mode = mode # type: int + + @classmethod + def InitFromBuf(cls, buf, pos): + loRa = LoRA() + loRa.Init(buf, pos) + return cls.InitFromObj(loRa) + + @classmethod + def InitFromPackedBuf(cls, buf, pos=0): + n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, pos) + return cls.InitFromBuf(buf, pos+n) + + @classmethod + def InitFromObj(cls, loRa): + x = LoRAT() + x._UnPack(loRa) + return x + + # LoRAT + def _UnPack(self, loRa): + if loRa is None: + return + self.file = loRa.File() + self.weight = loRa.Weight() + self.mode = loRa.Mode() + + # LoRAT + def Pack(self, builder): + if self.file is not None: + file = builder.CreateString(self.file) + LoRAStart(builder) + if self.file is not None: + LoRAAddFile(builder, file) + LoRAAddWeight(builder, self.weight) + LoRAAddMode(builder, self.mode) + loRa = LoRAEnd(builder) + return loRa + + +class GenerationConfiguration(object): + __slots__ = ['_tab'] + + @classmethod + def GetRootAs(cls, buf, offset: int = 0): + n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset) + x = GenerationConfiguration() + x.Init(buf, n + offset) + return x + + @classmethod + def GetRootAsGenerationConfiguration(cls, buf, offset=0): + """This method is deprecated. Please switch to GetRootAs.""" + return cls.GetRootAs(buf, offset) + # GenerationConfiguration + def Init(self, buf: bytes, pos: int): + self._tab = flatbuffers.table.Table(buf, pos) + + # GenerationConfiguration + def Id(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int64Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def StartWidth(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint16Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def StartHeight(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint16Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def Seed(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def Steps(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def GuidanceScale(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 0.0 + + # GenerationConfiguration + def Strength(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 0.0 + + # GenerationConfiguration + def Model(self) -> Optional[str]: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # GenerationConfiguration + def Sampler(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(20)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int8Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def BatchCount(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(22)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 1 + + # GenerationConfiguration + def BatchSize(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(24)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 1 + + # GenerationConfiguration + def HiresFix(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(26)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # GenerationConfiguration + def HiresFixStartWidth(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(28)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint16Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def HiresFixStartHeight(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(30)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint16Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def HiresFixStrength(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(32)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 0.7 + + # GenerationConfiguration + def Upscaler(self) -> Optional[str]: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(34)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # GenerationConfiguration + def ImageGuidanceScale(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(36)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 1.5 + + # GenerationConfiguration + def SeedMode(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(38)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int8Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def ClipSkip(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(40)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 1 + + # GenerationConfiguration + def Controls(self, j: int) -> Optional[Control]: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(42)) + if o != 0: + x = self._tab.Vector(o) + x += flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4 + x = self._tab.Indirect(x) + obj = Control() + obj.Init(self._tab.Bytes, x) + return obj + return None + + # GenerationConfiguration + def ControlsLength(self) -> int: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(42)) + if o != 0: + return self._tab.VectorLen(o) + return 0 + + # GenerationConfiguration + def ControlsIsNone(self) -> bool: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(42)) + return o == 0 + + # GenerationConfiguration + def Loras(self, j: int) -> Optional[LoRA]: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(44)) + if o != 0: + x = self._tab.Vector(o) + x += flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4 + x = self._tab.Indirect(x) + obj = LoRA() + obj.Init(self._tab.Bytes, x) + return obj + return None + + # GenerationConfiguration + def LorasLength(self) -> int: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(44)) + if o != 0: + return self._tab.VectorLen(o) + return 0 + + # GenerationConfiguration + def LorasIsNone(self) -> bool: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(44)) + return o == 0 + + # GenerationConfiguration + def MaskBlur(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(46)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 0.0 + + # GenerationConfiguration + def FaceRestoration(self) -> Optional[str]: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(48)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # GenerationConfiguration + def ClipWeight(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(54)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 1.0 + + # GenerationConfiguration + def NegativePromptForImagePrior(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(56)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return True + + # GenerationConfiguration + def ImagePriorSteps(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(58)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 5 + + # GenerationConfiguration + def RefinerModel(self) -> Optional[str]: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(60)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # GenerationConfiguration + def OriginalImageHeight(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(62)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def OriginalImageWidth(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(64)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def CropTop(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(66)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def CropLeft(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(68)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def TargetImageHeight(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(70)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def TargetImageWidth(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(72)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def AestheticScore(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(74)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 6.0 + + # GenerationConfiguration + def NegativeAestheticScore(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(76)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 2.5 + + # GenerationConfiguration + def ZeroNegativePrompt(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(78)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # GenerationConfiguration + def RefinerStart(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(80)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 0.7 + + # GenerationConfiguration + def NegativeOriginalImageHeight(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(82)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def NegativeOriginalImageWidth(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(84)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def Name(self) -> Optional[str]: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(86)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # GenerationConfiguration + def FpsId(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(88)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 5 + + # GenerationConfiguration + def MotionBucketId(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(90)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 127 + + # GenerationConfiguration + def CondAug(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(92)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 0.02 + + # GenerationConfiguration + def StartFrameCfg(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(94)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 1.0 + + # GenerationConfiguration + def NumFrames(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(96)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 14 + + # GenerationConfiguration + def MaskBlurOutset(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(98)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def Sharpness(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(100)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 0.0 + + # GenerationConfiguration + def Shift(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(102)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 1.0 + + # GenerationConfiguration + def Stage2Steps(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(104)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint32Flags, o + self._tab.Pos) + return 10 + + # GenerationConfiguration + def Stage2Cfg(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(106)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 1.0 + + # GenerationConfiguration + def Stage2Shift(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(108)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 1.0 + + # GenerationConfiguration + def TiledDecoding(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(110)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # GenerationConfiguration + def DecodingTileWidth(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(112)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint16Flags, o + self._tab.Pos) + return 10 + + # GenerationConfiguration + def DecodingTileHeight(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(114)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint16Flags, o + self._tab.Pos) + return 10 + + # GenerationConfiguration + def DecodingTileOverlap(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(116)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint16Flags, o + self._tab.Pos) + return 2 + + # GenerationConfiguration + def StochasticSamplingGamma(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(118)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 0.3 + + # GenerationConfiguration + def PreserveOriginalAfterInpaint(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(120)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return True + + # GenerationConfiguration + def TiledDiffusion(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(122)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # GenerationConfiguration + def DiffusionTileWidth(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(124)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint16Flags, o + self._tab.Pos) + return 16 + + # GenerationConfiguration + def DiffusionTileHeight(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(126)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint16Flags, o + self._tab.Pos) + return 16 + + # GenerationConfiguration + def DiffusionTileOverlap(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(128)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint16Flags, o + self._tab.Pos) + return 2 + + # GenerationConfiguration + def UpscalerScaleFactor(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(130)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def T5TextEncoder(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(132)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return True + + # GenerationConfiguration + def SeparateClipL(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(134)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # GenerationConfiguration + def ClipLText(self) -> Optional[str]: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(136)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # GenerationConfiguration + def SeparateOpenClipG(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(138)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # GenerationConfiguration + def OpenClipGText(self) -> Optional[str]: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(140)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # GenerationConfiguration + def SpeedUpWithGuidanceEmbed(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(142)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return True + + # GenerationConfiguration + def GuidanceEmbed(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(144)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 3.5 + + # GenerationConfiguration + def ResolutionDependentShift(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(146)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return True + + # GenerationConfiguration + def TeaCacheStart(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(148)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos) + return 5 + + # GenerationConfiguration + def TeaCacheEnd(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(150)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos) + return -1 + + # GenerationConfiguration + def TeaCacheThreshold(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(152)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 0.06 + + # GenerationConfiguration + def TeaCache(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(154)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # GenerationConfiguration + def SeparateT5(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(156)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # GenerationConfiguration + def T5Text(self) -> Optional[str]: + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(158)) + if o != 0: + return self._tab.String(o + self._tab.Pos) + return None + + # GenerationConfiguration + def TeaCacheMaxSkipSteps(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(160)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos) + return 3 + + # GenerationConfiguration + def CausalInferenceEnabled(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(162)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # GenerationConfiguration + def CausalInference(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(164)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos) + return 3 + + # GenerationConfiguration + def CausalInferencePad(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(166)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def CfgZeroStar(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(168)) + if o != 0: + return bool(self._tab.Get(flatbuffers.number_types.BoolFlags, o + self._tab.Pos)) + return False + + # GenerationConfiguration + def CfgZeroInitSteps(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(170)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int32Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def CompressionArtifacts(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(172)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int8Flags, o + self._tab.Pos) + return 0 + + # GenerationConfiguration + def CompressionArtifactsQuality(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(174)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Float32Flags, o + self._tab.Pos) + return 43.1 + + # GenerationConfiguration + def ColorCalibration(self): + o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(176)) + if o != 0: + return self._tab.Get(flatbuffers.number_types.Int8Flags, o + self._tab.Pos) + return 0 + +def GenerationConfigurationStart(builder: flatbuffers.Builder): + builder.StartObject(87) + +def GenerationConfigurationAddId(builder: flatbuffers.Builder, id: int): + builder.PrependInt64Slot(0, id, 0) + +def GenerationConfigurationAddStartWidth(builder: flatbuffers.Builder, startWidth: int): + builder.PrependUint16Slot(1, startWidth, 0) + +def GenerationConfigurationAddStartHeight(builder: flatbuffers.Builder, startHeight: int): + builder.PrependUint16Slot(2, startHeight, 0) + +def GenerationConfigurationAddSeed(builder: flatbuffers.Builder, seed: int): + builder.PrependUint32Slot(3, seed, 0) + +def GenerationConfigurationAddSteps(builder: flatbuffers.Builder, steps: int): + builder.PrependUint32Slot(4, steps, 0) + +def GenerationConfigurationAddGuidanceScale(builder: flatbuffers.Builder, guidanceScale: float): + builder.PrependFloat32Slot(5, guidanceScale, 0.0) + +def GenerationConfigurationAddStrength(builder: flatbuffers.Builder, strength: float): + builder.PrependFloat32Slot(6, strength, 0.0) + +def GenerationConfigurationAddModel(builder: flatbuffers.Builder, model: int): + builder.PrependUOffsetTRelativeSlot(7, flatbuffers.number_types.UOffsetTFlags.py_type(model), 0) + +def GenerationConfigurationAddSampler(builder: flatbuffers.Builder, sampler: int): + builder.PrependInt8Slot(8, sampler, 0) + +def GenerationConfigurationAddBatchCount(builder: flatbuffers.Builder, batchCount: int): + builder.PrependUint32Slot(9, batchCount, 1) + +def GenerationConfigurationAddBatchSize(builder: flatbuffers.Builder, batchSize: int): + builder.PrependUint32Slot(10, batchSize, 1) + +def GenerationConfigurationAddHiresFix(builder: flatbuffers.Builder, hiresFix: bool): + builder.PrependBoolSlot(11, hiresFix, 0) + +def GenerationConfigurationAddHiresFixStartWidth(builder: flatbuffers.Builder, hiresFixStartWidth: int): + builder.PrependUint16Slot(12, hiresFixStartWidth, 0) + +def GenerationConfigurationAddHiresFixStartHeight(builder: flatbuffers.Builder, hiresFixStartHeight: int): + builder.PrependUint16Slot(13, hiresFixStartHeight, 0) + +def GenerationConfigurationAddHiresFixStrength(builder: flatbuffers.Builder, hiresFixStrength: float): + builder.PrependFloat32Slot(14, hiresFixStrength, 0.7) + +def GenerationConfigurationAddUpscaler(builder: flatbuffers.Builder, upscaler: int): + builder.PrependUOffsetTRelativeSlot(15, flatbuffers.number_types.UOffsetTFlags.py_type(upscaler), 0) + +def GenerationConfigurationAddImageGuidanceScale(builder: flatbuffers.Builder, imageGuidanceScale: float): + builder.PrependFloat32Slot(16, imageGuidanceScale, 1.5) + +def GenerationConfigurationAddSeedMode(builder: flatbuffers.Builder, seedMode: int): + builder.PrependInt8Slot(17, seedMode, 0) + +def GenerationConfigurationAddClipSkip(builder: flatbuffers.Builder, clipSkip: int): + builder.PrependUint32Slot(18, clipSkip, 1) + +def GenerationConfigurationAddControls(builder: flatbuffers.Builder, controls: int): + builder.PrependUOffsetTRelativeSlot(19, flatbuffers.number_types.UOffsetTFlags.py_type(controls), 0) + +def GenerationConfigurationStartControlsVector(builder, numElems: int) -> int: + return builder.StartVector(4, numElems, 4) + +def GenerationConfigurationAddLoras(builder: flatbuffers.Builder, loras: int): + builder.PrependUOffsetTRelativeSlot(20, flatbuffers.number_types.UOffsetTFlags.py_type(loras), 0) + +def GenerationConfigurationStartLorasVector(builder, numElems: int) -> int: + return builder.StartVector(4, numElems, 4) + +def GenerationConfigurationAddMaskBlur(builder: flatbuffers.Builder, maskBlur: float): + builder.PrependFloat32Slot(21, maskBlur, 0.0) + +def GenerationConfigurationAddFaceRestoration(builder: flatbuffers.Builder, faceRestoration: int): + builder.PrependUOffsetTRelativeSlot(22, flatbuffers.number_types.UOffsetTFlags.py_type(faceRestoration), 0) + +def GenerationConfigurationAddClipWeight(builder: flatbuffers.Builder, clipWeight: float): + builder.PrependFloat32Slot(25, clipWeight, 1.0) + +def GenerationConfigurationAddNegativePromptForImagePrior(builder: flatbuffers.Builder, negativePromptForImagePrior: bool): + builder.PrependBoolSlot(26, negativePromptForImagePrior, 1) + +def GenerationConfigurationAddImagePriorSteps(builder: flatbuffers.Builder, imagePriorSteps: int): + builder.PrependUint32Slot(27, imagePriorSteps, 5) + +def GenerationConfigurationAddRefinerModel(builder: flatbuffers.Builder, refinerModel: int): + builder.PrependUOffsetTRelativeSlot(28, flatbuffers.number_types.UOffsetTFlags.py_type(refinerModel), 0) + +def GenerationConfigurationAddOriginalImageHeight(builder: flatbuffers.Builder, originalImageHeight: int): + builder.PrependUint32Slot(29, originalImageHeight, 0) + +def GenerationConfigurationAddOriginalImageWidth(builder: flatbuffers.Builder, originalImageWidth: int): + builder.PrependUint32Slot(30, originalImageWidth, 0) + +def GenerationConfigurationAddCropTop(builder: flatbuffers.Builder, cropTop: int): + builder.PrependInt32Slot(31, cropTop, 0) + +def GenerationConfigurationAddCropLeft(builder: flatbuffers.Builder, cropLeft: int): + builder.PrependInt32Slot(32, cropLeft, 0) + +def GenerationConfigurationAddTargetImageHeight(builder: flatbuffers.Builder, targetImageHeight: int): + builder.PrependUint32Slot(33, targetImageHeight, 0) + +def GenerationConfigurationAddTargetImageWidth(builder: flatbuffers.Builder, targetImageWidth: int): + builder.PrependUint32Slot(34, targetImageWidth, 0) + +def GenerationConfigurationAddAestheticScore(builder: flatbuffers.Builder, aestheticScore: float): + builder.PrependFloat32Slot(35, aestheticScore, 6.0) + +def GenerationConfigurationAddNegativeAestheticScore(builder: flatbuffers.Builder, negativeAestheticScore: float): + builder.PrependFloat32Slot(36, negativeAestheticScore, 2.5) + +def GenerationConfigurationAddZeroNegativePrompt(builder: flatbuffers.Builder, zeroNegativePrompt: bool): + builder.PrependBoolSlot(37, zeroNegativePrompt, 0) + +def GenerationConfigurationAddRefinerStart(builder: flatbuffers.Builder, refinerStart: float): + builder.PrependFloat32Slot(38, refinerStart, 0.7) + +def GenerationConfigurationAddNegativeOriginalImageHeight(builder: flatbuffers.Builder, negativeOriginalImageHeight: int): + builder.PrependUint32Slot(39, negativeOriginalImageHeight, 0) + +def GenerationConfigurationAddNegativeOriginalImageWidth(builder: flatbuffers.Builder, negativeOriginalImageWidth: int): + builder.PrependUint32Slot(40, negativeOriginalImageWidth, 0) + +def GenerationConfigurationAddName(builder: flatbuffers.Builder, name: int): + builder.PrependUOffsetTRelativeSlot(41, flatbuffers.number_types.UOffsetTFlags.py_type(name), 0) + +def GenerationConfigurationAddFpsId(builder: flatbuffers.Builder, fpsId: int): + builder.PrependUint32Slot(42, fpsId, 5) + +def GenerationConfigurationAddMotionBucketId(builder: flatbuffers.Builder, motionBucketId: int): + builder.PrependUint32Slot(43, motionBucketId, 127) + +def GenerationConfigurationAddCondAug(builder: flatbuffers.Builder, condAug: float): + builder.PrependFloat32Slot(44, condAug, 0.02) + +def GenerationConfigurationAddStartFrameCfg(builder: flatbuffers.Builder, startFrameCfg: float): + builder.PrependFloat32Slot(45, startFrameCfg, 1.0) + +def GenerationConfigurationAddNumFrames(builder: flatbuffers.Builder, numFrames: int): + builder.PrependUint32Slot(46, numFrames, 14) + +def GenerationConfigurationAddMaskBlurOutset(builder: flatbuffers.Builder, maskBlurOutset: int): + builder.PrependInt32Slot(47, maskBlurOutset, 0) + +def GenerationConfigurationAddSharpness(builder: flatbuffers.Builder, sharpness: float): + builder.PrependFloat32Slot(48, sharpness, 0.0) + +def GenerationConfigurationAddShift(builder: flatbuffers.Builder, shift: float): + builder.PrependFloat32Slot(49, shift, 1.0) + +def GenerationConfigurationAddStage2Steps(builder: flatbuffers.Builder, stage2Steps: int): + builder.PrependUint32Slot(50, stage2Steps, 10) + +def GenerationConfigurationAddStage2Cfg(builder: flatbuffers.Builder, stage2Cfg: float): + builder.PrependFloat32Slot(51, stage2Cfg, 1.0) + +def GenerationConfigurationAddStage2Shift(builder: flatbuffers.Builder, stage2Shift: float): + builder.PrependFloat32Slot(52, stage2Shift, 1.0) + +def GenerationConfigurationAddTiledDecoding(builder: flatbuffers.Builder, tiledDecoding: bool): + builder.PrependBoolSlot(53, tiledDecoding, 0) + +def GenerationConfigurationAddDecodingTileWidth(builder: flatbuffers.Builder, decodingTileWidth: int): + builder.PrependUint16Slot(54, decodingTileWidth, 10) + +def GenerationConfigurationAddDecodingTileHeight(builder: flatbuffers.Builder, decodingTileHeight: int): + builder.PrependUint16Slot(55, decodingTileHeight, 10) + +def GenerationConfigurationAddDecodingTileOverlap(builder: flatbuffers.Builder, decodingTileOverlap: int): + builder.PrependUint16Slot(56, decodingTileOverlap, 2) + +def GenerationConfigurationAddStochasticSamplingGamma(builder: flatbuffers.Builder, stochasticSamplingGamma: float): + builder.PrependFloat32Slot(57, stochasticSamplingGamma, 0.3) + +def GenerationConfigurationAddPreserveOriginalAfterInpaint(builder: flatbuffers.Builder, preserveOriginalAfterInpaint: bool): + builder.PrependBoolSlot(58, preserveOriginalAfterInpaint, 1) + +def GenerationConfigurationAddTiledDiffusion(builder: flatbuffers.Builder, tiledDiffusion: bool): + builder.PrependBoolSlot(59, tiledDiffusion, 0) + +def GenerationConfigurationAddDiffusionTileWidth(builder: flatbuffers.Builder, diffusionTileWidth: int): + builder.PrependUint16Slot(60, diffusionTileWidth, 16) + +def GenerationConfigurationAddDiffusionTileHeight(builder: flatbuffers.Builder, diffusionTileHeight: int): + builder.PrependUint16Slot(61, diffusionTileHeight, 16) + +def GenerationConfigurationAddDiffusionTileOverlap(builder: flatbuffers.Builder, diffusionTileOverlap: int): + builder.PrependUint16Slot(62, diffusionTileOverlap, 2) + +def GenerationConfigurationAddUpscalerScaleFactor(builder: flatbuffers.Builder, upscalerScaleFactor: int): + builder.PrependUint8Slot(63, upscalerScaleFactor, 0) + +def GenerationConfigurationAddT5TextEncoder(builder: flatbuffers.Builder, t5TextEncoder: bool): + builder.PrependBoolSlot(64, t5TextEncoder, 1) + +def GenerationConfigurationAddSeparateClipL(builder: flatbuffers.Builder, separateClipL: bool): + builder.PrependBoolSlot(65, separateClipL, 0) + +def GenerationConfigurationAddClipLText(builder: flatbuffers.Builder, clipLText: int): + builder.PrependUOffsetTRelativeSlot(66, flatbuffers.number_types.UOffsetTFlags.py_type(clipLText), 0) + +def GenerationConfigurationAddSeparateOpenClipG(builder: flatbuffers.Builder, separateOpenClipG: bool): + builder.PrependBoolSlot(67, separateOpenClipG, 0) + +def GenerationConfigurationAddOpenClipGText(builder: flatbuffers.Builder, openClipGText: int): + builder.PrependUOffsetTRelativeSlot(68, flatbuffers.number_types.UOffsetTFlags.py_type(openClipGText), 0) + +def GenerationConfigurationAddSpeedUpWithGuidanceEmbed(builder: flatbuffers.Builder, speedUpWithGuidanceEmbed: bool): + builder.PrependBoolSlot(69, speedUpWithGuidanceEmbed, 1) + +def GenerationConfigurationAddGuidanceEmbed(builder: flatbuffers.Builder, guidanceEmbed: float): + builder.PrependFloat32Slot(70, guidanceEmbed, 3.5) + +def GenerationConfigurationAddResolutionDependentShift(builder: flatbuffers.Builder, resolutionDependentShift: bool): + builder.PrependBoolSlot(71, resolutionDependentShift, 1) + +def GenerationConfigurationAddTeaCacheStart(builder: flatbuffers.Builder, teaCacheStart: int): + builder.PrependInt32Slot(72, teaCacheStart, 5) + +def GenerationConfigurationAddTeaCacheEnd(builder: flatbuffers.Builder, teaCacheEnd: int): + builder.PrependInt32Slot(73, teaCacheEnd, -1) + +def GenerationConfigurationAddTeaCacheThreshold(builder: flatbuffers.Builder, teaCacheThreshold: float): + builder.PrependFloat32Slot(74, teaCacheThreshold, 0.06) + +def GenerationConfigurationAddTeaCache(builder: flatbuffers.Builder, teaCache: bool): + builder.PrependBoolSlot(75, teaCache, 0) + +def GenerationConfigurationAddSeparateT5(builder: flatbuffers.Builder, separateT5: bool): + builder.PrependBoolSlot(76, separateT5, 0) + +def GenerationConfigurationAddT5Text(builder: flatbuffers.Builder, t5Text: int): + builder.PrependUOffsetTRelativeSlot(77, flatbuffers.number_types.UOffsetTFlags.py_type(t5Text), 0) + +def GenerationConfigurationAddTeaCacheMaxSkipSteps(builder: flatbuffers.Builder, teaCacheMaxSkipSteps: int): + builder.PrependInt32Slot(78, teaCacheMaxSkipSteps, 3) + +def GenerationConfigurationAddCausalInferenceEnabled(builder: flatbuffers.Builder, causalInferenceEnabled: bool): + builder.PrependBoolSlot(79, causalInferenceEnabled, 0) + +def GenerationConfigurationAddCausalInference(builder: flatbuffers.Builder, causalInference: int): + builder.PrependInt32Slot(80, causalInference, 3) + +def GenerationConfigurationAddCausalInferencePad(builder: flatbuffers.Builder, causalInferencePad: int): + builder.PrependInt32Slot(81, causalInferencePad, 0) + +def GenerationConfigurationAddCfgZeroStar(builder: flatbuffers.Builder, cfgZeroStar: bool): + builder.PrependBoolSlot(82, cfgZeroStar, 0) + +def GenerationConfigurationAddCfgZeroInitSteps(builder: flatbuffers.Builder, cfgZeroInitSteps: int): + builder.PrependInt32Slot(83, cfgZeroInitSteps, 0) + +def GenerationConfigurationAddCompressionArtifacts(builder: flatbuffers.Builder, compressionArtifacts: int): + builder.PrependInt8Slot(84, compressionArtifacts, 0) + +def GenerationConfigurationAddCompressionArtifactsQuality(builder: flatbuffers.Builder, compressionArtifactsQuality: float): + builder.PrependFloat32Slot(85, compressionArtifactsQuality, 43.1) + +def GenerationConfigurationAddColorCalibration(builder: flatbuffers.Builder, colorCalibration: int): + builder.PrependInt8Slot(86, colorCalibration, 0) + +def GenerationConfigurationEnd(builder: flatbuffers.Builder) -> int: + return builder.EndObject() + + +try: + from typing import List +except: + pass + +class GenerationConfigurationT(object): + + # GenerationConfigurationT + def __init__( + self, + id = 0, + startWidth = 0, + startHeight = 0, + seed = 0, + steps = 0, + guidanceScale = 0.0, + strength = 0.0, + model = None, + sampler = 0, + batchCount = 1, + batchSize = 1, + hiresFix = False, + hiresFixStartWidth = 0, + hiresFixStartHeight = 0, + hiresFixStrength = 0.7, + upscaler = None, + imageGuidanceScale = 1.5, + seedMode = 0, + clipSkip = 1, + controls = None, + loras = None, + maskBlur = 0.0, + faceRestoration = None, + clipWeight = 1.0, + negativePromptForImagePrior = True, + imagePriorSteps = 5, + refinerModel = None, + originalImageHeight = 0, + originalImageWidth = 0, + cropTop = 0, + cropLeft = 0, + targetImageHeight = 0, + targetImageWidth = 0, + aestheticScore = 6.0, + negativeAestheticScore = 2.5, + zeroNegativePrompt = False, + refinerStart = 0.7, + negativeOriginalImageHeight = 0, + negativeOriginalImageWidth = 0, + name = None, + fpsId = 5, + motionBucketId = 127, + condAug = 0.02, + startFrameCfg = 1.0, + numFrames = 14, + maskBlurOutset = 0, + sharpness = 0.0, + shift = 1.0, + stage2Steps = 10, + stage2Cfg = 1.0, + stage2Shift = 1.0, + tiledDecoding = False, + decodingTileWidth = 10, + decodingTileHeight = 10, + decodingTileOverlap = 2, + stochasticSamplingGamma = 0.3, + preserveOriginalAfterInpaint = True, + tiledDiffusion = False, + diffusionTileWidth = 16, + diffusionTileHeight = 16, + diffusionTileOverlap = 2, + upscalerScaleFactor = 0, + t5TextEncoder = True, + separateClipL = False, + clipLText = None, + separateOpenClipG = False, + openClipGText = None, + speedUpWithGuidanceEmbed = True, + guidanceEmbed = 3.5, + resolutionDependentShift = True, + teaCacheStart = 5, + teaCacheEnd = -1, + teaCacheThreshold = 0.06, + teaCache = False, + separateT5 = False, + t5Text = None, + teaCacheMaxSkipSteps = 3, + causalInferenceEnabled = False, + causalInference = 3, + causalInferencePad = 0, + cfgZeroStar = False, + cfgZeroInitSteps = 0, + compressionArtifacts = 0, + compressionArtifactsQuality = 43.1, + colorCalibration = 0, + ): + self.id = id # type: int + self.startWidth = startWidth # type: int + self.startHeight = startHeight # type: int + self.seed = seed # type: int + self.steps = steps # type: int + self.guidanceScale = guidanceScale # type: float + self.strength = strength # type: float + self.model = model # type: Optional[str] + self.sampler = sampler # type: int + self.batchCount = batchCount # type: int + self.batchSize = batchSize # type: int + self.hiresFix = hiresFix # type: bool + self.hiresFixStartWidth = hiresFixStartWidth # type: int + self.hiresFixStartHeight = hiresFixStartHeight # type: int + self.hiresFixStrength = hiresFixStrength # type: float + self.upscaler = upscaler # type: Optional[str] + self.imageGuidanceScale = imageGuidanceScale # type: float + self.seedMode = seedMode # type: int + self.clipSkip = clipSkip # type: int + self.controls = controls # type: Optional[List[ControlT]] + self.loras = loras # type: Optional[List[LoRAT]] + self.maskBlur = maskBlur # type: float + self.faceRestoration = faceRestoration # type: Optional[str] + self.clipWeight = clipWeight # type: float + self.negativePromptForImagePrior = negativePromptForImagePrior # type: bool + self.imagePriorSteps = imagePriorSteps # type: int + self.refinerModel = refinerModel # type: Optional[str] + self.originalImageHeight = originalImageHeight # type: int + self.originalImageWidth = originalImageWidth # type: int + self.cropTop = cropTop # type: int + self.cropLeft = cropLeft # type: int + self.targetImageHeight = targetImageHeight # type: int + self.targetImageWidth = targetImageWidth # type: int + self.aestheticScore = aestheticScore # type: float + self.negativeAestheticScore = negativeAestheticScore # type: float + self.zeroNegativePrompt = zeroNegativePrompt # type: bool + self.refinerStart = refinerStart # type: float + self.negativeOriginalImageHeight = negativeOriginalImageHeight # type: int + self.negativeOriginalImageWidth = negativeOriginalImageWidth # type: int + self.name = name # type: Optional[str] + self.fpsId = fpsId # type: int + self.motionBucketId = motionBucketId # type: int + self.condAug = condAug # type: float + self.startFrameCfg = startFrameCfg # type: float + self.numFrames = numFrames # type: int + self.maskBlurOutset = maskBlurOutset # type: int + self.sharpness = sharpness # type: float + self.shift = shift # type: float + self.stage2Steps = stage2Steps # type: int + self.stage2Cfg = stage2Cfg # type: float + self.stage2Shift = stage2Shift # type: float + self.tiledDecoding = tiledDecoding # type: bool + self.decodingTileWidth = decodingTileWidth # type: int + self.decodingTileHeight = decodingTileHeight # type: int + self.decodingTileOverlap = decodingTileOverlap # type: int + self.stochasticSamplingGamma = stochasticSamplingGamma # type: float + self.preserveOriginalAfterInpaint = preserveOriginalAfterInpaint # type: bool + self.tiledDiffusion = tiledDiffusion # type: bool + self.diffusionTileWidth = diffusionTileWidth # type: int + self.diffusionTileHeight = diffusionTileHeight # type: int + self.diffusionTileOverlap = diffusionTileOverlap # type: int + self.upscalerScaleFactor = upscalerScaleFactor # type: int + self.t5TextEncoder = t5TextEncoder # type: bool + self.separateClipL = separateClipL # type: bool + self.clipLText = clipLText # type: Optional[str] + self.separateOpenClipG = separateOpenClipG # type: bool + self.openClipGText = openClipGText # type: Optional[str] + self.speedUpWithGuidanceEmbed = speedUpWithGuidanceEmbed # type: bool + self.guidanceEmbed = guidanceEmbed # type: float + self.resolutionDependentShift = resolutionDependentShift # type: bool + self.teaCacheStart = teaCacheStart # type: int + self.teaCacheEnd = teaCacheEnd # type: int + self.teaCacheThreshold = teaCacheThreshold # type: float + self.teaCache = teaCache # type: bool + self.separateT5 = separateT5 # type: bool + self.t5Text = t5Text # type: Optional[str] + self.teaCacheMaxSkipSteps = teaCacheMaxSkipSteps # type: int + self.causalInferenceEnabled = causalInferenceEnabled # type: bool + self.causalInference = causalInference # type: int + self.causalInferencePad = causalInferencePad # type: int + self.cfgZeroStar = cfgZeroStar # type: bool + self.cfgZeroInitSteps = cfgZeroInitSteps # type: int + self.compressionArtifacts = compressionArtifacts # type: int + self.compressionArtifactsQuality = compressionArtifactsQuality # type: float + self.colorCalibration = colorCalibration # type: int + + @classmethod + def InitFromBuf(cls, buf, pos): + generationConfiguration = GenerationConfiguration() + generationConfiguration.Init(buf, pos) + return cls.InitFromObj(generationConfiguration) + + @classmethod + def InitFromPackedBuf(cls, buf, pos=0): + n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, pos) + return cls.InitFromBuf(buf, pos+n) + + @classmethod + def InitFromObj(cls, generationConfiguration): + x = GenerationConfigurationT() + x._UnPack(generationConfiguration) + return x + + # GenerationConfigurationT + def _UnPack(self, generationConfiguration): + if generationConfiguration is None: + return + self.id = generationConfiguration.Id() + self.startWidth = generationConfiguration.StartWidth() + self.startHeight = generationConfiguration.StartHeight() + self.seed = generationConfiguration.Seed() + self.steps = generationConfiguration.Steps() + self.guidanceScale = generationConfiguration.GuidanceScale() + self.strength = generationConfiguration.Strength() + self.model = generationConfiguration.Model() + self.sampler = generationConfiguration.Sampler() + self.batchCount = generationConfiguration.BatchCount() + self.batchSize = generationConfiguration.BatchSize() + self.hiresFix = generationConfiguration.HiresFix() + self.hiresFixStartWidth = generationConfiguration.HiresFixStartWidth() + self.hiresFixStartHeight = generationConfiguration.HiresFixStartHeight() + self.hiresFixStrength = generationConfiguration.HiresFixStrength() + self.upscaler = generationConfiguration.Upscaler() + self.imageGuidanceScale = generationConfiguration.ImageGuidanceScale() + self.seedMode = generationConfiguration.SeedMode() + self.clipSkip = generationConfiguration.ClipSkip() + if not generationConfiguration.ControlsIsNone(): + self.controls = [] + for i in range(generationConfiguration.ControlsLength()): + if generationConfiguration.Controls(i) is None: + self.controls.append(None) + else: + control_ = ControlT.InitFromObj(generationConfiguration.Controls(i)) + self.controls.append(control_) + if not generationConfiguration.LorasIsNone(): + self.loras = [] + for i in range(generationConfiguration.LorasLength()): + if generationConfiguration.Loras(i) is None: + self.loras.append(None) + else: + loRA_ = LoRAT.InitFromObj(generationConfiguration.Loras(i)) + self.loras.append(loRA_) + self.maskBlur = generationConfiguration.MaskBlur() + self.faceRestoration = generationConfiguration.FaceRestoration() + self.clipWeight = generationConfiguration.ClipWeight() + self.negativePromptForImagePrior = generationConfiguration.NegativePromptForImagePrior() + self.imagePriorSteps = generationConfiguration.ImagePriorSteps() + self.refinerModel = generationConfiguration.RefinerModel() + self.originalImageHeight = generationConfiguration.OriginalImageHeight() + self.originalImageWidth = generationConfiguration.OriginalImageWidth() + self.cropTop = generationConfiguration.CropTop() + self.cropLeft = generationConfiguration.CropLeft() + self.targetImageHeight = generationConfiguration.TargetImageHeight() + self.targetImageWidth = generationConfiguration.TargetImageWidth() + self.aestheticScore = generationConfiguration.AestheticScore() + self.negativeAestheticScore = generationConfiguration.NegativeAestheticScore() + self.zeroNegativePrompt = generationConfiguration.ZeroNegativePrompt() + self.refinerStart = generationConfiguration.RefinerStart() + self.negativeOriginalImageHeight = generationConfiguration.NegativeOriginalImageHeight() + self.negativeOriginalImageWidth = generationConfiguration.NegativeOriginalImageWidth() + self.name = generationConfiguration.Name() + self.fpsId = generationConfiguration.FpsId() + self.motionBucketId = generationConfiguration.MotionBucketId() + self.condAug = generationConfiguration.CondAug() + self.startFrameCfg = generationConfiguration.StartFrameCfg() + self.numFrames = generationConfiguration.NumFrames() + self.maskBlurOutset = generationConfiguration.MaskBlurOutset() + self.sharpness = generationConfiguration.Sharpness() + self.shift = generationConfiguration.Shift() + self.stage2Steps = generationConfiguration.Stage2Steps() + self.stage2Cfg = generationConfiguration.Stage2Cfg() + self.stage2Shift = generationConfiguration.Stage2Shift() + self.tiledDecoding = generationConfiguration.TiledDecoding() + self.decodingTileWidth = generationConfiguration.DecodingTileWidth() + self.decodingTileHeight = generationConfiguration.DecodingTileHeight() + self.decodingTileOverlap = generationConfiguration.DecodingTileOverlap() + self.stochasticSamplingGamma = generationConfiguration.StochasticSamplingGamma() + self.preserveOriginalAfterInpaint = generationConfiguration.PreserveOriginalAfterInpaint() + self.tiledDiffusion = generationConfiguration.TiledDiffusion() + self.diffusionTileWidth = generationConfiguration.DiffusionTileWidth() + self.diffusionTileHeight = generationConfiguration.DiffusionTileHeight() + self.diffusionTileOverlap = generationConfiguration.DiffusionTileOverlap() + self.upscalerScaleFactor = generationConfiguration.UpscalerScaleFactor() + self.t5TextEncoder = generationConfiguration.T5TextEncoder() + self.separateClipL = generationConfiguration.SeparateClipL() + self.clipLText = generationConfiguration.ClipLText() + self.separateOpenClipG = generationConfiguration.SeparateOpenClipG() + self.openClipGText = generationConfiguration.OpenClipGText() + self.speedUpWithGuidanceEmbed = generationConfiguration.SpeedUpWithGuidanceEmbed() + self.guidanceEmbed = generationConfiguration.GuidanceEmbed() + self.resolutionDependentShift = generationConfiguration.ResolutionDependentShift() + self.teaCacheStart = generationConfiguration.TeaCacheStart() + self.teaCacheEnd = generationConfiguration.TeaCacheEnd() + self.teaCacheThreshold = generationConfiguration.TeaCacheThreshold() + self.teaCache = generationConfiguration.TeaCache() + self.separateT5 = generationConfiguration.SeparateT5() + self.t5Text = generationConfiguration.T5Text() + self.teaCacheMaxSkipSteps = generationConfiguration.TeaCacheMaxSkipSteps() + self.causalInferenceEnabled = generationConfiguration.CausalInferenceEnabled() + self.causalInference = generationConfiguration.CausalInference() + self.causalInferencePad = generationConfiguration.CausalInferencePad() + self.cfgZeroStar = generationConfiguration.CfgZeroStar() + self.cfgZeroInitSteps = generationConfiguration.CfgZeroInitSteps() + self.compressionArtifacts = generationConfiguration.CompressionArtifacts() + self.compressionArtifactsQuality = generationConfiguration.CompressionArtifactsQuality() + self.colorCalibration = generationConfiguration.ColorCalibration() + + # GenerationConfigurationT + def Pack(self, builder): + if self.model is not None: + model = builder.CreateString(self.model) + if self.upscaler is not None: + upscaler = builder.CreateString(self.upscaler) + if self.controls is not None: + controlslist = [] + for i in range(len(self.controls)): + controlslist.append(self.controls[i].Pack(builder)) + GenerationConfigurationStartControlsVector(builder, len(self.controls)) + for i in reversed(range(len(self.controls))): + builder.PrependUOffsetTRelative(controlslist[i]) + controls = builder.EndVector() + if self.loras is not None: + loraslist = [] + for i in range(len(self.loras)): + loraslist.append(self.loras[i].Pack(builder)) + GenerationConfigurationStartLorasVector(builder, len(self.loras)) + for i in reversed(range(len(self.loras))): + builder.PrependUOffsetTRelative(loraslist[i]) + loras = builder.EndVector() + if self.faceRestoration is not None: + faceRestoration = builder.CreateString(self.faceRestoration) + if self.refinerModel is not None: + refinerModel = builder.CreateString(self.refinerModel) + if self.name is not None: + name = builder.CreateString(self.name) + if self.clipLText is not None: + clipLText = builder.CreateString(self.clipLText) + if self.openClipGText is not None: + openClipGText = builder.CreateString(self.openClipGText) + if self.t5Text is not None: + t5Text = builder.CreateString(self.t5Text) + GenerationConfigurationStart(builder) + GenerationConfigurationAddId(builder, self.id) + GenerationConfigurationAddStartWidth(builder, self.startWidth) + GenerationConfigurationAddStartHeight(builder, self.startHeight) + GenerationConfigurationAddSeed(builder, self.seed) + GenerationConfigurationAddSteps(builder, self.steps) + GenerationConfigurationAddGuidanceScale(builder, self.guidanceScale) + GenerationConfigurationAddStrength(builder, self.strength) + if self.model is not None: + GenerationConfigurationAddModel(builder, model) + GenerationConfigurationAddSampler(builder, self.sampler) + GenerationConfigurationAddBatchCount(builder, self.batchCount) + GenerationConfigurationAddBatchSize(builder, self.batchSize) + GenerationConfigurationAddHiresFix(builder, self.hiresFix) + GenerationConfigurationAddHiresFixStartWidth(builder, self.hiresFixStartWidth) + GenerationConfigurationAddHiresFixStartHeight(builder, self.hiresFixStartHeight) + GenerationConfigurationAddHiresFixStrength(builder, self.hiresFixStrength) + if self.upscaler is not None: + GenerationConfigurationAddUpscaler(builder, upscaler) + GenerationConfigurationAddImageGuidanceScale(builder, self.imageGuidanceScale) + GenerationConfigurationAddSeedMode(builder, self.seedMode) + GenerationConfigurationAddClipSkip(builder, self.clipSkip) + if self.controls is not None: + GenerationConfigurationAddControls(builder, controls) + if self.loras is not None: + GenerationConfigurationAddLoras(builder, loras) + GenerationConfigurationAddMaskBlur(builder, self.maskBlur) + if self.faceRestoration is not None: + GenerationConfigurationAddFaceRestoration(builder, faceRestoration) + GenerationConfigurationAddClipWeight(builder, self.clipWeight) + GenerationConfigurationAddNegativePromptForImagePrior(builder, self.negativePromptForImagePrior) + GenerationConfigurationAddImagePriorSteps(builder, self.imagePriorSteps) + if self.refinerModel is not None: + GenerationConfigurationAddRefinerModel(builder, refinerModel) + GenerationConfigurationAddOriginalImageHeight(builder, self.originalImageHeight) + GenerationConfigurationAddOriginalImageWidth(builder, self.originalImageWidth) + GenerationConfigurationAddCropTop(builder, self.cropTop) + GenerationConfigurationAddCropLeft(builder, self.cropLeft) + GenerationConfigurationAddTargetImageHeight(builder, self.targetImageHeight) + GenerationConfigurationAddTargetImageWidth(builder, self.targetImageWidth) + GenerationConfigurationAddAestheticScore(builder, self.aestheticScore) + GenerationConfigurationAddNegativeAestheticScore(builder, self.negativeAestheticScore) + GenerationConfigurationAddZeroNegativePrompt(builder, self.zeroNegativePrompt) + GenerationConfigurationAddRefinerStart(builder, self.refinerStart) + GenerationConfigurationAddNegativeOriginalImageHeight(builder, self.negativeOriginalImageHeight) + GenerationConfigurationAddNegativeOriginalImageWidth(builder, self.negativeOriginalImageWidth) + if self.name is not None: + GenerationConfigurationAddName(builder, name) + GenerationConfigurationAddFpsId(builder, self.fpsId) + GenerationConfigurationAddMotionBucketId(builder, self.motionBucketId) + GenerationConfigurationAddCondAug(builder, self.condAug) + GenerationConfigurationAddStartFrameCfg(builder, self.startFrameCfg) + GenerationConfigurationAddNumFrames(builder, self.numFrames) + GenerationConfigurationAddMaskBlurOutset(builder, self.maskBlurOutset) + GenerationConfigurationAddSharpness(builder, self.sharpness) + GenerationConfigurationAddShift(builder, self.shift) + GenerationConfigurationAddStage2Steps(builder, self.stage2Steps) + GenerationConfigurationAddStage2Cfg(builder, self.stage2Cfg) + GenerationConfigurationAddStage2Shift(builder, self.stage2Shift) + GenerationConfigurationAddTiledDecoding(builder, self.tiledDecoding) + GenerationConfigurationAddDecodingTileWidth(builder, self.decodingTileWidth) + GenerationConfigurationAddDecodingTileHeight(builder, self.decodingTileHeight) + GenerationConfigurationAddDecodingTileOverlap(builder, self.decodingTileOverlap) + GenerationConfigurationAddStochasticSamplingGamma(builder, self.stochasticSamplingGamma) + GenerationConfigurationAddPreserveOriginalAfterInpaint(builder, self.preserveOriginalAfterInpaint) + GenerationConfigurationAddTiledDiffusion(builder, self.tiledDiffusion) + GenerationConfigurationAddDiffusionTileWidth(builder, self.diffusionTileWidth) + GenerationConfigurationAddDiffusionTileHeight(builder, self.diffusionTileHeight) + GenerationConfigurationAddDiffusionTileOverlap(builder, self.diffusionTileOverlap) + GenerationConfigurationAddUpscalerScaleFactor(builder, self.upscalerScaleFactor) + GenerationConfigurationAddT5TextEncoder(builder, self.t5TextEncoder) + GenerationConfigurationAddSeparateClipL(builder, self.separateClipL) + if self.clipLText is not None: + GenerationConfigurationAddClipLText(builder, clipLText) + GenerationConfigurationAddSeparateOpenClipG(builder, self.separateOpenClipG) + if self.openClipGText is not None: + GenerationConfigurationAddOpenClipGText(builder, openClipGText) + GenerationConfigurationAddSpeedUpWithGuidanceEmbed(builder, self.speedUpWithGuidanceEmbed) + GenerationConfigurationAddGuidanceEmbed(builder, self.guidanceEmbed) + GenerationConfigurationAddResolutionDependentShift(builder, self.resolutionDependentShift) + GenerationConfigurationAddTeaCacheStart(builder, self.teaCacheStart) + GenerationConfigurationAddTeaCacheEnd(builder, self.teaCacheEnd) + GenerationConfigurationAddTeaCacheThreshold(builder, self.teaCacheThreshold) + GenerationConfigurationAddTeaCache(builder, self.teaCache) + GenerationConfigurationAddSeparateT5(builder, self.separateT5) + if self.t5Text is not None: + GenerationConfigurationAddT5Text(builder, t5Text) + GenerationConfigurationAddTeaCacheMaxSkipSteps(builder, self.teaCacheMaxSkipSteps) + GenerationConfigurationAddCausalInferenceEnabled(builder, self.causalInferenceEnabled) + GenerationConfigurationAddCausalInference(builder, self.causalInference) + GenerationConfigurationAddCausalInferencePad(builder, self.causalInferencePad) + GenerationConfigurationAddCfgZeroStar(builder, self.cfgZeroStar) + GenerationConfigurationAddCfgZeroInitSteps(builder, self.cfgZeroInitSteps) + GenerationConfigurationAddCompressionArtifacts(builder, self.compressionArtifacts) + GenerationConfigurationAddCompressionArtifactsQuality(builder, self.compressionArtifactsQuality) + GenerationConfigurationAddColorCalibration(builder, self.colorCalibration) + generationConfiguration = GenerationConfigurationEnd(builder) + return generationConfiguration + + diff --git a/CLI/macos/drawthings/generated/config_generated.pyi b/CLI/macos/drawthings/generated/config_generated.pyi new file mode 100644 index 000000000..4569c15f4 --- /dev/null +++ b/CLI/macos/drawthings/generated/config_generated.pyi @@ -0,0 +1,534 @@ +from __future__ import annotations + +import flatbuffers +import numpy as np + +import typing +from typing import cast + +uoffset: typing.TypeAlias = flatbuffers.number_types.UOffsetTFlags.py_type + +class SamplerType(object): + DPMPP2MKarras = cast(int, ...) + EulerA = cast(int, ...) + DDIM = cast(int, ...) + PLMS = cast(int, ...) + DPMPPSDEKarras = cast(int, ...) + UniPC = cast(int, ...) + LCM = cast(int, ...) + EulerASubstep = cast(int, ...) + DPMPPSDESubstep = cast(int, ...) + TCD = cast(int, ...) + EulerATrailing = cast(int, ...) + DPMPPSDETrailing = cast(int, ...) + DPMPP2MAYS = cast(int, ...) + EulerAAYS = cast(int, ...) + DPMPPSDEAYS = cast(int, ...) + DPMPP2MTrailing = cast(int, ...) + DDIMTrailing = cast(int, ...) + UniPCTrailing = cast(int, ...) + UniPCAYS = cast(int, ...) + TCDTrailing = cast(int, ...) +class SeedMode(object): + Legacy = cast(int, ...) + TorchCpuCompatible = cast(int, ...) + ScaleAlike = cast(int, ...) + NvidiaGpuCompatible = cast(int, ...) +class ControlMode(object): + Balanced = cast(int, ...) + Prompt = cast(int, ...) + Control = cast(int, ...) +class ControlInputType(object): + Unspecified = cast(int, ...) + Custom = cast(int, ...) + Depth = cast(int, ...) + Canny = cast(int, ...) + Scribble = cast(int, ...) + Pose = cast(int, ...) + Normalbae = cast(int, ...) + Color = cast(int, ...) + Lineart = cast(int, ...) + Softedge = cast(int, ...) + Seg = cast(int, ...) + Inpaint = cast(int, ...) + Ip2p = cast(int, ...) + Shuffle = cast(int, ...) + Mlsd = cast(int, ...) + Tile = cast(int, ...) + Blur = cast(int, ...) + Lowquality = cast(int, ...) + Gray = cast(int, ...) +class LoRAMode(object): + All = cast(int, ...) + Base = cast(int, ...) + Refiner = cast(int, ...) +class CompressionMethod(object): + Disabled = cast(int, ...) + H264 = cast(int, ...) + H265 = cast(int, ...) + Jpeg = cast(int, ...) +class ColorCalibration(object): + Disabled = cast(int, ...) + Lab = cast(int, ...) +class Control(object): + @classmethod + def GetRootAs(cls, buf: bytes, offset: int) -> Control: ... + @classmethod + def GetRootAsControl(cls, buf: bytes, offset: int) -> Control: ... + def Init(self, buf: bytes, pos: int) -> None: ... + def File(self) -> str | None: ... + def Weight(self) -> float: ... + def GuidanceStart(self) -> float: ... + def GuidanceEnd(self) -> float: ... + def NoPrompt(self) -> bool: ... + def GlobalAveragePooling(self) -> bool: ... + def DownSamplingRate(self) -> float: ... + def ControlMode(self) -> typing.Literal[ControlMode.Balanced, ControlMode.Prompt, ControlMode.Control]: ... + def TargetBlocks(self, i: int) -> str: ... + def TargetBlocksLength(self) -> int: ... + def TargetBlocksIsNone(self) -> bool: ... + def InputOverride(self) -> typing.Literal[ControlInputType.Unspecified, ControlInputType.Custom, ControlInputType.Depth, ControlInputType.Canny, ControlInputType.Scribble, ControlInputType.Pose, ControlInputType.Normalbae, ControlInputType.Color, ControlInputType.Lineart, ControlInputType.Softedge, ControlInputType.Seg, ControlInputType.Inpaint, ControlInputType.Ip2p, ControlInputType.Shuffle, ControlInputType.Mlsd, ControlInputType.Tile, ControlInputType.Blur, ControlInputType.Lowquality, ControlInputType.Gray]: ... +class ControlT(object): + file: str | None + weight: float + guidanceStart: float + guidanceEnd: float + noPrompt: bool + globalAveragePooling: bool + downSamplingRate: float + controlMode: typing.Literal[ControlMode.Balanced, ControlMode.Prompt, ControlMode.Control] + targetBlocks: typing.List[str] + inputOverride: typing.Literal[ControlInputType.Unspecified, ControlInputType.Custom, ControlInputType.Depth, ControlInputType.Canny, ControlInputType.Scribble, ControlInputType.Pose, ControlInputType.Normalbae, ControlInputType.Color, ControlInputType.Lineart, ControlInputType.Softedge, ControlInputType.Seg, ControlInputType.Inpaint, ControlInputType.Ip2p, ControlInputType.Shuffle, ControlInputType.Mlsd, ControlInputType.Tile, ControlInputType.Blur, ControlInputType.Lowquality, ControlInputType.Gray] + def __init__( + self, + file: str | None = ..., + weight: float = ..., + guidanceStart: float = ..., + guidanceEnd: float = ..., + noPrompt: bool = ..., + globalAveragePooling: bool = ..., + downSamplingRate: float = ..., + controlMode: typing.Literal[ControlMode.Balanced, ControlMode.Prompt, ControlMode.Control] = ..., + targetBlocks: typing.List[str] | None = ..., + inputOverride: typing.Literal[ControlInputType.Unspecified, ControlInputType.Custom, ControlInputType.Depth, ControlInputType.Canny, ControlInputType.Scribble, ControlInputType.Pose, ControlInputType.Normalbae, ControlInputType.Color, ControlInputType.Lineart, ControlInputType.Softedge, ControlInputType.Seg, ControlInputType.Inpaint, ControlInputType.Ip2p, ControlInputType.Shuffle, ControlInputType.Mlsd, ControlInputType.Tile, ControlInputType.Blur, ControlInputType.Lowquality, ControlInputType.Gray] = ..., + ) -> None: ... + @classmethod + def InitFromBuf(cls, buf: bytes, pos: int) -> ControlT: ... + @classmethod + def InitFromPackedBuf(cls, buf: bytes, pos: int = 0) -> ControlT: ... + @classmethod + def InitFromObj(cls, control: Control) -> ControlT: ... + def _UnPack(self, control: Control) -> None: ... + def Pack(self, builder: flatbuffers.Builder) -> None: ... +def ControlStart(builder: flatbuffers.Builder) -> None: ... +def ControlAddFile(builder: flatbuffers.Builder, file: uoffset) -> None: ... +def ControlAddWeight(builder: flatbuffers.Builder, weight: float) -> None: ... +def ControlAddGuidanceStart(builder: flatbuffers.Builder, guidanceStart: float) -> None: ... +def ControlAddGuidanceEnd(builder: flatbuffers.Builder, guidanceEnd: float) -> None: ... +def ControlAddNoPrompt(builder: flatbuffers.Builder, noPrompt: bool) -> None: ... +def ControlAddGlobalAveragePooling(builder: flatbuffers.Builder, globalAveragePooling: bool) -> None: ... +def ControlAddDownSamplingRate(builder: flatbuffers.Builder, downSamplingRate: float) -> None: ... +def ControlAddControlMode(builder: flatbuffers.Builder, controlMode: typing.Literal[ControlMode.Balanced, ControlMode.Prompt, ControlMode.Control]) -> None: ... +def ControlAddTargetBlocks(builder: flatbuffers.Builder, targetBlocks: uoffset) -> None: ... +def ControlStartTargetBlocksVector(builder: flatbuffers.Builder, num_elems: int) -> uoffset: ... +def ControlAddInputOverride(builder: flatbuffers.Builder, inputOverride: typing.Literal[ControlInputType.Unspecified, ControlInputType.Custom, ControlInputType.Depth, ControlInputType.Canny, ControlInputType.Scribble, ControlInputType.Pose, ControlInputType.Normalbae, ControlInputType.Color, ControlInputType.Lineart, ControlInputType.Softedge, ControlInputType.Seg, ControlInputType.Inpaint, ControlInputType.Ip2p, ControlInputType.Shuffle, ControlInputType.Mlsd, ControlInputType.Tile, ControlInputType.Blur, ControlInputType.Lowquality, ControlInputType.Gray]) -> None: ... +def ControlEnd(builder: flatbuffers.Builder) -> uoffset: ... +class LoRA(object): + @classmethod + def GetRootAs(cls, buf: bytes, offset: int) -> LoRA: ... + @classmethod + def GetRootAsLoRA(cls, buf: bytes, offset: int) -> LoRA: ... + def Init(self, buf: bytes, pos: int) -> None: ... + def File(self) -> str | None: ... + def Weight(self) -> float: ... + def Mode(self) -> typing.Literal[LoRAMode.All, LoRAMode.Base, LoRAMode.Refiner]: ... +class LoRAT(object): + file: str | None + weight: float + mode: typing.Literal[LoRAMode.All, LoRAMode.Base, LoRAMode.Refiner] + def __init__( + self, + file: str | None = ..., + weight: float = ..., + mode: typing.Literal[LoRAMode.All, LoRAMode.Base, LoRAMode.Refiner] = ..., + ) -> None: ... + @classmethod + def InitFromBuf(cls, buf: bytes, pos: int) -> LoRAT: ... + @classmethod + def InitFromPackedBuf(cls, buf: bytes, pos: int = 0) -> LoRAT: ... + @classmethod + def InitFromObj(cls, loRa: LoRA) -> LoRAT: ... + def _UnPack(self, loRa: LoRA) -> None: ... + def Pack(self, builder: flatbuffers.Builder) -> None: ... +def LoRAStart(builder: flatbuffers.Builder) -> None: ... +def LoRAAddFile(builder: flatbuffers.Builder, file: uoffset) -> None: ... +def LoRAAddWeight(builder: flatbuffers.Builder, weight: float) -> None: ... +def LoRAAddMode(builder: flatbuffers.Builder, mode: typing.Literal[LoRAMode.All, LoRAMode.Base, LoRAMode.Refiner]) -> None: ... +def LoRAEnd(builder: flatbuffers.Builder) -> uoffset: ... +class GenerationConfiguration(object): + @classmethod + def GetRootAs(cls, buf: bytes, offset: int) -> GenerationConfiguration: ... + @classmethod + def GetRootAsGenerationConfiguration(cls, buf: bytes, offset: int) -> GenerationConfiguration: ... + def Init(self, buf: bytes, pos: int) -> None: ... + def Id(self) -> int: ... + def StartWidth(self) -> int: ... + def StartHeight(self) -> int: ... + def Seed(self) -> int: ... + def Steps(self) -> int: ... + def GuidanceScale(self) -> float: ... + def Strength(self) -> float: ... + def Model(self) -> str | None: ... + def Sampler(self) -> typing.Literal[SamplerType.DPMPP2MKarras, SamplerType.EulerA, SamplerType.DDIM, SamplerType.PLMS, SamplerType.DPMPPSDEKarras, SamplerType.UniPC, SamplerType.LCM, SamplerType.EulerASubstep, SamplerType.DPMPPSDESubstep, SamplerType.TCD, SamplerType.EulerATrailing, SamplerType.DPMPPSDETrailing, SamplerType.DPMPP2MAYS, SamplerType.EulerAAYS, SamplerType.DPMPPSDEAYS, SamplerType.DPMPP2MTrailing, SamplerType.DDIMTrailing, SamplerType.UniPCTrailing, SamplerType.UniPCAYS, SamplerType.TCDTrailing]: ... + def BatchCount(self) -> int: ... + def BatchSize(self) -> int: ... + def HiresFix(self) -> bool: ... + def HiresFixStartWidth(self) -> int: ... + def HiresFixStartHeight(self) -> int: ... + def HiresFixStrength(self) -> float: ... + def Upscaler(self) -> str | None: ... + def ImageGuidanceScale(self) -> float: ... + def SeedMode(self) -> typing.Literal[SeedMode.Legacy, SeedMode.TorchCpuCompatible, SeedMode.ScaleAlike, SeedMode.NvidiaGpuCompatible]: ... + def ClipSkip(self) -> int: ... + def Controls(self, i: int) -> Control | None: ... + def ControlsLength(self) -> int: ... + def ControlsIsNone(self) -> bool: ... + def Loras(self, i: int) -> LoRA | None: ... + def LorasLength(self) -> int: ... + def LorasIsNone(self) -> bool: ... + def MaskBlur(self) -> float: ... + def FaceRestoration(self) -> str | None: ... + def ClipWeight(self) -> float: ... + def NegativePromptForImagePrior(self) -> bool: ... + def ImagePriorSteps(self) -> int: ... + def RefinerModel(self) -> str | None: ... + def OriginalImageHeight(self) -> int: ... + def OriginalImageWidth(self) -> int: ... + def CropTop(self) -> int: ... + def CropLeft(self) -> int: ... + def TargetImageHeight(self) -> int: ... + def TargetImageWidth(self) -> int: ... + def AestheticScore(self) -> float: ... + def NegativeAestheticScore(self) -> float: ... + def ZeroNegativePrompt(self) -> bool: ... + def RefinerStart(self) -> float: ... + def NegativeOriginalImageHeight(self) -> int: ... + def NegativeOriginalImageWidth(self) -> int: ... + def Name(self) -> str | None: ... + def FpsId(self) -> int: ... + def MotionBucketId(self) -> int: ... + def CondAug(self) -> float: ... + def StartFrameCfg(self) -> float: ... + def NumFrames(self) -> int: ... + def MaskBlurOutset(self) -> int: ... + def Sharpness(self) -> float: ... + def Shift(self) -> float: ... + def Stage2Steps(self) -> int: ... + def Stage2Cfg(self) -> float: ... + def Stage2Shift(self) -> float: ... + def TiledDecoding(self) -> bool: ... + def DecodingTileWidth(self) -> int: ... + def DecodingTileHeight(self) -> int: ... + def DecodingTileOverlap(self) -> int: ... + def StochasticSamplingGamma(self) -> float: ... + def PreserveOriginalAfterInpaint(self) -> bool: ... + def TiledDiffusion(self) -> bool: ... + def DiffusionTileWidth(self) -> int: ... + def DiffusionTileHeight(self) -> int: ... + def DiffusionTileOverlap(self) -> int: ... + def UpscalerScaleFactor(self) -> int: ... + def T5TextEncoder(self) -> bool: ... + def SeparateClipL(self) -> bool: ... + def ClipLText(self) -> str | None: ... + def SeparateOpenClipG(self) -> bool: ... + def OpenClipGText(self) -> str | None: ... + def SpeedUpWithGuidanceEmbed(self) -> bool: ... + def GuidanceEmbed(self) -> float: ... + def ResolutionDependentShift(self) -> bool: ... + def TeaCacheStart(self) -> int: ... + def TeaCacheEnd(self) -> int: ... + def TeaCacheThreshold(self) -> float: ... + def TeaCache(self) -> bool: ... + def SeparateT5(self) -> bool: ... + def T5Text(self) -> str | None: ... + def TeaCacheMaxSkipSteps(self) -> int: ... + def CausalInferenceEnabled(self) -> bool: ... + def CausalInference(self) -> int: ... + def CausalInferencePad(self) -> int: ... + def CfgZeroStar(self) -> bool: ... + def CfgZeroInitSteps(self) -> int: ... + def CompressionArtifacts(self) -> typing.Literal[CompressionMethod.Disabled, CompressionMethod.H264, CompressionMethod.H265, CompressionMethod.Jpeg]: ... + def CompressionArtifactsQuality(self) -> float: ... + def ColorCalibration(self) -> typing.Literal[ColorCalibration.Disabled, ColorCalibration.Lab]: ... +class GenerationConfigurationT(object): + id: int + startWidth: int + startHeight: int + seed: int + steps: int + guidanceScale: float + strength: float + model: str | None + sampler: typing.Literal[SamplerType.DPMPP2MKarras, SamplerType.EulerA, SamplerType.DDIM, SamplerType.PLMS, SamplerType.DPMPPSDEKarras, SamplerType.UniPC, SamplerType.LCM, SamplerType.EulerASubstep, SamplerType.DPMPPSDESubstep, SamplerType.TCD, SamplerType.EulerATrailing, SamplerType.DPMPPSDETrailing, SamplerType.DPMPP2MAYS, SamplerType.EulerAAYS, SamplerType.DPMPPSDEAYS, SamplerType.DPMPP2MTrailing, SamplerType.DDIMTrailing, SamplerType.UniPCTrailing, SamplerType.UniPCAYS, SamplerType.TCDTrailing] + batchCount: int + batchSize: int + hiresFix: bool + hiresFixStartWidth: int + hiresFixStartHeight: int + hiresFixStrength: float + upscaler: str | None + imageGuidanceScale: float + seedMode: typing.Literal[SeedMode.Legacy, SeedMode.TorchCpuCompatible, SeedMode.ScaleAlike, SeedMode.NvidiaGpuCompatible] + clipSkip: int + controls: typing.List[ControlT] + loras: typing.List[LoRAT] + maskBlur: float + faceRestoration: str | None + clipWeight: float + negativePromptForImagePrior: bool + imagePriorSteps: int + refinerModel: str | None + originalImageHeight: int + originalImageWidth: int + cropTop: int + cropLeft: int + targetImageHeight: int + targetImageWidth: int + aestheticScore: float + negativeAestheticScore: float + zeroNegativePrompt: bool + refinerStart: float + negativeOriginalImageHeight: int + negativeOriginalImageWidth: int + name: str | None + fpsId: int + motionBucketId: int + condAug: float + startFrameCfg: float + numFrames: int + maskBlurOutset: int + sharpness: float + shift: float + stage2Steps: int + stage2Cfg: float + stage2Shift: float + tiledDecoding: bool + decodingTileWidth: int + decodingTileHeight: int + decodingTileOverlap: int + stochasticSamplingGamma: float + preserveOriginalAfterInpaint: bool + tiledDiffusion: bool + diffusionTileWidth: int + diffusionTileHeight: int + diffusionTileOverlap: int + upscalerScaleFactor: int + t5TextEncoder: bool + separateClipL: bool + clipLText: str | None + separateOpenClipG: bool + openClipGText: str | None + speedUpWithGuidanceEmbed: bool + guidanceEmbed: float + resolutionDependentShift: bool + teaCacheStart: int + teaCacheEnd: int + teaCacheThreshold: float + teaCache: bool + separateT5: bool + t5Text: str | None + teaCacheMaxSkipSteps: int + causalInferenceEnabled: bool + causalInference: int + causalInferencePad: int + cfgZeroStar: bool + cfgZeroInitSteps: int + compressionArtifacts: typing.Literal[CompressionMethod.Disabled, CompressionMethod.H264, CompressionMethod.H265, CompressionMethod.Jpeg] + compressionArtifactsQuality: float + colorCalibration: typing.Literal[ColorCalibration.Disabled, ColorCalibration.Lab] + def __init__( + self, + id: int = ..., + startWidth: int = ..., + startHeight: int = ..., + seed: int = ..., + steps: int = ..., + guidanceScale: float = ..., + strength: float = ..., + model: str | None = ..., + sampler: typing.Literal[SamplerType.DPMPP2MKarras, SamplerType.EulerA, SamplerType.DDIM, SamplerType.PLMS, SamplerType.DPMPPSDEKarras, SamplerType.UniPC, SamplerType.LCM, SamplerType.EulerASubstep, SamplerType.DPMPPSDESubstep, SamplerType.TCD, SamplerType.EulerATrailing, SamplerType.DPMPPSDETrailing, SamplerType.DPMPP2MAYS, SamplerType.EulerAAYS, SamplerType.DPMPPSDEAYS, SamplerType.DPMPP2MTrailing, SamplerType.DDIMTrailing, SamplerType.UniPCTrailing, SamplerType.UniPCAYS, SamplerType.TCDTrailing] = ..., + batchCount: int = ..., + batchSize: int = ..., + hiresFix: bool = ..., + hiresFixStartWidth: int = ..., + hiresFixStartHeight: int = ..., + hiresFixStrength: float = ..., + upscaler: str | None = ..., + imageGuidanceScale: float = ..., + seedMode: typing.Literal[SeedMode.Legacy, SeedMode.TorchCpuCompatible, SeedMode.ScaleAlike, SeedMode.NvidiaGpuCompatible] = ..., + clipSkip: int = ..., + controls: typing.List['ControlT'] | None = ..., + loras: typing.List['LoRAT'] | None = ..., + maskBlur: float = ..., + faceRestoration: str | None = ..., + clipWeight: float = ..., + negativePromptForImagePrior: bool = ..., + imagePriorSteps: int = ..., + refinerModel: str | None = ..., + originalImageHeight: int = ..., + originalImageWidth: int = ..., + cropTop: int = ..., + cropLeft: int = ..., + targetImageHeight: int = ..., + targetImageWidth: int = ..., + aestheticScore: float = ..., + negativeAestheticScore: float = ..., + zeroNegativePrompt: bool = ..., + refinerStart: float = ..., + negativeOriginalImageHeight: int = ..., + negativeOriginalImageWidth: int = ..., + name: str | None = ..., + fpsId: int = ..., + motionBucketId: int = ..., + condAug: float = ..., + startFrameCfg: float = ..., + numFrames: int = ..., + maskBlurOutset: int = ..., + sharpness: float = ..., + shift: float = ..., + stage2Steps: int = ..., + stage2Cfg: float = ..., + stage2Shift: float = ..., + tiledDecoding: bool = ..., + decodingTileWidth: int = ..., + decodingTileHeight: int = ..., + decodingTileOverlap: int = ..., + stochasticSamplingGamma: float = ..., + preserveOriginalAfterInpaint: bool = ..., + tiledDiffusion: bool = ..., + diffusionTileWidth: int = ..., + diffusionTileHeight: int = ..., + diffusionTileOverlap: int = ..., + upscalerScaleFactor: int = ..., + t5TextEncoder: bool = ..., + separateClipL: bool = ..., + clipLText: str | None = ..., + separateOpenClipG: bool = ..., + openClipGText: str | None = ..., + speedUpWithGuidanceEmbed: bool = ..., + guidanceEmbed: float = ..., + resolutionDependentShift: bool = ..., + teaCacheStart: int = ..., + teaCacheEnd: int = ..., + teaCacheThreshold: float = ..., + teaCache: bool = ..., + separateT5: bool = ..., + t5Text: str | None = ..., + teaCacheMaxSkipSteps: int = ..., + causalInferenceEnabled: bool = ..., + causalInference: int = ..., + causalInferencePad: int = ..., + cfgZeroStar: bool = ..., + cfgZeroInitSteps: int = ..., + compressionArtifacts: typing.Literal[CompressionMethod.Disabled, CompressionMethod.H264, CompressionMethod.H265, CompressionMethod.Jpeg] = ..., + compressionArtifactsQuality: float = ..., + colorCalibration: typing.Literal[ColorCalibration.Disabled, ColorCalibration.Lab] = ..., + ) -> None: ... + @classmethod + def InitFromBuf(cls, buf: bytes, pos: int) -> GenerationConfigurationT: ... + @classmethod + def InitFromPackedBuf(cls, buf: bytes, pos: int = 0) -> GenerationConfigurationT: ... + @classmethod + def InitFromObj(cls, generationConfiguration: GenerationConfiguration) -> GenerationConfigurationT: ... + def _UnPack(self, generationConfiguration: GenerationConfiguration) -> None: ... + def Pack(self, builder: flatbuffers.Builder) -> None: ... +def GenerationConfigurationStart(builder: flatbuffers.Builder) -> None: ... +def GenerationConfigurationAddId(builder: flatbuffers.Builder, id: int) -> None: ... +def GenerationConfigurationAddStartWidth(builder: flatbuffers.Builder, startWidth: int) -> None: ... +def GenerationConfigurationAddStartHeight(builder: flatbuffers.Builder, startHeight: int) -> None: ... +def GenerationConfigurationAddSeed(builder: flatbuffers.Builder, seed: int) -> None: ... +def GenerationConfigurationAddSteps(builder: flatbuffers.Builder, steps: int) -> None: ... +def GenerationConfigurationAddGuidanceScale(builder: flatbuffers.Builder, guidanceScale: float) -> None: ... +def GenerationConfigurationAddStrength(builder: flatbuffers.Builder, strength: float) -> None: ... +def GenerationConfigurationAddModel(builder: flatbuffers.Builder, model: uoffset) -> None: ... +def GenerationConfigurationAddSampler(builder: flatbuffers.Builder, sampler: typing.Literal[SamplerType.DPMPP2MKarras, SamplerType.EulerA, SamplerType.DDIM, SamplerType.PLMS, SamplerType.DPMPPSDEKarras, SamplerType.UniPC, SamplerType.LCM, SamplerType.EulerASubstep, SamplerType.DPMPPSDESubstep, SamplerType.TCD, SamplerType.EulerATrailing, SamplerType.DPMPPSDETrailing, SamplerType.DPMPP2MAYS, SamplerType.EulerAAYS, SamplerType.DPMPPSDEAYS, SamplerType.DPMPP2MTrailing, SamplerType.DDIMTrailing, SamplerType.UniPCTrailing, SamplerType.UniPCAYS, SamplerType.TCDTrailing]) -> None: ... +def GenerationConfigurationAddBatchCount(builder: flatbuffers.Builder, batchCount: int) -> None: ... +def GenerationConfigurationAddBatchSize(builder: flatbuffers.Builder, batchSize: int) -> None: ... +def GenerationConfigurationAddHiresFix(builder: flatbuffers.Builder, hiresFix: bool) -> None: ... +def GenerationConfigurationAddHiresFixStartWidth(builder: flatbuffers.Builder, hiresFixStartWidth: int) -> None: ... +def GenerationConfigurationAddHiresFixStartHeight(builder: flatbuffers.Builder, hiresFixStartHeight: int) -> None: ... +def GenerationConfigurationAddHiresFixStrength(builder: flatbuffers.Builder, hiresFixStrength: float) -> None: ... +def GenerationConfigurationAddUpscaler(builder: flatbuffers.Builder, upscaler: uoffset) -> None: ... +def GenerationConfigurationAddImageGuidanceScale(builder: flatbuffers.Builder, imageGuidanceScale: float) -> None: ... +def GenerationConfigurationAddSeedMode(builder: flatbuffers.Builder, seedMode: typing.Literal[SeedMode.Legacy, SeedMode.TorchCpuCompatible, SeedMode.ScaleAlike, SeedMode.NvidiaGpuCompatible]) -> None: ... +def GenerationConfigurationAddClipSkip(builder: flatbuffers.Builder, clipSkip: int) -> None: ... +def GenerationConfigurationAddControls(builder: flatbuffers.Builder, controls: uoffset) -> None: ... +def GenerationConfigurationStartControlsVector(builder: flatbuffers.Builder, num_elems: int) -> uoffset: ... +def GenerationConfigurationAddLoras(builder: flatbuffers.Builder, loras: uoffset) -> None: ... +def GenerationConfigurationStartLorasVector(builder: flatbuffers.Builder, num_elems: int) -> uoffset: ... +def GenerationConfigurationAddMaskBlur(builder: flatbuffers.Builder, maskBlur: float) -> None: ... +def GenerationConfigurationAddFaceRestoration(builder: flatbuffers.Builder, faceRestoration: uoffset) -> None: ... +def GenerationConfigurationAddClipWeight(builder: flatbuffers.Builder, clipWeight: float) -> None: ... +def GenerationConfigurationAddNegativePromptForImagePrior(builder: flatbuffers.Builder, negativePromptForImagePrior: bool) -> None: ... +def GenerationConfigurationAddImagePriorSteps(builder: flatbuffers.Builder, imagePriorSteps: int) -> None: ... +def GenerationConfigurationAddRefinerModel(builder: flatbuffers.Builder, refinerModel: uoffset) -> None: ... +def GenerationConfigurationAddOriginalImageHeight(builder: flatbuffers.Builder, originalImageHeight: int) -> None: ... +def GenerationConfigurationAddOriginalImageWidth(builder: flatbuffers.Builder, originalImageWidth: int) -> None: ... +def GenerationConfigurationAddCropTop(builder: flatbuffers.Builder, cropTop: int) -> None: ... +def GenerationConfigurationAddCropLeft(builder: flatbuffers.Builder, cropLeft: int) -> None: ... +def GenerationConfigurationAddTargetImageHeight(builder: flatbuffers.Builder, targetImageHeight: int) -> None: ... +def GenerationConfigurationAddTargetImageWidth(builder: flatbuffers.Builder, targetImageWidth: int) -> None: ... +def GenerationConfigurationAddAestheticScore(builder: flatbuffers.Builder, aestheticScore: float) -> None: ... +def GenerationConfigurationAddNegativeAestheticScore(builder: flatbuffers.Builder, negativeAestheticScore: float) -> None: ... +def GenerationConfigurationAddZeroNegativePrompt(builder: flatbuffers.Builder, zeroNegativePrompt: bool) -> None: ... +def GenerationConfigurationAddRefinerStart(builder: flatbuffers.Builder, refinerStart: float) -> None: ... +def GenerationConfigurationAddNegativeOriginalImageHeight(builder: flatbuffers.Builder, negativeOriginalImageHeight: int) -> None: ... +def GenerationConfigurationAddNegativeOriginalImageWidth(builder: flatbuffers.Builder, negativeOriginalImageWidth: int) -> None: ... +def GenerationConfigurationAddName(builder: flatbuffers.Builder, name: uoffset) -> None: ... +def GenerationConfigurationAddFpsId(builder: flatbuffers.Builder, fpsId: int) -> None: ... +def GenerationConfigurationAddMotionBucketId(builder: flatbuffers.Builder, motionBucketId: int) -> None: ... +def GenerationConfigurationAddCondAug(builder: flatbuffers.Builder, condAug: float) -> None: ... +def GenerationConfigurationAddStartFrameCfg(builder: flatbuffers.Builder, startFrameCfg: float) -> None: ... +def GenerationConfigurationAddNumFrames(builder: flatbuffers.Builder, numFrames: int) -> None: ... +def GenerationConfigurationAddMaskBlurOutset(builder: flatbuffers.Builder, maskBlurOutset: int) -> None: ... +def GenerationConfigurationAddSharpness(builder: flatbuffers.Builder, sharpness: float) -> None: ... +def GenerationConfigurationAddShift(builder: flatbuffers.Builder, shift: float) -> None: ... +def GenerationConfigurationAddStage2Steps(builder: flatbuffers.Builder, stage2Steps: int) -> None: ... +def GenerationConfigurationAddStage2Cfg(builder: flatbuffers.Builder, stage2Cfg: float) -> None: ... +def GenerationConfigurationAddStage2Shift(builder: flatbuffers.Builder, stage2Shift: float) -> None: ... +def GenerationConfigurationAddTiledDecoding(builder: flatbuffers.Builder, tiledDecoding: bool) -> None: ... +def GenerationConfigurationAddDecodingTileWidth(builder: flatbuffers.Builder, decodingTileWidth: int) -> None: ... +def GenerationConfigurationAddDecodingTileHeight(builder: flatbuffers.Builder, decodingTileHeight: int) -> None: ... +def GenerationConfigurationAddDecodingTileOverlap(builder: flatbuffers.Builder, decodingTileOverlap: int) -> None: ... +def GenerationConfigurationAddStochasticSamplingGamma(builder: flatbuffers.Builder, stochasticSamplingGamma: float) -> None: ... +def GenerationConfigurationAddPreserveOriginalAfterInpaint(builder: flatbuffers.Builder, preserveOriginalAfterInpaint: bool) -> None: ... +def GenerationConfigurationAddTiledDiffusion(builder: flatbuffers.Builder, tiledDiffusion: bool) -> None: ... +def GenerationConfigurationAddDiffusionTileWidth(builder: flatbuffers.Builder, diffusionTileWidth: int) -> None: ... +def GenerationConfigurationAddDiffusionTileHeight(builder: flatbuffers.Builder, diffusionTileHeight: int) -> None: ... +def GenerationConfigurationAddDiffusionTileOverlap(builder: flatbuffers.Builder, diffusionTileOverlap: int) -> None: ... +def GenerationConfigurationAddUpscalerScaleFactor(builder: flatbuffers.Builder, upscalerScaleFactor: int) -> None: ... +def GenerationConfigurationAddT5TextEncoder(builder: flatbuffers.Builder, t5TextEncoder: bool) -> None: ... +def GenerationConfigurationAddSeparateClipL(builder: flatbuffers.Builder, separateClipL: bool) -> None: ... +def GenerationConfigurationAddClipLText(builder: flatbuffers.Builder, clipLText: uoffset) -> None: ... +def GenerationConfigurationAddSeparateOpenClipG(builder: flatbuffers.Builder, separateOpenClipG: bool) -> None: ... +def GenerationConfigurationAddOpenClipGText(builder: flatbuffers.Builder, openClipGText: uoffset) -> None: ... +def GenerationConfigurationAddSpeedUpWithGuidanceEmbed(builder: flatbuffers.Builder, speedUpWithGuidanceEmbed: bool) -> None: ... +def GenerationConfigurationAddGuidanceEmbed(builder: flatbuffers.Builder, guidanceEmbed: float) -> None: ... +def GenerationConfigurationAddResolutionDependentShift(builder: flatbuffers.Builder, resolutionDependentShift: bool) -> None: ... +def GenerationConfigurationAddTeaCacheStart(builder: flatbuffers.Builder, teaCacheStart: int) -> None: ... +def GenerationConfigurationAddTeaCacheEnd(builder: flatbuffers.Builder, teaCacheEnd: int) -> None: ... +def GenerationConfigurationAddTeaCacheThreshold(builder: flatbuffers.Builder, teaCacheThreshold: float) -> None: ... +def GenerationConfigurationAddTeaCache(builder: flatbuffers.Builder, teaCache: bool) -> None: ... +def GenerationConfigurationAddSeparateT5(builder: flatbuffers.Builder, separateT5: bool) -> None: ... +def GenerationConfigurationAddT5Text(builder: flatbuffers.Builder, t5Text: uoffset) -> None: ... +def GenerationConfigurationAddTeaCacheMaxSkipSteps(builder: flatbuffers.Builder, teaCacheMaxSkipSteps: int) -> None: ... +def GenerationConfigurationAddCausalInferenceEnabled(builder: flatbuffers.Builder, causalInferenceEnabled: bool) -> None: ... +def GenerationConfigurationAddCausalInference(builder: flatbuffers.Builder, causalInference: int) -> None: ... +def GenerationConfigurationAddCausalInferencePad(builder: flatbuffers.Builder, causalInferencePad: int) -> None: ... +def GenerationConfigurationAddCfgZeroStar(builder: flatbuffers.Builder, cfgZeroStar: bool) -> None: ... +def GenerationConfigurationAddCfgZeroInitSteps(builder: flatbuffers.Builder, cfgZeroInitSteps: int) -> None: ... +def GenerationConfigurationAddCompressionArtifacts(builder: flatbuffers.Builder, compressionArtifacts: typing.Literal[CompressionMethod.Disabled, CompressionMethod.H264, CompressionMethod.H265, CompressionMethod.Jpeg]) -> None: ... +def GenerationConfigurationAddCompressionArtifactsQuality(builder: flatbuffers.Builder, compressionArtifactsQuality: float) -> None: ... +def GenerationConfigurationAddColorCalibration(builder: flatbuffers.Builder, colorCalibration: typing.Literal[ColorCalibration.Disabled, ColorCalibration.Lab]) -> None: ... +def GenerationConfigurationEnd(builder: flatbuffers.Builder) -> uoffset: ... + diff --git a/CLI/macos/drawthings/generated/imageService_pb2.py b/CLI/macos/drawthings/generated/imageService_pb2.py new file mode 100644 index 000000000..0c77dbba8 --- /dev/null +++ b/CLI/macos/drawthings/generated/imageService_pb2.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: imageService.proto +# Protobuf Python Version: 5.29.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 0, + '', + 'imageService.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12imageService.proto\"G\n\x0b\x45\x63hoRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x19\n\x0csharedSecret\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0f\n\r_sharedSecret\"I\n\x14\x43omputeUnitThreshold\x12\x11\n\tcommunity\x18\x01 \x01(\x01\x12\x0c\n\x04plus\x18\x02 \x01(\x01\x12\x10\n\x08\x65xpireAt\x18\x03 \x01(\x03\"\xd8\x01\n\tEchoReply\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\r\n\x05\x66iles\x18\x02 \x03(\t\x12(\n\x08override\x18\x03 \x01(\x0b\x32\x11.MetadataOverrideH\x00\x88\x01\x01\x12\x1b\n\x13sharedSecretMissing\x18\x04 \x01(\x08\x12.\n\nthresholds\x18\x05 \x01(\x0b\x32\x15.ComputeUnitThresholdH\x01\x88\x01\x01\x12\x18\n\x10serverIdentifier\x18\x06 \x01(\x04\x42\x0b\n\t_overrideB\r\n\x0b_thresholds\"c\n\x0f\x46ileListRequest\x12\r\n\x05\x66iles\x18\x01 \x03(\t\x12\x15\n\rfilesWithHash\x18\x02 \x03(\t\x12\x19\n\x0csharedSecret\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0f\n\r_sharedSecret\"J\n\x15\x46ileExistenceResponse\x12\r\n\x05\x66iles\x18\x01 \x03(\t\x12\x12\n\nexistences\x18\x02 \x03(\x08\x12\x0e\n\x06hashes\x18\x03 \x03(\x0c\"t\n\x10MetadataOverride\x12\x0e\n\x06models\x18\x01 \x01(\x0c\x12\r\n\x05loras\x18\x02 \x01(\x0c\x12\x13\n\x0b\x63ontrolNets\x18\x03 \x01(\x0c\x12\x19\n\x11textualInversions\x18\x04 \x01(\x0c\x12\x11\n\tupscalers\x18\x05 \x01(\x0c\"\xf2\x02\n\x16ImageGenerationRequest\x12\x12\n\x05image\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x13\n\x0bscaleFactor\x18\x02 \x01(\x05\x12\x11\n\x04mask\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x12\x19\n\x05hints\x18\x04 \x03(\x0b\x32\n.HintProto\x12\x0e\n\x06prompt\x18\x05 \x01(\t\x12\x16\n\x0enegativePrompt\x18\x06 \x01(\t\x12\x15\n\rconfiguration\x18\x07 \x01(\x0c\x12#\n\x08override\x18\x08 \x01(\x0b\x32\x11.MetadataOverride\x12\x10\n\x08keywords\x18\t \x03(\t\x12\x0c\n\x04user\x18\n \x01(\t\x12\x1b\n\x06\x64\x65vice\x18\x0b \x01(\x0e\x32\x0b.DeviceType\x12\x10\n\x08\x63ontents\x18\x0c \x03(\x0c\x12\x19\n\x0csharedSecret\x18\r \x01(\tH\x02\x88\x01\x01\x12\x0f\n\x07\x63hunked\x18\x0e \x01(\x08\x42\x08\n\x06_imageB\x07\n\x05_maskB\x0f\n\r_sharedSecret\"@\n\tHintProto\x12\x10\n\x08hintType\x18\x01 \x01(\t\x12!\n\x07tensors\x18\x02 \x03(\x0b\x32\x10.TensorAndWeight\"1\n\x0fTensorAndWeight\x12\x0e\n\x06tensor\x18\x01 \x01(\x0c\x12\x0e\n\x06weight\x18\x02 \x01(\x02\"\xfc\x06\n\x1cImageGenerationSignpostProto\x12@\n\x0btextEncoded\x18\x01 \x01(\x0b\x32).ImageGenerationSignpostProto.TextEncodedH\x00\x12\x42\n\x0cimageEncoded\x18\x02 \x01(\x0b\x32*.ImageGenerationSignpostProto.ImageEncodedH\x00\x12:\n\x08sampling\x18\x03 \x01(\x0b\x32&.ImageGenerationSignpostProto.SamplingH\x00\x12\x42\n\x0cimageDecoded\x18\x04 \x01(\x0b\x32*.ImageGenerationSignpostProto.ImageDecodedH\x00\x12V\n\x16secondPassImageEncoded\x18\x05 \x01(\x0b\x32\x34.ImageGenerationSignpostProto.SecondPassImageEncodedH\x00\x12N\n\x12secondPassSampling\x18\x06 \x01(\x0b\x32\x30.ImageGenerationSignpostProto.SecondPassSamplingH\x00\x12V\n\x16secondPassImageDecoded\x18\x07 \x01(\x0b\x32\x34.ImageGenerationSignpostProto.SecondPassImageDecodedH\x00\x12\x42\n\x0c\x66\x61\x63\x65Restored\x18\x08 \x01(\x0b\x32*.ImageGenerationSignpostProto.FaceRestoredH\x00\x12\x44\n\rimageUpscaled\x18\t \x01(\x0b\x32+.ImageGenerationSignpostProto.ImageUpscaledH\x00\x1a\r\n\x0bTextEncoded\x1a\x0e\n\x0cImageEncoded\x1a\x18\n\x08Sampling\x12\x0c\n\x04step\x18\x01 \x01(\x05\x1a\x0e\n\x0cImageDecoded\x1a\x18\n\x16SecondPassImageEncoded\x1a\"\n\x12SecondPassSampling\x12\x0c\n\x04step\x18\x01 \x01(\x05\x1a\x18\n\x16SecondPassImageDecoded\x1a\x0e\n\x0c\x46\x61\x63\x65Restored\x1a\x0f\n\rImageUpscaledB\n\n\x08signpost\"x\n\x16RemoteDownloadResponse\x12\x15\n\rbytesReceived\x18\x01 \x01(\x03\x12\x15\n\rbytesExpected\x18\x02 \x01(\x03\x12\x0c\n\x04item\x18\x03 \x01(\x05\x12\x15\n\ritemsExpected\x18\x04 \x01(\x05\x12\x0b\n\x03tag\x18\x05 \x01(\t\"\xc7\x03\n\x17ImageGenerationResponse\x12\x17\n\x0fgeneratedImages\x18\x01 \x03(\x0c\x12;\n\x0f\x63urrentSignpost\x18\x02 \x01(\x0b\x32\x1d.ImageGenerationSignpostProtoH\x00\x88\x01\x01\x12\x30\n\tsignposts\x18\x03 \x03(\x0b\x32\x1d.ImageGenerationSignpostProto\x12\x19\n\x0cpreviewImage\x18\x04 \x01(\x0cH\x01\x88\x01\x01\x12\x18\n\x0bscaleFactor\x18\x05 \x01(\x05H\x02\x88\x01\x01\x12\x0c\n\x04tags\x18\x06 \x03(\t\x12\x19\n\x0c\x64ownloadSize\x18\x07 \x01(\x03H\x03\x88\x01\x01\x12\x1f\n\nchunkState\x18\x08 \x01(\x0e\x32\x0b.ChunkState\x12\x34\n\x0eremoteDownload\x18\t \x01(\x0b\x32\x17.RemoteDownloadResponseH\x04\x88\x01\x01\x12\x16\n\x0egeneratedAudio\x18\n \x03(\x0c\x42\x12\n\x10_currentSignpostB\x0f\n\r_previewImageB\x0e\n\x0c_scaleFactorB\x0f\n\r_downloadSizeB\x11\n\x0f_remoteDownload\">\n\tFileChunk\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\x0c\x12\x10\n\x08\x66ilename\x18\x02 \x01(\t\x12\x0e\n\x06offset\x18\x03 \x01(\x03\"H\n\x11InitUploadRequest\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x0e\n\x06sha256\x18\x02 \x01(\x0c\x12\x11\n\ttotalSize\x18\x03 \x01(\x03\"g\n\x0eUploadResponse\x12\x1a\n\x12\x63hunkUploadSuccess\x18\x01 \x01(\x08\x12\x16\n\x0ereceivedOffset\x18\x02 \x01(\x03\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x10\n\x08\x66ilename\x18\x04 \x01(\t\"\x92\x01\n\x11\x46ileUploadRequest\x12)\n\x0binitRequest\x18\x01 \x01(\x0b\x32\x12.InitUploadRequestH\x00\x12\x1b\n\x05\x63hunk\x18\x02 \x01(\x0b\x32\n.FileChunkH\x00\x12\x19\n\x0csharedSecret\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\t\n\x07requestB\x0f\n\r_sharedSecret\"\x1d\n\rPubkeyRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"1\n\x0ePubkeyResponse\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0e\n\x06pubkey\x18\x02 \x01(\t\"\x0e\n\x0cHoursRequest\":\n\rHoursResponse\x12)\n\nthresholds\x18\x01 \x01(\x0b\x32\x15.ComputeUnitThreshold*/\n\nDeviceType\x12\t\n\x05PHONE\x10\x00\x12\n\n\x06TABLET\x10\x01\x12\n\n\x06LAPTOP\x10\x02*-\n\nChunkState\x12\x0e\n\nLAST_CHUNK\x10\x00\x12\x0f\n\x0bMORE_CHUNKS\x10\x01\x32\xc2\x02\n\x16ImageGenerationService\x12\x44\n\rGenerateImage\x12\x17.ImageGenerationRequest\x1a\x18.ImageGenerationResponse0\x01\x12\x36\n\nFilesExist\x12\x10.FileListRequest\x1a\x16.FileExistenceResponse\x12\x35\n\nUploadFile\x12\x12.FileUploadRequest\x1a\x0f.UploadResponse(\x01\x30\x01\x12 \n\x04\x45\x63ho\x12\x0c.EchoRequest\x1a\n.EchoReply\x12)\n\x06Pubkey\x12\x0e.PubkeyRequest\x1a\x0f.PubkeyResponse\x12&\n\x05Hours\x12\r.HoursRequest\x1a\x0e.HoursResponseb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'imageService_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_DEVICETYPE']._serialized_start=3199 + _globals['_DEVICETYPE']._serialized_end=3246 + _globals['_CHUNKSTATE']._serialized_start=3248 + _globals['_CHUNKSTATE']._serialized_end=3293 + _globals['_ECHOREQUEST']._serialized_start=22 + _globals['_ECHOREQUEST']._serialized_end=93 + _globals['_COMPUTEUNITTHRESHOLD']._serialized_start=95 + _globals['_COMPUTEUNITTHRESHOLD']._serialized_end=168 + _globals['_ECHOREPLY']._serialized_start=171 + _globals['_ECHOREPLY']._serialized_end=387 + _globals['_FILELISTREQUEST']._serialized_start=389 + _globals['_FILELISTREQUEST']._serialized_end=488 + _globals['_FILEEXISTENCERESPONSE']._serialized_start=490 + _globals['_FILEEXISTENCERESPONSE']._serialized_end=564 + _globals['_METADATAOVERRIDE']._serialized_start=566 + _globals['_METADATAOVERRIDE']._serialized_end=682 + _globals['_IMAGEGENERATIONREQUEST']._serialized_start=685 + _globals['_IMAGEGENERATIONREQUEST']._serialized_end=1055 + _globals['_HINTPROTO']._serialized_start=1057 + _globals['_HINTPROTO']._serialized_end=1121 + _globals['_TENSORANDWEIGHT']._serialized_start=1123 + _globals['_TENSORANDWEIGHT']._serialized_end=1172 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO']._serialized_start=1175 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO']._serialized_end=2067 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_TEXTENCODED']._serialized_start=1863 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_TEXTENCODED']._serialized_end=1876 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_IMAGEENCODED']._serialized_start=1878 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_IMAGEENCODED']._serialized_end=1892 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_SAMPLING']._serialized_start=1894 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_SAMPLING']._serialized_end=1918 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_IMAGEDECODED']._serialized_start=1920 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_IMAGEDECODED']._serialized_end=1934 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_SECONDPASSIMAGEENCODED']._serialized_start=1936 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_SECONDPASSIMAGEENCODED']._serialized_end=1960 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_SECONDPASSSAMPLING']._serialized_start=1962 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_SECONDPASSSAMPLING']._serialized_end=1996 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_SECONDPASSIMAGEDECODED']._serialized_start=1998 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_SECONDPASSIMAGEDECODED']._serialized_end=2022 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_FACERESTORED']._serialized_start=2024 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_FACERESTORED']._serialized_end=2038 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_IMAGEUPSCALED']._serialized_start=2040 + _globals['_IMAGEGENERATIONSIGNPOSTPROTO_IMAGEUPSCALED']._serialized_end=2055 + _globals['_REMOTEDOWNLOADRESPONSE']._serialized_start=2069 + _globals['_REMOTEDOWNLOADRESPONSE']._serialized_end=2189 + _globals['_IMAGEGENERATIONRESPONSE']._serialized_start=2192 + _globals['_IMAGEGENERATIONRESPONSE']._serialized_end=2647 + _globals['_FILECHUNK']._serialized_start=2649 + _globals['_FILECHUNK']._serialized_end=2711 + _globals['_INITUPLOADREQUEST']._serialized_start=2713 + _globals['_INITUPLOADREQUEST']._serialized_end=2785 + _globals['_UPLOADRESPONSE']._serialized_start=2787 + _globals['_UPLOADRESPONSE']._serialized_end=2890 + _globals['_FILEUPLOADREQUEST']._serialized_start=2893 + _globals['_FILEUPLOADREQUEST']._serialized_end=3039 + _globals['_PUBKEYREQUEST']._serialized_start=3041 + _globals['_PUBKEYREQUEST']._serialized_end=3070 + _globals['_PUBKEYRESPONSE']._serialized_start=3072 + _globals['_PUBKEYRESPONSE']._serialized_end=3121 + _globals['_HOURSREQUEST']._serialized_start=3123 + _globals['_HOURSREQUEST']._serialized_end=3137 + _globals['_HOURSRESPONSE']._serialized_start=3139 + _globals['_HOURSRESPONSE']._serialized_end=3197 + _globals['_IMAGEGENERATIONSERVICE']._serialized_start=3296 + _globals['_IMAGEGENERATIONSERVICE']._serialized_end=3618 +# @@protoc_insertion_point(module_scope) diff --git a/CLI/macos/drawthings/generated/imageService_pb2.pyi b/CLI/macos/drawthings/generated/imageService_pb2.pyi new file mode 100644 index 000000000..0bc53805b --- /dev/null +++ b/CLI/macos/drawthings/generated/imageService_pb2.pyi @@ -0,0 +1,296 @@ +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class DeviceType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + PHONE: _ClassVar[DeviceType] + TABLET: _ClassVar[DeviceType] + LAPTOP: _ClassVar[DeviceType] + +class ChunkState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + LAST_CHUNK: _ClassVar[ChunkState] + MORE_CHUNKS: _ClassVar[ChunkState] +PHONE: DeviceType +TABLET: DeviceType +LAPTOP: DeviceType +LAST_CHUNK: ChunkState +MORE_CHUNKS: ChunkState + +class EchoRequest(_message.Message): + __slots__ = ("name", "sharedSecret") + NAME_FIELD_NUMBER: _ClassVar[int] + SHAREDSECRET_FIELD_NUMBER: _ClassVar[int] + name: str + sharedSecret: str + def __init__(self, name: _Optional[str] = ..., sharedSecret: _Optional[str] = ...) -> None: ... + +class ComputeUnitThreshold(_message.Message): + __slots__ = ("community", "plus", "expireAt") + COMMUNITY_FIELD_NUMBER: _ClassVar[int] + PLUS_FIELD_NUMBER: _ClassVar[int] + EXPIREAT_FIELD_NUMBER: _ClassVar[int] + community: float + plus: float + expireAt: int + def __init__(self, community: _Optional[float] = ..., plus: _Optional[float] = ..., expireAt: _Optional[int] = ...) -> None: ... + +class EchoReply(_message.Message): + __slots__ = ("message", "files", "override", "sharedSecretMissing", "thresholds", "serverIdentifier") + MESSAGE_FIELD_NUMBER: _ClassVar[int] + FILES_FIELD_NUMBER: _ClassVar[int] + OVERRIDE_FIELD_NUMBER: _ClassVar[int] + SHAREDSECRETMISSING_FIELD_NUMBER: _ClassVar[int] + THRESHOLDS_FIELD_NUMBER: _ClassVar[int] + SERVERIDENTIFIER_FIELD_NUMBER: _ClassVar[int] + message: str + files: _containers.RepeatedScalarFieldContainer[str] + override: MetadataOverride + sharedSecretMissing: bool + thresholds: ComputeUnitThreshold + serverIdentifier: int + def __init__(self, message: _Optional[str] = ..., files: _Optional[_Iterable[str]] = ..., override: _Optional[_Union[MetadataOverride, _Mapping]] = ..., sharedSecretMissing: bool = ..., thresholds: _Optional[_Union[ComputeUnitThreshold, _Mapping]] = ..., serverIdentifier: _Optional[int] = ...) -> None: ... + +class FileListRequest(_message.Message): + __slots__ = ("files", "filesWithHash", "sharedSecret") + FILES_FIELD_NUMBER: _ClassVar[int] + FILESWITHHASH_FIELD_NUMBER: _ClassVar[int] + SHAREDSECRET_FIELD_NUMBER: _ClassVar[int] + files: _containers.RepeatedScalarFieldContainer[str] + filesWithHash: _containers.RepeatedScalarFieldContainer[str] + sharedSecret: str + def __init__(self, files: _Optional[_Iterable[str]] = ..., filesWithHash: _Optional[_Iterable[str]] = ..., sharedSecret: _Optional[str] = ...) -> None: ... + +class FileExistenceResponse(_message.Message): + __slots__ = ("files", "existences", "hashes") + FILES_FIELD_NUMBER: _ClassVar[int] + EXISTENCES_FIELD_NUMBER: _ClassVar[int] + HASHES_FIELD_NUMBER: _ClassVar[int] + files: _containers.RepeatedScalarFieldContainer[str] + existences: _containers.RepeatedScalarFieldContainer[bool] + hashes: _containers.RepeatedScalarFieldContainer[bytes] + def __init__(self, files: _Optional[_Iterable[str]] = ..., existences: _Optional[_Iterable[bool]] = ..., hashes: _Optional[_Iterable[bytes]] = ...) -> None: ... + +class MetadataOverride(_message.Message): + __slots__ = ("models", "loras", "controlNets", "textualInversions", "upscalers") + MODELS_FIELD_NUMBER: _ClassVar[int] + LORAS_FIELD_NUMBER: _ClassVar[int] + CONTROLNETS_FIELD_NUMBER: _ClassVar[int] + TEXTUALINVERSIONS_FIELD_NUMBER: _ClassVar[int] + UPSCALERS_FIELD_NUMBER: _ClassVar[int] + models: bytes + loras: bytes + controlNets: bytes + textualInversions: bytes + upscalers: bytes + def __init__(self, models: _Optional[bytes] = ..., loras: _Optional[bytes] = ..., controlNets: _Optional[bytes] = ..., textualInversions: _Optional[bytes] = ..., upscalers: _Optional[bytes] = ...) -> None: ... + +class ImageGenerationRequest(_message.Message): + __slots__ = ("image", "scaleFactor", "mask", "hints", "prompt", "negativePrompt", "configuration", "override", "keywords", "user", "device", "contents", "sharedSecret", "chunked") + IMAGE_FIELD_NUMBER: _ClassVar[int] + SCALEFACTOR_FIELD_NUMBER: _ClassVar[int] + MASK_FIELD_NUMBER: _ClassVar[int] + HINTS_FIELD_NUMBER: _ClassVar[int] + PROMPT_FIELD_NUMBER: _ClassVar[int] + NEGATIVEPROMPT_FIELD_NUMBER: _ClassVar[int] + CONFIGURATION_FIELD_NUMBER: _ClassVar[int] + OVERRIDE_FIELD_NUMBER: _ClassVar[int] + KEYWORDS_FIELD_NUMBER: _ClassVar[int] + USER_FIELD_NUMBER: _ClassVar[int] + DEVICE_FIELD_NUMBER: _ClassVar[int] + CONTENTS_FIELD_NUMBER: _ClassVar[int] + SHAREDSECRET_FIELD_NUMBER: _ClassVar[int] + CHUNKED_FIELD_NUMBER: _ClassVar[int] + image: bytes + scaleFactor: int + mask: bytes + hints: _containers.RepeatedCompositeFieldContainer[HintProto] + prompt: str + negativePrompt: str + configuration: bytes + override: MetadataOverride + keywords: _containers.RepeatedScalarFieldContainer[str] + user: str + device: DeviceType + contents: _containers.RepeatedScalarFieldContainer[bytes] + sharedSecret: str + chunked: bool + def __init__(self, image: _Optional[bytes] = ..., scaleFactor: _Optional[int] = ..., mask: _Optional[bytes] = ..., hints: _Optional[_Iterable[_Union[HintProto, _Mapping]]] = ..., prompt: _Optional[str] = ..., negativePrompt: _Optional[str] = ..., configuration: _Optional[bytes] = ..., override: _Optional[_Union[MetadataOverride, _Mapping]] = ..., keywords: _Optional[_Iterable[str]] = ..., user: _Optional[str] = ..., device: _Optional[_Union[DeviceType, str]] = ..., contents: _Optional[_Iterable[bytes]] = ..., sharedSecret: _Optional[str] = ..., chunked: bool = ...) -> None: ... + +class HintProto(_message.Message): + __slots__ = ("hintType", "tensors") + HINTTYPE_FIELD_NUMBER: _ClassVar[int] + TENSORS_FIELD_NUMBER: _ClassVar[int] + hintType: str + tensors: _containers.RepeatedCompositeFieldContainer[TensorAndWeight] + def __init__(self, hintType: _Optional[str] = ..., tensors: _Optional[_Iterable[_Union[TensorAndWeight, _Mapping]]] = ...) -> None: ... + +class TensorAndWeight(_message.Message): + __slots__ = ("tensor", "weight") + TENSOR_FIELD_NUMBER: _ClassVar[int] + WEIGHT_FIELD_NUMBER: _ClassVar[int] + tensor: bytes + weight: float + def __init__(self, tensor: _Optional[bytes] = ..., weight: _Optional[float] = ...) -> None: ... + +class ImageGenerationSignpostProto(_message.Message): + __slots__ = ("textEncoded", "imageEncoded", "sampling", "imageDecoded", "secondPassImageEncoded", "secondPassSampling", "secondPassImageDecoded", "faceRestored", "imageUpscaled") + class TextEncoded(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + class ImageEncoded(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + class Sampling(_message.Message): + __slots__ = ("step",) + STEP_FIELD_NUMBER: _ClassVar[int] + step: int + def __init__(self, step: _Optional[int] = ...) -> None: ... + class ImageDecoded(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + class SecondPassImageEncoded(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + class SecondPassSampling(_message.Message): + __slots__ = ("step",) + STEP_FIELD_NUMBER: _ClassVar[int] + step: int + def __init__(self, step: _Optional[int] = ...) -> None: ... + class SecondPassImageDecoded(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + class FaceRestored(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + class ImageUpscaled(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + TEXTENCODED_FIELD_NUMBER: _ClassVar[int] + IMAGEENCODED_FIELD_NUMBER: _ClassVar[int] + SAMPLING_FIELD_NUMBER: _ClassVar[int] + IMAGEDECODED_FIELD_NUMBER: _ClassVar[int] + SECONDPASSIMAGEENCODED_FIELD_NUMBER: _ClassVar[int] + SECONDPASSSAMPLING_FIELD_NUMBER: _ClassVar[int] + SECONDPASSIMAGEDECODED_FIELD_NUMBER: _ClassVar[int] + FACERESTORED_FIELD_NUMBER: _ClassVar[int] + IMAGEUPSCALED_FIELD_NUMBER: _ClassVar[int] + textEncoded: ImageGenerationSignpostProto.TextEncoded + imageEncoded: ImageGenerationSignpostProto.ImageEncoded + sampling: ImageGenerationSignpostProto.Sampling + imageDecoded: ImageGenerationSignpostProto.ImageDecoded + secondPassImageEncoded: ImageGenerationSignpostProto.SecondPassImageEncoded + secondPassSampling: ImageGenerationSignpostProto.SecondPassSampling + secondPassImageDecoded: ImageGenerationSignpostProto.SecondPassImageDecoded + faceRestored: ImageGenerationSignpostProto.FaceRestored + imageUpscaled: ImageGenerationSignpostProto.ImageUpscaled + def __init__(self, textEncoded: _Optional[_Union[ImageGenerationSignpostProto.TextEncoded, _Mapping]] = ..., imageEncoded: _Optional[_Union[ImageGenerationSignpostProto.ImageEncoded, _Mapping]] = ..., sampling: _Optional[_Union[ImageGenerationSignpostProto.Sampling, _Mapping]] = ..., imageDecoded: _Optional[_Union[ImageGenerationSignpostProto.ImageDecoded, _Mapping]] = ..., secondPassImageEncoded: _Optional[_Union[ImageGenerationSignpostProto.SecondPassImageEncoded, _Mapping]] = ..., secondPassSampling: _Optional[_Union[ImageGenerationSignpostProto.SecondPassSampling, _Mapping]] = ..., secondPassImageDecoded: _Optional[_Union[ImageGenerationSignpostProto.SecondPassImageDecoded, _Mapping]] = ..., faceRestored: _Optional[_Union[ImageGenerationSignpostProto.FaceRestored, _Mapping]] = ..., imageUpscaled: _Optional[_Union[ImageGenerationSignpostProto.ImageUpscaled, _Mapping]] = ...) -> None: ... + +class RemoteDownloadResponse(_message.Message): + __slots__ = ("bytesReceived", "bytesExpected", "item", "itemsExpected", "tag") + BYTESRECEIVED_FIELD_NUMBER: _ClassVar[int] + BYTESEXPECTED_FIELD_NUMBER: _ClassVar[int] + ITEM_FIELD_NUMBER: _ClassVar[int] + ITEMSEXPECTED_FIELD_NUMBER: _ClassVar[int] + TAG_FIELD_NUMBER: _ClassVar[int] + bytesReceived: int + bytesExpected: int + item: int + itemsExpected: int + tag: str + def __init__(self, bytesReceived: _Optional[int] = ..., bytesExpected: _Optional[int] = ..., item: _Optional[int] = ..., itemsExpected: _Optional[int] = ..., tag: _Optional[str] = ...) -> None: ... + +class ImageGenerationResponse(_message.Message): + __slots__ = ("generatedImages", "currentSignpost", "signposts", "previewImage", "scaleFactor", "tags", "downloadSize", "chunkState", "remoteDownload", "generatedAudio") + GENERATEDIMAGES_FIELD_NUMBER: _ClassVar[int] + CURRENTSIGNPOST_FIELD_NUMBER: _ClassVar[int] + SIGNPOSTS_FIELD_NUMBER: _ClassVar[int] + PREVIEWIMAGE_FIELD_NUMBER: _ClassVar[int] + SCALEFACTOR_FIELD_NUMBER: _ClassVar[int] + TAGS_FIELD_NUMBER: _ClassVar[int] + DOWNLOADSIZE_FIELD_NUMBER: _ClassVar[int] + CHUNKSTATE_FIELD_NUMBER: _ClassVar[int] + REMOTEDOWNLOAD_FIELD_NUMBER: _ClassVar[int] + GENERATEDAUDIO_FIELD_NUMBER: _ClassVar[int] + generatedImages: _containers.RepeatedScalarFieldContainer[bytes] + currentSignpost: ImageGenerationSignpostProto + signposts: _containers.RepeatedCompositeFieldContainer[ImageGenerationSignpostProto] + previewImage: bytes + scaleFactor: int + tags: _containers.RepeatedScalarFieldContainer[str] + downloadSize: int + chunkState: ChunkState + remoteDownload: RemoteDownloadResponse + generatedAudio: _containers.RepeatedScalarFieldContainer[bytes] + def __init__(self, generatedImages: _Optional[_Iterable[bytes]] = ..., currentSignpost: _Optional[_Union[ImageGenerationSignpostProto, _Mapping]] = ..., signposts: _Optional[_Iterable[_Union[ImageGenerationSignpostProto, _Mapping]]] = ..., previewImage: _Optional[bytes] = ..., scaleFactor: _Optional[int] = ..., tags: _Optional[_Iterable[str]] = ..., downloadSize: _Optional[int] = ..., chunkState: _Optional[_Union[ChunkState, str]] = ..., remoteDownload: _Optional[_Union[RemoteDownloadResponse, _Mapping]] = ..., generatedAudio: _Optional[_Iterable[bytes]] = ...) -> None: ... + +class FileChunk(_message.Message): + __slots__ = ("content", "filename", "offset") + CONTENT_FIELD_NUMBER: _ClassVar[int] + FILENAME_FIELD_NUMBER: _ClassVar[int] + OFFSET_FIELD_NUMBER: _ClassVar[int] + content: bytes + filename: str + offset: int + def __init__(self, content: _Optional[bytes] = ..., filename: _Optional[str] = ..., offset: _Optional[int] = ...) -> None: ... + +class InitUploadRequest(_message.Message): + __slots__ = ("filename", "sha256", "totalSize") + FILENAME_FIELD_NUMBER: _ClassVar[int] + SHA256_FIELD_NUMBER: _ClassVar[int] + TOTALSIZE_FIELD_NUMBER: _ClassVar[int] + filename: str + sha256: bytes + totalSize: int + def __init__(self, filename: _Optional[str] = ..., sha256: _Optional[bytes] = ..., totalSize: _Optional[int] = ...) -> None: ... + +class UploadResponse(_message.Message): + __slots__ = ("chunkUploadSuccess", "receivedOffset", "message", "filename") + CHUNKUPLOADSUCCESS_FIELD_NUMBER: _ClassVar[int] + RECEIVEDOFFSET_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + FILENAME_FIELD_NUMBER: _ClassVar[int] + chunkUploadSuccess: bool + receivedOffset: int + message: str + filename: str + def __init__(self, chunkUploadSuccess: bool = ..., receivedOffset: _Optional[int] = ..., message: _Optional[str] = ..., filename: _Optional[str] = ...) -> None: ... + +class FileUploadRequest(_message.Message): + __slots__ = ("initRequest", "chunk", "sharedSecret") + INITREQUEST_FIELD_NUMBER: _ClassVar[int] + CHUNK_FIELD_NUMBER: _ClassVar[int] + SHAREDSECRET_FIELD_NUMBER: _ClassVar[int] + initRequest: InitUploadRequest + chunk: FileChunk + sharedSecret: str + def __init__(self, initRequest: _Optional[_Union[InitUploadRequest, _Mapping]] = ..., chunk: _Optional[_Union[FileChunk, _Mapping]] = ..., sharedSecret: _Optional[str] = ...) -> None: ... + +class PubkeyRequest(_message.Message): + __slots__ = ("name",) + NAME_FIELD_NUMBER: _ClassVar[int] + name: str + def __init__(self, name: _Optional[str] = ...) -> None: ... + +class PubkeyResponse(_message.Message): + __slots__ = ("message", "pubkey") + MESSAGE_FIELD_NUMBER: _ClassVar[int] + PUBKEY_FIELD_NUMBER: _ClassVar[int] + message: str + pubkey: str + def __init__(self, message: _Optional[str] = ..., pubkey: _Optional[str] = ...) -> None: ... + +class HoursRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class HoursResponse(_message.Message): + __slots__ = ("thresholds",) + THRESHOLDS_FIELD_NUMBER: _ClassVar[int] + thresholds: ComputeUnitThreshold + def __init__(self, thresholds: _Optional[_Union[ComputeUnitThreshold, _Mapping]] = ...) -> None: ... diff --git a/CLI/macos/drawthings/generated/imageService_pb2_grpc.py b/CLI/macos/drawthings/generated/imageService_pb2_grpc.py new file mode 100644 index 000000000..55d07036b --- /dev/null +++ b/CLI/macos/drawthings/generated/imageService_pb2_grpc.py @@ -0,0 +1,312 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from . import imageService_pb2 as imageService__pb2 + +GRPC_GENERATED_VERSION = '1.71.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in imageService_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class ImageGenerationServiceStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GenerateImage = channel.unary_stream( + '/ImageGenerationService/GenerateImage', + request_serializer=imageService__pb2.ImageGenerationRequest.SerializeToString, + response_deserializer=imageService__pb2.ImageGenerationResponse.FromString, + _registered_method=True) + self.FilesExist = channel.unary_unary( + '/ImageGenerationService/FilesExist', + request_serializer=imageService__pb2.FileListRequest.SerializeToString, + response_deserializer=imageService__pb2.FileExistenceResponse.FromString, + _registered_method=True) + self.UploadFile = channel.stream_stream( + '/ImageGenerationService/UploadFile', + request_serializer=imageService__pb2.FileUploadRequest.SerializeToString, + response_deserializer=imageService__pb2.UploadResponse.FromString, + _registered_method=True) + self.Echo = channel.unary_unary( + '/ImageGenerationService/Echo', + request_serializer=imageService__pb2.EchoRequest.SerializeToString, + response_deserializer=imageService__pb2.EchoReply.FromString, + _registered_method=True) + self.Pubkey = channel.unary_unary( + '/ImageGenerationService/Pubkey', + request_serializer=imageService__pb2.PubkeyRequest.SerializeToString, + response_deserializer=imageService__pb2.PubkeyResponse.FromString, + _registered_method=True) + self.Hours = channel.unary_unary( + '/ImageGenerationService/Hours', + request_serializer=imageService__pb2.HoursRequest.SerializeToString, + response_deserializer=imageService__pb2.HoursResponse.FromString, + _registered_method=True) + + +class ImageGenerationServiceServicer(object): + """Missing associated documentation comment in .proto file.""" + + def GenerateImage(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def FilesExist(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UploadFile(self, request_iterator, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Echo(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Pubkey(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Hours(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_ImageGenerationServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GenerateImage': grpc.unary_stream_rpc_method_handler( + servicer.GenerateImage, + request_deserializer=imageService__pb2.ImageGenerationRequest.FromString, + response_serializer=imageService__pb2.ImageGenerationResponse.SerializeToString, + ), + 'FilesExist': grpc.unary_unary_rpc_method_handler( + servicer.FilesExist, + request_deserializer=imageService__pb2.FileListRequest.FromString, + response_serializer=imageService__pb2.FileExistenceResponse.SerializeToString, + ), + 'UploadFile': grpc.stream_stream_rpc_method_handler( + servicer.UploadFile, + request_deserializer=imageService__pb2.FileUploadRequest.FromString, + response_serializer=imageService__pb2.UploadResponse.SerializeToString, + ), + 'Echo': grpc.unary_unary_rpc_method_handler( + servicer.Echo, + request_deserializer=imageService__pb2.EchoRequest.FromString, + response_serializer=imageService__pb2.EchoReply.SerializeToString, + ), + 'Pubkey': grpc.unary_unary_rpc_method_handler( + servicer.Pubkey, + request_deserializer=imageService__pb2.PubkeyRequest.FromString, + response_serializer=imageService__pb2.PubkeyResponse.SerializeToString, + ), + 'Hours': grpc.unary_unary_rpc_method_handler( + servicer.Hours, + request_deserializer=imageService__pb2.HoursRequest.FromString, + response_serializer=imageService__pb2.HoursResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'ImageGenerationService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('ImageGenerationService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class ImageGenerationService(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def GenerateImage(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/ImageGenerationService/GenerateImage', + imageService__pb2.ImageGenerationRequest.SerializeToString, + imageService__pb2.ImageGenerationResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def FilesExist(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/ImageGenerationService/FilesExist', + imageService__pb2.FileListRequest.SerializeToString, + imageService__pb2.FileExistenceResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UploadFile(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream( + request_iterator, + target, + '/ImageGenerationService/UploadFile', + imageService__pb2.FileUploadRequest.SerializeToString, + imageService__pb2.UploadResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def Echo(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/ImageGenerationService/Echo', + imageService__pb2.EchoRequest.SerializeToString, + imageService__pb2.EchoReply.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def Pubkey(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/ImageGenerationService/Pubkey', + imageService__pb2.PubkeyRequest.SerializeToString, + imageService__pb2.PubkeyResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def Hours(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/ImageGenerationService/Hours', + imageService__pb2.HoursRequest.SerializeToString, + imageService__pb2.HoursResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/main.py b/main.py index 958361b43..91a71c35e 100755 --- a/main.py +++ b/main.py @@ -11248,7 +11248,7 @@ async def generate_ai_image(prompt, size, quality, model, reference_images=None, if is_tudou_provider(provider): model = tudou_image_model_for_request(model) if is_draw_things_provider(provider): - from draw_things_grpc import draw_things_model_supports_editing + from CLI.macos.drawthings.draw_things_grpc import draw_things_model_supports_editing drawthings_references = [] drawthings_masks = [] @@ -11306,7 +11306,7 @@ async def generate_ai_image(prompt, size, quality, model, reference_images=None, detail="遮罩需要与一张输入图一起使用。", ) try: - from draw_things_grpc import generate_draw_things_image + from CLI.macos.drawthings.draw_things_grpc import generate_draw_things_image # The provider's saved host:port overrides environment defaults. request_options = { "endpoint": provider.get("base_url") or "", @@ -13679,7 +13679,7 @@ async def test_provider_connection(payload: TestConnectionPayload): } if protocol == "grpc": try: - from draw_things_grpc import list_draw_things_models + from CLI.macos.drawthings.draw_things_grpc import list_draw_things_models result = await list_draw_things_models(payload.base_url) except Exception as exc: result = {"connected": False, "models": [], "error": str(exc)} @@ -13944,7 +13944,7 @@ async def fetch_models_from_upstream(base_url: str, api_key: str, protocol: str return payload if protocol == "grpc": try: - from draw_things_grpc import list_draw_things_models + from CLI.macos.drawthings.draw_things_grpc import list_draw_things_models result = await list_draw_things_models(base_url) except Exception as exc: result = {"connected": False, "models": [], "error": str(exc)} From b10b9bd1f2894cc8578399b1dca587e6d58a7ccb Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Tue, 4 Aug 2026 22:52:48 +0800 Subject: [PATCH 11/20] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Agnes=20=E5=9B=BE?= =?UTF-8?q?=E7=94=9F=E8=A7=86=E9=A2=91=E5=8F=82=E8=80=83=E5=9B=BE=E5=85=AC?= =?UTF-8?q?=E7=BD=91=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 181 ++++++++++++++++++++++++++++++++++---- static/canvas.html | 2 +- static/js/canvas.js | 20 +++-- static/js/smart-canvas.js | 2 + static/smart-canvas.html | 2 +- 5 files changed, 180 insertions(+), 27 deletions(-) diff --git a/main.py b/main.py index 91a71c35e..06154a289 100755 --- a/main.py +++ b/main.py @@ -8812,7 +8812,7 @@ def public_media_url_suffix() -> str: def local_asset_public_url(value: str) -> str: text = str(value or "").strip() - if not text.startswith(("/output/", "/assets/")): + if not text.startswith(("/output/", "/assets/", "/api/storage-files/")): return "" if not output_file_from_url(text): return "" @@ -8857,6 +8857,36 @@ async def openai_video_proxy_public_reference_url(ref) -> str: ) raise HTTPException(status_code=400, detail=f"参考图不是公网 URL,无法传给上游:{text[:160]}") +def local_media_reference_path(ref_url: str) -> str: + """将画布引用中的本机 URL 还原为后端可读取的本地媒体 URL。""" + text = str(ref_url or "").strip() + if not text: + return "" + parsed = urllib.parse.urlsplit(text) + if parsed.scheme in {"http", "https"}: + host = (parsed.hostname or "").lower() + is_local_host = ( + host in {"127.0.0.1", "localhost", "::1"} + or re.match(r"^(192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)", host) + or host.endswith(".local") + ) + if not is_local_host: + return "" + text = urllib.parse.unquote(parsed.path or "") + if parsed.path == "/api/media-preview": + nested = urllib.parse.parse_qs(parsed.query).get("url", [""])[0] + text = urllib.parse.unquote(nested or "") + elif parsed.scheme == "file": + text = urllib.parse.unquote(parsed.path or "") + elif parsed.scheme: + return "" + if parsed.path == "/api/media-preview": + nested = urllib.parse.parse_qs(parsed.query).get("url", [""])[0] + text = urllib.parse.unquote(nested or "") + if text.startswith(("/output/", "/assets/", "/api/storage-files/")): + return text + return "" + def openai_video_proxy_local_image_path(ref) -> str: raw = ref.get("url", "") if isinstance(ref, dict) else ref text = str(raw or "").strip() @@ -9448,11 +9478,12 @@ def local_media_path_for_cloud_upload(ref_url: str, allowed_prefixes=("image/", ref_url = str(ref_url or "").strip() if not ref_url: raise HTTPException(status_code=400, detail="没有可上传的媒体文件") - if ref_url.startswith("http://") or ref_url.startswith("https://"): - return "" - if not (ref_url.startswith("/output/") or ref_url.startswith("/assets/")): + local_ref = local_media_reference_path(ref_url) or ref_url + if not local_ref.startswith(("/output/", "/assets/", "/api/storage-files/")): + if ref_url.startswith(("http://", "https://")): + return "" raise HTTPException(status_code=400, detail="云端上传只支持画布里的本地图片或视频文件") - path = output_file_from_url(ref_url) + path = output_file_from_url(local_ref) if not path: raise HTTPException(status_code=404, detail="本地媒体文件不存在或已被删除") ct = content_type_for_path(path) @@ -9507,20 +9538,92 @@ async def upload_video_to_temp_sh(path: str, source_url: str) -> Dict[str, str]: except Exception as exc: raise HTTPException(status_code=502, detail=f"Temp.sh 上传异常:{exc}") from exc +async def upload_media_to_uguu(path: str, source_url: str) -> Dict[str, str]: + upload_url = os.getenv("UGUU_UPLOAD_URL", "https://uguu.se/upload.php").strip() or "https://uguu.se/upload.php" + ct = content_type_for_path(path) + try: + async with httpx.AsyncClient(timeout=httpx.Timeout(connect=20.0, read=600.0, write=600.0, pool=20.0), follow_redirects=True) as client: + with open(path, "rb") as fh: + files = {"files[]": (os.path.basename(path), fh, ct)} + response = await client.post(upload_url, files=files) + if not response.is_success: + raise HTTPException(status_code=response.status_code, detail=f"Uguu 上传失败:{response.text[:300]}") + try: + payload = response.json() + except Exception as exc: + raise HTTPException(status_code=502, detail=f"Uguu 返回了非 JSON 响应:{response.text[:300]}") from exc + items = payload.get("files") if isinstance(payload, dict) else None + direct_url = str((items[0] if isinstance(items, list) and items else {}).get("url") or "").strip() + if not re.match(r"^https?://", direct_url, re.I): + raise HTTPException(status_code=502, detail=f"Uguu 返回了无法识别的链接:{str(payload)[:300]}") + return {"url": direct_url, "source": source_url, "name": os.path.basename(path), "expires": "temporary", "service": "uguu"} + except HTTPException: + raise + except Exception as exc: + raise HTTPException(status_code=502, detail=f"Uguu 上传异常:{exc}") from exc + +async def verify_public_media_url(url: str, expected_content_type: str = "") -> Dict[str, str]: + parsed = urllib.parse.urlsplit(str(url or "").strip()) + if parsed.scheme not in {"http", "https"} or not parsed.netloc: + raise HTTPException(status_code=502, detail="上传服务返回的不是有效公网 URL") + expected = str(expected_content_type or "").lower().split(";", 1)[0].strip() + try: + async with httpx.AsyncClient( + timeout=httpx.Timeout(connect=20.0, read=60.0, write=30.0, pool=20.0), + follow_redirects=True, + headers={"User-Agent": "Infinite-Canvas/1.0"}, + ) as client: + async with client.stream("GET", str(url).strip()) as response: + if not response.is_success: + raise HTTPException(status_code=502, detail=f"公网媒体返回 HTTP {response.status_code}") + content_type = str(response.headers.get("content-type") or "").lower().split(";", 1)[0].strip() + if expected.startswith("image/"): + supported = {"image/png", "image/jpeg", "image/jpg", "image/webp"} + if content_type not in supported: + raise HTTPException(status_code=502, detail=f"公网地址返回的不是可用图片(Content-Type: {content_type or '未知'})") + elif expected.startswith("video/") and not content_type.startswith("video/"): + raise HTTPException(status_code=502, detail=f"公网地址返回的不是可用视频(Content-Type: {content_type or '未知'})") + first_chunk = b"" + async for chunk in response.aiter_bytes(): + first_chunk += chunk + if len(first_chunk) >= 32: + break + if expected.startswith("image/") and not first_chunk: + raise HTTPException(status_code=502, detail="公网地址返回了空图片") + return {"content_type": content_type, "status": str(response.status_code), "host": parsed.hostname or ""} + except HTTPException: + raise + except Exception as exc: + raise HTTPException(status_code=502, detail=f"公网媒体地址无法访问:{exc}") from exc + async def upload_local_video_to_cloud(ref_url: str, service: str = "auto") -> Dict[str, str]: ref_url = str(ref_url or "").strip() - if ref_url.startswith("http://") or ref_url.startswith("https://"): + local_ref = local_media_reference_path(ref_url) + if local_ref: + ref_url = local_ref + elif ref_url.startswith(("http://", "https://")): return {"url": ref_url, "source": ref_url, "service": "existing"} path = local_media_path_for_cloud_upload(ref_url) - service = str(service or os.getenv("CLOUD_VIDEO_UPLOAD_SERVICE", "auto") or "auto").strip().lower() - if service in {"litterbox", "catbox"}: - return await upload_video_to_litterbox(path, ref_url) - if service in {"temp", "temp.sh", "tempsh"}: - return await upload_video_to_temp_sh(path, ref_url) + requested_service = str(service or "").strip().lower() + service = requested_service if requested_service not in {"", "auto"} else str(os.getenv("CLOUD_VIDEO_UPLOAD_SERVICE", "auto") or "auto").strip().lower() + uploaders = { + "uguu": upload_media_to_uguu, + "litterbox": upload_video_to_litterbox, + "catbox": upload_video_to_litterbox, + "temp": upload_video_to_temp_sh, + "temp.sh": upload_video_to_temp_sh, + "tempsh": upload_video_to_temp_sh, + } + if service in uploaders: + result = await uploaders[service](path, ref_url) + result.update(await verify_public_media_url(result.get("url", ""), content_type_for_path(path))) + return result errors = [] - for name, func in (("litterbox", upload_video_to_litterbox), ("temp.sh", upload_video_to_temp_sh)): + for name, func in (("uguu", upload_media_to_uguu), ("litterbox", upload_video_to_litterbox), ("temp.sh", upload_video_to_temp_sh)): try: - return await func(path, ref_url) + result = await func(path, ref_url) + result.update(await verify_public_media_url(result.get("url", ""), content_type_for_path(path))) + return result except HTTPException as exc: errors.append(f"{name}: {exc.detail}") raise HTTPException(status_code=502, detail="云端上传失败:" + ";".join(errors)) @@ -15055,13 +15158,55 @@ def agnes_video_frame_count(duration, fps=24): return min(441, max(9, 8 * n + 1)), frame_rate async def agnes_video_image_url(ref): - url = str(getattr(ref, "url", "") or "").strip() + if isinstance(ref, dict): + url = str(ref.get("url", "") or "").strip() + original_urls = [ref.get("originalLocalUrl", ""), ref.get("original_url", ""), ref.get("source_url", "")] + else: + url = str(getattr(ref, "url", "") or "").strip() + original_urls = [getattr(ref, "originalLocalUrl", ""), getattr(ref, "original_url", ""), getattr(ref, "source_url", "")] if not url: return "" - if url.startswith("http://") or url.startswith("https://"): - return url - uploaded = await upload_local_video_to_cloud(url, "auto") - return uploaded.get("url") or "" + + local_ref = "" + for value in original_urls: + local_ref = local_media_reference_path(value) + if local_ref: + break + local_ref = local_ref or local_media_reference_path(url) + remote_error = "" + if url.startswith(("http://", "https://")): + try: + await verify_public_media_url(url, "image/png") + return url + except HTTPException as exc: + remote_error = str(exc.detail) + + if local_ref: + upload_error = "" + try: + uploaded = await upload_local_video_to_cloud(local_ref, "auto") + uploaded_url = str((uploaded or {}).get("url") or "").strip() + if uploaded_url.startswith(("http://", "https://")): + return uploaded_url + except HTTPException as exc: + upload_error = str(exc.detail) + public_url = local_asset_public_url(local_ref) + if public_url: + try: + await verify_public_media_url(public_url, "image/png") + return public_url + except HTTPException as exc: + upload_error = f"{upload_error or '云端上传失败'};公网回源地址无效:{exc.detail}" + raise HTTPException( + status_code=400, + detail=f"Agnes 参考图无法转成可下载的公网图片:{upload_error[:200] or remote_error[:200] or '云端上传失败'}。请检查网络后重试。" + ) + + if url.startswith(("http://", "https://")): + raise HTTPException(status_code=400, detail=f"Agnes 参考图公网地址无效:{remote_error[:240] or '无法下载图片'}") + if url.startswith("data:image/"): + raise HTTPException(status_code=400, detail="Agnes 图生视频暂不接受 data:image;请使用画布中的本地图片或公网图片 URL") + raise HTTPException(status_code=400, detail=f"Agnes 参考图地址无法识别:{url[:160]}") async def wait_for_agnes_video_task(client, provider, video_id, model): base_url = video_api_root(provider) diff --git a/static/canvas.html b/static/canvas.html index 2b4d8709c..561a498da 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -352,6 +352,6 @@ - + diff --git a/static/js/canvas.js b/static/js/canvas.js index df9f7b631..5fd1eb20a 100644 --- a/static/js/canvas.js +++ b/static/js/canvas.js @@ -10496,7 +10496,13 @@ function generatedImageRefs(node){ const url = outputUrlValue(item); if(!url) return null; const kind = mediaKindForOutputItem(item); - return {url, name:outputImageName(url) || `${node.type || 'generated'}-${i + 1}`, kind, index:i}; + return { + url, + name:outputImageName(url) || `${node.type || 'generated'}-${i + 1}`, + kind, + originalLocalUrl:item?.originalLocalUrl || '', + index:i + }; }) .filter(Boolean) .filter(ref => keepGeneratedMedia || ref.kind === 'image') @@ -10509,20 +10515,20 @@ function mediaRefsFromNode(node){ if(!node) return []; if(node.type === 'image' && node.url){ const kind = mediaKindForNode(node); - return [{url:node.url, name:node.name || kind, role:node.role || '', kind}]; + return [{url:node.url, name:node.name || kind, role:node.role || '', kind, originalLocalUrl:node.originalLocalUrl || ''}]; } if(node.type === 'group'){ return (node.items || []) .map(id => nodes.find(x => x.id === id)) .filter(x => x?.type === 'image' && x?.url) - .map(item => ({url:item.url, name:item.name || mediaKindForNode(item), role:item.role || '', kind:mediaKindForNode(item)})); + .map(item => ({url:item.url, name:item.name || mediaKindForNode(item), role:item.role || '', kind:mediaKindForNode(item), originalLocalUrl:item.originalLocalUrl || ''})); } if(node.type === 'output'){ return (node.images || []).map((item, i) => { const url = outputUrlValue(item); if(!url) return null; const kind = mediaKindForOutputItem(item); - return {url, name:outputImageName(url) || `output-${i + 1}`, kind, nodeId:node.id, outputIndex:i}; + return {url, name:outputImageName(url) || `output-${i + 1}`, kind, originalLocalUrl:item?.originalLocalUrl || '', nodeId:node.id, outputIndex:i}; }).filter(Boolean); } if(CANVAS_MEDIA_OUTPUT_TYPES.includes(node.type)) return generatedImageRefs(node); @@ -10537,7 +10543,7 @@ function generatorSources(gen){ if(found){ const last = outputUrlValue(found.item); const kind = mediaKindForOutputItem(found.item); - return {id:n.id, type:'outputImage', label:'上游输出', preview:last, refs:[{url:last, name:'output.png', kind, nodeId:n.id, outputIndex:found.index}], prompt:''}; + return {id:n.id, type:'outputImage', label:'上游输出', preview:last, refs:[{url:last, name:'output.png', kind, originalLocalUrl:found.item?.originalLocalUrl || '', nodeId:n.id, outputIndex:found.index}], prompt:''}; } } if(CANVAS_MEDIA_OUTPUT_TYPES.includes(n.type)){ @@ -10555,7 +10561,7 @@ function generatorSources(gen){ } if(n.type === 'image' && n.url) { const kind = mediaKindForNode(n); - return {id:n.id, type:kind, label:n.name || kind, preview:n.url, refs:[{url:n.url, name:n.name || kind, role:n.role || '', kind}], prompt:''}; + return {id:n.id, type:kind, label:n.name || kind, preview:n.url, refs:[{url:n.url, name:n.name || kind, role:n.role || '', kind, originalLocalUrl:n.originalLocalUrl || ''}], prompt:''}; } if(n.type === 'group') { const items = (n.items || []).map(id => nodes.find(x => x.id === id)).filter(Boolean); @@ -10566,7 +10572,7 @@ function generatorSources(gen){ imageId:img.id, label:img.name || mediaKindForNode(img), preview:img.url, - refs:[{url:img.url, name:img.name || mediaKindForNode(img), role:img.role || '', kind:mediaKindForNode(img)}], + refs:[{url:img.url, name:img.name || mediaKindForNode(img), role:img.role || '', kind:mediaKindForNode(img), originalLocalUrl:img.originalLocalUrl || ''}], prompt:'' })); const prompts = items.filter(x => x.type === 'prompt').map(p => p.text || '').filter(Boolean); diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index e9bbc3b07..0aa243f26 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -15577,6 +15577,8 @@ async function runApiVideoGeneration(prompt, refs, runSettings=settings){ }; const refImages = imageRefsOnly(uploadedRefs).map((ref, i) => { const item = {url:effUrl(ref), name:ref.name || `图${i + 1}`}; + const originalLocalUrl = ref.originalLocalUrl || ref.sourceUrl || ''; + if(originalLocalUrl) item.originalLocalUrl = originalLocalUrl; if(runSettings.videoUseFrameRoles){ if(i === 0) item.role = 'first_frame'; else if(i === 1) item.role = 'last_frame'; diff --git a/static/smart-canvas.html b/static/smart-canvas.html index d64e91f13..0128952c9 100644 --- a/static/smart-canvas.html +++ b/static/smart-canvas.html @@ -437,6 +437,6 @@
preview
- + From 4c3348c74ed6c1013d4427b574bf333ae99f4c34 Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Thu, 6 Aug 2026 11:06:16 +0800 Subject: [PATCH 12/20] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20Qwen=20Edit=202511?= =?UTF-8?q?=20=E7=94=9F=E5=9B=BE=E9=A2=84=E8=AE=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLI/macos/drawthings/draw_things_grpc.py | 9 ++++++++- static/js/smart-canvas.js | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CLI/macos/drawthings/draw_things_grpc.py b/CLI/macos/drawthings/draw_things_grpc.py index 387c1ec6f..818273976 100644 --- a/CLI/macos/drawthings/draw_things_grpc.py +++ b/CLI/macos/drawthings/draw_things_grpc.py @@ -87,13 +87,14 @@ def _build_configuration( is_klein_4b = "flux_2_klein_4b" in model_name is_klein = "klein" in model_name is_z_image = "z_image" in model_name or "zimage" in model_name + is_qwen_edit = "qwen" in model_name and "edit" in model_name config = config_generated.GenerationConfigurationT() config.model = model config.startWidth = width // 64 config.startHeight = height // 64 default_steps = "8" if (is_z_image or is_klein_9b) else "4" config.steps = int(os.getenv("DRAW_THINGS_GRPC_STEPS", default_steps)) - default_guidance = "1.0" if (is_klein or is_z_image) else "3.5" + default_guidance = "1.0" if (is_klein or is_z_image or is_qwen_edit) else "3.5" config.guidanceScale = float( os.getenv("DRAW_THINGS_GRPC_GUIDANCE", default_guidance) ) @@ -154,6 +155,12 @@ def _build_configuration( config.maskBlur = 2.5 config.speedUpWithGuidanceEmbed = False config.guidanceEmbed = 0.0 + elif is_qwen_edit: + config.sampler = 17 # SamplerType.UniPCTrailing + config.seedMode = 2 # SeedMode.ScaleAlike + config.shift = 3.0 + config.resolutionDependentShift = False + config.maskBlur = 2.5 builder = flatbuffers.Builder(0) builder.Finish(config.Pack(builder)) diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 0aa243f26..100b7496f 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -2495,8 +2495,8 @@ function drawThingsLoraCompatibility(runSettings=settings){ if(!currentFamily || !loraFamily || currentFamily === loraFamily) return ''; return trf('smart.drawThingsLoraIncompatible', {model:currentFamily, lora:loraFamily}); } -async function refreshSmartDrawThingsModels(){ - const provider = drawThingsProvider(); +async function refreshSmartDrawThingsModels(providerId=settings.provider_id){ + const provider = drawThingsProvider(providerId); if(!provider) return; try { const data = await fetch('/api/drawthings/models').then(response => response.json()); @@ -4295,6 +4295,9 @@ function setDynamicSetting(key, value){ persistActiveSmartSettings(); rememberRecentSmartSettings(settings, activeSettingsSubject()); if(layoutKeys.has(key)) renderDynamicParams(); + if(key === 'provider_id' && isDrawThingsProvider(settings.provider_id)) { + void refreshSmartDrawThingsModels(settings.provider_id); + } scheduleSave(); } function closeAllSmartPopovers(){ @@ -4317,7 +4320,12 @@ function bindDynamicParams(){ const ctrl = pill.parentElement; const wasPinned = ctrl.classList.contains('pinned'); closeAllSmartPopovers(); - if(!wasPinned) ctrl.classList.add('pinned'); + if(!wasPinned) { + ctrl.classList.add('pinned'); + if(ctrl.classList.contains('drawthings-lora-control')) { + void refreshSmartDrawThingsModels(settings.provider_id); + } + } }; }); dynamicParams.querySelectorAll('[data-smart-param]').forEach(btn => { @@ -4565,7 +4573,8 @@ async function loadConfig(){ // 提供商配置已就绪即先渲染参数面板,避免等工作流/RunningHub 预取完成后参数才「突然刷新出来」。 sanitizeSmartApiSelection(settings); updateProviderModels(); - if(apiProviders.some(provider => isDrawThingsProvider(provider.id))) void refreshSmartDrawThingsModels(); + const drawThings = apiProviders.find(provider => isDrawThingsProvider(provider.id)); + if(drawThings) void refreshSmartDrawThingsModels(drawThings.id); const wf = await fetch('/api/workflows').then(r => r.json()).catch(() => ({workflows:[]})); comfyWorkflows = Array.isArray(wf.workflows) ? wf.workflows : []; runningHubWorkflowCache = {}; From ece3d39608e7b3fdda67c4e56bf71ac0f095966f Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Fri, 7 Aug 2026 00:51:42 +0800 Subject: [PATCH 13/20] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20Klein=20=E7=94=9F?= =?UTF-8?q?=E5=9B=BE=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLI/macos/drawthings/draw_things_grpc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CLI/macos/drawthings/draw_things_grpc.py b/CLI/macos/drawthings/draw_things_grpc.py index 818273976..c04a451b6 100644 --- a/CLI/macos/drawthings/draw_things_grpc.py +++ b/CLI/macos/drawthings/draw_things_grpc.py @@ -92,7 +92,9 @@ def _build_configuration( config.model = model config.startWidth = width // 64 config.startHeight = height // 64 - default_steps = "8" if (is_z_image or is_klein_9b) else "4" + # Draw Things' Klein 9B preset uses four DDIM-trailing steps. Keeping it + # at eight steps doubles the diffusion work for the same request. + default_steps = "8" if is_z_image else "4" config.steps = int(os.getenv("DRAW_THINGS_GRPC_STEPS", default_steps)) default_guidance = "1.0" if (is_klein or is_z_image or is_qwen_edit) else "3.5" config.guidanceScale = float( From 7c411fc43e316dab63be6743c61b74cde90d0f40 Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Fri, 7 Aug 2026 22:15:19 +0800 Subject: [PATCH 14/20] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89Klein=E6=A8=A1=E5=9E=8B=E7=9A=84=E5=A4=9A=E5=9B=BE?= =?UTF-8?q?=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/js/smart-canvas.js | 17 +- .../custom/Flux2-Klein-custom.config.json | 137 +++++ workflows/custom/Flux2-Klein-custom.json | 527 ++++++++++++++++++ 3 files changed, 678 insertions(+), 3 deletions(-) create mode 100644 workflows/custom/Flux2-Klein-custom.config.json create mode 100644 workflows/custom/Flux2-Klein-custom.json diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index dbbb7706d..e246717d9 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -3397,7 +3397,7 @@ function renderComfyParams(){ ${settings.editUpscale ? renderUpscalePill('editUpscaleRes', Number(settings.editUpscaleRes || 2048)) : ''}`; } else { const wf = comfyWorkflowCache[settings.comfyWorkflow]; - const fields = (wf?.config?.fields || []).filter(f => comfyFieldKind(f) === 'setting'); + const fields = (wf?.config?.fields || []).filter(f => comfyFieldKind(f) === 'setting' && !f.auto); html += renderComfyWorkflowControl(); html += fields.length ? fields.map(renderComfySettingField).join('') : (settings.comfyWorkflow ? '' : `
${escapeHtml(tr('smart.noWorkflow'))}
`); } @@ -16039,7 +16039,10 @@ async function generateComfyUrlsWithSettings(runSettings, prompt, refs){ await assignMediaFields(fields.filter(f => comfyFieldKind(f) === 'video'), videoRefsOnly(allRefs)); await assignMediaFields(fields.filter(f => comfyFieldKind(f) === 'audio'), audioRefsOnly(allRefs)); fields.filter(f => comfyFieldKind(f) === 'setting').forEach(field => { - if(comfyRandomEnabledField(field) && smartComfyRandomActiveFor(runSettings, field.id)){ + const autoValue = comfyAutoFieldValue(field, imageRefs.length); + if(autoValue !== undefined){ + values[field.id] = autoValue; + } else if(comfyRandomEnabledField(field) && smartComfyRandomActiveFor(runSettings, field.id)){ values[field.id] = smartComfyRandomValue(field); } else { values[field.id] = runSettings.comfyParams?.[field.id] ?? field.default; @@ -16868,6 +16871,11 @@ async function runPromptLLMNode(nodeId){ render(); } } +function comfyAutoFieldValue(field, imageCount=0){ + if(field?.auto !== 'image_count') return undefined; + const minimum = Number(field.image_count_min ?? 1); + return imageCount >= (Number.isFinite(minimum) ? minimum : 1); +} function comfyFieldKind(field){ if(['image','video','audio'].includes(field?.type)) return field.type; const key = `${field?.input || ''} ${field?.name || ''}`.toLowerCase(); @@ -17252,7 +17260,10 @@ async function runComfyGeneration(node, prompt, refs, pendingNode, meta, runSett await assignMediaFields(fields.filter(f => comfyFieldKind(f) === 'video'), videoRefsOnly(allRefs)); await assignMediaFields(fields.filter(f => comfyFieldKind(f) === 'audio'), audioRefsOnly(allRefs)); fields.filter(f => comfyFieldKind(f) === 'setting').forEach(field => { - if(comfyRandomEnabledField(field) && smartComfyRandomActiveFor(runSettings, field.id)){ + const autoValue = comfyAutoFieldValue(field, refs.length); + if(autoValue !== undefined){ + values[field.id] = autoValue; + } else if(comfyRandomEnabledField(field) && smartComfyRandomActiveFor(runSettings, field.id)){ values[field.id] = smartComfyRandomValue(field); } else { values[field.id] = runSettings.comfyParams?.[field.id] ?? field.default; diff --git a/workflows/custom/Flux2-Klein-custom.config.json b/workflows/custom/Flux2-Klein-custom.config.json new file mode 100644 index 000000000..f84ac3bdc --- /dev/null +++ b/workflows/custom/Flux2-Klein-custom.config.json @@ -0,0 +1,137 @@ +{ + "title": "custom/Flux2-Klein-custom", + "fields": [ + { + "id": "f_klein_prompt", + "node": "168", + "input": "text", + "name": "提示词", + "type": "textarea", + "default": "", + "min": null, + "max": null, + "step": null, + "options": [], + "random_enabled": false + }, + { + "id": "f_klein_image_1", + "node": "278", + "input": "image", + "name": "输入图像 1", + "type": "image", + "default": "", + "min": null, + "max": null, + "step": null, + "options": [], + "random_enabled": false + }, + { + "id": "f_klein_image_2", + "node": "270", + "input": "image", + "name": "输入图像 2", + "type": "image", + "default": "", + "min": null, + "max": null, + "step": null, + "options": [], + "random_enabled": false + }, + { + "id": "f_klein_image_3", + "node": "292", + "input": "image", + "name": "输入图像 3", + "type": "image", + "default": "", + "min": null, + "max": null, + "step": null, + "options": [], + "random_enabled": false + }, + { + "id": "f_klein_multi_2", + "node": "313", + "input": "value", + "name": "启用第二张输入图", + "type": "boolean", + "default": false, + "auto": "image_count", + "image_count_min": 2, + "min": null, + "max": null, + "step": null, + "options": [], + "random_enabled": false + }, + { + "id": "f_klein_multi_3", + "node": "314", + "input": "value", + "name": "启用第三张输入图", + "type": "boolean", + "default": false, + "auto": "image_count", + "image_count_min": 3, + "min": null, + "max": null, + "step": null, + "options": [], + "random_enabled": false + }, + { + "id": "f_zbe4wlu", + "node": "320", + "input": "lora_name", + "name": "LoRA 模型", + "type": "dropdown", + "default": "Klein一致性增强.safetensors", + "min": null, + "max": null, + "step": null, + "options": [ + "None", + "Klein-W万物迁移.safetensors", + "Flux2-Klein-9B-consistency-V2.safetensors", + "realistic(Flux2-Klein-9B-Enhanced-Details).safetensors", + "Klein2-9B-jhuangswap-SmartCharacterSwap.safetensors" + ], + "random_enabled": false + }, + { + "id": "f_m50c4ff", + "node": "320", + "input": "strength_model", + "name": "模型强度", + "type": "number", + "default": 0.5, + "min": 0.0, + "max": 2.0, + "step": 0.1, + "options": [], + "random_enabled": false + } + ], + "mini_cards": { + "prompt": { + "x": 24, + "y": 30 + }, + "image": { + "x": 24, + "y": 210 + }, + "custom": { + "x": 280, + "y": 78 + }, + "output": { + "x": 540, + "y": 120 + } + } +} diff --git a/workflows/custom/Flux2-Klein-custom.json b/workflows/custom/Flux2-Klein-custom.json new file mode 100644 index 000000000..e2b31aaf5 --- /dev/null +++ b/workflows/custom/Flux2-Klein-custom.json @@ -0,0 +1,527 @@ +{ + "151": { + "inputs": { + "sampler_name": "euler" + }, + "class_type": "KSamplerSelect", + "_meta": { + "title": "K采样器选择" + } + }, + "152": { + "inputs": { + "steps": 4, + "width": [ + "157", + 1 + ], + "height": [ + "157", + 1 + ] + }, + "class_type": "Flux2Scheduler", + "_meta": { + "title": "Flux2Scheduler" + } + }, + "153": { + "inputs": { + "cfg": 1, + "model": [ + "320", + 0 + ], + "positive": [ + "307", + 0 + ], + "negative": [ + "308", + 0 + ] + }, + "class_type": "CFGGuider", + "_meta": { + "title": "CFG引导" + } + }, + "154": { + "inputs": { + "noise": [ + "158", + 0 + ], + "guider": [ + "153", + 0 + ], + "sampler": [ + "151", + 0 + ], + "sigmas": [ + "152", + 0 + ], + "latent_image": [ + "156", + 0 + ] + }, + "class_type": "SamplerCustomAdvanced", + "_meta": { + "title": "自定义采样器(高级)" + } + }, + "155": { + "inputs": { + "samples": [ + "154", + 0 + ], + "vae": [ + "174", + 0 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE解码" + } + }, + "156": { + "inputs": { + "width": [ + "157", + 0 + ], + "height": [ + "157", + 1 + ], + "batch_size": 1 + }, + "class_type": "EmptyFlux2LatentImage", + "_meta": { + "title": "Empty Flux 2 Latent" + } + }, + "157": { + "inputs": { + "image": [ + "291", + 0 + ] + }, + "class_type": "GetImageSize", + "_meta": { + "title": "获取图像尺寸" + } + }, + "158": { + "inputs": { + "noise_seed": 1046852288816614 + }, + "class_type": "RandomNoise", + "_meta": { + "title": "随机噪波" + } + }, + "159": { + "inputs": { + "conditioning": [ + "168", + 0 + ], + "latent": [ + "162", + 0 + ] + }, + "class_type": "ReferenceLatent", + "_meta": { + "title": "参考Latent" + } + }, + "160": { + "inputs": { + "conditioning": [ + "167", + 0 + ], + "latent": [ + "162", + 0 + ] + }, + "class_type": "ReferenceLatent", + "_meta": { + "title": "参考Latent" + } + }, + "162": { + "inputs": { + "pixels": [ + "291", + 0 + ], + "vae": [ + "174", + 0 + ] + }, + "class_type": "VAEEncode", + "_meta": { + "title": "VAE编码" + } + }, + "164": { + "inputs": { + "pixels": [ + "271", + 0 + ], + "vae": [ + "174", + 0 + ] + }, + "class_type": "VAEEncode", + "_meta": { + "title": "VAE编码" + } + }, + "165": { + "inputs": { + "conditioning": [ + "159", + 0 + ], + "latent": [ + "164", + 0 + ] + }, + "class_type": "ReferenceLatent", + "_meta": { + "title": "参考Latent" + } + }, + "166": { + "inputs": { + "conditioning": [ + "160", + 0 + ], + "latent": [ + "164", + 0 + ] + }, + "class_type": "ReferenceLatent", + "_meta": { + "title": "参考Latent" + } + }, + "167": { + "inputs": { + "conditioning": [ + "168", + 0 + ] + }, + "class_type": "ConditioningZeroOut", + "_meta": { + "title": "条件零化" + } + }, + "168": { + "inputs": { + "text": "", + "clip": [ + "316", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP文本编码器" + } + }, + "174": { + "inputs": { + "vae_name": "flux2-vae.safetensors" + }, + "class_type": "VAELoader", + "_meta": { + "title": "VAE加载器" + } + }, + "178": { + "inputs": { + "pixels": [ + "294", + 0 + ], + "vae": [ + "174", + 0 + ] + }, + "class_type": "VAEEncode", + "_meta": { + "title": "VAE编码" + } + }, + "179": { + "inputs": { + "conditioning": [ + "165", + 0 + ], + "latent": [ + "178", + 0 + ] + }, + "class_type": "ReferenceLatent", + "_meta": { + "title": "参考Latent" + } + }, + "180": { + "inputs": { + "conditioning": [ + "166", + 0 + ], + "latent": [ + "178", + 0 + ] + }, + "class_type": "ReferenceLatent", + "_meta": { + "title": "参考Latent" + } + }, + "270": { + "inputs": { + "image": "05-1.jpg" + }, + "class_type": "LoadImage", + "_meta": { + "title": "加载图像" + } + }, + "271": { + "inputs": { + "upscale_method": "lanczos", + "megapixels": 1, + "resolution_steps": 1, + "image": [ + "270", + 0 + ] + }, + "class_type": "ImageScaleToTotalPixels", + "_meta": { + "title": "图像按像素缩放" + } + }, + "278": { + "inputs": { + "image": "1 (4).jpg" + }, + "class_type": "LoadImage", + "_meta": { + "title": "加载图像" + } + }, + "291": { + "inputs": { + "upscale_method": "lanczos", + "megapixels": 1, + "resolution_steps": 1, + "image": [ + "278", + 0 + ] + }, + "class_type": "ImageScaleToTotalPixels", + "_meta": { + "title": "图像按像素缩放" + } + }, + "292": { + "inputs": { + "image": "05-1.jpg" + }, + "class_type": "LoadImage", + "_meta": { + "title": "加载图像" + } + }, + "294": { + "inputs": { + "upscale_method": "lanczos", + "megapixels": 1, + "resolution_steps": 1, + "image": [ + "292", + 0 + ] + }, + "class_type": "ImageScaleToTotalPixels", + "_meta": { + "title": "图像按像素缩放" + } + }, + "305": { + "inputs": { + "switch": [ + "313", + 0 + ], + "on_false": [ + "160", + 0 + ], + "on_true": [ + "166", + 0 + ] + }, + "class_type": "ComfySwitchNode", + "_meta": { + "title": "Switch" + } + }, + "306": { + "inputs": { + "switch": [ + "313", + 0 + ], + "on_false": [ + "159", + 0 + ], + "on_true": [ + "165", + 0 + ] + }, + "class_type": "ComfySwitchNode", + "_meta": { + "title": "Switch" + } + }, + "307": { + "inputs": { + "switch": [ + "314", + 0 + ], + "on_false": [ + "306", + 0 + ], + "on_true": [ + "179", + 0 + ] + }, + "class_type": "ComfySwitchNode", + "_meta": { + "title": "Switch" + } + }, + "308": { + "inputs": { + "switch": [ + "314", + 0 + ], + "on_false": [ + "305", + 0 + ], + "on_true": [ + "180", + 0 + ] + }, + "class_type": "ComfySwitchNode", + "_meta": { + "title": "Switch" + } + }, + "313": { + "inputs": { + "value": false + }, + "class_type": "PrimitiveBoolean", + "_meta": { + "title": "布尔值2" + } + }, + "314": { + "inputs": { + "value": false + }, + "class_type": "PrimitiveBoolean", + "_meta": { + "title": "布尔值3" + } + }, + "315": { + "inputs": { + "filename_prefix": "ComfyUI", + "images": [ + "155", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "保存图像" + } + }, + "316": { + "inputs": { + "clip_name": "Qwen3-8B-int4-ConvRot.safetensors", + "type": "flux2", + "device": "default" + }, + "class_type": "CLIPLoader", + "_meta": { + "title": "加载CLIP" + } + }, + "317": { + "inputs": { + "unet_name": "flux-2-klein-9b-int8-ConvRot-comfyui.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "UNet加载器" + } + }, + "320": { + "inputs": { + "lora_name": "Klein一致性增强.safetensors", + "strength_model": 0.5, + "model": [ + "317", + 0 + ] + }, + "class_type": "LoraLoaderModelOnly", + "_meta": { + "title": "LoRA加载器(仅模型)" + } + } +} \ No newline at end of file From 106a65acb90dea46c66d9fac47bf1507cd3a9f81 Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Sun, 9 Aug 2026 00:58:41 +0800 Subject: [PATCH 15/20] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=BF=9C=E7=AB=AF=20Co?= =?UTF-8?q?mfyUI=20=E7=BB=93=E6=9E=9C=E5=9B=9E=E4=BC=A0=E8=B6=85=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 181 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 152 insertions(+), 29 deletions(-) diff --git a/main.py b/main.py index a3398ee4d..7fef38b9c 100755 --- a/main.py +++ b/main.py @@ -593,9 +593,16 @@ def load_env_file(): IMAGE_POLL_INTERVAL = float(os.getenv("IMAGE_POLL_INTERVAL", "2")) IMAGE_TASK_TIMEOUT = float(os.getenv("IMAGE_TASK_TIMEOUT", str(AI_REQUEST_TIMEOUT))) COMFYUI_HISTORY_TIMEOUT = int(float(os.getenv("COMFYUI_HISTORY_TIMEOUT", "1800"))) -# 下载 ComfyUI 产物的 socket 超时(秒,作用于连接和每次 read)。没有它时一次网络卡顿会让 urlopen 永久挂起, -# 导致 generate() 不返回、画布卡片一直转圈拿不到结果。给得足够大以容纳大视频/大图的正常下载。 -COMFYUI_DOWNLOAD_TIMEOUT = float(os.getenv("COMFYUI_DOWNLOAD_TIMEOUT", "120")) +# Cloudflare Access TCP 转发可能比局域网后端有更高的连接延迟,所有远端请求都必须有明确的单次超时。 +COMFYUI_BACKEND_CHECK_TIMEOUT = float(os.getenv("COMFYUI_BACKEND_CHECK_TIMEOUT", "5")) +COMFYUI_HTTP_TIMEOUT = float(os.getenv("COMFYUI_HTTP_TIMEOUT", "30")) +COMFYUI_HISTORY_REQUEST_TIMEOUT = float(os.getenv("COMFYUI_HISTORY_REQUEST_TIMEOUT", "15")) +# 下载 ComfyUI 产物的 socket 超时(秒,作用于连接和每次 read)。结果通过 Cloudflare 隧道回传时, +# 连接可能会短暂重置,因此下载层会自动重试,而不是直接把远端 URL 交给前端再次下载。 +COMFYUI_DOWNLOAD_TIMEOUT = float(os.getenv("COMFYUI_DOWNLOAD_TIMEOUT", "300")) +COMFYUI_DOWNLOAD_RETRIES = max(1, int(float(os.getenv("COMFYUI_DOWNLOAD_RETRIES", "3")))) +COMFYUI_DOWNLOAD_RETRY_DELAY = max(0.0, float(os.getenv("COMFYUI_DOWNLOAD_RETRY_DELAY", "1"))) +COMFYUI_UPLOAD_TIMEOUT = float(os.getenv("COMFYUI_UPLOAD_TIMEOUT", "60")) APIMART_IMAGE_TASK_TIMEOUT = float(os.getenv("APIMART_IMAGE_TASK_TIMEOUT", "1800")) APIMART_IMAGE_POLL_INTERVAL = float(os.getenv("APIMART_IMAGE_POLL_INTERVAL", "5")) APIMART_IMAGE_INITIAL_POLL_DELAY = float(os.getenv("APIMART_IMAGE_INITIAL_POLL_DELAY", "10")) @@ -3223,7 +3230,7 @@ def check_images_exist(backend_addr, images): for img in images: try: url = f"http://{backend_addr}/view?filename={urllib.parse.quote(img)}&type=input" - r = requests.get(url, stream=True, timeout=0.5) + r = requests.get(url, stream=True, timeout=COMFYUI_BACKEND_CHECK_TIMEOUT) r.close() if r.status_code != 200: return False except: return False @@ -3296,7 +3303,11 @@ def get_best_backend(required_images: List[str] = None): for addr in COMFYUI_INSTANCES: try: - with urllib.request.urlopen(f"http://{addr}/queue", timeout=1) as response: + request = urllib.request.Request( + f"http://{addr}/queue", + headers={"Connection": "close", "Cache-Control": "no-cache"}, + ) + with urllib.request.urlopen(request, timeout=COMFYUI_BACKEND_CHECK_TIMEOUT) as response: data = json.loads(response.read()) remote_load = len(data.get('queue_running', [])) + len(data.get('queue_pending', [])) with LOAD_LOCK: @@ -3323,7 +3334,11 @@ def reserve_best_backend(required_images: List[str] = None): backend_stats = {} for addr in COMFYUI_INSTANCES: try: - with urllib.request.urlopen(f"http://{addr}/queue", timeout=1) as response: + request = urllib.request.Request( + f"http://{addr}/queue", + headers={"Connection": "close", "Cache-Control": "no-cache"}, + ) + with urllib.request.urlopen(request, timeout=COMFYUI_BACKEND_CHECK_TIMEOUT) as response: data = json.loads(response.read()) remote_load = len(data.get('queue_running', [])) + len(data.get('queue_pending', [])) has_images = check_images_exist(addr, required_images) @@ -3345,13 +3360,78 @@ def reserve_best_backend(required_images: List[str] = None): # --- 辅助工具 --- +def _download_comfy_file_atomic(full_url, local_path, validate_image=False, alternate_urls=None): + """Download a ComfyUI file atomically with retries for tunnel resets.""" + directory = os.path.dirname(local_path) or "." + urls = [str(full_url)] + for alternate_url in alternate_urls or []: + if alternate_url and str(alternate_url) not in urls: + urls.append(str(alternate_url)) + last_error = None + + for attempt in range(COMFYUI_DOWNLOAD_RETRIES): + for url in urls: + temp_path = "" + try: + prefix = f".{os.path.basename(local_path)}." + fd, temp_path = tempfile.mkstemp(prefix=prefix, suffix=".part", dir=directory) + total_bytes = 0 + request = urllib.request.Request( + url, + headers={"Connection": "close", "Cache-Control": "no-cache"}, + ) + with os.fdopen(fd, "wb") as out_file: + with urllib.request.urlopen(request, timeout=COMFYUI_DOWNLOAD_TIMEOUT) as response: + expected_length = response.headers.get("Content-Length") + try: + expected_length = int(expected_length) if expected_length else None + except (TypeError, ValueError): + expected_length = None + while True: + chunk = response.read(1024 * 1024) + if not chunk: + break + out_file.write(chunk) + total_bytes += len(chunk) + out_file.flush() + os.fsync(out_file.fileno()) + if expected_length is not None and total_bytes != expected_length: + raise IOError( + f"ComfyUI 文件下载不完整:收到 {total_bytes} 字节,期望 {expected_length} 字节" + ) + if total_bytes <= 0: + raise IOError("ComfyUI 返回了空文件") + if validate_image: + with Image.open(temp_path) as image: + image.verify() + os.replace(temp_path, local_path) + temp_path = "" + return + except Exception as exc: + last_error = exc + finally: + if temp_path: + try: + os.remove(temp_path) + except OSError: + pass + if attempt + 1 < COMFYUI_DOWNLOAD_RETRIES: + time.sleep(COMFYUI_DOWNLOAD_RETRY_DELAY) + + raise last_error or IOError("ComfyUI 文件下载失败") + def download_image(comfy_address, comfy_url_path, prefix="studio_"): filename = f"{prefix}{uuid.uuid4().hex[:10]}.png" local_path = output_path_for(filename, "output") full_url = f"http://{comfy_address}{comfy_url_path}" try: - with urllib.request.urlopen(full_url, timeout=COMFYUI_DOWNLOAD_TIMEOUT) as response, open(local_path, 'wb') as out_file: - shutil.copyfileobj(response, out_file) + alternate_url = full_url.replace("/view?", "/api/view?", 1) + _download_comfy_file_atomic( + full_url, + local_path, + validate_image=True, + alternate_urls=[alternate_url], + ) return output_url_for(filename, "output") except Exception as e: print(f"下载图片失败: {e}") @@ -3416,15 +3496,18 @@ def download_comfy_output(comfy_address, item, prefix="studio_"): file_type = urllib.parse.quote(str(item.get("type") or "output")) comfy_url_path = f"/view?filename={urllib.parse.quote(str(item['filename']))}&subfolder={subfolder}&type={file_type}" full_url = f"http://{comfy_address}{comfy_url_path}" + alternate_url = full_url.replace("/view?", "/api/view?", 1) try: - with urllib.request.urlopen(full_url, timeout=COMFYUI_DOWNLOAD_TIMEOUT) as response, open(local_path, 'wb') as out_file: - shutil.copyfileobj(response, out_file) + _download_comfy_file_atomic( + full_url, + local_path, + validate_image=ext in {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff"}, + alternate_urls=[alternate_url], + ) return output_url_for(filename, "output") except Exception as e: print(f"下载 ComfyUI 输出失败: {e}") - if comfy_url_path.startswith("/view"): - return comfy_url_path.replace("/view", "/api/view", 1) - return full_url + raise RuntimeError(f"ComfyUI 输出回传失败:{e}") from e def save_comfy_text_output(value, prefix="studio_", name=""): text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, indent=2) @@ -3507,7 +3590,11 @@ def save_to_history(record): def get_comfy_history(comfy_address, prompt_id): try: - with urllib.request.urlopen(f"http://{comfy_address}/history/{prompt_id}") as response: + request = urllib.request.Request( + f"http://{comfy_address}/history/{prompt_id}", + headers={"Connection": "close", "Cache-Control": "no-cache"}, + ) + with urllib.request.urlopen(request, timeout=COMFYUI_HISTORY_REQUEST_TIMEOUT) as response: return json.loads(response.read()) except Exception as e: return {} @@ -7054,10 +7141,7 @@ def smart_owned_result_items(images: List[Any], paths: List[str]) -> List[Any]: item for item in images if isinstance(item, dict) and item.get("loopInputPreview") is not True - and ( - item.get("generatedResult") is True - or any(json_references_media_path(item, path) for path in paths) - ) + and item.get("generatedResult") is True ] def expand_canvas_generated_media_paths(canvas: Dict[str, Any], paths: List[str]) -> List[str]: @@ -12248,20 +12332,31 @@ async def upload_image(files: List[UploadFile] = File(...)): for file, content in files_content: success_count = 0 last_result = None + upload_errors = [] for addr in COMFYUI_INSTANCES: try: files_data = {'image': (file.filename, content, file.content_type)} - response = requests.post(f"http://{addr}/upload/image", files=files_data, timeout=5) + response = requests.post( + f"http://{addr}/upload/image", + files=files_data, + timeout=(10, COMFYUI_UPLOAD_TIMEOUT), + ) if response.status_code == 200: last_result = response.json() success_count += 1 + else: + upload_errors.append(f"{addr}: HTTP {response.status_code}") except Exception as e: print(f"Upload error for {addr}: {e}") + upload_errors.append(f"{addr}: {e}") if success_count > 0 and last_result: uploaded_files.append({"comfy_name": last_result.get("name", file.filename)}) else: - raise HTTPException(status_code=500, detail="Failed to upload to any backend") + detail = "Failed to upload to any backend" + if upload_errors: + detail += f": {'; '.join(upload_errors[:3])}" + raise HTTPException(status_code=502, detail=detail) return {"files": uploaded_files} @@ -12304,8 +12399,7 @@ async def upload_ai_reference(files: List[UploadFile] = File(...)): ext = ".bin" filename = f"ai_ref_{uuid.uuid4().hex[:12]}{ext}" path = output_path_for(filename, "input") - with open(path, "wb") as f: - f.write(content) + write_uploaded_content_atomic(path, content) uploaded.append({"url": output_url_for(filename, "input"), "name": file.filename or filename, "kind": kind, "mime": content_type}) return {"files": uploaded} @@ -12337,8 +12431,7 @@ async def upload_ai_base64(payload: Base64UploadRequest): kind, ext = "image", ".png" filename = f"ai_ref_{uuid.uuid4().hex[:12]}{ext}" path = output_path_for(filename, "input") - with open(path, "wb") as f: - f.write(content) + write_uploaded_content_atomic(path, content) return {"files": [{"url": output_url_for(filename, "input"), "name": payload.name or filename, "kind": kind}]} @app.post("/api/comfyui/upload-base64") @@ -12391,6 +12484,25 @@ def _local_upload_kind_ext(filename, content_type): return "image", ext return None, ext +def write_uploaded_content_atomic(path: str, content: bytes): + directory = os.path.dirname(path) or "." + prefix = f".{os.path.basename(path)}." + temp_path = "" + try: + fd, temp_path = tempfile.mkstemp(prefix=prefix, suffix=".part", dir=directory) + with os.fdopen(fd, "wb") as handle: + handle.write(content) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temp_path, path) + temp_path = "" + finally: + if temp_path: + try: + os.remove(temp_path) + except OSError: + pass + def _local_upload_display_name(filename): # 文件名形如 up__<原始名>;去掉前缀还原展示名 base = os.path.basename(str(filename or "")) @@ -17866,7 +17978,6 @@ def remove_log_record(): reset_node_ids = [] if payload.reset_referencing_nodes and candidate_paths: - candidate_paths = expand_canvas_generated_media_paths(canvas, candidate_paths) reset_node_ids = reset_canvas_result_nodes_for_media(canvas, candidate_paths) canvas["logs"] = [item for item in logs if str(item.get("id") or "") != log_id] @@ -18854,14 +18965,24 @@ def generate(req: GenerateRequest): p = {"prompt": workflow, "client_id": CLIENT_ID} data = json.dumps(p).encode('utf-8') try: - post_req = urllib.request.Request(f"http://{target_backend}/prompt", data=data) - prompt_id = json.loads(urllib.request.urlopen(post_req, timeout=10).read())['prompt_id'] + post_req = urllib.request.Request( + f"http://{target_backend}/prompt", + data=data, + headers={ + "Content-Type": "application/json", + "Connection": "close", + }, + ) + prompt_id = json.loads( + urllib.request.urlopen(post_req, timeout=COMFYUI_HTTP_TIMEOUT).read() + )['prompt_id'] except urllib.error.HTTPError as e: error_body = e.read().decode('utf-8') raise Exception(comfy_prompt_error_message(e.code, error_body)) history_data = None - for i in range(COMFYUI_HISTORY_TIMEOUT): + history_deadline = time.monotonic() + COMFYUI_HISTORY_TIMEOUT + while time.monotonic() < history_deadline: try: res = get_comfy_history(target_backend, prompt_id) if prompt_id in res: @@ -18869,7 +18990,7 @@ def generate(req: GenerateRequest): break except Exception: pass - time.sleep(1) + time.sleep(min(1, max(0, history_deadline - time.monotonic()))) if not history_data: raise Exception("ComfyUI 渲染超时") @@ -19005,6 +19126,8 @@ class WorkflowField(BaseModel): step: Optional[float] = None options: List[str] = [] random_enabled: bool = False + auto: str = "" + image_count_min: Optional[int] = None class WorkflowConfig(BaseModel): title: str = "" From f6a8b53a8a35b6b96701d0b38edcbd652dfe6830 Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Sun, 9 Aug 2026 01:04:46 +0800 Subject: [PATCH 16/20] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=BB=8A=E6=97=A5=20Co?= =?UTF-8?q?mfyUI=20gRPC=20=E4=B8=8E=E5=B7=A5=E4=BD=9C=E6=B5=81=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLI/macos/drawthings/draw_things_grpc.py | 47 +- static/angle.html | 18 +- static/api-settings.html | 14 +- static/asset-manager.html | 10 +- static/canvas-list.html | 16 +- static/canvas.html | 18 +- static/comfyui-settings.html | 14 +- static/enhance.html | 16 +- static/gpt-chat.html | 12 +- static/index.html | 28 +- static/js/canvas.js | 89 ++- static/js/smart-canvas.js | 15 +- static/klein.html | 16 +- static/online.html | 16 +- static/smart-canvas.html | 12 +- static/zimage.html | 14 +- workflows/2511.json | 15 +- workflows/Flux2-Klein.json | 74 +- workflows/LTX-API_GGUF.json | 716 +++++++++++++++++ workflows/Z-Image-Enhance.json | 746 +++++++++--------- workflows/Z-Image.json | 97 +-- .../custom/Flux2-Klein-custom.config.json | 39 +- ...4\215\346\224\276\345\244\247.config.json" | 92 +++ ...\345\244\215\346\224\276\345\244\247.json" | 185 +++++ ...ux-2-klein-4B-360-erp-outpaint.config.json | 82 ++ .../flux-2-klein-4B-360-erp-outpaint.json | 255 ++++++ 26 files changed, 2008 insertions(+), 648 deletions(-) create mode 100644 workflows/LTX-API_GGUF.json create mode 100644 "workflows/custom/klein9b-\351\253\230\346\270\205\344\277\256\345\244\215\346\224\276\345\244\247.config.json" create mode 100644 "workflows/custom/klein9b-\351\253\230\346\270\205\344\277\256\345\244\215\346\224\276\345\244\247.json" create mode 100644 workflows/flux-2-klein-4B-360-erp-outpaint.config.json create mode 100644 workflows/flux-2-klein-4B-360-erp-outpaint.json diff --git a/CLI/macos/drawthings/draw_things_grpc.py b/CLI/macos/drawthings/draw_things_grpc.py index c04a451b6..b35de1cee 100644 --- a/CLI/macos/drawthings/draw_things_grpc.py +++ b/CLI/macos/drawthings/draw_things_grpc.py @@ -7,6 +7,7 @@ import os import secrets import struct +import time from pathlib import Path from urllib.parse import urlsplit @@ -203,11 +204,43 @@ def _reference_image_bytes(reference: object) -> bytes: ) path = next((candidate for candidate in candidates if candidate.is_file()), candidates[0]) try: - return path.read_bytes() + last_data = b"" + for attempt in range(3): + before = path.stat() + last_data = path.read_bytes() + after = path.stat() + if before.st_size == after.st_size == len(last_data): + return last_data + if attempt < 2: + time.sleep(0.05) + return last_data except OSError as exc: raise ValueError(f"参考图无法读取:{path}") from exc +def _open_reference_image(reference: object, label: str): + from PIL import Image + + raw = _reference_image_bytes(reference) + source_name = "" + if isinstance(reference, dict): + source_name = str( + reference.get("path") + or reference.get("url") + or reference.get("name") + or "" + ).strip() + try: + with Image.open(io.BytesIO(raw)) as source: + source.load() + return source.copy() + except OSError as exc: + raise ValueError( + f"{label}文件损坏或不完整:{source_name or '未知文件'}," + f"读取到 {len(raw)} 字节;{exc}" + ) from exc + + def _resize_crop_reference(image, width: int, height: int): """Match Draw Things' resize-then-center-crop behavior.""" from PIL import Image @@ -252,10 +285,9 @@ def _encode_image_for_request(reference: object, width: int, height: int) -> byt import numpy as np from PIL import Image - raw = _reference_image_bytes(reference) - with Image.open(io.BytesIO(raw)) as source: - image = _resize_crop_reference(source, width, height) - pixels = np.asarray(image, dtype=np.float32) / 255.0 * 2.0 - 1.0 + source = _open_reference_image(reference, "输入图") + image = _resize_crop_reference(source, width, height) + pixels = np.asarray(image, dtype=np.float32) / 255.0 * 2.0 - 1.0 # This is Draw Things' 68-byte CCV header and HWC FP16 image payload. # It is an image input, not a HintProto. @@ -282,9 +314,8 @@ def _encode_mask_for_request(reference: object, width: int, height: int) -> byte """Encode a black/white mask as Draw Things' NCHW 8-bit tensor.""" from PIL import Image - raw = _reference_image_bytes(reference) - with Image.open(io.BytesIO(raw)) as source: - image = _resize_crop_mask(source, width, height) + source = _open_reference_image(reference, "遮罩图") + image = _resize_crop_mask(source, width, height) # Draw Things uses 0 for retained pixels and 2 for pixels redrawn with # the configured img2img strength. This matches the ComfyUI node's mask diff --git a/static/angle.html b/static/angle.html index 7f2a7c314..64c379dab 100644 --- a/static/angle.html +++ b/static/angle.html @@ -17,21 +17,21 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/api-settings.html b/static/api-settings.html index da2474839..f56fdf0ae 100644 --- a/static/api-settings.html +++ b/static/api-settings.html @@ -16,12 +16,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -568,6 +568,6 @@
- + diff --git a/static/asset-manager.html b/static/asset-manager.html index 043505a6b..d5cc381f5 100644 --- a/static/asset-manager.html +++ b/static/asset-manager.html @@ -16,10 +16,10 @@ } catch(e) {} })(); - - - - + + + +
@@ -47,6 +47,6 @@
- + diff --git a/static/canvas-list.html b/static/canvas-list.html index f639bcbdb..79f78b2f3 100644 --- a/static/canvas-list.html +++ b/static/canvas-list.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -90,6 +90,6 @@
- + diff --git a/static/canvas.html b/static/canvas.html index 36bc31fe7..4e9ba8665 100644 --- a/static/canvas.html +++ b/static/canvas.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - - - + + + + + + +
@@ -353,7 +353,7 @@
- - + + diff --git a/static/comfyui-settings.html b/static/comfyui-settings.html index 2eacc0248..dfb18a113 100644 --- a/static/comfyui-settings.html +++ b/static/comfyui-settings.html @@ -15,12 +15,12 @@ } catch(e) {} })(); - - - - - - + + + + + +
@@ -122,6 +122,6 @@ preview
- + diff --git a/static/enhance.html b/static/enhance.html index 54caa5b03..ce2f57029 100644 --- a/static/enhance.html +++ b/static/enhance.html @@ -17,14 +17,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + diff --git a/static/gpt-chat.html b/static/gpt-chat.html index 6f6521a5e..339aff3e7 100644 --- a/static/gpt-chat.html +++ b/static/gpt-chat.html @@ -16,13 +16,13 @@ } catch(e) {} })(); - - - - - + + + + + - + diff --git a/static/online.html b/static/online.html index 39ee13f48..f18696af3 100644 --- a/static/online.html +++ b/static/online.html @@ -16,14 +16,14 @@ } catch(e) {} })(); - - - - - - + + + + + + - + - + diff --git a/workflows/2511.json b/workflows/2511.json index 14531af16..4c369572b 100644 --- a/workflows/2511.json +++ b/workflows/2511.json @@ -2,6 +2,7 @@ "1": { "inputs": { "strength": 1, + "pre_cfg": false, "model": [ "2", 0 @@ -28,12 +29,6 @@ "3": { "inputs": { "prompt": "", - "speak_and_recognation": { - "__value__": [ - false, - true - ] - }, "clip": [ "87", 0 @@ -71,12 +66,6 @@ "11": { "inputs": { "prompt": "将摄像机向右旋转45度", - "speak_and_recognation": { - "__value__": [ - false, - true - ] - }, "clip": [ "87", 0 @@ -242,7 +231,7 @@ }, "86": { "inputs": { - "unet_name": "qwen_image_edit_2511_fp8_e4m3fn.safetensors", + "unet_name": "qwen_image_edit_2511_int8_convrot.safetensors", "weight_dtype": "default" }, "class_type": "UNETLoader", diff --git a/workflows/Flux2-Klein.json b/workflows/Flux2-Klein.json index 25c6b7076..e2b31aaf5 100644 --- a/workflows/Flux2-Klein.json +++ b/workflows/Flux2-Klein.json @@ -29,7 +29,7 @@ "inputs": { "cfg": 1, "model": [ - "296", + "320", 0 ], "positive": [ @@ -238,15 +238,9 @@ }, "168": { "inputs": { - "text": "改为夜晚", - "speak_and_recognation": { - "__value__": [ - false, - true - ] - }, + "text": "", "clip": [ - "295", + "316", 0 ] }, @@ -384,33 +378,6 @@ "title": "图像按像素缩放" } }, - "295": { - "inputs": { - "model_name1": "qwen_3_8b_fp8mixed.safetensors", - "model_name2": "None", - "model_name3": "None", - "type": "stable_diffusion", - "key_opt": "", - "mode": "Auto", - "device": "default" - }, - "class_type": "LoadTextEncoderShared //Inspire", - "_meta": { - "title": "Shared Text Encoder Loader (Inspire)" - } - }, - "296": { - "inputs": { - "model_name": "flux-2-klein-9b-fp8.safetensors", - "weight_dtype": "default", - "key_opt": "", - "mode": "Auto" - }, - "class_type": "LoadDiffusionModelShared //Inspire", - "_meta": { - "title": "Shared Diffusion Model Loader (Inspire)" - } - }, "305": { "inputs": { "switch": [ @@ -521,5 +488,40 @@ "_meta": { "title": "保存图像" } + }, + "316": { + "inputs": { + "clip_name": "Qwen3-8B-int4-ConvRot.safetensors", + "type": "flux2", + "device": "default" + }, + "class_type": "CLIPLoader", + "_meta": { + "title": "加载CLIP" + } + }, + "317": { + "inputs": { + "unet_name": "flux-2-klein-9b-int8-ConvRot-comfyui.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "UNet加载器" + } + }, + "320": { + "inputs": { + "lora_name": "Klein一致性增强.safetensors", + "strength_model": 0.5, + "model": [ + "317", + 0 + ] + }, + "class_type": "LoraLoaderModelOnly", + "_meta": { + "title": "LoRA加载器(仅模型)" + } } } \ No newline at end of file diff --git a/workflows/LTX-API_GGUF.json b/workflows/LTX-API_GGUF.json new file mode 100644 index 000000000..87e02e286 --- /dev/null +++ b/workflows/LTX-API_GGUF.json @@ -0,0 +1,716 @@ +{ + "100": { + "inputs": { + "sigmas": "0.909375, 0.725, 0.421875, 0.0" + }, + "class_type": "ManualSigmas", + "_meta": { + "title": "ManualSigmas (LTX 2.0)" + } + }, + "103": { + "inputs": { + "cfg": 1, + "model": [ + "342", + 0 + ], + "positive": [ + "107", + 0 + ], + "negative": [ + "107", + 1 + ] + }, + "class_type": "CFGGuider", + "_meta": { + "title": "CFG引导器" + } + }, + "107": { + "inputs": { + "frame_rate": [ + "285", + 0 + ], + "positive": [ + "121", + 0 + ], + "negative": [ + "110", + 0 + ] + }, + "class_type": "LTXVConditioning", + "_meta": { + "title": "LTXV条件" + } + }, + "108": { + "inputs": { + "width": [ + "163", + 0 + ], + "height": [ + "163", + 1 + ], + "length": [ + "287", + 1 + ], + "batch_size": 1 + }, + "class_type": "EmptyLTXVLatentVideo", + "_meta": { + "title": "空Latent视频(LTXV)" + } + }, + "109": { + "inputs": { + "video_latent": [ + "161", + 0 + ], + "audio_latent": [ + "199", + 0 + ] + }, + "class_type": "LTXVConcatAVLatent", + "_meta": { + "title": "LTXVConcatAVLatent" + } + }, + "110": { + "inputs": { + "text": "blurry, oversaturated, pixelated, low resolution, grainy, distorted, noise, compression artifacts, jpeg artifacts, glitches, watermark, text, logo, signature, copyright, subtitles, distorted sound, saturated sound, loud", + "clip": [ + "346", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP文本编码" + } + }, + "113": { + "inputs": { + "noise": [ + "115", + 0 + ], + "guider": [ + "129", + 0 + ], + "sampler": [ + "137", + 0 + ], + "sigmas": [ + "360", + 0 + ], + "latent_image": [ + "109", + 0 + ] + }, + "class_type": "SamplerCustomAdvanced", + "_meta": { + "title": "自定义采样器(高级)" + } + }, + "114": { + "inputs": { + "noise_seed": 420 + }, + "class_type": "RandomNoise", + "_meta": { + "title": "随机噪波" + } + }, + "115": { + "inputs": { + "noise_seed": 43 + }, + "class_type": "RandomNoise", + "_meta": { + "title": "随机噪波" + } + }, + "116": { + "inputs": { + "av_latent": [ + "113", + 0 + ] + }, + "class_type": "LTXVSeparateAVLatent", + "_meta": { + "title": "LTXV分离音视频潜空间" + } + }, + "117": { + "inputs": { + "video_latent": [ + "160", + 0 + ], + "audio_latent": [ + "116", + 1 + ] + }, + "class_type": "LTXVConcatAVLatent", + "_meta": { + "title": "LTXVConcatAVLatent" + } + }, + "118": { + "inputs": { + "samples": [ + "116", + 0 + ], + "upscale_model": [ + "189", + 0 + ], + "vae": [ + "184", + 0 + ] + }, + "class_type": "LTXVLatentUpsampler", + "_meta": { + "title": "spatial" + } + }, + "119": { + "inputs": { + "noise": [ + "114", + 0 + ], + "guider": [ + "103", + 0 + ], + "sampler": [ + "138", + 0 + ], + "sigmas": [ + "359", + 0 + ], + "latent_image": [ + "117", + 0 + ] + }, + "class_type": "SamplerCustomAdvanced", + "_meta": { + "title": "自定义采样器(高级)" + } + }, + "121": { + "inputs": { + "text": "4K/高分辨率,电影级质感,特写镜头。一位戴黑框眼镜的亚裔男性,深色西装,在专业访谈场景中。柔和的伦勃朗光照,浅景深背景虚化,强调其面部微表情的真实感。他认真讲述,肢体语言自然流畅,捕捉到眼神的专注、嘴型同步的细腻变化。画面沉稳、逼真,光影勾勒出成熟与真诚的气质。\n 他讲的内容:\"我们人上去要拆的时候,日本人就说了,就是说在日本的法绿上这个也不行,那个也不行。\"\n", + "clip": [ + "346", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP文本编码" + } + }, + "125": { + "inputs": { + "av_latent": [ + "119", + 0 + ] + }, + "class_type": "LTXVSeparateAVLatent", + "_meta": { + "title": "LTXV分离音视频潜空间" + } + }, + "127": { + "inputs": { + "tile_size": 512, + "overlap": 64, + "temporal_size": 4096, + "temporal_overlap": 8, + "samples": [ + "125", + 0 + ], + "vae": [ + "184", + 0 + ] + }, + "class_type": "VAEDecodeTiled", + "_meta": { + "title": "VAE解码(分块)" + } + }, + "129": { + "inputs": { + "cfg": 1, + "model": [ + "342", + 0 + ], + "positive": [ + "107", + 0 + ], + "negative": [ + "107", + 1 + ] + }, + "class_type": "CFGGuider", + "_meta": { + "title": "CFG引导器" + } + }, + "137": { + "inputs": { + "sampler_name": "lcm" + }, + "class_type": "KSamplerSelect", + "_meta": { + "title": "K采样器选择" + } + }, + "138": { + "inputs": { + "sampler_name": "euler_cfg_pp" + }, + "class_type": "KSamplerSelect", + "_meta": { + "title": "K采样器选择" + } + }, + "140": { + "inputs": { + "frame_rate": [ + "285", + 0 + ], + "loop_count": 0, + "filename_prefix": "LTX-2", + "format": "video/h264-mp4", + "pix_fmt": "yuv420p", + "crf": 19, + "save_metadata": true, + "trim_to_audio": false, + "pingpong": false, + "save_output": true, + "images": [ + "127", + 0 + ], + "audio": [ + "201", + 0 + ] + }, + "class_type": "VHS_VideoCombine", + "_meta": { + "title": "Video Combine 🎥🅥🅗🅢" + } + }, + "160": { + "inputs": { + "strength": 1, + "bypass": false, + "vae": [ + "184", + 0 + ], + "image": [ + "246", + 0 + ], + "latent": [ + "118", + 0 + ] + }, + "class_type": "LTXVImgToVideoInplace", + "_meta": { + "title": "LTXV图像转视频(原地)" + } + }, + "161": { + "inputs": { + "strength": 1, + "bypass": false, + "vae": [ + "184", + 0 + ], + "image": [ + "162", + 0 + ], + "latent": [ + "108", + 0 + ] + }, + "class_type": "LTXVImgToVideoInplace", + "_meta": { + "title": "LTXV图像转视频(原地)" + } + }, + "162": { + "inputs": { + "img_compression": 33, + "image": [ + "246", + 0 + ] + }, + "class_type": "LTXVPreprocess", + "_meta": { + "title": "LTXV预处理" + } + }, + "163": { + "inputs": { + "image": [ + "164", + 0 + ] + }, + "class_type": "GetImageSize", + "_meta": { + "title": "获取图像尺寸" + } + }, + "164": { + "inputs": { + "resize_type": "scale by multiplier", + "resize_type.multiplier": 0.5, + "scale_method": "area", + "input": [ + "165", + 0 + ] + }, + "class_type": "ResizeImageMaskNode", + "_meta": { + "title": "调整图像/掩码大小" + } + }, + "165": { + "inputs": { + "width": [ + "292", + 0 + ], + "height": [ + "293", + 0 + ], + "upscale_method": "nearest-exact", + "keep_proportion": "crop", + "pad_color": "0, 0, 0", + "crop_position": "center", + "divisible_by": 32, + "device": "cpu", + "image": [ + "167", + 0 + ] + }, + "class_type": "ImageResizeKJv2", + "_meta": { + "title": "Resize Image v2" + } + }, + "167": { + "inputs": { + "image": "郝鹏合成图.png" + }, + "class_type": "LoadImage", + "_meta": { + "title": "加载图像" + } + }, + "184": { + "inputs": { + "vae_name": "LTX23_video_vae_bf16.safetensors" + }, + "class_type": "VAELoader", + "_meta": { + "title": "Load VAE (video VAE)" + } + }, + "189": { + "inputs": { + "model_name": "ltx-2.3-spatial-upscaler-x2-1.1.safetensors" + }, + "class_type": "LatentUpscaleModelLoader", + "_meta": { + "title": "加载Latent放大模型" + } + }, + "196": { + "inputs": { + "vae_name": "LTX23_audio_vae_bf16.safetensors", + "device": "main_device", + "weight_dtype": "bf16" + }, + "class_type": "VAELoaderKJ", + "_meta": { + "title": "VAELoader KJ (audio VAE)" + } + }, + "199": { + "inputs": { + "frames_number": [ + "287", + 1 + ], + "frame_rate": [ + "311", + 1 + ], + "batch_size": 1, + "audio_vae": [ + "196", + 0 + ] + }, + "class_type": "LTXVEmptyLatentAudio", + "_meta": { + "title": "LTXV 空音频潜空间" + } + }, + "201": { + "inputs": { + "samples": [ + "125", + 1 + ], + "audio_vae": [ + "196", + 0 + ] + }, + "class_type": "LTXVAudioVAEDecode", + "_meta": { + "title": "LTXV音频VAE解码" + } + }, + "206": { + "inputs": { + "steps": 8, + "max_shift": 2.05, + "base_shift": 0.95, + "stretch": true, + "terminal": 0.1, + "latent": [ + "109", + 0 + ] + }, + "class_type": "LTXVScheduler", + "_meta": { + "title": "LTXVScheduler (for more steps)" + } + }, + "246": { + "inputs": { + "longer_edge": 1536, + "images": [ + "165", + 0 + ] + }, + "class_type": "ResizeImagesByLongerEdge", + "_meta": { + "title": "缩放图像(长边)" + } + }, + "285": { + "inputs": { + "value": 24 + }, + "class_type": "PrimitiveFloat", + "_meta": { + "title": "FPS" + } + }, + "287": { + "inputs": { + "expression": "1+ 8*(round(a*b)/8)" + }, + "class_type": "SimpleCalculatorKJ", + "_meta": { + "title": "SimpleCalculatorKJ" + } + }, + "291": { + "inputs": { + "value": 8 + }, + "class_type": "INTConstant", + "_meta": { + "title": "LENGTH (in seconds)" + } + }, + "292": { + "inputs": { + "value": 720 + }, + "class_type": "INTConstant", + "_meta": { + "title": "WIDTH" + } + }, + "293": { + "inputs": { + "value": 405 + }, + "class_type": "INTConstant", + "_meta": { + "title": "HEIGHT" + } + }, + "301": { + "inputs": { + "PowerLoraLoaderHeaderWidget": { + "type": "PowerLoraLoaderHeaderWidget" + }, + "➕ Add Lora": "", + "model": [ + "332", + 0 + ] + }, + "class_type": "Power Lora Loader (rgthree)", + "_meta": { + "title": "Power Lora Loader (rgthree)" + } + }, + "311": { + "inputs": { + "expression": "a", + "variables.a": [ + "285", + 0 + ] + }, + "class_type": "SimpleCalculatorKJ", + "_meta": { + "title": "SimpleCalculatorKJ" + } + }, + "332": { + "inputs": { + "chunks": 2, + "dim_threshold": 4096, + "model": [ + "361", + 0 + ] + }, + "class_type": "LTXVChunkFeedForward", + "_meta": { + "title": "LTXV Chunk FeedForward (for low VRAM)" + } + }, + "337": { + "inputs": { + "preview_rate": 8, + "model": [ + "301", + 0 + ] + }, + "class_type": "LTX2SamplingPreviewOverride", + "_meta": { + "title": "LTX2 Sampling Preview Override" + } + }, + "342": { + "inputs": { + "nag_scale": 11, + "nag_alpha": 0.25, + "nag_tau": 2.5, + "inplace": true, + "model": [ + "337", + 0 + ], + "nag_cond_video": [ + "107", + 1 + ], + "nag_cond_audio": [ + "107", + 1 + ] + }, + "class_type": "LTX2_NAG", + "_meta": { + "title": "LTX2 NAG" + } + }, + "346": { + "inputs": { + "clip_name1": "gemma-3-12b-it-Q4_K_M.gguf", + "clip_name2": "ltx-2.3_text_projection_bf16.safetensors", + "type": "ltxv" + }, + "class_type": "DualCLIPLoaderGGUF", + "_meta": { + "title": "CLIP GGUF (Gemma + text projection)" + } + }, + "359": { + "inputs": { + "sigmas": "0.85, 0.7250, 0.4219, 0.0" + }, + "class_type": "ManualSigmas", + "_meta": { + "title": "自定义Sigmas" + } + }, + "360": { + "inputs": { + "sigmas": "1.0, 0.99375, 0.9875, 0.98125, 0.975, 0.909375, 0.725, 0.421875, 0.0" + }, + "class_type": "ManualSigmas", + "_meta": { + "title": "自定义Sigmas" + } + }, + "361": { + "inputs": { + "unet_name": "ltx-2.3-22b-distilled-1.1_transformer_only_int8_convrot.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "UNet加载器" + } + } +} \ No newline at end of file diff --git a/workflows/Z-Image-Enhance.json b/workflows/Z-Image-Enhance.json index 1ff82e349..798e25ee2 100644 --- a/workflows/Z-Image-Enhance.json +++ b/workflows/Z-Image-Enhance.json @@ -1,383 +1,365 @@ -{ - "15": { - "inputs": { - "image": "pasted/image (794).png" - }, - "class_type": "LoadImage", - "_meta": { - "title": "加载图像" - } - }, - "23": { - "inputs": { - "text": "丰富的细节", - "speak_and_recognation": { - "__value__": [ - false, - true - ] - }, - "clip": [ - "34", - 0 - ] - }, - "class_type": "CLIPTextEncode", - "_meta": { - "title": "CLIP文本编码器" - } - }, - "24": { - "inputs": { - "conditioning": [ - "23", - 0 - ] - }, - "class_type": "ConditioningZeroOut", - "_meta": { - "title": "条件零化" - } - }, - "27": { - "inputs": { - "vae_name": "ae.safetensors" - }, - "class_type": "VAELoader", - "_meta": { - "title": "VAE加载器" - } - }, - "33": { - "inputs": { - "model_name": "z_image_turbo_bf16.safetensors", - "weight_dtype": "default", - "key_opt": "", - "mode": "Auto" - }, - "class_type": "LoadDiffusionModelShared //Inspire", - "_meta": { - "title": "Shared Diffusion Model Loader (Inspire)" - } - }, - "34": { - "inputs": { - "model_name1": "qwen_3_4b.safetensors", - "model_name2": "None", - "model_name3": "None", - "type": "stable_diffusion", - "key_opt": "", - "mode": "Auto", - "device": "default" - }, - "class_type": "LoadTextEncoderShared //Inspire", - "_meta": { - "title": "Shared Text Encoder Loader (Inspire)" - } - }, - "142": { - "inputs": { - "pixels": [ - "15", - 0 - ], - "vae": [ - "27", - 0 - ] - }, - "class_type": "VAEEncode", - "_meta": { - "title": "VAE编码" - } - }, - "146": { - "inputs": { - "seed": 279629946795727, - "steps": 10, - "cfg": 0.7, - "sampler_name": "euler", - "scheduler": "sgm_uniform", - "denoise": [ - "202", - 0 - ], - "model": [ - "166", - 0 - ], - "positive": [ - "23", - 0 - ], - "negative": [ - "24", - 0 - ], - "latent_image": [ - "142", - 0 - ] - }, - "class_type": "KSampler", - "_meta": { - "title": "K采样器" - } - }, - "147": { - "inputs": { - "samples": [ - "146", - 0 - ], - "vae": [ - "27", - 0 - ] - }, - "class_type": "VAEDecode", - "_meta": { - "title": "VAE解码" - } - }, - "164": { - "inputs": { - "preprocessor": "DepthAnythingV2Preprocessor", - "resolution": 1024, - "image": [ - "15", - 0 - ] - }, - "class_type": "AIO_Preprocessor", - "_meta": { - "title": "Aux集成预处理器" - } - }, - "165": { - "inputs": { - "name": "Z-Image-Turbo-Fun-Controlnet-Union.safetensors" - }, - "class_type": "ModelPatchLoader", - "_meta": { - "title": "加载模型补丁" - } - }, - "166": { - "inputs": { - "strength": 0.8, - "model": [ - "33", - 0 - ], - "model_patch": [ - "165", - 0 - ], - "vae": [ - "27", - 0 - ], - "image": [ - "164", - 0 - ] - }, - "class_type": "QwenImageDiffsynthControlnet", - "_meta": { - "title": "QwenImageDiffsynthControlnet" - } - }, - "174": { - "inputs": { - "filename_prefix": "ComfyUI", - "images": [ - "180", - 0 - ] - }, - "class_type": "SaveImage", - "_meta": { - "title": "保存图像" - } - }, - "180": { - "inputs": { - "samples": [ - "181", - 0 - ], - "vae": [ - "27", - 0 - ] - }, - "class_type": "VAEDecode", - "_meta": { - "title": "VAE解码" - } - }, - "181": { - "inputs": { - "seed": 820960346993579, - "steps": 10, - "cfg": 1, - "sampler_name": "euler_cfg_pp", - "scheduler": "simple", - "denoise": [ - "202", - 0 - ], - "model": [ - "33", - 0 - ], - "positive": [ - "23", - 0 - ], - "negative": [ - "24", - 0 - ], - "latent_image": [ - "186", - 0 - ] - }, - "class_type": "KSampler", - "_meta": { - "title": "K采样器" - } - }, - "184": { - "inputs": { - "seed": 882274830236035, - "strength": [ - "202", - 0 - ], - "image": [ - "15", - 0 - ] - }, - "class_type": "ImageAddNoise", - "_meta": { - "title": "图像添加噪声" - } - }, - "186": { - "inputs": { - "pixels": [ - "189", - 0 - ], - "vae": [ - "27", - 0 - ] - }, - "class_type": "VAEEncode", - "_meta": { - "title": "VAE编码" - } - }, - "189": { - "inputs": { - "blend_factor": [ - "202", - 0 - ], - "blend_mode": "multiply", - "image1": [ - "191", - 0 - ], - "image2": [ - "184", - 0 - ] - }, - "class_type": "ImageBlend", - "_meta": { - "title": "图像混合" - } - }, - "191": { - "inputs": { - "sharpen_radius": 1, - "sigma": 0.5, - "alpha": 0.5, - "image": [ - "147", - 0 - ] - }, - "class_type": "ImageSharpen", - "_meta": { - "title": "图像锐化" - } - }, - "193": { - "inputs": { - "image": [ - "15", - 0 - ] - }, - "class_type": "GetImageSize+", - "_meta": { - "title": "获取图像尺寸" - } - }, - "201": { - "inputs": { - "expression": "(a+b)*c/10000", - "speak_and_recognation": { - "__value__": [ - false, - true - ] - }, - "a": [ - "193", - 0 - ], - "b": [ - "193", - 1 - ], - "c": [ - "204", - 0 - ] - }, - "class_type": "MathExpression|pysssss", - "_meta": { - "title": "数学表达式" - } - }, - "202": { - "inputs": { - "output_type": "float", - "*": [ - "201", - 1 - ] - }, - "class_type": "easy convertAnything", - "_meta": { - "title": "转换任何" - } - }, - "204": { - "inputs": { - "value": 0.5 - }, - "class_type": "FloatConstant", - "_meta": { - "title": "浮点常量" - } - } +{ + "15": { + "inputs": { + "image": "03bf24f5aea63cc83f77eb93b23ecdc4.png" + }, + "class_type": "LoadImage", + "_meta": { + "title": "加载图像" + } + }, + "23": { + "inputs": { + "text": "丰富的细节", + "clip": [ + "206", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP文本编码器" + } + }, + "24": { + "inputs": { + "conditioning": [ + "23", + 0 + ] + }, + "class_type": "ConditioningZeroOut", + "_meta": { + "title": "条件零化" + } + }, + "27": { + "inputs": { + "vae_name": "z_image_turbo_vae.safetensors" + }, + "class_type": "VAELoader", + "_meta": { + "title": "VAE加载器" + } + }, + "142": { + "inputs": { + "pixels": [ + "15", + 0 + ], + "vae": [ + "27", + 0 + ] + }, + "class_type": "VAEEncode", + "_meta": { + "title": "VAE编码" + } + }, + "146": { + "inputs": { + "seed": 702670365108595, + "steps": 10, + "cfg": 0.7, + "sampler_name": "euler", + "scheduler": "sgm_uniform", + "denoise": [ + "202", + 0 + ], + "model": [ + "166", + 0 + ], + "positive": [ + "23", + 0 + ], + "negative": [ + "24", + 0 + ], + "latent_image": [ + "142", + 0 + ] + }, + "class_type": "KSampler", + "_meta": { + "title": "K采样器" + } + }, + "147": { + "inputs": { + "samples": [ + "146", + 0 + ], + "vae": [ + "27", + 0 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE解码" + } + }, + "164": { + "inputs": { + "preprocessor": "DepthAnythingV2Preprocessor", + "resolution": 1024, + "image": [ + "15", + 0 + ] + }, + "class_type": "AIO_Preprocessor", + "_meta": { + "title": "Aux集成预处理器" + } + }, + "165": { + "inputs": { + "name": "Z-Image-Turbo-Fun-Controlnet-Union-2.1.safetensors" + }, + "class_type": "ModelPatchLoader", + "_meta": { + "title": "加载模型补丁" + } + }, + "166": { + "inputs": { + "strength": 0.8, + "model": [ + "205", + 0 + ], + "model_patch": [ + "165", + 0 + ], + "vae": [ + "27", + 0 + ], + "image": [ + "164", + 0 + ] + }, + "class_type": "QwenImageDiffsynthControlnet", + "_meta": { + "title": "QwenImageDiffsynthControlnet" + } + }, + "174": { + "inputs": { + "filename_prefix": "ComfyUI", + "images": [ + "180", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "保存图像" + } + }, + "180": { + "inputs": { + "samples": [ + "181", + 0 + ], + "vae": [ + "27", + 0 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE解码" + } + }, + "181": { + "inputs": { + "seed": 82007311503644, + "steps": 10, + "cfg": 1, + "sampler_name": "euler_cfg_pp", + "scheduler": "simple", + "denoise": [ + "202", + 0 + ], + "model": [ + "205", + 0 + ], + "positive": [ + "23", + 0 + ], + "negative": [ + "24", + 0 + ], + "latent_image": [ + "186", + 0 + ] + }, + "class_type": "KSampler", + "_meta": { + "title": "K采样器" + } + }, + "184": { + "inputs": { + "seed": 154276203336202, + "strength": [ + "202", + 0 + ], + "image": [ + "15", + 0 + ] + }, + "class_type": "ImageAddNoise", + "_meta": { + "title": "图像添加噪声" + } + }, + "186": { + "inputs": { + "pixels": [ + "189", + 0 + ], + "vae": [ + "27", + 0 + ] + }, + "class_type": "VAEEncode", + "_meta": { + "title": "VAE编码" + } + }, + "189": { + "inputs": { + "blend_factor": [ + "202", + 0 + ], + "blend_mode": "multiply", + "image1": [ + "191", + 0 + ], + "image2": [ + "184", + 0 + ] + }, + "class_type": "ImageBlend", + "_meta": { + "title": "图像混合" + } + }, + "191": { + "inputs": { + "sharpen_radius": 1, + "sigma": 0.5, + "alpha": 0.5, + "image": [ + "147", + 0 + ] + }, + "class_type": "ImageSharpen", + "_meta": { + "title": "图像锐化" + } + }, + "193": { + "inputs": { + "image": [ + "15", + 0 + ] + }, + "class_type": "GetImageSize+", + "_meta": { + "title": "获取图像尺寸" + } + }, + "201": { + "inputs": { + "expression": "(a+b)*c/10000", + "a": [ + "193", + 0 + ], + "b": [ + "193", + 1 + ], + "c": [ + "204", + 0 + ] + }, + "class_type": "MathExpression|pysssss", + "_meta": { + "title": "数学表达式" + } + }, + "202": { + "inputs": { + "output_type": "float", + "*": [ + "201", + 1 + ] + }, + "class_type": "easy convertAnything", + "_meta": { + "title": "转换任何" + } + }, + "204": { + "inputs": { + "value": 0.5 + }, + "class_type": "FloatConstant", + "_meta": { + "title": "浮点常量" + } + }, + "205": { + "inputs": { + "unet_name": "z_image_turbo_int8_convrot.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "UNet加载器" + } + }, + "206": { + "inputs": { + "clip_name": "qwen_3_4b.safetensors", + "type": "stable_diffusion", + "device": "default" + }, + "class_type": "CLIPLoader", + "_meta": { + "title": "加载CLIP" + } + } } \ No newline at end of file diff --git a/workflows/Z-Image.json b/workflows/Z-Image.json index 1ad3ef043..02aaebff2 100644 --- a/workflows/Z-Image.json +++ b/workflows/Z-Image.json @@ -24,7 +24,7 @@ "scheduler": "simple", "denoise": 1, "model": [ - "33", + "146", 0 ], "positive": [ @@ -48,14 +48,8 @@ "23": { "inputs": { "text": "", - "speak_and_recognation": { - "__value__": [ - false, - true - ] - }, "clip": [ - "34", + "147", 0 ] }, @@ -78,99 +72,56 @@ }, "27": { "inputs": { - "vae_name": "flux ultra vae.safetensors" + "vae_name": "z_image_turbo_vae.safetensors" }, "class_type": "VAELoader", "_meta": { "title": "VAE加载器" } }, - "33": { - "inputs": { - "model_name": "z_image_turbo_bf16.safetensors", - "weight_dtype": "default", - "key_opt": "", - "mode": "Auto" - }, - "class_type": "LoadDiffusionModelShared //Inspire", - "_meta": { - "title": "Shared Diffusion Model Loader (Inspire)" - } - }, - "34": { - "inputs": { - "model_name1": "qwen_3_4b.safetensors", - "model_name2": "None", - "model_name3": "None", - "type": "stable_diffusion", - "key_opt": "", - "mode": "Auto", - "device": "default" - }, - "class_type": "LoadTextEncoderShared //Inspire", - "_meta": { - "title": "Shared Text Encoder Loader (Inspire)" - } - }, - "89": { + "144": { "inputs": { - "rgthree_comparer": { - "images": [ - { - "name": "A", - "selected": true, - "url": "/api/view?filename=rgthree.compare._temp_gmjxy_00001_.png&type=temp&subfolder=&rand=0.7230996201608886" - }, - { - "name": "B", - "selected": true, - "url": "/api/view?filename=rgthree.compare._temp_gmjxy_00002_.png&type=temp&subfolder=&rand=0.6218111120009978" - } - ] - }, - "image_a": [ - "20", - 0 - ] + "width": 512, + "height": 512, + "batch_size": 1 }, - "class_type": "Image Comparer (rgthree)", + "class_type": "EmptyLatentImage", "_meta": { - "title": "图像对比" + "title": "空Latent" } }, - "137": { + "145": { "inputs": { + "filename_prefix": "ComfyUI", "images": [ "20", 0 ] }, - "class_type": "PreviewImage", + "class_type": "SaveImage", "_meta": { - "title": "预览图像" + "title": "保存图像" } }, - "142": { + "146": { "inputs": { - "vae": [ - "27", - 0 - ] + "unet_name": "z_image_turbo_int8_convrot.safetensors", + "weight_dtype": "default" }, - "class_type": "VAEEncode", + "class_type": "UNETLoader", "_meta": { - "title": "VAE编码" + "title": "UNet加载器" } }, - "144": { + "147": { "inputs": { - "width": 512, - "height": 512, - "batch_size": 1 + "clip_name": "qwen_3_4b.safetensors", + "type": "stable_diffusion", + "device": "default" }, - "class_type": "EmptyLatentImage", + "class_type": "CLIPLoader", "_meta": { - "title": "空Latent" + "title": "加载CLIP" } } } \ No newline at end of file diff --git a/workflows/custom/Flux2-Klein-custom.config.json b/workflows/custom/Flux2-Klein-custom.config.json index f84ac3bdc..49883dce0 100644 --- a/workflows/custom/Flux2-Klein-custom.config.json +++ b/workflows/custom/Flux2-Klein-custom.config.json @@ -12,7 +12,9 @@ "max": null, "step": null, "options": [], - "random_enabled": false + "random_enabled": false, + "auto": "", + "image_count_min": null }, { "id": "f_klein_image_1", @@ -25,7 +27,9 @@ "max": null, "step": null, "options": [], - "random_enabled": false + "random_enabled": false, + "auto": "", + "image_count_min": null }, { "id": "f_klein_image_2", @@ -38,7 +42,9 @@ "max": null, "step": null, "options": [], - "random_enabled": false + "random_enabled": false, + "auto": "", + "image_count_min": null }, { "id": "f_klein_image_3", @@ -51,7 +57,9 @@ "max": null, "step": null, "options": [], - "random_enabled": false + "random_enabled": false, + "auto": "", + "image_count_min": null }, { "id": "f_klein_multi_2", @@ -60,13 +68,13 @@ "name": "启用第二张输入图", "type": "boolean", "default": false, - "auto": "image_count", - "image_count_min": 2, "min": null, "max": null, "step": null, "options": [], - "random_enabled": false + "random_enabled": false, + "auto": "image_count", + "image_count_min": 2 }, { "id": "f_klein_multi_3", @@ -75,13 +83,13 @@ "name": "启用第三张输入图", "type": "boolean", "default": false, - "auto": "image_count", - "image_count_min": 3, "min": null, "max": null, "step": null, "options": [], - "random_enabled": false + "random_enabled": false, + "auto": "image_count", + "image_count_min": 3 }, { "id": "f_zbe4wlu", @@ -94,13 +102,14 @@ "max": null, "step": null, "options": [ - "None", "Klein-W万物迁移.safetensors", "Flux2-Klein-9B-consistency-V2.safetensors", "realistic(Flux2-Klein-9B-Enhanced-Details).safetensors", "Klein2-9B-jhuangswap-SmartCharacterSwap.safetensors" ], - "random_enabled": false + "random_enabled": false, + "auto": "", + "image_count_min": null }, { "id": "f_m50c4ff", @@ -113,7 +122,9 @@ "max": 2.0, "step": 0.1, "options": [], - "random_enabled": false + "random_enabled": false, + "auto": "", + "image_count_min": null } ], "mini_cards": { @@ -134,4 +145,4 @@ "y": 120 } } -} +} \ No newline at end of file diff --git "a/workflows/custom/klein9b-\351\253\230\346\270\205\344\277\256\345\244\215\346\224\276\345\244\247.config.json" "b/workflows/custom/klein9b-\351\253\230\346\270\205\344\277\256\345\244\215\346\224\276\345\244\247.config.json" new file mode 100644 index 000000000..e1bd7b6dc --- /dev/null +++ "b/workflows/custom/klein9b-\351\253\230\346\270\205\344\277\256\345\244\215\346\224\276\345\244\247.config.json" @@ -0,0 +1,92 @@ +{ + "title": "klein9b-高清修复放大", + "fields": [ + { + "id": "f_zbn0t1h", + "node": "113", + "input": "lora_name", + "name": "LoRA 模型", + "type": "dropdown", + "default": "f2k-9B_consis.safetensors", + "min": null, + "max": null, + "step": null, + "options": [ + "Klein一致性增强.safetensors", + "Flux2-Klein-9B-consistency-V2.safetensors", + "Klein-W万物迁移.safetensors" + ], + "random_enabled": false + }, + { + "id": "f_zhmodo9", + "node": "113", + "input": "strength_model", + "name": "模型强度", + "type": "number", + "default": 0.5, + "min": 0.0, + "max": 2.0, + "step": 0.1, + "options": [], + "random_enabled": false + }, + { + "id": "f_36dfth7", + "node": "96", + "input": "scale_by", + "name": "Latent缩放", + "type": "number", + "default": 1, + "min": 0.0, + "max": 8.0, + "step": 0.5, + "options": [], + "random_enabled": false + }, + { + "id": "f_7zppojg", + "node": "93", + "input": "image", + "name": "图片", + "type": "image", + "default": "pasted/image (41).png", + "min": null, + "max": null, + "step": null, + "options": [], + "random_enabled": false + }, + { + "id": "f_ry3p9ow", + "node": "64", + "input": "text", + "name": "提示词文本", + "type": "textarea", + "default": "高清修复", + "min": null, + "max": null, + "step": null, + "options": [], + "random_enabled": false + } + ], + "mini_cards": { + "prompt": { + "x": 24, + "y": 30 + }, + "image": { + "x": 24, + "y": 210 + }, + "custom": { + "x": 280, + "y": 78 + }, + "output": { + "x": 540, + "y": 120 + } + } +} diff --git "a/workflows/custom/klein9b-\351\253\230\346\270\205\344\277\256\345\244\215\346\224\276\345\244\247.json" "b/workflows/custom/klein9b-\351\253\230\346\270\205\344\277\256\345\244\215\346\224\276\345\244\247.json" new file mode 100644 index 000000000..d814fa270 --- /dev/null +++ "b/workflows/custom/klein9b-\351\253\230\346\270\205\344\277\256\345\244\215\346\224\276\345\244\247.json" @@ -0,0 +1,185 @@ +{ + "62": { + "inputs": { + "conditioning": [ + "64", + 0 + ] + }, + "class_type": "ConditioningZeroOut", + "_meta": { + "title": "条件零化" + } + }, + "63": { + "inputs": { + "samples": [ + "66", + 0 + ], + "vae": [ + "77", + 0 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE解码" + } + }, + "64": { + "inputs": { + "text": "高清修复", + "clip": [ + "108", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP文本编码" + } + }, + "66": { + "inputs": { + "seed": 657673929738321, + "steps": 4, + "cfg": 1, + "sampler_name": "euler", + "scheduler": "simple", + "denoise": 1, + "model": [ + "113", + 0 + ], + "positive": [ + "102", + 0 + ], + "negative": [ + "62", + 0 + ], + "latent_image": [ + "96", + 0 + ] + }, + "class_type": "KSampler", + "_meta": { + "title": "K采样器" + } + }, + "77": { + "inputs": { + "vae_name": "flux2-vae.safetensors" + }, + "class_type": "VAELoader", + "_meta": { + "title": "加载VAE" + } + }, + "90": { + "inputs": { + "unet_name": "flux-2-klein-9b-int8-ConvRot-comfyui.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "UNet加载器" + } + }, + "93": { + "inputs": { + "image": "pasted/image (41).png" + }, + "class_type": "LoadImage", + "_meta": { + "title": "加载图像" + } + }, + "94": { + "inputs": { + "pixels": [ + "93", + 0 + ], + "vae": [ + "77", + 0 + ] + }, + "class_type": "VAEEncode", + "_meta": { + "title": "VAE编码" + } + }, + "96": { + "inputs": { + "upscale_method": "nearest-exact", + "scale_by": 1, + "samples": [ + "94", + 0 + ] + }, + "class_type": "LatentUpscaleBy", + "_meta": { + "title": "缩放Latent(比例)" + } + }, + "102": { + "inputs": { + "conditioning": [ + "64", + 0 + ], + "latent": [ + "94", + 0 + ] + }, + "class_type": "ReferenceLatent", + "_meta": { + "title": "参考Latent" + } + }, + "108": { + "inputs": { + "clip_name": "Qwen3-8B-int4-ConvRot.safetensors", + "type": "flux2", + "device": "default" + }, + "class_type": "CLIPLoader", + "_meta": { + "title": "加载CLIP" + } + }, + "113": { + "inputs": { + "lora_name": "f2k-9B_consis.safetensors", + "strength_model": 0.2, + "model": [ + "90", + 0 + ] + }, + "class_type": "LoraLoaderModelOnly", + "_meta": { + "title": "LoRA加载器(仅模型)" + } + }, + "114": { + "inputs": { + "filename_prefix": "ComfyUI", + "images": [ + "63", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "保存图像" + } + } +} diff --git a/workflows/flux-2-klein-4B-360-erp-outpaint.config.json b/workflows/flux-2-klein-4B-360-erp-outpaint.config.json new file mode 100644 index 000000000..8ec45688f --- /dev/null +++ b/workflows/flux-2-klein-4B-360-erp-outpaint.config.json @@ -0,0 +1,82 @@ +{ + "title": "flux-2-klein-4B-360-erp-outpaint", + "fields": [ + { + "id": "f_hs12dis", + "node": "70", + "input": "output_preset", + "name": "output_preset", + "type": "dropdown", + "default": "2048", + "min": null, + "max": null, + "step": null, + "options": [ + "\"1024\"", + "\"2048\"", + "\"4096\"" + ], + "random_enabled": false + }, + { + "id": "f_gphzh40", + "node": "70", + "input": "coverage", + "name": "coverage", + "type": "dropdown", + "default": "360", + "min": null, + "max": null, + "step": null, + "options": [ + "\"360\"", + "\"180\"" + ], + "random_enabled": false + }, + { + "id": "f_u7zqx2x", + "node": "6", + "input": "text", + "name": "提示词文本", + "type": "textarea", + "default": "Fill the green spaces according to the image. Outpaint as a seamless 360 equirectangular panorama (2:1). Keep the horizon level. Match left and right edges.这个是一个工业风格的办公室空间。", + "min": null, + "max": null, + "step": null, + "options": [], + "random_enabled": false + }, + { + "id": "f_2xl7wtn", + "node": "70", + "input": "Open Stickers Editor", + "name": "Open Stickers Editor", + "type": "image", + "default": null, + "min": null, + "max": null, + "step": null, + "options": [], + "random_enabled": false + } + ], + "mini_cards": { + "prompt": { + "x": 24, + "y": 30 + }, + "image": { + "x": 24, + "y": 210 + }, + "custom": { + "x": 280, + "y": 78 + }, + "output": { + "x": 540, + "y": 120 + } + } +} \ No newline at end of file diff --git a/workflows/flux-2-klein-4B-360-erp-outpaint.json b/workflows/flux-2-klein-4B-360-erp-outpaint.json new file mode 100644 index 000000000..2fbedf851 --- /dev/null +++ b/workflows/flux-2-klein-4B-360-erp-outpaint.json @@ -0,0 +1,255 @@ +{ + "6": { + "inputs": { + "text": "Fill the green spaces according to the image. Outpaint as a seamless 360 equirectangular panorama (2:1). Keep the horizon level. Match left and right edges.这个是一个工业风格的办公室空间。", + "clip": [ + "44", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Positive Prompt)" + } + }, + "8": { + "inputs": { + "samples": [ + "31", + 0 + ], + "vae": [ + "43", + 0 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE解码" + } + }, + "31": { + "inputs": { + "seed": 12345, + "steps": 8, + "cfg": 1, + "sampler_name": "euler", + "scheduler": "simple", + "denoise": 1, + "model": [ + "63", + 0 + ], + "positive": [ + "49", + 0 + ], + "negative": [ + "55", + 0 + ], + "latent_image": [ + "52", + 0 + ] + }, + "class_type": "KSampler", + "_meta": { + "title": "K采样器" + } + }, + "33": { + "inputs": { + "text": "text, worst quality, blurry, ugly", + "clip": [ + "44", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Negative Prompt)" + } + }, + "43": { + "inputs": { + "vae_name": "flux2-vae.safetensors" + }, + "class_type": "VAELoader", + "_meta": { + "title": "加载VAE" + } + }, + "44": { + "inputs": { + "clip_name": "qwen_3_8b_fp8mixed.safetensors", + "type": "flux2", + "device": "default" + }, + "class_type": "CLIPLoader", + "_meta": { + "title": "加载CLIP" + } + }, + "48": { + "inputs": { + "unet_name": "flux-2-klein-9b-int8-ConvRot-comfyui.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "UNet加载器" + } + }, + "49": { + "inputs": { + "conditioning": [ + "6", + 0 + ], + "latent": [ + "52", + 0 + ] + }, + "class_type": "ReferenceLatent", + "_meta": { + "title": "参考Latent" + } + }, + "52": { + "inputs": { + "pixels": [ + "70", + 0 + ], + "vae": [ + "43", + 0 + ] + }, + "class_type": "VAEEncode", + "_meta": { + "title": "VAE编码" + } + }, + "55": { + "inputs": { + "conditioning": [ + "33", + 0 + ], + "latent": [ + "52", + 0 + ] + }, + "class_type": "ReferenceLatent", + "_meta": { + "title": "参考Latent" + } + }, + "58": { + "inputs": { + "coverage": "360", + "fps": 24, + "Open Preview": null, + "erp_image": [ + "8", + 0 + ] + }, + "class_type": "PanoramaPreview", + "_meta": { + "title": "Panorama Preview" + } + }, + "60": { + "inputs": { + "images": [ + "70", + 0 + ] + }, + "class_type": "PreviewImage", + "_meta": { + "title": "预览图像" + } + }, + "63": { + "inputs": { + "lora_name": "flux-2-klein-9B-360-erp-outpaint-lora_V1.safetensors", + "strength_model": 0.9, + "model": [ + "48", + 0 + ] + }, + "class_type": "LoraLoaderModelOnly", + "_meta": { + "title": "LoRA加载器(仅模型)" + } + }, + "66": { + "inputs": { + "filename_prefix": "ComfyUI", + "images": [ + "71", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "保存图像" + } + }, + "70": { + "inputs": { + "output_preset": "2048", + "coverage": "360", + "bg_color": "#00ff00", + "state_json": "{\"version\": 1, \"projection_model\": \"pinhole_rectilinear\", \"alpha_mode\": \"straight\", \"coverage\": \"360\", \"bg_color\": \"#00ff00\", \"output_preset\": \"2048\", \"assets\": {\"asset_43e97d1b\": {\"type\": \"comfy_image\", \"filename\": \"image (29).png\", \"subfolder\": \"panorama_stickers\", \"storage\": \"input\", \"name\": \"image (29).png\"}}, \"stickers\": [{\"id\": \"st_4916c061\", \"asset_id\": \"asset_43e97d1b\", \"yaw_deg\": -3.7037037037035816, \"pitch_deg\": -3.086418809960307, \"hFOV_deg\": 46.91685694694386, \"vFOV_deg\": 79.92354207262883, \"rot_deg\": 0, \"z_index\": 0, \"aspect_id\": \"259:500\"}], \"shots\": [], \"painting\": {\"version\": 1, \"groups\": [], \"paint\": {\"strokes\": []}, \"mask\": {\"strokes\": []}, \"raster_objects\": []}, \"painting_layer\": null, \"ui_settings\": {\"invert_view_x\": false, \"invert_view_y\": false, \"preview_quality\": \"balanced\"}, \"active\": {\"selected_sticker_id\": \"st_4916c061\", \"selected_shot_id\": null}}", + "fps": 24, + "Open Stickers Editor": null + }, + "class_type": "PanoramaStickers", + "_meta": { + "title": "Panorama Stickers" + } + }, + "71": { + "inputs": { + "coverage": "360", + "state_json": "{\"version\": 1, \"projection_model\": \"pinhole_rectilinear\", \"alpha_mode\": \"straight\", \"coverage\": \"360\", \"bg_color\": \"#00ff00\", \"output_preset\": \"2048\", \"assets\": {}, \"stickers\": [], \"shots\": [], \"painting\": {\"version\": 1, \"groups\": [], \"paint\": {\"strokes\": []}, \"mask\": {\"strokes\": []}, \"raster_objects\": []}, \"painting_layer\": null, \"ui_settings\": {\"invert_view_x\": false, \"invert_view_y\": false, \"preview_quality\": \"balanced\"}, \"active\": {\"selected_sticker_id\": null, \"selected_shot_id\": null}}", + "output_megapixels": 1, + "fps": 24, + "Open Cutout Editor": null, + "erp_image": [ + "8", + 0 + ] + }, + "class_type": "PanoramaCutout", + "_meta": { + "title": "Panorama Cutout" + } + }, + "72": { + "inputs": { + "image": "image (29).png" + }, + "class_type": "LoadImage", + "_meta": { + "title": "加载图像" + } + }, + "73": { + "inputs": { + "image": "flower_road_4k.jpeg" + }, + "class_type": "LoadImage", + "_meta": { + "title": "加载图像" + } + } +} \ No newline at end of file From 91998ee4d0113b6d047108dcf9d8ddca0b3018aa Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Sun, 9 Aug 2026 14:25:25 +0800 Subject: [PATCH 17/20] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Klein=20=E5=A4=9A?= =?UTF-8?q?=E5=9B=BE=E7=BC=96=E8=BE=91=E6=A8=A1=E5=9E=8B=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLI/macos/drawthings/draw_things_grpc.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CLI/macos/drawthings/draw_things_grpc.py b/CLI/macos/drawthings/draw_things_grpc.py index b35de1cee..47e1bd621 100644 --- a/CLI/macos/drawthings/draw_things_grpc.py +++ b/CLI/macos/drawthings/draw_things_grpc.py @@ -567,10 +567,16 @@ async def generate_draw_things_image( import grpc from .generated import imageService_pb2, imageService_pb2_grpc + selected_model = str(model or "").strip() + if not selected_model: + raise RuntimeError( + "Draw Things gRPCServerCLI 未选择模型。请连接服务后从实时模型列表中选择。" + ) + editing_model = draw_things_model_supports_editing(selected_model) references = [item for item in (reference_images or []) if item] - if len(references) > 1: + if len(references) > 1 and not editing_model: raise RuntimeError( - "当前 Draw Things 模型不支持多图图像编辑,请只保留一张输入图,或切换到 Klein/Qwen Edit 模型。" + "当前 Draw Things 模型不支持多图图像编辑,请只保留一张输入图,或切换到支持多图编辑的 Klein/Qwen Edit 模型。" ) masks = [item for item in (mask_images or []) if item] if len(masks) > 1: @@ -585,11 +591,6 @@ async def generate_draw_things_image( host, port, use_tls, shared_secret = _settings(endpoint) target = f"{host}:{port}" - selected_model = str(model or "").strip() - if not selected_model: - raise RuntimeError( - "Draw Things gRPCServerCLI 未选择模型。请连接服务后从实时模型列表中选择。" - ) width, height = _parse_size(size) input_image = None image_strength = None From 8f9001aa2bfb77585d36e1cefdd363c25408ecce Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Sun, 9 Aug 2026 15:18:48 +0800 Subject: [PATCH 18/20] =?UTF-8?q?=E4=BF=AE=E5=A4=8DKlein=E9=81=AE=E7=BD=A9?= =?UTF-8?q?=E5=A4=9A=E5=9B=BE=E5=8F=82=E8=80=83=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLI/macos/drawthings/draw_things_grpc.py | 4 ++-- main.py | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CLI/macos/drawthings/draw_things_grpc.py b/CLI/macos/drawthings/draw_things_grpc.py index 47e1bd621..58366a708 100644 --- a/CLI/macos/drawthings/draw_things_grpc.py +++ b/CLI/macos/drawthings/draw_things_grpc.py @@ -584,9 +584,9 @@ async def generate_draw_things_image( if masks and not references: raise RuntimeError("Draw Things 遮罩需要与一张输入图一起使用。") hints = [item for item in (hint_images or []) if item] - if references and hints: + if references and hints and not editing_model: raise RuntimeError( - "Draw Things gRPC 请求不能同时使用普通图生图 image 和 Hint 输入。" + "当前 Draw Things 非编辑模型不能同时使用普通图生图 image 和 Hint 输入。" ) host, port, use_tls, shared_secret = _settings(endpoint) diff --git a/main.py b/main.py index 7fef38b9c..8e8d8dd1c 100755 --- a/main.py +++ b/main.py @@ -11650,10 +11650,21 @@ async def generate_ai_image(prompt, size, quality, model, reference_images=None, if drawthings_masks: # Draw Things masks belong to ImageGenerationRequest.mask and # must not be counted as a second ordinary reference image. - request_options.update( - reference_images=drawthings_references, - mask_images=drawthings_masks, - ) + # For an editing model, the first image is the masked base + # image and the remaining references stay in the HintProto + # stack, matching the official Draw Things ComfyUI node. + if editing_model and len(drawthings_references) > 1: + request_options.update( + reference_images=drawthings_references[:1], + hint_images=drawthings_references[1:], + hint_type="shuffle", + mask_images=drawthings_masks, + ) + else: + request_options.update( + reference_images=drawthings_references, + mask_images=drawthings_masks, + ) elif editing_model: request_options.update( hint_images=drawthings_references, From b78fd5085b33f7d51d82ebf2e139d039b30050e6 Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Wed, 12 Aug 2026 17:03:40 +0800 Subject: [PATCH 19/20] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20EXELLOME=20=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E7=BC=96=E8=BE=91=E4=BC=A0=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 8e8d8dd1c..b93e98cae 100755 --- a/main.py +++ b/main.py @@ -11739,13 +11739,16 @@ async def post_openai_edits(edit_files=None): local_image_paths = [openai_video_proxy_local_image_path(ref) for ref in refs_for_proxy] has_local_images = any(local_image_paths) if has_local_images: - form_data = [(key, value) for key, value in body.items()] + form_data = dict(body) + remote_image_urls = [] for ref, local_path in zip(refs_for_proxy, local_image_paths): if local_path: continue url = await openai_video_proxy_public_reference_url(ref) if url: - form_data.append(("images", url)) + remote_image_urls.append(url) + if remote_image_urls: + form_data["images"] = remote_image_urls files = [] opened = [] try: From bafa4b16a0302f0a6c635ae401cb3035774f9ad2 Mon Sep 17 00:00:00 2001 From: hanqing ren Date: Wed, 12 Aug 2026 18:48:08 +0800 Subject: [PATCH 20/20] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20ComfyUI=20=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=B7=A5=E4=BD=9C=E6=B5=81=20seed=20?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 20 ++++++++++++ static/js/canvas.js | 33 +++++++++++++++++++ static/js/smart-canvas.js | 68 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index b93e98cae..0a803db6f 100755 --- a/main.py +++ b/main.py @@ -18976,6 +18976,25 @@ def generate(req: GenerateRequest): "_meta": node_inputs.get("_meta") if isinstance(node_inputs.get("_meta"), dict) else {"title": str(node_inputs.get("class_type"))}, } + # Custom workflows can put their seed on any node, so the generic + # random value above is not necessarily the value sent to ComfyUI. + # Collect the numeric seed inputs after applying all frontend params. + seed_values = {} + for node_id, node_data in workflow.items(): + inputs = node_data.get("inputs") if isinstance(node_data, dict) else None + if not isinstance(inputs, dict): + continue + for input_name, value in inputs.items(): + normalized_name = str(input_name or "").strip().lower().replace("-", "_").replace(" ", "_") + if normalized_name not in {"seed", "noise_seed", "random_seed"}: + continue + try: + seed_values[f"{node_id}:{input_name}"] = int(float(value)) + except (TypeError, ValueError): + continue + if seed_values: + seed = next(iter(seed_values.values())) + p = {"prompt": workflow, "client_id": CLIENT_ID} data = json.dumps(p).encode('utf-8') try: @@ -19096,6 +19115,7 @@ def _class_type_of(nid): "items": local_items, "outputs": local_urls, "seed": seed, + "seed_values": seed_values, "timestamp": current_timestamp, "type": req.type, "workflow_json": req.workflow_json, diff --git a/static/js/canvas.js b/static/js/canvas.js index 445c22e38..851b58172 100644 --- a/static/js/canvas.js +++ b/static/js/canvas.js @@ -12974,6 +12974,7 @@ async function runComfyNode(nodeId, opts={}){ if(result.error) throw new Error(actionFailed('canvas.comfyCustom', result.error)); images = comfyResultOutputs(result); if(!images.length) throw new Error(noReturnedImage('canvas.comfyCustom')); + applyComfySeedResult(node, settingFields, result); rememberGenerationOutputs(node, fixedSeedCacheKey, images); } else { run.taskLabel = tr('canvas.comfyEdit'); @@ -13641,6 +13642,38 @@ function requestMetaFromResult(result={}){ seed: result.seed || '', }; } +function applyComfySeedResult(node, fields=[], result={}){ + if(!node || !Array.isArray(fields) || !result) return false; + const seedValues = result.seed_values && typeof result.seed_values === 'object' ? result.seed_values : {}; + const fallbackSeed = Number(result.seed); + const seedFields = fields.filter(generationSeedField); + let changed = false; + fields.filter(generationSeedField).forEach(field => { + const keys = [ + `${field.node}:${field.input}`, + `${field.node}.${field.input}`, + String(field.input || ''), + ]; + let value; + for(const key of keys){ + if(seedValues[key] !== undefined){ + value = seedValues[key]; + break; + } + } + if(value === undefined && seedFields.length === 1 && Number.isFinite(fallbackSeed)) value = fallbackSeed; + if(value === undefined) return; + const numericValue = Number(value); + if(!Number.isFinite(numericValue)) return; + node.comfyParams = node.comfyParams || {}; + if(Number(node.comfyParams[field.id]) !== numericValue){ + node.comfyParams[field.id] = numericValue; + changed = true; + } + }); + if(changed) refreshNodes([node.id]); + return changed; +} function runPlatformLabel(run){ const node = run?.node || {}; if(run?.nodeType === 'generator') return providerById(node.apiProvider || 'comfly')?.name || node.apiProvider || 'API'; diff --git a/static/js/smart-canvas.js b/static/js/smart-canvas.js index 26d232044..41f843368 100644 --- a/static/js/smart-canvas.js +++ b/static/js/smart-canvas.js @@ -1241,6 +1241,13 @@ function restoreSmartSettingsAfterRun(runNode, previousSettings){ const selected = selectedNode(); const activeSubject = isSmartRunnableNode(selected) ? selected : activeSettingsSubject(); if(activeSubject?.id === runNode?.id || !activeSubject){ + if(activeSubject?.id === runNode?.id + && runNode?.runSettings?.engine === 'comfy' + && runNode?.runSettings?.comfyMode === 'custom' + && runNode.runSettings.comfyParams + && typeof runNode.runSettings.comfyParams === 'object'){ + previousSettings.comfyParams = cloneSmartSettings(runNode.runSettings.comfyParams); + } settings = previousSettings; return; } @@ -15939,6 +15946,37 @@ function comfyParamsFromWorkflowValues(config, values={}){ }); return params; } +function applySmartComfySeedResult(runSettings, fields=[], result={}){ + if(!runSettings || !Array.isArray(fields) || !result) return false; + const seedValues = result.seed_values && typeof result.seed_values === 'object' ? result.seed_values : {}; + const fallbackSeed = Number(result.seed); + const seedFields = fields.filter(generationSeedField); + let changed = false; + runSettings.comfyParams = runSettings.comfyParams || {}; + seedFields.forEach(field => { + if(comfyRandomEnabledField(field) && !smartComfyRandomActiveFor(runSettings, field.id)) return; + const keys = [ + `${field.node}:${field.input}`, + `${field.node}.${field.input}`, + String(field.input || ''), + ]; + let value; + for(const key of keys){ + if(seedValues[key] !== undefined){ + value = seedValues[key]; + break; + } + } + if(value === undefined && seedFields.length === 1 && Number.isFinite(fallbackSeed)) value = fallbackSeed; + const numericValue = Number(value); + if(!Number.isFinite(numericValue)) return; + if(Number(runSettings.comfyParams[field.id]) !== numericValue){ + runSettings.comfyParams[field.id] = numericValue; + changed = true; + } + }); + return changed; +} function buildPromptRequestForNode(node, defaultImages, ctx=smartLoopContext){ const oldHtml = promptInput.innerHTML; loadNodePromptDraftToInput(node); @@ -16068,10 +16106,17 @@ async function generateComfyUrlsWithSettings(runSettings, prompt, refs){ if(cached.length) return {urls:cached.slice(), kind:'image', cached:true, cacheKey}; } const result = await runQueuedSmartComfyGenerate({prompt, workflow_json:workflowName, params, type:'workflow-custom', client_id:smartClientId}); + applySmartComfySeedResult(runSettings, fields, result); const urls = resultMediaUrls(result); const fallbackKind = result.videos?.length ? 'video' : result.audios?.length ? 'audio' : result.texts?.length ? 'text' : 'image'; rememberFixedSeedGenerationResult(cacheKey, urls); - return {urls, kind:mediaKindForUrls(urls, fallbackKind), cacheKey}; + return { + urls, + kind:mediaKindForUrls(urls, fallbackKind), + cacheKey, + seed:result.seed, + seed_values:result.seed_values + }; } async function runCascadeStepIntoNode(sourceNode, targetNode, inputRefs, ctx=smartLoopContext){ const outputNode = targetNode || sourceNode; @@ -16142,7 +16187,24 @@ async function runCascadeStepIntoNode(sourceNode, targetNode, inputRefs, ctx=sma settings = previousSettings; try { const result = await generateUrlsForCurrentSettings(requestNode, prompt, request.refs || [], runSettings); + if(runSettings.engine === 'comfy' && runSettings.comfyMode === 'custom'){ + const workflowName = runSettings.comfyWorkflow || comfyWorkflows[0]?.name || ''; + const wf = workflowName ? await ensureComfyWorkflow(workflowName) : null; + applySmartComfySeedResult(meta.settings, wf?.config?.fields || [], { + seed:result.seed, + seed_values:result.seed_values + }); + } const appliedSeed = applyDrawThingsSeedResult(result, runSettings, previousSettings, meta, outputNode); + if(runSettings.engine === 'comfy' && runSettings.comfyMode === 'custom'){ + const workflowName = runSettings.comfyWorkflow || comfyWorkflows[0]?.name || ''; + const wf = workflowName ? await ensureComfyWorkflow(workflowName) : null; + const fields = wf?.config?.fields || []; + applySmartComfySeedResult(targetPromptState.runSettings, fields, { + seed:result.seed, + seed_values:result.seed_values + }); + } // 级联节点完成后会恢复这份旧快照;保留本次真实提交的 seed,避免它覆盖新值。 if(appliedSeed && targetPromptState.runSettings && isDrawThingsProvider(targetPromptState.runSettings.provider_id || runSettings.provider_id)){ targetPromptState.runSettings.drawThingsSeed = appliedSeed; @@ -17285,7 +17347,7 @@ async function runComfyGeneration(node, prompt, refs, pendingNode, meta, runSett fields:fields.filter(f => comfyFieldKind(f) === 'setting'), valueFor:field => values[field.id], randomEnabled:comfyRandomEnabledField, - randomActive:field => smartComfyRandomActive(field.id) + randomActive:field => smartComfyRandomActiveFor(runSettings, field.id) }); const cacheKey = generationRequestFingerprint({engine:'comfy', mode, workflow:workflowName, prompt, refs:allRefs, params}, 1, fixedSeedState); const cached = cacheKey ? fixedSeedGenerationCache.get(cacheKey) || [] : []; @@ -17297,6 +17359,8 @@ async function runComfyGeneration(node, prompt, refs, pendingNode, meta, runSett return; } const result = await runQueuedSmartComfyGenerate({prompt, workflow_json:workflowName, params, type:'workflow-custom', client_id:smartClientId}); + applySmartComfySeedResult(runSettings, fields, result); + applySmartComfySeedResult(meta?.settings, fields, result); const urls = resultMediaUrls(result); if(!urls.length) throw new Error(tr('smart.errComfyNoImages')); const kind = mediaKindForUrls(urls, result.videos?.length ? 'video' : result.audios?.length ? 'audio' : result.texts?.length ? 'text' : 'image');