Setting the file. One moment.
Heygen Voice Test · Media Use · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page Resolve
audio/scripts/heygen-voice.test.mjs
audio/scripts/ heygen-voice.test.mjs
JavaScript · 233 lines · 8 KB
realFetch
=
globalThis.fetch;
9 const savedEnv = {
10 HEYGEN_ACCESS_TOKEN: process.env. HEYGEN_ACCESS_TOKEN ,
11 HEYGEN_API_KEY: process.env. HEYGEN_API_KEY ,
12 };
13 let dir;
14 let calls;
15
16 beforeEach (() => {
17 dir = mkdtempSync ( join ( tmpdir (), "heygen-voice-" ));
18 delete process.env. HEYGEN_ACCESS_TOKEN ;
19 process.env. HEYGEN_API_KEY = "hg_test" ;
20 calls = [];
21 });
22
23 afterEach (() => {
24 globalThis.fetch = realFetch;
25 for ( const [ k , v ] of Object. entries (savedEnv)) {
26 if (v === undefined ) delete process.env[k];
27 else process.env[k] = v;
28 }
29 rmSync (dir, { recursive: true , force: true });
30 });
31
32 // routes: "METHOD /path" → array of [status, body] answered in order (last one repeats).
33 function mockFetch ( routes ) {
34 globalThis. fetch = async ( url , opts ) => {
35 const key = `${ opts . method } ${ String ( url ). replace ( "https://api.heygen.com/v3" , "" ) }` ;
36 calls. push ({ key, opts });
37 const queue = routes[key];
38 if ( ! queue) throw new Error ( `unexpected request ${ key }` );
39 const [ status , body ] = queue. length > 1 ? queue. shift () : queue[ 0 ];
40 return new Response (body === undefined ? null : JSON . stringify (body), { status });
41 };
42 }
43
44 async function run ( argv , { sleep = async () => {}, now = () => 0 } = {}) {
45 const out = [];
46 const err = [];
47 const code = await main (argv, {
48 sleep,
49 now,
50 out : ( s ) => out. push (s),
51 err : ( s ) => err. push (s),
52 });
53 return { code, out: out. join ( " \n " ), err: err. join ( " \n " ) };
54 }
55
56 function audioFile ( name = "sample.mp3" ) {
57 const path = join (dir, name);
58 writeFileSync (path, Buffer. from ( "fake-audio" ));
59 return path;
60 }
61
62 test ( "clone uploads base64 audio, polls until complete, prints the voice id" , async () => {
63 mockFetch ({
64 "POST /voices/clone" : [[ 200 , { data: { voice_clone_id: "vc1" } }]],
65 "GET /voices/vc1" : [
66 [ 200 , { data: { status: "processing" , voice_id: "vc1" } }],
67 [ 200 , { data: { status: "complete" , voice_id: "vc1" , name: "Me" } }],
68 ],
69 });
70 const sleeps = [];
71 const r = await run ([ "clone" , audioFile (), "--name" , "Me" ], {
72 sleep : async ( ms ) => sleeps. push (ms),
73 });
74 assert. deepEqual (r, { code: 0 , out: '{"voice_id":"vc1"}' , err: "" });
75 assert. deepEqual (sleeps, [ 2000 ]);
76 const post = calls[ 0 ];
77 assert. equal (post.opts.headers[ "X-Api-Key" ], "hg_test" );
78 assert. deepEqual ( JSON . parse (post.opts.body), {
79 voice_name: "Me" ,
80 audio: {
81 type: "base64" ,
82 media_type: "audio/mpeg" ,
83 data: Buffer. from ( "fake-audio" ). toString ( "base64" ),
84 },
85 });
86 });
87
88 test ( "clone sends audio/wav for a .wav file" , async () => {
89 mockFetch ({
90 "POST /voices/clone" : [[ 200 , { data: { voice_clone_id: "vc1" } }]],
91 "GET /voices/vc1" : [[ 200 , { data: { status: "complete" , voice_id: "vc1" } }]],
92 });
93 const r = await run ([ "clone" , audioFile ( "take.WAV" ), "--name" , "Me" ]);
94 assert. equal (r.code, 0 );
95 assert. equal ( JSON . parse (calls[ 0 ].opts.body).audio.media_type, "audio/wav" );
96 });
97
98 test ( "clone prints HeyGen's refusal verbatim and exits 1" , async () => {
99 const message =
100 "Voice clone limit reached (10). Delete unused clones or contact support to increase your limit." ;
101 mockFetch ({
102 "POST /voices/clone" : [[ 400 , { error: { code: "resource_limit_reached" , message } }]],
103 });
104 const r = await run ([ "clone" , audioFile (), "--name" , "Me" ]);
105 assert. deepEqual (r, { code: 1 , out: "" , err: message });
106 });
107
108 test ( "clone surfaces a failed clone's failure_message" , async () => {
109 mockFetch ({
110 "POST /voices/clone" : [[ 200 , { data: { voice_clone_id: "vc1" } }]],
111 "GET /voices/vc1" : [[ 200 , { data: { status: "failed" , failure_message: "Audio too noisy" } }]],
112 });
113 const r = await run ([ "clone" , audioFile (), "--name" , "Me" ]);
114 assert. deepEqual (r, { code: 1 , out: "" , err: "Audio too noisy" });
115 });
116
117 test ( "clone gives up after the 120 s deadline" , async () => {
118 mockFetch ({
119 "POST /voices/clone" : [[ 200 , { data: { voice_clone_id: "vc1" } }]],
120 "GET /voices/vc1" : [[ 200 , { data: { status: "processing" } }]],
121 });
122 let clock = 0 ;
123 const r = await run ([ "clone" , audioFile (), "--name" , "Me" ], {
124 now : () => clock,
125 sleep : async ( ms ) => {
126 clock += ms;
127 },
128 });
129 assert. deepEqual (r, { code: 1 , out: "" , err: "voice clone vc1 not ready after 120 s" });
130 assert. equal (calls. filter (( c ) => c.key === "GET /voices/vc1" ). length , 61 );
131 });
132
133 test ( "clone rejects an audio type other than mp3 or wav without calling HeyGen" , async () => {
134 mockFetch ({});
135 const r = await run ([ "clone" , audioFile ( "take.ogg" ), "--name" , "Me" ]);
136 assert. equal (r.code, 1 );
137 assert. match (r.err, /use \. mp3 or \. wav/ );
138 assert. equal (calls. length , 0 );
139 });
140
141 test ( "delete calls DELETE /voices/<id> and exits 0" , async () => {
142 mockFetch ({ "DELETE /voices/vc1" : [[ 200 , { data: {} }]] });
143 const r = await run ([ "delete" , "vc1" ]);
144 assert. deepEqual (r, { code: 0 , out: "" , err: "" });
145 });
146
147 test ( "delete exits 0 on a 204 with no body, and escapes the id in the path" , async () => {
148 mockFetch ({ "DELETE /voices/a%2Fb" : [[ 204 ]] });
149 const r = await run ([ "delete" , "a/b" ]);
150 assert. deepEqual (r, { code: 0 , out: "" , err: "" });
151 });
152
153 test ( "clone falls back to the clone id when the finished voice has no voice_id" , async () => {
154 mockFetch ({
155 "POST /voices/clone" : [[ 200 , { data: { voice_clone_id: "vc9" } }]],
156 "GET /voices/vc9" : [[ 200 , { data: { status: "complete" } }]],
157 });
158 const r = await run ([ "clone" , audioFile (), "--name" , "Me" ]);
159 assert. deepEqual (r, { code: 0 , out: '{"voice_id":"vc9"}' , err: "" });
160 });
161
162 test ( "list skips a nameless voice and stops on a page token HeyGen repeats" , async () => {
163 mockFetch ({
164 "GET /voices?type=private&limit=100" : [
165 [ 200 , { data: [{ voice_id: "n" , name: null }], has_more: true , next_token: "t2" }],
166 ],
167 "GET /voices?type=private&limit=100&token=t2" : [
168 [ 200 , { data: [], has_more: true , next_token: "t2" }],
169 ],
170 });
171 // A fake answers at once, so a list that never stops would starve any timeout: cap the calls instead.
172 const answer = globalThis.fetch;
173 globalThis. fetch = ( ... args ) =>
174 calls. length >= 10 ? Promise . reject ( new Error ( "paged forever" )) : answer ( ... args);
175 const r = await run ([ "list" , "--prefix" , "desk-" ]);
176 assert. deepEqual (r, { code: 1 , out: "" , err: "HeyGen returned page token t2 twice" });
177 });
178
179 test ( "delete prints HeyGen's 404 message verbatim and exits 1" , async () => {
180 mockFetch ({
181 "DELETE /voices/gone" : [
182 [ 404 , { error: { code: "voice_not_found" , message: "Voice not found: gone" } }],
183 ],
184 });
185 const r = await run ([ "delete" , "gone" ]);
186 assert. deepEqual (r, { code: 1 , out: "" , err: "Voice not found: gone" });
187 });
188
189 test ( "list pages through private voices and returns prefix matches with created_at" , async () => {
190 mockFetch ({
191 "GET /voices?type=private&limit=100" : [
192 [
193 200 ,
194 {
195 data: [
196 { voice_id: "a" , name: "desk-1" },
197 { voice_id: "b" , name: "other" },
198 ],
199 has_more: true ,
200 next_token: "t2" ,
201 },
202 ],
203 ],
204 "GET /voices?type=private&limit=100&token=t2" : [
205 [ 200 , { data: [{ voice_id: "c" , name: "desk-2" }], has_more: false }],
206 ],
207 "GET /voices/a" : [[ 200 , { data: { voice_id: "a" , created_at: 1700000000 } }]],
208 "GET /voices/c" : [[ 200 , { data: { voice_id: "c" , created_at: 1700000100 } }]],
209 });
210 const r = await run ([ "list" , "--prefix" , "desk-" ]);
211 assert. equal (r.code, 0 );
212 assert. deepEqual ( JSON . parse (r.out), [
213 { voice_id: "a" , name: "desk-1" , created_at: 1700000000 },
214 { voice_id: "c" , name: "desk-2" , created_at: 1700000100 },
215 ]);
216 });
217
218 test ( "list prints HeyGen's refusal verbatim and exits 1" , async () => {
219 const message = "Invalid or expired API key. Verify your x-api-key header." ;
220 mockFetch ({
221 "GET /voices?type=private&limit=100" : [[ 401 , { error: { code: "unauthorized" , message } }]],
222 });
223 const r = await run ([ "list" , "--prefix" , "desk-" ]);
224 assert. deepEqual (r, { code: 1 , out: "" , err: message });
225 });
226
227 test ( "a host-injected HEYGEN_ACCESS_TOKEN is sent as Bearer" , async () => {
228 process.env. HEYGEN_ACCESS_TOKEN = "at_host" ;
229 mockFetch ({ "DELETE /voices/vc1" : [[ 200 , { data: {} }]] });
230 await run ([ "delete" , "vc1" ]);
231 assert. equal (calls[ 0 ].opts.headers.Authorization, "Bearer at_host" );
232 assert. equal (calls[ 0 ].opts.headers[ "X-Api-Key" ], undefined );
233 });