decolua/9router · error · Error

Gemini TTS failed: ${res.status}

Error message

Gemini TTS failed: ${res.status}

What it means

When the Gemini generateContent call (with responseModalities AUDIO) returns a non-OK HTTP status, gemini.js surfaces Google's `error.message` if present, else this generic `Gemini TTS failed: <status>`. It means Google rejected the synthesis request or the call failed upstream; the status code distinguishes auth (401/403), bad model/voice (400/404), quota (429), and outages (5xx).

Source

Thrown at open-sse/handlers/ttsProviders/gemini.js:79

export default {
  async synthesize(text, model, credentials, _responseFormat, opts = {}) {
    if (!credentials?.apiKey) throw new Error("No Gemini API key configured");
    const { modelId, voiceId } = parseGeminiModelVoice(model);
    const url = `${TTS_BASE}/${modelId}:generateContent?key=${credentials.apiKey}`;
    const res = await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        contents: [{ parts: [{ text: buildPrompt(text, opts.language) }] }],
        generationConfig: {
          responseModalities: ["AUDIO"],
          speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: voiceId } } },
        },
      }),
    });
    if (!res.ok) {
      const err = await res.json().catch(() => ({}));
      throw new Error(err?.error?.message || `Gemini TTS failed: ${res.status}`);
    }
    const data = await res.json();
    const b64 = data?.candidates?.[0]?.content?.parts?.find((p) => p.inlineData?.data)?.inlineData?.data;
    if (!b64) {
      const reason = data?.candidates?.[0]?.finishReason || data?.promptFeedback?.blockReason || "unknown";
      throw new Error(`Gemini TTS returned no audio (finishReason: ${reason}, voice: ${voiceId}, model: ${modelId})`);
    }
    const wav = pcmToWav(Buffer.from(b64, "base64"));
    return { base64: wav.toString("base64"), format: "wav" };
  },
};

// Voice fetcher — return prebuilt voices (Gemini has no list API)
const PREBUILT_VOICES = [
  { id: "Zephyr", lang: "en", gender: "Female" },
  { id: "Puck", lang: "en", gender: "Male" },
  { id: "Charon", lang: "en", gender: "Male" },
  { id: "Kore", lang: "en", gender: "Female" },

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Read the status: 400/404 → check that modelId is a currently available Gemini TTS model and voiceName matches a prebuilt voice exactly (case-sensitive); 401/403 → fix the API key and enable the Generative Language API; 429 → wait or raise quota.
  2. Update the configured TTS model to a currently supported one (the module's KNOWN_MODELS list comes from config/providerModels.js — keep it current).
  3. Test the key and model directly with curl against the generateContent endpoint to see Google's full error body.
  4. Retry on 429/5xx with backoff.
  5. Fall back to a different TTS provider if the preview model is deprecated in your region/account.

Example fix

// before: hardcoded retired preview model
await gemini.synthesize(text, "gemini-3.1-flash-tts-preview/kore", creds);
// after: use a live model + exact-case voice
const model = "gemini-2.5-flash-preview-tts"; // verify availability first
await gemini.synthesize(text, `${model}/Kore`, creds);
Defensive patterns

Strategy: retry

Validate before calling

const VALID_VOICES = ["Zephyr","Puck","Charon","Kore","Fenrir","Leda","Orus","Aoede"];
if (voiceId && !VALID_VOICES.includes(voiceId)) throw new Error(`Unknown Gemini voice: ${voiceId} (case-sensitive)`);
if (modelId && !/tts/.test(modelId)) throw new Error(`${modelId} is not a Gemini TTS model`);

Try / catch

try {
  const audio = await gemini.synthesize(text, `${modelId}/${voiceId}`, creds);
} catch (e) {
  if (/failed: 429/.test(e.message)) {
    await sleep(10000);
    return gemini.synthesize(text, `${modelId}/${voiceId}`, creds);
  }
  if (/failed: (400|404)/.test(e.message)) throw new Error(`Gemini model/voice invalid: ${modelId}/${voiceId}`);
  if (/failed: 40[13]/.test(e.message)) throw new Error("Gemini API key invalid or API not enabled");
  throw e;
}

Prevention

When it happens

Trigger: Non-OK response from POST {TTS_BASE}/{modelId}:generateContent?key=... — invalid/expired API key (401/403), unknown modelId (404/400, e.g. a preview TTS model that was retired), unsupported voiceName or malformed request body (400), quota/rate limits (429), or Gemini outage (500/503).

Common situations: Preview TTS model names (like gemini-2.5/3.1-flash-tts-preview) rotating out of availability so the configured model id 404s; API key from a project without the Generative Language API enabled; free-tier quota exhausted; voice name typo (voice ids are case-sensitive, e.g. "Kore" not "kore").

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/83a1ab8024265bca. Report an issue: GitHub.