llamadart 本地语音实战:让 Llama Village 开口说话

llamadart 在 macOS 上跑通本地语音全链路,并揭示小模型对更快但整回合更慢的取舍。
文章记录了用 llamadart 在 macOS 上为 Llama Village 角色接入本地语音的完整实验,走通「语音输入 → 对话生成 → 语音输出」链路,并对比了经由文本转录和直接处理音频两条路线。作者用 Qwen3-ASR、Qwen3.5 0.8B、Qwen3-TTS 0.6B 的小模型对和 3B/1.7B 原始组合做了实测:小模型对体积减半、峰值内存降约 35%,对话环节也更快,但因为无视「一句话回答」的指令生成了更长文本,完整回合反而更慢。TTS 支持用参考音频为角色指定音色,但 Base 模型不支持语气指令控制。此外两个对话模型都在未被提供事实时编造了信息,说明知识约束必须放在提示词之外;TTS API 也要等整段音频生成完才能播放,游戏集成时建议先显示文字再播放语音。
llamadart 本地语音实战:让 Llama Village 开口说话
在 Llama Village 这个由本地 AI 驱动的虚拟世界里,角色之间的对话一直停留在文字层面。开发者 gde 在系列文章第三篇中,尝试用 llamadart 给这个村庄装上声音——让角色 Dash 飞过去问 Mo 一句“你看到 Pip 的红围巾了吗?”,并让 Mo 用语音回答。这个实验展示了在 macOS 上完全用本地模型跑通「语音输入 → 对话生成 → 语音输出」全链路的具体路径。
语音链路的两条实现路线

从玩家的语音到角色的语音回复,文章给出了两条技术路线。两条路线都以 TTS 收尾,区别在于中间如何处理音频。
第一条是经由文本:Qwen3-ASR 先把录音转成文字,对话模型基于文字生成回答,再由 Qwen3-TTS 合成语音。这条路线的优势是保留转录文本,当“Pip”这类专有名词被听错时可以排查和纠正。
第二条是直接音频:Qwen2.5-Omni 直接接收录音并写出回答,没有独立的转录步骤。不过 GGUF 转换版 Omni 只能接受音频、不能生成音频,所以 Mo 的回复仍然要走 TTS。
两条路线使用同一个 3B 对话模型做对照实验。第一种路线中模型接收文本,第二种路线中模型额外加载音频投影器。作者强调这是一个经过实测的多模态路线,而非宣称所有多模态模型都需要单独的 TTS。
为了控制体积,文章还测试了更小的一对模型组合:Qwen3.5 0.8B 负责对话,Qwen3-TTS 0.6B Base 负责语音,Qwen3-ASR 0.6B 继续负责识别。小模型对是默认配置,staged 模式则保留原始的 3B/1.7B 组合用于对比。
Qwen3-ASR / Qwen3-TTS / Qwen2.5-Omni 是什么:这一族模型来自阿里通义千问广告团队,各自负责语音链路中的不同环节。ASR(Automatic Speech Recognition,自动语音识别)把音频转写成文字;TTS(Text-to-Speech,文本转语音)把文字合成为音频,Qwen3-TTS 属于支持零样本音色克隆的一类模型,可凭一段短参考音频指定说话人;Omni 则是原生多模态模型,能在同一个模型里同时接收文本、图像和音频输入,Qwen2.5-Omni 的 GGUF 转换版本目前只保留了音频输入能力,输出端仍需借助 TTS。
GGUF 与 llamadart 的关系:GGUF 是 llama.cpp 生态使用的模型文件格式,把权重、分词器和元数据打包成单文件,并针对 CPU/GPU 混合推理做了量化优化。llamadart 是 llama.cpp 的 Dart 绑定,让 Dart 程序可以直接加载 GGUF 模型,并通过统一的 load() / download 管理模型拉取与缓存,因此文章里才不需要额外的 curl 脚本来准备模型文件。
在 Mac 上跑通完整示例
整个示例需要 macOS、Dart 3.10.7 或更新版本,五个小模型文件约占用 2.31 GB。作者在 M4 Max(64 GB 内存)上用 Dart 3.13.5 和 llamadart 0.11.0 做了测试。命令依赖 macOS 自带的 say、afconvert 和 afplay,这是一个命令行程序,不是手机或浏览器演示。
项目搭建与程序结构
创建项目只需三步:新建目录并配置 pubspec.yaml 依赖 llamadart 0.11.0,运行 dart pub get,然后写入主程序。程序使用固定的 ModelSource.huggingFace 引用,每个引擎的 load() 方法通过 llamadart 的下载管理器按需拉取缺失文件并复用缓存,不需要额外的 curl 脚本。
程序从头到尾只加载一个引擎,用完即释放,再加载下一个。核心调用包括三步:asr.transcribeOnce() 把录音变成文本,llm.create() 生成回复,tts.synthesizeOnce() 把文本合成为 WAV 文件。
用合成问题测试全链路
不需要麦克风包或真实录音,用 macOS 的 say 生成一个合成问题即可测试完整路径:
say -o question.aiff "Mo, have you seen Pip's red scarf?"
afconvert -f WAVE -d LEI16@16000 -c 1 question.aiff question.wav
dart run bin/voice.dart question.wav small
afplay reply.wav
程序会打印转录文本和模型回答,并把回复保存为 reply.wav。切换到 audio 模式时,Omni 的音频模型和投影器会被下载加载,加上原始的 1.7B TTS 组合。这条路线跳过 ASR,直接把 LlamaAudioContent 发给对话模型。音频路线文件总量约 5.13 GB,三种模式全部缓存约需 7.44 GB。
让 Mo 拥有独特声音

