fix(server): stream one container per response instead of one per chunk - #899
Open
arvindvenkataramani wants to merge 1 commit into
Open
fix(server): stream one container per response instead of one per chunk#899arvindvenkataramani wants to merge 1 commit into
arvindvenkataramani wants to merge 1 commit into
Conversation
`_emit_audio` called `audio_write` per streamed chunk, and `_encode_ffmpeg` spawns an ffmpeg that exits after each call. A process that exits finalizes its container, so every chunk went out as a complete standalone file with its own header, encoder delay and end padding, concatenated on the wire. Two consequences: the response carries one container header per chunk where a well-formed stream has exactly one, and the accumulated per-segment delay and padding are audible as silence at every seam. Measured on pocket-tts at `streaming_interval: 4`, streamed mp3 ran ~700ms longer than a non-streamed render of the same text across 10 seams. Raw/pcm was unaffected, having no container. Adds `StreamingEncoder`, which holds one libav container and encoder open for the duration of a response and returns only the bytes each chunk produced. `_emit_audio` keeps one per in-flight request, keyed by request id; every path that ends a stream finalizes it, and `run_serial` discards on any exception so an open container cannot leak. The codec and container tables mirror `_encode_ffmpeg` exactly, including FLAC-in-Ogg for ogg/vorbis, so streamed and non-streamed output agree on codec choice. Non-streaming and raw/pcm paths are untouched. Verified across mp3, webm, opus, ogg, vorbis, flac and wav: one container header per response, and decoded duration matching the input to within one mp3 frame. Closes Blaizzy#898
Collaborator
|
@arvindvenkataramani Thanks, I took a quick look and see some issues:
I think the idea is sound but the implementation needs some work. Let me know if you'd rather me handle it or you want to do it yourself. |
Author
|
Please feel free to handle it yourself, you know your codebase better. I honestly even felt bad submitting a Claude Code generated PR. I will note that this patch has been working okay for me so far. I can have Claude look into the PyAV fallback again. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was written by Claude (Anthropic's Claude Code). Human review was limited to listening to the output and confirming the seams are gone; the implementation itself has not been line-by-line reviewed by a person. Please read it accordingly.
Apologies for arriving as a PR rather than a discussion. It appears to work and seemed more useful to share than to sit on — treat it as a starting point rather than something to merge as-is.
Context
Closes #898.
Streaming
/v1/audio/speechencodes each chunk as a complete standalone container file — its own header, its own encoder delay, its own end padding — and concatenates them on the wire. A response therefore carries one container header per chunk where a well-formed stream has exactly one, and the accumulated per-segment delay and padding are audible as silence at every seam.Measured on
mlx-community/pocket-ttsatstreaming_interval: 4: streamed mp3 ran roughly 700ms longer than a non-streamed render of the same text, across ten seams.raw/pcmwas unaffected, having no container.Description
_emit_audiocallsaudio_writeonce per streamed chunk, and_encode_ffmpegspawns an ffmpeg process that exits after each call. A process that exits finalises its container, so each chunk necessarily becomes a complete file.This adds
StreamingEncoder, which holds one libav container and encoder open for the duration of a response and returns only the bytes each chunk produces._emit_audiokeeps one per in-flight request, keyed by request id.Every path that ends a stream finalises the encoder — normal completion, cancellation, client disconnect, the continuous-batch session's done, error and fail branches — and
run_serialdiscards on any exception, so an open container cannot leak.Changes in the codebase
mlx_audio/streaming_encoder.py(new) — incremental encoder. Its codec and container tables mirror_encode_ffmpegexactly, including FLAC-in-Ogg forogg/vorbis, so streamed and non-streamed output agree on codec choice.mlx_audio/server.py— per-request encoder held inTTSExecutionAdapter, guarded by a lock since worker threads reach it concurrently. Non-streaming andraw/pcmpaths are untouched, and the import is guarded so a missing PyAV falls back to current behaviour rather than raising.mlx_audio/tests/test_streaming_encoder.py(new) — 17 tests covering single-container output, duration parity against the input, chunk-size independence, and float scaling.pyproject.toml— addsavto theserverextra.CONTRIBUTIONS.md— the incremental-encoding approach follows Kokoro-FastAPI'sStreamingAudioWriter(Apache-2.0), including its ordering fix for muxers that write their trailing bytes during close.Changes outside the codebase
None.
Additional information
PyAV is a new dependency, scoped to the
serverextra. A long-livedsubprocess.Popenffmpeg would fix the same bug without it, at the cost of a reader thread and process lifecycle handling on every request-termination path. The in-process encoder avoids that, but takes the dependency; the subprocess version is a reasonable alternative if the dependency is unwelcome.Verified across mp3, webm, opus, ogg, vorbis, flac and wav: one container header per response, and decoded duration matching the input to within one mp3 frame.
Checklist