perfect-postcode/video/src/record.ts
2026-05-13 12:12:11 +01:00

101 lines
3.1 KiB
TypeScript

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<void> {
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<void> {
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);
});