101 lines
3.1 KiB
TypeScript
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);
|
|
});
|