sipeed/picoclaw · error
failed to open audio file %s: %w
Error message
failed to open audio file %s: %w
What it means
Thrown when os.Open(audioFilePath) fails at the start of WhisperTranscriber.Transcribe — the file-based counterpart of the bytes-based path. Wraps a *os.PathError with Op 'open'; nothing else (model config, network, API key) is consulted yet, so the whisper endpoint/provider is irrelevant to this error.
Source
Thrown at pkg/audio/asr/whisper_transcriber.go:142
if err = writer.Close(); err != nil {
logger.ErrorCF("voice", "Failed to close whisper multipart writer", map[string]any{"error": err})
return nil, fmt.Errorf("failed to close multipart writer: %w", err)
}
return t.doRequest(ctx, &requestBody, writer.FormDataContentType(), int64(len(data)))
}
func (t *WhisperTranscriber) Transcribe(ctx context.Context, audioFilePath string) (*TranscriptionResponse, error) {
logger.InfoCF("voice", "Starting whisper transcription", map[string]any{
"audio_file": audioFilePath,
"model": t.modelID,
"provider": t.providerName,
})
audioFile, err := os.Open(audioFilePath)
if err != nil {
return nil, fmt.Errorf("failed to open audio file %s: %w", audioFilePath, err)
}
defer audioFile.Close()
fileInfo, err := audioFile.Stat()
if err != nil {
return nil, fmt.Errorf("failed to stat audio file %s: %w", audioFilePath, err)
}
var requestBody bytes.Buffer
writer := multipart.NewWriter(&requestBody)
part, err := writer.CreateFormFile("file", filepath.Base(audioFilePath))
if err != nil {
return nil, fmt.Errorf("failed to create form file: %w", err)
}
if _, copyErr := io.Copy(part, audioFile); copyErr != nil {
return nil, fmt.Errorf("failed to copy audio data: %w", copyErr)View on GitHub (pinned to 49183d7e8d)
Solutions
- os.Stat the file immediately before calling Transcribe and treat missing files as a pipeline-ordering bug.
- Fix permissions or path construction; prefer absolute paths.
- Ensure cleanup callbacks do not race the transcription (delete after Transcribe returns).
Example fix
// before
resp, err := whisper.Transcribe(ctx, path)
// after
if _, statErr := os.Stat(path); statErr != nil {
return nil, fmt.Errorf("audio not available for whisper: %w", statErr)
}
resp, err := whisper.Transcribe(ctx, path) Defensive patterns
Strategy: validation
Validate before calling
func ensureAudioFile(path string) (*os.FileInfo, error) {
fi, err := os.Stat(path)
if err != nil { return nil, fmt.Errorf("audio file not ready: %w", err) }
if fi.IsDir() { return nil, fmt.Errorf("audio path is a directory: %s", path) }
if fi.Size() == 0 { return nil, fmt.Errorf("audio file empty: %s", path) }
return &fi, nil
} Type guard
func isMissingFile(err error) bool { return errors.Is(err, fs.ErrNotExist) } Try / catch
if _, err := whisper.Transcribe(ctx, path); err != nil {
if isMissingFile(err) {
// ordering bug in the voice pipeline: file deleted/renamed before transcription
return nil, fmt.Errorf("audio lifecycle bug, %s missing: %w", path, err)
}
return nil, err
} Prevention
- Schedule deletion after transcription completes, not on cancel.
- Pre-stat and size-check every file before Transcribe.
- Use absolute paths under a directory the service owns.
When it happens
Trigger: ENOENT (file not yet written or already deleted), EACCES (read permission), EISDIR (directory passed), ELOOP (symlink loop from broken temp-dir cleanup).
Common situations: Voice pipeline deletes the temp file on context cancellation while the transcription goroutine is starting; recordings directory rotated between capture and transcription; path with a trailing newline from config.
Related errors
- failed to read audio file: %w
- failed to open audio file: %w
- failed to stat audio file %s: %w
- transcription request failed: %w
- failed to get file info: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/80a7fce0ff33b0e1.
Report an issue: GitHub.