sipeed/picoclaw · error
failed to write tts audio: %w
Error message
failed to write tts audio: %w
What it means
io.Copy(file, stream) failed at pkg/audio/tts/tts.go:150 while copying the provider's synthesized audio (OpenAI-protocol or mimo TTS HTTP body) into the temp file. Failure is either upstream (connection dropped mid-body, provider closed early, proxy cut the stream) or local (disk write error). The temp file is closed and removed by the removeTemp defer, so no partial file leaks.
Source
Thrown at pkg/audio/tts/tts.go:150
}
}
file, err := os.CreateTemp(media.TempDir(), "tts-*"+fileExt)
if err != nil {
return "", fmt.Errorf("failed to create temp file: %w", err)
}
removeTemp := true
defer func() {
if removeTemp {
_ = os.Remove(file.Name())
}
}()
_, err = io.Copy(file, stream)
if err != nil {
_ = file.Close()
return "", fmt.Errorf("failed to write tts audio: %w", err)
}
err = file.Close()
if err != nil {
return "", fmt.Errorf("failed to close tts audio file: %w", err)
}
filename = strings.TrimSpace(filename)
if filename == "" {
filename = fmt.Sprintf("tts-%d%s", time.Now().Unix(), fileExt)
}
ext := strings.ToLower(filepath.Ext(filename))
if ext == "" {
filename += fileExt
} else if ext != fileExt {
filename = strings.TrimSuffix(filename, filepath.Ext(filename)) + fileExt
}View on GitHub (pinned to 49183d7e8d)
Solutions
- Retry the request once — transient stream breaks are the most common cause
- Shorten the input text and retry; long inputs stretch the transfer window
- Check disk space (df -h) to rule out ENOSPC during the write
- Verify provider health directly (curl the TTS endpoint with the same payload) and check quota/billing
- If behind a proxy, raise its response/stream timeout or bypass it for the TTS host
Example fix
// before: single-shot synthesis of a huge text blob
ref, err := tts.SynthesizeToStore(ctx, veryLongText, ...)
// after: chunk long input and retry transient stream failures
var ref string
err := retry(2, func() error {
r, e := tts.SynthesizeToStore(ctx, truncate(text, 4000), ...)
if e != nil { return e }
ref = r
return nil
}) Defensive patterns
Strategy: retry
Validate before calling
func ttsPayloadSafe(text string) error {
if strings.TrimSpace(text) == "" {
return fmt.Errorf("text is required")
}
if len(text) > 4000 { // provider-dependent cap
return fmt.Errorf("text too long (%d bytes), chunk it", len(text))
}
return nil
} Try / catch
var ref string
err := retryN(2, 750*time.Millisecond, func() error {
r, e := tts.SynthesizeToStore(ctx, text, ch, chatID, filename)
if e != nil && strings.Contains(e.Error(), "failed to write tts audio") {
return e // transient stream break: retry
}
if e != nil { return retryStop{e} } // non-stream errors: do not retry
ref = r
return nil
}) Prevention
- Chunk long TTS inputs instead of one giant synthesis
- Keep provider traffic off aggressively short-timeout proxies
- Retry stream-write failures once or twice before surfacing them
- Watch disk space so ENOSPC never masquerades as a network error
When it happens
Trigger: Synthesizing long text where the provider or an intermediary proxy times out mid-body; provider rate-limiting that truncates the response; connection reset between Synthesize() returning and the copy finishing; ENOSPC while writing the audio bytes.
Common situations: Flaky egress NAT dropping idle-ish streaming connections; corporate proxy with short body timeouts; very large TTS payloads on slow links; disk filling up from previous media files; provider-side 5xx that manifests as a broken chunked body.
Related errors
- failed to read response: %w
- after %d retries: %w
- LLM call failed after retries: %w
- transcription request failed: %w
- failed to send request: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/12f281d62fc01890.
Report an issue: GitHub.