Setting the file. One moment.
Heygen TTS · Media Use · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page ⋯
scripts/11 files
audio/scripts/heygen-tts.mjs
audio/scripts/ heygen-tts.mjs
JavaScript · 121 lines · 4 KB
// Flags: -o/--output (.wav → ffmpeg transcode; .mp3 → raw bytes), --words,
15 // --voice (starfish id), --speed, --lang, --list.
16 // Requires: $HEYGEN_API_KEY / OAuth ~/.heygen credentials and ffmpeg for .wav output.
17
18 import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs" ;
19 import { dirname, resolve } from "node:path" ;
20 import { heygenAuthHeaders, heygenJSON, loadEnvFromDir } from "./lib/heygen.mjs" ;
21 import { ffprobeDuration, resolveVoiceId, synthesizeOne, withWordIds } from "./lib/tts.mjs" ;
22
23 const argv = process.argv. slice ( 2 );
24 function flag ( name , def ) {
25 const i = argv. indexOf ( `--${ name }` );
26 if (i < 0 ) return def;
27 if (i + 1 >= argv. length ) return true ;
28 const v = argv[i + 1 ];
29 return v. startsWith ( "--" ) ? true : v;
30 }
31 const die = ( m ) => {
32 console. error ( `✗ heygen-tts: ${ m }` );
33 process. exit ( 1 );
34 };
35
36 // First arg that isn't a flag or the -o value is the text / .txt path.
37 const positional = (() => {
38 for ( let i = 0 ; i < argv. length ; i ++ ) {
39 const a = argv[i];
40 if (a. startsWith ( "--" )) {
41 const next = argv[i + 1 ];
42 if (next && ! next. startsWith ( "--" )) i ++ ;
43 continue ;
44 }
45 if (a === "-o" ) {
46 i ++ ;
47 continue ;
48 }
49 return a;
50 }
51 return null ;
52 })();
53
54 const output = resolve (
55 ( typeof flag ( "output" ) === "string" && flag ( "output" )) ||
56 (argv. includes ( "-o" ) && argv[argv. indexOf ( "-o" ) + 1 ]) ||
57 "narration.wav" ,
58 );
59 const wordsPath = typeof flag ( "words" ) === "string" ? resolve ( flag ( "words" )) : null ;
60 const userVoice = typeof flag ( "voice" ) === "string" ? flag ( "voice" ) : null ;
61 const speedRaw = typeof flag ( "speed" ) === "string" ? Number ( flag ( "speed" )) : 1.0 ;
62 const speed = isFinite (speedRaw) && speedRaw > 0 ? speedRaw : 1.0 ;
63 const lang = typeof flag ( "lang" ) === "string" ? flag ( "lang" ) : "en" ;
64 const listOnly = flag ( "list" ) === true ;
65
66 loadEnvFromDir (process. cwd ());
67 let authHeaders;
68 try {
69 authHeaders = heygenAuthHeaders ();
70 } catch (e) {
71 die (e.message);
72 }
73
74 // ---------- --list ----------
75 if (listOnly) {
76 const payload = await heygenJSON ( `/voices?engine=starfish&type=public&limit=50` , {
77 headers: authHeaders,
78 });
79 for ( const v of payload.data ?? payload.voices ?? []) {
80 console. log ( `${ v . voice_id } \t ${ v . name } \t ${ v . language ?? ""}` );
81 }
82 process. exit ( 0 );
83 }
84
85 // ---------- resolve text + voice ----------
86 if ( ! positional) die ( "no text given. Pass a string or a .txt path, or use --list." );
87 const text =
88 positional. endsWith ( ".txt" ) && existsSync ( resolve (positional))
89 ? readFileSync ( resolve (positional), "utf8" ). trim ()
90 : positional;
91 if ( ! text) die ( "input text is empty" );
92
93 const voiceId = await resolveVoiceId ({ provider: "heygen" , userVoice, lang });
94 if ( ! userVoice) console. error ( `· using voice ${ voiceId }` );
95
96 // ---------- synthesize (shared engine code) ----------
97 const { ok , words } = await synthesizeOne ({
98 provider: "heygen" ,
99 text,
100 voiceId,
101 lang,
102 speed,
103 wavAbs: output,
104 hyperframesDir: process. cwd (),
105 });
106 if ( ! ok) die ( "synthesis failed (HeyGen request/transcode error)" );
107
108 let wordCount = 0 ;
109 if (wordsPath) {
110 if (words && words. length ) {
111 mkdirSync ( dirname (wordsPath), { recursive: true });
112 writeFileSync (wordsPath, JSON . stringify ( withWordIds (words), null , 2 ));
113 wordCount = words. length ;
114 } else {
115 console. error ( "⚠ no word_timestamps in response — run `hyperframes transcribe` instead" );
116 }
117 }
118
119 const dur = ffprobeDuration (output);
120 const durStr = isFinite (dur) ? ` (${ dur . toFixed ( 2 ) }s)` : "" ;
121 console. log ( `✓ ${ output }${ durStr }${ wordCount ? ` · ${ wordsPath } (${ wordCount } words)` : ""}` );