Skip to content

feat(soniox): add TTS speed support#2005

Open
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
meters-crane-brazen
Open

feat(soniox): add TTS speed support#2005
rosetta-livekit-bot[bot] wants to merge 1 commit into
mainfrom
meters-crane-brazen

Conversation

@rosetta-livekit-bot

@rosetta-livekit-bot rosetta-livekit-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add Soniox TTS support to the JS plugin with the source PR's speed option, validation range [0.7, 1.3], and WebSocket payload forwarding.
  • Export the new TTS API and update the API report.
  • Add a minor changeset for the new public Soniox TTS API.

Source diff coverage

Coverage classification
Source file Classification Target coverage
livekit-plugins/livekit-plugins-soniox/livekit/plugins/soniox/tts.py Adapted Target had no Soniox TTS counterpart. Ported the needed Soniox TTS infrastructure into plugins/soniox/src/tts.ts, exported it from plugins/soniox/src/index.ts, updated plugins/soniox/etc/agents-plugin-soniox.api.md, and added .changeset/soft-tigers-speak.md. The source PR's speed default, range validation, update option, and WebSocket speed field are included.

No source test files were added or modified by the source diff, so no new tests were ported.

Notes

  • The Python Soniox TTS implementation can request non-PCM audio_format values and pass encoded audio through its audio emitter. The JS TTS provider interface emits AudioFrames, so this port explicitly supports and validates the source default pcm_s16le; other audioFormat values throw instead of being silently mis-decoded.

Verification

  • pnpm --filter @livekit/agents-plugin-soniox build
  • pnpm --filter @livekit/agents-plugin-soniox api:check
  • pnpm --filter @livekit/agents-plugin-soniox lint
  • pnpm test plugins/soniox
  • pnpm build
  • pnpm 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__ and update_options, validate it against the supported range, and include it in the config message sent over the WebSocket, consistent with how bitrate is handled.

@rosetta-livekit-bot rosetta-livekit-bot Bot requested a review from a team as a code owner July 9, 2026 05:37
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a42f520

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 36 packages
Name Type
@livekit/agents-plugin-soniox Major
@livekit/agents Major
@livekit/agents-plugin-anam Major
@livekit/agents-plugin-anthropic Major
@livekit/agents-plugin-assemblyai Major
@livekit/agents-plugin-baseten Major
@livekit/agents-plugin-bey Major
@livekit/agents-plugin-cartesia Major
@livekit/agents-plugin-cerebras Major
@livekit/agents-plugin-deepgram Major
@livekit/agents-plugin-did Major
@livekit/agents-plugin-elevenlabs Major
@livekit/agents-plugin-fishaudio Major
@livekit/agents-plugin-google Major
@livekit/agents-plugin-hedra Major
@livekit/agents-plugin-hume Major
@livekit/agents-plugin-inworld Major
@livekit/agents-plugin-lemonslice Major
@livekit/agents-plugin-liveavatar Major
@livekit/agents-plugin-livekit Major
@livekit/agents-plugin-minimax Major
@livekit/agents-plugin-mistral Major
@livekit/agents-plugin-mistralai Major
@livekit/agents-plugin-neuphonic Major
@livekit/agents-plugin-openai Major
@livekit/agents-plugin-perplexity Major
@livekit/agents-plugin-phonic Major
@livekit/agents-plugin-resemble Major
@livekit/agents-plugin-rime Major
@livekit/agents-plugin-runway Major
@livekit/agents-plugin-sarvam Major
@livekit/agents-plugin-silero Major
@livekit/agents-plugin-tavus Major
@livekit/agents-plugin-trugen Major
@livekit/agents-plugin-xai Major
@livekit/agents-plugins-test Major

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

@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 2 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment thread plugins/soniox/src/tts.ts
Comment on lines +275 to +277
} finally {
this.queue.close();
}

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.

🟡 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.

Suggested change
} finally {
this.queue.close();
}
} finally {
connection!?.unregisterStream(streamId);
this.queue.close();
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread plugins/soniox/src/tts.ts
Comment on lines +622 to +623
}
return;

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.

🟡 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.

Suggested change
}
return;
)
}
this.#streams.delete(streamId);
return;
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

0 participants