fix(transcription): always transcribe the currently playing episode#182
Conversation
Transcription resolved its audio through downloadEpisode(), which derived an
on-disk path from the user's download-path template and reused any file already
at that path without confirming it belonged to the episode. When the template
was not per-episode unique (the default empty path -> ".mp3", or any path
lacking {{title}}), different episodes mapped to the same file, so the
transcriber read a different episode's audio and saved it under the current
episode's note.
Replace that path-keyed reuse with getEpisodeAudioBuffer(episode), which always
yields the episode's own audio: a resolved local file, an already-downloaded
copy confirmed by the downloaded-episodes registry (keyed by the episode, not a
collidable path), or an in-memory fetch of the episode's stream URL. Nothing is
written to the vault during transcription, so the audio can no longer collide
with another episode. A non-audio response (e.g. an expired private feed) now
fails clearly instead of producing a placeholder-only transcript.
Note: transcription no longer writes audio to the vault or marks an episode as
downloaded; it reuses an already-downloaded copy when one exists.
Deploying podnotes with
|
| Latest commit: |
4517127
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5d1578f3.podnotes.pages.dev |
| Branch Preview URL: | https://chhoumann-107-transcribe-wro.podnotes.pages.dev |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc7028f6c8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const registered = downloadedEpisodes.getEpisode(episode); | ||
| if (registered?.filePath) { | ||
| const existingFile = app.vault.getAbstractFileByPath(registered.filePath); | ||
| if (existingFile instanceof TFile) { | ||
| return readVaultAudio(registered.filePath); |
There was a problem hiding this comment.
Confirm registry match before reusing audio
When a podcast has two different episodes with the same title and only one of them has been downloaded, this path can still transcribe the wrong audio: downloadedEpisodes.getEpisode() matches entries by podcastName and title only (src/store/index.ts:418-421), so a later episode with the same title will reuse the earlier episode's filePath instead of fetching its own streamUrl. This is especially likely when transcript paths include dates/other unique fields, because the old path-based flow would fetch the current episode if its own templated audio path was absent.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — addressed in 0abf632. getEpisodeAudioBuffer now reuses a registry-confirmed file only when the stored streamUrl also matches the current episode's; otherwise it fetches the episode's own audio fresh. Added a regression test for the same-podcast/same-title/different-stream case.
…cribe-wrong-episode # Conflicts: # src/TemplateEngine.ts # src/downloadEpisode.test.ts # src/downloadEpisode.ts
…ded copy The downloaded-episodes registry is keyed by podcastName+title, which two distinct episodes can share (re-releases, placeholder titles). Reusing a registry-confirmed file on title alone could still transcribe the wrong episode's audio, so also require the stored stream URL to match; otherwise fetch the episode's own audio fresh. Addresses Codex review feedback on #182.
…ed downloadEpisode - Reuse a registry-confirmed download when the stream URL's origin+path match, so a rotated signed-CDN query token no longer forces a full re-download of an already-cached episode (still safe: a different episode has a different path). - Add a warning banner to the now-unused downloadEpisode noting it carries the #107 collision behavior and must not be used for transcription. Addresses adversarial review feedback on #182.
# [2.17.0](2.16.0...2.17.0) (2026-06-22) ### Bug Fixes * behavioral-audit logic and robustness fixes (back-end, 1/2) ([#213](#213)) ([4e2845d](4e2845d)) * **download:** default per-episode download path and migrate empty default ([#183](#183)) ([#186](#186)) ([46a6486](46a6486)) * **download:** prevent Android crash and create missing folders on download ([#178](#178)) ([ecd09d5](ecd09d5)), closes [#113](#113) [#86](#86) [#113](#113) [#86](#86) * **lifecycle:** defer mobile podcast view startup ([#208](#208)) ([8bf9ce4](8bf9ce4)) * **notes:** cap note path length and harden folder creation ([#22](#22), [#87](#87)) ([#192](#192)) ([858e280](858e280)), closes [#87-class](#87) * **playback:** persist listened time during playback ([#33](#33)) ([#190](#190)) ([e3433c2](e3433c2)), closes [#191](#191) [#108](#108) [#163](#163) [#183](#183) * **playback:** play local files and downloads on iOS via resource path ([#100](#100)) ([#184](#184)) ([12c503a](12c503a)) * **player:** clear progress on episode switch to stop end-of-playback glitch ([#94](#94)) ([#194](#194)) ([59dccb3](59dccb3)) * **player:** reveal PodNotes view on Play with PodNotes so local files play ([#84](#84)) ([#198](#198)) ([5953625](5953625)) * **settings:** show labelled Add/Remove buttons in podcast search ([#109](#109)) ([#195](#195)) ([33399f5](33399f5)) * show downloaded episodes in the Local Files playlist ([#176](#176)) ([#177](#177)) ([184188c](184188c)) * **timestamps:** capture into the cursor's table cell without breaking the row ([#165](#165)) ([#203](#203)) ([964e342](964e342)) * **transcription:** always transcribe the currently playing episode ([#182](#182)) ([62b488a](62b488a)), closes [#107](#107) * **uri:** preserve '+' in episode titles and paths for timestamp links ([#181](#181)) ([8ad7aa5](8ad7aa5)), closes [#164](#164) * **view:** reliably reveal PodNotes view via command + ribbon icon ([#55](#55)) ([#199](#199)) ([fa1d708](fa1d708)) ### Features * add podcast segment links ([#205](#205)) ([d97e59e](d97e59e)) * **api:** expose generated episode transcripts ([e264465](e264465)), closes [#105](#105) * behavioral-audit UI and interaction fixes (front-end, 2/2) ([#215](#215)) ([894d93d](894d93d)) * **commands:** add playback rate and media timestamp controls ([#206](#206)) ([a8bb44a](a8bb44a)) * **devx:** isolated per-worktree Obsidian E2E vault wrapper ([#188](#188)) ([a8b7a4a](a8b7a4a)) * **episodes:** add a setting to control the Latest Episodes list length ([#114](#114)) ([#200](#200)) ([7b3e3c6](7b3e3c6)) * **notes:** add {{episodelink}} template tag to resume an episode from its note ([#35](#35)) ([#193](#193)) ([8c1ddd6](8c1ddd6)) * **notes:** add podcast feed-level notes ([#163](#163)) ([#187](#187)) ([db0de47](db0de47)), closes [#161](#161) [#160](#160) * **notes:** ship a Bases-friendly default episode note template ([#160](#160)) ([#201](#201)) ([209431d](209431d)), closes [#163](#163) [#183](#183) * **player:** scale episode title font size to its length ([#81](#81)) ([#202](#202)) ([659b6b8](659b6b8)) * **player:** support video episode playback ([#209](#209)) ([f92a91f](f92a91f)) * **queue:** add setting to disable queue auto-population and auto-advance ([#108](#108)) ([#185](#185)) ([cf3d73c](cf3d73c)) * **queue:** allow reordering the playback queue ([#80](#80)) ([#179](#179)) ([d994d61](d994d61)), closes [#173](#173) * **settings:** import/export settings & templates ([#180](#180)) ([a27d23d](a27d23d)), closes [#162](#162) [#162](#162) * **templates:** add {{currentDate}}, {{episodeNumber}}, {{duration}} template variables ([#189](#189)) ([ec573eb](ec573eb)), closes [#75](#75) [#34](#34) [#88](#88) [163/#186](#186) * **templates:** add episode chapters tag ([#207](#207)) ([9c98863](9c98863)) * **transcripts:** opt-in speaker diarization for transcripts ([#168](#168)) ([#204](#204)) ([a96e12f](a96e12f))
|
🎉 This PR is included in version 2.17.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
"Transcribe current episode" could transcribe a different episode than the one playing: the transcript note was titled/dated for the playing episode but its content came from another episode's audio.
Root cause
Transcription resolved its audio via
downloadEpisode(episode, settings.download.path), which derived an on-disk path from the download-path template and returned any existing file at that path without confirming it belonged to the episode. When the template wasn't per-episode-unique — the default emptydownload.path(→.mp3for every episode) or any path without{{title}}(e.g.Downloads.mp3) — episodes collided and the transcriber read the wrong audio.Fix
New
getEpisodeAudioBuffer(episode)always yields the episode's own audio, independent of the download-path template:streamUrl(no vault write).TranscriptionService.transcribeEpisodenow callsgetEpisodeAudioBuffer(episode)instead ofdownloadEpisode(...). A non-audio200response (e.g. an expired private feed serving HTML) now fails clearly instead of producing a placeholder-only transcript.This branch is merged up to current
master, sogetEpisodeAudioBufferis built on the Blob-free download path from #178 (the Android-OOM fix):downloadFilereturns a singleArrayBufferthat is threaded straight into chunking, and extension detection uses the synchronousinferFileExtensionFromDownload/detectAudioFileExtension.Notes for the reviewer
downloadEpisodeWithNotice) is unchanged.downloadEpisode(the path-template-based function) is now unused by production code — onlygetEpisodeAudioBufferis used for transcription. I deliberately leftdownloadEpisode(and its fix(download): prevent Android crash and create missing folders on download #178 tests) in place rather than delete freshly-merged, tested upstream code in a merge; it's a good candidate for a follow-up removal if you'd prefer it gone.Verification
podnotes:podnotes-transcribecommand: with one episode playing, the transcriber read a different episode's bytes from a colliding path; the playing episode's stream was never fetched.src/downloadEpisode.test.tsadds agetEpisodeAudioBuffersuite: per-episode stream fetch, registry-confirmed reuse, stale-registry fallback, ignoring a colliding wrong-episode file, the non-audio guard, and local-file resolution. (fix(download): prevent Android crash and create missing folders on download #178'sdetectAudioFileExtension/downloadEpisodetests are preserved.)master:npm run typecheck,npm run lint,npm run format:check,npm run test(249 passing),npm run build,npm run docs:build— all pass.Release / migration impact
fix:. No settings migration.download.path === ""also affects the regular Download command. Pre-existing junk.mp3files are left for manual cleanup rather than auto-deleted.Fixes #107