Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

speaker-identify

Status: Current Last updated: 2026-09-09 13:24 EDT

Score each timed utterance of a CHAT transcript against one or more voices you enroll from the recording itself, and write the scores and verdicts beside the transcript as evidence.

You supply one or more enrollment spans: stretches of the same recording known to contain a single speaker alone. The command embeds each of them, embeds every timed utterance, and reports how acoustically similar each utterance is to each enrolled voice.

It does not modify the transcript. The output is a JSON evidence file. What to do with a verdict, in particular whether to change a speaker code, is a decision about your corpus’s own conventions, and it stays yours.


Quick start

# One investigator, enrolled from the opening span of the recording
batchalign3 speaker-identify session.cha \
  --enroll 1500-9000:INV \
  --threshold 0.62 \
  -o evidence/

# Two known voices, and only the utterances currently on *PAR0
batchalign3 speaker-identify corpus/ \
  --enroll 1500-9000:INV \
  --enroll 12000-20000:CHI \
  --tiers PAR0 \
  --threshold 0.62 \
  -o evidence/

For session.cha the artifact is session_speaker_identity.json.


What an enrollment span is

A span of the recording, in milliseconds from its start, that you know holds one speaker, alone. The same coordinates a CHAT timing bullet uses.

--enroll <start_ms>-<end_ms>:<label>

The label names the voice (INV, CHI, MOT, anything without a colon, a dash or a space). It is what appears in every score and verdict.

Choosing the span is the part that matters, and only you can do it:

  • Longer is better. A few seconds of continuous speech is a much steadier acoustic identity than half a second. Spans below the model’s own minimum (about 105 ms) are refused outright.
  • One voice only. If anyone else speaks inside the span, or the span includes a long silence, the enrolled vector describes a mixture, and every score computed against it inherits that.
  • From this recording. Enrolling from a different session’s audio compares the two rooms and the two microphones as much as the two people.

Common source of a good span: an opening stretch where one person is speaking alone before anyone else joins.


Options

OptionDefaultMeaning
PATHS...Input .cha transcripts and/or directories
-o, --output DIROutput directory for the evidence artifacts
--enroll SPANrequired<start_ms>-<end_ms>:<label>. Repeat once per known voice
--threshold FrequiredSimilarity at or above which a voice counts as a match
--tiers CODESall tiersSpeaker tiers to score, comma-separated or repeated
--permutations N1000Shuffled labellings drawn for each track contrast’s p-value; a thousand resolve p to 0.001
--permutation-seed S0Seed of that shuffle, so a run reproduces byte for byte. Both are recorded in the file
--lang CODEeng3-letter ISO code, for worker-pool selection only; embedding is language-independent

Why --threshold has no default

How much acoustic agreement counts as “the same person” depends on the recording, the microphone, how much enrollment audio you gave, and what a wrong answer would cost you. Nothing in this tool knows any of that. A built-in number would produce confident verdicts under a value nobody chose, and no reader of the output could tell it had been chosen by accident. So you state it, and the value you stated is written into the evidence beside the verdicts it produced.

How to pick one. Run once on a session where you already know the answer for a handful of utterances, read the scores in the output, and choose a value that separates them. Cosine similarity runs from -1 to 1; values in the 0.5-0.75 region are where useful thresholds usually fall for this model, but that is an observation about where to start looking, not a recommendation.


Enrollment rules the command enforces

  • At least one --enroll. With none there is nothing to identify against.
  • Labels are unique. Each label names one voice.
  • Enrollments may not overlap. Two spans claiming the same audio for two different single speakers cannot both be true, so the run is refused rather than producing two contaminated vectors that look like ordinary ones.

Spans that touch end-to-start (0-5000 and 5000-9000) do not overlap and are fine.


Output format

For session.cha, session_speaker_identity.json:

