KyosukeIchikawa commited on
Commit
9a2f504
·
1 Parent(s): a0a44b7

依存関係にe2kを追加し、音声生成機能のカタカナ変換処理をalkanaからe2kに移行しました。また、関連するユニットテストを更新し、モックを使用して新しい変換ロジックを検証しました。これにより、カタカナ変換の精度が向上しました。

Browse files
app/components/audio_generator.py CHANGED
@@ -11,7 +11,7 @@ import uuid
11
  from pathlib import Path
12
  from typing import List, Optional
13
 
14
- import alkana
15
 
16
  from app.utils.logger import logger
17
 
@@ -132,28 +132,41 @@ class AudioGenerator:
132
  Returns:
133
  str: 英単語がカタカナに変換されたテキスト
134
  """
135
- # 英単語を検出する正規表現
136
- word_pattern = re.compile(r"[a-zA-Z][a-zA-Z\'-]*[a-zA-Z]|[a-zA-Z]")
137
-
138
- def replace_word(match: re.Match) -> str:
139
- word: str = match.group(0).lower()
140
-
141
- # ハイフンが含まれている場合、各部分を個別に変換
142
- parts = word.split("-")
143
- katakana_parts = []
144
-
145
- for part in parts:
146
- if part: # 空の部分をスキップ
147
- katakana = alkana.get_kana(part)
148
- katakana_parts.append(katakana if katakana else part)
149
- else:
150
- katakana_parts.append("")
151
-
152
- # 再結合(ハイフンは音声に必要ないため捨てる)
153
- return "".join(katakana_parts)
154
-
155
- # 英単語をカタカナに置換
156
- return word_pattern.sub(replace_word, text)
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
  def _create_final_audio_file(self, temp_wav_files: List[str]) -> str:
159
  """
 
11
  from pathlib import Path
12
  from typing import List, Optional
13
 
14
+ import e2k
15
 
16
  from app.utils.logger import logger
17
 
 
132
  Returns:
133
  str: 英単語がカタカナに変換されたテキスト
134
  """
135
+ c2k = e2k.C2K()
136
+ # 英単語と非英単語を分割するための正規表現パターン
137
+ pattern = r"([A-Za-z]+|[^A-Za-z]+)"
138
+ parts = re.findall(pattern, text)
139
+
140
+ # 大文字で始まる部分を分割する
141
+ split_parts = []
142
+ for part in parts:
143
+ if re.match(r"^[A-Za-z]+$", part):
144
+ # 連続する大文字は一つの略語として扱う
145
+ word_parts = re.findall(r"[A-Z]{2,}|[A-Z][a-z]*|[a-z]+", part)
146
+ split_parts.extend(word_parts)
147
+ else:
148
+ # 非英単語はそのまま追加
149
+ split_parts.append(part)
150
+
151
+ # 英単語をカタカナに変換
152
+ result = []
153
+ for part in split_parts:
154
+ # 下記であればカタカナに変換し, そうでなければ変換せずにそのまま追加
155
+ # - 2文字以上である
156
+ # - アルファベットのみで構成されている
157
+ # - 大文字のみで2~5文字でない(頭文字で構成された略語はそのまま読むスタンスだが, 6文字以上であればカタカナ読みする確率が高そう & アルファベット読みはくどい)
158
+ if (
159
+ len(part) > 1
160
+ and re.match(r"^[A-Za-z]+$", part)
161
+ and not re.match(r"^[A-Z]{2,5}$", part)
162
+ ):
163
+ result.append(c2k(part))
164
+ elif part == "-":
165
+ # ハイフンは捨てる
166
+ pass
167
+ else:
168
+ result.append(part)
169
+ return "".join(result)
170
 
171
  def _create_final_audio_file(self, temp_wav_files: List[str]) -> str:
172
  """
requirements.in CHANGED
@@ -1,7 +1,7 @@
1
- alkana
2
  autoflake
3
  autopep8
4
  black
 
5
  ffmpeg-python
6
  flake8
7
  gradio
 
 
1
  autoflake
2
  autopep8
3
  black
4
+ e2k
5
  ffmpeg-python
6
  flake8
7
  gradio
requirements.txt CHANGED
@@ -6,8 +6,6 @@
6
  #
7
  aiofiles==24.1.0
8
  # via gradio
9
- alkana==0.0.3
10
- # via -r requirements.in
11
  annotated-types==0.7.0
12
  # via pydantic
13
  anyio==4.9.0
@@ -69,6 +67,8 @@ distlib==0.3.9
69
  # via virtualenv
