Module livekit.plugins.baseten
Functions
async def list_voices(*, model_endpoint: str, api_key: str | None = None) ‑> dict[str, typing.Any]-
Expand source code
async def list_voices(*, model_endpoint: str, api_key: str | None = None) -> dict[str, Any]: """`voices` is built-ins (empty on Base); `uploaded_voices` is per-replica.""" response = await _control(model_endpoint, api_key, {"type": "voice.list"}) if response.get("type") == "error": raise RuntimeError(f"voice.list failed: {response.get('message')}") return responsevoicesis built-ins (empty on Base);uploaded_voicesis per-replica. async def register_voice(*,
model_endpoint: str,
name: str,
ref_audio_path: str,
ref_text: str | None = None,
api_key: str | None = None,
consent: str = 'user_consent') ‑> dict[str, typing.Any]-
Expand source code
async def register_voice( *, model_endpoint: str, name: str, ref_audio_path: str, ref_text: str | None = None, api_key: str | None = None, consent: str = "user_consent", ) -> dict[str, Any]: """Clone a voice from 10-20s of clean speech. Omitting ref_text is less faithful. The clone lives on one replica's local disk and dies with the container. """ with open(ref_audio_path, "rb") as f: audio_b64 = base64.b64encode(f.read()).decode() message: dict[str, Any] = { "type": "voice.add", "name": name, "consent": consent, "audio_data": audio_b64, "audio_format": os.path.splitext(ref_audio_path)[1].lstrip(".").lower() or "wav", } if ref_text: message["ref_text"] = ref_text response = await _control(model_endpoint, api_key, message) if response.get("type") == "error" or not response.get("success"): raise RuntimeError(f"voice.add failed: {response.get('message') or response}") voice: dict[str, Any] = response.get("voice", response) return voiceClone a voice from 10-20s of clean speech. Omitting ref_text is less faithful.
The clone lives on one replica's local disk and dies with the container.
Classes
class LLM (*,
model: str | LLMModels = 'meta-llama/Llama-4-Maverick-17B-128E-Instruct',
api_key: NotGivenOr[str] = NOT_GIVEN,
user: NotGivenOr[str] = NOT_GIVEN,
safety_identifier: NotGivenOr[str] = NOT_GIVEN,
prompt_cache_key: NotGivenOr[str] = NOT_GIVEN,
temperature: NotGivenOr[float] = NOT_GIVEN,
top_p: NotGivenOr[float] = NOT_GIVEN,
parallel_tool_calls: NotGivenOr[bool] = NOT_GIVEN,
tool_choice: NotGivenOr[ToolChoice] = NOT_GIVEN,
reasoning_effort: NotGivenOr[ReasoningEffort] = NOT_GIVEN,
base_url: NotGivenOr[str] = 'https://inference.baseten.co/v1',
client: openai.AsyncClient | None = None,
timeout: httpx.Timeout | None = None)-
Expand source code
class LLM(OpenAILLM): def __init__( self, *, model: str | LLMModels = "meta-llama/Llama-4-Maverick-17B-128E-Instruct", api_key: NotGivenOr[str] = NOT_GIVEN, user: NotGivenOr[str] = NOT_GIVEN, safety_identifier: NotGivenOr[str] = NOT_GIVEN, prompt_cache_key: NotGivenOr[str] = NOT_GIVEN, temperature: NotGivenOr[float] = NOT_GIVEN, top_p: NotGivenOr[float] = NOT_GIVEN, parallel_tool_calls: NotGivenOr[bool] = NOT_GIVEN, tool_choice: NotGivenOr[ToolChoice] = NOT_GIVEN, reasoning_effort: NotGivenOr[ReasoningEffort] = NOT_GIVEN, base_url: NotGivenOr[str] = "https://inference.baseten.co/v1", client: openai.AsyncClient | None = None, timeout: httpx.Timeout | None = None, ): """ Create a new instance of Baseten LLM. ``api_key`` must be set to your Baseten API key, either using the argument or by setting the ``BASETEN_API_KEY`` environmental variable. """ api_key = api_key if is_given(api_key) else os.environ.get("BASETEN_API_KEY", "") if not api_key: raise ValueError( "BASETEN_API_KEY is required, either as argument or set BASETEN_API_KEY environmental variable" # noqa: E501 ) if not is_given(reasoning_effort): if model == "openai/gpt-oss-120b": reasoning_effort = "low" super().__init__( model=model, api_key=api_key, base_url=base_url, client=client, user=user, safety_identifier=safety_identifier, prompt_cache_key=prompt_cache_key, temperature=temperature, top_p=top_p, parallel_tool_calls=parallel_tool_calls, tool_choice=tool_choice, timeout=timeout, reasoning_effort=reasoning_effort, ) @property def model(self) -> str: return self._opts.model @property def provider(self) -> str: return "Baseten"Helper class that provides a standard way to create an ABC using inheritance.
Create a new instance of Baseten LLM.
api_keymust be set to your Baseten API key, either using the argument or by setting theBASETEN_API_KEYenvironmental variable.Ancestors
- livekit.plugins.openai.llm.LLM
- livekit.agents.llm.llm.LLM
- abc.ABC
- EventEmitter
- typing.Generic
Instance variables
prop model : str-
Expand source code
@property def model(self) -> str: return self._opts.modelGet the model name/identifier for this LLM instance.
Returns
The model name if available, "unknown" otherwise.
Note
Plugins should override this property to provide their model information.
prop provider : str-
Expand source code
@property def provider(self) -> str: return "Baseten"Get the provider name/identifier for this LLM instance.
Returns
The provider name if available, "unknown" otherwise.
Note
Plugins should override this property to provide their provider information.
Inherited members
class STT (*,
model: STTModels = 'whisper',
api_key: str | None = None,
model_endpoint: str | None = None,
model_id: str | None = None,
chain_id: str | None = None,
sample_rate: int = 16000,
encoding: NotGivenOr[STTEncoding] = NOT_GIVEN,
buffer_size_seconds: float = 0.032,
language: NotGivenOr[str] = NOT_GIVEN,
language_options: list[str] | None = None,
enable_partial_transcripts: bool = True,
partial_transcript_interval_s: NotGivenOr[float] = NOT_GIVEN,
final_transcript_max_duration_s: int = 30,
show_word_timestamps: NotGivenOr[bool] = NOT_GIVEN,
vad_threshold: float = 0.5,
vad_min_silence_duration_ms: NotGivenOr[int] = NOT_GIVEN,
vad_speech_pad_ms: NotGivenOr[int] = NOT_GIVEN,
http_session: aiohttp.ClientSession | None = None)-
Expand source code
class STT(stt.STT): _TRUSS_URL_TEMPLATE = "wss://model-{model_id}.api.baseten.co/environments/production/websocket" _CHAIN_URL_TEMPLATE = "wss://chain-{chain_id}.api.baseten.co/environments/production/websocket" def __init__( self, *, model: STTModels = "whisper", api_key: str | None = None, model_endpoint: str | None = None, model_id: str | None = None, chain_id: str | None = None, sample_rate: int = 16000, encoding: NotGivenOr[STTEncoding] = NOT_GIVEN, buffer_size_seconds: float = 0.032, language: NotGivenOr[str] = NOT_GIVEN, language_options: list[str] | None = None, enable_partial_transcripts: bool = True, partial_transcript_interval_s: NotGivenOr[float] = NOT_GIVEN, final_transcript_max_duration_s: int = 30, show_word_timestamps: NotGivenOr[bool] = NOT_GIVEN, vad_threshold: float = 0.5, vad_min_silence_duration_ms: NotGivenOr[int] = NOT_GIVEN, vad_speech_pad_ms: NotGivenOr[int] = NOT_GIVEN, http_session: aiohttp.ClientSession | None = None, ): """Baseten Speech-to-Text provider. Connects to a Baseten Whisper Streaming WebSocket model for real-time transcription. Works with both **truss** and **chain** deployments. There are three ways to specify the endpoint (in priority order): 1. ``model_endpoint`` – pass the full WebSocket URL directly. 2. ``model_id`` – auto-constructs a **truss** endpoint URL:: wss://model-{model_id}.api.baseten.co/environments/production/websocket 3. ``chain_id`` – auto-constructs a **chain** endpoint URL:: wss://chain-{chain_id}.api.baseten.co/environments/production/websocket If none of the above are provided, the ``BASETEN_MODEL_ENDPOINT`` environment variable is used as a fallback. Args: api_key: Baseten API key. Falls back to the ``BASETEN_API_KEY`` env var. model_endpoint: Full WebSocket URL of the deployed model. Takes priority over ``model_id`` and ``chain_id``. model_id: Baseten **truss** model ID. The plugin builds the endpoint URL automatically. Ignored when ``model_endpoint`` is given. chain_id: Baseten **chain** ID. The plugin builds the endpoint URL automatically. Ignored when ``model_endpoint`` is given. sample_rate: Audio sample rate in Hz (default ``16000``). encoding: Audio encoding – ``pcm_s16le`` (default) or ``pcm_mulaw``. buffer_size_seconds: Audio buffer size in seconds. language: For ``whisper``, a BCP-47 code (default ``en``); use ``auto`` for detection. For ``qwen3-asr``, a Qwen3-ASR canonical language name (``English``, ``Cantonese``) or ``auto`` (the default) — forcing one also sets the *output* language and can translate, so use it only to pin what is actually spoken. language_options: Restrict automatic detection to this set of language codes, e.g. ``["en", "de"]``. Detection across all supported languages is unreliable on the one- to two-second utterances typical of telephony, so scoping it to the languages an agent actually supports is usually more accurate than ``auto``. Requires Baseten Whisper runtime v0.5.0 or newer; empty (the default) leaves detection unrestricted. enable_partial_transcripts: Emit interim transcripts while the speaker is still talking. Defaults to ``True``. partial_transcript_interval_s: Interval (seconds) between partial transcript updates. Defaults to 1.0 for ``whisper`` and 0.5 for ``qwen3-asr``, matching each deployment's own default. final_transcript_max_duration_s: Maximum seconds of audio before the server forces a final transcript. show_word_timestamps: Include word-level timestamps. Defaults to on for ``whisper``; off for ``qwen3-asr``, whose aligner is opt-in on the deployment (``STREAM_ALIGNER=mms``). vad_threshold: Server-side VAD threshold (0.0–1.0). vad_min_silence_duration_ms: Minimum silence (ms) to end an utterance. Defaults to 300 (``whisper``) / 500 (``qwen3-asr``). vad_speech_pad_ms: Padding (ms) around detected speech. Defaults to 30 (``whisper``) / 100 (``qwen3-asr``). http_session: Optional :class:`aiohttp.ClientSession` to reuse. """ self._model_name = model self._qwen3: qwen3_stt._Qwen3Backend | None = None if model == "qwen3-asr": self._qwen3 = qwen3_stt._Qwen3Backend( model_endpoint=model_endpoint, model_id=model_id, chain_id=chain_id, api_key=api_key, language=language if is_given(language) else "auto", interim_results=enable_partial_transcripts, partial_transcript_interval_s=( partial_transcript_interval_s if is_given(partial_transcript_interval_s) else 0.5 ), final_transcript_max_duration_s=final_transcript_max_duration_s, vad_threshold=vad_threshold, vad_min_silence_duration_ms=( vad_min_silence_duration_ms if is_given(vad_min_silence_duration_ms) else 500 ), vad_speech_pad_ms=vad_speech_pad_ms if is_given(vad_speech_pad_ms) else 100, # Off unless asked for: the aligner is opt-in on the deployment # (STREAM_ALIGNER), so defaulting it on would advertise a # capability the server may silently not honour. word_timestamps=show_word_timestamps if is_given(show_word_timestamps) else False, http_session=http_session, ) super().__init__( capabilities=stt.STTCapabilities( streaming=True, interim_results=enable_partial_transcripts, aligned_transcript="word" if show_word_timestamps else False, offline_recognize=True, ) ) self._session = http_session self._streams = weakref.WeakSet[stt.SpeechStream]() return super().__init__( capabilities=stt.STTCapabilities( streaming=True, interim_results=True, aligned_transcript="word", offline_recognize=False, ), ) api_key = api_key or os.environ.get("BASETEN_API_KEY") if not api_key: raise ValueError( "Baseten API key is required. " "Pass one in via the `api_key` parameter, " "or set it as the `BASETEN_API_KEY` environment variable" ) self._api_key = api_key # Resolve the WebSocket endpoint URL. # Priority: model_endpoint > model_id > chain_id > env var endpoint: str | None = None if model_endpoint: endpoint = model_endpoint elif model_id: endpoint = self._TRUSS_URL_TEMPLATE.format(model_id=model_id) elif chain_id: endpoint = self._CHAIN_URL_TEMPLATE.format(chain_id=chain_id) else: endpoint = os.environ.get("BASETEN_MODEL_ENDPOINT") if not endpoint: raise ValueError( "A Baseten endpoint is required. Provide one of: " "model_endpoint, model_id, or chain_id. " "Alternatively, set the BASETEN_MODEL_ENDPOINT environment variable." ) self._model_endpoint = endpoint self._opts = STTOptions( sample_rate=sample_rate, buffer_size_seconds=buffer_size_seconds, language=LanguageCode(language if is_given(language) else "en"), language_options=[LanguageCode(lang) for lang in language_options or []], enable_partial_transcripts=enable_partial_transcripts, partial_transcript_interval_s=( partial_transcript_interval_s if is_given(partial_transcript_interval_s) else 1.0 ), final_transcript_max_duration_s=final_transcript_max_duration_s, show_word_timestamps=(show_word_timestamps if is_given(show_word_timestamps) else True), vad_threshold=vad_threshold, vad_min_silence_duration_ms=( vad_min_silence_duration_ms if is_given(vad_min_silence_duration_ms) else 300 ), vad_speech_pad_ms=vad_speech_pad_ms if is_given(vad_speech_pad_ms) else 30, ) if is_given(encoding): self._opts.encoding = encoding self._session = http_session self._streams = weakref.WeakSet[stt.SpeechStream]() @property def model(self) -> str: return self._model_name @property def provider(self) -> str: return "Baseten" @property def session(self) -> aiohttp.ClientSession: if not self._session: self._session = utils.http_context.http_session() return self._session async def _recognize_impl( self, buffer: AudioBuffer, *, language: NotGivenOr[str] = NOT_GIVEN, conn_options: APIConnectOptions, ) -> stt.SpeechEvent: if self._qwen3 is not None: return await self._qwen3.recognize_via_stream( self, buffer, language=language, conn_options=conn_options ) raise NotImplementedError("Not implemented") def stream( self, *, language: NotGivenOr[str] = NOT_GIVEN, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS, ) -> stt.SpeechStream: if self._qwen3 is not None: qwen3_stream = self._qwen3.make_stream( self, language=language, conn_options=conn_options ) self._streams.add(qwen3_stream) return qwen3_stream config = dataclasses.replace(self._opts) stream = SpeechStream( stt=self, conn_options=conn_options, opts=config, api_key=self._api_key, model_endpoint=self._model_endpoint, http_session=self.session, ) self._streams.add(stream) return stream def update_options( self, *, vad_threshold: NotGivenOr[float] = NOT_GIVEN, vad_min_silence_duration_ms: NotGivenOr[int] = NOT_GIVEN, vad_speech_pad_ms: NotGivenOr[int] = NOT_GIVEN, language: NotGivenOr[str] = NOT_GIVEN, language_options: NotGivenOr[list[str]] = NOT_GIVEN, buffer_size_seconds: NotGivenOr[float] = NOT_GIVEN, ) -> None: if self._qwen3 is not None: self._qwen3.update_options( language=language, vad_threshold=vad_threshold, vad_min_silence_duration_ms=vad_min_silence_duration_ms, vad_speech_pad_ms=vad_speech_pad_ms, ) # These are Whisper-only; say so rather than dropping them quietly. for name, value in ( ("language_options", language_options), ("buffer_size_seconds", buffer_size_seconds), ): if is_given(value): logger.warning( "%s does not apply to model=%r and was ignored", name, self._model_name ) return if is_given(vad_threshold): self._opts.vad_threshold = vad_threshold if is_given(vad_min_silence_duration_ms): self._opts.vad_min_silence_duration_ms = vad_min_silence_duration_ms if is_given(vad_speech_pad_ms): self._opts.vad_speech_pad_ms = vad_speech_pad_ms if is_given(language): self._opts.language = LanguageCode(language) if is_given(language_options): self._opts.language_options = [LanguageCode(lang) for lang in language_options] if is_given(buffer_size_seconds): self._opts.buffer_size_seconds = buffer_size_seconds for stream in self._streams: # Only the Whisper stream carries these knobs; a qwen3-asr stream # cannot be in here, since that path returns above. if not isinstance(stream, SpeechStream): continue stream.update_options( vad_threshold=vad_threshold, vad_min_silence_duration_ms=vad_min_silence_duration_ms, vad_speech_pad_ms=vad_speech_pad_ms, language=language, language_options=language_options, buffer_size_seconds=buffer_size_seconds, )Helper class that provides a standard way to create an ABC using inheritance.
Baseten Speech-to-Text provider.
Connects to a Baseten Whisper Streaming WebSocket model for real-time transcription. Works with both truss and chain deployments.
There are three ways to specify the endpoint (in priority order):
model_endpoint– pass the full WebSocket URL directly.-
model_id– auto-constructs a truss endpoint URL::wss://model-{model_id}.api.baseten.co/environments/production/websocket
-
chain_id– auto-constructs a chain endpoint URL::wss://chain-{chain_id}.api.baseten.co/environments/production/websocket
If none of the above are provided, the
BASETEN_MODEL_ENDPOINTenvironment variable is used as a fallback.Args
api_key- Baseten API key.
Falls back to the
BASETEN_API_KEYenv var. model_endpoint- Full WebSocket URL of the deployed model.
Takes
priority over
model_idandchain_id. model_id- Baseten truss model ID.
The plugin builds the endpoint
URL automatically.
Ignored when
model_endpointis given. chain_id- Baseten chain ID.
The plugin builds the endpoint URL
automatically.
Ignored when
model_endpointis given. sample_rate- Audio sample rate in Hz (default
16000). encoding- Audio encoding –
pcm_s16le(default) orpcm_mulaw. buffer_size_seconds- Audio buffer size in seconds.
language- For
whisper, a BCP-47 code (defaulten); useautofor detection. Forqwen3-asr, a Qwen3-ASR canonical language name (English,Cantonese) orauto(the default) — forcing one also sets the output language and can translate, so use it only to pin what is actually spoken. language_options- Restrict automatic detection to this set of
language codes, e.g.
["en", "de"]. Detection across all supported languages is unreliable on the one- to two-second utterances typical of telephony, so scoping it to the languages an agent actually supports is usually more accurate thanauto. Requires Baseten Whisper runtime v0.5.0 or newer; empty (the default) leaves detection unrestricted. enable_partial_transcripts- Emit interim transcripts while the speaker
is still talking.
Defaults to
True. partial_transcript_interval_s- Interval (seconds) between partial
transcript updates. Defaults to 1.0 for
whisperand 0.5 forqwen3-asr, matching each deployment's own default. final_transcript_max_duration_s- Maximum seconds of audio before the server forces a final transcript.
show_word_timestamps- Include word-level timestamps. Defaults to
on for
whisper; off forqwen3-asr, whose aligner is opt-in on the deployment (STREAM_ALIGNER=mms). vad_threshold- Server-side VAD threshold (0.0–1.0).
vad_min_silence_duration_ms- Minimum silence (ms) to end an
utterance. Defaults to 300 (
whisper) / 500 (qwen3-asr). vad_speech_pad_ms- Padding (ms) around detected speech. Defaults to
30 (
whisper) / 100 (qwen3-asr). http_session- Optional :class:
aiohttp.ClientSessionto reuse.
Ancestors
- livekit.agents.stt.stt.STT
- abc.ABC
- EventEmitter
- typing.Generic
Instance variables
prop model : str-
Expand source code
@property def model(self) -> str: return self._model_nameGet the model name/identifier for this STT instance.
Returns
The model name if available, "unknown" otherwise.
Note
Plugins should override this property to provide their model information.
prop provider : str-
Expand source code
@property def provider(self) -> str: return "Baseten"Get the provider name/identifier for this STT instance.
Returns
The provider name if available, "unknown" otherwise.
Note
Plugins should override this property to provide their provider information.
prop session : aiohttp.ClientSession-
Expand source code
@property def session(self) -> aiohttp.ClientSession: if not self._session: self._session = utils.http_context.http_session() return self._session
Methods
def stream(self,
*,
language: NotGivenOr[str] = NOT_GIVEN,
conn_options: APIConnectOptions = APIConnectOptions(max_retry=3, retry_interval=2.0, timeout=10.0)) ‑> livekit.agents.stt.stt.RecognizeStream-
Expand source code
def stream( self, *, language: NotGivenOr[str] = NOT_GIVEN, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS, ) -> stt.SpeechStream: if self._qwen3 is not None: qwen3_stream = self._qwen3.make_stream( self, language=language, conn_options=conn_options ) self._streams.add(qwen3_stream) return qwen3_stream config = dataclasses.replace(self._opts) stream = SpeechStream( stt=self, conn_options=conn_options, opts=config, api_key=self._api_key, model_endpoint=self._model_endpoint, http_session=self.session, ) self._streams.add(stream) return stream def update_options(self,
*,
vad_threshold: NotGivenOr[float] = NOT_GIVEN,
vad_min_silence_duration_ms: NotGivenOr[int] = NOT_GIVEN,
vad_speech_pad_ms: NotGivenOr[int] = NOT_GIVEN,
language: NotGivenOr[str] = NOT_GIVEN,
language_options: NotGivenOr[list[str]] = NOT_GIVEN,
buffer_size_seconds: NotGivenOr[float] = NOT_GIVEN) ‑> None-
Expand source code
def update_options( self, *, vad_threshold: NotGivenOr[float] = NOT_GIVEN, vad_min_silence_duration_ms: NotGivenOr[int] = NOT_GIVEN, vad_speech_pad_ms: NotGivenOr[int] = NOT_GIVEN, language: NotGivenOr[str] = NOT_GIVEN, language_options: NotGivenOr[list[str]] = NOT_GIVEN, buffer_size_seconds: NotGivenOr[float] = NOT_GIVEN, ) -> None: if self._qwen3 is not None: self._qwen3.update_options( language=language, vad_threshold=vad_threshold, vad_min_silence_duration_ms=vad_min_silence_duration_ms, vad_speech_pad_ms=vad_speech_pad_ms, ) # These are Whisper-only; say so rather than dropping them quietly. for name, value in ( ("language_options", language_options), ("buffer_size_seconds", buffer_size_seconds), ): if is_given(value): logger.warning( "%s does not apply to model=%r and was ignored", name, self._model_name ) return if is_given(vad_threshold): self._opts.vad_threshold = vad_threshold if is_given(vad_min_silence_duration_ms): self._opts.vad_min_silence_duration_ms = vad_min_silence_duration_ms if is_given(vad_speech_pad_ms): self._opts.vad_speech_pad_ms = vad_speech_pad_ms if is_given(language): self._opts.language = LanguageCode(language) if is_given(language_options): self._opts.language_options = [LanguageCode(lang) for lang in language_options] if is_given(buffer_size_seconds): self._opts.buffer_size_seconds = buffer_size_seconds for stream in self._streams: # Only the Whisper stream carries these knobs; a qwen3-asr stream # cannot be in here, since that path returns above. if not isinstance(stream, SpeechStream): continue stream.update_options( vad_threshold=vad_threshold, vad_min_silence_duration_ms=vad_min_silence_duration_ms, vad_speech_pad_ms=vad_speech_pad_ms, language=language, language_options=language_options, buffer_size_seconds=buffer_size_seconds, )
Inherited members
class SpeechStream (*,
stt: STT,
opts: STTOptions,
conn_options: APIConnectOptions,
api_key: str,
model_endpoint: str,
http_session: aiohttp.ClientSession)-
Expand source code
class SpeechStream(stt.SpeechStream): """A streaming speech-to-text session connected to Baseten via WebSocket.""" # Used to close websocket _CLOSE_MSG: str = json.dumps({"terminate_session": True}) def __init__( self, *, stt: STT, opts: STTOptions, conn_options: APIConnectOptions, api_key: str, model_endpoint: str, http_session: aiohttp.ClientSession, ) -> None: super().__init__(stt=stt, conn_options=conn_options, sample_rate=opts.sample_rate) self._opts = opts self._api_key = api_key self._model_endpoint = model_endpoint self._session = http_session self._speech_duration: float = 0 # keep a list of final transcripts to combine them inside the END_OF_SPEECH event self._final_events: list[SpeechEvent] = [] self._reconnect_event = asyncio.Event() def update_options( self, *, vad_threshold: NotGivenOr[float] = NOT_GIVEN, vad_min_silence_duration_ms: NotGivenOr[int] = NOT_GIVEN, vad_speech_pad_ms: NotGivenOr[int] = NOT_GIVEN, language: NotGivenOr[str] = NOT_GIVEN, language_options: NotGivenOr[list[str]] = NOT_GIVEN, buffer_size_seconds: NotGivenOr[float] = NOT_GIVEN, ) -> None: if is_given(vad_threshold): self._opts.vad_threshold = vad_threshold if is_given(vad_min_silence_duration_ms): self._opts.vad_min_silence_duration_ms = vad_min_silence_duration_ms if is_given(vad_speech_pad_ms): self._opts.vad_speech_pad_ms = vad_speech_pad_ms if is_given(language): self._opts.language = LanguageCode(language) if is_given(language_options): self._opts.language_options = [LanguageCode(lang) for lang in language_options] if is_given(buffer_size_seconds): self._opts.buffer_size_seconds = buffer_size_seconds self._reconnect_event.set() async def _run(self) -> None: """ Run a single websocket connection to Baseten and make sure to reconnect when something went wrong. """ closing_ws = False async def send_task(ws: aiohttp.ClientWebSocketResponse) -> None: samples_per_buffer = 512 audio_bstream = utils.audio.AudioByteStream( sample_rate=self._opts.sample_rate, num_channels=1, samples_per_channel=samples_per_buffer, ) try: async for data in self._input_ch: if isinstance(data, self._FlushSentinel): frames = audio_bstream.flush() else: frames = audio_bstream.write(data.data.tobytes()) for frame in frames: if len(frame.data) % 2 != 0: logger.warning("Frame data size not aligned to float32 (multiple of 4)") int16_array = np.frombuffer(frame.data, dtype=np.int16) await ws.send_bytes(int16_array.tobytes()) except (aiohttp.ClientError, ConnectionError) as e: if closing_ws or self._session.closed: return raise APIConnectionError("Baseten connection closed unexpectedly") from e async def recv_task(ws: aiohttp.ClientWebSocketResponse) -> None: nonlocal closing_ws while True: try: msg = await asyncio.wait_for(ws.receive(), timeout=5) except asyncio.TimeoutError: if closing_ws: break continue if msg.type in ( aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING, ): if closing_ws: return raise APIStatusError( "Baseten connection closed unexpectedly", status_code=ws.close_code or -1, body=f"{msg.data=} {msg.extra=}", ) if msg.type != aiohttp.WSMsgType.TEXT: logger.error("Unexpected Baseten message type: %s", msg.type) continue try: data = json.loads(msg.data) # Skip non-transcription messages (e.g. error, status) msg_type = data.get("type") if msg_type and msg_type not in ("transcription",): logger.debug("Ignoring message type: %s", msg_type) continue is_final = data.get("is_final", True) segments = data.get("segments", []) # Build transcript text: prefer top-level "transcript" if present, # otherwise concatenate segment texts (Baseten standard format). text = ( data.get("transcript") or " ".join(seg.get("text", "") for seg in segments).strip() ) confidence = data.get("confidence", 0.0) # Build timed words – prefer word-level timestamps when available, # fall back to segment-level timing. timed_words: list[TimedString] = [] for segment in segments: word_timestamps = segment.get("word_timestamps", []) if word_timestamps: for w in word_timestamps: timed_words.append( TimedString( text=w.get("word", ""), start_time=( w.get("start_time", 0.0) + self.start_time_offset ), end_time=(w.get("end_time", 0.0) + self.start_time_offset), start_time_offset=self.start_time_offset, ) ) else: timed_words.append( TimedString( text=segment.get("text", ""), start_time=( segment.get("start_time", 0.0) + self.start_time_offset ), end_time=( segment.get("end_time", 0.0) + self.start_time_offset ), start_time_offset=self.start_time_offset, ) ) start_time = ( segments[0].get("start_time", 0.0) if segments else 0.0 ) + self.start_time_offset end_time = ( segments[-1].get("end_time", 0.0) if segments else 0.0 ) + self.start_time_offset if not is_final: if text: event = stt.SpeechEvent( type=stt.SpeechEventType.INTERIM_TRANSCRIPT, alternatives=[ stt.SpeechData( language=LanguageCode(""), text=text, confidence=confidence, start_time=start_time, end_time=end_time, words=timed_words, ) ], ) self._event_ch.send_nowait(event) elif is_final: language = LanguageCode(data.get("language_code", self._opts.language)) if text: event = stt.SpeechEvent( type=stt.SpeechEventType.FINAL_TRANSCRIPT, alternatives=[ stt.SpeechData( language=language, text=text, confidence=confidence, start_time=start_time, end_time=end_time, words=timed_words, ) ], ) self._final_events.append(event) self._event_ch.send_nowait(event) except Exception: logger.exception("Failed to process message from Baseten") ws: aiohttp.ClientWebSocketResponse | None = None while True: try: ws = await self._connect_ws() tasks = [ asyncio.create_task(send_task(ws)), asyncio.create_task(recv_task(ws)), ] wait_reconnect_task = asyncio.create_task(self._reconnect_event.wait()) try: done, _ = await asyncio.wait( (asyncio.gather(*tasks), wait_reconnect_task), return_when=asyncio.FIRST_COMPLETED, ) for task in done: if task != wait_reconnect_task: task.result() if wait_reconnect_task not in done: break self._reconnect_event.clear() finally: await utils.aio.gracefully_cancel(*tasks, wait_reconnect_task) finally: if ws is not None: await ws.close() async def _connect_ws(self) -> aiohttp.ClientWebSocketResponse: """Open a WebSocket and send the ``StreamingWhisperInput`` metadata message. The metadata schema must match the Baseten server's ``StreamingWhisperInput`` Pydantic model exactly (which uses ``extra="forbid"``). Field names are: - ``whisper_params`` – Whisper model parameters (language, word timestamps, …) - ``streaming_params`` – encoding, sample rate, partial transcript settings - ``streaming_vad_config`` – server-side Silero VAD configuration """ headers = { "Authorization": f"Api-Key {self._api_key}", } ws = await self._session.ws_connect(self._model_endpoint, headers=headers, ssl=ssl_context) # Build metadata matching Baseten's StreamingWhisperInput schema. # See: https://docs.baseten.co/reference/inference-api/predict-endpoints/streaming-transcription-api metadata: dict[str, dict[str, Any]] = { "whisper_params": { "audio_language": self._opts.language, "show_word_timestamps": self._opts.show_word_timestamps, }, "streaming_params": { "encoding": self._opts.encoding, "sample_rate": self._opts.sample_rate, "enable_partial_transcripts": self._opts.enable_partial_transcripts, "partial_transcript_interval_s": self._opts.partial_transcript_interval_s, "final_transcript_max_duration_s": self._opts.final_transcript_max_duration_s, }, "streaming_vad_config": { "threshold": self._opts.vad_threshold, "min_silence_duration_ms": self._opts.vad_min_silence_duration_ms, "speech_pad_ms": self._opts.vad_speech_pad_ms, }, } if self._opts.language_options: # Omitted unless set: older Whisper runtimes (< v0.5.0) reject # unknown whisper_params fields. metadata["whisper_params"]["language_options"] = [ str(lang) for lang in self._opts.language_options ] await ws.send_str(json.dumps(metadata)) return wsA streaming speech-to-text session connected to Baseten via WebSocket.
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
Methods
def update_options(self,
*,
vad_threshold: NotGivenOr[float] = NOT_GIVEN,
vad_min_silence_duration_ms: NotGivenOr[int] = NOT_GIVEN,
vad_speech_pad_ms: NotGivenOr[int] = NOT_GIVEN,
language: NotGivenOr[str] = NOT_GIVEN,
language_options: NotGivenOr[list[str]] = NOT_GIVEN,
buffer_size_seconds: NotGivenOr[float] = NOT_GIVEN) ‑> None-
Expand source code
def update_options( self, *, vad_threshold: NotGivenOr[float] = NOT_GIVEN, vad_min_silence_duration_ms: NotGivenOr[int] = NOT_GIVEN, vad_speech_pad_ms: NotGivenOr[int] = NOT_GIVEN, language: NotGivenOr[str] = NOT_GIVEN, language_options: NotGivenOr[list[str]] = NOT_GIVEN, buffer_size_seconds: NotGivenOr[float] = NOT_GIVEN, ) -> None: if is_given(vad_threshold): self._opts.vad_threshold = vad_threshold if is_given(vad_min_silence_duration_ms): self._opts.vad_min_silence_duration_ms = vad_min_silence_duration_ms if is_given(vad_speech_pad_ms): self._opts.vad_speech_pad_ms = vad_speech_pad_ms if is_given(language): self._opts.language = LanguageCode(language) if is_given(language_options): self._opts.language_options = [LanguageCode(lang) for lang in language_options] if is_given(buffer_size_seconds): self._opts.buffer_size_seconds = buffer_size_seconds self._reconnect_event.set()
class SynthesizeStream (*,
tts: TTS,
conn_options: APIConnectOptions)-
Expand source code
class SynthesizeStream(tts.SynthesizeStream): def __init__( self, *, tts: TTS, conn_options: APIConnectOptions, ) -> None: super().__init__(tts=tts, conn_options=conn_options) self._tts: TTS = tts self._opts = replace(tts._opts) async def _run(self, output_emitter: tts.AudioEmitter) -> None: request_id = utils.shortuuid() output_emitter.initialize( request_id=request_id, sample_rate=24000, num_channels=1, mime_type="audio/pcm", stream=True, ) async def _send_task(ws: aiohttp.ClientWebSocketResponse) -> None: async for data in self._input_ch: if isinstance(data, self._FlushSentinel): continue self._mark_started() await ws.send_str(data) await ws.send_str(_END_SENTINEL) async def _recv_task(ws: aiohttp.ClientWebSocketResponse) -> None: output_emitter.start_segment(segment_id=request_id) async for msg in ws: if msg.type == aiohttp.WSMsgType.BINARY: output_emitter.push(msg.data) elif msg.type in ( aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSING, ): break elif msg.type == aiohttp.WSMsgType.ERROR: raise APIConnectionError() output_emitter.end_input() try: async with self._tts._ensure_session().ws_connect( self._tts._model_endpoint, headers={"Authorization": f"Api-Key {self._tts._api_key}"}, ssl=ssl_context, ) as ws: await ws.send_json( { "voice": self._opts.voice, "max_tokens": self._opts.max_tokens, "buffer_size": self._opts.buffer_size, } ) tasks = [ asyncio.create_task(_send_task(ws)), asyncio.create_task(_recv_task(ws)), ] try: await asyncio.gather(*tasks) finally: await utils.aio.gracefully_cancel(*tasks) except asyncio.TimeoutError: raise APITimeoutError() from None except aiohttp.ClientResponseError as e: raise APIStatusError( message=e.message, status_code=e.status, request_id=None, body=None ) from None except (APIConnectionError, APIStatusError, APITimeoutError): raise except Exception as e: raise APIConnectionError() from eHelper class that provides a standard way to create an ABC using inheritance.
Ancestors
- livekit.agents.tts.tts.SynthesizeStream
- abc.ABC
class TTS (*,
model: TTSModels = 'orpheus',
api_key: str | None = None,
model_endpoint: str | None = None,
model_id: str | None = None,
chain_id: str | None = None,
voice: NotGivenOr[str] = NOT_GIVEN,
language: NotGivenOr[str] = NOT_GIVEN,
temperature: float = 0.6,
max_tokens: int = 2000,
buffer_size: int = 10,
task_type: str = 'Base',
instructions: str | None = None,
max_new_tokens: int | None = None,
initial_codec_chunk_frames: int | None = None,
x_vector_only_mode: bool | None = None,
ref_audio: str | None = None,
ref_text: str | None = None,
word_timestamps: bool = False,
extra_config: dict[str, Any] | None = None,
http_session: aiohttp.ClientSession | None = None)-
Expand source code
class TTS(tts.TTS): """Text-to-speech for a Baseten-hosted model. ``model`` selects the wire protocol, since Baseten's TTS deployments do not share one. ``orpheus`` (the default) is the existing behaviour; ``qwen3-tts`` speaks the session.config protocol and takes a registered voice clone. """ def __init__( self, *, model: TTSModels = "orpheus", api_key: str | None = None, model_endpoint: str | None = None, model_id: str | None = None, chain_id: str | None = None, voice: NotGivenOr[str] = NOT_GIVEN, language: NotGivenOr[str] = NOT_GIVEN, temperature: float = 0.6, max_tokens: int = 2000, buffer_size: int = 10, task_type: str = "Base", instructions: str | None = None, max_new_tokens: int | None = None, initial_codec_chunk_frames: int | None = None, x_vector_only_mode: bool | None = None, ref_audio: str | None = None, ref_text: str | None = None, word_timestamps: bool = False, extra_config: dict[str, Any] | None = None, http_session: aiohttp.ClientSession | None = None, ) -> None: """ Initialize the Baseten TTS. Args: model: Which deployment protocol to speak — ``orpheus`` (default) or ``qwen3-tts``. They are not interchangeable. api_key: Baseten API key, or ``BASETEN_API_KEY`` env var. model_id: Baseten truss model ID; builds the endpoint URL for you (``qwen3-tts`` only). chain_id: Baseten chain ID; builds the endpoint URL for you (``qwen3-tts`` only). task_type: ``qwen3-tts`` only. ``Base`` for the voice-cloning checkpoint; ``CustomVoice``/``VoiceDesign`` deployments expose preset speaker names. instructions: ``qwen3-tts`` style prompt (CustomVoice/VoiceDesign). max_new_tokens: ``qwen3-tts`` cap on audio tokens per sentence. initial_codec_chunk_frames: ``qwen3-tts`` first-chunk size; larger trades time-to-first-audio for onset quality. x_vector_only_mode: ``qwen3-tts``; condition on the speaker embedding alone and skip in-context learning from the reference. ref_audio: ``qwen3-tts`` reference clip for inline cloning — an http(s) URL or a local file path. ref_text: Transcript of ``ref_audio``; improves clone fidelity. word_timestamps: ``qwen3-tts``; forward word-level alignment to LiveKit as a timed transcript. extra_config: ``qwen3-tts``; merged into ``session.config`` last. model_endpoint: Baseten model endpoint, or ``BASETEN_MODEL_ENDPOINT`` env var. Pass a ``wss://`` URL for streaming or an ``https://`` URL for non-streaming. voice: Speaker voice. Defaults to ``tara`` for ``orpheus``. Required for ``qwen3-tts``, where it names a registered voice clone — that checkpoint ships no built-in speakers (see `register_voice`). language: Language code. Defaults to ``en`` for ``orpheus`` and ``Auto`` for ``qwen3-tts``. temperature: Sampling temperature. Defaults to 0.6. max_tokens: Maximum tokens for generation. Defaults to 2000. buffer_size: Number of words per chunk for streaming. Defaults to 10. http_session: Optional aiohttp session to reuse. Raises: ValueError: If the API key or endpoint cannot be resolved, or if ``model="qwen3-tts"`` without a ``voice``. """ self._model_name = model self._qwen3: qwen3_tts._Qwen3Backend | None = None if model == "qwen3-tts": self._qwen3 = qwen3_tts._Qwen3Backend( model_endpoint=model_endpoint, model_id=model_id, chain_id=chain_id, api_key=api_key, voice=voice if is_given(voice) else "", task_type=task_type, language=language if is_given(language) else "Auto", instructions=instructions, max_new_tokens=max_new_tokens, initial_codec_chunk_frames=initial_codec_chunk_frames, x_vector_only_mode=x_vector_only_mode, ref_audio=ref_audio, ref_text=ref_text, word_timestamps=word_timestamps, extra_config=extra_config, http_session=http_session, ) super().__init__( capabilities=tts.TTSCapabilities( streaming=True, aligned_transcript=word_timestamps ), sample_rate=qwen3_tts.SAMPLE_RATE, num_channels=qwen3_tts.NUM_CHANNELS, ) self._session = http_session self._streams = weakref.WeakSet[tts.SynthesizeStream]() return api_key = api_key or os.environ.get("BASETEN_API_KEY") if not api_key: raise ValueError( "Baseten API key is required. " "Pass one in via the `api_key` parameter, " "or set it as the `BASETEN_API_KEY` environment variable" ) model_endpoint = model_endpoint or os.environ.get("BASETEN_MODEL_ENDPOINT") if not model_endpoint: raise ValueError( "model_endpoint is required. " "Provide it via the constructor or BASETEN_MODEL_ENDPOINT env var." ) is_ws = model_endpoint.startswith(("wss://", "ws://")) super().__init__( capabilities=tts.TTSCapabilities(streaming=is_ws), sample_rate=24000, num_channels=1, ) self._api_key = api_key self._model_endpoint = model_endpoint self._opts = _TTSOptions( voice=voice if is_given(voice) else "tara", language=language if is_given(language) else "en", temperature=temperature, max_tokens=max_tokens, buffer_size=buffer_size, ) self._session = http_session self._streams = weakref.WeakSet[tts.SynthesizeStream]() @property def model(self) -> str: return self._model_name @property def provider(self) -> str: return "Baseten" def _ensure_session(self) -> aiohttp.ClientSession: if not self._session: self._session = utils.http_context.http_session() return self._session def update_options( self, *, voice: NotGivenOr[str] = NOT_GIVEN, language: NotGivenOr[str] = NOT_GIVEN, temperature: NotGivenOr[float] = NOT_GIVEN, max_tokens: NotGivenOr[int] = NOT_GIVEN, buffer_size: NotGivenOr[int] = NOT_GIVEN, ) -> None: if self._qwen3 is not None: self._qwen3.update_options(voice=voice, language=language) return if is_given(voice): self._opts.voice = voice if is_given(language): self._opts.language = language if is_given(temperature): self._opts.temperature = temperature if is_given(max_tokens): self._opts.max_tokens = max_tokens if is_given(buffer_size): self._opts.buffer_size = buffer_size def synthesize( self, text: str, *, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS ) -> tts.ChunkedStream: if self._qwen3 is not None: # Qwen3-TTS is streaming-only; the framework helper disables retries # on the inner stream and forwards timed transcripts. return self._synthesize_with_stream(text, conn_options=conn_options) return ChunkedStream(tts=self, input_text=text, conn_options=conn_options) def stream( self, *, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS ) -> tts.SynthesizeStream: stream: tts.SynthesizeStream if self._qwen3 is not None: stream = qwen3_tts.Qwen3SynthesizeStream( tts=self, backend=self._qwen3, conn_options=conn_options ) else: stream = SynthesizeStream(tts=self, conn_options=conn_options) self._streams.add(stream) return stream async def aclose(self) -> None: for stream in list(self._streams): await stream.aclose() self._streams.clear() if self._qwen3 is not None: await self._qwen3.aclose()Text-to-speech for a Baseten-hosted model.
modelselects the wire protocol, since Baseten's TTS deployments do not share one.orpheus(the default) is the existing behaviour;qwen3-ttsspeaks the session.config protocol and takes a registered voice clone.Initialize the Baseten TTS.
Args
model- Which deployment protocol to speak —
orpheus(default) orqwen3-tts. They are not interchangeable. api_key- Baseten API key, or
BASETEN_API_KEYenv var. model_id- Baseten truss model ID; builds the endpoint URL for you
(
qwen3-ttsonly). chain_id- Baseten chain ID; builds the endpoint URL for you
(
qwen3-ttsonly). task_typeqwen3-ttsonly.Basefor the voice-cloning checkpoint;CustomVoice/VoiceDesigndeployments expose preset speaker names.instructionsqwen3-ttsstyle prompt (CustomVoice/VoiceDesign).max_new_tokensqwen3-ttscap on audio tokens per sentence.initial_codec_chunk_framesqwen3-ttsfirst-chunk size; larger trades time-to-first-audio for onset quality.x_vector_only_modeqwen3-tts; condition on the speaker embedding alone and skip in-context learning from the reference.ref_audioqwen3-ttsreference clip for inline cloning — an http(s) URL or a local file path.ref_text- Transcript of
ref_audio; improves clone fidelity. word_timestampsqwen3-tts; forward word-level alignment to LiveKit as a timed transcript.extra_configqwen3-tts; merged intosession.configlast.model_endpoint- Baseten model endpoint, or
BASETEN_MODEL_ENDPOINTenv var. Pass awss://URL for streaming or anhttps://URL for non-streaming. voice- Speaker voice. Defaults to
tarafororpheus. Required forqwen3-tts, where it names a registered voice clone — that checkpoint ships no built-in speakers (seeregister_voice()). language- Language code. Defaults to
enfororpheusandAutoforqwen3-tts. temperature- Sampling temperature. Defaults to 0.6.
max_tokens- Maximum tokens for generation. Defaults to 2000.
buffer_size- Number of words per chunk for streaming. Defaults to 10.
http_session- Optional aiohttp session to reuse.
Raises
ValueError- If the API key or endpoint cannot be resolved, or if
model="qwen3-tts"without avoice.
Ancestors
- livekit.agents.tts.tts.TTS
- abc.ABC
- EventEmitter
- typing.Generic
Instance variables
prop model : str-
Expand source code
@property def model(self) -> str: return self._model_nameGet the model name/identifier for this TTS instance.
Returns
The model name if available, "unknown" otherwise.
Note
Plugins should override this property to provide their model information.
prop provider : str-
Expand source code
@property def provider(self) -> str: return "Baseten"Get the provider name/identifier for this TTS instance.
Returns
The provider name if available, "unknown" otherwise.
Note
Plugins should override this property to provide their provider information.
Methods
async def aclose(self) ‑> None-
Expand source code
async def aclose(self) -> None: for stream in list(self._streams): await stream.aclose() self._streams.clear() if self._qwen3 is not None: await self._qwen3.aclose() def stream(self,
*,
conn_options: APIConnectOptions = APIConnectOptions(max_retry=3, retry_interval=2.0, timeout=10.0)) ‑> livekit.agents.tts.tts.SynthesizeStream-
Expand source code
def stream( self, *, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS ) -> tts.SynthesizeStream: stream: tts.SynthesizeStream if self._qwen3 is not None: stream = qwen3_tts.Qwen3SynthesizeStream( tts=self, backend=self._qwen3, conn_options=conn_options ) else: stream = SynthesizeStream(tts=self, conn_options=conn_options) self._streams.add(stream) return stream 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: if self._qwen3 is not None: # Qwen3-TTS is streaming-only; the framework helper disables retries # on the inner stream and forwards timed transcripts. return self._synthesize_with_stream(text, conn_options=conn_options) return ChunkedStream(tts=self, input_text=text, conn_options=conn_options) def update_options(self,
*,
voice: NotGivenOr[str] = NOT_GIVEN,
language: NotGivenOr[str] = NOT_GIVEN,
temperature: NotGivenOr[float] = NOT_GIVEN,
max_tokens: NotGivenOr[int] = NOT_GIVEN,
buffer_size: NotGivenOr[int] = NOT_GIVEN) ‑> None-
Expand source code
def update_options( self, *, voice: NotGivenOr[str] = NOT_GIVEN, language: NotGivenOr[str] = NOT_GIVEN, temperature: NotGivenOr[float] = NOT_GIVEN, max_tokens: NotGivenOr[int] = NOT_GIVEN, buffer_size: NotGivenOr[int] = NOT_GIVEN, ) -> None: if self._qwen3 is not None: self._qwen3.update_options(voice=voice, language=language) return if is_given(voice): self._opts.voice = voice if is_given(language): self._opts.language = language if is_given(temperature): self._opts.temperature = temperature if is_given(max_tokens): self._opts.max_tokens = max_tokens if is_given(buffer_size): self._opts.buffer_size = buffer_size
Inherited members