Module livekit.plugins.palabra
Palabra plugin for LiveKit Agents: realtime STT and TTS on the palabra-ai SDK.
Importing this package registers the plugin via Plugin.register_plugin.
Docs: https://docs.livekit.io/agents/models/stt/ and …/tts/.
Classes
class STT (*,
api_key: str | None = None,
language: str | None = None,
translate_languages: str | list[str] | None = None,
filler_filter: bool | None = None,
sample_rate: int = 16000,
region: str | None = None)-
Expand source code
class STT(stt.STT): def __init__( self, *, api_key: str | None = None, language: str | None = None, translate_languages: str | list[str] | None = None, filler_filter: bool | None = None, sample_rate: int = DEFAULT_STT_SAMPLE_RATE, region: str | None = None, ) -> None: """Create a new instance of Palabra realtime STT. Args: api_key: Palabra API Key (create one at https://platform.palabra.ai/api-keys). Falls back to the ``PALABRA_API_KEY`` environment variable. language: Source language code. Defaults to ``None`` (server auto-detect). translate_languages: Target language(s) for live translation. When set, the emitted ``SpeechData`` maps as ``language``/``text`` = the translation and ``source_languages``/``source_texts`` = the original speech. When several target languages are requested, only the first translation to finalize is emitted; multi-target aggregation is planned. filler_filter: Drop filler words ("uh", "um") from transcripts. Unset by default, so the server default is used. sample_rate: Input sample rate in Hz sent to the server; room audio is resampled to it. Defaults to ``16000`` (the recommended ASR rate). region: Palabra region (``"eu"`` / ``"us"``). Falls back to the ``PALABRA_REGION`` environment variable, then ``"eu"``. Raises: ValueError: no ``api_key`` argument and no ``PALABRA_API_KEY`` in the environment. """ super().__init__( capabilities=stt.STTCapabilities( streaming=True, interim_results=True, offline_recognize=False, ) ) api_key = api_key or os.environ.get("PALABRA_API_KEY") if not api_key: raise ValueError( "Palabra api_key is required, either as an argument or set the" " PALABRA_API_KEY environment variable" ) if translate_languages is None: translate_languages = [] elif isinstance(translate_languages, str): translate_languages = [translate_languages] else: translate_languages = list(translate_languages) self._client = Palabra(api_key, region=region) self._opts = _STTOptions( language=language, translate_languages=translate_languages, filler_filter=filler_filter, sample_rate=sample_rate, ) self._streams = weakref.WeakSet[SpeechStream]() @property def model(self) -> str: """Recognition model id; always ``"default"`` — the server picks the model.""" return "default" @property def provider(self) -> str: """Provider display name used in metrics.""" return "Palabra" def update_options( self, *, language: NotGivenOr[str | None] = NOT_GIVEN, translate_languages: NotGivenOr[str | list[str] | None] = NOT_GIVEN, filler_filter: NotGivenOr[bool | None] = NOT_GIVEN, ) -> None: """Update recognition options. Changes apply to new streams only.""" if is_given(language): self._opts.language = language if is_given(translate_languages): if translate_languages is None: self._opts.translate_languages = [] elif isinstance(translate_languages, str): self._opts.translate_languages = [translate_languages] else: self._opts.translate_languages = list(translate_languages) if is_given(filler_filter): self._opts.filler_filter = filler_filter async def _recognize_impl( self, buffer: utils.AudioBuffer, *, language: NotGivenOr[str] = NOT_GIVEN, conn_options: APIConnectOptions, ) -> stt.SpeechEvent: raise NotImplementedError( "Palabra STT does not support batch recognition — use streaming via stream()" ) def stream( self, *, language: NotGivenOr[str] = NOT_GIVEN, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS, ) -> SpeechStream: """Open a streaming recognition session; ``language`` overrides the default.""" opts = _STTOptions( language=language if is_given(language) else self._opts.language, translate_languages=list(self._opts.translate_languages), filler_filter=self._opts.filler_filter, sample_rate=self._opts.sample_rate, ) stream = SpeechStream(stt=self, opts=opts, conn_options=conn_options) self._streams.add(stream) return stream async def aclose(self) -> None: """Close all active recognition streams.""" for stream in list(self._streams): await stream.aclose() self._streams.clear()Helper class that provides a standard way to create an ABC using inheritance.
Create a new instance of Palabra realtime STT.
Args
api_key- Palabra API Key (create one at https://platform.palabra.ai/api-keys).
Falls back to the
PALABRA_API_KEYenvironment variable. language- Source language code. Defaults to
None(server auto-detect). translate_languages- Target language(s) for live translation. When set, the
emitted
SpeechDatamaps aslanguage/text= the translation andsource_languages/source_texts= the original speech. When several target languages are requested, only the first translation to finalize is emitted; multi-target aggregation is planned. filler_filter- Drop filler words ("uh", "um") from transcripts. Unset by default, so the server default is used.
sample_rate- Input sample rate in Hz sent to the server; room audio is
resampled to it. Defaults to
16000(the recommended ASR rate). region- Palabra region (
"eu"/"us"). Falls back to thePALABRA_REGIONenvironment variable, then"eu".
Raises
ValueError- no
api_keyargument and noPALABRA_API_KEYin the environment.
Ancestors
- livekit.agents.stt.stt.STT
- abc.ABC
- EventEmitter
- typing.Generic
Instance variables
prop model : str-
Expand source code
@property def model(self) -> str: """Recognition model id; always ``"default"`` — the server picks the model.""" return "default"Recognition model id; always
"default"— the server picks the model. prop provider : str-
Expand source code
@property def provider(self) -> str: """Provider display name used in metrics.""" return "Palabra"Provider display name used in metrics.
Methods
async def aclose(self) ‑> None-
Expand source code
async def aclose(self) -> None: """Close all active recognition streams.""" for stream in list(self._streams): await stream.aclose() self._streams.clear()Close all active recognition streams.
def stream(self,
*,
language: NotGivenOr[str] = NOT_GIVEN,
conn_options: APIConnectOptions = APIConnectOptions(max_retry=3, retry_interval=2.0, timeout=10.0)) ‑> livekit.plugins.palabra.stt.SpeechStream-
Expand source code
def stream( self, *, language: NotGivenOr[str] = NOT_GIVEN, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS, ) -> SpeechStream: """Open a streaming recognition session; ``language`` overrides the default.""" opts = _STTOptions( language=language if is_given(language) else self._opts.language, translate_languages=list(self._opts.translate_languages), filler_filter=self._opts.filler_filter, sample_rate=self._opts.sample_rate, ) stream = SpeechStream(stt=self, opts=opts, conn_options=conn_options) self._streams.add(stream) return streamOpen a streaming recognition session;
languageoverrides the default. def update_options(self,
*,
language: NotGivenOr[str | None] = NOT_GIVEN,
translate_languages: NotGivenOr[str | list[str] | None] = NOT_GIVEN,
filler_filter: NotGivenOr[bool | None] = NOT_GIVEN) ‑> None-
Expand source code
def update_options( self, *, language: NotGivenOr[str | None] = NOT_GIVEN, translate_languages: NotGivenOr[str | list[str] | None] = NOT_GIVEN, filler_filter: NotGivenOr[bool | None] = NOT_GIVEN, ) -> None: """Update recognition options. Changes apply to new streams only.""" if is_given(language): self._opts.language = language if is_given(translate_languages): if translate_languages is None: self._opts.translate_languages = [] elif isinstance(translate_languages, str): self._opts.translate_languages = [translate_languages] else: self._opts.translate_languages = list(translate_languages) if is_given(filler_filter): self._opts.filler_filter = filler_filterUpdate recognition options. Changes apply to new streams only.
Inherited members
class SpeechStream (*,
stt: STT,
opts: _STTOptions,
conn_options: APIConnectOptions)-
Expand source code
class SpeechStream(stt.RecognizeStream): """Streaming speech recognition over one Palabra STT WebSocket session. Created by ``STT.stream()``; not instantiated directly. """ def __init__(self, *, stt: STT, opts: _STTOptions, conn_options: APIConnectOptions) -> None: super().__init__(stt=stt, conn_options=conn_options, sample_rate=opts.sample_rate) self._stt: STT = stt self._opts = opts async def _run(self) -> None: request_id = utils.shortuuid() input_closed = asyncio.Event() utterances: dict[str, _Utterance] = {} def _make_session() -> SttSession: return self._stt._client.stt( self._opts.language, sample_rate=self._opts.sample_rate, translate_languages=self._opts.translate_languages or None, enable_filler_filter=self._opts.filler_filter, ) async def _send_task(session: SttSession) -> None: samples_per_chunk = self._opts.sample_rate * _CHUNK_MS // 1000 audio_bstream = utils.audio.AudioByteStream( sample_rate=self._opts.sample_rate, num_channels=1, samples_per_channel=samples_per_chunk, ) async for data in self._input_ch: if isinstance(data, self._FlushSentinel): frames = audio_bstream.flush() else: frames = audio_bstream.push(data.data.tobytes()) for frame in frames: await session.send_audio(frame.data.tobytes()) for frame in audio_bstream.flush(): await session.send_audio(frame.data.tobytes()) input_closed.set() # The protocol has no end-of-stream message; closing the WS is the signal. # The close also makes session.receive() return None, which ends _recv_task. await session.close() def _speech_data(ev: SttTranscript) -> stt.SpeechData: offset = self.start_time_offset data = stt.SpeechData( language=LanguageCode(ev.language), text=ev.text, start_time=(ev.start_time or 0.0) + offset, end_time=(ev.end_time or 0.0) + offset, ) if ev.is_translation: source = utterances[ev.transcription_id].source if source is not None: data.source_languages = [LanguageCode(source.language)] data.source_texts = [source.text] data.target_languages = [LanguageCode(ev.language)] data.target_texts = [ev.text] return data def _emit(ev: SttTranscript) -> None: utt = utterances.setdefault(ev.transcription_id, _Utterance()) if utt.done: return translation_active = bool(self._opts.translate_languages) if translation_active and not ev.is_translation: # Source-language event: stored for pairing; only the translation is emitted. utt.source = ev return if not ev.text and not utt.started: return # filler-filter can produce empty segments; nothing to report if not utt.started: utt.started = True self._event_ch.send_nowait( stt.SpeechEvent(type=stt.SpeechEventType.START_OF_SPEECH, request_id=request_id) ) if not ev.is_eos: self._event_ch.send_nowait( stt.SpeechEvent( type=stt.SpeechEventType.INTERIM_TRANSCRIPT, request_id=request_id, alternatives=[_speech_data(ev)], ) ) return utt.done = True self._event_ch.send_nowait( stt.SpeechEvent( type=stt.SpeechEventType.FINAL_TRANSCRIPT, request_id=request_id, alternatives=[_speech_data(ev)], ) ) self._event_ch.send_nowait( stt.SpeechEvent(type=stt.SpeechEventType.END_OF_SPEECH, request_id=request_id) ) # Keep the finished entry on purpose. # The server can resend this id: segment updates, extra translation targets. # The `done` check above drops those events. utt.source = None # the stored source transcript is no longer needed if len(utterances) > _MAX_TRACKED_UTTERANCES: for tid, u in list(utterances.items()): # insertion order = oldest first if not u.done: continue utterances.pop(tid, None) if len(utterances) <= _MAX_TRACKED_UTTERANCES: break async def _recv_task(session: SttSession) -> None: async for event in session: if isinstance(event, ServerError): logger.error("palabra stt error: %s", event.code) raise APIError( f"palabra stt error: {event.code}", retryable=event.code in _RETRYABLE_ERROR_CODES, ) if isinstance(event, SttTranscript): _emit(event) # The iterator ends when the connection closes. # That is expected only after _send_task closed the WS at end of input. if not input_closed.is_set(): raise APIConnectionError("palabra stt connection closed unexpectedly") session: SttSession | None = None try: # The session object exists before connecting, so the finally below # also closes a connection abandoned by the timeout. session = _make_session() await asyncio.wait_for(session.__aenter__(), timeout=self._conn_options.timeout) tasks = [ asyncio.create_task(_send_task(session)), asyncio.create_task(_recv_task(session)), ] try: await asyncio.gather(*tasks) finally: await utils.aio.gracefully_cancel(*tasks) except asyncio.TimeoutError: raise APITimeoutError() from None except AuthError as e: raise APIStatusError(str(e), status_code=401, request_id=None, body=None) from None except TaskError as e: # `code` is a short server enum (e.g. VALIDATION_ERROR); `desc` is free-form # server text, so only the code is surfaced. raise APIConnectionError(f"palabra stt error: {e.code}") from None except (SessionError, PalabraError) as e: # The SDK interpolates the underlying websockets error into its message, and the # connect URL carries the API key as a ?token= query parameter -- so neither # str(e) nor the __cause__ chain is safe to surface. raise APIConnectionError(type(e).__name__) from None finally: if session is not None: with contextlib.suppress(Exception): await session.close()Streaming speech recognition over one Palabra STT WebSocket session.
Created by
STT.stream(); not instantiated directly.Args: sample_rate : int or None, optional The desired sample rate for the audio input. If specified, the audio input will be automatically resampled to match the given sample rate before being processed for Speech-to-Text. If not provided (None), the input will retain its original sample rate.
Ancestors
- livekit.agents.stt.stt.RecognizeStream
- abc.ABC
class SynthesizeStream (*,
tts: TTS,
conn_options: APIConnectOptions)-
Expand source code
class SynthesizeStream(tts.SynthesizeStream): """One streaming synthesis request, multiplexed onto the shared ``TtsSession``. Created by ``TTS.stream()`` / ``TTS.synthesize()``; not instantiated directly. """ def __init__(self, *, tts: TTS, conn_options: APIConnectOptions) -> None: super().__init__(tts=tts, conn_options=conn_options) self._tts: TTS = tts async def _run(self, output_emitter: tts.AudioEmitter) -> None: request_id = utils.shortuuid() output_emitter.initialize( request_id=request_id, sample_rate=self._tts._opts.sample_rate, num_channels=1, mime_type="audio/pcm", stream=True, ) sent_tokenizer_stream = self._tts._sentence_tokenizer.stream() send_complete = asyncio.Event() # One generation_id per text message. # The TTS reader delivers audio_chunk frames into this mailbox by that id. # - my_gens = every id this stream sent; its routes are removed on exit; # - pending_gens = the ids still waiting for their last_chunk. mailbox: asyncio.Queue[TtsChunk | ServerError | None] = asyncio.Queue() my_gens: set[str] = set() pending_gens: set[str] = set() async def _input_task() -> None: async for data in self._input_ch: if isinstance(data, self._FlushSentinel): sent_tokenizer_stream.flush() continue sent_tokenizer_stream.push_text(data) sent_tokenizer_stream.end_input() async def _send_task(session: TtsSession) -> None: loop = asyncio.get_running_loop() async for ev in sent_tokenizer_stream: text = ev.token if not text: continue # Each text message is a separate generation on the server. # It always ends with its own last_chunk, even when eos=False. pieces = [text[i : i + _MAX_TEXT_LEN] for i in range(0, len(text), _MAX_TEXT_LEN)] for idx, piece in enumerate(pieces): gen_id = utils.shortuuid("PLBR_") # Register before sending: audio_chunk can arrive before send_text returns. my_gens.add(gen_id) pending_gens.add(gen_id) self._tts._routes[gen_id] = mailbox self._mark_started() try: async with self._tts._send_lock: delay = self._tts._last_send + _MIN_SEND_INTERVAL - loop.time() if delay > 0: await asyncio.sleep(delay) await session.send_text( piece, eos=idx == len(pieces) - 1, generation_id=gen_id ) self._tts._last_send = loop.time() except BaseException: pending_gens.discard(gen_id) self._tts._routes.pop(gen_id, None) raise send_complete.set() async def _recv_task() -> None: got_audio = False segment_started = False while not (send_complete.is_set() and not pending_gens): if pending_gens: # Text was sent and its audio has not arrived yet. # Silence longer than the timeout here means the server is stuck. try: event = await asyncio.wait_for( mailbox.get(), timeout=self._conn_options.timeout ) except asyncio.TimeoutError: # A sent generation never got its last_chunk. # The server ends errored or stalled generations without one. if got_audio: break # deliver the partial audio instead of failing the reply raise APITimeoutError() from None else: # No sent text is waiting for audio: the pause comes from the LLM input. # Wait in short steps. # After the next send the branch above applies the real timeout. try: event = await asyncio.wait_for(mailbox.get(), timeout=0.25) except asyncio.TimeoutError: continue if event is None: raise APIConnectionError("palabra tts session closed unexpectedly") if isinstance(event, ServerError): logger.error("palabra tts error: %s", event.code) raise APIError( f"palabra tts error: {event.code}", retryable=event.code in _RETRYABLE_ERROR_CODES, ) if isinstance(event, TtsChunk): if event.audio: got_audio = True if not segment_started: # One segment per stream: the agent opens a fresh stream() per reply. output_emitter.start_segment(segment_id=request_id) segment_started = True output_emitter.push(event.audio) if event.last_chunk: pending_gens.discard(event.generation_id) output_emitter.end_input() async def _teardown() -> None: for gen_id in my_gens: self._tts._routes.pop(gen_id, None) await self._tts._drop_session_if_idle(current_stream_active=session_acquired) session: TtsSession | None = None session_acquired = False try: async with self._tts._send_lock: session = await self._tts._ensure_session(self._conn_options.timeout) self._tts._active_streams_cnt += 1 session_acquired = True tasks = [ asyncio.create_task(_input_task()), asyncio.create_task(_send_task(session)), asyncio.create_task(_recv_task()), ] try: await asyncio.gather(*tasks) finally: await sent_tokenizer_stream.aclose() await utils.aio.gracefully_cancel(*tasks) except asyncio.CancelledError: # Interruption: the framework awaits this task before the next reply, so return fast. # Cancel only our unfinished generations by id; other streams are not affected. if session is not None and pending_gens: logger.debug("interrupted: cancelling %d generations", len(pending_gens)) with contextlib.suppress(Exception): await asyncio.wait_for( self._send_cancel(session, set(pending_gens)), timeout=1.0 ) logger.debug("interrupted: server cancel sent") raise except APIError: await _teardown() raise except asyncio.TimeoutError: await _teardown() raise APITimeoutError() from None except AuthError as e: await _teardown() raise APIStatusError(str(e), status_code=401, request_id=None, body=None) from None except TaskError as e: await _teardown() # `code` is a short server enum (e.g. VALIDATION_ERROR); `desc` is free-form # server text, so only the code is surfaced. raise APIConnectionError(f"palabra tts error: {e.code}") from None except (SessionError, PalabraError) as e: await _teardown() # The SDK interpolates the underlying websockets error into its message, and the # connect URL carries the API key as a ?token= query parameter -- so neither # str(e) nor the __cause__ chain is safe to surface. raise APIConnectionError(type(e).__name__) from None except Exception as e: await _teardown() logger.exception("palabra tts error") raise APIConnectionError() from e finally: for gen_id in my_gens: self._tts._routes.pop(gen_id, None) if session_acquired: async with self._tts._send_lock: self._tts._active_streams_cnt -= 1 async def _send_cancel(self, session: TtsSession, gen_ids: set[str]) -> None: async with self._tts._send_lock: for gen_id in gen_ids: # The SDK's cancel() takes no generation argument yet. # Send the frame directly until palabra-ai exposes cancel(generation_id). await session._send({"type": "cancel", "generation_id": gen_id})One streaming synthesis request, multiplexed onto the shared
TtsSession.Created by
TTS.stream()/TTS.synthesize(); not instantiated directly.Ancestors
- livekit.agents.tts.tts.SynthesizeStream
- abc.ABC
class TTS (*,
api_key: str | None = None,
language: str = 'en',
voice_id: str = 'default_low',
speed: NotGivenOr[float] = NOT_GIVEN,
deaccent_strength: float = 1.0,
model: TTSModels | str = 'auto',
sample_rate: int = 24000,
region: str | None = None,
tokenizer: NotGivenOr[tokenize.SentenceTokenizer] = NOT_GIVEN)-
Expand source code
class TTS(tts.TTS): def __init__( self, *, api_key: str | None = None, language: str = DEFAULT_LANGUAGE, voice_id: str = DEFAULT_VOICE_ID, speed: NotGivenOr[float] = NOT_GIVEN, deaccent_strength: float = DEFAULT_DEACCENT_STRENGTH, model: TTSModels | str = DEFAULT_MODEL, sample_rate: int = DEFAULT_SAMPLE_RATE, region: str | None = None, tokenizer: NotGivenOr[tokenize.SentenceTokenizer] = NOT_GIVEN, ) -> None: """Create a new instance of Palabra realtime TTS. Args: api_key: Palabra API Key (create one at https://platform.palabra.ai/api-keys). Falls back to the ``PALABRA_API_KEY`` environment variable. language: BCP-47 language code for synthesis. Defaults to ``"en"``. voice_id: Voice identifier; ``"default_low"`` / ``"default_high"`` pick the language default voice. Defaults to ``"default_low"``. speed: Speech rate in the ``0.0``-``1.0`` range (``0.5`` is natural pace). Unset by default, so the server default is used. deaccent_strength: Foreign-accent reduction in the ``0.0``-``1.0`` range. model: Synthesis model. Defaults to ``"auto"`` (server picks). sample_rate: Output sample rate in Hz (``8000``-``48000``). Defaults to ``24000``. region: Palabra region (``"eu"`` / ``"us"``). Falls back to the ``PALABRA_REGION`` environment variable, then ``"eu"``. tokenizer: Sentence tokenizer used to chunk streamed text into utterances. Defaults to ``livekit.agents.tokenize.blingfire.SentenceTokenizer``. Raises: ValueError: no ``api_key`` argument and no ``PALABRA_API_KEY`` in the environment. """ super().__init__( capabilities=tts.TTSCapabilities(streaming=True), sample_rate=sample_rate, num_channels=1, ) api_key = api_key or os.environ.get("PALABRA_API_KEY") if not api_key: raise ValueError( "Palabra api_key is required, either as an argument or set the" " PALABRA_API_KEY environment variable" ) self._client = Palabra(api_key, region=region) self._opts = _TTSOptions( language=language, voice_id=voice_id, speed=speed if is_given(speed) else None, deaccent_strength=deaccent_strength, model=model, sample_rate=sample_rate, ) # Invariants of the shared session state: # - one TtsSession (one WebSocket) is shared by every stream of this TTS; # - the `init` frame is sent once per connection, so update_options() only # marks _opts_dirty and the next _ensure_session() reopens the session; # - _send_lock orders every socket write (text and cancel) and session open/close; # - _send_lock is held per operation, never for a stream's lifetime. self._tts_session: TtsSession | None = None self._opts_dirty = False self._send_lock = asyncio.Lock() self._last_send = 0.0 self._active_streams_cnt = 0 # Demultiplexer: one background reader per session. # It routes each received event by generation_id into the sending stream's mailbox. # - _routes: generation_id -> owning stream's mailbox; # - unknown generation_id = the generation was already cancelled: dropped; # - ServerError has no generation_id: delivered to every mailbox. self._routes: dict[str, asyncio.Queue[TtsChunk | ServerError | None]] = {} self._reader_task: asyncio.Task[None] | None = None self._prewarm_task: asyncio.Task[None] | None = None self._closed = False self._streams = weakref.WeakSet[SynthesizeStream]() self._sentence_tokenizer = ( tokenizer if is_given(tokenizer) else tokenize.blingfire.SentenceTokenizer() ) @property def model(self) -> str: """Active synthesis model id.""" return str(self._opts.model) @property def provider(self) -> str: """Provider display name used in metrics.""" return "Palabra" def _new_tts(self) -> TtsSession: return self._client.tts( language=self._opts.language, voice_id=self._opts.voice_id, speed=self._opts.speed, deaccent_strength=self._opts.deaccent_strength, model=self._opts.model, format="pcm", sample_rate=self._opts.sample_rate, ) async def _ensure_session(self, timeout: float) -> TtsSession: # Caller must hold `self._send_lock`. if self._closed: raise APIConnectionError("palabra tts is closed", retryable=False) if self._opts_dirty and self._active_streams_cnt == 0: await self._reset_session() self._opts_dirty = False if self._tts_session is not None and ( not _session_alive(self._tts_session) or self._reader_task is None or self._reader_task.done() ): await self._reset_session() if self._tts_session is None: session = self._new_tts() try: await asyncio.wait_for(session.__aenter__(), timeout=timeout) except BaseException: with contextlib.suppress(Exception): await session.close() raise self._tts_session = session self._reader_task = asyncio.create_task(self._read_loop(session)) return self._tts_session async def _read_loop(self, session: TtsSession) -> None: # Sole consumer of the session's events; streams read their own mailboxes. stale_chunks = 0 try: async for event in session: if isinstance(event, TtsChunk): route = self._routes.get(event.generation_id) if route is None: stale_chunks += 1 # frame of an already-cancelled generation continue route.put_nowait(event) elif isinstance(event, ServerError): for route in set(self._routes.values()): route.put_nowait(event) except asyncio.CancelledError: raise except Exception: logger.exception("palabra tts reader failed") finally: if stale_chunks: logger.debug("dropped %d audio chunks of cancelled generations", stale_chunks) for route in set(self._routes.values()): route.put_nowait(None) # the session is gone: wake every stream async def _reset_session(self) -> None: reader, self._reader_task = self._reader_task, None if reader is not None: reader.cancel() with contextlib.suppress(asyncio.CancelledError, Exception): await reader session, self._tts_session = self._tts_session, None if session is not None: with contextlib.suppress(Exception): await session.close() async def _drop_session_if_idle(self, *, current_stream_active: bool) -> None: # A failed request must not close the socket under other active streams. async with self._send_lock: dead = self._tts_session is not None and not _session_alive(self._tts_session) # Error teardown runs before the failing stream releases its active count. other_streams = self._active_streams_cnt - int(current_stream_active) if dead or other_streams <= 0: await self._reset_session() def prewarm(self) -> None: """Open the realtime TTS session ahead of the first request. Returns immediately; the connection is opened in a background task. Failures are ignored — the first stream()/synthesize() call reconnects. Does nothing when called outside a running event loop. """ with contextlib.suppress(RuntimeError): self._prewarm_task = asyncio.get_running_loop().create_task(self._safe_prewarm()) async def _safe_prewarm(self) -> None: with contextlib.suppress(Exception): async with self._send_lock: if self._closed: return await self._ensure_session(DEFAULT_API_CONNECT_OPTIONS.timeout) def update_options( self, *, language: NotGivenOr[str] = NOT_GIVEN, voice_id: NotGivenOr[str] = NOT_GIVEN, speed: NotGivenOr[float] = NOT_GIVEN, deaccent_strength: NotGivenOr[float] = NOT_GIVEN, model: NotGivenOr[TTSModels | str] = NOT_GIVEN, ) -> None: """Update synthesis options; unset arguments keep their current value. The Palabra ``init`` frame is sent once per connection, so the session is reopened on the next stream() call for the new options to take effect. Streams already running keep the options they were created with. """ if is_given(language): self._opts.language = language if is_given(voice_id): self._opts.voice_id = voice_id if is_given(speed): self._opts.speed = speed if is_given(deaccent_strength): self._opts.deaccent_strength = deaccent_strength if is_given(model): self._opts.model = model self._opts_dirty = True def synthesize( self, text: str, *, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS ) -> tts.ChunkedStream: """Synthesize ``text`` in a single request, over the shared realtime session.""" return self._synthesize_with_stream(text, conn_options=conn_options) def stream( self, *, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS ) -> SynthesizeStream: """Open a streaming synthesis session that accepts incremental text.""" stream = SynthesizeStream(tts=self, conn_options=conn_options) self._streams.add(stream) return stream async def aclose(self) -> None: """Close all active streams and the realtime TTS session.""" self._closed = True if self._prewarm_task is not None: self._prewarm_task.cancel() with contextlib.suppress(asyncio.CancelledError, Exception): await self._prewarm_task self._prewarm_task = None for stream in list(self._streams): await stream.aclose() self._streams.clear() async with self._send_lock: await self._reset_session()Helper class that provides a standard way to create an ABC using inheritance.
Create a new instance of Palabra realtime TTS.
Args
api_key- Palabra API Key (create one at https://platform.palabra.ai/api-keys).
Falls back to the
PALABRA_API_KEYenvironment variable. language- BCP-47 language code for synthesis. Defaults to
"en". voice_id- Voice identifier;
"default_low"/"default_high"pick the language default voice. Defaults to"default_low". speed- Speech rate in the
0.0-1.0range (0.5is natural pace). Unset by default, so the server default is used. deaccent_strength- Foreign-accent reduction in the
0.0-1.0range. model- Synthesis model. Defaults to
"auto"(server picks). sample_rate- Output sample rate in Hz (
8000-48000). Defaults to24000. region- Palabra region (
"eu"/"us"). Falls back to thePALABRA_REGIONenvironment variable, then"eu". tokenizer- Sentence tokenizer used to chunk streamed text into utterances.
Defaults to
SentenceTokenizer.
Raises
ValueError- no
api_keyargument and noPALABRA_API_KEYin the environment.
Ancestors
- livekit.agents.tts.tts.TTS
- abc.ABC
- EventEmitter
- typing.Generic
Instance variables
prop model : str-
Expand source code
@property def model(self) -> str: """Active synthesis model id.""" return str(self._opts.model)Active synthesis model id.
prop provider : str-
Expand source code
@property def provider(self) -> str: """Provider display name used in metrics.""" return "Palabra"Provider display name used in metrics.
Methods
async def aclose(self) ‑> None-
Expand source code
async def aclose(self) -> None: """Close all active streams and the realtime TTS session.""" self._closed = True if self._prewarm_task is not None: self._prewarm_task.cancel() with contextlib.suppress(asyncio.CancelledError, Exception): await self._prewarm_task self._prewarm_task = None for stream in list(self._streams): await stream.aclose() self._streams.clear() async with self._send_lock: await self._reset_session()Close all active streams and the realtime TTS session.
def prewarm(self) ‑> None-
Expand source code
def prewarm(self) -> None: """Open the realtime TTS session ahead of the first request. Returns immediately; the connection is opened in a background task. Failures are ignored — the first stream()/synthesize() call reconnects. Does nothing when called outside a running event loop. """ with contextlib.suppress(RuntimeError): self._prewarm_task = asyncio.get_running_loop().create_task(self._safe_prewarm())Open the realtime TTS session ahead of the first request.
Returns immediately; the connection is opened in a background task. Failures are ignored — the first stream()/synthesize() call reconnects. Does nothing when called outside a running event loop.
def stream(self,
*,
conn_options: APIConnectOptions = APIConnectOptions(max_retry=3, retry_interval=2.0, timeout=10.0)) ‑> livekit.plugins.palabra.tts.SynthesizeStream-
Expand source code
def stream( self, *, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS ) -> SynthesizeStream: """Open a streaming synthesis session that accepts incremental text.""" stream = SynthesizeStream(tts=self, conn_options=conn_options) self._streams.add(stream) return streamOpen a streaming synthesis session that accepts incremental text.
def synthesize(self,
text: str,
*,
conn_options: APIConnectOptions = APIConnectOptions(max_retry=3, retry_interval=2.0, timeout=10.0)) ‑> livekit.agents.tts.tts.ChunkedStream-
Expand source code
def synthesize( self, text: str, *, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS ) -> tts.ChunkedStream: """Synthesize ``text`` in a single request, over the shared realtime session.""" return self._synthesize_with_stream(text, conn_options=conn_options)Synthesize
textin a single request, over the shared realtime session. def update_options(self,
*,
language: NotGivenOr[str] = NOT_GIVEN,
voice_id: NotGivenOr[str] = NOT_GIVEN,
speed: NotGivenOr[float] = NOT_GIVEN,
deaccent_strength: NotGivenOr[float] = NOT_GIVEN,
model: NotGivenOr[TTSModels | str] = NOT_GIVEN) ‑> None-
Expand source code
def update_options( self, *, language: NotGivenOr[str] = NOT_GIVEN, voice_id: NotGivenOr[str] = NOT_GIVEN, speed: NotGivenOr[float] = NOT_GIVEN, deaccent_strength: NotGivenOr[float] = NOT_GIVEN, model: NotGivenOr[TTSModels | str] = NOT_GIVEN, ) -> None: """Update synthesis options; unset arguments keep their current value. The Palabra ``init`` frame is sent once per connection, so the session is reopened on the next stream() call for the new options to take effect. Streams already running keep the options they were created with. """ if is_given(language): self._opts.language = language if is_given(voice_id): self._opts.voice_id = voice_id if is_given(speed): self._opts.speed = speed if is_given(deaccent_strength): self._opts.deaccent_strength = deaccent_strength if is_given(model): self._opts.model = model self._opts_dirty = TrueUpdate synthesis options; unset arguments keep their current value.
The Palabra
initframe is sent once per connection, so the session is reopened on the next stream() call for the new options to take effect. Streams already running keep the options they were created with.
Inherited members