70
  distro==1.9.0
71
  # via openai
 
 
72
  execnet==2.1.1
73
  # via pytest-xdist
74
  fastapi==0.115.12
@@ -126,7 +126,7 @@ gradio==5.29.0
126
  # via -r requirements.in
127
  gradio-client==1.10.0
128
  # via gradio
129
- greenlet==3.2.1
130
  # via playwright
131
  groovy==0.1.2
132
  # via gradio
@@ -141,8 +141,6 @@ h11==0.16.0
141
  # httpcore
142
  # uvicorn
143
  # wsproto
144
- hf-xet==1.1.0
145
- # via huggingface-hub
146
  httpcore==1.0.9
147
  # via httpx
148
  httplib2==0.22.0
@@ -156,7 +154,7 @@ httpx==0.28.1
156
  # gradio-client
157
  # openai
158
  # safehttpx
159
- huggingface-hub==0.31.1
160
  # via
161
  # -r requirements.in
162
  # gradio
@@ -220,6 +218,7 @@ nodeenv==1.9.1
220
  numpy==2.2.5
221
  # via
222
  # -r requirements.in
 
223
  # gradio
224
  # magika
225
  # onnxruntime
@@ -262,11 +261,11 @@ nvidia-nvjitlink-cu12==12.6.85
262
  # torch
263
  nvidia-nvtx-cu12==12.6.77
264
  # via torch
265
- onnxruntime==1.21.1
266
  # via
267
  # -r requirements.in
268
  # magika
269
- openai==1.78.0
270
  # via -r requirements.in
271
  orjson==3.10.18
272
  # via gradio
@@ -434,7 +433,7 @@ rope==1.13.0
434
  # via -r requirements.in
435
  rsa==4.9.1
436
  # via google-auth
437
- ruff==0.11.8
438
  # via gradio
439
  safehttpx==0.1.6
440
  # via gradio
@@ -493,7 +492,7 @@ trio-websocket==0.12.2
493
  # via selenium
494
  triton==3.3.0
495
  # via torch
496
- typer==0.15.3
497
  # via gradio
498
  types-requests==2.32.0.20250328
499
  # via -r requirements.in
 
6
  #
7
  aiofiles==24.1.0
8
  # via gradio
 
 
9
  annotated-types==0.7.0
10
  # via pydantic
11
  anyio==4.9.0
 
67
  # via virtualenv
68
  distro==1.9.0
69
  # via openai
70
+ e2k==0.6.2
71
+ # via -r requirements.in
72
  execnet==2.1.1
73
  # via pytest-xdist
74
  fastapi==0.115.12
 
126
  # via -r requirements.in
127
  gradio-client==1.10.0
128
  # via gradio
129
+ greenlet==3.2.2
130
  # via playwright
131
  groovy==0.1.2
132
  # via gradio
 
141
  # httpcore
142
  # uvicorn
143
  # wsproto
 
 
144
  httpcore==1.0.9
145
  # via httpx
146
  httplib2==0.22.0
 
154
  # gradio-client
155
  # openai
156
  # safehttpx
157
+ huggingface-hub==0.31.2
158
  # via
159
  # -r requirements.in
160
  # gradio
 
218
  numpy==2.2.5
219
  # via
220
  # -r requirements.in
221
+ # e2k
222
  # gradio
223
  # magika
224
  # onnxruntime
 
261
  # torch
262
  nvidia-nvtx-cu12==12.6.77
263
  # via torch
264
+ onnxruntime==1.22.0
265
  # via
266
  # -r requirements.in
267
  # magika
268
+ openai==1.78.1
269
  # via -r requirements.in
270
  orjson==3.10.18
271
  # via gradio
 
433
  # via -r requirements.in
434
  rsa==4.9.1
435
  # via google-auth
436
+ ruff==0.11.9
437
  # via gradio
438
  safehttpx==0.1.6
439
  # via gradio
 
492
  # via selenium
493
  triton==3.3.0
494
  # via torch
495
+ typer==0.15.4
496
  # via gradio
497
  types-requests==2.32.0.20250328
498
  # via -r requirements.in
tests/unit/test_audio_generator.py CHANGED
@@ -1,5 +1,5 @@
1
  import unittest
2
- from unittest.mock import patch
3
 
4
  from app.components.audio_generator import AudioGenerator
5
 
@@ -9,85 +9,110 @@ class TestAudioGenerator(unittest.TestCase):
9
 
10
  def setUp(self):
11
  """テスト実行前のセットアップ"""
