Ir al contenido
charmotion

Avatar de agente de ElevenLabs con onModeChange

Por el equipo de Charmotion · Actualizado el

Ilustración de portada de la guía de avatares para asistentes de IA: avatar de agente de ElevenLabs con onModeChange, con personajes de demostración de Charmotion

En el SDK de React de ElevenLabs, useConversation informa el mode del agente como 'speaking' o 'listening' y llama a onModeChange cuando cambia. Asigna speaking a un loop de habla (talking) y listening a un loop de escucha (listening), muestra un estado de carga mientras status sea connecting y reproduce las reacciones de entrada y salida al conectar y desconectar.

Puntos clave

  • mode es 'speaking' o 'listening'; isSpeaking e isListening son atajos booleanos.
  • El status de la conexión pasa por disconnected, connecting y connected.
  • Actualiza el avatar desde los callbacks mediante una ref, no desde el estado de React, para que el audio en streaming no provoque renders adicionales.
  • getOutputByteFrequencyData() y getInputVolume() están pensados para visuales reactivos al audio.

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

¿Qué eventos de ElevenLabs me indican que el agente está hablando?

El SDK de React de ElevenLabs Agents gira en torno al hook useConversation. Su documentación enumera los campos y callbacks que necesita un avatar (SDK de React de ElevenLabs):

Campo o callbackValoresUso en el avatar
mode'speaking' | 'listening'Loop de habla vs loop de escucha
isSpeaking, isListeningBooleanosLo mismo, como indicadores
onModeChangeSe llama al cambiar el modoDefinir el estado de ánimo de forma imperativa
statusdisconnected | connecting | connectedCarga mientras se conecta
onConnect, onDisconnectInicio y fin de la sesiónReacciones de entrada y salida
onStatusChangeSe llama al cambiar el estadoAlternativa a los dos anteriores
onErrorDetalles del errorReacción de error
getInputVolume(), getOutputByteFrequencyData()Datos de audioVisuales reactivos al audio

A diferencia de la OpenAI Realtime API, no tienes que deducir la fase de habla a partir de eventos de audio sin procesar. El SDK gestiona la reproducción y te avisa cuando el agente pasa de hablar a escuchar y viceversa.

En esa lista no hay un modo "thinking" aparte. El agente está hablando o escuchando; la pausa después de que el usuario termina forma parte de la escucha. Si quieres un estado thinking visible, consulta más abajo la sección sobre los estados de conexión para ver una forma de agregarlo sin adivinar.

¿Cómo asigno mode y status a estados de ánimo del avatar?

Define el estado de ánimo directamente dentro de los callbacks. Accede al view model de Rive mediante una ref para que los callbacks siempre vean la instancia actual, aunque se hayan definido en el primer render. Es un componente de cliente, así que en Next.js empieza con "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>
  );
}

Consulta la referencia del SDK para ver las opciones exactas de startSession que necesita tu versión (ID del agente o URL firmada, tipo de conexión). La lógica de asignación no cambia con ellas.

¿Puedo controlar el movimiento de la boca con el audio de salida del agente?

El SDK expone getOutputByteFrequencyData() para la voz del agente y getInputVolume() para el micrófono, y su documentación los presenta como la forma de crear visualizaciones de audio (SDK de React de ElevenLabs). Los datos de frecuencia tienen el mismo formato que los de un AnalyserNode de Web Audio: valores de byte de 0 a 255 por cada bin de frecuencia (MDN).

Lo que puedes controlar con ellos depende del personaje:

  • Un número de apertura de boca en el archivo: promedia los bins en cada fotograma de animación, suaviza el valor y escríbelo en ese número.
  • Solo un loop de habla: no necesitas los datos de audio, porque onModeChange ya te indica cuándo hablar. Usa los datos para un anillo visualizador aparte si quieres más movimiento.
  • Visemas: el espectro de frecuencias no contiene la identidad de los fonemas, así que no puede producir un lip-sync preciso. Consulta lip-sync vs amplitud para saber cuándo valen la pena los visemas.
// 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;
}

