scriba
scriba takes a recorded conversation and writes a Markdown document that says who said what. Transcription is whisperX, diarization is pyannote, and both run on your own machine. The voice registry is the part worth having: pyannote computes a 256-dimensional embedding per speaker anyway, and scriba files it under the name you assign. The same person is picked out by name in the next recording instead of arriving as SPEAKER_02.
scriba run meeting.m4ascriba name meeting.m4a SPEAKER_00=Ada SPEAKER_01=Rafiqscriba run tomorrow.m4a # Ada and Rafiq are picked out on their ownA SwiftUI app sits on top of the Python command line tool. It implements none of the work: it runs the same scriba and reads its JSON state.
What it does not do
Section titled “What it does not do”- It does not run anywhere except macOS on Apple Silicon, with Python 3.10 or newer,
ffmpeg, and a Hugging Face account for the pyannote models. - It does not transcribe faster than the recording lasts on a stock install.
ctranslate2, the engine under faster-whisper, has no Metal backend in any released version, so transcription stays on the CPU. Metal sits behind an open pull request you have to build yourself. - It does not guess a name when two enrolled voices come out close. A match wants cosine similarity of 0.75 and a margin of 0.05 over the runner-up; between 0.55 and 0.75 it suggests and waits. A wrong name raises no error and quietly poisons every transcript downstream, so an unanswered question is the better failure.
- It does not treat those thresholds as published constants. They are a starting point to calibrate on your own material, and for diarization in Italian there is no benchmark to argue with.
- It does not hide the voices it could not identify. Every line spoken by one says so inside the document, and the header lists them too.
- It is an afternoon’s worth of code, written in one sitting, paired with Claude. Take it for what it is.
Install
Section titled “Install”brew install ffmpegconda create -n scriba python=3.11 -y && conda activate scribapip install git+https://github.com/nerln/scribaA plain virtual environment does the same job. The scriba command lives only inside that environment, so every use starts by activating it. The README carries a launcher for ~/.local/bin that finds it wherever conda put it.
The pyannote token goes into the Keychain rather than into a file:
scriba token hf_xxxxxxxxThen accept the conditions on speaker-diarization-3.1, segmentation-3.0 and speaker-diarization-community-1 with the same account. Skip the third and 3.1 breaks too under pyannote 4, which pulls one of its files from there, and the failure is a bare 403.
pyannote 4 ships a telemetry module that reports on every model load and every file processed. scriba turns it off in scriba/__init__.py, before any import can reach pyannote.
scriba run recording.m4ascriba run meeting.m4a --lang es --min-speakers 2 --max-speakers 2scriba dossier meeting.m4a # who talks how much, longest turns, names said out loudscriba name meeting.m4a SPEAKER_00=Ada SPEAKER_01=Rafiqscriba show meeting.m4a # where the document wentscriba voices listscriba watch ~/Memosscriba jobs listscriba whoami ~/Recordings diarizes a folder without transcribing it, compares speakers across recordings while ignoring pairs from inside the same one, and reports the voice present in the most, which is whoever owns the microphone. Confirm with --name and every print of that voice is enrolled at once. The same scan prints where the same-speaker and different-speaker populations separate in your own audio, so the thresholds above are measured rather than assumed.
How it works
Section titled “How it works”Every stage is cached under ~/.scriba/jobs/<name>/, so redoing the names re-transcribes nothing. --force asr|diar|lang|all reruns the stage you name.
lang.pysamples five windows across the file and votes, weighting each by confidence. Left alone, Whisper picks the language from the first 30 seconds, which in a conversation is small talk, and a wrong guess raises no error: the output is fluent invented text.diarize.pycalls pyannote directly, which keeps one file working on pyannote 3 and 4 alike and preserves the segmentation whisperX’s wrapper drops.- Whisper cuts roughly every 30 seconds whoever is speaking. On a seven-minute test file that gave 16 blocks, each holding more than one speaker. Word-level realignment turned it into 41 turns.
export.pywrites a readable document first: participants, duration and language in a header, then turns with the name in bold and a timestamp. Five other formats come out of the same run: plain Markdown, text, SRT, VTT, JSON.
What is checked
Section titled “What is checked”pytest runs the suite in about eight seconds, because none of the tests load a model. They were written by agents pointed at one module each, told to report defects rather than fix them, which turned up around twenty. The commit log holds the ones worth reading: a registry save that could hand one person’s voice print to another, and a language vote that reported a file nobody could identify as its most confident result.
Metal diarization became the default only after its output was compared against the CPU: same 165 turns, same labels, every boundary matching to the millisecond. pyannote issue 1337 reports wrong timestamps under Metal and was closed with no fix, so diarize_device=cpu stays available.
Measured on an M4 with 16 GB, 6:45 of Spanish audio, same settings, warm cache.
| Stage | whisperX 3.3.1 | whisperX 3.8.6 |
|---|---|---|
Load large-v3 int8 |
9.1s | 34.7s |
| Transcription, 8 threads | 601.8s | 443.1s |
| Load the aligner | 0.5s | 2.8s |
| Forced alignment, word level | 13.3s | 14.1s |
| Diarization, pyannote 3.1 on CPU | 372.3s | 211.0s |
| Diarization, community-1 on CPU | 225.9s | |
| Diarization, community-1 on Metal | 35.7s | |
| Total, best of each | 16m 37s | 8m 13s |
The newer whisperX is 26% faster on the transcription. That is not the reason to move to it. In 3.3.1 every token containing digits came out of the aligner with no timestamp at all, 5 out of 5 on the test file. In 3.8.6 that is 0 out of 7.
Two checks run before a commit. tools/stylecheck.py holds the writing rules for prose and comments. tools/check-output-style.py renders all six formats plus the briefing from stand-in data, because a document assembled from fragments can pass line by line and still be wrong as a whole.
Licence MIT. Source: github.com/nerln/scriba.