""" 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'(? 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