import re from underthesea import text_normalize # Dictionary to map numbers to Vietnamese words number_to_words = { 0: 'không', 1: 'một', 2: 'hai', 3: 'ba', 4: 'bốn', 5: 'năm', 6: 'sáu', 7: 'bảy', 8: 'tám', 9: 'chín', 10: 'mười', 100: 'trăm', 1000: 'nghìn', 1000000: 'triệu', 1000000000: 'tỷ' } # Dictionary to map Roman numerals to integers roman_to_int = { 'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000 } # Function to convert Roman numerals to integers def roman_to_integer(roman): total = 0 prev_value = 0 for char in reversed(roman): value = roman_to_int.get(char, 0) if value < prev_value: total -= value else: total += value prev_value = value return total currency_symbols ={ '~': '~ ', '%': 'phần trăm', '$': 'đô la', '₫': 'đồng', 'đ': 'đồng', '€': 'ơ rô', '£': 'bảng', '¥': 'yên', '₹': 'ru pi', '₽': 'rúp', '₺': 'li ra', '₩': 'uôn', } def currency_symbol_to_word(currency_sign): if currency_sign in currency_symbols: return currency_symbols[currency_sign] return currency_sign def detect_number_format(number_str): # Check if the number contains a comma and a dot if ',' in number_str and '.' in number_str: # If the last comma is after the last dot, it's Vietnamese if number_str.rfind(',') > number_str.rfind('.'): # Validate Vietnamese format if re.match(r'^\d{1,3}(?:\.\d{3})*(?:,\d+)?$', number_str): return "Vietnamese" else: return "Invalid" # Otherwise, it's US else: # Validate US format if re.match(r'^\d{1,3}(?:,\d{3})*(?:\.\d+)?$', number_str): return "US" else: return "Invalid" # If only commas are present elif ',' in number_str: if re.match(r'^\d{1,3}(?:,\d{3})*(?:\.\d+)?$', number_str): return "US" elif re.match(r'^(\d+,\d+)?$', number_str): return "Vietnamese" else: return "Invalid" # If only dots are present elif '.' in number_str: if re.match(r'^\d{1,3}(?:\.\d{3})*(?:,\d+)?$', number_str): return "Vietnamese" elif re.match(r'^(\d+\.\d+)?$', number_str): return "US" else: return "Invalid" # If no separators are present, assume Vietnamese (default) else: return "Vietnamese" # Function to convert numbers to Vietnamese words def number_to_vietnamese_words(number_str): number_str = str(number_str) if detect_number_format(number_str) == 'Invalid': return number_str if detect_number_format(number_str) == 'US': # convert US number to Vietnamese one: 1,234.5 to 1234,5 number = re.sub(r'\.', ',', re.sub(r',', '', number_str)) else: # remove any dot inside number number = re.sub(r'\.', '', number_str) if isinstance(number, str) and ',' in number: # Handle decimal numbers (e.g., "120,57") integer_part, decimal_part = number.split(',') integer_words = _convert_integer_part(int(integer_part)) decimal_words = _convert_decimal_part(decimal_part) return f"{integer_words} phẩy {decimal_words}" else: # Handle integer numbers return _convert_integer_part(int(number)) # Helper function to convert the integer part of a number def _convert_integer_part(number): if number == 0: return number_to_words[0] words = [] # Handle billions if number >= 1000000000: billion = number // 1000000000 words.append(_convert_integer_part(billion)) words.append(number_to_words[1000000000]) number %= 1000000000 # Handle millions if number >= 1000000: million = number // 1000000 words.append(_convert_integer_part(million)) words.append(number_to_words[1000000]) number %= 1000000 # Handle thousands if number >= 1000: thousand = number // 1000 words.append(_convert_integer_part(thousand)) words.append(number_to_words[1000]) number %= 1000 if number < 100 and number > 0: words.append('không trăm') if number < 10 and number > 0: words.append('không') # Handle hundreds if number >= 100: hundred = number // 100 words.append(number_to_words[hundred]) words.append(number_to_words[100]) number %= 100 if number > 0 and number < 10: words.append('lẻ') # Add "lẻ" for numbers like 106 (một trăm lẻ sáu) # Handle tens and units if number >= 20: ten = number // 10 words.append(number_to_words[ten]) words.append('mươi') number %= 10 elif number >= 10: words.append(number_to_words[10]) number %= 10 # Handle units (1-9) if number > 0: if number == 5 and len(words) > 1 and not words[-1] in['lẻ', 'không']: w = 'lăm' elif number == 1 and len(words) > 1 and not words[-1] in ['lẻ', 'mười', 'không']: w = 'mốt' else: w = number_to_words[number] words.append(w) return ' '.join(words) # Helper function to convert the decimal part of a number def _convert_decimal_part(decimal_part): words = [] for digit in decimal_part: words.append(number_to_words[int(digit)]) return ' '.join(words) # abbreviation replacement abbreviation_map = { "AI": "Ây Ai", "ASEAN": "A Xê An", "ATGT": "An toàn giao thông", "BCA": "Bộ Công an", "BCH": "Ban chấp hành", "BCHTW": "Ban Chấp hành Trung ương", "BCT": "Bộ Chính trị", "BGD": "Bộ Giáo dục", "BKH": "Bộ Khoa học và Công nghệ", "BNN": "Bộ Nông nghiệp", "BQP": "Bộ Quốc phòng", "BTC": "Ban tổ chức", "BTL": "Bộ Tư lệnh", "BYT": "Bộ Y tế", "CA" : "công an", "CAND" : "Công an nhân dân", "CNCS": "chủ nghĩa cộng sản", "CNTB": "chủ nghĩa tư bản", "CNXH": "chủ nghĩa xã hội", "CNY": "nhân dân tệ", "CSGT": "Cảnh sát giao thông", "CTN": "Chủ tịch nước", "ĐBQH": "Đại biểu Quốc hội", "ĐBSCL": "Đồng bằng sông Cửu Long", "ĐCS": "Đảng cộng sản", "ĐH": "Đại học", "ĐHBK": "Đại học Bách khoa", "ĐHKHTN": "Đại học Khoa học tự nhiên", "ĐHQG": "Đại học Quốc gia", "ĐSQ": "Đại sứ quán", "EU": "Ơ u", "GD": "Giáo dục", "HCM": "Hồ Chí Minh", "HĐBA": "Hội đồng bảo an", "HĐND": "Hội đồng nhân dân", "HĐQT": "Hội đồng quản trị", "HN": "Hà Nội", "HV": "Học viện", "KHXH&NV": "Khoa học Xã hội và Nhân văn", "KT": "Kinh tế", "KTQS": "Kỹ thuật Quân sự", "LĐ": "lao động", "KHKT": "khoa học kỹ thuật", "km": "ki lô mét", "LHQ": "Liên Hiệp Quốc", "NATO": "Na tô", "ND": "nhân dân", "NHNN": "ngân hàng nhà nước", "NXB": "Nhà xuất bản", "PCCC": "Phòng cháy chữa cháy", "PTTH": "Phổ thông trung học", "PTCS": "Phổ thông cơ sở", "QĐND" : "Quân đội nhân dân", "QĐNDVN" : "Quân đội nhân dân Việt Nam", "QG": "Quốc gia", "QK": "Quân khu", "sau CN": "sau công nguyên", "SG": "Sài Gòn", "TAND": "Tòa án nhân dân", "TBCN": "tư bản chủ nghĩa", "TBT": "Tổng bí thư", "TCN": "trước công nguyên", "TCT": "Tổng công ty", "THCS": "Trung học cơ sở", "THPT": "Trung học phổ thông", "TNHH": "Trách nhiệm hữu hạn", "TNHH MTV": "Trách nhiệm hữu hạn một thành viên", "TP": "thành phố", "TP.": "thành phố", "TPHCM": "Thành phố Hồ Chí Minh", "TT": "Thủ tướng", "TTCK": "Thị trường chứng khoán", "TTTC": "Thị trường tài chính", "TTCP": "Thủ tướng chính phủ", "TTNT": "Trí tuệ nhân tạo", "TTXVN": "Thông tấn xã Việt Nam", "TƯ": "Trung ương", "TW": "Trung ương", "UB": "Ủy ban", "UBND": "Ủy ban nhân dân", "VH": "Văn hóa", "VKSND": "Viện kiểm sát nhân dân", "VN": "Việt Nam", "VND": "Việt Nam đồng", "XH": "Xã hội", "XHCN": "xã hội chủ nghĩa", "%": "phần trăm", "@": "a còng", "&": "và", } abbreviation_pattern = re.compile(r'\b(' + '|'.join(re.escape(key) for key in abbreviation_map.keys()) + r')\b') def replace_abbreviations(text): def replacement(match): return abbreviation_map[match.group(0)] return abbreviation_pattern.sub(replacement, text) def convert_abbreviations(text): """Converts abbreviations like M.A.S.H. to MASH""" return re.sub(r"([A-Z]\.){2,}", lambda match: "".join(c for c in match.group(0) if c.isalpha()), text) # Function to normalize Vietnamese text def normalize_vietnamese_text(text): text = text_normalize(text) def replace_slash_with_word(text): def replacement(match): word = match.group(1) if word in ['ngày', 'giờ', 'tháng', 'quí', 'quý', 'năm']: return f" mỗi {word}" else: return f" trên {word}" return re.sub(r'/(\w+)', replacement, text) # find and replace "/word" with "per word" text = replace_slash_with_word(text) # Convert standalone currency amounts (e.g., $200, ₫200, €50, £75, ¥1000) def replace_currency(match): currency_sign = match.group(1) amount = match.group(2) return f"{number_to_vietnamese_words(amount)} {currency_symbol_to_word(currency_sign)}" text = re.sub(r'([$₫đ€£¥₹₽₩₺])([\d.,]+)', replace_currency, text) # (reverse case) convert standalone currency amounts (e.g., 200$, 200đ, 50€, 75£, 1000¥) def replace_currency_suffix(match): amount = match.group(1) currency_sign = match.group(2) return f"{number_to_vietnamese_words(amount)} {currency_symbol_to_word(currency_sign)}" text = re.sub(r'([\d.,]+)([$₫đ€£¥₹₽₩₺%])', replace_currency_suffix, text) # in case symbol [¥] is used for Chinese currency and followed by CNY text = text.replace('yên CNY', 'nhân dân tệ') # Replace abbreviations text = convert_abbreviations(text) text = replace_abbreviations(text) # Convert Roman numerals to integers def replace_roman(match): roman_numeral = match.group() return str(roman_to_integer(roman_numeral)) # Replace Roman numerals with integers text = re.sub(r'\b[IVXLCDM]+\b', replace_roman, text) # Convert standalone numbers to words text = re.sub(r'\b[\d.,]+\b', lambda match: number_to_vietnamese_words(match.group()), text) # Fix common grammar errors text = re.sub(r'\s+', ' ', text) # Remove extra spaces text = re.sub(r'\s([,\.])', r'\1', text) # Remove space before punctuation text = re.sub(r'([,\.])(\S)', r'\1 \2', text) # Add space after punctuation text = ( text.replace("..", ".") .replace("!.", "!") .replace("?.", "?") .replace(" .", ".") .replace(" ,", ",") .replace(" (", ", ") .replace(") ", ", ") ) return text.strip()