Voice Mode

Habla con tu asistente. Ocho estilos que reaccionan al audio, desde un cielo nublado y una mancha de ferrofluido hasta píxeles con dithering, ASCII, un planeta CRT, puntos de semitono, un solo anillo y un aura suave, más cuatro puntos reactivos. Una sesión a pantalla completa con silencio, interrupción y subtítulos, una píldora de voz en el chat, un selector de voz y un motor del navegador que escucha, espera a que termines y responde.

Voice Mode es el visual y los controles para hablar con un asistente. Elige uno de ocho estilos de shader (Sky, Dither, ASCII, CRT, Ferrofluid, Halftone, Ring o Aura) o cuatro puntos simples. Cada uno escucha, piensa y responde moviéndose con el sonido, y los estilos monocromos siguen tu tema claro u oscuro.

Los niveles de audio se suavizan y se miden contra un piso de ruido aprendido, de modo que una sala silenciosa se lee como silenciosa y el movimiento se mantiene suave. El renderizado se pausa cuando el visual está fuera de pantalla o la pestaña está oculta, y el movimiento reducido deja todos los estilos quietos.

VoiceSession lo distribuye a pantalla completa con ancho de teléfono, con subtítulos, una línea de estado, y silenciar y terminar al alcance del pulgar. Toca el visual, pulsa Space o simplemente empieza a hablar para interrumpir. VoiceComposer es una versión compacta que reemplaza al compositor dentro de un chat.

useBrowserVoice ejecuta un bucle completo con el reconocimiento y la síntesis de voz del propio navegador: espera una pausa antes de responder, puede permanecer en silencio y mostrar las respuestas como subtítulos, e ignora sus propias palabras cuando regresan por los altavoces. Para APIs de voz en tiempo real, pasa los flujos del micrófono y de la respuesta a useAudioLevels y establece el estado a partir de los eventos de la sesión.

  1. Añade el registro Pro a components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Añade tu token

    Crea un token en tu página de cuenta y colócalo en .env.local como HEXTAUI_PRO_TOKEN.

  3. Añade el bloque

    pnpm dlx shadcn@latest add @hextaui-pro/voice-mode

Con la voz del navegador

useBrowserVoice escucha con el micrófono y el reconocimiento de voz del navegador, espera una pausa, llama a respond con lo que dijiste y responde hablando con la voz del navegador. Devuelve la respuesta de tu modelo desde respond.

"use client"

import { useBrowserVoice } from "@/components/blocks/voice-mode/use-browser-voice"
import { VoiceSession } from "@/components/blocks/voice-mode/voice-session"

export function VoiceChat({ onClose }: { onClose: () => void }) {
  const voice = useBrowserVoice({
    pauseMs: 900,
    respond: async (text, turns, signal) => {
      const response = await fetch("/api/voice", {
        method: "POST",
        body: JSON.stringify({ text, turns }),
        signal,
      })
      const { reply } = await response.json()
      return reply
    },
  })

  return (
    <VoiceSession
      state={voice.state}
      levels={voice.levels}
      muted={voice.muted}
      onMutedChange={voice.setMuted}
      onInterrupt={voice.interrupt}
      onEnd={() => {
        voice.end()
        onClose()
      }}
      captions={voice.captions}
      showCaptions
      error={voice.error}
    />
  )
}

Con una API de voz en tiempo real

Para modelos de voz a voz sobre WebRTC, controla el estado a partir de los eventos de la sesión y pasa ambos flujos de audio a useAudioLevels, de modo que el orbe te siga mientras hablas y siga al modelo mientras responde.

"use client"

import * as React from "react"

import { useAudioLevels } from "@/components/blocks/voice-mode/audio"
import type { VoiceState } from "@/components/blocks/voice-mode/voice-orb"
import { VoiceSession } from "@/components/blocks/voice-mode/voice-session"

