Ir al contenido
charmotion

Avatar de agente de voz: mapeo de eventos de OpenAI Realtime

Por el equipo de Charmotion · Actualizado el

Ilustración de portada de la guía de avatares para asistentes de IA: Avatar para agente de voz: mapeo de eventos de OpenAI Realtime, con personajes de demostración de Charmotion

Controla el avatar de un agente de voz con los eventos del servidor de OpenAI Realtime: input_audio_buffer.speech_started pasa a listening, speech_stopped a thinking, el primer audio de una respuesta a talking y el fin de la reproducción a idle. Con WebRTC, output_audio_buffer.started y stopped marcan la reproducción; con WebSocket, lleva el control de tu propia cola de reproducción.

Puntos clave

  • input_audio_buffer.speech_started y speech_stopped provienen del VAD del servidor y se asignan a listening y thinking.
  • El nombre actual del evento de audio es response.output_audio.delta; los tutoriales más antiguos usan response.audio.delta.
  • output_audio_buffer.started, stopped y cleared están documentados solo para WebRTC/SIP.
  • Trata como algo normal una respuesta cancelada después de que el usuario interrumpa (barge-in), y ten un plan alternativo por si el evento stopped llega tarde.

¿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 Realtime me indican que el usuario empezó o dejó de hablar?

Con la detección de actividad de voz (VAD) del servidor activada, la API Realtime envía input_audio_buffer.speech_started cuando detecta voz en el búfer de audio. La referencia de OpenAI indica que el cliente puede usar este evento para interrumpir la reproducción de audio u ofrecer feedback visual, que es justo para lo que sirve un avatar. Después envía input_audio_buffer.speech_stopped cuando termina la voz (eventos del servidor de OpenAI).

Avatar de mascota en tres estados de agente de voz con barras de nivel de audio: escucha mientras el usuario habla, piensa mientras espera y habla mientras suena el audio de salida
Asigna el audio del agente de voz a los estados de ánimo del avatar.

Asígnalos así:

  • speech_started → listening. Cambia de inmediato. Es el momento de "te escucho", y cualquier retraso da la impresión de que el agente no está prestando atención.
  • speech_stopped → thinking. El modelo está a punto de responder. Este intervalo suele ser breve, pero se nota.

Estos eventos dependen del VAD. Si desactivas el VAD y confirmas tú mismo el búfer de audio (push-to-talk), ya sabes cuándo el usuario empieza y deja de hablar, porque lo hacen tus propios handlers del botón. Activa listening al presionar y thinking al soltar.

En hilos de la comunidad se informa que speech_stopped a veces llega tarde o no llega en entornos ruidosos. No dejes que el avatar se quede atascado: si una respuesta empieza mientras sigues en listening, pasa directamente a talking.

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

¿Cómo sé cuándo el modelo empieza y deja de hablar?

Aquí importa el transporte. La API Realtime puede funcionar sobre WebRTC (audio en una pista de medios, eventos en un canal de datos) o sobre WebSocket (audio y eventos como mensajes JSON).

FaseWebRTCWebSocketAvatar
El usuario empieza a hablarinput_audio_buffer.speech_startedIgualListening (6)
El usuario deja de hablarinput_audio_buffer.speech_stoppedIgualThinking (3)
Empieza el audio del modelooutput_audio_buffer.startedPrimer response.output_audio.deltaTalking (7)
Termina el audio del modelooutput_audio_buffer.stoppedSe vacía tu cola de reproducciónIdle (0) o listening (6)
El usuario interrumpeoutput_audio_buffer.clearedspeech_started + detén tu reproducciónListening (6)
La respuesta fallóresponse.done, estado failedIgualDispara error

OpenAI documenta que output_audio_buffer.started se emite cuando el servidor empieza a transmitir audio al cliente, y que stopped se emite cuando el búfer de salida se vació por completo en el servidor, después de response.done. Ambos están marcados como exclusivos de WebRTC/SIP. Sobre WebSocket, el audio llega en fragmentos Base64 en response.output_audio.delta, seguidos de response.output_audio.done (eventos del servidor de OpenAI).

Ten en cuenta que response.output_audio.done significa que el servidor terminó de enviar el audio, no que el usuario terminó de escucharlo. Los fragmentos que siguen en la cola de tu reproductor pueden sonar durante varios segundos después.

¿Cómo conecto los eventos a un avatar de Rive sobre WebRTC?

Sobre WebRTC, los eventos del servidor llegan como cadenas JSON en el canal de datos que creaste para los eventos. Analiza cada mensaje y usa un switch sobre type. El avatar se actualiza con la API de Data Binding del runtime web de Rive: number("mood").value para los loops y trigger("error").trigger() para las reacciones puntuales (Data Binding de Rive para web).

