【RUST AI】把 TTS 搬进浏览器:kokoroi-rs 的 WASM 实践

把 TTS 搬进浏览器:kokoroi-rs 的 WASM 实践

摘要:本文深入解析 kokoroi-rs 项目的 WASM 模块,展示如何将 Kokoro TTS 模型编译为 WebAssembly,在浏览器中实现纯前端、零服务器的高质量中文语音合成。文章涵盖整体架构(G2P 引擎 + ONNX 推理)、两种推理后端(ONNX Runtime Web 与纯 Rust oxionnx)的深度对比、JavaScript API 设计、构建与 Demo 演示,以及隐私安全、性能表现与未来方向。适合关注 Rust + WASM + AI 的前端开发者、隐私敏感应用及边缘计算场景。

关键词:TTS, WebAssembly, WASM, Rust, 语音合成, 浏览器, kokoroi-rs, ONNX, 隐私安全, 前端AI
纯前端运行,隐私数据不出设备,一个 3MB 的 WASM 模块实现高质量中文语音合成

引子:云 TTS 的隐忧与 WASM 的机遇

语音合成(TTS)服务已经非常普及,但大多数解决方案依赖云端 API:用户把文本传到服务器,服务器生成音频后返回。这个过程带来了几个绕不开的问题:

  • 隐私风险:文本内容会经过第三方服务器,对于涉及个人隐私或商业机密的场景,这是一个硬伤。
  • 网络依赖:需要稳定的互联网连接,弱网环境下体验打折扣。
  • 成本:商用 API 按字符计费,大规模使用不是小数目。

那么,有没有可能让 TTS 完全在浏览器本地运行?WebAssembly 的出现让这种想法成为可能。kokoroi-rs 的 WASM 模块正是这样一个尝试------将 Kokoro TTS 的核心能力编译为 WebAssembly,让高质量语音合成直接运行在用户的浏览器中,无需任何后端服务器。

本文将深入拆解 kokoroi-rs 的 WASM 架构、两种推理后端、JavaScript API 设计以及实际使用中的性能表现。


一、整体架构:WASM 模块 + 推理后端

kokoroi-rs 的 WASM 模块将 Rust 代码编译为 wasm32-unknown-unknown 目标,通过 wasm-bindgen 生成 JavaScript 胶水代码,最终交付一个约 3MB 的 WASM 二进制文件。

整个系统的核心功能分两层:

  1. G2P 引擎:将中文文本转换为 Bopomofo(注音符号)音素序列------这是模型输入的前置步骤,完全在 Rust 中实现。
  2. ONNX 推理引擎:将音素序列 + 发音人风格嵌入 → PCM 音频采样。

推理部分提供了两种后端选择,开发者可以根据需求灵活切换:
graph TB subgraph Browser"浏览器环境" JS"JavaScript 调用层" WASM"Rust WASM 模块\
(kokoros_bg.wasm)"
subgraph WASM"WASM 内部功能" G2P"ChineseG2P\
字素转音素"
WAV"WAV 编码器" OX"oxionnx 推理\
(纯 Rust)"
end ORT"ONNX Runtime Web\
(CDN 加载)"
end subgraph Data"数据文件" MODEL"ONNX 模型文件\
(\~80MB)"
STYLE"发音人嵌入\
(\*.bin)"
end JS -->|init| WASM JS -->|phonemize| G2P JS -->|synthesize| OX JS -->|InferenceSession| ORT G2P -->|Bopomofo| OX G2P -->|Bopomofo| ORT OX -->|PCM| WAV ORT -->|PCM| WAV WAV -->|WAV Uint8| JS OX -.->|加载| MODEL ORT -.->|加载| MODEL JS -.->|fetch| STYLE

这两个后端各有侧重:ONNX Runtime Web 是微软官方方案,稳定性好、优化成熟;而 oxionnx 是纯 Rust 实现,与 WASM 集成更紧密,无需额外加载推理库。


二、G2P 引擎:中文处理的基石

文本到音素的转换是 TTS 的第一步,也是中文场景中最复杂的环节之一。WASM 模块中的 ChineseG2P 引擎与 Native 版本共享同一套代码,保证了处理逻辑的一致性和准确性。

