Voice Mode

アシスタントと話せます。曇り空やフェロフルイドの塊から、ディザリングされたピクセル、ASCII、CRTの惑星、ハーフトーンのドット、1つのリング、やわらかなオーラまで、音に反応する8つのスタイルと、4つのリアクティブなドットを備えています。ミュート、割り込み、字幕に対応した全画面セッション、チャット内の音声ピル、音声ピッカー、聞き取り、話し終えるのを待って返答するブラウザーエンジンも含みます。

Voice Mode は、アシスタントと会話するためのビジュアルとコントロールです。8 つのシェーダースタイル(Sky、Dither、ASCII、CRT、Ferrofluid、Halftone、Ring、Aura)または 4 つのシンプルなドットから選べます。いずれも聞き取り、思考、応答の間、音に合わせて動き、モノクロのスタイルはライトテーマ・ダークテーマに従います。

音声レベルは平滑化され、学習したノイズフロアを基準に測定されるため、静かな部屋は静かなものとして表示され、動きも穏やかに保たれます。ビジュアルが画面外にあるときやタブが非表示のときは描画が一時停止し、モーションの低減が有効な場合はすべてのスタイルが静止します。

VoiceSession は、スマートフォンの幅でフルスクリーン表示し、字幕、ステータス行、親指で届く位置のミュートと終了ボタンを配置します。ビジュアルのタップ、Space キー、または単に話し始めることで割り込めます。VoiceComposer は、チャット内でコンポーザーの代わりに使えるコンパクト版です。

useBrowserVoice は、ブラウザ標準の音声認識と音声合成で完全なループを実行します。間を待ってから応答し、無音のまま返答を字幕として表示することもでき、スピーカーから戻ってきた自分の声は無視します。リアルタイム音声 API では、マイクと返答のストリームを useAudioLevels に渡し、セッションのイベントから状態を設定してください。

  1. Proレジストリをcomponents.jsonに追加する

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. トークンを追加する

    アカウントページでトークンを作成し、.env.local に HEXTAUI_PRO_TOKEN として設定してください。

  3. ブロックを追加する

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

ブラウザの音声を使う場合

useBrowserVoice はマイクとブラウザの音声認識で聞き取り、間を待って、話した内容を渡して respond を呼び出し、ブラウザの音声で返答を読み上げます。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}
    />
  )
}

リアルタイム音声 API を使う場合

WebRTC 上の speech-to-speech モデルでは、セッションのイベントから状態を制御し、両方の音声ストリームを useAudioLevels に渡します。これにより、話している間はあなたに、応答中はモデルにオーブが反応します。

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

チャット内での利用

VoiceComposer は、音声セッションの実行中にコンポーザーの代わりに表示され、小さなオーブ、ステータス、タイマー、ミュート、終了ボタンを備えます。あなたの発言と返答は、テキストとしてスレッドにストリーミングできます。

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

構造

外側から内側へ組み合わせるパーツ。

パーツ説明
VoiceVisualkind によるスタイルの指定。sky、dither、ascii、crt、ferrofluid、halftone、ring、aura、bars のいずれか。
VoiceOrbシェーダーのオーブ。状態、ミュート、levels の ref を受け取ります。
VoiceBars同じ props を持つ、4 つのドットのビジュアル。
VoiceShader共有の WebGL エンジン。独自のフラグメントシェーダーを渡すと、状態、平滑化されたレベル、4 つの帯域、3 色という同じ入力で新しいスタイルを作れます。
VoiceSessionフルスクリーンのレイアウト。アクション、ビジュアル、字幕、ステータス、ミュート、終了を備えます。
VoiceComposerチャット内で使うコンパクトな音声ピル。
VoicePicker色付きのオーブが並ぶ、音声選択用のポップオーバー。
useBrowserVoiceブラウザ API だけで動く完全な音声ループ。聞き取り、ターン終了の検出、応答、読み上げ、割り込みに対応します。
useAudioLevels任意の MediaStream またはメディア要素から、レベルと 4 つの帯域を再レンダリングなしで ref に読み取ります。

VoiceVisual

すべての div の props も受け付けます。サイズは幅のクラスで指定でき、正方形のまま保たれます。

プロパティ型デフォルト
kind描画するスタイル。
"sky" | "dither" | "ascii" | "crt" | "ferrofluid" | "halftone" | "ring" | "aura" | "bars""sky"
colorsシェーダー用の 3 色。16 進数、任意の CSS カラー、または currentColor。各スタイルには専用の色が付属します。
[string, string, string]–
paletteVoicePicker の音声で使われる sky の色。
{ deep, sky, cloud }–
state聞き取り、思考、発話の動きを制御します。
VoiceState–
levelsライブ音声。
RefObject<VoiceLevels>–
muted彩度を落とし、反応を止めます。リングは破線になります。
booleanfalse

