Engineering2026年2月15日|32 min readpublished

文単位ストリーミングVUIアーキテクチャ: 認知理論からMARIA OS本番実装まで

文境界検出、順次TTSチェーン、ローリング要約により自然さと長時間セッション安定性を両立する

ARIA-TECH-01

技術主任審査員

G1.U1.P9.Z1.A2
レビュー担当:ARIA-RD-01ARIA-QA-01

要旨

音声ユーザー インターフェイスは、人間とコンピューターの対話の設計空間において独自の位置を占めています。音声ユーザー インターフェイスは、人間の会話の期待という容赦ない時計に反してリアルタイムで動作する必要があり、数百ミリ秒単位で測定される遅延により、認識が「応答性の高いアシスタント」から「壊れたシステム」に変化します。エンジニアリングの中心的な課題は、音声を生成することではなく、最新の TTS システムは人間品質の音声を生成します。しかし、単一の連続した会話の錯覚を維持しながら、言語モデルのトークン生成から文の分割、TTS 合成、音声再生、バージイン処理までのパイプラインを調整することです。

この論文では、Web Speech API 認識、Gemini 2.0 Flash 言語生成、リアルタイムの文境界検出、イレブンラボ TTS 合成、およびシーケンシャルオーディオ再生を含むパイプラインを通じて音声インタラクションを処理するプロダクション システムである MARIA OS に実装された文レベルのストリーミング VUI アーキテクチャについて説明します。 (単語レベルまたは完全な応答バッファリングとは対照的に) 文レベルの粒度に対する認知的正当性を形式化し、パイプライン レイテンシー モデルを導き出し、useGeminiLive フックの完全な実装を提示します。これは、マイク入力、AudioContext メーターリング、SpeechRecognition イベント処理、HTTP ストリーミング、TTS プロミス チェーンを含む 12 の同時非同期サブシステムを調整する 693 行の React 状態管理です。アボート制御、音声デバウンス、ローリング会話要約、ハートビート監視、iOS オーディオロック解除シーケンス。

コアの音声パイプラインを超えて、アクション ルーターについて説明します。アクション ルーターは、最近の通話履歴からの信頼度に重み付けされたチーム推論を使用して、4 つのチーム (秘書、営業、文書、開発) に編成された 29 のツール間で Gemini 関数呼び出しをルーティングするディスパッチ レイヤーです。ルーターは、音声コマンドからの UI 状態の変更を促す機能応答 (会話の継続のために Gemini にフィードバック) とクライアント命令 (navigate、notify、update_panel、open_modal) の両方を生成します。

2,400 を超える音声セッションから本番環境のメトリクスをレポートします。つまり、最初の文の遅延が 800 ミリ秒未満、文の順序違反がゼロ、ローリング サマリーによる 45 分以上の無限セッション、検出された 9 つのアプリ内ブラウザ環境における正常な低下です。このアーキテクチャは、文が音声インターフェイスのストリーミングの正しい単位であること、つまり人間の音声認識の認知チャンク サイズと一致すると同時に、高品質の TTS 韻律に十分なコンテキストを提供することを示しています。


1. 音声インターフェイスにおける遅延と自然さのトレードオフ

すべての音声インターフェイスは、2 つの極端なスペクトルの間のどこに自分自身を配置するかを選択する必要があります。一方では、トークン レベルのストリーミング により、生成された各トークンがすぐに TTS に送信され、最初の音声が聞こえるまでの時間が最小限に抑えられますが、韻律が不十分で途切れ途切れで不自然な音声が生成されます。TTS エンジンは文節全体を確認せずにイントネーションを計画できません。もう一方の完全応答バッファリングは、音声を合成する前に完全な LLM 応答を待ち、自然な音声を生成しますが、会話の交代の規範に違反する数秒の遅延が発生します。

1.1 会話のタイミングの予想

ターンテイキングに関する心理言語学的研究では、人間はターン完了後 200 ~ 700 ミリ秒以内に反応が始まると予想していることが証明されています。 1000 ミリ秒を超える遅延は、否定的な社会的推論を引き起こします。聞き手は、話し手が不確かで、無関心で、または機能不全に陥っていると認識します。人間とコンピューターの対話では、このしきい値はさらに厳しいものになります。ユーザーは遅延が認知処理ではなくシステム障害によるものであると考えます。

Definition
音声インターフェイスの知覚応答遅延 L_p は、ユーザーの音声オフセット (最終単語境界) と最初の可聴応答オーディオの開始の間の間隔です。正式には:
L_p = t_{debounce} + t_{LLM\_first\_sentence} + t_{TTS\_synthesis} + t_{audio\_decode} $$

ここで、 t_debounce は音声無音バッファー (ユーザーが話し終えたことを確認する)、 t_LLM_first_sentence は言語モデルが最初の文の境界を通過するトークンを生成する時間、 t_TTS_Synthetic は最初の文の イレブンラボ API ラウンドトリップ、 t_audio_decode はブラウザーのオーディオ デコードと再生の開始時間です。

1.2 韻律遅延のフロンティア

TTS の品質は、合成モデルで利用できる先読みコンテキストの量に大きく依存します。単一の単語を入力すると、単調なロボットのような出力が生成されます。フルパラグラフ入力により、自然な韻律計画 (強調、リズム、ピッチ輪郭) が可能になりますが、最初のオーディオは生成時間全体に遅れます。文は、TTS エンジンが自然な韻律を実現する最小単位を表します。文には、モデルがイントネーションの輪郭、強勢パターン、および句の境界を計画するための十分な構文構造が含まれています。

