/** * Mograph render plan — turn a linted pack into the EXACT per-block * submissions a provider will receive (review-first: the plan IS the * contract), and compile the batch-eligible slice into a batch-queue manifest * so the existing `batch-submit` / `batch-monitor` machinery does the * submit/poll/download work. No new submit loop — the batch queue already * solved resumability, throttling, and wedge handling. * * v2v blocks need a route that accepts a source video (omni-flash on * veo-useapi); batch routes do not, so the manifest compiler sets them aside * explicitly rather than dropping them silently. */ import type { BatchQueueManifest, BatchRouteId } from '../batch-queue.js'; import { assembleBlockPrompt, blockReferencePaths, filterBlocks } from './pack.js'; import type { MographLintIssue, MographPriority, MographSubmission, MotionPackArtifact, MotionSheetArtifact, } from './types.js'; /** Routes the batch queue can carry (mirrors the batch manifest schema enum). */ export const MOGRAPH_BATCH_ROUTES: BatchRouteId[] = ['runway-useapi', 'dreamina-useapi', 'seedance-direct']; /** The one route with true V2V + native sound-design audio. */ export const MOGRAPH_V2V_ROUTE = 'veo-useapi'; export const PROMPT_ONLY_ROUTE = 'prompt-only'; /** * Routes where an attached IMAGE reference destroys on-screen text control and * must be omitted (the clip renders prose-only from the style lock). Validated * across ~20 pilot renders on Omni (`veo-useapi` omni-flash): the model * reproduces the style board's sample copy or invents its own text whenever any * image ref is present. The ≤120-word style-lock prose alone carries the look. * This governs IMAGE refs only — a v2v block's `videoSource` (the edit source) * is a different channel and is preserved. */ export const MOGRAPH_REFERENCE_FREE_ROUTES = new Set(['veo-useapi']); /** True when the route renders mograph clips prose-only (no image refs). */ export function isReferenceFreeRoute(route: string): boolean { return MOGRAPH_REFERENCE_FREE_ROUTES.has(route); } /** Seedance-family reference budget (images) per submission. */ const MAX_IMAGE_REFS = 9; export interface MographPlanOptions { /** Cumulative priority scope (P2 = P1+P2); ignored when blockIds is set. */ priority?: MographPriority; blockIds?: string[]; /** Route for non-v2v blocks. Default: the pack's route, else seedance-direct. */ route?: string; } export interface MographRenderPlan { submissions: MographSubmission[]; issues: MographLintIssue[]; } /** * Build the submission plan: assembled prompt + resolved refs + route per * block. Pure; refs are echoed as given (existence checks are the handler's * I/O concern). */ export function planMographRender( pack: MotionPackArtifact, sheet: MotionSheetArtifact, opts: MographPlanOptions = {}, ): MographRenderPlan { const issues: MographLintIssue[] = []; const route = opts.route ?? pack.route ?? 'seedance-direct'; const blocks = filterBlocks(pack, { priority: opts.priority, blockIds: opts.blockIds }); if (blocks.length === 0) { issues.push({ code: 'plan-empty', severity: 'error', message: opts.blockIds?.length ? `no blocks match ids ${opts.blockIds.join(', ')}` : `no blocks in scope${opts.priority ? ` ${opts.priority}` : ''} — author blocks before planning a render`, }); return { submissions: [], issues }; } if (!sheet.refImage?.path) { issues.push({ code: 'sheet-ref-image-missing', severity: 'error', message: 'the motion sheet has no reference image — render and approve the sheet before rendering clips (a weak sheet makes every clip weak)', }); } const submissions: MographSubmission[] = []; let omittedForOmni = 0; for (const block of blocks) { const isV2v = block.mode.startsWith('v2v'); const blockRoute = route === PROMPT_ONLY_ROUTE ? PROMPT_ONLY_ROUTE : isV2v ? MOGRAPH_V2V_ROUTE : route; // Transport-aware refs: on Omni-family routes an image ref hijacks the // clip's on-screen text, so submit prose-only. The v2v videoSource channel // is untouched. Every other route keeps the sheet/logo refs. const refs = isReferenceFreeRoute(blockRoute) ? [] : blockReferencePaths(block, pack, sheet); if (isReferenceFreeRoute(blockRoute) && blockReferencePaths(block, pack, sheet).length > 0) { omittedForOmni += 1; } if (refs.length > MAX_IMAGE_REFS) { issues.push({ code: 'reference-budget-exceeded', severity: 'error', blockId: block.id, message: `${refs.length} image references (max ${MAX_IMAGE_REFS}) — trim per-block refs`, }); } submissions.push({ blockId: block.id, priority: block.priority, mode: block.mode, prompt: assembleBlockPrompt(block, sheet, { aspect: pack.aspect }), refs, ...(block.videoSource ? { videoSource: block.videoSource } : {}), durationSec: pack.durationSec, aspect: pack.aspect, route: blockRoute, }); } if (omittedForOmni > 0) { issues.push({ code: 'refs-omitted-omni', severity: 'warning', message: `${omittedForOmni} block(s) route to an Omni-family transport (${[...MOGRAPH_REFERENCE_FREE_ROUTES].join(', ')}) — image references are omitted and the clips render prose-only from the style lock (an attached ref would hijack the on-screen text).`, }); } return { submissions, issues }; } export interface MographBatchCompilation { manifest: BatchQueueManifest | null; /** Block ids excluded from the batch (v2v) — they need the V2V route or prompt-only. */ excludedV2v: string[]; } /** * Compile the batch-eligible submissions into a batch-queue manifest. * References ride in `characterRefs` — the provider REFERENCE slot, never the * first frame, so the style board defines the language without becoming * frame 1 of the clip. */ export function toBatchManifest( plan: MographRenderPlan, route: BatchRouteId, ): MographBatchCompilation { const eligible = plan.submissions.filter((s) => !s.mode.startsWith('v2v') && s.route !== PROMPT_ONLY_ROUTE); const excludedV2v = plan.submissions.filter((s) => s.mode.startsWith('v2v')).map((s) => s.blockId); if (eligible.length === 0) return { manifest: null, excludedV2v }; const aspect = eligible[0].aspect; const manifest: BatchQueueManifest = { schemaVersion: 1, route, defaults: { seconds: eligible[0].durationSec, aspectRatio: aspect as '16:9' | '9:16' | '1:1', }, jobs: eligible.map((s) => ({ id: s.blockId, prompt: s.prompt, ...(s.refs.length > 0 ? { characterRefs: s.refs } : {}), })), }; return { manifest, excludedV2v }; } export interface MographSidecar { schemaVersion: 1; blockId: string; priority: MographPriority; mode: string; route: string; prompt: string; refs: string[]; videoSource?: string; durationSec: number; aspect: string; vo?: string; generatedAt: string; } /** The per-clip audit record (exact prompt + refs + route) — written next to outputs. */ export function buildSidecar( submission: MographSubmission, vo: string | undefined, generatedAt: string, ): MographSidecar { return { schemaVersion: 1, blockId: submission.blockId, priority: submission.priority, mode: submission.mode, route: submission.route, prompt: submission.prompt, refs: submission.refs, ...(submission.videoSource ? { videoSource: submission.videoSource } : {}), durationSec: submission.durationSec, aspect: submission.aspect, ...(vo ? { vo } : {}), generatedAt, }; }