| Symptom | Likely cause | Fix |
|---|---|---|
| No sound, and the mouth doesn’t move | The browser blocked audio before any user interaction | Call attachStream or speak from a click |
| The voice is heard twice | The audio is also played elsewhere (for example track.attach() or an <audio> element) | Only give it to the avatar |
| The avatar doesn’t appear | The container has no size, the URL isn’t a .glb, or CORS blocks it | Size the container; check the browser’s network tab |
| The avatar appears but the mouth never moves | The model has no viseme_* morph targets (see the console warning) | Use an avatar with Oculus visemes, or map its names with morphTargets |
viseme model … HTTP 4xx | modelUrl is wrong, or jsDelivr is blocked | Self-host the model |
A CSP error mentioning blob: | script-src doesn’t allow blob: | Add blob: to script-src |
| The lips look late | streamDelayMs is too low for this device | Raise streamDelayMs |
| The lip sync is poor | Noisy or unclear audio | Use a TTS voice, or cleaner audio. See Best results. |
attachStream rejects | The MediaStream has no audio track | Pass a stream with an audio track |
| The microphone doesn’t work | The page isn’t on HTTPS, or permission was denied | Use HTTPS (or localhost) and allow the microphone |
Catch errors
Background errors (avatar loading, stream analysis) go toonError, which defaults to console.error. Methods that return promises reject on failure, so catch them:
const avatar = new TalkingAvatar({ container, avatarUrl, onError: (error) => report(error) });
await avatar.ready.catch((error) => showFallback(error)); // the first avatar failed to load
avatar.speak("/hello.mp3").catch(console.error);