Skip to content

fix(stt): verify cached model checksum - #239

Merged
EtienneLescot merged 4 commits into
getopenscreen:mainfrom
arhxam:codex/verify-cached-stt-model
Aug 4, 2026
Merged

fix(stt): verify cached model checksum#239
EtienneLescot merged 4 commits into
getopenscreen:mainfrom
arhxam:codex/verify-cached-stt-model

Conversation

@arhxam

@arhxam arhxam commented Aug 3, 2026

Copy link
Copy Markdown

Summary

  • verify the SHA-256 of a non-empty cached STT model before accepting it
  • quarantine corrupt cache entries and re-download a verified replacement
  • verify downloads before the atomic final rename, so an invalid file never occupies the live model path

Related issue

No linked issue; found by corrupting a non-empty cached model and exercising startup.

Type of change

  • Bug fix
  • Feature
  • Enhancement
  • Documentation
  • Refactor / maintenance
  • Performance
  • Security

Release impact

  • Patch
  • Minor
  • Major / breaking change
  • No release note needed

Desktop impact

  • Windows
  • macOS
  • Linux
  • Installer / packaging
  • Not platform-specific

Screenshots / video

Not applicable; model-cache integrity behavior.

Testing

  • npx vitest --run electron/stt/modelManager.test.ts electron/stt/index.test.ts (14 passed)
  • npx vitest --run --exclude electron/recording/webm-seek-index.test.ts (1,531 passed)
  • npx tsc --noEmit
  • npx tsc -p tsconfig.test.json --noEmit
  • npm run lint (passes with 11 existing warnings)
  • npm run docs:check
  • npm run i18n:check
  • npx vite build

Summary by CodeRabbit

  • Bug Fixes
    • Added checksum validation for cached speech-to-text model files.
    • Invalid cached files are quarantined and automatically replaced with verified downloads.
    • Improved download handling to validate files before making them active, preventing corrupted models from being used.

@arhxam
arhxam requested a review from EtienneLescot as a code owner August 3, 2026 23:18
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@EtienneLescot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ee8a1ed-a6f4-45d3-94ff-7e4251fdf226

📥 Commits

Reviewing files that changed from the base of the PR and between 82a6471 and d81bcbb.

📒 Files selected for processing (3)
  • electron/stt/modelManager.test.ts
  • electron/stt/modelManager.ts
  • technical-documentation/architecture/transcription-and-captions.md
📝 Walkthrough

Walkthrough

The model manager now validates cached and downloaded model files against expected SHA-256 hashes. Invalid files are quarantined with a .bad suffix. Tests cover valid cache hits and replacement of invalid cached models.

Changes

Model cache validation

Layer / File(s) Summary
Cache validation and quarantine
electron/stt/modelManager.ts
ensureFile validates existing and temporary files before completing downloads. Invalid files are quarantined with a .bad suffix.
Checksum-aware cache tests
electron/stt/modelManager.test.ts
Tests cover checksum-valid cache hits and replacement of invalid cached models while preserving the corrupt files.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: etiennelescot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies checksum verification for cached STT models, which is the main change.
Description check ✅ Passed The description covers the change, issue status, classifications, platform impact, screenshots, and testing results.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@EtienneLescot
EtienneLescot force-pushed the main branch 2 times, most recently from 89e07a8 to 545043d Compare August 4, 2026 00:00
@arhxam
arhxam force-pushed the codex/verify-cached-stt-model branch from 594c29f to 597f664 Compare August 4, 2026 00:04
@arhxam
arhxam force-pushed the codex/verify-cached-stt-model branch from 597f664 to ac1bce7 Compare August 4, 2026 00:05

@EtienneLescot EtienneLescot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Both holes are real. The second is the better catch: if (await areModelsPresent(opts.baseDir)) continue; gated the entire loop on the whisper file existing, so a second entry in STT_MODELS would silently never download. And verifying before the final rename rather than after is plainly right — main's version parks a corrupt file at the live model path on its way to throwing.

