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 }