Hmbown/CodeWhale · error

unsupported voice clone sample extension '{other}'. Use .mp3

Error message

unsupported voice clone sample extension '{other}'. Use .mp3 or .wav.

What it means

voice_clone_data_uri_from_bytes maps the sample's file extension to a MIME type and supports only .mp3 (audio/mpeg) and .wav (audio/wav). Any other extension, including no extension at all (the extension defaults to empty), is rejected because the provider needs a concrete audio MIME type in the data URI.

Source

Thrown at crates/tui/src/tools/speech.rs:421

fn voice_clone_data_uri_from_bytes(path: &Path, bytes: &[u8]) -> anyhow::Result<String> {
    let base64_audio = general_purpose::STANDARD.encode(bytes);
    if base64_audio.len() > VOICE_CLONE_BASE64_MAX_BYTES {
        anyhow::bail!(
            "voice clone sample is too large after base64 encoding ({} bytes > 10 MB)",
            base64_audio.len()
        );
    }

    let extension = path
        .extension()
        .and_then(|value| value.to_str())
        .unwrap_or_default()
        .to_ascii_lowercase();
    let mime = match extension.as_str() {
        "mp3" => "audio/mpeg",
        "wav" => "audio/wav",
        other => {
            anyhow::bail!("unsupported voice clone sample extension '{other}'. Use .mp3 or .wav.");
        }
    };

    Ok(format!("data:{mime};base64,{base64_audio}"))
}

pub(crate) fn describe_speech_voice(voice: &str) -> String {
    if voice.starts_with("data:") {
        "embedded voice clone sample".to_string()
    } else {
        voice.to_string()
    }
}

fn openai_compatible_base_url(base_url: &str) -> String {
    let trimmed = base_url.trim_end_matches('/');
    if trimmed.ends_with("/v1") || trimmed.ends_with("/beta") {
        trimmed.to_string()

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Convert the sample to mp3 or wav before uploading: ffmpeg -i in.m4a -codec:a libmp3lame -b:a 128k sample.mp3.
  2. If the content is already mp3/wav with a wrong suffix, rename the file so the extension matches the actual container.
  3. Ensure tempfile-based flows preserve a .mp3/.wav suffix instead of a random name.

Example fix

# before
ffmpeg -i recording.m4a voice.wav   # then pass voice.wav? no - pass original .m4a -> rejected

# after
ffmpeg -i recording.m4a -codec:a libmp3lame -b:a 128k voice.mp3
# pass voice.mp3 -> accepted
Defensive patterns

Strategy: type-guard

Validate before calling

fn sample_extension_supported(path: &Path) -> bool {
    matches!(
        path.extension().and_then(|e| e.to_str()).map(|e| e.to_ascii_lowercase()).as_deref(),
        Some("mp3" | "wav")
    )
}

Type guard

fn is_supported_voice_sample(path: &Path) -> bool {
    matches!(
        path.extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).as_deref(),
        Some("mp3" | "wav")
    )
}

Prevention

When it happens

Trigger: Passing .m4a, .ogg, .flac, .opus, .webm, uppercase variants are fine (lowercased) but .MP3 works while .mp4 fails; passing a file with no extension; passing a temporary file whose suffix was stripped.

Common situations: iPhone/Android recordings (.m4a, .amr); Audacity exports as .flac or .ogg; voice memos renamed without extension; tempfiles created without suffix.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/9b9c16dd85ebc575. Report an issue: GitHub.