pnnbao-ump commited on
Commit
ba1319e
·
verified ·
1 Parent(s): b26d891

Upload 9 files

Browse files
.gitattributes CHANGED
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  *.wav filter=lfs diff=lfs merge=lfs -text
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  *.wav filter=lfs diff=lfs merge=lfs -text
37
+ utils/phoneme_dict.json filter=lfs diff=lfs merge=lfs -text
utils/__pycache__/__init__.cpython-312.pyc CHANGED
Binary files a/utils/__pycache__/__init__.cpython-312.pyc and b/utils/__pycache__/__init__.cpython-312.pyc differ
 
utils/__pycache__/core_utils.cpython-312.pyc CHANGED
Binary files a/utils/__pycache__/core_utils.cpython-312.pyc and b/utils/__pycache__/core_utils.cpython-312.pyc differ
 
utils/__pycache__/normalize_text.cpython-312.pyc CHANGED
Binary files a/utils/__pycache__/normalize_text.cpython-312.pyc and b/utils/__pycache__/normalize_text.cpython-312.pyc differ
 
utils/__pycache__/phonemize_text.cpython-312.pyc CHANGED
Binary files a/utils/__pycache__/phonemize_text.cpython-312.pyc and b/utils/__pycache__/phonemize_text.cpython-312.pyc differ
 
utils/core_utils.py CHANGED
@@ -1,12 +1,12 @@
1
  import re
 
2
  from typing import List
3
 
4
  def split_text_into_chunks(text: str, max_chars: int = 256) -> List[str]:
5
  """
6
  Split raw text into chunks no longer than max_chars.
7
- Preference is given to sentence boundaries; otherwise falls back to word-based splitting.
8
  """
9
- sentences = re.split(r"(?<=[\.\!\?\…])\s+", text.strip())
10
  chunks: List[str] = []
11
  buffer = ""
12
 
@@ -45,3 +45,9 @@ def split_text_into_chunks(text: str, max_chars: int = 256) -> List[str]:
45
 
46
  flush_buffer()
47
  return [chunk for chunk in chunks if chunk]
 
 
 
 
 
 
 
1
  import re
2
+ import os
3
  from typing import List
4
 
5
  def split_text_into_chunks(text: str, max_chars: int = 256) -> List[str]:
6
  """
7
  Split raw text into chunks no longer than max_chars.
 
8
  """
9
+ sentences = re.split(r"(?<=[\.\!\?\…\n])\s+|(?<=\n)", text.strip())
10
  chunks: List[str] = []
11
  buffer = ""
12
 
 
45
 
46
  flush_buffer()
47
  return [chunk for chunk in chunks if chunk]
48
+
49
+ def env_bool(name: str, default: bool = False) -> bool:
50
+ v = os.getenv(name)
51
+ if v is None:
52
+ return default
53
+ return v.strip().lower() in ("1", "true", "yes", "y", "on")
utils/normalize_text.py CHANGED
@@ -45,7 +45,18 @@ class VietnameseTTSNormalizer:
45
  'năm', 'sáu', 'bảy', 'tám', 'chín']
46
 
47
  def normalize(self, text):
48
- """Main normalization pipeline."""
 
 
 
 
 
 
 
 
 
 
 
49
  text = text.lower()
50
  text = self._normalize_temperature(text)
51
  text = self._normalize_currency(text)
@@ -58,6 +69,14 @@ class VietnameseTTSNormalizer:
58
  text = self._number_to_words(text)
59
  text = self._normalize_special_chars(text)
60
  text = self._normalize_whitespace(text)
 
 
 
 
 
 
 
 
61
  return text
62
 
63
  def _normalize_temperature(self, text):
@@ -141,9 +160,8 @@ class VietnameseTTSNormalizer:
141
  hour, minute, second = groups
142
  hour_int, minute_int, second_int = int(hour), int(minute), int(second)
143
 
