Continuity: say which reference the carried frame went into

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tw9fjYBYFyV8YG5YDfYPRb
This commit is contained in:
Claude
2026-11-02 13:48:48 +00:00
parent d8ce442c04
commit 716e5d414f
3 changed files with 14 additions and 4 deletions
+2 -1
View File
@@ -100,13 +100,14 @@ export default {
if (group.every((s) => s.excluded) || !isGenerative(root)) continue; // all left out, or not made by the model
// The unit is made once at the length of its clips together (each cut adds one shared frame).
const frames = group.reduce((t, s) => t + effFrames(s, snap), 0) - (group.length - 1);
const { inputs, unwired, left, noPrompt, notes } = await buildInputs({ project: snap, scene: root, slots: frozen, expand: same, nodeInfo: app.api.wireTargets, frames, hooks: app.inputHooks, ...(Number.isInteger(once.seed) ? { seed: () => once.seed } : fixedSeed ? { seed: null } : {}), prefix: (app.promptPrefix || []).flatMap((f) => { try { return f(root, snap); } catch { return []; } }) });
const { inputs, unwired, left, noPrompt, notes, info } = await buildInputs({ project: snap, scene: root, slots: frozen, expand: same, nodeInfo: app.api.wireTargets, frames, hooks: app.inputHooks, ...(Number.isInteger(once.seed) ? { seed: () => once.seed } : fixedSeed ? { seed: null } : {}), prefix: (app.promptPrefix || []).flatMap((f) => { try { return f(root, snap); } catch { return []; } }) });
app.lastRun.typed = (root.text || "").trim(); // what a Chat comment made now would be about
const seeded = rolesAt(frozen, "generation.seed")[0];
if (seeded) app.lastRun.seed = (inputs[seeded.slot.id] || {})[seeded.role.input] ?? (seeded.slot.inputs || {})[seeded.role.input]; // shown in ⏱ Sampler, to keep a liked one
if (stopped) break;
if (noPrompt && !made) tell("This pipeline has no prompt input, so the scene text is not sent.");
notes.forEach(tell);
(info || []).forEach((text) => c.toast.info({ text })); // what was done as asked, said plainly, not as a warning
if (unwired) tell(`${unwired} reference(s) did not fit this pipeline and are not used: ${(left || []).map((l) => `${names.get(l.id) || l.id} (looked for ${l.at.replace("assets.", "")}: ${l.why})`).join("; ")}.`);
// The page's own guard does not see another tab: ComfyUI's queue does. Asked per shot: between this run's shots it is empty.
if (app.api.projectQueued && (await app.api.projectQueued(pid))) { tell("This project already has a run in ComfyUI's queue (another tab or window?). Wait for it, or Stop it there."); break; }
+10 -3
View File
@@ -24,12 +24,12 @@ export async function buildInputs({ project, scene, slots, expand, nodeInfo, fra
const plan = sceneReferences(scene, project.fixed_references);
// Hooks run first: one may add pictures to this run's references (a carried frame takes the next free slot), which the
// start and the wiring below must see. What else they return (inputs, notes) is merged further down, where it always was.
const outs = [], hookNotes = [];
const outs = [], hookNotes = [], carried = [];
for (const hook of hooks) {
let out;
try { out = (await hook({ project, scene, slots })) || {}; } catch (err) { hookNotes.push(`An add-on's input could not be added to this run: ${err.message}`); continue; }
outs.push(out);
for (const id of out.references || []) if (id && !plan.ids.includes(id)) { plan.ids = [...plan.ids, id]; if (!kinds.has(id)) kinds.set(id, "image"); }
for (const id of out.references || []) if (id && !plan.ids.includes(id)) { plan.ids = [...plan.ids, id]; if (!kinds.has(id)) kinds.set(id, "image"); carried.push(id); }
}
// A pipeline that takes whole media where it takes a picture (LTX): with no start picked, the first reference is the start:
// a VIDEO first (it is then not also sent as a reference: nothing takes it there), else the first picture (which stays a
@@ -122,10 +122,17 @@ export async function buildInputs({ project, scene, slots, expand, nodeInfo, fra
}
// Features that add inputs of their own (a Chat comment for the enhancer, say) say so through a hook: { inputs: {slot: {name: value}}, notes: [text] }.
const noVideo = takesVideo && videoOnly && !startId ? ["This pipeline works from a video: pick one as the start, or add it under References."] : [];
const told = [];
const notes = failedExpand ? ["The prompt could not be expanded (shortcuts, $variables, anchor, postfix): the text was sent as typed."] : [];
if (!textOnly) notes.push(...noVideo);
else notes.push("Nothing to feed the model, so this shot is made from the prompt alone (text to video).");
notes.push(...hookNotes);
// A picture a hook added to the references: said where it landed (or, if it found no place, by the "did not fit" note).
for (const id of carried) {
const slotId = Object.keys(overrides).find((k) => overrides[k].media_id === id);
const at = slotId && ((slots || []).find((x) => x.id === slotId)?.roles || []).map((r) => /^assets\.reference_(\d+)$/.exec(r.at || "")).find(Boolean);
if (at) told.push(`Continuity: the last frame of the clip before it (${id}) went into reference ${at[1]}.`);
}
for (const out of outs) {
for (const [id, fields] of Object.entries(out.inputs || {})) raw[id] = { ...raw[id], ...fields };
notes.push(...(out.notes || []));
@@ -134,5 +141,5 @@ export async function buildInputs({ project, scene, slots, expand, nodeInfo, fra
// Judged on what is SENT (a carried frame counts; a picture t2v blanks, or one this pipeline has no input for, does not).
const picture = (source && (raw[source.slot.id] || {}).media_id) || Object.keys(overrides).length;
if (prompt && !String((raw[prompt.slot.id] || {})[prompt.role.input] ?? "").trim() && !picture) notes.push("This shot has no text and no start picture, so the model makes whatever it likes.");
return { inputs: raw, unwired: unwired + (ignored ? 1 : 0), left, noPrompt: !prompt, notes };
return { inputs: raw, unwired: unwired + (ignored ? 1 : 0), left, noPrompt: !prompt, notes, info: told };
}
+2
View File
@@ -264,7 +264,9 @@ test("a hook's carried frame takes the next free reference slot, after the scene
const run = (scene, extra = {}) => buildInputs({ project: {}, scene, slots: refs, expand: async ({ text }) => ({ text }), hooks: carry, ...extra });
const one = await run({ text: "", references: ["mine"] });
assert.deepEqual([one.inputs.r1.media_id, one.inputs.r2.media_id, one.unwired], ["mine", "frame", 0]);
assert.ok(one.info.some((n) => /went into reference 2\./.test(n) && n.includes("frame")));
const full = await run({ text: "", references: ["a", "b"] });
assert.deepEqual([full.inputs.r1.media_id, full.inputs.r2.media_id, full.unwired], ["a", "b", 1]);
assert.ok(!full.info.some((n) => /went into/.test(n)));
assert.equal(kinds.get("frame"), "image");
});