12
- # VOICEVOXの初期化をモックする
13
  with patch("app.components.audio_generator.VOICEVOX_CORE_AVAILABLE", False):
14
- self.audio_generator = AudioGenerator()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  def test_convert_english_to_katakana_basic(self):
17
  """基本的な英単語のカタカナ変換テスト"""
18
- # alkana.get_kanaをモックする
19
- with patch("app.components.audio_generator.alkana.get_kana") as mock_get_kana:
20
- # モックの戻り値を設定
21
- mock_get_kana.side_effect = lambda word: {
22
- "hello": "ハロー",
23
- "world": "ワールド",
24
- }.get(word, None)
25
-
26
- # 通常の英単語
27
- result = self.audio_generator._convert_english_to_katakana("Hello World!")
28
- self.assertEqual(result, "ハロー ワールド!")
 
29
 
30
  def test_convert_english_to_katakana_with_hyphen(self):
31
  """ハイフンを含む英単語のカタカナ変換テスト"""
32
- # alkana.get_kanaをモックする
33
- with patch("app.components.audio_generator.alkana.get_kana") as mock_get_kana:
34
- # モックの戻り値を設定
35
- mock_get_kana.side_effect = lambda word: {
36
- "user": "ユーザー",
37
- "friendly": "フレンドリー",
38
- }.get(word, None)
39
-
40
- # ハイフンを含む英単語
41
- result = self.audio_generator._convert_english_to_katakana("user-friendly")
42
- self.assertEqual(result, "ユーザーフレンドリー")
 
43
 
44
  def test_convert_english_to_katakana_with_multiple_hyphens(self):
45
  """複数のハイフンを含む英単語のカタカナ変換テスト"""
46
- # alkana.get_kanaをモックする
47
- with patch("app.components.audio_generator.alkana.get_kana") as mock_get_kana:
48
- # モックの戻り値を設定
49
- mock_get_kana.side_effect = lambda word: {
50
- "deep": "ディープ",
51
- "learning": "ラーニング",
52
- "ai": "エーアイ",
53
- }.get(word, None)
54
-
55
- # 複数のハイフンを含む英単語
56
- result = self.audio_generator._convert_english_to_katakana(
57
- "deep-learning-ai"
58
- )
59
- self.assertEqual(result, "ディープラーニングエーアイ")
60
 
61
  def test_convert_english_to_katakana_with_unknown_parts(self):
62
  """変換できない部分を含む英単語のカタカナ変換テスト"""
63
- # alkana.get_kanaをモックする
64
- with patch("app.components.audio_generator.alkana.get_kana") as mock_get_kana:
65
- # モックの戻り値を設定
66
- mock_get_kana.side_effect = lambda word: {
67
- "user": "ユーザー",
68
- "test": "テスト",
69
- # unknownはNoneを返す(変換できない)
70
- }.get(word, None)
71
-
72
- # 変換できない部分を含む英単語
73
- result = self.audio_generator._convert_english_to_katakana(
74
- "user-unknown-test"
75
- )
76
- self.assertEqual(result, "ユーザーunknownテスト")
 
 
 
 
 
77
 
78
  def test_convert_english_to_katakana_with_consecutive_hyphens(self):
79
  """連続したハイフンを含む英単語のカタカナ変換テスト"""
80
- # alkana.get_kanaをモックする
81
- with patch("app.components.audio_generator.alkana.get_kana") as mock_get_kana:
82
- # モックの戻り値を設定
83
- mock_get_kana.side_effect = lambda word: {
84
- "test": "テスト",
85
- "hello": "ハロー",
86
- }.get(word, None)
87
-
88
- # 連続したハイフンを含む英単語
89
- result = self.audio_generator._convert_english_to_katakana("test--hello")
90
- self.assertEqual(result, "テストハロー")
 
91
 
92
 
93
  if __name__ == "__main__":
 
1
  import unittest
2
+ from unittest.mock import MagicMock, patch
3
 
4
  from app.components.audio_generator import AudioGenerator
5
 
 
9
 
10
  def setUp(self):
11
  """テスト実行前のセットアップ"""
12
+ # VOICEVOXの初期化をモック
13
  with patch("app.components.audio_generator.VOICEVOX_CORE_AVAILABLE", False):