144
- # Validate ranges
145
  if not (0 <= hour_int <= 23):
146
- return match.group(0) # Return original if invalid
147
  if not (0 <= minute_int <= 59):
148
  return match.group(0)
149
  if not (0 <= second_int <= 59):
@@ -156,7 +174,6 @@ class VietnameseTTSNormalizer:
156
  hour, minute = groups
157
  hour_int, minute_int = int(hour), int(minute)
158
 
159
- # Validate ranges
160
  if not (0 <= hour_int <= 23):
161
  return match.group(0)
162
  if not (0 <= minute_int <= 59):
@@ -174,7 +191,6 @@ class VietnameseTTSNormalizer:
174
 
175
  return f"{hour} giờ"
176
 
177
- # Apply patterns with validation
178
  text = re.sub(r'(\d{1,2}):(\d{2}):(\d{2})', validate_and_convert_time, text)
179
  text = re.sub(r'(\d{1,2}):(\d{2})', validate_and_convert_time, text)
180
  text = re.sub(r'(\d{1,2})h(\d{2})', validate_and_convert_time, text)
@@ -189,7 +205,6 @@ class VietnameseTTSNormalizer:
189
  """Check if date components are valid."""
190
  day, month, year = int(day), int(month), int(year)
191
 
192
- # Basic range checks
193
  if not (1 <= day <= 31):
194
  return False
195
  if not (1 <= month <= 12):
@@ -201,7 +216,7 @@ class VietnameseTTSNormalizer:
201
  day, month, year = match.groups()
202
  if is_valid_date(day, month, year):
203
  return f"ngày {day} tháng {month} năm {year}"
204
- return match.group(0) # Return original if invalid
205
 
206
  def date_iso_to_text(match):
207
  year, month, day = match.groups()
@@ -216,7 +231,6 @@ class VietnameseTTSNormalizer:
216
  return f"ngày {day} tháng {month} năm {full_year}"
217
  return match.group(0)
218
 
219
- # Apply patterns with validation
220
  text = re.sub(r'\bngày\s+(\d{1,2})[/\-](\d{1,2})[/\-](\d{4})\b',
221
  lambda m: date_to_text(m).replace('ngày ngày', 'ngày'), text)
