fix: download progress stuck at 95% and slow first transcription - #16
Merged
Conversation
Download progress fixes: - Use 'try await Task.sleep' (not 'try?') so cancellation properly stops the simulated progress loop instead of silently swallowing the error - Add Task.isCancelled guard to prevent stale progress updates after the download completes - Add small delay after cancelling progress task before sending the final 1.0 update, preventing the race where 0.95 overwrites 1.0 - Cap simulated progress at 90% (not 95%) for a more visible jump to completion - Fix race condition in AppState.downloadModel where refreshModelStatuses() could clear downloadProgress before the final progress callback settled on MainActor Slow first transcription fix: - Enable WhisperKit model prewarming (config.prewarm = true) when loading models. This compiles the CoreML Metal/ANE pipeline at load time instead of lazily on the first transcription request, which was causing extreme latency on the first dictation after switching models.
WhisperKit stores downloaded models at: downloadBase/models/argmaxinc/whisperkit-coreml/<model_name>/ But ModelManager was checking for models at: downloadBase/<model_name>/ This caused isModelDownloaded() to always return false, so: - Downloaded models always showed 'Download & Load' instead of 'Load' - Model disk usage showed 0 bytes - modelFolder() returned nil, forcing re-downloads on every load Added modelStorageBase computed property that resolves to the correct nested path and updated all methods that check/access model files.
Removes all VocaMac data: - Running process - Downloaded models (~/.../Application Support/VocaMac/) - CoreML compilation cache - Launcher scripts (~/.local/bin/vocamac*) - App bundle (VocaMac.app) - UserDefaults/preferences - Build artifacts (optional --keep-build flag) Usage: ./scripts/uninstall.sh [--keep-build]
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.
Issues Fixed
1. Download progress stuck at 95%
Root cause: Three race conditions in the simulated progress system:
The progress simulation task used
try? await Task.sleepwhich silently swallowedCancellationError. When the download completed andprogressTask.cancel()was called, the loop kept running and wrote 0.95 after the completion handler wrote 1.0.No
Task.isCancelledguard between the sleep and the progress update, so a cancelled task could still fire off one more stale update.In
AppState.downloadModel,refreshModelStatuses()setdownloadProgress = nilimmediately after the download, but the finalonProgress(1.0)callback was still in-flight on@MainActor, so the 1.0 update would arrive after the nil — leaving the UI stuck showing the last simulated value.Fix:
try await Task.sleep(throws on cancel) + explicitTask.isCancelledguarddownloadModelbeforerefreshModelStatuses()to let the final callback settle2. First transcription extremely slow after switching models
Root cause:
WhisperService.loadModel()was not prewarming the CoreML pipeline. Without prewarming, the first call totranscribe()triggers CoreML's Metal shader compilation and ANE graph optimization — which can take 10-30 seconds depending on model size.Fix: Set
config.prewarm = trueinWhisperKitConfig. This moves the pipeline compilation cost to model load time (where the user already expects a loading phase) instead of surprising them on the first dictation.Files Changed
Sources/VocaMac/Services/ModelManager.swift— Fixed progress simulation race conditionsSources/VocaMac/Models/AppState.swift— Fixed progress callback orderingSources/VocaMac/Services/WhisperService.swift— Enabled model prewarming