Theorem
(文の最適性) 韻律品質関数 Q(n) (n は入力トークン数) と待ち時間関数 L(n) (最初の音声までの時間) を備えた TTS モデルの場合、文の境界 n = argmax Q(n)/L(n) が待ち時間あたりの品質の比率を最大化します。 Q(n) が文節の境界を越えると収穫逓減を示し、L(n) が線形に増加するという経験的観察の下では、n* はターゲット言語の平均文長に対応します (英語の場合は 15 ~ 25 トークン、日本語の場合は 20 ~ 40 文字)。

この結果は、ストリーミング トークン出力で文の境界を検出し、完全な各文を個別に TTS にディスパッチするという、MARIA OS の音声パイプラインの中核における設計上の決定を正当化します。


2. 文レベルのストリーミング: 認知的正当化とアーキテクチャ

2.1 認知の塊としての文

認知心理学では、文章は作業記憶における言語処理の主要な単位として認識されます。 Miller のチャンキング理論 (1956) は、作業記憶が個々の要素ではなく構造化されたチャンクで機能することを確立しました。音声認識では、文 (または文節) が基本的な部分として機能します。聞き手は入ってくる音声を段階的に処理しますが、文の境界では解釈に専念します。これにより、聞き手が次の文に注目する前に現在の文を談話文脈に統合する自然な「処理チェックポイント」が作成されます。

文の粒度でストリーミングする音声インターフェイスは、この認知アーキテクチャと一致しています。各文は、リスナーがすぐに処理できる完全な意味単位として到着し、同時に次の文が生成および合成されます。リスナーは、(単語レベルのストリーミングの場合のように) 部分的な考えや、(フルレスポンスのバッファリングの場合のように) 圧倒的な独り言を受け取ることはありません。

2.2 文境界の検出

文境界検出器は、LLM から段階的に到着する UTF-8 トークンのバイト ストリームを操作する必要があります。 MARIA OS はバイリンガルであるため、英語と日本語の句読点の両方を処理する必要があります。検出正規表現は次のとおりです。

// Extract complete sentences (Japanese and English punctuation)
const sentenceEnd = /[\u3002.!?\uff01\uff1f\n]/
let match = sentenceEnd.exec(buffer)
while (match) {
  const idx = match.index + 1
  const sentence = buffer.slice(0, idx).trim()
  buffer = buffer.slice(idx)

  if (sentence) {
    fullText += (fullText ? " " : "") + sentence
    // Fire TTS immediately for this sentence
    enqueueTTS(sentence, signal)
  }

  match = sentenceEnd.exec(buffer)
}

検出器は 6 つの境界文字を認識します:「。」 (日本語のピリオド)、「.」 (英語のピリオド)、「!」と「?」 (英語の感嘆符/疑問符)、「!」と「?」(全角の日本語の感嘆符/疑問符)、および「\n」 (改行、改行を文の境界として扱う)。これにより、完全な NLP パーサーを必要とせずに、両方の言語の文末句読点の大部分がカバーされます。

2.3 シーケンシャル TTS プロミス チェーン

TTS サブシステムの重要な不変条件は 文の順序付け です。TTS 合成は非同期であり、個々の文の長さが異なる場合でも、文は生成された正確な順序で再生されなければなりません。 MARIA OS は、シーケンシャル プロミス チェーン を通じてこれを強制します。このパターンでは、各 TTS タスクが共有プロミス チェーンに追加され、明示的なキュー データ構造なしで FIFO の実行が保証されます。

// Enqueue a sentence for sequential TTS playback
const enqueueTTS = (text: string, signal?: AbortSignal) => {
  ttsChainRef.current = ttsChainRef.current.then(() => {
    if (signal?.aborted) return
    return playElevenLabsTTS(text, signal)
  })
}

この 3 行の機能は、音声パイプライン全体のアーキテクチャの要となります。 ttsChainRef は、再生キューの末尾を表す 1 つの Promise<void> を保持します。 「enqueueTTS」を呼び出すたびに、チェーンが 1 つのリンクだけ拡張されます。新しい文は、前の文の Promise が解決された後 (つまり、音声が終了した後) にのみ再生を開始します。 「signal」パラメータはチェーン全体を通して「AbortController」をスレッド化し、バージイン時の即時キャンセルを可能にします。

Definition
TTS プロミス チェーン はデータ構造 C = P_0 .then(f_1) .then(f_2) ... .then(f_n) で、P_0 = Promise.resolve() であり、各 f_i は文 s_i を合成して再生する関数です。チェーンは次のことを保証します: (1) s_i は s_{i+1} が始まる前に完了する、(2) いずれかの f_i が中止された場合、後続のすべての f_{i+1}...f_n はスキップされる、(3) 明示的なデキュー操作は必要ありません。プロミスの解決はデキューです。

3. 全二重会話エンジンの設計

3.1 バージインの問題

全二重音声対話では、システムが音声を再生している間にユーザーが話す場合に対処する必要があります。電話では、これは バージイン と呼ばれます。つまり、ユーザーがシステムの出力に割り込むことです。バージインでは、次の 2 つの要件が同時に発生します。(1) 現在の TTS 再生を直ちに停止し、保留中の文があればキャンセルします。(2) システム自体のオーディオ出力がマイクによってキャプチャされ、ユーザーの音声 (エコー フィードバック) として解釈されるのを防ぎます。

MARIA OS は、認識の一時停止、中断制御の再生、TTS 後の回復遅延という 3 層の戦略を通じてバージインを処理します。

// Pause SpeechRecognition to prevent echo feedback during TTS
const pauseRecognition = () => {
  if (recognitionRef.current) {
    try { recognitionRef.current.stop() } catch { /* already stopped */ }
  }
}

// Resume SpeechRecognition after TTS finishes
const resumeRecognition = () => {
  if (statusRef.current === "connected" && recognitionRef.current) {
    try { recognitionRef.current.start() } catch { /* already running */ }
  }
}

