From df4e441be7f97aade0336654b28cf71fa3990038 Mon Sep 17 00:00:00 2001 From: Noura Attili Date: Wed, 22 Apr 2026 13:47:43 -0400 Subject: [PATCH] Prevent emojis from being spoken aloud by avatar Add EmojiTextFilter to strip emoji characters before TTS synthesis, and strengthen the system prompt to explicitly instruct the LLM not to use emojis (since they are read aloud as their names by TTS engines). Co-Authored-By: Claude Sonnet 4.6 --- src/server/bot/core/component_factory.py | 10 ++++- .../custom_services/emoji_text_filter.py | 39 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 src/server/bot/transport/custom_services/emoji_text_filter.py diff --git a/src/server/bot/core/component_factory.py b/src/server/bot/core/component_factory.py index c7be831..921c92b 100644 --- a/src/server/bot/core/component_factory.py +++ b/src/server/bot/core/component_factory.py @@ -23,6 +23,7 @@ from ..components.llm_tools.end_conversation_handler import EndConversationHandler from ..processors.speech.lipsync_processor import LipsyncProcessor from ..transport.custom_services.kokoro_service import KokoroTTSService +from ..transport.custom_services.emoji_text_filter import EmojiTextFilter from ..utils.device_utils import get_best_device from ..transport.custom_services.ollama_service import CustomOLLamaLLMService from ..components.memory import MemoryHandler @@ -178,6 +179,10 @@ def build_instruction(self) -> str: "discourse markers (you know, I mean), and brief pauses (uh, um). " "Use sparingly; never in numbers or names.\n" ) + instruction += ( + "Important: Do not use emojis or any special unicode symbols. " + "Your responses are converted directly to audio — emojis will be read aloud as their names.\n" + ) return instruction.strip() @@ -377,20 +382,23 @@ def _build_tts_service(self) -> Optional[object]: if not self.tts_type: return None + emoji_filter = EmojiTextFilter() if self.tts_type == "openai": return OpenAITTSService( voice=self._get_voice_for_openai(), model=(self.tts_params or {}).get("model", "gpt-4o-mini-tts"), + text_filters=[emoji_filter], ) elif self.tts_type == "elevenlabs": return ElevenLabsTTSService( api_key=os.getenv("ELEVENLABS_API_KEY"), voice_id=self._get_voice_id_for_elevenlabs(), model="eleven_flash_v2_5", + text_filters=[emoji_filter], ) elif self.tts_type == "kokoro": device = get_best_device() return KokoroTTSService( - voice=self._get_voice_id_for_kokoro(), device=device + voice=self._get_voice_id_for_kokoro(), device=device, text_filters=[emoji_filter] ) return None diff --git a/src/server/bot/transport/custom_services/emoji_text_filter.py b/src/server/bot/transport/custom_services/emoji_text_filter.py new file mode 100644 index 0000000..fe2e091 --- /dev/null +++ b/src/server/bot/transport/custom_services/emoji_text_filter.py @@ -0,0 +1,39 @@ +import re +from typing import Any, Mapping + +from pipecat.utils.text.base_text_filter import BaseTextFilter + +_EMOJI_RE = re.compile( + "[" + "\U0001F600-\U0001F64F" + "\U0001F300-\U0001F5FF" + "\U0001F680-\U0001F6FF" + "\U0001F1E0-\U0001F1FF" + "\U00002600-\U000026FF" + "\U00002700-\U000027BF" + "\U0000FE00-\U0000FE0F" + "\U0001F900-\U0001F9FF" + "\U0001FA70-\U0001FAFF" + "\U00002300-\U000023FF" + "\U00002B50-\U00002B55" + "\U0001F004" + "\U0001F0CF" + "]+", + flags=re.UNICODE, +) + + +class EmojiTextFilter(BaseTextFilter): + """Strips emoji characters from text before TTS synthesis.""" + + async def update_settings(self, settings: Mapping[str, Any]): + pass + + async def filter(self, text: str) -> str: + return _EMOJI_RE.sub("", text).strip() + + async def handle_interruption(self): + pass + + async def reset_interruption(self): + pass