⚠ 翻訳が古くなっています

VoiceChat のツール呼び出しと性能に関する最新情報は英語版を参照してください。

音声対音声モデル

Soniqoは、会話向けのネイティブMLX音声対音声モデルを2系統サポートしています:PersonaPlex 7BVoiceChat 11Bです。どちらも音声を受け取り、モデル音声を直接生成しますが、デュプレックス構成とランタイム契約が異なります。

モデルを選ぶ

モデル会話方式適した用途
PersonaPlex 7BMoshi、Depformer、Mimiによる同時フルデュプレックス聞く・話すの重なりと、選択可能な18音声
VoiceChat 11BFastConformer、Nemotron-H、EAR-TTS、ニューラルコーデックによる連続フレーム同期デュプレックス80 msストリーミングフレーム、テキスト/関数イベント、チェックポイント固有音声

ここで扱うのは会話型の音声対音声モデルです。Hibikiも音声を直接音声へ変換しますが、用途はストリーミング音声翻訳のため、別ページで説明しています。

PersonaPlex 7B

Moshiアーキテクチャ(Kyutai)に基づく全二重音声間対話モデル。PersonaPlex 7Bは、音声入力から直接音声応答を生成します — 中間テキストパイプラインは必要ありません。モデルは18のボイスプリセットを備え、8ビット(推奨)と4ビット量子化で利用可能です。8ビットがデフォルトです — 30%高速で首尾一貫した応答を生成しますが、4ビットは出力品質を劣化させます。

VoiceChat 11B

全二重 speech-to-speech の参照:SALM-Duplex: Efficient and Direct Duplex Modeling for Speech-to-Speech Language Model(Interspeech 2025)。NVIDIA の上流 VoiceChat モデルカードが引用しています。

M5 Pro、48 GB、Release、80 ms ライブ周期、保護ヘッド INT5:3 回の制御済み実行でパイプライン全体の RTF は 0.94、0.92、0.92、ピーク RSS は約 8.70 GB でした。最初の発話テキストは 44.3–46.7 ms、最初に再生可能な音声は 74.0–76.4 ms で得られました。これらはウォーム状態の計算値であり、学習されたターンテイキング遅延ではありません。最適化後の INT8 性能はまだ再測定していません。

VoiceChat は speech-swift のもう一つのネイティブ MLX 音声間ランタイムです。因果 FastConformer 知覚、Nemotron-H の全二重テキスト/関数チャンネル、独立したテキスト条件付き EAR-TTS、22.05 kHz ニューラルコーデックを組み合わせます。セッションは 16 kHz モノラル音声を連続して受け取り、80 ms ごとにテキストイベントと 1,764 サンプルの出力フレームを返します。

VoiceChatModel.load(from:) で読み込み、VoiceChatSession を開始して pushAudio で音声を送ります。完全な bundle には encoder/llm/tts/ が必要で、旧来の理解専用 bundle は拒否されます。

現在の性能

INT5 はテストした M5 Pro で RTF 1 未満の平均スループットを維持します。3 回すべてでフレーム合計 p95 も 76.2–78.6 ms と 80 ms 未満でしたが、デッドラインの余裕はまだ小さいです。モデルの発話開始とハードウェア計算遅延は別々に測定してください。

ライブ VoiceChat CLI

NVIDIA Nemotron VoiceChat 11B を基盤とする完全な protected-head INT5 bundle で Soniqo とライブ会話します。このコマンドは Apple AEC のために入出力を1つの AVAudioEngine にまとめ、bundle 内蔵の RNN-T ユーザー字幕をストリーミングし、聞き取りを続けながら 22.05 kHz のモデル音声を再生します。

既定の Soniqo prompt は機能範囲を明示します。この CLI は会話や質問への回答はできますが、カレンダー、リマインダー、アプリ、アカウント、デバイス、外部サービスにはアクセスできません。そのような依頼には、実行できないことを明確に伝え、果たせない確認を求めず、ユーザー自身が取れる方法を簡潔に案内します。

speech voice-chat

speech voice-chat --input question.wav --output response.wav

speech voice-chat \\
  --mcp-config Examples/VoiceChatMCP/apple-reminders.json

設定可能な MCP ツール

同梱の apple-reminders-eventkit アダプターがモデルに公開するのは、list_reminderscreate_reminderupdate_reminder の3つだけです。リスト探索はアダプター内部で行います。更新ではユーザーが話したリマインダー名を、実行時に信頼できる非公開の EventKit ID へ解決し、その ID はモデル向けの結果に含めません。作成と期限は引き続き1回の EventKit 保存で確定します。初回起動は最低60秒、通常のツール呼び出しは既定で15秒です。

