{"record":{"id":"29c52bccf438cd0d","repo":"HeyPuter/puter","slug":"upstream-bad-request-29c52b","errorCode":"upstream_bad_request","errorMessage":"Gemini TTS did not return audio data","messagePattern":"Gemini TTS did not return audio data","errorType":"http","errorClass":"HttpError","httpStatus":400,"severity":"error","filePath":"src/backend/drivers/ai-tts/providers/gemini/GeminiTTSProvider.ts","lineNumber":253,"sourceCode":"        // status and caused 4xx validation errors to page.\n        // eslint-disable-next-line @typescript-eslint/no-explicit-any\n        const response: any = await this.#client.models.generateContent({\n            model,\n            contents: [{ parts: [{ text: inputText }] }],\n            config: {\n                responseModalities: ['AUDIO'],\n                speechConfig: {\n                    voiceConfig: {\n                        prebuiltVoiceConfig: { voiceName: voice },\n                    },\n                },\n            },\n        });\n\n        // Extract audio data from response\n        const part = response?.candidates?.[0]?.content?.parts?.[0];\n        if (!part?.inlineData?.data) {\n            throw new HttpError(400, 'Gemini TTS did not return audio data', {\n                legacyCode: 'upstream_bad_request',\n                fields: { provider: 'gemini' },\n            });\n        }\n\n        const audioBase64: string = part.inlineData.data;\n        const mimeType: string =\n            part.inlineData.mimeType || 'audio/L16;rate=24000';\n\n        // Convert base64 PCM to a WAV buffer for broad client compatibility\n        const pcmBuffer = Buffer.from(audioBase64, 'base64');\n        let outputBuffer: Buffer;\n        let contentType: string;\n\n        if (mimeType.startsWith('audio/L16') || mimeType === 'audio/pcm') {\n            // Wrap raw PCM (16-bit LE, 24kHz, mono) in a WAV container\n            outputBuffer = this.#wrapPcmInWav(pcmBuffer, 24000, 1, 16);\n            contentType = 'audio/wav';","sourceCodeStart":235,"sourceCodeEnd":271,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/drivers/ai-tts/providers/gemini/GeminiTTSProvider.ts#L235-L271","documentation":"After Gemini returns from generateContent with responseModalities ['AUDIO'], GeminiTTSProvider extracts candidates[0].content.parts[0].inlineData.data. If that path is missing (no audio payload), it throws HTTP 400 (legacyCode upstream_bad_request) with fields.provider='gemini'. Commonly the model returned text or nothing instead of audio — e.g. the input tripped a safety filter, the model refused, or the response shape was unexpected.","triggerScenarios":"Sending text Gemini declines to vocalize (disallowed content triggering safety filters), an empty/whitespace result after framing, a model/voice combo that returns text instead of audio, or an upstream response-format change. The provider does not retry; it surfaces the empty-audio result as a 400.","commonSituations":"Content that hits Gemini safety settings; very short or symbolic text the model doesn't treat as speech; API version drift where inlineData is nested differently; rate/quota responses that still return 200 but with no audio.","solutions":["Rephrase or sanitize the text to avoid content Gemini's safety filters reject, then retry.","Confirm the model is a '-tts' model and the voice is a valid prebuilt name.","Try the default voice/model to isolate whether the voice or model caused the empty audio.","Inspect the full Gemini response server-side (add temporary logging of response.candidates) to see finishReason or safetyRatings.","Retry once — transient empty responses can occur under load."],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"// Reduce the chance of an empty-audio response: sanitize/shorten text and\n// stick to supported voice/model combos.\nfunction sanitizeForGeminiTTS(text) {\n  if (typeof text !== 'string' || !text.trim()) return null;\n  // drop content likely to trip safety filters before sending\n  return text.trim().slice(0, 5000);\n}\nconst clean = sanitizeForGeminiTTS(userText);\nif (clean) await driver.synthesize({ text: clean, provider: 'gemini', voice: 'Kore' });","typeGuard":null,"tryCatchPattern":"try {\n  await driver.synthesize({ text, provider: 'gemini', voice });\n} catch (e) {\n  if (e?.fields?.legacyCode === 'upstream_bad_request' && /did not return audio/.test(e.message)) {\n    // likely a safety filter / model refusal — rephrase, switch voice/model, or fall back\n    await driver.synthesize({ text: rephrased, provider: 'gemini', voice: 'Kore', model: 'gemini-2.5-flash-preview-tts' });\n  } else throw e;\n}","preventionTips":["Sanitize text to avoid content Gemini's safety filters reject.","Keep voice/model to the supported prebuilt sets so the response shape is predictable.","Inspect response.candidates[].finishReason and safetyRatings server-side when this recurs.","Fall back to another configured TTS provider when Gemini returns no audio."],"tags":["gemini","tts","upstream","safety","audio","validation"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}