From 4209fd769584807758bab1193c800d2faea09bc2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Nov 2026 22:20:51 +0000 Subject: [PATCH] Pinned references feed the model input of their number per kind; a video's own sound with no input is said; preview rechecks after the upscale wait; lint covers rgb/hsl; tests Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tw9fjX7o2YfeGQE18MEtPu --- .../tests/rule_kit_uses_the_scale.test.js | 4 ++-- app/ui/features/generate/index.js | 4 ++-- app/ui/features/media/index.js | 6 ++++-- app/ui/shell/reference_wiring.js | 8 ++++--- app/ui/shell/tests/dom.test.js | 4 ++++ app/ui/shell/tests/reference_wiring.test.js | 21 +++++++++++++++++++ 6 files changed, 38 insertions(+), 9 deletions(-) diff --git a/app/ui/composer/tests/rule_kit_uses_the_scale.test.js b/app/ui/composer/tests/rule_kit_uses_the_scale.test.js index bfb021b1..2aef2eaf 100644 --- a/app/ui/composer/tests/rule_kit_uses_the_scale.test.js +++ b/app/ui/composer/tests/rule_kit_uses_the_scale.test.js @@ -24,11 +24,11 @@ test("element CSS has no literal pixel sizes", () => { assert.equal(found, 0, `${found} literal px values in element CSS: use a --sp-* step or a component size from tokens.css`); }); -test("element CSS has no literal colours outside token definitions", () => { +test("element CSS has no hex, rgb() or hsl() colours outside token definitions", () => { const bad = []; for (const f of sheets()) { const css = readFileSync(f, "utf8").replace(/\/\*[\s\S]*?\*\//g, ""); - for (const decl of css.split(/[;{}]/)) if (!/^\s*--/.test(decl) && /#[0-9a-fA-F]{3,8}\b/.test(decl)) bad.push(`${f}: ${decl.trim().slice(0, 60)}`); + for (const decl of css.split(/[;{}]/)) if (!/^\s*--/.test(decl) && /#[0-9a-fA-F]{3,8}\b|\b(?:rgb|rgba|hsl|hsla)\(/.test(decl)) bad.push(`${f}: ${decl.trim().slice(0, 60)}`); } assert.deepEqual(bad, [], "literal colours in element CSS: use a token from tokens.css"); }); diff --git a/app/ui/features/generate/index.js b/app/ui/features/generate/index.js index aa0f8149..f8aa6aa8 100644 --- a/app/ui/features/generate/index.js +++ b/app/ui/features/generate/index.js @@ -173,9 +173,9 @@ export default { /** One run for the Training window: the pipeline as it is, its LoRA loader pointed at `lora`, the picked scene (else the first one) and the project's own settings. * Nothing is put on the timeline; resolves to {image} or {error}. */ app.runner.preview = async ({ lora, strength = 1 }) => { - if (busy || held || g.state().phase === QUEUED || g.state().phase === RUNNING) return { error: "A run is already going: the preview waits for it." }; - if (!p.project) return { error: "Open a project first: the preview uses its settings." }; if (await upscaling()) return { error: "An upscale model is running. It shares ComfyUI's model memory, so the preview waits for it." }; + if (busy || held || g.state().phase === QUEUED || g.state().phase === RUNNING) return { error: "A run is already going: the preview waits for it." }; // after the wait: a Generate may have started meanwhile + if (!p.project) return { error: "Open a project first: the preview uses its settings." }; const frozen = structuredClone(app.pipeline.slots() || []); const slot = frozen.find((s) => s.node === "FunUILoraLoader" && !s.bypassed) || frozen.find((s) => s.node === "FunUILoraLoader"); if (!slot) return { error: "This pipeline has no LoRA loader to try the LoRA in. Add one in Models & Pipeline." }; diff --git a/app/ui/features/media/index.js b/app/ui/features/media/index.js index 59e5efeb..f7120a40 100644 --- a/app/ui/features/media/index.js +++ b/app/ui/features/media/index.js @@ -74,9 +74,11 @@ export default { const sc = p.selected; if (!sc) return null; const plan = sceneReferences(sc, (p.project || {}).fixed_references), i = plan.ids.indexOf(id); - return i < 0 ? null : { n: referenceNumbers(plan.ids, plan.pins)[i], pinned: id in plan.pins }; + if (i < 0) return null; + const nums = referenceNumbers(plan.ids, plan.pins), kindOf = (x) => (items.find((m) => m.id === x) || {}).kind; + return { n: nums[i], ord: plan.ids.filter((r, j) => nums[j] <= nums[i] && kindOf(r) === kindOf(id)).length, pinned: id in plan.pins }; // ord: the number the tile shows (per kind) }; - const pinItem = (it) => { const s = slotOf(it.id); return s && s.n <= 16 ? [{ id: "pinref", label: s.pinned ? `Unpin reference from slot R${s.n}` : `Pin reference to slot R${s.n}` }] : []; }; + const pinItem = (it) => { const s = slotOf(it.id); return s && s.n <= 16 ? [{ id: "pinref", label: s.pinned ? `Unpin reference from slot R${s.ord}` : `Pin reference to slot R${s.ord}` }] : []; }; const extras = (it) => (app.mediaMenu || []).flatMap((f) => { try { return f(it, items.find((m) => m.id === it.id)) || []; } catch { return []; } }); // entries other features add to a tile's menu: { id, label, run } const context = (it, e) => c.menu.context({ x: e.clientX, y: e.clientY, onPick: (id) => (act[id] ? act[id](it) : extras(it).find((x) => x.id === id)?.run()), items: [{ id: "look", label: "Look at it on the monitor" }, ...(p.selected && p.selected.source_image === it.id ? [{ id: "unres", label: "Stop using as the selected scene's aspect ratio source" }] diff --git a/app/ui/shell/reference_wiring.js b/app/ui/shell/reference_wiring.js index 167c8477..7d0ae597 100644 --- a/app/ui/shell/reference_wiring.js +++ b/app/ui/shell/reference_wiring.js @@ -26,7 +26,8 @@ export function wireReferences(mediaIds, slots, reservedSlotIds, pins, kindOf, r // before it is switched off; the rest take the free numbers in the order they were added. const kind = (id) => (kindOf && kindOf(id)) || "image"; const numbers = referenceNumbers(mediaIds, pins); - const seen = {}; // how many of each kind went in: a video or audio input is numbered per kind + // A video or sound input is numbered per kind, in the order of the reference numbers (a pinned one keeps its place), not of the list. + const rank = (i) => 1 + mediaIds.filter((id, j) => j !== i && kind(id) === kind(mediaIds[i]) && numbers[j] < numbers[i]).length; mediaIds.forEach((mediaId, i) => { const at = `assets.reference_${numbers[i]}`; const slot = pool.find((s) => (s.roles || []).some((r) => r.at === at)); @@ -37,8 +38,8 @@ export function wireReferences(mediaIds, slots, reservedSlotIds, pins, kindOf, r const role = slot.roles.find((r) => r.at === at); const k = kind(mediaId); const route = routeOf && routeOf(k, slot, role); - if (k !== "image" && !route) { unwired += 1; left.push({ id: mediaId, at, why: `this pipeline has no input for a ${k}` }); return; } - const n = seen[k] = (seen[k] || 0) + 1; + if (k !== "image" && !route) { unwired += 1; left.push({ id: mediaId, at, why: `this pipeline has no input for ${/^[aeiou]/.test(k) ? "an" : "a"} ${k}` }); return; } + const n = rank(i); if (route && n > route.names.length) { unwired += 1; left.push({ id: mediaId, at, why: `the model takes only ${route.names.length} ${k}${route.names.length > 1 ? "s" : ""}` }); return; } const wire = route ? { slot: route.slot, input: route.names[n - 1], output: route.output } : role.wireTo && { ...role.wireTo, output: 0 }; // A destination can hold one value. Two references claiming the SAME slot or the SAME wire target are @@ -52,6 +53,7 @@ export function wireReferences(mediaIds, slots, reservedSlotIds, pins, kindOf, r // A video with a soundtrack also feeds the input paired with its own number (video 1's sound is audio 1). const track = k === "video" && hasSound && hasSound(mediaId) && routeOf && routeOf("videosound", slot, role); if (track && track.names[n - 1]) overrides[track.slot] = { ...overrides[track.slot], [track.names[n - 1]]: [slot.id, track.output] }; + else if (track) { unwired += 1; left.push({ id: mediaId, at, why: "the model has no free input for this video's own sound" }); } }); return { overrides, unwired, left }; } diff --git a/app/ui/shell/tests/dom.test.js b/app/ui/shell/tests/dom.test.js index 629ea7a3..8d3fd008 100644 --- a/app/ui/shell/tests/dom.test.js +++ b/app/ui/shell/tests/dom.test.js @@ -40,6 +40,10 @@ test("a dialog on screen is reported, and a landmark is found by name", () => { assert.equal(dom.landmark("menubar").className, "cx-frame-head"); assert.equal(dom.landmark("zone:1"), document.querySelectorAll("section.cx-zone")[1]); assert.equal(dom.landmark("button:▶ Generate").tagName, "BUTTON"); + assert.equal(dom.landmark("button:Generating…|▶ Generate").tagName, "BUTTON"); // "a|b": either label + document.querySelector("button").textContent = "Generating…"; + assert.equal(dom.landmark("button:▶ Generate|Generating…").tagName, "BUTTON"); + assert.equal(dom.landmark("button:▶ Generate"), null); assert.equal(dom.landmark("zone:9"), null); assert.equal(dom.landmark("nonsense"), null); }); diff --git a/app/ui/shell/tests/reference_wiring.test.js b/app/ui/shell/tests/reference_wiring.test.js index 18690784..c9989d79 100644 --- a/app/ui/shell/tests/reference_wiring.test.js +++ b/app/ui/shell/tests/reference_wiring.test.js @@ -188,3 +188,24 @@ test("fifteen references of mixed kinds all find an input: 9 pictures, 3 videos assert.deepEqual(overrides.r2v["ref_video_audios.ref_video_audio_2"], ["s12", 2]); assert.deepEqual(overrides.r2v["ref_audios.ref_audio_2"], ["s15", 2]); }); + +test("a pinned reference feeds the model input of its number, per kind, not of its place in the list", () => { + const slot = (n) => ({ id: `ref_source_${n}`, node: "Loader", roles: [{ at: `assets.reference_${n}`, input: "media_id", wireTo: { slot: "r2v", input: `ref_images.ref_image_${n - 1}` } }] }); + const kinds = { pic1: "image", pic2: "image", snd1: "audio", snd2: "audio" }; + const routeOf = (k, s, role) => routeFor(k, R2V, LOADER, role.wireTo.slot); + // pic1 is pinned to number 2, so it is listed first but is the SECOND picture; the same for the sound pinned to 2 + const { overrides, unwired } = wireReferences(["pic1", "snd1", "pic2", "snd2"], [slot(1), slot(2), slot(3), slot(4)], [], { pic1: 2, snd1: 4 }, (id) => kinds[id], routeOf); + assert.equal(unwired, 0); + assert.equal(overrides.r2v["ref_images.ref_image_1"][0], "ref_source_2"); // pic1 (number 2) is picture 2 + assert.equal(overrides.r2v["ref_images.ref_image_0"][0], "ref_source_1"); // pic2 takes the free number 1 + assert.equal(overrides.r2v["ref_audios.ref_audio_1"][0], "ref_source_4"); // snd1 (number 4) is sound 2 + assert.equal(overrides.r2v["ref_audios.ref_audio_0"][0], "ref_source_3"); +}); + +test("a video's own sound with no free input to go to is said, not dropped", () => { + const slot = { id: "ref_source_1", node: "Loader", roles: [{ at: "assets.reference_1", input: "media_id", wireTo: { slot: "r2v", input: "x" } }] }; + const routeOf = (k, s, role) => (k === "video" ? { slot: "r2v", names: ["v0"], output: 1 } : k === "videosound" ? { slot: "r2v", names: [], output: 2 } : null); + const r = wireReferences(["vid"], [slot], [], {}, () => "video", routeOf, () => true); + assert.equal(r.unwired, 1); + assert.match(r.left[0].why, /video's own sound/); +});