diff --git a/src/utils/normalize.test.ts b/src/utils/normalize.test.ts index 40e7ac7..5f825a6 100644 --- a/src/utils/normalize.test.ts +++ b/src/utils/normalize.test.ts @@ -54,6 +54,32 @@ describe('utils/normalize.ts', () => { expect(normalizeRomanText('Seibuen')).toBe('Seibuen'); }); + it('replaces Mine with a spelling English TTS reads as みね', () => { + // 英語 TTS は "Mine" を英単語の「まいん」と読むため、辞書語の綴りへ倒す + expect(normalizeRomanText('Change here for the Mine Line.')).toBe( + 'Change here for the Me-nay Line.' + ); + expect(normalizeRomanText('The next station is MINE.')).toBe( + 'The next station is Me-nay.' + ); + expect(normalizeRomanText('The next station is Nishi-Mine.')).toBe( + 'The next station is Nishi-me-nay.' + ); + // 宇都宮ライトレールの峰(Mine)のような単独の駅名 + expect(normalizeRomanText('The next stop is Mine.')).toBe( + 'The next stop is Me-nay.' + ); + expect(normalizeRomanText('This train is bound for Mine.')).toBe( + 'This train is bound for Me-nay.' + ); + // 語中・語頭に埋まった mine も置換する + expect(normalizeRomanText('Takamine')).toBe('Taka-me-nay'); + expect(normalizeRomanText('Minezaki')).toBe('Me-nay-zaki'); + expect(normalizeRomanText('KAMINEYAMA')).toBe('Ka-me-nay-yama'); + // mine を含まない語は変えない + expect(normalizeRomanText('Minami-Urawa')).toBe('Minami-urawa'); + }); + it('keeps Kay-say stable when normalized twice', () => { // 二重に適用しても結果が変わらないこと(キャッシュキーの安定性) expect(normalizeRomanText(normalizeRomanText('Keisei Main Line'))).toBe( @@ -62,6 +88,12 @@ describe('utils/normalize.ts', () => { expect(normalizeRomanText(normalizeRomanText('Seibu Shinjuku Line'))).toBe( 'Say-boo Shinjuku Line' ); + expect(normalizeRomanText(normalizeRomanText('Mine Line'))).toBe( + 'Me-nay Line' + ); + expect(normalizeRomanText(normalizeRomanText('Takamine'))).toBe( + 'Taka-me-nay' + ); }); it.each(['Tokyo', 'tOkyo'])('text: %s', (text) => { diff --git a/src/utils/normalize.ts b/src/utils/normalize.ts index cfd4deb..6c44efc 100644 --- a/src/utils/normalize.ts +++ b/src/utils/normalize.ts @@ -15,6 +15,16 @@ const capitalizeSegment = (seg: string): string => { : seg; }; +// Takamine / Minezaki のように語中・語頭に埋まった "mine" も読み替える。 +// 前後に英字が続く場合はハイフンで区切って "Me-nay" を独立させ、 +// 語頭のときだけ先頭を大文字にする(Takamine → Taka-me-nay) +const replaceMine = (match: string, offset: number, str: string): string => { + const before = offset > 0 && /[A-Za-z]/.test(str.charAt(offset - 1)); + const after = /[A-Za-z]/.test(str.charAt(offset + match.length)); + const head = /[A-Z]/.test(match.charAt(0)) ? 'Me' : 'me'; + return `${before ? '-' : ''}${head}-nay${after ? '-' : ''}`; +}; + // テキストノード(SSML タグの外側)だけに掛ける正規化。タグやその属性値 // ( や 等)を壊さないため、タグ部分には適用しない。 const normalizeTextNode = (text: string): string => @@ -43,6 +53,9 @@ const normalizeTextNode = (text: string): string => // ハイフン連結の駅名も語単位で置換する .replace(/\bKeisei\b/gi, 'Kay-say') .replace(/\bSeibu\b/gi, 'Say-boo') + // 「mine(美祢・峰など)」は英語 TTS が英単語 "mine"(まいん)として読むため、 + // 同じく辞書語 "Me" + "nay" の連結で /mi.neɪ/(みね)に寄せる + .replace(/mine/gi, replaceMine) // 都営バスを想定 .replace(/\bSta\./gi, ' Station') .replace(/\bUniv\./gi, ' University')