ddidderr 24b1030776 feat: enable diarization by default with an opt-out
Request speaker diarization through OpenRouter's Azure provider options for
all transcription uploads. Add --no-diarization to explicitly disable it for
both plain text and SRT output; returned speaker labels let existing SRT
cue generation split at speaker changes.

Default to 10-minute chunks when diarization is enabled because Microsoft
reports failures around 15 minutes and longer. Preserve the 30-minute
default with the opt-out and honor explicit --chunk-minutes values in both
modes. Document the options, duration limits, and output behavior.

Test Plan:
- python3 -B -m unittest -v: all 32 tests passed, including request options,
  default upload limits, opt-out combinations, and SRT speaker boundaries.
- bash -n transcribe: passed.
- ./transcribe --help: verified the new option and chunk defaults.
- git diff --cached --check: passed.
- Provider responses were mocked; no live API request was made.
2026-09-12 09:04:29 +02:00
2026-09-12 08:26:16 +02:00

Transcribe

Requires Bash, FFmpeg, curl, jq, Python 3, and standard coreutils. Keep chunks.py and subtitles.py beside the script. The API key is read using the 1Password CLI (op) from op://API Keys/openrouter-mai-transcribe-2/credential.

./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

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), 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 provider.options.azure.diarization.enabled, following OpenRouter's speaker diarization API. Use --no-diarization to explicitly disable it for every request, with either plain text or --srt output. SRT uses returned speaker labels to break cues at speaker changes; neither output format prints speaker names or IDs.

Long recordings are automatically split into uploads of at most 10 minutes with diarization, or 30 minutes with --no-diarization, including overlap and MP3 padding. These defaults account for Microsoft's documented diarization failures around 15 minutes and longer. Use --chunk-minutes N (an integer from 1 to 30) to override the limit in either mode. Larger chunks with diarization may fail with a timeout or diarization_unavailable; reduce the chunk size or use --no-diarization. At 64 kbps, a 10-minute upload is about 4.8 MB of MP3 or 6.4 MB of base64; a 30-minute upload is about 14.4 MB of MP3 or 19.2 MB of base64. These are local defaults, not guaranteed provider limits: OpenRouter documents a 60-second processing timeout, so reduce the chunk size if requests still time out.

For each boundary, the script looks for a pause of at least 0.3 seconds in the last 30 seconds before the limit, using a -35 dBFS peak threshold. It joins in the middle of the last suitable pause, or at the duration boundary if there is no pause. Each upload includes three seconds beyond the join, so neighbouring uploads share six seconds of audio even during continuous speech. The duration cap includes this overlap; the next chunk starts before the previous one ends.

Chunked requests always ask for word timestamps, including for plain text. The script matches words and their times in the overlap, switches transcripts at a shared word, and keeps the repeated passage once. It prefers a matching phrase to distinguish intentional repetitions. If no words match, it uses timestamp ownership and warns when speech near the join needs review. Overlap protects words at the audio cuts, but recognition errors or inaccurate model timestamps can still require corrections. Missing or invalid word timestamps 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. Speaker labels, when returned, are local to each request; the script does not identify the same speaker across chunks. Short recordings still use one request.

Audio is first decoded to temporary mono 16 kHz PCM (about 115 MB per hour), so all cuts share the same sample clock even when container duration metadata is missing. Only one MP3 chunk and its request payload are staged at a time. Requests run sequentially. A failed request stops the run; there is no automatic retry or resume, and rerunning sends the earlier chunks again.

--srt requests word timestamps using OpenRouter's verbose_json response. Words are grouped into cues of at most two lines (42 characters per line, except an unusually long unbroken word) and six seconds. Cues also break at sentence endings, pauses of at least 0.7 seconds, and speaker changes when speaker labels are available. Timing comes from the words, rounded to milliseconds. If word start times move backwards, they are clamped to the preceding word's start without reordering the transcript; end times are extended to the adjusted start only when needed. Subtitle output goes to stdout; missing or invalid word timestamps produce an error. Without --srt, output remains plain text.

One input file per invocation. To run the local regression tests:

python3 -B -m unittest -v

OpenRouter model and API reference

S
Description
No description provided
Readme
272 KiB
0 Stars 1 Watchers 0 Forks
Languages
Python 91.7%
Shell 8.3%