From 3ce667fa290823a95b36944aac9a122de308e6db Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Tue, 22 Sep 2026 08:51:04 +0900 Subject: [PATCH] =?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=E7=AB=AF=E6=9C=AB=E5=86=85?= =?UTF-8?q?=E8=94=B5TTS=E5=90=91=E3=81=91=E3=81=AE=E8=AA=AD=E3=81=BF?= =?UTF-8?q?=E6=9B=BF=E3=81=88=E8=A1=A8=E3=81=AB=E8=BF=BD=E5=8A=A0=E3=81=97?= =?UTF-8?q?=E3=81=A6=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- src/utils/englishReading.test.ts | 33 ++++++++++++++++++++++++++++++++ src/utils/englishReading.ts | 33 ++++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/utils/englishReading.test.ts b/src/utils/englishReading.test.ts index 883626e73..4b2c19502 100644 --- a/src/utils/englishReading.test.ts +++ b/src/utils/englishReading.test.ts @@ -34,10 +34,43 @@ describe('fixEnglishReading', () => { ); }); + it('「Mine」を英語 TTS が「みね」と読む表記へ置換する', () => { + expect(fixEnglishReading('Change here for the Mine Line.')).toBe( + 'Change here for the Me-nay Line.' + ); + expect(fixEnglishReading('The next station is MINE.')).toBe( + 'The next station is Me-nay.' + ); + expect(fixEnglishReading('The next station is Nishi-Mine.')).toBe( + 'The next station is Nishi-Me-nay.' + ); + }); + + it('語中の「mine」も前後にハイフンを挟んで置換する', () => { + expect(fixEnglishReading('The next station is Tsurugamine.')).toBe( + 'The next station is Tsuruga-me-nay.' + ); + expect(fixEnglishReading('Mitsumineguchi')).toBe('Mitsu-me-nay-guchi'); + expect(fixEnglishReading('Minenobu')).toBe('Me-nay-nobu'); + expect(fixEnglishReading('Takamine')).toBe('Taka-me-nay'); + // マクロン付きの文字も語の一部として扱う + expect(fixEnglishReading('Minami-Ōmine')).toBe('Minami-Ō-me-nay'); + }); + + it('二重に適用しても結果が変わらない', () => { + expect(fixEnglishReading(fixEnglishReading('Mine Line'))).toBe( + 'Me-nay Line' + ); + expect(fixEnglishReading(fixEnglishReading('Tsurugamine'))).toBe( + 'Tsuruga-me-nay' + ); + }); + it('別語の一部は置換しない', () => { expect(fixEnglishReading('Keiseibus')).toBe('Keiseibus'); // 西武園 (Seibuen) は 1 語なので語単位の一致では対象外 expect(fixEnglishReading('Seibuen')).toBe('Seibuen'); + expect(fixEnglishReading('Minami-Urawa')).toBe('Minami-Urawa'); }); it('対象を含まないテキストはそのまま返す', () => { diff --git a/src/utils/englishReading.ts b/src/utils/englishReading.ts index be492c932..4abbd5882 100644 --- a/src/utils/englishReading.ts +++ b/src/utils/englishReading.ts @@ -5,10 +5,28 @@ // 表示用テキストには適用しないこと (TTS 生成時専用)。 type EnglishReadingRule = { - // 置換対象。ASCII 英字の単語境界 (`\b`) で囲み、`Keisei-Ueno` のような - // ハイフン連結の駅名でも語単位で一致させる。 + // 置換対象。原則は ASCII 英字の単語境界 (`\b`) で囲み、`Keisei-Ueno` のような + // ハイフン連結の駅名でも語単位で一致させる。語中も置換する場合 (Mine) は + // reading を関数にして、前後の文字との区切りを補う。 pattern: RegExp; - reading: string; + reading: string | ((match: string, offset: number, text: string) => string); +}; + +// ローマ字表記の文字か。端末内蔵 TTS 向けの経路ではマクロンを除去しないため、 +// 「Ō」などのラテン拡張文字も語の一部として扱う。 +const isRomanLetter = (char: string | undefined): boolean => + char !== undefined && /[A-Za-z\u00C0-\u024F]/.test(char); + +// 「mine」を読み替え先へ置換する。Tsurugamine のように語中にある場合も置換し、 +// 前後の文字とつながって別の綴りとして読まれないよう、接する側にハイフンを挟む。 +// 先頭の大文字・小文字は元の綴りに合わせる (Mine → Me-nay, Takamine → Taka-me-nay)。 +// TrainLCD/functions の normalizeRomanText (replaceMine) と同じ置換にして、 +// リモート TTS が無効な回とも読みを揃える。 +const toMineReading = (match: string, offset: number, text: string) => { + const before = isRomanLetter(text[offset - 1]) ? '-' : ''; + const after = isRomanLetter(text[offset + match.length]) ? '-' : ''; + const head = /[A-Z]/.test(match.charAt(0)) ? 'Me' : 'me'; + return `${before}${head}-nay${after}`; }; // NOTE: 読み替え先は必ず英語の辞書語 (か、辞書語のハイフン連結) にする。 @@ -20,6 +38,10 @@ const ENGLISH_READING_RULES: readonly EnglishReadingRule[] = [ // 「Seibu (西武)」も同じく "ei" を /aɪ/ と推定して「さいぶ」と読むため、 // "Say" + "boo" で /seɪ.buː/ (せいぶ) を確定させる。 { pattern: /\bSeibu\b/gi, reading: 'Say-boo' }, + // 「Mine (美祢・峰など)」は英語 TTS が英単語 "mine" (まいん) として読むため、 + // "Me" + "nay" で /mi.neɪ/ (みね) に寄せる。Tsurugamine・Mitsumineguchi の + // ように語中にある場合も対象にするため、単語境界では区切らない。 + { pattern: /mine/gi, reading: toMineReading }, ]; /** @@ -27,6 +49,9 @@ const ENGLISH_READING_RULES: readonly EnglishReadingRule[] = [ */ export const fixEnglishReading = (text: string): string => ENGLISH_READING_RULES.reduce( - (acc, { pattern, reading }) => acc.replace(pattern, reading), + (acc, { pattern, reading }) => + typeof reading === 'string' + ? acc.replace(pattern, reading) + : acc.replace(pattern, reading), text );