Skip to content

Speaker diarization

The problem. A meeting room, a podcast, an interview: several voices on one microphone, one stream of text. You cannot tell what the client said from what the advisor said. Diarization labels each utterance and each word with a speaker number.

Where it works. Every model, on live audio.

{ "diarization": true }

Each program streams a 16-bit WAV file and prints what comes back. Put your API key in the GLADIA_API_KEY environment variable, then run python diarization.py meeting.wav.

diarization.py
# pip install gladiaio-sdk requests
import os
import sys
import threading
import wave
import requests
from gladiaio_sdk import GladiaClient, LiveV2ConnectSessionOptions
API_URL = os.environ.get("GLADIA_API_URL", "https://eu1.api.gladia.io")
API_KEY = os.environ["GLADIA_API_KEY"]
with wave.open(sys.argv[1]) as wav: # a 16-bit PCM WAV file
rate, channels = wav.getframerate(), wav.getnchannels()
audio = wav.readframes(wav.getnframes())
config = {
"encoding": "wav/pcm",
"sample_rate": rate,
"bit_depth": 16,
"channels": channels,
"model": "solaria-3",
"language_config": {
"languages": [
"en"
]
},
"diarization": True,
"diarization_config": {
"max_speakers": 3
}
}
client = GladiaClient(api_key=API_KEY, api_url=API_URL)
# `diarization` is a Gladia Enterprise option the Python SDK does not send yet
# (gladiaio/sdk#61): create the session over HTTP, then let the SDK stream it.
created = requests.post(f"{API_URL}/v2/live", headers={"x-gladia-key": API_KEY}, json=config, timeout=10)
created.raise_for_status()
info = created.json()
session = client.live_v2().connect_session(LiveV2ConnectSessionOptions(id=info["id"], url=info["url"]))
ended, errors = threading.Event(), []
@session.on("message")
def on_message(message):
if message.type == "transcript" and message.data.is_final:
utterance = message.data.utterance
print(f"Speaker {utterance.speaker}: {utterance.text}")
session.on("error", errors.append)
session.once("ended", lambda _: ended.set())
step = rate * channels * 2 // 10 # 100 ms of audio per chunk
for start in range(0, len(audio), step):
session.send_audio(audio[start : start + step]) # live audio: send each chunk as it is captured
session.stop_recording()
ended.wait(timeout=60)
sys.exit(1 if errors or not ended.is_set() else 0)
{
"type": "transcript",
"data": {
"id": "00_00000004",
"is_final": true,
"utterance": {
"text": "Right. Shall we start?",
"start": 31.2,
"end": 32.6,
"speaker": 1,
"channel": 0,
"words": [
{ "word": "Right.", "start": 31.2, "end": 31.5, "confidence": 0.97, "speaker": 0 },
{ "word": "Shall", "start": 31.7, "end": 31.9, "confidence": 0.98, "speaker": 1 },
{ "word": "we", "start": 31.9, "end": 32.0, "confidence": 0.99, "speaker": 1 },
{ "word": "start?", "start": 32.0, "end": 32.6, "confidence": 0.98, "speaker": 1 }
]
}
}
}

Read the speaker of each word, not the utterance’s. An utterance ends on a pause, not on a change of speaker, so one utterance can hold the words of two people: here speaker 0 says “Right.” and speaker 1 asks the question. The utterance’s speaker is only the one who speaks longest in it. Each word’s speaker is the label to trust.

To show who said what, cut each final where its words change speaker:

def turns(utterance):
"""The utterance's words as (speaker, text) turns, in order."""
out = []
for word in utterance["words"]:
if out and out[-1][0] == word["speaker"]:
out[-1] = (word["speaker"], out[-1][1] + " " + word["word"])
else:
out.append((word["speaker"], word["word"]))
return out

A label can also be wrong on one or two words (see the limits below). If short flips bother your display more than lost interjections, merge them client-side: give a run of one or two words the speaker of the runs on both sides of it when both are the same speaker. This also hides real one-word answers (“yes”, “okay”) from that speaker.

def merge_islands(words, max_words=2):
"""Relabel a run of at most max_words words framed by one same speaker on both sides."""
words = [dict(w) for w in words]
runs, start = [], 0 # (first, last + 1) index of each run of one speaker
for i in range(1, len(words) + 1):
if i == len(words) or words[i]["speaker"] != words[start]["speaker"]:
runs.append((start, i))
start = i
for (a, _), (b, e), (c, _) in zip(runs, runs[1:], runs[2:]):
if e - b <= max_words and words[a]["speaker"] == words[c]["speaker"]:
for w in words[b:e]:
w["speaker"] = words[a]["speaker"]
return words

Run it on the words of consecutive finals put together, so a flip on the first word of a final is seen between the previous final and the rest of this one.

option what it does
max_speakers The most speakers to tell apart, when you know it.
number_of_speakers Gladia’s name for the same setting.
  • Up to 4 speakers are told apart in one session. More voices than that share the 4 numbers.
  • Finals arrive a little later, the time the speakers are known (under a second). Partials are not labelled.
  • Numbers, not names. Speaker numbers are valid within the session only: the same person in two sessions may get another number. Map numbers to people in your application.
  • Separate channels are better. When each person has their own channel (a call recorded in stereo), send the channels instead (Audio formats): it is exact, and needs no diarization.
  • Overlapping speech is one utterance: two people talking at once are not split into two.
  • One utterance, several speakers. Utterances are not cut where the speaker changes: a quick reply right after another person spoke often lands in the same final. Read each word’s speaker (step 3).
  • One or two words can flip to another speaker. The speaker is chosen word by word, and a short word over another voice can take the wrong label. On Gladia’s diarization quality study (19 recordings, 1.6 h, 659 finals), the first word’s speaker differs from the utterance’s speaker in 17.3 % of the finals (114). In 55 of them the final really starts with another person’s words. In the other 59 the first words carry a wrong label, and in 46 of those the wrong run is one or two words long. Counting every place in a final, there are 0.276 runs of one or two words between two runs of one same speaker per final, real short turns included.
  • No smoothing by default. The inference has an optional server-side smoothing that gives a run of up to two words back to the speaker around it. It is off because it also erases real short turns. On the same study it relabelled 65 runs: 38 wrong labels fixed, 27 real interjections lost. On the meeting recordings, the share of speaker changes found fell from 67.8 % to 59.5 %. Merge short runs client-side (step 4) when you want that trade-off.
  • min_speakers and enhanced (Gladia’s cloud fields) are accepted and ignored.
symptom cause fix
everyone is speaker 0 the voices are very similar, or one microphone picks one voice much louder use one channel per person when you can
5 people, only 4 numbers the 4-speaker limit split the session by channel
finals feel slower with diarization they wait for their speakers expected: use partials for live display
the first word or two of an utterance belong to someone else the reply starts in the same final, or a short label flip group by each word’s speaker; merge one- or two-word runs client-side if you prefer stable turns (step 4)