Skip to content

feat(smallestai): add TTS continuations protocol support - #7111

Open
harshitajain165 wants to merge 2 commits into
livekit:mainfrom
harshitajain165:feat/smallest-tts-continuations
Open

feat(smallestai): add TTS continuations protocol support#7111
harshitajain165 wants to merge 2 commits into
livekit:mainfrom
harshitajain165:feat/smallest-tts-continuations

Conversation

@harshitajain165

Copy link
Copy Markdown
Contributor

What

Smallest AI's WebSocket TTS API recently added a "continuations" protocol (context_id-based) that releases buffered text at natural sentence boundaries instead of holding everything until an explicit flush, and primes each release with the previous one's audio so prosody stays continuous across fragments.

SynthesizeStream now defaults to this protocol instead of the legacy continue/flush fields:

  • New use_continuations (default True) and max_buffer_delay_ms options; max_buffer_flush_ms is kept for the use_continuations=False fallback.
  • Continuations can emit multiple complete frames per context with no terminal marker, so _recv_task drains on a short idle gap after the closing fragment instead of returning on the first complete.
  • Best-effort cancel_request cleanup if a stream is interrupted (e.g. barge-in) mid-context, so an open context isn't left behind on a pooled connection that gets reused for the next turn.

Testing

  • ruff format / ruff check / mypy pass.
  • Hermetic unit tests updated and extended (tests/test_plugin_smallestai_tts.py, kept local): legacy protocol path, continuations-is-default path, and a new test confirming cancel_request is sent when a stream is interrupted mid-context.
  • Verified live against the real API in both continuations and legacy modes.
  • Ran a full console voice-agent session (Pulse STT + Electron LLM + Lightning TTS) including a real user interruption mid-response, with no errors.

Smallest AI's WebSocket TTS API recently added a "continuations" protocol
(context_id-based) that releases buffered text at natural sentence
boundaries instead of holding everything until an explicit flush, and
primes each release with the previous one's audio so prosody stays
continuous across fragments.

SynthesizeStream now defaults to this protocol instead of the legacy
continue/flush fields:
- New use_continuations (default True) and max_buffer_delay_ms options;
  max_buffer_flush_ms is kept for the use_continuations=False fallback.
- Continuations can emit multiple `complete` frames per context with no
  terminal marker, so _recv_task drains on a short idle gap after the
  closing fragment instead of returning on the first `complete`.
- Best-effort cancel_request cleanup if a stream is interrupted (e.g.
  barge-in) mid-context, so an open context isn't left behind on a
  pooled connection that gets reused for the next turn.
@harshitajain165
harshitajain165 requested a review from a team as a code owner September 3, 2026 22:04

@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

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 2 potential issues.

Devin Review

Comment on lines +461 to +468
timeout = (
_CONTINUATIONS_IDLE_TIMEOUT if drained_after_close else self._conn_options.timeout
)
try:
msg = await ws.receive(timeout=timeout)
except asyncio.TimeoutError:
if drained_after_close:
return

@devin-ai-integration devin-ai-integration Bot Sep 3, 2026

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.

🔴 Idle gaps truncate continuation audio

After closure, _recv_one treats the first 600 ms response gap as completion without receiving a completion frame. Slow synthesis loses pending audio.

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.

Fixed in 8dd9247: connection is now discarded (pool.remove()) instead of reused whenever continuations completion is inferred rather than protocol-confirmed, so a still-draining context can't leak into the next request.

Comment thread livekit-plugins/livekit-plugins-smallestai/livekit/plugins/smallestai/tts.py Outdated
…in continuations

Addresses two review findings on the continuations receive loop:

- The completion check depended on ctx_state["finalized"] being set at the
  exact moment a `complete` frame was processed. If the server's reply for
  the closing fragment was scheduled before send_task's continuation set
  that flag, the signal was silently dropped and the stream would hang
  until the full connection timeout. Fixed by no longer reacting to which
  `complete` event arrives when; the continuations loop now polls with a
  short idle timeout and re-checks `finalized` fresh on every tick, which
  is immune to task-scheduling order.

- The idle-drain heuristic has no way to be certain a context has fully
  drained (continuations gives no terminal marker), so a connection that
  finished this way could still have trailing frames in flight when
  returned to the pool, corrupting whatever request reuses it next.
  _recv_task now reports whether completion was protocol-confirmed
  (legacy) or merely inferred (continuations), and _run explicitly removes
  the connection from the pool instead of returning it whenever it wasn't
  confirmed.

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

Devin Review

Comment on lines +539 to +542
elif status == "error":
raise APIConnectionError(
f"SmallestAI TTS error: {event.get('message', 'unknown error')}"
)

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.

🟨 Provider errors expose customer content

_parse_status_event copies provider text into an exception recorded by TTS telemetry. Provider errors can expose customer speech without PII tagging.

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.

1 participant