export function RealtimeVoice({ token }: { token: string }) {
  const [state, setState] = React.useState<VoiceState>("connecting")
  const [mic, setMic] = React.useState<MediaStream | null>(null)
  const [reply, setReply] = React.useState<MediaStream | null>(null)
  const [muted, setMuted] = React.useState(false)
  const peer = React.useRef<RTCPeerConnection | null>(null)
  const listening = useAudioLevels(mic)
  const speaking = useAudioLevels(reply)

  React.useEffect(() => {
    const connection = new RTCPeerConnection()
    peer.current = connection
    connection.ontrack = (event) => setReply(event.streams[0])
    const events = connection.createDataChannel("oai-events")
    events.onmessage = (message) => {
      const event = JSON.parse(message.data)
      if (event.type === "input_audio_buffer.speech_started") setState("listening")
      if (event.type === "input_audio_buffer.committed") setState("thinking")
      if (event.type === "output_audio_buffer.started") setState("speaking")
      if (event.type === "output_audio_buffer.stopped") setState("listening")
    }
    navigator.mediaDevices.getUserMedia({ audio: true }).then(async (stream) => {
      setMic(stream)
      stream.getTracks().forEach((track) => connection.addTrack(track, stream))
      const offer = await connection.createOffer()
      await connection.setLocalDescription(offer)
      const answer = await fetch("https://api.openai.com/v1/realtime/calls", {
        method: "POST",
        body: offer.sdp,
        headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/sdp" },
      })
      await connection.setRemoteDescription({ type: "answer", sdp: await answer.text() })
      setState("listening")
    })
    return () => connection.close()
  }, [token])

  React.useEffect(() => {
    mic?.getAudioTracks().forEach((track) => (track.enabled = !muted))
  }, [mic, muted])

  return (
    <VoiceSession
      state={state}
      levels={state === "speaking" ? speaking : listening}
      muted={muted}
      onMutedChange={setMuted}
      onEnd={() => peer.current?.close()}
    />
  )
}

Dentro de un chat

VoiceComposer reemplaza al compositor mientras dura una sesión de voz, con un orbe pequeño, el estado, un temporizador, silenciar y End. Tus palabras y las respuestas pueden fluir al hilo como texto.

"use client"

import { useBrowserVoice } from "@/components/blocks/voice-mode/use-browser-voice"
import { VoiceComposer } from "@/components/blocks/voice-mode/voice-session"

export function ChatVoice({ respond }: { respond: (text: string) => Promise<string> }) {
  const voice = useBrowserVoice({ respond: (text) => respond(text) })

  if (voice.state === "idle") {
    return <button onClick={() => void voice.start()}>Start voice</button>
  }

  return (
    <VoiceComposer
      state={voice.state}
      levels={voice.levels}
      muted={voice.muted}
      onMutedChange={voice.setMuted}
      onInterrupt={voice.interrupt}
      onEnd={voice.end}
      startedAt={voice.startedAt}
    />
  )
}

Anatomía

Las partes que compones, de fuera hacia dentro.

ParteDescripción
VoiceVisualCualquier estilo según kind: sky, dither, ascii, crt, ferrofluid, halftone, ring, aura o bars.
VoiceOrbEl orbe con shader. Recibe el estado, el silencio y una ref de niveles.
VoiceBarsEl visual de cuatro puntos, con las mismas props.
VoiceShaderEl motor WebGL compartido. Pasa tu propio fragment shader para crear un estilo nuevo con las mismas entradas: estado, nivel suavizado, cuatro bandas y tres colores.
VoiceSessionEl diseño a pantalla completa: acciones, visual, subtítulos, estado, silenciar y terminar.
VoiceComposerLa píldora de voz compacta para usar dentro de un chat.
VoicePickerUn popover de voces con orbes teñidos.
useBrowserVoiceUn bucle de voz completo con las APIs del navegador: escuchar, detectar el final de un turno, responder, hablar e interrumpir.
useAudioLevelsLee un nivel y cuatro bandas de cualquier MediaStream o elemento multimedia en una ref, sin volver a renderizar.

VoiceVisual

También acepta todas las props de div. Define su tamaño con una clase de ancho; se mantiene cuadrado.

PropTipoPredeterminado
kindQué estilo renderizar.
"sky" | "dither" | "ascii" | "crt" | "ferrofluid" | "halftone" | "ring" | "aura" | "bars""sky"
colorsTres colores para el shader. Hex, cualquier color CSS o currentColor. Cada estilo incluye los suyos.
[string, string, string]–
paletteColores del cielo, tal como los usan las voces de VoicePicker.
{ deep, sky, cloud }–
stateControla el movimiento al escuchar, pensar y hablar.
VoiceState–
levelsAudio en vivo.
RefObject<VoiceLevels>–
mutedSe desatura y deja de reaccionar. El anillo pasa a ser discontinuo.
booleanfalse

VoiceOrb

También acepta todas las props de div. Define su tamaño con una clase de ancho; se mantiene cuadrado.

