an.audio.pipeline#
Audio pipeline orchestration: dialogue → audio → visemes → IR mutation.
Phase 3 wires the pieces together. produce_audio_for_scene walks every
Dialogue in the scene, synthesizes its audio + viseme track via the
configured providers, persists artifacts to mall["audio"] /
mall["visemes"], and stamps the resulting VisemeTrack and timing
back onto the Dialogue line so renderers can find it.
Who speaks a line (an#305): with no tts given, each line’s voice
document decides — its provider (an.audio.voices.declared_provider())
— and a voice that names none is spoken by the offline provider (silent WAV;
lip-sync defaults to deterministic offline visemes), so a project that declares
no provider runs without API keys or external binaries, exactly as before. A
tts given is an override for every line; a line spoken by another provider
than its voice declares is a VoiceStandInWarning (an error under
strict). See tts_chooser().
>>> from an.audio.pipeline import default_tts, default_lipsync
>>> default_tts().name
'offline'
Module Attributes
The |
|
Who speaks a line whose voice declares no provider, when no |
|
|
|
a frame at 60 fps (the same as |
|
The stores the audio pipeline writes what it makes to. |
|
only a new roll (new keys) replaces it. |
|
How far a legacy sidecar's duration may sit from its audio's and still be the same take: a frame at 60 fps. |
Functions
|
Content key of a line's audio: text, voice, provider, and — only when the voice declares them — its effects, the provider voice it names (an#194), the provider's synthesis options (model, settings, seed, audio tags — an#209). |
The default lip-sync provider: the loaded genre's |
|
The default TTS provider: |
|
|
One message per synthesized line that ends past its shot's end. |
|
Whether |
|
|
|
Whether |
|
Synthesize audio + visemes for one dialogue line. |
|
Walk every dialogue line, synthesize, and stamp viseme tracks back. |
|
Mark the recorded takes of the lines whose text contains |
|
Stamp every synthesized line's |
|
Stamp |
|
The provider-specific |
|
What synthesizing |
|
|
|
Content key of a line's viseme track (a function of the audio HEARD). |
|
One message per voice whose lines are spoken by another provider than its document declares; |
Classes
|
A store view that reads through and keeps every write IN MEMORY: what a reader of a render's cache keys stamps free audio into (an#311), so the keys it computes next see that audio while the store itself is untouched. |
Exceptions
A line's audio (or its visemes) is not in the content-keyed stores, so stamping it would need a synthesis this caller does not allow. |
|
The scene declares audio the pipeline cannot produce. |
|
A synthesized line runs past its shot's end, so its tail is cut. |
|
The audio restored for a line's recorded take is not the audio the record names. |
|
Under |
|
A line is spoken by another provider than its voice document declares — an override for every line, or a provider |
- an.audio.pipeline.AUDIO_OUTPUT_STORES: tuple[str, ...] = ('audio', 'visemes')#
The stores the audio pipeline writes what it makes to.
- exception an.audio.pipeline.AudioNotCachedError[source]#
Bases:
AudioPipelineErrorA line’s audio (or its visemes) is not in the content-keyed stores, so stamping it would need a synthesis this caller does not allow.
- exception an.audio.pipeline.AudioPipelineError[source]#
Bases:
RuntimeErrorThe scene declares audio the pipeline cannot produce. Carries detail.
- an.audio.pipeline.DEFAULT_TTS_NAME: str = 'offline'#
Who speaks a line whose voice declares no provider, when no
ttsoverrides it.
- exception an.audio.pipeline.DialogueOverrunWarning[source]#
Bases:
UserWarningA synthesized line runs past its shot’s end, so its tail is cut.
- class an.audio.pipeline.InMemoryOverlay(store)[source]#
Bases:
MutableMappingA store view that reads through and keeps every write IN MEMORY: what a reader of a render’s cache keys stamps free audio into (an#311), so the keys it computes next see that audio while the store itself is untouched. Deleting is refused.
- an.audio.pipeline.LEGACY_DURATION_TOLERANCE_S: float = 0.016666666666666666#
How far a legacy sidecar’s duration may sit from its audio’s and still be the same take: a frame at 60 fps.
- an.audio.pipeline.OVERRUN_TOLERANCE_S: float = 0.016666666666666666#
a frame at 60 fps (the same as
an validate’s).- Type:
Slack before a line counts as running past its shot
- an.audio.pipeline.REROLL_ONLY_HINT: str = 'Re-roll it with `an voices reroll <project> <words of the line>` (new takes under new keys, billed; the render prints the cost first)'#
only a new roll (new keys) replaces it.
- Type:
The remedy when a recorded take is gone
- exception an.audio.pipeline.TakeDigestWarning[source]#
Bases:
UserWarningThe audio restored for a line’s recorded take is not the audio the record names.
- an.audio.pipeline.TakeScorerFactory#
(TakesSpec) -> TakeScorer— the seam that turns a takes spec into its scorer.alias of
Callable[[TakesSpec],TakeScorer]
- an.audio.pipeline.VOICE_TTS: str = 'voice'#
The
ttsthat means “each line’s voice document names its provider” — the default of a render (an#305).Noneand""mean the same.
- exception an.audio.pipeline.VoiceStandInError[source]#
Bases:
AudioPipelineErrorUnder
strict: a line would be spoken by another provider than its voice declares. Raised before any request.
- exception an.audio.pipeline.VoiceStandInWarning[source]#
Bases:
UserWarningA line is spoken by another provider than its voice document declares — an override for every line, or a provider
anhas no TTS for (an#305).
- an.audio.pipeline.audio_key(text, voice_id, tts_name, effects=None, *, provider_voice=None, options=None, takes=None, take=None, roll=None)[source]#
Content key of a line’s audio: text, voice, provider, and — only when the voice declares them — its effects, the provider voice it names (an#194), the provider’s synthesis options (model, settings, seed, audio tags — an#209). With none of them, the payload is exactly the pre-effects one, so every key a project already has is unchanged.
take(1, 2, … — never 0) androll(1, 2, … afteran voices reroll) key one candidate take of a best-of-N line; take 0 of roll 0 is the single-take request and keeps its key.takes(an.audio.takes.takes_choice_part()) keys the CHOICE among a line’s takes — the record inmall["takes"], not an audio blob.- Return type:
>>> audio_key("hi", "default", "offline") == audio_key( ... "hi", "default", "offline", {}, provider_voice=None, options={}, ... takes=None, take=0, roll=0) True
- an.audio.pipeline.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.pipeline.dialogue_overruns(scene, *, tolerance_s=0.016666666666666666, mall=None)[source]#
One message per synthesized line that ends past its shot’s end.
an validatewarns before synthesis from an estimate; this is the exact check AFTER it — a voice’stempo(or a real voice’s own pace, or the silence it pads a line with) can make a line longer than estimated, and the render cuts the shot’s audio at the shot’s end, so the tail would otherwise be lost silently. It isan validate’s own check (an.ir.validate.shot_dialogue_overruns()), over the synthesized lines;malllets its fix name the voice’strim_silence.>>> from an.ir.schema import Dialogue, SceneIR, Shot >>> shot = Shot(id="s", duration=1.0, dialogue=[ ... Dialogue(speaker="a", text="hi", start=0.2, duration=1.3, audio_ref="k")]) >>> dialogue_overruns(SceneIR(timeline=[shot]))[0][:46] "shot 's': line 0 (a) ends at 1.30s as synthesi"
- an.audio.pipeline.free_and_repeatable(provider)[source]#
Whether
providermay be run where nothing may be spent or written (an#311): it DECLARESrepeatable = True(the same request gives the same bytes:offline,mac_say) andbilled = False— stated, never inferred from a missing method — and has nobilled_charactershook. Re-running it reproduces exactly what a render stored; anything else would give new bytes, so new keys, or cost money.- Return type:
>>> from an.audio.offline_tts import OfflineTTS >>> free_and_repeatable(OfflineTTS()), free_and_repeatable(object()) (True, False)
- an.audio.pipeline.in_memory_audio_mall(mall)[source]#
mallwith its audio and visemes stores behindInMemoryOverlays — hand it tostamp_from_stores()(free_in_memory=True) and then to whatever keys the stamped scene (an#311).
- an.audio.pipeline.is_voice_tts(tts)[source]#
Whether
ttsmeans “each voice’s own provider”:None,""orVOICE_TTS.- Return type:
>>> [is_voice_tts(t) for t in (None, "", "voice", " Voice ", "offline")] [True, True, True, True, False]
- an.audio.pipeline.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.pipeline.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.pipeline.retake_lines(scene, mall, match, *, tts, rescore=False, take_scorer=<function make_take_scorer>)[source]#
Mark the recorded takes of the lines whose text contains
matchto be chosen again on the next render; one message per matching line.rescore=Truere-chooses among the takes already synthesized (with the current scorer; nothing billed while they are cached); otherwise a new roll of takes is synthesized (billed — the render prints the cost first). Either way the replaced choice is kept in the record’shistory, and the new take has its own key, so its visemes and word timings are aligned afresh. This is the only way a recorded take is replaced (ADR 0003).
- an.audio.pipeline.retime_dialogue(scene, *, timed_shots_only=False)[source]#
Stamp every synthesized line’s
startfrom itspause/at.Pure — synthesizes nothing, reads no store — and idempotent. The audio pipeline ends with it, and every path that skips the pipeline (
renderwithauto_audio=False,an preview) runs it too, so a pause edited after synthesis is never played at the stale stamp (an#187). A shot is re-timed up to its first line with noduration(never synthesized: nothing after it has a known start). Mutates in place and returnsscene.timed_shots_only=True— what the paths that skip synthesis pass — leaves a shot whose lines carry nopause/atexactly as stamped: a hand-built scene may stampstartitself (a test, a fixture), and without the pipeline there is no authority to say that stamp is stale.- Return type:
>>> from an.ir.schema import Dialogue, SceneIR, Shot >>> shot = Shot(id="s", dialogue=[ ... Dialogue(speaker="a", text="hi", start=0.0, duration=0.5, audio_ref="k1"), ... Dialogue(speaker="b", text="bye", start=0.5, duration=0.4, audio_ref="k2", ... pause=1.5)]) >>> [d.start for d in retime_dialogue(SceneIR(timeline=[shot])).timeline[0].dialogue] [0.0, 2.0]
- an.audio.pipeline.stamp_from_stores(scene, mall, *, tts=None, lipsync, tts_factory=None, free_in_memory=False)[source]#
Stamp
scene’s dialogue exactly asproduce_audio_for_scene()would with these providers —ttsas it takes it:Nonefor each voice’s own (an#305) — from the content-keyedaudioandvisemesstores only. Synthesises, aligns, writes and announces nothing; a line the stores cannot answer raisesAudioNotCachedError.free_in_memory(an#311): a line the stores cannot answer whose provider is free and repeatable (free_and_repeatable(): offline speech) is synthesised and aligned for real — it reproduces the bytes a render stored — into the mall’sInMemoryOverlays, whichin_memory_audio_mall()puts in front of the audio and visemes stores (required: nothing is ever written to a store). Billed or non-repeatable providers still raise. Still nothing is announced.What a reader of the render’s cache keys needs (an#274): a
scene.mdedit drops every stamp on re-sync, and the next render re-stamps the same audio from the stores, so the keys a render WILL use are these, not the unstamped IR’s. Mutatesscenein place and returns it.- Return type:
- an.audio.pipeline.synthesis_options(tts, line, mall, voice_id)[source]#
The provider-specific
synthesizekwargs forlineinvoice_id.{}for a provider without asynthesis_optionshook (offline, mac_say) and for a voice written for another provider — which is what keeps their cache keys where they were.
- an.audio.pipeline.takes_cost_message(lines, tts, audio_store)[source]#
What synthesizing
lineswithttswill bill, when any of them takes best-of-N orttsbills per character (an#305: a voice that names a paid provider is spoken by it with no flag, so its cost is said first); else"".Counts only the requests not already in
audio_store(a cached take is free; a recorded take is restored, never billed), and the provider’s billed characters per request (itsbilled_characters(text, **options)hook — ElevenLabs counts the audio tags too). A provider without the hook bills no characters, and the message says so.- Return type:
- an.audio.pipeline.tts_chooser(tts, mall, *, make=None)[source]#
voice_id -> provider: who speaks a line in that voice (an#305).A
ttsgiven (a provider, or a namemakebuilds) speaks every line — an override. Otherwise (None,"",VOICE_TTS) the voice document’sproviderdecides, and a voice that declares none — or onemakehas no provider for (voice_stand_ins()reports it) — is spoken byDEFAULT_TTS_NAME. One instance per provider name.makedefaults toan.audio.providers.make_tts().- Return type:
Callable[[str],TTSProvider]
>>> mall = {"voices": {"bob": {"provider": "mac_say"}, "ann": {}}} >>> choose = tts_chooser(None, mall) >>> choose("bob").name, choose("ann").name, choose("nobody").name ('mac_say', 'offline', 'offline') >>> tts_chooser("offline", mall)("bob").name 'offline'
- an.audio.pipeline.viseme_key(audio_key_, lipsync_name, transcript)[source]#
Content key of a line’s viseme track (a function of the audio HEARD).
- Return type:
- an.audio.pipeline.voice_stand_ins(lines, mall, *, overridden)[source]#
One message per voice whose lines are spoken by another provider than its document declares;
linesis(voice_id, provider)per line, andoverriddensays whether attswas given for every line.>>> from an.audio.offline_tts import OfflineTTS >>> mall = {"voices": {"bob": {"provider": "elevenlabs"}}} >>> print(voice_stand_ins([("bob", OfflineTTS())] * 2, mall, overridden=True)[0]) voice 'bob' declares provider 'elevenlabs', but its 2 line(s) are spoken by 'offline' — SILENT audio — because `tts` overrides every voice. Drop `--tts` to let each voice speak with its own provider. >>> voice_stand_ins([("bob", OfflineTTS())], {}, overridden=True) []