zeroclaw-labs/zeroclaw · error · anyhow::Error
ffmpeg transcode to opus failed: {stderr}
Error message
ffmpeg transcode to opus failed: {stderr} What it means
TtsManager::synthesize_opus calls transcode_to_opus whenever the resolved provider's output_format is not already opus. The function pipes the provider's audio into `ffmpeg -i pipe:0 -f ogg -acodec libopus -b:a 32k -vbr on pipe:1`; if ffmpeg exits non-zero, the drained stderr is bailed with this message. A separate earlier error covers ffmpeg failing to spawn, and a follow-up ensure! covers empty-but-successful output.
Source
Thrown at crates/zeroclaw-channels/src/tts.rs:976
"-vbr",
"on",
"pipe:1",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.context(
"failed to spawn ffmpeg — ensure ffmpeg with libopus support is installed \
(e.g. `sudo dnf install ffmpeg` / `sudo apt install ffmpeg`)",
)?;
let output = write_audio_and_wait_with_output(child, audio, FFMPEG_TRANSCODE_TIMEOUT).await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("ffmpeg transcode to opus failed: {stderr}");
}
anyhow::ensure!(
!output.stdout.is_empty(),
"ffmpeg produced empty output — check that libopus is available"
);
Ok(output.stdout)
}
pub struct TtsManager {
tts_providers: HashMap<String, Box<dyn TtsProvider>>,
voice_by_alias: HashMap<String, String>,
/// Resolved alias for the agent that owns this manager. Empty when
/// the agent has no TTS preference (opt-out).
agent_tts_provider: String,
default_voice: String,
max_text_length: usize,View on GitHub (pinned to 88bb9c8533)
Solutions
- Read the stderr in the message; "Unknown encoder 'libopus'" is the classic case.
- Install an ffmpeg build with libopus (`sudo apt install ffmpeg` on Debian/Ubuntu) and verify with `ffmpeg -encoders | grep libopus`.
- Prefer provider-native opus: set [providers.tts.openai.<alias>].response_format = "opus" so synthesize_opus returns the bytes without ffmpeg.
- If stderr shows a decode error, the input audio from the provider is corrupt — re-check the provider response, not ffmpeg.
Example fix
# before — ffmpeg present but built without libopus # stderr: Unknown encoder 'libopus' # after — install a full build and verify the encoder exists sudo apt install ffmpeg ffmpeg -hide_banner -encoders | grep libopus # must list libopus # alternative — skip the transcode entirely [providers.tts.openai.main] response_format = "opus"
Defensive patterns
Strategy: fallback
Validate before calling
// Startup check: ffmpeg must exist AND expose the libopus encoder.
fn ffmpeg_has_libopus() -> bool {
std::process::Command::new("ffmpeg")
.args(["-hide_banner", "-encoders"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).contains("libopus"))
.unwrap_or(false)
}
assert!(ffmpeg_has_libopus(), "transcode_to_opus requires ffmpeg built with libopus"); Try / catch
// Prefer native opus; if the transcode fails, degrade gracefully to the
// provider's native format instead of dropping the voice note.
match mgr.synthesize_opus(text).await {
Ok(opus) => send_voice_note(&opus).await,
Err(err) if err.to_string().contains("ffmpeg transcode to opus failed") => {
let native = mgr.synthesize(text).await?; // e.g. mp3/wav
send_audio_file(&native).await
}
Err(err) => Err(err),
} Prevention
- Prefer provider-native opus ([providers.tts.openai.<alias>].response_format = "opus") so synthesize_opus skips ffmpeg entirely.
- Base deployment images on ffmpeg builds that include libopus; verify with `ffmpeg -encoders | grep libopus` in CI.
- Treat 'Unknown encoder libopus' in stderr as an environment defect, not a code bug — fix the image, not the call.
When it happens
Trigger: ffmpeg is installed without libopus, so encoding fails with "Unknown encoder 'libopus'" on stderr; the provider bytes are not valid audio ffmpeg can decode (corrupt upstream response); an exotic/old ffmpeg build rejecting the ogg muxer flags.
Common situations: Minimal distro containers (Alpine, slim images) ship ffmpeg without libopus. Debian/Ubuntu main builds include it, self-compiled builds frequently omit --enable-libopus. The cheapest escape is to make the provider emit opus natively, which skips transcode entirely.
Related errors
- TTS returned empty audio
- Failed to speak: {}
- Telegram sendVoice failed: {err}
- OpenAI TTS API error ({}): {}
- ElevenLabs voice ID contains invalid characters: {voice}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/5fc31e57cb2718d8.
Report an issue: GitHub.