Skip to content

fix(whisper): per-model download progress + skip strict size check for STT models#419

Merged
alichherawalla merged 2 commits into
mainfrom
fix/whisper-concurrent-progress
Jun 26, 2026
Merged

fix(whisper): per-model download progress + skip strict size check for STT models#419
alichherawalla merged 2 commits into
mainfrom
fix/whisper-concurrent-progress

Conversation

@Anurag-Wednesday

@Anurag-Wednesday Anurag-Wednesday commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

What

Two fixes for transcription (Whisper) model downloads.

1. Per-model download progress (concurrent downloads)

Downloading more than one Whisper model at once showed a single progress bar that jumped between them. The store tracked one downloadingId + one downloadProgress, so both in-flight downloads wrote to the same value.

Replaced those with a per-model map:

downloadProgressById: Record<string, number>  // id -> 0..1, present only while downloading
  • A key is added when a model starts downloading, updated by its own progress callback, and removed on completion/failure.
  • downloadModel / downloadFromUrl write only their model's entry (helpers setProgress/clearProgress), so concurrent downloads never clobber each other.

Updated the three consumers:

  • TranscriptionModelsTab — each card reads downloadProgressById[id]; disk re-probe now keys off "any downloading".
  • WhisperPickerSheet — per-row percentage, and only the busy row is disabled (you can start others).
  • VoiceRecordButton — scoped to the base.en model it downloads, so another model's download doesn't drive its progress.

ChatScreen / HomeScreen only read downloadedModelId and are unaffected.

2. Skip strict size validation for Whisper downloads

The seeded totalBytes is a rounded-MB approximation (e.g. base.en 142 MB = 148,897,792 B vs the real 147,964,211 B — the gap is proportionally largest for smaller models). The Android worker compares the downloaded size against that expected total within 0.1%, and since whisper.cpp ships no SHA to verify against, a fully-downloaded file was deleted as FILE_CORRUPTED.

whisperService.downloadModel now passes metadataJson: { skipSizeValidation: true } (the same opt-out curated offgrid/* models use). downloadFileTo's param type is widened to allow metadataJson. Integrity is covered by HTTPS + the host allowlist on the pinned ggerganov/whisper.cpp URL. This complements the calculateTotalBytes fix already on main.

Tests

  • New whisperStore test for concurrent downloads (no cross-talk: each model's progress is independent; finishing one leaves the other's intact).
  • Updated store/helper/component tests to the per-model shape.
  • Full related suite green via the pre-push gate (eslint, tsc, 3988 tests).

Note

No native changes in this PR, so the Android gradle gate doesn't apply.

3. Cancelling a download now updates the screen

Cancelling from the Download Manager removed the manager row but left the model showing as "downloading" everywhere else. Native cancelDownload emits no complete/error event and tears down its work observer, so any downloadFileTo() promise awaiting that id (whisperService.downloadModel) hung forever — its finally never ran, so downloadProgressById was never cleared and the Transcription tab / picker / voice button stayed stuck.

  • backgroundDownloadService.cancelDownload now synthesizes a user_cancelled error event after the native cancel (even if the bridge throws), so awaiting downloadFileTo() promises settle. downloadFileTo marks the rejected error .cancelled so callers treat it as a cancel, not a failure.
  • whisperService logs the cancel quietly and still removes the partial file.
  • whisperStore clears the per-model progress entry on cancel without surfacing an error on the row.

This also fixes the same latent hang for every downloadFileTo() consumer (TTS, image zips), not just Whisper. Added tests for the synthetic cancel event, downloadFileTo rejecting as cancelled, native-throw resilience, and the quiet store cleanup.

…r STT models

Two fixes for transcription (Whisper) model downloads.

1. Concurrent downloads showed one progress bar jumping between models. The
   whisper store tracked a single downloadingId + downloadProgress, so two
   in-flight downloads wrote to the same value and the bar flipped between them.
   Replace those with a per-model `downloadProgressById: Record<id, 0..1>` map:
   a key exists only while that model downloads and is removed on completion or
   failure, so several models can download at once, each with its own bar.
   Updated the three consumers — TranscriptionModelsTab, WhisperPickerSheet
   (now shows per-row %, and only the busy row is disabled), and VoiceRecordButton
   (scoped to the base.en model it downloads). ChatScreen/HomeScreen only read
   downloadedModelId and are unaffected.

2. Skip the Android worker's strict final-size check for Whisper downloads. The
   seeded totalBytes is a rounded-MB approximation (e.g. base.en 142 MB =
   148,897,792 B vs the real 147,964,211 B; the gap is largest for smaller
   models), and with no SHA to verify against, a fully-downloaded file was
   deleted as FILE_CORRUPTED. Pass metadataJson: { skipSizeValidation: true }
   (the same opt-out curated offgrid/* models use); integrity is covered by HTTPS
   + the host allowlist on the pinned ggerganov/whisper.cpp URL. Complements the
   calculateTotalBytes fix already on main.

Tests: per-model store behaviour incl. a concurrent-download no-cross-talk case;
updated store/helper/component tests to the new shape.
Cancelling a download from the Download Manager removed the manager row but
left the model showing as "downloading" everywhere else. Native cancelDownload
emits no complete/error event and tears down its work observer, so any
downloadFileTo() promise awaiting that id (e.g. whisperService.downloadModel)
hung forever — its finally never ran, so whisperStore.downloadProgressById was
never cleared and the Transcription tab / picker / voice button stayed stuck.

- backgroundDownloadService.cancelDownload now synthesizes a user_cancelled
  error event after the native cancel (even if the bridge throws), so awaiting
  downloadFileTo() promises settle. downloadFileTo marks the rejected error
  .cancelled so callers can treat it as a cancel, not a failure.
- whisperService logs the cancel quietly and still cleans up the partial file.
- whisperStore download actions clear the per-model progress entry on cancel
  without surfacing an error on the row.

Fixes the same latent hang for every downloadFileTo() consumer (TTS, image zips),
not just Whisper. Tests cover the synthetic cancel event, downloadFileTo
rejecting as cancelled, native-throw resilience, and the quiet store cleanup.
@alichherawalla alichherawalla merged commit 44cff92 into main Jun 26, 2026
3 of 4 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.

2 participants