Setting the file. One moment. Recording Test · Media Use · heygen-com/hyperframes · Skills DocsResolve
(opens in a new tab)
audio/scripts/recording.test.mjs
JavaScript·139 lines·5 KB
from
"node:url"
;
8import { atempoChain, parseWords, scaleWords } from "./lib/recording.mjs";
9
10const AUDIO = join(dirname(fileURLToPath(import.meta.url)), "audio.mjs");
11
12test("atempoChain splits speeds outside ffmpeg's 0.5..2 stage range", () => {
13 assert.equal(atempoChain(1.5), "atempo=1.5");
14 assert.equal(atempoChain(3), "atempo=2,atempo=1.5");
15 assert.equal(atempoChain(0.25), "atempo=0.5,atempo=0.5");
16});
17
18test("parseWords takes text or word and rejects a missing time", () => {
19 assert.deepEqual(parseWords([{ word: "hi", start: 0, end: 1 }]), [
20 { text: "hi", start: 0, end: 1 },
21 ]);
22 assert.equal(parseWords(undefined), null);
23 assert.throws(() => parseWords([{ text: "hi", start: 0 }]), /words\[0\]/);
24});
25
26test("scaleWords moves words by 1/speed", () => {
27 assert.deepEqual(scaleWords([{ text: "a", start: 1, end: 2 }], 2), [
28 { text: "a", start: 0.5, end: 1 },
29 ]);
30});
31
32test("a recording line with malformed words is omitted with the reason, not a crash", () => {
33 const dir = mkdtempSync(join(tmpdir(), "hf-rec-"));
34 try {
35 const make = spawnSync(
36 "ffmpeg",
37 ["-y", "-loglevel", "error", "-f", "lavfi", "-i", "sine=duration=1", join(dir, "take.wav")],
38 { encoding: "utf8" },
39 );
40 assert.equal(make.status, 0, make.stderr);
41 writeFileSync(
42 join(dir, "audio_request.json"),
43 JSON.stringify({
44 lines: [{ id: "01", audio: "take.wav", words: [{ text: "x", start: 0 }] }],
45 }),
46 );
47 const run = spawnSync(
48 process.execPath,
49 [
50 AUDIO,
51 "--request",
52 join(dir, "audio_request.json"),
53 "--hyperframes",
54 dir,
55 "--out",
56 join(dir, "m.json"),
57 "--only",
58 "tts",
59 ],
60 { encoding: "utf8", env: { ...process.env, HOME: dir } },
61 );
62 assert.equal(run.status, 0, run.stderr);
63 assert.match(run.stdout, /line 01: recording failed — omitted \(words\[0\] needs text/);
64 assert.deepEqual(JSON.parse(readFileSync(join(dir, "m.json"), "utf8")).voices, []);
65 } finally {
66 rmSync(dir, { recursive: true, force: true });
67 }
68});
69
70// A 1 s gap then a 1 s tone is a "word" at 1..2 s. At speed 2 the tone must start at 0.5 s in the
71// stretched file, which is where the scaled word says it is.
72test("a recording at speed 2: the file is stretched and the words still land on the audio", () => {
73 const dir = mkdtempSync(join(tmpdir(), "hf-rec-"));
74 try {
75 const make = spawnSync(
76 "ffmpeg",
77 [
78 "-y",
79 "-loglevel",
80 "error",
81 "-f",
82 "lavfi",
83 "-i",
84 "anullsrc=r=44100:cl=mono",
85 "-f",
86 "lavfi",
87 "-i",
88 "sine=frequency=440:duration=1:sample_rate=44100",
89 "-filter_complex",
90 "[0]atrim=duration=1[s];[s][1]concat=n=2:v=0:a=1",
91 join(dir, "take.wav"),
92 ],
93 { encoding: "utf8" },
94 );
95 assert.equal(make.status, 0, make.stderr);
96 writeFileSync(
97 join(dir, "audio_request.json"),
98 JSON.stringify({
99 speed: 2,
100 lines: [{ id: "01", audio: "take.wav", words: [{ text: "tone", start: 1, end: 2 }] }],
101 }),
102 );
103 const run = spawnSync(
104 process.execPath,
105 [
106 AUDIO,
107 "--request",
108 join(dir, "audio_request.json"),
109 "--hyperframes",
110 dir,
111 "--out",
112 join(dir, "audio_meta.json"),
113 "--only",
114 "tts",
115 ],
116 { encoding: "utf8", env: { ...process.env, HOME: dir } },
117 );
118 assert.equal(run.status, 0, run.stderr + run.stdout);
119 const meta = JSON.parse(readFileSync(join(dir, "audio_meta.json"), "utf8"));
120 const voice = meta.voices[0];
121 assert.equal(meta.tts_provider, "recording");
122 assert.equal(voice.source, "recording");
123 assert.equal(meta.voice_credit, "Voice: your own recording");
124 assert.ok(Math.abs(voice.duration_s - 1) < 0.05, `duration ${voice.duration_s}`);
125 assert.deepEqual([voice.words[0].start, voice.words[0].end], [0.5, 1]);
126 const probe = spawnSync(
127 "ffmpeg",
128 ["-i", join(dir, voice.path), "-af", "silencedetect=noise=-40dB:d=0.2", "-f", "null", "-"],
129 { encoding: "utf8" },
130 );
131 const silenceEnd = Number(/silence_end: ([\d.]+)/.exec(probe.stderr)?.[1]);
132 assert.ok(
133 Math.abs(silenceEnd - voice.words[0].start) < 0.08,
134 `tone starts at ${silenceEnd}s, word says ${voice.words[0].start}s`,
135 );
136 } finally {
137 rmSync(dir, { recursive: true, force: true });
138 }
139});