import { app } from "../../../scripts/app.js"; // ── Seedance 多模态节点参数联动 ─────────────────────────────────────────────── // // fast、mini 不支持 1080p / 4k;2.5 支持全部分辨率和最长 30 秒。 // 当用户切换到受限模型,或选择不支持的分辨率时, // 弹 toast 报错并把分辨率回退到 720p,避免提交后才在后端报错浪费一次调用。 // // 注:「模型」下拉存的是展示名(seedance 2.0 fast 等),非真实模型 ID。 const LIMITED_MODELS = new Set([ "seedance 2.0 fast", "seedance 2.0 mini", ]); const isLimitedModel = (v) => LIMITED_MODELS.has(v); const MODEL_25 = "seedance 2.5"; const ALL_RESOLUTIONS = ["720p", "1080p", "4k", "480p"]; const LIMITED_RESOLUTIONS = ["720p", "480p"]; const RESOLUTION_FALLBACK = "720p"; const durationOptions = (maximum) => [ "自动", ...Array.from({ length: maximum - 3 }, (_, index) => `${index + 4}秒`), ]; const DURATIONS_20 = durationOptions(15); const DURATIONS_25 = durationOptions(30); const SEEDANCE_NODES = new Set(["SeedanceMultiModal"]); function notify(detail) { app.extensionManager?.toast?.add({ severity: "warn", summary: "Seedance 多模态参数已调整", detail, life: 4000, }); } function findWidget(node, name) { return node.widgets?.find((w) => w.name === name); } function setWidgetValue(widget, value) { if (!widget || widget.value === value) return; widget.value = value; widget.callback?.(value); } function updateDynamicParameters(node, silent = false) { const modelWidget = findWidget(node, "主模型"); const resWidget = findWidget(node, "分辨率"); const durationWidget = findWidget(node, "时长"); if (!modelWidget || !resWidget || !durationWidget) return; const limited = isLimitedModel(modelWidget.value); const maximumDuration = modelWidget.value === MODEL_25 ? 30 : 15; resWidget.options ??= {}; resWidget.options.values = limited ? LIMITED_RESOLUTIONS : ALL_RESOLUTIONS; durationWidget.options ??= {}; durationWidget.options.values = maximumDuration === 30 ? DURATIONS_25 : DURATIONS_20; if (limited && !LIMITED_RESOLUTIONS.includes(resWidget.value)) { setWidgetValue(resWidget, RESOLUTION_FALLBACK); if (!silent) { notify(`${modelWidget.value} 仅支持 480p/720p,已自动切换为 ${RESOLUTION_FALLBACK}。`); } } const duration = Number.parseInt(durationWidget.value, 10); if (Number.isFinite(duration) && duration > maximumDuration) { setWidgetValue(durationWidget, `${maximumDuration}秒`); if (!silent) { notify(`${modelWidget.value} 的最大时长为 ${maximumDuration} 秒,已自动调整。`); } } node.setDirtyCanvas?.(true, true); } function guardNode(node) { if (node.__o1keySeedanceParameterGuard) return; node.__o1keySeedanceParameterGuard = true; const modelWidget = findWidget(node, "主模型"); const resWidget = findWidget(node, "分辨率"); const durationWidget = findWidget(node, "时长"); if (!modelWidget || !resWidget || !durationWidget) return; const origModelCb = modelWidget.callback; modelWidget.callback = function (value) { const ret = origModelCb?.apply(this, arguments); updateDynamicParameters(node); return ret; }; const origResCb = resWidget.callback; resWidget.callback = function (value) { if (isLimitedModel(modelWidget.value) && !LIMITED_RESOLUTIONS.includes(value)) { resWidget.value = RESOLUTION_FALLBACK; notify(`${modelWidget.value} 仅支持 480p/720p。`); node.setDirtyCanvas?.(true, true); return origResCb?.call(this, RESOLUTION_FALLBACK); } return origResCb?.apply(this, arguments); }; const origDurationCb = durationWidget.callback; durationWidget.callback = function (value) { const maximumDuration = modelWidget.value === MODEL_25 ? 30 : 15; const duration = Number.parseInt(value, 10); if (Number.isFinite(duration) && duration > maximumDuration) { const fallback = `${maximumDuration}秒`; durationWidget.value = fallback; notify(`${modelWidget.value} 的最大时长为 ${maximumDuration} 秒。`); node.setDirtyCanvas?.(true, true); return origDurationCb?.call(this, fallback); } return origDurationCb?.apply(this, arguments); }; updateDynamicParameters(node, true); } app.registerExtension({ name: "o1key.seedanceResolutionGuard", nodeCreated(node) { if (SEEDANCE_NODES.has(node.comfyClass)) { guardNode(node); } }, loadedGraphNode(node) { if (!SEEDANCE_NODES.has(node.comfyClass)) return; guardNode(node); updateDynamicParameters(node, true); }, });