KyosukeIchikawa commited on
Commit
09caf12
·
1 Parent(s): 76124d6

大文字アルファベットでも読み方判定できるis_romaji_readable関数を追加し、audio_generatorで活用

Browse files
app/components/audio_generator.py CHANGED
@@ -15,6 +15,7 @@ from typing import List, Optional
15
  import e2k
16
 
17
  from app.utils.logger import logger
 
18
 
19
  # VOICEVOX Core imports
20
  try:
@@ -157,11 +158,14 @@ class AudioGenerator:
157
  # 下記であればカタカナに変換し, そうでなければ変換せずにそのまま追加
158
  # - 2文字以上である
159
  # - アルファベットのみで構成されている
160
- # - 大文字のみで2~5文字でない(頭文字で構成され略語まま読むスタンスだが, 6文字以上であればカタカナみす確率が高そう & アルファベット読みはくどい)
161
  if (
162
- len(part) > 1
163
  and re.match(r"^[A-Za-z]+$", part)
164
- and not re.match(r"^[A-Z]{2,5}$", part)
 
 
 
165
  ):
166
  katakana_part = c2k(part)
167
 
 
15
  import e2k
16
 
17
  from app.utils.logger import logger
18
+ from app.utils.text_utils import is_romaji_readable
19
 
20
  # VOICEVOX Core imports
21
  try:
 
158
  # 下記であればカタカナに変換し, そうでなければ変換せずにそのまま追加
159
  # - 2文字以上である
160
  # - アルファベットのみで構成されている
161
+ # - 大文字のみでない、または大文字だが4文字以上かつローマ字として
162
  if (
163
+ len(part) >= 2
164
  and re.match(r"^[A-Za-z]+$", part)
165
+ and (
166
+ not re.match(r"^[A-Z]+$", part)
167
+ or (len(part) >= 4 and is_romaji_readable(part))
168
+ )
169
  ):
170
  katakana_part = c2k(part)
171
 
app/utils/__init__.py CHANGED
@@ -2,3 +2,7 @@
2
 
3
  Contains utility functions for file processing, logging, and more
4
  """
 
 
 
 
 
2
 
3
  Contains utility functions for file processing, logging, and more
4
  """
