1. 为什么轻小说配音比想象中更麻烦?先理清需求
喜欢看轻小说的朋友,多少都有过这样的念头:要是小说里的对话能像动画一样“演”出来就好了。正经的广播剧、有声书当然好听,但制作门槛高,也不一定能选到自己喜欢的角色声线。于是就有不少人把目光转向TTS(Text To Speech,文本转语音)——现在开源社区里的TTS工具已经能做出相当自然的语音,稍微调一调,就能给小说里的不同角色分配不同的音色,生成一段有“演出感”的多角色配音。
但真正上手你就会发现,这事比“装个软件点一下”要复杂得多。轻小说动辄几十万字,对话密度极高,角色动辄十几个,光是把正文和台词分开就已经够麻烦。更麻烦的是,同一个角色要保持声音统一,不同的角色之间又要有明显区分,情绪还要跟着剧情走。这背后其实是一套完整的TTS技术栈在支撑:首先要有人工合成的神经网络TTS模型,能把文字变成自然语音;其次要把模型转换成适合本地CPU推理的ONNX格式,否则你只能依赖云端服务;最后还要有能批量处理长文本、自动切分对话、逐句合成的工程流程。
这篇文章我打算从实际做过的项目出发,聊一聊“轻小说多角色配音TTS”是怎么一步步落地的。内容包括底层原理、方案选型、基于sherpa-onnx的完整流程、以及对CPU用户特别友好的优化思路。无论你是只想给一部小说配个音自娱自乐,还是想批量给几十部小说做有声化,这套思路都能直接套用。
先直接说结论:本地CPU跑TTS完全可行,用onnx格式的神经网络TTS,配合sherpa-onnx这类推理引擎,一台普通笔记本就能在几分钟内生成一小时的配音内容。不需要买显卡,不需要充API额度,所有数据都在本地,安全性上也更可控。适合想低成本尝试、或者对语音数据比较敏感的人群。
接下来,我会从头把这个项目拆开,尽量少讲空话,多给能直接落地的经验和代码。
1.1 多角色配音到底难在哪里
轻小说配音的第一道障碍不是声音合成,而是“角色分配”。一部轻小说通常包含大量对话,并且对话之间夹杂着叙述、动作描写和心理活动。你要做的第一件事是识别出每句台词由谁说,然后给每个说话人一个独立的角色ID,再映射到不同的音色上。
听起来简单,实际处理却经常翻车。中文文本没有天然分隔符,引号、冒号、破折号都需要处理;更复杂的是有些角色在对话中还会转述别人的话,出现“他笑着说:‘我当时说:没问题。’”这样的嵌套引号。如果直接用正则表达式一刀切,很容易切错。
我的做法是分三步走。第一步,先把文本按“引号”切块,区分出对话和叙述。第二步,利用上一句的人物提示(比如“林默走进来说道”)判断当前说话人是谁,如果一句话没有提示,就默认沿用上一位说话人。第三步,再人工抽查几个高风险段落。这个规则对大多数轻小说都能达到90%以上的准确率,剩下那10%就得靠人工修正。
角色ID确定之后,下一个问题是音色选择。开源TTS模型通常内置的音色数量有限,有的只有两三个,有的多一些,比如vits家族经常有几十个。但小说里的女角色可能有四五个,男角色也有四五个,音色不够用怎么办?一种做法是同一个音色给多个角色用,再通过语速、音调微调来区分;另一种做法是换用支持“说话人嵌入”的模型,从预训练空间里插值出新的音色。后者听起来高级,但实际效果不稳定,我后面会细说。
1.2 现有方案:云端大模型、本地TTS、APK工具怎么选
现在市面上的TTS方案,大致可以分成四类,各有各的坑。
第一类是云端商用大模型TTS,比如智云TTS、各家云厂商的语音合成接口。优点是音质极佳、情感丰富,支持实时流式合成;缺点也很明显:要走API,按字符计费,长文本的金额吃不消,而且网络延迟意味着你没法快速批量生成上万条台词。还有隐私问题,小说稿子如果没公开过,传给第三方总有些顾虑。
第二类是开源神经TTS的本地推理方案,也就是我这次主要使用的路线。常见的模型有VITS、Tacotron2、FastSpeech2,以及各种变种。这些模型在训练完成并导出成ONNX之后,可以在CPU上很快地跑完合成任务。优点是免费、离线、可控,缺点是模型本身不会像商用大模型那样智能处理停顿和重音,需要你在文本侧做不少工程。
第三类是基于Android的TTS APK工具,比如sherpa-onnx本身就提供安卓demo,你可以直接编译成APK装到手机上。这类方案很适合“移动端”场景,把模型和工具打包成App,随时随地点开就能生成语音。我之前试过在手机上跑一个小型中文TTS模型,一段几十字的台词两秒内就能出结果,体验不错。但手机端的内存和调度限制,不适合一口气生成几十万字,更适合零散给几句话配音。
第四类是“语音朗读包”,比如某些阅读软件里的朗读包。这里说的“阅读3.0语音朗读包TTS”算是很典型的形态:给阅读软件挂一个TTS引擎,在阅读时实时读出来。这类方案的优点是和小说阅读器无缝集成,但通常只适合“整本朗读书籍”,给某个角色单独换声线就非常吃力了——毕竟朗读包面向的是全本连续朗读,不是多角色扮演。
综合下来,如果你目标是“轻小说多角色配音”,想做完整的“广播剧版”小说,最合理的路径还是本地TTS脚本配合角色映射。云端作为辅助参考或最终质量天花板,手机端作为顺手监听工具,主力还是得放在一台能批量执行脚本的电脑上。
2. 本地TTS的核心技术拆解:从神经网络到ONNX导出
既然选择了本地方案,你就不能只停留在“录音”层面,得稍微理解一下TTS是怎么工作的。不然参数调整、模型选择都会变成瞎猜。
2.1 神经网络TTS的原理一句话讲清
现代神经网络TTS本质上是一个“文本到声学特征,再到波形”的生成过程。文本编码器把文字序列转成语义向量,然后经过声学模型或端到端模型,预测出梅尔频谱——你可以把梅尔频谱理解成声音的“乐谱”,里面记录了每一小段时间内各个频段的能量分布。最后通过声码器(Vocoder),把梅尔频谱变回人耳能听到的波形。
这里的端到端模型比如VITS,它把文本编码器、声学模型和声码器统一到一个网络里,训练完成后直接输入文字,输出就是16kHz或22.05kHz的WAV。相比传统的Tacotron+HifiGAN两级结构,端到端模型更紧凑,合成速度快,语气也更自然。
但这里有个关键点:神经网络TTS训练时会固定说话人ID。模型通过一个说话人嵌入向量来控制音色,多角色TTS本质上就是切换这个向量。VITS这类模型一般支持多说话人,会有一个说话人嵌入表,你想用哪个音色,就在输入里带一个对应的ID。所以在给轻小说配音前,你需要先查清楚模型的说话人列表,或者看看能不能通过修改嵌入向量来合成新音色。
2.2 为什么ONNX和sherpa-onnx是本地部署的关键
提到本地部署,绕不开ONNX。ONNX是一个开放的模型中间表示格式,它存在的意义是让模型不绑定某个深度学习框架。你用PyTorch训练出来的VITS,权限很广,但你不需要训练,只需要推理,那就没必要装一堆GPU版深度学习库。把模型导出成ONNX后,你可以用ONNX Runtime在CPU上跑,也可以用sherpa-onnx这个专门为语音推理优化的C++库来跑。
sherpa-onnx是k2-fsa社区下的开源项目,它一个库就把TTS、语音识别、说话人识别都覆盖了。对TTS来说,sherpa-onnx的优势在于三点:一是模型格式很标准,支持VITS、Matcha-TTS、Piper等生成模型转换成ONNX推理;二是部署简单,支持Python、C++、Java、Kotlin甚至JavaScript,还有Android/iOS的APK和App导出方式;三是它内置了VAD、音频播放等常用工具,省得自己造轮子。
我为什么强烈推荐用ONNX而不是直接拉PyTorch仓库跑推理?因为PyTorch推理的开销和依赖太重了。比如你只是想给一段文字合成语音,拉一个完整的Python深度学习框架,光是安装可能就要几个GB,而且每次启动都要加载动态图环境,CPU推理速度还会慢不少。导出成ONNX之后,模型文件通常只有几十MB,ONNX Runtime的CPU版本非常轻量,极大提升了工程化程度。
另外,很多开源TTS仓库也把ONNX作为默认发布格式。你可以直接下载别人转好的ONNX模型,省去自己导出的调试时间。像sherpa-onnx的模型仓库里就有很多现成的中文TTS模型,一个命令就能下载,非常适合快速启动项目。
2.3 CPU上跑TTS的性能真相
一提“神经网络合成语音”,很多人第一反应是“必须要有GPU”。这是个很大的误解。推理和训练是两码事,训练需要大量反向传播计算,GPU绝对优势;推理只是前向传播,模型参数量通常不大,CPU完全能胜任。
我实测过几个常见的VITS中文模型,文本长度在30个汉字以内时,单句合成时间基本在0.5秒到1.5秒之间。模型文件多采用quantize(量化)过的ONNX,也就是把权重从float32压到float16或int8,这样推理内存更小、速度更快,音质的损失一般听不太出来。
不过CPU推理的性能上限取决于你的并行策略。如果你单纯写一个for循环逐句合成1000句话,每句1秒,那么总耗时就是1000秒,相当于17分钟,肉眼可见地等。但如果你用多线程并行合成,把10个线程分别跑不同角色、不同段落,总耗时能压到几分钟。再加上批处理、缓存、格式转换,整本书的配音完全可以在一顿午饭的时间内搞完。
出于经验,有两类CPU上的坑要提醒。第一,不要用默认线程数跑所有进程,因为CPU同时跑太多TTS进程会导致高频争抢,总时间反而变慢。一般来说,每个核心分配一个线程就够了,超线程可以适当做2倍。第二,内存方面,单个模型加载后大约占用300MB到1GB内存,如果你并行加载多个不同的模型,内存会线性上涨。为了控制内存,多角色配音最好共用同一个模型,只是切换说话人ID,而不是加载多个模型文件。
3. 实操流程:用sherpa-onnx搭一个多角色TTS脚本
理论说了一堆,下面直接上实操。我用的是sherpa-onnx的Python接口,项目结构非常简单:负责加载模型、文本预处理、语音合成、写入文件。整个过程不依赖GPU,也没有复杂的训练环节,所以你可以在自己的电脑上立即开始尝试。
3.1 第一步:准备模型和工具
首先安装依赖。官方推荐使用pip安装sherpa-onnx,也可以直接从源码编译,但pip最省事。建议新建一个虚拟环境,避免污染系统Python环境。
python -m venv ttsenv source ttsenv/bin/activate # Windows下是 ttsenv\Scripts\activate pip install sherpa-onnx soundfile模型方面,推荐去HuggingFace或sherpa-onnx官方模型库找一个支持多说话人的中文TTS模型。我用的模型基于VITS,支持两个或更多说话人。下载后应该有类似model.onnx、tokens.txt、speakers.txt的文件结构。speakers.txt就是说话人ID对应的音色名称列表。
我自己习惯把所有模型文件丢在一个目录下,然后写一个简单的配置文件来引用,这样换模型时不用改脚本。
3.2 第二步:加载模型生成单角色语音
sherpa-onnx的Python API很直观。核心逻辑是创建一个OfflineTtsConfig,里面指定模型路径,然后调用OfflineTts类的generate方法。下面是一个最小示例,目标是让一个角色说一句话并保存成WAV。
import sherpa_onnx import soundfile as sf config = sherpa_onnx.OfflineTtsConfig( model=sherpa_onnx.OfflineTtsModelConfig( vits=sherpa_onnx.OfflineTtsVitsModelConfig( model="path/to/model.onnx", tokens="path/to/tokens.txt", lexicon="", # 中文一般不用lexicon dict_dir="", data_dir="", ), num_threads=4, debug=False, ), rule_fsts="", max_num_sentences=1, max_sentence_len=200, sample_rate=16000, ) tts = sherpa_onnx.OfflineTts(config) speaker_id = 0 # 对应speakers.txt里的第一个说话人 text = "今天天气真不错,我们出去走走吧。" audio = tts.generate(text, sid=speaker_id, speed=1.0) if audio.samples: sf.write("output.wav", audio.samples, audio.sample_rate)这里有两个细节要注意。一是sample_rate要匹配模型的采样率,很多中文模型是22050Hz,也有16000Hz的,如果写错了,生成的语音要么变调,要么时间轴错乱。二是speed参数,默认为1.0,调低会变慢但可能更清晰,调高会让角色语速更快。对于活泼的角色,可以设1.05或1.1;对于沉稳角色,用0.95左右,能很快制造性格差异。
如果模型支持的人声很多,你可以在generate时传不同的sid,得到不同音色的音频。这个sid一定要在speakers.txt的范围内,否则会报错或生成奇怪的音色。
3.3 第三步:实现多角色对话脚本
真正做小说配音时,我们不是为单个句子调用接口,而是希望输入一段带有角色标记的“剧本”,自动合成一个多角色对话的音频文件。我设计了一种简单的标记格式:把文本写成“角色名: 台词”,每行一句,脚本解析后调用TTS生成并拼接。
举个例子,有一小段对话:
林默: 你说这里真的有宝藏? 苏菲: 喏,地图上的箭头指的就是这儿。 旁白: 两个人站在废弃的神殿门前,四周静得可怕。解析逻辑也很简单:按“角色名: 台词”结构拆开,维护一个角色到speaker_id的映射表。旁白可以是固定的叙述者音色,每个角色对应一个speaker_id。然后依次调用generate,把每句合成的音频排到同一个时间轴上。
拼接时注意一点:TTS生成的句子开头和结尾通常会有一点静音,不同句子之间需要做一个超简单的声音淡入淡出,否则听感会很顿挫。可以每隔一句之间加80到150毫秒的间隔,这个间隔可以根据角色情绪来调整。激烈争吵时间隔缩短,思考停顿时间隔拉长。
下面是我项目里核心的多角色合成函数片段:
import numpy as np import soundfile as sf from pathlib import Path def synthesize(tts, script_lines, role_sid_map, out_path, silence_ms=100): chunks = [] sr = tts.sample_rate for line in script_lines: role, text = line.split(":", 1) sid = role_sid_map.get(role.strip(), 0) audio = tts.generate(text.strip(), sid=sid) if not audio.samples: continue chunks.append(np.array(audio.samples, dtype=np.float32)) chunks.append(np.zeros(int(sr * silence_ms / 1000), dtype=np.float32)) audio_parts = chunks[:-1] if chunks else [] if audio_parts: final = np.concatenate(audio_parts) sf.write(out_path, final, sr)这个函数已经可以直接跑通多角色对话。但距离一本小说还有几件事要做:长文本切分、音色分配和自动添加背景音。
3.4 第四步:后处理与音量平衡
多角色配音里最常被忽略的是音量一致性。不同音色、不同句子在TTS模型输出后,响度差异可能很大。有的角色声音很小,有的角色突然炸麦。有些模型内部有自动增益,但效果有限。我建议在拼接后做一个响度归一化。
这里有个简单方法:用librosa计算每段音频的RMS(均方根值),然后调整增益到目标值。因为我不想给项目增加太多依赖,也可以直接用ffmpeg过滤,比如:
ffmpeg -i input.wav -af loudnorm=I=-16:TP=-1.5:LRA=11 output.wav这个命令把整体响度标准化,就不容易出现某句话特别突兀的情况。
还有一个实操技巧:对话密集的段落,可以在每个角色说话时加入轻微的房间混响——这个不是必需的,但加了之后听感会自然很多。轻小说场景多在户外、教室、异世界城堡,可以通过ffmpeg的aecho或SOX的reverb特效模拟空间感。别加太多,否则人声会糊掉。我给混响效果设置的常用比例是干声0.85,湿声0.15左右。
4. 常见问题、避坑经验与效果调优
一路跑下来,肯定会遇到各种问题。这里我把最常踩的坑和排查方法整理一下。
4.1 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 生成声音只有噪音或沙沙声 | 采样率不匹配,模型期望22050Hz但配置成16000Hz | 检查模型文档,修改sample_rate设置 |
| 某些说话人ID报错 | speakers.txt中不存在该ID | 检查speakers.txt,使用模型支持的ID范围 |
| 中文停顿和断句不理想 | 模型对过长的句子支持不好;标点符号没被利用 | 先按分句规则切分,确保单句不超过100字符,并保留逗号、句号、问号 |
| 生成的语音语速过快或过慢 | speed参数设置不当 | 调speed值,一般0.9到1.1之间微调 |
| 两个角色声音太像 | 音色映射重叠,模型说话人个数有限 | 优先把差异大的角色分配给不同的speaker_id;再通过speed差异补一层区分 |
| 长文本合成造成内存飙升 | max_sentence_len设置过大 | 限制max_sentence_len,并开启文本自动切分 |
| 拼接后的音频有爆音 | 缓冲区整数转换溢出或拼接处相位跳变 | 开头和结尾加短淡入淡出,使用浮点计算;用sf.write处理int16前做clip |
我遇到过最离谱的一次是模型下载下来音色列表有90多个,实际可用的只有前几个,后面的ID合成全是一个腔调。后来看文档才知道,那是预训练模型仅对前几个说话人做了充分优化,其余ID属于冷启动。所以还是老老实实先手工试每一个ID的音色,再决定角色映射。
4.2 配音质量提升技巧
给文本加“表演提示”。有些TTS模型支持情绪标签,或者你可以通过改变文本标点来改变语气。比如增加感叹号会让句子更有力度,句号更平淡,省略号会有停顿感。多角色配音时,最好在剧本中手动调整标点为最终语气服务。
善用旁白和对话的“速度差”。旁白适合用正常速度、平稳语气;角色对话可以适当放慢或加快。只要速度差控制在0.1以内,听起来不会觉得突兀,却能让角色更立体。
插入呼吸声和停顿。VITS模型本身能生成自然的词间停顿,但句间停顿时长通常较短。如果想营造紧张感,可以在剧本中插入“(沉默)”这样的标记,让纯文本排除,在拼接时加入额外静音。这比后处理硬加静音要准确得多。
不要盲目追求“更像人声”。很多新手花大量时间刷模型、调参数,想要的不是TTS,而是重配音演员。真要达到广播剧水平,需要更专业的方案:比如先训练一个角色专属的VITS模型,还要标注语料。但在一部小说的多角色制作里,核心诉求是“快速、稳定、角色区分度”,不是“以假乱真”。所以平衡好投入产出比。
4.3 其他工具与扩展方向
如果你不想写代码,或者需要把TTS塞进阅读器,下面这些方向可以关注。
第一类是开箱即用的APK工具。sherpa-onnx提供了Android端demo,你编译后能直接在手机上选择模型文件和说话人ID,输入文本或从小说中粘贴段落,即时生成音频。优点是方便,缺点是批量处理能力弱,角色映射也需要手动切换。适合在出外勤时临时验证一句台词的音色效果。
第二类是阅读器插件。像“阅读3.0”这类阅读器,可以通过外挂TTS服务实现朗读。常见做法是把本地TTS封装成HTTP服务,阅读器通过系统TTS接口或自定义接口调用。但注意,阅读器一般只支持单角色朗读,多角色需要你在脚本端把“角色标记”转化成“不同演员的声音”再输出成一个文件。所以这条路更适合“整本有声书”而不是“角色扮演式配音”。
第三类是更自然的神经网络TTS模型。目前已经有类似Matcha-TTS这样基于扩散模型的TTS,语音自然度更高,而且已支持ONNX推理。如果你觉得VITS音色有点“电子味”,可以试试。代价是模型文件更大,CPU合成速度略慢,但对多角色TTS而言,换模型的成本比换整个流程低得多。
第四类是“音色插值”。如果同一模型里只有男声和女声,没有中间声线,你可以尝试修改模型中的说话人嵌入向量,比如直接把两个speaker的embedded vector做线性插值,得到一个新的中间音色。VITS模型的说话人嵌入一般存储在模型内部,想这么做需要动模型结构然后把ONNX重新导出。这个我不建议新手碰,但可以作为进阶玩法探索。
5. 从“能跑”到“好用”的最后几步经验心得
最后的最后,想分享几个实际项目中总结出来的经验,不放大道理,就是能让过程顺滑很多的细节。
第一,先做10分钟小样,再全书批量。别一上来就把整本小说扔进脚本。先摘出前三章的对话,建好角色音色表,合成一段3分钟的小样,反复听。确认满意了再写批处理脚本。因为角色映射、音色风格、语速范围这些决定一旦做错了,后面整本书都要重新生成,时间成本很高。
第二,角色音色表要用配置文件维护。我遇到过把“苏菲”和“小菲”搞混的情况,两个角色音色调换,导致整章重做。后来我直接用JSON存储角色映射关系,包含角色名、speaker_id、语速、音调偏移、备注等。哪怕小说有四十个角色,管理起来也不会乱。
第三,别忘了给输出文件起有规律的名字。建议按“书名_章节号_角色名_序号.wav”来存,方便检查。如果生成过程中途断了,你可以根据文件名定位到断点,避免从头再来。
第四,给每本书建一个“文本清洗规则”。轻小说的作者排版习惯各不相同,有人用中文引号“”,有人用直角引号「」,还有人在对话前加缩进。你可以先用正则把不同引号统一成一种,再走角色识别流程。一小段规则代码,能省掉你未来几小时的修正时间。
我在做这类项目时最大的感受是:TTS本身已经不是瓶颈了,真正决定成品质量的是文本处理和工程组织能力。神经网络TTS把音色、发音、韵律这些问题解决得很好,剩下的“怎么让一段几十万字的文本变成一部有声角色剧”的细节,每一位创作者都能靠自己的流程优化出不同的味道。
如果你手头正好有喜欢的轻小说想试试,不妨从今天开始,先选一个TTS模型,做一个角色说一句话,体会一下那种“角色活了”的惊喜。然后往里面加第二个、第三个角色,很快你就能完成属于自己的多角色配音剧场。这个过程没有想象中那么困难,但也没有简单到一键生成,需要的就是耐心和一点点工程折腾。祝你成功。