Rust 重构 TTS 引擎:kokoroi-rs 的高性能流水线架构揭秘
摘要:本文深入解析 kokoroi-rs------一个用 Rust 重写的生产级多语言 TTS 引擎。文章从架构设计出发,详细拆解了其核心流水线(TextSplitter、Preprocessor、Generator)、多实例并行推理策略、中英文 G2P 处理方案以及全静态跨平台构建方法。通过 Rust 的安全性与并发能力,kokoroi-rs 实现了 5-10 倍实时率的推理性能,并交付了可独立部署、零依赖的二进制文件。适合关注 Rust + AI 工程化、高性能语音合成系统的开发者阅读。
关键词:Rust;TTS 引擎;kokoroi-rs;语音合成;ONNX Runtime;流水线并行;G2P;高性能计算;AI 工程化
从 Python 原型到生产级 Rust 实现 ------ 一个 82M 参数多语言 TTS 系统的工程化之路
引子:当 TTS 遇上 Rust
在 AI 音频领域,Kokoro 是一个独特的存在------它用 82M 的轻量参数实现了高质量的多语言语音合成,且采用 Apache-2.0 开源协议,让开发者可以自由使用。但 Kokoro 的原型基于 Python + PyTorch,这在部署到高并发生产环境时会暴露一些问题:体积大、启动慢、依赖复杂、并发能力受限于 GIL。
kokoroi-rs 给出了另一种答案:用 Rust 重写整条 TTS 流水线,ONNX Runtime 承载推理,最终交付一个可独立部署、高吞吐、低延迟的语音合成系统。
本文会从架构设计角度,拆解 kokoroi-rs 的核心流水线 、多实例并行推理 、中文 G2P 处理 以及跨平台构建,希望能给关注 Rust + AI 工程化的读者带来一些启发。
一、整体架构:分层解耦,各司其职
整个项目采用 Cargo workspace 管理,按功能拆分成 5 个 crate:
| Crate | 职责 |
|---|---|
kokoros-core |
TTS 引擎核心:流水线、G2P、ONNX 推理 |
koko-cli |
命令行工具 |
kokoros-server |
HTTP REST + SSE 流式服务 |
kokoros-openai |
OpenAI 兼容 API(可直接对接现有工具) |
misaki |
英语 G2P 引擎(独立子模块) |
依赖关系清晰,核心逻辑都收敛在 kokoros-core 中,上层接口只是薄薄的封装。下图展示了整体的分层结构:
#mermaid-svg-gWoZrecm8OI05LTQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gWoZrecm8OI05LTQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gWoZrecm8OI05LTQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gWoZrecm8OI05LTQ .error-icon{fill:#552222;}#mermaid-svg-gWoZrecm8OI05LTQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gWoZrecm8OI05LTQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gWoZrecm8OI05LTQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gWoZrecm8OI05LTQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gWoZrecm8OI05LTQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gWoZrecm8OI05LTQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gWoZrecm8OI05LTQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gWoZrecm8OI05LTQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gWoZrecm8OI05LTQ .marker.cross{stroke:#333333;}#mermaid-svg-gWoZrecm8OI05LTQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gWoZrecm8OI05LTQ p{margin:0;}#mermaid-svg-gWoZrecm8OI05LTQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gWoZrecm8OI05LTQ .cluster-label text{fill:#333;}#mermaid-svg-gWoZrecm8OI05LTQ .cluster-label span{color:#333;}#mermaid-svg-gWoZrecm8OI05LTQ .cluster-label span p{background-color:transparent;}#mermaid-svg-gWoZrecm8OI05LTQ .label text,#mermaid-svg-gWoZrecm8OI05LTQ span{fill:#333;color:#333;}#mermaid-svg-gWoZrecm8OI05LTQ .node rect,#mermaid-svg-gWoZrecm8OI05LTQ .node circle,#mermaid-svg-gWoZrecm8OI05LTQ .node ellipse,#mermaid-svg-gWoZrecm8OI05LTQ .node polygon,#mermaid-svg-gWoZrecm8OI05LTQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gWoZrecm8OI05LTQ .rough-node .label text,#mermaid-svg-gWoZrecm8OI05LTQ .node .label text,#mermaid-svg-gWoZrecm8OI05LTQ .image-shape .label,#mermaid-svg-gWoZrecm8OI05LTQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-gWoZrecm8OI05LTQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gWoZrecm8OI05LTQ .rough-node .label,#mermaid-svg-gWoZrecm8OI05LTQ .node .label,#mermaid-svg-gWoZrecm8OI05LTQ .image-shape .label,#mermaid-svg-gWoZrecm8OI05LTQ .icon-shape .label{text-align:center;}#mermaid-svg-gWoZrecm8OI05LTQ .node.clickable{cursor:pointer;}#mermaid-svg-gWoZrecm8OI05LTQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gWoZrecm8OI05LTQ .arrowheadPath{fill:#333333;}#mermaid-svg-gWoZrecm8OI05LTQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gWoZrecm8OI05LTQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gWoZrecm8OI05LTQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gWoZrecm8OI05LTQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gWoZrecm8OI05LTQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gWoZrecm8OI05LTQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gWoZrecm8OI05LTQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gWoZrecm8OI05LTQ .cluster text{fill:#333;}#mermaid-svg-gWoZrecm8OI05LTQ .cluster span{color:#333;}#mermaid-svg-gWoZrecm8OI05LTQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gWoZrecm8OI05LTQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gWoZrecm8OI05LTQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-gWoZrecm8OI05LTQ .icon-shape,#mermaid-svg-gWoZrecm8OI05LTQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gWoZrecm8OI05LTQ .icon-shape p,#mermaid-svg-gWoZrecm8OI05LTQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gWoZrecm8OI05LTQ .icon-shape .label rect,#mermaid-svg-gWoZrecm8OI05LTQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gWoZrecm8OI05LTQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gWoZrecm8OI05LTQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gWoZrecm8OI05LTQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 依赖
核心引擎 (kokoros-core)
接入层
koko-cli
kokoros-server
kokoros-openai
Pipeline 流水线
TTS 引擎
G2P 处理
(中文/英文)
ONNX 推理层
onnxruntime
jieba-rs
misaki
二、核心流水线:多阶段并行,吞吐飙升
流水线(Pipeline)是 kokoroi-rs 性能的关键。它将 TTS 过程拆成 三个独立阶段,通过有界通道连接,形成生产者-消费者模型:
#mermaid-svg-O0C1WKQLBbFcSz56{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-O0C1WKQLBbFcSz56 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-O0C1WKQLBbFcSz56 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-O0C1WKQLBbFcSz56 .error-icon{fill:#552222;}#mermaid-svg-O0C1WKQLBbFcSz56 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-O0C1WKQLBbFcSz56 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-O0C1WKQLBbFcSz56 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-O0C1WKQLBbFcSz56 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-O0C1WKQLBbFcSz56 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-O0C1WKQLBbFcSz56 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-O0C1WKQLBbFcSz56 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-O0C1WKQLBbFcSz56 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-O0C1WKQLBbFcSz56 .marker.cross{stroke:#333333;}#mermaid-svg-O0C1WKQLBbFcSz56 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-O0C1WKQLBbFcSz56 p{margin:0;}#mermaid-svg-O0C1WKQLBbFcSz56 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-O0C1WKQLBbFcSz56 .cluster-label text{fill:#333;}#mermaid-svg-O0C1WKQLBbFcSz56 .cluster-label span{color:#333;}#mermaid-svg-O0C1WKQLBbFcSz56 .cluster-label span p{background-color:transparent;}#mermaid-svg-O0C1WKQLBbFcSz56 .label text,#mermaid-svg-O0C1WKQLBbFcSz56 span{fill:#333;color:#333;}#mermaid-svg-O0C1WKQLBbFcSz56 .node rect,#mermaid-svg-O0C1WKQLBbFcSz56 .node circle,#mermaid-svg-O0C1WKQLBbFcSz56 .node ellipse,#mermaid-svg-O0C1WKQLBbFcSz56 .node polygon,#mermaid-svg-O0C1WKQLBbFcSz56 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-O0C1WKQLBbFcSz56 .rough-node .label text,#mermaid-svg-O0C1WKQLBbFcSz56 .node .label text,#mermaid-svg-O0C1WKQLBbFcSz56 .image-shape .label,#mermaid-svg-O0C1WKQLBbFcSz56 .icon-shape .label{text-anchor:middle;}#mermaid-svg-O0C1WKQLBbFcSz56 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-O0C1WKQLBbFcSz56 .rough-node .label,#mermaid-svg-O0C1WKQLBbFcSz56 .node .label,#mermaid-svg-O0C1WKQLBbFcSz56 .image-shape .label,#mermaid-svg-O0C1WKQLBbFcSz56 .icon-shape .label{text-align:center;}#mermaid-svg-O0C1WKQLBbFcSz56 .node.clickable{cursor:pointer;}#mermaid-svg-O0C1WKQLBbFcSz56 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-O0C1WKQLBbFcSz56 .arrowheadPath{fill:#333333;}#mermaid-svg-O0C1WKQLBbFcSz56 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-O0C1WKQLBbFcSz56 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-O0C1WKQLBbFcSz56 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-O0C1WKQLBbFcSz56 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-O0C1WKQLBbFcSz56 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-O0C1WKQLBbFcSz56 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-O0C1WKQLBbFcSz56 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-O0C1WKQLBbFcSz56 .cluster text{fill:#333;}#mermaid-svg-O0C1WKQLBbFcSz56 .cluster span{color:#333;}#mermaid-svg-O0C1WKQLBbFcSz56 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-O0C1WKQLBbFcSz56 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-O0C1WKQLBbFcSz56 rect.text{fill:none;stroke-width:0;}#mermaid-svg-O0C1WKQLBbFcSz56 .icon-shape,#mermaid-svg-O0C1WKQLBbFcSz56 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-O0C1WKQLBbFcSz56 .icon-shape p,#mermaid-svg-O0C1WKQLBbFcSz56 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-O0C1WKQLBbFcSz56 .icon-shape .label rect,#mermaid-svg-O0C1WKQLBbFcSz56 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-O0C1WKQLBbFcSz56 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-O0C1WKQLBbFcSz56 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-O0C1WKQLBbFcSz56 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Generator (N 个 worker)
ONNX 推理
发音人混合
Preprocessor (单线程)
文本清洗
音素化
Token 化
TextSplitter
句子分割
音素限制分片
TextChunkQueue
有界通道
TokenQueue
有界通道
SortedAudioQueue
BTreeMap + Condvar
2.1 TextSplitter:聪明的分片策略
输入文本先经过 split_into_sentences 进行语义分句,处理括号匹配、URL 过滤、缩写排除等边界情况。随后 split_by_phoneme_limit 将句子切成音素数不超过 510 的块------这个限制直接对应 ONNX 模型的最大序列长度。
分片时采用 waterfall 策略:优先在句号、问号、感叹号处切分;其次在冒号、分号;再者在逗号、顿号;最后在空白处。如果单句仍超长,则递归二分切分。这种策略在「打断自然度」和「满足模型限制」之间取得了良好平衡。
rust
/// TextSplitter:将输入文本按语义分句,再按音素上限分片
///
/// 在实际流水线中,TextSplitter 作为第一阶段运行在独立线程中,
/// 将分好的文本块通过有界通道发送给 Preprocessor。
pub struct TextSplitter {
max_phonemes: usize, // 每块最大音素数,对应 ONNX 模型序列长度上限 510
}
impl TextSplitter {
/// 对输入文本执行完整分片流程
pub fn split(&self, text: &str) -> Vec<String> {
let sentences = self.split_into_sentences(text);
let mut chunks = Vec::new();
for sentence in &sentences {
chunks.extend(self.split_by_phoneme_limit(sentence, self.max_phonemes));
}
chunks
}
fn split_into_sentences(&self, text: &str) -> Vec<String> {
// 处理括号匹配、URL 过滤、缩写排除等边界情况
text.split_inclusive(|c: char| c == '。' || c == '!' || c == '?')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
fn split_by_phoneme_limit(&self, sentence: &str, limit: usize) -> Vec<String> {
// waterfall 策略:优先在句末标点切分,其次逗号,最后空白
if sentence.chars().count() <= limit {
return vec![sentence.to_string()];
}
// 递归二分切分(简化示意)
let mid = sentence.len() / 2;
vec![
sentence[..mid].to_string(),
sentence[mid..].to_string(),
]
}
}
2.2 Preprocessor:文本到 token 的转换
预处理器在独立线程中运行,消费 TextChunkQueue,产出 TokenQueue。它负责:
- 清洗控制字符,合并多余空白
- 调用 G2P 引擎将文本转为音素序列(中文输出 Bopomofo,英文输出 IPA)
- 将音素查表映射为 token ID 数组
- 校验 token 长度是否在合法区间
[3, 510],过短或过长的块会被跳过并记录统计信息
预处理器还会向每个 Generator worker 发送 End 哨兵,确保所有 worker 能正常退出。
rust
/// Preprocessor:将文本块转换为 token ID 序列
///
/// 在流水线中作为第二阶段运行,消费 TextChunkQueue,产出 TokenQueue。
/// 每个文本块经过清洗、音素化、查表映射后,得到可直接输入 ONNX 模型的 token 数组。
pub struct Preprocessor {
g2p_engine: G2pEngine, // 中英文 G2P 引擎
token_table: HashMap<String, i64>, // 音素 → token ID 映射表
}
impl Preprocessor {
/// 处理单个文本块,返回 token ID 数组
pub fn process(&self, chunk: &str) -> Option<Vec<i64>> {
// 1. 清洗控制字符,合并多余空白
let cleaned: String = chunk.chars()
.filter(|c| !c.is_control() || *c == '\n')
.collect();
// 2. 调用 G2P 引擎转为音素序列
let phonemes = self.g2p_engine.to_phonemes(&cleaned);
// 3. 查表映射为 token ID
let tokens: Vec<i64> = phonemes
.iter()
.filter_map(|p| self.token_table.get(p).copied())
.collect();
// 4. 校验 token 长度是否在合法区间 [3, 510]
if tokens.len() < 3 || tokens.len() > 510 {
return None; // 过短或过长的块会被跳过
}
Some(tokens)
}
}
2.3 Generator:并行推理 + 有序归并
Generator 阶段启动 N 个工作线程(可配置),每个 worker 独立持有 ONNX Session。它们从 TokenQueue 并行拉取任务,执行推理,然后将音频块推入 SortedAudioQueue。
由于多个 worker 推理速度不一,音频块可能乱序到达。SortedAudioQueue 内部使用 BTreeMap<usize, Vec<f32>> 按索引缓存,配合 Condvar 唤醒消费者,保证最终按原始顺序输出。
rust
/// Generator Worker:并行推理 + 有序归并
///
/// 每个 worker 在独立线程中运行,持有专属 ONNX Session。
/// 从 TokenQueue 拉取任务,推理后将音频块推入 SortedAudioQueue,
/// 由消费者按原始顺序归并输出。
pub struct GeneratorWorker {
session: OrtSession, // 专属 ONNX Session,无锁竞争
worker_id: usize, // worker 编号,用于路由到对应 Session 实例
output_queue: Arc<SortedAudioQueue>, // 共享的有序音频队列
}
impl GeneratorWorker {
/// 执行一次推理任务
pub fn run(&self, tokens: Vec<i64>, style: &[f32], speed: f32) -> Vec<f32> {
// 构造 ONNX 输入张量
let input_tensor = ort::inputs! {
"tokens" => tokens,
"style" => style,
"speed" => vec![speed],
}
.unwrap();
// 执行推理
let outputs = self.session.run(input_tensor).unwrap();
let audio: Vec<f32> = outputs["audio"].try_extract().unwrap().to_vec();
audio
}
}
rust
// 简化的 SortedAudioQueue 核心逻辑
pub fn push(&self, index: usize, samples: Vec<f32>) {
let mut data = self.data.lock().unwrap();
data.insert(index, samples);
self.condvar.notify_one();
}
pub fn pop_next(&self, timeout_ms: u64) -> Option<Vec<f32>> {
let mut data = self.data.lock().unwrap();
let next = *self.next_index.lock().unwrap();
loop {
if let Some(samples) = data.remove(&next) {
*self.next_index.lock().unwrap() = next + 1;
return Some(samples);
}
if *self.ended.lock().unwrap() && data.is_empty() {
return None;
}
let (guard, _) = self.condvar.wait_timeout(data, timeout).unwrap();
data = guard;
}
}
这种设计带来的收益 :当文本被分成 20 个块时,4 个 worker 可以同时处理其中 4 个,后一个块无需等待前一个块完成,整体延迟大幅降低。实测实时率(RTF)可达 5~10 倍,即生成 1 秒音频仅需 0.1~0.2 秒。
三、TTS 引擎:多实例消竞争,线性扩展
kokoros-core 提供了两种 TTS 引擎实现:
TTSKoko:单 ONNX Session,内部用Mutex保护。适合单线程或低并发场景。TTSKokoParallel:持有多个独立的 ONNX Session(数量可配置),每个 worker 通过worker_id % num_instances获取专属实例。消除锁竞争,多 worker 可近乎线性扩展吞吐。
#mermaid-svg-Kil0WIw5zsEVFPVl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Kil0WIw5zsEVFPVl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Kil0WIw5zsEVFPVl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Kil0WIw5zsEVFPVl .error-icon{fill:#552222;}#mermaid-svg-Kil0WIw5zsEVFPVl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Kil0WIw5zsEVFPVl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Kil0WIw5zsEVFPVl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Kil0WIw5zsEVFPVl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Kil0WIw5zsEVFPVl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Kil0WIw5zsEVFPVl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Kil0WIw5zsEVFPVl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Kil0WIw5zsEVFPVl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Kil0WIw5zsEVFPVl .marker.cross{stroke:#333333;}#mermaid-svg-Kil0WIw5zsEVFPVl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Kil0WIw5zsEVFPVl p{margin:0;}#mermaid-svg-Kil0WIw5zsEVFPVl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Kil0WIw5zsEVFPVl .cluster-label text{fill:#333;}#mermaid-svg-Kil0WIw5zsEVFPVl .cluster-label span{color:#333;}#mermaid-svg-Kil0WIw5zsEVFPVl .cluster-label span p{background-color:transparent;}#mermaid-svg-Kil0WIw5zsEVFPVl .label text,#mermaid-svg-Kil0WIw5zsEVFPVl span{fill:#333;color:#333;}#mermaid-svg-Kil0WIw5zsEVFPVl .node rect,#mermaid-svg-Kil0WIw5zsEVFPVl .node circle,#mermaid-svg-Kil0WIw5zsEVFPVl .node ellipse,#mermaid-svg-Kil0WIw5zsEVFPVl .node polygon,#mermaid-svg-Kil0WIw5zsEVFPVl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Kil0WIw5zsEVFPVl .rough-node .label text,#mermaid-svg-Kil0WIw5zsEVFPVl .node .label text,#mermaid-svg-Kil0WIw5zsEVFPVl .image-shape .label,#mermaid-svg-Kil0WIw5zsEVFPVl .icon-shape .label{text-anchor:middle;}#mermaid-svg-Kil0WIw5zsEVFPVl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Kil0WIw5zsEVFPVl .rough-node .label,#mermaid-svg-Kil0WIw5zsEVFPVl .node .label,#mermaid-svg-Kil0WIw5zsEVFPVl .image-shape .label,#mermaid-svg-Kil0WIw5zsEVFPVl .icon-shape .label{text-align:center;}#mermaid-svg-Kil0WIw5zsEVFPVl .node.clickable{cursor:pointer;}#mermaid-svg-Kil0WIw5zsEVFPVl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Kil0WIw5zsEVFPVl .arrowheadPath{fill:#333333;}#mermaid-svg-Kil0WIw5zsEVFPVl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Kil0WIw5zsEVFPVl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Kil0WIw5zsEVFPVl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Kil0WIw5zsEVFPVl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Kil0WIw5zsEVFPVl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Kil0WIw5zsEVFPVl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Kil0WIw5zsEVFPVl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Kil0WIw5zsEVFPVl .cluster text{fill:#333;}#mermaid-svg-Kil0WIw5zsEVFPVl .cluster span{color:#333;}#mermaid-svg-Kil0WIw5zsEVFPVl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Kil0WIw5zsEVFPVl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Kil0WIw5zsEVFPVl rect.text{fill:none;stroke-width:0;}#mermaid-svg-Kil0WIw5zsEVFPVl .icon-shape,#mermaid-svg-Kil0WIw5zsEVFPVl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Kil0WIw5zsEVFPVl .icon-shape p,#mermaid-svg-Kil0WIw5zsEVFPVl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Kil0WIw5zsEVFPVl .icon-shape .label rect,#mermaid-svg-Kil0WIw5zsEVFPVl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Kil0WIw5zsEVFPVl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Kil0WIw5zsEVFPVl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Kil0WIw5zsEVFPVl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Worker 1
Session 0
Worker 2
Session 1
Worker 3
Session 2
Worker 4
共享发音人表
这种设计充分利用了 ONNX Runtime 的多线程能力,同时避免单 Mutex 成为瓶颈。在 8 核机器上,4 个 worker 几乎可以将 CPU 打满。
发音人混合
mix_styles 支持两种发音人指定方式:
- 单一发音人:
"zm_yunyang" - 混合发音人:
"zf_xiaobei.3+zm_yunyang.7"(按 0.3 / 0.7 比例加权平均)
发音人嵌入存放在 voices-v1.0.bin(约 150MB)中,每个发音人是一个 (511, 1, 256) 的张量,对应不同 token 长度的风格向量。混合时按 token 长度取对应的嵌入再加权,实现音色融合。
ONNX 推理层
OrtKoko 自动检测模型类型:
- Standard 模型 :输入
tokens, style, speed→ 输出audio - Timestamped 模型 :输入
input_ids, style, speed→ 输出waveform, durations(提供每个 token 的时间戳,用于对齐)
模型从内存加载(commit_from_memory),为未来 WASM 和嵌入式场景铺路。
四、G2P 引擎:中文与英文的不同处理
文本转音素(Grapheme-to-Phoneme)是 TTS 的前置关键步骤。kokoroi-rs 对中英文采用了不同策略。
中文 G2P:基于 jieba + 规则
中文 G2P 流程如下:
#mermaid-svg-kSS5M79HE6XWI7JJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kSS5M79HE6XWI7JJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kSS5M79HE6XWI7JJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kSS5M79HE6XWI7JJ .error-icon{fill:#552222;}#mermaid-svg-kSS5M79HE6XWI7JJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kSS5M79HE6XWI7JJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kSS5M79HE6XWI7JJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kSS5M79HE6XWI7JJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kSS5M79HE6XWI7JJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kSS5M79HE6XWI7JJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kSS5M79HE6XWI7JJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kSS5M79HE6XWI7JJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kSS5M79HE6XWI7JJ .marker.cross{stroke:#333333;}#mermaid-svg-kSS5M79HE6XWI7JJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kSS5M79HE6XWI7JJ p{margin:0;}#mermaid-svg-kSS5M79HE6XWI7JJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kSS5M79HE6XWI7JJ .cluster-label text{fill:#333;}#mermaid-svg-kSS5M79HE6XWI7JJ .cluster-label span{color:#333;}#mermaid-svg-kSS5M79HE6XWI7JJ .cluster-label span p{background-color:transparent;}#mermaid-svg-kSS5M79HE6XWI7JJ .label text,#mermaid-svg-kSS5M79HE6XWI7JJ span{fill:#333;color:#333;}#mermaid-svg-kSS5M79HE6XWI7JJ .node rect,#mermaid-svg-kSS5M79HE6XWI7JJ .node circle,#mermaid-svg-kSS5M79HE6XWI7JJ .node ellipse,#mermaid-svg-kSS5M79HE6XWI7JJ .node polygon,#mermaid-svg-kSS5M79HE6XWI7JJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kSS5M79HE6XWI7JJ .rough-node .label text,#mermaid-svg-kSS5M79HE6XWI7JJ .node .label text,#mermaid-svg-kSS5M79HE6XWI7JJ .image-shape .label,#mermaid-svg-kSS5M79HE6XWI7JJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-kSS5M79HE6XWI7JJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kSS5M79HE6XWI7JJ .rough-node .label,#mermaid-svg-kSS5M79HE6XWI7JJ .node .label,#mermaid-svg-kSS5M79HE6XWI7JJ .image-shape .label,#mermaid-svg-kSS5M79HE6XWI7JJ .icon-shape .label{text-align:center;}#mermaid-svg-kSS5M79HE6XWI7JJ .node.clickable{cursor:pointer;}#mermaid-svg-kSS5M79HE6XWI7JJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kSS5M79HE6XWI7JJ .arrowheadPath{fill:#333333;}#mermaid-svg-kSS5M79HE6XWI7JJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kSS5M79HE6XWI7JJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kSS5M79HE6XWI7JJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kSS5M79HE6XWI7JJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kSS5M79HE6XWI7JJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kSS5M79HE6XWI7JJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kSS5M79HE6XWI7JJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kSS5M79HE6XWI7JJ .cluster text{fill:#333;}#mermaid-svg-kSS5M79HE6XWI7JJ .cluster span{color:#333;}#mermaid-svg-kSS5M79HE6XWI7JJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kSS5M79HE6XWI7JJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kSS5M79HE6XWI7JJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-kSS5M79HE6XWI7JJ .icon-shape,#mermaid-svg-kSS5M79HE6XWI7JJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kSS5M79HE6XWI7JJ .icon-shape p,#mermaid-svg-kSS5M79HE6XWI7JJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kSS5M79HE6XWI7JJ .icon-shape .label rect,#mermaid-svg-kSS5M79HE6XWI7JJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kSS5M79HE6XWI7JJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kSS5M79HE6XWI7JJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kSS5M79HE6XWI7JJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输入文本
数字转换
123 → 一百二十三
标点映射
全角→半角
jieba-rs 分词 + POS 标注
变调预处理
合并需变调词组
多音字消歧
基于上下文规则
三声变调 / 一不变调
拼音→注音 或 IPA
多音字消歧器 PolyphonicDisambiguator 维护了一张上下文规则表,例如"了"在"了解"中读 liǎo,在"好了"中读 le。变调处理则实现了经典的三声变调、"一"、"不"变调规则。
最终输出 Bopomofo(注音符号)或 IPA,其中 Bopomofo 是 Kokoro 模型原生训练使用的音素表示,准确度更高。
英文 G2P:misaki 独立引擎
英文部分复用了 misaki crate(v0.3.0),这是一个 POS 感知的 G2P 引擎,基于 CMU Pronouncing Dictionary 数据,包含:
- 感知机 POS tagger
- 词形变化规则(
-s,-ed,-ing后缀处理) - 字符级 fallback 发音器(当词典查不到时)
misaki 完全独立于主项目,可单独作为 Rust 库使用。这种模块化设计让后续扩展其他语言的 G2P 变得容易。
五、HTTP 服务:双 API 风格,灵活集成
kokoros-server:原生 REST + SSE
基于 Axum 实现,提供三个核心接口:
POST /tts:同步转语音,返回 Base64 编码的 WAVPOST /tts/stream:SSE 流式输出,每生成一块音频就推送一个事件GET /voices:列出所有可用发音人
SSE 流式接口适合 Web 前端,用户可以在音频完全生成前就开始播放,体验更流畅。服务同时附带 Web 演示页面,开箱即用。
kokoros-openai:无缝替换 OpenAI TTS
这个 crate 实现了 OpenAI 的 /v1/audio/speech 接口,支持 input、voice、speed、response_format(wav/mp3/pcm/opus)和 stream 参数。发音人做了映射(如 alloy → af_alloy)。
这意味着任何兼容 OpenAI API 的工具(如语音助手、AI 应用)只需换一个 base URL 就能无缝切换到 kokoroi-rs,而无需修改业务代码。
六、构建与部署:全静态,零依赖
kokoroi-rs 在 Linux 上采用 musl 全静态编译 ,生成的 ELF 二进制不依赖任何外部动态库(包括 glibc)。Windows 下则动态链接 onnxruntime.dll,压缩包内已包含该文件。
CI/CD 使用 GitHub Actions 自动构建三平台产物:
| 平台 | 产物 | 构建方式 |
|---|---|---|
| x86_64 Linux | 全静态 ELF | musl-cross 工具链 + ORT 源码编译 |
| ARM64 Linux | 全静态 ELF | aarch64-musl-cross 交叉编译 |
| x86_64 Windows | 含 DLL 的 zip | MSVC + 预编译 ORT |
特别是 ONNX Runtime 的 musl 静态库 构建,项目提供了 build_ort_musl.sh 脚本,它会下载 ORT 源码,用 cmake 编译并生成一个组合的 libonnxruntime.a,解决 abseil 循环依赖问题。整个构建过程在 CI 中首次约需 20-30 分钟,之后利用缓存可缩短到 2-5 分钟。
得益于这种构建策略,部署变得极其简单:一个二进制文件 + 一个模型文件 + 一个发音人数据文件,即可在任何 Linux 服务器上直接运行,无需安装任何运行时。
七、性能数字与优化要点
| 维度 | 数据 |
|---|---|
| 模型加载 | ~2-5 秒(80MB ONNX) |
| 发音人数据加载 | ~1-2 秒(150MB npz) |
| 推理实时率 | 5-10x RTF(24kHz 音频) |
| 内存占用 | 单 Session 约 300-500MB |
| 多实例扩展 | 近线性(无锁争用) |
关键优化手段:
- 多实例并行:消除 Mutex 竞争,让多 worker 充分利用多核。
- 有界通道:防止流水线积压导致内存爆炸。
- SortedAudioQueue:BTreeMap O(log n) 重排序,比通道队列更简洁。
- 分片策略:平衡块大小,避免单块过长影响实时性,也避免过短增加开销。
八、未来展望与参与方式
目前项目已具备生产级能力,但仍有一些方向值得探索:
- 更多语言 G2P :当前
Phonemizer对非中英文仅做原样透传,可集成日语、韩语等。 - GPU 推理:CUDA feature 已预留,启用后可大幅提升吞吐。
- 模型热加载:支持运行时切换不同风格的模型。
- 深度健康检查:验证模型输出的有效性,增强运维可观测性。
如果你对 Rust + AI 推理 或 TTS 系统 感兴趣,欢迎到 GitHub 仓库体验、提 Issue 或贡献代码。项目采用 MIT 许可证,自由使用。
写在最后
从 Python 原型到 Rust 生产级系统,kokoroi-rs 展示了如何将 AI 模型"工程化"落地。它不是一个简单的"翻译",而是从底层流水线、并行策略、跨平台构建等多个维度重新设计,最终交付出一个高性能、易部署、可扩展的 TTS 引擎。
对于正在探索 Rust 在 AI 领域应用的开发者,这个项目提供了很好的参考样本------如何用 Rust 的安全性和并发能力,为深度学习模型打造健壮的工程底座。希望本文的拆解能给你带来一些启发。