PropTipoPredeterminado
stateControla el remolino, el giro y las ondas.
"idle" | "connecting" | "listening" | "thinking" | "speaking" | "error""idle"
levelsAudio en vivo, leído en cada fotograma. Normalmente proviene de useAudioLevels o useBrowserVoice.
RefObject<VoiceLevels>–
mutedSe desatura y deja de reaccionar.
booleanfalse
paletteTres colores hex.
{ deep, sky, cloud }blue sky
PropTipoPredeterminado
stateEl estado de la conversación.
VoiceState–
levelsSe pasa al visual.
RefObject<VoiceLevels>–
variantQué estilo mostrar.
VoiceVisualKind"sky"
colorsReemplaza los colores del estilo.
[string, string, string]–
mutedEstado de silencio.
booleanfalse
onMutedChangeMuestra el botón de silenciar y el atajo M.
(muted: boolean) => void–
onInterruptConvierte el visual en un botón mientras piensa o habla, además de Space.
() => void–
onEndMuestra el botón de terminar y Escape.
() => void–
captionsLas últimas palabras de cada parte.
{ user, assistant }–
showCaptionsMuestra subtítulos bajo el visual.
booleanfalse
onShowCaptionsChangeMuestra el interruptor de subtítulos y C.
(show: boolean) => void–
statusReemplaza la etiqueta de estado.
ReactNode–
footnoteEntre los botones, como un temporizador o los minutos restantes.
ReactNode–
errorSe muestra en lugar del estado.
string | null–
actionsControles superiores derechos, como VoicePicker.
ReactNode–
paletteColores del orbe.
VoicePalette–
PropTipoPredeterminado
stateEl estado de la conversación.
VoiceState–
levelsSe pasa al orbe pequeño.
RefObject<VoiceLevels>–
onEndTermina la voz y devuelve el compositor.
() => void–
muted / onMutedChangeBotón de silenciar.
boolean / (muted) => void–
onInterruptToca el orbe para interrumpir.
() => void–
startedAtHora de inicio en ms, para el temporizador.
number | null–

useBrowserVoice(options)

Devuelve { state, muted, setMuted, start, end, interrupt, levels, turns, captions, error, startedAt }.

PropTipoPredeterminado
respondSe llama cuando terminas una frase. Devuelve la respuesta; la señal se aborta al interrumpir.
(text, turns, signal) => Promise<string>–
pauseMsSilencio antes de que termine tu turno.
number900
bargeInPermite que hablar interrumpa la respuesta.
booleantrue
voiceOutputLee las respuestas en voz alta. Cuando es false, las respuestas aparecen como subtítulos y el orbe sigue animándose.
booleantrue
voiceVoces del navegador preferidas y entonación.
{ names?, pitch?, rate? }–
greetingLo que se dice cuando empieza la sesión.
string–
langIdioma de reconocimiento y de voz.
string"en-US"

useAudioLevels(source, target?)

Devuelve una ref de { level, bands } actualizada en cada fotograma.

PropTipoPredeterminado
sourceMicrófono, flujo WebRTC remoto o un elemento audio.
MediaStream | HTMLMediaElement | null–
targetEscribe en una ref existente en lugar de una nueva.
RefObject<VoiceLevels>–
KeyAcción
SpaceInterrumpe mientras piensa o habla.
MSilencia o reactiva tu micrófono.
CMuestra u oculta los subtítulos.
EscTermina la sesión de voz.
TabRecorre el interruptor de subtítulos, el selector de voz, el orbe cuando se puede interrumpir, silenciar y terminar.
  • La sesión es una región con etiqueta y la línea de estado es una región activa polite, así que los cambios de estado como “Listening” y “Thinking” se anuncian.
  • El visual es un botón real con la etiqueta “Interrupt” mientras se puede interrumpir, y está deshabilitado en caso contrario.
  • Silenciar usa aria-pressed, y cada control tiene un tooltip con su atajo.
  • Los subtítulos ofrecen una alternativa en texto a todo lo que se dice, y las respuestas pueden mostrarse sin audio.
  • Los atajos se ignoran mientras escribes en un campo, excepto Escape.
  • Con movimiento reducido, el orbe y los puntos se quedan quietos y solo cambian con el estado.
  • Sin WebGL, todos los estilos de shader recurren a un degradado estático, y los puntos funcionan en todas partes.

Construido con

Los componentes gratuitos de HextaUI con los que está hecho Voice Mode. Cada uno se instala por separado.

Código

9 archivos, añadidos a components/blocks/voice-mode.