Open-source aligner adapters¶
OpenFaceFX ships timing adapters for every commercial TTS source (Azure,
ElevenLabs, Kokoro, Google, Piper, Cartesia, Polly). Issue #54 closes the
asymmetry with first-class adapters for the free, open-source aligners/ASR
that indie and research users actually have — so a user with only a WAV gets
accurate, model-backed timings into the pipeline without MFA and without writing
glue. They are siblings of anchors.from_azure_word_boundaries, stdlib json
only, and additive (with no --anchors-format, output is unchanged).
| adapter | source | returns |
|---|---|---|
from_whisper_json |
OpenAI Whisper verbose_json (segments[].words[] or a flat words[]) |
word Anchors |
from_whisperx |
WhisperX segments[].words[] |
word Anchors |
from_gentle |
Gentle words[] (case == "success") |
word Anchors |
from_gentle_phones |
Gentle per-word phones[] |
PhonemeSegments (phone path) |
# word timings — the aligner supplies the words, so no --text needed
python -m openfacefx naive --anchors words.json --anchors-format whisper -o out.json
python -m openfacefx naive --anchors align.json --anchors-format whisperx --wav vo.wav -o out.json
# Gentle's free phoneme timings — the accurate path, straight to generate_from_alignment
python -m openfacefx naive --anchors gentle.json --anchors-format gentle-phones -o out.json
Tolerance + the deterministic drop rule¶
The adapters tolerate the key variance across openai-whisper / faster-whisper /
whisper.cpp (word vs text, probability vs score). Aligners leave some words
unaligned — Whisper omits the timestamp, Gentle marks case != "success" — and
such a word is dropped deterministically: its neighbours' anchors still pin the
timeline and the aligner spreads the gap. A phone/word symbol outside the ARPAbet
inventory (Gentle oov, a non-English token) passes through and falls to sil at
the viseme stage (documented), never a crash.
The Gentle phone path¶
Gentle's per-word phones[] carry a relative duration and an ARPAbet symbol with
a _B/_I/_E/_S position suffix. from_gentle_phones strips the suffix,
upper-cases, and accumulates the durations from each word's start into absolute
PhonemeSegments (the last phone ends at the word span within float tolerance),
with silence filling the gaps between words. This skips the naive spacer and
feeds generate_from_alignment directly — a genuinely phone-accurate track from a
free tool.
openfacefx.aligners
¶
First-class adapters for the free, open-source aligners / ASR (issue #54).
OpenFaceFX ships timing adapters for every commercial TTS source (Azure,
ElevenLabs, Kokoro, Google, Piper, Cartesia, Polly) but used to punt the
open-source tools — Whisper, WhisperX, Gentle — to the user with a "write a
~15-line adapter" note in three places. These are those adapters: siblings of
:func:openfacefx.anchors.from_azure_word_boundaries, stdlib json only,
returning the normalized :class:~openfacefx.anchors.Anchor list (or, for
Gentle's phone timings, :class:~openfacefx.alignment.PhonemeSegment s directly).
- :func:
from_whisper_json— OpenAI Whisperverbose_jsonword timestamps (segments[].words[]or a top-levelwords[]). Tolerant of the key variance across openai-whisper / faster-whisper / whisper.cpp (wordvstext,probabilityvsscore). - :func:
from_whisperx— WhisperXsegments[].words[](word/start/end/score). - :func:
from_gentle— Gentlewords[](word-level anchors); and :func:from_gentle_phones— Gentle's per-wordphones[](relativedurations, ARPAbet with_B/_I/_E/_Sposition suffixes) accumulated from the word start into phone-level segments, a path that skips the naive spacer entirely.
Deterministic missing-timestamp rule: aligners leave some words unaligned
(Whisper omits timestamps, Gentle marks case != "success"). Such a word is
dropped — its neighbours' anchors still pin the timeline and the aligner
spreads the gap. A phone/word symbol outside the ARPAbet inventory passes through
and falls to sil at the viseme stage (documented), never a crash.
from_whisper_json(json_text: str) -> List[Anchor]
¶
OpenAI Whisper verbose_json (word_timestamps=True) -> word anchors.
Accepts the {"segments": [{"words": [...]}]} shape and the flat
{"words": [...]} / bare-array shapes different wrappers emit, and the
word/text + probability/score key variance across
openai-whisper, faster-whisper and whisper.cpp. Words Whisper left without a
timestamp are dropped.
Source code in src/openfacefx/aligners.py
from_whisperx(json_text: str) -> List[Anchor]
¶
WhisperX alignment (segments[].words[] with word/start/end/
score) -> word anchors. Words WhisperX could not align (no start) are
dropped, the same deterministic rule as :func:from_whisper_json.
Source code in src/openfacefx/aligners.py
from_gentle(json_text: str) -> List[Anchor]
¶
Gentle forced-aligner JSON (words[]) -> word anchors. Only
case == "success" words are kept; not-found-in-audio /
not-found-in-transcript words fall out as gaps.
Source code in src/openfacefx/aligners.py
from_gentle_phones(json_text: str) -> List[PhonemeSegment]
¶
Gentle words[].phones[] -> phone-level :class:PhonemeSegment s.
Each successful word's phones carry a relative duration; accumulating
them from the word's start gives absolute phone times (so the last phone
ends at the word span within float tolerance). Gaps between successful words
become silence. This is the accurate phone path — it skips the naive spacer
and feeds generate_from_alignment directly.