fix(whisper): per-model download progress + skip strict size check for STT models#419
Merged
Merged
Conversation
…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.
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.
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+ onedownloadProgress, so both in-flight downloads wrote to the same value.Replaced those with a per-model map:
downloadModel/downloadFromUrlwrite only their model's entry (helperssetProgress/clearProgress), so concurrent downloads never clobber each other.Updated the three consumers:
downloadProgressById[id]; disk re-probe now keys off "any downloading".base.enmodel it downloads, so another model's download doesn't drive its progress.ChatScreen/HomeScreenonly readdownloadedModelIdand are unaffected.2. Skip strict size validation for Whisper downloads
The seeded
totalBytesis a rounded-MB approximation (e.g.base.en142 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 asFILE_CORRUPTED.whisperService.downloadModelnow passesmetadataJson: { skipSizeValidation: true }(the same opt-out curatedoffgrid/*models use).downloadFileTo's param type is widened to allowmetadataJson. Integrity is covered by HTTPS + the host allowlist on the pinnedggerganov/whisper.cppURL. This complements thecalculateTotalBytesfix already on main.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
cancelDownloademits no complete/error event and tears down its work observer, so anydownloadFileTo()promise awaiting that id (whisperService.downloadModel) hung forever — itsfinallynever ran, sodownloadProgressByIdwas never cleared and the Transcription tab / picker / voice button stayed stuck.backgroundDownloadService.cancelDownloadnow synthesizes auser_cancellederror event after the native cancel (even if the bridge throws), so awaitingdownloadFileTo()promises settle.downloadFileTomarks the rejected error.cancelledso callers treat it as a cancel, not a failure.whisperServicelogs the cancel quietly and still removes the partial file.whisperStoreclears 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,downloadFileTorejecting as cancelled, native-throw resilience, and the quiet store cleanup.