Skip to content

feat(transcription): move transcription onto one engine and current models - #538

Open
chhoumann wants to merge 27 commits into
masterfrom
feat/transcription-engine
Open

chhoumann wants to merge 27 commits into
masterfrom
feat/transcription-engine

Conversation

@chhoumann

@chhoumann chhoumann commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

PodNotes transcribed audio with two models that OpenAI shuts down on 2027-02-26, through code in which each provider was wired by hand. This PR moves audio transcription onto one engine in src/services/transcription/, where a provider is one file, and onto current models. It is step 2 of the provider plan and stacks on #536.

  • OpenAI transcribes with gpt-transcribe. whisper-1 and gpt-4o-transcribe-diarize are gone from the bundle.
  • Speaker labels with OpenAI. gpt-transcribe returns text only. With speaker labels on and OpenAI selected, the transcript is saved without labels, and a notice that stays until clicked names the providers that label speakers.
  • Deepgram transcripts get a timestamp on each paragraph, from the renderer that published transcripts use. Speakers are numbered in the order they first speak.
  • Address or upload. Deepgram gets the address of an episode that a saved public feed lists, so PodNotes downloads and uploads nothing. A private feed's episode, a local file, an address that carries a credential, and an address on a private network are uploaded, and their address is in no request. If Deepgram cannot fetch an address, PodNotes uploads the audio once.
  • Failures. A failure notice stays until clicked and reads as PodNotes' own sentence. A provider's response text is never shown or logged, because it can quote a private address. Parts go out one at a time. A 401, 402, 404, or 400 stops the job before the next part. A 429, a 5xx, or a lost connection is sent up to three times and honors Retry-After. A timeout, and an answer that asks to wait over a minute, are sent once.

Not in this PR: new settings, a provider or model picker, ElevenLabs, OpenRouter, and the docs, which describe the released version until this ships.

Verification

Node 22.23.2, Linux. All pass: npm run lint, npm run format:check, npm run typecheck, npm run build, npm run size, npm run test (2,796 unit tests), npm run test:tooling (48 tests), and npm run docs:build.

Lint rejects an import of the folder's files from outside it. A probe file that imported ./transcription/http, src/services/transcription/split, and two files under providers/ got four no-restricted-imports errors, and none for the folder's index.

E2E. The whole suite passed 253 of 254 on 989185fc. On the head, which adds one small fix commit, npm run test:e2e -- -t "transcript|normalizes raw async rejection" passed its 20 tests. The one failure is in quickadd-capture.test.ts ("Import settings brings back the choice that Export settings wrote, and captures run it"), and the base commit a9a3b07d fails it the same way in the whole suite: the test reads data.json while PodNotes writes it and gets Unexpected end of JSON input. The test passes when its file runs alone. An earlier run of the suite also failed a marketplace review test that called TranscriptionService.waitForRetry, which the engine replaced. That test now aborts a wait in waitForLifecycle instead (plant 33).

Bundle. main.js is 104,290 gzip bytes. The engine alone measured 104,060, under master's 104,145. On top of it, 170 bytes check the length of audio in other formats against each model's cap, for at most 5 s and only while the job runs, and 60 keep a signed enclosure URL from reaching a provider. The budget is raised to exactly 104,290.

Since review. These fixes came from the independent verifiers, Bugbot and Macroscope, and each has tests and caught plants:

  • The MP3 splitter fixes from fix(transcription): cap OpenAI diarization chunks at 1400 s #536, ported to split.ts.
  • A long .m4a or .ogg under the size cap is split under the length cap instead of sent whole.
  • An enclosure signed by AWS, Google Cloud, Azure or Akamai (hdnts/hdnea) is uploaded, so its address reaches no provider.
  • The metadata probe gives up after 5 s or on unload.

Runtime. Obsidian 1.14.4 on Linux (headless), in this worktree's isolated vault, through the palette command Transcribe current episode from audio. The verify skill's fixture provider (net.mjs watch) answered both providers, so no real key was used. These runs are from 989185fc. The commit after it changes only what a job does after an abort and after a Retry-After of over a minute.

