☰
VOICEVOX 引擎 Mock(Engine Mock)完全指南:不依赖通信的确定性语音合成模拟实现剖析
2026/10/4 10:21:14 网站建设 项目流程
  • 桌面应用
  • 语音

【免费下载链接】voicevox

無料で使える中品質なテキスト読み上げソフトウェア、VOICEVOXのエディター

项目地址:https://gitcode.com/gh_mirrors/vo/voicevox
点击查看免费下载

VOICEVOX 编辑器(本仓库)在开发与测试过程中需要一套无需启动真实引擎进程、无需网络通信即可完成"文本 → 音声"全流程的模拟实现,这就是src/mock/engineMock模块。本文以该模块的官方说明文档(src/mock/engineMock/README.md)为主体骨架,结合仓库内各 Mock 文件的实际实现与 EngineConnector.ts 的接入方式,系统讲解其设计理念、构建策略、文件构成与逐文件源码级原理。读完本文,你将理解 VOICEVOX 如何用一套确定性算法模拟形态素解析、音高/时长估计与波形合成,并能在自己的前端工程中复刻同样的"确定性 Mock 引擎"思路。

一、什么是引擎 Mock:设计理念与核心约束

1.1 概述:不做通信的引擎

根据 README 的定义,引擎 Mock 是"不通过通信即可进行语音合成的引擎模拟"。真实 VOICEVOX 引擎以 HTTP 服务形式运行,编辑器通过 OpenAPI 客户端与其交互;而 Mock 则在编辑器进程内直接实现同一套 OpenAPI 接口(DefaultApiInterface),从而让 UI 与业务逻辑可以脱离真实引擎独立开发、演示与测试。

1.2 两条核心设计原则

  • 确定性(Deterministic):"同样的输入必须返回同样的输出,不同的输入必须返回不同的输出"。这是 Mock 能被用于自动化测试的前提——测试断言必须可复现。
  • 直觉可验证(Intuitive):"输出尽量符合直觉,以便通过观察输出发现 UI 或处理实现的异常"。README 给出的两个例子非常直观:
    • 调低音量参数 → 生成的音频音量变小;
    • 音高(pitch)与频率(f0)保持一致。

这两条原则贯穿全部 Mock 文件,是理解每一处"魔法数字"的钥匙。

1.3 变更自由

README 明确说明:"Mock 的实现可以放心地进行破坏性变更"。这意味着 Mock 代码不承担任何向后兼容义务,只服务于编辑器自身的开发调试,重构时可以大胆修改。

二、构建策略:轻量、可嵌入、浏览器可用

README 规定 Mock 引擎必须以"可嵌入软件"的形态实现,从而保证浏览器版(Browser 构建)也能使用。为此制定了两条构建期策略:

策略具体做法对应实现
不执行任何重处理避免词典初始化、图片加载等开销见下方 talkModelMock 与 characterResourceMock 的轻量化手段
尽量不把重文件打进构建产物形态素解析词典文件、占位图片等不入包kuromoji 词典在 Node 下从node_modules读取、在浏览器下走 CDN;图片通过import.meta.glob按需打包

这一策略在 talkModelMock.ts 中体现得最直接:getDicPath()根据运行环境选择词典路径——Node 环境指向本地node_modules/kuromoji/dict,浏览器环境则指向 jsDelivr CDN,从而避免把体积较大的形态素词典随构建产物一起分发。

三、文件构成总览

README 给出了模块的七个核心文件及其职责,结合仓库实际目录(src/mock/engineMock/),完整清单如下:

文件职责
talkModelMock.tsトーク(语音合成 Talk)用音声查询生成之前的处理,即"文本 → アクセント句(accent phrase)"
singModelMock.tsソング(歌唱合成 Song)用音声查询生成之前的处理,即"音符(Note)→ 音素/音高/音量"
audioQueryMock.ts音声查询(AudioQuery / FrameAudioQuery)相关,负责把查询参数落实到帧级数据
synthesisMock.ts音声波形合成,把 f0/音量/音素序列变成 WAV 音频数据
characterResourceMock.ts角色名、立绘、图标等资源
phonemeMock.ts音素表:片假名(モーラ)→ 音素(辅音/元音)的静态映射
manifestMock.ts引擎清单(Engine Manifest)

此外还有两个 README 未列出的配套文件:

  • dictMock.ts:用户词典 Mock(词条增删改查 + 文本替换);
  • index.ts:模块总入口createOpenAPIEngineMock(),把所有 Mock 组装成DefaultApiInterface。

四、总入口:createOpenAPIEngineMock 与 EngineConnector 的接入

4.1 组装入口

