VisemeTimeline — plain,
versioned JSON.
Generate → persist → replay
Generate once
Run
processAudio (vanilla) or useProcessAudio (React). result.timeline
is the artifact.React
Vanilla
Assembling a timeline yourself
If you already have per-frame viseme ids (e.g. from your own batch job), build a timeline with the pure converters instead of running inference:Prefetching & queues
Because a timeline is detached from the model, you can compute many ahead of time and play them instantly later:- Prefetch on idle — generate timelines for likely-next utterances during idle time; play from cache the moment they are needed (no inference latency at play time).
- Queue playback — store a list of
{ audioUrl, timeline }pairs; for each, start the audio andplayback.setTimeline(timeline)in lockstep. - Server-side precompute — generate timelines in a build step or backend job, ship the JSON with your assets, and the client never runs inference for that content at all.
speechMs rides inside the timeline, so cached replay never re-meters and
never re-infers.
Deterministic video export
For frame-accurate rendering (recording the avatar to video), a timeline gives you a fixed, inspectable script: the same JSON produces the same mouth frames every run. Driveplayback.seek(ms) to a render clock instead of wall-clock
playback, capture the canvas per frame, and mux against the original audio.
Because there is no live inference in the loop, export is reproducible and as
fast as your renderer.
Versioning & the trust boundary
parseTimeline validates untrusted/persisted JSON and throws a LipsyncError
with .code === "bad_timeline" on a version or shape mismatch — so a stale
stored timeline fails loudly instead of animating garbage:
parseTimeline, never JSON.parse
alone. VISEME_TIMELINE_VERSION bumps on breaking changes; old artifacts are
rejected deterministically.
Next
Visemes & the timeline
The timeline format in detail.
Core client
processAudio and helpers.Error codes
bad_timeline and the rest.