Case What PodNotes sent The note
OpenAI, a 70 min local MP3 (4.2 MB at 8 kbps) Three POSTs of 1,400,700, 1,400,512, and 1,400,440 bytes, one after the other Paragraphs without timestamps, the fixture text three times
Deepgram, Fixture Episode 1 of a saved public feed One POST, application/json, 50 bytes. No request reached the feed server Timestamps, speakers 1 and 2
Deepgram, an episode of a private feed at priv3.supercast.com GET /p/episode-1.wav, then one POST, audio/wav, 960,044 bytes Timestamps, speakers 1 and 2
Deepgram, a local WAV One POST, audio/wav, 960,044 bytes, and no GET Timestamps, speakers 1 and 2
OpenAI with speaker labels on, an episode of the public feed GET /episode-2.wav, then one POST of 960,438 bytes No labels. The notice was still up 60 s later and went away on a click
Deepgram, public episode by address OpenAI with speaker labels on, 60 s after the save
A Deepgram transcript note with linked timestamps and speakers 1 and 2 The notice that the transcript has no speaker labels, still on screen

More screenshots: the OpenAI note from three parts, the Deepgram note for the private feed's episode, and the provider option in Settings.

Not verified. No request reached a real provider. Two answers are read from descriptions, not from a live response: Deepgram's REMOTE_CONTENT_ERROR for an address it cannot fetch, and OpenAI's insufficient_quota for a used-up quota. If either differs, a fetch failure shows as "could not transcribe this audio" with no upload, and a used-up quota shows as a rate limit.

Planted defects (36, each caught by the test named, with npm run plant)
# Planted defect Caught by
1 Private feed: checks the saved address's shape, not that the feed is public TranscriptionService.test.ts: a private feed's episode is uploaded, and no request holds its address or its feed's
2 Credential-bearing address: no credential check TranscriptionService.test.ts: an episode whose address carries a credential is uploaded, and no request holds the address
3 Non-public target: no public host check TranscriptionService.test.ts: an episode on a private network is uploaded, and no request holds its address
4 No positive match of the enclosure to the saved feed TranscriptionService.test.ts: an episode its saved feed does not list is uploaded, and no request holds its address
5 Local file: the episode's address goes to any model that takes one TranscriptionService.test.ts: a local file is uploaded, and no request holds its path
6 Always load: the audio is downloaded before the request by address TranscriptionService.test.ts: a public episode goes by its address, and its audio is never loaded
7 Fall back to an upload on any failure TranscriptionService.test.ts: a 401 for a request by address is not followed by an upload
8 No fallback: Deepgram's fetch failure reads as a rejection TranscriptionService.test.ts: an address Deepgram could not fetch is followed by one upload
9 Drop start: cues carry no word time TranscriptionService.test.ts: has a timestamp on each paragraph and numbers the speakers in the order they first speak
10 Drop speaker: Deepgram words carry no speaker TranscriptionService.test.ts: has a timestamp on each paragraph and numbers the speakers in the order they first speak
11 Released numbering: Deepgram's speaker number plus one TranscriptionService.test.ts: has a timestamp on each paragraph and numbers the speakers in the order they first speak
12 Drop the offset: word times stay relative to their part cues.test.ts: times each word from the start of the episode, and never shares a speaker between parts
13 Speaker labels shared between parts cues.test.ts: times each word from the start of the episode, and never shares a speaker between parts
14 MP3 parts carry no start time split.test.ts: under the size cap is cut into equal parts no longer than maxSeconds, each starting where the last ended
15 WAV parts carry no start time split.test.ts: a .ogg file without MP3 frames is decoded into WAV parts under the cap, each with its start time
16 Retired model in the request (unit) TranscriptionService.test.ts: sends OpenAI the model, the episode file and the JSON format
17 Retired model in the bundle (tooling) transcriptionModels.test.ts: main.js names gpt-transcribe and no model OpenAI shuts down on 2027-02-26
18 No response_format in the OpenAI request TranscriptionService.test.ts: sends OpenAI the model, the episode file and the JSON format
19 No length cap on OpenAI parts TranscriptionService.test.ts: goes to OpenAI in parts no longer than 2,070 s, one at a time, and reads in order
20 Every part sent at once TranscriptionService.test.ts: goes to OpenAI in parts no longer than 2,070 s, one at a time, and reads in order
21 Drop the OpenAI speaker-label notice TranscriptionService.test.ts: with speaker labels on is saved without them, and the notice names the providers that label speakers and stays
22 A warning hides after 5 s, like a clean save TranscriptionService.test.ts: with speaker labels on is saved without them, and the notice names the providers that label speakers and stays
23 A failure notice hides after 5 s, as released TranscriptionService.test.ts: stays until clicked after a failure
24 A 401 on one part does not stop the job TranscriptionService.test.ts: stops at a 401 on the first part and sends no second part
25 A note of only a failed part's marker is saved TranscriptionService.test.ts: writes no note when one part fails and the other holds no speech
26 Every failure is sent three times TranscriptionService.test.ts: 401 saves no note after 1 of 3 sends
27 A timeout is sent again, as released TranscriptionService.test.ts: timeout saves no note after 1 of 3 sends
28 A used-up OpenAI quota reads as a rate limit TranscriptionService.test.ts: 429 for a used-up quota saves no note after 1 of 3 sends
29 Nothing is sent again TranscriptionService.test.ts: 503 is sent again after the wait and the note is saved
30 Retry-After is ignored TranscriptionService.test.ts: 429 with Retry-After is sent again after the wait and the note is saved
31 Append the provider's own words to the notice TranscriptionService.test.ts: shows and logs none of the provider's own words, which can quote a private address
32 No abort handling in the engine: a request goes out after unload TranscriptionService.test.ts: sends nothing for audio that arrives after unload
33 An aborted wait rejects with the raw abort reason marketplace-review-runtime.test.ts (e2e): normalizes raw async rejection reasons to Error objects in the live bundle
34 A wait of over a minute is ignored and the request is sent again at once, as released TranscriptionService.test.ts: 429 that asks to wait over a minute saves no note after 1 of 3 sends
35 Audio that arrives after unload is still split TranscriptionService.test.ts: decodes no audio that arrives after unload
36 A job unloaded during the feed lookup still downloads the audio TranscriptionService.test.ts: downloads no audio when unloaded while it looks the episode up in its feed

