Skip to content

sadda.tts

Backend-agnostic text-to-speech: synthesize text (and whole narration scripts) to WAV through a pluggable backend. The immediate driver is voiceover for generated documentation; the surface is general enough for any TTS task. PROVISIONAL tier.

For a task-oriented walk-through — narration scripts, caching, custom backends, and localization — see the Text-to-speech & voiceover guide.

The default backend, espeak-ng, requires the espeak-ng system binary (it is not a pip dependency). A high-quality neural backend (Kokoro) is planned behind a future sadda[tts] extra; requesting backend="kokoro" today raises an actionable error.

Synthesis

synthesize

synthesize(text: str, out_path: Union[str, Path], *, backend: Union = None, voice: Optional[str] = None, rate: Optional[float] = None) -> SynthesisResult

One-shot: synthesize text to out_path. Convenience over a backend.

synthesize_script

synthesize_script(script: NarrationScript, out_dir: Union[str, Path], *, backend: Union = None, cache_dir: Union[str, Path, None] = None, assemble: bool = True, combined_name: str = 'narration.wav') -> ScriptResult

Synthesize every segment of script, caching by content hash.

Each segment is synthesized into cache_dir (default out_dir/.cache) under its :func:cache_key; a cache hit is reused verbatim, so re-running after editing one line only re-synthesizes that line. Per-segment WAVs are also copied to out_dir as segment_000.wav … for inspection.

If assemble (the default), the segments are concatenated into out_dir/<combined_name> honoring each segment's pause_after_s.

ScriptResult

ScriptResult(segments: tuple[SynthesisResult, ...], combined: Optional[Path], total_duration_s: float)

The outcome of synthesizing a whole :class:NarrationScript.

__annotations__ class-attribute

__annotations__ = {'segments': 'tuple[SynthesisResult, ...]', 'combined': 'Optional[Path]', 'total_duration_s': 'float'}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__dataclass_fields__ class-attribute