222
  text = re.sub(r'\bngày\s+(\d{1,2})[/\-](\d{1,2})[/\-](\d{2})\b',
@@ -248,10 +262,8 @@ class VietnameseTTSNormalizer:
248
 
249
  def _normalize_numbers(self, text):
250
  text = re.sub(r'(\d+(?:[,.]\d+)?)%', lambda m: f'{m.group(1)} phần trăm', text)
251
- # 1. Xóa dấu thousand separator trước
252
  text = re.sub(r'(\d{1,3})(?:\.(\d{3}))+', lambda m: m.group(0).replace('.', ''), text)
253
 
254
- # 2. Chuyển số thập phân thành chữ
255
  def decimal_to_words(match):
256
  whole = match.group(1)
257
  decimal = match.group(2)
@@ -259,9 +271,7 @@ class VietnameseTTSNormalizer:
259
  separator = 'phẩy' if ',' in match.group(0) else 'chấm'
260
  return f"{whole} {separator} {decimal_words}"
261
 
262
- # 2a. Dấu phẩy
263
  text = re.sub(r'(\d+),(\d+)', decimal_to_words, text)
264
- # 2b. Dấu chấm (1-2 chữ số thập phân)
265
  text = re.sub(r'(\d+)\.(\d{1,2})\b', decimal_to_words, text)
266
 
267
  return text
@@ -335,7 +345,9 @@ class VietnameseTTSNormalizer:
335
  remainder = num % 1000
336
  result = f"{self._read_three_digits(thousand)} nghìn"
337
  if remainder > 0:
338
- if remainder < 100:
 
 
339
  result += f" không trăm {self._read_two_digits(remainder)}"
340
  else:
341
  result += f" {self._read_three_digits(remainder)}"
@@ -363,7 +375,7 @@ class VietnameseTTSNormalizer:
363
  text = re.sub(r'\s+[-–—]+\s+', ' ', text)
364
  text = re.sub(r'\.{2,}', ' ', text)
365
  text = re.sub(r'\s+\.\s+', ' ', text)
366
- text = re.sub(r'[^\w\sàáảãạăắằẳẵặâấầẩẫậèéẻẽẹêếềểễệìíỉĩịòóỏõọôốồổỗộơớờởỡợùúủũụưứừửữựỳýỷỹỵđ.,!?;:@%]', ' ', text)
367
  return text
368
 
369
  def _normalize_whitespace(self, text):
@@ -377,32 +389,19 @@ if __name__ == "__main__":
377
  normalizer = VietnameseTTSNormalizer()
378
 
379
  test_texts = [
380
- "Giá 2.500.000đ (giảm 50%), mua trước 14h30 ngày 15/12/2025",
381
- "Liên hệ: 0912-345-678 hoặc email@example.com",
382
- "Tốc độ 120km/h, trọng lượng 75kg",
383
- "Nhiệt độ 36,5°C, độ ẩm 80%",
384
- "Số pi = 3,14159",
385
- "Giá trị tăng 2.5M, đạt 10B",
386
- "Nhiệt độ -15°C vào mùa đông",
387
- "Điện áp 220V, công suất 2.5kW, tần số 50Hz",
388
- "Tôi đi lấy l nước về nhà",
389
- "Cần 5l nước cho công thức này",
390
- "Vận tốc ánh sáng 299792km/s",
391
- "Mật độ dân số 450 người/km2",
392
- "Công suất 100 W/m2",
393
- "Hôm nay 2025-01-15",
394
- "Gọi +84 912 345 678",
395
- "Nhiệt độ 25°C lúc 14:30:45",
396
- "Ngày 15/12/25",
397
- "Giá 3.140.159",
398
  ]
399
 
400
  print("=" * 80)
401
- print("VIETNAMESE TTS NORMALIZATION TEST")
402
  print("=" * 80)
403
 
404
  for text in test_texts:
405
  print(f"\n📝 Input: {text}")
406
  normalized = normalizer.normalize(text)
407
  print(f"🎵 Output: {normalized}")
408
- print("-" * 80)
 
45
  'năm', 'sáu', 'bảy', 'tám', 'chín']
46
 
47
  def normalize(self, text):
48
+ """Main normalization pipeline with EN tag protection."""
49
+ # Step 1: Extract and protect EN tags
50
+ en_contents = []
51
+ placeholder_pattern = "___EN_PLACEHOLDER_{}___ "
52
+
53
+ def extract_en(match):
54
+ en_contents.append(match.group(0))
55
+ return placeholder_pattern.format(len(en_contents) - 1)
56
+
57
+ text = re.sub(r'<en>.*?</en>', extract_en, text, flags=re.IGNORECASE)
58
+
59
+ # Step 2: Normal normalization pipeline
60
  text = text.lower()
61
  text = self._normalize_temperature(text)
62
  text = self._normalize_currency(text)
 
69
  text = self._number_to_words(text)
70
  text = self._normalize_special_chars(text)
71
  text = self._normalize_whitespace(text)
72
+
73
+ # Step 3: Restore EN tags
74
+ for idx, en_content in enumerate(en_contents):
75
+ text = text.replace(placeholder_pattern.format(idx).lower(), en_content + ' ')
76
+
77
+ # Final whitespace cleanup
78
+ text = self._normalize_whitespace(text)
79
+
80
  return text
81
 
82
  def _normalize_temperature(self, text):
 
160
  hour, minute, second = groups
161
  hour_int, minute_int, second_int = int(hour), int(minute), int(second)
162
 
 
163
  if not (0 <= hour_int <= 23):
164
+ return match.group(0)
165
  if not (0 <= minute_int <= 59):