Release impact

  • OpenAI users move to gpt-transcribe. Same key. Sentences are grouped into paragraphs of about 500 characters, where each sentence had its own paragraph. An upload holds at most 24 MB and 2,070 s.
  • OpenAI speaker labels are retired. transcript.diarization.provider: "openai" stays valid, and the run says in a lasting notice that the transcript has no labels. The provider option in Settings reads OpenAI (no speaker labels).
  • Deepgram transcripts gain timestamps, as links or plain times by the Timestamp template. Speaker numbers follow the order of first speech, where they were Deepgram's number plus one.
  • Deepgram fetches a public episode itself. The podcast's host then sees a download from Deepgram, not from the user's device.
  • Notices. A failed transcription stays on screen until clicked, and its text is PodNotes' own.
  • A failed part leaves [Error transcribing chunk N], now counted from 1 in every transcript.
  • Stored data. No settings field or stored shape changes, and there is no migration.
  • API. api.transcript and getTranscript() return the note's text, so Deepgram transcripts read through them now carry timestamps.
  • Bundle budget. Unchanged.

Note

High Risk
Large rewrite of paid transcription paths with external API calls, audio URL vs upload decisions, and credential/private-feed handling; mistakes could leak signed URLs or bill users incorrectly despite extensive tests.

Overview
Replaces the hand-wired Whisper/diarization stack with a unified engine under src/services/transcription/. TranscriptionService now delegates audio jobs to transcribe(), which picks OpenAI vs Deepgram via transcriptionChoice(), sends HTTP through a shared retry layer, and merges multi-part results into the same published-transcript renderer.

OpenAI moves to gpt-transcribe (upload-only, no timestamps or speaker labels); long audio is split under 24 MB / 2,070 s, sent one part at a time. With diarization enabled but OpenAI selected, saves still complete but show a lasting notice that labels require Deepgram.

Deepgram uses nova-3: public episodes listed on a saved feed go out as a JSON URL request; private feeds, signed URLs, local files, and unlisted episodes upload bytes with no credential-bearing URLs in the payload. Fetch failures on URL mode trigger a single upload fallback.

