Source

Transcription Pipeline

A watch-folder pipeline that turns recordings into transcripts that say who said what: local Whisper by default or a hosted engine for quality, word-level alignment, pyannote speaker separation, and a copy filed into a vault inbox. The manual transcriber it grew out of ships alongside.

  • Python
  • WhisperX
  • pyannote.audio
Repository
mattrguerra/transcription-pipeline
Branch
main
Files
39 files · 9 folders
Languages
Python 99% · Shell 1%
Visibility
Private on GitHub, published here in full

transcription-pipeline

[tests]

Drop a recording in a folder; get back a transcript that says who said what, stamped to the word, filed where the rest of your notes live.

inbox/  --launchd WatchPaths-->  transcribe inbox
  1. take files whose size has stopped changing; one run at a time (lock file)
  2. ffmpeg          any audio or video -> 16 kHz mono WAV
  3. transcribe      local: faster-whisper large-v3-turbo, int8   (default)
                     deepinfra: hosted Whisper large-v3-turbo     (optional)
  4. align           wav2vec2 forced alignment -> a start and end time for every word
  5. diarize         pyannote -> who spoke when; each word gets a speaker
  6. shape           sentences (split at punctuation and at every change of speaker),
                     speakers renumbered "Speaker 1, 2, ..." by first appearance
  7. write           transcripts/<name>.txt .md .json (+ .srt .vtt); the .md also to a vault
  8. file            recording -> done/, or failed/ with <name>.error.txt

Steps 3 to 5 are WhisperX. The rest is this package: the watch folder, the sentence and speaker shaping, the writers, and the plumbing that makes it safe to leave running.

What a transcript looks like

From the integration test, which runs on two LibriSpeech readers taking turns (tests/fixtures/two-speakers.flac):

[00:00:00] Speaker 1: for a full hour he had paced up and down waiting but he could wait no longer
[00:00:06] Speaker 2: harangue the tiresome product of a tireless tongue
[00:00:13] Speaker 1: pride after satisfaction uplifted him like long slow waves
[00:00:19] Speaker 2: anger pain painful to hear

The model returned that clip as one unpunctuated segment. It still comes out as four turns, because sentences are also split wherever the speaker changes.

The .md is a vault note: this frontmatter, then one paragraph per speaker turn. The .json keeps every segment, every timed word and the run's metadata.

---
type: transcript
title: "two-speakers"
recorded: 2026-09-28
transcribed: 2026-09-28T06:15:09
duration: "0:00:24"
engine: local
language: en
source: "two-speakers.flac"
speakers:
  - "Speaker 1"
  - "Speaker 2"
tags:
  - transcript
---

Use it

transcribe call.m4a                          # local, with speakers; outputs beside the file
transcribe *.mp3 --out ~/transcripts --formats txt,srt
transcribe interview.mov --speakers 2        # you know how many voices there are
transcribe memo.m4a --no-diarize             # just the words
transcribe call.m4a --engine deepinfra       # hosted Whisper; uploads the audio
transcribe inbox --root ~/Transcription      # what launchd runs

Install

python3.12 -m venv .venv && .venv/bin/pip install -e ".[test]"

Python 3.10 to 3.13 (WhisperX does not support 3.14 yet). Tokens, the gated diarization model and the launchd job: docs/SETUP.md.

Tests

pytest -q                                          # unit tier: no network, no models (CI)
TP_INTEGRATION=1 pytest -q tests/integration       # real models; add HF_TOKEN for pyannote

The integration tier checks every reference word, monotonic word times and the four turn boundaries on four utterances from LibriSpeech test-clean (Panayotov, Chen, Povey and Khudanpur, 2015), used under CC BY 4.0.

Choices worth knowing

  • Local by default. DeepInfra's hosted Whisper has transcribed noticeably better than local int8 faster-whisper; use it when quality matters and uploading is fine.
  • Timed to the word. A sentence starts at its first aligned word; where nothing aligns, the time is interpolated and the JSON marks it "timed": false.
  • Sentence rules pinned by a golden file. sentences.py ports a JavaScript splitter; tests/fixtures/sentences-golden.json holds that splitter's output.
  • Speakers. Sentences split at every change of speaker; a one-word run is diarization noise; labels are renumbered by first appearance.
  • Safe to leave running. Settle window, one run at a time with stale-lock takeover, failed/ with the reason, never an overwrite; tokens from the env or the Keychain.

Layout

Path What it is
transcription_pipeline/ The package: cli, pipeline, inbox, audio, engines/, align, diarize, sentences, shape, render, filing, secrets, steps.
tests/ Unit tier (runs in CI) and tests/integration (real models on real speech).
scheduling/ The launchd plist and the script it runs.
docs/SETUP.md Install, tokens, the watch folder.

Where it came from

It grew out of a small DeepInfra transcriber with a one-page UI. The pipeline kept its sentence rules, [HH:MM:SS] lines and never-overwrite filing, and added the rest.

License

MIT.