本文记录 Timeline Studio 浏览器本地声音克隆功能的完整技术实现,包括模型组织、音频预处理、ONNX Runtime Web 推理、浏览器缓存、IndexedDB 声音档案、时间线集成以及输出音频处理。
项目仓库:
https://github.com/MartinDelophy/ai-video-editor
本次实现的目标是在浏览器中完成声音样本录制、音色特征提取、基础语音生成和音色迁移。
完整用户工作流为:
录制或上传参考声音
↓
浏览器本地提取音色特征
↓
生成指定语言的基础语音
↓
转换为目标音色
↓
试听转换结果
↓
保存为浏览器声音档案
↓
用于后续配音或已有音频片段参考声音和音色特征不会上传到业务服务器。
浏览器运行时使用两个 FP16 ONNX 模型:
文件 | 大小 | 用途 |
|---|---|---|
| 1,637,269 Bytes | 提取参考声音的音色特征 |
| 64,314,222 Bytes | 执行源音频到目标音色的转换 |
合计 | 65,951,491 Bytes | 浏览器声音克隆运行包 |
模型目录:
openvoice-v2-converter-fp16/
├── LICENSE
├── README.md
├── config.json
├── reference-encoder.onnx
└── converter.onnxHugging Face:
ModelScope:
生产环境固定到不可变 revision,不从 main 分支动态加载模型。
声音克隆运行在独立 Web Worker 中。
React UI
↓ postMessage
OpenVoice Worker
├── 模型下载与缓存
├── Reference Encoder Session
├── Converter Session
├── FFT 与频谱计算
├── 音色特征提取
├── 音色迁移
└── 尾部清理
↓ Transferable ArrayBuffer
Main Thread
├── WAV 编码
├── IndexedDB 保存
├── My assets
└── Timeline Clip主线程负责界面和项目状态,Worker 负责模型加载和音频计算。
音频数据通过 Transferable ArrayBuffer 传递,避免在主线程与 Worker 之间复制大块 PCM 数据。
取消任务时会直接终止 Worker,并清理所有未完成的 Promise。
用户上传的参考音频可能是 WAV、MP3、M4A 或 WebM。
浏览器首先使用 AudioContext.decodeAudioData() 解码,再通过 OfflineAudioContext 统一重采样。
内部音频格式为:
参数 | 数值 |
|---|---|
采样率 | 22,050 Hz |
声道 | 单声道 |
内部采样 | Float32 PCM |
最终输出 | 16-bit PCM WAV |
处理过程:
Audio Blob
↓
decodeAudioData
↓
OfflineAudioContext
↓
22.05 kHz Mono Float32Array统一音频格式后,模型不需要处理浏览器上传文件之间的采样率和声道差异。
OpenVoice Converter 使用频谱作为输入。
浏览器端实现了 Hann 窗、FFT 和 STFT 计算。
参数 | 数值 |
|---|---|
FFT Size | 1024 |
Hop Size | 256 |
Frequency Bins | 513 |
Reference Encoder 输入形状:
spectrogram_frames: [1, FrameCount, 513]Converter 输入形状:
spectrogram: [1, 513, FrameCount]
frame_mask: [1, 1, FrameCount]频谱计算和维度转换全部在 Worker 中完成。
参考声音经过 reference-encoder.onnx 后生成 256 维音色特征:
speaker_embedding: [1, 256, 1]这个 Embedding 是浏览器声音档案的核心数据。
当用户保存克隆声音时,系统同时保存:
referenceBlob
testBlob
speakerEmbedding
sourceKind
favorite
authorized
createdAt
updatedAt再次使用已经保存的声音时,可以直接读取 Embedding,不需要重新分析参考音频。
语言生成和音色迁移是两个独立阶段。
文本
↓
根据目标语言选择基础 TTS
↓
生成目标语言源音频
↓
提取源音频的音色特征
↓
加载已保存的目标音色特征
↓
OpenVoice Converter
↓
目标语言 + 目标音色中文克隆测试使用中文基础语音,英文使用英文基础语音,其他语言按照所选声音模型处理。
OpenVoice 在这个工作流中只负责音色迁移。文本解析、语言发音和说话节奏由基础 TTS 负责。
Converter 的主要输入为:
spectrogram: [1, 513, T]
frame_mask: [1, 1, T]
source_embedding: [1, 256, 1]
target_embedding: [1, 256, 1]
noise: [1, 192, T]噪声输入使用固定种子生成,保证相同输入具有可复现性。
运行时使用:
import * as ort from "onnxruntime-web/webgpu";执行策略:
Reference Encoder
└── WASM Execution Provider
Converter
├── WebGPU Execution Provider
└── WASM FallbackConverter 首先尝试:
executionProviders: ["webgpu", "wasm"]如果 WebGPU Session 初始化失败,则重新创建 WASM Session。
所有 Session 使用:
graphOptimizationLevel: "all"WebGPU 适配器使用:
ort.env.webgpu.powerPreference = "high-performance";WASM 配置:
ort.env.wasm.simd = true;
ort.env.wasm.numThreads = crossOriginIsolated
? Math.min(4, navigator.hardwareConcurrency)
: 1;模型文件并行下载,Session 顺序初始化,避免两个模型同时编译造成较高的瞬时内存占用。
当前生产模型固定使用 FP16。
这里的 FP16 是浮点降精度部署,不等同于传统 INT8 整数量化。
没有发布 FP8 版本的原因是当前浏览器 WebGPU 计算链路缺少通用、稳定的 Float8 原生执行能力。
ONNX 文件格式可以描述 Float8 类型,但模型进入浏览器后仍可能需要:
FP8 Weight
↓
Cast / Dequantize
↓
FP16 或 FP32 Tensor
↓
WebGPU Kernel这意味着 FP8 文件可以减少网络传输和磁盘占用,但不一定减少实际计算张量和 GPU Buffer 的体积。
同时还会增加:
因此,当前版本将 FP16 作为浏览器生产格式。FP8 保留为后续实验方向,在浏览器运行时、硬件支持和音频质量验证链路成熟后再重新评估。
模型同时存储在 Hugging Face 和 ModelScope。
路由规则:
中文界面或国内环境
→ ModelScope
→ Hugging Face fallback
其他环境
→ Hugging Face
→ ModelScope fallback两个来源使用相同的 provider-independent cache identity。
因此,切换模型源不会在浏览器中保存两份相同模型。
模型加载前会请求:
navigator.storage.persist()如果浏览器拒绝持久化,功能仍然可以运行。
缓存写入发生 QuotaExceededError 时,处理策略是:
Failed to fetch早期预发布缓存会在新运行时初始化时清理,生产环境只保留 Service Worker 管理的统一缓存。
模型转换结果曾出现轻微长尾声音。
原因是输出包含频谱补齐区域,随机噪声输入可能在这些无效区域产生低幅值波形。
为此增加了基于源音频 RMS 的结束位置检测。
参数 | 数值 |
|---|---|
RMS 窗口 | 20 ms |
滑动步长 | 10 ms |
最低有效峰值 | 0.0025 |
活动阈值 |
|
尾部保留 | 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);最终重新编码为 22.05 kHz、单声道、16-bit PCM WAV。
已保存声音存储在 IndexedDB,而不是 Local Storage。
数据结构包含:
{
id,
name,
sourceKind,
referenceBlob,
testBlob,
embedding,
favorite,
authorized,
createdAt,
updatedAt
}IndexedDB 可以直接保存 Blob 和 TypedArray,适合保存参考音频、试听音频和音色 Embedding。
声音档案支持:
声音克隆不仅存在于语音生成面板,也被接入音频片段属性。
选中配音或其他可转换音频后,可以进入“音色”面板:
当前源音频
↓
选择已保存声音
或上传/录制临时参考声音
↓
提取或读取目标 Embedding
↓
执行转换
↓
试听迁移结果
↓
保存到 My assets
↓
替换当前片段片段替换时会保留原始音频信息。
用户可以执行:
替换当前片段
↓
继续编辑
↓
恢复原始声音转换结果不会在生成后自动插入时间线,必须由用户确认替换。
本次声音克隆工作流覆盖 11 种界面语言:
声音克隆相关状态均有独立翻译,包括:
不同语言的 Tab 文案按照各自语言习惯处理,没有对所有语言机械使用相同长度。
本文作为 Timeline Studio 浏览器本地声音克隆功能的工程实现归档,后续模型格式、浏览器执行后端、缓存版本和音频处理参数发生变化时,将继续按照版本提交记录更新。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。