Skip to content

Commit

Permalink
モーラごとの調整結果保持のテストを追加 (VOICEVOX#1646)
Browse files Browse the repository at this point in the history
Co-authored-by: wewewe-ok <[email protected]>
  • Loading branch information
Hiroshiba and weweweok authored Nov 10, 2023
1 parent 16c6380 commit aee7d1f
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 24 deletions.
59 changes: 35 additions & 24 deletions src/store/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,9 @@ function skipMemoText(targettext: string): string {
/**
* 2つのアクセント句配列を比べて同じだと思われるモーラの調整結果を転写し
* 変更前のアクセント句の調整結果を変更後のアクセント句に保持する。
* 「こんにちは」 -> 「こんばんは」と変更した場合、以下の例において[]に囲まれる部分は、変更前のモーラが再利用される。
* <例>
*
* <例>
* 「こんにちは」 -> 「こんばんは」と変更した場合、[]に囲まれる部分で変更前のモーラが転写される。
* 「 [こん]ばん[は] 」
*/
export class TuningTranscription {
Expand All @@ -190,8 +190,11 @@ export class TuningTranscription {
this.afterAccent = JSON.parse(JSON.stringify(afterAccent));
}

createFlatArray<T, K extends keyof T>(collection: T[], key: K): T[K][] {
const result: T[K][] = [];
private createFlatArray<T, K extends keyof T>(
collection: T[],
key: K
): T[K] extends (infer U)[] ? U[] : T[K][] {
const result = [];
for (const element of collection) {
const value = element[key];
if (Array.isArray(value)) {
Expand All @@ -200,64 +203,72 @@ export class TuningTranscription {
result.push(value);
}
}
return result;
return result as T[K] extends (infer U)[] ? U[] : T[K][];
}

/**
* 変更前の配列を操作してpatchMora配列を作る。
*
* <例> (Uはundefined)
* 変更前のテキスト差分: [ "ズ", "ン", "ダ", "モ", "ン", "ナ", "ノ", "ダ" ]
* 変更後のテキスト差分: [ "ボ", "ク", "ズ", "ン", "ダ", "ナ", "ノ", "デ", "ス" ]
* ↓
* ↓ 再利用される文字列とundefinedで構成されたデータを作る。
* ↓ 比較しやすいように文字列とundefinedを記述しているが、
* ↓ 実際には"ズ"などの文字列部分が{text: "ズ"...}のようなデータ構造となる。
* ↓
* [ U , U , "ズ", "ン", "ダ", "ナ", "ノ", U , U ]
* 変更前 [ ズ, ン, ダ, モ, ン, ナ, ノ, ダ ]
* 変更後 [ ボ, ク, ズ, ン, ダ, ナ, ノ, デ, ス ]
*
* 再利用される文字列とundefinedで構成されたデータを作る。
* [ U, U, ズ, ン, ダ, ナ, ノ, U, U ]
*
* したがって、最終的にこちらのようなデータ構造(↓)が出力される
* 実際に作られるpatchMora配列: [ U , U , {text: "ズ"...}, {text: "ン"...}, {text: "ダ"...},{text: "ナ"...},{text: "ノ"...}, U , U ]
* 実際には"ズ"などの文字列部分は{text: "ズ"...}のようなデータ構造になっている
* [ U, U, {text: "ズ"...}, {text: "ン"...}, {text: "ダ"...}, {text: "ナ"...}, {text: "ノ"...}, U, U ]
*/
createDiffPatch() {
private createDiffPatch() {
const before = structuredClone(this.beforeAccent);
const after = structuredClone(this.afterAccent);

const beforeFlatArray = this.createFlatArray(before, "moras");
const afterFlatArray = this.createFlatArray(after, "moras");
const diffed = diffArrays(
this.createFlatArray(structuredClone(beforeFlatArray), "text" as never),
this.createFlatArray(structuredClone(afterFlatArray), "text" as never)
this.createFlatArray(beforeFlatArray, "text"),
this.createFlatArray(afterFlatArray, "text")
);

// FIXME: beforeFlatArrayを破壊的に変更しなくても良いようにしてasを不要にする
let currentTextIndex = 0;
for (const diff of diffed) {
if (diff.removed) {
beforeFlatArray.splice(currentTextIndex, diff.count);
} else if (diff.added) {
diff.value.forEach(() => {
beforeFlatArray.splice(currentTextIndex, 0, undefined as never);
beforeFlatArray.splice(
currentTextIndex,
0,
undefined as never as Mora
);
currentTextIndex++;
});
} else {
currentTextIndex += diff.value.length;
}
}
return beforeFlatArray;
return beforeFlatArray as (Mora | undefined)[];
}

/**
* 「こんにちは」 -> 「こんばんは」 とテキストを変更した場合、以下の例のように、moraPatch配列とafter(AccentPhrases)を比較し、
* text(key)の値が一致するとき、after[...]["moras"][moraIndex] = moraPatch[moraPatchIndex]と代入することで、モーラを再利用する。
* moraPatchとafterAccentを比較し、textが一致するモーラを転写する。
*
* <例> (「||」は等号記号を表す)
* 「こんにちは」 -> 「こんばんは」 とテキストを変更した場合、以下の例のように比較する。
*
* moraPatch = [ {text: "コ"...}, {text: "ン"...}, undefined , undefined , {text: "ハ"...} ]
* || || ||
* after[...]["moras"] = [ {text: "コ"...}, {text: "ン"...}, {text: "バ"...}, {text: "ン"...}, {text: "ハ"...} ]
*
* あとは一致したモーラを転写するだけ。
*
*/
mergeAccentPhrases(moraPatch: (Mora | undefined)[]): AccentPhrase[] {
private mergeAccentPhrases(moraPatch: (Mora | undefined)[]): AccentPhrase[] {
const after: AccentPhrase[] = structuredClone(this.afterAccent);
let moraPatchIndex = 0;

// 与えられたアクセント句は、AccentPhrases[ Number ][ Object Key ][ Number ]の順番で、モーラを操作できるため、二重forで回す
// AccentPhrasesから[ accentIndex ]["moras"][ moraIndex ]でモーラが得られる
for (let accentIndex = 0; accentIndex < after.length; accentIndex++) {
for (
let moraIndex = 0;
Expand Down
95 changes: 95 additions & 0 deletions tests/unit/lib/tuningTranscription.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { AccentPhrase, Mora } from "@/openapi";
import { TuningTranscription } from "@/store/utility";

function createDummyMora(text: string): Mora {
return {
text,
vowel: "dummy",
vowelLength: Math.random(),
pitch: Math.random(),
};
}

function createDummyAccentPhrase(moraTexts: string[]): AccentPhrase {
return {
moras: moraTexts.map(createDummyMora),
accent: Math.random(),
};
}

// AccentPhrasesから特定のmora textを持つものMoraを返す
function findMora(
accentPhrases: AccentPhrase[],
text: string
): Mora | undefined {
let candidate: Mora | undefined;
for (let i = 0; i < accentPhrases.length; i++) {
for (let j = 0; j < accentPhrases[i].moras.length; j++) {
if (accentPhrases[i].moras[j].text === text) {
if (candidate != undefined) {
throw new Error(`AccentPhraseに${text}が複数見つかりました`);
}
candidate = accentPhrases[i].moras[j];
}
}
}
return candidate;
}

it("2つ以上のアクセント句でも正しくデータを転写できる", async () => {
const before: AccentPhrase[] = [
createDummyAccentPhrase(["い", "え"]),
createDummyAccentPhrase(["か", "き", "く", "け", "こ"]),
createDummyAccentPhrase(["さ", "し", "す", "せ", "そ"]),
];
const after: AccentPhrase[] = [
createDummyAccentPhrase(["あ", "い", "う", "え", "お"]), // 最初・真ん中・最後に追加
createDummyAccentPhrase(["き", "け"]), // 最初・真ん中・最後を消去
createDummyAccentPhrase(["た", "ち", "つ", "て", "と"]), // すべて置き換え
];
const tuningTransctiption = new TuningTranscription(before, after);
const result = tuningTransctiption.transcribe();

// モーラ数などは変わっていない
expect(result.length).toEqual(after.length);
for (let i = 0; i < result.length; i++) {
expect(result[i].moras.length).toEqual(after[i].moras.length);
}

// 転写されている
["い", "え", "き", "け"].forEach((moraText) => {
expect(findMora(result, moraText)).toEqual(findMora(before, moraText));
});

// 転写されていない
["あ", "う", "お", "た", "ち", "つ", "て", "と"].forEach((moraText) => {
expect(findMora(result, moraText)).not.toEqual(findMora(before, moraText));
});
});

it("拗音のあるモーラも正しくデータを転写できる", async () => {
const before = [
createDummyAccentPhrase(["い", "しぃ", "う", "しゅ", "お", "しょ"]),
];
const after = [
createDummyAccentPhrase(["あ", "しゃ", "き", "きゅ", "お", "しょ"]),
];
const tuningTransctiption = new TuningTranscription(before, after);
const result = tuningTransctiption.transcribe();

// モーラ数などは変わっていない
expect(result.length).toEqual(after.length);
for (let i = 0; i < result.length; i++) {
expect(result[i].moras.length).toEqual(after[i].moras.length);
}

// 転写されている
["お", "しょ"].forEach((moraText) => {
expect(findMora(result, moraText)).toEqual(findMora(before, moraText));
});

// 転写されていない
["あ", "しゃ", "き", "きゅ"].forEach((moraText) => {
expect(findMora(result, moraText)).not.toEqual(findMora(before, moraText));
});
});

0 comments on commit aee7d1f

Please sign in to comment.