speech-diarizer
turn a local media file into a speaker-labeled transcript where each turn is attributed to a speaker (SPEAKER_00, SPEAKER_01, ...). this is the diarization-specialized counterpart to speech-video-transcriber: use this one whenever the recording has more than one voice and the user cares who said what.
the pipeline is fully local and runs well on apple silicon:
- transcribe with
faster-whisper(via whisperx), default modellarge-v3, on cpu (ctranslate2 is cpu-only on macos) - align to word-level timestamps
- diarize with
pyannote/speaker-diarization-3.1(uses mps on apple silicon when available) - assign speakers to words and write
.txt,.srt, and.json
self-contained scripts
scripts/diarize.py- the engine. a PEP 723 inline-script declaring its own python (>=3.10,<3.13) and dependency (whisperx>=3.3), souv run --scriptbuilds and caches an isolated environment automatically. nothing is installed into the repo's shared.venv.scripts/enroll.py- names speakers in the voiceprint store (PEP 723, depends only onnumpy).scripts/voiceprints.py- shared store + cosine-matching module imported by both (not run directly).- the enrolled voiceprint store is machine-global, not bundled with the skill. it lives at
~/.config/speech-diarizer/voiceprints.jsonby default, so every copy of the scripts on this machine shares one set of voiceprints. override with--store /pathor$SPEECH_DIARIZER_VOICEPRINTS(point it at an iCloud/synced path for cross-machine use).
speaker identification (who, not just which)
diarization alone gives anonymous SPEAKER_00/01/02 labels that mean nothing across recordings. this skill adds identification: pyannote already emits a voice embedding ("voiceprint") per speaker, and the skill matches those against an enrolled store by cosine similarity.
the flow is learn-as-you-go:
- run
diarize.py- it caches each speaker's embedding to~/.cache/speech-diarizer/, and auto-labels any speaker matching an enrolled voiceprint above--threshold. unmatched speakers staySPEAKER_XX(and the.mdfrontmatter lists them as unrecognized). - read the
.md, recognize an unknown speaker, and enroll them once withenroll.py <run-stem> SPEAKER_XX=Name. - every future recording auto-resolves that person. enrolling the same person from more recordings sharpens their voiceprint (embeddings are averaged into a centroid).
constraints:
- embeddings only compare within ONE diarization model. the store records its model on first enroll and refuses to mix; always run with the same
--diarization-model(defaultpyannote/speaker-diarization-3.1). - default threshold is
0.5. observed margins are wide (a person matches their own voiceprint near1.0while different speakers score near0), so0.5is safe; raise it if you ever see a false match, lower it if a known person is missed.
when to use it
use this skill when the user wants any of the following:
- a transcript that distinguishes multiple speakers
- "who said what" from an interview, meeting, call, or podcast
- speaker-attributed quotes for later analysis or coaching
- subtitles (
.srt) with speaker tags
if the recording is a single speaker, prefer speech-video-transcriber - it is lighter and faster. use this skill specifically when speaker separation matters.
required setup
ffmpegon PATH (whisperx uses it to load audio)uv(the script runs viauv run --script; deps install on first run)- huggingface auth, resolved in this order:
--hf-tokenflag, then$HF_TOKEN, then the cachedhf auth logintoken (~/.cache/huggingface/token). being logged in via the cli is enough - no.enventry required. - accepted licenses for the two gated pyannote models (one-time, free), on the logged-in account:
if not logged in, run hf auth login once, or add export HF_TOKEN=hf_... to ai-agents-config/.env and source .env before running.
workflow
- resolve the media path to an absolute path before running anything
- if the user knows the speaker count, pass
--num-speakers(or--min-speakers/--max-speakers) - this sharply improves accuracy - if the user gives a language hint, pass
--language - run the script (the diarization step is the slow part; large-v3 on cpu is fine on m-series but not instant)
- return the output paths, and optionally relabel
SPEAKER_00/01/...with real names from the.txtif the user identifies them
command
the first invocation resolves and caches the environment, so it takes longer; later runs are fast to start.
cd /Users/rami/Documents/life-os/ai-agents-config/skills
uv run --script speech-diarizer/scripts/diarize.py "/absolute/path/to/recording.m4a"
common options:
# known number of speakers (most accurate)
uv run --script speech-diarizer/scripts/diarize.py \
"/absolute/path/to/call.m4a" \
--num-speakers 3 --language en
# bound the speaker count when exact number is unknown
uv run --script speech-diarizer/scripts/diarize.py \
"/absolute/path/to/meeting.mp4" \
--min-speakers 2 --max-speakers 5
# write outputs to a chosen directory (default: ~/Documents/transcriptions)
uv run --script speech-diarizer/scripts/diarize.py \
"/absolute/path/to/interview.wav" \
--num-speakers 2 \
-o "/absolute/path/to/transcripts"
# multiple files in one run
uv run --script speech-diarizer/scripts/diarize.py a.m4a b.m4a c.wav --num-speakers 2
# faster, lower-accuracy pass for a quick check
uv run --script speech-diarizer/scripts/diarize.py \
"/absolute/path/to/recording.m4a" --model small
enrolling speakers
after a run, name the speakers you recognize so they auto-resolve next time. reference the raw SPEAKER_XX labels (visible in the run's stderr summary and the sidecar):
cd /Users/rami/Documents/life-os/ai-agents-config/skills
# enroll by run stem - enroll.py finds the cached embeddings automatically
uv run --script speech-diarizer/scripts/enroll.py call SPEAKER_00=Rami "SPEAKER_01=Full Name"
# (also accepts a full base path or the .voiceprints.json directly)
# add another sample for someone already enrolled (just enroll again from a new run)
uv run --script speech-diarizer/scripts/enroll.py another-call SPEAKER_02=Rami
# inspect or prune the store
uv run --script speech-diarizer/scripts/enroll.py --list
uv run --script speech-diarizer/scripts/enroll.py --remove Keith
options
audio- one or more media files (positional; wav, mp3, m4a, mp4, etc.)--num-speakers N- exact speaker count if known (best accuracy)--min-speakers N/--max-speakers N- bound the count when exact is unknown--model- whisper size, defaultlarge-v3(trysmallormediumfor speed)--language- language code likeen(default: auto-detect)--diarization-model- pyannote pipeline (defaultpyannote/speaker-diarization-3.1; trypyannote/speaker-diarization-community-1if whisperx ships pyannote.audio>=4)--batch-size- transcription batch size (default 8)--hf-token- token override (default$HF_TOKEN)-o/--output-dir- output directory (default:$SPEECH_DIARIZER_OUTPUT_DIRor~/Documents/transcriptions)--threshold- cosine similarity needed to attach an enrolled name (default0.5)--no-identify- skip voiceprint matching entirely (always anonymousSPEAKER_XX)--store- voiceprint store path (default$SPEECH_DIARIZER_VOICEPRINTSor~/.config/speech-diarizer/voiceprints.json);enroll.pytakes the same flag
output contract
the deliverable is ONE clean file. for each input the script writes, into the output dir (-o, else $SPEECH_DIARIZER_OUTPUT_DIR, else ~/Documents/transcriptions):
<name>.md- the primary output. yaml frontmatter (source, generated, duration, language, speakers, models) followed by speaker-labeled turns:**[mm:ss] <speaker>:** text, consecutive same-speaker segments merged.<speaker>is the enrolled name when identified, elseSPEAKER_XX. readable by both a person and an ai tool, and the frontmatterspeakerslist flags who still needs enrolling. this is the only file written by default.
extras, only when asked:
--srt-><name>.srtsubtitles with[<speaker>]tags--json-><name>.jsonfull whisperx result (word-level timing + speakers) for programmatic use
machinery, never in the output dir:
- per-run speaker embeddings are cached at
~/.cache/speech-diarizer/<name>.voiceprints.jsonfor later enrollment. you don't manage these -enroll.pyreads them by run stem.
the script overwrites files of the same name. write to a fresh -o dir to preserve prior runs.
failure handling
- if no token is found (
HF_TOKENunset and no--hf-token), the script exits with setup instructions. trysource .envfirst. - if a gated model returns 401/403, the script detects it and tells the user the model gate hasn't been accepted on that account (links above)
- if
ffmpegis missing, whisperx audio loading fails - install it (brew install ffmpeg) - do not paraphrase the transcript in place of the output files