Commit ·
09caf12
1
Parent(s): 76124d6
大文字アルファベットでも読み方判定できるis_romaji_readable関数を追加し、audio_generatorで活用
Browse files- app/components/audio_generator.py +7 -3
- app/utils/__init__.py +4 -0
- app/utils/text_utils.py +89 -0
- tests/unit/test_text_utils.py +58 -0
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 |
-
# - 大文字のみで
|
| 161 |
if (
|
| 162 |
-
len(part) >
|
| 163 |
and re.match(r"^[A-Za-z]+$", part)
|
| 164 |
-
and
|
|
|
|
|
|
|
|
|
|
| 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
|