Compare commits

...
173 changed files with 4532 additions and 135 deletions
@@ -13,12 +13,14 @@ from calliope.comfyui.registry import (
IMAGE_OUTPUT_CLASSES,
IMAGE_URL_CLASSES,
NUMBER_CLASSES,
OPTIONAL_FRAME_INPUTS,
TEXT_AREA_CLASSES,
VIDEO_CLASSES,
VIDEO_OUTPUT_CLASSES,
ComfyOutputKind,
class_to_input_kind,
class_to_output_kind,
optional_frame_sibling,
)
from calliope.comfyui.roles import (
USAGE_VALUES,
@@ -689,6 +691,8 @@ def detect_untagged_inputs(workflow: dict[str, Any]) -> list[dict[str, Any]]:
)
)
media = _with_optional_frames(workflow, media)
results: list[dict[str, Any]] = []
positives = [t for t in texts if not t[3]]
negatives = [t for t in texts if t[3]]
@@ -732,6 +736,42 @@ def detect_untagged_inputs(workflow: dict[str, Any]) -> list[dict[str, Any]]:
return results
def _with_optional_frames(
workflow: dict[str, Any], media: list[dict[str, Any]]
) -> list[dict[str, Any]]:
"""Insert an optional slot after each frame slot whose node also accepts
an unwired optional frame (H3 FL2VA: 首帧 → 尾帧)."""
out = list(media)
for nid, node in sorted(
((str(k), v) for k, v in workflow.items() if isinstance(v, dict)),
key=lambda kv: _node_sort_key(kv[0]),
):
for field in OPTIONAL_FRAME_INPUTS.get(str(node.get("class_type") or ""), {}):
sibling = optional_frame_sibling(node, field)
if sibling is None:
continue
loader_id = str(node["inputs"][sibling][0])
loader = workflow.get(loader_id)
if not isinstance(loader, dict) or loader.get("class_type") not in IMAGE_CLASSES:
continue
at = next(
(
i
for i, e in enumerate(out)
if e["nodeId"] == _key(loader_id, loader, "image")
),
None,
)
if at is None:
continue
role, label = _FRAME_SLOT_ROLES[field]
out.insert(
at + 1,
_entry(f"{nid}{FIELD_KEY_SEP}{field}", label, role, "image", None, False),
)
return out
def _is_negative(
node_id: str, lowered_title: str, consumers: dict[str, list[tuple[str, str]]]
) -> bool:
@@ -11,6 +11,7 @@ from calliope.comfyui.registry import (
VIDEO_CLASSES,
VIDEO_FILE_CLASSES,
class_to_patch_field,
optional_frame_sibling,
)
# Input keys of the form "<nodeId>@<widget>" target one named widget on a
@@ -142,6 +143,26 @@ def apply_media_group(
apply_image_group = apply_media_group
def apply_optional_frame(
workflow: dict[str, Any], node_id: str, field: str, sibling: str, path: str
) -> None:
"""Wire ``path`` into an unwired optional frame input (H3 ``last_frame``)
through a new LoadImage cloned from the ``sibling`` frame's loader."""
node = workflow[node_id]
inputs = dict(node.get("inputs") or {})
src = workflow.get(str(inputs[sibling][0]))
if isinstance(src, dict) and str(src.get("class_type") or "") in IMAGE_CLASSES:
loader = copy.deepcopy(src)
else:
loader = {"class_type": "LoadImage", "inputs": {}}
loader["inputs"] = {**(loader.get("inputs") or {}), "image": path}
loader["_meta"] = {"title": "尾帧" if field == "last_frame" else field}
nid = _next_node_id(workflow)
workflow[nid] = loader
inputs[field] = [nid, 0]
node["inputs"] = inputs
def patch_workflow(
base: dict[str, Any],
values_by_node_id: dict[str, Any],
@@ -167,6 +188,10 @@ def patch_workflow(
if clean:
apply_media_group(patched, key, field, clean)
continue
if field is not None and (sibling := optional_frame_sibling(node, field)):
if isinstance(value, str) and value.strip():
apply_optional_frame(patched, key, field, sibling, value)
continue
if field is None:
field = resolve_patch_field(node)
elif isinstance(inputs.get(field), list):
@@ -1,7 +1,7 @@
"""Unified class_type → kind + patch-field registry (MuseAgent port)."""
from __future__ import annotations
from typing import Literal
from typing import Any, Literal
ComfyInputKind = Literal["text", "textarea", "number", "image", "image_url", "audio", "video"]
ComfyOutputKind = Literal["image", "video", "other"]
@@ -33,6 +33,24 @@ IMAGE_OUTPUT_CLASSES = frozenset(
{"SaveImage", "PreviewImage", "SaveImageWebsocket", "ETN_SendImageWebSocket",
"SaveImageAdvanced"}
)
# Frame-conditioned video nodes and the optional frame input an export may
# leave unwired. MiniMax H3's FL2VA node takes `first_frame` plus an
# optional `last_frame`; the studio offers the slot and the patcher adds
# the LoadImage only when the user supplies that frame.
OPTIONAL_FRAME_INPUTS: dict[str, dict[str, str]] = {
"MiniMaxH3ImageToVideo": {"last_frame": "first_frame"},
}
def optional_frame_sibling(node: dict[str, Any], field: str) -> str | None:
"""The wired frame input an unwired optional ``field`` mirrors
(``last_frame`` → ``first_frame``); None when it does not apply."""
inputs = node.get("inputs") or {}
sibling = OPTIONAL_FRAME_INPUTS.get(str(node.get("class_type") or ""), {}).get(field)
if sibling is None or field in inputs:
return None
link = inputs.get(sibling)
return sibling if isinstance(link, list) and len(link) == 2 else None
def class_to_input_kind(class_type: str) -> ComfyInputKind:
+39
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import re
import sqlite3
from pathlib import Path
from typing import Any
@@ -410,6 +411,43 @@ def _backfill_workflow_output_map(conn: sqlite3.Connection) -> None:
)
# HEL-671: the user's H3 import went in under a first-frame-only name
# (“H3图生视频·首帧…”) although its graph is the FL2VA node that also takes
# a last frame. A row is renamed only when BOTH hold: its graph carries a
# wired MiniMaxH3ImageToVideo node, and its name still reads like a
# first-frame-only workflow — a row the user renamed, or any other graph,
# is left alone. Idempotent: the new name no longer matches.
_FIRST_FRAME_NAME_RE = re.compile(r"图生视频\s*[·\-—~]?\s*首帧")
_FL2VA_DESCRIPTION = "首帧必填,尾帧可选:只传首帧为图生视频,同时传尾帧为首尾帧生视频"
def _rename_workflows(conn: sqlite3.Connection) -> None:
for wid, name, wf_json in conn.execute(
"SELECT id, name, workflow_json FROM workflows"
).fetchall():
if not name or "尾帧" in name or not _FIRST_FRAME_NAME_RE.search(name):
continue
try:
workflow = json.loads(wf_json) if wf_json else None
except (json.JSONDecodeError, TypeError):
continue
if not isinstance(workflow, dict):
continue
fl2va = any(
isinstance(node, dict)
and str(node.get("class_type") or "") == "MiniMaxH3ImageToVideo"
and isinstance((node.get("inputs") or {}).get("first_frame"), list)
for node in workflow.values()
)
if not fl2va:
continue
conn.execute(
"UPDATE workflows SET name = ?,"
" description = COALESCE(NULLIF(description, ''), ?) WHERE id = ?",
(_FIRST_FRAME_NAME_RE.sub("首尾帧生视频", name), _FL2VA_DESCRIPTION, wid),
)
async def migrate_db(db_path: Path) -> None:
db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(db_path)
@@ -472,6 +510,7 @@ async def migrate_db(db_path: Path) -> None:
# upgrades can settle them); confirmed rows are never touched.
_backfill_workflow_usage(conn)
_backfill_workflow_output_map(conn)
_rename_workflows(conn)
scene_cols = {r[1] for r in conn.execute("PRAGMA table_info(scenes)").fetchall()}
if "location_id" not in scene_cols:
conn.execute("ALTER TABLE scenes ADD COLUMN location_id INTEGER")
@@ -36,6 +36,7 @@ QWEN_EDIT = "qwen21_edit_sage_API.json" # qwen-image-2.1-图片编辑-sage加
EXAMPLES = Path(__file__).resolve().parents[2] / "example_ComfyUI_workflows"
REF_KEY = "131@ref_images"
LAST_KEY = "131@last_frame"
def _load(name: str) -> dict:
@@ -61,7 +62,8 @@ def _ref_links(wf: dict, node_id: str = "131") -> list[tuple[str, str]]:
# ---------------------------------------------------------------------------
def test_i2v_exposes_one_required_first_frame_and_no_last_frame():
def test_i2v_exposes_required_first_frame_and_optional_last_frame():
"""HEL-671: the 04 export is H3 FL2VA — `last_frame` is unwired but real."""
inputs = parse_dynamic_inputs(_load(I2V))
media = [i for i in inputs if i["kind"] in ("image", "image_list", "video", "audio")]
assert media == [
@@ -73,10 +75,36 @@ def test_i2v_exposes_one_required_first_frame_and_no_last_frame():
"defaultValue": None,
"required": True,
"auto": True,
}
},
{
"nodeId": LAST_KEY,
"label": "尾帧",
"role": "last_frame",
"kind": "image",
"defaultValue": None,
"required": False,
"auto": True,
},
]
# the title comment about FL2V is not a slot
assert not any(i.get("role") == "last_frame" for i in inputs)
def test_wired_last_frame_is_its_own_slot_not_a_second_virtual_one():
wf = _load(I2V)
wf["950"] = {"class_type": "LoadImage", "inputs": {"image": "last.png"}}
wf["131"]["inputs"]["last_frame"] = ["950", 0]
media = [i for i in parse_dynamic_inputs(wf) if i["kind"] == "image"]
assert [(i["nodeId"], i["role"], i["required"]) for i in media] == [
("901", "first_frame", True),
("950", "last_frame", True),
]
def test_first_frame_through_a_custom_loader_gets_no_last_frame_slot():
# Repo example: VHS_LoadImagePath → resize → first_frame; no slot pair to mirror.
wf = json.loads(
(EXAMPLES / "minimax_h3-Turbo_Image2Video_20260813_API.json").read_text(encoding="utf-8")
)
assert not any(i.get("role") == "last_frame" for i in parse_dynamic_inputs(wf))
def test_ref2v_exposes_one_group_of_one_to_nine_reference_images():
@@ -201,6 +229,29 @@ def test_single_slot_values_patch_first_frame_and_qwen_input():
assert qwen["4"]["inputs"]["resolution"] == 1536
def test_patch_last_frame_adds_its_own_load_image():
base = _load(I2V)
wf = patch_workflow(base, {"901": "/u/first.png", LAST_KEY: "/u/last.png"})
node = wf["131"]["inputs"]
assert node["first_frame"] == ["901", 0]
last_id = node["last_frame"][0]
assert last_id not in base and node["last_frame"] == [last_id, 0]
assert wf[last_id]["class_type"] == "LoadImage"
assert wf[last_id]["inputs"] == {"image": "/u/last.png"}
assert wf["901"]["inputs"]["image"] == "/u/first.png"
# base untouched; patching the result again rewires nothing
assert "last_frame" not in base["131"]["inputs"]
again = patch_workflow(wf, {LAST_KEY: "/u/other.png"})
assert again["131"]["inputs"]["last_frame"] == [last_id, 0]
def test_patch_without_last_frame_leaves_first_frame_only_graph():
for values in ({"901": "/u/first.png"}, {"901": "/u/first.png", LAST_KEY: ""}):
wf = patch_workflow(_load(I2V), values)
assert "last_frame" not in wf["131"]["inputs"]
assert sorted(wf) == sorted(_load(I2V))
def test_legacy_job_values_keyed_by_load_image_still_patch():
"""Jobs queued before HEL-633 carry the LoadImage id, not the group key."""
wf = patch_workflow(_load(REF), {"901": "/assets/uploads/old.png"})
@@ -265,6 +316,14 @@ def test_validate_rejects_bad_reference_files(uploads_dir):
def test_validate_requires_first_frame_and_input_image():
_, errs = validate_input_values(parse_dynamic_inputs(_load(I2V)), {"1": "p"})
assert errs == ["请上传首帧"]
_, errs = validate_input_values(
parse_dynamic_inputs(_load(I2V)), {"1": "p", LAST_KEY: "/u/l.png"}
)
assert errs == ["请上传首帧"]
clean, errs = validate_input_values(
parse_dynamic_inputs(_load(I2V)), {"1": "p", "901": "/u/f.png"}
)
assert errs == [] and LAST_KEY not in clean
_, errs = validate_input_values(parse_dynamic_inputs(_load(QWEN_EDIT)), {"4@prompt": "p"})
assert errs == ["请上传输入图"]
@@ -403,9 +462,67 @@ def test_i2v_chain_uploads_first_frame(client, named_comfy):
prompt = named_comfy.prompts[0]["prompt"]
assert prompt["901"]["inputs"]["image"] == f"calliope/{Path(first).name}"
assert prompt["131"]["inputs"]["first_frame"] == ["901", 0]
assert "last_frame" not in prompt["131"]["inputs"]
assert prompt["124"]["inputs"]["denoise"] == 0.8
def test_fl2v_chain_uploads_first_and_last_frame_into_their_own_inputs(client, named_comfy):
with _real_run(client, named_comfy):
wf_id = _import_workflow(client, "H3 首尾帧生视频", "video", _load(I2V))
listed = next(w for w in client.get("/api/workflows").json() if w["id"] == wf_id)
assert listed["usage"] == "image2video"
slots = {i["nodeId"]: i for i in listed["input_schema"] if i["kind"] == "image"}
assert slots["901"]["role"] == "first_frame" and slots[LAST_KEY]["role"] == "last_frame"
last = _upload(client, "尾帧.png")
r = _generate(client, wf_id, {"1": "从白天过渡到夜晚", LAST_KEY: last})
assert r.status_code == 400 and "请上传首帧" in r.json()["detail"]
first = _upload(client, "首帧.png")
r = _generate(client, wf_id, {"1": "从白天过渡到夜晚", "901": first, LAST_KEY: last})
assert r.status_code == 200, r.text
assert _wait_done(client, r.json()["job"]["id"])["status"] == "done"
prompt = named_comfy.prompts[0]["prompt"]
node = prompt["131"]["inputs"]
assert prompt[node["first_frame"][0]]["inputs"]["image"] == f"calliope/{Path(first).name}"
assert prompt[node["last_frame"][0]]["inputs"]["image"] == f"calliope/{Path(last).name}"
assert node["first_frame"][0] != node["last_frame"][0]
assert sorted(named_comfy.upload_names) == sorted([Path(first).name, Path(last).name])
def test_migration_renames_the_imported_first_frame_workflow(tmp_path):
import asyncio
import sqlite3
from calliope.db import migrate_db
db = tmp_path / "c.db"
asyncio.run(migrate_db(db))
conn = sqlite3.connect(db)
i2v, ref = json.dumps(_load(I2V)), json.dumps(_load(REF))
conn.executemany(
"INSERT INTO workflows (name, kind, workflow_json, description) VALUES (?, 'video', ?, ?)",
[
("H3 图生视频·首帧(04 用户提供)", i2v, None),
("H3图生视频·首帧", i2v, "我自己写的说明"),
("图生视频·首帧模板", ref, None), # not the FL2VA graph — left alone
("我的视频工作流", i2v, None), # user already renamed — left alone
],
)
conn.commit()
conn.close()
asyncio.run(migrate_db(db))
asyncio.run(migrate_db(db)) # idempotent
conn = sqlite3.connect(db)
rows = conn.execute("SELECT name, description FROM workflows ORDER BY id").fetchall()
conn.close()
assert rows[0] == (
"H3 首尾帧生视频(04 用户提供)",
"首帧必填,尾帧可选:只传首帧为图生视频,同时传尾帧为首尾帧生视频",
)
assert rows[1] == ("H3首尾帧生视频", "我自己写的说明") # suffix kept, description kept
assert rows[2] == ("图生视频·首帧模板", None)
assert rows[3] == ("我的视频工作流", None)
def test_broken_qwen_import_warns_and_generation_is_refused(client, named_comfy):
with _real_run(client, named_comfy):
analyzed = client.post("/api/workflows/analyze", json={"workflow_json": _load(QWEN_EDIT)})
@@ -82,13 +82,16 @@ def test_a2_qwen_edit_variant_with_new_ids_titles_and_model_stays_image2image():
assert len(mask) == 1 # mask never counts as an input image
def test_a3_i2v_variant_renamed_still_image2video_first_frame_only():
def test_a3_i2v_variant_renamed_still_image2video_with_optional_last_frame():
wf = _rename_everything(_load(FIXTURES / "h3_i2v_04_API.json"))
c = classify_usage(wf)
assert c["usage"] == "image2video"
media = _media(parse_dynamic_inputs(wf))
assert [i.get("role") for i in media] == ["first_frame"]
# A3: no first/last pair → the pair validation must not fire
# HEL-671: the FL2VA node's unwired last_frame is offered, but optional
assert [(i.get("role"), i["required"]) for i in media] == [
("first_frame", True),
("last_frame", False),
]
assert blocking_problem(wf) is None
@@ -167,28 +167,34 @@
<div class="stage-media" in:xfade out:xfade>
{#if output && video}
<!-- svelte-ignore a11y_media_has_caption -->
<video
bind:this={videoEl}
src={assetUrl(output)}
controls
playsinline
preload="metadata"
onloadedmetadata={(e) => {
clearOutputMissing(output);
noteVideo(e);
}}
onerror={() => output && markOutputMissing(output)}
></video>
<div class="print a-photo">
{#if st.tone === 'ok' && !fileGone}<span class="a-fix">{t('jobst.done')}</span>{/if}
<video
bind:this={videoEl}
src={assetUrl(output)}
controls
playsinline
preload="metadata"
onloadedmetadata={(e) => {
clearOutputMissing(output);
noteVideo(e);
}}
onerror={() => output && markOutputMissing(output)}
></video>
</div>
{:else if output}
<img
src={assetUrl(output)}
alt={prompt || t('desk.tabResult')}
onload={(e) => {
clearOutputMissing(output);
noteImg(e);
}}
onerror={() => output && markOutputMissing(output)}
/>
<div class="print a-photo">
{#if st.tone === 'ok' && !fileGone}<span class="a-fix">{t('jobst.done')}</span>{/if}
<img
src={assetUrl(output)}
alt={prompt || t('desk.tabResult')}
onload={(e) => {
clearOutputMissing(output);
noteImg(e);
}}
onerror={() => output && markOutputMissing(output)}
/>
</div>
{:else if fileGone}
<div class="stage-empty gone">
<span>{t('desk.missingTitle')}</span>
@@ -362,14 +368,24 @@
inset: 0;
display: grid;
place-items: center;
padding: var(--sp-5);
box-sizing: border-box;
}
.stage-media > img,
.stage-media > video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
/* 相纸白边 (HEL-680):检视舞台上的结果按相纸呈现,媒体自适应内含 */
.print {
position: relative;
display: grid;
max-width: 100%;
max-height: 100%;
--paper-w: 5px;
}
.print > img,
.print > video {
display: block;
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
}
.stage-empty {
display: grid;
@@ -154,6 +154,7 @@
{@const reject = rejects[s.inp.nodeId]?.[0]}
<MediaSlot
label={s.label}
hint={s.lastFrame && !s.inp.required ? t('studio.slotOptional') : undefined}
required={s.inp.required}
value={String(values[s.inp.nodeId] ?? '')}
kind={s.mediaKind}
@@ -183,7 +183,8 @@
<div class="out">
{#if output}
<div class="thumb" class:portrait>
<div class="thumb a-photo" class:portrait>
{#if st.tone === 'ok' && !fileGone}<span class="a-fix">{t('jobst.done')}</span>{/if}
{#if isVideoOutput(job, output)}
<video
src={assetUrl(output)}
@@ -238,15 +239,42 @@
background-color var(--d-state) ease,
box-shadow var(--d-state) ease;
}
/* 选中框:金色描边从上一条记录滑到新选中的记录(feed 驱动 WAAPI) */
/* 选中框:取景框角线从上一条记录滑到新选中的记录(feed 驱动 WAAPI,
HEL-680 四角短线取代整框描边) */
.sel-mark {
position: absolute;
inset: -1px;
z-index: 3;
display: none;
border: 1px solid var(--accent-line);
border-radius: inherit;
pointer-events: none;
background-image:
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0);
background-position:
left top,
left top,
right top,
right top,
left bottom,
left bottom,
right bottom,
right bottom;
background-size:
14px 2px,
2px 14px,
14px 2px,
2px 14px,
14px 2px,
2px 14px,
14px 2px,
2px 14px;
background-repeat: no-repeat;
}
.take.selected {
border-color: transparent;
@@ -458,6 +486,12 @@
background: var(--bg-4);
box-shadow: inset 0 0 0 1px var(--on-media-line);
}
/* 相纸白边 (HEL-680):有产出的记录缩略图按相纸呈现,压过占位态样式 */
.thumb.a-photo {
border-radius: 2px;
background: var(--accent);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
}
.thumb.portrait {
width: 96px;
aspect-ratio: 3 / 4;
@@ -282,7 +282,9 @@ export function validateForm(
if (first && last) {
const a = String(values[first.inp.nodeId] ?? '');
const b = String(values[last.inp.nodeId] ?? '');
if ((!a || !b) && (a || b)) {
// An optional last frame (H3 FL2VA) may stay empty; it never goes without a first frame.
const unpaired = last.inp.required ? (!a || !b) && (a || b) : !a && !!b;
if (unpaired) {
errs.firstLast = t('studio.errFirstLast');
if (!a) errs[first.inp.nodeId] = ' ';
if (!b) errs[last.inp.nodeId] = ' ';
@@ -52,7 +52,12 @@
'var(--avatar-4)',
'var(--avatar-5)',
];
const placeholder = $derived(PLACEHOLDER_GRADIENTS[project.id % PLACEHOLDER_GRADIENTS.length]);
const placeholderIdx = $derived(project.id % PLACEHOLDER_GRADIENTS.length);
const placeholder = $derived(PLACEHOLDER_GRADIENTS[placeholderIdx]);
// HEL-680 灰阶封面占位:1–2 亮阶配深图标,3–5 暗阶配浅图标
const placeholderIconColor = $derived(
placeholderIdx <= 1 ? 'var(--accent-ink)' : 'var(--text)',
);
const coverUrl = $derived(assetUrl(project.cover_path));
@@ -174,7 +179,12 @@
onerror={() => (coverFailed = true)}
/>
{:else}
<span class="cover-ph" style:background={placeholder} aria-hidden="true">
<span
class="cover-ph"
style:background={placeholder}
style:color={placeholderIconColor}
aria-hidden="true"
>
<Icon name="film" size={22} />
</span>
{/if}
@@ -402,6 +402,7 @@
flex-shrink: 0;
margin-bottom: var(--sp-5);
border-radius: var(--r-md);
border-bottom: 2px solid var(--accent-lo);
display: grid;
place-items: center;
background: var(--accent-grad);
@@ -132,9 +132,7 @@ let {
min-height: 0;
width: 100%;
height: 100%;
background:
radial-gradient(ellipse at center, rgba(139, 92, 246, 0.08), transparent 55%),
#050508;
background: var(--stage-bg);
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
@@ -79,14 +79,14 @@
let criticNotes = $state<string[]>([]);
const preview = createMutation({
mutationFn: async (vars?: { force?: boolean }) => {
mutationFn: async (vars: { force?: boolean } | void) => {
if (!clip && !scene) throw new Error(t('promptPreview.noClipSelected'));
return jobsApi.previewPrompt(projectId, {
clip_id: clip?.id,
scene_id: clip ? undefined : scene?.id,
workflow_id: workflow?.id,
input_values: compactInputValues(inputValues),
force: Boolean(vars?.force),
force: Boolean((vars as { force?: boolean } | undefined)?.force),
});
},
onSuccess: (data) => {
@@ -203,7 +203,7 @@ title={t('filmstrip.clipTitle', {
inset: 0 0 auto 0;
height: 3px;
z-index: 1;
background: #3f3f46;
background: var(--bg-4);
}
.clip.status-done .bar {
+2 -1
View File
@@ -1558,6 +1558,7 @@ export const de: Dict = {
'studio.slotMask': 'Maske',
'studio.slotFirst': 'Erster Frame',
'studio.slotLast': 'Letzter Frame',
'studio.slotOptional': 'Optional',
'studio.slotRefVideo': 'Referenzvideo',
'studio.slotAudio': 'Referenzaudio',
'studio.slotPick': 'Klicken oder Datei ziehen',
@@ -1901,7 +1902,7 @@ export const de: Dict = {
'home.stageProgress': 'Fünf-Phasen-Fortschritt',
'tasks.sourceWorkbench': 'Kreativ-Fluss',
'jobst.idSuffix': ' · ComfyUI-Prompt-ID {id}',
'jobst.done': 'Fertig',
'jobst.done': 'Fixiert',
'jobst.doneDetail': 'ComfyUI ist fertig, Ergebnisse gespeichert',
'jobst.dryRun': 'Platzhalter',
'jobst.dryRunDetail': 'Nur Platzhalter: Dry-run ist aktiv, ComfyUI wurde nicht aufgerufen',
+2 -1
View File
@@ -1557,6 +1557,7 @@ export const en = {
'studio.slotMask': 'Mask',
'studio.slotFirst': 'First frame',
'studio.slotLast': 'Last frame',
'studio.slotOptional': 'Optional',
'studio.slotRefVideo': 'Reference video',
'studio.slotAudio': 'Reference audio',
'studio.slotPick': 'Click or drop a file',
@@ -1900,7 +1901,7 @@ export const en = {
'home.stageProgress': 'Five-stage progress',
'tasks.sourceWorkbench': 'Creation flow',
'jobst.idSuffix': ' · ComfyUI prompt ID {id}',
'jobst.done': 'Done',
'jobst.done': 'Fixed',
'jobst.doneDetail': 'ComfyUI finished and the results are saved',
'jobst.dryRun': 'Placeholder',
'jobst.dryRunDetail': 'Placeholder only: Dry-run is on, ComfyUI was not called',
+2 -1
View File
@@ -1558,6 +1558,7 @@ export const es: Dict = {
'studio.slotMask': 'Máscara',
'studio.slotFirst': 'Primer fotograma',
'studio.slotLast': 'Último fotograma',
'studio.slotOptional': 'Opcional',
'studio.slotRefVideo': 'Vídeo de referencia',
'studio.slotAudio': 'Audio de referencia',
'studio.slotPick': 'Haz clic o arrastra',
@@ -1901,7 +1902,7 @@ export const es: Dict = {
'home.stageProgress': 'Progreso de cinco etapas',
'tasks.sourceWorkbench': 'Flujo de creación',
'jobst.idSuffix': ' · ID de prompt de ComfyUI {id}',
'jobst.done': 'Listo',
'jobst.done': 'Fijado',
'jobst.doneDetail': 'ComfyUI terminó y los resultados se guardaron',
'jobst.dryRun': 'Marcador',
'jobst.dryRunDetail': 'Solo marcador: Dry-run activo, no se llamó a ComfyUI',
+2 -1
View File
@@ -1558,6 +1558,7 @@ export const fr: Dict = {
'studio.slotMask': 'Masque',
'studio.slotFirst': 'Premier cadre',
'studio.slotLast': 'Dernier cadre',
'studio.slotOptional': 'Facultatif',
'studio.slotRefVideo': 'Vidéo de référence',
'studio.slotAudio': 'Audio de référence',
'studio.slotPick': 'Cliquez ou déposez',
@@ -1901,7 +1902,7 @@ export const fr: Dict = {
'home.stageProgress': 'Progression en cinq étapes',
'tasks.sourceWorkbench': 'Flux de création',
'jobst.idSuffix': ' · ID de prompt ComfyUI {id}',
'jobst.done': 'Terminé',
'jobst.done': 'Fixé',
'jobst.doneDetail': 'ComfyUI a terminé, résultats enregistrés',
'jobst.dryRun': 'Substitut',
'jobst.dryRunDetail': 'Substitut uniquement : Dry-run actif, ComfyUI n\'a pas été appelé',
+2 -1
View File
@@ -1556,6 +1556,7 @@ export const ja: Dict = {
'studio.slotMask': 'マスク',
'studio.slotFirst': '始端フレーム',
'studio.slotLast': '終端フレーム',
'studio.slotOptional': '任意',
'studio.slotRefVideo': '参照動画',
'studio.slotAudio': '参照音声',
'studio.slotPick': 'クリックまたはドロップ',
@@ -1899,7 +1900,7 @@ export const ja: Dict = {
'home.stageProgress': '五段階の進捗',
'tasks.sourceWorkbench': '制作フロー',
'jobst.idSuffix': ' · ComfyUI プロンプト ID {id}',
'jobst.done': '完了',
'jobst.done': '定着',
'jobst.doneDetail': 'ComfyUI の実行が完了し、結果を保存しました',
'jobst.dryRun': 'プレースホルダー',
'jobst.dryRunDetail': 'プレースホルダーのみ:Dry-run が有効で ComfyUI は呼び出されていません',
+2 -1
View File
@@ -1556,6 +1556,7 @@ export const ko: Dict = {
'studio.slotMask': '마스크',
'studio.slotFirst': '첫 프레임',
'studio.slotLast': '마지막 프레임',
'studio.slotOptional': '선택',
'studio.slotRefVideo': '참조 비디오',
'studio.slotAudio': '참조 오디오',
'studio.slotPick': '클릭하거나 끌어다 놓기',
@@ -1899,7 +1900,7 @@ export const ko: Dict = {
'home.stageProgress': '5단계 진행',
'tasks.sourceWorkbench': '제작 흐름',
'jobst.idSuffix': ' · ComfyUI 프롬프트 ID {id}',
'jobst.done': '완료',
'jobst.done': '정착',
'jobst.doneDetail': 'ComfyUI 실행이 끝나 결과를 저장했습니다',
'jobst.dryRun': '자리표시자',
'jobst.dryRunDetail': '자리표시자만: Dry-run이 켜져 있어 ComfyUI를 호출하지 않았습니다',
+3 -2
View File
@@ -1555,6 +1555,7 @@ export const zh: Dict = {
'studio.slotMask': '遮罩',
'studio.slotFirst': '首帧',
'studio.slotLast': '尾帧',
'studio.slotOptional': '可选',
'studio.slotRefVideo': '参考视频',
'studio.slotAudio': '参考音频',
'studio.slotPick': '点击或拖入',
@@ -1899,8 +1900,8 @@ export const zh: Dict = {
'home.stageProgress': '五段进度',
'tasks.sourceWorkbench': '创作流程',
'jobst.idSuffix': ' · ComfyUI 任务 ID {id}',
'jobst.done': '完成',
'jobst.doneDetail': 'ComfyUI 已执行完成,结果已保存',
'jobst.done': '定影',
'jobst.doneDetail': '生成完成,结果已定影保存',
'jobst.dryRun': '占位结果',
'jobst.dryRunDetail': '占位结果:当前开启了 Dry-run,没有调用 ComfyUI,不是真实生成',
'jobst.noOutput': '无产出',
+7 -2
View File
@@ -589,6 +589,11 @@
function avatarStyle(n: number): string {
return `var(--avatar-${Math.min(Math.max(n, 1), 5)})`;
}
// HEL-680 灰阶头像:1–2 亮阶配深字,3–5 暗阶配浅字,按阶数决定。
function avatarLight(n: number | undefined): boolean {
return Math.min(Math.max(n ?? 1, 1), 5) <= 2;
}
</script>
{#snippet skillRow(skill: SkillAdminEntry, detail: string)}
@@ -655,7 +660,7 @@
class:selected
onclick={() => selectAgent(agent.key)}
>
<span class="avatar" class:dark={agent.avatar_dark} style:background={avatarStyle(agent.avatar)}>
<span class="avatar" class:dark={avatarLight(agent.avatar)} style:background={avatarStyle(agent.avatar)}>
{agent.char}
</span>
<span class="agent-meta">
@@ -693,7 +698,7 @@
<section class="mid">
{#if activeAgent}
<header class="chat-head">
<span class="avatar lg" class:dark={activeAgent.avatar_dark} style:background={avatarStyle(activeAgent.avatar)}>
<span class="avatar lg" class:dark={avatarLight(activeAgent.avatar)} style:background={avatarStyle(activeAgent.avatar)}>
{activeAgent.char}
</span>
<div class="chat-head-meta">
+181 -59
View File
@@ -4,62 +4,67 @@
:root {
/* ------------------------------------------------------------------
* 融合视觉 design tokens — HEL-649 整站施工规范 §一(流水台结构 ×
* 放映厅配色)。Single source of truth for the whole app: pages and
* components reference these variables only — no hardcoded colors,
* no ad-hoc overrides. Gold is for key actions and the current
* selection only; status colors each carry exactly one meaning.
* 无彩灰阶 design tokens — HEL-680 S1(白栖知 theme-1「暗房放大机」
* × HEL-679 已确认融合样稿)。Single source of truth for the whole
* app: pages and components reference these variables only — no
* hardcoded colors, no ad-hoc overrides. 界面只有暖调黑白灰,作品是
* 屏幕上唯一成片的颜色;状态色仅按主题卡的两处有限例外(琥珀
* #A3915F、红 #B3564A)使用,不放大成全站主色。选中=取景框角线,
* 完成=「定影」,结果=相纸白边。
* ------------------------------------------------------------------ */
/* 底色:蓝黑四阶,逐层浮起 (§1.1) */
--bg: #07090e;
--bg-2: #0c0f16;
--bg-3: #121724;
--bg-4: #1a2133;
--line: #1b2233;
--line-2: #2a3450;
--page-glow: radial-gradient(1200px 500px at 70% -10%, rgba(38, 52, 92, 0.25), transparent 60%);
--float-bg: linear-gradient(180deg, #10152a, var(--bg-2));
--overlay: rgba(4, 6, 10, 0.6);
--media-scrim: rgba(7, 9, 14, 0.65);
/* 底色:显影灰阶四阶,逐层浮起(theme-1:#212120 底 / #2C2C2A 面板) */
--bg: #212120;
--bg-2: #262625;
--bg-3: #2c2c2a;
--bg-4: #383835;
--line: #313130;
--line-2: #454542;
--page-glow: radial-gradient(1200px 500px at 70% -10%, rgba(245, 244, 238, 0.035), transparent 60%);
--float-bg: linear-gradient(180deg, #2e2e2c, var(--bg-2));
--overlay: rgba(18, 18, 17, 0.62);
--media-scrim: rgba(18, 18, 17, 0.66);
--on-media: #fff;
--on-media-line: rgba(255, 255, 255, 0.05);
--composer-bg: linear-gradient(180deg, #0e1220, var(--bg-2));
--composer-focus: 0 0 0 1px rgba(242, 199, 132, 0.12), 0 8px 28px rgba(0, 0, 0, 0.4);
--composer-bg: linear-gradient(180deg, #2a2a28, var(--bg-2));
--composer-focus: 0 0 0 1px rgba(245, 244, 238, 0.12), 0 8px 28px rgba(0, 0, 0, 0.4);
/* 放映金:主按钮、当前选中、改过的参数、聚焦框、Toast 动作 (§1.2) */
--accent: #f2c784;
--accent-ink: #23170a;
--accent-soft: rgba(242, 199, 132, 0.13);
--accent-hi: #f7d9a2;
--accent-lo: #ecaf63;
--accent-glow: rgba(236, 175, 99, 0.28);
--accent-line: rgba(242, 199, 132, 0.55);
/* 相纸白(重点色):主按钮、当前选中、改过的参数、聚焦框、Toast 动作
(theme-1 #F5F4EE 重点) */
--accent: #f5f4ee;
--accent-ink: #212120;
--accent-soft: rgba(245, 244, 238, 0.08);
--accent-hi: #ffffff;
--accent-lo: #ddddd7;
--accent-glow: rgba(240, 239, 233, 0.2);
--accent-line: rgba(245, 244, 238, 0.55);
--accent-grad: linear-gradient(180deg, var(--accent-hi), var(--accent-lo));
--accent-grad-hover: linear-gradient(180deg, #fbe3b6, #f0b968);
--accent-ring: 0 0 0 1px rgba(242, 199, 132, 0.7), 0 0 22px var(--accent-glow);
--btn-glow: 0 2px 14px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, 0.35);
--btn-glow-hover: 0 4px 18px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, 0.4);
--accent-wash: linear-gradient(180deg, rgba(242, 199, 132, 0.05), transparent 40%);
--sel-shadow: 0 0 0 1px rgba(242, 199, 132, 0.15), 0 6px 24px rgba(0, 0, 0, 0.35);
--accent-grad-hover: linear-gradient(180deg, #ffffff, #e8e7df);
--accent-ring: 0 0 0 1px rgba(245, 244, 238, 0.7), 0 0 22px var(--accent-glow);
--btn-glow: 0 2px 10px rgba(0, 0, 0, 0.38), inset 0 1px 0 rgba(255, 255, 255, 0.55);
--btn-glow-hover: 0 3px 14px rgba(0, 0, 0, 0.44), inset 0 1px 0 rgba(255, 255, 255, 0.65);
--accent-wash: linear-gradient(180deg, rgba(245, 244, 238, 0.05), transparent 40%);
--sel-shadow: 0 0 0 1px rgba(245, 244, 238, 0.14), 0 6px 24px rgba(0, 0, 0, 0.35);
/* 信号色:绿=完成/已确认/通过,青=进行中/在线,红=失败/危险,
琥珀=待确认/文件缺失/限额 (§1.3) */
--ok: #49cb90;
--ok-soft: rgba(73, 203, 144, 0.13);
--run: #54dfd2;
--run-soft: rgba(84, 223, 210, 0.13);
--run-grad: linear-gradient(90deg, #3bbfb4, var(--run));
--err: #f0645a;
--err-soft: rgba(240, 100, 90, 0.13);
--err-line: rgba(240, 100, 90, 0.6);
--warn: #f0b45c;
--warn-soft: rgba(240, 180, 92, 0.13);
/* 信号色:完成=显影亮灰(「定影」,配图标/文字双编码),进行中=灰阶
+动效;红/琥珀只按 theme-1 卡片的有限例外使用(红=失败/危险
#B3564A 系,琥珀=待确认/文件缺失 #A3915F) */
--ok: #ddddd7;
--ok-soft: rgba(221, 221, 215, 0.1);
--run: #b8b6ac;
--run-soft: rgba(184, 182, 172, 0.12);
--run-grad: linear-gradient(90deg, #8f8e85, #c6c5c0);
--err: #d07a6b;
--err-solid: #b3564a;
--err-soft: rgba(181, 86, 74, 0.15);
--err-line: rgba(208, 122, 107, 0.55);
--warn: #a3915f;
--warn-soft: rgba(163, 145, 95, 0.13);
/* 文字与字体 (§1.4);--text-2 是深底可读性下限,不得再暗 */
--text: #f4f6fa;
--text-2: #aab2c5;
--text-3: #6d7690;
/* 文字与字体;--text-3 是深底可读性下限(≥4.5:1 on --bg-3),不得再暗 */
--text: #f0efe9;
--text-2: #a8a69c;
--text-3: #94938c;
--font: 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', system-ui, sans-serif;
--mono: 'JetBrains Mono', ui-monospace, monospace;
--fs-title: 15px;
@@ -95,8 +100,14 @@
/* 专属符号 (§1.7):齿孔只在记录流/输入台分隔带,光晕只在播放舞台 */
--sprocket: radial-gradient(circle 2px, var(--line-2) 98%, transparent);
--stage-bg:
radial-gradient(60% 45% at 50% 38%, rgba(242, 199, 132, 0.07), transparent 70%),
radial-gradient(80% 70% at 50% 50%, #0d1220, #05070c);
radial-gradient(60% 45% at 50% 38%, rgba(245, 244, 238, 0.045), transparent 70%),
radial-gradient(80% 70% at 50% 50%, #232322, #171716);
/* 取景框角线与相纸白边 (HEL-680):内容级选中/结果呈现的统一语言 */
--frame-w: 2px;
--frame-arm: 12px;
--frame-inset: -4px;
--paper-w: 4px;
/* 动效 (§1.6,取 HEL-648 实测值);只动 transform / opacity */
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
@@ -113,13 +124,13 @@
--d-progress: 400ms;
--press-scale: 0.97;
/* Avatar five-color set — five agents only; the one colorful gradient
family the spec keeps (§3.6). */
--avatar-1: linear-gradient(135deg, #e8a33d, #b96a1b);
--avatar-2: linear-gradient(135deg, #5b8def, #3a5fa9);
--avatar-3: linear-gradient(135deg, #3fb97f, #2a7a54);
--avatar-4: linear-gradient(135deg, #b98be0, #7a4fa9);
--avatar-5: linear-gradient(135deg, #d98e6a, #a95535);
/* Avatar five-step grayscale (HEL-680 无彩化):1–2 亮阶配深字,
3–5 暗阶配浅字;仍是五个身份位,只是从彩色渐变改为显影灰阶。 */
--avatar-1: linear-gradient(135deg, #e8e7df, #cdcbc4);
--avatar-2: linear-gradient(135deg, #b5b3aa, #9c9a91);
--avatar-3: linear-gradient(135deg, #6b6a66, #55544f);
--avatar-4: linear-gradient(135deg, #55544f, #424140);
--avatar-5: linear-gradient(135deg, #424140, #2e2e2c);
/* ------------------------------------------------------------------
* Legacy token names (§六 映射表) — aliases only, never new values,
@@ -297,7 +308,7 @@ code,
color: var(--err);
}
.a-btn.danger-solid {
background: var(--err);
background: var(--err-solid);
color: var(--on-media);
}
@media (hover: hover) and (pointer: fine) {
@@ -550,11 +561,47 @@ button.status-chip:active {
.a-chip.changed {
color: var(--accent);
}
/* Selected filter chip (§3.5 筛选芯片组): 当前选中 = 金色文字 + accent-soft 底 */
/* Selected filter chip (§3.5 筛选芯片组): 当前选中 = 取景框角线缩影 +
相纸白文字 + accent-soft 底 (HEL-680) */
.a-chip.sel {
position: relative;
background: var(--accent-soft);
color: var(--accent);
}
.a-chip.sel::after {
content: '';
position: absolute;
inset: 2px;
pointer-events: none;
background-image:
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0);
background-position:
left top,
left top,
right top,
right top,
left bottom,
left bottom,
right bottom,
right bottom;
background-size:
6px 1.5px,
1.5px 6px,
6px 1.5px,
1.5px 6px,
6px 1.5px,
1.5px 6px,
6px 1.5px,
1.5px 6px;
background-repeat: no-repeat;
}
.a-chip.warn {
color: var(--warn);
}
@@ -753,6 +800,81 @@ button.a-tile:disabled {
box-shadow: 0 0 0 1px var(--line);
}
/* ------------------------------------------------------------------
* 取景框角线选中 (HEL-680):内容级选中态的唯一语言——四角短线取景,
* 不整框描边。挂在需要角线的元素上(元素自身可 overflow 可见;被
* overflow:hidden 裁切的容器改用 --frame-inset 正值画在内侧)。
* ------------------------------------------------------------------ */
.a-frame {
position: relative;
}
.a-frame::after {
content: '';
position: absolute;
inset: var(--frame-inset, -4px);
z-index: 3;
pointer-events: none;
background-image:
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0),
linear-gradient(var(--accent) 0 0);
background-position:
left top,
left top,
right top,
right top,
left bottom,
left bottom,
right bottom,
right bottom;
background-size:
var(--frame-arm, 12px) var(--frame-w, 2px),
var(--frame-w, 2px) var(--frame-arm, 12px),
var(--frame-arm, 12px) var(--frame-w, 2px),
var(--frame-w, 2px) var(--frame-arm, 12px),
var(--frame-arm, 12px) var(--frame-w, 2px),
var(--frame-w, 2px) var(--frame-arm, 12px),
var(--frame-arm, 12px) var(--frame-w, 2px),
var(--frame-w, 2px) var(--frame-arm, 12px);
background-repeat: no-repeat;
}
/* 相纸白边 (HEL-680):结果媒体外的相纸白框,底边略厚如手工相纸。
挂在承载 img/video 的媒体容器上;容器尺寸由调用方决定。 */
.a-photo {
background: var(--accent);
border: var(--paper-w, 4px) solid var(--accent);
border-bottom-width: calc(var(--paper-w, 4px) + 2px);
border-radius: 2px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
box-sizing: border-box;
}
/* 「定影」完成章 (HEL-680):已定影结果右上角的完成印记,随 jobst.done
文案走(中文=定影)。absolute 定位,挂在有媒体的结果容器内。 */
.a-fix {
position: absolute;
top: var(--sp-3);
right: var(--sp-3);
z-index: 2;
padding: 1px 6px;
border: 1px solid currentColor;
border-radius: 3px;
background: var(--media-scrim);
color: var(--ok);
font-size: var(--fs-mini);
font-weight: 700;
letter-spacing: 0.1em;
text-indent: 0.1em;
transform: rotate(-4deg);
pointer-events: none;
}
/* Floating surface for pop / drawer / confirm / toast / lightbox (§四.3). */
.a-float {
background: var(--float-bg);
+21 -8
View File
@@ -209,7 +209,13 @@
{:else}
<div class="grid">
{#each visible as item (item.path)}
<div class="card" class:selected={selected.has(item.path)}>
<div
class="card"
class:selected={selected.has(item.path)}
class:a-frame={selected.has(item.path)}
style:--frame-inset="3px"
style:--frame-arm="16px"
>
<label class="check">
<input
type="checkbox"
@@ -217,12 +223,12 @@
onchange={() => toggle(item.path)}
/>
</label>
<button
type="button"
class="media"
onclick={() => openPreview(item)}
title={item.name}
>
<button
type="button"
class="media a-photo"
onclick={() => openPreview(item)}
title={item.name}
>
<SafeMedia src={assetUrl(item.path)} kind={item.kind} alt={item.name} controls={false} />
</button>
<div class="meta">
@@ -341,7 +347,6 @@
transition: border-color var(--d-state) ease;
}
.card.selected {
border-color: var(--accent-line);
background: linear-gradient(180deg, var(--accent-soft), transparent 32%), var(--bg-2);
}
.card :global(.media-el) {
@@ -380,6 +385,14 @@
cursor: zoom-in;
overflow: hidden;
}
/* 相纸白边 (HEL-680):素材墙里的作品按相纸呈现 */
.media.a-photo {
border: var(--paper-w, 4px) solid var(--accent);
border-bottom-width: calc(var(--paper-w, 4px) + 2px);
background: var(--accent);
border-radius: 0;
box-sizing: border-box;
}
.media :global(img),
.media :global(video) {
width: 100%;
+2 -8
View File
@@ -392,14 +392,8 @@
char: string;
dark?: boolean;
}[] = [
{
role: 'story',
labelKey: 'models.agentStory',
color: 'var(--avatar-1)',
char: '剧',
dark: true,
},
{ role: 'style', labelKey: 'models.agentStyle', color: 'var(--avatar-2)', char: '视' },
{ role: 'story', labelKey: 'models.agentStory', color: 'var(--avatar-1)', char: '剧', dark: true },
{ role: 'style', labelKey: 'models.agentStyle', color: 'var(--avatar-2)', char: '视', dark: true },
{ role: 'script', labelKey: 'models.agentScript', color: 'var(--avatar-3)', char: '分' },
{ role: 'assets', labelKey: 'models.agentAssets', color: 'var(--avatar-4)', char: '素' },
{ role: 'video', labelKey: 'models.agentVideo', color: 'var(--avatar-5)', char: '镜' },
@@ -496,7 +496,11 @@
<aside class="side-col">
{#if currentAgent}
<div class="side-card agent-card">
<span class="agent-avatar" style:background={`var(--avatar-${currentAgent.avatar})`}>
<span
class="agent-avatar"
class:light={(currentAgent.avatar ?? 1) <= 2}
style:background={`var(--avatar-${currentAgent.avatar})`}
>
{currentAgent.char}
</span>
<div class="agent-copy">
@@ -740,6 +744,10 @@
color: var(--text);
flex-shrink: 0;
}
/* HEL-680 灰阶头像:1–2 亮阶用深字 */
.agent-avatar.light {
color: var(--accent-ink);
}
.agent-copy {
display: flex;
flex-direction: column;
@@ -0,0 +1,315 @@
{
"id": "e3f2b845-8f2c-4b5a-9caf-eac1029d3e7e",
"revision": 0,
"last_node_id": 173,
"last_link_id": 338,
"nodes": [
{
"id": 169,
"type": "MiniMaxH3GuidedWriter",
"pos": [
0,
0
],
"size": [
400,
654
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "options",
"shape": 7,
"type": "H3_REWRITER_OPTIONS",
"link": null
}
],
"outputs": [
{
"name": "rewritten_prompt",
"type": "STRING",
"links": [
336
]
},
{
"name": "integrated_multimodal_description",
"type": "STRING",
"links": null
},
{
"name": "overall_soundscape",
"type": "STRING",
"links": null
},
{
"name": "non_diegetic_music",
"type": "STRING",
"links": null
}
],
"title": "Write the prompt",
"properties": {
"Node name for S&R": "MiniMaxH3GuidedWriter",
"max_duration": 30,
"cnr_id": "minimax-h3-prompt-rewriter",
"ver": "a59888df88a69385d429b6865ea441e7844f49c9"
},
"widgets_values": [
"图1的男人和图2的女人赤身躺在床上,女人跨坐在他身上,缓慢地起伏,双手撑在他胸口,头向后仰,呼吸越来越急。男人仰面躺着,喘息着低声说:\"看着我。\"女人俯下身吻他,镜头从侧面缓慢推近。\n",
"on disk: Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf [qwen35, 5.2 GB]",
"T2VA",
"16:9",
10,
true,
42,
"fixed",
false,
"",
"",
false,
"",
false,
""
],
"widgets_values_named": {
"prompt": "图1的男人和图2的女人赤身躺在床上,女人跨坐在他身上,缓慢地起伏,双手撑在他胸口,头向后仰,呼吸越来越急。男人仰面躺着,喘息着低声说:\"看着我。\"女人俯下身吻他,镜头从侧面缓慢推近。\n",
"model": "on disk: Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf [qwen35, 5.2 GB]",
"task": "T2VA",
"resolution": "16:9",
"duration": 10,
"greedy": true,
"seed": 42,
"control_after_generate": "fixed",
"keep_model_loaded": false,
"aspect_ratio": "",
"reference_material": "",
"bypass": false,
"system_prompt": "",
"repeat_last": false,
"library_pick": ""
}
},
{
"id": 170,
"type": "MiniMaxH3PromptCheck",
"pos": [
460,
0
],
"size": [
390,
420
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "options",
"shape": 7,
"type": "H3_REWRITER_OPTIONS",
"link": null
},
{
"label": "ref_0",
"name": "references.ref_0",
"shape": 7,
"type": "IMAGE,VIDEO,AUDIO",
"link": null
},
{
"name": "prompt",
"type": "STRING",
"widget": {
"name": "prompt"
},
"link": 336
}
],
"outputs": [
{
"name": "prompt",
"type": "STRING",
"links": [
337
]
},
{
"name": "integrated_multimodal_description",
"type": "STRING",
"links": null
},
{
"name": "subject_definitions",
"type": "STRING",
"links": null
},
{
"name": "summary",
"type": "STRING",
"links": null
},
{
"name": "retention_analysis",
"type": "STRING",
"links": null
},
{
"name": "detailed_description",
"type": "STRING",
"links": null
},
{
"name": "overall_soundscape",
"type": "STRING",
"links": null
},
{
"name": "non_diegetic_music",
"type": "STRING",
"links": null
},
{
"name": "findings",
"type": "STRING",
"links": [
338
]
}
],
"title": "Check it against the H3 rules",
"properties": {
"Node name for S&R": "MiniMaxH3PromptCheck",
"max_duration": 30,
"cnr_id": "minimax-h3-prompt-rewriter",
"ver": "a59888df88a69385d429b6865ea441e7844f49c9"
},
"widgets_values": [
"",
"T2VA",
5
],
"widgets_values_named": {
"prompt": "",
"task": "T2VA",
"duration": 5
}
},
{
"id": 171,
"type": "PreviewAny",
"pos": [
900,
0
],
"size": [
560,
480
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 337
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null
}
],
"title": "The finished prompt",
"properties": {
"Node name for S&R": "PreviewAny",
"cnr_id": "comfy-core",
"ver": "0.30.0"
},
"widgets_values": [],
"widgets_values_named": {}
},
{
"id": 172,
"type": "PreviewAny",
"pos": [
900,
520
],
"size": [
560,
240
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 338
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": null
}
],
"title": "What the check found",
"properties": {
"Node name for S&R": "PreviewAny",
"cnr_id": "comfy-core",
"ver": "0.30.0"
},
"widgets_values": [],
"widgets_values_named": {}
}
],
"links": [
[
336,
169,
0,
170,
2,
"STRING"
],
[
337,
170,
0,
171,
0,
"STRING"
],
[
338,
170,
8,
172,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1.1772972972972988,
"offset": [
429.8282570474093,
283.1220192192576
]
},
"frontendVersion": "1.53.10"
},
"version": 0.4
}
@@ -0,0 +1 @@
{"id":"fae76a0b-6efc-499c-ae87-9d887bcd785f","revision":0,"last_node_id":10,"last_link_id":5,"nodes":[{"id":10,"type":"DJZ-LoadLatent","pos":[960,680],"size":[390,60],"flags":{},"order":1,"mode":0,"inputs":[{"label":"latent_file","localized_name":"latent_file","name":"latent_file","type":"COMBO","widget":{"name":"latent_file"},"link":null}],"outputs":[{"label":"LATENT","localized_name":"Latent","name":"LATENT","type":"LATENT","links":[5]}],"properties":{"cnr_id":"djz-nodes","ver":"f6ca9376132600e6245d3d9d2f1e5d0ffa448d99","Node name for S&R":"DJZ-LoadLatent","aux_id":"MushroomFleet/DJZ-Nodes","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.4.1"}},"widgets_values":["latents\\ComfyUI_00001_nmxvu_1762517653.latent"]},{"id":3,"type":"VAEDecode","pos":[1460,760],"size":[140,50],"flags":{},"order":2,"mode":0,"inputs":[{"label":"Latent","localized_name":"Latent","name":"samples","type":"LATENT","link":5},{"label":"VAE","localized_name":"vae","name":"vae","type":"VAE","link":4}],"outputs":[{"label":"图像","localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[2]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.57","Node name for S&R":"VAEDecode","ue_properties":{"widget_ue_connectable":{},"version":"7.1","input_ue_unconnectable":{}}},"widgets_values":[]},{"id":4,"type":"VHS_VideoCombine","pos":[1680,750],"size":[400,334],"flags":{},"order":3,"mode":0,"inputs":[{"label":"图像","localized_name":"图像","name":"images","type":"IMAGE","link":2},{"label":"音频","localized_name":"音频","name":"audio","shape":7,"type":"AUDIO","link":null},{"label":"批次管理","localized_name":"批次管理","name":"meta_batch","shape":7,"type":"VHS_BatchManager","link":null},{"localized_name":"vae","name":"vae","shape":7,"type":"VAE","link":null},{"label":"帧率","localized_name":"帧率","name":"frame_rate","type":"FLOAT","widget":{"name":"frame_rate"},"link":null},{"label":"循环次数","localized_name":"循环次数","name":"loop_count","type":"INT","widget":{"name":"loop_count"},"link":null},{"label":"文件名前缀","localized_name":"文件名前缀","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"label":"格式","localized_name":"格式","name":"format","type":"COMBO","widget":{"name":"format"},"link":null},{"label":"Ping-Pong","localized_name":"Ping-Pong","name":"pingpong","type":"BOOLEAN","widget":{"name":"pingpong"},"link":null},{"label":"保存到输出文件夹","localized_name":"保存到输出文件夹","name":"save_output","type":"BOOLEAN","widget":{"name":"save_output"},"link":null},{"name":"pix_fmt","type":["yuv420p","yuv420p10le"],"widget":{"name":"pix_fmt"},"link":null},{"name":"crf","type":"INT","widget":{"name":"crf"},"link":null},{"label":"保存元数据","localized_name":"保存元数据","name":"save_metadata","type":"BOOLEAN","widget":{"name":"save_metadata"},"link":null},{"label":"修剪音频","localized_name":"修剪音频","name":"trim_to_audio","type":"BOOLEAN","widget":{"name":"trim_to_audio"},"link":null}],"outputs":[{"label":"文件名","localized_name":"文件名","name":"Filenames","type":"VHS_FILENAMES","links":null}],"properties":{"cnr_id":"comfyui-videohelpersuite","ver":"8e4d79471bf1952154768e8435a9300077b534fa","Node name for S&R":"VHS_VideoCombine","ue_properties":{"widget_ue_connectable":{},"version":"7.1","input_ue_unconnectable":{}}},"widgets_values":{"frame_rate":16,"loop_count":0,"filename_prefix":"video/WanVideo_","format":"video/h264-mp4","pix_fmt":"yuv420p","crf":19,"save_metadata":true,"trim_to_audio":false,"pingpong":false,"save_output":true,"videopreview":{"hidden":false,"paused":false,"params":{"filename":"WanVideo__00001.mp4","subfolder":"video","type":"output","format":"video/h264-mp4","frame_rate":16,"workflow":"WanVideo__00001.png","fullpath":"E:\\ComfyUI-Data\\output\\video\\WanVideo__00001.mp4"}}}},{"id":5,"type":"VAELoader","pos":[980,820],"size":[360,60],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"vae名称","name":"vae_name","type":"COMBO","widget":{"name":"vae_name"},"link":null}],"outputs":[{"localized_name":"VAE","name":"VAE","type":"VAE","links":[4]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.68","Node name for S&R":"VAELoader","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.4.1"}},"widgets_values":["Wan2_1_VAE_bf16.safetensors"]}],"links":[[2,3,0,4,0,"IMAGE"],[4,5,0,3,1,"VAE"],[5,10,0,3,0,"LATENT"]],"groups":[],"config":{},"extra":{"ue_links":[],"ds":{"scale":1.0834705943389082,"offset":[-573.5954979317662,-431.50654580611246]},"links_added_by_ue":[]},"version":0.4}
@@ -0,0 +1 @@
{"id":"51bbecaf-42f9-4de3-95e4-b6db447eb5db","revision":0,"last_node_id":3,"last_link_id":2,"nodes":[{"id":3,"type":"PreviewImage","pos":[1480,500],"size":[400,260],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"图像","name":"images","type":"IMAGE","link":2}],"outputs":[{"localized_name":"images","name":"images","type":"IMAGE","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.21.1","Node name for S&R":"PreviewImage","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":[]},{"id":2,"type":"GuHaiAutoImageSplit","pos":[1160,500],"size":[270,210],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"图像","name":"图像","type":"IMAGE","link":1},{"localized_name":"保存目录","name":"保存目录","type":"STRING","widget":{"name":"保存目录"},"link":null},{"localized_name":"水平张数","name":"水平张数","type":"INT","widget":{"name":"水平张数"},"link":null},{"localized_name":"垂直张数","name":"垂直张数","type":"INT","widget":{"name":"垂直张数"},"link":null},{"localized_name":"移除画布边缘","name":"移除画布边缘","type":"BOOLEAN","widget":{"name":"移除画布边缘"},"link":null},{"localized_name":"移除描边","name":"移除描边","type":"INT","widget":{"name":"移除描边"},"link":null},{"localized_name":"文件名前缀","name":"文件名前缀","type":"STRING","widget":{"name":"文件名前缀"},"link":null},{"localized_name":"保存格式","name":"保存格式","type":"COMBO","widget":{"name":"保存格式"},"link":null}],"outputs":[{"localized_name":"分割图像","name":"分割图像","type":"IMAGE","links":[2]}],"properties":{"aux_id":"goohai/Goohaitools-comfyui","ver":"885cac3dd323d26a77b69ae904a9a344f0dee7d0","Node name for S&R":"GuHaiAutoImageSplit","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":["output",3,3,true,8,"孤海图像分割_","PNG"]},{"id":1,"type":"LoadImage","pos":[850,490],"size":[270,320],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"图像","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[1]},{"localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.21.1","Node name for S&R":"LoadImage","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.8"}},"widgets_values":["Image (4) (1).png","image"]}],"links":[[1,1,0,2,0,"IMAGE"],[2,2,0,3,0,"IMAGE"]],"groups":[{"id":1,"title":"图像分割","bounding":[810,420,1100,400],"color":"#3f789e","flags":{}}],"config":{},"extra":{"ds":{"scale":0.9090909090909091,"offset":[-259.9355305037313,15.820561879510858]},"ue_links":[]},"version":0.4}
@@ -0,0 +1 @@
{"id":"74ef48f7-4441-45af-a6a0-a2bba28aeff5","revision":0,"last_node_id":18,"last_link_id":19,"nodes":[{"id":18,"type":"AudioCrop","pos":[-322,508],"size":[315,82],"flags":{},"order":1,"mode":0,"inputs":[{"label":"audio","localized_name":"audio","name":"audio","type":"AUDIO","link":18},{"label":"start_time","localized_name":"start_time","name":"start_time","type":"STRING","widget":{"name":"start_time"},"link":null},{"label":"end_time","localized_name":"end_time","name":"end_time","type":"STRING","widget":{"name":"end_time"},"link":null}],"outputs":[{"label":"AUDIO","localized_name":"音频","name":"AUDIO","type":"AUDIO","slot_index":0,"links":[19]}],"properties":{"cnr_id":"audio-separation-nodes-comfyui","ver":"1.4.0","Node name for S&R":"AudioCrop","ue_properties":{"widget_ue_connectable":{},"version":"7.4.1","input_ue_unconnectable":{}}},"widgets_values":["0:00","1:00"]},{"id":10,"type":"LoadAudio","pos":[-765,507],"size":[381.5714111328125,144],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"音频","name":"audio","type":"COMBO","widget":{"name":"audio"},"link":null},{"localized_name":"音频UI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"AUDIOUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"label":"音频","localized_name":"音频","name":"AUDIO","type":"AUDIO","slot_index":0,"links":[18]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.68","Node name for S&R":"LoadAudio","ue_properties":{"widget_ue_connectable":{},"version":"7.4.1","input_ue_unconnectable":{}}},"widgets_values":["没出息.WAV",null,null]},{"id":15,"type":"PreviewAudio","pos":[416,514],"size":[318.9408688964845,88],"flags":{},"order":3,"mode":0,"inputs":[{"label":"音频","localized_name":"音频","name":"audio","type":"AUDIO","link":15},{"localized_name":"音频UI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.68","Node name for S&R":"PreviewAudio","ue_properties":{"widget_ue_connectable":{},"version":"7.4.1","input_ue_unconnectable":{}}},"widgets_values":[]},{"id":11,"type":"AudioSeparation","pos":[45,494],"size":[315,166],"flags":{},"order":2,"mode":0,"inputs":[{"label":"audio","localized_name":"audio","name":"audio","type":"AUDIO","link":19},{"label":"chunk_fade_shape","localized_name":"chunk_fade_shape","name":"chunk_fade_shape","shape":7,"type":"COMBO","widget":{"name":"chunk_fade_shape"},"link":null},{"label":"chunk_length","localized_name":"chunk_length","name":"chunk_length","shape":7,"type":"FLOAT","widget":{"name":"chunk_length"},"link":null},{"label":"chunk_overlap","localized_name":"chunk_overlap","name":"chunk_overlap","shape":7,"type":"FLOAT","widget":{"name":"chunk_overlap"},"link":null}],"outputs":[{"label":"Bass","localized_name":"Bass","name":"Bass","type":"AUDIO","links":null},{"label":"Drums","localized_name":"Drums","name":"Drums","type":"AUDIO","links":null},{"label":"Other","localized_name":"Other","name":"Other","type":"AUDIO","links":null},{"label":"Vocals","localized_name":"Vocals","name":"Vocals","type":"AUDIO","slot_index":3,"links":[15]}],"properties":{"cnr_id":"audio-separation-nodes-comfyui","ver":"1.4.0","Node name for S&R":"AudioSeparation","ue_properties":{"widget_ue_connectable":{},"version":"7.4.1","input_ue_unconnectable":{}}},"widgets_values":["logarithmic",16,1.1]}],"links":[[15,11,3,15,0,"AUDIO"],[18,10,0,18,0,"AUDIO"],[19,18,0,11,0,"AUDIO"]],"groups":[],"config":{},"extra":{"ue_links":[],"links_added_by_ue":[],"ds":{"scale":0.9158251011505646,"offset":[1084.0109668805326,-51.53135329805568]}},"version":0.4}
@@ -0,0 +1 @@
{"id":"48bb093c-f7a8-421c-8fc4-2b6acc133512","revision":0,"last_node_id":4,"last_link_id":3,"nodes":[{"id":4,"type":"SaveAudioMP3","pos":[580,23110],"size":[270,136],"flags":{},"order":1,"mode":0,"inputs":[{"label":"音频","localized_name":"音频","name":"audio","type":"AUDIO","link":3},{"label":"文件名前缀","localized_name":"文件名前缀","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"label":"品质","localized_name":"品质","name":"quality","type":"COMBO","widget":{"name":"quality"},"link":null},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null}],"outputs":[],"properties":{"ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{}},"Node name for S&R":"SaveAudioMP3"},"widgets_values":["audio/ComfyUI","V0"]},{"id":1,"type":"LoadAudio","pos":[180,23110],"size":[310,140],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"音频","name":"audio","type":"COMBO","widget":{"name":"audio"},"link":null},{"localized_name":"音频UI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"AUDIOUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"音频","name":"AUDIO","type":"AUDIO","links":[3]}],"properties":{"ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{}},"Node name for S&R":"LoadAudio"},"widgets_values":["0001.G.E.M. 邓紫棋 - 光年之外.m4a",null,""]}],"links":[[3,1,0,4,0,"AUDIO"]],"groups":[],"config":{},"extra":{"ue_links":[],"links_added_by_ue":[],"ds":{"scale":1.2839025177495265,"offset":[349.7067995460809,-22788.486109072262]}},"version":0.4}
@@ -0,0 +1 @@
{"id":"e6b92a63-6d7e-4d2b-b9e3-2e054825db02","revision":0,"last_node_id":7,"last_link_id":6,"nodes":[{"id":3,"type":"SaveImage","pos":[-500,-530],"size":[280,280],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"图片","name":"images","type":"IMAGE","link":2},{"localized_name":"文件名前缀","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null}],"outputs":[],"properties":{"Node name for S&R":"SaveImage","cnr_id":"comfy-core","ver":"0.16.4","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.7","input_ue_unconnectable":{}}},"widgets_values":["ComfyUI"]},{"id":1,"type":"RTXVideoSuperResolution","pos":[-830,-560],"size":[300,110],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":1},{"localized_name":"resize_type","name":"resize_type","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"resize_type"},"link":null},{"localized_name":"resize_type.scale","name":"resize_type.scale","type":"FLOAT","widget":{"name":"resize_type.scale"},"link":null},{"localized_name":"quality","name":"quality","type":"COMBO","widget":{"name":"quality"},"link":null}],"outputs":[{"localized_name":"upscaled_images","name":"upscaled_images","type":"IMAGE","links":[2,5]}],"properties":{"Node name for S&R":"RTXVideoSuperResolution","aux_id":"Comfy-Org/Nvidia_RTX_Nodes_ComfyUI","ver":"0.1.0","cnr_id":"comfyui_nvidia_rtx_nodes","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.7","input_ue_unconnectable":{}}},"widgets_values":["scale by multiplier",2,"ULTRA"]},{"id":2,"type":"LoadImage","pos":[-1150,-630],"size":[290,320],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"图像","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[1,6]},{"localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"Node name for S&R":"LoadImage","cnr_id":"comfy-core","ver":"0.16.4","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.7","input_ue_unconnectable":{}}},"widgets_values":["ComfyUI_01117_.png","image"]},{"id":7,"type":"Image Comparer (rgthree)","pos":[-180,-620],"size":[210,78],"flags":{},"order":3,"mode":0,"inputs":[{"dir":3,"label":"图像_A","localized_name":"图像_A","name":"image_a","type":"IMAGE","link":6},{"dir":3,"label":"图像_B","localized_name":"图像_B","name":"image_b","type":"IMAGE","link":5}],"outputs":[],"title":"图像对比","properties":{"ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{}},"comparer_mode":"Slide"},"widgets_values":[[]]}],"links":[[1,2,0,1,0,"IMAGE"],[2,1,0,3,0,"IMAGE"],[5,1,0,7,1,"IMAGE"],[6,2,0,7,0,"IMAGE"]],"groups":[],"config":{},"extra":{"workflowRendererVersion":"Vue-corrected","ds":{"scale":1.7985878990921387,"offset":[589.555558343045,830.7344763619243]},"frontendVersion":"1.39.19","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true,"ue_links":[],"links_added_by_ue":[]},"version":0.4}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More