Voice Mode

Parlez à votre assistant. Huit styles réactifs à l’audio, d’un ciel nuageux et d’une goutte de ferrofluide à des pixels tramés, de l’ASCII, une planète CRT, des points de demi-teinte, un anneau unique et une aura douce, plus quatre points réactifs. Une session plein écran avec coupure du micro, interruption et sous-titres, une pastille vocale dans le chat, un sélecteur de voix, et un moteur de navigateur qui écoute, attend que vous ayez fini et répond.

Voice Mode est le visuel et les contrôles pour parler à un assistant. Choisissez l’un des huit styles shader (Sky, Dither, ASCII, CRT, Ferrofluid, Halftone, Ring ou Aura) ou quatre simples points. Chacun écoute, réfléchit et répond en bougeant avec le son, et les styles monochromes suivent votre thème clair ou sombre.

Les niveaux audio sont lissés et mesurés par rapport à un bruit de fond appris : une pièce calme est donc perçue comme calme et le mouvement reste doux. Le rendu se met en pause lorsque le visuel est hors écran ou que l’onglet est masqué, et la réduction des animations fige tous les styles.

VoiceSession l’affiche en plein écran à la largeur d’un téléphone, avec sous-titres, ligne de statut, ainsi que sourdine et fin à portée de pouce. Touchez le visuel, appuyez sur Space ou commencez simplement à parler pour interrompre. VoiceComposer en est une version compacte qui remplace le champ de saisie dans une conversation.

useBrowserVoice exécute une boucle complète avec la reconnaissance et la synthèse vocales du navigateur : il attend une pause avant de répondre, peut rester silencieux et afficher les réponses en sous-titres, et ignore ses propres paroles lorsqu’elles reviennent par les haut-parleurs. Pour les API vocales temps réel, passez les flux du microphone et de la réponse à useAudioLevels et définissez l’état à partir des événements de la session.

  1. Ajouter le registre Pro à components.json

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

    Créez un token sur votre page de compte et placez-le dans .env.local sous le nom HEXTAUI_PRO_TOKEN.

  3. Ajouter le block

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

Avec la voix du navigateur

useBrowserVoice écoute avec le microphone et la reconnaissance vocale du navigateur, attend une pause, appelle respond avec ce que vous avez dit, puis répond avec la synthèse vocale du navigateur. Retournez la réponse de votre modèle depuis 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}
    />
  )
}

Avec une API vocale temps réel

Pour les modèles speech-to-speech via WebRTC, pilotez l’état à partir des événements de la session et envoyez les deux flux audio à useAudioLevels : l’orbe vous suit pendant que vous parlez, puis le modèle pendant qu’il répond.

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

Dans une conversation

VoiceComposer remplace le champ de saisie pendant une session vocale, avec une petite orbe, le statut, un minuteur, la sourdine et End. Vos paroles et les réponses peuvent s’afficher en texte dans le fil.

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

Anatomie

Les parties à composer, de l’extérieur vers l’intérieur.

PartieDescription
VoiceVisualN’importe quel style selon kind : sky, dither, ascii, crt, ferrofluid, halftone, ring, aura ou bars.
VoiceOrbL’orbe shader. Prend l’état, la sourdine et une ref de niveaux.
VoiceBarsLe visuel à quatre points, avec les mêmes props.
VoiceShaderLe moteur WebGL partagé. Passez votre propre fragment shader pour créer un nouveau style avec les mêmes entrées : état, niveau lissé, quatre bandes et trois couleurs.
VoiceSessionLa disposition plein écran : actions, visuel, sous-titres, statut, sourdine et fin.
VoiceComposerLa pastille vocale compacte à utiliser dans une conversation.
VoicePickerUn popover de voix avec des orbes teintés.
useBrowserVoiceUne boucle vocale complète basée sur les API du navigateur : écouter, détecter la fin d’un tour de parole, répondre, parler, interrompre.
useAudioLevelsLit un niveau et quatre bandes depuis n’importe quel MediaStream ou élément média dans une ref, sans nouveau rendu.

VoiceVisual

Accepte aussi toutes les props de div. Dimensionnez-le avec une classe de largeur ; il reste carré.

PropTypePar défaut
kindLe style à afficher.
"sky" | "dither" | "ascii" | "crt" | "ferrofluid" | "halftone" | "ring" | "aura" | "bars""sky"
colorsTrois couleurs pour le shader. Hex, n’importe quelle couleur CSS ou currentColor. Chaque style fournit les siennes.
[string, string, string]–
paletteCouleurs du ciel, telles qu’utilisées par les voix de VoicePicker.
{ deep, sky, cloud }–
statePilote les mouvements d’écoute, de réflexion et de parole.
VoiceState–
levelsAudio en direct.
RefObject<VoiceLevels>–
mutedDésature et cesse de réagir. L’anneau passe en pointillés.
booleanfalse

