Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] エンジンのモック作成+それを使ったコンポーネントテスト #2152

Draft
wants to merge 80 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
34dcfc2
stash
Hiroshiba Jun 30, 2024
c05f3b6
scssが読み込めない!!!
Hiroshiba Jul 2, 2024
b482e48
stash
Hiroshiba Jul 2, 2024
0168fb7
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Jul 6, 2024
409ca08
mainに近づける
Hiroshiba Jul 6, 2024
1712692
テーマ周りをリファクタリング
Hiroshiba Jul 6, 2024
b1b10e4
stash
Hiroshiba Jul 7, 2024
0631c14
なぜかspec.ts側でStoryが動かせない・・・
Hiroshiba Jul 27, 2024
79d4a66
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Aug 3, 2024
602fdd4
スナップショットできた!
Hiroshiba Aug 3, 2024
e29a8f4
8.2.7
Hiroshiba Aug 3, 2024
4d16516
typo
Hiroshiba Aug 3, 2024
aa38200
いらなかった
Hiroshiba Aug 3, 2024
36382bd
ThemeConf[]
Hiroshiba Aug 3, 2024
1240838
トーク音声を再生できるように
Hiroshiba Aug 10, 2024
4d4524e
addActionsWithEmitsを削除
Hiroshiba Aug 10, 2024
518354c
snapshot更新
Hiroshiba Aug 10, 2024
067a356
Merge branch 'main' into エンジンのmockを作る
Hiroshiba Aug 11, 2024
c83f5cb
ちょっと型調整
Hiroshiba Aug 11, 2024
2009e2d
stash
Hiroshiba Aug 12, 2024
a47e3a3
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Aug 17, 2024
d7eee81
stash
Hiroshiba Aug 18, 2024
2bc525f
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Aug 24, 2024
315fff6
openapi更新
Hiroshiba Aug 24, 2024
37c17ca
たぶんsing engineのモックができた、SingEditorのstoriesを作って試す
Hiroshiba Aug 24, 2024
208d9a5
stash
Hiroshiba Aug 24, 2024
cc641d6
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Aug 31, 2024
3134578
package-lockをもどす
Hiroshiba Aug 31, 2024
97b8e1f
package-lockを最新へ
Hiroshiba Aug 31, 2024
0dd1d5f
忘れてた
Hiroshiba Aug 31, 2024
f83ddf2
定数を移動
Hiroshiba Aug 31, 2024
5a80811
TODO: SingEditorの縦幅を変わらないようにする
Hiroshiba Aug 31, 2024
52c3e72
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Sep 7, 2024
cdec85a
エディタ表示できた!
Hiroshiba Sep 7, 2024
5b706a5
たぶんドラッグができなくてテスト不可能
Hiroshiba Sep 7, 2024
9758fd8
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Sep 15, 2024
2ddde64
storybookへの追加やめる
Hiroshiba Sep 15, 2024
5c58de9
忘れてた
Hiroshiba Sep 15, 2024
24cde26
stash。GENERATE_AUDIO_ITEMを解体するか、モックを刺す。
Hiroshiba Sep 15, 2024
0b277a4
メモ:ソフトウェアが正しく起動したあとのstateをモックとして用意しておく。speakerUuidとか。
Hiroshiba Sep 21, 2024
0cccb9a
audio.spec.ts追加
Hiroshiba Sep 28, 2024
3cbb332
これでいいのかなぁ
Hiroshiba Sep 28, 2024
47688dd
不要な変更を戻す
Hiroshiba Sep 28, 2024
aa84925
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Oct 5, 2024
8aa41e8
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Oct 12, 2024
eb87e62
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Oct 12, 2024
0d972b9
convertToUrlString関数導入
Hiroshiba Oct 12, 2024
581fc30
いろいろ調整
Hiroshiba Oct 12, 2024
79882dc
微調整
Hiroshiba Oct 12, 2024
df5efb7
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Oct 19, 2024
3144503
(スナップショットを更新)
github-actions[bot] Oct 19, 2024
0080948
ブラウザ版e2eがたぶんエンジンなしで動く!
Hiroshiba Oct 19, 2024
6f03c4f
dicPathを外部に指定
Hiroshiba Oct 26, 2024
d59a090
revert
Hiroshiba Oct 26, 2024
9ddecc6
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Oct 26, 2024
968a2bc
とりあえず起動や再生はできるようになった
Hiroshiba Oct 26, 2024
815dd56
画像ファイルの場所を変更
Hiroshiba Nov 3, 2024
54faac9
もう一度移動
Hiroshiba Nov 3, 2024
b565864
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Nov 3, 2024
2caf711
[update snapshots]
Hiroshiba Nov 3, 2024
41030db
ブラウザのエンジン起動を戻す
Hiroshiba Nov 3, 2024
67fa7cd
typos
Hiroshiba Nov 3, 2024
d0febc3
audio.spec.tsのテストが通らない
Hiroshiba Nov 3, 2024
a70082d
mockSandbox足した
Hiroshiba Nov 4, 2024
5a1bfbe
Merge branch 'main' into エンジンのmockを作る
Hiroshiba Nov 6, 2024
b536c2b
テスト可能にした
Hiroshiba Nov 8, 2024
57337a3
sing系のテストも追加
Hiroshiba Nov 8, 2024
b5fdcad
Merge branch 'main' into エンジンのmockを作る
Hiroshiba Nov 9, 2024
ab16acc
不要なファイルを除去
Hiroshiba Nov 9, 2024
d6c3fd0
createStore
Hiroshiba Nov 9, 2024
d2c47c1
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Nov 16, 2024
e5a317c
Refactor: エンジンのURL生成を関数に統合し、キャラクター情報取得を非同期化
Hiroshiba Nov 16, 2024
d967d64
Refactor: audioQueryMockとsynthesisMockで音量スケールを適用し、波形生成を改善
Hiroshiba Nov 16, 2024
b323d80
Add: エンジンモックのドキュメントを追加
Hiroshiba Nov 16, 2024
6ed2174
Update:
Hiroshiba Nov 16, 2024
4b88939
Refactor: 非同期処理を適用し、キャラクター情報の取得を改善
Hiroshiba Nov 16, 2024
2d0fd0c
Merge remote-tracking branch 'upstream/main' into エンジンのmockを作る
Hiroshiba Nov 23, 2024
bc82675
kuromoji更新
Hiroshiba Nov 23, 2024
5840f12
main追従
Hiroshiba Nov 23, 2024
26b61dc
Update: 本番環境の設定をモックエンジン用に変更
Hiroshiba Nov 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .env.production
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
VITE_APP_NAME=voicevox
VITE_DEFAULT_ENGINE_INFOS=`[
{
"uuid": "074fc39e-678b-4c13-8916-ffca8d505d1d",
"name": "VOICEVOX Engine",
"executionEnabled": true,
"executionFilePath": "vv-engine/run.exe",
"name": "Mock Engine",
"uuid": "00000000-0000-0000-0000-000000000000",
"executionEnabled": false,
"executionFilePath": "いらないはず",
"executionArgs": [],
"host": "http://127.0.0.1:50021"
"host": "http://mock"
}
]`
VITE_OFFICIAL_WEBSITE_URL=https://voicevox.hiroshiba.jp/
Expand Down
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"@openapitools/openapi-generator-cli": "2.15.3",
"@playwright/test": "1.48.2",
"@quasar/vite-plugin": "1.8.1",
"@sglkc/kuromoji": "github:sglkc/kuromoji.js#3d4e9e12cf0f60a33019afb881945c348dc9dea6",
"@storybook/addon-essentials": "8.4.4",
"@storybook/addon-links": "8.4.4",
"@storybook/addon-themes": "8.4.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
export const convertToWavFileData = (audioBuffer: AudioBuffer) => {
export const convertToWavFileData = (
audioBuffer:
| AudioBuffer
| {
sampleRate: number;
length: number;
numberOfChannels: number;
getChannelData(channel: number): Float32Array;
},
) => {
const bytesPerSample = 4; // Float32
const formatCode = 3; // WAVE_FORMAT_IEEE_FLOAT

Expand Down
32 changes: 32 additions & 0 deletions src/infrastructures/EngineConnector.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createEngineUrl, EngineUrlParams } from "@/domain/url";
import { createOpenAPIEngineMock } from "@/mock/engineMock";
import { Configuration, DefaultApi, DefaultApiInterface } from "@/openapi";

export interface IEngineConnectorFactory {
Expand All @@ -22,5 +24,35 @@ const OpenAPIEngineConnectorFactoryImpl = (): IEngineConnectorFactory => {
};
};

/** WEB APIを持つエンジン用 */
export const OpenAPIEngineConnectorFactory =
OpenAPIEngineConnectorFactoryImpl();

const OpenAPIMockEngineConnectorFactoryImpl = (): IEngineConnectorFactory => ({
instance: () => createOpenAPIEngineMock(),
});

/** WEB APIを持つエンジンを模したモック用 */
export const OpenAPIMockEngineConnectorFactory =
OpenAPIMockEngineConnectorFactoryImpl();

/** モック用エンジンのURLは `http://mock` とする */
export const mockUrlParams = {
protocol: "http:",
hostname: "mock",
port: "",
pathname: "",
} satisfies EngineUrlParams;

/**
* WEB API エンジンとモックを使い分ける用。
* モック用エンジンのURLのときはモックを返す。
*/
export const OpenAPIEngineAndMockConnectorFactory: IEngineConnectorFactory = {
instance: (host: string) => {
if (host === createEngineUrl(mockUrlParams)) {
return OpenAPIMockEngineConnectorFactory.instance(host);
}
return OpenAPIEngineConnectorFactory.instance(host);
},
};
39 changes: 39 additions & 0 deletions src/mock/engineMock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# エンジンモックのドキュメント

## 概要

通信を介さずに音声合成できるエンジンのモックです。

同じ入力には同じ出力を返し、別の入力には別の出力を返すようになっています。
また出力を見たときにUIや処理の実装の異常に気付けるように、ある程度直感に合う出力を返すよう努力されています。

例:音量を下げると音声が小さくなる、音程と周波数が一致する、など。

モックの実装は気軽に破壊的変更しても問題ありません。

## ビルド戦略

ブラウザ版でも使えるようにすべく、ソフトウェアにも組み込める形で実装されています。
ビルド時のモックエンジンの取り扱いポリシーはこんな感じです。

- 重い処理が一切実行されないようにする
- 辞書の初期化、画像の読み込みなど
- なるべく重いファイルはビルドに含まれないようにする
- 形態素解析の辞書ファイルやダミー画像など

## ファイル構成

- `talkModelMock.ts`
- トーク用の音声クエリを作るまでの処理周り
- `singModelMock.ts`
- ソング用の音声クエリを作るまでの処理周り
- `audioQueryMock.ts`
- 音声クエリ周り
- `synthesisMock.ts`
- 音声波形の合成周り
- `characterResourceMock.ts`
- キャラ名や画像などのリソース周り
- `phonemeMock.ts`
- 音素周り
- `manifestMock.ts`
- エンジンのマニフェスト周り
195 changes: 195 additions & 0 deletions src/mock/engineMock/audioQueryMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/**
* AudioQueryとFrameAudioQueryのモック。
* VOICEVOX ENGINEリポジトリの処理とほぼ同じ。
*/

import { AccentPhrase, AudioQuery, FrameAudioQuery, Mora } from "@/openapi";

function generateSilenceMora(length: number): Mora {
return {
text: " ",
vowel: "sil",
vowelLength: length,
pitch: 0.0,
};
}

function toFlattenMoras(accentPhrases: AccentPhrase[]): Mora[] {
let moras: Mora[] = [];
accentPhrases.forEach((accentPhrase) => {
moras = moras.concat(accentPhrase.moras);
if (accentPhrase.pauseMora) {
moras.push(accentPhrase.pauseMora);
}
});
return moras;
}

function toFlattenPhonemes(moras: Mora[]): string[] {
const phonemes: string[] = [];
for (const mora of moras) {
if (mora.consonant) {
phonemes.push(mora.consonant);
}
phonemes.push(mora.vowel);
}
return phonemes;
}

/** 前後の無音モーラを追加する */
function applyPrePostSilence(moras: Mora[], query: AudioQuery): Mora[] {
const preSilenceMoras = [generateSilenceMora(query.prePhonemeLength)];
const postSilenceMoras = [generateSilenceMora(query.postPhonemeLength)];
return preSilenceMoras.concat(moras).concat(postSilenceMoras);
}

/** 無音時間を置き換える */
function applyPauseLength(moras: Mora[], query: AudioQuery): Mora[] {
if (query.pauseLength != undefined) {
for (const mora of moras) {
if (mora.vowel == "pau") {
mora.vowelLength = query.pauseLength;
}
}
}
return moras;
}

/** 無音時間スケールを適用する */
function applyPauseLengthScale(moras: Mora[], query: AudioQuery): Mora[] {
if (query.pauseLengthScale != undefined) {
for (const mora of moras) {
if (mora.vowel == "pau") {
mora.vowelLength *= query.pauseLengthScale;
}
}
}
return moras;
}

/** 話速スケールを適用する */
function applySpeedScale(moras: Mora[], query: AudioQuery): Mora[] {
for (const mora of moras) {
mora.vowelLength /= query.speedScale;
if (mora.consonantLength) {
mora.consonantLength /= query.speedScale;
}
}
return moras;
}

/** 音高スケールを適用する */
function applyPitchScale(moras: Mora[], query: AudioQuery): Mora[] {
for (const mora of moras) {
mora.pitch *= 2 ** query.pitchScale;
}
return moras;
}

/** 抑揚スケールを適用する */
function applyIntonationScale(moras: Mora[], query: AudioQuery): Mora[] {
const voiced = moras.filter((mora) => mora.pitch > 0);
if (voiced.length == 0) {
return moras;
}

const meanF0 =
voiced.reduce((sum, mora) => sum + mora.pitch, 0) / voiced.length;
for (const mora of voiced) {
mora.pitch = (mora.pitch - meanF0) * query.intonationScale + meanF0;
}
return moras;
}

/** 疑問文の最後に音高の高いモーラを追加する */
function applyInterrogativeUpspeak(accentPhrases: Array<AccentPhrase>) {
accentPhrases.forEach((accentPhrase) => {
const moras = accentPhrase.moras;
if (
moras.length > 0 &&
accentPhrase.isInterrogative &&
moras[moras.length - 1].pitch > 0
) {
const lastMora = moras[moras.length - 1];
const upspeakMora: Mora = {
text: "ー",
vowel: lastMora.vowel,
vowelLength: 0.15,
pitch: lastMora.pitch + 0.3,
};
accentPhrase.moras.push(upspeakMora);
}
});
}

function secondToFrame(second: number): number {
const FRAME_RATE = 24000 / 256;
return Math.round(second * FRAME_RATE);
}

/** モーラや音素ごとのフレーム数を数える */
function countFramePerUnit(moras: Mora[]): {
framePerPhoneme: number[];
framePerMora: number[];
} {
const framePerPhoneme: number[] = [];
const framePerMora: number[] = [];

for (const mora of moras) {
const vowelFrames = secondToFrame(mora.vowelLength);
const consonantFrames = mora.consonantLength
? secondToFrame(mora.consonantLength)
: 0;
const moraFrames = vowelFrames + consonantFrames;

if (mora.consonant) {
framePerPhoneme.push(consonantFrames);
}
framePerPhoneme.push(vowelFrames);
framePerMora.push(moraFrames);
}

return { framePerPhoneme, framePerMora };
}

/** AudioQueryを適当にFrameAudioQueryに変換する */
export function audioQueryToFrameAudioQueryMock(
audioQuery: AudioQuery,
{ enableInterrogativeUpspeak }: { enableInterrogativeUpspeak: boolean },
): FrameAudioQuery {
const accentPhrases = audioQuery.accentPhrases;

if (enableInterrogativeUpspeak) {
applyInterrogativeUpspeak(accentPhrases);
}

let moras = toFlattenMoras(accentPhrases);
moras = applyPrePostSilence(moras, audioQuery);
moras = applyPauseLength(moras, audioQuery);
moras = applyPauseLengthScale(moras, audioQuery);
moras = applySpeedScale(moras, audioQuery);
moras = applyPitchScale(moras, audioQuery);
moras = applyIntonationScale(moras, audioQuery);

const { framePerPhoneme, framePerMora } = countFramePerUnit(moras);

const f0 = moras.flatMap((mora, i) =>
Array<number>(framePerMora[i]).fill(
mora.pitch == 0 ? 0 : Math.exp(mora.pitch),
),
);
const volume = Array<number>(f0.length).fill(audioQuery.volumeScale);
const phonemes = toFlattenPhonemes(moras).map((phoneme, i) => ({
phoneme,
frameLength: framePerPhoneme[i],
}));

return {
f0,
volume,
phonemes,
volumeScale: audioQuery.volumeScale,
outputSamplingRate: audioQuery.outputSamplingRate,
outputStereo: audioQuery.outputStereo,
};
}
Loading
Loading