From c5e08b24090d34d91af6e0d6b2f340e23c33b3ae Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Tue, 5 May 2026 17:28:20 +0300 Subject: [PATCH] Return the requested format for OpenAI TTS (#169839) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../components/openai_conversation/tts.py | 21 +++--- .../openai_conversation/test_tts.py | 73 +++++++++++++++++++ 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/openai_conversation/tts.py b/homeassistant/components/openai_conversation/tts.py index f3ee614d7470..9aab49a465a5 100644 --- a/homeassistant/components/openai_conversation/tts.py +++ b/homeassistant/components/openai_conversation/tts.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal from openai import OpenAIError from propcache.api import cached_property @@ -166,14 +166,15 @@ class OpenAITTSEntity(TextToSpeechEntity, OpenAIBaseLLMEntity): client = self.entry.runtime_data response_format = options[ATTR_PREFERRED_FORMAT] - if response_format not in self._supported_formats: - # common aliases - if response_format == "ogg": - response_format = "opus" - elif response_format == "raw": - response_format = "pcm" - else: - response_format = self.default_options[ATTR_PREFERRED_FORMAT] + if response_format in ("ogg", "oga"): + codec: Literal["mp3", "opus", "aac", "flac", "wav", "pcm"] = "opus" + elif response_format == "raw": + response_format = codec = "pcm" + elif response_format not in self._supported_formats: + response_format = self.default_options[ATTR_PREFERRED_FORMAT] + codec = response_format + else: + codec = response_format try: async with client.audio.speech.with_streaming_response.create( @@ -182,7 +183,7 @@ class OpenAITTSEntity(TextToSpeechEntity, OpenAIBaseLLMEntity): input=message, instructions=str(options.get(CONF_PROMPT)), speed=options.get(CONF_TTS_SPEED, RECOMMENDED_TTS_SPEED), - response_format=response_format, + response_format=codec, ) as response: response_data = bytearray() async for chunk in response.iter_bytes(): diff --git a/tests/components/openai_conversation/test_tts.py b/tests/components/openai_conversation/test_tts.py index ffbe7117c1e8..45c3a0a96204 100644 --- a/tests/components/openai_conversation/test_tts.py +++ b/tests/components/openai_conversation/test_tts.py @@ -118,6 +118,79 @@ async def test_tts( ) +@pytest.mark.parametrize( + ("preferred_format", "expected_response_format"), + [ + ("ogg", "opus"), + ("oga", "opus"), + ("mp3", "mp3"), + ], +) +@pytest.mark.usefixtures("mock_init_component") +async def test_tts_preferred_format( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_create_speech: MagicMock, + calls: list[ServiceCall], + preferred_format: str, + expected_response_format: str, +) -> None: + """Test text to speech preferred format handling.""" + mock_create_speech.return_value = [b"mock audio data"] + + await hass.services.async_call( + tts.DOMAIN, + "speak", + { + ATTR_ENTITY_ID: "tts.openai_tts", + tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + tts.ATTR_OPTIONS: {tts.ATTR_PREFERRED_FORMAT: preferred_format}, + }, + blocking=True, + ) + + assert len(calls) == 1 + assert ( + await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + == HTTPStatus.OK + ) + mock_create_speech.assert_called_once_with( + model="gpt-4o-mini-tts", + voice="marin", + input="There is a person at the front door.", + instructions="", + speed=1.0, + response_format=expected_response_format, + ) + + +@pytest.mark.usefixtures("mock_init_component") +async def test_tts_raw_preferred_format_returns_pcm( + hass: HomeAssistant, + mock_create_speech: MagicMock, +) -> None: + """Test raw preferred format is returned as pcm.""" + tts_entity = hass.data[tts.DOMAIN].get_entity("tts.openai_tts") + mock_create_speech.return_value = [b"mock audio data"] + + result = await tts_entity.async_get_tts_audio( + "There is a person at the front door.", + "en-US", + {tts.ATTR_PREFERRED_FORMAT: "raw", tts.ATTR_VOICE: "marin"}, + ) + + assert result == ("pcm", b"mock audio data") + mock_create_speech.assert_called_once_with( + model="gpt-4o-mini-tts", + voice="marin", + input="There is a person at the front door.", + instructions="", + speed=1.0, + response_format="pcm", + ) + + @pytest.mark.usefixtures("mock_init_component") async def test_tts_error( hass: HomeAssistant,