From dfea18d68984cff637deaf3bc0ab98ed8a3523f3 Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:40:23 +0530 Subject: [PATCH 1/7] fix: normalize TTS dtype configuration --- src/agents/voice/model.py | 7 +++++++ tests/voice/test_tts_model_settings.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 tests/voice/test_tts_model_settings.py diff --git a/src/agents/voice/model.py b/src/agents/voice/model.py index 3b4a8e85b5..c4dce67e96 100644 --- a/src/agents/voice/model.py +++ b/src/agents/voice/model.py @@ -87,6 +87,12 @@ class TTSModelSettings: speed: float | None = None """The speed with which the TTS model will read the text. Between 0.25 and 4.0.""" + def __post_init__(self) -> None: + # Configurations loaded from JSON/YAML commonly represent NumPy dtypes as strings. + # Normalize those spellings once at the settings boundary so downstream consumers can + # compare against the supported NumPy dtypes consistently. + self.dtype = np.dtype(self.dtype) + class TTSModel(abc.ABC): """A text-to-speech model that can convert text into audio output.""" @@ -228,3 +234,4 @@ def get_stt_model(self, model_name: str | None) -> STTModel: @abc.abstractmethod def get_tts_model(self, model_name: str | None) -> TTSModel: """Get a text-to-speech model by name.""" + pass diff --git a/tests/voice/test_tts_model_settings.py b/tests/voice/test_tts_model_settings.py new file mode 100644 index 0000000000..db2470c234 --- /dev/null +++ b/tests/voice/test_tts_model_settings.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import numpy as np + +from agents.voice import TTSModelSettings + + +def test_tts_model_settings_normalizes_string_dtype() -> None: + settings = TTSModelSettings(dtype="float32") + + assert settings.dtype == np.dtype("float32") + + +def test_tts_model_settings_normalizes_int16_dtype() -> None: + settings = TTSModelSettings(dtype="int16") + + assert settings.dtype == np.dtype("int16") + + +def test_tts_model_settings_accepts_numpy_dtype() -> None: + settings = TTSModelSettings(dtype=np.float32) + + assert settings.dtype == np.dtype("float32") From a4fdfa13e550af993504e5c2f651db3976dbf48d Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Wed, 2 Sep 2026 03:13:57 +0530 Subject: [PATCH 2/7] fix(voice): preserve dtype error contract --- src/agents/voice/model.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/agents/voice/model.py b/src/agents/voice/model.py index c4dce67e96..28e5b9d8da 100644 --- a/src/agents/voice/model.py +++ b/src/agents/voice/model.py @@ -7,6 +7,7 @@ from typing_extensions import TypedDict +from .exceptions import UserError from .imports import np, npt from .input import AudioInput, StreamedAudioInput from .utils import get_sentence_based_splitter @@ -91,7 +92,10 @@ def __post_init__(self) -> None: # Configurations loaded from JSON/YAML commonly represent NumPy dtypes as strings. # Normalize those spellings once at the settings boundary so downstream consumers can # compare against the supported NumPy dtypes consistently. - self.dtype = np.dtype(self.dtype) + try: + self.dtype = np.dtype(self.dtype) + except (TypeError, ValueError) as error: + raise UserError("Invalid output dtype") from error class TTSModel(abc.ABC): @@ -180,9 +184,6 @@ async def transcribe( Args: input: The audio input to transcribe. - settings: The settings to use for the transcription. - trace_include_sensitive_data: Whether to include sensitive data in traces. - trace_include_sensitive_audio_data: Whether to include sensitive audio data in traces. Returns: The text transcription of the audio input. @@ -234,4 +235,3 @@ def get_stt_model(self, model_name: str | None) -> STTModel: @abc.abstractmethod def get_tts_model(self, model_name: str | None) -> TTSModel: """Get a text-to-speech model by name.""" - pass From 733d194115b652229b5011e7bc746ce59dfc6255 Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Wed, 2 Sep 2026 03:14:14 +0530 Subject: [PATCH 3/7] test(voice): cover configured TTS dtype behavior --- tests/voice/test_tts_model_settings.py | 64 ++++++++++++++++++-------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/tests/voice/test_tts_model_settings.py b/tests/voice/test_tts_model_settings.py index db2470c234..2b3ea77294 100644 --- a/tests/voice/test_tts_model_settings.py +++ b/tests/voice/test_tts_model_settings.py @@ -1,23 +1,47 @@ from __future__ import annotations import numpy as np - -from agents.voice import TTSModelSettings - - -def test_tts_model_settings_normalizes_string_dtype() -> None: - settings = TTSModelSettings(dtype="float32") - - assert settings.dtype == np.dtype("float32") - - -def test_tts_model_settings_normalizes_int16_dtype() -> None: - settings = TTSModelSettings(dtype="int16") - - assert settings.dtype == np.dtype("int16") - - -def test_tts_model_settings_accepts_numpy_dtype() -> None: - settings = TTSModelSettings(dtype=np.float32) - - assert settings.dtype == np.dtype("float32") +import pytest + +from agents.exceptions import UserError +from agents.voice import AudioInput, TTSModelSettings, VoicePipeline + +from .helpers import extract_events +from .pipeline_test_models import QueuedSTTModel, QueuedVoiceWorkflow, ZeroPcmTTSModel + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("dtype", "expected_dtype"), + [("int16", np.int16), ("float32", np.float32), ("f4", np.float32)], + ids=["int16-string", "float32-string", "float32-alias"], +) +async def test_voicepipeline_accepts_string_tts_dtype_from_dictionary_config( + dtype: str, + expected_dtype: type[np.int16] | type[np.float32], +) -> None: + fake_stt = QueuedSTTModel(["first"]) + fake_tts = ZeroPcmTTSModel() + pipeline = VoicePipeline( + workflow=QueuedVoiceWorkflow([["out_1"]]), + stt_model=fake_stt, + tts_model=fake_tts, + config={"tts_settings": {"buffer_size": 1, "dtype": dtype}}, + ) + + result = await pipeline.run(AudioInput(buffer=np.zeros(2, dtype=np.int16))) + events, audio_chunks = await extract_events(result) + + assert events == ["turn_started", "audio", "turn_ended", "session_ended"] + decoded_audio = np.frombuffer(audio_chunks[0], dtype=expected_dtype) + assert decoded_audio.dtype == np.dtype(expected_dtype) + + +@pytest.mark.parametrize( + "dtype", + ["not-a-dtype", {"names": ["x"], "formats": []}], + ids=["unparseable-string", "malformed-structured-dtype"], +) +def test_tts_model_settings_preserves_user_error_for_invalid_dtype(dtype: object) -> None: + with pytest.raises(UserError, match="Invalid output dtype"): + TTSModelSettings(dtype=dtype) # type: ignore[arg-type] From 29d544682b9bd06ae3838fd6fb4936426511cec0 Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Wed, 2 Sep 2026 03:14:38 +0530 Subject: [PATCH 4/7] style(voice): restore TTS settings documentation --- src/agents/voice/model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/agents/voice/model.py b/src/agents/voice/model.py index 28e5b9d8da..079704e56e 100644 --- a/src/agents/voice/model.py +++ b/src/agents/voice/model.py @@ -184,6 +184,9 @@ async def transcribe( Args: input: The audio input to transcribe. + settings: The settings to use for the transcription. + trace_include_sensitive_data: Whether to include sensitive data in traces. + trace_include_sensitive_audio_data: Whether to include sensitive audio data in traces. Returns: The text transcription of the audio input. From aa9e014fa89f57e6bbc63daaf4d5a0bed84ef873 Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Thu, 3 Sep 2026 21:50:40 +0530 Subject: [PATCH 5/7] fix(voice): import UserError from package exceptions --- src/agents/voice/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/voice/model.py b/src/agents/voice/model.py index 079704e56e..f0dfdab06f 100644 --- a/src/agents/voice/model.py +++ b/src/agents/voice/model.py @@ -7,7 +7,7 @@ from typing_extensions import TypedDict -from .exceptions import UserError +from ..exceptions import UserError from .imports import np, npt from .input import AudioInput, StreamedAudioInput from .utils import get_sentence_based_splitter From 30d194c3e0a6c786ca4d760a481396f473b8d029 Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Thu, 3 Sep 2026 22:41:21 +0530 Subject: [PATCH 6/7] fix(voice): preserve non-string TTS dtype inputs --- src/agents/voice/model.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/agents/voice/model.py b/src/agents/voice/model.py index f0dfdab06f..574f34675f 100644 --- a/src/agents/voice/model.py +++ b/src/agents/voice/model.py @@ -89,13 +89,15 @@ class TTSModelSettings: """The speed with which the TTS model will read the text. Between 0.25 and 4.0.""" def __post_init__(self) -> None: - # Configurations loaded from JSON/YAML commonly represent NumPy dtypes as strings. - # Normalize those spellings once at the settings boundary so downstream consumers can - # compare against the supported NumPy dtypes consistently. - try: - self.dtype = np.dtype(self.dtype) - except (TypeError, ValueError) as error: - raise UserError("Invalid output dtype") from error + # Serialized configs commonly carry NumPy dtypes as strings. Normalize only those string + # spellings: custom TTS providers receive this settings object directly and may rely on + # non-string DTypeLike inputs (for example the callable np.int16 scalar class) retaining + # their original representation. + if isinstance(self.dtype, str): + try: + self.dtype = np.dtype(self.dtype) + except (TypeError, ValueError) as error: + raise UserError("Invalid output dtype") from error class TTSModel(abc.ABC): From 1b8bc99a800221adc489bb470740c61ab5610581 Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Thu, 3 Sep 2026 22:41:41 +0530 Subject: [PATCH 7/7] test(voice): cover emitted dtype and provider compatibility --- tests/voice/test_tts_model_settings.py | 52 ++++++++++++++++++++------ 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/tests/voice/test_tts_model_settings.py b/tests/voice/test_tts_model_settings.py index 2b3ea77294..31c1d28bb3 100644 --- a/tests/voice/test_tts_model_settings.py +++ b/tests/voice/test_tts_model_settings.py @@ -1,12 +1,13 @@ from __future__ import annotations +from collections.abc import AsyncIterator + import numpy as np import pytest from agents.exceptions import UserError from agents.voice import AudioInput, TTSModelSettings, VoicePipeline -from .helpers import extract_events from .pipeline_test_models import QueuedSTTModel, QueuedVoiceWorkflow, ZeroPcmTTSModel @@ -30,18 +31,47 @@ async def test_voicepipeline_accepts_string_tts_dtype_from_dictionary_config( ) result = await pipeline.run(AudioInput(buffer=np.zeros(2, dtype=np.int16))) - events, audio_chunks = await extract_events(result) + events: list[str] = [] + seen_dtypes: list[np.dtype[object]] = [] + async for event in result.stream(): + if event.type == "voice_stream_event_audio": + events.append("audio") + if event.data is not None: + seen_dtypes.append(event.data.dtype) + elif event.type == "voice_stream_event_lifecycle": + events.append(event.event) + elif event.type == "voice_stream_event_error": + events.append("error") assert events == ["turn_started", "audio", "turn_ended", "session_ended"] - decoded_audio = np.frombuffer(audio_chunks[0], dtype=expected_dtype) - assert decoded_audio.dtype == np.dtype(expected_dtype) + assert seen_dtypes + assert all(dtype == np.dtype(expected_dtype) for dtype in seen_dtypes) -@pytest.mark.parametrize( - "dtype", - ["not-a-dtype", {"names": ["x"], "formats": []}], - ids=["unparseable-string", "malformed-structured-dtype"], -) -def test_tts_model_settings_preserves_user_error_for_invalid_dtype(dtype: object) -> None: +@pytest.mark.parametrize("dtype", ["not-a-dtype"], ids=["unparseable-string"]) +def test_tts_model_settings_preserves_user_error_for_invalid_string_dtype(dtype: str) -> None: with pytest.raises(UserError, match="Invalid output dtype"): - TTSModelSettings(dtype=dtype) # type: ignore[arg-type] + TTSModelSettings(dtype=dtype) + + +@pytest.mark.asyncio +async def test_voicepipeline_preserves_non_string_dtype_for_custom_tts_provider() -> None: + class CallableDtypeTTSModel(ZeroPcmTTSModel): + async def run(self, text: str, settings: TTSModelSettings) -> AsyncIterator[bytes]: + del text + # Custom providers historically receive the original DTypeLike object. In particular, + # NumPy scalar classes are callable and provider code may legitimately use that API. + assert settings.dtype is np.int16 + assert settings.dtype(1) == np.int16(1) # type: ignore[operator] + yield np.zeros(2, dtype=np.int16).tobytes() + + pipeline = VoicePipeline( + workflow=QueuedVoiceWorkflow([["out_1"]]), + stt_model=QueuedSTTModel(["first"]), + tts_model=CallableDtypeTTSModel(), + config={"tts_settings": {"buffer_size": 1, "dtype": np.int16}}, + ) + + result = await pipeline.run(AudioInput(buffer=np.zeros(2, dtype=np.int16))) + async for _ in result.stream(): + pass