feat: transcribe long recordings in overlapping chunks
Sending entire recordings in one request can exceed provider limits. Decode audio once to a shared PCM sample clock and split uploads at a configurable maximum of 1 to 30 minutes, defaulting to 30 minutes. Prefer nearby pauses and include six seconds of overlap to protect words at audio cuts while keeping each encoded MP3 within the duration cap. Request word timestamps for chunked text and SRT output. Match overlapping words and times to retain each passage once, warn when a join falls back to timestamps, and generate SRT cues on the original recording timeline. Reuse subtitle timestamp validation without reordering regressing words. Only emit output after every request and conversion succeeds. Document the Python dependency, temporary storage, boundary limitations, and lack of automatic retry or resume. Keep single-request text formatting. Test Plan: - python3 -B -m unittest -v: all 29 tests passed, including mocked provider failures, native MP3 payload checks, boundary stitching, and SRT timing. - bash -n transcribe and shellcheck transcribe: passed. - git diff --cached --check: passed. - Locally decoded and encoded the supplied 74-minute WebM into three MP3 chunks; ffprobe confirmed each duration was below 30 minutes. - Live transcription was not verified: the configured 1Password credential was unavailable.
This commit is contained in:
1 parent
300203d33f
commit
a5c535cfb9
7 files changed
+635
-57
No files matched your search
@@ -1,19 +1,56 @@
|
||||
# Transcribe
|
||||
|
||||
Requires Bash, FFmpeg, curl, jq, and standard coreutils; `--srt` also requires
|
||||
Python 3 and the accompanying `subtitles.py` file.
|
||||
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`.
|
||||
|
||||
```sh
|
||||
./transcribe recording.m4a
|
||||
./transcribe video.mp4 > transcript.txt
|
||||
./transcribe --srt 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; errors to stderr.
|
||||
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.
|
||||
|
||||
Long recordings are automatically split into uploads of at most **30 minutes**,
|
||||
including overlap and MP3 padding. Use `--chunk-minutes N` (an integer from 1 to
|
||||
30) to lower the limit. At 64 kbps, a 30-minute upload is about 14.4 MB of MP3
|
||||
or 19.2 MB of base64. This is a local default, not a guaranteed provider limit:
|
||||
[OpenRouter documents a 60-second processing timeout](https://openrouter.ai/docs/guides/overview/multimodal/stt),
|
||||
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,
|
||||
@@ -26,7 +63,10 @@ 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 file per invocation; the whole recording is sent in one request, so
|
||||
the service's audio length and request size limits apply.
|
||||
One input file per invocation. To run the local regression tests:
|
||||
|
||||
```sh
|
||||
python3 -B -m unittest -v
|
||||
```
|
||||
|
||||
[OpenRouter model and API reference](https://openrouter.ai/microsoft/mai-transcribe-2)
|
||||
Reference in new issue
Block a user