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. - 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 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, so no schema
version bump was needed.
Deferred (flagged for a follow-up)¶
Two lower-value habits — short-obstruent / nasal "leave-the-jaw-untouched" and word-final anticipatory lip shape — and the NVIDIA-A2F-style tongue-channel gain/offset mapping fields (with the ARKit tongue targets) are not in this pass. Adding the gain/offset fields would bump the mapping schema version; it is left out here so the schema and shipped ARKit preset stay byte-identical.
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;
- 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.
Deferred (flagged, not yet implemented): the short-obstruent / nasal "leave-the-jaw-untouched" and word-final anticipatory-lip habits, and the tongue-channel gain/offset mapping fields (NVIDIA A2F style) with the ARKit tongue targets — those touch the mapping schema / shipped presets and are left for a follow-up.
RULE_IDS: Tuple[str, ...] = ('bilabial_close', 'labiodental_teeth', 'sibilant_jaw', 'nonnasal_lip_open', 'duplicated_merge', 'lip_heavy', 'tongue_no_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 — and the
lip-heavy habit extends both sides by the lip-protrusion constant.
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 to the dense matrix, in place. Called
from :func:coarticulation._blend in place of the legacy
_enforce_closures when JALI is on.