"""Strip formatting that only makes sense on a screen, before it is spoken. `VOICE_STYLE` asks Claude not to emit markdown, and mostly he doesn't — but asking is probabilistic and hearing "asterisk asterisk aloud asterisk asterisk" once is enough. This is the deterministic half. Pipecat's `MarkdownTextFilter` does most of it and runs *after* sentence aggregation, so formatting split across streaming chunks is already reassembled by the time it sees the text. What it leaves behind, measured: 'Use ~~strike~~' -> 'Use ~~strike~~' still spoken as tildes '- first bullet' -> '- first bullet' spoken as "dash" 'the sample_rate is' -> 'the sample_rate is' spoken as "underscore" 'Multiply 3 * 4' -> 'Multiply 3 4' the "times" is lost so this subclass handles those four and tidies the spacing. """ import re from pipecat.utils.text.markdown_text_filter import MarkdownTextFilter # Run before the markdown filter, which would otherwise eat the asterisk. _TIMES = re.compile(r"(?<=\d)\s*\*\s*(?=\d)") _STRIKETHROUGH = re.compile(r"~~(.+?)~~") # Only at the start of a line, so a hyphenated word is untouched. _LIST_MARKER = re.compile(r"^[ \t]*[-*•]\s+", re.MULTILINE) # Identifiers read better as words: "sample_rate" -> "sample rate". _UNDERSCORE_WORD = re.compile(r"(?<=\w)_(?=\w)") _EXTRA_SPACE = re.compile(r"[ \t]{2,}") _CONTROL_TAGS = re.compile(r"\[(COMPLETE|NEEDS_DEEP|STATUS:[^\]]+)\]", re.IGNORECASE) _VOICE_TAG = re.compile(r"\[Voice:\s*([a-zA-Z0-9_\-]+)\]", re.IGNORECASE) class SpokenTextFilter(MarkdownTextFilter): """Markdown filtering, plus voice tag parsing and leftovers that matter when read aloud.""" def __init__(self, voice_manager=None, **kwargs): super().__init__(**kwargs) self._voice_manager = voice_manager async def filter(self, text: str) -> str: if self._voice_manager: self._voice_manager.sync_voice() # Intercept and set active voice on [Voice:VoiceName] tags match = _VOICE_TAG.search(text) if match: new_voice = match.group(1) text = _VOICE_TAG.sub("", text) if self._voice_manager: try: self._voice_manager.set_voice(new_voice) except Exception: pass text = _TIMES.sub(" times ", text) text = await super().filter(text) text = _CONTROL_TAGS.sub("", text) text = _STRIKETHROUGH.sub(r"\1", text) text = _LIST_MARKER.sub("", text) text = _UNDERSCORE_WORD.sub(" ", text) return _EXTRA_SPACE.sub(" ", text)