Skip to content

feat: ElevenLabs Scribe backend with speaker diarization - #40

Merged
Railly merged 4 commits into
mainfrom
feat/elevenlabs-backend
Aug 19, 2026
Merged

feat: ElevenLabs Scribe backend with speaker diarization#40
Railly merged 4 commits into
mainfrom
feat/elevenlabs-backend

Conversation

@Railly

@Railly Railly commented Aug 19, 2026

Copy link
Copy Markdown
Member

Fourth transcription backend, and the first that reports who is speaking.

trx interview.m4a -b elevenlabs --speakers 2 -l spa
1
00:00:01,900 --> 00:00:03,400
[speaker_0] Hola, que tal, Fiorela? Escuchas?

2
00:00:04,520 --> 00:00:05,900
[speaker_1] Si, te escucho bien. Que tal?

What it adds

  • --backend elevenlabs, models scribe_v2 (default) and scribe_v1
  • --diarize labels every cue with its speaker and turns the .txt into a conversation, one paragraph per turn
  • --speakers <n> (1 to 32, the range the API accepts) implies --diarize
  • ELEVENLABS_API_KEY from the environment, falling back to the macOS login Keychain entry elevenlabs
  • ISO 639-3 language codes on this backend only (es and spa both work; Scribe normalizes both to spa)

No chunking path: the API limit is 5 GB, not the 25 MB and 100 MB that make the other cloud backends split files.

Design decision worth reviewing

Scribe timestamps every word, so cues have to be grouped or the SRT is one cue per word. Three cut rules, in priority order:

  1. a change of speaker_id always cuts, not subject to the other two, because a cue holding two voices misattributes the line
  2. a pause of 0.6s or more cuts, approximating a sentence boundary since Scribe does not always punctuate
  3. 84 characters caps the rest, the two-line subtitle convention

Rules 2 and 3 are conventions picked to read well, not measurements derived from any corpus. That is the main thing to review.

Two smaller calls:

  • --diarize and --speakers are an error on the other backends rather than ignored. Silently dropping them returns an undiarized transcript that looks like the request succeeded.
  • Entries with type: "spacing" are filtered out. They are the gaps between words, and counting them inflates word counts and emits empty cues.

Verification

  • biome check and bun test both exit 0. 84 pass, 0 fail, 20 new tests
  • Re-run under the exact CI environment (seeded config with no elevenlabs block, no ELEVENLABS_API_KEY, no Keychain reachable): still 84 pass. No test reaches the API
  • A pre-upgrade ~/.trx/config.json with no elevenlabs key still loads, defaults merged, existing settings preserved
  • End to end on a real 26 minute two-speaker Spanish interview: exit 0 in 39s, 4855 words, 2 speakers, 488 cues. Validated structurally: sequential indices, monotonic non-negative timestamps, and no cue mixing two speakers. Transcript covers 25:56 of a 25:59 recording, and the opening turns alternate correctly
  • Non-diarized run on the same audio emits no labels

Version left at 0.8.1. Bumping it triggers the publish workflow, which is a release decision rather than part of this change. The 0.9.0 changelog entry is included and ready for whenever that bump happens.

Railly added 4 commits August 19, 2026 16:32
Fourth transcription backend, and the first that reports who is speaking.
No chunking path: the API limit is 5 GB, unlike the 25 MB and 100 MB that
make the OpenAI and Vercel backends split files.

Scribe timestamps every word, so words are grouped into readable cues:
a pause of 0.6s or more cuts, a cue is capped at 84 characters, and a
change of speaker always cuts regardless of the other two, because a cue
holding two voices misattributes the line. The first two thresholds are
subtitle conventions chosen to read well, not measurements.

Entries with type "spacing" are filtered out; they are the gaps between
words and would inflate word counts and emit empty cues.

The key comes from ELEVENLABS_API_KEY first, matching the other cloud
backends so CI and containers work with no Keychain, then from the macOS
login Keychain so a key stored there needs no export.
--diarize and --speakers <n> reach the elevenlabs backend, and are an
error on local, openai and vercel rather than being ignored: no other
backend returns speaker labels, so dropping the flag quietly would hand
back an undiarized transcript that looks like the request succeeded.

--speakers implies --diarize, since the API only returns labels when
diarization is on, and is rejected outside 1 to 32, the range the API
accepts.

Language validation now runs after the backend is resolved, because the
accepted set depends on it: Scribe takes ISO 639-1 and ISO 639-3 alike
and normalizes both to 639-3, while the existing table is the set
whisper.cpp accepts and cannot be widened for every backend.

doctor resolves the key through the same path a real run uses, so it
cannot report missing a key that trx would have found in the Keychain.
Covers the speaker-change cut, the pause and length cuts, that spacing
entries never become cues or words, that labels appear only when
diarization is on, and that --diarize and --speakers are rejected where
they cannot be applied. No test reaches the API.
Schemas, AGENTS.md, the bundled skill, README and the site all enumerate
the backends, so each one gains elevenlabs, --diarize and --speakers.
Adds the 0.9.0 changelog entry. Version left at 0.8.1: bumping it
triggers the publish workflow, which is a release decision.
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
trx Ready Ready Preview Aug 19, 2026 7:33pm
trx-website Ready Ready Preview Aug 19, 2026 7:33pm

@Railly
Railly merged commit f8809ef into main Aug 19, 2026
5 checks passed
@Railly
Railly deleted the feat/elevenlabs-backend branch August 19, 2026 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant