feat: add Lokutor TTS plugin#5925
Conversation
Adds a new TTS plugin for Lokutor (lokutor.com), a cost-effective CPU-based TTS provider supporting 10 voices and 30+ languages. - Implements TTS, ChunkedStream, and SynthesizeStream using persistent WebSocket connections via ConnectionPool - Supports streaming and non-streaming synthesis - Auto-reads LOKUTOR_API_KEY environment variable - 25 unit tests covering configuration, defaults, and API request building - Follows existing plugin conventions (pyproject.toml, package.json, Plugin registration, Google-style docstrings)
| except Exception as e: | ||
| raise APIConnectionError() from e |
There was a problem hiding this comment.
π΄ except Exception catches and wraps APIStatusError/APIError raised inside the try block (SynthesizeStream)
Same issue as in ChunkedStream._run, but in SynthesizeStream._run. The code raises APIStatusError (line 295) and APIError (line 310) inside the try block, but these are caught by except Exception as e at line 325 and wrapped in APIConnectionError. This loses the original error information and changes the retryable flag, causing the base class retry logic (livekit-agents/livekit/agents/tts/tts.py:500-535) to incorrectly retry non-retryable errors.
Was this helpful? React with π or π to provide feedback.
| except Exception as e: | ||
| raise APIConnectionError() from e |
There was a problem hiding this comment.
π΄ except Exception catches and wraps APIStatusError/APIError raised inside the try block
In ChunkedStream._run, the code raises APIStatusError (line 220) when the WebSocket closes unexpectedly, and APIError (line 235) when the server returns an error message. Both of these inherit from Exception (APIStatusError β APIError β Exception), so they are caught by the except Exception as e clause at line 246 and incorrectly wrapped inside an APIConnectionError. This loses the original error type, status code, and message. Critically, APIConnectionError defaults to retryable=True, so a non-retryable error (e.g., a 401 from the server) would become retryable, causing unnecessary retry loops in the base class's _main_task (livekit-agents/livekit/agents/tts/tts.py:286-498). Other plugins like Cartesia avoid this because they don't raise APIError/APIStatusError inside the same try block that has except Exception.
Was this helpful? React with π or π to provide feedback.
| if isinstance(data, self._FlushSentinel): | ||
| output_emitter.end_segment() | ||
| continue |
There was a problem hiding this comment.
π‘ SynthesizeStream incorrectly calls end_segment() on FlushSentinel, deviating from all other TTS plugins
At line 291, output_emitter.end_segment() is called when a _FlushSentinel is received. No other TTS plugin in the codebase does this β all other plugins either skip the sentinel with continue (Soniox soniox/tts.py:285-286, Baseten baseten/tts.py:255-256) or flush a tokenizer stream (Cartesia, ElevenLabs, etc.). This causes end_segment() to be called twice: once at line 291 (on FlushSentinel) and again at line 333 (after the loop exits). The second call is a no-op in the current AudioEmitter. More importantly, after end_segment() at line 291, the segment is ended but no new start_segment() is called β if the framework ever allows text after a flush, subsequent output_emitter.push() calls would trigger RuntimeError: "start_segment() must be called before pushing audio data" (livekit-agents/livekit/agents/tts/tts.py:1149-1151).
Was this helpful? React with π or π to provide feedback.
Description
Adds a new Text-to-Speech plugin for Lokutor (https://lokutor.com), a cost-effective CPU-based TTS provider supporting 10 voices (F1-F5, M1-M5) and 30+ languages.
Files added
Files modified
pyproject.toml: addedlivekit-plugins-lokutor = { workspace = true }to[tool.uv.sources]Implementation details
tts.TTSwith WebSocket connection pooling (utils.ConnectionPool) for persistent connectionssynthesize()βChunkedStreamandstream()βSynthesizeStreamwss://api.lokutor.com/ws/ttswith per-chunk request-response protocolLOKUTOR_API_KEYenvironment variableAPITimeoutError,APIStatusError,APIConnectionErrorTest instructions
export LOKUTOR_API_KEY=your-key uv run --with pytest --with pytest-asyncio python -m pytest tests/ -vAll 25 tests pass. Also tested live: synthesizes a 3.8s audio file correctly.
Checklist
Plugin.register_plugin()modelandproviderproperties implementedaclose()properly cleans up resourcespackage.jsonincluded for CI