Skip to content

Reuse TTS synthesizer in API server#212

Draft
CAMOBAP wants to merge 2 commits into
dnhkng:mainfrom
CAMOBAP:fix/api-tts-singleton
Draft

Reuse TTS synthesizer in API server#212
CAMOBAP wants to merge 2 commits into
dnhkng:mainfrom
CAMOBAP:fix/api-tts-singleton

Conversation

@CAMOBAP

@CAMOBAP CAMOBAP commented Jul 12, 2026

Copy link
Copy Markdown

Summary

  • Cache SpeechSynthesizer and SpokenTextConverter when reuse_tts is enabled (default)
  • Preload both during Litestar startup via a lifespan hook
  • Add ApiConfig + configs/api_config.yaml and GLADOS_API_REUSE_TTS env override
  • reuse_tts: false restores the previous per-request model loading behavior

The main Glados engine already creates one synthesizer at startup and reuses it in TextToSpeechSynthesizer. The thin API wrapper did not, so each HTTP call reloaded the ONNX session before inference.

Configuration

# configs/api_config.yaml
Api:
  reuse_tts: true
GLADOS_API_REUSE_TTS=false ./scripts/serve

Test plan

  • pytest tests/test_api_config.py
  • Start API: ./scripts/serve
  • Call /v1/audio/speech twice with reuse_tts: true; second request should skip ONNX reload
  • Start with reuse_tts: false; each request should reload the ONNX session
  • Confirm MP3/WAV output still sounds correct

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The TTS API adds configurable synthesizer reuse, YAML and environment-based configuration, startup warming through a Litestar lifespan handler, and application-factory entrypoints for container and script launches.

Changes

Configurable TTS API startup

Layer / File(s) Summary
API configuration and validation
src/glados/api/config.py, configs/api_config.yaml, tests/test_api_config.py
ApiConfig loads reuse_tts from YAML or environment variables, caches the active configuration, and has coverage for defaults, overrides, and reset behavior.
Cached TTS factories
src/glados/api/tts.py
Cached synthesizer and converter factories support reuse and startup warming, while audio generation selects cached or per-call instances based on reuse_tts.
Application lifespan registration
src/glados/api/app.py
create_app() registers a lifespan handler that calls warm_tts() when reuse is enabled before serving requests.
API launch wiring and documentation
Dockerfile, scripts/serve, README.md
Runtime entrypoints use create_app, and documentation describes the GLaDOS endpoint and TTS reuse controls.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Server
  participant lifespan
  participant get_api_config
  participant warm_tts
  participant TTSFactories
  Server->>lifespan: start Litestar application
  lifespan->>get_api_config: read reuse_tts
  get_api_config-->>lifespan: return configuration
  lifespan->>warm_tts: initialize cached TTS resources when enabled
  warm_tts->>TTSFactories: create synthesizer and converter
  TTSFactories-->>warm_tts: return initialized resources
  warm_tts-->>lifespan: return
  lifespan-->>Server: serve requests
Loading

Possibly related PRs

  • dnhkng/GLaDOS#107: Introduced and integrated SpokenTextConverter into the TTS pipeline used by this change.
  • dnhkng/GLaDOS#127: Introduced write_glados_audio_file, which this change updates with cached TTS reuse.
  • dnhkng/GLaDOS#151: Modified synthesizer instantiation in the same TTS audio-generation path.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: caching and reusing the TTS synthesizer in the API server.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@CAMOBAP
CAMOBAP force-pushed the fix/api-tts-singleton branch from 643ad8f to 0d2f5df Compare July 12, 2026 21:04
@CAMOBAP
CAMOBAP marked this pull request as draft July 12, 2026 21:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/glados/api/app.py (1)

58-62: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider offloading warm_tts() to a thread executor.

warm_tts() is a synchronous blocking call (ONNX session loading) inside an async context manager. While this runs during startup before any requests are served, it blocks the event loop and could delay ASGI lifespan completion, affecting orchestrator startup probes. Wrapping it in asyncio.to_thread() would keep the event loop responsive.

This is low-priority since the block is one-time and startup-only.

♻️ Optional refactor
     `@asynccontextmanager`
     async def lifespan(app: Litestar) -> AsyncIterator[None]:
         if get_api_config().reuse_tts:
