fix: warm models up after download instead of mid-dictation - #119
Merged
Conversation
Four separate defects made the first dictation a bad first experience. 1. Nothing warmed the model up ahead of time. The hero's "Use this model" only wrote the AppStorage key, and DashboardView's preload runs solely while that screen is on-screen — so the model loaded lazily inside the first dictation, which is exactly when the user is mid-sentence. Models now warm up when a download finishes and when one is selected, via a deduped TranscriptionManager.warmUp that refuses to load a model that isn't downloaded (loadModel would otherwise fall through to FluidAudio's download path and silently pull gigabytes with no progress UI). 2. The status text was clipped. TranscriptionManager.loadModel set activeKind *after* the load, so isLoading/loadingStage reported the idle Whisper engine for the whole time a Parakeet model was warming. The recorder never entered its .warming phase and fell back to the processing pill, where "Warming up model — first use is slower..." was truncated by lineLimit(1). activeKind now switches before the load. 3. Escape left the HUD wedged open. The processing branch of handleEscape called onCancel — which only tells the window to settle back to idle — without clearing isProcessing, so displayPhase stayed .processing and the text sat there with no way to dismiss it. 4. There was no sense of how long the wait would be. The warming pill now shows the engine's live loading stage, elapsed seconds after 3s, that it only happens on first load, and an "esc" affordance. displayPhase deliberately no longer keys off transcription.isLoading, so a background warm-up never expands the HUD for work the user didn't ask for.
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.
Reported from a first-run session: the recorder pill showed "Warming up model — first use is..." — clipped mid-word, stuck on screen with no way to dismiss it, no indication of how long it would take, and it appeared at the worst possible moment (mid-sentence, on the first dictation).
Four separate defects behind that one screenshot.
1. Nothing warmed the model up ahead of time
The hero's "Use this model" only wrote the AppStorage key — no load at all.
DashboardView's preload only runs while that screen is on-screen, and after a download the user is on AI Models, not the dashboard. So the model loaded lazily inside the first dictation.Models now warm up when a download finishes and when one is selected, through a new deduped
TranscriptionManager.warmUp(variant:). It refuses to load a model that isn't fully downloaded —loadModelotherwise falls through to FluidAudio'sdownloadAndLoad, which would silently pull ~2 GB behind a "warming up" label with no progress UI.DashboardView's two preloads now route through it as well, so two screens asking for the same model no longer start two concurrent loads of the same weights.2. The text was clipped because the pill was in the wrong phase
TranscriptionManager.loadModelsetactiveKindafter the load. The display-state accessors read whichever engineactiveKindnames, soisLoading/loadingStagereported the idle Whisper engine for the entire time a Parakeet model was warming.displayPhasenever became.warming, so the pill rendered the.processingbranch — alineLimit(1)Text(statusMessage)in a fixed 210pt capsule, holding the long sentence "Warming up model — first use is slower...". Hence the truncation in the screenshot.activeKindnow switches before the load.3. Escape left the HUD wedged open
The processing branch of
handleEscapecalledonCancel— which perMiniRecorderWindowControlleronly tells the window to settle back to idle, it does not hide anything — without ever clearingisProcessing.displayPhasestayed.processing, so the status text sat on screen with no way to dismiss it. Escape now clears both flags; the load continues in the background and is simply ready for the next dictation.4. No sense of how long
The warming pill now shows the engine's live loading stage, elapsed seconds once it stops feeling instant (3s), that it only happens on first load, and an
escaffordance. Widened 200 → 320pt so real text fits.One deliberate call:
displayPhaseno longer keys offtranscription.isLoading. That is also true during a background warm-up, and with fix #2 in place it would have expanded the HUD unprompted after every download and every launch. The pill now only reflects warm-ups it started itself.Testing
New
TranscriptionWarmUpTestscovering the warm-up guard rules (empty variant, not downloaded, partially downloaded) and the sharedisDownloadedreadiness check. The positive path deliberately isn't unit-tested — it would load real multi-GB CoreML weights.Note: authored on a machine without Xcode, so CI is the build/test gate here; all files pass
swiftc -parse.