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
¶
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
¶
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
¶
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'.
SynthesisResult ¶
The outcome of synthesizing one span of text to a WAV file.
__annotations__
class-attribute
¶
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
¶
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
¶
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
¶
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'.
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 ¶
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), orNoneto 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.0is the backend's natural rate,1.2is 20% faster,0.9is 10% slower.Nonefalls 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
¶
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
¶
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'.
pause_after_s
class-attribute
¶
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 ¶
The voice a segment should use, applying the fallback chain.
resolved_rate ¶
The rate a segment should use, applying the fallback chain.
parse_script ¶
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 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 ¶
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-ngexecutable. Defaults to whatever is onPATH. Raises :class:FileNotFoundErrorat construction if none is found. -
base_words_per_minute(int, default:175) –The rate that a
ratemultiplier of1.0maps 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 ¶
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.
register_backend ¶
Register a backend factory under name (overwrites any existing one).