Skip to content
Merged
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
2 changes: 1 addition & 1 deletion assemblyai/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.64.25"
__version__ = "0.64.26"
53 changes: 48 additions & 5 deletions assemblyai/streaming/v3/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@
from enum import Enum
from typing import Any, List, Literal, Optional, Union

from pydantic import BaseModel
try:
Comment thread
bgotthold-aai marked this conversation as resolved.
# pydantic v2 import
from pydantic import BaseModel, model_validator

pydantic_v2 = True
except ImportError:
# pydantic v1 import (fallback for Python < 3.14)
from pydantic import BaseModel, root_validator

pydantic_v2 = False


class LLMGatewayMessage(BaseModel):
Expand Down Expand Up @@ -153,11 +162,12 @@ class StreamingSessionParameters(BaseModel):
class Encoding(str, Enum):
pcm_s16le = "pcm_s16le"
pcm_mulaw = "pcm_mulaw"
# Raw Opus packets, one packet per binary WS message. `sample_rate` is
# optional and ignored — the Opus stream is self-describing.
# Raw Opus packets, one packet per binary WS message. `sample_rate` may be
# omitted — the Opus stream is self-describing and the server ignores it.
opus = "opus"
# Ogg-encapsulated Opus byte stream (ffmpeg, gstreamer, opusenc, browser
# MediaRecorder output). `sample_rate` is optional and ignored.
# MediaRecorder output). `sample_rate` may be omitted — the Opus stream is
# self-describing and the server ignores it.
ogg_opus = "ogg_opus"

def __str__(self):
Expand Down Expand Up @@ -263,7 +273,10 @@ def __str__(self):


class StreamingParameters(StreamingSessionParameters):
sample_rate: int
# Required for PCM encodings. May be omitted for Opus encodings
# (opus, ogg_opus) — the stream is self-describing and the server
# ignores the value.
sample_rate: Optional[int] = None
encoding: Optional[Encoding] = None
speech_model: Optional[SpeechModel] = None
# Deprecated: use language_codes instead (pass a single-element list, e.g.
Expand Down Expand Up @@ -291,6 +304,36 @@ class StreamingParameters(StreamingSessionParameters):
redact_pii_sub: Optional[StreamingPiiSubstitution] = None
mode: Optional[StreamingMode] = None

if pydantic_v2:

@model_validator(mode="after")
def _require_sample_rate_for_non_opus(self):
if self.sample_rate is None and self.encoding not in (
Encoding.opus,
Encoding.ogg_opus,
):
raise ValueError(
"sample_rate is required; it may only be omitted when "
"encoding is 'opus' or 'ogg_opus' (the Opus stream is "
"self-describing)."
)
return self

else:

@root_validator(skip_on_failure=True)
def _require_sample_rate_for_non_opus(cls, values):
if values.get("sample_rate") is None and values.get("encoding") not in (
Encoding.opus,
Encoding.ogg_opus,
):
raise ValueError(
"sample_rate is required; it may only be omitted when "
"encoding is 'opus' or 'ogg_opus' (the Opus stream is "
"self-describing)."
)
return values


class UpdateConfiguration(StreamingSessionParameters):
type: Literal["UpdateConfiguration"] = "UpdateConfiguration"
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/test_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import threading
import time
from types import SimpleNamespace
from typing import Optional
from urllib.parse import urlencode

import pytest
Expand Down Expand Up @@ -412,6 +413,53 @@ def mocked_websocket_connect(
assert f"encoding={encoding.value}" in actual_url


@pytest.mark.parametrize("encoding", [Encoding.opus, Encoding.ogg_opus])
def test_client_connect_opus_without_sample_rate(
mocker: MockFixture, encoding: Encoding
):
# Given: client + an Opus encoding and no sample_rate (the Opus stream is
# self-describing, so the parameter may be omitted)
actual_url = None

def mocked_websocket_connect(
url: str, additional_headers: dict, open_timeout: float
):
nonlocal actual_url
actual_url = url

mocker.patch(
"assemblyai.streaming.v3.client.websocket_connect",
new=mocked_websocket_connect,
)
_disable_rw_threads(mocker)
client = StreamingClient(
StreamingClientOptions(api_key="test", api_host="api.example.com")
)
params = StreamingParameters(
speech_model=SpeechModel.universal_3_5_pro,
encoding=encoding,
)

# When: connect
client.connect(params)

# Then: the encoding is forwarded and sample_rate is absent from the URL
assert f"encoding={encoding.value}" in actual_url
assert "sample_rate" not in actual_url


@pytest.mark.parametrize("encoding", [None, Encoding.pcm_s16le, Encoding.pcm_mulaw])
def test_sample_rate_required_for_non_opus_encodings(encoding: Optional[Encoding]):
# Given/When: constructing parameters without sample_rate for a PCM (or
# unset) encoding
# Then: validation rejects it
with pytest.raises(ValueError, match="sample_rate is required"):
StreamingParameters(
speech_model=SpeechModel.universal_3_5_pro,
encoding=encoding,
)


def test_noise_suppression_deprecated_alias_migrates_to_voice_focus(
mocker: MockFixture, caplog: pytest.LogCaptureFixture
):
Expand Down
Loading