JALI coarticulation rules¶
JALI (Edwards et al., SIGGRAPH
2016) publishes a concrete rule set and measured onset/decay constants that
extend exactly the machinery OpenFaceFX already ships (Cohen–Massaro blending,
per-class timing, bilabial closure). Issue #19 adopts them as a data-driven
rule table (data/jali_rules.json) over the
component stage.
It is entirely opt-in. With the default (JALI off)
build_viseme_curves is byte-identical to before — the whole existing suite
stays green and a diff against the released wheel is identical. Turn it on, and
select rules, through CoartParams:
from openfacefx import CoartParams, build_viseme_curves, JALI_RULE_IDS
# all JALI rules + empirical timing
params = CoartParams(jali=True)
# or just a couple, individually toggled (JALI_RULE_IDS lists them all)
params = CoartParams(jali=True, jali_rules=("sibilant_jaw", "lip_heavy"))
times, matrix = build_viseme_curves(segments, mapping=mapping, params=params)
What it adds (the prioritised set)¶
- Hard constraints (post-blend forcings over articulator classes, each
toggleable):
bilabial_close(lips seal),labiodental_teeth(bottom lip to teeth),sibilant_jaw(sibilants narrow the jaw),nonnasal_lip_open(a non-nasal open segment opens the lips, so a neighbour's closure can't bleed across). When JALI is on these replace the legacy lips-only closure pass. - Habits:
duplicated_mergecollapses adjacent same-viseme segments across a word boundary into one hold ("po_p m_an");lip_heavygives the rounded/ protruded visemes (UW OW OY w S Z J C) an earlier onset and longer hold;tongue_no_lipguarantees a tongue articulation (l n t d g k ng) never pulls a lip channel;short_no_jaw(#53) holds the jaw at the neighbouring level through a short obstruent or nasal so a quick stop/nasal can't dip it; andwordfinal_lip(#53) gives a word-final lip-shaped phoneme an earlier onset so its lip shape anticipates (word-final is approximated as pre-silence / utterance-final, as the phoneme stream carries no inter-word boundaries). - Empirical timing (
jali_timing, on by default when JALI is on): a per-phoneme, context-dependent onset/decay lookup — onset ~120 ms before the apex, tighter after a vowel (~60 ms) than after a pause (~120 ms), with a ~150 ms lip-protrusion extension — replacing the per-classleadconstants.
Data, not code¶
The categories (phoneme sets), constraint floors/caps, habit thresholds and
timing constants live in data/jali_rules.json so new measurements drop in
without touching code. The tongue articulator class was already in the mapping
schema; issue #53 adds optional per-target gain/offset fields (NVIDIA-A2F-style
channel tuning), which bump the mapping schema to version 2 — version-1 files
still load, the absent fields reading as the no-op defaults.
Follow-up (#53)¶
Issue #53 completes the two remaining JALI habits above — short_no_jaw and
wordfinal_lip, both off by default and byte-identical when off — and adds the
tongue-channel gain/offset mapping fields (schema v2, applied at keyframe
reduction; see openfacefx.mapping and
reduce_to_track). At the no-op defaults
(gain=1.0, offset=0.0) a mapping is byte-identical to before.
openfacefx.coart_jali
¶
JALI coarticulation rules over the component stage (issue #19).
JALI (Edwards et al., SIGGRAPH 2016, https://www.dgp.toronto.edu/~elf/JALISIG16.pdf)
publishes a concrete rule set and measured onset/decay constants that extend the
machinery OpenFaceFX already ships (Cohen–Massaro blending, per-class timing,
bilabial closure). This module encodes those rules as a data-driven table
(data/jali_rules.json — plain data so new measurements drop in) evaluated over
articulator classes, plus the empirical timing lookup.
Everything here is opt-in: it runs only when :attr:CoartParams.jali is set,
so with the default (JALI off) :func:openfacefx.coarticulation.build_viseme_curves
is byte-identical to before. Each constraint/habit is individually toggleable via
:attr:CoartParams.jali_rules (None = all). Implemented (the issue's
prioritised set):
- hard constraints — bilabial lip closure, labiodental bottom-lip-to-teeth, sibilant jaw narrowing, non-nasal lip opening;
- habits — duplicated-viseme merge across word boundaries ("po_p m_an"), lip-heavy anticipation/hysteresis (UW/OW/OY/w/S/Z/J/C start early & end late), tongue-only visemes never influence lip channels, short obstruents/nasals leave the jaw untouched, and a word-final lip shape anticipates (#53);
- empirical timing — per-phoneme, context-dependent onset/decay (post-pause
vs post-vowel; a 150 ms lip-protrusion extension) replacing the per-class
leadconstants when :attr:CoartParams.jali_timingis on.
The NVIDIA-A2F-style per-target tongue gain/offset fields (#53) live in the
mapping schema (:mod:openfacefx.mapping) and are applied at keyframe reduction,
not here.
RULE_IDS: Tuple[str, ...] = ('bilabial_close', 'labiodental_teeth', 'sibilant_jaw', 'nonnasal_lip_open', 'duplicated_merge', 'lip_heavy', 'tongue_no_lip', 'short_no_jaw', 'wordfinal_lip')
module-attribute
¶
load_rules() -> dict
cached
¶
The JALI rule table (cached). Plain JSON data — categories, constraints and the empirical timing constants.
Source code in src/openfacefx/coart_jali.py
rule_enabled(params, rule_id: str) -> bool
¶
True if rule_id is active: JALI on and either all rules (jali_rules
is None) or this id is in the selected set.
Source code in src/openfacefx/coart_jali.py
merge_duplicates(segments: List[PhonemeSegment], mapping) -> List[PhonemeSegment]
¶
Merge adjacent segments that map to the same viseme into one longer segment (JALI's duplicated-viseme merge) — the closing /p/ of "pop" and the /m/ of "man" are one bilabial hold, not two. Silence is never merged.
Source code in src/openfacefx/coart_jali.py
timing_leads(segments: List[PhonemeSegment], params) -> Tuple[np.ndarray, np.ndarray]
¶
Per-segment (lead_in, lead_out) influence extents (seconds) from the
empirical table, replacing the per-class lead constants. Onset is
context-dependent — longer after a pause, tighter after a vowel — the
lip-heavy habit extends both sides by the lip-protrusion constant, and the
word-final habit (#53) extends the onset of a word-final lip-shaped phoneme so
its lip shape anticipates.
Source code in src/openfacefx/coart_jali.py
mask_tongue_lips(weights: np.ndarray, segments: List[PhonemeSegment], mapping, names: List[str]) -> None
¶
Zero every tongue-class segment's weight on lip-class targets, in place — a tongue articulation (l/n/t/d/g/k/ng) must not pull the lips.
Source code in src/openfacefx/coart_jali.py
apply_constraints(times: np.ndarray, matrix: np.ndarray, segments: List[PhonemeSegment], mapping, weights: np.ndarray, names: List[str], params) -> None
¶
Apply the enabled hard constraints (and the #53 short-obstruent jaw habit)
to the dense matrix, in place. Called from :func:coarticulation._blend in
place of the legacy _enforce_closures when JALI is on.