Skip to content
Merged
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ aai.settings.api_key = "your-key"
- `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
- `aai.SyncTranscriptionConfig` — Sync options: `model` (default `universal-3-5-pro`), `prompt`, `keyterms_prompt`, `conversation_context`, `language_codes`, `timestamps`, `sample_rate`, `channels`
- `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`
- `aai.DictationTranscriber` — Dictation API: audio uploaded as it is spoken over one live request, transcript (and optional LLM pass) out. Methods: `open_live()` (push-style `DictationLiveSession`: `write()`/`close()`/`result()`/`abort()`), `transcribe_live()` (iterable, file object, bytes or path), `warm()`. There is no buffered `transcribe()`: every entry point posts to `/v1/transcribe/live`
- `aai.AsyncDictationTranscriber` — Asyncio counterpart of `DictationTranscriber`; `open_live()` returns an `AsyncDictationLiveSession`. Owns an HTTP pool: use `async with` or `await aclose()`
- `aai.DictationConfig` — Dictation options: `sample_rate`, `channels`, `language_codes`, `keyterms_prompt`, `llm_instruction`. Rejects unknown fields
- `aai.DictationResponse` — Dictation result: `.text`, `.words` (`DictationWord` with `text` + `confidence`), `.confidence`, `.llm_response`, `.llm_error`, `.audio_duration_ms`, `.session_id`, `.request_time_ms`, `.sync_time_ms`, and `.final_text` (the LLM rewrite, falling back to `.text`)
- `assemblyai.streaming.v3.RealTimeTranscriber` — Real-time streaming with event-based API (threaded)
- `assemblyai.streaming.v3.AsyncRealTimeTranscriber` — Asyncio-native counterpart; same options/events

Expand Down
208 changes: 207 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,12 @@ aai.settings.api_key = f"{ASSEMBLYAI_API_KEY}"
| `assemblyai.prerecorded.v2.AsyncTranscriber` | The same, from asyncio code |
| `assemblyai.sync.v1.SyncTranscriber` | Short clips (≤120s, ≤40MB) where you want the transcript back in one request, at the lowest latency — from a file you already have (`transcribe()`) or uploaded while it is still being recorded (`open_live()`, `transcribe_live()`) |
| `assemblyai.sync.v1.AsyncSyncTranscriber` | The same, from asyncio code |
| `assemblyai.dictation.v1.DictationTranscriber` | Dictation: short spoken notes uploaded as they are spoken, returned as one transcript with an optional LLM rewrite (`final_text`). Push audio from a callback (`open_live()`) or hand over an iterator, file object, bytes or path (`transcribe_live()`) |
| `assemblyai.dictation.v1.AsyncDictationTranscriber` | The same, from asyncio code |
| `assemblyai.streaming.v3.RealTimeTranscriber` | Live audio (microphone, telephony, voice agents), transcribed as it arrives over a websocket session |
| `assemblyai.streaming.v3.AsyncRealTimeTranscriber` | The same, from asyncio code |

The versioned path is the preferred import for new code. The prerecorded and sync classes are also available as top-level shortcuts (`aai.Transcriber`, `aai.SyncTranscriber`, …) — see [Migrating to 1.0](MIGRATION.md) for the details.
The versioned path is the preferred import for new code. The prerecorded, sync and dictation classes are also available as top-level shortcuts (`aai.Transcriber`, `aai.SyncTranscriber`, `aai.DictationTranscriber`, …) — see [Migrating to 1.0](MIGRATION.md) for the details.

---

Expand Down Expand Up @@ -677,6 +679,205 @@ except aai.SyncTranscriptError as error:

---

### **Dictation Examples**

`aai.DictationTranscriber` is for dictation: a person speaks a short note and gets it back as text, optionally cleaned up or reformatted by an LLM. It has one connection, the live upload — audio is sent as it is spoken, the service transcribes each speech segment as it lands, and when the speaker stops what remains is the final segment and the LLM pass. There is no job id and no polling. Audio must be WAV or raw 16-bit PCM, up to 120 s per request.

The result is a `DictationResponse`: `text` is the raw transcript, `llm_response` the rewritten text when the LLM pass ran, and `final_text` is the one to show the user — the rewrite when there is one, the raw transcript otherwise.

<details>
<summary>Dictate from a microphone (`open_live()`)</summary>

Most audio sources deliver chunks in a callback. `open_live()` is built for that: the request starts the moment the session opens, `write()` from the callback (safe from any thread, never blocks), leave the `with` block when the speaker stops, and `result()` returns the transcript.

```python
import assemblyai as aai
import sounddevice as sd # pip install sounddevice

