- 桌面应用
- 语音
【免费下载链接】voicevox
無料で使える中品質なテキスト読み上げソフトウェア、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)完整流程:
- 分词:用 kuromoji 将文本切为 token;
- 按词性分组:遇到
記号则插入pauseMora(无声,vowel: "pau")并切句;遇到助詞则切出一个アクセント句;其余 token 按reading(读音)拼进当前句; - 去掉句末无声:最后一个アクセント句不保留
pauseMora; - 无声化处理:带无声的アクセント句若末尾モーラ为
ス/ツ,将其元音改为U且音高置 0(模拟日语无声音节); - 赋值时长与音高:调用
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/postPhonemeLength | vowel: "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 / adjustPauseLength | true | 支持各参数调整 |
| interrogativeUpspeak | true | 支持疑问句上扬 |
| synthesisMorphing | false | 不支持变声融合 |
| sing | true | 支持歌唱 |
| manageLibrary | false | 不支持库管理 |
| returnResourceUrl | true | 资源以 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 引擎承担了两类用途:
- 单元快照测试:tests/unit/mock/engineMock/ 下存在针对 Mock 输出的快照(snapshot)测试,利用其确定性保证"输出可断言、可对比";
- 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のエディター
相关推荐
SSLsplit终极指南:透明SSL/TLS拦截工具完全解析
SSLsplit终极指南:透明SSL/TLS拦截工具完全解析 SSLsplit是一款强大的透明SSL/TLS拦截工具,专门设计用于网络取证、应用安全分析和渗透测
Moto Polly 模拟服务完全指南:语音描述、发音词典操作与合成语音的本地 Mock 实践
Moto Polly 模拟服务完全指南:语音描述、发音词典操作与合成语音的本地 Mock 实践 导读 本文以 docs/docs/services/polly.
Mock测试ngx-admin API 模拟数据工具:Mockaroo 使用指南
ngx admin API 模拟数据工具:Mockaroo 使用指南 你是否还在为后端接口未就绪而阻碍前端开发进度?是否需要大量真实感的测试数据却无从获取?本文
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考