/** * Retake protocol — a PURE, deterministic analyzer over a per-scene shot log. * No I/O, no Date, no Math.random. * * Encodes the iteration economy from the MIT `Emily2040/seedance-2.0` repo * (`references/retake-protocol.md`, pinned commit 63b32dc): a structured shot * log plus the two hard rules that stop runaway iteration — * 1. two takes with the same flaw is a REWRITE, by rule (not a third re-roll); * 2. an attempt budget with a stop condition, so a $5 shot does not become a * $100 shot. * * Advisory by construction: `analyzeRetakes` returns warnings; nothing here * blocks a reroll. The verdict taxonomy ships as reference data + `triageVerdict`. */ export const RETAKE_VERDICTS = ['keep', 'fix-in-post', 'edit', 're-roll', 'rewrite'] as const; export type RetakeVerdict = (typeof RETAKE_VERDICTS)[number]; /** Canonical when/next-move guidance, condensed from the retake-protocol triage table. */ export const VERDICT_GUIDANCE: Record = { keep: { when: 'The primary spend (what this shot is FOR) is delivered and nothing is fatal.', nextMove: 'Lock it, log it, move on. Perfection in secondary details is post\'s job.', }, 'fix-in-post': { when: 'The flaw lives in post\'s domain: color, on-screen text, sound mix, trim, a few unstable end frames.', nextMove: 'Never burn takes on what an editor fixes in minutes.', }, edit: { when: 'Composition and timing are right; exactly one layer is wrong and the surface supports edit.', nextMove: 'Preserve the take as the source clip; change only the failing layer.', }, 're-roll': { when: 'The prompt is right; the sample was unlucky (sampling variance).', nextMove: 'Same prompt, new seed. Two or three re-rolls maximum — then the prompt is the problem.', }, rewrite: { when: 'The same flaw appears in two or more takes.', nextMove: 'It is systematic, not luck. Diagnose by mechanism and change the prompt.', }, }; export interface ShotLogEntry { /** 1-based take number, per scene. */ take: number; sceneIndex: number; verdict?: RetakeVerdict; /** The ONE variable changed this take (prompt clause, seed, mode, or one reference). */ changed?: string; seed?: 'same' | 'new'; /** What's wrong with this take — drives the same-flaw rule. */ flaw?: string; /** One-sentence note. */ evidence?: string; } export type RetakeAdvisoryCode = 'same-flaw-rewrite' | 'attempt-budget' | 'one-variable'; export interface RetakeAdvisory { code: RetakeAdvisoryCode; severity: 'warning'; message: string; } export const DEFAULT_ATTEMPT_BUDGET = 5; /** Next 1-based take number for a scene given its prior logged takes. */ export function nextTakeNumber(priorSceneTakes: ShotLogEntry[]): number { return priorSceneTakes.length + 1; } /** Canonical when/next-move guidance for a verdict. */ export function triageVerdict(verdict: RetakeVerdict): { when: string; nextMove: string } { return VERDICT_GUIDANCE[verdict]; } function normalizeFlaw(flaw: string): string { return flaw.toLowerCase().trim().replace(/\s+/g, ' '); } /** Count the distinct variables named in a `changed` field ("seed and prompt" -> 2). */ function countChangedVariables(changed: string): number { return changed .split(/\s*(?:,|;|\band\b|&)\s*/i) .map((part) => part.trim()) .filter(Boolean).length; } /** * The two hard rules + the one-variable nudge over a SINGLE scene's takes * (caller filters by scene). Pure: depends only on its arguments. */ export function analyzeRetakes( sceneTakes: ShotLogEntry[], opts: { attemptBudget?: number } = {}, ): RetakeAdvisory[] { const advisories: RetakeAdvisory[] = []; if (sceneTakes.length === 0) return advisories; const sceneIndex = sceneTakes[0]!.sceneIndex; const budget = opts.attemptBudget ?? DEFAULT_ATTEMPT_BUDGET; // Rule 1 — same flaw across >=2 takes is a rewrite, by rule. Group by // normalized flaw in first-appearance order; report the most-repeated. const counts = new Map(); sceneTakes.forEach((entry, index) => { const flaw = entry.flaw?.trim(); if (!flaw) return; const key = normalizeFlaw(flaw); const existing = counts.get(key); if (existing) existing.count += 1; else counts.set(key, { label: flaw, count: 1, firstSeen: index }); }); const repeated = [...counts.values()] .filter((entry) => entry.count >= 2) .sort((a, b) => b.count - a.count || a.firstSeen - b.firstSeen); if (repeated.length > 0) { const worst = repeated[0]!; advisories.push({ code: 'same-flaw-rewrite', severity: 'warning', message: `scene ${sceneIndex}: ${worst.count} takes share the flaw "${worst.label}" — systematic, not luck. Rewrite the prompt (by rule), don't re-roll again.`, }); } // Rule 2 — attempt budget with a stop condition (no kept winner yet). const hasWinner = sceneTakes.some((entry) => entry.verdict === 'keep'); if (!hasWinner && sceneTakes.length >= budget) { advisories.push({ code: 'attempt-budget', severity: 'warning', message: `scene ${sceneIndex}: ${sceneTakes.length} takes logged (budget ${budget}) with no winner selected — stop iterating and change strategy: a different mode, decomposition into more shots, or filming it for real.`, }); } // One-variable nudge — the latest take should change exactly one thing. const latest = sceneTakes[sceneTakes.length - 1]!; if (latest.changed && countChangedVariables(latest.changed) >= 2) { advisories.push({ code: 'one-variable', severity: 'warning', message: `change ONE thing per retake — you changed "${latest.changed}"; same seed + one prompt change is the only controlled experiment.`, }); } return advisories; }