feat(soniox): add TTS speed support#2005
Conversation
🦋 Changeset detectedLatest commit: a42f520 The changes in this PR will be included in the next version bump. This PR includes changesets to release 36 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| } finally { | ||
| this.queue.close(); | ||
| } |
There was a problem hiding this comment.
🟡 Non-streaming speech synthesis leaks its registration on the shared connection, preventing connection cleanup
The non-streaming synthesis path never removes its registration from the shared connection (unregisterStream is never called in ChunkedStream.run() at plugins/soniox/src/tts.ts:275-277), so the connection's internal map retains a stale entry after the request completes or errors.
Impact: The shared WebSocket connection can never auto-close when it becomes idle, leaking resources for the lifetime of the TTS instance.
Missing cleanup in ChunkedStream.run() finally block
Compare SynthesizeStream.run() which correctly calls this.#connection.unregisterStream(this.#streamId) in its finally block at plugins/soniox/src/tts.ts:354-355. The ChunkedStream.run() finally block at lines 275-277 only closes the queue but never unregisters the stream.
The stream is registered at plugins/soniox/src/tts.ts:248 via connection.registerStream(streamId, ...). On the happy path, the server sends a terminated message which causes #handleMessage to delete the stream at plugins/soniox/src/tts.ts:649. However, on the error path (server sends error_code at lines 612-623), the stream is NOT deleted — the waiter is rejected and the method returns early at line 623 before reaching the terminated handler.
Since Connection.markNonCurrent() at line 454 and Connection.unregisterStream() at line 472 both check this.#streams.size === 0 before auto-closing, a leaked stream entry prevents the connection from ever being cleaned up.
| } finally { | |
| this.queue.close(); | |
| } | |
| } finally { | |
| connection!?.unregisterStream(streamId); | |
| this.queue.close(); | |
Was this helpful? React with 👍 or 👎 to provide feedback.
| } | ||
| return; |
There was a problem hiding this comment.
🟡 Server-side stream error leaves a stale entry in the connection's stream map, leaking resources
When the server reports a per-stream error, the stream's completion signal is rejected but the stream entry is never removed from the connection's internal map (#handleMessage at plugins/soniox/src/tts.ts:623 returns early without deleting the stream), so the connection retains a stale reference.
Impact: After a server-side stream error, the shared WebSocket connection cannot auto-close when idle, causing a resource leak.
Error path missing stream cleanup in #handleMessage
At plugins/soniox/src/tts.ts:612-623, when response.error_code is present, the handler rejects the stream's waiter and returns immediately. Compare with the terminated handler at lines 635-650 which correctly calls this.#streams.delete(streamId) at line 649.
The missing deletion means the stream stays in this.#streams even though it's effectively dead (waiter is done). This prevents markNonCurrent() (line 454) and unregisterStream() (line 472) from auto-closing the connection because they check this.#streams.size === 0.
For SynthesizeStream, this is partially mitigated because its finally block calls unregisterStream. For ChunkedStream (see BUG-0001), there is no such mitigation.
| } | |
| return; | |
| ) | |
| } | |
| this.#streams.delete(streamId); | |
| return; |
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
speedoption, validation range[0.7, 1.3], and WebSocket payload forwarding.Source diff coverage
Coverage classification
livekit-plugins/livekit-plugins-soniox/livekit/plugins/soniox/tts.pyplugins/soniox/src/tts.ts, exported it fromplugins/soniox/src/index.ts, updatedplugins/soniox/etc/agents-plugin-soniox.api.md, and added.changeset/soft-tigers-speak.md. The source PR'sspeeddefault, range validation, update option, and WebSocketspeedfield are included.No source test files were added or modified by the source diff, so no new tests were ported.
Notes
audio_formatvalues and pass encoded audio through its audio emitter. The JS TTS provider interface emitsAudioFrames, so this port explicitly supports and validates the source defaultpcm_s16le; otheraudioFormatvalues throw instead of being silently mis-decoded.Verification
pnpm --filter @livekit/agents-plugin-soniox buildpnpm --filter @livekit/agents-plugin-soniox api:checkpnpm --filter @livekit/agents-plugin-soniox lintpnpm test plugins/sonioxpnpm buildpnpm lint(passes; reports existing warnings in unrelated packages)Ported from livekit/agents#6339
Original PR description
The Soniox TTS WebSocket API accepts a speed field in the stream config to control the speaking rate (supported range 0.7–1.3, default 1.0), but the plugin never exposed it, so there was no way to change the speaking rate.
Add speed to
TTS.__init__andupdate_options, validate it against the supported range, and include it in the config message sent over the WebSocket, consistent with howbitrateis handled.