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

  1. os.Stat the file immediately before calling Transcribe and treat missing files as a pipeline-ordering bug.
  2. Fix permissions or path construction; prefer absolute paths.
  3. 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

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


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