/** * Motion-overlay work-folder emitter. * * Given a finished `MotionOverlayPlan` and a target work directory, this writes * the on-disk surface the operator (and the execute phase) consume: * / * ├── motion-overlay-plan.json # the manifest (stamped generatedAt) * ├── README.md # human overview + how-to * ├── source/ # (dir created; populated by ingest) * ├── takes/ # (dir created; populated by ingest cuts) * ├── frames/ # (dir created; populated by ingest) * └── prompts/take-NN_…md # one ready-to-send prompt per take * * Pure-ish side effect: only filesystem writes under `workdir`. No network, no * ffmpeg, no providers. The manifest is written verbatim from `plan` with a * `generatedAt` stamp applied by the caller-supplied `now` (so tests are * deterministic). */ import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import type { MotionOverlayPlan, MotionTake } from './types.js'; export interface WriteWorkFolderOptions { /** ISO timestamp stamped onto the manifest's generatedAt (deterministic in tests). */ generatedAt?: string; } export interface WriteWorkFolderResult { workdir: string; manifestPath: string; readmePath: string; promptPaths: string[]; } /** * Emit the work folder + README + manifest + per-take prompt markdown files. * Creates `source/`, `takes/`, `frames/`, `prompts/` subdirs (ingest fills the * media ones later). Returns the absolute paths written. */ export async function writeWorkFolder( workdir: string, plan: MotionOverlayPlan, options: WriteWorkFolderOptions = {}, ): Promise { await mkdir(workdir, { recursive: true }); await Promise.all([ mkdir(join(workdir, 'source'), { recursive: true }), mkdir(join(workdir, 'takes'), { recursive: true }), mkdir(join(workdir, 'frames'), { recursive: true }), mkdir(join(workdir, 'prompts'), { recursive: true }), ]); const stampedPlan: MotionOverlayPlan = { ...plan, generatedAt: options.generatedAt ?? new Date().toISOString(), }; const manifestPath = join(workdir, 'motion-overlay-plan.json'); await writeFile(manifestPath, `${JSON.stringify(stampedPlan, null, 2)}\n`, 'utf-8'); const readmePath = join(workdir, 'README.md'); await writeFile(readmePath, renderReadme(stampedPlan), 'utf-8'); const promptPaths: string[] = []; for (const take of plan.takes) { const promptPath = join(workdir, take.promptFile); await writeFile(promptPath, renderTakePrompt(take), 'utf-8'); promptPaths.push(promptPath); } return { workdir, manifestPath, readmePath, promptPaths }; } /** Render one take's prompt markdown — the prompt body lives in a 3-backtick fence. */ export function renderTakePrompt(take: MotionTake): string { const lines = [ `# Take ${take.index + 1} — ${take.anatomy}`, '', `- **Clip:** \`${take.file}\``, `- **Span:** ${fmt(take.start)} → ${fmt(take.end)} (${fmt(take.durationSeconds)}s)`, `- **Reel beat:** ${take.anatomy}`, '', 'Send the prompt below to the Omni Flash V2V transport with this take clip as the base footage.', '', '```', take.prompt, '```', '', ]; return lines.join('\n'); } /** Render the work-folder README. */ export function renderReadme(plan: MotionOverlayPlan): string { const lines: string[] = [ '# Motion-overlay work folder', '', `Generated by \`vclaw video motion-overlay\`${plan.generatedAt ? ` at ${plan.generatedAt}` : ''}.`, '', '## Plan', '', `- **Input:** \`${plan.input.path}\``, `- **Duration:** ${fmt(plan.input.durationSeconds)}s`, `- **Frame:** ${plan.input.width}×${plan.input.height} (${plan.input.aspect})`, `- **Layout:** ${plan.layout}`, `- **Style:** ${plan.style}`, `- **Accent:** ${plan.accent}`, `- **Language:** ${plan.language}`, `- **Takes:** ${plan.takes.length}`, '', '## Takes', '', '| # | Beat | Span | Clip | Prompt |', '| - | ---- | ---- | ---- | ------ |', ...plan.takes.map( (t) => `| ${t.index + 1} | ${t.anatomy} | ${fmt(t.start)}–${fmt(t.end)}s | \`${t.file}\` | \`${t.promptFile}\` |`, ), '', '## Layout', '', '```', 'source/ original.mp4, audio, transcript.json', 'takes/ one frame-accurate clip per take', 'frames/ sampled reference frames', 'prompts/ one ready-to-send Omni prompt per take', '```', '', '## Next', '', 'This is a PLAN. Review the per-take prompts under `prompts/`, then run', '`vclaw video motion-overlay --execute --confirm-spend` to render each take', 'through Omni Flash V2V, restore the original audio, and stitch the reel.', '', ]; return lines.join('\n'); } function fmt(seconds: number): string { return (Math.round(seconds * 10) / 10).toFixed(1); }