diff --git a/stationapi/src/domain/ipa.rs b/stationapi/src/domain/ipa.rs index d9504231..6a34639c 100644 --- a/stationapi/src/domain/ipa.rs +++ b/stationapi/src/domain/ipa.rs @@ -176,13 +176,33 @@ pub fn station_name_to_tts_segments( name_katakana: &str, name_roman: Option<&str>, ) -> Vec { - name_roman + let mut segments = name_roman .map(str::trim) .filter(|name| !name.is_empty()) .and_then(romanized_name_to_tts_segments) .filter(|segments| !segments.is_empty()) .or_else(|| katakana_name_to_tts_segments(name_katakana)) - .unwrap_or_default() + .unwrap_or_default(); + + // tts_segments と、これらを連結した name_roman_ipa は英語音声 (Azure Dragon HD 等) + // で読み上げる「英語読み」トラック。ら行の弾き音 ɾ は英語ではフラップ /t/・/d/ + // (water /ˈwɔːɾɚ/ の t) として実現され、「ら行」ではなく「た/だ/ち」に化ける + // (例: ロッポンギ Roppongi → とっぽんぎ)。そのためこのトラックに限り ら行を + // 側面接近音 l に置換する (例: トリデ toɾide→tolide)。英単語の R は別記号 ɹ を + // 使うので影響しない。日本語音声で読む name_ipa (katakana_to_ipa) は honest な ɾ の + // まま残し、将来 ja-JP 音声で読む場合に備える。 + for segment in &mut segments { + segment.pronunciation = lateralize_ra_row(&segment.pronunciation); + } + + segments +} + +/// 英語読みトラックのら行 ɾ を側面接近音 l に置換する。英単語の R (ɹ) は対象外。 +/// 英語音声では ɾ が英語のフラップ /t/ として鳴り「ら行」に聞こえないため +/// (例: ロッポンギ → とっぽんぎ)。語頭・語中・語末いずれの ɾ も対象とする。 +fn lateralize_ra_row(pronunciation: &str) -> String { + pronunciation.replace('ɾ', "l") } fn join_tts_segment_pronunciations(segments: &[TtsNameSegment]) -> Option { @@ -1406,9 +1426,10 @@ mod tests { #[test] fn test_station_name_ipa_uses_official_english_wording() { + // 英語読みトラックなのでら行は l (Rinkai → liŋka.i) assert_eq!( station_name_to_ipa("カサイリンカイコウエン", Some("Kasai-Rinkai Park")), - Some("kasa.i ɾiŋka.i pɑɹk".to_string()) + Some("kasa.i liŋka.i pɑɹk".to_string()) ); } @@ -1416,12 +1437,13 @@ mod tests { fn test_station_name_ipa_supports_english_and_digits() { assert_eq!( station_name_to_ipa("ナリタクウコウ", Some("Narita Airport Terminal 1")), - Some("naɾita ɛɚpɔɹt tɚmɪnəl wʌn".to_string()) + Some("nalita ɛɚpɔɹt tɚmɪnəl wʌn".to_string()) ); } #[test] fn test_station_name_ipa_supports_multi_digit_numbers() { + // 英単語の R (ɹ) は対象外なので zɪɹoʊ の ɹ はそのまま。ら行由来の ɾ のみ l 化。 assert_eq!( station_name_to_ipa("ハネダクウコウ", Some("Haneda Airport Terminal 10")), Some("haneda ɛɚpɔɹt tɚmɪnəl wʌnzɪɹoʊ".to_string()) @@ -1436,6 +1458,30 @@ mod tests { ); } + #[test] + fn test_romanized_track_lateralizes_ra_row() { + // 英語読みトラック (station_name_to_ipa = name_roman_ipa / tts_segments) は + // 英語音声で読むため、ら行を l に置換する。語頭・語中・語末いずれも対象。 + assert_eq!( + station_name_to_ipa("トリデ", Some("Toride")), + Some("tolide".to_string()) + ); + // 一方 name_ipa 用の katakana_to_ipa は同じ語でも honest な ɾ を維持する。 + assert_eq!(katakana_to_ipa("トリデ"), Some("toɾide".to_string())); + } + + #[test] + fn test_roppongi_romanized_track_lateralizes_word_initial_r() { + // 語頭のら行 ɾ は英語音声で /t/ 化し「とっぽんぎ」に聞こえるため、英語読み + // トラックでは l に置換する (ロッポンギ ɾoppoŋgi → loppoŋgi)。 + assert_eq!( + station_name_to_ipa("ロッポンギ", Some("Roppongi")), + Some("loppoŋgi".to_string()) + ); + // name_ipa 用 (日本語音声) は honest な ɾ を維持する。 + assert_eq!(katakana_to_ipa("ロッポンギ"), Some("ɾoppoŋgi".to_string())); + } + #[test] fn test_station_name_ipa_supports_mixed_english_facility_words() { assert_eq!( @@ -1470,9 +1516,10 @@ mod tests { #[test] fn test_station_name_ipa_splits_other_compound_kaigan_suffix() { + // 英語読みトラックなのでら行は l (omoɾi → omoli) assert_eq!( station_name_to_ipa("オオモリカイガン", Some("Omorikaigan")), - Some("omoɾi ka.igaɴ".to_string()) + Some("omoli ka.igaɴ".to_string()) ); } diff --git a/stationapi/src/use_case/dto/station.rs b/stationapi/src/use_case/dto/station.rs index 147a5d6a..b8ed4715 100644 --- a/stationapi/src/use_case/dto/station.rs +++ b/stationapi/src/use_case/dto/station.rs @@ -185,7 +185,8 @@ mod tests { assert_eq!(grpc_station.name_tts_segments[0].separator, " "); assert_eq!(grpc_station.name_tts_segments[1].surface, "Rinkai"); assert_eq!(grpc_station.name_tts_segments[1].fallback_text, "りんかい"); - assert_eq!(grpc_station.name_tts_segments[1].pronunciation, "ɾiŋka.i"); + // 英語読みトラック (tts_segments) なのでら行は l (Rinkai → liŋka.i) + assert_eq!(grpc_station.name_tts_segments[1].pronunciation, "liŋka.i"); assert_eq!(grpc_station.name_tts_segments[1].separator, " "); assert_eq!(grpc_station.name_tts_segments[2].surface, "Park"); assert_eq!(grpc_station.name_tts_segments[2].fallback_text, "Park");