From 248206e234def6c6a65dd7b348610b8337d65ab1 Mon Sep 17 00:00:00 2001 From: Yohan Liyanage Date: Sat, 2 Aug 2025 12:49:29 +0530 Subject: [PATCH 1/2] Fixes 2277 - SSML reserved characters in LLM generated text causes Azure TTS to fail. --- src/pipecat/services/azure/tts.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/pipecat/services/azure/tts.py b/src/pipecat/services/azure/tts.py index 0cf029c6b..49470f85a 100644 --- a/src/pipecat/services/azure/tts.py +++ b/src/pipecat/services/azure/tts.py @@ -128,6 +128,14 @@ class AzureBaseTTSService(TTSService): "volume": params.volume, } + # Define SSML escape mappings based on SSML reserved characters + # See - https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-structure + self.ssml_escape_chars = { + '&': '&', + '<': '<', + '>': '>' + } + self._api_key = api_key self._region = region self._voice_id = voice @@ -154,6 +162,10 @@ class AzureBaseTTSService(TTSService): def _construct_ssml(self, text: str) -> str: language = self._settings["language"] + + # Escape special characters + escaped_text = self._escape_text(text) + ssml = ( f"" - ssml += text + ssml += escaped_text if self._settings["emphasis"]: ssml += "" @@ -197,6 +209,20 @@ class AzureBaseTTSService(TTSService): return ssml + def _escape_text(self, text: str) -> str: + """Escape special characters in text for SSML. + + Args: + text: The text to escape. + + Returns: + The escaped text. + """ + escaped_text = text + for char, escape_code in self.ssml_escape_chars.items(): + escaped_text = escaped_text.replace(char, escape_code) + return escaped_text + class AzureTTSService(AzureBaseTTSService): """Azure Cognitive Services streaming TTS service. From 4bcca7956ec0fb3dec3654e3d03f20add98489b8 Mon Sep 17 00:00:00 2001 From: Yohan Liyanage Date: Wed, 13 Aug 2025 16:34:33 +0530 Subject: [PATCH 2/2] Refactors the code based on PR comments and adds the relevant changelog entry. --- CHANGELOG.md | 2 ++ src/pipecat/services/azure/tts.py | 29 +++++++++++++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d31a76c75..528ec3944 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `on_transcription_stopped` and `on_transcription_error` to Daily callbacks. +- Added SSML reserved character escaping to `AzureBaseTTSService` to properly handle special characters in text sent to Azure TTS. This fixes an issue where characters like `&`, `<`, `>`, `"`, and `'` in LLM-generated text would cause TTS failures. +- ### Changed - Changed the default `url` for `NeuphonicTTSService` to diff --git a/src/pipecat/services/azure/tts.py b/src/pipecat/services/azure/tts.py index 49470f85a..15b4f1256 100644 --- a/src/pipecat/services/azure/tts.py +++ b/src/pipecat/services/azure/tts.py @@ -68,6 +68,16 @@ class AzureBaseTTSService(TTSService): construction, voice configuration, and parameter management. """ + # Define SSML escape mappings based on SSML reserved characters + # See - https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-structure + SSML_ESCAPE_CHARS = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + } + class InputParams(BaseModel): """Input parameters for Azure TTS voice configuration. @@ -128,14 +138,6 @@ class AzureBaseTTSService(TTSService): "volume": params.volume, } - # Define SSML escape mappings based on SSML reserved characters - # See - https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-structure - self.ssml_escape_chars = { - '&': '&', - '<': '<', - '>': '>' - } - self._api_key = api_key self._region = region self._voice_id = voice @@ -210,7 +212,14 @@ class AzureBaseTTSService(TTSService): return ssml def _escape_text(self, text: str) -> str: - """Escape special characters in text for SSML. + """Escapes XML/SSML reserved characters according to Microsoft documentation. + + This method escapes the following characters: + - & becomes & + - < becomes < + - > becomes > + - " becomes " + - ' becomes ' Args: text: The text to escape. @@ -219,7 +228,7 @@ class AzureBaseTTSService(TTSService): The escaped text. """ escaped_text = text - for char, escape_code in self.ssml_escape_chars.items(): + for char, escape_code in AzureBaseTTSService.SSML_ESCAPE_CHARS.items(): escaped_text = escaped_text.replace(char, escape_code) return escaped_text