Voice Mode

Converse com o seu assistente. Oito estilos reativos ao áudio, de um céu nublado e uma bolha de ferrofluido a pixels com dithering, ASCII, um planeta CRT, pontos de meio-tom, um único anel e uma aura suave, além de quatro pontos reativos. Uma sessão em tela cheia com silenciar, interromper e legendas, uma pílula de voz no chat, um seletor de voz e um motor de navegador que escuta, espera você terminar e responde.

Voice Mode é o visual e os controles para conversar com um assistente. Escolha um dos oito estilos com shader (Sky, Dither, ASCII, CRT, Ferrofluid, Halftone, Ring ou Aura) ou quatro pontos simples. Cada um escuta, pensa e responde acompanhando o som, e os estilos monocromáticos seguem o seu tema claro ou escuro.

Os níveis de áudio são suavizados e medidos em relação a um piso de ruído aprendido, então uma sala silenciosa parece silenciosa e o movimento continua suave. A renderização pausa quando o visual está fora da tela ou a aba está oculta, e o movimento reduzido mantém todos os estilos parados.

VoiceSession organiza tudo em tela cheia na largura de um celular, com legendas, uma linha de status, e mudo e encerrar ao alcance do polegar. Toque no visual, pressione Space ou simplesmente comece a falar para interromper. VoiceComposer é uma versão compacta que substitui o composer dentro de um chat.

useBrowserVoice executa um loop completo com o reconhecimento de fala e a síntese de fala do próprio navegador: espera uma pausa antes de responder, pode ficar em silêncio e exibir as respostas como legendas, e ignora as próprias palavras quando elas voltam pelos alto-falantes. Para APIs de voz em tempo real, passe os streams do microfone e da resposta para useAudioLevels e defina o estado a partir dos eventos da sessão.

  1. Adicione o registro Pro ao components.json

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

    Crie um token na sua página de conta e coloque-o em .env.local como HEXTAUI_PRO_TOKEN.

  3. Adicione o bloco

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

Com a voz do navegador

useBrowserVoice escuta com o microfone e o reconhecimento de fala do navegador, espera uma pausa, chama respond com o que você disse e responde com a fala do navegador. Retorne a resposta do seu modelo em 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}
    />
  )
}

Com uma API de voz em tempo real

Para modelos de fala para fala via WebRTC, controle o estado a partir dos eventos da sessão e envie os dois fluxos de áudio para useAudioLevels, para que o orb acompanhe você enquanto fala e o modelo enquanto 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 um chat

VoiceComposer substitui o composer enquanto uma sessão de voz está ativa, com um orb pequeno, o status, um cronômetro, mudo e End. O que você diz e as respostas podem chegar à conversa como texto, via streaming.

"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}
    />
  )
}

Anatomia

As partes que você compõe, de fora para dentro.

ParteDescrição
VoiceVisualQualquer estilo por kind: sky, dither, ascii, crt, ferrofluid, halftone, ring, aura ou bars.
VoiceOrbO orb com shader. Recebe o estado, o mudo e uma ref de levels.
VoiceBarsO visual de quatro pontos, com as mesmas props.
VoiceShaderO engine WebGL compartilhado. Passe seu próprio fragment shader para criar um novo estilo com as mesmas entradas: estado, nível suavizado, quatro bandas e três cores.
VoiceSessionO layout em tela cheia: ações, visual, legendas, status, mudo e encerrar.
VoiceComposerA pílula de voz compacta para usar dentro de um chat.
VoicePickerUm popover de vozes com orbs coloridos.
useBrowserVoiceUm loop de voz completo com APIs do navegador: ouvir, detectar o fim de um turno, responder, falar, interromper.
useAudioLevelsLê um nível e quatro bandas de qualquer MediaStream ou elemento de mídia para uma ref, sem renderizar novamente.

VoiceVisual

Também aceita todas as props de div. Defina o tamanho com uma classe de largura; ele continua quadrado.

PropTipoPadrão
kindQual estilo renderizar.
"sky" | "dither" | "ascii" | "crt" | "ferrofluid" | "halftone" | "ring" | "aura" | "bars""sky"
colorsTrês cores para o shader. Hex, qualquer cor CSS ou currentColor. Cada estilo vem com as suas.
[string, string, string]–
paletteCores do sky, como as usadas pelas vozes de VoicePicker.
{ deep, sky, cloud }–
stateControla o movimento de ouvindo, pensando e falando.
VoiceState–
levelsÁudio ao vivo.
RefObject<VoiceLevels>–
mutedDessatura e para de reagir. O anel fica tracejado.
booleanfalse

