mcpbeat

Lipsync

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.

23k tokens
context cost
the whole folder, loaded on every use
18
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
455
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/google/xrblocks --skill lipsync

What comes with it

86 345 bytes besides the instruction
BlendshapeReducer.test.ts
BlendshapeReducer.ts
FormantVisemeMapper.test.ts
FormantVisemeMapper.ts
LipsyncMouth.test.ts
LipsyncMouth.ts
MfccExtractor.test.ts
MfccExtractor.ts
README.md
computeAudioFeatures.test.ts
computeAudioFeatures.ts
index.ts
samples/index.html
samples/netblocks/index.html
samples/netblocks/main.ts
samples/puppet/index.html
samples/puppet/main.ts

The instruction itself

6 sections, as written by the author

lipsync — audio-driven mouths for XR Blocks

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/.

When to use

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.

Mental model

  • 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.

Public surface

| 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. |

Lifecycle and ownership

  • The caller owns the MediaStream. LipsyncMouth.dispose() disconnects audio nodes but never stops tracks. If you got the stream from getUserMedia, stop the tracks yourself when done.
  • The caller owns the 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.
  • The caller owns the 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.
  • Instances are one-shot. After dispose, construct a new LipsyncMouth.

Browser quirks worth knowing

  • Microphone access requires HTTPS in modern browsers. Use localhost or a real cert for cross-device testing.
  • Browsers can drop a 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.
  • High-pitched voices (children, sopranos) push formants up and reduce vowel separation. Speaker-relative normalisation would help and is a sensible follow-up.

How to use it

Copy the folder

Take google/lipsync from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

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.