Repository navigation
Streaming overlap: drain listener tail, fast duration estimates, seam-safe chunk scheduling - #5
Open
WinstonLiyt wants to merge 4 commits into
Open
WinstonLiyt wants to merge 4 commits into
WinstonLiyt wants to merge 4 commits into
Conversation
The streaming TTS pipeline calls the FastSpeech2 duration estimator for every refinement candidate, and each call constructed a new FastSpeechWrapper, i.e. it reloaded the checkpoint and the HiFi-GAN vocoder from disk, re-read the 206k-line lexicon, rebuilt the G2p model, and for long inputs recomputed the sinusoid position table in pure Python (~1M calls). Median cost per estimate was 4.9 s (p90 9 s) and each process held ~20 GB of GPU memory. - Add get_shared_wrapper(): a lazily created, process-wide instance guarded by a lock; inference is serialized on the same lock. - Skip the vocoder by default: audio length equals mel_len * hop_length, so the duration is taken from the encoder + variance adaptor output and the decoder, postnet and vocoder are never run. Results are bit-identical to the old path (verified on 7 paragraphs, max abs diff 0.0 s). - Cache the lexicon and G2p instance across calls. - Replace get_sinusoid_encoding_table with a vectorized numpy version. - LengthEstimator now uses the shared instance as well. Per-estimate latency drops from 4.9 s to ~0.01 s; GPU memory per process from ~20 GB to ~0.14 GB.
…scheduling Measured on four full debates (log ids 130-133 before, 135-138 after; same motions, same models): playback starvation events 5 -> 0, timed-out refinements 11% -> 0%, TTS pipeline wall clock per speech 67.9 s -> 21.6 s, first-chunk latency median 60 s -> 41 s. Audio totals stay within +-4 s of the 240 s budget. Rewrite latency - Call gpt-5-mini with reasoning_effort="minimal" in _revise_to_n_words. A length-constrained rewrite needs no chain of thought; the default effort spent 700-1600 reasoning tokens per call (10.8 s vs 1.8 s, same word-count accuracy). Chunking - Add split_into_chunks(): paragraphs remain the unit, but a text with fewer than MIN_STREAM_CHUNKS paragraphs is re-chunked by packing whole sentences to ~TARGET_CHUNK_S of audio, and a paragraph longer than MAX_CHUNK_CHARS is split at sentence boundaries into balanced pieces. Two of eight closings previously came back as a single unbroken paragraph and became one ~100 s chunk with no streaming, no refinement and up to 40 s of unused budget. Scheduling at the chunk boundary - Stop refining REFINE_DEADLINE_MARGIN_S (2 s) before the previous chunk finishes playing; everything after selection (decode, file write, bridge copy, playback stability poll) used to land past the deadline and produced audible gaps of up to 5.7 s. - Do not join refine workers after selection; a worker blocked in an LLM call stalled the main pipeline by the join timeout (2 s per worker). - Only re-synthesize with adjusted speed when at least SPEED_ADJUST_MIN_SLACK_S of playback slack remains, and log when it happens. - Write chunk_NNN.mp3 via a temp file + rename so the copy bridge never sees a partial file. Seam quality - Normalize head/tail silence of every chunk (_normalize_seam_silence): keep 60 ms of leading silence, exactly 250 ms of trailing silence, 10 ms fades. tts-1 output carried 100-1200 ms of trailing silence, so consecutive chunks paused anywhere between 0.2 s and 1.24 s; seam silence p90 goes from 720 ms to 390 ms (max 1240 -> 425 ms). Budget accounting uses the trimmed length. Reporting - chunk 0 is still never refined, but its FastSpeech estimate is now computed (cheap after the estimator change) and in_range reflects the actual audio instead of a hard-coded True, so chunk_profile.csv no longer overstates the hit rate. Ablation - Optional online calibration of the duration estimate (TTS_STREAM_CALIB=1), off by default: an offline replay over 206 chunks showed the residual is text-dependent noise (lag-1 autocorrelation -0.03, mean bias -1%), and an EMA correction slightly increases error.
…SR, remove file races Listener (streaming/env.py) - Drain at turn end: transcribe whatever audio remains past the last emitted window (>= 1 s) and push it into the debate tree with a forced flush. The listener processes audio in min_audio_seconds (15 s) windows and previously discarded the remainder when the speech ended; because overlap turns are marked tree_via_streaming and skip the full-text analysis, the last 8-14 s of every speech (measured on 24 speeches) never reached the opponent's tree. - Cache the decoded continuous_audio.mp3 keyed by (mtime, size). The file is rewritten once per chunk but was polled and fully decoded every second (~0.5 s of CPU for a 4-minute file). - Give the Whisper request a 30 s timeout with up to 3 attempts. One request in run 138 never returned, the listener thread froze at 135 s of a 236 s speech, and the main thread waited the full 300 s join timeout. Playback simulator (streaming/overlap.py) - Export continuous_audio.mp3 to a temp file and os.replace() it, so the listener never opens a half-written file (46 ffmpeg decode warnings over four runs before, 0 after). - Charge the decode + export time of each chunk against its simulated playback sleep instead of inserting it as silence between chunks. The cursor still advances by the chunk's real duration, so the listener is unaffected; the per-seam timeline overhead drops from 1.07 s to 0.17 s.
Across 48 speeches, tts-1 (voice "echo") speaks at 2.46 words/s, so the prompt's "around 520 words" for a 4-minute statement corresponds to 211 s and "around 260 words" for closing to 106 s. Original drafts came out at a median 94% of the budget (closings ~80%), and the streaming refinement then rewrote most chunks upward (median 1.16x the original length, 21 of 70 rewritten chunks expanded by more than 20%), i.e. it was generating filler to fill time. With 580 / 580 / 290 words the drafts land at 96% of budget and the rewrite ratio drops to 1.03x.
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.
Summary
Fixes and speed-ups for the streaming overlap pipeline (
python -m streaming.overlap: streaming TTS on the speaker side, streaming ASR + tree updates on the listener side).Opening/rebuttal audio totals stay within 232-238 s of the 240 s budget in both versions; overrun is unchanged (2.7 s -> 2.5 s median).
What was wrong
tree_via_streaming, the full-text analysis was skipped too, so the closing sentences of every speech never reached the opponent's tree.in_rangewas hard-codedTrue.continuous_audio.mp3was rewritten in place while the listener read it; a Whisper request with no timeout once hung the listener for the rest of a turn; the playback simulator inserted ~1 s of bookkeeping between chunks.echo, so refinement was mostly expanding text (median 1.16x); with 580 / 290 it is 1.03x.Changes by file
src/utils/fs_wrapper.py,src/utils/time_estimator.py: shared FastSpeech2 instance, duration-only forward (no decoder/vocoder), cached lexicon/G2p, vectorized position table. Bit-identical durations.src/tts_streaming.py:reasoning_effort="minimal"for rewrites; sentence-packing chunker; 2 s deadline margin; non-blocking worker cleanup; gated speed adjustment; seam silence normalization; atomic chunk writes; honest chunk-0in_range; optional calibration ablation (TTS_STREAM_CALIB=1, off by default, documented negative result).src/streaming/env.py: final drain of the remaining audio at turn end; decode cache keyed by mtime/size; Whisper call with 30 s timeout and retries.src/streaming/overlap.py: atomic write ofcontinuous_audio.mp3; bookkeeping time charged against playback sleep.src/utils/prompts/others.py: word targets 520/520/260 -> 580/580/290.Not changed, worth knowing
TTS-Start, not the TTS pipeline.How to reproduce
Per-chunk timing lands in
log_files/<id>_outputs/*_chunks/chunk_profile.csv; playback and listener events inlog_files/<id>.log.