Setting the file. One moment.
Assemble Index Test · Product Launch Video · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page (opens in a new tab)
scripts/ assemble-index.test.mjs
JavaScript · 127 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 '<div data-composition-id="01-a" data-width="1920" data-height="1080">' +
28 '<section class="clip" data-start="0" data-duration="3"></section></div>' ,
29 );
30 if (audioMeta) writeFileSync ( join (dir, "audio_meta.json" ), JSON . stringify (audioMeta));
31 if (beforeAssemble) beforeAssemble (dir);
32 const r = spawnSync (
33 process.execPath,
34 [
35 assembleScript,
36 "--storyboard" ,
37 join (dir, "STORYBOARD.md" ),
38 "--hyperframes" ,
39 dir,
40 ... extraArgs,
41 ],
42 { encoding: "utf8" },
43 );
44 return { dir, r };
45 }
46
47 test ( "assemble REFUSES while bgm_pending and no bed on disk" , () => {
48 const { dir , r } = assembleWith ({
49 audioMeta: { bgm: null , bgm_pending: true , voices: [], sfx: [] },
50 });
51
52 assert. notEqual (r.status, 0 , "should not assemble a silent film over a pending bed" );
53 assert. match (r.stderr, /bgm_pending/ );
54 // Refusing means producing nothing, not a half-built index.
55 assert. equal ( existsSync ( join (dir, "index.html" )), false );
56 });
57
58 test ( "--allow-pending-bgm assembles anyway, and says so" , () => {
59 const { dir , r } = assembleWith ({
60 audioMeta: { bgm: null , bgm_pending: true , voices: [], sfx: [] },
61 extraArgs: [ "--allow-pending-bgm" ],
62 });
63
64 assert. equal (r.status, 0 , r.stderr);
65 assert. equal ( existsSync ( join (dir, "index.html" )), true );
66 assert. match (r.stdout + r.stderr, /pending/ i );
67 });
68
69 test ( "a film that is silent BY DESIGN still assembles untouched" , () => {
70 // The whole point of carrying the flag: this case must stay distinguishable from the above.
71 const { dir , r } = assembleWith ({ audioMeta: { bgm: null , voices: [], sfx: [] } });
72
73 assert. equal (r.status, 0 , r.stderr);
74 assert. equal ( existsSync ( join (dir, "index.html" )), true );
75 assert. doesNotMatch (r.stderr, /bgm_pending/ );
76 });
77
78 // ── PRINFRA-309: loop-extended BGM must be a real .mp3, not MP3-in-a-.wav ────
79 // ensureBgmCovers() always encodes the loop-extended track with libmp3lame — so a source
80 // asset named "*.wav" that's short of TOTAL used to come out as MP3 audio inside a
81 // .loop.wav-named file: decodes fine via ffprobe/ffmpeg/Chromium (which sniff the real
82 // WAVE_FORMAT_MPEGLAYER3 tag), but hard-fails in any naive/strict WAV parser (e.g. Python's
83 // stdlib `wave` module) that doesn't do full format-tag dispatch.
84 test (
85 "loop-extended bgm is written as .mp3, not .loop.wav, even when the source is a .wav" ,
86 { skip: ! HAS_FFMPEG },
87 () => {
88 const { dir , r } = assembleWith ({
89 audioMeta: { bgm: { path: "assets/bgm/bed.wav" }, voices: [], sfx: [] },
90 beforeAssemble : ( projectDir ) => {
91 mkdirSync ( join (projectDir, "assets" , "bgm" ), { recursive: true });
92 // 1s tone, well short of the 3s TOTAL from the single storyboard frame above —
93 // guarantees ensureBgmCovers() takes the loop-extend branch.
94 const gen = spawnSync (
95 "ffmpeg" ,
96 [
97 "-y" ,
98 "-f" ,
99 "lavfi" ,
100 "-i" ,
101 "sine=frequency=440:duration=1" ,
102 join (projectDir, "assets" , "bgm" , "bed.wav" ),
103 ],
104 { encoding: "utf8" },
105 );
106 assert. equal (gen.status, 0 , gen.stderr);
107 },
108 });
109
110 assert. equal (r.status, 0 , r.stderr);
111 const html = readFileSync ( join (dir, "index.html" ), "utf8" );
112 const bgmEl = html. match ( /id="el-bgm" [\s\S] *? src="( [ ^ "] + )"/ );
113 assert. ok (bgmEl, "expected a bgm <audio> element in index.html" );
114 const bgmSrc = bgmEl[ 1 ];
115 assert. match (bgmSrc, / \. mp3 $ / , `bgm src should end in .mp3, got "${ bgmSrc }"` );
116 assert. doesNotMatch (bgmSrc, / \. wav $ / );
117
118 const outPath = join (dir, bgmSrc);
119 assert. equal ( existsSync (outPath), true );
120 const probe = spawnSync (
121 "ffprobe" ,
122 [ "-v" , "error" , "-show_entries" , "stream=codec_name" , "-of" , "csv=p=0" , "--" , outPath],
123 { encoding: "utf8" },
124 );
125 assert. equal (probe.stdout. trim (), "mp3" );
126 },
127 );