import type { ViewModelInstance } from "@rive-app/webgl2";

const MOOD = { idle: 0, thinking: 3, listening: 6, talking: 7 } as const;

export function bindAvatarToRealtime(dc: RTCDataChannel, vmi: ViewModelInstance) {
  const mood = vmi.number("mood");
  const setMood = (m: number) => {
    if (mood) mood.value = m;
  };

  dc.addEventListener("message", (e) => {
    const ev = JSON.parse(e.data);
    switch (ev.type) {
      case "input_audio_buffer.speech_started":
      case "output_audio_buffer.cleared":
        setMood(MOOD.listening);
        break;
      case "input_audio_buffer.speech_stopped":
        setMood(MOOD.thinking);
        break;
      case "output_audio_buffer.started":
        setMood(MOOD.talking);
        break;
      case "output_audio_buffer.stopped":
        setMood(MOOD.idle);
        break;
      case "response.done":
        if (ev.response?.status === "failed") vmi.trigger("error")?.trigger();
        break;
    }
  });
}

Obtén vmi de rive.viewModelInstance después de cargar el archivo con autoBind: true. En React, llama a esto desde un effect cuando ya existan tanto el canal de datos como la instancia de Rive, y mantén el avatar en un componente "use client".

Elige idle o listening como estado de reposo entre turnos. Un agente de voz con el micrófono siempre abierto suele verse más natural reposando en listening, porque de hecho está escuchando.

¿Por qué no hay evento output_audio_buffer sobre WebSocket?

Sobre WebSocket, tu código es el reproductor de audio. El servidor envía los deltas tan rápido como los genera y no tiene forma de saber cuándo terminan tus altavoces, así que no hay un búfer del lado del servidor sobre el que informar. Tienes que detectar tú mismo el final de la reproducción.

Si programas los fragmentos decodificados en un AudioContext de Web Audio, lleva un "cabezal de reproducción" que indique cuándo termina el audio en cola:

const ctx = new AudioContext();
let playhead = 0;        // AudioContext time when queued audio ends
let responseDone = false;
let talking = false;

function playChunk(buffer: AudioBuffer) {
  if (!talking) {
    talking = true;
    setMood(MOOD.talking);
  }
  const src = ctx.createBufferSource();
  src.buffer = buffer;
  src.connect(ctx.destination);
  playhead = Math.max(playhead, ctx.currentTime);
  src.start(playhead);
  playhead += buffer.duration;
  src.onended = () => {
    if (responseDone && ctx.currentTime >= playhead - 0.01) {
      talking = false;
      setMood(MOOD.idle);
    }
  };
}

ws.addEventListener("message", (e) => {
  const ev = JSON.parse(e.data);
  if (ev.type === "response.created") responseDone = false;
  if (ev.type === "response.output_audio.delta") playChunk(decode(ev.delta));
  if (ev.type === "response.output_audio.done") responseDone = true;
});

decode() representa tu conversión del delta Base64 a un AudioBuffer; el formato exacto depende de la configuración de audio de salida de tu sesión. Además, los navegadores exigen un gesto del usuario antes de que un AudioContext pueda iniciarse, así que créalo o reanúdalo desde el botón que inicia la sesión (guía de reproducción automática de MDN).

¿Cómo manejo el barge-in (cuando el usuario interrumpe)?

El barge-in ocurre cuando el usuario empieza a hablar encima del agente. El avatar debe pasar a listening de inmediato, porque es lo que haría una persona.

  • WebRTC: en modo VAD, el servidor emite output_audio_buffer.cleared cuando el usuario interrumpe, junto con input_audio_buffer.speech_started. Cualquiera de los dos pasa el avatar a listening; el código anterior maneja ambos.
  • WebSocket: con speech_started, detén cada AudioBufferSourceNode en cola, reinicia tu cabezal de reproducción y activa listening. Si no detienes la reproducción, el avatar muestra listening mientras la respuesta anterior sigue sonando, lo cual resulta confuso.

La respuesta interrumpida igual termina con response.done. OpenAI indica que este evento se emite siempre, sea cual sea el estado final, y que los clientes deben revisar su status, que puede ser completed, cancelled, failed o incomplete (eventos del servidor de OpenAI). Solo failed merece una reacción de error. Una respuesta cancelled después de un barge-in es el sistema funcionando como se diseñó; no hagas que el avatar se vea triste por eso.

Como cada fase de esta guía cambia el estado de ánimo en lugar de disparar un trigger, las interrupciones salen baratas: asignar un nuevo estado de ánimo simplemente cambia de loop. Conviene reservar las reacciones puntuales para resultados claros, como una llamada a herramienta completada.

