Lip-sync vs amplitud para un avatar que habla
Por el equipo de Charmotion · Actualizado el

Para la mayoría de los avatares estilizados, la amplitud es suficiente: mide el nivel de la voz con un AnalyserNode de Web Audio, calcula el RMS y activa y desactiva un loop de talking con histéresis. Una sincronización labial real necesita la temporización de visemas de tu proveedor de voz, como los eventos de visemas de Azure Speech o las speech marks de Amazon Polly, además de un personaje con una forma de boca para cada visema.
Puntos clave
- La activación por amplitud responde a "¿se está reproduciendo la voz ahora mismo?". Los visemas responden a "¿qué forma de boca corresponde a este sonido?".
- Usa
getByteTimeDomainData, convierte los datos a RMS y aplica dos umbrales más un tiempo de retención para que el estado no parpadee. - Azure Speech envía 22 ID de visemas con offsets en ticks de 100 nanosegundos; Polly devuelve speech marks con tiempos en milisegundos.
- Los datos de visemas no sirven de nada sin formas de boca dibujadas para cada visema, así que revisa tu personaje antes de elegir un enfoque.
¿Ya tienes un personaje o logo? Mira tu personaje en pantallas reales de una app: pantallas de splash, onboarding, carga y estado vacío. Es gratis, funciona en tu navegador y no sube nada.
¿Necesito sincronización labial real o basta con la amplitud?
Depende de la cara. La sincronización labial (lip-sync) consiste en hacer coincidir la boca con los sonidos que se pronuncian. Eso requiere dos cosas: datos de temporización que indiquen qué sonido ocurre en qué momento, y un personaje dibujado con una pose de boca distinta para cada grupo de sonidos (un visema). La animación de habla basada en amplitud no necesita ninguna de las dos. Solo comprueba si la voz se oye en ese momento y reproduce un loop de habla mientras se oiga.
| Activación por amplitud | Visemas de Azure | Speech marks de Polly | |
|---|---|---|---|
| Lo que obtienes | Un valor de volumen en vivo | 22 ID de visemas, blend shapes opcionales | Nombres de visemas con tiempos |
| Unidad de tiempo | Por fotograma de animación | Ticks de 100 ns (divide entre 10 000 para obtener ms) | Milisegundos desde el inicio del audio |
| Funciona con cualquier fuente de voz | Sí, incluidos agentes en tiempo real | Solo Azure TTS | Solo Polly TTS |
| Lo que necesita el personaje | Un loop de habla | Una pose de boca por visema | Una pose de boca por visema |
| Esfuerzo | Bajo | Alto | Alto |
Fuentes: Microsoft Learn, documentación de Amazon Polly.
Los personajes pequeños y estilizados, como un badge de 64 px junto a un chat o una mascota de marca con una boca simple, suelen percibirse como "hablando" solo por el ritmo. Los visemas empiezan a valer la pena en caras más grandes y más humanas, donde una boca que no coincide se nota.
Sé honesto sobre lo que admite tu personaje. Los sets de Charmotion, por ejemplo, incluyen un loop de habla (mood 7), pero no inputs de forma de boca ni de visemas, así que funcionan con activación por amplitud, no con sincronización labial por fonemas. Es una elección deliberada para agentes de chat y de voz en los que las palabras también aparecen como texto.
¿Cómo mido el nivel de voz con un AnalyserNode?
El AnalyserNode de la Web Audio API expone datos en tiempo real del dominio del tiempo y de frecuencia sin modificar el audio que pasa por él (MDN). Para una compuerta de habla, la forma de onda en el dominio del tiempo es la entrada más simple. getByteTimeDomainData() llena un Uint8Array con muestras de 0 a 255, donde 128 es silencio (MDN). Conviértelas al rango −1…1 y calcula la media cuadrática (RMS):
const ctx = new AudioContext();
const analyser = ctx.createAnalyser();
analyser.fftSize = 1024;
// Pick the source that carries the assistant's voice:
const source = ctx.createMediaElementSource(audioElement); // <audio> playback
// const source = ctx.createMediaStreamSource(remoteStream); // WebRTC track
source.connect(analyser);
analyser.connect(ctx.destination); // only for media element sources, so it stays audible
const samples = new Uint8Array(analyser.fftSize);
function rms(): number {
analyser.getByteTimeDomainData(samples);
let sum = 0;
for (let i = 0; i < samples.length; i++) {
const v = (samples[i] - 128) / 128;
sum += v * v;
}
return Math.sqrt(sum / samples.length);
}
Una fuente de elemento multimedia redirige el audio del elemento a través del grafo, así que conecta el analizador hacia el destino o la voz quedará en silencio. Una fuente de stream de una pista WebRTC solo intercepta el audio; deja que el elemento <audio> que reproduce la pista se encargue de la salida.
Crea o reanuda el AudioContext a partir de un gesto del usuario, como el botón que inicia la conversación, o los navegadores lo mantendrán suspendido (guía de reproducción automática de MDN).
getByteFrequencyData() también funciona si quieres ponderar las frecuencias de la voz: devuelve frequencyBinCount valores de 0 a 255, distribuidos linealmente desde 0 Hz hasta la mitad de la frecuencia de muestreo (MDN). Para una compuerta de encendido y apagado, el RMS es más simple y más que suficiente.
¿Cómo convierto el nivel en habla o reposo (idle) sin parpadeos?
El habla está llena de pausas cortas entre sílabas y palabras. Un solo umbral hace que el avatar cambie entre habla y reposo varias veces por segundo, y parece que algo falla. Usa histéresis: un umbral más alto para empezar a hablar, uno más bajo para detenerse y un tiempo de espera antes de detenerse de verdad.
const ON = 0.04; // start talking above this RMS
const OFF = 0.02; // consider silence below this
const HOLD_MS = 300; // silence must last this long before stopping
let talking = false;
let quietSince = 0;
let level = 0;
function tick(now: number) {
level = level * 0.8 + rms() * 0.2; // light smoothing
if (!talking && level > ON) {
talking = true;
setMood(MOOD.talking);
} else if (talking) {
if (level > OFF) quietSince = 0;
else if (!quietSince) quietSince = now;
else if (now - quietSince > HOLD_MS) {
talking = false;
quietSince = 0;
setMood(MOOD.listening); // or MOOD.idle
}
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
Los umbrales y el tiempo de espera son valores iniciales que debes ajustar según tu voz y tu volumen reales, no estándares. Registra level durante algunas frases y fija ON justo por encima del ruido de fondo.
Llama a setMood solo cuando cambie el estado, como arriba. Escribir el mismo valor en cada fotograma es trabajo desperdiciado. Con un view model de Rive, setMood puede ser tan simple como vmi.number("mood").value = m.
Si tu personaje sí expone un número continuo de apertura de boca, puedes pasarle level escalado a su rango en cada fotograma. Eso depende del archivo concreto; muchos sets de mascotas de marca usan un loop de habla en su lugar.
¿Qué son los visemas y qué proveedores de TTS los envían?
Un visema es el equivalente visual de un fonema: la posición de la boca para un grupo de sonidos que se ven iguales. Por ejemplo, "p", "b" y "m" cierran los labios, así que comparten visema. Los motores de texto a voz que saben qué fonemas producen pueden informar los visemas con marcas de tiempo.
Azure AI Speech ofrece 22 visemas, numerados del 0 (silencio) al 21 (p, b, m). El Speech SDK emite un evento VisemeReceived por cada uno, con un offset de audio en ticks de 100 nanosegundos. Azure también puede devolver fotogramas de blend shapes a 60 cuadros por segundo, cada uno con 55 posiciones faciales, para caras 3D, e imágenes SVG de la boca, que solo se admiten para la configuración regional en-US (Microsoft Learn).
Amazon Polly devuelve speech marks: metadatos que describen el habla y que se solicitan en lugar del audio. Llegan como un stream JSON delimitado por líneas en el que cada objeto tiene un time en milisegundos desde el inicio del audio, un type (sentence, word, viseme o ssml) y un value, que en los visemas es el nombre del visema (Amazon Polly). Polly documenta las speech marks para sus motores neural, long-form y standard (Amazon Polly).
Los modelos de voz a voz en tiempo real, como la OpenAI Realtime API, transmiten audio sin temporización por fonema en los eventos que cubre nuestra guía de Realtime, y esa es una de las razones por las que la activación por amplitud es la opción habitual para agentes de voz.
¿En qué se diferencian los ID de visemas de Azure y las speech marks de Polly?
Las principales diferencias están en la entrega y en las unidades. Azure envía eventos de visemas durante la síntesis; Polly te da una respuesta de metadatos aparte que obtienes junto con el audio.
Con el Azure Speech SDK para JavaScript, suscríbete a visemeReceived antes de sintetizar y convierte el offset a milisegundos:
import * as sdk from "microsoft-cognitiveservices-speech-sdk";
const synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig);
const visemes: { id: number; ms: number }[] = [];
synthesizer.visemeReceived = (_sender, e) => {
visemes.push({ id: e.visemeId, ms: e.audioOffset / 10000 }); // 100-ns ticks -> ms
};
synthesizer.speakTextAsync(text, () => synthesizer.close(), (err) => {
console.error(err);
synthesizer.close();
});
Con Polly, haz una solicitud con OutputFormat: "json" y SpeechMarkTypes: ["viseme"], y una segunda solicitud para el audio en sí con el mismo texto y la misma voz. Analiza las marcas dividiendo por saltos de línea y llamando a JSON.parse en cada línea.
Los conjuntos de ID también son distintos. Azure usa números del 0 al 21; Polly usa nombres cortos de visemas. Cada uno necesita su propia tabla de correspondencia con las posiciones de boca de tu personaje, y ambos dan por hecho que esas posiciones existen. Un personaje que solo tiene un loop de habla no gana nada con ninguno de los dos.
¿Cómo sincronizo las marcas de tiempo de los visemas con la reproducción del audio?
Los tiempos de los visemas son relativos al inicio del audio, así que prográmalos según el reloj de reproducción, no según el momento en que llegaron los datos. Leer audio.currentTime en cada fotograma de animación y aplicar todas las marcas pendientes mantiene la boca sincronizada aunque la reproducción se detenga o la pestaña pierda fotogramas:
type Mark = { time: number; value: string }; // Polly: ms from audio start
function playWithVisemes(audio: HTMLAudioElement, marks: Mark[], show: (v: string) => void) {
let i = 0;
const tick = () => {
const now = audio.currentTime * 1000;
while (i < marks.length && marks[i].time <= now) show(marks[i++].value);
if (!audio.paused && !audio.ended) requestAnimationFrame(tick);
else show("sil");
};
audio.addEventListener("play", () => requestAnimationFrame(tick), { once: true });
audio.play();
}
El mismo loop funciona con Azure si guardas ms en lugar de time. Si el usuario salta a otro punto del audio o reinicias la reproducción, restablece i buscando la primera marca posterior a la nueva posición.
Suaviza el resultado. Saltar directamente de una posición de boca a otra en cada marca se ve mecánico; una transición breve entre posiciones en el archivo de animación, o ignorar las marcas más cortas que un fotograma, se ve mucho mejor. La misma idea aplica a la amplitud: el factor de suavizado y el tiempo de retención de la compuerta anterior existen para que el movimiento sea tranquilo.
Preguntas frecuentes
¿Basta con la amplitud para animar un avatar que habla?
Para mascotas de marca 2D estilizadas, normalmente sí. Activar un loop de habla según el nivel de audio de un AnalyserNode se percibe como habla sin necesidad de datos de fonemas y funciona con cualquier fuente de voz, incluidos los agentes en tiempo real. Un lip-sync real necesita la temporización de visemas del proveedor de TTS y una forma de boca para cada visema.
¿Debo usar getByteTimeDomainData o getByteFrequencyData?
Para una compuerta de habla de encendido y apagado, getByteTimeDomainData es lo más sencillo: convierte las muestras en torno a 128 al rango −1…1 y calcula el RMS. getByteFrequencyData sirve cuando quieres ponderar las frecuencias del habla o dibujar un visualizador. En cualquier caso, suaviza el valor y usa histéresis para que el avatar no parpadee.
¿Cuántos visemas ofrece Azure Speech?
Azure Speech ofrece 22 ID de visemas, desde el 0 para el silencio hasta el 21 para p, b y m. El SDK de JavaScript los entrega mediante visemeReceived con audioOffset en ticks de 100 nanosegundos, así que divide entre 10,000 para obtener milisegundos. Azure también puede devolver fotogramas de blend shapes de 55 valores a 60 cuadros por segundo.
¿Puedo hacer lip-sync con un personaje que solo tiene un loop de habla?
No de forma útil. Los datos de visemas indican qué forma de boca mostrar, así que requieren un personaje dibujado con esas formas. Si solo tienes un loop de habla, el mejor resultado se obtiene activándolo por amplitud: reproduce el loop mientras se oye la voz y vuelve al loop de escucha o de reposo (idle) en las pausas.
Respuestas rápidas de esta guía
Fuentes
- AnalyserNode, MDN Web Docs
- AnalyserNode: getByteTimeDomainData() method, MDN Web Docs
- AnalyserNode: getByteFrequencyData() method, MDN Web Docs
- Autoplay guide for media and Web Audio APIs, MDN Web Docs
- Get facial position with viseme, Microsoft Learn
- Speech mark output, Amazon Polly Developer Guide
- Using speech marks, Amazon Polly Developer Guide