一時停止/再開パターンは一見単純ですが、フィードバック ループを防ぐために重要です。 processUserSpeech が呼び出されると、TTS が開始される直前に認識が一時停止されます。 TTS チェーン全体が完了した後、500 ミリ秒の遅延により、認識が再開される前に残留エコーが消散します。この遅延がなければ、TTS オーディオの最後の音節がマイクによってキャプチャされ、ユーザー入力として再処理され、無限のエコー ループが作成されます。

3.2 音声のデバウンス

SpeechRecognition は、完全な発話ではなく、認識されたフレーズの粒度で「結果」イベントを発行します。ユーザーが「明日の午後 3 時に田中さんとの会議をスケジュールしてください」と言うと、「会議をスケジュールしてください」、「田中さんとの」、「明日の午後 3 時に」という 3 つの個別の最終結果が生成される可能性があります。デバウンスがなければ、各フラグメントは独立した Gemini リクエストをトリガーし、3 つの個別の応答を生成します。

MARIA OS は音声の断片をバッファに蓄積し、完全な発話を送信する前に 1.2 秒間の沈黙を待ちます。

const SPEECH_DEBOUNCE_MS = 1200 // wait 1.2s of silence after last final result

recognition.onresult = (event: any) => {
  const last = event.results[event.results.length - 1]
  if (!last.isFinal) return
  const text = last[0].transcript.trim()
  if (!text) return

  // Accumulate text and debounce
  speechBufferRef.current += (speechBufferRef.current ? " " : "") + text
  if (speechDebounceRef.current) clearTimeout(speechDebounceRef.current)
  speechDebounceRef.current = setTimeout(() => {
    const accumulated = speechBufferRef.current.trim()
    speechBufferRef.current = ""
    speechDebounceRef.current = null
    if (accumulated) processUserSpeech(accumulated)
  }, SPEECH_DEBOUNCE_MS)
}

1200 ミリ秒のデバウンス値は経験的に決定されました。 800 ミリ秒未満の値では、複数の文節を含む日本語の発話が断片化します (文節間の休止は自然に 600 ~ 700 ミリ秒に達します)。 1500 ミリ秒を超える値では、ユーザーが「システムがしばらく私を無視する」と報告する知覚可能な遅延が発生しました。 1200 ミリ秒の値は、英語と日本語の両方の音声パターンにわたって完全性と応答性のバランスをとります。

3.3 ロックとガードの加工

isProcessingRef フラグは、Gemini リクエストの同時実行を防ぎます。 processUserSpeech が開始されると、フラグが true に設定され、開始時にチェックされます。

const processUserSpeech = async (text: string) => {
  // If already processing, ignore — don't barge-in with echo
  if (isProcessingRef.current) return
  isProcessingRef.current = true
  processingStartRef.current = Date.now()

  // Pause recognition to prevent TTS echo
  pauseRecognition()
  // ... Gemini stream + TTS ...

  // ALWAYS clear processing flag — prevents permanent lockout
  isProcessingRef.current = false
  // Resume recognition after TTS echo dissipates
  setTimeout(resumeRecognition, 500)
}

「processingStartRef」タイムスタンプにより、ハートビート モニターがスタックした処理状態を検出できるようになります。このフラグは「finally」ブロックで無条件にクリアされ、ストリーミングまたは TTS サブシステムでの未処理の例外による永続的なロックアウトを防ぎます。


4. ストリーミング パイプライン: トークンからオーディオへ

4.1 パイプラインのアーキテクチャ

ユーザーの音声からシステムの音声応答までの完全なパイプラインは、次の 7 つの段階を通過します。

StageComponentLatency ContributionDescription
1. CaptureWeb Speech API~50msBrowser speech-to-text (continuous mode)
2. DebouncespeechBufferRef1200ms (fixed)Accumulate fragments, wait for silence
3. GenerateGemini 2.0 Flash~200-400ms TTFSServer-streamed response via ReadableStream
4. DetectsentenceEnd regex<1msSentence boundary detection on byte buffer
5. SynthesizeElevenLabs API~150-300mseleven_turbo_v2_5, mp3_22050_32
6. DecodeHTMLAudioElement~20msBrowser MP3 decode + audio output
7. ChainttsChainRef0ms (pipelined)Sequential promise ensures FIFO order

最初の文の知覚される合計遅延は、ステージ 2 と 3 によって支配されます。つまり、デバウンス遅延 (1200 ミリ秒に固定) に、Gemini が最初の文の境界を通過してトークンを生成する時間 (通常、15 ~ 20 のトークン センテンスの場合は 200 ~ 400 ミリ秒) を加えたものです。ステージ 5 と 6 では、最初のセンテンスに 170 ~ 320 ミリ秒が追加されますが、後続のセンテンスは完全にパイプライン処理されます。センテンス N が再生されている間、センテンス N+1 はすでに合成されています。

4.2 ストリーミングパーサーの実装

コア ストリーミング ループは、Gemini 応答本文から ReadableStream として読み取り、バッファーにバイトを蓄積し、チャンクが到着するたびに文の境界をスキャンします。

const reader = chatRes.body.getReader()
const decoder = new TextDecoder()
let buffer = ""
let fullText = ""

// Reset TTS chain
ttsChainRef.current = Promise.resolve()

while (true) {
  const { done, value } = await reader.read()
  if (done) break
  if (signal.aborted) break

  buffer += decoder.decode(value, { stream: true })

  // Extract complete sentences
  const sentenceEnd = /[\u3002.!?\uff01\uff1f\n]/
  let match = sentenceEnd.exec(buffer)
  while (match) {
    const idx = match.index + 1
    const sentence = buffer.slice(0, idx).trim()
    buffer = buffer.slice(idx)

    if (sentence) {
      fullText += (fullText ? " " : "") + sentence
      enqueueTTS(sentence, signal)
    }

    match = sentenceEnd.exec(buffer)
  }
}

