diff --git a/src/store/utility.ts b/src/store/utility.ts index ad0207694f..480a16ff8d 100644 --- a/src/store/utility.ts +++ b/src/store/utility.ts @@ -177,9 +177,9 @@ function skipMemoText(targettext: string): string { /** * 2つのアクセント句配列を比べて同じだと思われるモーラの調整結果を転写し * 変更前のアクセント句の調整結果を変更後のアクセント句に保持する。 - * 「こんにちは」 -> 「こんばんは」と変更した場合、以下の例において[]に囲まれる部分は、変更前のモーラが再利用される。 - * <例> * + * <例> + * 「こんにちは」 -> 「こんばんは」と変更した場合、[]に囲まれる部分で変更前のモーラが転写される。 * 「 [こん]ばん[は] 」 */ export class TuningTranscription { @@ -190,8 +190,11 @@ export class TuningTranscription { this.afterAccent = JSON.parse(JSON.stringify(afterAccent)); } - createFlatArray(collection: T[], key: K): T[K][] { - const result: T[K][] = []; + private createFlatArray( + 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)) { @@ -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; diff --git a/tests/unit/lib/tuningTranscription.spec.ts b/tests/unit/lib/tuningTranscription.spec.ts new file mode 100644 index 0000000000..d14e5ce4a4 --- /dev/null +++ b/tests/unit/lib/tuningTranscription.spec.ts @@ -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)); + }); +});