The old diarization/ and openaiTranscription modules are removed; getEpisodeAudioBuffer no longer returns basename. Tooling adds a private verify fixture, richer net.mjs mocks, import boundaries for the transcription folder, and a higher gzip bundle budget (104,290).

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

Note

Move transcription onto a single provider-engine architecture with gpt-transcribe and Deepgram Nova-3

  • Replaces the separate OpenAI and Deepgram transcription paths in TranscriptionService with a unified provider layer in src/services/transcription/. A registry selects the provider via transcriptionChoice: OpenAI gpt-transcribe by default, Deepgram Nova-3 when diarization is enabled and selected
  • The new transcribe.ts workflow sends a public episode URL when the provider supports it, otherwise fetches, splits, and uploads audio parts sequentially. It can return partial transcripts with warnings for recoverable part failures, and stops early on auth, credits, model, and rejection errors
  • http.ts adds typed failures with user-facing messages, timeout handling, and retries for rate-limit, server, and network errors
  • Removes the old diarization module and its tests; OpenAI no longer supports speaker labels, and a persistent notice tells users which providers do
  • Risk: retired OpenAI Whisper models are gone — a production-bundle test in transcriptionModels.test.ts fails if they reappear; getEpisodeAudioBuffer no longer returns a basename field

Macroscope summarized 3641fb8.

gpt-4o-transcribe-diarize rejects requests with more than 1400 s of audio,
but MP3 chunks were split by size alone. Any MP3 under about 120 kbps put
more than 1400 s in a 20 MB chunk, so every chunk failed.

For diarization, split MP3 at frame boundaries into the fewest equal parts
that stay under both 20 MB and 1400 s. Whisper keeps its size-only chunks.

Fixes #532
A chunk that OpenAI rejected with a status such as 400 or 401 was sent
three times, although each send fails the same way. A long diarized
episode went up three times before the error showed.

Retry only the statuses openai-node retries: 408, 409, 429 and 5xx, plus
network errors.

Refs #532
gpt-4o-transcribe-diarize takes up to 0.58 s per second of audio, measured
over 81 requests, so the 1,072 s parts a 54 min episode got would take about
10 min each, at the request timeout. Parts are now at most 600 s, which the
model answers in under 6 min.

Refs #532
The published transcript renderer is the reader of the format that stays
when the diarization folder goes. The default template now lives only in
DEFAULT_SETTINGS, so src/constants.ts imports no service code.
src/services/transcription/ turns an episode into a transcript with any
provider. A provider is one file: its models, its request, and how to read
its answers. OpenAI (gpt-transcribe) and Deepgram (nova-3) are the first two.

The engine owns what providers share:

- audioSource.ts decides whether a provider may be given the episode's
  address. Only an address that a saved public feed lists, that carries no
  credential, and that names a public host passes. All other audio is
  uploaded.
- split.ts is audioChunker.ts with the caps as arguments and a start time
  on each part.
- http.ts sends a request at most three times, and only after a 429, a 5xx,
  or a lost connection. Its failure sentences never include the provider's
  own text, which can quote a private address.
- cues.ts turns words or text into the shapes of a published transcript.

Nothing calls the engine yet. Lint rejects an import of the folder's files
from outside it, except its index.
TranscriptionService keeps its queue, the published-transcript path, the
progress notice, and note saving. It calls the engine for audio.

- OpenAI transcribes with gpt-transcribe. whisper-1 and
  gpt-4o-transcribe-diarize, which OpenAI shuts down on 2027-02-26, are
  gone. A tooling test searches the built main.js for them.
- With speaker labels on and OpenAI selected, the transcript is saved
  without labels. A notice that stays until clicked names the providers
  that label speakers.
- Deepgram transcripts go through renderTranscriptBody, so each paragraph
  has a timestamp. Speakers are numbered in the order they first speak.
- Deepgram gets the address of a public episode and downloads it itself.
- A failure notice stays until clicked. Parts go out one at a time, and a
  401, 402, 404, or 400 stops the job before the next part.