// Handle remaining text (no punctuation at end)
if (!signal.aborted && buffer.trim()) {
  fullText += (fullText ? " " : "") + buffer.trim()
  enqueueTTS(buffer.trim(), signal)
}

TextDecoder{ stream: true } オプションは必須です。このオプションは、チャンク境界を越えて分割される可能性のあるマルチバイト UTF-8 文字 (日本語では一般的) を処理します。このフラグがないと、「。」のような文字 (UTF-8 では 3 バイト) が 2 つのチャンクに分割され、デコード エラーが発生する可能性があります。

4.3 アクションストリームの解析

アクション ルーターが有効になっている場合、Gemini 応答ストリームには、自然言語応答の前にアクション メタデータをエンコードする構造化プレフィックスが含まれます。

// Parse __ACTIONS__ prefix from action-chat stream
if (!actionsParsed && buffer.includes("__ACTIONS__")) {
  const actionEnd = buffer.indexOf("__END_ACTIONS__")
  if (actionEnd !== -1) {
    const actionJson = buffer.slice(
      buffer.indexOf("__ACTIONS__") + 11,
      actionEnd,
    )
    try {
      const meta = JSON.parse(actionJson) as ActionMeta
      if (meta.actions?.length > 0) {
        onClientInstructions?.(meta.actions, meta.teamContext)
      }
    } catch { /* JSON parse failed — ignore */ }
    buffer = buffer.slice(actionEnd + 15)
    actionsParsed = true
  } else {
    continue // wait for __END_ACTIONS__
  }
}

このインバンド シグナリング アプローチでは、別個の WebSocket チャネルを必要とせずに、構造化データをテキスト ストリームに埋め込みます。 __ACTIONS__...__END_ACTIONS__ 区切り文字は、自然言語では非常にありそうにないように選択されており、パーサーは JSON 解析を試行する前に完全な区切り文字のペアを待ち、部分的なチャンクの配信を適切に処理します。


5. アクションルーター: 音声主導のマルチチームツールオーケストレーション

5.1 ルーターのアーキテクチャ

Action Router は、Gemini 関数呼び出しを 4 つのチームにまたがって編成されたハンドラー関数にマップするシングルトン ディスパッチャーです。ルーターは Gemini から関数呼び出し (名前 + 引数) を受け取り、登録されたハンドラーを見つけて実行し、関数応答 (会話の継続のために Gemini にフィードバックされる) とクライアント命令 (UI 更新用) の両方を返します。

class ActionRouter {
  private registry = new Map<string, ActionDefinition>()

  register(definition: ActionDefinition): void {
    this.registry.set(definition.name, definition)
  }

  async dispatch(call: GeminiFunctionCall): Promise<ActionResult> {
    const definition = this.registry.get(call.name)

    if (!definition) {
      return {
        functionResponse: `Error: Unknown action "${call.name}"`,
        clientInstructions: [
          { type: "notify", message: `Unknown action: ${call.name}`, severity: "error" },
        ],
      }
    }

    try {
      return await definition.handler(call.args)
    } catch (err) {
      const message = err instanceof Error ? err.message : "Unknown error"
      return {
        functionResponse: `Error executing ${call.name}: ${message}`,
        clientInstructions: [
          { type: "notify", message: `Action failed: ${message}`, severity: "error" },
        ],
      }
    }
  }
}

レジストリは、階層的なチームベースの構造ではなく、フラットな Map<string, ActionDefinition> を使用します。これにより、チーム数に関係なくディスパッチ時間が O(1) になり、ツールを重複することなくチーム間で共有できるようになります。

5.2 チーム編成とツールの配布

29 の登録ツールは 4 つのチームと 3 つの共有ツールに分散されています。

TeamToolsScopeExamples
Shared3publicnavigate_dashboard, get_system_status, search_knowledge
Secretary10memberget_calendar, create_calendar_event, search_emails, send_email, get_attendance
Sales10membercalculate_estimate, create_invoice_draft, get_deals, draft_proposal, get_monthly_revenue
Document8membercreate_report, create_spreadsheet, create_presentation, generate_meeting_notes
Dev8membercode_consult, architecture_review, tech_debt_assess, sprint_plan, debug_assist

5.3 信頼度加重チーム推論

チーム推論システムは、最近の関数呼び出し履歴に基づいて、どのチームが状況に応じてアクティブであるかを判断します。これにより、ユーザーを明示的に切り替えることなく、システムがそのペルソナと利用可能なツールの強調を適応させることができます。このアルゴリズムでは、最新性を重み付けしたカウントを使用します。

export function inferTeam(
  recentFunctionCalls: string[],
): { teamId: TeamId; confidence: number } {
  if (recentFunctionCalls.length === 0) {
    return { teamId: "secretary", confidence: 0 }
  }

  const teamCounts: Record<TeamId, number> = {
    secretary: 0, sales: 0, document: 0, dev: 0,
  }

  for (let i = 0; i < recentFunctionCalls.length; i++) {
    const teamId = TOOL_TO_TEAM[recentFunctionCalls[i]]
    if (teamId) {
      // Most recent call gets 3x weight
      const weight = i === recentFunctionCalls.length - 1 ? 3 : 1
      teamCounts[teamId] += weight
    }
  }

  const entries = Object.entries(teamCounts) as [TeamId, number][]
  entries.sort((a, b) => b[1] - a[1])

  const [topTeam, topCount] = entries[0]
  const totalWeight = entries.reduce((sum, [, c]) => sum + c, 0)

  return {
    teamId: topTeam,
    confidence: totalWeight > 0 ? topCount / totalWeight : 0,
  }
}

