Pular para o conteúdo
charmotion

Avatar de agente ElevenLabs com onModeChange

Por equipe Charmotion · Atualizado em

Ilustração de capa do guia de avatares para assistentes de IA: avatar de agente ElevenLabs com onModeChange, com personagens de demonstração do Charmotion

No React SDK da ElevenLabs, useConversation informa o mode do agente como 'speaking' ou 'listening' e chama onModeChange quando ele muda. Associe speaking a um loop de fala e listening a um loop de escuta, mostre um estado de carregamento enquanto status for connecting e toque as reações de entrada e saída ao conectar e desconectar.

Principais pontos

  • mode é 'speaking' ou 'listening'; isSpeaking e isListening são atalhos booleanos.
  • O status da conexão passa por disconnected, connecting e connected.
  • Atualize o avatar a partir dos callbacks por meio de uma ref, e não do state do React, para que o streaming de áudio não cause renderizações extras.
  • getOutputByteFrequencyData() e getInputVolume() existem para visuais que reagem ao áudio.

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.

Quais eventos da ElevenLabs indicam que o agente está falando?

O React SDK dos Agents da ElevenLabs gira em torno do hook useConversation. A documentação lista os campos e callbacks de que um avatar precisa (React SDK da ElevenLabs):

Campo ou callbackValoresUso no avatar
mode'speaking' | 'listening'Loop de fala vs loop de escuta
isSpeaking, isListeningBooleanosO mesmo, como flags
onModeChangeChamado quando o mode mudaDefinir o humor de forma imperativa
statusdisconnected | connecting | connectedCarregamento durante a conexão
onConnect, onDisconnectInício e fim da sessãoReações de entrada e saída
onStatusChangeChamado quando o status mudaAlternativa aos dois acima
onErrorDetalhes do erroReação de erro
getInputVolume(), getOutputByteFrequencyData()Dados de áudioVisuais que reagem ao áudio

Ao contrário da OpenAI Realtime API, você não precisa deduzir a fase de fala a partir de eventos brutos de áudio. O SDK gerencia a reprodução e avisa quando o agente alterna entre falar e ouvir.

Não há um modo "thinking" separado nessa lista. O agente está falando ou ouvindo; o intervalo depois que o usuário termina faz parte do listening. Se você quiser um estado thinking visível, veja a seção sobre estados de conexão abaixo para uma forma de adicioná-lo sem precisar adivinhar.

Como associo mode e status aos humores do avatar?

Defina o humor diretamente dentro dos callbacks. Acesse o view model do Rive por uma ref para que os callbacks sempre vejam a instância atual, mesmo tendo sido definidos na primeira renderização. Este é um client component, então no Next.js ele começa com "use client".

"use client";

import { useRef } from "react";
import { useConversation } from "@elevenlabs/react";
import { useRive } from "@rive-app/react-webgl2";
import type { ViewModelInstance } from "@rive-app/react-webgl2";

const MOOD = { idle: 0, sad: 4, loading: 5, listening: 6, talking: 7 } as const;

export function AgentAvatar() {
  const { rive, RiveComponent } = useRive({
    src: "/character.riv",
    stateMachine: "CharacterViewModelMachine",
    autoplay: true,
    autoBind: true,
  });
  const vmi = useRef<ViewModelInstance | null>(null);
  vmi.current = rive?.viewModelInstance ?? null;

  const setMood = (m: number) => {
    const p = vmi.current?.number("mood");
    if (p) p.value = m;
  };
  const fire = (name: string) => vmi.current?.trigger(name)?.trigger();

  const conversation = useConversation({
    onConnect: () => {
      fire("enter");
      setMood(MOOD.listening);
    },
    onDisconnect: () => {
      fire("exit");
      setMood(MOOD.idle);
    },
    onModeChange: ({ mode }) => setMood(mode === "speaking" ? MOOD.talking : MOOD.listening),
    onError: () => {
      fire("error");
      setMood(MOOD.sad);
    },
  });

  const start = async () => {
    setMood(MOOD.loading);
    await navigator.mediaDevices.getUserMedia({ audio: true });
    await conversation.startSession({ agentId: "your-agent-id" });
  };

  return (
    <div>
      <div aria-hidden="true" style={{ width: 160, height: 160 }}>
        <RiveComponent />
      </div>
      <button onClick={start} disabled={conversation.status !== "disconnected"}>Talk</button>
      <button onClick={() => conversation.endSession()}>End</button>
      <p role="status">{conversation.status === "connected" ? (conversation.isSpeaking ? "Speaking" : "Listening") : conversation.status}</p>
    </div>
  );
}

Confira na referência do SDK as opções exatas de startSession que a sua versão exige (ID do agente ou URL assinada, tipo de conexão). A lógica de associação não muda por causa delas.

Posso controlar o movimento da boca pelo áudio de saída do agente?

