Skip to content
This repository was archived by the owner on Oct 4, 2026. It is now read-only.
This repository was archived by the owner on Oct 4, 2026. It is now read-only.

[Robustness] Model download & switch: cancellable lifecycle, integrity checks, and failure recovery #102

Open

Description

@rcoenen

[Robustness] Model download & switch: cancellable lifecycle, integrity checks, and failure recovery

Umbrella ticket for local model download / load / switch robustness. Consolidates several
reported symptoms that share the same root causes.

Problem

Local model download and switching have no failure recovery. A stalled download, a
partially-written model, or a failed switch leaves the app in a state the user cannot escape:
a progress bar or spinner that never resolves, and a model offered as usable when its bytes
never landed.

Root causes (observed)

  1. Directory-exists = installed. refreshDownloadedModels marks a model installed if its
    folder exists (ModelManager.swift:888-905), with no completeness check. A killed or partial
    download reads as installed and is offered for switching.
  2. No download timeout; inconsistent load timeouts. WhisperKit.download and the prewarm
    compile have no timeout (ModelManager.swift:701-711, 1075-1102). Load timeouts are
    inconsistent (60 s in one alert, 120 s in another) and are only discovered after long doomed
    work.
  3. Concurrency is not gated. ModelManager.downloadModel has an !isDownloading lock
    (:1024-1027), but the Models UI does not enforce it, and switching triggers a download as
    a side effect
    (repair path). A second request fails only after a multi-minute load attempt:
    13:45:58  Switching to model: openai_whisper-large-v3_turbo
    13:45:59  Loading ... from local folder
    13:47:59  ERROR Model loading timed out after 120s
    13:47:59  attemptWhisperModelRepairAndReload begin
    13:47:59  ERROR downloadModel rejected: another download in progress current=parakeet-tdt-0.6b-v3
    
  4. Path / location mismatch. Parakeet downloads to
    ~/Library/Application Support/Pindrop/FluidInference/parakeet-coreml/..., but the app is
    configured for ~/Library/Application Support/FluidAudio/Models (AppCoordinator.swift:872-874).
    Downloader and loader disagree on location → re-download / hang / timeout (see [Bug]: Parakeet v3 times out during load and startup #86, [Bug]: Speaker diarization download succeeds but validation checks legacy model directory #92).
  5. Silent switch exits. Every branch of switchToModel(named:) that returns without a
    terminal event leaves the Models row in a permanent indeterminate spinner
    (AppCoordinator.swift:7346, 7351, 7359, 7364, 7389, 7395; UI clear path only at
    ModelsSettingsView.swift:117-122).

Workstreams

1. Cancellable, time-bounded download lifecycle

  • Add a cancellation handle to downloadModel; expose cancel/abort.
  • Add a watchdog/timeout around WhisperKit.download, prewarm compile, and Parakeet/SenseVoice
    fetches. On timeout: typed failure, clear state, leave the filesystem clean.
  • Adopt one consistent load-timeout policy (not 60 s here, 120 s there).

Acceptance: a stalled download fails within a bounded time with an error; cancel stops it and
cleans partial assets; isDownloading / currentDownloadModel cleared on every exit path.

2. Model integrity validation

  • Replace directory-existence with a completeness check (required files / manifest) for
    WhisperKit, Parakeet, and SenseVoice before "Installed" or switch is offered.
  • Reuse the diarization isOfflineDiarizationReady() pattern.

Acceptance: a partial/corrupt folder is never reported as downloaded.

3. Startup reconciliation

  • On launch, detect partial/corrupt model assets and offer verify / repair / delete instead of
    presenting or selecting them.

Acceptance: installing after a killed mid-download leaves the app usable; the model shows not
installed (or "needs repair").

4. Locate the model where the loader expects it

Acceptance: a completed download is found and loaded with no re-download; no second copy in
another Application Support directory.

5. Switch as explicit request/response

  • Every switchToModel(named:) path must post a terminal outcome (success or failure); no silent
    return. On failure/no-op, clear pending UI state and surface an error/retry.
  • Replace the fire-and-forget NotificationCenter request with a mechanism that carries completion.

Acceptance: switchingToModel is always cleared; every branch is UI-visible.

6. Prevent or queue concurrent downloads

  • Disable/queue conflicting Download and switch actions while a download is running.
  • Fail fast at request time if concurrency is unsupported — never after minutes of loading.

Acceptance: no user action can start a second download that only fails later.

7. Per-model management surface

  • Retry / Cancel / Delete / Verify on the Models row for local models (Delete exists; add the rest).

8. Disk-usage accounting

  • "ON DISK" sums catalog labels for every directory that exists, including partial ones
    (ModelsSettingsView.swift:61-72). Make it reflect real installed bytes.

Related

Key files

  • Packages/PindropShared/Sources/PindropSpeech/ModelManager.swift
  • Packages/PindropShared/Sources/PindropSpeech/Transcription/ParakeetEngine.swift
  • Pindrop/UI/Settings/ModelsSettingsView.swift
  • Pindrop/AppCoordinator.swift (switchToModel :7345, loadAndActivateModel :1673,
    setActiveModel :1596, attemptWhisperModelRepairAndReload :1717)

Notes

Full technical diagnosis: notes/model-download-robustness.md.
Diariation already validates completeness; reuse that approach.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions