matcha-tts / text_processor.py
Cong123779's picture
feat: deploy standalone Matcha-TTS 22kHz ONNX Web App & API
2c1ed68 verified
Raw History Blame Contribute Delete
11.4 kB
"""
Module Tiền Xử Lý Văn Bản Tự Độc Lập Cho Matcha-TTS Tiếng Việt
============================================================
Bao gồm:
1. Master Vietnamese Normalizer (số đếm, tiền tệ, ngày tháng, biển số xe, cccd...)
2. Foreign Symbols & Acronyms Translator (AI, RAM, CPU, WiFi, @, #, $, %...)
3. Text cleaner & Symbols to Sequence converter
"""
import os
import re
import unicodedata
from typing import List, Tuple
# Mapping of symbols
_pad = "_"
_punctuation = ';:,.!?¡¿—…"«»“” '
_letters = "aáàảãạăắằẳẵặâấầẩẫậeéèẻẽẹêếềểễệiíìỉĩịoóòỏõọôốồổỗộơớờởỡợuúùủũụưứừửữựyýỳỷỹỵbcdđghklmnpqrstvwxz"
_letters_ipa = "ɑɐɒæɓʙβɔɕçɗɖðʤəɘɚɛɜɝɞɟʄɡɠɢʛɦɧħɥʜɨɪʝɭɬɫɮʟɱɯɰŋɳɲɴøœɶʘɸɹɺɾɻʀʁɽʂʃʈʧʉʊʋⱱʌɣɤʍχʎʏʑʐʒʔʡʕʢǀǁǂǃˈˌːˑʼʴʰʱʲʷˠˤ˞↓↑→↗↘'̩'ᵻ"
symbols = [_pad] + list(_punctuation) + list(_letters) + list(_letters_ipa)
_symbol_to_id = {s: i for i, s in enumerate(symbols)}
_id_to_symbol = {i: s for i, s in enumerate(symbols)}
UNITS = ["không", "một", "hai", "ba", "bốn", "năm", "sáu", "bảy", "tám", "chín"]
FOREIGN_TERMS = {
"AI": "ây ai",
"A.I": "ây ai",
"CPU": "sê pê u",
"GPU": "gờ pê u",
"RAM": "ram",
"ROM": "rom",
"SSD": "ét ét đê",
"HDD": "hát đê đê",
"USB": "u ét bê",
"WIFI": "oai phai",
"WI-FI": "oai phai",
"API": "a pê i",
"SDK": "ét đê ca",
"APP": "áp",
"APPS": "áp",
"TTS": "tê tê ét",
"ASR": "a ét rờ",
"BOT": "bót",
"CHATBOT": "chát bót",
"GPT": "gờ pê tê",
"CHATGPT": "chát gờ pê tê",
"CEO": "sê e o",
"CTO": "sê tê o",
"IT": "ai ti",
"DEV": "đép",
"TESTER": "tét tơ",
"BUG": "bấc",
"OK": "ô kê",
"O.K": "ô kê",
"YES": "dét",
"NO": "nô",
"BYE": "bai",
"HELLO": "hê lâu",
"HI": "hai",
"FAN": "phan",
"FANS": "phan",
"LIKE": "lai",
"SHARE": "se",
"COMMENT": "còm men",
"VIEW": "viu",
"VIEWS": "viu",
"SUB": "sắp",
"STREAM": "sờ trim",
"LIVESTREAM": "lai sờ trim",
"CLIP": "clíp",
"VIDEO": "vi đê ô",
"AUDIO": "ao đi ô",
"PODCAST": "pót cát",
"HOT": "hót",
"TOP": "tóp",
"PRO": "prô",
"VIP": "víp",
"LINK": "linh",
"WEB": "oép",
"WEBSITE": "oép sai",
"ONLINE": "on lai",
"OFFLINE": "ọp lai",
"GAME": "gêm",
"GAMER": "gê mơ",
"SHOW": "sô",
"MC": "em xi",
"PR": "pê rờ",
"KOL": "cây o eo",
"ID": "ai đi",
"PASS": "pát",
"PASSWORD": "pát uốt",
"LOGIN": "lốc in",
"LOGOUT": "lốc ao",
"CHECKIN": "chếch in",
"CHECKOUT": "chếch ao",
"SALE": "sêu",
"OFF": "ọp",
"DISCOUNT": "đít scao",
"VOUCHER": "vau chờ",
"SHIP": "síp",
"SHIPPER": "síp pơ",
"COD": "cót",
"SHOP": "sóp",
"STORE": "sờ to",
"BILL": "biu",
"TEAM": "tim",
"GROUP": "gờ rúp",
"CLUB": "cơ lặp",
"STAFF": "sờ táp",
"BOSS": "bót",
"LEADER": "lít đơ",
"MANAGER": "ma na gơ",
"DEAL": "điu",
"TEST": "tét",
"DEMO": "đê mô",
"UPDATE": "ắp đết",
"UPGRADE": "ắp gờ rết",
"FIX": "phích",
"SET": "sét",
"SETUP": "sét ắp",
"RESET": "ri sét",
"ERROR": "e rơ",
"CODE": "cốt",
"DATA": "đa ta",
"SERVER": "sơ vơ",
"CLOUD": "cờ lao",
"SMS": "ét em ét",
"EMAIL": "i meo",
"MAIL": "meo",
"FB": "phây búc",
"FACEBOOK": "phây búc",
"YOUTUBE": "du túp",
"TIKTOK": "tích tốc",
"ZALO": "da lô",
"INSTA": "in sta",
"INSTAGRAM": "in sta gram",
"HOTLINE": "hót lai",
"SMARTPHONE": "sờ mát phôn",
"IPHONE": "ai phôn",
"IPAD": "ai pát",
"LAPTOP": "láp tóp",
"PC": "pê sê"
}
LATIN_LETTERS = {
'A': 'a', 'B': 'bê', 'C': 'xê', 'D': 'đê', 'E': 'e',
'F': 'ép', 'G': 'gờ', 'H': 'hát', 'I': 'i', 'J': 'giây',
'K': 'ca', 'L': 'e-lờ', 'M': 'em', 'N': 'en', 'O': 'o',
'P': 'pê', 'Q': 'quy', 'R': 'e-rờ', 'S': 'ét', 'T': 'tê',
'U': 'u', 'V': 'vê', 'W': 'vê kép', 'X': 'ích', 'Y': 'i dài', 'Z': 'dét'
}
UNITS_DICT = {
r'(\d+)\s*(?:km/h|kmh)': r'\1 ki-lô-mét trên giờ',
r'(\d+)\s*(?:m/s|ms)': r'\1 mét trên giây',
r'(\d+)\s*km\b': r'\1 ki-lô-mét',
r'(\d+)\s*m\b': r'\1 mét',
r'(\d+)\s*cm\b': r'\1 xen-ti-mét',
r'(\d+)\s*mm\b': r'\1 mi-li-mét',
r'(\d+)\s*(?:kg|kilo|kí)\b': r'\1 ki-lô-gam',
r'(\d+)\s*g\b': r'\1 gam',
r'(\d+)\s*l\b': r'\1 lít',
r'(\d+)\s*ml\b': r'\1 mi-li-lít',
r'(\d+)\s*°C\b': r'\1 độ xê',
r'(\d+)\s*°F\b': r'\1 độ ép',
r'(\d+)\s*%\b': r'\1 phần trăm',
r'(\d+)\s*(?:usd|\$)\b': r'\1 đô la',
r'(\d+)\s*(?:vnd|vnđ|đ)\b': r'\1 đồng',
r'(\d+)\s*(?:gb|gigabyte)\b': r'\1 ghi-ga-bai',
r'(\d+)\s*(?:mb|megabyte)\b': r'\1 mê-ga-bai',
r'(\d+)\s*(?:kb|kilobyte)\b': r'\1 ki-lô-bai',
r'(\d+)\s*hz\b': r'\1 héc',
r'(\d+)\s*khz\b': r'\1 ki-lô-héc',
r'(\d+)\s*mhz\b': r'\1 mê-ga-héc',
r'(\d+)\s*ghz\b': r'\1 ghi-ga-héc'
}
def read_integer(n: int) -> str:
if n < 0:
return f"âm {read_integer(abs(n))}"
if n < 10:
return UNITS[n]
if n < 100:
ten, unit = n // 10, n % 10
ten_str = "mười" if ten == 1 else f"{UNITS[ten]} mươi"
if unit == 0:
return ten_str
elif unit == 1:
return f"{ten_str} một" if ten == 1 else f"{ten_str} mốt"
elif unit == 4:
return f"{ten_str} bốn" if ten == 1 else f"{ten_str} tư"
elif unit == 5:
return f"{ten_str} lăm"
else:
return f"{ten_str} {UNITS[unit]}"
if n < 1000:
hundred, rem = n // 100, n % 100
hundred_str = f"{UNITS[hundred]} trăm"
if rem == 0:
return hundred_str
elif rem < 10:
return f"{hundred_str} lẻ {UNITS[rem]}"
else:
return f"{hundred_str} {read_integer(rem)}"
if n < 1000000:
thous, rem = n // 1000, n % 1000
thous_str = f"{read_integer(thous)} ngàn"
if rem == 0:
return thous_str
elif rem < 10:
return f"{thous_str} không trăm lẻ {UNITS[rem]}"
elif rem < 100:
return f"{thous_str} không trăm {read_integer(rem)}"
else:
return f"{thous_str} {read_integer(rem)}"
if n < 1000000000:
mil, rem = n // 1000000, n % 1000000
mil_str = f"{read_integer(mil)} triệu"
if rem == 0:
return mil_str
elif rem < 1000:
return f"{mil_str} không trăm ngàn {read_integer(rem)}"
else:
return f"{mil_str} {read_integer(rem)}"
bil, rem = n // 1000000000, n % 1000000000
bil_str = f"{read_integer(bil)} tỷ"
if rem == 0:
return bil_str
return f"{bil_str} {read_integer(rem)}"
def read_digits_individually(s: str) -> str:
return " ".join(UNITS[int(d)] for d in s if d.isdigit())
def read_decimal(num_str: str) -> str:
parts = re.split(r'[,.]', num_str)
if len(parts) == 2:
integer_part = read_integer(int(parts[0]))
decimal_digits = read_digits_individually(parts[1])
return f"{integer_part} phẩy {decimal_digits}"
return num_str
def translate_foreign_symbols_and_words(text: str) -> str:
# Math & Special characters
char_map = {
"/": " xuyệt ", "\\": " xuyệt ngược ", "_": " gạch dưới ",
"@": " a còng ", "#": " thăng ", "$": " đô la ",
"%": " phần trăm ", "^": " mũ ", "&": " và ",
"*": " nhân ", "+": " cộng ", "=": " bằng ",
"<": " nhỏ hơn ", ">": " lớn hơn ", "|": " hoặc ", "~": " khoảng "
}
for char, replacement in char_map.items():
text = text.replace(char, replacement)
# Foreign vocabulary replacement
for term, trans in sorted(FOREIGN_TERMS.items(), key=lambda x: len(x[0]), reverse=True):
pattern = r'\b' + re.escape(term) + r'\b'
text = re.sub(pattern, trans, text, flags=re.IGNORECASE)
# Latin isolated letters
def replace_letter(match):
char = match.group(0).upper()
return LATIN_LETTERS.get(char, match.group(0))
text = re.sub(r'(?<!\w)[A-Za-z](?!\w)', replace_letter, text)
return text
def normalize_vietnamese_text(text: str) -> str:
if not text:
return ""
text = unicodedata.normalize('NFC', text)
text = text.replace('\r\n', '\n').replace('\r', '\n')
# Date format DD/MM/YYYY
def date_repl(match):
d, m, y = match.group(1), match.group(2), match.group(3)
return f"ngày {read_integer(int(d))} tháng {read_integer(int(m))} năm {read_integer(int(y))}"
text = re.sub(r'\b(\d{1,2})[/.-](\d{1,2})[/.-](\d{4})\b', date_repl, text)
# Time format HH:MM
def time_repl(match):
h, m = match.group(1), match.group(2)
m_int = int(m)
if m_int == 0:
return f"{read_integer(int(h))} giờ"
return f"{read_integer(int(h))} giờ {read_integer(m_int)} phút"
text = re.sub(r'\b(\d{1,2})[h:](\d{2})\b', time_repl, text)
# Phone numbers
def phone_repl(match):
digits = match.group(0).replace(" ", "").replace(".", "").replace("-", "")
return read_digits_individually(digits)
text = re.sub(r'\b(?:0|\+84)(?:[.\s-]?\d){8,10}\b', phone_repl, text)
# Units
for pattern, repl in UNITS_DICT.items():
def unit_sub(m):
num = read_integer(int(m.group(1)))
return re.sub(r'\b' + m.group(1) + r'\b', num, repl.replace(r'\1', num))
text = re.sub(pattern, unit_sub, text, flags=re.IGNORECASE)
# Currency format with dots: 1.250.000
def dot_num_repl(match):
val = int(match.group(0).replace('.', '').replace(',', ''))
return read_integer(val)
text = re.sub(r'\b\d{1,3}(?:\.\d{3})+\b', dot_num_repl, text)
# Decimals: 3.14 or 3,14
def dec_repl(match):
return read_decimal(match.group(0))
text = re.sub(r'\b\d+[,.]\d+\b', dec_repl, text)
# Regular numbers
def int_repl(match):
val = int(match.group(0))
if len(match.group(0)) >= 6:
return read_digits_individually(match.group(0))
return read_integer(val)
text = re.sub(r'\b\d+\b', int_repl, text)
# Foreign terms & special characters
text = translate_foreign_symbols_and_words(text)
# Final cleanup
text = re.sub(r'[«»“”„“”"\x27`\(\)\[\]\{\}]', '', text)
text = re.sub(r'[ \t]+', ' ', text).strip()
return text
def intersperse(lst: List[int], item: int) -> List[int]:
result = [item] * (len(lst) * 2 + 1)
result[1::2] = lst
return result
def text_to_sequence(text: str) -> Tuple[List[int], str]:
cleaned = normalize_vietnamese_text(text).lower()
cleaned = cleaned.replace("–", "-").replace("—", "-").replace("…", "...")
sequence = []
for symbol in cleaned:
if symbol in _symbol_to_id:
sequence.append(_symbol_to_id[symbol])
return sequence, cleaned