O SDK expõe getOutputByteFrequencyData() para a voz do agente e getInputVolume() para o microfone, e a documentação os apresenta como a forma de criar visualizações de áudio (React SDK da ElevenLabs). Os dados de frequência seguem o mesmo formato de um AnalyserNode da Web Audio: valores em bytes de 0 a 255 por faixa de frequência (MDN).

O que você consegue controlar com isso depende do personagem:

  • Um número de abertura da boca no arquivo: calcule a média das faixas a cada frame de animação, suavize o valor e grave-o nesse número.
  • Apenas um loop de fala: você nem precisa dos dados de áudio, porque onModeChange já diz quando falar. Use os dados para um anel visualizador separado se quiser movimento extra.
  • Visemas: o espectro de frequências não contém a identidade dos fonemas, então não consegue produzir um lip-sync preciso. Veja lip-sync vs amplitude para saber quando os visemas valem a pena.
// Optional: a 0–1 level from the agent's voice, sampled once per frame.
function outputLevel(): number {
  const bins = conversation.getOutputByteFrequencyData();
  if (!bins?.length) return 0;
  let sum = 0;
  for (let i = 0; i < bins.length; i++) sum += bins[i];
  return sum / bins.length / 255;
}

Rode isso dentro de requestAnimationFrame, e não no state do React. Chamar setState 60 vezes por segundo renderiza de novo a árvore de componentes sem nenhum ganho visual.

Como mostro os estados de conexão e desconexão?

Sessões de voz levam um momento para começar: o navegador pede permissão para o microfone e depois o SDK abre a conexão. Durante esse tempo, status é connecting. Mostre isso, ou o usuário vai tocar no botão de novo.

  • Antes de startSession: defina um humor de carregamento. O pedido de microfone pode levar segundos se o usuário parar para ler.
  • onConnect: toque uma reação de entrada (ou mude rapidamente para o humor wave) e depois fique em listening, já que o agente está pronto para receber a fala do usuário.
  • onDisconnect: toque uma reação de saída e fique em idle ou sleeping.
  • Permissão negada: getUserMedia é rejeitado. Capture o erro, volte para idle e explique em texto como liberar o microfone.

Os arquivos do Charmotion incluem exatamente estas peças: um humor loading (5), um humor listening (6), um humor talking (7), sleeping (8) e os triggers enter e exit no view model Character. Uma mudança de humor nunca corta uma reação única, então disparar enter e definir listening no mesmo callback toca a entrada completa antes de o loop começar. O Charmotion gera o conjunto a partir do seu próprio personagem ou logo; o .riv vem com todo plano pago (a partir de US$ 20/mês).

Para um momento de reflexão, você pode usar os próprios eventos do agente em vez de adivinhar: se o seu agente chama client tools, defina thinking quando uma chamada de ferramenta começar e volte ao estado controlado pelo mode quando a fala recomeçar.

listening: mood 6, enquanto o usuário fala ou digita.

Qual hook granular evita renderizações extras?

useConversation retorna muito state, e um componente que o lê é renderizado de novo sempre que qualquer parte dele muda. A ElevenLabs documenta hooks granulares para isso, incluindo useConversationMode() só para o estado speaking/listening, e os recomenda para reduzir renderizações (React SDK da ElevenLabs). Confira na referência o conjunto exato e qualquer provider que eles exijam na sua versão do SDK.

No caso específico do avatar, o padrão mais barato é o do código acima: controlar o Rive a partir de callbacks, e não de valores renderizados. Callbacks como onModeChange rodam fora do ciclo de renderização do React, e escrever em uma propriedade do view model não causa nenhuma nova renderização. O canvas do Rive se atualiza sozinho.

Isso também protege o canvas. O Rive avisa que remontar o canvas pode reiniciar a animação ou deixá-la em branco (boas práticas do Rive para React). Mantenha useRive e RiveComponent em um único componente que permaneça montado durante toda a sessão, e deixe apenas o pequeno texto de status ser renderizado novamente.

Perguntas frequentes

Como detectar se o agente está falando ou ouvindo com o SDK React da ElevenLabs?

O useConversation expõe mode, que vale 'speaking' ou 'listening', além dos booleanos isSpeaking e isListening e de um callback onModeChange. Use o onModeChange para definir o mood do avatar diretamente: talking enquanto o agente fala, listening no restante do tempo. O status da conexão é informado separadamente como disconnected, connecting ou connected.

A ElevenLabs envia um estado thinking?

O mode do SDK React tem dois valores, speaking e listening, então não existe um modo thinking nativo. A pausa depois que o usuário para de falar entra em listening. Se o seu agente usa client tools, você pode mostrar thinking enquanto uma ferramenta roda e voltar aos estados controlados por mode depois.

Dá para sincronizar os lábios de um avatar com um agente da ElevenLabs?

Não com precisão usando apenas os helpers de áudio do SDK React. O getOutputByteFrequencyData() fornece a intensidade por frequência, o que permite abrir mais a boca em sons mais altos, mas não identifica fonemas. Para um personagem com um loop talking, o onModeChange é tudo de que você precisa para iniciar e parar o talking.

Respostas rápidas deste guia

Fontes

Guias relacionados