TTS 模型支持通过一段短参考音频来指定说话人音色。作者的设想是让 Mo 听起来温暖从容,Dash 更明亮轻快,多个角色共享同一个 TTS 模型但各自拥有不同声音。当玩家扮演 Dash 时,玩家自己的声音天然就是 Dash 的音色。
程序中第三个可选参数就是参考音频:small 表示 0.8B/0.6B 组合,staged 表示原始的 3B/1.7B 组合。参考音频会被作为 speakerReference 传入 TextToSpeechRequest。作者在测试中用 macOS 的 Daniel 语音合成了一段参考音频。
Qwen 的 voice-design 工作流还允许在开发阶段创建一个声音,然后用生成的片段配合 Base 模型使用。玩家只需下载合成模型和参考音频,不需要 voice-design 模型本身,不过作者并未测试这条路径。
这里有一个能力边界值得注意:当前测试的 API 无法实现“用紧张的语气说这句话”这类指令控制。这不是 Dart 参数缺失的问题——Qwen 的 1.7B CustomVoice 和 VoiceDesign 模型支持指令控制,但本文使用的 Base 模型不提供该模式。要暴露这个能力,需要兼容的模型、原生运行时支持和 Dart API 三者齐备。修改对话提示词只能改变文字内容,改变不了语气。
参考音频与 speakerReference:TTS 的零样本音色克隆并不训练新模型,而是把一段几秒到十几秒的干净人声编码成一个说话人嵌入(speaker embedding),在合成时作为条件注入。因此同一份权重可以“扮演”任意多个角色,只要提供各自的参考音频。参考音频的质量很关键:单说话人、无背景音乐、无混响,否则音色会带上杂音。文章中用 macOS say 合成 Daniel 语音作为参考,相当于用合成人声去引导模型,会引入轻微的“二手”音色损失,真实录音通常会得到更自然的结果。
指令控制(instruction control):Qwen 的 CustomVoice 和 VoiceDesign 模型支持在文本之外附加情感、语气、语速等指令,例如“用紧张的语气说”。这类能力依赖模型在训练时见过指令-语音配对数据,Base 模型没有这部分能力,所以即便 Dart 侧传入指令参数也无处生效,必须模型、原生运行时和 Dart API 三层同时支持才可用。
小模型更快吗?实测数据
作者在同一台 M4 Max 上对比了小模型对和原始模型对。两者都保留 Qwen3-ASR 0.6B,使用相同的合成问题和 Daniel 语音参考。
| 文本路线 | 模型文件 | 完整回合中位数 | 峰值进程 RSS |
|---|---|---|---|
| Omni 3B + TTS 1.7B | 4.61 GB | 4.27 秒 | 3.86 GiB |
| Qwen3.5 0.8B + TTS 0.6B | 2.31 GB | 5.83 秒 | 2.51 GiB |
结果有些反直觉:下载体积减半,峰值内存降低约 35%,但在这个试验中完整回合反而更慢。
原因之一是小模型回复更长。3B 模型说“是的,Pip 的红围巾在池塘边的芦苇丛里”,0.8B 模型则说“是的,我看到了!它被塞在池塘边的芦苇丛里”,并且忽略了“一句话回答”的指令。更多的字意味着更多的语音需要合成。
分离组件的测试数据更清晰:
| 加载的组件 | 生成时间中位数 | 峰值进程 RSS |
|---|---|---|
| Omni 3B 对话 | 171 ms | 2.57 GiB |
| Qwen3.5 0.8B 对话 | 96 ms | 1.08 GiB |
| Qwen3-TTS 1.7B | 1.78 s | 3.85 GiB |
| Qwen3-TTS 0.6B | 1.78 s | 2.48 GiB |
对话环节 0.8B 确实更快(96 ms 对 171 ms),内存也低得多。但 TTS 环节两者生成时间中位数相同,且 0.6B 合成的音频更长(4.80–6.48 秒,而 1.7B 是 4.16–4.56 秒)。
模型说对话了吗?
两个对话模型在回答围巾位置和颜色时都正确。但当问**“是谁把围巾放进池塘的?”**时,两个模型都归咎于 Pip——这个事实从未被提供过,即便提示词里写了“不要编造你不知道的事实”。小模型的“我看到了”还额外添加了一个关于 Mo 如何得知的未经支持的声明。
TTS 方面,六个测量片段的语音都能被 Qwen3-ASR 正确转录回原句,说明可懂度没问题,但这不等于自然度、情感或与 Daniel 音色的相似度评分。作者建议在确定角色声音前多听一些句子。
作者的结论是:在这个原型中会选用小模型对来控制存储预算;会把游戏的知识规则放在提示词之外;并且会在真实对话场景中实测之后再宣称延迟优势。
为什么对话更快但整回合更慢:整回合耗时大致等于 ASR 时间 + 对话生成时间 + TTS 合成时间。0.8B 在对话环节确实更快(96 ms 对 171 ms),但 TTS 环节两者生成时间中位数相近,而 TTS 的耗时与输出文本长度近似线性相关。0.8B 无视“一句话回答”的指令、生成了更长的句子,于是多出来的词全部转化为额外的合成时间,抵消并反超了对话环节省下的时间。
峰值 RSS 的含义:RSS(Resident Set Size,常驻内存集)指进程当前实际驻留在物理内存中的页数,包含模型权重、KV 缓存和运行时缓冲区。由于程序按顺序加载 ASR、对话模型和 TTS 并在用完后释放,峰值 RSS 主要反映当时最大的单个引擎,而不是三者之和;这也是模型文件总大小(2.31 GB / 4.61 GB)与峰值内存(2.51 GiB / 3.86 GiB)不完全一致的原因。
游戏集成时的工程取舍
TTS API 目前返回完整音频后才开始播放,进度事件不是可播放的音频块。作者的建议是:先把 Mo 的回复以文字形式立即显示,等合成完成后再播放语音。
对于游戏的第一版,作者倾向采用按键说话(push-to-talk)、Mo 说话时停止录音、玩家开启新回合时丢弃旧回复等策略。文字路径需要在语音关闭或模型无法加载时仍然可用。在把语音集成进游戏之前,还需要测量语音与渲染同时运行时的表现。
整个实验揭示了一个务实的判断:本地语音在技术上已经跑通,但要在游戏中良好体验,需要处理模型能力边界、知识约束和延迟体验三件事,而这三件事都无法仅靠提示词解决。
相关推荐

OpenAI与Shield合作:Astra智能体如何革新IT故障修复
OpenAI与Shield合作推出Astra智能体,可自动登录电脑和遗留系统,实时修复IT故障,比传统模型快35%到40%,仅在需要人类判断时引入人工。

iPhone Duo续航实测:一天一充跑不掉,大屏是耗电元凶
iPhone Duo续航实测:重度使用一天需至少充电一次,6.5小时亮屏约剩30%电量。大屏展开频率是耗电关键,MagSafe电池背夹可救场,长期衰减同样值得关注。

Kimi K3.1 Pro开源发布:2.8万亿参数逼近Opus
Moonshot发布开源MoE模型Kimi K3.1 Pro,2.8万亿总参数、1040亿激活参数,Terminal Bench 4.0仅落后Opus 5.5不到1分,单任务成本约三分之一,API价格减半,开源权重11月4日开放。