{"record":{"id":"890dbc2c93742cdc","repo":"sipeed/picoclaw","slug":"tts-synthesize-failed-w","errorCode":null,"errorMessage":"tts synthesize failed: %w","messagePattern":"tts synthesize failed: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/audio/tts/tts.go","lineNumber":113,"sourceCode":"\tchannel string,\n\tchatID string,\n) (string, error) {\n\tif provider == nil {\n\t\treturn \"\", fmt.Errorf(\"tts provider is not configured\")\n\t}\n\tif store == nil {\n\t\treturn \"\", fmt.Errorf(\"media store not configured\")\n\t}\n\tif channel == \"\" || chatID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no target channel/chat available\")\n\t}\n\tif strings.TrimSpace(text) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"text is required\")\n\t}\n\n\tstream, err := provider.Synthesize(ctx, text)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"tts synthesize failed: %w\", err)\n\t}\n\tdefer stream.Close()\n\n\terr = os.MkdirAll(media.TempDir(), 0o700)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create media temp dir: %w\", err)\n\t}\n\n\tfileExt := \".ogg\"\n\tcontentType := \"audio/ogg\"\n\tif provider.Name() == \"mimo-tts\" {\n\t\tfileExt = \".mp3\"\n\t\tcontentType = \"audio/mpeg\"\n\t}\n\tif metaProvider, ok := stream.(ttsAudioMetaProvider); ok {\n\t\tif ext, ct := metaProvider.AudioFileMeta(); ext != \"\" && ct != \"\" {\n\t\t\tfileExt = ext\n\t\t\tcontentType = ct","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/audio/tts/tts.go#L95-L131","documentation":"Returned by tts.SynthesizeAndStore wrapping any failure from provider.Synthesize — the top-level TTS error. The chain carries the concrete cause: mimo/openai transport errors ('failed to send request'), provider rejections ('API error (status ...)'), JSON decode failures, or missing audio data. Unwrap the chain to see which layer failed.","triggerScenarios":"Any provider-level failure: network outage to api.openai.com or api.xiaomimimo.com, 401/429 from the provider, schema or base64 encoding drift, a model that returns no audio.","commonSituations":"Production voice pipelines; provider incidents; expired API keys; rate limits during bulk synthesis.","solutions":["Unwrap the error chain (errors.As / errors.Is) to classify the root cause before acting","Fix per root cause: 401 → key config, 429/5xx/network → backoff and retry, decode → provider drift","Configure a fallback TTSProvider so voice output survives one provider failing","Retry idempotent synthesis with backoff for transient causes"],"exampleFix":"// before\nref, err := tts.SynthesizeAndStore(ctx, p, s, text, name, ch, id)\n// after\nref, err := tts.SynthesizeAndStore(ctx, p, s, text, name, ch, id)\nif err != nil && strings.HasPrefix(err.Error(), `tts synthesize failed`) && fallback != nil {\n    ref, err = tts.SynthesizeAndStore(ctx, fallback, s, text, name, ch, id)\n}","handlingStrategy":"fallback","validationCode":null,"typeGuard":"func isTTSWrap(err error) bool {\n    return err != nil && strings.HasPrefix(err.Error(), `tts synthesize failed`)\n}","tryCatchPattern":"if err != nil && isTTSWrap(err) {\n    // unwrap: errors.Unwrap(err) yields the provider-level cause (transport / API status / decode)\n    if fallback != nil {\n        ref, err = tts.SynthesizeAndStore(ctx, fallback, store, text, name, channel, chatID)\n    }\n}","preventionTips":["Unwrap and classify the cause before choosing retry vs fallback","Keep a second TTSProvider configured for failover","Retry only transient causes (network, 429, 5xx) with backoff","Alert on repeated synthesis failures — usually key expiry or provider incidents"],"tags":["tts","wrapper","api","network"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}