diff --git a/src/pipecat/services/inworld/tts.py b/src/pipecat/services/inworld/tts.py index 48449f123..d6c037a63 100644 --- a/src/pipecat/services/inworld/tts.py +++ b/src/pipecat/services/inworld/tts.py @@ -60,9 +60,47 @@ from pipecat.frames.frames import ( ) from pipecat.processors.frame_processor import FrameDirection from pipecat.services.tts_service import TextAggregationMode, TTSService, WebsocketTTSService +from pipecat.transcriptions.language import Language from pipecat.utils.tracing.service_decorators import traced_tts +def language_to_inworld_language(language: Language) -> str: + """Convert a Language enum to an Inworld TTS language code. + + Inworld TTS accepts BCP-47 language tags. For the generally available + languages, Inworld's Playground emits regional tags (e.g. ``en-US``, + ``ru-RU``), so base Pipecat languages resolve to those canonical locales. + Regional variants can be used to nudge a voice toward a specific accent. + + Args: + language: The Language enum value to convert. + + Returns: + The corresponding Inworld language code. Base GA language enums are + normalized to Inworld's canonical regional locales. Other language enums + are returned as their BCP-47 string values. + """ + CANONICAL_LOCALES = { + Language.AR: "ar-SA", + Language.DE: "de-DE", + Language.EN: "en-US", + Language.ES: "es-ES", + Language.FR: "fr-FR", + Language.HE: "he-IL", + Language.HI: "hi-IN", + Language.IT: "it-IT", + Language.JA: "ja-JP", + Language.KO: "ko-KR", + Language.NL: "nl-NL", + Language.PL: "pl-PL", + Language.PT: "pt-BR", + Language.RU: "ru-RU", + Language.ZH: "zh-CN", + } + + return CANONICAL_LOCALES.get(language, str(language)) + + @dataclass class InworldTTSSettings(TTSSettings): """Settings for InworldTTSService and InworldHttpTTSService. @@ -234,6 +272,17 @@ class InworldHttpTTSService(TTSService): """ return True + def language_to_service_language(self, language: Language) -> str | None: + """Convert a Language enum to Inworld language format. + + Args: + language: The language to convert. + + Returns: + The Inworld-specific BCP-47 language code, or None if not supported. + """ + return language_to_inworld_language(language) + async def start(self, frame: StartFrame): """Start the Inworld TTS service. @@ -321,7 +370,7 @@ class InworldHttpTTSService(TTSService): if self._settings.temperature is not None: payload["temperature"] = self._settings.temperature if self._settings.delivery_mode is not None: - payload["delivery_mode"] = self._settings.delivery_mode + payload["deliveryMode"] = self._settings.delivery_mode if self._settings.language is not None: payload["language"] = self._settings.language @@ -712,6 +761,17 @@ class InworldTTSService(WebsocketTTSService): """ return True + def language_to_service_language(self, language: Language) -> str | None: + """Convert a Language enum to Inworld language format. + + Args: + language: The language to convert. + + Returns: + The Inworld-specific BCP-47 language code, or None if not supported. + """ + return language_to_inworld_language(language) + async def start(self, frame: StartFrame): """Start the Inworld WebSocket TTS service. @@ -1102,7 +1162,7 @@ class InworldTTSService(WebsocketTTSService): if self._settings.temperature is not None: create_config["temperature"] = self._settings.temperature if self._settings.delivery_mode is not None: - create_config["delivery_mode"] = self._settings.delivery_mode + create_config["deliveryMode"] = self._settings.delivery_mode if self._settings.language is not None: create_config["language"] = self._settings.language if self._apply_text_normalization is not None: diff --git a/tests/test_inworld_tts_language.py b/tests/test_inworld_tts_language.py new file mode 100644 index 000000000..07c53034e --- /dev/null +++ b/tests/test_inworld_tts_language.py @@ -0,0 +1,31 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Tests for Inworld TTS language code mapping.""" + +from pipecat.services.inworld.tts import language_to_inworld_language +from pipecat.transcriptions.language import Language + + +def test_inworld_base_languages_resolve_to_canonical_regional_tags(): + """Base GA languages should use the regional tags emitted by Inworld Playground.""" + assert language_to_inworld_language(Language.EN) == "en-US" + assert language_to_inworld_language(Language.RU) == "ru-RU" + assert language_to_inworld_language(Language.FR) == "fr-FR" + assert language_to_inworld_language(Language.ZH) == "zh-CN" + + +def test_inworld_regional_languages_are_preserved(): + """Explicit regional variants should be passed through as supported BCP-47 tags.""" + assert language_to_inworld_language(Language.EN_GB) == "en-GB" + assert language_to_inworld_language(Language.PT_PT) == "pt-PT" + assert language_to_inworld_language(Language.RU_RU) == "ru-RU" + + +def test_inworld_other_languages_are_passed_through_as_bcp47_tags(): + """Languages outside the canonical locale map should keep their BCP-47 enum value.""" + assert language_to_inworld_language(Language.SV_SE) == "sv-SE" + assert language_to_inworld_language(Language.UK_UA) == "uk-UA"