index.ts 定义createOpenAPIEngineMock(): DefaultApiInterface,通过satisfies Partial<DefaultApiInterface>实现了一套完整但不全量的 OpenAPI 接口。它实现的函数按功能分组为:

  • 元信息:version()(固定返回"mock")、engineManifest()、supportedDevices()(返回{ cpu: true, cuda: false, dml: false });
  • 角色信息:speakers()、speakerInfo()、singers()、singerInfo()、isInitializedSpeaker()、initializeSpeaker();
  • トーク系:audioQuery()、accentPhrases()、moraData()、synthesis();
  • ソング系:singFrameAudioQuery()、singFrameF0()、singFrameVolume()、frameSynthesis();
  • 辞書系:由dictMock.createDictMockApi()展开的getUserDictWords/addUserDictWord/rewriteUserDictWord/deleteUserDictWord。

其中synthesis()与frameSynthesis()都返回Blob(audio/wav),与真实引擎的 HTTP 响应形态一致。

4.2 通过 EngineConnector 接入编辑器

Mock 并非绕过架构被硬编码调用,而是通过 src/infrastructures/EngineConnector.ts 与真实引擎统一管理:

  • OpenAPIEngineConnectorFactory:按host缓存真实引擎的DefaultApi实例;
  • OpenAPIMockEngineConnectorFactory:单例缓存createOpenAPIEngineMock()的结果;
  • OpenAPIEngineAndMockConnectorFactory:当 host 等于mock://mock(protocol: "mock:"、hostname: "mock")时返回 Mock 实例,否则返回真实引擎实例(见 EngineConnector.ts)。

也就是说,Mock 引擎与真实引擎对上层完全透明,切换只需改变引擎 URL。这也是为什么 README 强调 Mock 必须实现"与 OpenAPI 相同的接口"。

五、トーク系:文本如何变成アクセント句(talkModelMock.ts)

5.1 形态素解析:kuromoji.js 及其 Fork

README 特别说明:"本家 kuromoji.js 在路径操作上会报错,因此使用 Fork 版",且"除 Mock 用途外没有使用 kuromoji.js 的计划,若 Fork 失效会考虑移除依赖"。在 talkModelMock.ts 中可以看到它从kuromoji导入builder、Tokenizer。

Tokenizer 采用惰性单例(_tokenizer缓存,createOrGetTokenizer()),避免重复构建;且 Node 与浏览器使用不同的dicPath(见上文构建策略)。

5.2 哈希式伪随机:Mock 确定性的根基

alphabetsToNumber(text)(talkModelMock.ts)把任意字符串的字符码求和后对 256 取模,映射到 0~1 区间——这是一个纯函数哈希,同一字符串永远得到同一数值,这正是"相同输入 → 相同输出"的底层保证。基于它衍生出两个确定性估值函数:

  • phonemeToLengthMock:音素时长 ∈ [0.01, 0.25] 秒;
  • phonemeToPitchMock:音高 ∈ [3, 5]。

5.3 文本 → アクセント句的处理管线

textToActtentPhrasesMock(text, styleId)(talkModelMock.ts)完整流程:

  1. 分词:用 kuromoji 将文本切为 token;
  2. 按词性分组:遇到記号则插入pauseMora(无声,vowel: "pau")并切句;遇到助詞则切出一个アクセント句;其余 token 按reading(读音)拼进当前句;
  3. 去掉句末无声:最后一个アクセント句不保留pauseMora;
  4. 无声化处理:带无声的アクセント句若末尾モーラ为ス/ツ,将其元音改为U且音高置 0(模拟日语无声音节);
  5. 赋值时长与音高:调用replaceLengthMock与replacePitchMock。

其中两个"赋值"函数同样遵循确定性 + 直觉原则:

  • replaceLengthMock(talkModelMock.ts):每句最后一个モーラ加长 0.05s("句末拉长"的直觉),辅音时长再除以 5;
  • replacePitchMock(talkModelMock.ts):アクセント位置(accent)的モー拉音高 +0.3,无声音(vowel == "U")音高为 0——这正是 README"音高与频率一致"原则在モーラ层面的体现;
  • 两者都会叠加i * 0.01 + styleId * 0.03的偏移,确保不同アクセント句、不同角色输出不雷同。

此外aquestalkLikeToAccentPhrasesMock处理 AquesTalk 风格记法(由parseKana解析),仅复用上述两个赋值函数,逻辑与产品版"假名指定アクセント"接口对应。

5.4 音素表:phonemeMock.ts

phonemeMock.ts 维护了一个从片假名(含拗音、促音、ヴァ行、外来语小写假名等)到[辅音, 元音]的静态映射表moraToPhonemes。例如ア → [undefined, "a"](无辅音)、カ → ["k", "a"]、ン → [undefined, "N"]、ッ → [undefined, "cl"]。它是 talk 与 sing 两条管线共用的基础数据。

六、AudioQuery → FrameAudioQuery:参数如何落到帧数据(audioQueryMock.ts)

