Pular para o conteúdo
charmotion

Lip-sync vs amplitude para um avatar falante

Por equipe Charmotion · Atualizado em

Ilustração de capa do guia de avatares de assistente com IA: lip-sync vs amplitude para um avatar falante, com personagens de demonstração do Charmotion

Para a maioria dos avatares estilizados, a amplitude basta: meça o nível da voz com um AnalyserNode da Web Audio, calcule o RMS e ligue e desligue um loop talking com histerese. Um lip-sync de verdade exige o timing de visemas do seu provedor de fala, como os eventos de visema do Azure Speech ou as speech marks do Amazon Polly, além de um personagem com um formato de boca para cada visema.

Principais pontos

  • O controle por amplitude responde "a voz está tocando agora?". Os visemas respondem "qual formato de boca combina com este som?".
  • Use getByteTimeDomainData, converta para RMS e use dois limiares mais um tempo de espera para o estado não ficar piscando.
  • O Azure Speech envia 22 IDs de visema com offsets em ticks de 100 nanossegundos; o Polly retorna speech marks com tempos em milissegundos.
  • Dados de visema não servem para nada sem formatos de boca desenhados para cada visema, então confira o seu personagem antes de escolher uma abordagem.

Já tem um personagem ou logo? Veja seu personagem em telas reais de app: telas de splash, onboarding, carregamento e estado vazio. É grátis, roda no navegador e não envia nada.

Preciso de lip-sync de verdade ou a amplitude basta?

Depende do rosto. Lip-sync significa sincronizar a boca com os sons falados. Isso exige duas coisas: dados de timing que indicam qual som acontece em cada momento e um personagem desenhado com uma pose de boca distinta para cada grupo de sons (um visema). A animação de fala guiada por amplitude não precisa de nenhuma das duas. Ela só verifica se a voz está audível no momento e toca um loop talking enquanto estiver.

Controle por amplitudeVisemas do AzureSpeech marks do Polly
O que você recebeUm valor de volume ao vivo22 IDs de visema, blend shapes opcionaisNomes de visema com tempos
Unidade de tempoPor frame de animaçãoTicks de 100 ns (divida por 10.000 para obter ms)Milissegundos desde o início do áudio
Funciona com qualquer fonte de vozSim, inclusive agentes em tempo realSó TTS do AzureSó TTS do Polly
O personagem precisa deUm loop talkingUma pose de boca por visemaUma pose de boca por visema
EsforçoBaixoAltoAlto

Fontes: Microsoft Learn, documentação do Amazon Polly.

Personagens pequenos e estilizados, como um selo de 64 px ao lado de um chat ou um mascote com boca simples, costumam parecer que estão "falando" só pelo ritmo. Os visemas começam a compensar em rostos maiores e mais humanos, onde uma boca fora de sincronia chama atenção.

Seja honesto sobre o que o seu personagem suporta. Os conjuntos do Charmotion, por exemplo, incluem um loop talking (mood 7), mas nenhum input de formato de boca ou de visema, então eles combinam com controle por amplitude, não com lip-sync por fonemas. Essa é uma escolha intencional para agentes de chat e de voz em que as palavras também aparecem como texto.

Como medir o nível da fala com um AnalyserNode?

O AnalyserNode da Web Audio API expõe dados em tempo real no domínio do tempo e da frequência sem alterar o áudio que passa por ele (MDN). Para um controle de fala, a forma de onda no domínio do tempo é a entrada mais simples. getByteTimeDomainData() preenche um Uint8Array com amostras de 0 a 255, em que 128 é silêncio (MDN). Converta para −1…1 e calcule a raiz do valor quadrático médio (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);
}

Uma fonte de elemento de mídia redireciona o áudio do elemento pelo grafo, então conecte o analisador em seguida ao destino, senão a voz fica muda. Uma fonte de stream vinda de uma faixa WebRTC apenas capta o áudio; deixe que o elemento <audio> que reproduz a faixa cuide da saída.

Crie ou retome o AudioContext a partir de um gesto do usuário, como o botão que inicia a conversa, senão os navegadores vão mantê-lo suspenso (guia de reprodução automática do MDN).

getByteFrequencyData() também funciona se você quiser dar peso às frequências da fala: ele retorna frequencyBinCount valores de 0 a 255, distribuídos linearmente de 0 Hz até metade da taxa de amostragem (MDN). Para um controle liga/desliga, o RMS é mais simples e mais que suficiente.

Como transformar o nível em talking ou idle sem o estado piscar?

A fala é cheia de pequenas pausas entre sílabas e palavras. Com um único limiar, o avatar alterna entre talking e idle várias vezes por segundo, o que parece defeito. Use histerese: um limiar mais alto para começar a falar, um mais baixo para parar e um tempo de espera antes de parar de fato.

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);

Os limiares e o tempo de espera são valores iniciais para ajustar de acordo com a sua voz e o seu volume reais, não padrões. Registre o level durante algumas frases e defina ON logo acima do ruído de fundo.

Só chame setMood quando o estado mudar, como no exemplo acima. Gravar o mesmo valor a cada frame é trabalho desperdiçado. Com um view model do Rive, setMood pode ser tão simples quanto vmi.number("mood").value = m.

