Inspecting & validating assets¶
Two deterministic, read-only commands (issue #47) answer "what's in this
track?" and "is this asset well-formed?" without opening the previewer — the
CI-friendly home for the format contract io_export, edits, and events
already imply.
inspect¶
python -m openfacefx inspect track.json # human table
python -m openfacefx inspect track.json --json # schema-stable JSON stats
inspect prints duration, fps, channel/keyframe counts, per-channel key count /
min / max / start / end / time-coverage, event & variant counts, and the
weight-vs-pose-vs-gesture channel split. --json emits a schema-stable object
(format: openfacefx.inspect) — every documented key is always present, lists
empty rather than absent — so a CI step can assert on it. It reuses
qa.summarize / qa.cue_flags for the shared counters, so the
numbers match the generate commands' --json output.
validate¶
python -m openfacefx validate track.json # lint; exits nonzero on a violation
python -m openfacefx validate line.edits.json --json # deterministic problem list
python -m openfacefx validate events.json --strict # warnings become errors
validate is a lint gate. It auto-detects the asset kind — a .track.json, an
*.edits.json sidecar, or a standalone events file — parses it (via
io_export.from_dict / the edits / events validators), checks the contract,
and exits nonzero with a deterministic, sorted, machine-readable problem list
({severity, code, where, detail}) so CI diffs stay clean. Checks:
- per-channel key times monotonic non-decreasing and within
[0, duration]; - weight channels (viseme / blendshape / emotion) values in
[0, 1]; the signed pose channels (headYaw/Pitch/Roll,eyeYaw/Pitch— angles, in degrees or[-1, 1]) flagged only when wildly out of range (> ±360), not for being outside[0, 1]— the same weight-vs-angle distinction the CSV importer draws; viseme_set/target_setconsistent with the channel names;- event / variant blocks via
events.validate_events, plus a check that every eventtypeis a knownEVENT_TYPESmember; --strictpromotes warnings (empty channels, a zero-length track) to errors.
validate exits 0 on every track the generators and importers produce and
nonzero on a corrupted one (out-of-order times, out-of-range weight, unknown event
type, viseme_set mismatch). Library callers get inspect_track(track),
validate_asset(data) / validate_file(path), and detect_kind(data). Read-only
(never writes), stdlib only, deterministic across Python 3.9/3.13.
openfacefx.inspect
¶
Read-only track/asset inspection and a CI-friendly contract linter (issue #47).
Two deterministic, read-only views that never open the previewer and never write a file:
-
:func:
inspect_track-- what's in this track? A schema-stable stats dict (duration, fps, channel/keyframe counts, per-channel key count / min / max / time-coverage, event & variant counts, the weight/pose/gesture channel split), reusing :func:openfacefx.qa.summarizeandcue_flagsfor the shared counters. Every documented key is always present -- lists are empty, not absent -- so a CI step can assert on it without scraping console text. -
:func:
validate_asset-- is this asset well-formed? The format contractio_export/edits/eventsalready imply but never enforce standalone. It parses a.track.json, an*.edits.jsonsidecar, or a standalone events file (kind auto-detected), checks the contract, and returns a deterministic, sorted, machine-readable problem list (severity/code/where/detail) so a lint gate can fail a build with a clean diff.strictpromotes warnings (empty channels, zero-length track) to errors.
Weight vs pose: viseme / blendshape / emotion channels are [0, 1] weights;
the signed pose channels (head/eye angles) are flagged only when wildly out of
range, since they legitimately carry degrees or [-1, 1]. numpy is not needed
here -- pure stdlib (math/json), no clock, no RNG.
FORMAT = 'openfacefx.inspect'
module-attribute
¶
VERSION = 1
module-attribute
¶
POSE_CHANNELS = frozenset({'headPitch', 'headYaw', 'headRoll', 'eyePitch', 'eyeYaw'})
module-attribute
¶
inspect_track(track, *, segments=None, oov_words=None) -> Dict
¶
A deterministic, schema-stable stats dict for track.
Reuses :func:openfacefx.qa.summarize for the shared counters and cue-flag
logic, then adds per-channel detail and the weight/pose/gesture split. Every
key is always present; channel_detail is sorted by name.
Source code in src/openfacefx/inspect.py
render_inspect(doc: Dict) -> str
¶
A compact human table of an :func:inspect_track dict.
Source code in src/openfacefx/inspect.py
detect_kind(data) -> str
¶
track / edits / events / unknown from a parsed JSON dict.
Source code in src/openfacefx/inspect.py
validate_asset(data, *, kind: Optional[str] = None, strict: bool = False) -> Tuple[str, List[Dict]]
¶
Validate a parsed asset dict; returns (kind, problems).
problems is a deterministic, sorted list of {severity, code, where,
detail}. strict relabels every warning as an error (so the gate
fails on empty channels / a zero-length track too).
Source code in src/openfacefx/inspect.py
validate_file(path: str, *, strict: bool = False) -> Tuple[str, List[Dict]]
¶
Read path and :func:validate_asset it. JSON / read errors become a
single problem (so a lint gate still exits nonzero, never crashes).
Source code in src/openfacefx/inspect.py
render_problems(kind: str, problems: List[Dict]) -> str
¶
Human summary of a :func:validate_asset result.
Source code in src/openfacefx/inspect.py
diff¶
OpenFaceFX ships a hard determinism guarantee; diff is the golden-file / snapshot
gate that leverages it (issue #50) — "did this solver-param / coarticulation /
retarget change actually move the curves, and by how much?" It is distinct from
its neighbours: validate checks a single file against the contract, and
diff-edits writes a sidecar for re-application; diff always takes two
tracks and never writes. A raw cmp is too brittle (4-dp time quantisation,
RDP key placement), so it compares semantically.
python -m openfacefx diff golden.track.json candidate.track.json # exact-match gate
python -m openfacefx diff golden.track.json candidate.track.json --tolerance 0.002
python -m openfacefx diff a.track.json b.track.json --json > drift.json
The report gives the duration delta, an fps mismatch, per-channel added/removed,
and for shared channels the max-abs / RMS / mean-abs value delta on a shared
dense grid (the same np.interp resampling edits.sample uses) plus
time-coverage and first/last-key drift, and event add/remove/changed. It exits
0 when every delta ≤ --tolerance (default 0.0 → exact match) and nonzero
otherwise, emitting a deterministic, sorted problem list ({channel, metric,
value}) so CI diffs stay stable; --json prints the full schema-stable report,
human mode a worst-first table. The magnitudes are symmetric — diff(a, b) and
diff(b, a) agree up to sign, and added/removed swap. Library callers get
diff_tracks(a, b, *, tolerance); pure numpy + stdlib, no solver, no RNG, no
writes.
openfacefx.trackdiff
¶
Structured A/B track drift report with a tolerance-gated verdict (issue #50).
OpenFaceFX ships a hard determinism guarantee but no tool to leverage it. This
is the golden-file / snapshot gate: "did this solver-param / coarticulation /
retarget change actually move the curves, and by how much?" It is distinct from
its neighbours -- :func:openfacefx.inspect.validate_asset checks a single
file against the format contract, and :func:openfacefx.edits.diff_edits writes
a sidecar for re-application; diff always takes two tracks and never
writes. A raw cmp is too brittle (4-dp time quantisation, RDP key
placement), so this compares semantically: per-channel value deltas on a shared
dense grid (the same np.interp resampling :func:openfacefx.edits.sample
uses), plus structural drift (added/removed channels, duration/fps, events).
The report is a plain, JSON-ready dict with a deterministic, sorted problem list
({channel, metric, value}); ok is true iff every delta is within
tolerance and there is no structural drift. numpy + stdlib, no solver, no RNG,
no writes -- same inputs, same bytes on Python 3.9/3.13.
FORMAT = 'openfacefx.diff'
module-attribute
¶
VERSION = 1
module-attribute
¶
diff_tracks(a: FaceTrack, b: FaceTrack, *, tolerance: float = 0.0) -> Dict
¶
A structured drift report between tracks a and b.
tolerance (default 0.0 -> exact match) gates the ok verdict: every
per-channel value delta (max-abs / RMS / mean-abs, coverage and first/last-key
drift) and the duration/fps deltas must be <= tolerance, and there must be
no structural drift (added/removed channels, changed events). The magnitudes
are symmetric, so diff(a, b) and diff(b, a) agree up to sign and the
added/removed channel lists swap.
Source code in src/openfacefx/trackdiff.py
render_diff(report: Dict) -> str
¶
A compact worst-first human summary of a :func:diff_tracks report.