audioQueryMock.ts 头部注释明确写道"与 VOICEVOX ENGINE 仓库的处理几乎相同"——即它不是随意造假,而是复刻真实引擎的查询转换逻辑。核心函数audioQueryToFrameAudioQueryMock(audioQuery, { enableInterrogativeUpspeak })依次应用:

处理作用关键公式
applyInterrogativeUpspeak疑问句句尾追加高音モーラ(vowelLength: 0.15,音高 +0.3)需isInterrogative且句末音高 > 0
applyPrePostSilence前后各加一个无声モーラ,长度取prePhonemeLength/postPhonemeLengthvowel: "sil"
applyPauseLength统一替换pau时长取pauseLength
applyPauseLengthScale缩放pau时长vowelLength *= pauseLengthScale
applySpeedScale话速时长/= speedScale(越快越短)
applyPitchScale音高pitch *= 2 ** pitchScale
applyIntonationScale抑扬以有声モー拉平均音高为基准做线性缩放

帧化阶段,secondToFrame使用固定帧率FRAME_RATE = 24000 / 256(与 manifest 中defaultSamplingRate: 24000、frameRate: 93.75吻合),将每个モーラ/音素的秒数换算为帧数;随后生成三路帧序列:

  • f0:pitch == 0 ? 0 : Math.exp(pitch)——音高对数刻度转线性频率,再次体现"音程与频率一致";
  • volume:全帧填充volumeScale;
  • phonemes:每个音素附带frameLength。

七、波形合成:四种波形的确定性混音(synthesisMock.ts)

7.1 伪随机与四种波形

synthesisMock.ts 用线性同余生成器(LCG)Random(seed)产生 0~1 的伪随机数,seed 由 f0 与 volume 之和取整导出,保证"同一查询 → 同一波形"。generateWave支持四种波形类型:sine(正弦)、square(方波)、noise(噪声)、silence(静音)。

7.2 音素特征与波形配合率

phonemeFeatures将音素分为五类:有声母音、无声母音、无音(sil/pau/cl)、有声子音、无声子音;getWaveRate按类别返回四种波形的配合率:

  • 无音 → 几乎全静音;
  • 有声母音 → 无噪声,正弦/方波按索引比例分配;
  • 无声母音 → 噪声占比 0.3;
  • 有声子音 → 噪声占比 0.2,音量偏大;
  • 无声子音 → 噪声占比 0.1,音量偏小。

这使"不同音素听起来不同",且每帧波形随 f0 频率变化,能直观听出音高差异。

7.3 合成与输出

synthesisFrameAudioQueryMock(frameAudioQuery, styleId)以每帧 256 个采样(samplePerFrame = 256)展开波形;对配合率做约 10ms 的高斯滤波移动平均(applyGaussianFilter,见 src/song/utility.ts)以避免配合率突变造成刺耳声;最终混合后:

  • 限制在 [-1, 1] 并将音量压到 1/10(防爆音);
  • 叠加(styleId % 977) / 977 / 20的角色偏移(977 是注释中"随便选的素数"),让不同角色波形不雷同;
  • 依据outputStereo决定声道数,调用generateWavFileData以 float32 生成 WAV 字节(见 src/helpers/fileDataGenerator.ts)。

八、ソング系:音符如何变成音素/音高/音量(singModelMock.ts)

8.1 音符 → 音素(含子音挤入)

notesToFramePhonemesMock(singModelMock.ts)逐音符处理:

  • 休符(key == undefined且歌词为空)→ 生成pau音素,长度等于音符帧长;
  • 普通音符 → 用moraToPhonemes+convertHiraToKana(src/domain/japanese)把歌词(假名)转为[辅音, 元音];辅音时长取哈希估值并"挤入"前一个音符(若超过前一音符帧长则取其一半),元音占满本音符帧长——模拟真实歌唱中"子音起始于前音符"的发声特性。

8.2 音符 → 音高

notesAndFramePhonemesToPitchMock用noteNumberToFrequency(440 * 2^((noteNumber-69)/12),标准十二平均律 A4=440Hz)把音符编号转为基频,再按音素哈希在±30 cent范围内摇摆(phonemeAndKeyToPitchMock),并按1 + styleId * 0.03偏移;休符音高为 0。

8.3 音符 → 音量

notesAndFramePhonemesAndPitchToVolumeMock依据"音高越高音量越大"的直觉(phonemeAndPitchToVolumeMock,取值约 0.8~1.0,其中normalized按 note 1~128 的频率范围归一化),再乘1 - styleId * 0.03做角色区分。

8.4 关于 styleId 的一个特殊兼容

