feat(remotion): audio layer — self-authored music bed + transition SFX in FlexStory

Adds audio to the scene engine without any third-party/geo-blocked sourcing: the
beds + SFX are synthesized with ffmpeg, so they're license-free (CC0, self-authored)
and need no acquisition — the same play as self-authoring Lottie.

- public/audio/: music-ambient.mp3 (soft 3-tone pad, looped) + sfx-whoosh/pop/chime.
- FlexStory: optional music/musicVolume/sfx props (optional so the existing render
  binding needs no change). Renders <Audio loop> for the bed + a whoosh at each
  scene start and a chime on the final scene, driven by precomputed scene starts.
- check-assets: now also scans public/audio (+ lottie) with folder-prefixed keys;
  assets.json ledgers the 4 audio files (CC0 self-authored).

Verified: tsc clean; a 6s FlexStory render produces an MP4 with a real audio stream
(ffprobe: codec_type=audio). NOTE: these are placeholder/SFX-grade; a premium
curated music library (by vibe) is a separate sourcing sweep, and the studio music
picker → FlexStory `music` prop is a follow-up wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-23 17:31:19 +03:30
parent c0d04fa855
commit 3eab1056c8
7 changed files with 92 additions and 14 deletions
+11 -4
View File
@@ -24,14 +24,21 @@ function walk(dir) {
for (const e of readdirSync(dir)) {
const p = join(dir, e);
if (statSync(p).isDirectory()) out = out.concat(walk(p));
else if (/\.(svg|json)$/i.test(e) && e !== "assets.json") out.push(p);
else if (/\.(svg|json|mp3|wav|ogg|m4a)$/i.test(e) && e !== "assets.json") out.push(p);
}
return out;
}
// Asset files live under illustrations/<source>/ and lottie/.
const files = [...walk(ILLUS), ...walk(join(PUBLIC, "lottie"))]
.map((p) => relative(ILLUS, p).split("\\").join("/"));
// Ledger keys: illustrations are relative to illustrations/ (e.g. dicebear/x.svg);
// lottie + audio carry their folder prefix (lottie/x.json, audio/x.mp3).
const SCAN = [
{ root: ILLUS, prefix: "" },
{ root: join(PUBLIC, "lottie"), prefix: "lottie/" },
{ root: join(PUBLIC, "audio"), prefix: "audio/" },
];
const files = SCAN.flatMap(({ root, prefix }) =>
walk(root).map((p) => prefix + relative(root, p).split("\\").join("/"))
);
const missing = files.filter((f) => !ledger[f]);
if (missing.length) {