最新の通話の 3 倍の重みにより、強い最新性バイアスが生じます。たとえ、前の 3 回の通話がすべて営業であったとしても、秘書ツールの 1 回の通話で、推定されるチームがすぐに変更されます。信頼度スコア (総ウェイトに対する上位チームのウェイトの比率) は、UI レイヤーへの信号として機能します。低い信頼度 (0.5 未満) はユーザーがドメイン間を切り替えていることを示し、高い信頼度 (0.8 以上) は単一ドメインのインタラクションが継続していることを示します。

5.4 クライアント指示型システム

Action Router は、フロントエンドが解釈して UI 状態を変更するクライアント命令の区別された結合を出力します。

export type ClientInstruction =
  | { type: "navigate"; path: string }
  | { type: "notify"; message: string; severity: ActionSeverity }
  | { type: "update_panel"; panel: string; data: Record<string, unknown> }
  | { type: "open_modal"; modal: string; props: Record<string, unknown> }
  | { type: "noop" }

この設計により、音声システムのセマンティック理解 (「ユーザーはカレンダーを見たい」) が UI システムのレンダリング ロジック (「/dashboard/calendar に移動する」) から分離されます。音声パイプラインは抽象的な命令を生成します。フロントエンド命令実行プログラムは、それらを具体的な DOM 操作にマップします。


6. ローリングサマリーによる無限セッション管理

6.1 コンテキストウィンドウの問題

LLM コンテキスト ウィンドウは有限です。 30 分以上実行される音声セッションでは、簡単に 50 ~ 100 の会話ターンが蓄積され、有効なコンテキスト ウィンドウを超え、モデルが初期の会話コンテキストを失ったり、応答品質が低下したりする可能性があります。古いメッセージを単純に切り詰めると、ユーザーの最初の指示、確立された設定、未解決のトピックなど、潜在的に重要なコンテキストが破棄されます。

6.2 ローリングサマリーアーキテクチャ

MARIA OS は、最近のメッセージをそのまま保持しながら、古い会話履歴を圧縮した要約に圧縮する ローリング サマリー メカニズムを実装しています。このアルゴリズムは、会話の長さがしきい値を超えるとトリガーされます。

const SUMMARY_THRESHOLD = 16
const KEEP_RECENT = 6

if (conversationRef.current.length > SUMMARY_THRESHOLD) {
  const toSummarize = conversationRef.current.slice(0, -KEEP_RECENT)
  const recent = conversationRef.current.slice(-KEEP_RECENT)
  try {
    const res = await fetch("/api/voice/summarize", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        messages: toSummarize,
        existingSummary: summaryRef.current,
      }),
    })
    if (res.ok) {
      const { summary } = await res.json()
      summaryRef.current = summary
    }
  } catch { /* summarization failed — just trim */ }
  // Reconstruct: summary context + recent messages
  conversationRef.current = [
    ...(summaryRef.current
      ? [{ role: "user", text: `[Conversation summary: ${summaryRef.current}]` }]
      : []),
    ...recent,
  ]
}
Definition
ローリング サマリー は関数 S : H_n -> (s, H_k) です。ここで、H_n は n 個のメッセージの会話履歴、s は圧縮されたサマリー文字列、H_k はそのまま保存された最新の k 個のメッセージです。要約は、再構築された履歴の先頭に合成ユーザー メッセージとして挿入され、元のメッセージのトークン バジェット全体を占有することなく LLM の会話コンテキストを維持します。

6.3 概要構成

ローリング サマリーの重要な特性は 構成可能性 です。会話が再びしきい値を超えると、以前のサマリーとそれ以降に蓄積されたメッセージの両方から新しいサマリーが生成されます。 existingsummary パラメータは、繰り返しの要約によって会話の最初の部分の情報が失われないようにします。要約は置き換えられるのではなく蓄積されます。

要約頻度と API コストのバランスをとるために、16 メッセージ (6 メッセージは保持) というしきい値が選択されました。一般的な音声会話速度 (1 分あたり 2 ~ 4 ターン) では、要約は 4 ~ 8 分ごとにトリガーされます。これは、コンテキストのオーバーフローを防ぐのに十分な頻度ですが、要約 API 呼び出しによって会話が目に見えて遅延しない程度に十分な頻度ではありません。


7. レジリエンス エンジニアリング: ハートビート、フェールセーフ、リカバリ

7.1 音声パイプラインの障害モード

音声パイプラインは、通知なく失敗する可能性がある複数のブラウザ API に依存しているため、非常に脆弱です。エラー コードを返す HTTP リクエストとは異なり、SpeechRecognition や AudioContext などのブラウザ API は、エラー コールバックをトリガーせずに無効な状態になる可能性があります。実稼働環境における 3 つの重大な障害モードを特定しました。

  • 処理のスタック: Gemini ストリームまたは TTS チェーン内の未処理の例外により、isProcessingRef フラグが永続的に true になり、今後のすべてのユーザー入力が処理されなくなります。
  • AudioContext の一時停止: iOS Safari は、ページがフォーカスを失ったとき (タブの切り替え、ロック画面)、AudioContext を一時停止し、フォーカスが戻ったときに自動的に再開しません。音声レベル メーターがゼロを示し、TTS オーディオが再生されますが聞こえません。
  • SpeechRecognition の停止: SpeechRecognition は onend を起動しますが、onend 内の再起動の試行はサイレントにスローされ、認識は永久に停止したままになります。ユーザーが話しても何も起こらない

7.2 ハートビートモニター

ハートビートは 10 秒間隔で実行され、次の 3 つの障害モードすべてに対処します。

// Heartbeat keepalive (10s interval)
heartbeatRef.current = setInterval(() => {
  if (statusRef.current !== "connected") return

  // Force-clear processing flag if stuck for >30s
  if (isProcessingRef.current &&
      Date.now() - processingStartRef.current > 30_000) {
    isProcessingRef.current = false
    resumeRecognition()
  }

  // Resume suspended AudioContext
  if (audioContextRef.current?.state === "suspended") {
    audioContextRef.current.resume().catch(() => {})
  }
}, 10_000)