VoiceOrb

Também aceita todas as props de div. Defina o tamanho com uma classe de largura; ele continua quadrado.

PropTipoPadrão
stateControla churn, whirl e ripples.
"idle" | "connecting" | "listening" | "thinking" | "speaking" | "error""idle"
levelsÁudio ao vivo, lido a cada frame. Geralmente vem de useAudioLevels ou useBrowserVoice.
RefObject<VoiceLevels>–
mutedDessatura e para de reagir.
booleanfalse
paletteTrês cores hex.
{ deep, sky, cloud }blue sky
PropTipoPadrão
stateO estado da conversa.
VoiceState–
levelsRepassado ao visual.
RefObject<VoiceLevels>–
variantQual estilo exibir.
VoiceVisualKind"sky"
colorsSubstitui as cores do estilo.
[string, string, string]–
mutedEstado de mudo.
booleanfalse
onMutedChangeMostra o botão de mudo e o atalho M.
(muted: boolean) => void–
onInterruptTransforma o visual em um botão enquanto pensa ou fala, além de Space.
() => void–
onEndMostra o botão de encerrar e Escape.
() => void–
captionsAs últimas palavras de cada lado.
{ user, assistant }–
showCaptionsExibe legendas sob o visual.
booleanfalse
onShowCaptionsChangeMostra o alternador de legendas e C.
(show: boolean) => void–
statusSubstitui o rótulo de estado.
ReactNode–
footnoteEntre os botões, como um cronômetro ou os minutos restantes.
ReactNode–
errorExibido no lugar do status.
string | null–
actionsControles no canto superior direito, como VoicePicker.
ReactNode–
paletteCores do orb.
VoicePalette–
PropTipoPadrão
stateO estado da conversa.
VoiceState–
levelsRepassado ao orb pequeno.
RefObject<VoiceLevels>–
onEndEncerra a voz e traz de volta o composer.
() => void–
muted / onMutedChangeBotão de mudo.
boolean / (muted) => void–
onInterruptToque no orb para interromper.
() => void–
startedAtHorário de início em ms, para o cronômetro.
number | null–

useBrowserVoice(options)

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

PropTipoPadrão
respondChamado quando você termina uma frase. Retorne a resposta; o signal é abortado ao interromper.
(text, turns, signal) => Promise<string>–
pauseMsSilêncio antes de o seu turno terminar.
number900
bargeInPermite que a fala interrompa a resposta.
booleantrue
voiceOutputFala as respostas em voz alta. Quando false, as respostas aparecem como legendas e o orb continua animado.
booleantrue
voiceVozes do navegador preferidas e entonação.
{ names?, pitch?, rate? }–
greetingDito quando a sessão começa.
string–
langIdioma de reconhecimento e de fala.
string"en-US"

useAudioLevels(source, target?)

Retorna uma ref de { level, bands } atualizada a cada frame.

PropTipoPadrão
sourceMicrofone, stream WebRTC remoto ou um elemento de áudio.
MediaStream | HTMLMediaElement | null–
targetEscreve em uma ref existente em vez de criar uma nova.
RefObject<VoiceLevels>–
TeclaAção
SpaceInterrompe enquanto está pensando ou falando.
MSilencia ou reativa o microfone.
CMostra ou oculta as legendas.
EscEncerra a sessão de voz.
TabPercorre o alternador de legendas, o seletor de voz, o orb quando é interrompível, o mudo e o encerrar.
  • A sessão é uma região rotulada, e a linha de status é uma região live educada (polite), então mudanças de estado como “Listening” e “Thinking” são anunciadas.
  • O visual é um botão de verdade rotulado “Interrupt” enquanto pode ser interrompido, e fica desativado nos demais casos.
  • O mudo usa aria-pressed, e todo controle tem um tooltip com o nome do seu atalho.
  • As legendas oferecem uma alternativa em texto para tudo o que é dito, e as respostas podem ser exibidas sem áudio.
  • Os atalhos são ignorados enquanto você digita em um campo, exceto Escape.
  • Com movimento reduzido, o orb e os pontos ficam parados e só mudam com o estado.
  • Sem WebGL, todos os estilos com shader recorrem a um gradiente estático, e os pontos funcionam em qualquer lugar.

Construído com

Os componentes gratuitos do HextaUI de que Voice Mode é feito. Cada um é instalado separadamente.

Código

9 arquivos, adicionados a components/blocks/voice-mode.