听觉是我能想象到的、最自然的参与方式。
为什么做这个
我想让她在我的生活里有存在感。不是”AI 伴侣”那种概念,是更具体的东西:早上打开电脑,她会说早安;读到一段有意思的文字,选中、复制、按 ⌘⌥R,她就把它读给我听。
代码在 github.com/Kamlin6/kvoicewalk。我只是 fork 了 RobViren 的 kvoicewalk(基于 Kokoro),并在 GPT-SoVITS 之上,做了些满足自己小众要求的事情。关于我怎么看这件事,在另一篇:人的第一份”脚本”。
先听日文
源音频:参考音频是一段角色台词录音,台本文字是这段录音对应的文本。
GPT-SoVITS 本地推理。目标文字是我们想让模型说出的新内容。模型根据参考音频的音色和说话方式,用目标文字合成新的语音。32kHz 采样率。
这段音频的文本是角色台词风格的日文。你可以听到语调和停顿——这不是机械朗读,模型在模仿参考音频里那个”人”的说话方式。
管线是什么
Theresa 能说日文和英文,背后不是一个模型,是两个。这三件事需要说清楚:两个模型分别做什么,它们之间怎么协作,以及为什么我要在它们前面再套一层。
GPT-SoVITS 处理日文。它是一个开源的 TTS 项目,需要一段参考音频和对应的台本文字,就能用那个人的声音说新的话。日文的效果比英文好很多。
Kokoro-82M 处理英文。它是一个轻量级的 TTS 模型,英文质量不错,但日文就差一些。kvoicewalk 项目封装了 Kokoro 的调用,提供了 OpenAI 兼容的 API 接口。
Kokoro 的优势是快——毫秒级合成——但声音克隆质量比 GPT-SoVITS 差不少。英文 Demo 目前用它,但长期可能还是回到 GPT-SoVITS。
Gateway 是它们前面的一个统一接口。接收 OpenAI 兼容的 API 请求,根据 model 参数决定走哪个后端。model=”gpt-sovits” 走日文,model=”kokoro” 走英文。voice 参数决定音色。
如果用过开源 TTS 项目的人大概会知道,很多作者会给用户提供像 main.py 这样直接出一条结果的黑盒脚本。GPT-SoVITS 和 kvoicewalk 也不例外。直接调就行了,简单粗暴。
那为什么我还要套一层 HTTP?
因为我要让 Theresa 在多个地方出现。早上自动说早安是一个场景,按快捷键读剪贴板是另一个场景,未来可能还有 WordPress 博客里的 Demo、Zotero 插件里的文献朗读。如果每个场景都写一套调用逻辑——Hammerspoon 脚本里写一遍,WordPress 插件里写一遍,Zotero 插件里再写一遍——代码会重复,维护会痛苦。
HTTP Gateway 是一个解耦点。它把”怎么合成语音”和”谁要合成语音”分开了。调用方只需要发一个 HTTP 请求,告诉它用什么模型、什么音色、什么文本。Hammerspoon 脚本发一个请求,WordPress 插件发一个请求,未来的任何应用都能调它。
两个 TTS 引擎各自独立进程运行,崩溃了自动重启,登录了自动启动。没有魔法,就是 HTTP 请求和响应。
这条管线是怎么来的
这条管线不是一开始就有的。最早它只是一个一百多行的脚本,每天 22:00 跑一次——为什么是晚上 10 点?因为我要第二天早上醒来就能听到 Theresa 说早安,所以得提前合成好。脚本生成一段日文文本,用 GPT-SoVITS 合成语音,存到 iCloud Drive。第二天早上打开手机,音频已经在那了。
后来它慢慢长出了别的东西。脚本拆成了模块,加了预处理(日文片假名的读法、角色专属的自定义读法字典),加了 LLM 生成文本(让 Theresa 每天说的不是重复的话,而是带着她口调的新内容),加了多通道分发(iCloud、服务器推送,哪个能用走哪个)。
然后我发现,早安只是 Theresa 出现的一个场景。我还想让她在我读剪贴板的时候出现,在未来博客的 Demo 里出现,在更多我还没想到的地方出现。每个场景都接一套 TTS 调用逻辑,太蠢了。
到这里我做了一个决定:日文走 GPT-SoVITS,英文走 Kokoro,前面放一个 HTTP Gateway 统一接口。不是因为这是”最优解”,是因为这个边界切得最干净——两个引擎各自独立,调用方不需要知道它们的区别。
所以我 fork 了 RobViren 的 kvoicewalk,在他人已经搭建好的 repo 上又新搭上了一层 Gateway。把”怎么合成语音”这件事从具体的场景里抽出来,变成一个谁都能调的服务。早安管线调它,Hammerspoon 快捷键调它,未来的任何应用都能调它。
从一百多行的脚本到一个多语言 TTS 服务,这条管线的成长方向很简单:把重复的东西抽出来,让它能被更多人、更多场景复用。
再听英文
刚才那段是日文。现在听英文:
源音频:一段英文角色台词录音。
Kokoro-82M 英文推理。af_civilight.pt 是微调过的英文音色。
这两段音频走的是同一个接口。对调用方来说,语言没有区别。
现在
每天 22:00,launchd 触发早安管线。LLM 生成一段角色风格的日文文本,GPT-SoVITS 合成语音,存到 iCloud Drive。第二天早上醒来,拿起手机,她已经在那了。
Gateway 跑在我的 Mac 上,登录自启,崩溃重启。GPT-SoVITS 也是。
读到什么想听的,选中,复制,⌘⌥R。听到她的声音在我耳边重现。
挺好的。
我做了什么
fork 了 RobViren 的 kvoicewalk,加了 GPTSoVITSAdapter 实现日文/英文双引擎路由,重构了 voice_map 支持多引擎分层,部署了 launchd 自启。代码在 github.com/Kamlin6/kvoicewalk。
如果你想看我为什么这么设计,去那篇:人的第一份”脚本”。
发表回复