__dataclass_fields__ = {'segments': Field(name='segments',type='tuple[SynthesisResult, ...]',default=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD), 'combined': Field(name='combined',type='Optional[Path]',default=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD), 'total_duration_s': Field(name='total_duration_s',type='float',default=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__doc__ class-attribute

__doc__ = 'The outcome of synthesizing a whole :class:`NarrationScript`.'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

__match_args__ class-attribute

__match_args__ = ('segments', 'combined', 'total_duration_s')

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__module__ class-attribute

__module__ = 'sadda.tts.pipeline'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

__weakref__ property

__weakref__

list of weak references to the object

SynthesisResult

SynthesisResult(path: Path, sample_rate: int, duration_s: float)

The outcome of synthesizing one span of text to a WAV file.

__annotations__ class-attribute

__annotations__ = {'path': 'Path', 'sample_rate': 'int', 'duration_s': 'float'}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__dataclass_fields__ class-attribute

__dataclass_fields__ = {'path': Field(name='path',type='Path',default=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD), 'sample_rate': Field(name='sample_rate',type='int',default=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD), 'duration_s': Field(name='duration_s',type='float',default=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__doc__ class-attribute

__doc__ = 'The outcome of synthesizing one span of text to a WAV file.'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

__match_args__ class-attribute

__match_args__ = ('path', 'sample_rate', 'duration_s')

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__module__ class-attribute

__module__ = 'sadda.tts.backends'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

__weakref__ property

__weakref__

list of weak references to the object

Assembly & caching

concat_wavs

concat_wavs(paths: Sequence[Union[str, Path]], out_path: Union[str, Path], *, pauses_s: Optional[Sequence[float]] = None) -> SynthesisResult

Concatenate WAV files into one, inserting silence between them.

pauses_s[i] is the silence appended after paths[i]. All inputs must share the same sample rate, channel count, and sample width; a mismatch raises :class:ValueError rather than producing a corrupt file.

cache_key

cache_key(text: str, *, backend: str, voice: Optional[str], rate: Optional[float]) -> str

Deterministic hex digest identifying one synthesized span.

Any change to the text, voice, rate, or backend name yields a different key; identical inputs always yield the same key.

Narration scripts

Segment

Segment(text: str, voice: Optional[str] = None, rate: Optional[float] = None, pause_after_s: float = 0.0, id: Optional[str] = None)

One span of narration.

Attributes:

  • text

    The words to speak. Leading/trailing whitespace is ignored.

  • voice

    Backend-specific voice id (e.g. "en-us" for espeak-ng), or None to fall back to the script default, then the backend default. Voice strings are intentionally backend-specific — the abstraction passes them through unchanged.

  • rate

    Relative speaking-rate multiplier where 1.0 is the backend's natural rate, 1.2 is 20% faster, 0.9 is 10% slower. None falls back to the script default, then the backend default.

  • pause_after_s

    Silence, in seconds, appended after this segment when a script is assembled into a single track.

  • id

    Optional stable identifier (e.g. a scene/slide name). Not used for synthesis; carried through so callers can align segments with screencast markers or captions.

__annotations__ class-attribute

__annotations__ = {'text': 'str', 'voice': 'Optional[str]', 'rate': 'Optional[float]', 'pause_after_s': 'float', 'id': 'Optional[str]'}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__dataclass_fields__ class-attribute

__dataclass_fields__ = {'text': Field(name='text',type='str',default=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,default_factory=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD), 'voice': Field(name='voice',type='Optional[str]',default=None,default_factory=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD), 'rate': Field(name='rate',type='Optional[float]',default=None,default_factory=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD), 'pause_after_s': Field(name='pause_after_s',type='float',default=0.0,default_factory=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD), 'id': Field(name='id',type='Optional[str]',default=None,default_factory=<dataclasses._MISSING_TYPE object at 0x7f0dcc0ee060>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__doc__ class-attribute

__doc__ = 'One span of narration.\n\n    Attributes:\n        text: The words to speak. Leading/trailing whitespace is ignored.\n        voice: Backend-specific voice id (e.g. ``"en-us"`` for espeak-ng),\n            or ``None`` to fall back to the script default, then the backend\n            default. Voice strings are intentionally backend-specific — the\n            abstraction passes them through unchanged.\n        rate: Relative speaking-rate multiplier where ``1.0`` is the backend\'s\n            natural rate, ``1.2`` is 20% faster, ``0.9`` is 10% slower.\n            ``None`` falls back to the script default, then the backend default.\n        pause_after_s: Silence, in seconds, appended after this segment when a\n            script is assembled into a single track.\n        id: Optional stable identifier (e.g. a scene/slide name). Not used for\n            synthesis; carried through so callers can align segments with\n            screencast markers or captions.\n    '

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

__match_args__ class-attribute

__match_args__ = ('text', 'voice', 'rate', 'pause_after_s', 'id')

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__module__ class-attribute

__module__ = 'sadda.tts.script'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

__weakref__ property

__weakref__

list of weak references to the object

pause_after_s class-attribute

pause_after_s = 0.0

Convert a string or number to a floating point number, if possible.

NarrationScript

NarrationScript(segments: tuple[Segment, ...] = <factory>, voice: Optional[str] = None, rate: Optional[float] = None)

An ordered collection of :class:Segment\ s with script-wide defaults.

A segment's own voice / rate win over the script-wide defaults, which in turn win over the backend's defaults.

from_texts classmethod

from_texts(texts: Iterable[str], *, voice: Optional[str] = None, rate: Optional[float] = None, pause_after_s: float = 0.0) -> NarrationScript

Build a script from plain strings, one :class:Segment each.

resolved_voice

resolved_voice(segment: Segment) -> Optional[str]

The voice a segment should use, applying the fallback chain.

resolved_rate

resolved_rate(segment: Segment) -> Optional[float]

The rate a segment should use, applying the fallback chain.

parse_script

parse_script(text: str) -> NarrationScript

Parse a minimal plain-text narration script.

Blank-line-separated paragraphs become one :class:Segment each; internal single newlines are collapsed to spaces so a soft-wrapped paragraph reads as one utterance. Empty paragraphs are dropped.

This is intentionally minimal (see the module docstring). For anything richer, build :class:Segment\ s directly.

load_script

load_script(path: Union[str, Path]) -> NarrationScript

Load a narration script from a text file via :func:parse_script.

Backends

The TTSBackend protocol

A backend is any object satisfying this structural (runtime-checkable) protocol — a name and a synthesize method. No subclassing is required; isinstance(obj, sadda.tts.TTSBackend) checks the shape.

class TTSBackend(Protocol):
    name: str

    def synthesize(
        self,
        text: str,
        out_path: str | Path,
        *,
        voice: str | None = None,
        rate: float | None = None,
    ) -> SynthesisResult:
        """Synthesize `text` to a WAV at `out_path` and describe the result."""

voice is a backend-specific id (e.g. "en-us" for espeak-ng); rate is a relative multiplier where 1.0 is the backend's natural rate. Both may be None, meaning "use the backend default". See the voiceover guide for a worked example.

EspeakNgBackend

EspeakNgBackend(binary: Optional[Union[str, Path]] = None, *, base_words_per_minute: int = 175)

Synthesize speech by shelling out to the espeak-ng binary.

eSpeak NG writes 22.05 kHz mono 16-bit PCM WAV. It is a formant synthesizer: robotic, but zero-dependency, fully offline, deterministic, and available on every platform — which makes it the reference backend for tests and doc builds where reproducibility beats naturalness.

Parameters:

  • binary (Optional[Union[str, Path]], default: None ) –

    Path to the espeak-ng executable. Defaults to whatever is on PATH. Raises :class:FileNotFoundError at construction if none is found.

  • base_words_per_minute (int, default: 175 ) –

    The rate that a rate multiplier of 1.0 maps to. eSpeak's own default is 175 wpm.

synthesize

synthesize(text: str, out_path: Union[str, Path], *, voice: Optional[str] = None, rate: Optional[float] = None) -> SynthesisResult

get_backend

get_backend(name: Optional[str] = None, **kwargs: object) -> TTSBackend

Instantiate a backend by name.

name defaults to $SADDA_TTS_BACKEND then :data:DEFAULT_BACKEND. Extra keyword arguments are forwarded to the backend's constructor.

list_backends

list_backends() -> list[str]

Return the registered backend names, sorted.

register_backend

register_backend(name: str, factory: Callable[..., TTSBackend]) -> None

Register a backend factory under name (overwrites any existing one).