核心流程如下:
flowchart TD TEXT输入中文文本 --> NORM文本规范化 NORM --> SEGjieba-rs 分词 + POS 标注 SEG --> DISAMB多音字消歧\
基于上下文规则
DISAMB --> SANDHI变调处理\
三声变调 / 一不变调
SANDHI --> MAP拼音 → Bopomofo 映射 MAP --> OUT输出音素序列

几个关键点:

  • jieba-rs 分词:WASM 版本同样使用 jieba-rs,词典数据以压缩形式嵌入 WASM 二进制中,首次调用时解压(约数百毫秒一次性开销),之后常驻内存。
  • 多音字消歧PolyphonicDisambiguator 维护了一套基于规则的上文消歧表,例如"了"在"了解"中读 liǎo,在"好了"中读 `le"。
  • 变调处理:实现了经典的三声变调规则(两个三声相连,前变二声)以及"一"、"不"的变调规律。

最终输出 Bopomofo(注音符号)序列,这是 Kokoro 模型原生训练使用的音素表示,准确度优于 IPA。


三、JavaScript API:简洁易用

WASM 模块通过 wasm-bindgen 导出清晰的 JavaScript 接口,开发者可以像使用普通 JS 库一样调用。

初始化

javascript 复制代码
import init, { KokoroWASM, pcm_samples_to_wav_data } from './wasm-pkg/kokoros.js';

// 加载 WASM 运行时
await init();

// 创建 TTS 实例
const kokoro = new KokoroWASM({ usePolyphonic: true });

实战示例:从文本输入到音频播放/下载

下面是一个完整的 HTML 页面示例,展示如何将文本合成为语音并播放或下载,包含完善的错误处理和进度提示:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>kokoroi-rs TTS 演示</title>
  <style>
    body { font-family: sans-serif; max-width: 600px; margin: 2rem auto; padding: 0 1rem; }
    textarea { width: 100%; height: 100px; margin-bottom: 1rem; }
    button { padding: 0.5rem 1rem; margin-right: 0.5rem; cursor: pointer; }
    .status { margin-top: 1rem; padding: 0.5rem; border-radius: 4px; }
    .status.loading { background: #fff3cd; color: #856404; }
    .status.success { background: #d4edda; color: #155724; }
    .status.error { background: #f8d7da; color: #721c24; }
    progress { width: 100%; margin-top: 0.5rem; }
  </style>
</head>
<body>
  <h1>kokoroi-rs 浏览器 TTS</h1>
  <textarea id="textInput" placeholder="请输入要合成语音的中文文本...">你好,欢迎体验浏览器端语音合成。</textarea>
  <button id="playBtn">🔊 播放</button>
  <button id="downloadBtn">⬇️ 下载 WAV</button>
  <progress id="progressBar" value="0" max="100" style="display:none;"></progress>
  <div id="status" class="status">就绪</div>

  <script type="module">
    import init, { KokoroWASM, pcm_samples_to_wav_data } from './wasm-pkg/kokoros.js';

    const textInput = document.getElementById('textInput');
    const playBtn = document.getElementById('playBtn');
    const downloadBtn = document.getElementById('downloadBtn');
    const progressBar = document.getElementById('progressBar');
    const statusDiv = document.getElementById('status');

    let kokoro = null;
    let lastAudioBuffer = null; // 缓存最近一次合成的音频

    function setStatus(msg, type = '') {
      statusDiv.textContent = msg;
      statusDiv.className = 'status' + (type ? ' ' + type : '');
    }

    function setProgress(pct) {
      progressBar.style.display = pct >= 0 ? 'block' : 'none';
      progressBar.value = Math.max(0, Math.min(100, pct));
    }

    // 初始化 WASM 模块
    async function initTTS() {
      try {
        setStatus('正在加载 WASM 模块...', 'loading');
        setProgress(20);
        await init();
        setProgress(50);

        setStatus('正在初始化 TTS 引擎...', 'loading');
        kokoro = new KokoroWASM({ usePolyphonic: true });
        setProgress(80);

        // 加载 ONNX 模型(以 oxionnx 模式为例)
        setStatus('正在加载语音模型(约 80MB)...', 'loading');
        const modelResp = await fetch('/models/kokoro.onnx');
        if (!modelResp.ok) throw new Error(`模型下载失败:HTTP ${modelResp.status}`);
        const modelBytes = new Uint8Array(await modelResp.arrayBuffer());
        await kokoro.loadModel(modelBytes);
        setProgress(100);

        setStatus('✅ 就绪,可以开始合成语音', 'success');
        playBtn.disabled = false;
        downloadBtn.disabled = false;
      } catch (err) {
        setStatus(`❌ 初始化失败:${err.message}`, 'error');
        playBtn.disabled = true;
        downloadBtn.disabled = true;
        console.error('TTS init error:', err);
      } finally {
        setProgress(-1); // 隐藏进度条
      }
    }

    // 合成语音
    async function synthesize(text) {
      if (!kokoro) throw new Error('TTS 引擎未初始化');
      if (!text.trim()) throw new Error('请输入文本');

      setStatus('正在合成语音...', 'loading');
      setProgress(30);

      // 获取默认发音人
      const voices = await kokoro.getVoices();
      if (!voices.length) throw new Error('未找到可用发音人');
      const defaultVoice = voices[0];
      setProgress(50);

      // 执行合成
      const result = await kokoro.synthesize(text, defaultVoice.id, 1.0);
      setProgress(80);

      // 转换为 WAV
      const wavBytes = pcm_samples_to_wav_data(result.audio);
      setProgress(100);

      lastAudioBuffer = wavBytes;
      setStatus(`✅ 合成完成(音素:${result.phonemesDisplay})`, 'success');
      setProgress(-1);

      return { wavBytes, result };
    }

    // 播放音频
    async function handlePlay() {
      try {
        const { wavBytes } = await synthesize(textInput.value);
        const blob = new Blob([wavBytes], { type: 'audio/wav' });
        const url = URL.createObjectURL(blob);
        const audio = new Audio(url);
        audio.onended = () => URL.revokeObjectURL(url);
        await audio.play();
      } catch (err) {
        setStatus(`❌ 播放失败:${err.message}`, 'error');
        setProgress(-1);
      }
    }

    // 下载音频
    async function handleDownload() {
      try {
        const { wavBytes } = await synthesize(textInput.value);
        const blob = new Blob([wavBytes], { type: 'audio/wav' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `tts-${Date.now()}.wav`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
        setStatus('✅ 下载已开始', 'success');
      } catch (err) {
        setStatus(`❌ 下载失败:${err.message}`, 'error');
        setProgress(-1);
      }
    }

    // 绑定事件
    playBtn.addEventListener('click', handlePlay);
    downloadBtn.addEventListener('click', handleDownload);

    // 启动初始化
    playBtn.disabled = true;
    downloadBtn.disabled = true;
    initTTS();
  </script>
</body>
</html>

代码说明:

  • 初始化阶段 :通过 init() 加载 WASM 运行时,创建 KokoroWASM 实例,然后 fetch 下载 ONNX 模型并调用 loadModel() 加载。每一步都有进度提示和错误捕获。
  • 合成流程synthesize() 函数先获取默认发音人,再调用 synthesize(text, voiceId, speed) 执行推理,最后用 pcm_samples_to_wav_data() 将 PCM 数据编码为 WAV 格式。
  • 播放与下载handlePlay() 将 WAV 转为 Blob URL 后通过 Audio 播放;handleDownload() 创建 <a> 标签触发下载。
  • 错误处理 :网络错误(fetch 失败)、模型加载失败、无可用发音人等场景均有 try/catch 捕获并显示友好提示。
  • 进度提示 :使用 <progress> 元素和状态栏实时反馈加载与合成进度。

核心方法一览

方法 说明 返回值
phonemize(text) 文本 → Bopomofo 音素 Promise<string>
phonemizeIPA(text) 文本 → IPA 音素 Promise<string>
tokenize(phonemes) 音素 → token ID 数组 Promise<Uint32Array>
loadModel(bytes) 加载 ONNX 模型(oxionnx 模式) Promise<void>
synthesize(text, style, speed) 文本 → 语音(oxionnx 模式) Promise<SynthesisResult>
getVoices() 获取内置发音人列表 Promise<VoiceInfo[]>

类型安全

项目提供了完整的 TypeScript 类型声明(kokoros.d.ts),在 IDE 中可以获得智能提示:

typescript 复制代码
interface SynthesisResult {
  phonemes: string;          // Bopomofo 音素序列
  phonemesDisplay: string;   // 可读注音(带声调符号)
  text: string;              // 原始文本
  audio: Float32Array;       // PCM 音频数据(24000Hz 单声道)
  sampleRate: number;        // 固定 24000
}

辅助函数 pcm_samples_to_wav_data(samples) 将 Float32Array 转换为标准 WAV 格式的 Uint8Array,方便播放或下载。


四、两种推理模式深度对比

WASM 模块最有趣的设计是提供了两套推理路径,开发者可根据场景选择。

模式一:ONNX Runtime Web(混合架构)

sequenceDiagram participant JS participant WASM as Rust WASM (G2P) participant ORT as ONNX Runtime Web JS->>WASM: phonemize(text) WASM-->>JS: Bopomofo 音素 JS->>JS: tokenize + 构造 Tensor JS->>ORT: InferenceSession.run(feeds) ORT-->>JS: PCM Float32Array JS->>JS: 编码为 WAV

  • G2P 部分由 Rust WASM 完成(高效、小巧)
  • 模型推理 使用微软官方的 onnxruntime-web 库,从 CDN 加载 ort.min.js 及其 WASM 后端
  • 发音人嵌入由 JavaScript 通过 fetch 加载后传入

适合场景:对推理稳定性和算子覆盖度要求较高的生产应用。

模式二:纯 Rust WASM + oxionnx(全栈 Rust)

sequenceDiagram participant JS participant WASM as Rust WASM (G2P + oxionnx) JS->>WASM: loadModel(modelBytes) JS->>WASM: synthesize(text, style, speed) WASM->>WASM: G2P → Tokenize → oxionnx 推理 WASM-->>JS: PCM Float32Array + 音素信息 JS->>JS: 编码为 WAV

  • 全部逻辑(G2P、推理、WAV 编码)都在 Rust WASM 内部完成
  • 推理使用 oxionnx------一个纯 Rust 实现的 ONNX 推理库
  • 无需加载任何外部 JavaScript 推理库,零 CDN 依赖

适合场景:离线应用、对隐私要求极高、希望最小化外部依赖的场景。

对比维度 ONNX Runtime Web oxionnx(纯 Rust)
推理库来源 微软官方 CDN 编译进 WASM
WASM 体积 ~3MB(仅 G2P) ~3MB(含推理)
额外 JS 加载 ort.min.js (~200KB)
算子覆盖度 广泛 核心算子(持续完善中)
优化选项 丰富(图优化、量化等) 基础
硬件加速 WebGL / WASM SIMD WASM SIMD
离线使用 需缓存 CDN 资源 完全离线可用

五、构建与 Demo:从源码到浏览器

构建 WASM 模块

bash 复制代码
# 安装 WASM 目标
rustup target add wasm32-unknown-unknown

# 安装 wasm-pack
cargo install wasm-pack

# 一键构建(推荐)
./scripts/build_wasm.sh

# 或手动构建
wasm-pack build crates/kokoros-core \
    --target web \
    --out-dir ../../static/wasm-pkg \
    -- \
    --features wasm \
    --no-default-features

构建产物位于 static/wasm-pkg/ 目录下:

复制代码
static/wasm-pkg/
├── kokoros_bg.wasm       # WASM 二进制 (~3MB)
├── kokoros.js            # 胶水代码 (~28KB)
├── kokoros.d.ts          # TypeScript 类型声明
└── package.json

Demo 页面

项目提供了三个可直接运行的演示页面,位于 static/ 目录:

  1. wasm_demo.html --- 使用 ONNX Runtime Web 后端,展示完整的 G2P + 推理流程。
  2. browser_demo.html --- 流式合成体验,更接近实时对话场景。
  3. rust_wasm_demo.html --- 纯 Rust WASM(oxionnx)后端,展示零外部依赖的全离线方案。

运行方式:

bash 复制代码
cd static
python3 -m http.server 8080
# 或
npx serve .

打开浏览器访问 http://localhost:8080/wasm_demo.html 即可体验。


六、隐私与安全:数据不出设备

这是 WASM 版本最吸引人的特性之一。对比云 TTS 方案:

复制代码
传统云 TTS:
用户文本 ──(网络)──→ 云服务器 ──(网络)──→ 返回音频
  文本离开用户设备,存在隐私泄露风险

kokoroi-rs WASM:
用户文本 ──(本地)──→ WASM 模块 ──(本地)──→ 播放音频
  文本始终在浏览器沙箱内,永不离开用户设备

对于一些敏感场景(如医疗咨询、金融信息、个人日记等),本地 TTS 有着天然的优势。同时,由于无需网络请求,弱网环境下也能稳定工作


七、性能与局限

性能表现

维度 数据
G2P 处理速度 < 1ms / 字符
模型加载(80MB) 首次约 1-3 秒(取决于网络)
推理实时率 约 1-2 倍实时(受 WASM 引擎限制)
内存占用 约 100-200MB
WASM 体积 ~3MB(含 G2P + oxionnx)

当前局限

  1. 单线程推理 :WASM 不支持 std::thread,无法利用多核并行处理长文本分片。对于超长文本,目前的方案是整体推理,可能会造成 UI 卡顿(可通过 Web Worker 缓解)。
  2. 模型下载体积:ONNX 模型约 80MB,发音人嵌入约 150MB,首次加载需要一定时间。后续可以利用浏览器缓存或 OPFS 减少重复下载。
  3. 仅 WAV 输出:受限于 WASM 下的音频编码库支持,目前只支持 WAV 格式(PCM 16-bit)。MP3/Opus 等压缩格式暂不支持。
  4. 浏览器兼容性:需要支持 WebAssembly 和 SharedArrayBuffer(部分旧浏览器不支持)。

八、未来方向

WASM 模块的潜力远不止于此,团队已经在规划几个有意思的方向:

  • Web Worker 并行:利用多个 Web Worker 各加载一个 WASM 实例,实现分片并行推理,弥补单线程的不足。
  • OPFS 模型缓存:利用 Origin Private File System 将下载的模型持久化在本地,第二次访问时几乎秒开。
  • 流式合成:将 Native 版本的 SSE 流式机制移植到 WASM,实现"边合成边播放"的低延迟体验。
  • 混合模式:G2P 在本地 WASM 执行,推理通过 WebSocket 连接私有推理服务器------兼顾隐私和性能。

结语

kokoroi-rs 的 WASM 模块展示了如何将 AI 模型"搬进浏览器"------通过 Rust + WebAssembly,我们可以在不牺牲质量的前提下,让 TTS 服务脱离云端,真正属于用户自己。

对于前端开发者、隐私敏感应用以及边缘计算场景,这套方案提供了一条全新的技术路径。如果你也关注 Rust + WASM + AI 这个方向,不妨去 GitHub 仓库看一看,跑一跑 Demo,甚至参与进来一起完善。

项目地址:github.com/doiito/kokoroi-rs

本文基于 kokoroi-rs 项目 WASM 模块的文档和源码整理,欢迎交流讨论。

相关推荐
Rain的Java大神实战圈9 小时前
线上慢SQL的排查思路
经验分享·架构设计·场景设计题
亮亮在江湖9 小时前
YOLO训练 vs LlamaFactory训练 完整区分
ai
wabs6669 小时前
关于文献【RL/SFT】
ai·文献
ofoxcoding9 小时前
Seedance 2.0 与 Wan 2026 视频生成 API 成本效率深度对比分析
网络·人工智能·ai·音视频
程序员爱钓鱼9 小时前
Rust 元组 Tuple 详解:组合不同类型的数据
前端·后端·rust
码农杂谈00079 小时前
组织管理视角深度解读:时尚集团GEA智能体案例——AI重构创意行业的组织资产与人力博弈
ai·gea·context资产
wuyuanshun9 小时前
LangGraph原理逻辑-LangGraph接入MCP(三)
人工智能·ai
hhzz9 小时前
CNN图像分类入门:基于TensorFlow+Keras的CIFAR-10数据集全流程实战
图像处理·计算机视觉·ai·cnn·大模型
Summer-Bright10 小时前
深度 | Kimi K3 48 小时设计芯片:EDA 行业真的会被 AI 颠覆吗?
人工智能·ai·自然语言处理·agi