Setting the file. One moment.
Heygen Voice · Media Use · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page Resolve
audio/scripts/heygen-voice.mjs
audio/scripts/ heygen-voice.mjs
JavaScript · 98 lines · 4 KB
10
import
{ isMainModule }
from
"./lib/main-module.mjs"
;
11
12 const MEDIA_TYPES = { ".mp3" : "audio/mpeg" , ".wav" : "audio/wav" };
13 const POLL_MS = 2_000 ;
14 const DEADLINE_MS = 120_000 ;
15
16 async function clone ( file , name , headers , { sleep , now }) {
17 const mediaType = MEDIA_TYPES [ extname (file). toLowerCase ()];
18 if ( ! mediaType) throw new Error ( `unsupported audio file ${ file }: use .mp3 or .wav` );
19 const data = readFileSync (file). toString ( "base64" );
20 const created = await heygenJSON ( "/voices/clone" , {
21 method: "POST" ,
22 headers,
23 body: { voice_name: name, audio: { type: "base64" , media_type: mediaType, data } },
24 });
25 const id = created.data.voice_clone_id;
26 const deadline = now () + DEADLINE_MS ;
27 for (;;) {
28 const { data : voice } = await heygenJSON ( `/voices/${ id }` , { headers });
29 if (voice.status === "complete" ) return { voice_id: voice.voice_id ?? id };
30 if (voice.status === "failed" ) throw new Error (voice.failure_message || "voice clone failed" );
31 if ( now () >= deadline) throw new Error ( `voice clone ${ id } not ready after 120 s` );
32 await sleep ( POLL_MS );
33 }
34 }
35
36 async function list ( prefix , headers ) {
37 const mine = [];
38 const seen = new Set ();
39 let token;
40 do {
41 if (token && seen. has (token)) throw new Error ( `HeyGen returned page token ${ token } twice` );
42 if (token) seen. add (token);
43 const query = new URLSearchParams ({ type: "private" , limit: "100" });
44 if (token) query. set ( "token" , token);
45 const page = await heygenJSON ( `/voices?${ query }` , { headers });
46 mine. push ( ... page.data. filter (( v ) => (v.name ?? "" ). startsWith (prefix)));
47 token = page.has_more ? page.next_token : null ;
48 } while (token);
49 // ponytail: the list response has no created_at, so one GET per match; bounded by the clone limit.
50 return Promise . all (
51 mine. map ( async ({ voice_id , name }) => {
52 const { data } = await heygenJSON ( `/voices/${ voice_id }` , { headers });
53 return { voice_id, name, created_at: data.created_at ?? null };
54 }),
55 );
56 }
57
58 export async function main (
59 argv ,
60 {
61 sleep = ( ms ) => new Promise (( r ) => setTimeout (r, ms)),
62 now = Date.now,
63 out = ( s ) => process.stdout. write ( `${ s } \n ` ),
64 err = ( s ) => process.stderr. write ( `${ s } \n ` ),
65 } = {},
66 ) {
67 try {
68 const { positionals , values } = parseArgs ({
69 args: argv,
70 allowPositionals: true ,
71 options: { name: { type: "string" }, prefix: { type: "string" , default: "" } },
72 });
73 const [ command , arg ] = positionals;
74 if (command === "clone" ) {
75 if ( ! arg || ! values.name) throw new Error ( "usage: clone <audio-file> --name <name>" );
76 out ( JSON . stringify ( await clone (arg, values.name, heygenAuthHeaders (), { sleep, now })));
77 } else if (command === "delete" ) {
78 if ( ! arg) throw new Error ( "usage: delete <voice_id>" );
79 await heygenJSON ( `/voices/${ encodeURIComponent ( arg ) }` , {
80 method: "DELETE" ,
81 headers: heygenAuthHeaders (),
82 });
83 } else if (command === "list" ) {
84 out ( JSON . stringify ( await list (values.prefix, heygenAuthHeaders ())));
85 } else {
86 throw new Error ( "usage: heygen-voice.mjs clone|delete|list (see the header of this file)" );
87 }
88 return 0 ;
89 } catch (e) {
90 err ( heygenMessage (e));
91 return 1 ;
92 }
93 }
94
95 if ( isMainModule ( import . meta .url)) {
96 loadEnvFromDir (process. cwd ());
97 process.exitCode = await main (process.argv. slice ( 2 ));
98 }