Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/agents/voice/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from typing_extensions import TypedDict

from .exceptions import UserError

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Import UserError from the package-level exceptions module

Importing agents.voice now fails before any voice API can be used: its __init__ imports .model, but agents.voice.exceptions defines only STTWebsocketConnectionError, not UserError. Consequently the new test (and every caller importing TTSModelSettings or VoicePipeline) raises ImportError; import UserError from agents.exceptions via ..exceptions instead.

AGENTS.md reference: AGENTS.md:L147-L147

Useful? React with 👍 / 👎.

from .imports import np, npt
from .input import AudioInput, StreamedAudioInput
from .utils import get_sentence_based_splitter
Expand Down Expand Up @@ -87,6 +88,15 @@ 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.
try:
self.dtype = np.dtype(self.dtype)
except (TypeError, ValueError) as error:
raise UserError("Invalid output dtype") from error


class TTSModel(abc.ABC):
"""A text-to-speech model that can convert text into audio output."""
Expand Down
47 changes: 47 additions & 0 deletions tests/voice/test_tts_model_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from __future__ import annotations

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


@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]