Hmbown/CodeWhale · error · anyhow::Error

Model ' ' requires cloned voice data. Pass --clone-voice…

Error message

Model '{model}' requires cloned voice data. Pass --clone-voice <mp3|wav> or --voice <data-uri>.

What it means

Speech synthesis via a *voiceclone* model requires reference audio of the voice to clone. The client validates this before sending any request: if the resolved model name contains 'voiceclone' and no voice payload (`request.voice`, e.g. a data URI) was supplied, it fails fast with this error instead of producing a confusing provider-side 4xx. It is a local precondition check in `crates/tui/src/client.rs` around line 3360.

Solutions

  1. Pass `--clone-voice <mp3|wav>` so the CLI encodes the reference audio as a data URI into `voice`.
  2. Or pass `--voice <data-uri>` with an explicit data: URI payload for the reference audio.
  3. Verify the request struct's `voice` field is populated (non-empty after trim) before calling synthesis.
  4. If cloning is not intended, switch to a non-voiceclone model.

Example fix

// before
codewhale speak --model elevenlabs_voiceclone_v1 --text "hello"
// after
codewhale speak --model elevenlabs_voiceclone_v1 --text "hello" --clone-voice ./my-voice.mp3
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_voice_present(model: &str, voice: Option<&str>) -> Result<(), String> {
    if model.to_ascii_lowercase().contains("voiceclone")
        && voice.map(str::trim).filter(|v| !v.is_empty()).is_none()
    {
        return Err(format!("Model '{model}' requires --clone-voice <mp3|wav> or --voice <data-uri>."));
    }
    Ok(())
}

Type guard

fn has_voice(voice: Option<&str>) -> bool {
    voice.map(str::trim).filter(|v| !v.is_empty()).is_some()
}

Prevention

When it happens

Trigger: Calling speech synthesis with a model whose lowercased name contains "voiceclone" while `request.voice` is None, empty, or whitespace-only. This happens when the user omits `--clone-voice <mp3|wav>` / `--voice <data-uri>` on the CLI, or passes a path that failed to load into a data URI upstream.

Common situations: Developers switching a script from a standard TTS model to a clone model and forgetting the reference audio; passing a plain file path instead of a data URI so the voice field is treated as absent; environment configs where the clone audio is resolved conditionally and silently skipped.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/8246a879ef44373e. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/client.rs:3360

        let instruction = request
            .instruction
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty());
        let voice = request
            .voice
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(str::to_string);

        if model_lower.contains("voicedesign") && instruction.is_none() {
            anyhow::bail!(
                "Model '{model}' requires a voice design prompt. Pass --voice-prompt or --instruction."
            );
        }
        if model_lower.contains("voiceclone") && voice.is_none() {
            anyhow::bail!(
                "Model '{model}' requires cloned voice data. Pass --clone-voice <mp3|wav> or --voice <data-uri>."
            );
        }

        let mut audio = json!({
            "format": audio_format.clone(),
        });
        if let Some(voice) = voice.as_deref() {
            audio["voice"] = json!(voice);
        }

        let body = build_speech_synthesis_body(&model, &text, instruction, audio);

        let url = api_url(&self.base_url, "chat/completions");
        let response = self.send_json_with_retry(&url, &body).await?;
        let status = response.status();
        if !status.is_success() {
            let raw_error_text = bounded_error_text(response, ERROR_BODY_MAX_BYTES).await;

View on GitHub (pinned to 73e0f67d83)