Skip to content

feat(deepgram): support speed parameter in Flux TTS - #7483

Merged
davidzhao merged 3 commits into
livekit:mainfrom
73bruno:feat/deepgram-flux-tts-speed
Oct 17, 2026
Merged

davidzhao merged 3 commits into
livekit:mainfrom
73bruno:feat/deepgram-flux-tts-speed

Conversation

@73bruno

@73bruno 73bruno commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #7480, for Flux TTS (TTSv2, /v2/speak).

Flux takes a speed parameter from 0.5 to 1.5 in 0.05 steps (docs) on both wss://…/v2/speak and POST /v2/speak, but TTSv2 never sent it.

This adds speed to TTSv2.__init__ and update_options and sends it on both paths when set, the same way as in the v1 TTS:

  • stream() (WebSocket): added to the connection URL; update_options(speed=...) invalidates the pool like the other URL params.
  • synthesize() (HTTP): added to the request params.
  • The default is None, which leaves it out of the request.
  • Out-of-range values and values off the 0.05 step raise ValueError up front. The API rejects both with a 400, but on the WebSocket path that only surfaces as a failed handshake.

Tested against the live API (flux-alexis-en, the same sentence, speech length without the trailing silence Flux adds, median of 3 runs):

speed synthesize() stream()
not set 2.94 s 2.96 s
0.7 4.46 s 4.68 s
1.3 2.58 s 2.64 s
1.5 2.24 s 2.56 s

update_options(speed=1.5) on a TTS with a pooled socket took the next streams from 3.58 s to 2.20 s. 0.45, 1.55 and 1.12 return 400 from the API.

Unit tests cover both request paths, the default, the range and step checks and pool invalidation.

Flux can also change speed on an open socket with a Configure message, without reconnecting. I kept to the URL and pool invalidation to match the other options; happy to switch update_options to Configure if you'd prefer that.

Follow-up to livekit#7480 for TTSv2 (/v2/speak). Flux takes `speed` from 0.5 to 1.5
in 0.05 steps on both the WebSocket and the REST endpoint; the plugin never
sent it.

Add `speed` to `TTSv2.__init__` and `update_options`, check the range and the
step up front, and send it on both the streaming (WebSocket URL) and the
synthesize (HTTP) path when set, the same way as in the v1 TTS.
@73bruno
73bruno requested a review from a team as a code owner September 26, 2026 08:50

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

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 2 potential issues.

Devin Review

Comment on lines +184 to +185
if self._opts.speed is not None:
config["speed"] = self._opts.speed

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.

🔴 Existing Flux streams change speaking rate

When update_options changes speed before a pending stream connects, _connect_ws uses the new speed instead of the stream's snapshot. An active stream can also change rate between segments after pool invalidation.

Learn more

A Flux stream copies its options when constructed in SynthesizeStreamv2, but the shared connection callback reads the TTS instance's current speed. A stream that has not acquired a socket yet therefore uses a later speed. A stream already running can acquire another socket for its next flushed text segment, and pool invalidation forces that new socket to use the later speed. The emitter and other stream settings still use the original snapshot.

Example: Create a stream at speed 0.7, then call update_options(speed=1.5) before its first text segment. Its WebSocket requests 1.5 even though that stream captured 0.7; a stream spanning the update can speak its first and second segments at different rates.

Recommended fix: Pass the stream's copied connection parameters to the pool acquisition or otherwise bind each stream to a consistent speed across its segments. Ensure invalidation still affects new streams without changing the settings of existing ones.

Devin Review


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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This matches how the other connection params (model, encoding, sample_rate, bit_rate) already work here and in the v1 TTS (#7480): they are baked into the pooled socket, and update_options invalidates the pool so the next socket picks up the new value. For speed, picking up the new rate at the next segment is what a caller of update_options(speed=...) expects. Binding each stream to its own socket params would change how the pool works for every option, so I would keep it out of this PR.

A speed within the step tolerance but not exactly on it (1.00000001, or
1.5000000000000002 from float arithmetic) passed the check and was sent as is,
so Deepgram rejected it. Snap it to the step before checking the range and
store that value, so both request paths send e.g. 1.5.
devin-ai-integration[bot]

This comment was marked as resolved.

Check the range, with a little slack for float noise, before snapping to the 0.05 step, so inf, nan and huge values raise ValueError instead of OverflowError.
@davidzhao
davidzhao merged commit ceb3d59 into livekit:main Oct 17, 2026
17 checks passed
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