Repository navigation
Conversation
…isode Two parts that label Ann and Bob in the other order, read by a provider fixture that hears the voice each MP3 frame carries. Grok STT 2.0 should send one reel of 9 s clips between 1.5 s of silent frames, leave a voice under 2.6 s alone, and keep the parts' labels when the reel fails. Scribe v2, MAI-Transcribe 2, labels off and one part send no reel.
…an episode OpenRouter takes at most 18 MB and 1,800 s a request, so a long upload goes in parts, and each part numbers its speakers on its own. On the 54 min E231 in three parts, those labels agreed with a whole-file run 0.344 of the time. After the parts, Grok STT 2.0 now gets one more request: a reel with a clip of each part's every speaker label, from its longest turn of at least 2.6 s, 0.2 s in and at most 9 s, cut at MP3 frame starts and joined by 1.5 s of silent frames. Each speaker the reel's answer names is credited with the seconds it speaks over each clip, part labels whose clips go to one speaker become one, and a label with no clip stays its own. Measured through OpenRouter with Grok, this took E231 from 0.344 to 0.995 against a whole-file run, and NPR's Planet Money in 300 s parts from 0.367 to 0.949 against its human transcript, beside 0.942 for whole-file Grok. A model profile turns the reel on with mergeSpeakers. Scribe v2 stays off, because its reel heard the 10 voices of a Planet Money episode as 4, and so does MAI-Transcribe 2, which is not measured. Audio decoded to WAV parts sends no reel. A reel that fails keeps the parts' labels and the warning that they restart in each part, which the note no longer gets otherwise.
…measures The branch this builds on leaves main.js at 106,065 gzip bytes, built with the same vite command from an export of its head. Cutting the reel, merging labels from its answer and the mergeSpeakers profile add 808, which takes it to 106,873 and over the 106,162 budget. The budget rises by those 808 bytes to 106,970, so the 97 bytes of headroom stay as they were.
… a model would The fixture provider answered every OpenRouter request with the same two speakers, so a reel's answer could not say which part labels are one voice. It now decodes Grok's MP3 uploads with Chromium's decoder and hears a tone under 500 Hz as one speaker and a higher tone as another, numbered in the order they sound in each request. The log records each upload's decoded seconds and the speakers heard, and audio the decoder refuses is answered with a 400. Multipart and WAV uploads keep the fixed answer. The transcripts recipe writes a 70 min MP3 of two tones that take turns every 45 s, whose second part starts with the high tone, and checks three part requests, one 61.56 s reel and one number per voice in the note.
Deploying podnotes with
|
| Latest commit: |
0531b3c
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://62ff5d5f.podnotes.pages.dev |
| Branch Preview URL: | https://feat-transcription-speaker-m.podnotes.pages.dev |
This was referenced Nov 14, 2026
chhoumann
marked this pull request as ready for review
November 14, 2026 14:49
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5eebf37. Configure here.
The reel's answer was folded into turns before each clip was credited, and a turn runs from a speaker's first word to their last. When the model gave a clip no words, such as a clip it could not make out, the words of one speaker in the clips on both sides of it made a single turn over it. That speaker was then credited with the whole clip, and its part label was merged into theirs, so two people shared one number in the note. Each clip is now credited from the answer's words, so seconds no word spans go to no one, and a clip with no words keeps its own label.
The warning that speaker labels restart in each part was left out whenever the reel matched any label. A label whose voice never spoke 2.6 s at once gets no clip, and a clip the reel's answer gives no words gets no match, so both keep a number of their own that can belong to a voice another part labels too, and the note said nothing. The warning now stays unless every label the parts returned was heard in the reel. A label heard as a voice no earlier clip had stands for itself, and counts as matched. The wording is unchanged. main.js grows by 29 gzip bytes, inside the budget.
| const turns: Turn[] = []; | ||
| for (const { speaker, start, end } of words) { | ||
| const last = turns[turns.length - 1]; | ||
| if (last && last.speaker === speaker) turns[turns.length - 1] = { ...last, end }; |
There was a problem hiding this comment.
🟡 Medium transcription/speakers.ts:18
turnsOf merges same-speaker words across long silences, so longest can rank elapsed silence as speech and select a silent clip instead of a genuine shorter sample, preventing cross-part matching. Split turns at substantial timestamp gaps or choose a speech-containing window before selecting longest.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/services/transcription/speakers.ts around line 18:
`turnsOf` merges same-speaker words across long silences, so `longest` can rank elapsed silence as speech and select a silent clip instead of a genuine shorter sample, preventing cross-part matching. Split turns at substantial timestamp gaps or choose a speech-containing window before selecting `longest`.
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
OpenRouter takes at most 18 MB and 1,800 s a request, so PodNotes uploads a long episode in parts, and each part numbers its speakers on its own. On the 54 min E231 in 3 parts, those labels agreed with a whole-file run 0.344 of the time. With this change, Grok STT 2.0 gives each voice one number across all parts of an episode. Builds on #540.
mergeSpeakerson the model profile turns the reel on. It is on forx-ai/grok-stt-2.0and off forelevenlabs/scribe-v2andmicrosoft/mai-transcribe-2.Speaker accuracy of the lab implementation (
diarize-lab/scripts/step0.mjs) through OpenRouter:Scribe v2's reel heard Planet Money's 10 voices as 4 (0.378 to 0.414), so it stays off. MAI-Transcribe 2 is not measured. This PR's code has not run against the live API, because this session has no key.
Verification
npm run testpasses 2,888 tests. The new tests send MP3 fixtures whose frames carry a voice and a frame index through therequestUrlseam. The mock provider hears each frame's voice and numbers voices in the order they speak in each request, as a diarizing model does. On the base, the 4 tests for new behavior fail, for example with1 Ann, 2 Bob, 3 Bob, 4 Annwhere1 Ann, 2 Bob, 2 Bob, 1 Annis expected.1 failed) and passed once restored (1 passed), throughnpm run plant:3 Bobfor2 Bob)2 Cyfor3 Cy)lint,format:check,typecheck,build,test:tooling(48 passed) anddocs:buildpass.npm run test:e2e -- tests/e2e/journey-resilience.test.ts tests/e2e/journey-transcript.test.tspasses 26 tests.npm run sizereports 106,873 gzip bytes. The budget rises by the measured 808 bytes, against 106,065 for the base built the same way, and keeps the base's 97 bytes of headroom.Fixtures/Two Voices.mp3is 70 min of 8 kbps, 8 kHz mono, with the two tones taking turns every 45 s. With Grok and labels on,Transcribe current episode from audiosent three part POSTs of about 1.87 MB each, then one reel POST of 82,291 bytes that Chromium decoded to 61.56 s, which is six 9 s clips and five gaps of 21 silent frames. The second part starts with the high tone and numbers the voices the other way. The note still numbers all 47 low-voice paragraphs1and all 47 high-voice paragraphs2, and the paragraph at each part boundary runs on across it. No notice stayed. On Scribe v2 the same file went as three multipart POSTs with no reel, the note numbered six speakers, and the restart warning stayed.dev:errorsreported none.Real OpenRouter key (lead, after review)
Obsidian 1.14.4, Linux, build of
5eebf37b, Grok STT 2.0 (recommended), speaker labels on, the 54 min, 39 MB local E231 interview (two voices), palette command, a pass-through request log:Transcription completed and saved., no restart warning,dev:errorsnone.Release impact
None for settings, storage, the API or URIs. A Grok transcript of an episode over 18 MB or 1,800 s costs one more OpenRouter request of about a minute of audio. OpenRouter is unreleased, so no released behavior changes.
Note
Merge speaker labels across episode parts via a speaker reel for Grok STT
mergeSpeakers-enabled model (Grok STT 2.0) now get an extra provider request with an audio reel of each speaker's longest turn, and the reel response is used to map labels across parts into one speaker per voice.SameSpeakermapping when numbering speakers in timed transcripts; without a mapping, labels stay distinct per part.cutReel, which assembles clips from the MP3 at frame boundaries with at least 1.5 seconds of generated silence between them.TranscriptionError, the original transcripts are kept with unmatched labels and the multi-part restart warning is shown. Models marked withoutmergeSpeakers(ElevenLabs Scribe v2, MAI-Transcribe 2) keep per-part numbering.Macroscope summarized 0531b3c.
Note
Medium Risk
Adds an extra API call and ~808 bytes of production bundle for Grok multi-part jobs; reel failure paths must preserve transcripts without breaking numbering—covered by tests but behavior is new.
Overview
Multi-part OpenRouter transcriptions with speaker labels on can now merge the same voice into one number across parts. For models that opt in (Grok STT 2.0 via
mergeSpeakers), PodNotes sends an extra speaker reel after the part uploads: MP3 clips (longest turn per part-local label, frame-aligned, separated by silent frames) are transcribed once more; overlapping word timing maps part labels to a shared identity used when building cues intranscriptFromParts.ElevenLabs Scribe v2 and MAI-Transcribe 2 keep
mergeSpeakersoff; they still get the “labels restart in each part” warning. Reel failures do not abort the job—transcript saves with per-part labels and the restart warning. Single-part jobs, labels off, or non-MP3 paths skip the reel.E2E
net.mjsnow decodes Grok JSON MP3 uploads (tone-based fake diarization) for verify runs; gzip bundle budget rises by 808 bytes to 106,970.Reviewed by Cursor Bugbot for commit 0531b3c. Bugbot is set up for automated code reviews on this repo. Configure here.