本文记录 Timeline Studio 在浏览器中实现多语言声音克隆与音色迁移的工程方案,主要涉及:
- OpenVoice V2 FP16 ONNX 模型组织
- 浏览器音频预处理
- ONNX Runtime Web 推理
- WebGPU 与 WASM 执行
- ModelScope 与 Hugging Face 双源分发
- Cache Storage 模型缓存
- IndexedDB 声音档案
- 多语言 TTS 与音色迁移工作流
- 音频尾部清理与输出增益
- 时间线音频替换与恢复
项目仓库:
https://github.com/MartinDelophy/ai-video-editor
在线体验:
https://video-editor.ai-creator.top
一、功能工作流
Timeline Studio 将声音克隆拆分为两个独立阶段:
阶段一:生成目标语言语音
文本 → 基础 TTS → 中文、英文或其他语言音频
阶段二:迁移目标音色
源音频 → OpenVoice V2 → 克隆音色结果
完整浏览器工作流为:
录制或上传参考声音
↓
统一解码并重采样
↓
提取 256 维音色 Embedding
↓
生成所选语言的基础语音
↓
提取源音频音色
↓
执行 OpenVoice V2 音色迁移
↓
清理尾部填充与低幅噪声
↓
应用输出增益和软限幅
↓
试听并保存到 IndexedDB
↓
用于配音生成或时间线音频片段
语言由基础 TTS 决定,OpenVoice V2 负责音色迁移。
这样,同一个声音档案可以应用于多个语言,不需要在音色档案中绑定固定语言。
二、模型文件
浏览器版本使用两个 FP16 ONNX 文件:
| 文件 | 大小 | 用途 |
|---|---|---|
reference-encoder.onnx |
1,637,269 Bytes | 提取参考声音音色 |
converter.onnx |
64,314,222 Bytes | 执行音色迁移 |
| 合计 | 65,951,491 Bytes | 完整浏览器运行模型 |
模型目录:
openvoice-v2-converter-fp16/
├── LICENSE
├── README.md
├── config.json
├── reference-encoder.onnx
└── converter.onnx
ModelScope 固定版本:
Hugging Face 固定版本:
生产代码不从 main 动态加载模型,而是固定到不可变 revision。
这样可以保证:
- 模型文件不会被远端更新静默替换
- 浏览器缓存身份保持稳定
- 两个模型平台可以对应到同一运行版本
- 出现问题时能够定位到具体模型快照
三、浏览器音频预处理
用户上传的参考音频可能来自不同设备和编码格式。
浏览器首先通过:
AudioContext.decodeAudioData()
将音频解码,再使用:
OfflineAudioContext
统一重采样。
内部音频格式固定为:
| 参数 | 数值 |
|---|---|
| 采样率 | 22,050 Hz |
| 声道 | 单声道 |
| 内部数据 | Float32 PCM |
| 输出格式 | 16-bit PCM WAV |
数据流如下:
WAV / MP3 / M4A / WebM
↓
AudioContext 解码
↓
OfflineAudioContext 重采样
↓
22.05 kHz Mono Float32Array
参考声音、基础 TTS 音频和音色迁移输出都使用相同的内部采样率。
四、STFT 与频谱输入
OpenVoice Converter 需要频谱输入。
浏览器 Worker 中实现了 Hann Window、FFT 和 STFT。
参数为:
| 参数 | 数值 |
|---|---|
| FFT Size | 1024 |
| Hop Size | 256 |
| Frequency Bins | 513 |
Reference Encoder 的输入形状:
[1, FrameCount, 513]
Converter 的频谱输入形状:
[1, 513, FrameCount]
音频长度决定 FrameCount,因此 ONNX Session 需要处理动态时间维度。
频谱计算、转置和张量构造均在 Worker 中完成,不占用 React 主线程。
五、音色 Embedding
参考音频经过 Reference Encoder 后生成:
speaker_embedding: [1, 256, 1]
保存声音时,系统会保存:
{
id,
name,
sourceKind,
referenceBlob,
testBlob,
embedding,
favorite,
authorized,
createdAt,
updatedAt
}
其中:
referenceBlob:原始参考声音testBlob:克隆测试结果embedding:256 维音色特征sourceKind:录制或上传authorized:授权确认状态
再次使用这个声音时,可以直接读取 Embedding,不需要重新运行 Reference Encoder。
六、Converter 输入
Converter 使用以下张量:
spectrogram: [1, 513, T]
frame_mask: [1, 1, T]
source_embedding: [1, 256, 1]
target_embedding: [1, 256, 1]
noise: [1, 192, T]
其中:
source_embedding来自需要转换的源音频target_embedding来自用户保存的参考声音frame_mask标记有效频谱区域noise使用固定种子生成
固定种子保证相同输入具有可复现性,便于浏览器调试和结果验证。
七、Web Worker 运行时
OpenVoice Runtime 运行在独立 Worker 中:
Main Thread
├── React 界面
├── AudioContext 解码
├── IndexedDB
└── Timeline State
↓
postMessage
↓
OpenVoice Worker
├── 模型下载
├── ONNX Session
├── FFT / STFT
├── Embedding
├── Converter
└── 尾部处理
PCM 数据使用 Transferable ArrayBuffer 传输:
worker.postMessage(payload, [samples.buffer]);
这样可以避免浏览器在线程之间复制完整音频数组。
取消任务时直接终止 Worker:
worker.terminate();
同时拒绝所有尚未完成的请求,防止旧任务继续更新界面。
八、ONNX Runtime Web
浏览器使用:
import * as ort from "onnxruntime-web/webgpu";
WASM 配置:
ort.env.wasm.simd = true;
ort.env.wasm.numThreads = crossOriginIsolated
? Math.max(
1,
Math.min(4, navigator.hardwareConcurrency || 1)
)
: 1;
WebGPU 配置:
ort.env.webgpu.powerPreference = "high-performance";
Session 策略:
Reference Encoder
└── WASM
Converter
├── WebGPU
└── WASM Fallback
Converter 首先尝试:
ort.InferenceSession.create(modelBytes, {
executionProviders: ["webgpu", "wasm"],
graphOptimizationLevel: "all",
});
如果初始化失败,则重新创建 WASM Session。
模型文件采用并行下载:
Promise.all([
loadReferenceEncoder(),
loadConverter(),
]);
Session 顺序初始化,避免两个 ONNX 图同时编译造成瞬时内存峰值。
九、ModelScope 与 Hugging Face 双源分发
模型分发不是简单准备两个 URL。
系统为两个平台定义统一模型身份:
Repository
Revision
Relative Path
Cache Identity
路由逻辑为:
中文界面或国内网络环境
↓
ModelScope 优先
↓
失败后切换 Hugging Face
其他语言环境
↓
Hugging Face 优先
↓
失败后切换 ModelScope
第一次成功的模型来源会成为当前会话的优先线路。
如果网络环境发生变化或当前线路失败,运行时会清除会话线路记录,并尝试另一个来源。
两个平台使用相同缓存身份,因此不会出现:
ModelScope 缓存一份
+
Hugging Face 再缓存一份
十、Cache Storage
模型加载前会请求浏览器持久化存储:
navigator.storage.persist()
但是浏览器可能拒绝,因此缓存不能成为推理的前置条件。
实际策略为:
检查统一缓存
↓
命中:加载本地模型
↓
未命中:访问模型镜像
↓
下载成功:尝试写入缓存
↓
缓存写入失败:继续以内存运行
发生 QuotaExceededError 时:
- 不终止推理
- 不向用户显示原始异常
- 跳过当前持久化写入
- 继续使用已经下载到内存的模型
- 后续重新检查缓存和模型源
预发布阶段产生的重复缓存会在新 Worker 初始化时删除,避免多个缓存版本争用浏览器存储空间。
十一、FP16 部署记录
当前发布模型固定使用 FP16。
FP16 在当前工程中的作用包括:
- 降低模型下载大小
- 降低权重在 GPU Buffer 中的占用
- 保留 WebGPU 浮点执行路径
- 允许浏览器使用 WASM 回退
- 避免额外的反量化节点
FP8 没有进入当前生产链路。
虽然 ONNX 文件格式可以表达 Float8 类型,但浏览器 WebGPU 目前缺少稳定的通用 FP8 原生计算路径。
FP8 权重进入浏览器后,可能需要:
FP8
↓
Cast / Dequantize
↓
FP16 或 FP32
↓
WebGPU Kernel
这会增加:
- 类型转换节点
- 图切分概率
- WASM 回退概率
- CPU/GPU 数据交换
- 浏览器兼容验证范围
- 音频质量验证成本
因此,当前版本只保留已经验证的 FP16 模型。
FP8 作为后续独立实验,不进入当前生产模型目录和缓存身份。
十二、尾部低幅噪声处理
音色迁移结果的末尾可能包含模型填充区域。
随机噪声输入可能让这些区域产生轻微呼吸声或低幅噪声。
我们根据源音频 RMS 判断真实结束位置。
参数如下:
| 参数 | 数值 |
|---|---|
| RMS Window | 20 ms |
| RMS Hop | 10 ms |
| 最低有效峰值 | 0.0025 |
| 活动阈值 | max(0.0015, peakRms × 0.035) |
| 尾部保留 | 160 ms |
| 淡出 | 40 ms |
流程:
计算源音频分段 RMS
↓
找到最后一个活动窗口
↓
保留 160 ms 自然尾部
↓
裁掉后续模型填充
↓
应用 40 ms 余弦淡出
对于非常安静的输入,如果峰值不足以可靠判断活动边界,则保留原始长度,不主动裁剪。
十三、输出增益
音色迁移结果支持 0% 至 400% 增益。
高增益情况下使用 tanh 软限幅:
const amplified = sample * gain;
const output =
Math.tanh(amplified * 1.35) /
Math.tanh(1.35);
处理后的音频重新编码为:
RIFF WAV
PCM
Mono
22,050 Hz
16-bit
十四、IndexedDB 声音档案
声音档案保存到 IndexedDB。
IndexedDB 可以保存:
- Blob
- Float32Array
- 字符串和时间戳
- 收藏状态
- 授权状态
保存成功后,克隆声音会出现在:
- 声音克隆
- 语音合成声音列表
- 收藏声音
- 音频片段音色属性
用户可以取消收藏或彻底删除档案。
十五、时间线集成
选中音频片段后,属性面板包含独立的“音色”页面。
操作链路:
选择音频片段
↓
打开音色属性
↓
选择已保存声音
或上传/录制临时声音
↓
转换当前片段
↓
试听转换结果
↓
保存到 My assets
↓
替换当前片段
替换前会保留原始音频地址和元数据。
因此可以执行:
替换为克隆音色
↓
继续编辑
↓
恢复原始声音
生成结果不会自动插入时间线,必须由用户明确确认。
十六、多语言界面
声音克隆工作流已经覆盖:
- 中文
- English
- 日本語
- 한국어
- Español
- Français
- Deutsch
- Português
- ไทย
- Tiếng Việt
- Русский
本地化内容包括:
- 录制与上传
- 音色提取
- 模型准备
- 克隆试听
- 保存声音
- 收藏与删除
- 转换失败
- 本地处理说明
- 授权确认
- 片段替换与恢复
不同语言根据实际文本长度调整 Tab 和卡片排版,避免直接翻译后产生溢出。
十七、工程文件
核心运行文件:
src/lib/openVoiceRuntime.js
src/workers/openvoice.worker.js
src/lib/baseVoiceSynthesis.js
src/lib/voiceProfileStorage.js
src/hooks/useVoiceProfiles.js
src/hooks/useVoiceGeneration.js
src/components/VoicePanel.jsx
src/components/panels.jsx
src/config/voiceModels.js
public/model-cache-sw.js
本文用于归档 Timeline Studio 浏览器本地多语言音色迁移的当前生产实现。后续模型版本、ONNX Runtime Web、WebGPU 后端或缓存策略发生变化时,将继续按照固定 revision 和 Git 提交记录更新。