/** * Shared types for the `vclaw video assemble` post-execution stage. * * Sub-slice 3a establishes only the structural surface. The actual * TTS / PDF / title-card / slide-animation / music / stitch logic lands * in sub-slices 3b–3h. No video/audio I/O lives here yet. */ import type { VideoProjectWorkspace } from '../workspace.js'; import type { AssembleMediaQcReport } from './media-qc.js'; export interface AssembleInput { workspace: VideoProjectWorkspace; /** Path to the brand-profile.json for the active presenter (Slice 2 output). */ brandProfilePath?: string; /** Optional override of FFmpeg path. Defaults to `ffmpeg` on PATH. */ ffmpegBin?: string; /** Dry run produces the report but skips actual generation. */ dryRun?: boolean; /** * Clip-stitch escape hatch: allow assembling a film with storyboard scenes * that have NO rendered clip on disk. Default OFF — a missing scene is a * hard gate (`assemble_missing_scene_clips`), because a silently-short * master has shipped before (ep57: 88s from 12 scenes, 2026-07-22). * Dry runs always plan-and-warn regardless. */ allowMissingScenes?: boolean; /** * Drop the rendered clips' own audio from the mix, leaving only the score * and any explicit dialogue/SFX layers. * * Deliberately NOT inferred from `scene.silent`. A silent film means no * DIALOGUE, not no sound: the scene prompts actively request diegetic * ambience ("birdsong beginning, and a light wind in the pines") and the * standing render rules ask for "only diegetic ambient sound … NO speech". * Auto-stripping every silent film threw away a storm's worth of wanted * atmosphere (−25.9 LUFS on a lighthouse short) to fix a defect that had * occurred on one film. * * The division of labour: the mix keeps the ambience, `check_delivery.py * --silent` refuses a master that contains speech, and the operator then * either re-renders the offending shot or passes this flag. Neither half * degrades a film quietly. */ dropClipAudio?: boolean; /** * Scene indices whose clip audio is silenced, leaving every other scene's * ambience intact. The surgical form of {@link dropClipAudio} and the one to * reach for first: when a route invents dialogue into one shot of a * no-dialogue film, this costs that shot's ambience rather than the film's. */ muteClipAudioScenes?: number[]; /** * Clip-stitch mode. When set, the body segments are the per-scene rendered * clips `vclaw video execute` downloads to `outputs/scene-.mp4` (NOT * animated slides). Each clip's native audio (incl. omni-flash voice) is * kept; the background-music bed is the only optional extra layer; no TTS * narration and no dialogue/sfx auto-layers are added. */ fromRenderedClips?: boolean; /** * Music-bed volume (0..1) for the auto-attached selected soundtrack in * clip-stitch mode. Default: `CLIP_STITCH_SOUNDTRACK_VOLUME` (0.55) — tuned * for narration-led films where the sidechain ducks the bed under a separate * narration layer. Films whose dialogue lives INSIDE the clip audio (native * Seedance/Veo speech) get no ducking, so the bed rides at full mix volume * over the spoken lines — pass ~0.12–0.20 for those (the Last Call fix). */ musicVolume?: number; /** * Whole-cut finishing look, applied in the per-segment prep pass. * * `onTwos` holds every second frame so motion steps at 12fps on a 24fps * timebase — the hand-drawn "animated on twos" cadence. Generated video is * smooth at 24fps in a way drawn animation never is, and that smoothness is * most of what reads as machine-made. * * `sharpen` and `filmGrain` are the binding pass: a mild sharpen plus a * temporal grain field over every segment pulls separately-generated clips * into one cohesive-looking piece. * * All three default off; omitting them leaves the ffmpeg args unchanged. */ onTwos?: boolean; sharpen?: boolean; filmGrain?: number; } export interface AssembleManifestEntry { kind: 'narration' | 'music' | 'title-card' | 'slide-animation' | 'rendered-clip' | 'final-video'; path: string; durationMs: number; sceneIndex?: number; sizeBytes: number; generator: string; } export interface AssembleResult { status: 'complete' | 'partial' | 'dry-run'; outputPath: string; manifest: AssembleManifestEntry[]; events: string[]; warnings: string[]; qc?: AssembleMediaQcReport; }