-            warm_tts()
+            await asyncio.to_thread(warm_tts)
         yield
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/glados/api/app.py` around lines 58 - 62, Update the lifespan function to
offload the synchronous warm_tts call to a worker thread using
asyncio.to_thread, awaiting its completion before yield; add the required
asyncio import while preserving the existing reuse_tts condition.
tests/test_api_config.py (1)

48-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add test for existing YAML file with missing expected key

The test suite covers missing files but not the case where a YAML file exists yet lacks the "Api" top-level key. This is the exact edge case that triggers the KeyError identified in config.py lines 49-53. Adding a test here would verify the fix and prevent regression.

🧪 Suggested test
 def test_api_config_skips_missing_files() -> None:
     config = ApiConfig.from_yaml("/path/that/does/not/exist.yaml")
 
     assert config.reuse_tts is True
+
+
+def test_api_config_skips_missing_key(tmp_path: Path) -> None:
+    config_file = tmp_path / "api_config.yaml"
+    config_file.write_text("Other:\n  foo: bar\n", encoding="utf-8")
+
+    config = ApiConfig.from_yaml(config_file)
+
+    assert config.reuse_tts is True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_api_config.py` around lines 48 - 51, Add a test alongside
test_api_config_skips_missing_files that creates or loads an existing YAML file
without the top-level “Api” key, then asserts ApiConfig.from_yaml returns the
expected default configuration, including reuse_tts being True. Ensure the test
exercises the missing-key path rather than the nonexistent-file path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/glados/api/config.py`:
- Around line 49-53: Update the configuration traversal in the method containing
data, key_to_config, and config.update so each nested lookup safely handles
missing or non-dictionary YAML values by using an empty-dictionary fallback
instead of direct indexing. Preserve the existing config.update flow so missing
keys and empty YAML resolve through the model’s defaults without raising
KeyError or type errors.

---

Nitpick comments:
In `@src/glados/api/app.py`:
- Around line 58-62: Update the lifespan function to offload the synchronous
warm_tts call to a worker thread using asyncio.to_thread, awaiting its
completion before yield; add the required asyncio import while preserving the
existing reuse_tts condition.

In `@tests/test_api_config.py`:
- Around line 48-51: Add a test alongside test_api_config_skips_missing_files
that creates or loads an existing YAML file without the top-level “Api” key,
then asserts ApiConfig.from_yaml returns the expected default configuration,
including reuse_tts being True. Ensure the test exercises the missing-key path
rather than the nonexistent-file path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 411d0e2e-14ce-4c6c-a745-19fe0d0a02f8

📥 Commits

Reviewing files that changed from the base of the PR and between 441c354 and 0d2f5df.

📒 Files selected for processing (8)
  • Dockerfile
  • README.md
  • configs/api_config.yaml
  • scripts/serve
  • src/glados/api/app.py
  • src/glados/api/config.py
  • src/glados/api/tts.py
  • tests/test_api_config.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/glados/api/tts.py

Comment thread src/glados/api/config.py Outdated
Comment on lines +49 to +53
data = data or {}
for key in key_to_config:
data = data[key]

config.update(data)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unhandled KeyError when YAML file lacks the expected top-level key

data[key] on line 51 raises an unhandled KeyError if a YAML file exists but doesn't contain the expected key (e.g., "Api"). This also affects empty YAML files where yaml.safe_load returns Nonedata or {} produces {}, and the subsequent data["Api"] lookup crashes. The method docstring states "Missing files are skipped" but missing keys in existing files are not handled gracefully.

🛡️ Proposed fix: use `dict.get` with empty-dict fallback
             data = data or {}
             for key in key_to_config:
-                data = data[key]
+                if not isinstance(data, dict):
+                    data = {}
+                    break
+                data = data.get(key, {})

This skips missing keys (returning defaults via model_validate({})) and also guards against non-dict YAML values (e.g., a bare scalar or list).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
data = data or {}
for key in key_to_config:
data = data[key]
config.update(data)
data = data or {}
for key in key_to_config:
if not isinstance(data, dict):
data = {}
break
data = data.get(key, {})
config.update(data)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/glados/api/config.py` around lines 49 - 53, Update the configuration
traversal in the method containing data, key_to_config, and config.update so
each nested lookup safely handles missing or non-dictionary YAML values by using
an empty-dictionary fallback instead of direct indexing. Preserve the existing
config.update flow so missing keys and empty YAML resolve through the model’s
defaults without raising KeyError or type errors.

@CAMOBAP
CAMOBAP force-pushed the fix/api-tts-singleton branch from a4df594 to 8e83ebe Compare July 12, 2026 21:13
CAMOBAP and others added 2 commits July 12, 2026 23:16
The OpenAI-compatible TTS API recreated SpeechSynthesizer and
SpokenTextConverter on every request, reloading the ONNX session each
time. Cache both at module scope and warm them during Litestar startup,
matching the main Glados engine behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce ApiConfig (model-only subconfig) and configs/api_config.yaml
with reuse_tts defaulting to true, plus GLADOS_API_REUSE_TTS env override.
When enabled, cache SpeechSynthesizer and warm it at Litestar startup;
when disabled, keep per-request ONNX loading. Update scripts/serve and
Docker to use create_app.

Co-authored-by: Cursor <cursoragent@cursor.com>
@CAMOBAP
CAMOBAP force-pushed the fix/api-tts-singleton branch from 8e83ebe to 6ebf83d Compare July 12, 2026 21:16
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.

1 participant