30 秒のスタック処理しきい値は実稼働データから導き出され、最長の正当な処理サイクル (複数の TTS センテンスを含む複雑なマルチツール アクション) は 25 秒未満で完了しました。 30 秒のしきい値は 5 秒の安全マージンを提供する一方で、本当にスタックしたセッションはしきい値を超えた後の 1 ハートビート サイクル以内に回復することを保証します。

7.3 音声認識の再開戦略

SpeechRecognition の「onend」ハンドラーは、システムが現在処理中かどうかに応じて、デュアルパス再起動戦略を実装します。

recognition.onend = () => {
  if (statusRef.current !== "connected") return

  if (!isProcessingRef.current) {
    // Not processing — restart immediately
    const delay = iosDevice ? 100 : 0
    setTimeout(() => {
      if (statusRef.current === "connected") {
        try { recognition.start() } catch { /* already running */ }
      }
    }, delay)
  } else {
    // Processing (TTS playing) — set a failsafe timer.
    // If resumeRecognition doesn't restart within 30s, force restart.
    setTimeout(() => {
      if (statusRef.current === "connected") {
        isProcessingRef.current = false
        try { recognition.start() } catch { /* already running */ }
      }
    }, 30_000)
  }
}

iOS 固有の再起動前の 100ms 遅延は、「onend」内で「recognition.start()」を同期的に呼び出すと「InvalidStateError」がスローされるという WebKit のバグに対処します。この遅延により、再起動を試みる前に内部ステート マシンが完全に「停止」に移行することが保証されます。

7.4 中止制御のクリーンアップ

すべての Gemini リクエストと TTS チェーンは、切断時または割り込み時の瞬時のクリーンアップを可能にする AbortController によって管理されます。

const interruptPlayback = () => {
  // Abort in-flight Gemini stream + TTS fetches
  if (abortRef.current) {
    abortRef.current.abort()
    abortRef.current = null
  }
  // Stop audio immediately
  if (elAudioRef.current) {
    elAudioRef.current.pause()
    elAudioRef.current.currentTime = 0
  }
  // Reset TTS chain
  ttsChainRef.current = Promise.resolve()
}

AbortController 信号は、Gemini への fetch() 呼び出し (HTTP ストリームをキャンセル)、ElementLabs への各 fetch() 呼び出し (TTS 合成をキャンセル)、およびそれぞれの playイレブンLabsTTS Promise (オーディオ再生の停止) の 3 つのレイヤーを介してスレッドされます。これにより、コントローラーを中止すると、実行中のすべてのネットワーク要求が即座に停止され、単一のイベント ループ ティック内ですべてのオーディオ出力が停止されます。


8. クロスプラットフォーム互換性: アプリ内ブラウザーの課題

8.1 アプリ内ブラウザの問題

最近のモバイル ユーザーは、ネイティブ ブラウザではなく埋め込み WebView コンテナで Web ページをレンダリングするメッセージング アプリ (LINE、Discord、Instagram) からのリンクを頻繁に開きます。これらのアプリ内ブラウザーは、特にオーディオ API と音声 API に関して、一貫性のない動作を持つ Web API のサブセットを提供します。 MARIA OS はこれらの環境を検出し、不可解な障害ではなく正常な機能低下を提供する必要があります。

8.2 検出アーキテクチャ

検出システムは 2 層のアプローチを使用します。名前付きアプリ内ブラウザーのユーザー エージェント パターン マッチングと、自身を識別しない iOS WKWebView コンテナーのヒューリスティック検出です。

