From 7bf2c43a2e8d825682a1635f32970b06345d3983 Mon Sep 17 00:00:00 2001 From: Kedara Studios Date: Mon, 2 Feb 2026 20:27:15 +0100 Subject: [PATCH] feat: add two-phase streaming for reduced first-chunk latency - Phase 1: aggressive emit/decode settings for fast first chunk - Phase 2: stable settings with optimized decode for quality - New parameters: first_chunk_emit_every, first_chunk_decode_window, first_chunk_frames - Phase 1 disables torch.compile to allow flexible window sizes --- README.md | 125 ++++++++++----------- qwen_tts/README.md | 87 ++++++++++++++ qwen_tts/core/models/modeling_qwen3_tts.py | 31 ++++- qwen_tts/inference/qwen3_tts_model.py | 10 ++ 4 files changed, 183 insertions(+), 70 deletions(-) create mode 100644 qwen_tts/README.md diff --git a/README.md b/README.md index 9b4b115..3505d34 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,87 @@ # Qwen3-TTS Streaming -Streaming inference implementation for [Qwen3-TTS](https://github.com/QwenLM/Qwen3-TTS) that the official repo doesn't provide. +Real-time streaming audio generation for [Qwen3-TTS](https://github.com/QwenLM/Qwen3-TTS). -The official team mentions "Extreme Low-Latency Streaming Generation" in their paper and marketing, but the actual streaming code was never released - they point users to vLLM-Omni, which still doesn't support online serving. +## Features -This fork adds real streaming generation directly to the `qwen-tts` package. - -## What's Added - -- `stream_generate_pcm()` - real-time PCM audio streaming +From [dffdeeq/Qwen3-TTS-streaming](https://github.com/dffdeeq/Qwen3-TTS-streaming): - `stream_generate_voice_clone()` - streaming with voice cloning +- `stream_generate_pcm()` - real-time PCM audio streaming +- `torch.compile` + CUDA graphs optimization +- Crossfade overlap for seamless chunk transitions -## Benchmark (RTX 5090) +Added in this fork: +- **Two-phase streaming** - faster first-chunk latency -### Non-streaming (full inference) +## Two-Phase Streaming -image +Standard streaming with Qwen's TTS library waits for `emit_every_frames` (e.g., 12) before emitting the first audio. Two-phase uses aggressive settings for the first chunk to improve latency, then switches to stable settings. +``` +┌─────────────────────────────────────────────────────────────────┐ +│ PHASE 1 (First N frames) │ PHASE 2 (Rest of audio) │ +│ - emit_every = 5 (fast) │ - emit_every = 12 (stable) │ +│ - decode_window = 48 │ - decode_window = 80 │ +│ - optimized = OFF │ - optimized = ON │ +│ → FAST first chunk │ → QUALITY for rest │ +└─────────────────────────────────────────────────────────────────┘ +``` -### Streaming +### Benchmarks -image +| Test | Method | emit | 1st Chunk | 1st Spdup | Total | Tot Spdup | RTF | +|------|--------|------|-----------|-----------|-------|-----------|-----| +| 2 | Baseline (no opt) | 12 | 570ms | 1.00x | 3.16s | 1.00x | 0.56 | +| 3 | Optimized | 12 | 389ms | 1.47x | 2.37s | 1.34x | 0.37 | +| 4 | Optimized_2 (stable) | 12 | 382ms | 1.49x | 2.27s | 1.39x | 0.36 | +| 5 | **Two-phase (5→12)** | 5→12 | **208ms** | **2.75x** | 2.58s | 1.23x | 0.39 | +User hears audio **362ms earlier** vs baseline, **174ms earlier** vs only optimized. + +**First-chunk latency improvement:** +- vs Baseline: **2.75x faster** (570ms → 208ms, saves 362ms) +- vs Optimized: **1.87x faster** (389ms → 208ms, saves 181ms) +- vs Optimized_2: **1.84x faster** (382ms → 208ms, saves 174ms) ## Usage -See examples/ -- [test_streaming_optimized.py](https://github.com/dffdeeq/Qwen3-TTS-streaming/blob/main/examples/test_streaming_optimized.py) -- [test_optimized_no_streaming.py](https://github.com/dffdeeq/Qwen3-TTS-streaming/blob/main/examples/test_optimized_no_streaming.py) - -## Installation (python 3.12) - -> Note: torch versions differ between Linux/Windows due to available flash_attn prebuilt wheels. - -### 1. Install SOX - -**Linux:** -```bash -sudo apt install sox libsox-fmt-all -``` - -**Windows:** -```bash -# Download from https://sourceforge.net/projects/sox/ and add to PATH !! -``` - -### 2. Create environment -```bash -conda create -n qwen3-tts python=3.12 -y -conda activate qwen3-tts -``` - -### 3. Install dependencies - -**Linux:** -```bash -pip install torch==2.9.1 torchaudio==2.9.1 --index-url https://download.pytorch.org/whl/cu130 -pip install https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.6.8/flash_attn-2.8.3%2Bcu130torch2.9-cp312-cp312-linux_x86_64.whl -``` - -**Windows:** -```bash -pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu130 -pip install https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.7.12/flash_attn-2.8.3%2Bcu130torch2.10-cp312-cp312-win_amd64.whl -pip install -U "triton-windows<3.7" -``` - -### 4. Install package -```bash -git clone https://github.com/dffdeeq/Qwen3-TTS-streaming.git -cd Qwen3-TTS-streaming -pip install -e . +```python +for chunk, sr in model.stream_generate_voice_clone( + text="Hello!", + language="en", + voice_clone_prompt=prompt, + # Phase 2 settings + emit_every_frames=12, + decode_window_frames=80, + # Phase 1 settings (two-phase) + first_chunk_emit_every=5, + first_chunk_decode_window=48, + first_chunk_frames=48, +): + play_audio(chunk, sr) ``` ## Parameters | Parameter | Default | Description | |-----------|---------|-------------| -| `emit_every_frames` | 4 | Emit audio every N frames (~0.33s at 12Hz) | +| `emit_every_frames` | 8 | Emit audio every N frames | | `decode_window_frames` | 80 | Decoder context window | +| `overlap_samples` | 512 | Crossfade overlap between chunks | +| `first_chunk_emit_every` | 0 | Phase 1 emit interval (0 = disabled) | +| `first_chunk_decode_window` | 48 | Phase 1 decode window | +| `first_chunk_frames` | 48 | Switch to phase 2 after N frames | -## Why This Exists +## Installation -From official Qwen3-TTS README: -> Now only offline inference is supported. Online serving will be supported later. - -This fork provides streaming now, without waiting for vLLM-Omni updates. +```bash +sudo apt install sox +pip install torch torchaudio flash-attn +pip install -e . +``` --- -Based on [QwenLM/Qwen3-TTS](https://github.com/QwenLM/Qwen3-TTS) +Based on: +- [QwenLM/Qwen3-TTS](https://github.com/QwenLM/Qwen3-TTS) +- [dffdeeq/Qwen3-TTS-streaming](https://github.com/dffdeeq/Qwen3-TTS-streaming) diff --git a/qwen_tts/README.md b/qwen_tts/README.md new file mode 100644 index 0000000..3505d34 --- /dev/null +++ b/qwen_tts/README.md @@ -0,0 +1,87 @@ +# Qwen3-TTS Streaming + +Real-time streaming audio generation for [Qwen3-TTS](https://github.com/QwenLM/Qwen3-TTS). + +## Features + +From [dffdeeq/Qwen3-TTS-streaming](https://github.com/dffdeeq/Qwen3-TTS-streaming): +- `stream_generate_voice_clone()` - streaming with voice cloning +- `stream_generate_pcm()` - real-time PCM audio streaming +- `torch.compile` + CUDA graphs optimization +- Crossfade overlap for seamless chunk transitions + +Added in this fork: +- **Two-phase streaming** - faster first-chunk latency + +## Two-Phase Streaming + +Standard streaming with Qwen's TTS library waits for `emit_every_frames` (e.g., 12) before emitting the first audio. Two-phase uses aggressive settings for the first chunk to improve latency, then switches to stable settings. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ PHASE 1 (First N frames) │ PHASE 2 (Rest of audio) │ +│ - emit_every = 5 (fast) │ - emit_every = 12 (stable) │ +│ - decode_window = 48 │ - decode_window = 80 │ +│ - optimized = OFF │ - optimized = ON │ +│ → FAST first chunk │ → QUALITY for rest │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Benchmarks + +| Test | Method | emit | 1st Chunk | 1st Spdup | Total | Tot Spdup | RTF | +|------|--------|------|-----------|-----------|-------|-----------|-----| +| 2 | Baseline (no opt) | 12 | 570ms | 1.00x | 3.16s | 1.00x | 0.56 | +| 3 | Optimized | 12 | 389ms | 1.47x | 2.37s | 1.34x | 0.37 | +| 4 | Optimized_2 (stable) | 12 | 382ms | 1.49x | 2.27s | 1.39x | 0.36 | +| 5 | **Two-phase (5→12)** | 5→12 | **208ms** | **2.75x** | 2.58s | 1.23x | 0.39 | + +User hears audio **362ms earlier** vs baseline, **174ms earlier** vs only optimized. + +**First-chunk latency improvement:** +- vs Baseline: **2.75x faster** (570ms → 208ms, saves 362ms) +- vs Optimized: **1.87x faster** (389ms → 208ms, saves 181ms) +- vs Optimized_2: **1.84x faster** (382ms → 208ms, saves 174ms) + +## Usage + +```python +for chunk, sr in model.stream_generate_voice_clone( + text="Hello!", + language="en", + voice_clone_prompt=prompt, + # Phase 2 settings + emit_every_frames=12, + decode_window_frames=80, + # Phase 1 settings (two-phase) + first_chunk_emit_every=5, + first_chunk_decode_window=48, + first_chunk_frames=48, +): + play_audio(chunk, sr) +``` + +## Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `emit_every_frames` | 8 | Emit audio every N frames | +| `decode_window_frames` | 80 | Decoder context window | +| `overlap_samples` | 512 | Crossfade overlap between chunks | +| `first_chunk_emit_every` | 0 | Phase 1 emit interval (0 = disabled) | +| `first_chunk_decode_window` | 48 | Phase 1 decode window | +| `first_chunk_frames` | 48 | Switch to phase 2 after N frames | + +## Installation + +```bash +sudo apt install sox +pip install torch torchaudio flash-attn +pip install -e . +``` + +--- + +Based on: +- [QwenLM/Qwen3-TTS](https://github.com/QwenLM/Qwen3-TTS) +- [dffdeeq/Qwen3-TTS-streaming](https://github.com/dffdeeq/Qwen3-TTS-streaming) diff --git a/qwen_tts/core/models/modeling_qwen3_tts.py b/qwen_tts/core/models/modeling_qwen3_tts.py index bd8f02a..7a632f7 100644 --- a/qwen_tts/core/models/modeling_qwen3_tts.py +++ b/qwen_tts/core/models/modeling_qwen3_tts.py @@ -2605,6 +2605,10 @@ class Qwen3TTSForConditionalGeneration(Qwen3TTSPreTrainedModel, GenerationMixin) max_frames: int = 10000, # Optimization flags use_optimized_decode: bool = True, + # Two-phase streaming: aggressive first chunk + first_chunk_emit_every: int = 0, # 0 = disabled, use emit_every_frames throughout + first_chunk_decode_window: int = 48, + first_chunk_frames: int = 48, # Switch to stable after this many frames ) -> Generator[tuple[np.ndarray, int], None, None]: """ Stream audio generation, yielding PCM chunks as they are generated. @@ -2627,6 +2631,9 @@ class Qwen3TTSForConditionalGeneration(Qwen3TTSPreTrainedModel, GenerationMixin) overlap_samples: Overlap samples for crossfade between chunks max_frames: Maximum number of codec frames to generate use_optimized_decode: Use CUDA graph optimized decode when available (default True) + first_chunk_emit_every: Emit interval for first chunk phase (0 = disabled, use emit_every_frames) + first_chunk_decode_window: Decode window size for first chunk phase + first_chunk_frames: Switch to stable settings after this many frames Yields: tuple[np.ndarray, int]: (pcm_chunk as float32 array, sample_rate) @@ -2751,22 +2758,36 @@ class Qwen3TTSForConditionalGeneration(Qwen3TTSPreTrainedModel, GenerationMixin) token = torch.argmax(step_logits, dim=-1) frames_since_emit += 1 - if frames_since_emit < emit_every_frames: + + # Two-phase streaming: determine current phase settings + total_frames_generated = len(codes_buffer) + if first_chunk_emit_every > 0 and total_frames_generated < first_chunk_frames: + # Phase 1: Aggressive settings for first chunk (lower latency) + current_emit_every = first_chunk_emit_every + current_decode_window = first_chunk_decode_window + current_use_optimized = False # Non-optimized allows flexible window size + else: + # Phase 2: Stable settings (better quality) + current_emit_every = emit_every_frames + current_decode_window = decode_window_frames + current_use_optimized = use_optimized_decode + + if frames_since_emit < current_emit_every: continue frames_since_emit = 0 # Decode window of codec frames to PCM - start = max(0, len(codes_buffer) - decode_window_frames) + start = max(0, len(codes_buffer) - current_decode_window) window_codes = torch.stack(codes_buffer[start:], dim=0) # [T, num_code_groups] # Add ref_code as context prefix for stable decoder context from the start window, _ = _add_ref_code_context( - window_codes, ref_code_context, ref_code_frames, decode_window_frames + window_codes, ref_code_context, ref_code_frames, current_decode_window ) # Use optimized decode path when available # Pass pad_to_size to ensure fixed tensor size for torch.compile - if use_optimized_decode and hasattr(self.speech_tokenizer, 'decode_streaming'): + if current_use_optimized and hasattr(self.speech_tokenizer, 'decode_streaming'): wavs, sr = self.speech_tokenizer.decode_streaming( window.to(self.talker.device), use_optimized=True, @@ -2781,7 +2802,7 @@ class Qwen3TTSForConditionalGeneration(Qwen3TTSPreTrainedModel, GenerationMixin) # Extract only new samples (tail of decoded window) # Use fixed upsample rate to avoid floating-point drift samples_per_frame = self.speech_tokenizer.get_decode_upsample_rate() - step_samples = samples_per_frame * emit_every_frames + step_samples = samples_per_frame * current_emit_every chunk = wav[-step_samples:] if step_samples > 0 else wav # Crossfade with previous chunk tail for smooth transition diff --git a/qwen_tts/inference/qwen3_tts_model.py b/qwen_tts/inference/qwen3_tts_model.py index d1a4962..75b6d1b 100644 --- a/qwen_tts/inference/qwen3_tts_model.py +++ b/qwen_tts/inference/qwen3_tts_model.py @@ -700,6 +700,10 @@ class Qwen3TTSModel: max_frames: int = 10000, # Optimization use_optimized_decode: bool = True, + # Two-phase streaming: aggressive first chunk + first_chunk_emit_every: int = 0, # 0 = disabled, use emit_every_frames throughout + first_chunk_decode_window: int = 48, + first_chunk_frames: int = 48, # Switch to stable after this many frames **kwargs, ) -> Generator[Tuple[np.ndarray, int], None, None]: """ @@ -721,6 +725,9 @@ class Qwen3TTSModel: max_frames: Maximum codec frames to generate. use_optimized_decode: Use CUDA graph optimized decode when available (default True). Call enable_streaming_optimizations() first for best performance. + first_chunk_emit_every: Emit interval for first chunk phase (0 = disabled). + first_chunk_decode_window: Decode window size for first chunk phase. + first_chunk_frames: Switch to stable settings after this many frames. **kwargs: Generation parameters (do_sample, top_k, top_p, temperature, etc.) Yields: @@ -798,6 +805,9 @@ class Qwen3TTSModel: overlap_samples=overlap_samples, max_frames=max_frames, use_optimized_decode=use_optimized_decode, + first_chunk_emit_every=first_chunk_emit_every, + first_chunk_decode_window=first_chunk_decode_window, + first_chunk_frames=first_chunk_frames, **gen_kwargs, ): yield chunk, sr