fix: format text transcripts and show speaker markers

Chunked text output previously joined every word into one line and discarded
speaker labels. Add a shared plain-text formatter that wraps lines, breaks
paragraphs at speaker changes and pauses, and splits long turns while
preserving word order. Display provider speaker IDs with chunk scope where
needed, without implying that IDs identify the same person across requests.

Request structured output for short diarized recordings too, and warn when
speaker labels are missing or incomplete. Keep SRT formatting separate and
preserve existing line breaks for short requests without diarization.

Test Plan:
- python3 -B -m unittest -q: all 43 tests passed.
- bash -n transcribe and git diff --cached --check: passed.
- Reflowed the existing transcript locally and verified all 15,888 words
  were preserved; source audio and generated transcripts are not committed.
- A live excerpt check was blocked before upload by unavailable 1Password
  access; actual provider diarization remains unverified.
This commit is contained in:
ddidderr committed 2026-09-12 09:26:21 +02:00
1 parent 24b1030776
commit 38894c2549
7 files changed
+249 -19

No files matched your search

+27 -6
View File
@@ -1,7 +1,7 @@
# 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
`chunks.py`, `subtitles.py`, and `transcript.py` beside the script. The API key is read using
the 1Password CLI (`op`) from `op://API Keys/openrouter-mai-transcribe-2/credential`.
```sh
@@ -24,8 +24,25 @@ Speaker diarization is **enabled by default** 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. SRT uses returned speaker labels to break cues at
speaker changes; neither output format prints speaker names or IDs.
plain text or `--srt` output. Text output prints returned IDs above each paragraph:
```text
[Speaker 0]
How did you get started?
[Speaker 1]
It was a long time ago.
```
Text wraps at 80 columns (except an unusually long unbroken word), with blank
lines at speaker changes and pauses of at least two seconds. Long turns split
at sentence endings after about 400 characters, or at 800 characters even
without punctuation. The formatter preserves the words and their order.
Unlabelled passages use `[Speaker unknown]` when other words have labels; a
response without any labels is printed without speaker markers. A warning on
stderr reports missing or incomplete labels when diarization was requested.
Speaker IDs come from the provider; they are not names or inferred from text.
SRT uses labels to break cues at speaker changes but does not print the IDs.
Long recordings are automatically split into uploads of at most **10 minutes**
with diarization, or **30 minutes** with `--no-diarization`, including overlap
@@ -47,7 +64,9 @@ 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.
Requests with diarization and all chunked requests ask for word timestamps,
including for plain text. Short requests with `--no-diarization` retain the
provider's existing text line breaks and add wrapping to long lines.
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
@@ -58,8 +77,10 @@ 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.
are local to each request; text marks them as `[Chunk 1, Speaker 0]`,
`[Chunk 2, Speaker 0]`, etc. The same ID in different chunks does **not** establish
that it is the same person. The script does not identify speakers across
chunks. Short recordings still use one request and use `[Speaker 0]` markers.
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