Module livekit.plugins.speechify
Speechify plugin for LiveKit Agents
Provides Speechify text-to-speech for LiveKit voice agents. See https://docs.speechify.ai for API details.
Classes
class ChunkedStream (*,
tts: TTS,
input_text: str,
conn_options: APIConnectOptions)-
Expand source code
class ChunkedStream(tts.ChunkedStream): def __init__(self, *, tts: TTS, input_text: str, conn_options: APIConnectOptions) -> None: super().__init__(tts=tts, input_text=input_text, conn_options=conn_options) self._tts: TTS = tts self._opts = replace(tts._opts) async def _run(self, output_emitter: tts.AudioEmitter) -> None: try: response = await self._tts._client.audio.speech( **_request_kwargs(self._input_text, self._opts), request_options={"timeout_in_seconds": int(self._conn_options.timeout)}, ) output_emitter.initialize( request_id=utils.shortuuid(), sample_rate=SAMPLE_RATE, num_channels=NUM_CHANNELS, mime_type=MIME_TYPE, ) timed = _timed_transcript(response.speech_marks, 0.0) if timed: output_emitter.push_timed_transcript(timed) output_emitter.push(base64.b64decode(response.audio_data)) output_emitter.flush() except Exception as e: _raise_from(e)Used by the non-streamed synthesize API, some providers support chunked http responses
Ancestors
- livekit.agents.tts.tts.ChunkedStream
- abc.ABC
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=SAMPLE_RATE, num_channels=NUM_CHANNELS, mime_type=MIME_TYPE, stream=True, ) output_emitter.start_segment(segment_id=request_id) sent_stream = self._tts._tokenizer.stream() async def _forward_input() -> None: async for data in self._input_ch: if isinstance(data, self._FlushSentinel): sent_stream.flush() continue sent_stream.push_text(data) sent_stream.end_input() async def _synthesize() -> None: offset = 0.0 async for ev in sent_stream: if not (text := ev.token.strip()): continue self._mark_started() sentence_bytes = 0 if _supports_streaming_marks(self._opts.model): stream = self._tts._client.audio.stream_with_timestamps( **_stream_request_kwargs(text, self._opts), request_options={"timeout_in_seconds": int(self._conn_options.timeout)}, ) async for event in stream: if isinstance(event, SpeechStreamEvent_SpeechChunk): if event.audio: audio = base64.b64decode(event.audio) output_emitter.push(audio) sentence_bytes += len(audio) if event.speech_marks: timed = _marks_to_timed(event.speech_marks, offset) if timed: output_emitter.push_timed_transcript(timed) elif isinstance(event, SpeechStreamEvent_SpeechError): raise APIStatusError( message=event.error.message, status_code=-1, request_id=event.request_id, body=None, ) elif isinstance(event, SpeechStreamEvent_SpeechDone): break else: response = await self._tts._client.audio.speech( **_request_kwargs(text, self._opts), request_options={"timeout_in_seconds": int(self._conn_options.timeout)}, ) audio = base64.b64decode(response.audio_data) timed = _timed_transcript(response.speech_marks, offset) if timed: output_emitter.push_timed_transcript(timed) output_emitter.push(audio) sentence_bytes = len(audio) output_emitter.flush() offset += sentence_bytes / (2 * SAMPLE_RATE * NUM_CHANNELS) output_emitter.end_segment() tasks = [ asyncio.create_task(_forward_input()), asyncio.create_task(_synthesize()), ] try: await asyncio.gather(*tasks) except Exception as e: _raise_from(e) finally: await sent_stream.aclose() await utils.aio.cancel_and_wait(*tasks)Helper class that provides a standard way to create an ABC using inheritance.
Ancestors
- livekit.agents.tts.tts.SynthesizeStream
- abc.ABC
class TTS (*,
voice_id: str = 'dominic_32',
model: TTSModels = 'simba-3.2',
language: NotGivenOr[str] = NOT_GIVEN,
loudness_normalization: NotGivenOr[bool] = NOT_GIVEN,
text_normalization: NotGivenOr[bool] = NOT_GIVEN,
api_key: NotGivenOr[str] = NOT_GIVEN,
base_url: NotGivenOr[str] = NOT_GIVEN,
tokenizer: NotGivenOr[tokenize.SentenceTokenizer] = NOT_GIVEN,
client: AsyncSpeechify | None = None,
**kwargs: Any)-
Expand source code
class TTS(tts.TTS): def __init__( self, *, voice_id: str = DEFAULT_VOICE_ID, model: TTSModels = DEFAULT_MODEL, language: NotGivenOr[str] = NOT_GIVEN, loudness_normalization: NotGivenOr[bool] = NOT_GIVEN, text_normalization: NotGivenOr[bool] = NOT_GIVEN, api_key: NotGivenOr[str] = NOT_GIVEN, base_url: NotGivenOr[str] = NOT_GIVEN, tokenizer: NotGivenOr[tokenize.SentenceTokenizer] = NOT_GIVEN, client: AsyncSpeechify | None = None, **kwargs: Any, ) -> None: """Create a new instance of Speechify TTS. ``synthesize()`` uses the Speechify ``/v1/audio/speech`` endpoint, which returns raw PCM (24 kHz mono) together with word-level speech marks in a single non-streamed response. ``stream()`` uses the ``/v1/audio/stream/with-timestamps`` endpoint, which streams audio chunks together with word-level speech marks so audio and aligned timestamps are emitted as they become final. Input is split into sentences and one streaming request is issued per sentence. Speech marks are produced by the streaming-native models (``simba-3.0`` and ``simba-3.2``); the legacy ``simba-english`` and ``simba-multilingual`` models do not serve the streaming route and fall back to one ``/v1/audio/speech`` request per sentence. Args: voice_id: Id of the voice to synthesize with. The voice must support the chosen ``model`` (see the ``/v1/voices`` endpoint). Defaults to ``dominic_32``. Voices whose id carries a ``_32`` suffix are part of the curated simba-3.2 roster; a warning is logged when they are paired with any other model. model: Synthesis model. One of ``simba-english``, ``simba-multilingual``, ``simba-3.0`` or ``simba-3.2``. Defaults to ``simba-3.2``. language: BCP-47 language code of the input (e.g. ``en-US``). loudness_normalization: Normalize output loudness to a standard level. Increases latency slightly when enabled. text_normalization: Expand numbers, dates, etc. into words before synthesis. Increases latency slightly when enabled. api_key: Speechify API key. Falls back to the ``SPEECHIFY_API_KEY`` environment variable. base_url: Override the Speechify API base URL. tokenizer: Sentence tokenizer used to chunk input in ``stream()``. client: A preconfigured ``AsyncSpeechify`` client. When provided, ``api_key`` and ``base_url`` are ignored. **kwargs: Catches deprecated parameters. A warning is logged for any recognised deprecated name. """ super().__init__( capabilities=tts.TTSCapabilities(streaming=True, aligned_transcript=True), sample_rate=SAMPLE_RATE, num_channels=NUM_CHANNELS, ) self._owns_client = client is None if client is not None: self._client = client else: resolved_key = api_key if is_given(api_key) else os.environ.get("SPEECHIFY_API_KEY") if not resolved_key: raise ValueError( "Speechify API key is required, either as the api_key argument " "or via the SPEECHIFY_API_KEY environment variable" ) # Fixed httpx.AsyncClient default header so every request the SDK # issues is attributed to this integration, regardless of call site. # Timeout/limits mirror the openai plugin's owned-client defaults — # httpx's own 5s default is too short for longer synthesis requests. self._httpx_client = httpx.AsyncClient( headers={CALLER_HEADER: "livekit"}, timeout=httpx.Timeout(connect=15.0, read=30.0, write=30.0, pool=5.0), limits=httpx.Limits( max_connections=50, max_keepalive_connections=50, keepalive_expiry=120 ), ) self._client = AsyncSpeechify( token=resolved_key, base_url=base_url if is_given(base_url) else None, httpx_client=self._httpx_client, ) self._tokenizer = tokenizer if is_given(tokenizer) else tokenize.basic.SentenceTokenizer() self._opts = _TTSOptions( voice_id=voice_id, model=model, language=language, loudness_normalization=loudness_normalization, text_normalization=text_normalization, ) _check_deprecated_args(kwargs) _warn_voice_model_compat(self._opts.voice_id, self._opts.model) @property def model(self) -> str: return self._opts.model if is_given(self._opts.model) else "unknown" @property def provider(self) -> str: return "Speechify" async def aclose(self) -> None: if self._owns_client: await self._httpx_client.aclose() async def list_voices(self) -> list[Voice]: """List the voices available for the configured Speechify account.""" sdk_voices: list[GetVoice] = await self._client.voices.list() return [_voice_from_sdk(v) for v in sdk_voices] def update_options( self, *, voice_id: NotGivenOr[str] = NOT_GIVEN, model: NotGivenOr[TTSModels] = NOT_GIVEN, language: NotGivenOr[str] = NOT_GIVEN, loudness_normalization: NotGivenOr[bool] = NOT_GIVEN, text_normalization: NotGivenOr[bool] = NOT_GIVEN, ) -> None: if is_given(voice_id): self._opts.voice_id = voice_id if is_given(model): self._opts.model = model if is_given(language): self._opts.language = language if is_given(loudness_normalization): self._opts.loudness_normalization = loudness_normalization if is_given(text_normalization): self._opts.text_normalization = text_normalization _warn_voice_model_compat(self._opts.voice_id, self._opts.model) def synthesize( self, text: str, *, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS, ) -> ChunkedStream: return ChunkedStream(tts=self, input_text=text, conn_options=conn_options) def stream( self, *, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS, ) -> SynthesizeStream: return SynthesizeStream(tts=self, conn_options=conn_options)Helper class that provides a standard way to create an ABC using inheritance.
Create a new instance of Speechify TTS.
synthesize()uses the Speechify/v1/audio/speechendpoint, which returns raw PCM (24 kHz mono) together with word-level speech marks in a single non-streamed response.stream()uses the/v1/audio/stream/with-timestampsendpoint, which streams audio chunks together with word-level speech marks so audio and aligned timestamps are emitted as they become final. Input is split into sentences and one streaming request is issued per sentence. Speech marks are produced by the streaming-native models (simba-3.0andsimba-3.2); the legacysimba-englishandsimba-multilingualmodels do not serve the streaming route and fall back to one/v1/audio/speechrequest per sentence.Args
voice_id- Id of the voice to synthesize with. The voice must support
the chosen
model(see the/v1/voicesendpoint). Defaults todominic_32. Voices whose id carries a_32suffix are part of the curated simba-3.2 roster; a warning is logged when they are paired with any other model. model- Synthesis model. One of
simba-english,simba-multilingual,simba-3.0orsimba-3.2. Defaults tosimba-3.2. language- BCP-47 language code of the input (e.g.
en-US). loudness_normalization- Normalize output loudness to a standard level. Increases latency slightly when enabled.
text_normalization- Expand numbers, dates, etc. into words before synthesis. Increases latency slightly when enabled.
api_key- Speechify API key. Falls back to the
SPEECHIFY_API_KEYenvironment variable. base_url- Override the Speechify API base URL.
tokenizer- Sentence tokenizer used to chunk input in
stream(). client- A preconfigured
AsyncSpeechifyclient. When provided,api_keyandbase_urlare ignored. **kwargs- Catches deprecated parameters. A warning is logged for any recognised deprecated name.
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._opts.model if is_given(self._opts.model) else "unknown"Get 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 "Speechify"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: if self._owns_client: await self._httpx_client.aclose() async def list_voices(self) ‑> list[livekit.plugins.speechify.tts.Voice]-
Expand source code
async def list_voices(self) -> list[Voice]: """List the voices available for the configured Speechify account.""" sdk_voices: list[GetVoice] = await self._client.voices.list() return [_voice_from_sdk(v) for v in sdk_voices]List the voices available for the configured Speechify account.
def stream(self,
*,
conn_options: APIConnectOptions = APIConnectOptions(max_retry=3, retry_interval=2.0, timeout=10.0)) ‑> livekit.plugins.speechify.tts.SynthesizeStream-
Expand source code
def stream( self, *, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS, ) -> SynthesizeStream: return SynthesizeStream(tts=self, conn_options=conn_options) def synthesize(self,
text: str,
*,
conn_options: APIConnectOptions = APIConnectOptions(max_retry=3, retry_interval=2.0, timeout=10.0)) ‑> livekit.plugins.speechify.tts.ChunkedStream-
Expand source code
def synthesize( self, text: str, *, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS, ) -> ChunkedStream: return ChunkedStream(tts=self, input_text=text, conn_options=conn_options) def update_options(self,
*,
voice_id: NotGivenOr[str] = NOT_GIVEN,
model: NotGivenOr[TTSModels] = NOT_GIVEN,
language: NotGivenOr[str] = NOT_GIVEN,
loudness_normalization: NotGivenOr[bool] = NOT_GIVEN,
text_normalization: NotGivenOr[bool] = NOT_GIVEN) ‑> None-
Expand source code
def update_options( self, *, voice_id: NotGivenOr[str] = NOT_GIVEN, model: NotGivenOr[TTSModels] = NOT_GIVEN, language: NotGivenOr[str] = NOT_GIVEN, loudness_normalization: NotGivenOr[bool] = NOT_GIVEN, text_normalization: NotGivenOr[bool] = NOT_GIVEN, ) -> None: if is_given(voice_id): self._opts.voice_id = voice_id if is_given(model): self._opts.model = model if is_given(language): self._opts.language = language if is_given(loudness_normalization): self._opts.loudness_normalization = loudness_normalization if is_given(text_normalization): self._opts.text_normalization = text_normalization _warn_voice_model_compat(self._opts.voice_id, self._opts.model)
Inherited members
class Voice (id: str,
type: VoiceType,
display_name: str,
gender: Gender,
avatar_image: str | None,
models: list[TTSModels],
locale: str)-
Expand source code
@dataclass class Voice: id: str type: VoiceType display_name: str gender: Gender avatar_image: str | None models: list[TTSModels] locale: strVoice(id: 'str', type: 'VoiceType', display_name: 'str', gender: 'Gender', avatar_image: 'str | None', models: 'list[TTSModels]', locale: 'str')
Instance variables
var avatar_image : str | Nonevar display_name : strvar gender : Literal['male', 'female', 'neutral']var id : strvar locale : strvar models : list[typing.Literal['simba-english', 'simba-multilingual', 'simba-3.0', 'simba-3.2']]var type : Literal['shared', 'personal']