Deletes src/services/diarization/, openaiTranscription.ts, the Whisper
worker pool, and createBinaryChunkFiles. getEpisodeAudioBuffer no longer
returns a basename, because uploads are named for their format only.
net.mjs answers OpenAI with { text } and Deepgram with the words of two
speakers, and logs the content type of each request. fixtures.mjs serves a
feed at priv3.supercast.com, an address that PodNotes keeps private. The
transcripts recipe covers OpenAI in parts, and Deepgram by address and by
upload.
The marketplace review test called TranscriptionService.waitForRetry, which
the engine replaced. An aborted run now rejects through waitForLifecycle, so
the test aborts a pending wait there and expects the same Error.
Comment thread src/services/transcription/http.ts Resolved
Comment thread src/services/transcription/split.ts Resolved
An aborted job no longer downloads the episode or decodes it into parts.
Until now only its requests were refused.

A 429 or a 5xx that asks to wait over a minute is sent once. A sooner send
would only upload the audio to be refused again.
@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3641fb8
Status: ✅  Deploy successful!
Preview URL: https://c7568cfa.podnotes.pages.dev
Branch Preview URL: https://feat-transcription-engine.podnotes.pages.dev

View logs

…unted last frame

Parts were counted from rounded seconds, so an episode a frame over a
whole number of capped parts got equal parts one frame too long, and the
time cap then split off a final part of a single frame. Parts are now
counted in frames.

A last frame followed by metadata, such as an ID3v1 tag, failed the
next-frame check and was not counted, so the last part could run one
frame over maxSeconds. A frame that continues the frames before it now
counts.
Comment thread src/services/transcription/split.ts Resolved
…ame parts

Summing frame durations drifts past the cap: 25,000 frames of 24 ms sum to
600.0000000000218 s, so the per-frame seconds check cut each part a frame
early and a 49,999-frame episode went as 24,999 + 24,999 + 1 frames. Parts
are already counted in frames, which keeps them under maxSeconds, so the
check goes.
The planner fixed a frame count per part and checked the size cap
separately. When a leading tag or a variable bitrate put an equal split over
the cap, the size check cut a part short and the next kept the old frame
count, leaving a one-frame part that cannot be decoded: 43,690 frames of a
320 kbps MP3 with a 512-byte tag went as 21,844 + 21,845 + 1. The planner
now takes the fewest equal splits that fit both caps, adding a part when an
equal split does not fit.
Comment thread src/services/transcription/split.ts Resolved
Equal splits put the odd frame first, so a 512-byte tag and 43,689 frames of
a 320 kbps MP3 put the first of two parts 192 bytes over 20 MiB, and the
planner sent three where two fit. Each cut now takes an equal share of the
frames still to place and steps back while the part is over the size cap,
so 21,844 + 21,845 goes in two.
Cuts only moved back from an even split, so where the bitrate rose a part
was added although moving the cut forward fit: 21,847 low-bitrate frames
then 21,845 at 320 kbps and an ID3v1 tag went as three parts, not two. The
planner now finds the fewest parts by filling each as far as it fits, then
places each cut as close to an equal share as the parts before and after it
allow. A property test checks the count against a brute-force minimum on 300
random variable-bitrate streams.
Frame-like bytes inside a leading tag set the first frame's duration, and
counting every part in that frame's length sent a 1,728 s episode of 72 ms
frames up in one request. Parts now fit by the summed duration of their own
frames, and each part's start is the sum of the frames before it.
Audio frames inside a tag's declared size were counted and could be cut
through, sending an undecodable first part, and frame-like bytes in a
trailing tag added a part.
Base automatically changed from fix-issue-532 to master November 14, 2026 18:46
…engine

# Conflicts:
#	src/services/TranscriptionService.test.ts
#	src/services/TranscriptionService.ts
#	src/services/audioChunker.test.ts
#	src/services/audioChunker.ts
#	src/services/diarization/openaiProvider.ts
#	src/services/diarization/types.ts
#	src/services/openaiTranscription.test.ts
#	src/services/openaiTranscription.ts
#	tests/mocks/openaiFailures.ts
@chhoumann

Copy link
Copy Markdown
Owner Author

