diff --git a/CLAUDE.md b/CLAUDE.md index 7e05561..634e622 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,7 +42,7 @@ aai.settings.api_key = "your-key" - `aai.AsyncTranscript` — What `AsyncTranscriber` returns. Same fields as `Transcript`, coroutine methods - `aai.AsyncClient` — Shared `httpx.AsyncClient` pool for one or more `AsyncTranscriber`s - `assemblyai.prerecorded.v2` — Canonical module for both transcribers, matching the `/v2/transcript` API. The top-level `aai.*` names re-export it -- `aai.TranscriptionConfig` — All transcription options: `speech_models`, `speaker_labels`, `sentiment_analysis`, `entity_detection`, `auto_chapters`, `content_safety`, `language_detection`, `summarization`, `word_boost`, `disfluencies` +- `aai.TranscriptionConfig` — All transcription options: `speech_models`, `speaker_labels`, `sentiment_analysis`, `entity_detection`, `auto_chapters`, `content_safety`, `language_detection`, `summarization`, `word_boost`, `disfluencies`, `speech_understanding` - `aai.Transcript` — Result object with `.text`, `.status`, `.utterances`, `.words`, `.chapters`, `.entities`, `.sentiment_analysis`. Methods: `get_sentences()`, `get_paragraphs()`, `export_subtitles_srt()`, `export_subtitles_vtt()` - `aai.SyncTranscriber` — Synchronous pre-recorded transcription: audio in, transcript out, one request (no polling). Methods: `transcribe()`, `transcribe_live()`, `open_live()`, `transcribe_async()`, `warm()` - `aai.AsyncSyncTranscriber` — Asyncio counterpart of `SyncTranscriber`. Same input types, config, result, and errors; `transcribe()`, `transcribe_live()` and `warm()` are coroutines; `open_live()` returns an `AsyncLiveSession`. Owns an HTTP pool: use `async with` or `await aclose()`, or pass an `aai.AsyncClient` to share one diff --git a/assemblyai/__init__.py b/assemblyai/__init__.py index a4761f1..fe7bc86 100644 --- a/assemblyai/__init__.py +++ b/assemblyai/__init__.py @@ -67,6 +67,7 @@ SpeakerOptions, SpeakerType, SpeechModel, + SpeechUnderstandingEffort, SpeechUnderstandingFeatureRequests, SpeechUnderstandingFeatureResponses, SpeechUnderstandingRequest, @@ -175,6 +176,7 @@ def __getattr__(name: str): "SpeakerOptions", "SpeakerType", "SpeechModel", + "SpeechUnderstandingEffort", "SpeechUnderstandingFeatureRequests", "SpeechUnderstandingFeatureResponses", "SpeechUnderstandingRequest", diff --git a/assemblyai/__version__.py b/assemblyai/__version__.py index f49459c..51bbb3f 100644 --- a/assemblyai/__version__.py +++ b/assemblyai/__version__.py @@ -1 +1 @@ -__version__ = "1.6.1" +__version__ = "1.6.2" diff --git a/assemblyai/types.py b/assemblyai/types.py index 92377f6..4a47611 100644 --- a/assemblyai/types.py +++ b/assemblyai/types.py @@ -698,6 +698,19 @@ class SpeakerType(str, Enum): "Identify speakers by their name" +class SpeechUnderstandingEffort(str, Enum): + """ + How much processing power to spend on a speech understanding task. 'medium' produces + higher quality results on harder audio at a higher cost. + """ + + low = "low" + "Standard processing effort" + + medium = "medium" + "Higher quality processing effort for harder audio" + + class SpeakerIdentificationRequest(BaseModel): """ Speaker identification configuration for speech understanding @@ -712,6 +725,9 @@ class SpeakerIdentificationRequest(BaseModel): speakers: Optional[List[Dict[str, Any]]] = None "Known speaker definitions with optional descriptions for improved accuracy" + effort: Optional[Union[SpeechUnderstandingEffort, str]] = None + "How much effort to spend on this task ('low' or 'medium')" + class TranslationRequest(BaseModel): """ @@ -727,6 +743,12 @@ class TranslationRequest(BaseModel): match_original_utterance: Optional[bool] = False "Whether to match the original utterance structure in translations (default: False)" + force_translation: Optional[bool] = False + "Whether to translate even when the detected source language equals the target language (default: False)" + + effort: Optional[Union[SpeechUnderstandingEffort, str]] = None + "How much effort to spend on this task ('low' or 'medium')" + class CustomFormattingRequest(BaseModel): """ @@ -742,6 +764,9 @@ class CustomFormattingRequest(BaseModel): email: Optional[str] = None "Custom email format pattern (e.g., 'username@domain.com')" + effort: Optional[Union[SpeechUnderstandingEffort, str]] = None + "How much effort to spend on this task ('low' or 'medium')" + class SpeechUnderstandingFeatureRequests(BaseModel): """ diff --git a/tests/unit/test_speech_understanding.py b/tests/unit/test_speech_understanding.py new file mode 100644 index 0000000..04f770c --- /dev/null +++ b/tests/unit/test_speech_understanding.py @@ -0,0 +1,186 @@ +import assemblyai as aai + + +def test_speech_understanding_effort_enum(): + assert aai.SpeechUnderstandingEffort.low == "low" + assert aai.SpeechUnderstandingEffort.medium == "medium" + assert aai.SpeechUnderstandingEffort.low.value == "low" + assert aai.SpeechUnderstandingEffort.medium.value == "medium" + + +def test_speaker_identification_request_effort_enum(): + req = aai.SpeakerIdentificationRequest( + speaker_type=aai.SpeakerType.name, + effort=aai.SpeechUnderstandingEffort.medium, + ) + assert req.speaker_type == aai.SpeakerType.name + assert req.effort == aai.SpeechUnderstandingEffort.medium + data = req.dict(exclude_none=True) + assert data["effort"] == "medium" + assert data["speaker_type"] == "name" + + +def test_speaker_identification_request_effort_str(): + req = aai.SpeakerIdentificationRequest( + speaker_type=aai.SpeakerType.role, + known_values=["Agent", "Customer"], + effort="low", + ) + assert req.speaker_type == aai.SpeakerType.role + assert req.known_values == ["Agent", "Customer"] + assert req.effort == "low" + data = req.dict(exclude_none=True) + assert data["effort"] == "low" + + +def test_speaker_identification_request_effort_omitted(): + req = aai.SpeakerIdentificationRequest( + speaker_type=aai.SpeakerType.name, + ) + assert req.effort is None + data = req.dict(exclude_none=True) + assert "effort" not in data + + +def test_translation_request_effort_and_force_translation(): + req = aai.TranslationRequest( + target_languages=["es", "fr"], + force_translation=True, + effort=aai.SpeechUnderstandingEffort.medium, + ) + assert req.target_languages == ["es", "fr"] + assert req.force_translation is True + assert req.effort == aai.SpeechUnderstandingEffort.medium + assert req.formal is False + assert req.match_original_utterance is False + + data = req.dict(exclude_none=True) + assert data["target_languages"] == ["es", "fr"] + assert data["force_translation"] is True + assert data["effort"] == "medium" + + +def test_translation_request_defaults(): + req = aai.TranslationRequest(target_languages=["de"]) + assert req.force_translation is False + assert req.effort is None + + data = req.dict(exclude_none=True) + assert data["target_languages"] == ["de"] + assert data["force_translation"] is False + assert "effort" not in data + + +def test_custom_formatting_request_effort(): + req = aai.CustomFormattingRequest( + date="mm/dd/yyyy", + phone_number="(xxx)xxx-xxxx", + effort=aai.SpeechUnderstandingEffort.low, + ) + assert req.date == "mm/dd/yyyy" + assert req.phone_number == "(xxx)xxx-xxxx" + assert req.effort == aai.SpeechUnderstandingEffort.low + + data = req.dict(exclude_none=True) + assert data["date"] == "mm/dd/yyyy" + assert data["phone_number"] == "(xxx)xxx-xxxx" + assert data["effort"] == "low" + assert "email" not in data + + +def test_speech_understanding_request_nested_effort(): + req = aai.SpeechUnderstandingRequest( + request=aai.SpeechUnderstandingFeatureRequests( + speaker_identification=aai.SpeakerIdentificationRequest( + speaker_type=aai.SpeakerType.name, + effort=aai.SpeechUnderstandingEffort.medium, + ), + translation=aai.TranslationRequest( + target_languages=["es"], + force_translation=True, + effort=aai.SpeechUnderstandingEffort.low, + ), + custom_formatting=aai.CustomFormattingRequest( + date="mm/dd/yyyy", + effort=aai.SpeechUnderstandingEffort.medium, + ), + ) + ) + + data = req.dict(exclude_none=True) + assert data["request"]["speaker_identification"]["effort"] == "medium" + assert data["request"]["translation"]["effort"] == "low" + assert data["request"]["translation"]["force_translation"] is True + assert data["request"]["custom_formatting"]["effort"] == "medium" + + +def test_transcription_config_speech_understanding_dict_with_effort_and_force_translation(): + config = aai.TranscriptionConfig() + config.speech_understanding = { + "request": { + "speaker_identification": { + "speaker_type": "name", + "effort": "medium", + }, + "translation": { + "target_languages": ["es"], + "force_translation": True, + "effort": "low", + }, + "custom_formatting": { + "date": "mm/dd/yyyy", + "effort": "medium", + }, + } + } + + assert isinstance(config.speech_understanding, aai.SpeechUnderstandingRequest) + features = config.speech_understanding.request + assert features.speaker_identification.speaker_type == aai.SpeakerType.name + assert features.speaker_identification.effort == "medium" + assert features.translation.target_languages == ["es"] + assert features.translation.force_translation is True + assert features.translation.effort == "low" + assert features.custom_formatting.date == "mm/dd/yyyy" + assert features.custom_formatting.effort == "medium" + + +def test_speech_understanding_response_parses_request_with_effort_and_force_translation(): + raw_response = { + "request": { + "speaker_identification": { + "speaker_type": "name", + "effort": "medium", + }, + "translation": { + "target_languages": ["es"], + "force_translation": True, + "effort": "low", + }, + "custom_formatting": { + "date": "mm/dd/yyyy", + "effort": "medium", + }, + }, + "response": { + "speaker_identification": { + "status": "success", + "mapping": {"Speaker A": "Alice"}, + }, + "translation": { + "status": "success", + }, + "custom_formatting": { + "status": "success", + "formatted_text": "Call me at (123)456-7890.", + }, + }, + } + + res = aai.SpeechUnderstandingResponse.parse_obj(raw_response) + assert res.request.speaker_identification.effort == "medium" + assert res.request.translation.effort == "low" + assert res.request.translation.force_translation is True + assert res.request.custom_formatting.effort == "medium" + assert res.response.speaker_identification.status == "success" + assert res.response.speaker_identification.mapping == {"Speaker A": "Alice"}