aai.settings.api_key = "<YOUR_API_KEY>"

RATE = 16000
config = aai.DictationConfig(sample_rate=RATE, channels=1) # raw PCM needs both

with aai.DictationTranscriber() as transcriber:
with transcriber.open_live(config) as session:
microphone = sd.RawInputStream(
samplerate=RATE, channels=1, dtype="int16",
callback=lambda data, *_: session.write(bytes(data)),
)
with microphone:
input("Dictating, press Enter to stop... ")
# leaving the block ends the audio; the final segment is all that is left
print(session.result().final_text)
```

Leaving the `with` block calls `close()`. If the block raises, the session is aborted instead and `result()` raises. Call `abort()` yourself to drop a dictation you no longer want transcribed.

This is **not** the streaming API. A session returns one finished transcript when the audio ends; use `aai.RealTimeTranscriber` when you need words back while the speaker is still talking.

The server aborts an upload that goes silent for long, so keep writing until the speaker is done rather than pausing. Errors that would normally arrive at the end (bad key, rate limit, capacity, audio too large) can surface part-way through the upload; `result()` raises them as `DictationError`.

</details>

<details>
<summary>Dictate from an iterator, file object, bytes or path (`transcribe_live()`)</summary>

When the audio already comes as something you can iterate, or as a file object that fills over time, hand it to `transcribe_live()` directly. It blocks until the audio ends and returns the transcript. `open_live()` is this method with a queue in front of it.

Audio you already hold whole — raw `bytes`, or a local path — goes to `transcribe_live()` too. It travels the same connection as a single chunk; there is no separate buffered request.

```python
import subprocess
import assemblyai as aai

aai.settings.api_key = "<YOUR_API_KEY>"
config = aai.DictationConfig(sample_rate=16000, channels=1)

# A file object that fills over time, e.g. the stdout of a recorder (sox):
recorder = subprocess.Popen(
["rec", "-q", "-t", "raw", "-r", "16000", "-c", "1", "-b", "16", "-e", "signed", "-"],
stdout=subprocess.PIPE,
)
result = aai.DictationTranscriber().transcribe_live(recorder.stdout, config=config)

# Or any generator of chunks:
def from_call(call):
while call.active:
yield call.read_frame()

result = aai.DictationTranscriber().transcribe_live(from_call(call), config=config)

# Or a recording you already have (WAV carries its own rate and channels):
result = aai.DictationTranscriber().transcribe_live("./note.wav")
print(result.final_text)
```

End the iterator to finish. Anything the producer raises propagates unchanged and drops the upload. URLs are not accepted; use `aai.Transcriber` for URL ingestion.

</details>

<details>
<summary>Rewrite the transcript with an LLM instruction</summary>

Set `llm_instruction` to have the service run a follow-up pass over the transcript. The rewrite comes back in `llm_response`; the raw transcript stays in `text`. If the pass fails, `llm_error` says why and `final_text` falls back to `text`, so reading `final_text` is safe either way.

```python
import assemblyai as aai

aai.settings.api_key = "<YOUR_API_KEY>"

config = aai.DictationConfig(llm_instruction="Format this as a SOAP note.")

result = aai.DictationTranscriber().transcribe_live("./visit.wav", config=config)

print(result.final_text) # the SOAP note
print(result.text) # what was actually said
if result.llm_error:
print("LLM pass failed:", result.llm_error)
```

The instruction is capped at 2048 characters.

</details>

<details>
<summary>Configure the dictation</summary>

```python
import assemblyai as aai

aai.settings.api_key = "<YOUR_API_KEY>"

config = aai.DictationConfig(
language_codes=["en", "es"], # one code, or several for multilingual audio
keyterms_prompt=["AssemblyAI", "Universal"], # bias the decoder; ≤2048 chars total
llm_instruction="Fix punctuation only.", # optional LLM pass
)

result = aai.DictationTranscriber().transcribe_live("./note.wav", config=config)
```

`sample_rate` and `channels` are required only for raw PCM; WAV carries them in its own header. A config passed to `transcribe_live()` or `open_live()` overrides the transcriber's default config for that call. `DictationConfig` rejects unknown fields, so a typo or a sync-only option surfaces as a validation error instead of a setting that quietly does nothing.

</details>

<details>
<summary>Pre-warm the connection</summary>

A request that connects on demand pays the full DNS + TCP + TLS handshake before the first audio byte can leave. Call `warm()` as soon as you know audio is coming — when the user reaches for the record button — so the request that follows reuses the open connection.

```python
import assemblyai as aai

