import { existsSync, mkdirSync, statSync } from 'node:fs'; import { join } from 'node:path'; import { AUTH_STATE_PATH, LEAD_IN_S, OUTPUT_DIR } from './config.js'; import { assertHardwareWebGL, launchRecordingBrowser } from './browser.js'; import { narrationLog } from './narration.js'; import { installDemoRoutes } from './routes.js'; import type { Storyboard } from './script.js'; import { storyboards } from './storyboard.js'; import { prepareTimeline, runTimeline } from './timeline.js'; import { trimRecording } from './video.js'; async function recordOne(storyboard: Storyboard): Promise { const dir = join(OUTPUT_DIR, storyboard.name); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); const trimmedPath = join(dir, 'recording.webm'); const narrationPath = join(dir, 'narration.json'); if ( process.env.RESUME_RECORDINGS === '1' && existsSync(trimmedPath) && statSync(trimmedPath).size > 0 && existsSync(narrationPath) && statSync(narrationPath).size > 0 ) { console.log(`\n=== [${storyboard.name}] recording exists, skipping ===`); return; } console.log(`\n=== [${storyboard.name}] recording ===`); const { browser, context } = await launchRecordingBrowser(storyboard, { recordDir: dir, }); const page = await context.newPage(); await assertHardwareWebGL(page); const recordedVideo = page.video(); const recordStartMs = Date.now(); page.on('console', (m) => { if (m.type() === 'error' || m.type() === 'warning') { console.log(`[browser ${m.type()}] ${m.text()}`); } }); page.on('response', (r) => { const u = r.url(); if (r.status() === 401 || u.includes('ai-filters')) { console.log(`[net] ${r.status()} ${r.request().method()} ${u}`); } }); page.on('request', (r) => { const u = r.url(); if (u.includes('ai-filters')) console.log(`[req] ${r.method()} ${u}`); }); await installDemoRoutes(page, storyboard); const ctx = await prepareTimeline(page, storyboard); const timeline = await runTimeline(ctx, storyboard); await page.close(); const rawPath = join(dir, 'recording.raw.webm'); await context.close(); if (recordedVideo) await recordedVideo.saveAs(rawPath); await browser.close(); if (!recordedVideo || !statSync(rawPath).size) { throw new Error(`[${storyboard.name}] no recorded webm found`); } trimRecording(rawPath, trimmedPath, storyboard, { recordStartMs, ...timeline, }); const totalDurationMs = timeline.sceneEndMs - timeline.sceneStartMs + LEAD_IN_S * 1000; const cues = narrationLog.flush( narrationPath, totalDurationMs ); console.log( `[${storyboard.name}] wrote ${cues.length} narration cues → ${join(dir, 'narration.json')}` ); } async function main(): Promise { if (!existsSync(AUTH_STATE_PATH)) { console.error(`No ${AUTH_STATE_PATH} found. Run "npm run setup-auth" first.`); process.exit(1); } if (!existsSync(OUTPUT_DIR)) mkdirSync(OUTPUT_DIR, { recursive: true }); for (const sb of storyboards) { await recordOne(sb); } console.log(`\n=== recorded ${storyboards.length} storyboard(s) ===`); } main().catch((err) => { console.error(err); process.exit(1); });