an.audio.takes#
Best-of-N takes: re-roll a line, score every take, keep the best, record the choice.
An expressive TTS model does not repeat itself: eleven_v3 returned three
different files for three byte-identical requests (same text, voice, settings
and seed), and the spread between two takes of one line was larger than the
effect of any voice setting. So the way to land a delivery on its measured
targets is to synthesize a few takes and keep the one that measures closest.
A voice document opts in with takes:
{"voice_id": "...", "model_id": "eleven_v3",
"takes": {"n": 3, # takes per line (1 = off)
"targets": {"articulation_rate_sps": [5.2, 6.3], ...},
"cues": {"deadpan": {"n": 4, "targets": {...}}}}}
n— takes per line;1(the default) is today’s single take, byte for byte. A bare integer (takes: 3) is{"n": 3}.targets—[low, high]prosody targets (an.verify.prosody.METRICS) the default scorer measures each take against. A style’s targets are named by role in its spec;style_voice_role()resolves those names into values when the style is applied, so the voice document holds numbers, never names.reference_hz— the voice’s neutral median pitch, for aregister_sttarget.cues— per-line opt-in: a line whose{direction}carries one of these cues uses that entry’sn/targets/reference_hzover the voice’s (the first matching cue, in the line’s order, wins). So a narrator can re-roll only its{deadpan}punchlines, scored as punchlines.scorer— the scorer’s name (defaultprosody); the scorer itself is a seam (TakeScorer, the pipeline’stake_scorer=).
How the pipeline uses it (an.audio.pipeline):
Every take has its own content key. Take
0of roll0is the request a single take makes, under the same key — turning takes on for a line already synthesized reuses that take and pays forn - 1more. Takeiaddstake: i(and a re-roll addsroll: r) to its key, and a provider may vary its request per take (ElevenLabs offsets a declaredseedbyi). The audio a line KEEPS is the chosen take’s own heard key (its effects applied), so a different take is a differentaudio_ref, and its visemes, word timings and captions follow it.Each take is scored on the audio the viewer hears (the voice’s effects applied, so a
tempocounts toward the rate); the lowest score wins, ties to the lower take index.The record is the resolution (ADR 0003 decision 3). The choice is written to
mall["takes"]under the line’s choice key — the line’s request,n, the scorer’s name and its configuration (its targets), but NOT its version — with the chosen take, the sha256 of what it sounds like, every take’s keys, digests and scores, and the scorer that chose. Every render reads it first:the chosen take is restored from the record (its heard audio, else its raw audio with the effects re-applied) and never re-billed;
a hand edit of
chosenwins: that take is restored, the record sayssuperseded_by: handand the decision is logged;a chosen take whose audio is gone is a
TakeLostErrorraised before any request —an voices rerollis the explicit way to re-roll;a record made by an older scorer version is KEPT, and the render reports it —
an voices rescorere-chooses explicitly, from the cached takes.
Changing
n, the targets, the scorer or the voice’s effects is a new choice key, chosen from the takes already cached where it can.
>>> spec = takes_spec({"n": 3, "targets": {"f0_sd_st": [2, 4]}})
>>> spec.n, spec.scorer, dict(spec.targets)
(3, 'prosody', {'f0_sd_st': [2.0, 4.0]})
>>> takes_spec(None) is None and takes_spec(1) is None
True
>>> doc = {"n": 1, "cues": {"deadpan": {"n": 4, "targets": {"final_drop_st": [-3.6, -1.7]}}}}
>>> takes_spec(doc, direction=["deadpan"]).n, takes_spec(doc, direction=["excited"])
(4, None)
>>> takes_spec({"n": 3})
Traceback (most recent call last):
...
an.audio.takes.VoiceTakesError: takes: n=3 with the prosody scorer needs `targets` ([low, high] per metric) to choose by
>>> choose_take([TakeScore((0.5, 1.0)), TakeScore((0.0, 2.0)), TakeScore((0.0, 2.0))])
1
Module Attributes
The key, in a voice document, declaring best-of-N takes. |
|
The name of the store the choices are recorded in. |
|
The scorer a |
|
each one is a billed request. |
|
The shape of a record in |
|
The prosody scorer's own version (its distance and tie-break); the estimator's version ( |
|
The prosody scorer versions whose choice the current one agrees with on a line that is NOT short ( |
|
A line with fewer syllables than this ( |
|
The targets a short line cannot show — they need several words — and is not scored on (recorded as |
|
The sample rate takes are decoded at for scoring — the one the targets were measured at. |
|
What to run when a recorded take must be replaced (named in every error). |
|
|
Functions
|
The sha256 a takes record names a take's audio by. |
|
The index of the best take: the lowest score, ties to the lower index. |
|
|
|
Whether |
|
The scorer |
|
The canonical |
|
The takes record under |
Raise |
|
|
The scorer as a record names it: name, version and configuration. |
|
The partial voice document a style casts |
|
What identifies a CHOICE among |
|
The takes a line with |
|
The takes |
|
Write |
Classes
|
Scores a take by how far its prosody sits from |
|
A take's score — compared as a tuple, lower is better — and what it measured. |
|
Scores one take of a line. |
|
The takes ONE line gets: how many, and what chooses between them. |
Exceptions
A line's recorded take is gone from the audio store and cannot be restored. |
|
A |
|
A voice's |
- an.audio.takes.DEFAULT_SCORER: str = 'prosody'#
The scorer a
takesdeclaration uses when it names none.
- an.audio.takes.MAX_TAKES: int = 10#
each one is a billed request.
- Type:
More takes than this per line is refused
- an.audio.takes.PROSODY_SCORER_AGREES_OFF_SHORT: frozenset[str] = frozenset({'1'})#
The prosody scorer versions whose choice the current one agrees with on a line that is NOT short (
ProsodyTakeScorer.agrees_with()): version 2 changed short lines only, so a longer line’s recorded pick is no “older scorer” to report.
- an.audio.takes.PROSODY_SCORER_VERSION: str = '2'#
The prosody scorer’s own version (its distance and tie-break); the estimator’s version (
an.verify.prosody.ESTIMATOR_VERSION) is joined to it. 2 (an#404): a short line is scored only on the targets it can show.
- class an.audio.takes.ProsodyTakeScorer(targets, *, reference_hz=None, sr=16000)[source]#
Bases:
objectScores a take by how far its prosody sits from
[low, high]targets.The score is
an.verify.prosody.target_distance(): the summed distance outside the ranges, then the summed distance from their midpoints (the tie-break between takes all on target), in units of each range’s width.A SHORT line (fewer than
SHORT_LINE_SYLLABLESsyllables) is scored only on the targets it can show: the pause and rate targets (SHORT_LINE_INAPPLICABLE) are left out and recorded asnot_applicable(an#404). Otherwise a one-word line’s choice measured its shortness, every target it cannot measure costing a range-width. That changed version 1’s choices for short lines only; a pick already recorded keeps its take and its keys (the choice key never holds the version), and a longer line’s pick is not reported as made by an older scorer (agrees_with()).- SCORE_MEANS: str = "score = [outside, off_centre], lower is better; the take with the smallest pair is kept. outside: for each target in scorer.config.targets, how far the measured value sits outside its [low, high] range, in widths of that range (0 inside; a value this take cannot measure, listed in `unmeasured`, counts 1). off_centre: each value's distance from its range's middle, in the same units (the tie-break). `measured` holds the values, `misses` the targets missed. A line under 4 syllables is not scored on the targets it cannot show (pauses and rates), listed in `not_applicable`."#
What a take’s
scorepair means, written into each takes record (an#397).
- agrees_with(version, text)[source]#
Whether a choice recorded by scorer
versionis the one this scorer would make fortext: the same version, or an older one that scored a line like this one the same way (version 1, on a line that is not short).- Return type:
>>> s = ProsodyTakeScorer({"f0_sd_st": [2, 4]}) >>> old = s.version.replace("2+", "1+", 1) >>> s.agrees_with(old, "He did not do the job."), s.agrees_with(old, "Hi!") (True, False)
- applicable(text)[source]#
The targets
textis scored on: all of them, or for a short line those it can show (an#404).>>> s = ProsodyTakeScorer({"f0_sd_st": [2, 4], "pause_share": [0.1, 0.2]}) >>> sorted(s.applicable("Hi!")), sorted(s.applicable("He did not do the job.")) (['f0_sd_st'], ['f0_sd_st', 'pause_share'])
- an.audio.takes.REROLL_HINT: str = 're-roll it explicitly with `an voices reroll <project> <words of the line>` (new takes, billed) or, while every take of its roll is still cached, re-choose among them with `an voices rescore <project> <words of the line>`'#
What to run when a recorded take must be replaced (named in every error).
- an.audio.takes.SCORERS: dict[str, Callable[[TakesSpec], TakeScorer]] = {'prosody': <function <lambda>>}#
TakesSpec -> TakeScorer.- Type:
Scorer factories by name
- an.audio.takes.SCORING_SAMPLE_RATE: int = 16000#
The sample rate takes are decoded at for scoring — the one the targets were measured at.
- an.audio.takes.SHORT_LINE_INAPPLICABLE: frozenset[str] = frozenset({'articulation_rate_sps', 'pause_median_s', 'pause_p90_s', 'pause_share', 'pauses_per_min', 'speech_rate_sps'})#
The targets a short line cannot show — they need several words — and is not scored on (recorded as
not_applicablein the take’s detail).
- an.audio.takes.SHORT_LINE_SYLLABLES: int = 4#
A line with fewer syllables than this (
an.verify.prosody.count_syllables()) is SHORT: too short to show pauses or a rate (an#404, end-user test finding 8).
- an.audio.takes.TAKES_RECORD_VERSION: int = 1#
The shape of a record in
mall["takes"]; raised when a field changes meaning.
- exception an.audio.takes.TakeLostError[source]#
Bases:
RuntimeErrorA line’s recorded take is gone from the audio store and cannot be restored.
Raised before any request: re-rolling an approved take is never implicit.
- class an.audio.takes.TakeScore(value, detail=<factory>)[source]#
Bases:
objectA take’s score — compared as a tuple, lower is better — and what it measured.
- class an.audio.takes.TakeScorer(*args, **kwargs)[source]#
Bases:
ProtocolScores one take of a line. Deterministic: the same bytes score the same.
name,versionandconfigenter the line’s audio key, so a scorer that would choose differently must differ in one of them.
- exception an.audio.takes.TakesRecordError[source]#
Bases:
VoiceTakesErrorA
mall["takes"]record cannot be read as a takes record.
- class an.audio.takes.TakesSpec(n, scorer='prosody', targets=<factory>, reference_hz=None)[source]#
Bases:
objectThe takes ONE line gets: how many, and what chooses between them.
- exception an.audio.takes.VoiceTakesError[source]#
Bases:
ValueErrorA voice’s
takesdeclaration is malformed, or cannot be scored.
- an.audio.takes.audio_digest(audio)[source]#
The sha256 a takes record names a take’s audio by.
- Return type:
- an.audio.takes.choose_take(scores)[source]#
The index of the best take: the lowest score, ties to the lower index.
- Return type:
- an.audio.takes.decode_for_scoring(audio, *, sr=16000)[source]#
audioas mono float samples atsrHz, decoded by ffmpeg — the decoder the targets were measured with, and the only one, so the same bytes score the same on every machine. RaisesVoiceTakesErrorwithout ffmpeg.
- an.audio.takes.is_short_line(text)[source]#
Whether
textis too short to show pauses or a rate (an#404).- Return type:
>>> is_short_line("Hi!"), is_short_line("[deadpan] No."), is_short_line("He did not do the job.") (True, True, False)
- an.audio.takes.make_take_scorer(spec)[source]#
The scorer
specnames, fromSCORERS(the pipeline’s defaulttake_scorer).- Return type:
- an.audio.takes.normalize_takes(raw)[source]#
The canonical
takesdeclaration of a voice document ({}for none).>>> normalize_takes(3) {'n': 3} >>> normalize_takes({"cues": {"deadpan": 4}}) {'cues': {'deadpan': {'n': 4}}}
- an.audio.takes.read_takes_record(store, key)[source]#
The takes record under
keyinstore(mall["takes"]), orNone.Raises
TakesRecordError— naming the record and the remedy — for one that is not JSON, not a mapping, or whosechosennames no take.>>> read_takes_record({}, "k") is None True >>> read_takes_record({"k": b'{"chosen": 0, "takes": [{"take": 0}]}'}, "k")["chosen"] 0 >>> read_takes_record({"k": b"[1, 2]"}, "k") Traceback (most recent call last): ... an.audio.takes.TakesRecordError: the takes record 'k' is not a takes record (a JSON object with `chosen` and `takes`): ...
- an.audio.takes.require_ffmpeg()[source]#
Raise
VoiceTakesErrorwith the install hint when ffmpeg is not on PATH.- Return type:
- an.audio.takes.scorer_identity(scorer)[source]#
The scorer as a record names it: name, version and configuration.
- an.audio.takes.style_voice_role(spec, role)[source]#
The partial voice document a style casts
roleas, with target NAMES resolved.A style spec (
cutan.style_spec(name)) declareslive.voice.roles.<role>and may name a delivery’s targets by its key inprosody_targets(takes: {n: 3, targets: narrator}). This returns a copy whosetakes(and each cue’s) hold the target VALUES, ready to merge beside avoice_id:{**style_voice_role(spec, "narrator"), "voice_id": ...}.>>> spec = {"live": {"voice": {"roles": {"narrator": {"model_id": "eleven_v3", ... "takes": {"n": 2, "targets": "narrator", "cues": {"deadpan": {"targets": "punchline"}}}}}}}, ... "prosody_targets": {"narrator": {"f0_sd_st": [3.9, 5.2]}, "punchline": {"f0_sd_st": [2.2, 3.4]}}} >>> role = style_voice_role(spec, "narrator") >>> role["takes"]["targets"], role["takes"]["cues"]["deadpan"]["targets"] ({'f0_sd_st': [3.9, 5.2]}, {'f0_sd_st': [2.2, 3.4]})
- an.audio.takes.takes_choice_part(scorer, n)[source]#
What identifies a CHOICE among
ntakes: the scorer’s name and its configuration, never its version — a new scorer version keeps the takes it chose, and says so (REROLL_HINT).
- an.audio.takes.takes_spec(raw, *, direction=None)[source]#
The takes a line with
directiongets from a voice’stakes, orNonefor one take.Raises
VoiceTakesErrorfor a malformed declaration, and for more than one take with nothing to choose by.
- an.audio.takes.voice_takes(mall, voice_id, *, direction=None, tts_name=None, repeatable=False)[source]#
The takes
mall["voices"][voice_id]declares for a line withdirection.None(one take) for a voice not in the store, one declaring nothing, one written for another provider thantts_name, and any voice spoken by arepeatableprovider (offline,mac_say: the same request gives the same audio, so a second take is the first one again). A malformed declaration raisesVoiceTakesErrorin every case.