Setting the file. One moment.
Assemble Index Test · PR To Video · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page (opens in a new tab)
scripts/ assemble-index.test.mjs
JavaScript · 130 lines · 5 KB
const
assembleScript
=
new
URL
(
"./assemble-index.mjs"
,
import
.
meta
.url).pathname;
9 const HAS_FFMPEG =
10 spawnSync ( "ffmpeg" , [ "-version" ], { stdio: "ignore" }).status === 0 &&
11 spawnSync ( "ffprobe" , [ "-version" ], { stdio: "ignore" }).status === 0 ;
12
13 // ── bgm_pending at the assembly boundary ─────────────────────────────────────
14 // Regression: the flag survived into audio_meta.json but assemble rebuilt its audio object
15 // from three named keys and dropped it, so the step that actually builds the film could not
16 // tell "not ready yet" from "silent by design" and would ship the silent one.
17
18 function assembleWith ({ audioMeta , extraArgs = [], beforeAssemble }) {
19 const dir = mkdtempSync ( join ( tmpdir (), "product-launch-assemble-" ));
20 writeFileSync (
21 join (dir, "STORYBOARD.md" ),
22 "--- \n format: 1920x1080 \n message: T \n --- \n\n ## Frame 1 — A \n - duration: 3s \n - src: compositions/frames/01-a.html \n " ,
23 );
24 mkdirSync ( join (dir, "compositions" , "frames" ), { recursive: true });
25 writeFileSync (
26 join (dir, "compositions" , "frames" , "01-a.html" ),
27 // pr-to-video's frame contract wants one bare <template> whose inner root carries the
28 // composition id and duration (lib/frame-contract.mjs).
29 '<template><div data-composition-id="01-a" data-width="1920" data-height="1080" ' +
30 'data-duration="3"><section class="clip" data-start="0" data-duration="3"></section>' +
31 "</div></template>" ,
32 );
33 if (audioMeta) writeFileSync ( join (dir, "audio_meta.json" ), JSON . stringify (audioMeta));
34 if (beforeAssemble) beforeAssemble (dir);
35 const r = spawnSync (
36 process.execPath,
37 [
38 assembleScript,
39 "--storyboard" ,
40 join (dir, "STORYBOARD.md" ),
41 "--hyperframes" ,
42 dir,
43 ... extraArgs,
44 ],
45 { encoding: "utf8" },
46 );
47 return { dir, r };
48 }
49
50 test ( "assemble REFUSES while bgm_pending and no bed on disk" , () => {
51 const { dir , r } = assembleWith ({
52 audioMeta: { bgm: null , bgm_pending: true , voices: [], sfx: [] },
53 });
54
55 assert. notEqual (r.status, 0 , "should not assemble a silent film over a pending bed" );
56 assert. match (r.stderr, /bgm_pending/ );
57 // Refusing means producing nothing, not a half-built index.
58 assert. equal ( existsSync ( join (dir, "index.html" )), false );
59 });
60
61 test ( "--allow-pending-bgm assembles anyway, and says so" , () => {
62 const { dir , r } = assembleWith ({
63 audioMeta: { bgm: null , bgm_pending: true , voices: [], sfx: [] },
64 extraArgs: [ "--allow-pending-bgm" ],
65 });
66
67 assert. equal (r.status, 0 , r.stderr);
68 assert. equal ( existsSync ( join (dir, "index.html" )), true );
69 assert. match (r.stdout + r.stderr, /pending/ i );
70 });
71
72 test ( "a film that is silent BY DESIGN still assembles untouched" , () => {
73 // The whole point of carrying the flag: this case must stay distinguishable from the above.
74 const { dir , r } = assembleWith ({ audioMeta: { bgm: null , voices: [], sfx: [] } });
75
76 assert. equal (r.status, 0 , r.stderr);
77 assert. equal ( existsSync ( join (dir, "index.html" )), true );
78 assert. doesNotMatch (r.stderr, /bgm_pending/ );
79 });
80
81 // ── PRINFRA-309: loop-extended BGM must be a real .mp3, not MP3-in-a-.wav ────
82 // ensureBgmCovers() always encodes the loop-extended track with libmp3lame — so a source
83 // asset named "*.wav" that's short of TOTAL used to come out as MP3 audio inside a
84 // .loop.wav-named file: decodes fine via ffprobe/ffmpeg/Chromium (which sniff the real
85 // WAVE_FORMAT_MPEGLAYER3 tag), but hard-fails in any naive/strict WAV parser (e.g. Python's
86 // stdlib `wave` module) that doesn't do full format-tag dispatch.
87 test (
88 "loop-extended bgm is written as .mp3, not .loop.wav, even when the source is a .wav" ,
89 { skip: ! HAS_FFMPEG },
90 () => {
91 const { dir , r } = assembleWith ({
92 audioMeta: { bgm: { path: "assets/bgm/bed.wav" }, voices: [], sfx: [] },
93 beforeAssemble : ( projectDir ) => {
94 mkdirSync ( join (projectDir, "assets" , "bgm" ), { recursive: true });
95 // 1s tone, well short of the 3s TOTAL from the single storyboard frame above —
96 // guarantees ensureBgmCovers() takes the loop-extend branch.
97 const gen = spawnSync (
98 "ffmpeg" ,
99 [
100 "-y" ,
101 "-f" ,
102 "lavfi" ,
103 "-i" ,
104 "sine=frequency=440:duration=1" ,
105 join (projectDir, "assets" , "bgm" , "bed.wav" ),
106 ],
107 { encoding: "utf8" },
108 );
109 assert. equal (gen.status, 0 , gen.stderr);
110 },
111 });
112
113 assert. equal (r.status, 0 , r.stderr);
114 const html = readFileSync ( join (dir, "index.html" ), "utf8" );
115 const bgmEl = html. match ( /id="el-bgm" [\s\S] *? src="( [ ^ "] + )"/ );
116 assert. ok (bgmEl, "expected a bgm <audio> element in index.html" );
117 const bgmSrc = bgmEl[ 1 ];
118 assert. match (bgmSrc, / \. mp3 $ / , `bgm src should end in .mp3, got "${ bgmSrc }"` );
119 assert. doesNotMatch (bgmSrc, / \. wav $ / );
120
121 const outPath = join (dir, bgmSrc);
122 assert. equal ( existsSync (outPath), true );
123 const probe = spawnSync (
124 "ffprobe" ,
125 [ "-v" , "error" , "-show_entries" , "stream=codec_name" , "-of" , "csv=p=0" , "--" , outPath],
126 { encoding: "utf8" },
127 );
128 assert. equal (probe.stdout. trim (), "mp3" );
129 },
130 );