Skip to content

Commit 72c272f

Browse files
ccampbell-aaiAssemblyAI
andauthored
chore: sync sdk code with DeepLearning repo (#214)
Co-authored-by: AssemblyAI <engineering.sdk@assemblyai.com>
1 parent 4e805eb commit 72c272f

11 files changed

Lines changed: 356 additions & 674 deletions

File tree

CLAUDE.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ aai.settings.api_key = "your-key"
4141
- `aai.TranscriptionConfig` — All transcription options: `speech_models`, `speaker_labels`, `sentiment_analysis`, `entity_detection`, `auto_chapters`, `content_safety`, `language_detection`, `summarization`, `word_boost`, `disfluencies`
4242
- `aai.Transcript` — Result object with `.text`, `.status`, `.utterances`, `.words`, `.chapters`, `.entities`, `.sentiment_analysis`. Methods: `get_sentences()`, `get_paragraphs()`, `export_subtitles_srt()`, `export_subtitles_vtt()`
4343
- `aai.SyncTranscriber` — Synchronous pre-recorded transcription: audio in, transcript out, one request (no polling). Methods: `transcribe()`, `transcribe_async()`
44-
- `aai.SyncTranscriptionConfig` — Sync options: `model` (default `u3-sync-pro`), `prompt`, `word_boost`, `conversation_context`, `language_code`, `sample_rate`, `channels`
45-
- `aai.SyncTranscriptResponse` — Sync result: `.text`, `.words` (`Word` type with `start`/`end`/`confidence`), `.confidence`, `.audio_duration_ms`, `.session_id`, `.request_time_ms`
44+
- `aai.SyncTranscriptionConfig` — Sync options: `model` (default `u3-sync-pro`), `prompt`, `keyterms_prompt`, `conversation_context`, `language_codes`, `timestamps`, `sample_rate`, `channels`
45+
- `aai.SyncTranscriptResponse` — Sync result: `.text`, `.words` (`SyncWord` with `confidence` always, `start`/`end` only when `timestamps=True`), `.confidence`, `.audio_duration_ms`, `.session_id`, `.request_time_ms`
4646
- `assemblyai.streaming.v3.StreamingClient` — Real-time streaming with event-based API (threaded)
4747
- `assemblyai.streaming.v3.AsyncStreamingClient` — Asyncio-native counterpart; same options/events
4848

@@ -97,7 +97,7 @@ aai.settings.api_key = os.environ["ASSEMBLYAI_API_KEY"]
9797
result = aai.SyncTranscriber().transcribe("./call.wav")
9898
print(result.text, result.session_id)
9999
for w in result.words:
100-
print(w.text, w.start, w.end, w.confidence)
100+
print(w.text, w.confidence) # w.start/w.end need timestamps=True (see below)
101101
```
102102

103103
**Input**: a local file path, raw `bytes`, or a binary file object. **Not** a URL —
@@ -107,8 +107,8 @@ pass a path/bytes or use `Transcriber` for URL ingestion.
107107
```python
108108
config = aai.SyncTranscriptionConfig(
109109
prompt="Transcribe verbatim. Preserve disfluencies.", # max 4096 chars
110-
word_boost=["AssemblyAI", "Lemur", "U3-Pro"], # max 2048 chars total
111-
language_code="es", # or ["en", "es"]; defaults to English
110+
keyterms_prompt=["AssemblyAI", "Lemur", "U3-Pro"], # max 2048 chars total
111+
language_codes=["es"], # or e.g. ["en", "es"] for multilingual; defaults to English
112112
)
113113
result = aai.SyncTranscriber().transcribe("./call.wav", config=config)
114114
```
@@ -129,9 +129,20 @@ config = aai.SyncTranscriptionConfig(
129129
result = aai.SyncTranscriber().transcribe("./reply.wav", config=config)
130130
```
131131

132-
**Language**: `language_code` takes an ISO 639-1 code (or list of codes for multilingual
133-
audio) and steers the default prompt toward that language — ignored when you pass a custom
134-
`prompt`. Supported: en, es, de, fr, it, pt, tr, nl, sv, no, da, fi, hi, vi, ar, he, ja, ur, zh.
132+
**Language**: `language_codes` takes a list of ISO 639-1 codes — a single-element list
133+
for monolingual audio or several codes for multilingual audio — and steers the default
134+
prompt toward those languages; ignored when you pass a custom `prompt`.
135+
Supported: en, es, de, fr, it, pt, tr, nl, sv, no, da, fi, hi, vi, ar, he, ja, ur, zh.
136+
137+
**Word timestamps** are opt-in. By default words carry `text` and `confidence` only —
138+
`start`/`end` are `None`. `timestamps=True` computes accurate per-word timings for a
139+
small latency cost:
140+
```python
141+
config = aai.SyncTranscriptionConfig(timestamps=True)
142+
result = aai.SyncTranscriber().transcribe("./call.wav", config=config)
143+
for w in result.words:
144+
print(w.text, w.start, w.end) # milliseconds
145+
```
135146

136147
**Raw PCM** (S16LE) needs `sample_rate` + `channels`; WAV reads them from its header.
137148
Setting either field routes the audio as `audio/pcm`, and both must be present:

assemblyai/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
SyncTranscriptError,
6969
SyncTranscriptionConfig,
7070
SyncTranscriptResponse,
71+
SyncWord,
7172
Timestamp,
7273
TranscriptError,
7374
TranscriptionConfig,
@@ -148,6 +149,7 @@
148149
"SyncTranscriptError",
149150
"SyncTranscriptionConfig",
150151
"SyncTranscriptResponse",
152+
"SyncWord",
151153
"Timestamp",
152154
"Transcriber",
153155
"TranscriptionConfig",

assemblyai/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.64.26"
1+
__version__ = "0.64.29"

assemblyai/sync/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Backwards-compatibility package for the old flat ``sync.py`` module.
2+
3+
The sync (single-request) transcription product now lives in ``sync/v1/``,
4+
mirroring the ``streaming/v3/`` layout. This ``__init__`` re-exports the full
5+
surface the old ``assemblyai.sync`` module exposed so every existing import —
6+
``from assemblyai.sync import SyncTranscriber``, the ``AudioInput`` alias, and
7+
the private helpers — keeps working silently.
8+
"""
9+
10+
from .v1._base import ( # noqa: F401
11+
_PCM_SUFFIXES,
12+
AudioInput,
13+
_config_to_json,
14+
_resolve_audio,
15+
_SyncTranscriberImpl,
16+
)
17+
from .v1.client import SyncTranscriber
18+
19+
__all__ = [
20+
"AudioInput",
21+
"SyncTranscriber",
22+
]

assemblyai/sync/v1/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from ...types import (
2+
SyncSpeechModel,
3+
SyncTranscriptError,
4+
SyncTranscriptionConfig,
5+
SyncTranscriptResponse,
6+
SyncWord,
7+
)
8+
from ._base import AudioInput
9+
from .client import SyncTranscriber
10+
11+
__all__ = [
12+
"AudioInput",
13+
"SyncSpeechModel",
14+
"SyncTranscriber",
15+
"SyncTranscriptError",
16+
"SyncTranscriptionConfig",
17+
"SyncTranscriptResponse",
18+
"SyncWord",
19+
]

assemblyai/sync/v1/_base.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from __future__ import annotations
2+
3+
import os
4+
from typing import BinaryIO, Optional, Tuple, Union
5+
from urllib.parse import urlparse
6+
7+
from ... import client as _client
8+
from ... import types
9+
from . import api
10+
11+
AudioInput = Union[str, bytes, bytearray, "os.PathLike[str]", BinaryIO]
12+
13+
# Extensions that signal raw S16LE PCM rather than a WAV container.
14+
_PCM_SUFFIXES = (".pcm", ".raw")
15+
16+
17+
def _resolve_audio(
18+
data: AudioInput,
19+
config: types.SyncTranscriptionConfig,
20+
) -> Tuple[bytes, str, str]:
21+
"""
22+
Reads the audio input into bytes and decides its multipart Content-Type.
23+
24+
PCM is selected when the source has a `.pcm`/`.raw` extension or when
25+
`sample_rate`/`channels` are set on the config (the fields the sync API
26+
requires only for raw PCM) — and both must then be present. Everything
27+
else is treated as a WAV container. URLs are rejected — the sync API has
28+
no URL ingestion.
29+
30+
Returns: `(audio_bytes, filename, content_type)`.
31+
"""
32+
suffix = ""
33+
filename: Optional[str] = None
34+
35+
if isinstance(data, (bytes, bytearray)):
36+
audio = bytes(data)
37+
elif isinstance(data, (str, os.PathLike)):
38+
path = os.fspath(data)
39+
if urlparse(path).scheme in ("http", "https"):
40+
raise ValueError(
41+
"SyncTranscriber does not accept URLs. Pass a local file path or "
42+
"audio bytes, or use aai.Transcriber for URL/async transcription."
43+
)
44+
with open(path, "rb") as f:
45+
audio = f.read()
46+
filename = os.path.basename(path)
47+
suffix = os.path.splitext(path)[1].lower()
48+
elif hasattr(data, "read"):
49+
audio = data.read()
50+
name = getattr(data, "name", None)
51+
if name:
52+
filename = os.path.basename(name)
53+
suffix = os.path.splitext(name)[1].lower()
54+
else:
55+
raise TypeError(f"unsupported audio input type: {type(data).__name__}")
56+
57+
wants_pcm = config.sample_rate is not None or config.channels is not None
58+
is_pcm = suffix in _PCM_SUFFIXES or wants_pcm
59+
if is_pcm and (config.sample_rate is None or config.channels is None):
60+
raise ValueError(
61+
"raw PCM audio requires both sample_rate and channels in "
62+
"SyncTranscriptionConfig"
63+
)
64+
65+
content_type = "audio/pcm" if is_pcm else "audio/wav"
66+
if not filename:
67+
filename = "audio.pcm" if is_pcm else "audio.wav"
68+
69+
return audio, filename, content_type
70+
71+
72+
def _config_to_json(config: types.SyncTranscriptionConfig) -> Optional[dict]:
73+
"""Serializes the config to the JSON `config` part, dropping the routing model."""
74+
data = config.dict(exclude_none=True)
75+
data.pop("model", None)
76+
return data or None
77+
78+
79+
class _SyncTranscriberImpl:
80+
def __init__(
81+
self,
82+
*,
83+
client: _client.Client,
84+
config: types.SyncTranscriptionConfig,
85+
) -> None:
86+
self._client = client
87+
self.config = config
88+
89+
def transcribe(
90+
self,
91+
*,
92+
data: AudioInput,
93+
config: Optional[types.SyncTranscriptionConfig],
94+
) -> types.SyncTranscriptResponse:
95+
config = config or self.config
96+
audio, filename, content_type = _resolve_audio(data, config)
97+
return api.transcribe(
98+
self._client.http_client,
99+
base_url=self._client.settings.sync_base_url,
100+
audio=audio,
101+
filename=filename,
102+
audio_content_type=content_type,
103+
model=config.model,
104+
config=_config_to_json(config),
105+
timeout=self._client.settings.sync_http_timeout,
106+
)

assemblyai/sync/v1/api.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import json
2+
from typing import Dict, Optional, Tuple
3+
4+
import httpx
5+
6+
from ... import types
7+
8+
# Canonical paths since the sync API gained a /v1 prefix (#18103); the
9+
# unprefixed routes remain served for SDK versions that predate it.
10+
ENDPOINT_TRANSCRIBE = "/v1/transcribe"
11+
ENDPOINT_WARM = "/v1/warm"
12+
MODEL_HEADER = "X-AAI-Model"
13+
14+
15+
def _error_from_response(response: httpx.Response) -> types.SyncTranscriptError:
16+
"""
17+
Builds a `SyncTranscriptError` from a non-200 response.
18+
19+
The service returns an RFC 9457 problem-details envelope
20+
(`{"status", "title", "detail"}`); `error_code` is the snake_cased
21+
`title` (e.g. `"Audio Too Large"` -> `audio_too_large`). Older envelopes
22+
(`{"error_code", "message"}` and `{"detail"}`) are still accepted.
23+
"""
24+
error_code: Optional[str] = None
25+
message: Optional[str] = None
26+
27+
try:
28+
body = response.json()
29+
if isinstance(body, dict):
30+
error_code = body.get("error_code")
31+
title = body.get("title")
32+
if error_code is None and isinstance(title, str) and title:
33+
error_code = title.lower().replace(" ", "_")
34+
message = body.get("detail") or body.get("message")
35+
except Exception:
36+
message = response.text or None
37+
38+
if not message:
39+
message = f"sync transcription failed with status {response.status_code}"
40+
41+
retry_after_header = response.headers.get("retry-after")
42+
retry_after = (
43+
int(retry_after_header)
44+
if retry_after_header and retry_after_header.isdigit()
45+
else None
46+
)
47+
48+
return types.SyncTranscriptError(
49+
message,
50+
status_code=response.status_code,
51+
error_code=error_code,
52+
retry_after=retry_after,
53+
)
54+
55+
56+
def transcribe(
57+
client: httpx.Client,
58+
*,
59+
base_url: str,
60+
audio: bytes,
61+
filename: str,
62+
audio_content_type: str,
63+
model: str,
64+
config: Optional[dict],
65+
timeout: float,
66+
) -> types.SyncTranscriptResponse:
67+
"""
68+
Posts a single synchronous transcription request.
69+
70+
Args:
71+
client: the HTTP client (carries the `Authorization` header).
72+
base_url: the sync API base URL, e.g. `https://sync.assemblyai.com`.
73+
audio: raw audio bytes (WAV container or S16LE PCM).
74+
filename: name for the audio multipart part.
75+
audio_content_type: `audio/wav` or `audio/pcm`; selects the decoder.
76+
model: sent as the `X-AAI-Model` routing header.
77+
config: the JSON `config` part, or None to omit it.
78+
timeout: per-request timeout in seconds.
79+
80+
Returns: the parsed transcript response.
81+
82+
Raises: `SyncTranscriptError` on any non-200 response.
83+
"""
84+
files: Dict[str, Tuple[Optional[str], bytes, str]] = {
85+
"audio": (filename, audio, audio_content_type)
86+
}
87+
if config:
88+
# httpx <0.23 rejects a `str` multipart part; encode to bytes so the
89+
# config part works across the full supported httpx range (>=0.19).
90+
files["config"] = (
91+
None,
92+
json.dumps(config).encode("utf-8"),
93+
"application/json",
94+
)
95+
96+
response = client.post(
97+
base_url.rstrip("/") + ENDPOINT_TRANSCRIBE,
98+
files=files,
99+
headers={MODEL_HEADER: model},
100+
timeout=timeout,
101+
)
102+
103+
if response.status_code != httpx.codes.OK:
104+
raise _error_from_response(response)
105+
106+
return types.SyncTranscriptResponse.parse_obj(response.json())

0 commit comments

Comments
 (0)