Se o seu personagem expuser um número contínuo de abertura da boca, você pode alimentá-lo com o level ajustado à faixa dele a cada frame. Isso depende do arquivo específico; muitos conjuntos de mascote usam um loop talking no lugar.

O que são visemas e quais provedores de TTS os enviam?

Um visema é o equivalente visual de um fonema: a posição da boca para um grupo de sons que parecem iguais visualmente. "P", "b" e "m", por exemplo, fecham os lábios, então compartilham um visema. Mecanismos de conversão de texto em fala que sabem quais fonemas estão produzindo podem informar visemas com marcações de tempo.

O Azure AI Speech oferece 22 visemas, numerados de 0 (silêncio) a 21 (p, b, m). O Speech SDK dispara um evento VisemeReceived para cada um, com um offset de áudio em ticks de 100 nanossegundos. O Azure também pode retornar frames de blend shapes a 60 fps, cada um com 55 posições faciais, para rostos 3D, e imagens SVG de boca, que são suportadas apenas para a localidade en-US (Microsoft Learn).

O Amazon Polly retorna speech marks: metadados que descrevem a fala, solicitados no lugar do áudio. Eles chegam como um stream JSON delimitado por linhas, em que cada objeto tem um time em milissegundos desde o início do áudio, um type (sentence, word, viseme ou ssml) e um value, que no caso dos visemas é o nome do visema (Amazon Polly). O Polly documenta speech marks para os mecanismos neural, long-form e standard (Amazon Polly).

Modelos de fala para fala em tempo real, como a OpenAI Realtime API, transmitem áudio sem timing por fonema nos eventos abordados no nosso guia do Realtime, e esse é um dos motivos pelos quais o controle por amplitude é a escolha comum para agentes de voz.

Qual é a diferença entre os IDs de visema do Azure e as speech marks do Polly?

As principais diferenças estão na entrega e nas unidades. O Azure envia eventos de visema durante a síntese; o Polly entrega uma resposta de metadados separada, que você busca junto com o áudio.

Com o Azure Speech SDK para JavaScript, assine visemeReceived antes de sintetizar e converta o offset para milissegundos:

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();
});

Com o Polly, faça uma requisição com OutputFormat: "json" e SpeechMarkTypes: ["viseme"], e uma segunda requisição para o próprio áudio, com o mesmo texto e a mesma voz. Interprete as marks dividindo o conteúdo por quebras de linha e chamando JSON.parse em cada linha.

Os conjuntos de IDs também são diferentes. O Azure usa números de 0 a 21; o Polly usa nomes curtos de visemas. Cada um precisa da própria tabela de correspondência com as poses de boca do seu personagem, e ambos pressupõem que essas poses existam. Um personagem que só tem um loop de fala não ganha nada com nenhum dos dois.

Como sincronizo os timestamps dos visemas com a reprodução do áudio?

Os tempos dos visemas são relativos ao início do áudio, então agende-os pelo relógio da reprodução, e não pelo momento em que os dados chegaram. Ler audio.currentTime a cada frame de animação e aplicar todas as marcas que já venceram mantém a boca no ritmo, mesmo que a reprodução trave ou a aba perca frames:

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();
}

O mesmo loop funciona para o Azure se você armazenar ms em vez de time. Se o usuário pular para outro ponto ou você reiniciar a reprodução, redefina i buscando a primeira marca após a nova posição.

Suavize o resultado. Pular direto de uma pose de boca para outra a cada marca parece mecânico; uma transição curta entre as poses no arquivo de animação, ou ignorar marcas mais curtas que um frame, fica muito melhor. A mesma ideia vale para a amplitude: o fator de suavização e o tempo de retenção no gate acima existem para manter o movimento calmo.

Perguntas frequentes

A amplitude basta para animar um avatar falante?

Para mascotes 2D estilizados, geralmente sim. Acionar um loop de fala com base no nível de áudio de um AnalyserNode passa a impressão de fala sem dados de fonemas e funciona com qualquer fonte de voz, incluindo agentes em tempo real. Um lip-sync de verdade exige o timing de visemas do provedor de TTS e uma forma de boca para cada visema.

Devo usar getByteTimeDomainData ou getByteFrequencyData?

Para um gate de fala liga/desliga, getByteTimeDomainData é o mais simples: converta as amostras em torno de 128 para o intervalo −1…1 e calcule o RMS. getByteFrequencyData é útil quando você quer dar peso às frequências da fala ou desenhar um visualizador. Em qualquer caso, suavize o valor e use histerese para o avatar não piscar.

Quantos visemas o Azure Speech oferece?

O Azure Speech oferece 22 IDs de visemas, de 0 para silêncio até 21 para p, b e m. O SDK de JavaScript os entrega pelo visemeReceived com audioOffset em ticks de 100 nanossegundos, então divida por 10.000 para obter milissegundos. O Azure também pode retornar frames de blend shapes com 55 valores a 60 fps.

Dá para fazer lip-sync de um personagem que só tem um loop de fala?

Não de forma significativa. Os dados de visemas dizem qual forma de boca mostrar, então exigem um personagem desenhado com essas formas. Com apenas um loop de fala, o melhor resultado vem do gate por amplitude: toque o loop enquanto a voz estiver audível e volte para listening ou idle nas pausas.

Respostas rápidas deste guia

Fontes

Guias relacionados