Retargeted to master after #536 merged (3987258), and merged master in (7370a71).

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/services/transcription/split.ts Resolved
@chhoumann

Copy link
Copy Markdown
Owner Author

FAIL
Verified head 7370a71 by an independent verifier (GPT 6 Astra).

  • Compared master 39872583 and head in isolated Obsidian 1.14.4 on Linux through Play with PodNotes and the palette's Transcribe current episode from audio. Read the diff, comments/review threads and fix(transcription): cap OpenAI diarization chunks at 1400 s #536's predecessor verdicts. Used fake keys and fixture providers only.
  • All requested head checks pass under fnm exec --using 22.23.2: npm run lint, npm run format:check, npm run typecheck, npm run build, npm run size, npm run test (2,815 tests), and npm run test:tooling (48 tests). Bundle is 318,744 raw / 104,060 gzip bytes, leaving 85 gzip bytes.
  • The five requested flows work. Head sends the 70-minute MP3 as three decodable gpt-transcribe uploads, sends the saved public episode by URL, and uploads private-feed/local WAV audio without its address. Deepgram notes gain timestamps and first-appearance speaker numbering. The OpenAI no-label warning remains after 60 seconds and disappears on click.
  • Exercised all 50 predecessor MP3 fixtures through the exact head splitter. Its 74 parts preserve source bytes, minimum count, both caps and correct starts. ffmpeg decodes every part except the already-known malformed truncated-tag fixture. Independent dynamic-programming oracles pass another 21,906 feasible generated streams, including mixed rates and stacked/footer tags.
  • Live failure probes confirm stop-on-400/401/402/404, three sends per part for 429/503/lost connection, Retry-After waits, sanitized notices/logs and sequential successful parts. Over-minute waits and timeouts send each part once; the timeout probe accelerates only the network timer. Deepgram REMOTE_CONTENT_ERROR falls back to one upload.
  • P2 privacy regression in audioSource.ts:39. The reused credential detector misses AWS presigned parameters such as X-Amz-Credential and X-Amz-Signature. A saved public feed's enclosure carrying these is classified as public and its complete signed URL is disclosed to Deepgram. Treat signed media parameters as credentials before allowing URL mode.
  • Reproduced through the palette with http://podnotes.test:38857/signed.xml, whose WAV enclosure has X-Amz-Credential=REVIEW-CREDENTIAL&X-Amz-Signature=REVIEW-SECRET-SIGNATURE. Head sends a 177-byte JSON body containing both markers and no audio GET. Master sends a GET then 960,044 WAV bytes with neither marker. Repeated after restoring the pinned head with the same result. The markers are fake; no request reached a real provider.
  • Evidence and rerunnable drivers are in /home/christian/.t3/worktrees/PodNotes/ac87c26f-8062f98f5c161069/.obsidian-e2e-artifacts/review538/, starting with verification.md, restored-head-captured.jsonl, base-signed-captured.jsonl and the matching logs/notes. dev:errors reports no captured runtime errors. Restored and rebuilt the pinned head, changed no tracked files, and stopped the fixture server and this worktree's instance; final dry run reports pids: none.

…h cap

A file in a format other than MP3 under the size cap went whole whatever its
length, so a long .m4a passed a model's maxSeconds. Its length now comes from
its metadata through a media element, without decoding it, and audio over the
cap is decoded into WAV parts that fit both caps. Audio of an unknown length
still goes whole.
A public feed's enclosure with AWS or Google Cloud signing parameters passed the
feed credential check, so Deepgram got the whole signed address. A query
parameter whose name holds signature, credential or token, or Akamai's hdnts,
now uploads the audio instead.
@chhoumann

Copy link
Copy Markdown
Owner Author