VoiceOrb

すべての div の props も受け付けます。サイズは幅のクラスで指定でき、正方形のまま保たれます。

プロパティ型デフォルト
statechurn、whirl、ripples の動きを制御します。
"idle" | "connecting" | "listening" | "thinking" | "speaking" | "error""idle"
levelsフレームごとに読み取られるライブ音声。通常は useAudioLevels または useBrowserVoice から渡します。
RefObject<VoiceLevels>–
muted彩度を落とし、反応を止めます。
booleanfalse
palette3 つの 16 進数カラー。
{ deep, sky, cloud }blue sky
プロパティ型デフォルト
state会話の状態。
VoiceState–
levelsビジュアルに渡されます。
RefObject<VoiceLevels>–
variant表示するスタイル。
VoiceVisualKind"sky"
colorsスタイルの色を上書きします。
[string, string, string]–
mutedミュートの状態。
booleanfalse
onMutedChangeミュートボタンと M ショートカットを表示します。
(muted: boolean) => void–
onInterrupt思考中または発話中にビジュアルをボタンにし、Space キーも有効にします。
() => void–
onEnd終了ボタンと Escape キーを表示します。
() => void–
captions双方の最新の発言。
{ user, assistant }–
showCaptionsビジュアルの下に字幕を表示します。
booleanfalse
onShowCaptionsChange字幕の切り替えボタンと C キーを表示します。
(show: boolean) => void–
status状態ラベルを置き換えます。
ReactNode–
footnoteボタンの間に表示する、タイマーや残り時間など。
ReactNode–
errorステータスの代わりに表示されます。
string | null–
actions右上のコントロール。VoicePicker など。
ReactNode–
paletteオーブの色。
VoicePalette–
プロパティ型デフォルト
state会話の状態。
VoiceState–
levels小さなオーブに渡されます。
RefObject<VoiceLevels>–
onEnd音声を終了し、コンポーザーを元に戻します。
() => void–
muted / onMutedChangeミュートボタン。
boolean / (muted) => void–
onInterruptオーブをタップして割り込みます。
() => void–
startedAtタイマー用の開始時刻(ミリ秒)。
number | null–

useBrowserVoice(options)

{ state, muted, setMuted, start, end, interrupt, levels, turns, captions, error, startedAt } を返します。

プロパティ型デフォルト
respond文を話し終えたときに呼ばれます。返答を返してください。割り込みが発生すると signal が中断されます。
(text, turns, signal) => Promise<string>–
pauseMsターン終了とみなすまでの無音時間。
number900
bargeIn発話による返答への割り込みを許可します。
booleantrue
voiceOutput返答を音声で読み上げます。false の場合、オーブは動いたまま、返答が字幕として表示されます。
booleantrue
voice優先するブラウザの音声と話し方。
{ names?, pitch?, rate? }–
greetingセッション開始時に読み上げられる文。
string–
lang音声認識と読み上げの言語。
string"en-US"

useAudioLevels(source, target?)

フレームごとに更新される { level, bands } の ref を返します。

プロパティ型デフォルト
sourceマイク、リモートの WebRTC ストリーム、または audio 要素。
MediaStream | HTMLMediaElement | null–
target新しい ref を作らず、既存の ref に書き込みます。
RefObject<VoiceLevels>–
キーアクション
Space思考中または発話中に割り込みます。
Mマイクをミュートまたはミュート解除します。
C字幕の表示と非表示を切り替えます。
Esc音声セッションを終了します。
Tab字幕の切り替え、音声ピッカー、割り込み可能な場合はオーブ、ミュート、終了の順にフォーカスが移動します。
  • セッションにはラベル付きのリージョンが使われ、ステータス行は polite なライブリージョンなので、「Listening」や「Thinking」といった状態の変化が通知されます。
  • ビジュアルは、割り込み可能な間は「Interrupt」というラベルの付いた本物のボタンになり、それ以外は無効になります。
  • ミュートは aria-pressed を使い、すべてのコントロールにショートカットを示すツールチップがあります。
  • 字幕により、話された内容すべてにテキスト代替が提供され、音声なしで返答を表示することもできます。
  • 入力欄に入力している間、Escape 以外のショートカットは無視されます。
  • モーションの低減が有効な場合、オーブとドットは静止し、状態に応じてのみ変化します。
  • WebGL が使えない場合、すべてのシェーダースタイルは静止したグラデーションにフォールバックし、ドットはどこでも動作します。

使用技術

Voice Mode を構成する無料のHextaUIコンポーネントです。それぞれ単独でインストールできます。

コード

9 個のファイルを components/blocks/voice-mode に追加しました。