MCP は --mcp-config を指定した場合だけ有効です。汎用 JSON 設定で任意の stdio MCP server を起動し、enabledTools で最大5個のツールを明示的に選択します。readOnlyTools にないツールはすべて書き込みとして扱われます。書き込みの既定は confirm で、最初の呼び出しを保留します。Soniqo の決定論的な確認音声が解釈した引数を繰り返し、まだ何も変更されていないことを明示します。新しい肯定的なユーザー発話があった場合だけ1回実行されます。新しいユーザー発話なしに同一呼び出しを2回実行することは、allow でもできません。deny または明示承認の allow も選べます。実際の結果はモデルの関数チャンネルへ返され、成功には ok 応答が必要です。

書き込み引数は確認前にユーザーの文字起こしと照合されます。相対日付は現在のローカル日付から解決し、推測されたリスト名、古い日付、優先度など未確認の値は実行しません。新しい RNN-T 発話と発話終了により、文字起こしがリセットされた後でも肯定を解決できます。Soniqo が成功を伝えるのは MCP server が実際に ok: true を返した場合だけで、それ以外は実行を確認できなかったと伝えます。

「どんなリマインダーがある?」という質問は、全 EventKit リストを横断する1回のフラットな list_reminders 呼び出しになります。リスト名、期限、完了状態、実行時専用の安定 ID をキャッシュするため、詳細質問や安全な更新でリストを個別に読み直す必要はありません。ダッシュボードは関数デコードの経過時間、token step、token/s を音声 RTF と分けて表示し、RTF は引き続き前景の80 ms音声フレームだけを測定します。

確認音声は「続行しますか」と尋ね、肯定として許可される語句を意図的に使いません。そのため、再生音のエコーが保留中の操作を承認することはありません。

子 MCP server が継承するのは PATHHOME、一時ディレクトリ、ロケール変数だけです。認証情報は server の env map に明示的に指定する必要があり、親 process の無関係な secret は転送されません。

8-bit 関数射影は約 518 MB です。通常の発話中、VoiceChat が評価するのはキャッシュ済みの 36 KB の PAD/ツール開始プローブだけです。131,072 行の完全なヘッドは、ツール開始がまず PAD を上回った場合にだけ実行され、全体の argmax を検証した後、開いた JSON 呼び出しの生成中は有効なままです。MCP の結果が確定すると、VoiceChat はそれを上限付きの 16-token 因果 prefill チャンクで再生し、学習済みの関数フィードバックと language/speech cache 状態を保ちながら、結果 token ごとの 11B decode を避けます。これにより、ツール開始判断の精度を落とさず MCP 会話をリアルタイムに保ちます。

再生前には既定で 80 ms フレームを3つ(240 ms)バッファします。スピーカー音切れ後は8フレームの回復バッファを待ちます。最初の88 msコールバックまたは3フレームの待ちで音声精緻化を8段階から2段階へ下げ、120 ms、6フレーム、または入力再同期では直ちに1段階へ下げます。それでも既定の8フレーム/640 ms上限に達した場合は、新しい入力を受け入れるために必要な最小限の古いマイク音声だけを破棄します。継続する過負荷は1回の回復期間として扱い、RNN-T が確認済みのユーザーターンは繰り返しリセットされても失われません。欠けた単語は引き続き表示され、再発話が必要な場合があります。

ダッシュボードの behind は待機中のマイク音声、RTF 0.93× は計算時間÷音声時間(1未満なら追従、1超なら遅延)、last 74 ms for 80 ms audio は固定80 msモデルフレームの計算に74 msかかったことを示します。スピーカー音切れ、古い音声のスキップ、スキップされたマイク音声も明示します。 移動平均は直近120回のマイクコールバックを使い、リプレイイベントを追加の音声時間として数えません。

マイクモードでは NVIDIA 方式の RNN-T ターン制御が既定で有効です。モデルが学習した BOS/EOS 判断が通常の低遅延経路です。各 80 ms フレームの最初の予測を blank / non-blank として扱い、ユーザー音声の確認後は、連続する blank 40フレーム(3.2秒)が応答開始の安全フォールバックとしてのみ働き、新しい non-blank 40フレームが対応する割り込みフォールバックになります。Soniqo が話し始める際に音声カウンターをリセットするため、完了したユーザーターンの活動が応答を即座に切ることはありません。全音声フレームは引き続き duplex モデルへ送られます。これらのフォールバックを無効にして BOS/EOS を学習済み言語ヘッドだけに任せるには --no-rnnt-turn-taking を使用します。ダッシュボードは RNN-T barge-in とスピーカー音切れを別々に表示します。

