Source code for speaker_helper.types

"""
Typed data structures exchanged across speaker-helper's public surface.

Module summary
--------------
speaker-helper turns text into speech. The values
that cross its API boundaries are described here as dataclasses so callers
never guess dictionary shapes and editors can autocomplete. Audio is carried
as an in-memory ``bytes`` WAV payload plus the metadata a caller needs to save,
stream, or measure it (sample rate, duration, real-time factor).

Usage example
-------------
>>> from speaker_helper.types import Voice
>>> v = Voice(voice_id="ff_siwis", name="Siwis", language="fr", gender="female")
>>> v.language
'fr'

Author
------
Warith HARCHAOUI — https://linkedin.com/in/warith-harchaoui
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Literal

# A synthesis run targets one of two modes: ``offline`` optimises
# throughput/quality, ``streaming`` optimises time to first audio by splitting
# the text and emitting chunks as they are ready.
Mode = Literal["offline", "streaming"]


[docs] @dataclass(frozen=True) class Voice: """A preset voice offered by a backend. Parameters ---------- voice_id : str Engine-specific identifier (e.g. ``"ff_siwis"`` for kokoro French). name : str Human-readable display name. language : str ISO-639-1 language code the voice speaks (e.g. ``"fr"``, ``"en"``). gender : str Reported voice gender, or ``""`` when the engine does not expose one. engine : str Engine the voice belongs to (e.g. ``"kokoro"``). """ voice_id: str name: str language: str gender: str = "" engine: str = ""
[docs] @dataclass class AudioResult: """A synthesised audio payload and its measured metadata. Parameters ---------- wav_bytes : bytes The complete WAV file contents (RIFF header + PCM), ready to write to disk or stream to a client. sample_rate : int Sample rate of the audio in Hz. duration_s : float Audio duration in seconds. compute_s : float Wall-clock seconds spent producing this audio (network + synthesis). text : str The text that was synthesised. voice_id : str The voice used. language : str The language used. Notes ----- ``rtf`` (real-time factor) is derived, not stored: ``compute_s / duration_s``. A value below ``1.0`` means synthesis was faster than real time — the operating point speaker-helper aims for. """ wav_bytes: bytes sample_rate: int duration_s: float compute_s: float text: str voice_id: str language: str @property def rtf(self) -> float: """Real-time factor ``compute_s / duration_s`` (``inf`` if silent). Returns ------- float Below ``1.0`` means faster than real time. """ # Guard against a zero-duration (silent/empty) result: dividing would # raise, so report ``inf`` — treated as "infinitely slower than real # time" and caught as an anomaly downstream. return self.compute_s / self.duration_s if self.duration_s > 0 else float("inf")
[docs] @dataclass class StreamChunk: """One chunk of a streaming synthesis (one sentence-sized unit). Parameters ---------- seq : int Zero-based index of this chunk within the stream. audio : AudioResult The synthesised audio for this chunk. ttfa_s : float | None Time-to-first-audio in seconds, set only on the first chunk (``seq == 0``); ``None`` afterwards. is_final : bool ``True`` for the last chunk of the stream. """ seq: int audio: AudioResult ttfa_s: float | None = None is_final: bool = False
[docs] @dataclass class VoiceSample: """One reference recording used to clone a voice. Parameters ---------- audio : bytes or str The reference audio: raw bytes, or a filesystem path to an audio file. reference_text : str A transcript of exactly what is said in ``audio``. Cloning engines align the audio to this text, so it must match the recording. Notes ----- Keep samples short and clean (a few seconds of clear speech). Provide several for a more robust clone. """ audio: bytes | str reference_text: str
[docs] def read_bytes(self) -> bytes: """Return the audio as bytes, reading from disk if a path was given.""" # ``audio`` is either the raw payload already, or a path to read from. if isinstance(self.audio, bytes): return self.audio from pathlib import Path return Path(self.audio).read_bytes()
[docs] def filename(self) -> str: """Return a filename to send with the upload (from the path, or a default).""" # A path-backed sample keeps its basename (helps the engine label it); # an in-memory sample has no name, so fall back to a generic one. if isinstance(self.audio, str): from pathlib import Path return Path(self.audio).name return "sample.wav"
[docs] @dataclass class VoiceList: """Voices available for an engine. Parameters ---------- engine : str The engine the voices belong to. voices : list[Voice] The preset voices, possibly empty. """ engine: str voices: list[Voice] = field(default_factory=list)