From 2a2f449ed171a8cd48d85c2c28c1b2cc513e18e2 Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Tue, 22 Sep 2026 08:31:16 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E8=8B=B1=E8=AA=9ETTS=E3=81=A7Mine=E3=82=92?= =?UTF-8?q?=E3=80=8C=E3=83=9E=E3=82=A4=E3=83=B3=E3=80=8D=E3=81=A8=E8=AA=AD?= =?UTF-8?q?=E3=82=80=E8=AA=A4=E8=AA=AD=E3=82=92=E4=BF=AE=E6=AD=A3=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 英語 TTS が駅名・路線名の「Mine(みね)」を英単語 "mine" として 「まいん」と読むため、辞書語の連結 "Me-nay" に置換して /mi.neɪ/ に寄せる。 Keisei・Seibu と同様に単語境界で一致させ、別語の一部は置換しない。 Co-Authored-By: Claude Opus 5 --- src/utils/normalize.test.ts | 19 +++++++++++++++++++ src/utils/normalize.ts | 3 +++ 2 files changed, 22 insertions(+) diff --git a/src/utils/normalize.test.ts b/src/utils/normalize.test.ts index 40e7ac7..181d3f7 100644 --- a/src/utils/normalize.test.ts +++ b/src/utils/normalize.test.ts @@ -54,6 +54,22 @@ 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.' + ); + // 別語の一部は置換しない + expect(normalizeRomanText('Minami-Urawa')).toBe('Minami-urawa'); + expect(normalizeRomanText('Takamine')).toBe('Takamine'); + }); + it('keeps Kay-say stable when normalized twice', () => { // 二重に適用しても結果が変わらないこと(キャッシュキーの安定性) expect(normalizeRomanText(normalizeRomanText('Keisei Main Line'))).toBe( @@ -62,6 +78,9 @@ describe('utils/normalize.ts', () => { expect(normalizeRomanText(normalizeRomanText('Seibu Shinjuku Line'))).toBe( 'Say-boo Shinjuku Line' ); + expect(normalizeRomanText(normalizeRomanText('Mine Line'))).toBe( + 'Me-nay Line' + ); }); it.each(['Tokyo', 'tOkyo'])('text: %s', (text) => { diff --git a/src/utils/normalize.ts b/src/utils/normalize.ts index cfd4deb..d12847a 100644 --- a/src/utils/normalize.ts +++ b/src/utils/normalize.ts @@ -43,6 +43,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(/\bMine\b/gi, 'Me-nay') // 都営バスを想定 .replace(/\bSta\./gi, ' Station') .replace(/\bUniv\./gi, ' University') From c128dbb7f00d78c553ae79726a643f710342cbae Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Tue, 22 Sep 2026 08:35:25 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=E8=AA=9E=E4=B8=AD=E3=81=AB=E5=9F=8B?= =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=9Fmine=E3=82=82=E3=80=8C=E3=81=BF?= =?UTF-8?q?=E3=81=AD=E3=80=8D=E3=81=A8=E8=AA=AD=E3=82=80=E3=82=88=E3=81=86?= =?UTF-8?q?=E7=BD=AE=E6=8F=9B=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Takamine や Minezaki のように英名の語中・語頭に含まれる mine も 置換対象にし、前後に英字が続く場合はハイフンで区切って Me-nay を独立させる。 Co-Authored-By: Claude Opus 5 --- src/utils/normalize.test.ts | 12 +++++++++--- src/utils/normalize.ts | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/utils/normalize.test.ts b/src/utils/normalize.test.ts index 181d3f7..572b4fd 100644 --- a/src/utils/normalize.test.ts +++ b/src/utils/normalize.test.ts @@ -63,11 +63,14 @@ describe('utils/normalize.ts', () => { 'The next station is Me-nay.' ); expect(normalizeRomanText('The next station is Nishi-Mine.')).toBe( - 'The next station is Nishi-Me-nay.' + 'The next station is Nishi-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'); - expect(normalizeRomanText('Takamine')).toBe('Takamine'); }); it('keeps Kay-say stable when normalized twice', () => { @@ -81,6 +84,9 @@ describe('utils/normalize.ts', () => { 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 d12847a..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,9 +53,9 @@ const normalizeTextNode = (text: string): string => // ハイフン連結の駅名も語単位で置換する .replace(/\bKeisei\b/gi, 'Kay-say') .replace(/\bSeibu\b/gi, 'Say-boo') - // 「Mine(美祢・峰など)」は英語 TTS が英単語 "mine"(まいん)として読むため、 + // 「mine(美祢・峰など)」は英語 TTS が英単語 "mine"(まいん)として読むため、 // 同じく辞書語 "Me" + "nay" の連結で /mi.neɪ/(みね)に寄せる - .replace(/\bMine\b/gi, 'Me-nay') + .replace(/mine/gi, replaceMine) // 都営バスを想定 .replace(/\bSta\./gi, ' Station') .replace(/\bUniv\./gi, ' University') From e37e2a0b7930c73f5dadf902210e91ea15eb0e6f Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Tue, 22 Sep 2026 08:44:17 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=E5=AE=87=E9=83=BD=E5=AE=AE=E3=81=AE?= =?UTF-8?q?=E5=B6=BA=E9=A7=85=E3=82=92=E6=83=B3=E5=AE=9A=E3=81=97=E3=81=9F?= =?UTF-8?q?Mine=E7=BD=AE=E6=8F=9B=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- src/utils/normalize.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/normalize.test.ts b/src/utils/normalize.test.ts index 572b4fd..0713508 100644 --- a/src/utils/normalize.test.ts +++ b/src/utils/normalize.test.ts @@ -65,6 +65,13 @@ describe('utils/normalize.ts', () => { 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'); From 29a81fd06a4a336377a1dfcd57f446a1fd041a33 Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Tue, 22 Sep 2026 08:45:05 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=A1=E3=83=B3=E3=83=88=E3=81=AE=E9=A7=85=E5=90=8D=E3=82=92?= =?UTF-8?q?=E5=AE=87=E9=83=BD=E5=AE=AE=E3=83=A9=E3=82=A4=E3=83=88=E3=83=AC?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=81=AE=E5=B3=B0=E9=A7=85=E3=81=AB=E8=A8=82?= =?UTF-8?q?=E6=AD=A3=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- src/utils/normalize.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/normalize.test.ts b/src/utils/normalize.test.ts index 0713508..5f825a6 100644 --- a/src/utils/normalize.test.ts +++ b/src/utils/normalize.test.ts @@ -65,7 +65,7 @@ describe('utils/normalize.ts', () => { expect(normalizeRomanText('The next station is Nishi-Mine.')).toBe( 'The next station is Nishi-me-nay.' ); - // 宇都宮ライトレールの嶺(Mine)のような単独の駅名 + // 宇都宮ライトレールの峰(Mine)のような単独の駅名 expect(normalizeRomanText('The next stop is Mine.')).toBe( 'The next stop is Me-nay.' );