fix: reconcile speakers automatically by default

The plain transcribe command still used chunk overlap speaker matching,
so users had to discover a model flag or supply a known speaker count to
get the improved voice comparison behavior.

Enable OpenRouter voice comparison with automatic speaker counting for
normal text and SRT invocations. Keep --speakers and --speaker-model as
optional overrides, and let --no-diarization disable both provider labels
and voice comparison. Make the basic command the documentation entry point
and clarify the defaults in CLI help and repository guidance.

Cover bare invocations across one and multiple uploads, default SRT,
optional count constraints, opt-out ordering, and atomic output when the
default comparison fails.

Test Plan:
- python3 -B -m unittest -v: all 85 tests passed.
- bash -n transcribe: passed.
- git diff --cached --check: passed.
- Tests use real audio encoding with fake provider responses. Live voice
  accuracy remains unverified; no live API request was made for this change.
This commit is contained in:
ddidderr committed 2026-09-12 10:28:42 +02:00
1 parent 138c796399
commit d7827d39f5
5 files changed
+107 -48

No files matched your search

+25 -32
View File
@@ -1,23 +1,18 @@
# Transcribe
```sh
./transcribe too_long.webm > transcript.txt
```
Speaker detection and matching across the recording happen automatically.
You do not need to know the number of speakers or choose a model.
Use `--srt` for subtitles or `--no-diarization` for text without speaker labels.
Requires Bash 5.1 or newer, FFmpeg, curl, jq, Python 3, and standard coreutils. Keep
`chunks.py`, `subtitles.py`, `transcript.py`, and `diarization.py` beside the script.
The API key is read using the 1Password CLI (`op`) from
`op://API Keys/openrouter-mai-transcribe-2/credential`.
```sh
./transcribe recording.m4a
./transcribe video.mp4 > transcript.txt
./transcribe --srt video.mp4 > subtitles.srt
./transcribe --no-diarization recording.m4a > transcript.txt
./transcribe --srt --no-diarization video.mp4 > subtitles.srt
./transcribe --chunk-minutes 10 too_long.webm > transcript.txt
./transcribe --jobs 4 too_long.webm > transcript.txt
./transcribe --jobs 1 --srt too_long.webm > subtitles.srt
./transcribe --speakers 2 too_long.webm > transcript.txt
./transcribe --speaker-model google/gemini-3.8-flash recording.m4a > transcript.txt
```
Accepts m4a, mp4, aac, flac, opus, mp3, ogg, wav, and other formats your
FFmpeg supports. Uses the first audio track, converts it to mono 16 kHz MP3
at 64 kbps (requires FFmpeg's libmp3lame encoder),
@@ -25,11 +20,14 @@ and sends it to OpenRouter's `microsoft/mai-transcribe-2` model. Temporary
files are removed on exit. The transcript goes to stdout; progress and errors
go to stderr. Output is emitted only after all requests and conversion succeed.
Speaker diarization is **enabled by default** through
Speaker diarization and voice comparison are **enabled by default**.
MAI first labels speech through
`provider.options.azure.diarization.enabled`, following
[OpenRouter's speaker diarization API](https://openrouter.ai/docs/guides/overview/multimodal/stt#verbose-transcripts-timestamps-and-speakers).
Use `--no-diarization` to explicitly disable it for every request, with either
plain text or `--srt` output. Text output prints returned IDs above each paragraph:
Gemini then compares voice excerpts through OpenRouter to match speakers across
the recording, automatically estimating their number. `--no-diarization`
disables both steps, with either plain text or `--srt` output. Text output prints
speaker IDs above each paragraph:
```text
[Speaker 0]
@@ -83,22 +81,17 @@ are an error. Plain text for a chunked recording is assembled from the words.
SRT timestamps are shifted back to the original recording's timeline before
cue generation, with continuous cue numbering. Text uses `[Speaker 0]`,
`[Speaker 1]`, etc., without exposing upload boundaries to the reader.
For a chunked recording, provider IDs are mapped to transcript-wide IDs:
at least two unambiguous words shared at matching times in the overlap must
agree on the speaker correspondence. Conflicting or absent evidence gets a
new ID and a warning on stderr. This is not voice recognition across the whole
recording: the same person can receive another ID when absent from an overlap
or when diarization disagrees. Numeric provider IDs alone are never used to
identify a speaker across requests.
Provider IDs are request-local. The automatic voice comparison maps them to
transcript-wide IDs using audio evidence, including for people absent from a
chunk overlap. Numeric provider IDs alone never establish identity across
requests.
For interviews and other recordings with fragmented speaker IDs, use
`--speakers N` (1 to 64) to add a voice comparison pass through OpenRouter.
This selects `google/gemini-3.8-flash` and tells it the known number of people.
Use `--speaker-model MODEL` to select a different audio chat model supporting
multiple audio inputs and JSON schema output; without `--speakers`, the model
estimates the number of voices. Both options require
diarization and cannot be combined with `--no-diarization`. The normal command
without either option retains the overlap-based behavior described above.
Voice comparison uses `google/gemini-3.8-flash` by default and estimates the
number of speakers. If you already know the count, `--speakers N` (1 to 64)
supplies it as an optional constraint. `--speaker-model MODEL` overrides the
default with another audio chat model supporting multiple audio inputs and JSON
schema output. Neither flag is needed for the normal command. Both options
require diarization and cannot be combined with `--no-diarization`.
The comparison keeps MAI's words and timestamps, initially treating IDs from
different requests as separate. It takes up to three nonoverlapping excerpts
@@ -150,7 +143,7 @@ cancels the remaining workers. Interruptions also stop workers and their child
processes before temporary files are removed. Requests already accepted by the
provider may still be processed and billed. There is no automatic retry or
resume, and rerunning sends earlier chunks again. No transcript is emitted
unless every worker, any requested voice comparison, and the final conversion
unless every worker, the enabled voice comparison, and the final conversion
succeed.
`--srt` requests word timestamps using OpenRouter's `verbose_json` response.