{"record":{"id":"cd02d5de2f6e81ed","repo":"zeroclaw-labs/zeroclaw","slug":"google-tts-api-error","errorCode":null,"errorMessage":"Google TTS API error ({}): {}","messagePattern":"Google TTS API error \\((.+?)\\): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/tts.rs","lineNumber":368,"sourceCode":"            .client\n            .post(url)\n            .header(\"x-goog-api-key\", &self.api_key)\n            .json(&body)\n            .send()\n            .await\n            .context(\"Failed to send Google TTS request\")?;\n\n        let status = resp.status();\n        let resp_body: serde_json::Value = resp\n            .json()\n            .await\n            .context(\"Failed to parse Google TTS response\")?;\n\n        if !status.is_success() {\n            let msg = resp_body[\"error\"][\"message\"]\n                .as_str()\n                .unwrap_or(\"unknown error\");\n            bail!(\"Google TTS API error ({}): {}\", status, msg);\n        }\n\n        let audio_b64 = resp_body[\"audioContent\"]\n            .as_str()\n            .context(\"Google TTS response missing 'audioContent' field\")?;\n\n        use base64::Engine;\n        let bytes = base64::engine::general_purpose::STANDARD\n            .decode(audio_b64)\n            .context(\"Failed to decode Google TTS base64 audio\")?;\n        Ok(bytes)\n    }\n\n    fn supported_voices(&self) -> Vec<String> {\n        // Google voices vary by language; return common English defaults.\n        [\n            \"en-US-Standard-A\",\n            \"en-US-Standard-B\",","sourceCodeStart":350,"sourceCodeEnd":386,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/tts.rs#L350-L386","documentation":"Raised by GoogleTtsProvider::synthesize when POST https://texttospeech.googleapis.com/v1/text:synthesize returns non-2xx. The key travels in the x-goog-api-key header from [providers.tts.google.<alias>].api_key. Note the body is parsed as JSON first (a non-JSON body fails earlier with \"Failed to parse Google TTS response\"), then error.message is extracted, so this message means Google answered with a structured API error.","triggerScenarios":"403 with API_KEY_INVALID or SERVICE_DISABLED (Cloud Text-to-Speech API not enabled on the project), 400 when the voice name does not match language_code (default en-US, e.g. voice en-GB-Standard-A with en-US), 400 for a malformed language_code, and 429/403 for quota or billing not enabled.","commonSituations":"The key exists but the Text-to-Speech API was never enabled in the GCP console. Copying a voice name from another language's docs while language_code stays en-US. Using a browser-API key restricted to different services.","solutions":["For 403 SERVICE_DISABLED, enable \"Cloud Text-to-Speech API\" in the GCP console for the key's project and enable billing.","For 403 API_KEY_INVALID, fix [providers.tts.google.<alias>].api_key (env grammar ZEROCLAW_providers__tts__google__<alias>__api_key).","For 400 voice/language messages, make voice and language_code agree, e.g. language_code = \"en-US\" with voice = \"en-US-Standard-A\", or switch to the en-GB voice set.","For quota messages, raise the quota or back off; the built-in client already caps calls at 60s."],"exampleFix":"# before — voice language does not match language_code\n[providers.tts.google.main]\napi_key = \"AIza...\"\nlanguage_code = \"en-US\"\nvoice = \"en-GB-Standard-A\"\n\n# after\n[providers.tts.google.main]\napi_key = \"AIza...\"\nlanguage_code = \"en-US\"\nvoice = \"en-US-Standard-A\"","handlingStrategy":"retry","validationCode":"// Pre-flight: voice name must match its language prefix.\nfn voice_matches_language(voice: &str, language_code: &str) -> bool {\n    // voice names look like \"en-US-Standard-A\" / \"en-US-Neural2-F\"\n    let prefix: String = language_code.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '-').collect();\n    voice.starts_with(&prefix)\n}\n\nassert!(voice_matches_language(voice, \"en-US\"));","typeGuard":null,"tryCatchPattern":"match mgr.synthesize(text).await {\n    Ok(audio) => Ok(audio),\n    Err(err) if err.to_string().contains(\"Google TTS API error (403)\") => {\n        Err(err.context(\"enable Cloud Text-to-Speech API / check api_key\"))\n    }\n    Err(err) if err.to_string().contains(\"(429)\") => {\n        tokio::time::sleep(Duration::from_secs(2)).await;\n        mgr.synthesize(text).await\n    }\n    Err(err) => Err(err),\n}","preventionTips":["Enable the Cloud Text-to-Speech API and billing on the key's project before deploying.","Keep voice prefixes consistent with language_code in config review (en-US voice with en-US code).","Restrict the API key to the Text-to-Speech API in the GCP console to avoid cross-service confusion."],"tags":["google-cloud","tts","http","api","auth"],"backgroundTag":"google-cloud-api-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}