Spaces:
Paused
Paused
| /** | |
| * 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; | |
| } |