{
  "provenance": {
    "schema_version": 2,
    "interpretation": "Scores are acoustic AGREEMENT with an enrolled span ...",
    "transcript": "session.cha",
    "media": "/corpus/media/session.mp3",
    "prepared_sample_rate_hz": 16000,
    "embedding_backend": "pyannote",
    "embedding_model_revision": "pyannote-embedding:0ae88dca...",
    "embedding_dimension": 256,
    "embedding_minimum_frames": 1680,
    "match_threshold": 0.62,
    "tiers": ["PAR0"],
    "enrollments": [
      { "label": "INV", "start_ms": 1500, "end_ms": 9000 }
    ],
    "permutation": { "seed": 0, "count": 1000 },
    "produced_by": "batchalign3 <build>"
  },
  "utterances": [
    {
      "utterance_index": 0,
      "line": 42,
      "speaker": "PAR0",
      "start_ms": 12000,
      "end_ms": 14500,
      "scores": [{ "label": "INV", "score": 0.81 }],
      "verdict": { "verdict": "matches", "label": "INV", "score": 0.81 }
    }
  ],
  "tracks": [
    { "kind": "voiced", "track": "PAR0", "lines_embedded": 212, "lines_refused": 3,
      "centroid": ["..."], "scores": [{ "label": "INV", "score": 0.71 }] }
  ],
  "track_contrasts": [
    { "kind": "one_track", "label": "INV", "track": "PAR0" }
  ]
}

Every utterance carries its similarity to every enrolled voice, not only the winner, so you can try a different threshold without re-running the model.

Tracks as voices

Beside the per-line verdicts, every speaker code among the scored tiers is scored as one voice: the centroid of its lines’ vectors, compared to each enrolled voice. The mean of a track’s line scores is not the similarity of the track’s voice, and the centroid is robust to the short lines that drag a mean down. For each enrolled voice, track_contrasts then says how far the best track stands out from the runner-up, with a permutation p-value in place of a margin somebody chose: line-to-track membership is shuffled with track sizes kept, and the p-value is how often a shuffle reaches the observed margin. The seed and count are yours to set and are written into the file. Field by field: the evidence reference.

The three verdicts

verdictMeaning
matchesExactly one enrolled voice scored at or above the threshold, with nothing tied. Carries that label and score.
no_matchNo single voice was established. best names every label that reached the highest score, and that score.
unscoredNo similarity was computed, and reason says why.

A tie never matches. If two enrolled voices reach the same highest score, even above the threshold, the evidence does not say which speaker it is, because it does not know. best then lists both.

Why an utterance is unscored

reasonMeaning
too_short_for_embeddingShorter than the model can measure. Carries frames and minimum_frames.
no_bulletThe utterance has no timing, so there is no audio to embed.
no_comparable_embeddingIt had timing, but no enrolled embedding could be compared with its embedding.
audio_missingIts bullet names audio the recording does not contain. Carries the bullet and the recording’s length.
overlaps_enrollmentIt falls inside an enrolled span. Scoring it would compare that audio with a vector computed from it.

Unscored utterances are reported, never omitted: a file that dropped them would let you conclude the transcript has fewer utterances than it has.


Scores are agreement, not accuracy

A score says how similar two stretches of audio are under one embedding model. It does not say the speaker is who you think. The enrolled span is your own claim about who is talking, and it carries your error rate: if the span holds the wrong person, or two people, every score against it is confidently wrong in a direction nothing in the output reveals.

Treat a high score as evidence to act on, not as a verdict to publish. The evidence file repeats this in its own interpretation field, because the file is what gets forwarded and this page is not.


Gotchas

The enrollment span is not scored against itself. Utterances inside an enrolled span come back unscored with overlaps_enrollment. That is deliberate; a similarity there would measure the arithmetic.

No Hugging Face account is needed. Unlike diarize, this command loads only the speaker-embedding model, which lives in a public, ungated repository. It never builds the diarization pipeline, so it never reaches that pipeline’s gated calibration dependency.

Enrollment spans must be inside the recording. A span past the end fails the run rather than scoring everything against a truncated vector; an utterance past the end is unscored, because one bad bullet should not end a file.

A transcript with no timings produces an all-unscored file. That is the correct answer, not a failure: run align first if you want timings.


  • Speaker identity evidence, the artifact’s field-by-field reference
  • diarize, which finds anonymous speaker turns without enrollment
  • align, which produces the timings this command reads

This page last changed: 2026-09-10 (commit 423245db). The whole book last changed: 2026-09-16 (commit 34d249d8).