Skip to content

feat(transcription): offer Voxtral Mini Transcribe 3 and Qwen3 ASR on OpenRouter - #544

Open
chhoumann wants to merge 8 commits into
feat/transcription-speaker-mergefrom
feat/transcription-openrouter-models
Open

chhoumann wants to merge 8 commits into
feat/transcription-speaker-mergefrom
feat/transcription-openrouter-models

Conversation

@chhoumann

@chhoumann chhoumann commented Nov 14, 2026 •

Copy link
Copy Markdown
Owner

Why

Users should be able to pick from current transcription models without anyone landing on a worse or retired one. This PR adds the two plain OpenRouter models (no speaker labels) that measured best this week against NPR human transcripts.

Summary

The OpenRouter Model row now lists five models, in this order: Grok STT 2.0 (recommended), ElevenLabs Scribe v2, MAI-Transcribe 2, Voxtral Mini Transcribe 3, Qwen3 ASR 1.7B.

Model Mean word error Speaker labels Request Answer Part size
mistralai/voxtral-mini-transcribe-3-0 4.55%, the best measured on OpenRouter no multipart model, file, response_format=verbose_json, no timestamp_granularities (it returns 400 "Provider returned 400") segments[] with start, end, text, no words 12 MB, 900 s (20 MB took 33.5 s)
qwen/qwen3-asr-1.7b 6.05%, about $0.03 per hour no multipart model, file, response_format=verbose_json, timestamp_granularities[]=word timed words[] 12 MB, 900 s (20 MB took 34.5 s)
  • A part that comes back with only segments becomes timestamped paragraphs. Each segment is one cue, timed from the episode's start the same way words are. PartTranscript gains a { segments } variant. The wire shapes stay inside openrouter.ts.
  • An OpenRouter answer with neither words nor segments still fails with "OpenRouter found no speech in this episode." It is not saved as an untimed note. Only OpenAI, whose provider returns text by design, writes untimed notes.
  • If speaker labels are on and one of these models is chosen, PodNotes behaves as it does for any model without labels. The Speaker labels row reads " does not label speakers." and the note is saved without labels, with a notice that stays on screen.
  • One small fix comes along. The notice used to name the provider, so it read "because OpenRouter does not label speakers. These providers do: ..., OpenRouter." It now names the model ("because Voxtral Mini Transcribe 3 does not label speakers."), as the settings row already does. This notice is not on master, so no released wording changes.

Stacked on #542.

Verification

Commands, all with Node 22.23.2 through fnm exec:

  • npm run lint, npm run format:check, npm run typecheck, npm run build, npm run test (124 files, 2,894 tests), npm run test:tooling (48 tests), npm run docs:build: all exit 0.
  • npm run test:e2e -- tests/e2e/journey-resilience.test.ts -t "J-TR-": 11 passed, including the new J-TR-11 (Voxtral) and J-TR-12 (Qwen3 ASR) on Obsidian 1.14.4.
  • npm run size: gzip 107,084 bytes. The base commit 5eebf37, built from a git archive export with the same command, measured 106,873, so this PR adds 211 gzip bytes (728 raw). The gzip budget goes up by exactly those 211 bytes, from 106,970 to 107,181, which leaves the same 97 bytes of headroom as before. scripts/size-baseline.md records this.

Planted defects, each run with npm run plant. Every one failed the named test and passed again once restored:

