Setting the file. One moment. Gemini TTS Test · Media Use · heygen-com/hyperframes · Skills Docs⋯
scripts/11 files
audio/scripts/lib/gemini-tts.test.mjs
audio/scripts/lib/gemini-tts.test.mjs
JavaScript·243 lines·9 KB
from
"./tts.mjs"
;
8
9function fixture(t) {
10 const dir = mkdtempSync(join(tmpdir(), "gemini-tts-"));
11 const saved = { ...process.env };
12 process.env.GEMINI_API_KEY = "test-gemini-key";
13 delete process.env.GOOGLE_API_KEY;
14 delete process.env.GOOGLE_APPLICATION_CREDENTIALS;
15 delete process.env.GCS_CREDS;
16 t.after(() => {
17 process.env = saved;
18 rmSync(dir, { recursive: true, force: true });
19 });
20 const wav = Buffer.alloc(48);
21 wav.write("RIFF");
22 wav.writeUInt32LE(40, 4);
23 wav.write("WAVEfmt ", 8);
24 wav.writeUInt32LE(16, 16);
25 wav.writeUInt16LE(1, 20);
26 wav.writeUInt16LE(1, 22);
27 wav.writeUInt32LE(24000, 24);
28 wav.writeUInt32LE(48000, 28);
29 wav.writeUInt16LE(2, 32);
30 wav.writeUInt16LE(16, 34);
31 wav.write("data", 36);
32 wav.writeUInt32LE(4, 40);
33 const audio = { type: "audio", mime_type: "audio/wav", data: wav.toString("base64") };
34 const payload = { status: "completed", steps: [{ type: "model_output", content: [audio] }] };
35 return {
36 wav,
37 audio,
38 payload,
39 args: { text: "Hello there.", voiceId: "Kore", wavAbs: join(dir, "voice", "one.wav") },
40 };
41}
42
43test("Gemini preserves verbatim text, directs style separately, saves WAV and requests transcription", async (t) => {
44 const { wav, payload, args } = fixture(t);
45 t.mock.method(globalThis, "fetch", async (url, options) => {
46 assert.equal(url, "https://generativelanguage.googleapis.com/v1beta/interactions");
47 assert.equal(options.headers["x-goog-api-key"], "test-gemini-key");
48 const body = JSON.parse(options.body);
49 assert.equal(body.model, GEMINI_TTS_MODEL);
50 assert.equal(body.store, false);
51 assert.deepEqual(body.input[0].content, [
52 {
53 type: "text",
54 text: args.text,
55 annotations: [{ type: "speech_metadata", style: "Warm and calm" }],
56 },
57 ]);
58 assert.deepEqual(body.generation_config.speech_config, [{ voice: "Kore" }]);
59 assert.equal(body.response_format.mime_type, "audio/wav");
60 return Response.json(payload);
61 });
62 const result = await synthesizeOne({ ...args, provider: "gemini", style: "Warm and calm" });
63 assert.deepEqual(result, { ok: true, words: null });
64 assert.deepEqual(readFileSync(args.wavAbs), wav);
65});
66
67test("Flash-Lite and GOOGLE_API_KEY work without a Gemini key", async (t) => {
68 const { payload, args } = fixture(t);
69 delete process.env.GEMINI_API_KEY;
70 process.env.GOOGLE_API_KEY = "test-google-key";
71 assert.equal(pickProvider("gemini"), "gemini");
72 assert.equal(await resolveVoiceId({ provider: "gemini" }), "Kore");
73 const result = await synthesizeGemini(
74 { ...args, model: "gemini-3.8-flash-lite-tts" },
75 {
76 fetchImpl: async (_, options) => {
77 assert.equal(options.headers["x-goog-api-key"], "test-google-key");
78 const body = JSON.parse(options.body);
79 assert.equal(body.model, "gemini-3.8-flash-lite-tts");
80 assert.equal(body.input[0].content[0].annotations, undefined);
81 return Response.json(payload);
82 },
83 },
84 );
85 assert.equal(result.ok, true);
86});
87
88test("Gemini remains opt-in even with a key and preserves explicitly chosen voices", async (t) => {
89 fixture(t);
90 assert.notEqual(pickProvider(), "gemini");
91 assert.equal(
92 await resolveVoiceId({ provider: "gemini", userVoice: "voice_custom" }),
93 "voice_custom",
94 );
95 delete process.env.GEMINI_API_KEY;
96 assert.throws(() => pickProvider("gemini"), /GEMINI_API_KEY or GOOGLE_API_KEY/);
97});
98
99test("invalid config fails before spending a generation request", async (t) => {
100 const { args } = fixture(t);
101 const deps = { fetchImpl: () => assert.fail("must not call the API") };
102 for (const change of [{ model: "gemini-2.5-flash" }, { speed: 1.2 }]) {
103 const result = await synthesizeGemini({ ...args, ...change }, deps);
104 assert.equal(result.ok, false);
105 assert.ok(result.error);
106 }
107 delete process.env.GEMINI_API_KEY;
108 assert.match((await synthesizeGemini(args, deps)).error, /needs GEMINI_API_KEY/);
109 assert.equal(existsSync(args.wavAbs), false);
110});
111
112test("HTTP and network failures stay actionable without leaking the key", async (t) => {
113 const { args } = fixture(t);
114 const result = await synthesizeGemini(args, {
115 fetchImpl: async () => new Response("quota exceeded test-gemini-key", { status: 429 }),
116 });
117 assert.equal(result.ok, false);
118 assert.match(result.error, /HTTP 429: quota exceeded/);
119 assert.ok(!result.error.includes("test-gemini-key"));
120 const timeout = await synthesizeGemini(args, {
121 fetchImpl: async () => {
122 throw new Error("timed out");
123 },
124 });
125 assert.match(timeout.error, /timed out/);
126 assert.equal(existsSync(args.wavAbs), false);
127});
128
129test("incomplete, missing, raw PCM and malformed audio cannot become a successful WAV", async (t) => {
130 const { args, payload, audio } = fixture(t);
131 const cases = [
132 { ...payload, status: "incomplete" },
133 { status: "completed", steps: [] },
134 ...[{ ...audio, mime_type: "audio/l16" }, { ...audio, data: "not audio" }, audio].map(
135 (part, i) => ({
136 status: "completed",
137 steps: [{ type: "model_output", content: i === 2 ? [part, part] : [part] }],
138 }),
139 ),
140 ];
141 for (const body of cases) {
142 const result = await synthesizeGemini(args, { fetchImpl: async () => Response.json(body) });
143 assert.equal(result.ok, false);
144 assert.equal(existsSync(args.wavAbs), false);
145 }
146});
147
148for (const model of [
149 "gemini-3.1-flash-tts-preview",
150 "gemini-2.5-pro-preview-tts",
151 "gemini-2.5-flash-preview-tts",
152]) {
153 test(`${model} uses legacy delivery prompts and wraps PCM as mono WAV`, async (t) => {
154 const { args, wav } = fixture(t);
155 const pcm = wav.subarray(44);
156 const result = await synthesizeGemini(
157 { ...args, model, style: "Warm and clear" },
158 {
159 fetchImpl: async (_, options) => {
160 const body = JSON.parse(options.body);
161 assert.deepEqual(body.response_format, { type: "audio" });
162 assert.equal(body.input[0].content[0].annotations, undefined);
163 assert.ok(body.input[0].content[0].text.endsWith(args.text));
164 assert.ok(body.input[0].content[0].text.includes("Warm and clear"));
165 return Response.json({
166 status: "completed",
167 steps: [
168 {
169 type: "model_output",
170 content: [
171 {
172 type: "audio",
173 mime_type: "audio/L16;codec=pcm;rate=24000",
174 data: pcm.toString("base64"),
175 },
176 ],
177 },
178 ],
179 });
180 },
181 },
182 );
183 assert.equal(result.ok, true, result.error);
184 assert.deepEqual(readFileSync(args.wavAbs), wav);
185 });
186}
187
188test("service-account bearer and quota project reach synthesis, and token errors are redacted", async (t) => {
189 const { args, payload } = fixture(t);
190 const authenticate = () => ({
191 headers: { Authorization: "Bearer secret-token", "x-goog-user-project": "test-project" },
192 secret: "secret-token",
193 });
194 const ok = await synthesizeGemini(args, {
195 authenticate,
196 fetchImpl: async (_, options) => {
197 assert.equal(options.headers.Authorization, "Bearer secret-token");
198 assert.equal(options.headers["x-goog-user-project"], "test-project");
199 assert.equal(options.headers["x-goog-api-key"], undefined);
200 return Response.json(payload);
201 },
202 });
203 assert.equal(ok.ok, true);
204 const failed = await synthesizeGemini(args, {
205 authenticate,
206 fetchImpl: async () => new Response("denied secret-token", { status: 403 }),
207 });
208 assert.match(failed.error, /HTTP 403/);
209 assert.ok(!failed.error.includes("secret-token"));
210});
211
212test("older models reject unsupported PCM and custom voices", async (t) => {
213 const { args } = fixture(t);
214 for (const mime of [
215 "audio/l16",
216 "audio/l16;rate=0",
217 "audio/l16;rate=24000;channels=2",
218 "audio/l16;rate=24000;codec=other",
219 ]) {
220 const result = await synthesizeGemini(
221 { ...args, model: "gemini-3.1-flash-tts-preview" },
222 {
223 fetchImpl: async () =>
224 Response.json({
225 status: "completed",
226 steps: [
227 {
228 type: "model_output",
229 content: [{ type: "audio", mime_type: mime, data: "AAAAAA==" }],
230 },
231 ],
232 }),
233 },
234 );
235 assert.equal(result.ok, false);
236 assert.equal(existsSync(args.wavAbs), false);
237 }
238 const result = await synthesizeGemini(
239 { ...args, model: "gemini-2.5-pro-preview-tts", voiceId: "voice_custom" },
240 { fetchImpl: () => assert.fail("must not generate") },
241 );
242 assert.match(result.error, /Custom Gemini voices require/);
243});