VoiceOrb

Accepte aussi toutes les props de div. Dimensionnez-le avec une classe de largeur ; il reste carré.

PropTypePar défaut
statePilote l’agitation, le tourbillon et les ondulations.
"idle" | "connecting" | "listening" | "thinking" | "speaking" | "error""idle"
levelsAudio en direct, lu à chaque image. Généralement issu de useAudioLevels ou useBrowserVoice.
RefObject<VoiceLevels>–
mutedDésature et cesse de réagir.
booleanfalse
paletteTrois couleurs hexadécimales.
{ deep, sky, cloud }blue sky
PropTypePar défaut
stateL’état de la conversation.
VoiceState–
levelsTransmis au visuel.
RefObject<VoiceLevels>–
variantLe style à montrer.
VoiceVisualKind"sky"
colorsRemplace les couleurs du style.
[string, string, string]–
mutedÉtat de la sourdine.
booleanfalse
onMutedChangeAffiche le bouton de sourdine et le raccourci M.
(muted: boolean) => void–
onInterruptTransforme le visuel en bouton pendant la réflexion ou la parole, avec en plus Space.
() => void–
onEndAffiche le bouton de fin et Escape.
() => void–
captionsDerniers mots de chaque interlocuteur.
{ user, assistant }–
showCaptionsAffiche les sous-titres sous le visuel.
booleanfalse
onShowCaptionsChangeAffiche le bouton des sous-titres et C.
(show: boolean) => void–
statusRemplace le libellé de l’état.
ReactNode–
footnoteEntre les boutons, par exemple un minuteur ou le nombre de minutes restantes.
ReactNode–
errorAffiché à la place du statut.
string | null–
actionsContrôles en haut à droite, comme VoicePicker.
ReactNode–
paletteCouleurs de l’orbe.
VoicePalette–
PropTypePar défaut
stateL’état de la conversation.
VoiceState–
levelsTransmis à la petite orbe.
RefObject<VoiceLevels>–
onEndTermine la voix et réaffiche le champ de saisie.
() => void–
muted / onMutedChangeBouton de sourdine.
boolean / (muted) => void–
onInterruptTouchez l’orbe pour interrompre.
() => void–
startedAtHeure de début en ms, pour le minuteur.
number | null–

useBrowserVoice(options)

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

PropTypePar défaut
respondAppelé lorsque vous terminez une phrase. Retournez la réponse ; le signal s’interrompt en cas d’interruption.
(text, turns, signal) => Promise<string>–
pauseMsSilence avant la fin de votre tour de parole.
number900
bargeInPermet à la parole d’interrompre la réponse.
booleantrue
voiceOutputPrononce les réponses à voix haute. Si false, les réponses s’affichent en sous-titres et l’orbe reste animée.
booleantrue
voiceVoix du navigateur préférées et façon de parler.
{ names?, pitch?, rate? }–
greetingDit au démarrage de la session.
string–
langLangue de la reconnaissance et de la synthèse vocale.
string"en-US"

useAudioLevels(source, target?)

Retourne une ref de { level, bands } mise à jour à chaque image.

PropTypePar défaut
sourceMicrophone, flux WebRTC distant ou élément audio.
MediaStream | HTMLMediaElement | null–
targetÉcrit dans une ref existante au lieu d’en créer une nouvelle.
RefObject<VoiceLevels>–
ToucheAction
SpaceInterrompt pendant la réflexion ou la parole.
MCoupe ou réactive votre microphone.
CAffiche ou masque les sous-titres.
EscMet fin à la session vocale.
TabParcourt le bouton des sous-titres, le sélecteur de voix, l’orbe lorsqu’il est interruptible, la sourdine et la fin.
  • La session est une région étiquetée et la ligne de statut est une région live polie : les changements d’état comme « Listening » et « Thinking » sont donc annoncés.
  • Le visuel est un vrai bouton intitulé « Interrupt » tant qu’il peut être interrompu, et désactivé sinon.
  • La sourdine utilise aria-pressed, et chaque contrôle possède une infobulle indiquant son raccourci.
  • Les sous-titres offrent une alternative textuelle à tout ce qui est dit, et les réponses peuvent s’afficher sans audio.
  • Les raccourcis sont ignorés pendant la saisie dans un champ, sauf Escape.
  • Avec la réduction des animations, l’orbe et les points restent immobiles et ne changent qu’avec l’état.
  • Sans WebGL, chaque style shader se rabat sur un dégradé fixe, et les points fonctionnent partout.

Construit avec

Les composants HextaUI gratuits dont Voice Mode est constitué. Chacun s’installe séparément.

Code

9 fichiers, ajoutés à components/blocks/voice-mode.