notesAndFramePhonemesToPitchMock开头有styleId %= 6000(singModelMock.ts),注释说明"出于对产品版引擎的特殊处理,可能出现 styleId=6000"——这是一个来自真实业务场景的兼容性细节,说明 Mock 会同步产品版引擎的调用习惯。

九、角色资源与引擎清单

9.1 characterResourceMock.ts

  • 用import.meta.glob(Vite 特性,eager: true)把 assets/ 下的portrait_*.png与icon_*.png按序打包成 URL 数组,避免硬编码路径(characterResourceMock.ts);
  • baseCharactersMock定义了 4 个 dummy 角色(dummy1~dummy4),其 styles 组合刻意覆盖各种情况:dummy1 为"2 トーク + 2 frame_decode"、dummy2 为"2 トーク + 1 frame_decode + 1 sing"、dummy3 为"仅 1 トーク"、dummy4 为"仅 1 sing"——用于测试角色筛选逻辑;
  • getSpeakersMock过滤出只能说话的角色(style.type 为undefined或"talk"),getSingersMock过滤出能唱歌的角色(frame_decode或sing),空角色被剔除;
  • getSpeakerInfoMock返回policy、portrait与各 style 的icon(voiceSamples为空数组)。

9.2 manifestMock.ts

manifestMock.ts 返回名为DUMMY Engine的清单:manifestVersion: "0.13.1"、defaultSamplingRate: 24000、frameRate: 93.75、1px 的 base64 图标,以及supportedFeatures能力表:

能力字段值含义
adjustMoraPitch / adjustPhonemeLength / adjustSpeedScale / adjustPitchScale / adjustIntonationScale / adjustVolumeScale / adjustPauseLengthtrue支持各参数调整
interrogativeUpspeaktrue支持疑问句上扬
synthesisMorphingfalse不支持变声融合
singtrue支持歌唱
manageLibraryfalse不支持库管理
returnResourceUrltrue资源以 URL 形式返回

十、用户词典 Mock:dictMock.ts

dictMock.ts 的DictMock类内部用Map<UserDictWordId, UserDictWord>管理词条,applyDict对文本做简单的正则替换(new RegExp(word.surface, "g")→word.pronunciation);createDictMockApi输出四个字典 OpenAPI 函数:getUserDictWords、addUserDictWord(uuid4()生成 ID,默认priority: 5,词性固定为名词)、rewriteUserDictWord、deleteUserDictWord。它被index.ts的audioQuery/accentPhrases管线通过dictMock.applyDict(payload.text)调用,模拟"用户词典影响朗读"的链路。

十一、Mock 引擎在测试中的角色

从仓库测试结构看,Mock 引擎承担了两类用途:

  1. 单元快照测试:tests/unit/mock/engineMock/ 下存在针对 Mock 输出的快照(snapshot)测试,利用其确定性保证"输出可断言、可对比";
  2. E2E 测试:浏览器端 E2E 测试(如 tests/e2e/browser/音声.spec.ts、音声パラメータ.spec.ts 等)依赖 Mock 引擎在无真实引擎环境下的完整合成能力,包括音声写出(wav)与参数调整验证——这也正是"直觉可验证"原则的最大受益场景。

十二、维护注意事项与总结

  • 破坏性变更自由:Mock 实现以编辑器开发调试为唯一服务对象,README 明确允许随意破坏性变更,无需维护兼容层;
  • 外部依赖风险:kuromoji.js 使用 Fork 版规避路径操作 bug,若 Fork 失效将考虑移除依赖(README 已声明),迁移时需评估替代的形态素解析方案;
  • 确定性是底线:任何新增 Mock 功能都应沿用"纯函数哈希 + 固定偏移"的模式,保证相同输入必得相同输出,否则会破坏快照与 E2E 测试的可复现性;
  • 与真实引擎对齐:audioQueryMock 声明"与 VOICEVOX ENGINE 仓库处理几乎相同",新增/修改参数语义时应尽量与真实引擎保持一致,避免 Mock 与真实行为出现偏差。

综上,VOICEVOX 的引擎 Mock 是一套设计精巧的确定性模拟层:以createOpenAPIEngineMock为门面、七个职责单一的文件为骨架、哈希伪随机与固定偏移为确定性基石,既能在浏览器/Node 双环境下零通信运行,又能产出符合直觉、可听辨、可断言的合成结果。对于需要"无后端可用但又要全链路可测"的桌面/Web 应用,这是一个值得直接借鉴的工程范式。

  • 桌面应用
  • 语音

【免费下载链接】voicevox

無料で使える中品質なテキスト読み上げソフトウェア、VOICEVOXのエディター

项目地址:https://gitcode.com/gh_mirrors/vo/voicevox
点击查看免费下载
上一篇:daily.dev缓存失效策略:如何避免缓存一致性问题与性能瓶颈?
下一篇:Shopware 6 完整部署指南:从零开始构建现代化电商平台

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询