When the WebSocket closes before turn.end arrives (a connection dropped mid-synthesis), onclose ends every audio stream with push(null) — the exact same thing the turn.end handler does on a normal finish (src/MsEdgeTTS.ts, the onclose loop around L165–170). So a truncated synthesis is indistinguishable from a complete one: toStream() emits a clean end with a partial buffer, toFile() writes a short file, and there's no error anywhere to catch and retry on.
Where I keep running into it: routing Edge through a rotating/residential proxy (to spread outbound connections across IPs and avoid the per-IP throttle when synthesizing a lot). Those connections drop mid-stream fairly often. A representative run over one — a ~630-word input — dropped on roughly 1 in 6 attempts: the socket closed after ~360/630 words with no turn.end, and the stream just ended cleanly with a fraction of the audio. Direct connections almost never do this, which is why it's easy to miss until you're doing volume.
I think the fix is to remember whether turn.end was seen for a request and, on a close without it, error that stream instead of ending it as if it were complete. Happy to send a PR — it also needs a small tweak on the toFile() side, since .pipe() doesn't forward the source-stream error to the file writer.
When the WebSocket closes before
turn.endarrives (a connection dropped mid-synthesis),oncloseends every audio stream withpush(null)— the exact same thing theturn.endhandler does on a normal finish (src/MsEdgeTTS.ts, theoncloseloop around L165–170). So a truncated synthesis is indistinguishable from a complete one:toStream()emits a cleanendwith a partial buffer,toFile()writes a short file, and there's no error anywhere to catch and retry on.Where I keep running into it: routing Edge through a rotating/residential proxy (to spread outbound connections across IPs and avoid the per-IP throttle when synthesizing a lot). Those connections drop mid-stream fairly often. A representative run over one — a ~630-word input — dropped on roughly 1 in 6 attempts: the socket closed after ~360/630 words with no
turn.end, and the stream just ended cleanly with a fraction of the audio. Direct connections almost never do this, which is why it's easy to miss until you're doing volume.I think the fix is to remember whether
turn.endwas seen for a request and, on a close without it, error that stream instead of ending it as if it were complete. Happy to send a PR — it also needs a small tweak on thetoFile()side, since.pipe()doesn't forward the source-stream error to the file writer.