aai.settings.api_key = "<YOUR_API_KEY>"

with aai.DictationTranscriber() as transcriber:
transcriber.warm() # fire when the user is about to speak
with transcriber.open_live(config) as session:
record_into(session.write)
print(session.result().final_text)
```

</details>

<details>
<summary>Use it from asyncio (`AsyncDictationTranscriber`)</summary>

`aai.AsyncDictationTranscriber` is the asyncio counterpart — same audio sources, config, result and errors, with `transcribe_live()` and `warm()` as coroutines. `open_live()` returns an `AsyncDictationLiveSession`: `write()` and `close()` are plain functions so a callback can call them, and `result()` and `abort()` are coroutines. The request runs as a task on the event loop. `write()` must be called on the loop's thread; from an audio library's capture thread, schedule it with `loop.call_soon_threadsafe(session.write, chunk)`.

```python
import asyncio
import assemblyai as aai

aai.settings.api_key = "<YOUR_API_KEY>"
config = aai.DictationConfig(sample_rate=16000, channels=1)


async def handle(websocket):
"""Transcribes the PCM frames a browser sends over one websocket."""
async with aai.AsyncDictationTranscriber() as transcriber:
async with transcriber.open_live(config) as session:
async for frame in websocket:
session.write(frame)
result = await session.result()
await websocket.send(result.final_text)
```

`transcribe_live()` also takes an async iterable of chunks, as well as a file object, plain iterable, bytes or path. A file object or path is read in a worker thread, so a blocking `read()` is fine; a plain iterable is consumed inline and must not block the loop.

```python
async def frames(websocket):
async for frame in websocket:
yield frame

result = await transcriber.transcribe_live(frames(websocket), config=config)
```

The transcriber owns an HTTP connection pool: use `async with`, or call `await transcriber.aclose()`. To share one pool between transcribers, pass an `aai.AsyncClient`, which stays yours to close.

</details>

<details>
<summary>Handle errors</summary>

Failures raise `aai.DictationError` with the HTTP `status_code`, a machine-readable `error_code` (`bad_audio`, `audio_too_large`, `capacity_exceeded`, `inference_timeout`, …), and `retry_after` (seconds) on 429/503 responses. It is a separate class from `aai.SyncTranscriptError`.

```python
import assemblyai as aai

aai.settings.api_key = "<YOUR_API_KEY>"

try:
result = aai.DictationTranscriber().transcribe_live("./note.wav")
print(result.final_text)
except aai.DictationError as error:
print(error.status_code, error.error_code, error.retry_after)
```

</details>

---

### **Asyncio Examples**

`aai.AsyncTranscriber` is the asyncio counterpart of `aai.Transcriber`. Every method
Expand Down Expand Up @@ -1393,6 +1594,11 @@ aai.settings.polling_interval = 10.0
# need to cover the length of the recording.
aai.settings.sync_http_timeout = 60.0
aai.settings.sync_live_http_timeout = 180.0

# The Dictation API host and its per-operation timeout (default 300.0), sized
# to outlast the final segment's inference plus the LLM pass.
aai.settings.dictation_base_url = "https://dictation.assemblyai.com"
aai.settings.dictation_http_timeout = 300.0
```

</details>
Expand Down
14 changes: 14 additions & 0 deletions assemblyai/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from .__version__ import __version__
from .async_client import AsyncClient
from .client import Client
from .dictation.v1 import (
AsyncDictationTranscriber,
DictationConfig,
DictationError,
DictationResponse,
DictationTranscriber,
DictationWord,
)
from .prerecorded.v2 import AsyncTranscriber, AsyncTranscript
from .sync.v1 import AsyncSyncTranscriber, SyncTranscriber
from .transcriber import Transcriber, Transcript, TranscriptGroup
Expand Down Expand Up @@ -96,6 +104,7 @@ def __getattr__(name: str):
# types
"AssemblyAIError",
"AsyncClient",
"AsyncDictationTranscriber",
"AsyncSyncTranscriber",
"AsyncTranscriber",
"AsyncTranscript",
Expand All @@ -109,6 +118,11 @@ def __getattr__(name: str):
"ContentSafetySeverityScore",
"CustomFormattingRequest",
"CustomFormattingResponse",
"DictationConfig",
"DictationError",
"DictationResponse",
"DictationTranscriber",
"DictationWord",
"Entity",
"EntityType",
"IABLabelResult",
Expand Down
2 changes: 1 addition & 1 deletion assemblyai/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.4.0"
__version__ = "1.5.0"
Loading
Loading