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

  1. Retry the request once — transient stream breaks are the most common cause
  2. Shorten the input text and retry; long inputs stretch the transfer window
  3. Check disk space (df -h) to rule out ENOSPC during the write
  4. Verify provider health directly (curl the TTS endpoint with the same payload) and check quota/billing
  5. 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

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


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/12f281d62fc01890. Report an issue: GitHub.