Pipeline & generation¶
The end-to-end orchestration plus the input stages that feed it: the naive aligner and Montreal Forced Aligner parser, grapheme-to-phoneme conversion, the ARPAbet phoneme inventory, the transcript-free energy fallback, the prosody tracker that turns pitch and loudness into typed events, and the edit-preservation layer that carries hand-tweaks across a regeneration.
openfacefx.pipeline
¶
End-to-end pipeline: (audio, text) -> FaceTrack.
Two entry points:
-
generate_from_alignment-- you already have time-stamped phonemes (from MFA, or Gentle/Whisper/WhisperX via the built-in :mod:openfacefx.alignersadapters, issue #54). This is the accurate path. -
generate_naive-- you only have text and an audio duration. Uses G2P + NaiveAligner. Fast, dependency-free, approximate lip-sync for prototyping.
wav_duration(path: str) -> float
¶
Duration of a PCM WAV in seconds, using only the stdlib.
generate_from_alignment(segments: List[PhonemeSegment], fps: float = 60.0, epsilon: float = 0.015, mapping=None, params=None, gestures=None, wav: Optional[str] = None) -> FaceTrack
¶
gestures opts into the non-verbal gesture layer (issue #5): pass a
GestureParams (or True for defaults) to append blink/brow/head/eye
channels after viseme reduction. Off (None) leaves output byte-identical.
wav supplies the audio those energy-driven brows/nods read; without it
they degrade gracefully (stress still comes from the segments).
Source code in src/openfacefx/pipeline.py
derive_events(segments: Optional[List[PhonemeSegment]] = None, env_times=None, env=None, emphasis: bool = True, phrase: bool = True, energy_thresh: float = 0.55, min_prominence: float = 0.15, min_spacing: float = 0.4) -> list
¶
Auto-author a typed event layer from the speech itself (issue #6) --
emphasis events on stressed syllables / loudness peaks and phrase
boundary markers at pauses -- WITHOUT touching the gesture channels.
It reuses the exact stress/pause/peak detectors behind
:mod:openfacefx.gestures (gestures_layers), so an emphasis event and a
head-nod gesture agree on where the accents are, yet the two layers stay
independent and separately opt-in. Determinism is inherited from those
numpy detectors (no RNG here): identical inputs give identical events.
segments supply ARPABET stress digits and sil pauses; env_times /
env are an :func:openfacefx.energy.energy_envelope result. With
segments, stress drives emphasis and sil spans drive phrase markers; with
only an envelope (energy mode), loudness peaks drive emphasis and quiet runs
drive phrase markers. Returns a time-sorted list[Event].
Source code in src/openfacefx/pipeline.py
naive_segments(text: str, duration: float, g2p: Optional[G2P] = None) -> List[PhonemeSegment]
¶
Time-stamped phonemes for text spread over duration seconds.
This is the phoneme-timing layer the curve solver consumes; exporters that need phonemes rather than visemes (e.g. Bethesda .LIP) start here.
Source code in src/openfacefx/pipeline.py
generate_naive(text: str, duration: float, fps: float = 60.0, epsilon: float = 0.015, g2p: Optional[G2P] = None, mapping=None, params=None, gestures=None, wav: Optional[str] = None, preprocess: Optional[Callable[[str], str]] = None, parse_tags: bool = False) -> FaceTrack
¶
preprocess (issue #7) is an optional callable(text) -> text run on
the transcript before anything else, so a registered auto-tagger can insert
tags programmatically; injecting a tag this way is identical to hand-writing
it. parse_tags enables the transcript text-tag stage (see
:mod:openfacefx.texttags): curve tags become extra channels, event tags the
event layer, [emphasis] a local articulation boost and <T>/[pause]
chunk/silence the timeline. Both default off, so the plain naive path is
byte-identical; a parse_tags run on a transcript that carries no tags is
byte-identical too.
Source code in src/openfacefx/pipeline.py
openfacefx.edits
¶
Edit preservation: hand-tweaks that survive regeneration (issue #9).
The pipeline is a pure function (audio, text) -> FaceTrack; re-running it with
new dials (intensity, coarticulation, energy gain) throws away any manual curve
edits an animator made. This module mirrors FaceFX's two-layer ownership model so
it doesn't have to: analysis owns the generated curves, while a user keeps a
small, separate record of what they changed. FaceFX offers two ways to edit an
"Owned by Analysis" curve -- add an OFFSET curve (keys added to the analysis value,
then clamped to the node's min/max), or take full manual ownership (regeneration
skips the curve). We store that as a sidecar *.edits.json rather than
inline flags, so the .track stays clean, versioned interchange and the solver
stays untouched.
Regeneration is then: re-run the pipeline to a fresh :class:FaceTrack, then
:func:apply_edits overlays the sidecar back on. The inverse, :func:diff_edits,
captures what a user changed (a hand-edited track vs the baseline it came from)
into a sidecar.
Two per-channel edit modes, each optionally restricted to a time span (the
"locked region"):
offset-- FaceFX offset curve:keysare deltas added to whatever the solver now produces, then clamped. Being relative, an offset survives dial changes (intensity/coarticulation/energy), which is the primary use case.replace-- FaceFX manual ownership:keysare absolute values that replace the generated channel (whole-channel, or just insidespan).
Conflict handling is conservative: an edit on a channel the regeneration dropped
is preserved and flagged (keep-edit, the default -- a hand-edit is never
silently lost) or discarded (take-generated); a locked region always wins
over the regenerated content inside its span while the fresh curve shows through
everywhere else.
Deterministic and dependency-light: only numpy (interp/clip and the
existing :func:openfacefx.curves._rdp thinner), hashlib and json. No RNG,
stable 4-dp rounding, identical output across Python 3.9/3.13. Additive: a
track generated without a sidecar is byte-identical to previous releases -- the
whole layer is opt-in.
FORMAT = 'openfacefx.edits'
module-attribute
¶
VERSION = 1
module-attribute
¶
EditsDoc(channels: Dict[str, dict] = dict(), fps: float = 60.0, source_id: Optional[str] = None, base_hash: Optional[str] = None, viseme_set: Optional[List[str]] = None)
dataclass
¶
A parsed openfacefx.edits sidecar.
channels maps a channel name to an edit record -- a validated dict
{"mode": "offset"|"replace", "keys": [[t, v], ...], "clamp"?: [lo, hi],
"span"?: [t0, t1]}. base_hash / source_id are provenance (the
baseline the edits were captured from, and the audio id); fps /
viseme_set echo the track for reference.
channels: Dict[str, dict] = field(default_factory=dict)
class-attribute
instance-attribute
¶
fps: float = 60.0
class-attribute
instance-attribute
¶
source_id: Optional[str] = None
class-attribute
instance-attribute
¶
base_hash: Optional[str] = None
class-attribute
instance-attribute
¶
viseme_set: Optional[List[str]] = None
class-attribute
instance-attribute
¶
to_dict() -> Dict
¶
Source code in src/openfacefx/edits.py
from_dict(d: Dict) -> 'EditsDoc'
classmethod
¶
Parse and validate a sidecar dict, raising ValueError with a clear,
channel-named message on any malformed field.
Source code in src/openfacefx/edits.py
sample(keys, T) -> np.ndarray
¶
Values of a keyframe list at times T by linear interpolation.
Uses np.interp's endpoint-hold (flat before the first key / after the
last), matching how a curve editor reads a piecewise-linear channel. An empty
key list samples to zeros. keys may be a :class:Channel, a list of
:class:Keyframe, or [time, value] pairs (offset/replace records).
Source code in src/openfacefx/edits.py
save_edits(doc: EditsDoc, path: str) -> None
¶
Write a sidecar as pretty JSON (2-space indent, trailing newline).
load_edits(path: str) -> EditsDoc
¶
Read and validate a sidecar from path.
Source code in src/openfacefx/edits.py
diff_edits(base: FaceTrack, edited: FaceTrack, mode: str = 'offset', span: Optional[Tuple[float, float]] = None, tol: float = 0.01, eps: float = 0.01, clamps: Optional[Dict[str, Tuple[float, float]]] = None, source_id: Optional[str] = None) -> EditsDoc
¶
Build a sidecar from a hand-edited track and the base it was edited
from. A channel whose curves differ by more than tol anywhere is recorded;
identical channels are skipped (so an untouched track yields an empty sidecar).
mode='offset' (default) stores the delta edited - base (RDP-thinned at
eps) plus the per-channel clamp -- the relative form that survives
later dial changes. mode='replace' stores the absolute edited values.
span=(t0, t1) restricts capture to a time window (a locked region).
clamps maps channel -> (lo, hi) (e.g. from mapping.Target); missing
entries default to [0, 1].
Source code in src/openfacefx/edits.py
apply_edits(gen: FaceTrack, edits: EditsDoc, eps: float = 0.015, on_conflict: str = 'keep-edit') -> Tuple[FaceTrack, List[dict]]
¶
Overlay edits onto a freshly generated gen track and return
(merged_track, conflicts).
A generated channel with no record passes through untouched. An offset
record adds its (interpolated) deltas to the current curve and clamps; a
replace record substitutes its values. A span confines the edit to a
window -- the fresh curve is preserved exactly outside it, so a locked region
wins only where it was drawn.
conflicts lists edits whose channel is absent from gen (renamed,
or its word removed on re-alignment). on_conflict='keep-edit' (default)
re-injects them so a hand-edit is never silently lost;
'take-generated' drops them for the fresh output. Either way the conflict
is reported so the caller can warn.
Source code in src/openfacefx/edits.py
openfacefx.alignment
¶
Forced alignment: producing time-stamped phonemes.
This is the model-heavy stage of the pipeline. Rather than reinvent a speech
recogniser, OpenFaceFX defines a small PhonemeSegment contract and provides:
-
NaiveAligner-- no acoustic model. Given the phonemes for an utterance and the utterance duration (or word timings), it distributes phonemes over time using per-phoneme duration priors. Good enough to see the pipeline end to end; not accurate lip-sync. -
load_mfa_textgrid-- parse the output of the Montreal Forced Aligner (the recommended production aligner). MFA gives real acoustic alignment.
Built-in adapters for the open-source aligners live in
:mod:openfacefx.aligners (Whisper / WhisperX word timings, Gentle word and
phone timings, issue #54); any other source just needs to produce a list of
PhonemeSegment and the rest of the pipeline is unchanged.
PhonemeSegment(phoneme: str, start: float, end: float, confidence: Optional[float] = None)
dataclass
¶
NaiveAligner
¶
Distribute phonemes across a time span using duration priors.
align(phonemes: List[str], total_duration: float, start: float = 0.0) -> List[PhonemeSegment]
¶
Source code in src/openfacefx/alignment.py
align_words(words: List[tuple]) -> List[PhonemeSegment]
¶
When you already have word-level timings (common from ASR), align phonemes within each word span. Much better than utterance-level.
Source code in src/openfacefx/alignment.py
dump_segments(segments: List['PhonemeSegment']) -> List[dict]
¶
Serialise phoneme segments to the plain-JSON shape the HTML previewer
consumes (tools/build_preview.py --segments): a list of
{"phoneme", "start", "end"[, "confidence"]}. confidence is emitted
only when the aligner supplied one.
Source code in src/openfacefx/alignment.py
load_mfa_textgrid(path: str, tier: str = 'phones') -> List[PhonemeSegment]
¶
Parse a Praat TextGrid produced by the Montreal Forced Aligner.
Only the interval tier named tier is read. Empty / silence intervals
become the sil phoneme so the mouth relaxes between words.
Source code in src/openfacefx/alignment.py
openfacefx.g2p
¶
Grapheme-to-phoneme (word -> ARPAbet phonemes).
Priority order
- A pronunciation dictionary (CMUdict format) if one is loaded.
- A small built-in dictionary so the demo runs with no downloads.
- A crude rule-based fallback for out-of-vocabulary words.
For production accuracy, load the full CMU Pronouncing Dictionary via
G2P.load_cmudict(path) or plug in a neural G2P model. The fallback exists
only so nothing crashes on unknown words.
G2P(pronouncer=None, tokenizer=None)
¶
Source code in src/openfacefx/g2p.py
pronouncer = pronouncer
instance-attribute
¶
tokenizer = tokenizer
instance-attribute
¶
load_cmudict(path: str) -> int
¶
Load a CMUdict-format file. Returns the number of entries added.
Format per line: WORD P1 P2 P3 ... (alt pronunciations as WORD(2))
Source code in src/openfacefx/g2p.py
word(w: str) -> List[str]
¶
tokenize(text: str) -> List[str]
¶
Split text into word tokens. The English default is
[A-Za-z']+ (which drops non-Latin script); set tokenizer to a
per-language callable so other scripts survive (issue #8).
Source code in src/openfacefx/g2p.py
phrase(text: str) -> List[str]
¶
Source code in src/openfacefx/g2p.py
load_dictionary(path: str) -> int
¶
Load a locale dictionary (issue #8): a file declaring its locale
and phoneme alphabet (arpabet / ipa / sampa), each entry mapped into
the internal inventory via the alias tables. Returns the number of
entries added (the first spelling of a word wins, as with load_cmudict).
Source code in src/openfacefx/g2p.py
oov_words(text: str) -> List[str]
¶
Words in text that would fall through to the rule fallback —
the ones worth adding to a pronunciation dictionary (QA reporting).
Source code in src/openfacefx/g2p.py
openfacefx.phonemes
¶
ARPAbet phoneme set.
We use the CMU/ARPAbet inventory because it is what the CMU Pronouncing Dictionary, Montreal Forced Aligner, and most English G2P tools emit. Stress digits (0/1/2) on vowels are stripped before mapping to visemes.
ARPABET_VOWELS = {'AA', 'AE', 'AH', 'AO', 'AW', 'AY', 'EH', 'ER', 'EY', 'IH', 'IY', 'OW', 'OY', 'UH', 'UW'}
module-attribute
¶
ARPABET_CONSONANTS = {'B', 'CH', 'D', 'DH', 'F', 'G', 'HH', 'JH', 'K', 'L', 'M', 'N', 'NG', 'P', 'R', 'S', 'SH', 'T', 'TH', 'V', 'W', 'Y', 'Z', 'ZH'}
module-attribute
¶
ARPABET = ARPABET_VOWELS | ARPABET_CONSONANTS
module-attribute
¶
SILENCE = 'sil'
module-attribute
¶
IPA_ALIASES = {'AA': 'ɑ', 'AE': 'æ', 'AH': 'ʌ', 'AO': 'ɔ', 'AW': 'aʊ', 'AY': 'aɪ', 'EH': 'ɛ', 'ER': 'ɝ', 'EY': 'eɪ', 'IH': 'ɪ', 'IY': 'i', 'OW': 'oʊ', 'OY': 'ɔɪ', 'UH': 'ʊ', 'UW': 'u', 'B': 'b', 'CH': 'tʃ', 'D': 'd', 'DH': 'ð', 'F': 'f', 'G': 'ɡ', 'HH': 'h', 'JH': 'dʒ', 'K': 'k', 'L': 'l', 'M': 'm', 'N': 'n', 'NG': 'ŋ', 'P': 'p', 'R': 'ɹ', 'S': 's', 'SH': 'ʃ', 'T': 't', 'TH': 'θ', 'V': 'v', 'W': 'w', 'Y': 'j', 'Z': 'z', 'ZH': 'ʒ'}
module-attribute
¶
SAMPA_ALIASES = {'AA': 'A', 'AE': '{', 'AH': 'V', 'AO': 'O', 'AW': 'aU', 'AY': 'aI', 'EH': 'E', 'ER': '3`', 'EY': 'eI', 'IH': 'I', 'IY': 'i', 'OW': 'oU', 'OY': 'OI', 'UH': 'U', 'UW': 'u', 'B': 'b', 'CH': 'tS', 'D': 'd', 'DH': 'D', 'F': 'f', 'G': 'g', 'HH': 'h', 'JH': 'dZ', 'K': 'k', 'L': 'l', 'M': 'm', 'N': 'n', 'NG': 'N', 'P': 'p', 'R': 'r\\', 'S': 's', 'SH': 'S', 'T': 't', 'TH': 'T', 'V': 'v', 'W': 'w', 'Y': 'j', 'Z': 'z', 'ZH': 'Z'}
module-attribute
¶
ALPHABETS = ('arpabet', 'ipa', 'sampa')
module-attribute
¶
strip_stress(phoneme: str) -> str
¶
Remove the trailing stress digit ARPAbet attaches to vowels (e.g. AH0).
is_vowel(phoneme: str) -> bool
¶
to_ipa(phoneme: str) -> str
¶
Internal phoneme -> IPA (stress digit dropped); unknown symbols pass through unchanged so a mixed transcript never crashes.
to_sampa(phoneme: str) -> str
¶
from_ipa(symbol: str) -> str
¶
IPA -> internal ARPAbet; an unrecognised symbol passes through (and will
fall to sil at the viseme stage, the documented behaviour).
from_sampa(symbol: str) -> str
¶
from_alphabet(symbol: str, alphabet: str) -> str
¶
Map a phoneme symbol written in alphabet (arpabet | ipa |
sampa) into the internal inventory. arpabet is the identity.
Source code in src/openfacefx/phonemes.py
openfacefx.energy
¶
Audio-energy lip-sync fallback for untranscripted speech.
When there is no transcript and no aligner output, you can still drive a believable flapping mouth straight from the loudness of the audio. This is the single most common non-ML lip-sync mechanism in the wild — SALSA remaps RMS amplitude through low/high cutoffs, Moho drives openness from instantaneous loudness, Live2D bakes RMS volume into a mouth-open parameter. This module is OpenFaceFX's version of that idea.
PCM samples -> per-frame RMS -> noise-gated, robustly-normalized envelope
-> asymmetric attack/release smoothing -> mouth-open curves
This is an energy fallback, not lip-sync. It knows nothing about phonemes
or visemes: it cannot tell a /m/ from an /aa/, and it will happily open the
mouth on a cough. It drives one primary jaw-open channel plus a small, purely
aesthetic spread into two secondary mouth shapes so the result does not read
as a single channel flapping on and off. Use the transcript-based pipeline
(generate_naive / generate_from_alignment) whenever you have text; use
this only when you have nothing but a WAV.
Only numpy and the stdlib wave module are used. Input must be 16-bit PCM
WAV (mono or stereo — stereo is downmixed); convert other codecs first, e.g.
ffmpeg -i in.mp3 -c:a pcm_s16le -ar 16000 out.wav.
energy_envelope(wav_path: str, fps: float = 60.0, window: Optional[float] = None, gate: float = 0.06, smoothing: Tuple[float, float] = (0.012, 0.09)) -> Tuple[np.ndarray, np.ndarray]
¶
Loudness envelope of a WAV, sampled at fps, as (times, envelope).
envelope is in [0, 1]: per-frame RMS, noise-gated and normalized
against a high percentile (not the max — see _REF_PERCENTILE), then run
through an asymmetric attack/release follower.
Parameters¶
window : RMS analysis window in seconds (default 2 / fps — a mild
overlap between frames). Larger = smoother, blurrier.
gate : noise-floor gate as a fraction of the reference level in [0, 1).
Frames quieter than gate * reference read as full silence; the
remaining range is stretched back to [0, 1] (a SALSA-style
low/high cutoff remap (v - lo) / (hi - lo)).
smoothing : (attack_seconds, release_seconds) for the envelope
follower. The default opens fast and closes ~7x slower.
A clip whose reference level is below _SILENCE_REF is all silence and
returns an all-zero envelope.
Source code in src/openfacefx/energy.py
generate_from_energy(wav_path: str, fps: float = 60.0, epsilon: float = 0.015, mapping: Optional[Mapping] = None, intensity: float = 1.0, spread: float = 0.25, window: Optional[float] = None, gate: float = 0.06, smoothing: Tuple[float, float] = (0.012, 0.09), gestures=None, smooth: float = 0.0, lag: float = 0.0) -> FaceTrack
¶
Build a FaceTrack from audio loudness alone — no transcript needed.
Energy fallback, not viseme detection. The loudness envelope
(:func:energy_envelope) drives one primary jaw-open channel (aa); a
small spread fraction of that opening is bled into two secondary mouth
shapes purely so the motion does not look like a single channel flapping.
The spread rule is a deliberate aesthetic heuristic, not phoneme
recognition: louder frames lean rounded/open (O), quieter frames lean
mid-spread (E). Silent frames go to rest (sil). No claim is made
that any channel matches the sound actually being spoken.
Each frame partitions unit weight across sil and the mouth shapes
(sil + aa + E + O == 1), so sil is a true "mouth closed" amount.
Output is an ordinary Oculus-viseme track, so --retarget, .anim and
the cue exporters all compose downstream exactly as for the other modes.
Parameters¶
intensity : gain on the mouth opening (1.0 as-is; >1 opens wider on
quiet speech, <1 is subtler). Clamped into [0, 1] per frame.
spread : fraction of the opening lent to the secondary E/O shapes
(0 = pure jaw flap; default 0.25).
mapping : optional target vocabulary supplying channel names and per-target
clamps (like the other generators). Must contain aa and sil;
defaults to the built-in Oculus-15 set.
epsilon, window, gate, smoothing : forwarded to keyframe reduction and
:func:energy_envelope.
gestures : opt-in non-verbal gesture layer (issue #5); a GestureParams
(or True for defaults) appends blink/brow/head/eye channels driven
by this same envelope. None (default) leaves output byte-identical.
smooth, lag : opt-in post-solve curve conditioning (issue #10). smooth is
a temporal Gaussian sigma in seconds run over the mouth partition before
keyframe reduction (the partition aa + E + O + sil == 1 survives it);
lag slides the reduced keyframes in time (seconds; >0 lags, <0
leads the audio, clamped into the clip). Both 0.0 (default) are a
byte-identical no-op. No closures exist here, so nothing is re-sealed.
Output is deterministic: identical audio and parameters give an identical
track (the mouth path has no RNG; the gesture layer is seeded from
GestureParams.seed).
Source code in src/openfacefx/energy.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | |
openfacefx.prosody
¶
Prosody extraction: pitch, loudness and speaking-rate -> typed events.
Where :mod:openfacefx.energy follows how loud the voice is, this module also
follows how high it is: a short-time autocorrelation pitch tracker recovers
a per-frame fundamental frequency (F0) and a voiced/unvoiced flag, and — reusing
energy.py's WAV reader and RMS follower for loudness — turns the two tracks
into the prosodic events an animator wants: emphasis (pitch and loudness
spike together), phrase_boundary (a silent pause or the utterance end) and
question_rise (a rising terminal F0, the yes/no-question cue), plus a global
speaking rate (a syllable-ish proxy).
PCM samples -> framed short-time autocorrelation -> F0 + voicing + clarity
-> robust pitch/energy z-scores -> emphasis / boundary / question
events (:class:`openfacefx.events.Event`)
This is DSP heuristics, not an ML prosody model. The tracker is the standard non-neural pipeline (windowed autocorrelation debiased by the window's own autocorrelation, à la Boersma/Praat; parabolic peak interpolation; an octave-cost period pick), so expect it to behave accordingly:
- Accuracy / voicing. On clean speech F0 lands within a few percent on voiced frames but makes occasional octave errors and mislabels voicing on whispered/breathy/creaky voice or low SNR (the voicing gate is clarity+energy, not a trained VAD); an ML tracker (CREPE/pYIN) is cleaner. Fine here because the events need only relative pitch movement, not calibrated Hz — an octave-shifted F0 still lands the emphasis and question-rise correctly.
- Prominence / questions are rule-based cue layers, not ToBI labelling: emphasis keys on coincident pitch+energy peaks (~75-80 % vs ~85 % human agreement), and question detection keys purely on terminal F0 rise, so it misses falling wh-questions and can false-fire on list/uptalk intonation.
- Will misbehave on music/singing, background noise, overlapping speakers
and heavy reverb. Input must be 16-bit PCM WAV (same as
energy.py); convert first, e.g.ffmpeg -i in.mp3 -c:a pcm_s16le out.wav.
Events are ordinary :class:openfacefx.events.Event records (issue #6), so they
attach to a :class:~openfacefx.curves.FaceTrack and serialise through the JSON /
Unity .anim / Unreal-notify exporters like an authored layer. numpy + stdlib
wave only; no RNG, so identical audio gives identical events across runs,
platforms and Python versions.
Source = Union[str, np.ndarray]
module-attribute
¶
ProsodyParams(fmin: float = 80.0, fmax: float = 400.0, voicing_threshold: float = 0.45, silence_frac: float = 0.03, octave_cost: float = 0.01, emph_thresh: float = 1.0, min_emph: float = 0.05, emph_merge: float = 0.12, min_pause: float = 0.18, clause_max: float = 0.45, q_look: float = 0.3, q_min_voiced: int = 4, q_rise: float = 2.0, rate_smooth: float = 0.05, rate_min_spacing: float = 0.12)
dataclass
¶
Every threshold the tracker and event derivation use, in one dataclass so the defaults live in one place and callers can retune without touching the algorithm. Defaults follow Praat's raw-autocorrelation pitch settings and the prosodic-prominence literature (see the module docstring).
fmin: float = 80.0
class-attribute
instance-attribute
¶
fmax: float = 400.0
class-attribute
instance-attribute
¶
voicing_threshold: float = 0.45
class-attribute
instance-attribute
¶
silence_frac: float = 0.03
class-attribute
instance-attribute
¶
octave_cost: float = 0.01
class-attribute
instance-attribute
¶
emph_thresh: float = 1.0
class-attribute
instance-attribute
¶
min_emph: float = 0.05
class-attribute
instance-attribute
¶
emph_merge: float = 0.12
class-attribute
instance-attribute
¶
min_pause: float = 0.18
class-attribute
instance-attribute
¶
clause_max: float = 0.45
class-attribute
instance-attribute
¶
q_look: float = 0.3
class-attribute
instance-attribute
¶
q_min_voiced: int = 4
class-attribute
instance-attribute
¶
q_rise: float = 2.0
class-attribute
instance-attribute
¶
rate_smooth: float = 0.05
class-attribute
instance-attribute
¶
rate_min_spacing: float = 0.12
class-attribute
instance-attribute
¶
ProsodyTrack(fps: float, times: np.ndarray, f0: np.ndarray, voiced: np.ndarray, rms: np.ndarray, clarity: np.ndarray, speaking_rate: float = 0.0)
dataclass
¶
Per-frame prosody bundle at the analysis frame rate fps. f0 is Hz
on voiced frames and nan on unvoiced ones; voiced is the boolean gate;
rms is the loudness follower (same units as energy._frame_rms);
clarity is the 0..1 autocorrelation periodicity; speaking_rate is a
global syllable-per-(voiced-)second estimate.
pitch_track(source: Source, rate: Optional[int] = None, fps: float = 100.0, fmin: float = 80.0, fmax: float = 400.0, params: Optional[ProsodyParams] = None) -> Tuple[np.ndarray, np.ndarray, np.ndarray]
¶
Per-frame fundamental frequency of speech, as (times, f0, voiced).
source is a 16-bit PCM WAV path or a float sample array in [-1, 1]
(then rate is required). fps is the analysis frame rate (hop) in Hz —
100 by default, i.e. a 10 ms step; pitch accuracy is set by the window (which
holds ~2.5 of the lowest period), not by fps. f0 is Hz on voiced
frames and np.nan on unvoiced ones; voiced is the boolean gate. See
the module docstring for accuracy caveats — this is a heuristic tracker, not
an ML pitch model.
Source code in src/openfacefx/prosody.py
prosody_features(wav_path: str, fps: float = 100.0, params: Optional[ProsodyParams] = None) -> ProsodyTrack
¶
Bundle a WAV's pitch, loudness, voicing/clarity and speaking rate into a
:class:ProsodyTrack at the fps analysis rate. Reuses
:func:openfacefx.energy._frame_rms for the loudness track, so prosody and
the energy fallback measure loudness the same way.
Source code in src/openfacefx/prosody.py
detect_events(track: ProsodyTrack, params: Optional[ProsodyParams] = None, segments=None) -> List[Event]
¶
Derive the typed prosodic events from a :class:ProsodyTrack: emphasis
(pitch+loudness beats), phrase_boundary (pauses + utterance end) and
question_rise (rising terminal F0). Returns a time-sorted list[Event].
segments (optional phoneme segments) currently only sharpen the utterance
end used for the trailing phrase boundary; pitch/energy drive everything else.
All events are plain :class:openfacefx.events.Event, so they attach and
serialise exactly like an authored event layer.
Source code in src/openfacefx/prosody.py
prosody_events(wav_path: str, fps: float = 100.0, segments=None, params: Optional[ProsodyParams] = None) -> List[Event]
¶
One call from a WAV to typed prosodic events: extract the
:class:ProsodyTrack then :func:detect_events. fps is the analysis rate
(event times are absolute seconds, independent of any render fps). Reuses
energy.py's reader, so the same 16-bit-PCM-WAV constraint applies.