14
+ # e2kのモックを作成
15
+ with patch(
16
+ "app.components.audio_generator.e2k.C2K"
17
+ ) as mock_c2k_class, patch(
18
+ "app.components.audio_generator.e2k.NGram"
19
+ ) as mock_ngram_class:
20
+ # モックインスタンスの作成
21
+ self.mock_c2k = MagicMock()
22
+ self.mock_ngram = MagicMock()
23
+
24
+ # モッククラスがモックインスタンスを返すように設定
25
+ mock_c2k_class.return_value = self.mock_c2k
26
+ mock_ngram_class.return_value = self.mock_ngram
27
+
28
+ # デフォルトのNGram振る舞いを設定
29
+ self.mock_ngram.side_effect = None
30
+ self.mock_ngram.return_value = True # デフォルトで単語は有効と判定
31
+
32
+ self.audio_generator = AudioGenerator()
33
 
34
  def test_convert_english_to_katakana_basic(self):
35
  """基本的な英単語のカタカナ変換テスト"""
36
+ # e2k.C2Kのモック設定
37
+ self.mock_c2k.side_effect = lambda word, *args, **kwargs: {
38
+ "hello": "ヘロー",
39
+ "world": "ワールド",
40
+ }.get(word, None)
41
+
42
+ # NGramモデルは常にTrueを返す設定
43
+ self.mock_ngram.return_value = True
44
+
45
+ # 通常の英単語
46
+ result = self.audio_generator._convert_english_to_katakana("Hello World!")
47
+ self.assertEqual(result, "ヘロー ワールド!")
48
 
49
  def test_convert_english_to_katakana_with_hyphen(self):
50
  """ハイフンを含む英単語のカタカナ変換テスト"""
51
+ # e2k.C2Kのモック設定
52
+ self.mock_c2k.side_effect = lambda word, *args, **kwargs: {
53
+ "user": "ユーザー",
54
+ "friendly": "フレンドリー",
55
+ }.get(word, None)
56
+
57
+ # NGramモデルは常にTrueを返す設定
58
+ self.mock_ngram.return_value = True
59
+
60
+ # ハイフンを含む英単語
61
+ result = self.audio_generator._convert_english_to_katakana("user-friendly")
62
+ self.assertEqual(result, "ユーザーフレンドリー")
63
 
64
  def test_convert_english_to_katakana_with_multiple_hyphens(self):
65
  """複数のハイフンを含む英単語のカタカナ変換テスト"""
66
+ # e2k.C2Kのモック設定
67
+ self.mock_c2k.side_effect = lambda word, *args, **kwargs: {
68
+ "deep": "ディープ",
69
+ "learning": "ラーニング",
70
+ "AI": "AI",
71
+ }.get(word, None)
72
+
73
+ # NGramモデルは常にTrueを返す設定
74
+ self.mock_ngram.return_value = True
75
+
76
+ # 複数のハイフンを含む英単語
77
+ result = self.audio_generator._convert_english_to_katakana("deep-learning-ai")
78
+ self.assertEqual(result, "ディープラーニングAI")
 
79
 
80
  def test_convert_english_to_katakana_with_unknown_parts(self):
81
  """変換できない部分を含む英単語のカタカナ変換テスト"""
82
+ # e2k.C2Kのモック設定
83
+ self.mock_c2k.side_effect = lambda word, *args, **kwargs: {
84
+ "user": "ユーザー",
85
+ "test": "テスト",
86
+ # unknownはNoneを返す(変換できない)
87
+ }.get(word, None)
88
+
89
+ # NGramモデルの設定 - unknownは無効な単語として処理
90
+ def ngram_side_effect(word):
91
+ return word != "unknown"
92
+
93
+ self.mock_ngram.side_effect = ngram_side_effect
94
+
95
+ # as_is関数のモックを設定
96
+ self.mock_ngram.as_is.return_value = "アンノウン"
97
+
98
+ # 変換できない部分を含む英単語
99
+ result = self.audio_generator._convert_english_to_katakana("user-unknown-test")
100
+ self.assertEqual(result, "ユーザーアンノウンテスト")
101
 
102
  def test_convert_english_to_katakana_with_consecutive_hyphens(self):
103
  """連続したハイフンを含む英単語のカタカナ変換テスト"""
104
+ # e2k.C2Kのモック設定
105
+ self.mock_c2k.side_effect = lambda word, *args, **kwargs: {
106
+ "test": "テスト",
107
+ "hello": "ヘロー",
108
+ }.get(word, None)
109
+
110
+ # NGramモデルは常にTrueを返す設定
111
+ self.mock_ngram.return_value = True
112
+
113
+ # 連続したハイフンを含む英単語
114
+ result = self.audio_generator._convert_english_to_katakana("test--hello")
115
+ self.assertEqual(result, "テストヘロー")
116
 
117
 
118
  if __name__ == "__main__":