Video-to-video presets: the first video among the references is the start when none is picked

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzJLJWVu7RZnbRqm3nYAnX
This commit is contained in:
Claude
2026-10-27 08:09:44 +00:00
parent a9f5c9f35d
commit 1a5d9eab19
2 changed files with 25 additions and 4 deletions
+11 -4
View File
@@ -19,11 +19,17 @@ export async function buildInputs({ project, scene, slots, expand, nodeInfo, fra
if (seed) for (const { slot, role } of rolesAt(slots, "generation.seed")) put(slot, role.input, seed()); // null: the seed node's own number runs
for (const slot of (slots || []).filter((s) => s.node === "FunUIShotCamera")) put(slot, "project", project.id || ""); // what the node keeps per project (the words focus infusion draws on), also for pipelines saved before it was asked for
const source = rolesAt(slots, "assets.source_image")[0];
if (source) put(source.slot, "media_id", project.generation_mode === "t2v" ? "" : scene.source_image || ""); // "from a prompt" starts every shot without a picture
const plan = sceneReferences(scene, project.fixed_references);
// A pipeline that takes a whole video where it takes a picture (video-to-video): with no start picked, the first VIDEO
// among the references is the one it works from, and is not also sent as a reference. It is never blanked by "From a prompt".
const takesVideo = Boolean(source && source.role.video_frames);
const fromRefs = takesVideo && !scene.source_image ? plan.ids.find((id) => kinds.get(id) === "video") : null;
if (fromRefs) plan.ids = plan.ids.filter((id) => id !== fromRefs);
const startId = takesVideo ? scene.source_image || fromRefs || "" : project.generation_mode === "t2v" ? "" : scene.source_image;
if (source) put(source.slot, "media_id", startId || ""); // "from a prompt" starts every shot without a picture
// A pipeline that can take a whole video where it takes a picture (LTX) says so on the role: a picked VIDEO then
// supplies its frames (prepared as Video input says) to everything the picture fed, instead of only its first frame.
const startId = project.generation_mode === "t2v" ? "" : scene.source_image;
if (source && source.role.video_frames && startId && kinds.get(startId) === "video") {
if (takesVideo && startId && kinds.get(startId) === "video") {
put(source.slot, "full", true);
const use = activeVideoInput(project, scene);
if (use) put(source.slot, "prepare", videoInputJson(use, project, scene));
@@ -42,7 +48,6 @@ export async function buildInputs({ project, scene, slots, expand, nodeInfo, fra
const off = like.unsend && (slots || []).find((s) => s.id === like.unsend.slot); // a stand-in is not a picture to show anyone
if (off) put(off, like.unsend.input, false);
}
const plan = sceneReferences(scene, project.fixed_references);
// A reference is placed from what the nodes offer, so a pipeline someone edited needs nothing declared for it.
const needed = plan.ids.length > 0;
const info = new Map();
@@ -77,7 +82,9 @@ export async function buildInputs({ project, scene, slots, expand, nodeInfo, fra
if (!(body && typeof body.text === "string")) failedExpand = true;
}
// 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 && !startId ? ["This pipeline works from a video: pick one as the start, or add it under References."] : [];
const notes = failedExpand ? ["The prompt could not be expanded (shortcuts, $variables, anchor, postfix): the text was sent as typed."] : [];
notes.push(...noVideo);
for (const hook of hooks) {
let out;
try { out = (await hook({ project, scene, slots })) || {}; } catch (err) { notes.push(`An add-on's input could not be added to this run: ${err.message}`); continue; }
+14
View File
@@ -159,3 +159,17 @@ test("a video picked as the start feeds its frames to what the picture fed, when
assert.equal((await run(mk(true), "pic")).inputs.src.full, undefined); // a picture stays a picture
learn([]);
});
test("a video-in pipeline takes the first video among the references as its start, and says so when there is none", async () => {
const { learn } = await import("../../shell/bin.js");
learn([{ id: "vid", name: "v.mp4", kind: "video" }, { id: "pic", name: "p.png", kind: "image" }]);
const slots = [{ id: "src", node: "FunUILoadMedia", roles: [{ at: "assets.source_image", input: "media_id", video_frames: true }], inputs: {} }, { id: "a", node: "X", inputs: { image: ["src", 0] } }];
const run = (scene, project = {}) => buildInputs({ project: { video: {}, frame_rate: 25, ...project }, scene: { text: "x", ...scene }, slots, expand: async () => null });
const r = await run({ references: ["pic", "vid"] }, { generation_mode: "t2v" });
assert.equal(r.inputs.src.media_id, "vid");
assert.equal(r.inputs.src.full, true);
assert.deepEqual(r.inputs.a.image, ["src", 1]);
assert.equal((await run({ source_image: "pic", references: ["vid"] })).inputs.src.media_id, "pic"); // a picked start wins
assert.ok((await run({ references: ["pic"] })).notes.some((n) => /works from a video/.test(n)));
learn([]);
});