From 9b55d4ddd47199307fb3e70b3bb052f7099b655a Mon Sep 17 00:00:00 2001 From: TimTk Date: Tue, 12 May 2026 22:13:09 +0300 Subject: [PATCH 1/4] Add support for Inworld TTS v2 fields --- changelog/4450.added.md | 1 + src/pipecat/services/inworld/tts.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 changelog/4450.added.md diff --git a/changelog/4450.added.md b/changelog/4450.added.md new file mode 100644 index 000000000..0d60fadc9 --- /dev/null +++ b/changelog/4450.added.md @@ -0,0 +1 @@ +- Added support for Inworld TTS v2 fields. diff --git a/src/pipecat/services/inworld/tts.py b/src/pipecat/services/inworld/tts.py index fc6b32f89..48449f123 100644 --- a/src/pipecat/services/inworld/tts.py +++ b/src/pipecat/services/inworld/tts.py @@ -70,10 +70,16 @@ 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: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) _aliases: ClassVar[dict[str, str]] = { "voiceId": "voice", @@ -167,6 +173,7 @@ class InworldHttpTTSService(TTSService): language=None, speaking_rate=None, temperature=None, + delivery_mode=None, ) # 2. Apply direct init arg overrides (deprecated) @@ -313,6 +320,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["delivery_mode"] = 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 +620,7 @@ class InworldTTSService(WebsocketTTSService): language=None, speaking_rate=None, temperature=None, + delivery_mode=None, ) # 2. Apply direct init arg overrides (deprecated) @@ -1089,6 +1101,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["delivery_mode"] = 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: From ecaff1d1ebbf21373192966cab816f562304b723 Mon Sep 17 00:00:00 2001 From: TimTk Date: Tue, 12 May 2026 22:21:59 +0300 Subject: [PATCH 2/4] Fix changelog fragment number --- changelog/{4450.added.md => 4473.added.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog/{4450.added.md => 4473.added.md} (100%) diff --git a/changelog/4450.added.md b/changelog/4473.added.md similarity index 100% rename from changelog/4450.added.md rename to changelog/4473.added.md From 39e7f9e354983c08f8cd539356357318f63b209d Mon Sep 17 00:00:00 2001 From: Timofey Date: Wed, 13 May 2026 11:17:31 +0300 Subject: [PATCH 3/4] Fix Inworld TTS v2 request fields --- src/pipecat/services/inworld/tts.py | 64 ++++++++++++++++++++++++++++- tests/test_inworld_tts_language.py | 31 ++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 tests/test_inworld_tts_language.py 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" From 97f50c8aa2ac55230304c8984fec32b33848c202 Mon Sep 17 00:00:00 2001 From: TimTk Date: Wed, 13 May 2026 21:43:02 +0300 Subject: [PATCH 4/4] Address review: use resolve_language, narrow delivery_mode type, update changelog - Replace custom LANGUAGE_MAP fallback in language_to_inworld_language with resolve_language(language, LANGUAGE_MAP, use_base_code=False) to match the pattern used by other services and restore the unverified-language warning - Tighten delivery_mode type from str to Literal["STABLE", "BALANCED", "CREATIVE"] - Update changelog entry to mention delivery_mode and language normalization --- changelog/4473.added.md | 4 +++- src/pipecat/services/inworld/tts.py | 23 +++++++++-------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/changelog/4473.added.md b/changelog/4473.added.md index 0d60fadc9..099728922 100644 --- a/changelog/4473.added.md +++ b/changelog/4473.added.md @@ -1 +1,3 @@ -- Added support for Inworld TTS v2 fields. +- 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 d6c037a63..6bad30e28 100644 --- a/src/pipecat/services/inworld/tts.py +++ b/src/pipecat/services/inworld/tts.py @@ -60,27 +60,21 @@ 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.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 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. + """Convert a Language enum to an Inworld TTS BCP-47 language tag. 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. + The corresponding Inworld BCP-47 language tag (e.g. ``"en-US"``). + Unverified languages fall back to their BCP-47 string value with a warning. """ - CANONICAL_LOCALES = { + LANGUAGE_MAP = { Language.AR: "ar-SA", Language.DE: "de-DE", Language.EN: "en-US", @@ -97,8 +91,7 @@ def language_to_inworld_language(language: Language) -> str: Language.RU: "ru-RU", Language.ZH: "zh-CN", } - - return CANONICAL_LOCALES.get(language, str(language)) + return resolve_language(language, LANGUAGE_MAP, use_base_code=False) @dataclass @@ -117,7 +110,9 @@ class InworldTTSSettings(TTSSettings): speaking_rate: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) temperature: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) - delivery_mode: str | 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",