Skip to content

feat: add Lokutor TTS plugin - #7256

Open
danivs10 wants to merge 1 commit into
livekit:mainfrom
danivs10:add-lokutor-tts-plugin
Open

feat: add Lokutor TTS plugin#7256
danivs10 wants to merge 1 commit into
livekit:mainfrom
danivs10:add-lokutor-tts-plugin

Conversation

@danivs10

Copy link
Copy Markdown

Summary

Adds livekit-plugins-lokutor, a TTS integration for Lokutor β€” a CPU-only voice-AI platform (no GPU) with 10 voices (F1–F5, M1–M5) and 32 languages.

The plugin connects to Lokutor's WebSocket TTS API and supports both streaming (stream()) and one-shot (synthesize()) synthesis, following the standard LiveKit plugin conventions:

  • utils.ConnectionPool for pooled/prewarmed WebSocket connections (prewarm() supported)
  • tts.AudioEmitter for streaming PCM frames
  • Typed error mapping (APIStatusError / APITimeoutError / APIConnectionError)
  • py.typed (PEP 561)

Usage

from livekit.plugins import lokutor

session = AgentSession(
    stt=deepgram.STT(),
    llm=openai.LLM(),
    tts=lokutor.TTS(voice="F1", language="en"),  # LOKUTOR_API_KEY from env
    vad=silero.VAD.load(),
)

Testing

  • 25 unit tests (tests/test_plugin_lokutor_tts.py) covering config, request building, and stream construction.
  • Gated live-integration tests (tests/test_integration.py) that run against the real API when LOKUTOR_API_KEY is set, exercising the full connect β†’ request β†’ binary audio β†’ EOS path.
  • ruff check and ruff format --check pass.

Notes

Supersedes #5925 (that PR was opened from main, had drifted into conflicts, and shipped an older revision). This one is a clean feature branch on top of current main and uses Lokutor's current canonical request vocabulary (voice / language / speed / steps), with all 32 languages. CLA is already signed.

@danivs10
danivs10 requested a review from a team as a code owner September 13, 2026 01:04
@CLAassistant

CLAassistant commented Sep 13, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@danivs10 danivs10 mentioned this pull request Sep 13, 2026
10 tasks
devin-ai-integration[bot]

This comment was marked as resolved.

@danivs10

Copy link
Copy Markdown
Author

Thanks for the review β€” addressed the flagged issues in the latest commit:

  • Custom sample rates corrupt audio β€” Lokutor's TTS output is a fixed 44.1 kHz, so the configurable sample_rate was removed and the rate is pinned to 44100 (the LiveKit pipeline resamples downstream if needed).
  • Provider failures lose typed details β€” _run now re-raises APIError/APIStatusError instead of collapsing them into a generic APIConnectionError.
  • API key in WebSocket URL β€” the key is now sent via an X-API-Key header instead of the URL query string.
  • Provider content in logs / exceptions β€” unexpected text frames and provider error messages are no longer interpolated into log lines or exception messages.

ruff check, ruff format, and the unit tests pass locally.

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 new potential issue.

3 flags not posted on this PR by your GitHub settings β€” view them in Devin Review. (Configure)

Devin Review

Comment on lines +137 to +140
session.ws_connect(
self._opts.get_ws_url(),
max_msg_size=0,
headers={"X-API-Key": self._opts.api_key},

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.

πŸ”΄ Header authentication blocks all synthesis

Every TTS request sends X-API-Key, but Lokutor's WebSocket handshake accepts only the api_key query parameter. The upgrade fails before either synthesis path can emit audio.

Learn more

Lokutor authenticates /ws/tts during the WebSocket handshake. Its authentication and streaming API documentation specifies ?api_key=... for WebSockets; X-API-Key is documented for REST endpoints. The new request reaches the correct endpoint without the only supported WebSocket credential, so the server rejects it before any JSON synthesis request is sent.

Example: With api_key="sk_test", the plugin connects to wss://api.lokutor.com/ws/tts with an X-API-Key header. Lokutor expects wss://api.lokutor.com/ws/tts?api_key=sk_test, so both stream() and synthesize() fail during connection setup instead of returning audio.

Recommended fix: Confirm and adopt a Lokutor-supported WebSocket authentication mechanism that keeps credentials out of observable URLs. If header authentication is unavailable, coordinate provider support or add explicit sanitization around every URL-bearing exception and telemetry path before retaining query authentication.

Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Streaming + one-shot TTS integration for Lokutor (CPU-only voice AI, 10 voices,
32 languages) over Lokutor's WebSocket API. Follows the LiveKit plugin
conventions (ConnectionPool, AudioEmitter, prewarm, typed error mapping),
ships py.typed, sends the API key via header, pins the fixed 44.1 kHz output,
and includes unit tests plus gated live-integration tests. Registered in the
root workspace sources + uv.lock.
@danivs10
danivs10 force-pushed the add-lokutor-tts-plugin branch from 0327952 to acaf2d1 Compare September 13, 2026 07:40
@danivs10

Copy link
Copy Markdown
Author

Re: the "Header authentication blocks all synthesis" flag β€” this is a false positive. Lokutor's WebSocket handshake does accept the X-API-Key header; the public docs previously only mentioned the ?api_key= query parameter (now updated). The server reads X-API-Key first and falls back to the query param.

Verified against production wss://api.lokutor.com/ws/tts:

  • Invalid key via X-API-Key header β†’ 401 {"code":"auth.invalid_key"} (the header was read and the key evaluated)
  • No auth β†’ 401 {"code":"auth.missing_key"}

If the header were ignored, the header case would return auth.missing_key; it returns auth.invalid_key, confirming the header is honored. Synthesis is unaffected.

The remaining red ruff / type-check checks are unrelated to this plugin β€” they fail on a pre-existing Duplicate keyword argument "secondary_languages" syntax error in livekit-plugins-elevenlabs on the base branch. This plugin's own ruff, type-check, and unit tests pass.

@danivs10

Copy link
Copy Markdown
Author

This is ready for a maintainer review whenever someone has a moment πŸ™

Quick status recap:

  • CLA: signed βœ…
  • Plugin ruff, type-check, and unit-tests pass βœ…
  • The failing top-level ruff / type-check runs are not from this plugin β€” they fail on a pre-existing Duplicate keyword argument "secondary_languages" syntax error in livekit-plugins-elevenlabs on main, which breaks those jobs for every open PR right now. Happy to rebase the moment that's fixed on main.

Lokutor is a CPU-only TTS provider (10 voices, 32 languages); the plugin follows the standard conventions (ConnectionPool, AudioEmitter, prewarm, typed errors, py.typed) with unit + gated live-integration tests. Thanks for taking a look!

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.

2 participants