Voiceprint Speaker Gating
Given an enrolled speaker's voiceprint, the module analyses a 16 kHz audio stream in real time and reports, per frame, whether the enrolled speaker is talking — ignoring background noise, media audio and other voices.
Two calls: a REST endpoint to enroll a speaker, and a WebSocket to stream audio and receive gating events. Base URL https://gating.dollyglot.com. Interactive REST schema at /docs; live playground at the demo.
https://gating.dollyglot.com · trial key dg-trial-7762c25375eb0479 (send as X-API-Key) · interactive demo.Quickstart
- Enroll a speakerPOST 20–60 s of their speech to
/v1/profilesand keep the returnedprofile_id. - Open a streamConnect a WebSocket to
/v1/stream?profile_id=…. - Send audio, read eventsSend 16 kHz PCM frames; receive one JSON event every 20 ms with the
is_speakingflag and a similarity score.
Authentication
Every request needs an API key.
- REST — header
X-API-Key: <key> - WebSocket —
X-API-Keyheader, or?api_key=<key>query parameter (browsers can't set WebSocket headers)
Each key carries a name and a daily streaming-minutes quota. Enrollment does not consume streaming quota.
Enroll a speaker
Creates a speaker profile from one or more audio files (20–60 s of speech recommended, ≥ 8 s minimum). Audio is processed in memory and discarded immediately — only the embedding is stored.
curl -X POST https://gating.dollyglot.com/v1/profiles \ -H "X-API-Key: <key>" \ -F "files=@speaker.wav"
Response
{ "profile_id": "9cda2a8f09954db28d91bee1f56f6de9" }Delete a profile
Returns {"deleted":"<id>"}, or 404 with profile_not_found.
Stream audio
Open the socket, optionally send a config frame, then send binary PCM frames. The flag updates every 20 ms; the similarity score refreshes every 200 ms.
Client to server
| Message | Content |
|---|---|
| config (optional, first frame) | {"sensitivity":"low|default|high"} |
| audio | binary — 16-bit LE PCM, 16 kHz mono, any chunk size (20–40 ms recommended) |
| end | {"eof":true} — flush & close gracefully |
Server to client
One JSON text frame every 20 ms:
{ "t_ms": 600, "is_speaking": true, "score": 0.83, "speech": true, "score_smoothed": 0.79 }Event schema
| Field | Type | Description |
|---|---|---|
t_ms | int | audio timestamp (ms) |
is_speaking | bool | the decision — is the enrolled speaker talking now (VAD + identity + hysteresis + timing) |
score | float | null | raw voiceprint similarity in [0, 1], refreshed every 200 ms; null before enough speech |
speech | bool | VAD — is any voice present (independent of identity) |
score_smoothed | float | null | EMA-smoothed score — the value the decision is made on |
Error codes
On a problem the server sends {"error":"<code>","detail":"…"}. REST uses the HTTP status; WebSocket closes the socket.
| Code | Where | Meaning |
|---|---|---|
invalid_api_key | 401 / WS 1000 | missing or invalid key |
unsupported_sample_rate | 415 / WS | not 16 kHz |
stereo_not_supported | 415 / WS | not mono |
unsupported_bit_depth | 415 / WS | not 16-bit PCM |
insufficient_enrollment_audio | 422 | < 8 s of speech to enroll |
profile_not_found | 404 / WS 1000 | unknown profile id |
quota_exceeded | WS 1008 | daily streaming quota reached |
Sensitivity
Presets map to the identity thresholds (turn-on / turn-off, with hysteresis). Tune on your own target speaker and acoustic scenario for production.
| Preset | On | Off | Behaviour |
|---|---|---|---|
low | 0.62 | 0.50 | strict — fewest false accepts |
default | 0.55 | 0.45 | balanced (recommended) |
high | 0.50 | 0.40 | most responsive |
Example client
import asyncio, json, soundfile as sf, websockets
async def main(host, key, pid, wav):
audio, sr = sf.read(wav, dtype="int16") # must be 16 kHz mono
assert sr == 16000 and audio.ndim == 1
raw = audio.tobytes()
uri = f"{host}/v1/stream?profile_id={pid}&api_key={key}"
async with websockets.connect(uri) as ws:
await ws.send(json.dumps({"sensitivity": "default"}))
async def send():
for i in range(0, len(raw), 640): # 20 ms frames
await ws.send(raw[i:i+640]); await asyncio.sleep(0.02)
await ws.send(json.dumps({"eof": True}))
async def recv():
async for m in ws:
e = json.loads(m)
if e.get("is_speaking"):
print(e["t_ms"], "SPEAKING", e["score"])
await asyncio.gather(send(), recv())
asyncio.run(main("wss://gating.dollyglot.com", "<key>", "<profile_id>", "sample.wav"))