/** * Auto-chain fallback ladder. * * A chained scene can fail when its VIDEO reference (the prior scene's clip) is * rejected by the provider — e.g. Seedance's face filter (`fail_code 4011 * RejectFace`) rejects a specific upstream clip as an input reference, even * though other clips pass. Rather than fail-fast and stop the whole chain, the * ladder retries the scene with a degraded chain source, preferring continuity: * * 1. chain from the immediately-previous scene (the normal path) * 2. chain from the anchor (first) scene (skips the poisoned ref, * keeps SOME continuity) * 3. image-only (drop the video chain seed; * the scene still renders from * its character/identity refs) * * Pure: callers apply each rung (set the chain source / clear it), submit, and * advance to the next rung only if the render produced no usable candidate. */ /** One rung of the ladder: chain from a specific upstream scene, or no chain. */ export type ChainSource = | { kind: 'chain'; sourceSceneIndex: number } | { kind: 'image-only' }; /** * Build the ordered fallback ladder for the scene at `positionInOrder` within * `sceneOrder`. The first scene (position 0) has no chain ladder (it is the * chain root). Rungs are de-duplicated: when the previous scene IS the anchor * (position 1), the anchor rung is omitted. */ export function planChainFallbackLadder(input: { positionInOrder: number; sceneOrder: number[]; }): ChainSource[] { const { positionInOrder: pos, sceneOrder } = input; if (pos <= 0 || pos >= sceneOrder.length) return []; const rungs: ChainSource[] = []; const prev = sceneOrder[pos - 1]; rungs.push({ kind: 'chain', sourceSceneIndex: prev }); const anchor = sceneOrder[0]; if (anchor !== prev) rungs.push({ kind: 'chain', sourceSceneIndex: anchor }); rungs.push({ kind: 'image-only' }); return rungs; } /** Short label for a rung, for the auto-chain report / logs. */ export function describeChainSource(source: ChainSource): string { return source.kind === 'image-only' ? 'image-only' : `chain-from-${source.sourceSceneIndex}`; }