Skip to content

fix: warm models up after download instead of mid-dictation - #119

Merged
karansinghgit merged 1 commit into
mainfrom
fix/model-warm-up
Aug 20, 2026
Merged

fix: warm models up after download instead of mid-dictation#119
karansinghgit merged 1 commit into
mainfrom
fix/model-warm-up

Conversation

@karansinghgit

Copy link
Copy Markdown
Owner

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 — loadModel otherwise falls through to FluidAudio's downloadAndLoad, 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.loadModel set activeKind after the load. The display-state accessors read whichever engine activeKind names, so isLoading / loadingStage reported the idle Whisper engine for the entire time a Parakeet model was warming. displayPhase never became .warming, so the pill rendered the .processing branch — a lineLimit(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. activeKind now switches before the load.

3. Escape left the HUD wedged open

The processing branch of handleEscape called onCancel — which per MiniRecorderWindowController only tells the window to settle back to idle, it does not hide anything — without ever clearing isProcessing. displayPhase stayed .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 esc affordance. Widened 200 → 320pt so real text fits.

One deliberate call: displayPhase no longer keys off transcription.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 TranscriptionWarmUpTests covering the warm-up guard rules (empty variant, not downloaded, partially downloaded) and the shared isDownloaded readiness 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.

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.
@karansinghgit
karansinghgit merged commit ac5ddb4 into main Aug 20, 2026
2 checks passed
@karansinghgit
karansinghgit deleted the fix/model-warm-up branch August 20, 2026 08:16
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.

1 participant