What I can't take is the quarantine. rename(tmp, filePath) is already atomic, so .bad closes no window; it only destroys the user's only copy before we know a replacement is obtainable. Offline, HF 5xx, disk full — main left a model behind, this leaves none, plus a stranded 264 MB file nothing in this tree ever deletes.

The failure mode I'd want covered before merge is upstream digest drift; details inline. Short version: the model URL goes through resolve/main, so the pin is a bet, and this PR converts "breaks new installs" into "invalidates every existing cache at once."

Rebase note: main has moved 30 commits since your base, including an STT status rework, but it only added completedSec/totalSec to SttStatusEvent and modelManager.ts never touches that type — you're clean there. One UX consequence: prepare() emits phase: "model" before ensureModels, which the renderer maps to mediaStage.downloadingModel, so the new cache hash renders as "Downloading speech model" for a second. Worth a separate beat or a reworded string.

Comment thread electron/stt/modelManager.ts Outdated
Comment thread electron/stt/modelManager.ts
Comment thread electron/stt/modelManager.ts Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/stt/modelManager.test.ts`:
- Around line 71-93: Add a test in the same suite as the existing ensureModels
tests that makes the fetcher return content whose digest does not match
STT_MODELS.whisper.files[0].expectedSha256. Assert ensureModels rejects, the
downloaded content is written to `${paths.whisper}.bad`, and the original
paths.whisper file does not exist; restore any temporarily modified checksum in
cleanup.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7fb9e341-a059-4dd7-a1c5-e7ab914906c6

📥 Commits

Reviewing files that changed from the base of the PR and between 030ccf1 and 82a6471.

📒 Files selected for processing (2)
  • electron/stt/modelManager.test.ts
  • electron/stt/modelManager.ts

Comment thread electron/stt/modelManager.test.ts
EtienneLescot and others added 2 commits August 4, 2026 14:39
The cache path never checked the digest it carries: ensureFile returned as
soon as the file existed and was non-empty, so a model truncated by a full
disk or scrambled by a bad sector was handed straight to whisper.cpp, and the
only symptom the user got was garbage captions. ensureModels also bailed out
on areModelsPresent before touching the loop body, which made the check about
"is any model present" rather than "is this model present" — the moment a
second entry joins STT_MODELS it would silently never download.

Verifying the cache means an existing install can now fail the check on a file
it has been happily using, so what happens next matters more than it used to.
The stale copy stays exactly where it is while the replacement downloads: the
final rename is already atomic, so moving it aside first closes no window,
while costing the user their working model if the replacement never lands —
offline, HuggingFace 5xx, no space — and stranding 264 MB that nothing ever
reaps. If the replacement mismatches too, the download is discarded and the
original bytes are kept, because a double mismatch is precisely when those
bytes are worth looking at. That cleanup is guarded: a Windows AV scanner
still holding the handle raises EPERM, and the bare errno would escape in
place of the mismatch message that actually tells the user what went wrong.

The URL now resolves through a pinned commit instead of resolve/main. A
mutable branch pointer made the recorded digest a bet, and a re-upload used to
break only new installs; with the cache verified on every start it would
invalidate every installed cache at once. The pinned revision was checked
against HuggingFace's paths-info API: its LFS oid for ggml-small-q8_0.bin is
the digest already in the file, byte for byte. Bumping the model now means
bumping the revision and the digest together.

@EtienneLescot EtienneLescot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Threads cleared, including CodeRabbit's, which was reviewing the quarantine this branch removed.

Approving. The quarantine was the real problem: renaming the user's only copy away before knowing a replacement was obtainable turned an offline retry into no model at all. Verification now happens on the .partial and the existing atomic rename does the displacing. I checked the URL pin against the live registry myself — resolve/5359861c… serves 49c8fb02…f779f, byte-identical to the digest already in the file, so it is an invariant now rather than a bet on a branch pointer.

@EtienneLescot
EtienneLescot merged commit 29a7bc2 into getopenscreen:main Aug 4, 2026
18 of 21 checks passed
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.

3 participants