---
name: speaker-id
description: "Name anonymous diarized SRT speakers from a persistent local voiceprint library and repair speaker drift; local, CPU-only, no API key. Triggers \"who is speaker 2\", \"put names on the transcript\", \"label the meeting SRT\", \"the diarization split one person into three\", \"enroll my voice\". Backed by the pi-voiceid CLI."
---

# Speaker ID enrollment

Give diarized transcripts real names, and repair the drift that clustering
diarizers produce on long recordings. Post-hoc relabeling — it does **not**
create diarization, it relabels an existing one.

```bash
pi-voiceid analyze --srt talk.srt            # drift report, no enrollment needed
pi-voiceid enroll  --name "Alice" --srt talk.srt --label "Speaker 1"
pi-voiceid label   --srt talk.srt --dry-run  # always dry-run first
pi-voiceid label   --srt talk.srt            # writes talk.named.srt
```

The source SRT is **never** overwritten. Everything runs locally on CPU via
`sherpa-onnx`; no audio or embedding leaves the machine.

## How it works

1. Each anonymous cluster is profiled with a speaker-embedding model, sampling
   segments **across the whole timeline** (drift is temporal).
2. Cluster centroids are centered (a multi-speaker mean subtracted) and compared
   by cosine to the enrolled voiceprints.
3. A cluster is named only when the absolute threshold, the margin over the
   runner-up, and a minimum share of agreeing segments all pass. Otherwise the
   anonymous label is kept — never guessed.
4. Several clusters may map to one name: that is the drift repair.

## Setup (one time)

The embedding model is not vendored. On first `enroll`/`label` it is fetched
into `~/.pi/models/speaker/` (28 MB). Model choice is the single biggest
accuracy factor — see [BENCHMARK.md](BENCHMARK.md). **Do not "upgrade" to a
bigger model without re-running the benchmark**: the 114 MB VoxCeleb leader
scored *worse* than the 28 MB default on real meeting audio.

The native binding `sherpa-onnx-node` is an **optional dependency**. When it is
absent, transcription still works and `pi-voiceid` fails with an actionable
message naming the dependency and the install command.

## Voiceprint library

Default store: `~/.pi/voiceprints/voiceprints.json`. Override with `--store` or
the `PI_VOICEPRINT_STORE` env var. This is **biometric-derived data about
identifiable people**: it lives outside the repo by default, it is overridable,
and it must never be committed. If you point `--store` inside a repository, add
it to that repo's `.gitignore`.

`enroll` also embeds the source recording's **other** speakers, so the centering
mean stays multi-speaker (centering one speaker by their own mean cancels the
signal). Those third parties therefore end up in the store as unnamed
contributions. Erase with `forget`:

```bash
pi-voiceid forget --name "Alice"        # drops Alice's voiceprint + her contributions
pi-voiceid forget --recording <id>      # drops a whole recording's contributions
pi-voiceid list                         # shows the cohort and which clusters are unnamed
```

A never-enrolled speaker swept into the pool has no name, so only
`forget --recording <id>` reaches them. `list` makes that visible.

## Procedure

### 1. Check for drift first

```bash
pi-voiceid analyze --srt talk.srt
```

Prints per-cluster coherence and a channel-centered cluster-to-cluster cosine
matrix. Pairs at or above `--drift-threshold` (default 0.55) are flagged as
likely the same person split in two. `analyze` needs no library. The 0.55
default is weakly calibrated (n = 2) — treat it as a starting point.

### 2. Enroll a voice

Best: from a cluster you have already identified in an SRT (plenty of audio).

```bash
pi-voiceid enroll --name "Csákány Róbert" --srt talk.srt --label "Speaker 1"
```

From a standalone clip:

```bash
pi-voiceid enroll --name "Kovács Dániel" --audio sample.m4a --start 12 --end 75
```

Re-running the same name **merges** into the existing voiceprint (segment-count
weighted); `--replace` resets it instead. Enroll the same person from several
recordings — the cheapest accuracy win.

### 3. Label a transcript

```bash
pi-voiceid label --srt talk.srt --dry-run
pi-voiceid label --srt talk.srt
```

Always read the dry-run decision table. `--output` overrides the sibling
`*.named.srt` (and is required for the same-file refusal to be reachable).
`--relabel` allows an already-named transcript and leaves clusters whose name is
already in the library untouched, so the iterative workflow (label, enroll one
more person, label again) is safe.

## Reading the output

| Column | Meaning |
|---|---|
| `cos` | centered cosine of the cluster centroid to the best voiceprint |
| `2nd` / `margin` | runner-up and the gap — a small gap means "could be either" |
| `vote` | share of individual segments agreeing with the centroid's pick |
| `space` | whether the cohort mean or the recording fallback produced the score |

Healthy cross-recording match: `cos` 0.6–0.9, `margin` > 0.5, `vote` > 80 %.
Correct rejection of an un-enrolled person looks like `cos` ≈ 0.0–0.25.

## Pitfalls

- **Centering needs a cohort.** Below 40 embeddings the library-wide mean is not
  trusted; the tool falls back to the recording's own mean, raises the threshold
  by one margin minimum, and says so. Enroll ≥ 3 voices.
- **Never center by one speaker's own mean** — the tool refuses a pool with fewer
  than two speakers, or one speaker above 70 % of the pool's audio duration.
- **Channel mismatch is the dominant failure mode.** A voiceprint from a phone
  memo scores poorly against a conference capture. Enroll per capture setup.
- **Model mismatch is refused.** Voiceprints and cohort contributions record the
  model name and dimension; switching models requires re-enrolling.
- Short segments are noise (`--min-seg` defaults to 1.2 s; do not lower it).
- `pi-voiceid` needs the source media beside the SRT (or `--audio`). Media
  shorter than the last cue is refused — never embed ranges that do not exist.

## Limitations

This is approach (A): post-hoc cluster relabeling. It **cannot**:

- separate **overlapping speech** (two people at once);
- fix a cluster that already merges two speakers (check `analyze` coherence —
  below ~0.55 the cluster itself is impure);
- recover a speaker the diarizer never separated at all.

Those need approach (B), true **TS-VAD / personal VAD**, where the enrollment
embedding feeds a frame-level model. Requires training; out of scope.

Accuracy on real audio is a **lower bound**: it was measured against another
diarizer's labels, not human annotation. Do not quote the segment-level figure
as the product's accuracy.

## Verification

```bash
pi-voiceid label --srt "<enrollment source>.srt" --dry-run   # expect a 1:1 map, high cos
pi-voiceid label --srt "<unrelated recording>.srt" --dry-run # expect mostly UNKNOWN
pi-voiceid list                                              # enrolled voices must not resemble each other
```

Measured on the reference archive: self-consistency 3/3 at cos 0.89–0.99 with a
4th un-enrolled cluster rejected at 0.252; cross-recording matches at cos
0.72–0.89. Full numbers and the six-model comparison are in
[BENCHMARK.md](BENCHMARK.md).