Plant Caught by
Voxtral sends timestamp_granularities[]=word mistralai/voxtral-mini-transcribe-3-0 gets the upload as its measured multipart request, ... (unit), and J-TR-11 in real Obsidian, where the stub answers it with the measured 400
Qwen3 ASR omits timestamp_granularities (uses Voxtral's request) qwen/qwen3-asr-1.7b gets the upload as its measured multipart request, ...
Part offset dropped for segments (("words" in transcript ? start : 0) + item.start) Voxtral's segments from two parts of at most 900 s become paragraphs timed from the episode's start
Voxtral parts at 1,800 s instead of 900 s the same two-part test
An answer with no words and no segments saved as untimed text an answer with neither words nor segments writes no note
A segments answer treated as untimed (isTimed checks for words only) the Voxtral request test
Qwen3 ASR and Voxtral listed in the wrong order choosing it shows its API key row and its five models, Grok STT 2.0 recommended
Voxtral profile says speakers: true choosing mistralai/voxtral-mini-transcribe-3-0 says Speaker labels are not available, ...
Notice names the provider again (choice.label) with speaker labels on is saved without them, and the notice names the providers that label speakers and stays

Runtime check in this worktree's isolated Obsidian 1.14.4 on Linux (headless, Xvfb), using a fresh vault and a fake key. The .claude/skills/verify/net.mjs fixture provider answered every request, so no real key was involved. The local file was Fixtures/Two Voices.mp3, 70 min of 8 kbps mono, written with the recipe's ffmpeg line (4,200,452 bytes). For each model I chose it in Settings > PodNotes > Model with the keyboard, left Speaker labels on, played the file through Play with PodNotes in the file menu, and ran Transcribe current episode from audio from the palette.

  • Voxtral: five multipart/form-data POSTs of 840,311 to 840,715 bytes (840 s each), one after the other, and no reel. The fixture answers a word-times request with a 400, and the note was saved, so the request did not ask for word times. The note has paragraphs at 00:00:00, 00:00:12, 00:14:00 (time=840.096), 00:14:12, 00:28:00 and so on up to 00:56:12, with no speaker labels. Clicking 00:28:12 moved the player to 1,693.66 s, 1.5 s past the link's 1,692.192 while it kept playing. The notice read "Saved without speaker labels, because Voxtral Mini Transcribe 3 does not label speakers. These providers do: ElevenLabs, Deepgram, OpenRouter." dev:errors: none.
  • Qwen3 ASR: five POSTs of 840,409 to 840,813 bytes. Each is 98 bytes larger than Voxtral's, the size of its timestamp_granularities[] field. The note times match Voxtral's, and the notice names Qwen3 ASR 1.7B. data.json stored transcript.model: "qwen/qwen3-asr-1.7b".
Settings, Voxtral Note, Voxtral
Settings with Voxtral Mini Transcribe 3 chosen Voxtral note with episode-absolute timestamps
Settings, Qwen3 ASR Note, Qwen3 ASR
Settings with Qwen3 ASR 1.7B chosen Qwen3 ASR note with episode-absolute timestamps

Two things in the screenshots come from elsewhere. The warning icon on the key is Obsidian's own "Secrets are not encrypted", because headless Linux has no keyring. The doubled "Two Voices" is Obsidian's inline title shown above the template's # {{title}}.

I have not run this against the real OpenRouter API. The lead verifies with a real key.

Real OpenRouter key (lead, after review)

Obsidian 1.14.4, Linux, palette command, speaker labels off, a 20 min local clip of E231 (expect 2 parts at 900 s), a pass-through request log:

  • Voxtral Mini Transcribe 3: one catalog GET, two POSTs of 8.10 and 6.94 MB, saved in 25 s. 3,333 words, segment times running 00:00:19 to 00:19:42 across both parts.
  • Qwen3 ASR 1.7B before the fix below: the same two POSTs, saved in 22 s, but the note had no punctuation at all ("Hey everyone and welcome to another episode of The Five These Times My name is ...") and paragraphs cut every 60 s. Its words carry no punctuation (0 sentence ends in 3,785 words on Planet Money), while its text does (322).
  • Qwen3 ASR after the fix: saved in 33 s, "Hey everyone, and welcome to another episode of The Five These Times. My name is ...", 340 sentence ends in 3,366 words, paragraphs at 00:00:19, 00:00:54, 00:01:24. dev:errors none.

Review fix (lead)

  • Qwen3 ASR words take their punctuated token from text, matched in order with three tokens of lookahead, only when no word carries punctuation. Plants: keep the bare word on a match; one token of lookahead. Each is caught by Qwen's words, which it lists without punctuation, take their punctuation from its text.
  • Gzip budget raised by the measured 100 bytes to 107,281 (107,184 measured).
  • Checks: lint, format, typecheck, build, size, 2,895 unit tests, 48 tooling tests.

Release impact

There are no settings or storage changes. transcript.model can now hold two more OpenRouter IDs. An older PodNotes that reads them treats them as unknown and runs its recommended model, which is existing behavior. The speaker-label notice now names the model instead of the provider, and that notice has not been released yet.

Note

Add Voxtral Mini Transcribe 3 and Qwen3 ASR 1.7B to OpenRouter transcription

  • Adds OpenRouter model profiles for Voxtral Mini Transcribe 3 and Qwen3 ASR 1.7B in openrouter.ts. Both upload multipart audio with 12 MB and 900-second part caps and do not support speaker labels or URL input.
  • Voxtral requests verbose JSON with segment times; Qwen3 ASR requests word times. Timed segments are now parsed via a new Segment type in types.ts and cues.ts builds cues from words or segments.
  • Qwen3 ASR word responses without punctuation recover punctuation by matching words to response-text tokens with up to three tokens of lookahead.
  • Raises the gzip bundle budget from 106,970 to 107,281 bytes in size.mjs.
  • Behavioral Change: the no-speaker-labels warning in transcribe.ts now names the selected model instead of the provider choice label.

Macroscope summarized 92b5a19.


Note

Medium Risk
Changes OpenRouter request shapes, response parsing, and multi-part cue timing for production transcription paths; risk is mitigated by broad unit and E2E coverage but still touches external API behavior.

Overview
Adds Voxtral Mini Transcribe 3 and Qwen3 ASR 1.7B to OpenRouter’s transcription model list (five models total), with tighter per-part caps (12 MB / 900 s) and profiles that mark no speaker labels and no URL-based audio.

The OpenRouter integration now builds model-specific multipart requests (Voxtral: segment timing only, no word granularities; Qwen: word times with timestamp_granularities[]=word). Responses can carry timed segments as well as words: PartTranscript and cue assembly treat segments like timed cues across split parts. Qwen3 ASR restores punctuation on words from the response text when word tokens are bare.

When speaker labels are requested but the model cannot provide them, the save warning names the selected model (e.g. “GPT Transcribe”, “Voxtral Mini Transcribe 3”) instead of the provider. Answers with neither words nor segments fail as empty speech rather than saving untimed text.

Verification and CI: E2E journey tests and fixture stubs for OpenRouter/net.mjs cover the new models; the plugin gzip bundle budget rises to 107,281 bytes (+211 for the models, +100 for Qwen punctuation).

Reviewed by Cursor Bugbot for commit 92b5a19. Bugbot is set up for automated code reviews on this repo. Configure here.

A provider can offer models with and without speaker labels, so the notice
names the model, as the Speaker labels row already does.
… OpenRouter

Voxtral answers timed segments and no words, so a segments answer becomes
cues timed from the episode's start, as words do. Both models take parts of
12 MB and 900 s, because 20 MB took 33 to 35 s to answer.
…t and save timed notes

The OpenRouter stub answers them as they were measured: Voxtral with segments,
or a 400 when asked for word times, and Qwen3 ASR with words and no speakers.
… ASR measure

The branch this builds on leaves main.js at 106,873 gzip bytes, built with the
same vite command from an export of its head. The two OpenRouter models and
reading Voxtral's segment times add 211, which takes it to 107,084 and over
the 106,970 budget. The budget rises by those 211 bytes to 107,181, so the 97
bytes of headroom stay as they were.
…were measured

The fixture provider answers Voxtral with segments, or a 400 when asked for
word times, and Qwen3 ASR with words and no speakers. The transcripts recipe
covers both on a 70 min MP3.
Qwen3 ASR lists its words without punctuation and keeps it only in `text`,
so its notes ran on as one unpunctuated stream with paragraphs cut at fixed
lengths. Each word now takes its punctuated token from `text`.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: 92b5a19
Status: ✅  Deploy successful!
Preview URL: https://f4fd41f4.podnotes.pages.dev
Branch Preview URL: https://feat-transcription-openroute-x04d.podnotes.pages.dev

View logs

This branch has not been deployed

No deployments
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