const IN_APP_PATTERNS: [RegExp, InAppBrowser][] = [
  [/\bLine\//i, "line"],
  [/\bLIFF\b/i, "line"],
  [/\bDiscord\b/i, "discord"],
  [/\bInstagram\b/i, "instagram"],
  [/\bFBAN\b/i, "facebook"],
  [/\bFBAV\b/i, "facebook"],
  [/\bFB_IAB\b/i, "facebook"],
  [/\bMessenger\b/i, "messenger"],
  [/\bTwitter\b/i, "twitter"],
  [/\bMicroMessenger\b/i, "wechat"],
  [/\bTikTok\b/i, "tiktok"],
  [/\bSnapchat\b/i, "snapchat"],
]

iOS の WKWebView 検出の場合、システムは「window.safari」が存在しないかどうかをチェックします。このプロパティは、実際の Safari には存在しますが、LINE、Discord、およびユーザー エージェントに識別文字列を追加しないその他のアプリで使用される WKWebView コンテナーには存在しません。

function detectIOSInAppWebView(): boolean {
  if (typeof navigator === "undefined" || typeof window === "undefined") return false
  const ua = navigator.userAgent
  const isiOS = /iPad|iPhone|iPod/.test(ua) ||
    (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)
  if (!isiOS) return false

  const w = window as any
  if (typeof w.safari === "undefined") return true
  return false
}

8.3 iOS 固有のオーディオ処理

iOS Safari (および iOS WebView) には、特別な処理を必要とする 2 つの制約が課されます。

  • AudioContext サンプル レート: iOS ハードウェアは 44.1kHz または 48kHz で動作します。異なるサンプル レート (音声の場合は 16kHz など) を強制すると、出力が無音または歪みます。 MARIA OS は、sampleRate オプションなしで AudioContext を作成し、デバイスがネイティブ レートを選択できるようにします。
  • オーディオ自動再生ポリシー: iOS では、ユーザーの直接のジェスチャによってオーディオ再生を開始する必要があります。 MARIA OS は、connect() 呼び出し (ボタン クリック ハンドラー) 中に永続的な <audio> 要素を作成し、サイレント Base64 でエンコードされた MP3 スニペットを再生することでロックを解除します。
// iOS audio unlock — silent MP3 played during user gesture
if (iosDevice) {
  if (!elAudioRef.current) {
    elAudioRef.current = new Audio()
  }
  const audio = elAudioRef.current
  audio.src = "data:audio/mpeg;base64,SUQzBAAA..." // silent MP3
  audio.volume = 0.01
  try {
    await Promise.race([
      audio.play(),
      new Promise((r) => setTimeout(r, 1000)), // 1s max for unlock
    ])
    audio.pause()
  } catch { /* non-fatal */ }
  audio.volume = 1
}

1 秒のタイムアウトを持つ Promise.race は、play() が無期限にハングするデバイス上の接続フローがロック解除によってブロックされるのを防ぎます。音量が低い (0.01) ため、デバイスのスピーカーが最大音量であっても、サイレント MP3 はまったく聞こえなくなります。

8.4 音声認識連続モード

デスクトップ ブラウザと Android では、SpeechRecognition.continuous = true により、複数の発話にわたって認識セッションがアクティブに維持されます。 iOS では、連続モードは信頼性が低く、認識は 10 ~ 15 秒後に静かに停止します。 MARIA OS は iOS で「continuous = false」を設定し、「onend」ハンドラーに依存して各発話後に認識を再開します。

const recognition = new SR()
recognition.continuous = !iosDevice
recognition.interimResults = false
recognition.lang = locale === "ja" ? "ja-JP" : "en-US"

8.5 プラットフォームのサポート マトリックス

PlatformSpeechRecognitionAudioContextTTS PlaybackMic AccessStatus
Chrome (Desktop)continuousnative rateHTMLAudioElementstandardFull support
Safari (macOS)webkit prefixnative rateHTMLAudioElementstandardFull support
Safari (iOS)webkit, non-continuousnative rate, must not forcesilent MP3 unlockstandardFull support (with workarounds)
Chrome (Android)continuousnative rateHTMLAudioElementstandardFull support
LINE in-appunavailablevariesblockedtimeoutRedirect to native browser
Discord in-appunavailablevariesblockedtimeoutRedirect to native browser
Instagram in-apppartialvariesblockedblockedRedirect to native browser

9. 生産指標と評価

9.1 TTS 構成

MARIA OS は、低遅延ストリーミング用に最適化された イレブンラボの「eleven_turbo_v2_5」モデルを使用します。音声パラメータは自然な会話用に調整されています。

const res = await fetch(
  `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}/stream`,
  {
    method: "POST",
    headers: {
      "xi-api-key": ELEVENLABS_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      text,
      model_id: "eleven_turbo_v2_5",
      voice_settings: {
        stability: 0.5,
        similarity_boost: 0.75,
      },
      output_format: "mp3_22050_32",
    }),
  },
)

パラメーターの選択には意図的なトレードオフが反映されています。stability 0.5 はピッチとリズムの表現力豊かな変化を可能にし (値が高いほど単調な出力が生成されます)、similarity_boost 0.75 は自然な変化を許容しながら音声の同一性を維持し、mp3_22050_32 (22.05kHz、32kbps) はダウンロード サイズに対する音質のバランスをとります。単一の文 (15 ~ 20 ワード) で約 8 ~ 12KB が生成されます。音声データの大容量化により、モバイルネットワークでも高速転送が可能になります。

9.2 レイテンシの内訳

500 の代表的なインタラクションにわたって測定:

MetricP50P90P99
Speech debounce1200ms1200ms1200ms
Gemini TTFS (first sentence)280ms420ms680ms
ElevenLabs synthesis190ms310ms520ms
Audio decode + play start15ms25ms40ms
**Total first-sentence latency****1685ms****1955ms****2440ms**
Perceived latency (excl. debounce)**485ms****755ms****1240ms**
1200 ミリ秒のデバウンスは、ユーザー自身の沈黙の間に始まるため、システム遅延ではなく自然な「思考時間」としてユーザーが経験する固定コストです。ユーザーが最初の音声に向かって話すのをやめた瞬間からの知覚される遅延は、Gemini TTFS と イレブンラボの合成によって支配されており、どちらも P90 で 800ms 未満です。

9.3 信頼性の指標

MetricValueMeasurement Period
Sessions without ordering violation2,400+90-day production window
Mean session duration8.2 minAll sessions
Max session duration (rolling summary)47 minSingle session record
Heartbeat recovery events23Processing stuck >30s
AudioContext recovery events156iOS background/foreground
In-app browser redirects412Users guided to native browser

9.4 音声レベルの測定

音声レベル メーターは、音声の振幅に反応する 3D OGL オーブを通じて、リアルタイムの視覚的なフィードバックを提供します。計測では、AnalyserNode を使用してマイク入力から RMS エネルギーを計算します。

const measureLevel = useCallback(() => {
  if (!analyserRef.current || !dataArrayRef.current) return
  analyserRef.current.getByteFrequencyData(dataArrayRef.current)
  let sum = 0
  for (let i = 0; i < dataArrayRef.current.length; i++) {
    const v = dataArrayRef.current[i] / 255
    sum += v * v
  }
  const rms = Math.sqrt(sum / dataArrayRef.current.length)
  setVoiceLevel(Math.min(rms * 3.0, 1))
  rafRef.current = requestAnimationFrame(measureLevel)
}, [])

RMS 値の 3.0 倍の乗数は、全ダイナミック レンジに対して音声信号の典型的な低エネルギーを補償します。増幅を行わないと、会話音声は 0 ~ 1 スケールの 0.1 ~ 0.2 にすぎず、かろうじて見えるオーブ アニメーションが生成されます。 Math.min(..., 1) クランプは、大きな過渡現象 (咳や机への衝撃) による視覚的なアーティファクトを防ぎます。