Fixed in 7b64684. publicAudioUrl now also refuses an enclosure whose query has a parameter name containing signature, credential or token, or Akamai's hdnts. That covers AWS X-Amz-* and Google Cloud X-Goog-* presigned URLs. CloudFront Signature and Azure sig were already caught. Any refused address is uploaded, as master does.

  • New tests: an episode whose address is a {AWS, Google Cloud, CloudFront, Azure} signed URL is uploaded, and no request holds the address. AWS and Google Cloud were red on 7370a71. A guard test, a public episode whose address has tracking parameters goes by its address, uses Simplecast's aid/feed, Megaphone's updated and utm_source.
  • Plants caught: the check removed (AWS), an AWS-only pattern (Google Cloud), and a pattern that matches everything (the tracking guard).
  • Your signed-fixtures.mjs and signed.sh, rerun in my instance (Obsidian 1.14.4) at the fix: GET /episode-1.wav, then one Deepgram POST of audio/wav, 960,044 bytes, the same as master. The note has timestamps and speakers 1 and 2. No runtime errors.
  • Also in this push, for Bugbot's thread: long non-MP3 audio under the size cap is split under each model's length cap (cc142ba).
  • Checks: typecheck, lint, format:check, build, test (2,823). The gzip budget entry this PR adds now records 104,235: 118 bytes for the length check and 57 for this one.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 37fdc08. Configure here.

Comment thread src/services/transcription/split.ts Resolved
…nload

A WebView may load no metadata for media it does not play, so the length probe
could wait forever, and an unload could not stop it before a decode. The probe
now reads the length as unknown after 5 s or when the job is aborted, and an
aborted job decodes nothing.
@chhoumann

Copy link
Copy Markdown
Owner Author

PASS+NOTES
Verified head 3641fb8 by an independent verifier (GPT 6 Astra).

  • Compared master 39872583 and head in isolated Obsidian 1.14.4 on Linux through Play with PodNotes and the palette's Transcribe current episode from audio. Reviewed the diff, comments, resolved review threads and fix(transcription): cap OpenAI diarization chunks at 1400 s #536. Used fake keys and the fixture provider only; inspected request bodies, saved notes, notices and screenshots.
  • All requested head checks pass through fnm exec --using 22.23.2: npm run lint, npm run format:check, npm run typecheck, npm run build, npm run size, npm run test (2,828 tests), and npm run test:tooling (48 tests). Bundle is 319,405 raw / 104,290 gzip bytes, leaving zero gzip headroom.
  • Head sends the 70-minute MP3 as three decodable gpt-transcribe uploads. Each 2,400-second local .m4a and .ogg becomes nine WAV uploads within both caps; 60-second versions go whole without decoding. Master sends these files whole. Notes contain the expected responses. The OpenAI no-label notice remains after 60 seconds and disappears on click.
  • Deepgram receives public and tracking-only enclosure URLs without an audio download. Private-feed episodes and local WAVs upload bytes without their addresses. AWS, Google Cloud, Azure SAS, Akamai hdnts/hdnea, and a custom access-token query all upload 960,044 WAV bytes, matching master; no credential marker reaches a provider body. A simulated REMOTE_CONTENT_ERROR falls back to one upload. Head adds timestamps and renumbers fixture speakers 7 and 2 as 1 and 2; master shows 8 and 3.
  • Re-ran all 50 predecessor MP3 fixtures through the exact head splitter. The 74 parts preserve source bytes, minimum count, both caps and start times. ffmpeg decodes every part except the previously documented malformed truncated-tag fixture. Independent exhaustive/integer-duration oracles pass another 21,906 feasible generated cases.
  • Live failure probes confirm stop-on-400/401/402/404, at most three sends per part for 429/503/connection loss, Retry-After waits, sequential successful parts, sticky failure notices and no provider-text sentinel in notices or logs. Over-minute waits and simulated timeouts send each part once. A stalled metadata probe falls back after five seconds; unloading during measurement performs no decode, request or note write.
  • Inherited limitations, not new blockers: neither revision offers Play with PodNotes for long or short local .opus files, so that requested user path is unavailable. Unloading after native decoding has already started still allows WAV conversion to finish on both revisions. Head creates nine parts after unload; master reproduces this with the same .m4a padded past its byte cap and creates eleven. Neither sends a subsequent provider request or writes a note.
  • Evidence and rerunnable drivers are under /home/christian/.t3/worktrees/PodNotes/a41b8063-d252b9a402067b6f/.obsidian-e2e-artifacts/review538/, starting with verification.md. No captured runtime errors. Restored and rebuilt the pinned head, changed no tracked files, and stopped the fixture server and isolated instance; final dry run reports pids: none.

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