Repository navigation
Conversation
… one The tests for a provider and a model a newer PodNotes stored used OpenRouter and Grok STT 2.0, which this version is about to run. They now use AssemblyAI and universal-3-6-pro.
OpenRouter is a credential row, one provider file and a registry line. Its key reference is the new root field openRouterSecretId, stored in SecretStorage as podnotes-openrouter-api-key, and a settings export carries the key as secrets.openRouter. It offers three models, each with the request measured to give word times and speakers: - Grok STT 2.0, recommended: base64 JSON with verbose_json and word and segment times, routed to xAI with provider.only, which takes diarize from provider.options.xai. A request routed elsewhere is answered with no speakers. - ElevenLabs Scribe v2: multipart with diarize beside the model, or JSON with input_audio.url for an episode a saved public feed lists. - MAI-Transcribe 2: multipart with diarize beside the model. With speaker labels off, no request carries diarize. Sounds Scribe v2 lists as audio_event items are left out. OpenRouter takes 25 MB a request and gives the model 60 s, so an upload goes in parts of at most 18 MB and 1,800 s. Parts of 24 MB were answered in 20 to 31 s. A 402 says the account is out of credit, a 401 that the key was rejected, and a 404 that the model is not offered. A 404 whose message names the account's data policy or zero data retention says the model is not offered under the account's OpenRouter privacy settings. Each ends the job at the first part, and none quotes OpenRouter.
…t each part A note saved with speaker labels asked for and none returned now says so: "Saved without speaker labels, because Grok STT 2.0 returned none." OpenRouter answers 200 with no speakers when a request reaches a route that ignores the speaker option, so this is checked for every provider. Labels from two parts already name different speakers. An episode sent in parts now also says that its labels restart in each part, so one person can have several labels, until labels are merged across parts.
…ll lists PodNotes reads OpenRouter's public catalog of transcription models, without the key, when the Model row draws and when an OpenRouter job starts. It keeps the read in memory for 24 hours. A read that fails is not kept, so the next one asks again, and the tested list is offered as it is. The catalog only takes models away. A model it does not list is not offered, and one whose expiration_date has come is skipped. A model with a later end date reads "Grok STT 2.0, ends <date>" in the Model row. A catalog that would leave no model is not followed. A chosen model the catalog no longer lists stays selected in the Model row as "<label>, no longer offered". A job runs the first offered model instead and says "OpenRouter no longer offers ElevenLabs Scribe v2, so PodNotes used Grok STT 2.0." The row redraws when a read lands while it is open. A provider gets its catalog through an optional catalog() beside failure(), so the registry owns the cache and the filter for any provider that has one.
A stub answers OpenRouter's transcriptions with two speakers, the second 12 s in, and its model catalog with the three models PodNotes offers. It records what each request named and a hash of the audio it carried, decoded from base64 for JSON. J-TR-09 plays a public episode on Scribe v2: one JSON request names the episode's address, and the episode's audio is never fetched. J-TR-10 plays it on the recommended Grok STT 2.0: one JSON request carries the audio as base64, the same bytes the fixture server holds. Both notes have timestamps and numbered speakers, and the catalog is read once, with no Authorization. A journey seeds any provider by its credential row, and a model with it.
…eads them The fixture provider answers openrouter.ai transcriptions with verbose_json words from two speakers, the second 12 s in, and the model catalog with the three models PodNotes offers. The log keeps openrouter.ai paths and reads the address a JSON request names in input_audio.url. The transcript guide adds the OpenRouter rows, its runs (Scribe v2 by address, Grok by base64, a long MP3 in parts) and the notices that now name OpenRouter.
…ures The branch this builds on leaves main.js at 104,706 gzip bytes. The OpenRouter provider, its model catalog filter and the speaker label warnings add 1,296, which takes it to 106,002 and over the 104,866 budget. The budget rises by those 1,296 bytes to 106,162, so the 160 bytes of headroom stay as they were.
A part's start is a sum of MP3 frame lengths, so a word's time in the episode carried float noise into its link, as in time=1400.1119999994578 for a word 23:20 into a 70 minute episode Grok got in three parts. Cue times are now rounded to the millisecond, as finely as providers time words.
…cannot fetch Scribe v2 and MAI-Transcribe 2 list words only when asked for speakers or word times, so a request with speaker labels off failed with "found no speech". They now always ask for word timestamps. OpenRouter answers an address its provider cannot download with a 400 "Provider could not download input_audio.url"; that now falls back to one upload instead of failing the job.
Deploying podnotes with
|
| Latest commit: |
23a4d86
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://c7b0f154.podnotes.pages.dev |
| Branch Preview URL: | https://feat-transcription-openroute.podnotes.pages.dev |
This was referenced Nov 14, 2026
chhoumann
marked this pull request as ready for review
November 14, 2026 14:49
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PodNotes is moving transcription to current models from several providers, so that a provider is a credential row, one provider file and a registry line, and no user runs a retired model. This PR is step 4 of that plan: OpenRouter. It stacks on #539.
Transcription provideroffersOpenRouterbetween Deepgram and OpenAI. Choosing it shows theOpenRouter API keyrow and aModelrow withRecommended (Grok STT 2.0),Grok STT 2.0,ElevenLabs Scribe v2andMAI-Transcribe 2.provider.only, which takesdiarizefromprovider.options.xai. A request routed elsewhere answers 200 with no speakers. Scribe v2 and MAI-Transcribe 2 get multipart withdiarizebeside the model. Scribe v2 gets the address of an episode a saved public feed lists, so nothing is downloaded. With speaker labels off, no request carriesdiarize.OpenRouter transcribed this episode in 3 parts, and speaker labels restart in each, so one person can have several labels.Merging labels across parts is the next PR.Saved without speaker labels, because Grok STT 2.0 returned none.This holds for every provider.expiration_datehas come is skipped, and a later end date readsGrok STT 2.0, ends <date>. A chosen model the catalog drops stays selected as<label>, no longer offered, and the job runs the recommended model with a notice. A failed read is not kept and leaves the tested list.time=1400.112, nottime=1400.1119999994578.For review:
catalog()besidefailure(). The registry keeps the reads in a Svelte store, so the Model row redraws when a read lands while it is open, and tests reset it through the same handle.privacy, which ends the job likemodel-unavailable, because the user's action differs.withdrawnModelsits besideunknownModelon the choice, because each says something different: this version has no such model, or the provider's catalog dropped it.Not measured with a real key, for the lead's real-key check:
/data policy|zero data retention|\bzdr\b/ionerror.message. No probe saved the real body. The pattern follows OpenRouter's published "No endpoints found matching your data policy (Zero data retention)".format: "mp3", as measured on an MP3 episode. An M4A episode by address is unmeasured.verbose_jsonwithoutdiarize. Whether they still returnwordsis unmeasured.OpenRouter could not transcribe this audio.with no upload, because OpenRouter's answer for it is unknown.Not in this PR: merged speaker labels across parts, the weekly model review, and the docs, which describe the released version until the release.
Verification
Run on Linux with Node 22.23.4, on the final tree
c46f215a:npm run lint,npm run format:check,npm run typecheck,npm run buildandnpm run docs:buildpass.npm run test: 124 files, 2,878 tests pass.npm run test:tooling: 8 files, 48 tests pass.npm run test:e2eon Obsidian 1.14.4: 33 files, 257 tests pass, J-TR-09 and J-TR-10 new.PODNOTES_OBSIDIAN_APP=/opt/Obsidian/obsidian npm run test:e2e -- settings-tab journey-resilienceon Obsidian 1.13.7: 2 files, 23 tests pass.npm run size: 106,031 gzip bytes, 131 under the new 106,162 budget. The base measures 104,706 in a separate build of1eef3e6f. The budget rises by the 1,296 bytes OpenRouter measured ata844ca70, and the millisecond fix after it adds 29.npm run plant: 27 planted defects, each caught by the unit test named below, and 2 more caught bynpm run test:e2e.Obsidian 1.14.4 on Linux under Xvfb, in this worktree's isolated vault, with the verify skill's fixture provider answering OpenRouter and its catalog. No real key was used. The fixture feed is
http://podnotes.test:24280/feed.xml. The local file isFixtures/Long Fixture.mp3, 4,200 s of sine at 8 kbps mono from ffmpeg, 4,200,452 bytes.data.jsonheldprovider: "openrouter"andmodel: "". The network log showed oneGET https://openrouter.ai/api/v1/modelswith no upload. The warning beside the key is Obsidian's ownSecrets are not encrypted, because Xvfb has no keyring.ElevenLabs Scribe v2chosen by keyboard,Transcribe current episode from audioonFixture Episode 1sent onePOST https://openrouter.ai/api/v1/audio/transcriptions,application/json, 160 bytes, naminghttp://podnotes.test:24280/episode-1.wav. The fixture server saw no request during the command. The note has**1:**at 0 s and**2:**at 12 s.Recommended (Grok STT 2.0), after a plugin reload, the command sent the catalogGET, then threePOSTs of 1,867,279, 1,867,027 and 1,866,931 bytes of JSON, one after the other. The note numbers six speakers attime=0,12,1400.112,1412.112,2800.224and2812.224, and the restart notice stayed until clicked. Before the millisecond fix the same run linkedtime=1400.1119999994578.dev:errorsreported no error. The instance and the fixture server are stopped. The lead verifies with a real key separately.Planted defects, each caught by the named test through
npm run plantprovider.onlyproviders/openrouter.tsproviders/openrouter.tsproviders/openrouter.tsproviders/openrouter.tsproviders/openrouter.tsdiarizedifferentlyproviders/openrouter.tsproviders/openrouter.tscues.tsproviders/openrouter.tscues.tscues.tstranscribe.tstranscribe.tshttp.tsproviders/openrouter.tstranscribe.tsregistry.tsTranscriptionService.tsregistry.tsregistry.tsregistry.tsregistry.tsexpiration_datenot readproviders/openrouter.tsPodNotesSettingsTab.tsPodNotesSettingsTab.tsPodNotesSettingsTab.tsproviders/openrouter.tsproviders/openrouter.tsnpm run test:e2e)providers/openrouter.tsnpm run test:e2e)Real OpenRouter key (lead, after review)
Live probes first, then Obsidian 1.14.4, Linux, build of
23a4d86b, palette command, a pass-through request log (no fixture):https://feeds.npr.org/500005/podcast.xml), labels on: one 520-byte POST naming the address, no download, saved in 5 s with timestamps and speakers. Labels off: one 505-byte POST; the note has timestamps and no speakers.dev:errors: none.Review fixes (lead)
verbose_jsonwith nowordswhen neitherdiarizenor word timestamps are asked for, so a labels-off job failed with "OpenRouter found no speech". Both now always send word timestamps (timestamp_granularities[]=word, or["word"]in JSON). Plant: drop it (caught bywith speaker labels off, elevenlabs/scribe-v2 is still asked for word times, which OpenRouter sends only when asked).unfetched, so the job uploads once. Plant: return no failure kind (caught byScribe v2 uploads the episode once when OpenRouter's provider cannot download its address)./data policy|zero data retention|\bzdr\b/i) matches the real ZDR body captured earlier: "0 endpoints out of 1 requested are available matching your guardrail restrictions and data policy ... ZDR violation (account settings)".Release impact
openRouterSecretId, empty by default. The key lives in SecretStorage aspodnotes-openrouter-api-key. A settings export carries it assecrets.openRouterwhen keys are included. The schema stays at version 2. feat(transcription): choose a provider and model, with ElevenLabs as the default #539 measured that 2.24.0 keepselevenLabsSecretId, an unknown root field to it. This field takes the same path, which I inferred and did not run.transcript.providercan now beopenrouter. A version without OpenRouter keeps the value and refuses to transcribe with a notice, as feat(transcription): choose a provider and model, with ElevenLabs as the default #539 does for any unknown provider.GETtoopenrouter.ai/api/v1/models?output_modalities=transcriptionwhen the Model row draws or a job starts. After a read that works it waits a day. After one that fails it asks again next time.Note
Add OpenRouter as a transcription provider
openRouterSecretIdinDEFAULT_SETTINGS,CREDENTIALSmapping) and settings UI support with catalog-driven model dropdown, expiration dates, and a "no longer offered" label in PodNotesSettingsTab.ts.privacyfailure kind with a user-facing message for OpenRouter 404 privacy errors, and millisecond rounding of transcript cue timestamps in cues.ts.Macroscope summarized 23a4d86.
Note
Medium Risk
Introduces a new paid external API path and credential storage alongside transcription request shaping and catalog caching; risk is moderated by extensive unit and e2e coverage.
Overview
Adds OpenRouter as a fourth transcription provider (between Deepgram and OpenAI), with a new
openRouterSecretIdcredential field wired through settings, persistence, and export/import like the existing provider keys.The provider supports Grok STT 2.0 (base64 JSON routed to xAI), ElevenLabs Scribe v2 (public episode URL when possible), and MAI-Transcribe 2, including chunked uploads (18 MB / 1,800 s), speaker-label handling, and user-facing error mapping (including a new
privacyfailure kind for data-policy 404s). A daily-cached public model catalog filters expired or withdrawn models; settings and jobs refresh it viarefreshModels, and the Model dropdown shows end dates and “no longer offered” for stored models that fell off the catalog.Cross-cutting tweaks include millisecond-rounded cue times for multi-part transcripts, extra warnings (withdrawn model fallback, missing speakers, speaker restart across parts), verify/e2e fixture stubs for OpenRouter, and a gzip bundle budget bump from 104,866 to 106,162 bytes.
Reviewed by Cursor Bugbot for commit 23a4d86. Bugbot is set up for automated code reviews on this repo. Configure here.