Ejecuta esto dentro de requestAnimationFrame, no en el estado de React. Llamar a setState 60 veces por segundo vuelve a renderizar tu árbol de componentes sin ningún beneficio visual.

¿Cómo muestro los estados de conexión y desconexión?

Las sesiones de voz tardan un momento en iniciarse: el navegador pide permiso para usar el micrófono y luego el SDK abre la conexión. Durante ese tiempo, status es connecting. Muéstralo, o el usuario volverá a tocar el botón.

  • Antes de startSession: define un estado de ánimo de carga. El aviso del micrófono puede tardar varios segundos si el usuario lo lee.
  • onConnect: reproduce una reacción de entrada (o cambia brevemente al estado de ánimo wave) y luego quédate en listening, ya que el agente está listo para escuchar.
  • onDisconnect: reproduce una reacción de salida y quédate en idle o sleeping.
  • Permiso denegado: getUserMedia se rechaza. Captura el error, vuelve a idle y explica en texto cómo permitir el micrófono.

Los archivos de Charmotion incluyen exactamente estas piezas: un mood loading (5), un mood listening (6), un mood talking (7), sleeping (8) y los triggers enter y exit en el view model Character. Un cambio de mood nunca interrumpe una reacción puntual, así que disparar enter y asignar listening en el mismo callback reproduce la entrada completa antes de que empiece el loop. Charmotion genera el set a partir de tu propio personaje o logo; el .riv viene con todos los planes de pago (desde $20/mes).

Para un momento de reflexión, puedes usar los propios eventos del agente en lugar de adivinar: si tu agente llama a herramientas del cliente, define thinking cuando empiece una llamada a una herramienta y vuelve al estado que marca el modo cuando se reanude el habla.

listening: mood 6, mientras el usuario habla o escribe.

¿Qué hook granular evita renders adicionales?

useConversation devuelve mucho estado, y un componente que lo lee se vuelve a renderizar cada vez que cambia cualquier parte. ElevenLabs documenta hooks granulares para esto, como useConversationMode() para obtener solo el estado speaking/listening, y los recomienda para reducir los re-renders (SDK de React de ElevenLabs). Consulta la referencia para ver el conjunto exacto y cualquier provider que requieran en tu versión del SDK.

Para el avatar en concreto, el patrón más económico es el del código anterior: controla Rive desde callbacks, no desde valores renderizados. Los callbacks como onModeChange se ejecutan fuera del ciclo de renderizado de React, y escribir en una propiedad del view model no vuelve a renderizar nada. El canvas de Rive se actualiza por su cuenta.

Eso también protege el canvas. Rive advierte que volver a montar el canvas puede reiniciar la animación o dejarla en blanco (buenas prácticas de Rive para React). Mantén useRive y RiveComponent en un solo componente que permanezca montado durante toda la sesión, y deja que solo el pequeño texto de estado se vuelva a renderizar.

Preguntas frecuentes

¿Cómo detecto si habla o escucha con el SDK de React de ElevenLabs?

useConversation expone mode, que vale 'speaking' o 'listening', además de los booleanos isSpeaking e isListening y un callback onModeChange. Usa onModeChange para fijar directamente el estado de ánimo del avatar: talking mientras el agente habla, listening en cualquier otro caso. El estado de la conexión se informa por separado como disconnected, connecting o connected.

¿ElevenLabs envía un estado thinking?

El mode del SDK de React tiene dos valores, speaking y listening, así que no hay un modo thinking integrado. La pausa después de que el usuario deja de hablar cuenta como listening. Si tu agente usa client tools, puedes mostrar thinking mientras se ejecuta una herramienta y volver después a los estados que dicta mode.

¿Puedo sincronizar los labios de un avatar con un agente de ElevenLabs?

No con precisión usando solo las utilidades de audio del SDK de React. getOutputByteFrequencyData() da el volumen por frecuencia, lo que permite abrir más la boca con los sonidos más fuertes, pero no identifica fonemas. Para un personaje con un loop de talking, onModeChange es todo lo que necesitas para iniciar y detener talking.

Respuestas rápidas de esta guía

Fuentes

Guías relacionadas