From 4a6e3d6e85b95d6599e391c06f115e571d58d381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20=22Dezzy=22=20Victor?= Date: Thu, 30 Jul 2026 19:50:28 -0300 Subject: [PATCH] models: download outside Documents so iCloud cannot evict them WhisperKit's downloadBase defaults to ~/Documents/huggingface. With iCloud Desktop & Documents sync on, the model weights are replicated and, once the disk fills, evicted to dataless placeholders (SF_DATALESS, st_blocks=0). CoreML mmap()s the weight blob during ANE compilation, and mmap of an evicted file blocks indefinitely: the daemon prints "loading ..." and never reaches "listening on fn hold". A partially materialised read instead fails as CoreML error 3, "Failed to read first word from AudioEncoder.mlmodelc/ coremldata.bin. It is not a valid .mlmodelc file." Observed on macOS 26.4.1 with whisper-large-v3-turbo (1.6 GB) on a 94%-full disk. Point downloadBase at Application Support, which is neither user-visible nor sync-managed. Existing installs keep their Documents copy; note it on load so the space is reclaimable rather than silently abandoned. --- .../Transcription/WhisperKitTranscriber.swift | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Sources/parrot/Transcription/WhisperKitTranscriber.swift b/Sources/parrot/Transcription/WhisperKitTranscriber.swift index 8003194b..c031104d 100644 --- a/Sources/parrot/Transcription/WhisperKitTranscriber.swift +++ b/Sources/parrot/Transcription/WhisperKitTranscriber.swift @@ -2,6 +2,15 @@ import Foundation import WhisperKit actor WhisperKitTranscriber: Transcriber { + /// Where model weights live. WhisperKit defaults to `Documents`, which iCloud + /// replicates and evicts to dataless stubs — CoreML's mmap of an evicted + /// weight file then blocks forever and the daemon never finishes loading. + private static let modelStore = URL.applicationSupportDirectory.appending(path: "parrot/huggingface") + + /// Earlier versions downloaded into WhisperKit's `Documents` default. Say so + /// once rather than silently re-downloading gigabytes behind the user's back. + private static let legacyModelStore = URL.documentsDirectory.appending(path: "huggingface") + let modelID: String private let model: TranscriptionModel private var pipeline: WhisperKit? @@ -19,12 +28,29 @@ actor WhisperKitTranscriber: Transcriber { guard let whisperKitID = model.whisperKitID else { throw TranscriberError.missingEngineID } + Self.noteLegacyStore() FileHandle.standardError.write(Data("loading \(model.id)...\n".utf8)) - let config = WhisperKitConfig(model: whisperKitID, verbose: false, prewarm: true, load: true) + let config = WhisperKitConfig( + model: whisperKitID, + downloadBase: Self.modelStore, + verbose: false, + prewarm: true, + load: true + ) pipeline = try await WhisperKit(config) FileHandle.standardError.write(Data("✓ \(model.id) ready\n".utf8)) } + /// Point at leftover models from the old `Documents` location so the disk + /// space is reclaimable instead of just abandoned. + private static func noteLegacyStore() { + let path = legacyModelStore.path(percentEncoded: false) + guard FileManager.default.fileExists(atPath: path) else { return } + FileHandle.standardError.write(Data( + "models now live in \(modelStore.path(percentEncoded: false)) — \(path) is unused, delete it to reclaim space\n".utf8 + )) + } + func transcribe(_ audio: [Float]) async throws -> String { if pipeline == nil { try await warmUp() } guard let pipeline else { throw TranscriberError.notLoaded }