"""Language-safe text sanitizer for the OmniVoice TTS wrapper. Strips artifacts that are wrong to read aloud in ANY language — LLM reasoning/thinking blocks, markdown, HTML/XML tags, model control/special tokens, stray control characters — and normalizes Unicode (NFKC) + whitespace. DELIBERATELY does NOT do English-specific normalization (number / phone / email / currency expansion). OmniVoice is a 600+-language model; those rewrites are correct only for English and would corrupt non-English input. The fork we took the idea from (groxaxo/omnivoice-streaming) chains that English-only step in — we keep only the language-neutral subset (eshpfi session 2026-06-19). OmniVoice's own inline non-verbal symbols ([laughter], [sigh], [breath], …) are PRESERVED — they use square brackets and contain no "](...)" link tail, so none of the markdown/HTML rules below touch them. """ from __future__ import annotations import re import unicodedata # LLM reasoning/thinking blocks: , , etc. # Tag + content both removed (the content is private chain-of-thought, not speech). _THINK_RE = re.compile( r"<(think|thinking|reasoning|reflection|scratchpad)>.*?", re.IGNORECASE | re.DOTALL, ) # Markdown — applied while line breaks still exist (the (?m)^ anchors need them). _CODE_FENCE_RE = re.compile(r"```.*?```", re.DOTALL) # fenced code block → drop _IMAGE_RE = re.compile(r"!\[[^\]]*\]\([^)]*\)") # ![alt](url) → drop _LINK_RE = re.compile(r"\[([^\]]+)\]\([^)]*\)") # [text](url) → text _INLINE_CODE_RE = re.compile(r"`([^`]*)`") # `code` → code # Emphasis: ***, **, *, ~~, __ (single _ deliberately EXCLUDED so snake_case # words like "voice_clone_prompt" survive intact). _EMPHASIS_RE = re.compile(r"(\*\*\*|\*\*|\*|~~|__)(.*?)\1", re.DOTALL) _HEADING_RE = re.compile(r"(?m)^[ \t]{0,3}#{1,6}[ \t]+") # "## H" → "" _BLOCKQUOTE_RE = re.compile(r"(?m)^[ \t]{0,3}>[ \t]?") # "> q" → "q" _LIST_MARKER_RE = re.compile(r"(?m)^[ \t]*(?:[-*+]|\d+[.)])[ \t]+") # "- x" / "1. x" → "x" _HR_RE = re.compile(r"(?m)^[ \t]{0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$") # "---" rule → drop # Model / chat-template control + special tokens. _CONTROL_TOKEN_RE = re.compile( r"<\|[^>]*?\|>" # <|im_start|>, <|eot_id|>, … r"|" # r"|<(?:pad|unk|mask|bos|eos|sep|cls)>" # common HF special tokens r"|\[/?INST\]" # [INST] [/INST] r"|<>", # <> <> re.IGNORECASE, ) # Any remaining HTML/XML tags (run AFTER think-blocks so their content is gone). _HTML_TAG_RE = re.compile(r"<[^>]+>") # Stray control characters (keep \n and \t; collapsed in the final whitespace pass). _CTRL_CHARS_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]") def sanitize_tts_text(text: str) -> str: """Return ``text`` cleaned of artifacts that shouldn't be spoken, language-safe. Idempotent and Unicode-aware. Collapses all whitespace to single spaces at the end (the scheduler's splitter does the same, so paragraph structure is moot for synthesis). Returns ``""`` for falsy input. """ if not text: return "" # 1. LLM reasoning/thinking blocks (tag + content). text = _THINK_RE.sub(" ", text) # 2. Markdown constructs (order matters: fences/images/links before inline/emphasis). text = _CODE_FENCE_RE.sub(" ", text) text = _IMAGE_RE.sub(" ", text) text = _LINK_RE.sub(r"\1", text) text = _INLINE_CODE_RE.sub(r"\1", text) text = _EMPHASIS_RE.sub(r"\2", text) text = _HEADING_RE.sub("", text) text = _BLOCKQUOTE_RE.sub("", text) text = _LIST_MARKER_RE.sub("", text) text = _HR_RE.sub(" ", text) # 3. Model control/special tokens, then any leftover HTML/XML tags. text = _CONTROL_TOKEN_RE.sub(" ", text) text = _HTML_TAG_RE.sub(" ", text) # 4. Unicode normalize (NFKC) + drop stray control characters. text = unicodedata.normalize("NFKC", text) text = _CTRL_CHARS_RE.sub("", text) # 5. Collapse all whitespace (incl. newlines) to single spaces, trim. return " ".join(text.split())