Skip to content

Commit 32c51ec

Browse files
feat: add cooperative stop and generation management to streaming, improve UI responsiveness
1 parent 483511e commit 32c51ec

2 files changed

Lines changed: 32 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ task release
255255
- Added a dependency-free Python HTTP client for using KokoroTTS endpoints from application code.
256256
- Added `task client-test` for server-backed Python client coverage across discovery, generation, conversion, streaming, and validation paths.
257257
- Added optional UI/API audio controls for pitch, tempo, volume, and loudness normalization with neutral defaults for backward compatibility.
258+
- Improved the UI Stream tab so Stop cancels active streams cooperatively and starting a new stream clears stale audio before using the latest text.
258259
- Updated Docker publish workflow support for `vX.Y` tags, explicit `hangrylabs/kokorotts` publishing, and manual release dispatch with a selected checkout ref.
259260
- Removed unnecessary caution callouts from public docs and the Stream tab for a cleaner product-facing experience.
260261

kokorotts/app.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import subprocess
44
import tempfile
5+
import threading
56
import wave
67
from typing import Optional
78

@@ -83,6 +84,8 @@
8384
BUILD_ID = os.getenv("BUILD_ID", "stable")
8485
DEFAULT_DEVICE = os.getenv("KOKOROTTS_DEVICE", "auto")
8586
MODEL_CACHE = {}
87+
STREAM_LOCK = threading.Lock()
88+
STREAM_GENERATION = 0
8689
pipelines = {
8790
lang_code: KPipeline(lang_code=lang_code, repo_id=DEFAULT_REPO_ID, model=False)
8891
for lang_code in LANGUAGE_CHOICES
@@ -144,6 +147,23 @@ def get_model(device: str) -> KModel:
144147
return MODEL_CACHE[device]
145148

146149

150+
def next_stream_generation() -> int:
151+
global STREAM_GENERATION
152+
with STREAM_LOCK:
153+
STREAM_GENERATION += 1
154+
return STREAM_GENERATION
155+
156+
157+
def is_current_stream_generation(stream_generation: int) -> bool:
158+
with STREAM_LOCK:
159+
return stream_generation == STREAM_GENERATION
160+
161+
162+
def stop_active_stream():
163+
next_stream_generation()
164+
return SAMPLE_RATE, np.zeros(1, dtype=np.int16)
165+
166+
147167
def synthesize_full(text, voice="af_heart", speed=1, hardware="auto", use_gpu: Optional[bool] = None):
148168
pipeline = pipelines[voice[0]]
149169
pack = pipeline.load_voice(voice)
@@ -220,14 +240,19 @@ def generate_all(
220240
normalize=False,
221241
use_gpu: Optional[bool] = None,
222242
):
243+
if not (text or "").strip():
244+
raise gr.Error("Text must not be empty")
245+
stream_generation = next_stream_generation()
246+
yield SAMPLE_RATE, np.zeros(1, dtype=np.int16)
223247
pipeline = pipelines[voice[0]]
224248
pack = pipeline.load_voice(voice)
225249
if use_gpu is not None:
226250
hardware = "auto" if use_gpu else "cpu"
227251
resolved_device = normalize_device(hardware)
228252
model = get_model(resolved_device)
229-
first = True
230253
for _, ps, _ in pipeline(text, voice, speed):
254+
if not is_current_stream_generation(stream_generation):
255+
return
231256
ref_s = pack[len(ps) - 1]
232257
try:
233258
audio = model(ps, ref_s, speed)
@@ -246,10 +271,9 @@ def generate_all(
246271
volume,
247272
normalize,
248273
)
274+
if not is_current_stream_generation(stream_generation):
275+
return
249276
yield SAMPLE_RATE, processed_audio
250-
if first:
251-
first = False
252-
yield SAMPLE_RATE, np.zeros(1, dtype=np.int16)
253277

254278

255279
def audio_to_wav_bytes(audio: np.ndarray, sample_rate: int = SAMPLE_RATE) -> bytes:
@@ -675,12 +699,14 @@ def get_status_payload() -> dict:
675699
outputs=[out_audio, out_ps],
676700
)
677701
tokenize_btn.click(fn=tokenize_first, inputs=[text, voice], outputs=[out_ps])
702+
stream_btn.click(fn=stop_active_stream, outputs=[out_stream], queue=False)
678703
stream_event = stream_btn.click(
679704
fn=generate_all,
680705
inputs=[text, voice, speed, hardware, pitch_semitones, tempo, volume, normalize],
681706
outputs=[out_stream],
707+
trigger_mode="always_last",
682708
)
683-
stop_btn.click(fn=None, cancels=stream_event)
709+
stop_btn.click(fn=stop_active_stream, outputs=[out_stream], cancels=[stream_event], queue=False)
684710
predict_btn.click(fn=predict, inputs=[text, voice, speed], outputs=[out_audio])
685711

686712
api = FastAPI(

0 commit comments

Comments
 (0)