VoiceStudio 这两周一直挂在 GitHub 日榜上:开源、本地跑、被叫作 ElevenLabs 的免费替代,语音克隆、声音设计、配音、有声书一整套,星数已经过了 10 万(11 月 4 日 101,011,今天再看已是另一个数字)。
我没装它的桌面版。不是嫌弃,是因为这个项目真正值得看的部分在下面一层 —— 它默认的合成引擎 OmniVoice 有一份独立的 C++ 实现,纯 ggml,能编、能跑、能用命令行驱动。GUI 干的事,命令行都干得了,而且在无窗口环境里,我可以把每一步的耗时、内存、输出质量都量下来。
这篇给你三样东西:一条能照着敲的编译、下模型、合成路径;一张 M4/16GB 上的真实耗时表(官方仓库里那张 RTF 表到今天还是空的);三个我踩到的坑,其中一个会让你「明明生成了 wav,听起来却一个字都听不懂」,而程序不会报任何错。
它其实是个壳,模型在下面
VoiceStudio 本体不是模型。它是 Electron 桌面端 + Python 后端(默认 http://localhost:3900,本机回环免鉴权)+ 一个 MCP 服务器,底下挂十几个 TTS 引擎:OmniVoice(默认)、IndexTTS 2.5、CosyVoice 3、VoxCPM2、KittenTTS、dots.tts、gpt-sovits…… 转录那边是 WhisperX / Faster-Whisper / Parakeet 一堆。
真正干活的是引擎。默认引擎 OmniVoice 来自小米与 k2-fsa,骨干是 Qwen3 0.6B,外加一个 HuBERT + DAC + RVQ 的音频 tokenizer,输出 24kHz 单声道,官方标称 646 种语言。有个独立项目 omnivoice.cpp 把它移植成 C++17 + ggml,MIT 协议,能编到 CPU / CUDA / Metal / Vulkan —— 这就是本文的主角。
对 16GB 笔记本用户来说,关键点只有一个:Q8_0 量化的权重一共 945MB(语言模型 656MB + tokenizer 289MB),塞进内存毫无压力,跑起来还是 GPU 加速的。这跟 colibri 那种靠磁盘流式硬扛大模型的思路完全相反:模型够小,就不需要腾挪。
编译:cmake 配置 4.5 秒,编译 9 秒
cmake 我装在临时 venv 里,不动系统环境:
python3 -m venv /tmp/ovo-venv
/tmp/ovo-venv/bin/pip install cmake拉代码时一定要带 --recurse-submodules,ggml 是子模块,忘了它编译会直接失败:
git clone --recurse-submodules https://github.com/ServeurpersoCom/omnivoice.cpp.git
cd omnivoice.cpp
git checkout 886fc079838ca7400cb2b42b36e2a65aa1daabe8 # VoiceStudio 钉住的版本
/tmp/ovo-venv/bin/cmake -B build -DCMAKE_BUILD_TYPE=Release \
-DGGML_METAL=ON -DGGML_BLAS=ON -DGGML_NATIVE=ON
/tmp/ovo-venv/bin/cmake --build build -j 8M4 / macOS 26.5.2 / Apple Clang 上,配置 4.5 秒,编译 39 个编译单元加 Metal shader 嵌入一共 9 秒。产物 build/omnivoice-tts 只有 385KB —— 它就是个壳,真正的算子在 libggml*.dylib 里。这是我整条链里最省事的一步。
下模型之前,先看许可
权重在 Serveurperso/OmniVoice-GGUF,四档量化两两配对:
| 档位 | 语言模型 | tokenizer | 合计 | 官方定位 |
|---|---|---|---|---|
| F32 | 2.46 GB | 734 MB | 3.2 GB | 参考 / 调试 |
| BF16 | 1.23 GB | 373 MB | 1.6 GB | 最高精度 |
| Q8_0 | 656 MB | 289 MB | 945 MB | 推荐默认 |
| Q4_K_M | 407 MB | 252 MB | 659 MB | 显存最低 |
比容量更该先看的是许可:权重是 CC-BY-NC 4.0。 上游代码 Apache 2.0,但预训练权重因为训练数据(Emilia 等)的限制只能非商用,GGUF 是权重的转换,继承同一条款。VoiceStudio 的更新日志里写法一致:默认 OmniVoice 属于「非明确商用许可」,在模型目录里第一次使用要手动接受一次条款。
所以「免费替代 ElevenLabs」的准确说法是:个人用途、内部试验免费;拿去给客户做商业配音、塞进付费产品,默认模型不行。 要商用得换引擎,而换之前每个引擎的权重许可都得单独确认 —— 这是我看这类项目的第一顺位,比跑 demo 优先。
下载(本机 Hugging Face 直连可用,hf-mirror.com 也通,两条路都验过;但 10 月底那阵子直连是超时的,镜像更稳):
mkdir -p models && cd models
BASE=https://huggingface.co/Serveurperso/OmniVoice-GGUF/resolve/main
curl -L -O $BASE/omnivoice-base-Q8_0.gguf
curl -L -O $BASE/omnivoice-tokenizer-Q8_0.gguf
shasum -a 256 *.gguf # 和模型卡的 LFS oid 逐字对我这边总带宽被压到 1.2MB/s 左右,945MB 花了十九分钟。最后那句 shasum 不是仪式感 —— 下面第一个坑就是靠它抓出来的。
三种用法,命令行全覆盖
文本从 stdin 进,wav 从 -o 出,-o - 直接吐到 stdout(我用管道接了 2.16 秒的 wav,正常)。
BIN=./build/omnivoice-tts
M=models/omnivoice-base-Q8_0.gguf
C=models/omnivoice-tokenizer-Q8_0.gguf
# 1. 指定语言,让模型自己挑一个稳定音色
echo "本地语音合成这几年变化很大。" | $BIN --model $M --codec $C --lang Chinese -o out.wav
# 2. 声音设计:用属性描述一个声音
echo "这句话是用属性描述出来的声音。" | $BIN --model $M --codec $C --lang Chinese \
--instruct "女,青年,中音调" -o design.wav
# 3. 声音克隆:参考音频 + 它的逐字稿
echo "这句话是用属性描述出来的声音。" | $BIN --model $M --codec $C --lang Chinese \
--ref-wav ref.wav --ref-text ref.txt -o clone.wav--instruct 只有六类,每类最多一个值,冲突直接报错:性别(男/女)、年龄(儿童/少年/青年/中年/老年)、音调(极低/低/中/高/极高)、风格(耳语)、英语口音(10 种)、中文方言(河南话、陕西话、四川话、贵州话、云南话、桂林话、济南话、石家庄话、甘肃话、宁夏话、青岛话、东北话)。方言只有中文,口音只有英文。
长文本不用手动切。超过 30 秒估算时长会自动分块,每块约 15 秒、70 个字符上下,块间交叉淡入淡出。我拿 276 字的稿子试,日志给出 5 块、块长 70/66/70/74/29 字,交叉淡化后输出 60.4 秒整段。
实测:M4 / 16GB / Metal
环境:Apple M4(10 核 CPU / 10 核 GPU),16GB 统一内存,macOS 26.5.2,Python 3.13.12,模型 Q8_0 + Q8_0,全程 Metal 后端,--seed 默认随机。
每次调用都是独立进程,所以「总耗时」含一次完整加载。我用两字用例(你好。,输出 0.83 秒音频、总耗 2.1 秒)标定冷启动:加载大约 1.6 秒。表里的「净合成」是总耗时减去这 1.6 秒,RTF = 净合成 ÷ 音频时长。
| 用例 | 字数 | 音频时长 | 总耗时 | 净合成 | RTF | 峰值内存 | ASR 复读 CER |
|---|---|---|---|---|---|---|---|
| 默认音色(带 –lang) | 27 | 6.04s | 5.63s | 3.53s | 0.58 | 1306MB | 0.000 |
| 声音设计 女/青年/中音调 | 20 | 4.03s | 4.65s | 2.55s | 0.63 | 1306MB | 0.000 |
| 声音设计 男/老年/低音调 | 20 | 4.52s | 4.61s | 2.51s | 0.56 | 1306MB | 0.000 |
| 声音克隆(10 秒参考音) | 20 | 4.88s | 13.26s | 11.16s | 2.29 | 1306MB | 0.000 |
| 长稿 30 秒(单次生成) | 138 | 30.06s | 30.07s | 27.97s | 0.93 | 1306MB | 0.022 |
| 长稿 60 秒(分 5 块) | 276 | 60.40s | 101.20s | 99.10s | 1.64 | — | 0.022 |
| 不传 –lang 同句 | 27 | 6.04s | 5.65s | 3.55s | 0.59 | 1306MB | 0.000 |
几条读数值得单独拎出来:
RTF 0.56–0.63 是三种常用路径的水平,也就是合成 1 秒音频花半秒多。 这台机器没独显,这个成绩放进官方那张空白的 benchmarks 表里算能看的了。但注意它的耗时曲线不好看:30 秒单次生成 RTF 0.93,60 秒分块涨到 1.64 —— 文本越长,单位成本越高,跟「批量更划算」的直觉相反。我没做进一步拆解,只能说这是分块路径的实测表现,别拿短句的 RTF 去估长稿的交付时间。
克隆比默认音色慢四倍:RTF 2.29。 同样的 20 字、同样的输出时长量级,净合成 11.16 秒,而默认音色只要 2.5 秒。多出来的开销在参考音的前处理(静音裁剪、逐字稿对齐)和条件化上。做批量配音的话,这个倍数要算进排期。
声音设计确实改变语速。 同一个句子,「女,青年,中音调」出 4.03 秒,「男,老年,低音调」出 4.52 秒,长了 12%。也就是说想调语速,靠 instruct 只能间接影响,真要定长还是用 --duration。
–lang 不是可选项。 同一句话,写 --lang Chinese 时基频中位数 118.2Hz,不写变成 175.2Hz —— 音色是另一个。音频时长恰好都是 6.04 秒,但那是巧合,不是等价。中文内容一律显式写 --lang Chinese。
峰值内存 1306MB,其中语言模型 620.9MB。挂在后台常驻完全没负担。这个数字是按 Q8_0 那对权重量的。
坑一:权重坏了,程序照样给你一个 wav
这是整篇最值钱的一条。
我第一次下载用的是分段并发(六个线程按 4MB 切 range 打 resolve 链接)。文件大小对得上、命令正常打印 wrote out.wav, 72000 samples、退出码 0,一切看起来都好。生成的音频有谐波、有包络、有语句停顿,频谱平坦度 0.006(不是噪声)。
直到我把它丢给系统语音识别复读 —— 空的。英文也一样,识别出来只有一个「And」。
查下去才发现 shasum -a 256 和模型卡的 LFS oid 对不上。ggml 加载时只校验张量字节偏移是否落在文件长度内,不校验内容。 长度够,坏权重就能加载、能推理,然后吐出一段「像人声但不是人声」的东西。
量化特征也很清楚:那份输出的自相关峰只有 0.17,而正常浊音帧在 0.6 以上 —— 它有声带振动般的周期性外观,却没有稳定基频。这种输出,看文件大小、看采样率、看退出码、看有没有报错,全都发现不了。只有两个办法抓得住:校验 SHA256,或者让它自己念一遍再听写。
用 huggingface-cli download 会自动校验;用 curl 拉 resolve 链接,校验这一步得你自己补。
坑二:两处文档推荐的 codec 不是同一个
仓库 README 的 quick start 里,配 Q8_0 语言模型用的是 omnivoice-tokenizer-F32.gguf(734MB);而 Hugging Face 模型卡的文件表里,Q8_0 档位对应的是 omnivoice-tokenizer-Q8_0.gguf(289MB),还明写「recommended default」。
我一开始照 README 拿了 F32,白下 445MB。那一次的峰值内存是 2033MB,比 Q8_0 这套高 727MB —— 不过那一组数据同时还用着坏权重(见坑一),两个变量一起动了,不能算干净对照;能确定的是,codec 从 289MB 换到 734MB 之后内存确实明显上涨。两个都能跑,但 16GB 机器没有任何理由用大那个。照 README 抄命令的人,大概率会跟着多花这 445MB。
坑三:仓库里的二进制是 0 字节
如果你以为能直接抓一个现成的 omnivoice-tts 可执行文件,会踩到第三个坑。VoiceStudio 仓库的 bin/ 下确实有 omnivoice-tts-darwin-arm64 这类文件,但 git clone 下来的全是 0 字节占位符 —— 真二进制由 CI 构建,只随安装包分发,不进仓库。
代码层对这事有防御(binary_preflight.py 会检查文件非空、带真实 Mach-O/ELF 魔数,不合格就回落到 Python 内联后端),所以用 GUI 不会崩;但想直接拿二进制跑命令行,只有两条路:从安装包 resources/bin/ 里抠,或者像本文这样自己编译。
怎么验收:不靠耳朵也能判断合成质量
我听不到这些音频。没有试听条件的时候,「听起来自然吗」这个问题就没法回答,所以我搭了一套替代方案:系统 TTS 造参考音 + 系统 ASR 闭卷听写。
思路很直:让 macOS 自己念一段已知文本,导出 24kHz 单声道 wav 当克隆参考;合成完把输出丢回系统语音识别,让它「闭卷复读」,再跟原文算字错率(CER)。识别对了说明内容对;识别不出来,要么音频坏了,要么发音崩了。
参考音两行搞定:
say -v Meijia -o ref.aiff "大家好,我是这个博客的作者。今天我们来聊聊本地语音合成。"
afconvert -f WAVE -d LEI16@24000 -c 1 ref.aiff ref.wav
afinfo ref.wav | grep -E "duration|sample rate"复读用 macOS 26 的 SpeechAnalyzer(Swift,本地识别,不联网,100 行以内能写完)。这条路的产出正好能和 S1-mini 那篇的 ASR 洗稿流程接上:先用识别把音频压成文本,再决定要不要人工听。
这套方法有个必须先校准的地方:参考音本身要能通过复读。我第一次用系统 zh_CN 的 Flo 念参考音,同一句话识别 CER 0.361 —— 「暗夜独行」被念成「阅读行」,「语音合成」变成「本事何成」。换成 Meijia 后 CER 0.000。所以选参考音别随手挑一个中文嗓音,先用 ASR 验一遍,否则你后面所有打分都被这个噪声地板吃掉。
基线干净之后,表里那些 0.000 才有意义。同时也要说清楚它的边界:CER 0 只证明内容对,不证明音色像、不像真人。想看音色有没有学到,我用了另一个粗指标 —— 基频中位数。参考音 201.7Hz(p10–p90 154.8–283.0),克隆输出 240.0Hz(162.4–279.1),区间基本重合,说明音高范围被带过来了;但也确实往上偏了 38Hz。
至于长稿那 0.022 的 CER(138 字错 3 个字位),是三处同音字差异:「对齐字幕」听成「对其字幕」、「整段」听成「整断」、「机器味」听成「机器位」。这三处在我两次生成里都稳定复现。但我无法区分是模型读错了、还是识别听错了 —— 没有试听条件,这个结论只能停在「不能保证逐字正确」。这也是我建议正式稿件必须人工过一遍的原因。
顺带说:带 GPT-5.6、2026 年 11 月 5 日、0.15 美元、40% 的那句中英混排,CER 是 0.000。数字和缩写它读对了,这点比预想的好。
什么场景适合,什么场景别用
适合:
- 长稿批量出音。改一句重新生成一句,比重录便宜太多。有声书、课程稿、内部培训材料都在这一类。
- 给 Agent 加一张嘴。命令行 stdin/stdout 友好,
-o -能直接进管道;VoiceStudio 那边还带 MCP 服务器,generate_speech/clone_voice/transcribe三个工具就能让 Claude Code、Cursor 这类客户端念出来。音频理解那头的对照实验我在 LFM3 那篇做过,一头进一头出,正好凑成闭环。 - 离线、数据不出机器。音频素材不进别人服务器,这一条本身就值钱。
别用:
- 商业配音。默认权重 CC-BY-NC 4.0,除非换引擎并逐项核对许可。
- 要求一字不差的正式稿。CER 不为 0 就代表有字位可疑,数字、缩写、中英混排是高风险区(我这句侥幸全对,不代表每次都对)。
- 多说话人对话。一次一个音色,双人对谈得分别生成再拼,衔接得自己处理。
- 需要快速试错的场景。冷启动 1.6 秒、克隆 RTF 2.29,参数翻来覆去调的时候,等待感比想象中重。
还没测的部分
桌面版 GUI(没装,也没窗口环境)、视频配音与有声书两条工作流、本地 HTTP API(localhost:3900 的 /v1/audio/speech)、MCP 服务器的实际接入、Q4_K_M 与 BF16 的对比、Windows / Linux、646 种语言里中英文之外的语种。中文方言标签我只确认了参数会被接受,没逐条验证发音 —— 12 种方言听起来准不准,这个必须靠耳朵。
一句话收尾:这东西值得装,但请先看许可、先校验权重、别把「能生成 wav」当成成功。我在坏权重上浪费了将近半小时 —— 那段时间里它每一句都「成功」了。