Skip to content

Commit 08f0471

Browse files
authored
docs(examples): add microphone live transcription example (#780)
Closes #779 ## Summary - add a maintained Listen V1 microphone transcription example using the optional `sounddevice` dependency - support device listing, device selection by name or numeric ID, sample rate, channel count, block duration, and language configuration - decouple the PortAudio callback from WebSocket I/O with a bounded queue, reporting dropped chunks when the sender cannot keep up - handle final transcript events, SDK errors, Ctrl-C, Finalize, and CloseStream cleanly - list the new example in `examples/README.md` ## Scope This is an example only. It adds no core microphone capture abstraction, optional package extra, or platform-audio dependency to `deepgram-sdk`; microphone drivers, permissions, and backend installation remain owned by `sounddevice` and the host platform. ## Validation - live macOS microphone run with device ID 2: received final Deepgram transcripts and returned cleanly after Ctrl-C - verified the missing `sounddevice` path prints the install command without a traceback - verified `--list-devices` with an isolated fake backend - Python compilation, Ruff, and mypy pass - existing SDK suite: `1019 passed, 1 skipped` - `git diff --check` passes
1 parent 30ad152 commit 08f0471

2 files changed

Lines changed: 200 additions & 0 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
"""Live microphone transcription with Listen V1.
2+
3+
Install the optional capture dependency first:
4+
5+
pip install sounddevice
6+
7+
On Linux, the system PortAudio library may also be required. List available
8+
input devices before recording with:
9+
10+
python examples/18-transcription-live-microphone.py --list-devices
11+
12+
Then set DEEPGRAM_API_KEY and run:
13+
14+
python examples/18-transcription-live-microphone.py
15+
16+
This example sends 16-bit PCM (linear16) to Deepgram. The sample rate and
17+
channel count in the WebSocket connection always match the microphone stream.
18+
"""
19+
20+
import argparse
21+
import os
22+
import queue
23+
import sys
24+
import threading
25+
from typing import Any, Optional
26+
27+
from dotenv import load_dotenv
28+
29+
from deepgram import DeepgramClient
30+
from deepgram.core.events import EventType
31+
from deepgram.listen.v1.types import ListenV1Results
32+
33+
load_dotenv()
34+
35+
MAX_QUEUED_CHUNKS = 20
36+
37+
38+
def load_sounddevice() -> Any:
39+
try:
40+
import sounddevice # type: ignore[import-not-found,import-untyped]
41+
except (ImportError, OSError) as exc:
42+
raise RuntimeError(
43+
"Microphone capture requires sounddevice and the PortAudio library. "
44+
"Install them with: pip install sounddevice "
45+
"(on Debian/Ubuntu also: sudo apt install libportaudio2)"
46+
) from exc
47+
return sounddevice
48+
49+
50+
def parse_args() -> argparse.Namespace:
51+
parser = argparse.ArgumentParser(description="Transcribe microphone audio with Deepgram Listen V1")
52+
parser.add_argument("--list-devices", action="store_true", help="list microphone devices and exit")
53+
parser.add_argument("--device", help="input device name or numeric ID from --list-devices")
54+
parser.add_argument("--sample-rate", type=int, default=16000, help="input sample rate in Hz (default: 16000)")
55+
parser.add_argument("--channels", type=int, default=1, help="input channel count (default: 1)")
56+
parser.add_argument(
57+
"--block-duration",
58+
type=float,
59+
default=0.1,
60+
help="audio callback block duration in seconds (default: 0.1)",
61+
)
62+
parser.add_argument("--language", default="en", help="recognition language (default: en)")
63+
return parser.parse_args()
64+
65+
66+
def main() -> int:
67+
args = parse_args()
68+
try:
69+
sounddevice = load_sounddevice()
70+
except RuntimeError as exc:
71+
print(exc, file=sys.stderr)
72+
return 2
73+
74+
if args.list_devices:
75+
print(sounddevice.query_devices())
76+
return 0
77+
78+
if not os.getenv("DEEPGRAM_API_KEY"):
79+
print("Set DEEPGRAM_API_KEY before recording.", file=sys.stderr)
80+
return 2
81+
if args.sample_rate <= 0 or args.channels <= 0 or args.block_duration <= 0:
82+
print("sample-rate, channels, and block-duration must be positive.", file=sys.stderr)
83+
return 2
84+
85+
blocksize = int(args.sample_rate * args.block_duration)
86+
if blocksize == 0:
87+
print("block-duration is too small for the selected sample rate.", file=sys.stderr)
88+
return 2
89+
90+
device: Optional[object] = int(args.device) if args.device and args.device.isdigit() else args.device
91+
audio_queue: queue.Queue[bytes] = queue.Queue(maxsize=MAX_QUEUED_CHUNKS)
92+
stop = threading.Event()
93+
closed = threading.Event()
94+
sender_errors: list[Exception] = []
95+
dropped_chunks = 0
96+
input_status: Optional[str] = None
97+
98+
def on_message(message: object) -> None:
99+
if not isinstance(message, ListenV1Results) or not message.is_final:
100+
return
101+
if message.channel is None or not message.channel.alternatives:
102+
return
103+
transcript = message.channel.alternatives[0].transcript
104+
if transcript:
105+
print(f"Transcript: {transcript}")
106+
107+
def on_error(error: Exception) -> None:
108+
sender_errors.append(error)
109+
stop.set()
110+
111+
def on_close(_: object) -> None:
112+
closed.set()
113+
stop.set()
114+
115+
def on_audio(indata: Any, _frames: int, _time: Any, status: Any) -> None:
116+
nonlocal dropped_chunks, input_status
117+
if status:
118+
input_status = str(status)
119+
try:
120+
audio_queue.put_nowait(bytes(indata))
121+
except queue.Full:
122+
dropped_chunks += 1
123+
124+
client = DeepgramClient()
125+
try:
126+
with client.listen.v1.connect(
127+
model="nova-3",
128+
language=args.language,
129+
encoding="linear16",
130+
sample_rate=args.sample_rate,
131+
channels=args.channels,
132+
interim_results=True,
133+
smart_format=True,
134+
) as connection:
135+
connection.on(EventType.OPEN, lambda _: print("Connection opened"))
136+
connection.on(EventType.MESSAGE, on_message)
137+
connection.on(EventType.ERROR, on_error)
138+
connection.on(EventType.CLOSE, on_close)
139+
140+
listener = threading.Thread(target=connection.start_listening, daemon=True)
141+
listener.start()
142+
143+
def send_audio() -> None:
144+
while not stop.is_set():
145+
try:
146+
chunk = audio_queue.get(timeout=0.1)
147+
except queue.Empty:
148+
continue
149+
try:
150+
connection.send_media(chunk)
151+
except Exception as exc:
152+
sender_errors.append(exc)
153+
stop.set()
154+
finally:
155+
audio_queue.task_done()
156+
157+
sender = threading.Thread(target=send_audio, daemon=True)
158+
sender.start()
159+
160+
print("Recording... press Ctrl-C to stop.")
161+
try:
162+
with sounddevice.RawInputStream(
163+
samplerate=args.sample_rate,
164+
blocksize=blocksize,
165+
device=device,
166+
channels=args.channels,
167+
dtype="int16",
168+
callback=on_audio,
169+
):
170+
while not stop.wait(0.1):
171+
if sender_errors:
172+
raise sender_errors[0]
173+
except KeyboardInterrupt:
174+
print("\nStopping recording...")
175+
finally:
176+
stop.set()
177+
sender.join(timeout=2)
178+
if not closed.is_set():
179+
try:
180+
connection.send_finalize()
181+
connection.send_close_stream()
182+
except Exception:
183+
pass
184+
listener.join(timeout=5)
185+
if sender_errors:
186+
raise sender_errors[0]
187+
except Exception as exc:
188+
print(f"Microphone transcription failed: {type(exc).__name__}: {exc}", file=sys.stderr)
189+
return 1
190+
191+
if input_status:
192+
print(f"Microphone status: {input_status}", file=sys.stderr)
193+
if dropped_chunks:
194+
print(f"Warning: dropped {dropped_chunks} microphone chunk(s) because the sender could not keep up.")
195+
return 0
196+
197+
198+
if __name__ == "__main__":
199+
raise SystemExit(main())

‎examples/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ This directory contains comprehensive examples demonstrating how to use the Deep
1919
- **15-transcription-advanced-options.py** - Advanced transcription options
2020
- **16-transcription-force-end-turn.py** - Manual turn-ending control (Listen V2 / Flux)
2121
- **17-transcription-live-reconnect.py** - Production-grade reconnection: backoff with jitter, close-code handling, audio buffering across gaps, clean shutdown
22+
- **18-transcription-live-microphone.py** - Microphone transcription with the optional sounddevice dependency
2223

2324
### 20-29: Text-to-Speech (Speak)
2425

0 commit comments

Comments
 (0)