You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 862d166
Browse filesBrowse the repository at this point in the historyBrowse files
feat(sync): push-style live sessions via open_live()
transcribe_live() takes an iterator, but most live audio arrives in a
callback: sounddevice, PyAudio, WebRTC tracks, telephony media streams,
websocket frame handlers. Every such caller had to build the same bridge
(a queue, a stop flag, a drain generator and, on the sync side, a thread
to run the blocking call in) before getting a transcript.
SyncTranscriber.open_live() returns a LiveSession and
AsyncSyncTranscriber.open_live() an AsyncLiveSession. The request starts
the moment the session opens, on the transcriber's worker pool or as a
loop task; write() queues audio without blocking (thread-safe on the sync
side), close() ends it, result() waits for the transcript, and abort()
drops the request. Context-manager exit closes on success and aborts if
the block raised. Chunk types are validated at write() so a str shows up
immediately rather than in result().
Both sessions are thin layers over transcribe_live(): no new transport
code, same errors, same 120 s cap and silence-abort semantics. The
iterator form stays for sources that already are iterators or file
objects.
README leads the live examples with the session now (sounddevice, and a
websocket handler for asyncio), and keeps transcribe_live() for
iterator and file-object sources. 19 tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
-`aai.Transcript` — Result object with `.text`, `.status`, `.utterances`, `.words`, `.chapters`, `.entities`, `.sentiment_analysis`. Methods: `get_sentences()`, `get_paragraphs()`, `export_subtitles_srt()`, `export_subtitles_vtt()`
47
-
-`aai.SyncTranscriber` — Synchronous pre-recorded transcription: audio in, transcript out, one request (no polling). Methods: `transcribe()`, `transcribe_live()`, `transcribe_async()`, `warm()`
48
-
-`aai.AsyncSyncTranscriber` — Asyncio counterpart of `SyncTranscriber`. Same input types, config, result, and errors; `transcribe()`, `transcribe_live()` and `warm()` are coroutines. Owns an HTTP pool: use `async with` or `await aclose()`, or pass an `aai.AsyncClient` to share one
47
+
-`aai.SyncTranscriber` — Synchronous pre-recorded transcription: audio in, transcript out, one request (no polling). Methods: `transcribe()`, `transcribe_live()`, `open_live()`, `transcribe_async()`, `warm()`
48
+
-`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
|`assemblyai.prerecorded.v2.Transcriber`| Long-form audio, URLs, and the audio-intelligence features (speaker labels, chapters, sentiment, …), over the polled job API |
104
104
|`assemblyai.prerecorded.v2.AsyncTranscriber`| The same, from asyncio code |
105
-
|`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 (`transcribe_live()`) |
105
+
|`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()`) |
106
106
|`assemblyai.sync.v1.AsyncSyncTranscriber`| The same, from asyncio code |
107
107
|`assemblyai.streaming.v3.RealTimeTranscriber`| Live audio (microphone, telephony, voice agents), transcribed as it arrives over a websocket session |
108
108
|`assemblyai.streaming.v3.AsyncRealTimeTranscriber`| The same, from asyncio code |
@@ -519,107 +519,116 @@ with aai.SyncTranscriber() as transcriber:
519
519
</details>
520
520
521
521
<details>
522
-
<summary>Transcribe live audio as it is recorded (`transcribe_live()`)</summary>
522
+
<summary>Transcribe live audio as it is recorded (`open_live()`)</summary>
523
523
524
-
`transcribe()` needs the whole clip before it can send anything. `transcribe_live()`starts the request immediately and uploads audio as your code produces it, so the upload and all but the last speech segment are done by the time the speaker stops. What remains is one final segment: on a paced 7 s clip that cut the wait after the last byte roughly in half.
524
+
`transcribe()` needs the whole clip before it can send anything. A live session starts the request immediately and uploads audio as you produce it, so the upload and all but the last speech segment are done by the time the speaker stops. What remains is one final segment: on a paced 7 s clip that cut the wait after the last byte roughly in half.
525
525
526
-
It takes an iterable of `bytes`chunks or a binary file object read as it fills, and returns the same `SyncTranscriptResponse` as `transcribe()`. Pass raw S16LE PCM with `sample_rate` and `channels` set; that is what microphones and telephony hand you, and it needs no container header.
526
+
Most audio sources hand you chunks in a callback. `open_live()` is built for that: `write()` from the callback, `close()` when the speaker stops, and `result()` for the transcript, which is the same `SyncTranscriptResponse` that `transcribe()` returns. The request runs on one of the transcriber's worker threads, so `write()` never blocks and is safe from any thread.
527
527
528
528
```python
529
-
import queue
530
-
import threading
531
-
532
529
import assemblyai as aai
533
530
import sounddevice as sd # pip install sounddevice
# leaving the block ends the audio; the final segment is all that is left
546
+
print(session.result().text)
547
+
```
540
548
541
-
defmicrophone(stop: threading.Event):
542
-
"""Yields PCM chunks from the default microphone until `stop` is set."""
543
-
chunks: queue.Queue[bytes] = queue.Queue()
544
-
with sd.RawInputStream(
545
-
samplerate=RATE, channels=1, dtype="int16",
546
-
callback=lambdadata, *_: chunks.put(bytes(data)),
547
-
):
548
-
whilenot stop.is_set():
549
-
try:
550
-
yield chunks.get(timeout=0.1)
551
-
except queue.Empty:
552
-
pass
549
+
Leaving the `with` block calls `close()`. If the block raises, the session is aborted instead and `result()` raises. Call `abort()` yourself to drop a recording you no longer want transcribed.
553
550
551
+
This is **not** the streaming API. A live session returns one finished transcript when the audio ends; use `aai.RealTimeTranscriber` when you need words back while the speaker is still talking.
554
552
555
-
stop = threading.Event()
556
-
threading.Thread(target=lambda: (input("Recording, press Enter to stop... "), stop.set()), daemon=True).start()
553
+
Use it only when the audio is genuinely still being produced. Streaming a file that is already on disk is slower than `transcribe()`, which sends it in one piece. Short clips gain less: below about a minute the saving is the elided upload only. The same 120 s cap applies, and the server aborts an upload that goes silent for long, so keep writing until you are done rather than pausing.
557
554
558
-
result = aai.SyncTranscriber().transcribe_live(microphone(stop), config=config)
559
-
print(result.text)
560
-
```
555
+
Errors that would normally arrive at the end (bad key, rate limit, capacity) can surface part-way through the upload; `result()` raises them as `SyncTranscriptError`. A bad key is reported at the first segment boundary, roughly 30 s in, so `warm()` before you start recording if you want to fail fast.
561
556
562
-
Any producer works, not only a microphone: a file object on a pipe or socket, chunks arriving over a websocket, a telephony media stream. End the iterator to finish. The server aborts an upload that goes silent for long, so keep sending until you are done rather than pausing.
557
+
</details>
563
558
564
-
This is **not** the streaming API. `transcribe_live()` returns one finished transcript when the audio ends; use `aai.RealTimeTranscriber` when you need words back while the speaker is still talking.
559
+
<details>
560
+
<summary>Transcribe live audio from an iterator or file object (`transcribe_live()`)</summary>
565
561
566
-
Use it only when the audio is genuinely still being produced. Streaming a file that is already on disk is slower than `transcribe()`, which sends it in one piece. Short clips gain less: below about a minute the saving is the elided upload only. The same 120 s cap applies.
562
+
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 takes an iterable of `bytes` chunks or a binary file object, blocks until the audio ends, and returns the transcript. `open_live()` is this method with a queue in front of it.
567
563
568
564
```python
569
565
import subprocess
566
+
import assemblyai as aai
570
567
571
-
# Audio you already hold whole: use transcribe(), which is faster for it.
result = aai.SyncTranscriber().transcribe_live(recorder.stdout, config=config)
577
+
578
+
# Or any generator of chunks:
579
+
deffrom_call(call):
580
+
while call.active:
581
+
yield call.read_frame()
582
+
583
+
result = aai.SyncTranscriber().transcribe_live(from_call(call), config=config)
584
+
585
+
# Audio you already hold whole belongs in transcribe(), which is faster for it:
586
+
result = aai.SyncTranscriber().transcribe("./call.wav")
577
587
```
578
588
579
-
Errors that would normally arrive at the end (bad key, rate limit, capacity) can surface part-way through the upload as a `SyncTranscriptError`. A bad key is reported at the first segment boundary, roughly 30 s in, so `warm()` before you start recording if you want to fail fast.
589
+
End the iterator to finish. Anything the producer raises propagates unchanged and drops the upload.
580
590
581
591
</details>
582
592
583
593
<details>
584
594
<summary>Transcribe live audio from asyncio</summary>
585
595
586
-
`AsyncSyncTranscriber.transcribe_live()`takes an async iterable of chunks as well as a file object or plain iterable. A file object is read in a worker thread, so a blocking `read()`is fine; a plain iterable is consumed inline and must not block the loop.
596
+
`AsyncSyncTranscriber.open_live()`returns an `AsyncLiveSession`: `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)`.
"""Yields PCM chunks pushed onto `frames` by a capture callback; None ends it."""
599
-
while (chunk :=await frames.get()) isnotNone:
600
-
yield chunk
601
-
606
+
asyncdefhandle(websocket):
607
+
"""Transcribes the PCM frames a browser sends over one websocket."""
608
+
asyncwith aai.AsyncSyncTranscriber() as transcriber:
609
+
asyncwith transcriber.open_live(config) as session:
610
+
asyncfor frame in websocket:
611
+
session.write(frame)
612
+
result =await session.result()
613
+
await websocket.send(result.text)
614
+
```
602
615
603
-
asyncdefmain():
604
-
frames: asyncio.Queue = asyncio.Queue()
605
-
start_capture(lambdapcm: frames.put_nowait(pcm)) # your audio callback
616
+
`AsyncSyncTranscriber.transcribe_live()` is the pull-style twin: it takes an async iterable of chunks as well as a file object or plain iterable. A file object is read in a worker thread, so a blocking `read()` is fine; a plain iterable is consumed inline and must not block the loop.
606
617
607
-
asyncwith aai.AsyncSyncTranscriber() as transcriber:
await asyncio.sleep(5) # ... or until the user stops speaking
610
-
frames.put_nowait(None) # end the iterator; the upload finishes
611
-
result =await task
612
-
print(result.text)
618
+
```python
619
+
asyncdefframes(websocket):
620
+
asyncfor frame in websocket:
621
+
yield frame
613
622
614
-
asyncio.run(main())
623
+
result =await transcriber.transcribe_live(frames(websocket), config=config)
615
624
```
616
625
617
626
</details>
618
627
619
628
<details>
620
629
<summary>Use it from asyncio (`AsyncSyncTranscriber`)</summary>
621
630
622
-
`aai.AsyncSyncTranscriber` is the asyncio counterpart of `aai.SyncTranscriber` — same input types, config, result, and errors, with `transcribe()`, `transcribe_live()` and `warm()` as coroutines. Use it in asyncio code (FastAPI, aiohttp, voice agents), where the threaded `transcribe()` would block the event loop and `transcribe_async()`'s `concurrent.futures.Future` is not awaitable.
631
+
`aai.AsyncSyncTranscriber` is the asyncio counterpart of `aai.SyncTranscriber` — same input types, config, result, and errors, with `transcribe()`, `transcribe_live()` and `warm()` as coroutines and `open_live()` returning an `AsyncLiveSession`. Use it in asyncio code (FastAPI, aiohttp, voice agents), where the threaded `transcribe()` would block the event loop and `transcribe_async()`'s `concurrent.futures.Future` is not awaitable.
0 commit comments