166
  return match.group(0)
167
  if not (0 <= second_int <= 59):
 
174
  hour, minute = groups
175
  hour_int, minute_int = int(hour), int(minute)
176
 
 
177
  if not (0 <= hour_int <= 23):
178
  return match.group(0)
179
  if not (0 <= minute_int <= 59):
 
191
 
192
  return f"{hour} giờ"
193
 
 
194
  text = re.sub(r'(\d{1,2}):(\d{2}):(\d{2})', validate_and_convert_time, text)
195
  text = re.sub(r'(\d{1,2}):(\d{2})', validate_and_convert_time, text)
196
  text = re.sub(r'(\d{1,2})h(\d{2})', validate_and_convert_time, text)
 
205
  """Check if date components are valid."""
206
  day, month, year = int(day), int(month), int(year)
207
 
 
208
  if not (1 <= day <= 31):
209
  return False
210
  if not (1 <= month <= 12):
 
216
  day, month, year = match.groups()
217
  if is_valid_date(day, month, year):
218
  return f"ngày {day} tháng {month} năm {year}"
219
+ return match.group(0)
220
 
221
  def date_iso_to_text(match):
222
  year, month, day = match.groups()
 
231
  return f"ngày {day} tháng {month} năm {full_year}"
232
  return match.group(0)
233
 
 
234
  text = re.sub(r'\bngày\s+(\d{1,2})[/\-](\d{1,2})[/\-](\d{4})\b',
235
  lambda m: date_to_text(m).replace('ngày ngày', 'ngày'), text)
