/** * Vietnamese text processing for both display and TTS. * * DESIGN: * - Display text (chat & subtitles): numbers stay as digits for clean visual display * - TTS audio: numbers are converted to Vietnamese words for proper pronunciation * - Source links in greetings: rendered as clickable popup links */ // Vietnamese number words const DIGITS = [ "không", "một", "hai", "ba", "bốn", "năm", "sáu", "bảy", "tám", "chín", ]; function numberToVietnamese(n) { if (n === 0) return "không"; if (n < 0) return "âm " + numberToVietnamese(-n); const units = ["", "nghìn", "triệu", "tỷ"]; const groups = []; let temp = n; while (temp > 0) { groups.push(temp % 1000); temp = Math.floor(temp / 1000); } if (groups.length === 0) groups.push(0); const readGroup = (g) => { if (g === 0) return ""; const h = Math.floor(g / 100); const r = g % 100; let s = ""; if (h > 0) s += DIGITS[h] + " trăm "; else if (groups.length > 1) s += "không trăm "; if (r === 0) return s.trim(); if (r < 10) { s += (h > 0 && r === 5) ? "lẻ năm" : "lẻ " + DIGITS[r]; } else if (r < 20) { s += "mười" + (r === 10 ? "" : (r === 15 ? " lăm" : " " + DIGITS[r % 10])); } else { const t = Math.floor(r / 10); const o = r % 10; s += ["", "", "hai mươi", "ba mươi", "bốn mươi", "năm mươi", "sáu mươi", "bảy mươi", "tám mươi", "chín mươi"][t]; if (o === 1) s += " mốt"; else if (o === 5) s += " lăm"; else if (o > 0) s += " " + DIGITS[o]; } return s.replace(/\s+/g, " ").trim(); }; let result = ""; for (let i = groups.length - 1; i >= 0; i--) { const g = groups[i]; if (g === 0) continue; result += readGroup(g); if (i > 0) result += " " + units[i] + " "; } return result.replace(/\s+/g, " ").trim(); } /** * Normalize Vietnamese text for display. * Numbers remain as digits for clean visual display. */ export function normalizeVietnameseText(text) { if (!text || typeof text !== "string") return text; return text.replace(/\s+/g, " ").trim(); } /** * Check if text contains Vietnamese characters. */ export function containsVietnamese(text) { return /[àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ]/i.test(text); } /** * Convert Arabic numerals to Vietnamese words for TTS. * Display texts keep the original digits. */ export function numbersToVietnameseWords(text) { if (!text || typeof text !== "string") return text; const units = ["", "nghìn", "triệu", "tỷ"]; const numberToVietnameseLocal = (n) => { if (n === 0) return "không"; if (n < 0) return "âm " + numberToVietnameseLocal(-n); const groups = []; let temp = n; while (temp > 0) { groups.push(temp % 1000); temp = Math.floor(temp / 1000); } if (groups.length === 0) groups.push(0); const readGroup = (g) => { if (g === 0) return ""; const h = Math.floor(g / 100); const r = g % 100; let s = ""; if (h > 0) s += DIGITS[h] + " trăm "; else if (groups.length > 1) s += "không trăm "; if (r === 0) return s.trim(); if (r < 10) { s += (h > 0 && r === 5) ? "lẻ năm" : "lẻ " + DIGITS[r]; } else if (r < 20) { s += "mười" + (r === 10 ? "" : (r === 15 ? " lăm" : " " + DIGITS[r % 10])); } else { const t = Math.floor(r / 10); const o = r % 10; s += ["", "", "hai mươi", "ba mươi", "bốn mươi", "năm mươi", "sáu mươi", "bảy mươi", "tám mươi", "chín mươi"][t]; if (o === 1) s += " mốt"; else if (o === 5) s += " lăm"; else if (o > 0) s += " " + DIGITS[o]; } return s.replace(/\s+/g, " ").trim(); }; let result = ""; for (let i = groups.length - 1; i >= 0; i--) { const g = groups[i]; if (g === 0) continue; result += readGroup(g); if (i > 0) result += " " + units[i] + " "; } return result.replace(/\s+/g, " ").trim(); }; // Match integers (including negative) - convert to Vietnamese words return text.replace(/\b-?\d+\b/g, (match) => { const num = parseInt(match); return numberToVietnameseLocal(num); }); } /** * Smart normalizer: only run on Vietnamese text, only on final transcripts. */ export function smartNormalize(text, partial = false) { if (!text || partial) return text; if (containsVietnamese(text)) { return normalizeVietnameseText(text); } return text; } /** * Convert text for TTS: Vietnamese numbers to words for proper pronunciation. * Call this before sending text to TTS synthesis. */ export function prepareForTTS(text) { if (!text || typeof text !== "string") return text; if (containsVietnamese(text)) { return numbersToVietnameseWords(text); } return text; }