{"record":{"id":"5fc31e57cb2718d8","repo":"zeroclaw-labs/zeroclaw","slug":"ffmpeg-transcode-to-opus-failed-stderr","errorCode":null,"errorMessage":"ffmpeg transcode to opus failed: {stderr}","messagePattern":"ffmpeg transcode to opus failed: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/tts.rs","lineNumber":976,"sourceCode":"            \"-vbr\",\n            \"on\",\n            \"pipe:1\",\n        ])\n        .stdin(Stdio::piped())\n        .stdout(Stdio::piped())\n        .stderr(Stdio::piped())\n        .kill_on_drop(true)\n        .spawn()\n        .context(\n            \"failed to spawn ffmpeg — ensure ffmpeg with libopus support is installed \\\n             (e.g. `sudo dnf install ffmpeg` / `sudo apt install ffmpeg`)\",\n        )?;\n\n    let output = write_audio_and_wait_with_output(child, audio, FFMPEG_TRANSCODE_TIMEOUT).await?;\n\n    if !output.status.success() {\n        let stderr = String::from_utf8_lossy(&output.stderr);\n        bail!(\"ffmpeg transcode to opus failed: {stderr}\");\n    }\n\n    anyhow::ensure!(\n        !output.stdout.is_empty(),\n        \"ffmpeg produced empty output — check that libopus is available\"\n    );\n\n    Ok(output.stdout)\n}\n\npub struct TtsManager {\n    tts_providers: HashMap<String, Box<dyn TtsProvider>>,\n    voice_by_alias: HashMap<String, String>,\n    /// Resolved alias for the agent that owns this manager. Empty when\n    /// the agent has no TTS preference (opt-out).\n    agent_tts_provider: String,\n    default_voice: String,\n    max_text_length: usize,","sourceCodeStart":958,"sourceCodeEnd":994,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/tts.rs#L958-L994","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before — ffmpeg present but built without libopus\n# stderr: Unknown encoder 'libopus'\n\n# after — install a full build and verify the encoder exists\nsudo apt install ffmpeg\nffmpeg -hide_banner -encoders | grep libopus   # must list libopus\n\n# alternative — skip the transcode entirely\n[providers.tts.openai.main]\nresponse_format = \"opus\"","handlingStrategy":"fallback","validationCode":"// Startup check: ffmpeg must exist AND expose the libopus encoder.\nfn ffmpeg_has_libopus() -> bool {\n    std::process::Command::new(\"ffmpeg\")\n        .args([\"-hide_banner\", \"-encoders\"])\n        .output()\n        .map(|o| String::from_utf8_lossy(&o.stdout).contains(\"libopus\"))\n        .unwrap_or(false)\n}\n\nassert!(ffmpeg_has_libopus(), \"transcode_to_opus requires ffmpeg built with libopus\");","typeGuard":null,"tryCatchPattern":"// Prefer native opus; if the transcode fails, degrade gracefully to the\n// provider's native format instead of dropping the voice note.\nmatch mgr.synthesize_opus(text).await {\n    Ok(opus) => send_voice_note(&opus).await,\n    Err(err) if err.to_string().contains(\"ffmpeg transcode to opus failed\") => {\n        let native = mgr.synthesize(text).await?; // e.g. mp3/wav\n        send_audio_file(&native).await\n    }\n    Err(err) => Err(err),\n}","preventionTips":["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."],"tags":["ffmpeg","tts","opus","transcode","libopus"],"backgroundTag":"ffmpeg-conversion-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}