{"record":{"id":"3dae03eac71d1dd5","repo":"decolua/9router","slug":"gemini-tts-returned-no-audio-finishreason-reas","errorCode":null,"errorMessage":"Gemini TTS returned no audio (finishReason: ${reason}, voice: ${voiceId}, model: ${modelId})","messagePattern":"Gemini TTS returned no audio \\(finishReason: (.+?), voice: (.+?), model: (.+?)\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"open-sse/handlers/ttsProviders/gemini.js","lineNumber":85,"sourceCode":"      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({\n        contents: [{ parts: [{ text: buildPrompt(text, opts.language) }] }],\n        generationConfig: {\n          responseModalities: [\"AUDIO\"],\n          speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: voiceId } } },\n        },\n      }),\n    });\n    if (!res.ok) {\n      const err = await res.json().catch(() => ({}));\n      throw new Error(err?.error?.message || `Gemini TTS failed: ${res.status}`);\n    }\n    const data = await res.json();\n    const b64 = data?.candidates?.[0]?.content?.parts?.find((p) => p.inlineData?.data)?.inlineData?.data;\n    if (!b64) {\n      const reason = data?.candidates?.[0]?.finishReason || data?.promptFeedback?.blockReason || \"unknown\";\n      throw new Error(`Gemini TTS returned no audio (finishReason: ${reason}, voice: ${voiceId}, model: ${modelId})`);\n    }\n    const wav = pcmToWav(Buffer.from(b64, \"base64\"));\n    return { base64: wav.toString(\"base64\"), format: \"wav\" };\n  },\n};\n\n// Voice fetcher — return prebuilt voices (Gemini has no list API)\nconst PREBUILT_VOICES = [\n  { id: \"Zephyr\", lang: \"en\", gender: \"Female\" },\n  { id: \"Puck\", lang: \"en\", gender: \"Male\" },\n  { id: \"Charon\", lang: \"en\", gender: \"Male\" },\n  { id: \"Kore\", lang: \"en\", gender: \"Female\" },\n  { id: \"Fenrir\", lang: \"en\", gender: \"Male\" },\n  { id: \"Leda\", lang: \"en\", gender: \"Female\" },\n  { id: \"Orus\", lang: \"en\", gender: \"Male\" },\n  { id: \"Aoede\", lang: \"en\", gender: \"Female\" },\n  { id: \"Callirrhoe\", lang: \"en\", gender: \"Female\" },\n  { id: \"Autonoe\", lang: \"en\", gender: \"Female\" },","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/open-sse/handlers/ttsProviders/gemini.js#L67-L103","documentation":"Gemini returns audio as inlineData inside candidates[0].content.parts; if the 200 response contains no part with inlineData.data, gemini.js throws this error including the finishReason (or promptFeedback.blockReason), the requested voice and model. Common finish reasons are SAFETY (content blocked), STOP with missing audio (malformed request), RECITATION, or PROHIBITED_CONTENT — i.e. Google responded successfully but declined to produce speech.","triggerScenarios":"The generateContent call returns HTTP 200 but candidates[0] lacks an inlineData audio part — typically finishReason SAFETY/BLOCKLIST/PROHIBITED_CONTENT for flagged text, RECITATION for copyrighted-like text, MAX_TOKENS when the text is too long for the audio budget, or an empty candidate list (promptFeedback.blockReason set, e.g. blocked prompt).","commonSituations":"Synthesizing text containing profanity, violence, medical/self-harm topics, or quoted copyrighted material trips Gemini's safety filters; very long text exceeding the TTS output limit; an unsupported voiceName silently producing a candidate without audio; region/age restrictions on the project.","solutions":["Read the finishReason in the message: SAFETY/PROHIBITED_CONTENT/BLOCKLIST → rephrase or sanitize the input text to remove flagged content; RECITATION → remove quoted copyrighted text.","MAX_TOKENS/length issues → split the text into shorter segments and synthesize each separately.","If reason is unknown/empty with a nonstandard voice, retest with a known prebuilt voice (e.g. Kore) and the default model to isolate voice/model incompatibility.","Log the full response JSON on this path to see promptFeedback.blockReason when candidates are empty.","Add a fallback provider for texts Gemini refuses so the TTS pipeline degrades gracefully."],"exampleFix":"// before: send raw text, fail on blocked content\nawait gemini.synthesize(userText, undefined, creds);\n// after: screen and chunk long text\nconst safe = userText.length > 4000 ? userText.slice(0, 4000) : userText;\ntry {\n  return await gemini.synthesize(safe, \"gemini-2.5-flash-preview-tts/Kore\", creds);\n} catch (e) {\n  if (/finishReason: (SAFETY|PROHIBITED_CONTENT|RECITATION)/.test(e.message)) {\n    return await fallbackProvider.synthesize(safe, model, otherCreds);\n  }\n  throw e;\n}","handlingStrategy":"fallback","validationCode":"// pre-screen the most common blockers before calling Gemini\nif (!text?.trim()) throw new Error(\"TTS text is empty\");\nif (text.length > 4000) text = chunkForTts(text); // split long text into segments","typeGuard":"function hasAudioPart(geminiResponse) {\n  return Boolean(\n    geminiResponse?.candidates?.[0]?.content?.parts?.some(p => p.inlineData?.data)\n  );\n}","tryCatchPattern":"try {\n  return await gemini.synthesize(text, model, creds);\n} catch (e) {\n  const m = e.message.match(/finishReason: (\\w+)/);\n  if (m && [\"SAFETY\", \"PROHIBITED_CONTENT\", \"BLOCKLIST\", \"RECITATION\"].includes(m[1])) {\n    return await fallbackTtsProvider.synthesize(text, model, otherCreds); // content blocked — degrade gracefully\n  }\n  throw e;\n}","preventionTips":["Sanitize or pre-review user text that will be spoken (profanity/quotes trigger safety filters).","Chunk long texts — MAX_TOKENS finishReason means the audio budget was exceeded.","Always check finishReason/blockReason in the message to distinguish filtering from bugs.","Configure a non-Gemini TTS fallback for content Gemini refuses."],"tags":["tts","content-filter","google-gemini","empty-response"],"backgroundTag":"safety-filter-blocked-response","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}