¿Por qué output_audio_buffer.stopped llega tarde o no llega?

Desarrolladores del foro de la comunidad de OpenAI han informado que output_audio_buffer.stopped llega con mucho retraso, y que no se envía en absoluto mientras hay una llamada a función pendiente, hasta que se devuelve el resultado de la herramienta (hilo de la comunidad de OpenAI). Son informes de la comunidad y no comportamiento documentado, pero son lo bastante frecuentes como para tenerlos en cuenta en el diseño.

Defensas prácticas:

  • Quédate en thinking durante las llamadas a herramientas. Cuando el modelo llama a una función, el agente está trabajando, no hablando. Cambia a thinking (o loading) hasta que envíes el resultado y empiece el nuevo audio.
  • Agrega un temporizador de respaldo. Después de response.done, si stopped no llega unos segundos después de la duración de audio que esperas, vuelve de todos modos a tu estado de ánimo de reposo.
  • Usa el propio audio como segunda señal. Conecta la pista de audio remota a un AnalyserNode y considera varios cientos de milisegundos de silencio como el final del habla. En Lip-sync vs amplitud encontrarás el código de RMS e histéresis.

Gana la primera señal que se dispare, y todas las siguientes no hacen nada porque el estado de ánimo ya está asignado.

¿Cómo muestro un estado thinking entre turnos?

Thinking cubre el intervalo entre el final del habla del usuario y el primer audio del modelo. En este mapeo empieza con speech_stopped y termina con output_audio_buffer.started (WebRTC) o con el primer response.output_audio.delta (WebSocket). También puedes activarlo con response.created cuando inicias tú mismo las respuestas, por ejemplo después de enviar una entrada de texto.

La distribución de diez moods que se usa en esta guía coincide uno a uno con los turnos de Realtime: listening (6) mientras el usuario habla, thinking (3) mientras el modelo trabaja, talking (7) mientras suena el audio. Charmotion crea esa distribución a partir de tu propio personaje o logo en pocos minutos, con un view model Character que contiene el número mood y los triggers success/error, además de versiones en MP4 y video transparente para marketing. El set completo, con el .riv y el video transparente incluidos, viene con todos los planes de pago. Su estado talking es un loop y no sincronización labial, lo que encaja con un agente cuyas palabras también aparecen como transcripción.

Con cualquier personaje, mantén thinking sutil. Puede durar una fracción de segundo en la mayoría de los turnos y varios segundos cuando se ejecuta una llamada a herramienta, así que un loop suave que se vea bien en ambas duraciones es mejor que uno dramático. Acompaña las esperas largas con un breve estado en texto, como "Revisando tu calendario".

Diagrama de una máquina de estados de Rive: un estado de entrada, diez estados de loop elegidos por un número mood y estados de reacción puntual activados por triggers que vuelven al loop actual
Cómo está conectado un .riv de Charmotion: el número mood elige un loop y los triggers reproducen reacciones puntuales desde cualquier estado.

Preguntas frecuentes

¿Qué evento de Realtime indica que el usuario empezó a hablar?

input_audio_buffer.speech_started, que se envía en modo VAD del servidor cuando este detecta voz en el búfer de entrada. OpenAI sugiere usarlo para interrumpir la reproducción o dar retroalimentación visual, así que es el trigger natural para un estado listening. Cuando el usuario deja de hablar, llega input_audio_buffer.speech_stopped.

¿Sigue siendo response.audio.delta el nombre correcto del evento?

No. La referencia actual de Realtime lo llama response.output_audio.delta, seguido de response.output_audio.done. response.audio.delta proviene de ejemplos de la etapa beta. Si tu listener, copiado de un tutorial antiguo, nunca se dispara, cámbiale el nombre. El primer delta de una respuesta es una buena señal para iniciar un estado talking sobre WebSocket.

¿Funciona output_audio_buffer.stopped sobre WebSocket?

No. OpenAI documenta output_audio_buffer.started, stopped y cleared como exclusivos de WebRTC/SIP. Por WebSocket tú mismo reproduces el audio, así que detecta cuándo termina tu propia cola de reproducción y vuelve a poner el avatar en idle o listening en ese momento, no en response.output_audio.done.

¿Una respuesta cancelada debe mostrar una animación de error?

No. Después de una interrupción (barge-in), response.done llega con estado cancelled, lo cual es esperable. Solo un estado failed indica un problema real que merece una reacción de error. Pasa el avatar a listening al detectar la interrupción, detén cualquier reproducción en cola y deja que las respuestas canceladas pasen en silencio, sin ninguna reacción.

Respuestas rápidas de esta guía

Fuentes

Guías relacionadas