5
+
6
+ from app.utils.text_utils import is_romaji_readable
7
+
8
+ __all__ = ["is_romaji_readable"]
app/utils/text_utils.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Text processing utilities for the Paper Podcast Generator.
2
+
3
+ Contains utility functions for text processing, romanization support, and more.
4
+ """
5
+
6
+ import re
7
+
8
+
9
+ def is_romaji_readable(word: str) -> bool:
10
+ """
11
+ 大文字アルファベットで構成された単語が、日本語のローマ字として音節に分解して読めるかを判定します。
12
+
13
+ 判定基準:
14
+ - 単語は母音(A,I,U,E,O)、子音+母音、拗音(KYAなど)、撥音(N)の組み合わせで構成される。
15
+ - 撥音Nは、後に子音が続くか、語末にある場合に成立する。
16
+ - 促音(例: TT, SS, KK)は基本的に考慮しない。(例: URRI, LLA, KITTE, NISSAN は不可)
17
+ - ユーザー指定の例に基づき、HONDAやAIKOは可能、URRIやLLAは不可能とする。
18
+
19
+ Args:
20
+ word (str): 判定対象の大文字アルファベットの文字列。
21
+
22
+ Returns:
23
+ bool: ローマ字読み可能であればTrue、そうでなければFalse。
24
+ """
25
+ if not isinstance(word, str) or not word: # 空文字列やNoneはFalse
26
+ return False
27
+ if not re.fullmatch(r"[A-Z]+", word): # 大文字アルファベット以外が含まれている場合はFalse
28
+ return False
29
+
30
+ # 単一の母音のケース(1文字の場合はパターン照合前に判定)
31
+ if len(word) == 1:
32
+ return word in "AIUEO"
33
+
34
+ # ローマ字の音節パターンリスト(優先順位順に定義)
35
+ # 長いもの、特殊なものから順にマッチさせることで、正しい音節分割を促す。
36
+ syllable_patterns = [
37
+ # 特殊な複合子音
38
+ r"TSU", # つ
39
+ r"SHI", # し
40
+ r"CHI", # ち
41
+ # 特殊な拗音
42
+ r"CH[AUO]", # ちゃ, ちゅ, ちょ
43
+ r"SH[AUO]", # しゃ, しゅ, しょ
44
+ # 拗音 (子音 + Y + 母音)
45
+ # 例: KYA, SYA (訓令式 しゃ), TYA (訓令式 ちゃ), NYA, HYA, MYA, RYA, GYA, JYA, DYA, BYA, PYA
46
+ # SH[AUO], CH[AUO] は上で定義済みなので、ここではそれ以外の Y を含む拗音をカバー。
47
+ # ZY[AUO] (じゃ等)も JYA と同様にここでカバー。
48
+ r"(?:K|S|T|N|H|M|R|G|Z|J|D|B|P)Y[AUO]",
49
+ # 通常の子音 + 母音
50
+ # 例: KA, KI, FU, MI, TO, WA, YA, SU, JI (ZI,DIも含む), ZO
51
+ # F[AIUEO], W[AIUEO], Y[AUO] (YA,YU,YO) も含む
52
+ # J[AUO] (JA,JU,JO) も含む
53
+ # (SI, TI, HU, DI, ZU など、訓令式/日本式に近い表記も許容)
54
+ r"[BCDFGHJKLMNPQRSTVWXYZ][AIUEO]",
55
+ # 母音単独
56
+ r"[AIUEO]",
57
+ # 撥音「ん」
58
+ # このNは、正規表現のマッチングプロセスにより、
59
+ # Nの後に母音やYが続く場合は、上記のより長いパターン(例:NA, NI, NYA)で先にマッチされる。
60
+ # そのため、このNが単独でマッチするのは、主に後に子音が続く場合(例:HONDAのN)や
61
+ # 語末(例:KENのN)、またはNの後にさらにNで始まる音節が続く場合(例:GINNANの最初のN)。
62
+ r"N",
63
+ ]
64
+
65
+ # 各音節パターンを非キャプチャグループ (?:...) で囲み、OR (|) で結合
66
+ syllable_regex_part = "|".join([f"(?:{p})" for p in syllable_patterns])
67
+
68
+ # 単語全体が「(音節パターン1 | 音節パターン2 | ...)+」に完全一致するかを判定
69
+ # これにより、単語が上記の音節の繰り返しのみで構成されているかをチェックする。
70
+ full_word_regex = re.compile(f"^({syllable_regex_part})+$")
71
+
72
+ # 文字列がマッチするかどうかを判定
73
+ if full_word_regex.fullmatch(word):
74
+ return True
75
+
76
+ # 一部の複合音(SH+I, CH+I)のパターンを特別に許容する
77
+ # これは正規表現での優先的なマッチングが難しいため、手動チェックする
78
+ temp_word = word
79
+ temp_word = re.sub(r"SHI", "SI", temp_word)
80
+ temp_word = re.sub(r"CHI", "TI", temp_word)
81
+ temp_word = re.sub(r"SHA", "SA", temp_word)
82
+ temp_word = re.sub(r"SHU", "SU", temp_word)
83
+ temp_word = re.sub(r"SHO", "SO", temp_word)
84
+ temp_word = re.sub(r"CHA", "TA", temp_word)
85
+ temp_word = re.sub(r"CHU", "TU", temp_word)
86
+ temp_word = re.sub(r"CHO", "TO", temp_word)
87
+
88
+ # 置換後の文字列で再チェック
89
+ return bool(full_word_regex.fullmatch(temp_word))
tests/unit/test_text_utils.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for text_utils module"""
2
+
3
+ import pytest
4
+
5
+ from app.utils.text_utils import is_romaji_readable
6
+
7
+
8
+ class TestIsRomajiReadable:
9
+ """Test class for is_romaji_readable function"""
10
+
11
+ @pytest.mark.parametrize(
12
+ "word, expected",
13
+ [
14
+ # 基本的なローマ字読み可能な単語
15
+ ("HONDA", True), # ホ・ン・ダ - 単純な子音+母音の組み合わせと撥音
16
+ ("AIKO", True), # ア・イ・コ - 母音とKOの組み合わせ
17
+ ("TOKYO", True), # ト・ウ・キョ・ウ - 拗音KYOを含む
18
+ ("SUSHI", True), # ス・シ - SHIの組み合わせ
19
+ ("SAKURA", True), # サ・ク・ラ - 基本的なローマ字
20
+ ("NIHON", True), # ニ・ホ・ン - 語末のN
21
+ ("ICHIBAN", True), # イ・チ・バ・ン - CHを含む
22
+ ("GENKI", True), # ゲ・ン・キ - 中間のN
23
+ ("KONNICHIWA", True), # コ・ン・ニ・チ・ワ - 複数のNを含む
24
+ ("SHINBUN", True), # シ・ン・ブ・ン - SHを含む
25
+ # 特殊なパターン
26
+ ("TOKYO", True), # KYの拗音
27
+ ("RYOKO", True), # RYの拗音
28
+ ("CHANOYU", True), # チャノユ - CHAの拗音
29
+ ("DENSHA", True), # デ・ン・シャ - SHAの拗音
30
+ # ローマ字読み不可能な単語
31
+ ("URRI", False), # RRが続くため不可
32
+ ("LLA", False), # LLが続くため不可
33
+ ("KITTE", False), # TTが続くため不可
34
+ ("NISSAN", False), # SSが続くため不可
35
+ ("XML", False), # 子音MLだけのため不可
36
+ ("WTF", False), # 子音WTFだけのため不可
37
+ ("WWW", False), # 子音WWWだけのため不可
38
+ # エッジケース
39
+ ("A", True), # 母音1文字は読める
40
+ ("", False), # 空文字
41
+ ("abc", False), # 小文字(大文字のみ判定ではじかれる)
42
+ ("HONDA2", False), # 数字を含む
43
+ ("HONDA-KUN", False), # 記号を含む
44
+ # 促音を含む場合(現実装では不可)
45
+ ("MOTTO", False), # モット - TTが促音
46
+ ("RAKKYO", False), # ラッキョウ - KKが促音
47
+ ],
48
+ )
49
+ def test_romaji_readable_detection(self, word, expected):
50
+ """Test is_romaji_readable function correctly identifies romanizable words"""
51
+ assert is_romaji_readable(word) == expected
52
+
53
+ def test_invalid_input_types(self):
54
+ """Test function handles invalid input types"""
55
+ assert is_romaji_readable(None) is False # type: ignore
56
+ assert is_romaji_readable(123) is False # type: ignore
57
+ assert is_romaji_readable([]) is False # type: ignore
58
+ assert is_romaji_readable({}) is False # type: ignore