Files
ai-agent/reel/scripts/render.mjs
Gabriel Vidal 07bd51cef7 chore(reel): web encode settings, end-card poster, drop unused deps
- render.mjs: CRF 26 with x264 aq-mode 3, which spends the bits on the dark
  gradients this mostly-ink film is made of — the same look as CRF 23 at
  ~30% less (31 MB at 1080p, 15 MB at 720p); the poster is the end card
  (logo, line, address, the bill) at 115 s
- the score is plain TypeScript DSP now, so Tone.js and playwright-core go
- README: how the score is actually made

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 06:19:08 +02:00

84 lines
2.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* The final render: picture → mix → web encodes → poster.
*
* npm run render [-- --skip-video] [--skip-audio] [--concurrency 6] [--crf 23]
*
* out/video.mp4 Remotion's picture, near-lossless, silent
* out/audio/mix.wav scripts/mix.mjs (score + sound effects)
* out/tokunia-reel.mp4 1920×1080 web encode (the landing page's)
* out/tokunia-reel-720.mp4 1280×720, for small screens
* out/tokunia-reel-poster.jpg the poster frame (the end card)
*/
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const args = process.argv.slice(2);
const has = (k) => args.includes(k);
const opt = (k, d) => (has(k) ? args[args.indexOf(k) + 1] : d);
const CONCURRENCY = opt("--concurrency", "6");
const CRF = opt("--crf", "26");
const POSTER_T = Number(opt("--poster", "115.0"));
const OUT = path.join(ROOT, "out");
fs.mkdirSync(OUT, { recursive: true });
const run = (cmd, a) => execFileSync(cmd, a, { cwd: ROOT, stdio: "inherit" });
const ff = (a) => run("ffmpeg", ["-hide_banner", "-loglevel", "error", "-y", ...a]);
const mb = (f) => (fs.statSync(f).size / 1e6).toFixed(1) + " MB";
const video = path.join(OUT, "video.mp4");
const mix = path.join(OUT, "audio/mix.wav");
if (!has("--skip-video")) {
const t0 = Date.now();
run("npx", ["remotion", "render", "reel", video, "--codec=h264", "--crf=12", "--muted", `--concurrency=${CONCURRENCY}`]);
console.log(`picture in ${((Date.now() - t0) / 1000).toFixed(0)} s → ${video} (${mb(video)})`);
}
if (!has("--skip-audio")) run("node", ["--import", "tsx", "scripts/mix.mjs"]);
// libx264, slow preset; aq-mode 3 spends the bits on the dark gradients this
// mostly-ink film is made of (CRF 26 + aq3 ≈ CRF 23 to the eye, ~30% smaller);
// AAC 160k; +faststart so the landing page can start before the file is down.
const encode = (out, scale, crf) =>
ff([
"-i",
video,
"-i",
mix,
"-map",
"0:v",
"-map",
"1:a",
...(scale ? ["-vf", `scale=${scale}:flags=lanczos`] : []),
"-c:v",
"libx264",
"-preset",
"slow",
"-crf",
String(crf),
"-x264-params",
"aq-mode=3",
"-profile:v",
"high",
"-pix_fmt",
"yuv420p",
"-c:a",
"aac",
"-b:a",
"160k",
"-shortest",
"-movflags",
"+faststart",
out,
]);
const full = path.join(OUT, "tokunia-reel.mp4");
const small = path.join(OUT, "tokunia-reel-720.mp4");
encode(full, null, CRF);
encode(small, "1280:720", Number(CRF) + 1);
const poster = path.join(OUT, "tokunia-reel-poster.jpg");
ff(["-ss", String(POSTER_T), "-i", video, "-frames:v", "1", "-q:v", "3", poster]);
console.log(`→ ${full} (${mb(full)})\n→ ${small} (${mb(small)})\n→ ${poster} (${mb(poster)})`);