diff --git a/changelog/4473.added.md b/changelog/4473.added.md new file mode 100644 index 000000000..099728922 --- /dev/null +++ b/changelog/4473.added.md @@ -0,0 +1,3 @@ +- Inworld TTS updates: + - Added `delivery_mode` setting (`STABLE`/`BALANCED`/`CREATIVE`) to `InworldTTSService` and `InworldHttpTTSService`, enabling the stability-vs-creativity tradeoff in `inworld-tts-2`. + - Added language support to `InworldTTSService` and `InworldHttpTTSService`. The `language` setting is now forwarded to the API, and a new `language_to_inworld_language()` helper normalizes Pipecat `Language` enums to Inworld's BCP-47 locale tags. diff --git a/src/pipecat/services/inworld/tts.py b/src/pipecat/services/inworld/tts.py index fc6b32f89..6bad30e28 100644 --- a/src/pipecat/services/inworld/tts.py +++ b/src/pipecat/services/inworld/tts.py @@ -60,9 +60,40 @@ 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, resolve_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 BCP-47 language tag. + + Args: + language: The Language enum value to convert. + + Returns: + The corresponding Inworld BCP-47 language tag (e.g. ``"en-US"``). + Unverified languages fall back to their BCP-47 string value with a warning. + """ + LANGUAGE_MAP = { + 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 resolve_language(language, LANGUAGE_MAP, use_base_code=False) + + @dataclass class InworldTTSSettings(TTSSettings): """Settings for InworldTTSService and InworldHttpTTSService. @@ -70,10 +101,18 @@ class InworldTTSSettings(TTSSettings): Parameters: speaking_rate: Speaking rate for speech synthesis. temperature: Temperature for speech synthesis. + delivery_mode: Controls the stability vs. creativity tradeoff. + ``"STABLE"`` produces reliable, predictable speech. + ``"BALANCED"`` is the default midpoint. + ``"CREATIVE"`` produces more expressive, emotionally varied speech. + Only supported by ``inworld-tts-2``. """ speaking_rate: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) temperature: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + delivery_mode: Literal["STABLE", "BALANCED", "CREATIVE"] | None | _NotGiven = field( + default_factory=lambda: NOT_GIVEN + ) _aliases: ClassVar[dict[str, str]] = { "voiceId": "voice", @@ -167,6 +206,7 @@ class InworldHttpTTSService(TTSService): language=None, speaking_rate=None, temperature=None, + delivery_mode=None, ) # 2. Apply direct init arg overrides (deprecated) @@ -227,6 +267,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. @@ -313,6 +364,10 @@ class InworldHttpTTSService(TTSService): if self._settings.temperature is not None: payload["temperature"] = self._settings.temperature + if self._settings.delivery_mode is not None: + payload["deliveryMode"] = self._settings.delivery_mode + if self._settings.language is not None: + payload["language"] = self._settings.language # Use WORD timestamps for simplicity and correct spacing/capitalization payload["timestampType"] = self._timestamp_type @@ -609,6 +664,7 @@ class InworldTTSService(WebsocketTTSService): language=None, speaking_rate=None, temperature=None, + delivery_mode=None, ) # 2. Apply direct init arg overrides (deprecated) @@ -700,6 +756,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. @@ -1089,6 +1156,10 @@ 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["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: create_config["applyTextNormalization"] = self._apply_text_normalization if self._auto_mode 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"