Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tw9fjX7o2YfeGQE18MEtPu
117 lines
7.2 KiB
JavaScript
117 lines
7.2 KiB
JavaScript
// Turns a scene's picked reference-media ids into slot overrides for whatever
|
|
// pipeline is current -- pulled out of boot.js so it can be tested without
|
|
// booting the whole app (boot.js runs start() on import).
|
|
|
|
/**
|
|
* wireReferences(mediaIds, slots, reservedSlotIds) -> { overrides, unwired }
|
|
*
|
|
* `overrides` is keyed by slot id, merge these into the raw input overrides
|
|
* sent with a run. `unwired` counts references that did not make it into the
|
|
* run -- the pipeline has fewer "assets.reference_N" roles than the scene has
|
|
* references (e.g. added under a bigger preset, then the preset was swapped
|
|
* for a smaller one), two references' roles resolve to the same destination,
|
|
* or a reference's slot is one the caller already assigned to something else
|
|
* (`reservedSlotIds` -- e.g. a slot that also carries the scene's
|
|
* `assets.source_image` role). Any of these silently overwriting or dropping
|
|
* a reference would look like it was used; the caller is the one place that
|
|
* knows this happened and must say so.
|
|
*/
|
|
export function wireReferences(mediaIds, slots, reservedSlotIds, pins, kindOf, routeOf, hasSound) {
|
|
const pool = slots || [];
|
|
const overrides = {};
|
|
const claimed = new Set(reservedSlotIds || []);
|
|
let unwired = 0;
|
|
const left = []; // what was not wired, and the role it looked for: said by name, not just counted
|
|
// Each reference gets its slot number (1-based), so a pinned one keeps its number when a reference
|
|
// 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);
|
|
// 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));
|
|
if (!slot) { unwired += 1; left.push({ id: mediaId, at, why: "no slot with that role" }); return; }
|
|
// Where a reference lands is read off what the target node offers (`routeOf(kind, slot, role)`), numbered per kind:
|
|
// the model has 9 picture inputs however many slots there are. A picture with no answer from there keeps the
|
|
// role's own `wireTo`, which the preset declares.
|
|
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 ${/^[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
|
|
// the same failure: the second silently overwrites the first unless it is counted unwired instead.
|
|
const dest = wire ? `${wire.slot}.${wire.input}` : slot.id;
|
|
if (claimed.has(slot.id) || claimed.has(dest)) { unwired += 1; left.push({ id: mediaId, at, why: "its slot is already taken" }); return; }
|
|
claimed.add(slot.id);
|
|
claimed.add(dest);
|
|
overrides[slot.id] = { ...overrides[slot.id], media_id: mediaId, ...(k === "image" ? {} : { full: true }) };
|
|
if (wire) overrides[wire.slot] = { ...overrides[wire.slot], [wire.input]: [slot.id, wire.output] };
|
|
// 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 };
|
|
}
|
|
|
|
/**
|
|
* referenceNumbers(mediaIds, pins) -> [n per mediaId], 1-based reference numbers.
|
|
*
|
|
* `pins` is { mediaId: n } as the scene keeps it. A pinned reference is its number, as long as it is in the
|
|
* list; a pin that another pinned reference already holds, or that is out of range, is ignored. Everything
|
|
* else takes the lowest free number, in the order the references were added. With no pins this is simply
|
|
* 1, 2, 3, ... -- the positional wiring this file always did.
|
|
*/
|
|
export function referenceNumbers(mediaIds, pins) {
|
|
const ids = mediaIds || [], taken = new Set(), out = new Array(ids.length).fill(0);
|
|
ids.forEach((id, i) => {
|
|
const n = (pins || {})[id];
|
|
if (Number.isInteger(n) && n >= 1 && n <= MAX_REFERENCES && !taken.has(n)) { taken.add(n); out[i] = n; }
|
|
});
|
|
let next = 1;
|
|
ids.forEach((_id, i) => {
|
|
if (out[i]) return;
|
|
while (taken.has(next)) next += 1;
|
|
taken.add(next); out[i] = next;
|
|
});
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* sceneReferences(scene, fixed) -> { ids, pins }: what one scene sends. `fixed` is the project's { mediaId: n }
|
|
* pins, each at the number it was pinned at, for every scene. Pinned references come first.
|
|
*/
|
|
export function sceneReferences(scene, fixed) {
|
|
const fx = fixed || {};
|
|
const own = (scene.references || []).filter((id) => !(id in fx));
|
|
return { ids: [...Object.keys(fx), ...own], pins: fx };
|
|
}
|
|
|
|
export const MAX_REFERENCES = 16;
|
|
|
|
const natural = (a, b) => a.localeCompare(b, undefined, { numeric: true });
|
|
|
|
/**
|
|
* routeFor(kind, target, loader, targetSlot) -> { slot, names, output } | null
|
|
*
|
|
* Where a picked video or sound goes, decided from the nodes themselves: `target` is what the node the
|
|
* picture reference feeds offers ({ inputs: {name: type}, outputs }), `loader` what the reference slot's own node
|
|
* outputs. A video is the picture-typed inputs whose name says video, a sound the audio-typed inputs whose name says
|
|
* audio (not a video's own soundtrack input); the loader's second picture output carries a video's frames, its
|
|
* first audio output the sound. Null when either side has no such place.
|
|
*/
|
|
export function routeFor(kind, target, loader, targetSlot) {
|
|
const inputs = (target && target.inputs) || {}, outs = (loader && loader.outputs) || [];
|
|
const named = (type, wanted, unwanted) => Object.keys(inputs).filter((name) => {
|
|
const last = name.split(".").pop().toLowerCase();
|
|
return inputs[name] === type && last.includes(wanted) && !(unwanted && last.includes(unwanted));
|
|
}).sort(natural);
|
|
const names = kind === "image" ? named("IMAGE", "image") : kind === "video" ? named("IMAGE", "video") : kind === "videosound" ? named("AUDIO", "video") : named("AUDIO", "audio", "video");
|
|
const at = kind === "image" ? outs.indexOf("IMAGE") : kind === "video" ? outs.map((t, i) => (t === "IMAGE" ? i : -1)).filter((i) => i >= 0)[1] : outs.indexOf("AUDIO");
|
|
return names.length && at >= 0 ? { slot: targetSlot, names, output: at } : null;
|
|
}
|