From f82dc77564686bd4e2efe5daeb646b83b50f977a Mon Sep 17 00:00:00 2001 From: Bhaskar Deol Date: Thu, 30 Jul 2026 16:05:57 +0200 Subject: [PATCH] models: cache weights outside ~/Documents so the LaunchAgent can load them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WhisperKit delegates downloads to HubApi, whose downloadBase defaults to ~/Documents/huggingface. ~/Documents is TCC-protected on macOS, so: - launched from a terminal, parrot inherits the terminal's Documents access and model loading works, which is why this isn't visible during normal foreground use; - launched by launchd, parrot has no Documents access of its own and warmUp() fails, so `parrot install --launch-at-login` cannot start on a clean machine. The failure is also misreported. HubApi surfaces it as a corrupted-file error rather than a permission one: warmup failed: modelsUnavailable("Model not found. Please check the model or repo name and try again.\nError: invalidMetadataError(\"Could not remove corrupted metadata file: “model.mil.metadata” couldn’t be removed because you don’t have permission to access it.\")") Nothing is corrupt; the directory is unreadable. Combined with KeepAlive.SuccessfulExit=false the agent then restart-loops. Pass an explicit downloadBase of ~/Library/Application Support/parrot, which is outside TCC. This needs fewer privileges than the alternatives of granting parrot access to Documents or Full Disk Access. Existing users keep their weights by moving the cache once: mkdir -p ~/Library/Application\ Support/parrot mv ~/Documents/huggingface/models \ ~/Library/Application\ Support/parrot/models Co-Authored-By: Claude Opus 5 (1M context) --- .../Transcription/WhisperKitTranscriber.swift | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Sources/parrot/Transcription/WhisperKitTranscriber.swift b/Sources/parrot/Transcription/WhisperKitTranscriber.swift index 8003194b..57fcb7c6 100644 --- a/Sources/parrot/Transcription/WhisperKitTranscriber.swift +++ b/Sources/parrot/Transcription/WhisperKitTranscriber.swift @@ -11,6 +11,23 @@ actor WhisperKitTranscriber: Transcriber { self.model = model } + /// Where model weights are cached. + /// + /// WhisperKit delegates downloads to HubApi, which defaults to + /// ~/Documents/huggingface. ~/Documents is TCC-protected on macOS: a process + /// launched from a terminal inherits the terminal's access, but one started by + /// launchd has none of its own, so model loading fails with a permission + /// error. Application Support sits outside TCC, so caching there keeps the + /// LaunchAgent working without granting parrot access to Documents — fewer + /// privileges, not more. + static var modelCacheDirectory: URL { + let base = FileManager.default + .urls(for: .applicationSupportDirectory, in: .userDomainMask).first + ?? FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent("Library/Application Support", isDirectory: true) + return base.appendingPathComponent("parrot", isDirectory: true) + } + /// Loads the model into memory; downloads first if not already on disk. /// Call once at startup so the first hotkey press isn't blocked on model /// download/load. @@ -20,7 +37,17 @@ actor WhisperKitTranscriber: Transcriber { throw TranscriberError.missingEngineID } FileHandle.standardError.write(Data("loading \(model.id)...\n".utf8)) - let config = WhisperKitConfig(model: whisperKitID, verbose: false, prewarm: true, load: true) + + let cache = Self.modelCacheDirectory + try FileManager.default.createDirectory(at: cache, withIntermediateDirectories: true) + + let config = WhisperKitConfig( + model: whisperKitID, + downloadBase: cache, + verbose: false, + prewarm: true, + load: true + ) pipeline = try await WhisperKit(config) FileHandle.standardError.write(Data("✓ \(model.id) ready\n".utf8)) }