Skip to content

Sync fork to upstream v0.6.0#3

Open
Fieldnote-Echo wants to merge 55 commits into
masterfrom
sync/upstream-v0.6.0
Open

Sync fork to upstream v0.6.0#3
Fieldnote-Echo wants to merge 55 commits into
masterfrom
sync/upstream-v0.6.0

Conversation

@Fieldnote-Echo

Copy link
Copy Markdown
Owner

Mechanical catch-up of the fork's master to upstream remsky/Kokoro-FastAPI v0.6.0 (55 commits). No fork-specific changes here — this is upstream's own reviewed code, isolated so the fork's value-add can be reviewed separately on top of it (see the stacked PR "Fork value-add").

Notable upstream changes landing with this sync:

  • v0.6.0: opt-in /dev/* and /debug/* endpoints (ALLOW_DEV_UNLOAD, ENABLE_DEBUG_ENDPOINTS), voice + timestamp fixes
  • Non-English (espeak) word timestamps in /dev/captioned_speech
  • Docker restructure (multi-stage Dockerfile.optimized + docker-bake.hcl)

Deploy note: voice aliases nova/alloy/ash/coral/echo now map to bf_isabella (audible change if used); /v1/audio/voices returns objects (?legacy=true to opt out). Our narration tooling (rippling-lms) uses af_heart + ungated /dev/captioned_speech + /health, so it is unaffected — verified.

Merge this first, then the value-add PR.

jcheek and others added 30 commits July 26, 2025 17:07
- Updated GPU dependency from torch==2.8.0+cu129 to torch==2.8.0+cu126 in pyproject.toml
- Changed PyTorch CUDA index URL from https://download.pytorch.org/whl/cu129 to https://download.pytorch.org/whl/cu126
- This change ensures compatibility with CUDA 12.6 runtime while maintaining the same PyTorch version (2.8.0)
The finalize block in write_chunk() called output_buffer.getvalue() before
container.close(). For OGG/Opus, the final page of audio data is only written
to the buffer during close(), causing ~1-2 seconds of audio to be lost.

Swap the order: close container first, then read buffer.

Fixes: remsky#447
Fixes remsky#453

The generate() and generate_from_tokens() methods performed a full
voice tensor round-trip on every TTS request: load from .pt file →
deserialize to torch.Tensor → serialize back → write to new temp file.
This created ~20-30MB of transient allocations per request that
fragmented the Python heap, causing RSS to grow monotonically.

Changes:
- Add _voice_cache dict to KokoroV1 for in-memory voice tensor caching
- Add _get_voice_tensor() helper with cache lookup by path+device
- Replace direct load_voice_tensor() calls with cached version
- Only save to temp file when the file doesn't already exist
- Cache key includes device to handle GPU/CPU switches correctly
Addresses remsky#452

Voice .pt files contain simple tensors and do not require pickle
deserialization. Using weights_only=False allows arbitrary code
execution through crafted pickle payloads. This change sets
weights_only=True on all torch.load() calls for voice tensors,
matching the behavior already used for model weight loading in
load_model_weights().

Changes:
- api/src/core/paths.py: Change load_voice_tensor default from
  weights_only=False to weights_only=True
- api/src/services/tts_service.py: Add weights_only=True to
  torch.load call in _load_voice
…tial-state-on-apple-silicon

Fix test_initial_state() test failure on apple silicon.
fix: OGG/Opus audio truncation — final page lost in write_chunk finalize
…s-only

fix: use weights_only=True for voice tensor torch.load calls
fix: cache voice tensors to prevent per-request memory leak (remsky#453)
fix: downgrade PyTorch CUDA version from cu129 to cu126
…e image with cu126 wheels, compose image naming, add INCLUDE_JAPANESE flag for future build opts

build(bake): redirected to *.optimized dockerfiles

examples(transcription tests): long form and multilingual whisper transcription reporsts w/ WER scoring thresholds

chore: release alignment prep, version WIP bumped to 0.3.0-rc
…waveform bugfix

ci: build caching,

build: container naming consistency on local compose
…wnload_model scripts, static release artifact link
remsky and others added 24 commits May 22, 2026 00:34
Adds a -cu128 tag to the -gpu package while keeping the default -gpu image on cu126 for Maxwell/Pascal compatibility.

Explicit -cu126 aliases added to allow stable pinning against future default changes.
Compose users opt into cu128 by uncommenting preset args block in docker/gpu/docker-compose.yml.
…e short ciruit on no-up updates to preserve responsiveness on long sessions. (remsky#471)
…y#474)

* feat(api): add POST /dev/unload to release model from GPU VRAM

Allows homelab deployments to free GPU memory when the TTS service is idle
without stopping the container. The model reloads lazily on the next request.

- ModelManager.unload(): acquires lock, calls backend.unload(), nulls
  _backend, then calls torch.cuda.empty_cache() if CUDA is available
- ModelManager.generate(): lazy reinit when _backend is None (calls
  initialize() + load_model()) instead of raising RuntimeError
- POST /dev/unload: 200 on success, 503 if manager not initialised,
  500 on unexpected error
- TTSService.model_manager annotated as Optional[ModelManager] for
  correct mypy narrowing at the endpoint
- Full test coverage in api/tests/test_model_unload.py (11 tests)

Closes remsky#473 (partial)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(model-unload): lazy reload on next request after /dev/unload

Add ensure_backend() to ModelManager which reinitialises the backend and
reloads the model if /dev/unload was called. All three get_backend() call
sites in tts_service now await ensure_backend() first, so the first TTS
request after an unload reloads the model automatically rather than raising
RuntimeError: Backend not initialized.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(model-unload): add double-checked locking to ensure_backend()

Addresses review feedback: the original lazy-reload in generate() ran
without a lock, so a burst of requests landing while _backend is None
could trigger multiple concurrent initialize()/load_model() calls.

- ensure_backend(): fast-path check outside lock, then re-check inside
  _lock before initializing (double-checked locking pattern)
- generate(): routes through ensure_backend() instead of inline check,
  eliminating the duplicate code path
- test_ensure_backend_serializes_concurrent_reloads: 5-way concurrent
  gather confirms only one initialize/load_model cycle fires

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
-checkout@v4 → v5, setup-buildx-action@v3 → v4 across all workflows.
-Ruff formatting pass: quote style, import order, line length.
…emsky#483)

Currently /dev/unload is mounted by default with no authentication, so any caller that can reach the API can unload the active model from memory and can possibly force repeated reload cycles (remsky#478).

Add an 'allow_dev_unload' setting (default False) and return 403 when disabled, so the endpoint is off by default and can be re-enabled with ALLOW_DEV_UNLOAD=true for trusted deployments.
…ech (remsky#484)

KPipeline only attaches timed tokens for English (misaki) voices, so
/dev/captioned_speech silently returned timestamps: null for Spanish,
French, Italian, Hindi and Portuguese. The model does predict a duration
for every phoneme in every language (pred_dur) - the audio is rendered
from exactly those durations - but the espeak path discarded them.

Derive word timestamps for espeak pipelines from pred_dur directly:
pred_dur[0] is BOS and pred_dur[1+i] covers phonemes[i], so summing
per-character durations and splitting at the phoneme string's spaces
yields exact word times. Words espeak expands into several spoken words
(e.g. "1863") are reconciled by re-phonemizing per word; if counts still
disagree, no timestamps are emitted (previous behavior). English is
untouched, and ja/zh (non-espeak G2Ps, no space-separated word groups)
deliberately keep the previous behavior.

Fixes remsky#331

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request bumps the project version to 0.6.0 and introduces several major features, including a POST /dev/unload endpoint to release the model from GPU VRAM with lazy reloading, opt-in /debug/* endpoints, and word timestamp generation for non-English espeak pipelines derived from phoneme durations. It also optimizes voice tensor loading with in-memory caching, improves Web UI playback and seeking behavior, and adds an integration test suite. The review feedback highlights critical performance optimizations, such as converting the pred_dur PyTorch tensor to a Python list to prevent synchronous device-to-host transfers during timestamp generation, and vectorizing the silence-trimming boundary search with NumPy. Additionally, a minor typo in a user-facing streaming notice was identified for correction.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +34 to +36
try:
if pred_dur is None or not phonemes or len(pred_dur) < len(phonemes) + 1:
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In _espeak_word_timestamps, pred_dur is a PyTorch tensor. Indexing a PyTorch tensor (especially if it resides on a CUDA/GPU device) inside a Python loop (pred_dur[1 + i]) triggers synchronous device-to-host transfers on every iteration. This introduces a massive performance bottleneck. Converting the tensor to a standard Python list using .tolist() before the loop avoids these roundtrips and significantly speeds up execution.

Suggested change
try:
if pred_dur is None or not phonemes or len(pred_dur) < len(phonemes) + 1:
return None
try:
if pred_dur is None or not phonemes:
return None
if hasattr(pred_dur, "tolist"):
pred_dur = pred_dur.tolist()
if len(pred_dur) < len(phonemes) + 1:
return None

Comment thread api/src/services/audio.py
Comment on lines 80 to 90
non_silent_index_start, non_silent_index_end = None, None

for X in range(0, len(audio_data)):
if abs(audio_data[X]) > amplitude_threshold:
if abs(int(audio_data[X])) > amplitude_threshold:
non_silent_index_start = X
break

for X in range(len(audio_data) - 1, -1, -1):
if abs(audio_data[X]) > amplitude_threshold:
if abs(int(audio_data[X])) > amplitude_threshold:
non_silent_index_end = X
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Looping through the audio array elements one-by-one in Python to find the non-silent boundaries is highly inefficient and blocks the event loop for large audio inputs. Utilizing vectorized NumPy operations with np.where is significantly faster. To safely handle the potential int16 overflow for -32768 without slow per-element casting, we can cast the entire array to np.int32 before applying np.abs.

Suggested change
non_silent_index_start, non_silent_index_end = None, None
for X in range(0, len(audio_data)):
if abs(audio_data[X]) > amplitude_threshold:
if abs(int(audio_data[X])) > amplitude_threshold:
non_silent_index_start = X
break
for X in range(len(audio_data) - 1, -1, -1):
if abs(audio_data[X]) > amplitude_threshold:
if abs(int(audio_data[X])) > amplitude_threshold:
non_silent_index_end = X
break
import numpy as np
non_silent_indices = np.where(np.abs(audio_data.astype(np.int32)) > amplitude_threshold)[0]
if non_silent_indices.size > 0:
non_silent_index_start = int(non_silent_indices[0])
non_silent_index_end = int(non_silent_indices[-1])
else:
non_silent_index_start, non_silent_index_end = None, None

Comment thread web/src/App.js
message = `${formatLabel} output will be generated, playback and/or download will be available when generation finishes.`;
} else if (!this.audioService.supportsMSEMp3()) {
message = isFirefox
? 'Audio streaming is not currently supported in Firefox. Playback and/or download should stilll be available when generation finishes.'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a minor typo in the user-facing Firefox streaming notice message: stilll should be corrected to still.

Suggested change
? 'Audio streaming is not currently supported in Firefox. Playback and/or download should stilll be available when generation finishes.'
? 'Audio streaming is not currently supported in Firefox. Playback and/or download should still be available when generation finishes.'

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Sync fork to upstream v0.6.0 (dev/debug gating, unload, Docker rebuild)

✨ Enhancement 🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Sync to upstream v0.6.0 with opt-in /dev/* and /debug/* endpoints.
• Fix audio + voice behavior: OGG/Opus finalization, voice tensor caching, espeak timestamps.
• Rework Docker/CI/release pipelines and add integration + endpoint tests.
Diagram

graph TD
  A["Web UI"] --> B["/v1/audio/speech"] --> C["TTSService"] --> D["ModelManager"] --> E["KokoroV1"]
  A --> F["/v1/audio/voices"]
  C --> G["StreamingWriter"]
  H["/dev/unload"] --> D
  I["/debug/*"] --> J["System Introspection"]

  subgraph Legend
    direction LR
    _ui["UI"] ~~~ _api["API Route"] ~~~ _svc["Service"] ~~~ _inf["Inference"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep debug/dev routers unregistered unless enabled
  • ➕ Reduces routing surface entirely when disabled (no 403 endpoints exposed).
  • ➕ Avoids dependency-injection overhead on every debug route call.
  • ➖ Harder to toggle at runtime (requires app restart).
  • ➖ More conditional wiring in app startup code vs centralized dependency gate.
2. Swap MSE→file source immediately on completion (always)
  • ➕ Simpler logic: always get correct duration/seek ASAP after generation ends.
  • ➕ Less state to reason about (pause/ended triggers not needed).
  • ➖ Can interrupt active playback if completion occurs mid-listen.
  • ➖ Browser-specific timing issues (metadata loading) could create glitches without careful handling.
3. Persist voice tensors in a process-wide LRU cache
  • ➕ Avoids duplicate caches if multiple backend instances ever exist.
  • ➕ Can bound memory explicitly with eviction.
  • ➖ Adds cache invalidation complexity (device-specific entries, unload/reset semantics).
  • ➖ Current backend-scoped cache is simpler and cleared on unload().

Recommendation: The PR’s approach is solid for an upstream sync: gating debug/dev via settings-driven dependencies keeps wiring stable while still default-denying access, and backend-scoped voice caching aligns with backend lifecycle (cleared on unload). If you want an even smaller exposed surface, consider not registering debug/dev routers when disabled, but that trades off runtime toggle simplicity.

Files changed (89) +5196 / -599

Enhancement (18) +455 / -115
config.pyDerive API version dynamically and add dev/debug gates +21/-1

Derive API version dynamically and add dev/debug gates

• Reads api_version from VERSION/package metadata instead of a hardcoded constant, and adds Settings flags to gate /dev/unload and /debug/* routes.

api/src/core/config.py

openai_mappings.jsonAdd gpt-4o-mini-tts model alias and update voice short-names +6/-5

Add gpt-4o-mini-tts model alias and update voice short-names

• Maps gpt-4o-mini-tts to kokoro-v1_0 and updates OpenAI short voice names (e.g., nova/alloy/ash/coral/echo) to new internal voice IDs.

api/src/core/openai_mappings.json

kokoro_v1.pyCache voice tensors and derive espeak word timestamps +117/-14

Cache voice tensors and derive espeak word timestamps

• Adds in-memory caching of loaded voice tensors to prevent per-request memory growth, and implements espeak-based word timestamp derivation for non-English voices using predicted phoneme durations.

api/src/inference/kokoro_v1.py

model_manager.pyAdd async locking, lazy backend reload, and async unload +24/-2

Add async locking, lazy backend reload, and async unload

• Introduces an asyncio.Lock to serialize backend init/unload, adds ensure_backend() for post-unload lazy reload, and implements async unload() that clears backend and empties CUDA cache.

api/src/inference/model_manager.py

main.pyKeep debug router registered but default-deny via 403 gate +1/-1

Keep debug router registered but default-deny via 403 gate

• Clarifies debug router behavior: routes exist but return 403 unless enable_debug_endpoints is set.

api/src/main.py

debug.pyGate /debug/* endpoints and remove session pool route +13/-61

Gate /debug/* endpoints and remove session pool route

• Adds a dependency that enforces enable_debug_endpoints, returning 403 when disabled, and removes the legacy /debug/session_pools endpoint.

api/src/routers/debug.py

development.pyAdd POST /dev/unload (opt-in) and timestamp accumulator fix +28/-1

Add POST /dev/unload (opt-in) and timestamp accumulator fix

• Fixes timestamp accumulator concatenation formatting and introduces a gated /dev/unload endpoint that unloads the model manager without stopping the server.

api/src/routers/development.py

openai_compatible.pyExpose gpt-4o-mini-tts and change /audio/voices response shape +17/-3

Expose gpt-4o-mini-tts and change /audio/voices response shape

• Adds gpt-4o-mini-tts to model listing and changes /v1/audio/voices to return objects by default, with ?legacy=true to preserve old string list behavior.

api/src/routers/openai_compatible.py

web_player.pyExpose API version in web config response +2/-2

Expose API version in web config response

• Cleans up formatting and ensures web config includes the server version for UI badge display.

api/src/routers/web_player.py

tts_service.pyEnsure backend reload after unload and safer torch.load for voices +34/-16

Ensure backend reload after unload and safer torch.load for voices

• Calls model_manager.ensure_backend() before generation paths so /dev/unload reloads automatically, and uses weights_only=True when loading voice tensors from disk.

api/src/services/tts_service.py

index.htmlAdd UI elements for version/streaming notice +7/-0

Add UI elements for version/streaming notice

• Adds markup hooks used by the app to show version badge and streaming capability notices.

web/index.html

App.jsShow version badge and browser/format streaming notice +66/-2

Show version badge and browser/format streaming notice

• Fetches server version from /web/config for display and surfaces playback/streaming caveats (Firefox/non-MP3/PCM/autoplay) in the UI.

web/src/App.js

PlayerControls.jsImprove readiness gating and time formatting +14/-4

Improve readiness gating and time formatting

• Adds isReady-driven enabling of controls, hardens time formatting for non-finite values, and reacts to AudioService 'ready' events.

web/src/components/PlayerControls.js

config.jsTrack server version from /web/config +4/-0

Track server version from /web/config

• Extends web config initialization to store the server version for UI display.

web/src/config.js

VoiceService.jsSupport voices endpoint returning objects +1/-1

Support voices endpoint returning objects

• Accepts both legacy string voices and new {id,name} entries, normalizing to voice IDs for selection.

web/src/services/VoiceService.js

PlayerState.jsAdd isReady state and avoid redundant state broadcasts +15/-1

Add isReady state and avoid redundant state broadcasts

• Introduces isReady to represent playable/seekable readiness and prevents unnecessary subscriber updates when state doesn’t change.

web/src/state/PlayerState.js

badges.cssAdd styling for badges/labels +22/-0

Add styling for badges/labels

• Introduces CSS for badges (including version display).

web/styles/badges.css

base.cssUpdate base styling for new UI elements +63/-1

Update base styling for new UI elements

• Adjusts base CSS to support updated layout/notice/badge elements.

web/styles/base.css

Bug fix (5) +472 / -157
paths.pyHarden temp cleanup, add audio MIME types, and safer torch.load defaults +15/-3

Harden temp cleanup, add audio MIME types, and safer torch.load defaults

• Fixes temp dir cleanup age logic, serves correct audio content-types for downloads, and defaults voice tensor loading to weights_only=True.

api/src/core/paths.py

audio.pyPrevent numeric overflow in silence detection +2/-2

Prevent numeric overflow in silence detection

• Casts samples to int before abs() comparisons to avoid incorrect thresholding for int16-like arrays.

api/src/services/audio.py

streaming_audio_writer.pyFix OGG/Opus truncation and avoid WAV end-click in streaming finalize +20/-10

Fix OGG/Opus truncation and avoid WAV end-click in streaming finalize

• Ensures container.close() occurs before reading the output buffer so trailing pages are written for OGG/Opus, and suppresses problematic WAV finalize bytes that can cause an audible click.

api/src/services/streaming_audio_writer.py

WaveVisualizer.jsFix wave animation start/stop transitions +9/-6

Fix wave animation start/stop transitions

• Prevents multiple RAF loops by only calling start/stop on play-state transitions and makes cleanup resilient to missing methods.

web/src/components/WaveVisualizer.js

AudioService.jsHarden streaming playback and add Firefox-safe block-mode fallback +426/-136

Harden streaming playback and add Firefox-safe block-mode fallback

• Detects MSE MP3 support and falls back to full-file (block) mode when streaming isn’t supported; bounds MSE buffer growth with backpressure/eviction; swaps to the server-hosted file after generation for correct duration/seeking; improves cleanup and pending-operation handling.

web/src/services/AudioService.js

Refactor (5) +42 / -37
normalizer.pyNormalizer robustness/formatting updates +20/-19

Normalizer robustness/formatting updates

• Refactors symbol replacement literals and regex string usage; normalizes newline handling and minor formatting for consistency.

api/src/services/text_processing/normalizer.py

phonemizer.pyImport/whitespace cleanups in phonemizer flow +4/-4

Import/whitespace cleanups in phonemizer flow

• Reorders imports and trims extraneous whitespace while preserving phonemization behavior.

api/src/services/text_processing/phonemizer.py

text_processor.pyWhitespace/typing cleanups and pause parsing warning formatting +13/-8

Whitespace/typing cleanups and pause parsing warning formatting

• Adjusts typing imports/order, improves guard formatting for empty chunks, and tidies normalization call formatting; no functional behavior change intended beyond readability.

api/src/services/text_processing/text_processor.py

schemas.pySchema formatting fixes for volume_multiplier fields +3/-5

Schema formatting fixes for volume_multiplier fields

• Cleans up Pydantic Field parameter formatting and trailing commas in schema definitions.

api/src/structures/schemas.py

api.pyMinor API client tweak +2/-1

Minor API client tweak

• Small upstream sync change in UI Python client helper.

ui/lib/api.py

Tests (17) +1213 / -44
conftest.pyAdd integration test fixtures +96/-0

Add integration test fixtures

• Adds integration test configuration/fixtures for running end-to-end tests against a live server from the test-client container.

api/tests/integration/conftest.py

test_tts_roundtrip.pyAdd multilingual TTS→Whisper roundtrip smoke test +165/-0

Add multilingual TTS→Whisper roundtrip smoke test

• Introduces integration tests that synthesize audio across multiple languages/voices and validates intelligibility via faster-whisper + WER/CER thresholds.

api/tests/integration/test_tts_roundtrip.py

test_voices_endpoint.pyAdd integration tests for voices endpoint shape and mappings +53/-0

Add integration tests for voices endpoint shape and mappings

• Validates the default list[dict] voices shape, legacy mode behavior, and ensures OpenAI short-name mappings (e.g. nova) resolve to real audio.

api/tests/integration/test_voices_endpoint.py

test_debug_endpoints.pyTest debug endpoint default-deny gate +26/-0

Test debug endpoint default-deny gate

• Asserts /debug/* routes return 403 by default and succeed when enable_debug_endpoints is enabled.

api/tests/test_debug_endpoints.py

test_kokoro_v1.pyUpdate backend tests for new behaviors +75/-17

Update backend tests for new behaviors

• Adjusts KokoroV1 tests to reflect upstream changes (voice/timestamp/caching-related behavior).

api/tests/test_kokoro_v1.py

test_model_unload.pyAdd unit tests for ModelManager unload and /dev/unload endpoint +262/-0

Add unit tests for ModelManager unload and /dev/unload endpoint

• Covers async unload(), ensure_backend() concurrency behavior, lazy backend reinit on generate(), and HTTP behavior for gated /dev/unload.

api/tests/test_model_unload.py

test_normalizer.pyUpdate normalizer unit tests +5/-2

Update normalizer unit tests

• Adjusts expectations to match upstream normalization behavior and formatting changes.

api/tests/test_normalizer.py

test_openai_endpoints.pyUpdate OpenAI endpoint tests for voices shape changes +7/-4

Update OpenAI endpoint tests for voices shape changes

• Updates tests around /v1/audio/voices to account for new default object shape and legacy mode.

api/tests/test_openai_endpoints.py

test_paths.pyUpdate paths tests for cleanup/content-type behavior +11/-2

Update paths tests for cleanup/content-type behavior

• Aligns tests with corrected temp cleanup logic and expanded content-type mapping.

api/tests/test_paths.py

test_text_processor.pyUpdate text processor tests for formatting/guards +25/-16

Update text processor tests for formatting/guards

• Adjusts tests to match minor text processing guard/formatting changes.

api/tests/test_text_processor.py

test_tts_service.pyUpdate TTSService tests for ensure_backend/unload flow +4/-3

Update TTSService tests for ensure_backend/unload flow

• Adapts tests for new ensure_backend calls and voice loading changes (weights_only).

api/tests/test_tts_service.py

static-server.mjsAdd static server fixture for web E2E tests +52/-0

Add static server fixture for web E2E tests

• Provides a test fixture to serve the web UI for Playwright E2E runs.

web/tests/e2e/fixtures/static-server.mjs

long-playback.spec.mjsAdd long playback E2E test +106/-0

Add long playback E2E test

• Adds Playwright coverage for long playback/streaming behavior and stability.

web/tests/e2e/long-playback.spec.mjs

audio-service.test.mjsAdd unit tests for AudioService behaviors +18/-0

Add unit tests for AudioService behaviors

• Introduces unit tests to validate new AudioService logic around playback readiness and streaming mechanics.

web/tests/unit/audio-service.test.mjs

file-source-swap.test.mjsAdd tests for MSE-to-file source swap +192/-0

Add tests for MSE-to-file source swap

• Validates the post-generation swap from MSE streaming to server file source to restore seeking/duration correctness.

web/tests/unit/file-source-swap.test.mjs

index.test.mjsAdd basic unit test harness entry +3/-0

Add basic unit test harness entry

• Adds a minimal unit test entry to ensure harness wiring works.

web/tests/unit/index.test.mjs

player-controls.test.mjsAdd unit tests for PlayerControls readiness/slider logic +113/-0

Add unit tests for PlayerControls readiness/slider logic

• Covers PlayerControls enablement and time/seek logic with the new isReady model.

web/tests/unit/player-controls.test.mjs

Documentation (15) +1333 / -138
CHANGELOG.mdUpdate changelog for v0.6.0 upstream sync +131/-11

Update changelog for v0.6.0 upstream sync

• Brings changelog forward with upstream’s release notes and behavior changes.

CHANGELOG.md

README.mdRefresh README (badges, image matrix, updated run guidance) +162/-74

Refresh README (badges, image matrix, updated run guidance)

• Updates badges/links, clarifies platform/image selection (cu126 vs cu128 vs arm64), and documents current startup and integration guidance.

README.md

benchmark_first_token_stream_unified.pyUpdate streaming benchmark script +5/-2

Update streaming benchmark script

• Tweaks benchmark script to match upstream streaming behavior and output expectations.

examples/assorted_checks/benchmarks/benchmark_first_token_stream_unified.py

benchmark_model_unload.pyAdd model unload benchmark script +311/-0

Add model unload benchmark script

• Adds a benchmark harness to measure /dev/unload and reload performance characteristics.

examples/assorted_checks/benchmarks/benchmark_model_unload.py

first_token_benchmark_stream.jsonRefresh benchmark output fixture (stream) +25/-25

Refresh benchmark output fixture (stream)

• Updates benchmark output data to match upstream behavior.

examples/assorted_checks/benchmarks/output_data/first_token_benchmark_stream.json

first_token_benchmark_stream_openai.jsonRefresh benchmark output fixture (OpenAI stream) +25/-25

Refresh benchmark output fixture (OpenAI stream)

• Updates benchmark output data for OpenAI-compatible streaming expectations.

examples/assorted_checks/benchmarks/output_data/first_token_benchmark_stream_openai.json

BASELINE.mdAdd transcription baseline documentation +37/-0

Add transcription baseline documentation

• Documents baseline transcription expectations for assorted checks.

examples/assorted_checks/test_transcription/BASELINE.md

README.mdAdd transcription test README +47/-0

Add transcription test README

• Explains how to run transcription evaluation checks and interpret reports.

examples/assorted_checks/test_transcription/README.md

report.jsonAdd transcription report output +42/-0

Add transcription report output

• Adds sample output report for transcription checks.

examples/assorted_checks/test_transcription/output/report.json

long_form_report.jsonAdd long-form transcription report output +20/-0

Add long-form transcription report output

• Adds sample long-form transcription results.

examples/assorted_checks/test_transcription/output_long_form/long_form_report.json

report.jsonAdd multilingual transcription report output +155/-0

Add multilingual transcription report output

• Adds multilingual evaluation report fixture.

examples/assorted_checks/test_transcription/output_multilingual/report.json

report.jsonAdd multilingual CPU default report output +155/-0

Add multilingual CPU default report output

• Adds CPU-default multilingual evaluation fixture.

examples/assorted_checks/test_transcription/output_multilingual_cpu_default/report.json

report.jsonAdd multilingual CPU no-ja report output +155/-0

Add multilingual CPU no-ja report output

• Adds multilingual evaluation fixture variant excluding Japanese.

examples/assorted_checks/test_transcription/output_multilingual_cpu_noja/report.json

run_long_form.batAdd Windows runner for long-form transcription checks +62/-0

Add Windows runner for long-form transcription checks

• Adds a Windows batch script to run long-form transcription checks.

examples/assorted_checks/test_transcription/run_long_form.bat

validate_wav.pyMinor update to WAV validation utility +1/-1

Minor update to WAV validation utility

• Small adjustment to validation behavior/formatting.

examples/assorted_checks/validate_wav.py

Other (29) +1681 / -108
.codeql-config.ymlAdd/adjust CodeQL configuration +10/-0

Add/adjust CodeQL configuration

• Updates CodeQL configuration to match upstream’s security scanning setup.

.codeql-config.yml

ci.ymlModernize CI runner/actions and uv test invocation +3/-6

Modernize CI runner/actions and uv test invocation

• Moves CI to ubuntu-24.04/actions v5 and runs pytest via uv extras (test+cpu) rather than installing editable deps separately.

.github/workflows/ci.yml

release.ymlRework release build + manifest publishing (cu126/cu128 variants) +145/-42

Rework release build + manifest publishing (cu126/cu128 variants)

• Adds concurrency/ref resolution, tag-existence guard, build cache usage, and multi-target manifests including gpu-cu128. Publishes richer OCI labels/annotations and splits GPU builds by arch/CUDA wheel variants.

.github/workflows/release.yml

test_build.ymlExtend build-matrix workflow for gpu-cu128 + caching +14/-8

Extend build-matrix workflow for gpu-cu128 + caching

• Adds gpu-cu128 to the test build matrix, updates runners/actions, and enables buildx cache args for faster iterative builds.

.github/workflows/test_build.yml

test_client_image.ymlAdd workflow to build/publish integration test-client image +84/-0

Add workflow to build/publish integration test-client image

• Introduces a dedicated workflow to build and push the Whisper-equipped test client image to GHCR with caching and dispatch inputs.

.github/workflows/test_client_image.yml

VERSIONBump VERSION to v0.6.0 +1/-1

Bump VERSION to v0.6.0

• Updates the repo VERSION file from 0.2.4 to 0.6.0.

VERSION

bf_isabella.ptUpstream voice asset sync (bf_isabella) +0/-0

Upstream voice asset sync (bf_isabella)

• Includes upstream voice tensor asset reference as part of v0.6.0 sync.

api/src/voices/v1_0/bf_isabella.pt

Chart.yamlBump Helm chart metadata +2/-2

Bump Helm chart metadata

• Updates chart version/metadata to match upstream v0.6.0 sync.

charts/kokoro-fastapi/Chart.yaml

docker-bake.hclSwitch to optimized Dockerfiles, OCI metadata, and GPU per-arch targets +105/-26

Switch to optimized Dockerfiles, OCI metadata, and GPU per-arch targets

• Adds OCI labels/annotations, switches CPU/GPU to multi-stage optimized Dockerfiles, splits GPU builds by arch/CUDA version, and introduces gpu-cu128 target for Blackwell wheels.

docker-bake.hcl

Dockerfile.optimizedIntroduce multi-stage optimized CPU Dockerfile +85/-0

Introduce multi-stage optimized CPU Dockerfile

• Adds builder/runtime stages using uv venv sync, downloads model and UniDic optionally, and sets up a slim runtime image with appuser ownership.

docker/cpu/Dockerfile.optimized

docker-compose.ymlAdjust CPU compose for new Dockerfile layout +6/-2

Adjust CPU compose for new Dockerfile layout

• Updates compose configuration to align with optimized Dockerfile naming/behavior.

docker/cpu/docker-compose.yml

docker-compose.test.ymlAdd docker-compose integration test harness +52/-0

Add docker-compose integration test harness

• Introduces a compose file that runs the server plus the Whisper-equipped test-client to execute pytest -m integration against the live HTTP API.

docker/docker-compose.test.yml

Dockerfile.optimizedIntroduce multi-stage optimized GPU Dockerfile with CUDA_VERSION/GPU_EXTRA +98/-0

Introduce multi-stage optimized GPU Dockerfile with CUDA_VERSION/GPU_EXTRA

• Adds builder/runtime stages pinned to CUDA images, supports selecting torch wheel extras (gpu vs gpu-cu128), and keeps runtime slim while preserving uv-managed Python interpreter.

docker/gpu/Dockerfile.optimized

docker-compose.ymlAdjust GPU compose for optimized Dockerfile + args +10/-2

Adjust GPU compose for optimized Dockerfile + args

• Updates GPU compose configuration to align with optimized Dockerfile and CUDA/wheel selection behavior.

docker/gpu/docker-compose.yml

DockerfileUpdate ROCm Dockerfile for upstream changes +23/-2

Update ROCm Dockerfile for upstream changes

• Applies upstream tweaks to ROCm build (dependency/layout updates).

docker/rocm/Dockerfile

docker-compose.ymlUpdate ROCm compose configuration +24/-13

Update ROCm compose configuration

• Updates ROCm docker-compose settings to match upstream expectations and scripts.

docker/rocm/docker-compose.yml

warmup_miopen.pyAdd ROCm warmup helper for MIOpen +85/-0

Add ROCm warmup helper for MIOpen

• Adds a warmup script to pre-initialize ROCm/MIOpen behavior for more stable runtime performance.

docker/rocm/warmup_miopen.py

download_model.pyUpdate model download helper +16/-2

Update model download helper

• Adjusts model download script behavior/arguments to match new Docker flow and upstream packaging.

docker/scripts/download_model.py

download_model.shUpdate model download shell wrapper +1/-1

Update model download shell wrapper

• Minor update to keep script aligned with upstream changes.

docker/scripts/download_model.sh

DockerfileAdd integration test-client container (Whisper baked in) +50/-0

Add integration test-client container (Whisper baked in)

• Creates a dedicated image that installs pinned deps via uv and pre-fetches faster-whisper small CT2 weights so integration tests don’t hit Hugging Face at CI time.

docker/test-client/Dockerfile

pyproject.tomlPin test-client dependencies +17/-0

Pin test-client dependencies

• Defines Python deps for the integration test client (httpx/pytest/faster-whisper/jiwer/etc.).

docker/test-client/pyproject.toml

pytest.iniConfigure pytest for integration test-client runs +4/-0

Configure pytest for integration test-client runs

• Adds pytest configuration suitable for containerized integration execution.

docker/test-client/pytest.ini

test_long_form.pyAdd long-form transcription test harness +353/-0

Add long-form transcription test harness

• Adds a script/test to validate long-form synthesis/transcription behavior.

examples/assorted_checks/test_transcription/test_long_form.py

test_transcription.pyAdd transcription test harness +175/-0

Add transcription test harness

• Adds a baseline transcription validation script/test.

examples/assorted_checks/test_transcription/test_transcription.py

test_transcription_multilingual.pyAdd multilingual transcription test harness +244/-0

Add multilingual transcription test harness

• Adds multilingual transcription validation script/test.

examples/assorted_checks/test_transcription/test_transcription_multilingual.py

pyproject.tomlAdd examples project dependencies +37/-0

Add examples project dependencies

• Defines deps needed to run example scripts/benchmarks in a reproducible environment.

examples/pyproject.toml

package.jsonAdd web test/tooling dependencies +19/-0

Add web test/tooling dependencies

• Adds Node dependencies needed for Playwright/unit tests and web build tooling.

package.json

playwright.config.mjsAdd Playwright configuration for web E2E tests +15/-0

Add Playwright configuration for web E2E tests

• Configures Playwright test runner settings for the web player E2E suite.

playwright.config.mjs

pytest.iniAdjust pytest configuration for integration markers +3/-1

Adjust pytest configuration for integration markers

• Updates pytest settings to support integration marker behavior and test discovery alignment with upstream.

pytest.ini

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Unbounded voice cache 🐞 Bug ☼ Reliability
Description
KokoroV1 now stores every loaded voice tensor in _voice_cache keyed by voice_path with no
eviction, so distinct voice-paths accumulate indefinitely. Because combined voices are materialized
to temp files whose names include the full (user-controlled) voice expression (including float
weights), clients can generate unbounded unique keys and exhaust CPU/GPU memory over time.
Code

api/src/inference/kokoro_v1.py[R91-108]

+        self._voice_cache: Dict[str, torch.Tensor] = {}  # Cache voice tensors by path
+
+    async def _get_voice_tensor(self, voice_path: str) -> torch.Tensor:
+        """Load voice tensor with in-memory caching to avoid repeated file I/O.
+
+        Args:
+            voice_path: Path to the .pt voice file
+
+        Returns:
+            Voice tensor on the target device
+        """
+        cache_key = f"{voice_path}:{self._device}"
+        if cache_key not in self._voice_cache:
+            self._voice_cache[cache_key] = await paths.load_voice_tensor(
+                voice_path, device=self._device
+            )
+            logger.debug(f"Cached voice tensor from {voice_path}")
+        return self._voice_cache[cache_key]
Evidence
The new code caches tensors indefinitely in _voice_cache keyed by voice_path, and combined
voices are saved to temp files named from the full voice expression (including weights). Since the
OpenAI-compatible endpoints accept and pass through voice combination expressions, a client can
induce creation of many distinct temp voice paths, each producing a new cache entry and retained
tensor memory.

api/src/inference/kokoro_v1.py[81-109]
api/src/services/tts_service.py[180-254]
api/src/routers/openai_compatible.py[83-133]
api/src/routers/openai_compatible.py[135-158]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`KokoroV1` caches voice tensors in an unbounded dict keyed by `voice_path` (plus device). Since upstream supports combined voices and weights that are incorporated into a temp filename, callers can create an effectively unlimited number of distinct `voice_path` values, causing monotonically increasing CPU/GPU memory usage and eventual OOM.

### Issue Context
- `_voice_cache` retains tensors for the process lifetime unless the model is explicitly unloaded.
- Combined voices are saved under `tempfile.gettempdir()` with filenames derived from the full voice expression string (including weights), so varying weights creates new files/paths.

### Fix Focus Areas
- api/src/inference/kokoro_v1.py[84-109]
- api/src/services/tts_service.py[180-254]

Suggested implementation directions:
1) Replace `_voice_cache` with a bounded LRU (max entries or max bytes). Evict least-recently-used entries.
2) Avoid caching *generated* combined-voice tensors by default (cache only base voices shipped with the service), or canonicalize weights (e.g., round to N decimals) and enforce a max unique combinations per process.
3) Consider tracking and cleaning up combined-voice temp files when they fall out of cache / on a background janitor, so disk usage also remains bounded.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +91 to +108
self._voice_cache: Dict[str, torch.Tensor] = {} # Cache voice tensors by path

async def _get_voice_tensor(self, voice_path: str) -> torch.Tensor:
"""Load voice tensor with in-memory caching to avoid repeated file I/O.

Args:
voice_path: Path to the .pt voice file

Returns:
Voice tensor on the target device
"""
cache_key = f"{voice_path}:{self._device}"
if cache_key not in self._voice_cache:
self._voice_cache[cache_key] = await paths.load_voice_tensor(
voice_path, device=self._device
)
logger.debug(f"Cached voice tensor from {voice_path}")
return self._voice_cache[cache_key]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Unbounded voice cache 🐞 Bug ☼ Reliability

KokoroV1 now stores every loaded voice tensor in _voice_cache keyed by voice_path with no
eviction, so distinct voice-paths accumulate indefinitely. Because combined voices are materialized
to temp files whose names include the full (user-controlled) voice expression (including float
weights), clients can generate unbounded unique keys and exhaust CPU/GPU memory over time.
Agent Prompt
### Issue description
`KokoroV1` caches voice tensors in an unbounded dict keyed by `voice_path` (plus device). Since upstream supports combined voices and weights that are incorporated into a temp filename, callers can create an effectively unlimited number of distinct `voice_path` values, causing monotonically increasing CPU/GPU memory usage and eventual OOM.

### Issue Context
- `_voice_cache` retains tensors for the process lifetime unless the model is explicitly unloaded.
- Combined voices are saved under `tempfile.gettempdir()` with filenames derived from the full voice expression string (including weights), so varying weights creates new files/paths.

### Fix Focus Areas
- api/src/inference/kokoro_v1.py[84-109]
- api/src/services/tts_service.py[180-254]

Suggested implementation directions:
1) Replace `_voice_cache` with a bounded LRU (max entries or max bytes). Evict least-recently-used entries.
2) Avoid caching *generated* combined-voice tensors by default (cache only base voices shipped with the service), or canonicalize weights (e.g., round to N decimals) and enforce a max unique combinations per process.
3) Consider tracking and cleaning up combined-voice temp files when they fall out of cache / on a background janitor, so disk usage also remains bounded.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b291abe69e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +475 to +479
"id": "gpt-4o-mini-tts",
"object": "model",
"created": 1686935002,
"owned_by": "kokoro",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the advertised model retrievable

When a client follows the model ID added here and calls GET /v1/models/gpt-4o-mini-tts, retrieve_model() still uses a separate hard-coded map that omits this ID, so the endpoint returns 404 even though /v1/models advertises it and speech requests accept it via openai_mappings.json. Please add the same entry to retrieval or derive both responses from one model list.

Useful? React with 👍 / 👎.

WORKDIR /app

# Copy uv-managed Python interpreter (the venv's bin/python symlinks into here)
COPY --from=builder /opt/uv-python /opt/uv-python

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop copying an interpreter directory that uv never creates

The builder stage installs Python before running uv venv --python 3.10, so uv uses that system interpreter rather than installing a managed one under UV_PYTHON_INSTALL_DIR; in that normal path /opt/uv-python is never created, and this COPY --from=builder /opt/uv-python ... makes every GPU optimized image build fail before the app is copied. Either force a uv-managed Python or drop this copy and rely on the runtime Python already installed in the next stage.

Useful? React with 👍 / 👎.

Comment on lines +106 to +107
if self._backend:
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry lazy reloads after a failed load

When the first request after /dev/unload hits a load error, initialize() has already assigned _backend and load_model() raises without clearing it. Every later call then takes this early return even though the backend's model is still unloaded, so generation keeps failing with Model not loaded instead of retrying the lazy reload; clear _backend on load failure or check is_loaded here.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants