VoiceChat 工具调用和性能说明正在更新。最新信息请参阅英文版。
语音到语音模型
Soniqo 支持两个用于语音对话的原生 MLX 模型系列:PersonaPlex 7B 和 VoiceChat 11B。两者都接收语音并直接生成模型音频,但采用不同的双工架构和运行时约定。
选择模型
| 模型 | 对话方式 | 最适合 |
|---|---|---|
| PersonaPlex 7B | 基于 Moshi、Depformer 和 Mimi 的同步全双工 | 听说重叠和 18 种可选音色 |
| VoiceChat 11B | 基于 FastConformer、Nemotron-H、EAR-TTS 和神经编解码器的连续帧同步双工 | 80 ms 流式帧、文本/函数事件和 checkpoint 原生语音 |
这些是对话型语音到语音模型。Hibiki 也会把语音直接转换为语音,但其任务是流式语音翻译,因此单独记录。
PersonaPlex 7B
基于 Moshi 架构(Kyutai)的全双工语音到语音对话模型。PersonaPlex 7B 直接从语音输入生成语音响应——不需要中间文本流水线。模型自带 18 种预设音色,提供 8 位(推荐)和 4 位量化版本。8 位是默认选项——它快 30% 并能产生连贯的回答,而 4 位会降低输出质量。
VoiceChat 11B
双工语音到语音架构参考: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:三次受控测试的全流程 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 的总体吞吐量。三次测试的每帧总延迟 p95 均低于 80 ms,范围为 76.2–78.6 ms;因此截止时间余量仍然很小。模型轮次起点与硬件计算延迟应分别测量。
实时 VoiceChat CLI
使用由 NVIDIA Nemotron VoiceChat 11B 驱动的完整 protected-head INT5 bundle 与 Soniqo 进行实时对话。该命令在同一个 AVAudioEngine 中进行采集和播放以使用 Apple AEC,流式显示 bundle 自带的 RNN-T 用户字幕,并在继续监听的同时播放 22.05 kHz 模型音频。
Soniqo 的默认提示会明确能力边界:此 CLI 可以对话和回答问题,但无法访问日历、提醒事项、应用、账户、设备或外部服务。遇到此类请求时,Soniqo 会清楚说明无法执行,不会索要无法兑现的确认,并简要建议用户可以自行采取的操作。
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_reminders、create_reminder 和 update_reminder。列表发现由适配器私下完成。更新操作使用用户说出的提醒名称,运行时再将它解析为可信的私有 EventKit ID;该 ID 永远不会进入模型可见的工具结果。创建提醒及其到期时间仍在一次 EventKit 保存中提交。首次启动服务器至少允许 60 秒,普通工具调用默认 15 秒。
只有提供 --mcp-config 时才会启用 MCP。通用 JSON 配置可启动任意 stdio MCP 服务器,并通过 enabledTools 明确选择最多五个工具。未列入 readOnlyTools 的工具一律视为写操作。写操作默认采用 confirm 策略:首次调用会被暂存,Soniqo 的确定性确认语音会复述解析出的参数,并明确说明尚未发生任何更改。只有用户新的肯定答复才能执行一次。没有新的用户语音时,即使使用 allow,相同调用也不能执行两次。也可选择 deny 或显式授权的 allow。真实结果会通过模型的函数通道返回,只有收到 ok 才能报告成功。
写操作参数会在确认前与用户转写核对。相对日期按当前本地日期解析;虚构的列表名、过期日期、优先级及其他未经验证的值绝不会执行。新的 RNN-T 语音加上话语结束,即使转写重置后也能处理肯定答复。只有 MCP 服务器实际返回 ok: true 时,Soniqo 才会报告成功;否则会说明无法确认执行。
“我有哪些提醒?”现在只触发一次扁平化的 list_reminders 调用,覆盖所有 EventKit 列表。结果会缓存列表名、到期时间、完成状态和仅供运行时使用的稳定 ID,因此详情追问和安全更新无需逐个读取列表。面板还会单独显示函数解码的耗时、token 步数和 token/s;音频 RTF 仍只衡量前台的 80 毫秒音频帧。
语音确认会询问用户是否要“继续”,并刻意避开肯定词允许列表中的全部短语,因此播放回声无法授权待执行的操作。
子 MCP 服务器只继承 PATH、HOME、临时目录和区域设置变量。凭据必须在服务器的 env 映射中明确传入;父进程中无关的密钥不会被转发。
8-bit 函数投影约为 518 MB。普通语音期间,VoiceChat 只计算缓存的 36 KB PAD/工具起始探针。只有当工具起始分数先超过 PAD 时,完整的 131,072 行头才会运行,随后验证全局 argmax,并在生成已开启的 JSON 调用期间保持启用。MCP 结果一旦确定,VoiceChat 就会用有界的 16-token 因果预填充块重放它,在保留训练所得的函数反馈和语言/语音缓存状态的同时,避免为每个结果 token 单独执行一次 11B 解码。这样既不会削弱工具起始判断,也能让 MCP 对话保持实时。
默认会在播放前缓冲三个 80 ms 帧(240 ms)。发生扬声器断音后,播放会等到八帧恢复缓冲再启动。首次回调达到 88 ms 或排队三帧时,语音细化从八步降到两步;达到 120 ms、六帧或输入重新同步时直接降到一步。若队列仍达到默认八帧/640 ms 上限,系统只丢弃接纳新输入所需的最少旧麦克风音频。持续过载会合并为一次恢复过程,已经由 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;确认用户语音后,连续 40 个 blank 帧(3.2 秒)只作为启动回复的安全兜底,40 个新的 non-blank 帧则提供对应的插话兜底。Soniqo 开始说话时会重置语音计数,因此已完成用户轮次的活动不会立即截断回复。所有音频帧仍会连续送入双工模型;使用 --no-rnnt-turn-taking 可关闭这些兜底并完全交由语言头决定 BOS/EOS。面板会分别显示 RNN-T barge-in 和扬声器断音。
普通模式会在确认用户语音前抑制模型原生 BOS,因此 Soniqo 不会先开口。只有显式指定 --greet 才允许首次问候。可听回复内容结束后,blank PAD 至少在开放轮次中保留 16 帧;若每个内容 token 三帧得到的预算更长,则采用后者。第一个超出预算的 blank PAD 会关闭逻辑轮次。按内容长度缩放的尾音可避免固定 1.28 秒截止点把延迟生成的词静音。
交互模式在终端备用屏幕中使用固定面板,状态更新会原位重绘而不会塞满滚动历史;如需仅追加输出,请使用 --plain。
进行本地诊断时,--debug-timeline 会为每条用户和 Soniqo 短语添加时间戳,并按同一时间顺序插入已解析的原生调用参数、MCP 启动/完成以及结果缓存同步。它还会在最后一个可听见的生成 PCM 窗口标记 pronunciation ended;这是模型输出时间,而不是扬声器播放完成时间。Demo 会刻意省略历史阶段耗时块;原生解码、提供方、缓存和麦克风压力的详细指标保留在 voicechat-bench JSON 中。时间线可能暴露工具参数,因此不要用于共享日志。原生解码与等待提供方之间会重置阶段计时器。
实时会话会保留不可变的 37 帧说话人提示以及最近 20 秒的 EAR-TTS 历史;语义对话状态由 Nemotron-H 单独保留。按内容长度缩放的声学尾音内,PAD 仍会正常渲染;只有其后的静音 PAD 才以每批八帧的方式因果推进。这样既不会截断口语回复,也能避免 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。使用八步细化且正在生成语音的窗口达到 RTF 1.05 和 1.12,因此交互式 CLI 在输入排队时仍会可逆地降为两步,并在紧急模式下降为一步。顺序与批处理空闲缓存状态的最小余弦相似度为 0.9999999。
voicechat-bench 的起音爆点检测器会扫描生成音频的前导部分和持续语音的前 50 ms。只有样本跳变同时超过 0.05 幅度和稳定语音 p99 基线的六倍才会标记;场景可把 maximum_suspect_onset_transients 设为零以阻止回归。
在 M5 Pro 48 GB 上进行三次 Release 测试,RTF 均为 0.92,总帧 p95 为 74.8–75.8 ms,首个可播放音频为 74.2–74.9 ms,峰值 RSS 约 8.74 GB。三次测试的字幕和回复完全一致。
PersonaPlex 架构与用法
PersonaPlex 是一个多流自回归模型,由三个核心组件构成:
| 组件 | 细节 |
|---|---|
| Temporal Transformer | 32 层,dim=4096,32 heads,SwiGLU (hidden_scale=4.125),RoPE,默认 8 位量化 |
| Depformer | 6 层,dim=1024,16 heads,MultiLinear (weights_per_step=true),dep_q=16 |
| Mimi Codec | 16 个 codebook,12.5 Hz 帧率,24 kHz 音频输出 |
模型同时处理 17 个流:1 个文本流 + 8 个用户音频流 + 8 个 agent 音频流。这种架构实现了模型可以同时听和说的全双工对话。
预设音色
PersonaPlex 内置 18 种预设音色,涵盖自然和多样化的风格:
| 类别 | 预设 |
|---|---|
| Natural Female | NATF0, NATF1, NATF2, NATF3 |
| Natural Male | NATM0, NATM1, NATM2, NATM3 |
| Varied Female | VARF0, VARF1, VARF2, VARF3, VARF4 |
| Varied Male | VARM0, VARM1, VARM2, VARM3, VARM4 |
内部独白
PersonaPlex 在每一步都生成两个并行流:8 个用于 Mimi codec 的音频 codebook token 和一个模型内部独白的文本 token。文本流是模型在说话时"正在想"的内容——它与最终音频可能略有偏差,但实际上足够贴近口头回答,可以作为实时转写使用。
文本 token 以原始 SentencePiece piece 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 块——增量解码这些块即可驱动实时字幕视图,同时音频仍在生成。CLI 的 --transcript 标志正是在背后做同样的事。
为什么重要:SentencePieceDecoder 构建在共享的 AudioCommon.SentencePieceModel protobuf reader 之上,因此 PersonaPlex、OmnilingualASR 以及任何未来基于 SentencePiece 的模型都通过同一个 tokenizer 实现进行解码。参见 SentencePieceModel 参考。
System Prompt
可以将任意自定义 system prompt 作为普通字符串传入——不需要外部 tokenization:
let response = model.respond(
userAudio: audio,
voice: .NATM0,
systemPrompt: "You enjoy having a good conversation."
)
或使用内置预设:
assistant— 通用型有帮助的助手(默认)focused— 简洁、直接的回答customer-service— 礼貌、以解决方案为导向的客服 agentteacher— 耐心、善于讲解的教学风格
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 | 音色预设名(如 NATM0、VARF2) |
--system-prompt | system prompt 预设:assistant、focused、customer-service、teacher |
--system-prompt-text | 自定义 system prompt 文本(会覆盖 --system-prompt) |
--max-steps | 最大生成步数 |
--stream | 在生成过程中持续输出音频 chunk |
--compile | 使用 MLX 编译推理以更快生成 |
--transcript | 在音频之外输出文本转写 |
--json | 带元数据的 JSON 输出 |
采样参数也可覆盖:
| 选项 | 默认值 | 说明 |
|---|---|---|
--audio-temp | 0.8 | 音频 token 采样温度 |
--audio-top-k | 250 | 音频 token top-k 采样 |
--text-temp | 0.7 | 文本 token 采样温度 |
--text-top-k | 25 | 文本 token top-k 采样 |
流式
--stream 标志启用实时音频输出。音频 chunk 在生成时即被输出,因此在完整响应完成之前就可以开始播放。这对低延迟非常重要的交互式应用特别有用。
性能
| 指标 | 值 |
|---|---|
| 实时因子 (RTF) | ~1.4(8 位,接近实时) |
| 单步延迟 | M2 Max 上约 112 ms/step(8 位) |
| 模型大小(8 位) | ~9.1 GB |
| 峰值内存(8 位) | ~11 GB |
| 模型大小(4 位) | ~4.9 GB |
| 峰值内存(4 位) | ~7 GB |
PersonaPlex 7B(8 位)至少需要 24 GB 内存。4 位变体可在 16 GB 设备上运行,但输出质量会下降。在 8 GB 设备上,两种变体都无法放下。在支持的硬件上使用 --compile 可获得最佳性能。
模型变体
| 模型 | 大小 | HuggingFace |
|---|---|---|
| PersonaPlex-7B (8 位) 推荐 | 9.1 GB | aufklarer/PersonaPlex-7B-MLX-8bit |
| PersonaPlex-7B (4 位) | 4.9 GB | aufklarer/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"))