Skip to content

Commit 85d5879

Browse files
committed
update example
1 parent 16f70f4 commit 85d5879

6 files changed

Lines changed: 138 additions & 137 deletions

File tree

README.md

Lines changed: 49 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -170,17 +170,20 @@ async for chat_completion in stream:
170170

171171
## Realtime transcription
172172

173-
Stream audio over WebSocket and receive interim and final transcripts as the
174-
audio plays. Requires the `realtime` extra:
173+
Transcribe live audio — a microphone, a phone call, a meeting — as it is being
174+
spoken. You stream PCM audio over a WebSocket and receive interim text within
175+
moments and a finalized transcript for each utterance. Requires the `realtime`
176+
extra:
175177

176178
```sh
177179
pip install "together[realtime]"
178180
```
179181

180-
`client.realtime.transcription()` is the recommended surface: an
181-
auto-reconnecting session that buffers un-transcribed audio on the client and
182-
replays it after a connection drop, so transient WebSocket failures don't lose
183-
speech.
182+
Use `client.realtime.transcription()`: a session built for live sources. If
183+
the connection drops mid-conversation, the session holds on to the speech the
184+
server hadn't transcribed yet, reconnects automatically, and picks up where
185+
the transcript left off — words spoken during the outage still come back as
186+
text.
184187

