an.audio#
Audio pipeline — TTS and lip-sync providers + orchestration.
Phase 3 ships:
Protocols:
TTSProvider,LipSyncProvider(Phase 1).Default offline provider:
OfflineTTS(silent WAV). The lip-sync providers (OfflineLipSyncdeterministic char-to-viseme,RhubarbLipSync,WhisperLipSync) moved tocutanwith the cut-out genre (an#225); the old names still resolve from here, with a warning.Real providers:
ElevenLabsTTS(needsELEVEN_API_KEY).Orchestration:
produce_audio_for_dialogue/produce_audio_for_scenewalk a SceneIR, synthesize, persist to mall, stamp viseme tracks back onto the IR.
The defaults are intentionally offline so the entire an pipeline works
without external services.
Functions
|
Distribute viseme keyframes across word boundaries. |
The default TTS provider: |
|
The default lip-sync provider: the loaded genre's |
|
|
Synthesize audio + visemes for one dialogue line. |
|
Walk every dialogue line, synthesize, and stamp viseme tracks back. |
|
Instantiate a TTS provider by name. |
|
Instantiate a LipSync provider by name. |
Return the registered TTS provider names. |
|
Return the registered LipSync provider names. |
Classes
|
Text-to-speech provider. |
|
A rendered audio clip, on disk or in memory. |
|
Metadata for a TTS voice as exposed by a provider. |
|
Audio + transcript → aligned viseme track. |
|
A single mouth-shape keyframe. |
|
Aligned viseme sequence produced by a LipSyncProvider. |
|
Audio → |
The lip-sync provider of a core without a genre: no mouth shapes at all. |
|
|
Default TTS provider: silent WAV of length proportional to text. |
|
ElevenLabs-backed TTSProvider. |
|
macOS |
- class an.audio.AudioClip(path=None, bytes_=None, duration=0.0, sample_rate=44100, channels=1, voice_id=None, transcript=None)[source]#
Bases:
objectA rendered audio clip, on disk or in memory.
- class an.audio.ElevenLabsTTS(*, api_key=None, model_id='eleven_turbo_v2_5', output_format='mp3_44100_128', audio_tag_model_prefixes=('eleven_v3', 'eleven_v4'), client_factory=None)[source]#
Bases:
objectElevenLabs-backed TTSProvider. Constructor takes an optional api_key (falls back to
ELEVEN_API_KEY/ELEVENLABS_API_KEY).Implements the
TTSProviderprotocol, plus the optionalsynthesis_optionshook the audio pipeline reads (an#209).- billed_characters(text, *, audio_tags=None, **_options)[source]#
Characters one request bills: the text as sent, audio tags included.
- Return type:
>>> ElevenLabsTTS(api_key="unused").billed_characters("Hi!", audio_tags=["excited"]) 13
- check_available()[source]#
Raise
ElevenLabsUnavailableErrorwhen a request could not be sent (no API key) — what the audio pipeline asks before the first request of a render, so nothing is half-synthesized (an#305).- Return type:
- client_factory#
api_key -> client; tests inject a fake so nothing reaches the API.
- list_voices(*, search=None)[source]#
The account’s voices (its own plus the ones it saved), newest API.
searchfilters by name, description and labels on the server. An absent key or SDK yields[]; a key that is present and a call that fails RAISES — an empty listing must mean “no voices”, not “it broke”.
- synthesis_options(voice, *, emotion=None, direction=None)[source]#
The
synthesizekeyword arguments a voice document and a line imply.Only what is declared appears (
{}for a plain voice), so the audio cache key — which includes this dict when it is non-empty — is unchanged for every voice that declares nothing new.audio_tagsare the line’s[emotion](unlessneutral) then itsdirectioncues, and only on a model that reads tags; elsewhere a direction is dropped with a warning and the emotion stays a face-only cue, as before.
- synthesize(text, voice_id=None, *, model_id=None, voice_settings=None, seed=None, audio_tags=None, **kw)[source]#
Speak
text. The clip’stranscriptistextWITHOUT the tags, so alignment and captions never read a cue.- Return type:
- take_options(options, take)[source]#
The request for candidate
takeof a best-of-N line (an.audio.takes).A declared
seedis offset by the take, so a model that does honour it (every model buteleven_v3, measured) still returns different takes; without a seed the request is unchanged and the model’s own sampling varies the take.>>> ElevenLabsTTS(api_key="unused").take_options({"seed": 11}, 2) {'seed': 13}
- class an.audio.LipSyncProvider(*args, **kwargs)[source]#
Bases:
ProtocolAudio + transcript → aligned viseme track.
A provider that fills
VisemeTrack.wordsalso declaresemits_word_timings = True(a plain class attribute, absent means False). The audio pipeline reads it to decide whether a line whoseword_timingsare missing needs re-alignment — without the flag, a provider that cannot supply words would re-align every line forever.
- class an.audio.MacSayTTS(*, default_voice_id='Samantha', sample_rate=22050, rate_wpm=None)[source]#
Bases:
objectmacOS
say-backed TTSProvider.Implements the
TTSProviderprotocol. Audible, deterministic, and fully offline — uses Apple’s voice synthesis bundled with the OS.- repeatable: bool = True#
The same request gives the same audio, so best-of-N takes never apply (
an.audio.takes.voice_takes()) and nothing is billed.
- class an.audio.NullLipSync[source]#
Bases:
objectThe lip-sync provider of a core without a genre: no mouth shapes at all.
A genre that animates mouths (
cutan) registers real providers (offline,rhubarb,whisper); without one, a line’s audio is still synthesized and the track it gets is empty – nothing draws a mouth anyway.>>> NullLipSync().align(AudioClip(bytes_=b"", duration=0.5), "hi").duration 0.5
- class an.audio.OfflineTTS(*, sample_rate=22050, channels=1, seconds_per_char=0.06)[source]#
Bases:
objectDefault TTS provider: silent WAV of length proportional to text.
Implements the
TTSProviderprotocol.- repeatable: bool = True#
The same request gives the same audio, so best-of-N takes never apply (
an.audio.takes.voice_takes()) and nothing is billed.
- class an.audio.Viseme(time, code, intensity=1.0)[source]#
Bases:
objectA single mouth-shape keyframe.
- class an.audio.VisemeTrack(visemes=<factory>, convention='rhubarb', duration=0.0, words=None)[source]#
Bases:
objectAligned viseme sequence produced by a LipSyncProvider.
wordscarries the word timings the provider aligned from, when it had any (whisper,WordTimingsLipSync);Nonefor providers that never see words (offline, Rhubarb — whose JSON is mouth cues only). Retained since an#96 rather than discarded after the viseme conversion: captions (Wave 8) and any consumer that wants to know which word a mouth shape belongs to read them from the IR.
- class an.audio.VoiceMeta(voice_id, name, provider, language='en', gender=None, extra=<factory>)[source]#
Bases:
objectMetadata for a TTS voice as exposed by a provider.
- class an.audio.WordTimingProvider(*args, **kwargs)[source]#
Bases:
ProtocolAudio →
[(word, start_s, end_s), ...].Implementations may run a transcriber (Whisper / Scribe) or look up pre-computed timings (
muvid’s lacing store). Returned tuples are expected to be in ascending start-time order; gaps between words are fine and represent silence the lipsync provider should rest through.
- an.audio.default_lipsync()[source]#
The default lip-sync provider: the loaded genre’s
offlineone (the deterministic char-to-viseme provider ofcutan), elseNullLipSync.- Return type:
>>> default_lipsync().name in {"offline", "none"} True
- an.audio.make_lipsync(name, *, language='en')[source]#
Instantiate a LipSync provider by name.
language(BCP-47) reaches providers that select behaviour by language — Rhubarb’s recognizer today; a future aligner’s weight allowlist.The default name (
"offline") resolves toNullLipSyncwhen no loaded genre provides it, so voicing a line needs no genre; any other name that nothing provides is an error that says how to get it.- Return type:
>>> make_lipsync("offline").name in {"offline", "none"} True
- an.audio.make_tts(name)[source]#
Instantiate a TTS provider by name.
Raises
ValueErrorfor unknown names with a list of known options.- Return type:
- an.audio.produce_audio_for_dialogue(dialogue, mall=None, *, tts=None, lipsync=None, effects=None, voice_id=None, takes=<object object>, take_scorer=<function make_take_scorer>)[source]#
Synthesize audio + visemes for one dialogue line.
Side effects: when
mallis provided, persists the WAV tomall["audio"]keyed by the content-hash of the dialogue, and persists the viseme JSON tomall["visemes"]similarly. Cache-friendly: a second call with identical inputs returns the cached versions.effects(default: what the line’s voice declares inmall["voices"]) is applied to the synthesized audio BEFORE alignment, so the visemes are computed on the audio the viewer hears. The raw synthesis stays cached under its own key, so changing an effect never re-pays the TTS.tts(default: the provider the voice document declares, else offline —tts_chooser()) is an override when given.voice_id(default: the line’svoice_ref, else"default") is thevoices-store key;produce_audio_for_scene()passes the onean.audio.voices.line_voice_id()resolves, so a character’s bound voice reaches here (an#194). The provider is handed the voice document’s own voice id when it names one.What the provider’s optional
synthesis_optionshook derives from the voice document and the line (ElevenLabs:model_id,voice_settings,seed, and the[emotion]/directionaudio tags — an#209) is passed tosynthesizeand keyed; the text handed to alignment is always the baredialogue.text, never the tagged one.takes(default: what the voice declares for this line — seean.audio.takes;Noneforces one take) synthesizes several takes, scores each withtake_scorer(spec)on the audio the viewer hears, keeps the best and records the choice inmall["takes"]. A recorded choice is restored from the record and never re-rolled; a recorded take whose audio is gone raisesTakeLostErrorbefore any request.- Return type:
- an.audio.produce_audio_for_scene(scene, mall=None, *, tts=None, lipsync=None, take_scorer=<function make_take_scorer>, announce=<function _announce_to_stderr>, overruns=True, strict=False, tts_factory=None)[source]#
Walk every dialogue line, synthesize, and stamp viseme tracks back.
Mutates the
scenein place AND returns it (for chaining). StampsDialogue.duration,Dialogue.viseme_track, andDialogue.audio_ref(mall[“audio”] key) so the renderer can find the audio later. Lines with an existing viseme_track AND audio_ref are not re-synthesized (idempotent).Dialogue.startis DERIVED on every pass, synthesized or not, byDialogue.planned_start(): the line’satif set, else the previous line’s end plus itspause(an#187). So editing a pause re-times the shot without touching the audio, and every consumer ofstart— the mux, the visemes, captions, ducking — follows. Astarton a line that was never synthesized is an authored start from beforeatexisted, and is kept as the line’sat.Every line’s request is resolved BEFORE anything is synthesized, so a malformed voice (an effect, a
takes), a corrupt takes record or a recorded take whose audio is gone fails before a credit is spent; andannounce(default: a line on stderr;Nonefor silence) is told, before the first request, what best-of-N takes will bill (requests and the provider’s characters) and which recorded takes were chosen by an older scorer version than the current one (they are kept). After synthesis, a line that ends past its shot’s end (dialogue_overruns()) is announced too — or, withannounce=None, aDialogueOverrunWarning— unlessoverruns=False:an renderpasses that, because it reports every post-synthesis finding together in its summary (an#254).Who speaks each line (an#305,
tts_chooser()): withttsnot given (None,""orVOICE_TTS), the line’s voice document’sprovider, built bytts_factory(defaultan.audio.providers.make_tts()), else offline; attsgiven speaks every line. A line spoken by another provider than its voice declares is aVoiceStandInWarning— the silent offline voice says so — and, withstrict, aVoiceStandInErrorbefore any request. A provider with requests to send is askedcheck_available()first (ElevenLabs: is there a key?), and what a provider that bills per character will bill is announced before the first request — whatever chose the provider; a cached line is never billed and never counted.- Return type:
- an.audio.word_timings_to_visemes(words, *, total_duration, char_to_viseme, rest_viseme='X', min_gap_for_rest=0.2)[source]#
Distribute viseme keyframes across word boundaries.
Used by
WhisperLipSyncandWordTimingsLipSync. The algorithm: per word, walk its characters in order, look up each character’s viseme code, dedupe consecutive identical codes, and space the resulting keyframes evenly across the word’s[start, end]interval. In gaps wider thanmin_gap_for_rest— between two words, or after the last one beforetotal_duration— insert a single rest keyframe just after the previous word ended.Times are clamped to
[0, total_duration]since some transcribers occasionally round the last word’s end past the audio’s actual length.
Modules
|
|
Voice effects: a deterministic transform applied to a synthesized line (an#163). |
|
ElevenLabsTTS — real speech via the ElevenLabs API. |
|
Lip-sync provider protocol + viseme dataclasses. |
|
Voice loudness: one integrated loudness for every voice of a film (an#315). |
|
MacSayTTS — audible offline speech via macOS's built-in |
|
OfflineTTS — produces silent audio of plausible duration. |
|
Audio pipeline orchestration: dialogue → audio → visemes → IR mutation. |
|
Provider factory: name → concrete TTS/LipSync provider instance. |
|
Best-of-N takes: re-roll a line, score every take, keep the best, record the choice. |
|
TTS provider protocol + supporting dataclasses. |
|
Which voice speaks a dialogue line: the character → voice binding (an#194). |