Skip to content

Add respeecher tts plugin#3233

Open
mitrushchienkova wants to merge 21 commits into
livekit:mainfrom
respeecher:add-respeecher-TTS-plugin
Open

Add respeecher tts plugin#3233
mitrushchienkova wants to merge 21 commits into
livekit:mainfrom
respeecher:add-respeecher-TTS-plugin

Conversation

@mitrushchienkova
Copy link
Copy Markdown

@mitrushchienkova mitrushchienkova commented Aug 22, 2025

Description

This PR adds a new TTS plugin for LiveKit Agents, integrating Respeecher’s synthesis and streaming API. It currently supports the English voice model and the Ukrainian voice models.

Why Respeecher?

Respeecher’s technology is notable not just for voice quality but also for its ethical design and safeguards. This integration offers:

  • Professional-grade synthesis quality powered by advanced voice modeling.
  • Informed consent protocols: Respeecher requires explicit permission from voice owners before using their vocal data, ensuring ethical and lawful use.
  • Commitment to industry ethics: Respeecher is a founding partner in the Partnership on AI’s Responsible Practices for Synthetic Media, actively promoting standards for accountability, privacy, and transparency in synthetic media.

@mitrushchienkova mitrushchienkova force-pushed the add-respeecher-TTS-plugin branch from 6450e4e to 9af5141 Compare September 1, 2025 07:27
@mitrushchienkova mitrushchienkova force-pushed the add-respeecher-TTS-plugin branch 4 times, most recently from d844b57 to 2b09286 Compare September 8, 2025 11:50

Support for [Respeecher](https://respeecher.com/)'s TTS in LiveKit Agents.

More information is available in the docs for the [Respeecher](https://docs.livekit.io/agents/integrations/tts/respeecher/) integration.
Copy link
Copy Markdown
Author

@mitrushchienkova mitrushchienkova Sep 8, 2025

Choose a reason for hiding this comment

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

How can we add documentation for the new plugin?

Copy link
Copy Markdown
Contributor

@longcw longcw left a comment

Choose a reason for hiding this comment

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

when testing I got an error as following, is this correct to put the api key in the url?

aiohttp.client_exceptions.WSServerHandshakeError: 401, message='Invalid response status', url='wss://api.respeecher.com/v1/public/tts/en-rt/tts/websocket?api_key=[xxx]&source=LiveKit-Plugin-Respeecher-Version&version=0.1.0'

also, could you fix the type checks with mypy mypy --install-types --non-interactive -p livekit.plugins.respeecher

Comment thread uv.lock
Comment thread livekit-plugins/livekit-plugins-respeecher/livekit/plugins/respeecher/models.py Outdated
async def _connect_ws(self, timeout: float) -> aiohttp.ClientWebSocketResponse:
session = self._ensure_session()
ws_url = self._opts.base_url.replace("https://", "wss://").replace("http://", "ws://")
full_ws_url = f"{ws_url}{self._opts.model}/tts/websocket?api_key={self._opts.api_key}&source={API_VERSION_HEADER}&version={API_VERSION}"
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.

is this the correct that exposes api key in url?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hello!
Yes, this is intended since the Websocket protocol doesn't accept custom headers. Sending api keys via query parameters is common workaround. Other plugins, e.g. Cartesia does the same.
However, sending API keys as query parameters should be done only via TLS, so I've added a check for that.

@mitrushchienkova mitrushchienkova force-pushed the add-respeecher-TTS-plugin branch from 80047b2 to 1f8db02 Compare May 19, 2026 13:21
devin-ai-integration[bot]

This comment was marked as resolved.

mitrushchienkova and others added 3 commits May 19, 2026 22:34
Different Respeecher models expose different voice catalogs, so a
single hardcoded default (samantha) doesn't generalize once ua-rt is
available. Make voice_id a required argument and update callers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The session is lazily obtained from utils.http_context.http_session(),
a process-wide shared session. Closing it would break every other
plugin and HTTP request in the same job context. Match the pattern
used by Cartesia, Resemble, ElevenLabs, Deepgram, and friends.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Preserve APIError/APIStatusError raised inside _recv_task instead of
  rewrapping them as a generic APIConnectionError that loses the
  original message ("Respeecher returned error: ...").
- Pass conn_options.timeout to ws.receive() so a stalled stream is
  detected instead of hanging until external cancellation, matching
  the Cartesia/Deepgram pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

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.

View 8 additional findings in Devin Review.

Open in Devin Review

- Retire the old connection pool on model change instead of force-closing
  it. The model is baked into the WebSocket URL, so the pool must be
  swapped; force-closing terminated WebSockets held by in-flight streams.
  Now we hold a reference until aclose(), letting in-flight streams
  return their connections naturally.
- Export SynthesizeStream from the package so callers can use it for
  type annotations, matching ElevenLabs and other streaming plugins.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

- Snapshot _TTSOptions per stream via dataclasses.replace so concurrent
  update_options() can't mutate voice_id/voice_settings/etc. mid-stream.
- Track end-of-input with an input_ended flag instead of relying on
  sent_tokenizer_stream.closed. The tokenizer closes as soon as
  _input_task drains its channel, which fires before the final
  continue=false request is sent; a per-sentence "done" arriving in that
  window could terminate the recv loop prematurely.
- Use output_emitter.end_segment() in _recv_task instead of end_input().
  Closing the AudioEmitter write channel is the base class's job; the
  plugin only owns segment boundaries (matches Resemble).
- Handle aiohttp.WSMsgType.ERROR explicitly so transport-level failures
  surface immediately instead of waiting for the next receive timeout.

Also extracted the duplicated voice-payload construction into a small
helper to keep send paths in sync.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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