{"record":{"id":"128139913f089286","repo":"zeroclaw-labs/zeroclaw","slug":"edge-tts-subprocess-timed-out","errorCode":null,"errorMessage":"Edge TTS subprocess timed out","messagePattern":"Edge TTS subprocess timed out","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/tts.rs","lineNumber":791,"sourceCode":"                    }\n                }\n                Ok(Err(err)) => {\n                    reader.abort();\n                    let _ = reader.await;\n                    // Bound the kill-and-wait by the same absolute provider\n                    // deadline used above, so a child that ignores the kill\n                    // cannot hold `synthesize` past the provider timeout; the\n                    // artifact guard's `Drop` still owns the final reap.\n                    let _ = tokio::time::timeout_at(deadline, child.kill()).await;\n                    let _ = tokio::time::timeout_at(deadline, child.wait()).await;\n                    return Err(err).context(\"Failed to wait for edge-tts subprocess\");\n                }\n                Err(_elapsed) => {\n                    reader.abort();\n                    let _ = reader.await;\n                    let _ = tokio::time::timeout_at(deadline, child.kill()).await;\n                    let _ = tokio::time::timeout_at(deadline, child.wait()).await;\n                    bail!(\"Edge TTS subprocess timed out\");\n                }\n            }\n        };\n\n        if !status.success() {\n            bail!(\"edge-tts failed (exit {}): {}\", status, stderr);\n        }\n\n        let bytes = tokio::fs::read(&output_file)\n            .await\n            .context(\"Failed to read edge-tts output file\")?;\n\n        Ok(bytes)\n    }\n\n    fn supported_voices(&self) -> Vec<String> {\n        // Edge TTS has many voices; return common defaults.\n        [","sourceCodeStart":773,"sourceCodeEnd":809,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/tts.rs#L773-L809","documentation":"EdgeTtsProvider::synthesize spawns the edge-tts CLI and waits on an absolute deadline (TTS_HTTP_TIMEOUT, 60s) shared by the process wait and the stderr drain. If child.wait() does not finish in time, the child is killed and reaped and this error is returned. The EdgeTtsTempArtifact Drop guard still guarantees the temp mp3 is cleaned up after the child exits.","triggerScenarios":"edge-tts hangs instead of exiting: a stalled connection to the Microsoft speech endpoint, DNS black-holing, a very long text whose synthesis exceeds 60s, an edge-tts version waiting on interactive input, or a pinned old version whose endpoint handshake no longer completes.","commonSituations":"Egress-filtered servers silently drop packets to speech.platform.bing.com so the CLI waits forever. Upstream Microsoft DRM/token changes periodically break old edge-tts releases, which then hang mid-request. Synthesizing whole articles in one call pushes past the fixed 60s budget.","solutions":["Reproduce manually: `edge-tts --text \"hello\" --voice en-US-AriaNeural --write-media /tmp/t.mp3` and time it.","Upgrade the CLI (`pip install -U edge-tts`) when manual runs hang — upstream endpoint changes are the most frequent cause.","Check network egress/DNS to the Microsoft speech service from the runtime host.","Chunk long text into shorter per-call syntheses so each subprocess finishes well under 60s.","Retry once; transient network stalls often clear."],"exampleFix":"// before — one very long synthesis can outrun the fixed 60s subprocess timeout\nlet audio = mgr.synthesize(&long_article).await?;\n\n// after — synthesize in chunks, each comfortably inside the budget\nfor chunk in split_unicode_chars(&long_article, 800) {\n    let part = mgr.synthesize(&chunk).await?;\n    send_audio(&part).await?;\n}","handlingStrategy":"retry","validationCode":"// Pre-flight smoke run: the CLI must answer quickly, catching broken\n// installs and blocked egress before a real synthesis burns 60s.\nasync fn edge_tts_healthy() -> bool {\n    tokio::process::Command::new(\"edge-tts\")\n        .arg(\"--list-voices\")\n        .output()\n        .await\n        .map(|o| o.status.success())\n        .unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"const MAX_ATTEMPTS: u32 = 2;\nfor attempt in 1..=MAX_ATTEMPTS {\n    match mgr.synthesize(text).await {\n        Ok(audio) => return Ok(audio),\n        Err(err) if err.to_string().contains(\"Edge TTS subprocess timed out\") && attempt < MAX_ATTEMPTS => {\n            tokio::time::sleep(Duration::from_secs(2)).await; // transient stall\n        }\n        Err(err) => return Err(err),\n    }\n}\nunreachable!()","preventionTips":["Keep per-call text short (chunk long input) so the subprocess finishes far under the fixed 60s budget.","Pin and periodically upgrade edge-tts; endpoint changes make old versions hang.","Verify egress to the Microsoft speech endpoint from the runtime host (containers with deny-by-default egress will black-hole it)."],"tags":["edge-tts","tts","subprocess","timeout"],"backgroundTag":"process-timeout","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}