通常モードではユーザー音声が確認されるまでモデル由来の BOS を抑制するため、Soniqo が先に話すことはありません。--greet を指定した場合だけ最初の挨拶を明示的に許可します。聞こえる応答内容の後も、blank PAD は最低16フレーム、または内容 token ごとに3フレームの長い方まで開いたターン内で処理されます。予算を超えた最初の blank PAD が論理ターンを閉じます。この内容連動の末尾により、固定1.28秒の打ち切りで遅れて生成される単語が消えるのを防ぎます。

対話モードでは端末の代替画面に固定ダッシュボードを表示し、ステータス更新をスクロールバックへ追加せずその場で再描画します。追記形式の出力には --plain を使用してください。

ローカル診断では --debug-timeline がユーザーと Soniqo の各発話に時刻を付け、解析済みのネイティブ呼び出し引数、MCP の開始/完了、結果キャッシュ同期を同じ時系列へ挿入します。また最後の可聴生成 PCM 窓を pronunciation ended として記録します。これはモデル出力時刻であり、スピーカー再生完了時刻ではありません。デモは過去のフェーズ時間ブロックを表示せず、ネイティブデコード、プロバイダー、キャッシュ、マイク負荷の詳細は voicechat-bench JSON に残します。ツール引数が表示されるため、共有ログでは使用しないでください。フェーズタイマーはネイティブデコードとプロバイダー待機の間でリセットされます。

ライブセッションは不変の37フレーム話者プロンプトと直近20秒の EAR-TTS 履歴を保持し、意味的な会話履歴は Nemotron-H が別に保持します。内容連動の音声末尾に含まれる PAD は通常どおり生成し、その後の無音 PAD だけを8フレーム単位で因果的に進めます。応答音声を切らずに TTS の注意コストとアイドル処理の無制限な増加を防ぎます。--live-speech-context-seconds 0 で TTS 全履歴に戻せ、ファイルモードは既定で厳密な全履歴です。

M5 Pro の63.6秒プロファイルでは、安全な音声末尾を使うライブ経路で全体 RTF 0.87、最終ウィンドウ RTF 0.71、同ウィンドウの合成 4.1 ms/フレーム、ピーク RSS 8.72 GB、ピーク物理フットプリント 23.67 GB を測定しました。8段階の音声生成中のウィンドウは RTF 1.05 と1.12に達したため、入力がキューにたまると対話 CLI は引き続き可逆的に2段階へ切り替え、緊急時は1段階を使います。逐次処理とバッチ処理のアイドルキャッシュ状態の最小コサイン類似度は0.9999999でした。

voicechat-bench のオンセットポップ検出は、生成音声のリードインと持続音声の最初の 50 ms を調べます。サンプルジャンプが振幅 0.05 と定常音声 p99 基準の6倍をともに超えた場合だけ検出し、シナリオは maximum_suspect_onset_transients をゼロにして回帰を防げます。

ライブ字幕有効時の実測

M5 Pro 48 GB の Release で3回測定し、RTF はすべて 0.92、総フレーム p95 は 74.8–75.8 ms、最初の再生可能音声は 74.2–74.9 ms、ピーク RSS は約 8.74 GB でした。字幕と応答は3回とも同一でした。

PersonaPlexのアーキテクチャと使い方

PersonaPlexは3つのコアコンポーネントを持つマルチストリーム自己回帰モデルです:

コンポーネント詳細
Temporal Transformer32レイヤー、dim=4096、32ヘッド、SwiGLU (hidden_scale=4.125)、RoPE、8ビット量子化(デフォルト)
Depformer6レイヤー、dim=1024、16ヘッド、MultiLinear (weights_per_step=true)、dep_q=16
Mimi Codec16コードブック、12.5 Hzフレームレート、24 kHz音声出力

モデルは17ストリームを同時に処理します:1テキストストリーム + 8ユーザー音声ストリーム + 8agent音声ストリーム。このアーキテクチャにより、モデルが同時に聞いて話すことができる全二重会話が可能になります。

ボイスプリセット

PersonaPlexには、自然で多様なスタイルにわたる18の組み込みボイスプリセットが含まれています:

カテゴリプリセット
ナチュラル女性NATF0, NATF1, NATF2, NATF3
ナチュラル男性NATM0, NATM1, NATM2, NATM3
バリード女性VARF0, VARF1, VARF2, VARF3, VARF4
バリード男性VARM0, VARM1, VARM2, VARM3, VARM4

インナーモノローグ

PersonaPlexは各ステップで2つの並列ストリームを生成します:Mimiコーデック用の8つの音声コードブックトークンと、モデルの内部モノローグ用の1つのテキストトークン。テキストストリームは、モデルが話すときに「考えている」ことです — 最終音声から少し発散する可能性がありますが、実際にはライブトランスクリプトとして使用できるほど話された応答と密接に反映します。