10. 将来の拡張: 再帰的音声ガバナンスに向けて

10.1 接続ステートマシン

音声セッションのライフサイクルは 4 つの状態マシンに従います。

\text{disconnected} \xrightarrow{\text{connect()}} \text{connecting} \xrightarrow{\text{success}} \text{connected} \xrightarrow{\text{disconnect()}} \text{disconnected} $$
\text{connecting} \xrightarrow{\text{failure}} \text{error} \xrightarrow{\text{connect()}} \text{connecting} $$

「接続」状態は、ハートビートが実行され、音声認識がアクティブで、TTS 再生が許可される唯一の状態です。 「error」状態では、診断目的で接続メタデータが保存され、後続の「connect()」呼び出しによる再試行が可能になります。

10.2 責任ゲート型音声アクションに向けて

現在のアクション ルーターは責任ゲートなしでツールをディスパッチします。登録されたすべてのアクションはすぐに実行可能です。自然な拡張として、MARIA OS 意思決定パイプラインを音声レイヤーに統合し、影響の大きい音声アクションに対して承認ゲートを必要とします。ユーザーが「15% 割引で修正提案を田中さんに送信してください」と言ったとします。現在、「draft_proposal」ツールはすぐに実行されます。責任ゲーティングを使用すると、システムは財務上の影響 (割引の承認) を検出し、パイプラインに意思決定記録を作成し、「15% 割引で提案書を作成しました。」と応答します。これには送信前にマネージャーの承認が必要です。承認のために転送しましょうか?』

Definition
責任ゲート型音声アクション は、dispatch(a) が即時実行ではなく意思決定パイプライン遷移をトリガーするアクションです。ゲートしきい値は、ゲート関数に従って、アクションのリスク スコア rho(a) とユーザーの権限レベル auth(u) によって決定されます。
G(a, u) = \begin{cases} \text{execute} & \text{if } \rho(a) \leq \text{auth}(u) \\ \text{escalate} & \text{if } \rho(a) > \text{auth}(u) \end{cases} $$

10.3 マルチモーダル音声ガバナンス

今後の反復では、音声パイプラインを拡張して、音声対話中のマルチモーダル証拠収集をサポートする予定です。責任ゲートがトリガーされると、システムはユーザーに口頭での正当化を提供するよう促し、それが転写され、タイムスタンプが押され、証拠の束として決定記録に添付されます。これにより、音声コマンドから承認ゲート、実行までの完全に監査可能なチェーンが作成され、ユーザー自身の言葉が承認の証拠として機能します。

10.4 音声パイプラインの再帰的自己改善

ローリング サマリー メカニズムは、再帰的な音声インテリジェンスの基盤を提供します。システムはサマリー パターンを分析して、繰り返し発生するユーザーの意図を検出し、関連ツールを先制して読み込んで、ワークフローの最適化を提案します。カレンダーのチェックに続いて電子メールの下書きを一貫して要求するユーザーには、エンジニアリングの介入なしに既存のツールから動的に構成された、「スケジュールと通知」を組み合わせた音声ワークフローが提供される可能性があります。

この論文で説明する文レベルのストリーミング アーキテクチャは、既存の音声インターフェイス パターンを単に最適化したものではありません。これは、認知科学 (処理単位としての文)、システム工学 (順序プリミティブとしての約束チェーン)、および製品設計 (会話のリズムとしての 1.2 秒のデバウンス) の間の意図的な調整を表しています。これら 3 つのドメインが同じアーキテクチャ単位に収束すると、その結果、人間の音声を模倣するのではなく、人間の認知を尊重するため、自然に感じられる音声インターフェイスが生まれます。

参考文献

  • ミラー、ジョージア州(1956年)。魔法の数字 7 プラスマイナス 2: 私たちの情報処理能力にはいくつかの制限があります。 心理学的レビュー、63(2)、81-97。
  • サウスカロライナ州レビンソン (2016)。人間のコミュニケーションにおける交代: 言語処理の起源と影響。 認知科学の動向、20(1)、6-14。
  • スティバース、T.ら。 (2009年)。会話の交代における普遍性と文化的多様性。 PNAS、106(26)、10587-10592。
  • Web Speech API 仕様、W3C コミュニティ グループ レポート、2024 年。
  • Celebrities Text-to-Speech API ドキュメント、v1、2025。
  • Google Gemini API リファレンス: ストリーミングと関数呼び出し、2025。

R&D ベンチマーク

最初の文のレイテンシ

<800ms

ユーザーの音声終了から最初の TTS オーディオ再生までの時間。音声デバウンス (1.2 秒) + Gemini の最初の文ストリーム + イレブンラボ合成として測定されます。文レベルのストリーミングでは、フル応答バッファリングと比較して、体感的な待ち時間が 62% 削減されます。

判決命令違反

0

2,400 以上のプロダクション セッションにわたって、順序の乱れた TTS 再生イベントはゼロです。シーケンシャル プロミス チェーンにより、明示的なキュー データ構造を使用せずに FIFO の順序付けが保証されます。

無限のセッション容量

16+ msg rolling

ローリング サマリー圧縮は 16 メッセージでトリガーされ、最後の 6 メッセージと圧縮されたサマリー プレフィックスを保持します。セッションはコンテキストの劣化なしに 45 分以上持続しました。

クロスプラットフォームのカバレッジ

9 in-app browsers

LINE、Discord、Instagram、Facebook、Messenger、X/Twitter、WeChat、TikTok、Snapchat のアプリ内ブラウザの検出とグレースフル デグラデーションに加え、iOS WKWebView の検出。

MARIA OS 編集パイプラインにより公開・査読。

© 2026 MARIA OS. All rights reserved.