Setting the file. One moment. Gemini TTS · Media Use · heygen-com/hyperframes · Skills Docs⋯
scripts/11 files
audio/scripts/lib/gemini-tts.mjs
audio/scripts/lib/gemini-tts.mjs
JavaScript·122 lines·5 KB
,
10 "gemini-3.1-flash-tts-preview",
11 "gemini-2.5-pro-preview-tts",
12 "gemini-2.5-flash-preview-tts",
13];
14
15// 3.8 returns WAV; older models return PCM that we wrap without resampling.
16// The shared engine transcribes the saved audio for word timings.
17export async function synthesizeGemini(
18 { text, voiceId = "Kore", model = GEMINI_TTS_MODEL, style, speed = 1, wavAbs },
19 { fetchImpl = fetch, authenticate = geminiAuth } = {},
20) {
21 let secret;
22 try {
23 if (!GEMINI_TTS_MODELS.includes(model)) {
24 throw new Error(`Unsupported Gemini TTS model: ${model}`);
25 }
26 if (speed !== 1) throw new Error("Gemini TTS uses style for pacing; omit speed or use 1");
27 const modern = model.startsWith("gemini-3.8-");
28 if (!modern && /^(voice_|voicekey_)/.test(voiceId)) {
29 throw new Error(
30 "Custom Gemini voices require a 3.8 TTS model; use a prebuilt voice with older models",
31 );
32 }
33 const auth = authenticate();
34 secret = auth.secret;
35 const content = {
36 type: "text",
37 text:
38 !modern && style ? `Read the following text with this delivery: ${style}\n\n${text}` : text,
39 };
40 if (modern && style) content.annotations = [{ type: "speech_metadata", style }];
41 const response = await fetchImpl(
42 "https://generativelanguage.googleapis.com/v1beta/interactions",
43 {
44 method: "POST",
45 headers: { "Content-Type": "application/json", ...auth.headers },
46 signal: AbortSignal.timeout(120_000),
47 body: JSON.stringify({
48 model,
49 input: [{ type: "user_input", content: [content] }],
50 response_format: modern ? { type: "audio", mime_type: "audio/wav" } : { type: "audio" },
51 generation_config: { speech_config: [{ voice: voiceId }] },
52 store: false,
53 }),
54 },
55 );
56 if (!response.ok) {
57 const detail = await response.text();
58 throw new Error(`Gemini TTS HTTP ${response.status}: ${detail}`);
59 }
60 const payload = await response.json();
61 if (payload.status !== "completed") {
62 throw new Error(`Gemini TTS did not complete (${payload.status ?? "missing status"})`);
63 }
64 const audio = (payload.steps ?? [])
65 .filter((step) => step.type === "model_output")
66 .flatMap((step) => step.content ?? [])
67 .filter((part) => part.type === "audio");
68 if (audio.length !== 1 || !audio[0].data) {
69 throw new Error("Gemini TTS returned no single audio block");
70 }
71 let bytes = Buffer.from(audio[0].data, "base64");
72 if (!modern && /^audio\/l16(?:;|$)/i.test(audio[0].mime_type ?? "")) {
73 bytes = pcmToWav(bytes, audio[0].mime_type);
74 } else if (audio[0].mime_type !== "audio/wav") {
75 throw new Error("Gemini TTS returned an unsupported audio format");
76 }
77 if (
78 bytes.length <= 44 ||
79 bytes.toString("ascii", 0, 4) !== "RIFF" ||
80 bytes.toString("ascii", 8, 12) !== "WAVE"
81 ) {
82 throw new Error("Gemini TTS returned invalid WAV audio");
83 }
84 mkdirSync(dirname(wavAbs), { recursive: true });
85 writeFileSync(wavAbs, bytes);
86 return { ok: true, words: null };
87 } catch (error) {
88 // Error responses must never echo the credential into logs or metadata.
89 const message = String(error?.message ?? error);
90 return {
91 ok: false,
92 words: null,
93 error: secret ? message.split(secret).join("[redacted]") : message,
94 };
95 }
96}
97
98function pcmToWav(pcm, mime) {
99 const rate = Number(/(?:^|;)\s*rate=(\d+)(?:;|$)/i.exec(mime)?.[1]);
100 const channels = /(?:^|;)\s*channels=([^;]+)/i.exec(mime)?.[1];
101 const codec = /(?:^|;)\s*codec=([^;]+)/i.exec(mime)?.[1];
102 if ((channels && channels.trim() !== "1") || (codec && codec.trim() !== "pcm")) {
103 throw new Error("Gemini TTS returned unsupported PCM channels or codec");
104 }
105 if (!Number.isInteger(rate) || rate < 8000 || rate > 96000 || !pcm.length || pcm.length % 2) {
106 throw new Error("Gemini TTS returned invalid PCM audio or sample rate");
107 }
108 const header = Buffer.alloc(44);
109 header.write("RIFF");
110 header.writeUInt32LE(36 + pcm.length, 4);
111 header.write("WAVEfmt ", 8);
112 header.writeUInt32LE(16, 16);
113 header.writeUInt16LE(1, 20);
114 header.writeUInt16LE(1, 22);
115 header.writeUInt32LE(rate, 24);
116 header.writeUInt32LE(rate * 2, 28);
117 header.writeUInt16LE(2, 32);
118 header.writeUInt16LE(16, 34);
119 header.write("data", 36);
120 header.writeUInt32LE(pcm.length, 40);
121 return Buffer.concat([header, pcm]);
122}