テキストトークンは生のSentencePieceピースIDとして返されます。PersonaPlexに同梱されているSentencePieceDecoderでデコードしてください:

import PersonaPlex
import AudioCommon

let model = try await PersonaPlexModel.fromPretrained()
let decoder = model.tokenizer  // SentencePieceDecoder?

let result = model.respond(userAudio: userSamples, voice: .NATM0)
let transcript = decoder?.decode(result.textTokens) ?? ""
print(transcript)        // "Sure, I can help with that..."
playAudio(result.audio)  // 24 kHz mono Float32

ストリーミングモードでは、respondStreamが生成されるにつれてtextTokensチャンクを出力します — 音声がまだ生成中の間にライブキャプションビューを駆動するために、それらを増分的にデコードします。--transcript CLIフラグは、まさにこれを舞台裏で行います。

なぜ重要か: SentencePieceDecoderは共有のAudioCommon.SentencePieceModel protobufリーダー上に構築されているため、PersonaPlex、OmnilingualASR、および将来のSentencePieceベースのモデルはすべて、同じトークナイザー実装を通じてデコードします。SentencePieceModelリファレンスを参照してください。

システムプロンプト

カスタムシステムプロンプトをプレーンな文字列として渡します — 外部トークン化は不要:

let response = model.respond(
    userAudio: audio,
    voice: .NATM0,
    systemPrompt: "You enjoy having a good conversation."
)

または組み込みプリセットを使用:

CLIの使用法

音声入力から音声応答を生成します:

# Basic speech-to-speech
.build/release/speech respond --input question.wav

# Choose a voice preset
.build/release/speech respond --input question.wav --voice NATM0

# Stream audio output during generation
.build/release/speech respond --input question.wav --stream

# Custom system prompt text
.build/release/speech respond --input question.wav --system-prompt-text "You enjoy having a good conversation."

# Use a preset system prompt
.build/release/speech respond --input question.wav --system-prompt customer-service

# Get transcript alongside audio
.build/release/speech respond --input question.wav --transcript

# JSON output with metadata
.build/release/speech respond --input question.wav --json

オプション

オプション説明
--input入力音声ファイル(WAV、必須)
--voiceボイスプリセット名(例:NATM0VARF2
--system-promptシステムプロンプトプリセット:assistant、focused、customer-service、teacher
--system-prompt-textカスタムシステムプロンプトテキスト(--system-promptをオーバーライド)
--max-steps最大生成ステップ
--stream生成中に音声チャンクを出力
--compile高速生成のためにMLXコンパイル推論を使用
--transcript音声と一緒にテキストトランスクリプトを出力
--jsonメタデータ付きJSON出力

サンプリングパラメーターもオーバーライドできます:

オプションデフォルト説明
--audio-temp0.8音声トークンサンプリング温度
--audio-top-k250音声トークンtop-kサンプリング
--text-temp0.7テキストトークンサンプリング温度
--text-top-k25テキストトークンtop-kサンプリング

ストリーミング

--streamフラグはリアルタイム音声出力を有効にします。音声チャンクは生成されるにつれて出力されるため、完全な応答が完了する前に再生を開始できます。これは、低レイテンシーが重要なインタラクティブアプリケーションで特に有用です。

パフォーマンス

指標
リアルタイム係数 (RTF)約1.4(8ビット、ほぼリアルタイム)
ステップレイテンシーM2 Max上で約112 ms/ステップ(8ビット)
モデルサイズ(8ビット)約9.1 GB
ピークRAM(8ビット)約11 GB
モデルサイズ(4ビット)約4.9 GB
ピークRAM(4ビット)約7 GB
重要

PersonaPlex 7B(8ビット)は少なくとも24 GBのRAMを必要とします。4ビットバリアントは16 GBデバイスに収まりますが、劣化した出力を生成します。8 GBデバイスでは、どちらのバリアントも収まりません。サポートされているハードウェアで最高のパフォーマンスを得るには--compileを使用してください。

モデルバリアント

モデルサイズHuggingFace
PersonaPlex-7B (8ビット) 推奨9.1 GBaufklarer/PersonaPlex-7B-MLX-8bit
PersonaPlex-7B (4ビット)4.9 GBaufklarer/PersonaPlex-7B-MLX-4bit

Swift API

import PersonaPlex
import AudioCommon

let model = try await PersonaPlexModel.fromPretrained()
let response = model.respond(
    userAudio: userSamples,
    voice: .NATM0,
    systemPrompt: "You are a helpful assistant."
)
try WAVWriter.write(samples: response.audio, sampleRate: 24000, to: URL(filePath: "answer.wav"))