236
  text = re.sub(r'\bngày\s+(\d{1,2})[/\-](\d{1,2})[/\-](\d{2})\b',
 
262
 
263
  def _normalize_numbers(self, text):
264
  text = re.sub(r'(\d+(?:[,.]\d+)?)%', lambda m: f'{m.group(1)} phần trăm', text)
 
265
  text = re.sub(r'(\d{1,3})(?:\.(\d{3}))+', lambda m: m.group(0).replace('.', ''), text)
266
 
 
267
  def decimal_to_words(match):
268
  whole = match.group(1)
269
  decimal = match.group(2)
 
271
  separator = 'phẩy' if ',' in match.group(0) else 'chấm'
272
  return f"{whole} {separator} {decimal_words}"
273
 
 
274
  text = re.sub(r'(\d+),(\d+)', decimal_to_words, text)
 
275
  text = re.sub(r'(\d+)\.(\d{1,2})\b', decimal_to_words, text)
276
 
277
  return text
 
345
  remainder = num % 1000
346
  result = f"{self._read_three_digits(thousand)} nghìn"
347
  if remainder > 0:
348
+ if remainder < 10:
349
+ result += f" không trăm lẻ {self.digits[remainder]}"
350
+ elif remainder < 100:
351
  result += f" không trăm {self._read_two_digits(remainder)}"
352
  else:
353
  result += f" {self._read_three_digits(remainder)}"
 
375
  text = re.sub(r'\s+[-–—]+\s+', ' ', text)
376
  text = re.sub(r'\.{2,}', ' ', text)
377
  text = re.sub(r'\s+\.\s+', ' ', text)
378
+ text = re.sub(r'[^\w\sàáảãạăắằẳẵặâấầẩẫậèéẻẽẹêếềểễệìíỉĩịòóỏõọôốồổỗộơớờởỡợùúủũụưứừửữựỳýỷỹỵđ.,!?;:@%_]', ' ', text)
379
  return text
380
 
381
  def _normalize_whitespace(self, text):
 
389
  normalizer = VietnameseTTSNormalizer()
390
 
391
  test_texts = [
392
+ "Chào mừng <en>hello world</en> đến với AI",
393
+ "Công nghệ <en>machine learning</en> và <en>deep learning</en>",
394
+ "Giá 2.500.000đ với <en>discount</en> 50%",
395
+ "Nhiệt độ 25°C, <en>temperature</en> cao",
396
+ "Hệ thống <en>text-to-speech</en> tiếng Việt",
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  ]
398
 
399
  print("=" * 80)
400
+ print("VIETNAMESE TTS NORMALIZATION TEST (WITH EN TAG)")
401
  print("=" * 80)
402
 
403
  for text in test_texts:
404
  print(f"\n📝 Input: {text}")
405
  normalized = normalizer.normalize(text)
406
  print(f"🎵 Output: {normalized}")
407
+ print("-" * 80)
utils/phoneme_dict.json CHANGED
The diff for this file is too large to render. See raw diff
 
utils/phonemize_text.py CHANGED
@@ -2,6 +2,7 @@ import os
2
  import json
3
  import platform
4
  import glob
 
5
  from phonemizer import phonemize
6
  from phonemizer.backend.espeak.espeak import EspeakWrapper
7
  from utils.normalize_text import VietnameseTTSNormalizer
@@ -106,7 +107,10 @@ except Exception as e:
106
  raise
107
 
108
  def phonemize_text(text: str) -> str:
109
- """Convert text to phonemes using phonemizer."""
 
 
 
110
  text = normalizer.normalize(text)
111
  return phonemize(
112
  text,
@@ -117,34 +121,226 @@ def phonemize_text(text: str) -> str:
117
  language_switch="remove-flags"
118
  )
119
 
 
120
  def phonemize_with_dict(text: str, phoneme_dict=phoneme_dict) -> str:
121
- """Phonemize text with dictionary lookup."""
 
 
122
  text = normalizer.normalize(text)
123
- words = text.split()
124
- result = []
125
 
126
- for word in words:
127
- if word in phoneme_dict:
128
- phone_word = phoneme_dict[word]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  else:
130
- try:
131
- phone_word = phonemize(
132
- word,
133
- language='vi',
134
- backend='espeak',
135
- preserve_punctuation=True,
136
- with_stress=True,
137
- language_switch='remove-flags'
138
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
- if word.lower().startswith('r'):
141
- phone_word = 'ɹ' + phone_word[1:]
142
 
143
- phoneme_dict[word] = phone_word
144
- except Exception as e:
145
- print(f"Warning: Could not phonemize '{word}': {e}")
146
- phone_word = word
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
- result.append(phone_word)
 
 
149
 
150
- return ' '.join(result)
 
2
  import json
3
  import platform
4
  import glob
5
+ import re
6
  from phonemizer import phonemize
7
  from phonemizer.backend.espeak.espeak import EspeakWrapper
8
  from utils.normalize_text import VietnameseTTSNormalizer
 
107
  raise
108
 
109
  def phonemize_text(text: str) -> str:
110
+ """
111
+ Convert text to phonemes (simple version without dict, without EN tag).
112
+ Kept for backward compatibility.
113
+ """
114
  text = normalizer.normalize(text)
115
  return phonemize(
116
  text,
 
121
  language_switch="remove-flags"
122
  )
123
 
124
+
125
  def phonemize_with_dict(text: str, phoneme_dict=phoneme_dict) -> str:
126
+ """
127
+ Phonemize single text with dictionary lookup and EN tag support.
128
+ """
129
  text = normalizer.normalize(text)
 
 
130
 
131
+ # Split by EN tags
132
+ parts = re.split(r'(<en>.*?</en>)', text, flags=re.IGNORECASE)
133
+
134
+ en_texts = []
135
+ en_indices = []
136
+ vi_texts = []
137
+ vi_indices = []
138
+ vi_word_maps = []
139
+
140
+ processed_parts = []
141
+
142
+ for part_idx, part in enumerate(parts):
143
+ if re.match(r'<en>.*</en>', part, re.IGNORECASE):
144
+ # English part
145
+ en_content = re.sub(r'</?en>', '', part, flags=re.IGNORECASE).strip()
146
+ en_texts.append(en_content)
147
+ en_indices.append(part_idx)
148
+ processed_parts.append(None)
149
  else:
150
+ # Vietnamese part
151
+ words = part.split()
152
+ processed_words = []
153
+
154
+ for word_idx, word in enumerate(words):
155
+ match = re.match(r'^(\W*)(.*?)(\W*)$', word)
156
+ pre, core, suf = match.groups() if match else ("", word, "")
157
+
158
+ if not core:
159
+ processed_words.append(word)
160
+ elif core in phoneme_dict:
161
+ processed_words.append(f"{pre}{phoneme_dict[core]}{suf}")
162
+ else:
163
+ vi_texts.append(word)
164
+ vi_indices.append(part_idx)
165
+ vi_word_maps.append((part_idx, len(processed_words)))
166
+ processed_words.append(None)
167
+
168
+ processed_parts.append(processed_words)
169
+
170
+ if en_texts:
171
+ try:
172
+ en_phonemes = phonemize(
173
+ en_texts,
174
+ language='en-us',
175
+ backend='espeak',
176
+ preserve_punctuation=True,
177
+ with_stress=True,
178
+ language_switch="remove-flags"
179
+ )
180
+
181
+ if isinstance(en_phonemes, str):
182
+ en_phonemes = [en_phonemes]
183
+
184
+ for idx, (part_idx, phoneme) in enumerate(zip(en_indices, en_phonemes)):
185
+ processed_parts[part_idx] = phoneme.strip()
186
+ except Exception as e:
187
+ print(f"Warning: Could not phonemize EN texts: {e}")
188
+ for part_idx in en_indices:
189
+ processed_parts[part_idx] = en_texts[en_indices.index(part_idx)]
190
+
191
+ if vi_texts:
192
+ try:
193
+ vi_phonemes = phonemize(
194
+ vi_texts,
195
+ language='vi',
196
+ backend='espeak',
197
+ preserve_punctuation=True,
198
+ with_stress=True,
199
+ language_switch='remove-flags'
200
+ )
201
+
202
+ if isinstance(vi_phonemes, str):
203
+ vi_phonemes = [vi_phonemes]
204
+
205
+ for idx, (part_idx, word_idx) in enumerate(vi_word_maps):
206
+ phoneme = vi_phonemes[idx].strip()
207
+
208
+ original_word = vi_texts[idx]
209
+ if original_word.lower().startswith('r'):
210
+ phoneme = 'ɹ' + phoneme[1:] if len(phoneme) > 0 else phoneme
211
 
212
+ phoneme_dict[original_word] = phoneme
 
213
 
214
+ if processed_parts[part_idx] is not None:
215
+ processed_parts[part_idx][word_idx] = phoneme
216
+ except Exception as e:
217
+ print(f"Warning: Could not phonemize VI texts: {e}")
218
+ for idx, (part_idx, word_idx) in enumerate(vi_word_maps):
219
+ if processed_parts[part_idx] is not None:
220
+ processed_parts[part_idx][word_idx] = vi_texts[idx]
221
+
222
+ final_parts = []
223
+ for part in processed_parts:
224
+ if isinstance(part, list):
225
+ final_parts.append(' '.join(str(w) for w in part if w is not None))
226
+ elif part is not None:
227
+ final_parts.append(part)
228
+
229
+ result = ' '.join(final_parts)
230
+
231
+ result = re.sub(r'\s+([.,!?;:])', r'\1', result)
232
+
233
+ return result
234
+
235
+
236
+ def phonemize_batch(texts: list, phoneme_dict=phoneme_dict) -> list:
237
+ """
238
+ Phonemize multiple texts with optimal batching.
239
+
240
+ Args:
241
+ texts: List of text strings to phonemize
242
+ phoneme_dict: Phoneme dictionary for lookup
243
+
244
+ Returns:
245
+ List of phonemized texts
246
+ """
247
+ normalized_texts = [normalizer.normalize(text) for text in texts]
248
+
249
+ all_en_texts = []
250
+ all_en_maps = []
251
+
252
+ all_vi_texts = []
253
+ all_vi_maps = []
254
+
255
+ results = []
256
+
257
+ for text_idx, text in enumerate(normalized_texts):
258
+ parts = re.split(r'(<en>.*?</en>)', text, flags=re.IGNORECASE)
259
+ processed_parts = []
260
+
261
+ for part_idx, part in enumerate(parts):
262
+ if re.match(r'<en>.*</en>', part, re.IGNORECASE):
263
+ en_content = re.sub(r'</?en>', '', part, flags=re.IGNORECASE).strip()
264
+ all_en_texts.append(en_content)
265
+ all_en_maps.append((text_idx, part_idx))
266
+ processed_parts.append(None)
267
+ else:
268
+ words = part.split()
269
+ processed_words = []
270
+
271
+ for word in words:
272
+ match = re.match(r'^(\W*)(.*?)(\W*)$', word)
273
+ pre, core, suf = match.groups() if match else ("", word, "")
274
+
275
+ if not core:
276
+ processed_words.append(word)
277
+ elif core in phoneme_dict:
278
+ processed_words.append(f"{pre}{phoneme_dict[core]}{suf}")
279
+ else:
280
+ all_vi_texts.append(word)
281
+ all_vi_maps.append((text_idx, part_idx, len(processed_words)))
282
+ processed_words.append(None)
283
+
284
+ processed_parts.append(processed_words)
285
+
286
+ results.append(processed_parts)
287
+
288
+ if all_en_texts:
289
+ try:
290
+ en_phonemes = phonemize(
291
+ all_en_texts,
292
+ language='en-us',
293
+ backend='espeak',
294
+ preserve_punctuation=True,
295
+ with_stress=True,
296
+ language_switch="remove-flags"
297
+ )
298
+
299
+ if isinstance(en_phonemes, str):
300
+ en_phonemes = [en_phonemes]
301
+
302
+ for (text_idx, part_idx), phoneme in zip(all_en_maps, en_phonemes):
303
+ results[text_idx][part_idx] = phoneme.strip()
304
+ except Exception as e:
305
+ print(f"Warning: Batch EN phonemization failed: {e}")
306
+
307
+ if all_vi_texts:
308
+ try:
309
+ vi_phonemes = phonemize(
310
+ all_vi_texts,
311
+ language='vi',
312
+ backend='espeak',
313
+ preserve_punctuation=True,
314
+ with_stress=True,
315
+ language_switch='remove-flags'
316
+ )
317
+
318
+ if isinstance(vi_phonemes, str):
319
+ vi_phonemes = [vi_phonemes]
320
+
321
+ for idx, (text_idx, part_idx, word_idx) in enumerate(all_vi_maps):
322
+ phoneme = vi_phonemes[idx].strip()
323
+
324
+ original_word = all_vi_texts[idx]
325
+ if original_word.lower().startswith('r'):
326
+ phoneme = 'ɹ' + phoneme[1:] if len(phoneme) > 0 else phoneme
327
+
328
+ phoneme_dict[original_word] = phoneme
329
+ results[text_idx][part_idx][word_idx] = phoneme
330
+ except Exception as e:
331
+ print(f"Warning: Batch VI phonemization failed: {e}")
332
+
333
+ final_results = []
334
+ for processed_parts in results:
335
+ final_parts = []
336
+ for part in processed_parts:
337
+ if isinstance(part, list):
338
+ final_parts.append(' '.join(str(w) for w in part if w is not None))
339
+ elif part is not None:
340
+ final_parts.append(part)
341
 
342
+ result = ' '.join(final_parts)
343
+ result = re.sub(r'\s+([.,!?;:])', r'\1', result)
344
+ final_results.append(result)
345
 
346
+ return final_results