185188
```python
186189
from together import AsyncTogether
@@ -190,43 +193,50 @@ client = AsyncTogether()
190193

191194
async with client.realtime.transcription(
192195
model="openai/whisper-large-v3",
193-
sample_rate=16_000, # validated against input_audio_format
196+
sample_rate=16_000,
194197
) as session:
195-
await session.append(pcm_chunk) # 16 kHz mono s16le PCM, any chunk size
198+
# feed audio from your capture source as it arrives (any chunk size)
199+
await session.append(pcm_chunk) # 16 kHz mono 16-bit PCM
200+
196201
async for event in session:
197202
if isinstance(event, TranscriptDelta):
198-
print("interim:", event.text)
203+
print("interim:", event.text) # updates while a phrase is spoken
199204
elif isinstance(event, TranscriptCompleted):
200-
print("final:", event.text)
201-
transcript = await session.flush()
202-
```
203-
204-
Notes:
205-
206-
- Audio must be PCM signed 16-bit little-endian; the default format is
207-
`pcm_s16le_16000` (16 kHz mono). Resample before appending.
208-
- Server-side voice activity detection is on by default: final transcripts
209-
arrive per detected utterance without explicit commits. Pass
210-
`turn_detection={"type": "none"}` to segment manually with
211-
`await session.commit()`.
212-
- All server session parameters are supported: `language`, `prompt`,
213-
`rolling_prompt`, `energy_gate_rms`, turn-detection tuning via
214-
`turn_detection={...}` (`threshold`, `min_silence_duration_ms`,
215-
`max_speech_duration_s`, `speech_pad_ms`, Deepgram `eot_*`), and
216-
`session_params={...}` for engine-specific passthrough options.
217-
- On retryable failures (network errors, server hiccups, 429/5xx handshakes)
218-
the session reconnects with jittered exponential backoff and replays buffered
219-
audio from `max(head - max_replay_seconds, last transcribed position)`, where
220-
the transcribed position comes from the `completed` event's `start + duration`
221-
and `buffer={"max_replay_seconds": 5.0}` is the client-configurable cap
222-
(`0` resumes live with no replay; `None` replays the full untranscribed
223-
window). Events produced from replayed audio carry `replayed=True`.
224-
- Buffer gaps (audio dropped after outages exceeding the retention window) are
225-
always surfaced as `BufferGap` events — never silent.
226-
- The sync client (`Together().realtime.transcription(...)`) mirrors this API
227-
and runs the session on a background thread — intended for low session
228-
counts; use the async client for high-concurrency workloads.
229-
- For full manual control (raw wire events, no retries), use
205+
print("final:", event.text) # one per finished utterance
206+
207+
transcript = await session.flush() # finalize whatever was said last
208+
```
209+
210+
What to know before integrating:
211+
212+
- **Audio in**: 16 kHz mono 16-bit PCM (`pcm_s16le_16000`). Resample your
213+
source before appending — passing `sample_rate=` lets the SDK reject a
214+
mismatch loudly instead of incorrecly transcribing different sample rate .
215+
`append()` never blocks on network state, so it is safe to call from a capture loop.
216+
- **Utterance boundaries**: detected server-side by default — final
217+
transcripts arrive on their own as the speaker pauses. To control
218+
segmentation yourself, pass `turn_detection={"type": "none"}` and call
219+
`await session.commit()` when each segment ends.
220+
- **Tuning**: `language`, `prompt`, and turn-detection parameters
221+
(`min_silence_duration_ms`, `max_speech_duration_s`, ...) are passed
222+
through to the service.
223+
- **When the connection drops**: The session emits
224+
`Reconnecting`/`Reconnected` events and tries to retry the connection.
225+
Transcripts recomputed from speech that was carried across the reconnect are marked
226+
`replayed=True` and may overlap text you already received; voice agents
227+
that act on each final can set `buffer={"max_replay_seconds": 0}` to
228+
resume live with no re-emission instead.
229+
- **If audio is ever dropped** (an outage longer than the retention window),
230+
the session tells you with a `BufferGap` event.
231+
- **If an endpoint fails for good**, calls raise `RealtimeConnectionError`.
232+
To keep a conversation alive across endpoint outages, run a failover ring —
233+
see [`examples/realtime_failover.py`](examples/realtime_failover.py):
234+
on failure, `session.pending_audio()` hands you the un-transcribed speech
235+
to seed a new session on another endpoint.
236+
- **Sync applications**: `Together().realtime.transcription(...)` mirrors the
237+
async API on a background thread — good for a handful of sessions; use the
238+
async client for high concurrency.
239+
- **Full manual control** (raw wire events, no automatic recovery):
230240
`client.realtime.connect()`.
231241

232242
See [`examples/realtime_transcription.py`](examples/realtime_transcription.py)

examples/realtime_failover.py

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
#!/usr/bin/env python3
2-
"""Realtime transcription with endpoint failover.
2+
"""Realtime transcription with failover across multiple endpoints.
33
4-
Run against a primary endpoint; when it fails beyond recovery, resume on the
5-
next endpoint in the ring, carrying over any audio that was never transcribed
6-
so no speech is lost across the switch.
4+
Stream live audio to a primary endpoint; when it fails beyond recovery,
5+
resume on the next endpoint in the ring, carrying over any speech that was
6+
never transcribed so nothing is lost across the switch.
77
8-
Division of labor:
9-
- The SDK handles transient failures on the CURRENT endpoint internally
10-
(reconnect, backoff, replay) — you'll see Reconnecting/Reconnected events.
11-
- When an endpoint is unrecoverable, SDK calls raise RealtimeConnectionError.
12-
Moving to another endpoint is application code: the loop below.
8+
The SDK handles transient failures on a single endpoint internally.
9+
When an endpoint is unrecoverable, SDK calls raise RealtimeConnectionError.
10+
Moving to another endpoint is application code: the loop below handles failover.
1311
1412
Usage:
1513
pip install "together[realtime]"
@@ -28,26 +26,19 @@
2826
from together.lib.realtime import (
2927
BufferGap,
3028
SessionStarted,
29+
TranscriptDelta,
3130
TranscriptCompleted,
3231
RealtimeSessionEvent,
3332
RealtimeConnectionError,
3433
)
3534

36-
# Independent deployments of the same model, addressed directly (per-cluster
37-
# URLs) so the two ring entries share no failure domain. Order = preference;
38-
# on failure the next entry takes over, wrapping around.
39-
#
40-
# base_url is the API root and must end at /v1 — the SDK appends the
41-
# /realtime path (and any other resource paths) itself.
35+
# Independent deployments of the same model. Order = preference; on failure
36+
# the next entry takes over, wrapping around. Each entry is (base_url, model);
37+
# base_url is the API root and may point at region-specific hosts so the ring
38+
# entries share no failure domain.
4239
ENDPOINTS = [
43-
(
44-
"https://funkyfalcon.api.together.ai/pool/ipop-4-openai-whisper-large-v3-92f607e9/v1",
45-
"together_sso/openai/whisper-large-v3-47df6eb0",
46-
),
47-
(
48-
"https://trickytrout.api.together.ai/pool/ipop-4-openai-whisper-large-v3-e6e3a747/v1",
49-
"together_sso/openai/whisper-large-v3-e151bfbf",
50-
),
40+
("https://api.together.ai/v1", "openai/whisper-large-v3-endpoint1"),
41+
("https://api.together.ai/v1", "openai/whisper-large-v3-endpoint2"),
5142
]
5243

5344
SAMPLE_RATE = 16_000
@@ -56,18 +47,19 @@
5647

5748

5849
def on_event(event: RealtimeSessionEvent) -> None:
59-
"""Handle transcripts as they arrive (called for every session event)."""
50+
"""Handle session events as they arrive."""
6051
if isinstance(event, SessionStarted):
61-
# Log the server-assigned session id — invaluable when correlating a
62-
# stream with server-side logs or support requests.
52+
# Log the server-assigned session id — useful for correlating a session.
6353
print(f"session {event.session_id} started on {event.model}")
54+
elif isinstance(event, TranscriptDelta):
55+
print(f"interim: {event.text}", end="\r")
6456
elif isinstance(event, TranscriptCompleted):
6557
# `replayed` marks re-transcriptions of carried-over audio; they may
6658
# overlap text you already received.
6759
marker = " (replayed)" if event.replayed else ""
6860
print(f"final: {event.text}{marker}")
6961
elif isinstance(event, BufferGap):
70-
# Audio dropped beyond recovery — always announced, never silent.
62+
# Audio dropped beyond recovery.
7163
print(f"[gap: {event.dropped_seconds:.1f}s lost]")
7264

7365

@@ -83,32 +75,29 @@ async def transcribe_with_failover(audio: bytes) -> str:
8375

8476
for attempt in range(MAX_ATTEMPTS):
8577
client, model = clients[attempt % len(clients)]
86-
print(f"--- streaming to {model} via {client.base_url.host}")
78+
print(f"--- streaming to {model}")
8779

8880
session = client.realtime.transcription(
8981
model=model,
9082
sample_rate=SAMPLE_RATE,
9183
event_callback=on_event,
92-
# Fail fast: hand a dead endpoint straight to this loop instead of
93-
# retrying it. Raise max_attempts to retry in place before switching.
84+
# Switch endpoints immediately on failure. Raise max_attempts to
85+
# let the SDK retry the same endpoint first.
9486
reconnect={"max_attempts": 0},
9587
)
9688
try:
9789
async with session:
9890
if carry_over:
99-
# One append, no pacing: the un-transcribed speech from the
100-
# failed endpoint is transmitted as fast as the connection
101-
# allows, so the new endpoint catches up to live immediately.
91+
# The un-transcribed speech from the failed endpoint is transmitted
92+
# as fast as the connection allows, so the new endpoint catches up to live immediately.
10293
await session.append(carry_over)
10394

10495
while position < len(audio):
105-
await session.append(audio[position : position + CHUNK_BYTES])
96+
await session.append(audio[position: position + CHUNK_BYTES])
10697
position += CHUNK_BYTES
10798
await asyncio.sleep(0.1) # simulate a live capture cadence
108-
10999
transcripts.append(await session.flush())
110100
return " ".join(t for t in transcripts if t)
111-
112101
except RealtimeConnectionError as exc:
113102
# Endpoint is unrecoverable: keep its transcripts, take back the
114103
# audio it never transcribed, and move to the next endpoint.

examples/realtime_transcription.py

Lines changed: 43 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
#!/usr/bin/env python3
2-
"""Realtime transcription over WebSocket with automatic reconnection.
2+
"""Realtime transcription of live audio.
33
4-
Streams a 16 kHz mono PCM WAV (or raw PCM) file to the Together realtime API
5-
and prints interim and final transcripts. The session survives connection
6-
drops: buffered audio is replayed from the last confirmed transcript.
4+
Stream PCM audio as it is captured and receive interim text while a phrase
5+
is being spoken, plus a finalized transcript per utterance. The WAV file
6+
here stands in for a live source (microphone, phone call, meeting audio).
77
88
Usage:
99
pip install "together[realtime]"
1010
export TOGETHER_API_KEY=...
11-
python examples/realtime_transcription.py audio.wav
11+
python examples/realtime_transcription.py audio.wav # 16 kHz mono s16le WAV
1212
"""
1313

1414
from __future__ import annotations
@@ -20,70 +20,59 @@
2020

2121
from together import AsyncTogether
2222
from together.lib.realtime import (
23-
BufferGap,
24-
Reconnected,
25-
Reconnecting,
23+
SessionStarted,
2624
TranscriptDelta,
2725
TranscriptCompleted,
26+
RealtimeSessionEvent,
2827
)
2928

30-
CHUNK_MS = 40
29+
MODEL = "openai/whisper-large-v3"
30+
3131
SAMPLE_RATE = 16_000
32-
BYTES_PER_SECOND = SAMPLE_RATE * 2
32+
CHUNK_BYTES = SAMPLE_RATE * 2 // 10 # 100 ms per append, like a live source
3333

3434

35-
def load_pcm(path: Path) -> bytes:
36-
if path.suffix.lower() != ".wav":
37-
return path.read_bytes()
38-
with wave.open(str(path), "rb") as wav:
39-
if wav.getnchannels() != 1 or wav.getsampwidth() != 2 or wav.getframerate() != SAMPLE_RATE:
40-
raise SystemExit("expected mono 16-bit 16 kHz WAV (resample first)")
41-
return wav.readframes(wav.getnframes())
35+
def on_event(event: RealtimeSessionEvent) -> None:
36+
"""Handle session events as they arrive."""
37+
if isinstance(event, SessionStarted):
38+
# Log the server-assigned session id — useful when correlating a
39+
# stream with server-side logs or support requests.
40+
print(f"session {event.session_id} started on {event.model}")
41+
elif isinstance(event, TranscriptDelta):
42+
print(f"interim: {event.text}", end="\r")
43+
elif isinstance(event, TranscriptCompleted):
44+
print(f"final: {event.text}")
4245

4346

44-
async def main() -> None:
45-
audio = load_pcm(Path(sys.argv[1]))
47+
async def transcribe(audio: bytes) -> str:
4648
client = AsyncTogether()
4749

4850
async with client.realtime.transcription(
49-
# model="together_sso/openai/whisper-large-v3-47df6eb0",
50-
model="together_sso/openai/whisper-large-v3-e151bfbf",
51+
model=MODEL,
5152
sample_rate=SAMPLE_RATE,
53+
event_callback=on_event,
5254
) as session:
55+
position = 0
56+
while position < len(audio):
57+
await session.append(audio[position : position + CHUNK_BYTES])
58+
position += CHUNK_BYTES
59+
await asyncio.sleep(0.1) # simulate a live capture cadence
60+
61+
return await session.flush() # finalize whatever was said last
62+
5363

54-
async def feed() -> None:
55-
chunk_bytes = BYTES_PER_SECOND * CHUNK_MS // 1000
56-
for start in range(0, len(audio), chunk_bytes):
57-
await session.append(audio[start : start + chunk_bytes])
58-
await asyncio.sleep(CHUNK_MS / 1000) # pace like a live mic
59-
print("\n[audio fully sent — draining transcripts]")
60-
61-
feeder = asyncio.create_task(feed())
62-
63-
async def consume() -> None:
64-
async for event in session:
65-
if isinstance(event, TranscriptDelta):
66-
# print(f"\r… {event.text}", end="", flush=True)
67-
pass
68-
elif isinstance(event, TranscriptCompleted):
69-
# marker = " (replayed)" if event.replayed else ""
70-
# print(f"\rfinal: {event.text}{marker}")
71-
print(event.text)
72-
elif isinstance(event, Reconnecting):
73-
# print(f"\n[reconnecting: {event.reason} (attempt {event.attempt})]")
74-
pass
75-
elif isinstance(event, Reconnected):
76-
# print(f"[reconnected; replayed {event.replayed_seconds:.1f}s of audio]")
77-
pass
78-
elif isinstance(event, BufferGap):
79-
# print(f"[warning: {event.dropped_seconds:.1f}s of audio lost]")
80-
pass
81-
82-
consumer = asyncio.create_task(consume())
83-
await feeder
84-
transcript = await session.flush()
85-
consumer.cancel()
86-
print(f"\nfull transcript: {transcript}")
64+
def load_pcm(path: Path) -> bytes:
65+
with wave.open(str(path), "rb") as w:
66+
if (w.getnchannels(), w.getsampwidth(), w.getframerate()) != (1, 2, SAMPLE_RATE):
67+
raise SystemExit(f"expected mono 16-bit {SAMPLE_RATE} Hz WAV")
68+
return w.readframes(w.getnframes())
69+
70+
71+
async def main() -> None:
72+
if len(sys.argv) != 2:
73+
raise SystemExit(__doc__)
74+
transcript = await transcribe(load_pcm(Path(sys.argv[1])))
75+
print(f"\nfull transcript: {transcript}")
8776

8877

8978
if __name__ == "__main__":

src/together/lib/realtime/_session.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,17 @@ async def __aexit__(self, exc_type: object, *_exc: object) -> None:
218218
async def start(self) -> None:
219219
if self._connection is not None:
220220
return
221-
connection = await self._open_connection()
221+
try:
222+
connection = await self._open_connection()
223+
except Exception as exc:
224+
status = handshake_status_of(exc)
225+
detail = f"HTTP {status}" if status is not None else exc.__class__.__name__
226+
raise RealtimeConnectionError(
227+
f"could not open realtime connection to {self._client.base_url} ({detail}); "
228+
"check that base_url / TOGETHER_BASE_URL points at an API root that "
229+
"serves /realtime, e.g. https://api.together.ai/v1",
230+
cause=exc,
231+
) from exc
222232
created = await self._await_session_created(connection)
223233
self.state.begin_epoch(self.state.write_head)
224234
self._sent_offset = self.state.write_head
@@ -300,14 +310,18 @@ async def flush(self, *, quiescence: float = 2.0, timeout: float = 30.0) -> str:
300310
if self.state.write_head > (self.state.anchor or 0):
301311
await self.commit()
302312
self._raise_if_failed()
303-
deadline = self._now() + timeout
313+
started = self._now()
314+
deadline = started + timeout
304315
while self._now() < deadline:
305316
if self._failure is not None:
306317
raise self._failure
307318
if self._closed:
308319
break
309320
fully_sent = self._sent_offset >= self.state.write_head and not self._pending_controls
310-
quiet_for = self._now() - max(self._last_transcript_event, self._connected_at)
321+
# the quiescence clock starts at flush time so the commit sent
322+
# above always gets a response window: a final for the tail
323+
# utterance may land seconds after the last transcript event
324+
quiet_for = self._now() - max(self._last_transcript_event, self._connected_at, started)
311325
if fully_sent and quiet_for >= quiescence and self._reconnect_task is None:
312326
break
313327
await asyncio.sleep(min(0.1, quiescence / 4))

0 commit comments

Comments
 (0)