google/lipsync
>- Add audio-driven avatar mouths to an XR Blocks app with the lipsync addon — heuristic vowel-formant viseme mapping that turns any `MediaStream` (mic or remote peer's voice) into mouth shapes on a `StylizedFace` canvas decal attached to an avatar's head. No ML runtime, no model download. Use when authoring or debugging avatar mouths in single-user demos or in multiplayer scenes paired with `xb-netblocks` (every remote driver), `xb.StylizedFace` (the decal, in xrblocks core), `session.voice.onTrack` for the netblocks hook, plus the lower-level `FormantVisemeMapper`, `MfccExtractor`, `computeAudioFeatures` and types (`VisemeWeights`, `VisemeTarget`, `AudioFeatures`) for plugging in a model-based mapper later. For the DSP pipeline, caveats, and samples read this folder's README.md.
npx skills add https://github.com/google/xrblocks --skill lipsync
lipsync turns any avatar with a head pivot into a face that visibly mouths along to a MediaStream. Mental model: a per-frame FFT + formant analyser writes viseme weights into a target; the target (typically xb.StylizedFace) re-rasterises a small canvas decal anchored to the head sphere's local -Z. No model download, no ML runtime.
> Full reference (DSP pipeline, samples, caveats, public surface): README.md. Samples: samples/.
Pair with netblocks so every remote peer's voice stream drives their own avatar's mouth, turning shared rooms from silent spheres into faces that visibly speak. Standalone use is fine too: a TTS playback, an NPC, or a single-user puppet head. The face primitive (xb.StylizedFace) is in xrblocks core, so any consumer can drive it via setVisemes(VisemeWeights), not just lipsync.
For ML-grade phoneme accuracy, the lower-level pieces (FormantVisemeMapper, MfccExtractor, computeAudioFeatures) and types are exported so a model-based mapper can slot in without touching the addon's public surface.
MediaStream → WebAudio AnalyserNode → byte frequency + time-domain buffers each frame.computeAudioFeatures extracts RMS, voicing, F1, F2, and a few band energies.FormantVisemeMapper maps F1/F2 to six viseme weights (jawOpen, aa, oo, oh, ee, consonant) with frame-rate-independent smoothing (1 - exp(-dt / tau)).LipsyncMouth writes the weights to its target via setVisemes(...).xb.StylizedFace re-rasterises a 256×256 canvas (one dark mouth ellipse, optional eyes) and uploads it as a CanvasTexture on a small plane.| Symbol | Where | Purpose |
| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| LipsyncMouth | xrblocks/addons/lipsync/index.js | The driver xb.Script. Constructor: (stream: MediaStream, {target, audioContext?, fftSize?, silenceThreshold?, silenceHoldMs?}). |
| xb.StylizedFace | xrblocks core (import {StylizedFace} from 'xrblocks') | The face decal. Options: headRadius, textureSize, showEyes. |
| RemoteUserAvatar.face | xrblocks/addons/netblocks | Already a StylizedFace, ready for LipsyncMouth to drive. |
| FormantVisemeMapper, MfccExtractor, computeAudioFeatures | xrblocks/addons/lipsync/index.js | Pure modules a future ML mapper can plug into. |
| Types: VisemeWeights, VisemeTarget, FormantVisemeMapperOptions, MfccExtractorOptions, AudioFeatures, AudioFeatureInputs | xrblocks/addons/lipsync/index.js | Shared shapes for custom drivers. |
MediaStream. LipsyncMouth.dispose() disconnects audio nodes but never stops tracks. If you got the stream from getUserMedia, stop the tracks yourself when done.AudioContext when passed in. Always pass audioContext to reuse a shared context for any scene with more than one mouth (browsers cap contexts at around six per page). If omitted, LipsyncMouth creates its own and closes it on dispose.target face. dispose() resets it to rest pose so a speaker who stops mid-vowel never leaves their avatar's mouth frozen open, but never disposes the face itself.LipsyncMouth.localhost or a real cert for cross-device testing.MediaStreamAudioSourceNode unless the same stream is also being pumped by an HTMLMediaElement. LipsyncMouth creates a muted off-DOM <audio> primer per stream to keep WebAudio alive. Same workaround SpatialVoice uses.Take google/lipsync from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.