Skip to main content
Glama

Whispera

by sapoepsilon
AppLibraryManager.swift13 kB
import Foundation import AppKit import Observation struct ModelInfo { let name: String let displayName: String let path: URL let size: Int64 let sizeFormatted: String let isDownloaded: Bool } enum AppLibraryError: Error, LocalizedError { case directoryNotFound case accessDenied case deletionFailed(String) case calculationFailed var errorDescription: String? { switch self { case .directoryNotFound: return "App library directory not found" case .accessDenied: return "Access denied to app library" case .deletionFailed(let details): return "Failed to delete: \(details)" case .calculationFailed: return "Failed to calculate storage usage" } } } @Observable class AppLibraryManager { // MARK: - Observable Properties var totalStorageUsed: Int64 = 0 var totalStorageFormatted: String = "0 bytes" var downloadedModels: [ModelInfo] = [] var isCalculatingStorage = false var isRemovingModel = false var lastError: AppLibraryError? // MARK: - Computed Properties /// Base application support directory for Whispera var appSupportDirectory: URL? { guard let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { return nil } return appSupport.appendingPathComponent("Whispera") } /// WhisperKit models directory var modelsDirectory: URL? { return appSupportDirectory?.appendingPathComponent("models/argmaxinc/whisperkit-coreml") } /// Downloads directory where updates are stored var downloadsDirectory: URL? { return FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first } /// Logs directory var logsDirectory: URL? { return appSupportDirectory?.appendingPathComponent("Logs") } init() { Task { await refreshStorageInfo() } } // MARK: - Storage Calculation @MainActor func refreshStorageInfo() async { isCalculatingStorage = true defer { isCalculatingStorage = false } do { let models = try await scanForDownloadedModels() downloadedModels = models let totalBytes = models.reduce(0) { $0 + $1.size } totalStorageUsed = totalBytes totalStorageFormatted = ByteCountFormatter.string(fromByteCount: totalBytes, countStyle: .file) lastError = nil } catch { lastError = error as? AppLibraryError ?? .calculationFailed } } private func scanForDownloadedModels() async throws -> [ModelInfo] { guard let modelsDir = modelsDirectory else { throw AppLibraryError.directoryNotFound } guard FileManager.default.fileExists(atPath: modelsDir.path) else { return [] } do { let modelFolders = try FileManager.default.contentsOfDirectory(at: modelsDir, includingPropertiesForKeys: nil) var models: [ModelInfo] = [] for folder in modelFolders { if folder.hasDirectoryPath { let modelName = folder.lastPathComponent let size = try calculateDirectorySize(at: folder) let modelInfo = ModelInfo( name: modelName, displayName: formatModelDisplayName(modelName), path: folder, size: size, sizeFormatted: ByteCountFormatter.string(fromByteCount: size, countStyle: .file), isDownloaded: true ) models.append(modelInfo) } } return models.sorted { $0.displayName < $1.displayName } } catch { throw AppLibraryError.accessDenied } } private func calculateDirectorySize(at url: URL) throws -> Int64 { let resourceKeys: [URLResourceKey] = [.isRegularFileKey, .fileSizeKey] let enumerator = FileManager.default.enumerator( at: url, includingPropertiesForKeys: resourceKeys, options: [.skipsHiddenFiles] ) var totalSize: Int64 = 0 while let fileURL = enumerator?.nextObject() as? URL { let resourceValues = try fileURL.resourceValues(forKeys: Set(resourceKeys)) if resourceValues.isRegularFile == true { totalSize += Int64(resourceValues.fileSize ?? 0) } } return totalSize } private func formatModelDisplayName(_ modelName: String) -> String { let cleanName = modelName.replacingOccurrences(of: "openai_whisper-", with: "") switch cleanName { case "tiny.en": return "Tiny (English)" case "tiny": return "Tiny (Multilingual)" case "base.en": return "Base (English)" case "base": return "Base (Multilingual)" case "small.en": return "Small (English)" case "small": return "Small (Multilingual)" case "medium.en": return "Medium (English)" case "medium": return "Medium (Multilingual)" case "large-v2": return "Large v2" case "large-v3": return "Large v3" case "large-v3-turbo": return "Large v3 Turbo" case "distil-large-v2": return "Distil Large v2" case "distil-large-v3": return "Distil Large v3" default: return cleanName.capitalized } } // MARK: - Model Management @MainActor func removeModel(_ modelInfo: ModelInfo) async throws { isRemovingModel = true defer { isRemovingModel = false } do { try FileManager.default.removeItem(at: modelInfo.path) // Refresh storage info after removal await refreshStorageInfo() lastError = nil } catch { let errorMessage = "Failed to remove \(modelInfo.displayName): \(error.localizedDescription)" lastError = .deletionFailed(errorMessage) throw lastError! } } @MainActor func removeAllModels() async throws { guard let modelsDir = modelsDirectory else { throw AppLibraryError.directoryNotFound } isRemovingModel = true defer { isRemovingModel = false } do { if FileManager.default.fileExists(atPath: modelsDir.path) { try FileManager.default.removeItem(at: modelsDir) } // Refresh storage info after removal await refreshStorageInfo() lastError = nil } catch { let errorMessage = "Failed to remove all models: \(error.localizedDescription)" lastError = .deletionFailed(errorMessage) throw lastError! } } // MARK: - File System Operations func openAppLibraryInFinder() { guard let appSupportDir = appSupportDirectory else { showFinderError("Unable to locate app library directory") return } // Create directory if it doesn't exist if !FileManager.default.fileExists(atPath: appSupportDir.path) { do { try FileManager.default.createDirectory(at: appSupportDir, withIntermediateDirectories: true) } catch { showFinderError("Failed to create app library directory: \(error.localizedDescription)") return } } let success = NSWorkspace.shared.open(appSupportDir) if !success { showFinderError("Failed to open app library in Finder") } } func openDownloadsInFinder() { guard let downloadsDir = downloadsDirectory else { showFinderError("Unable to locate downloads directory") return } // Ensure downloads directory exists if !FileManager.default.fileExists(atPath: downloadsDir.path) { do { try FileManager.default.createDirectory(at: downloadsDir, withIntermediateDirectories: true) } catch { showFinderError("Failed to create downloads directory: \(error.localizedDescription)") return } } let success = NSWorkspace.shared.open(downloadsDir) if !success { showFinderError("Failed to open downloads in Finder") } } func openLogsInFinder() { guard let logsDir = logsDirectory else { showFinderError("Unable to locate logs directory") return } // Create logs directory if it doesn't exist if !FileManager.default.fileExists(atPath: logsDir.path) { do { try FileManager.default.createDirectory(at: logsDir, withIntermediateDirectories: true) } catch { showFinderError("Failed to create logs directory: \(error.localizedDescription)") return } } let success = NSWorkspace.shared.open(logsDir) if !success { showFinderError("Failed to open logs in Finder") } } func revealModelInFinder(_ modelInfo: ModelInfo) { NSWorkspace.shared.selectFile(modelInfo.path.path, inFileViewerRootedAtPath: "") } // MARK: - Update File Management func getDownloadedUpdates() -> [URL] { guard let downloadsDir = downloadsDirectory else { return [] } do { let files = try FileManager.default.contentsOfDirectory(at: downloadsDir, includingPropertiesForKeys: nil) return files.filter { $0.pathExtension == "dmg" && $0.lastPathComponent.hasPrefix("Whispera-") } } catch { return [] } } func removeDownloadedUpdate(at url: URL) throws { try FileManager.default.removeItem(at: url) } func getUpdateFileSize(at url: URL) -> Int64 { do { let attributes = try FileManager.default.attributesOfItem(atPath: url.path) return attributes[.size] as? Int64 ?? 0 } catch { return 0 } } // MARK: - Storage Utilities func formatBytes(_ bytes: Int64) -> String { return ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file) } // MARK: - Log Management func getLogsSize() async -> (size: Int64, formatted: String) { let size = LogManager.shared.calculateLogsSize() let formatted = formatBytes(size) return (size, formatted) } func clearLogs() async throws { try LogManager.shared.clearAllLogs() } var hasModels: Bool { return !downloadedModels.isEmpty } var modelsCount: Int { return downloadedModels.count } // MARK: - Error Handling private func showFinderError(_ message: String) { DispatchQueue.main.async { let alert = NSAlert() alert.messageText = "Finder Error" alert.informativeText = message alert.alertStyle = .warning alert.addButton(withTitle: "OK") alert.runModal() } } } // MARK: - Storage Summary Extension extension AppLibraryManager { /// Gets a summary of storage usage for display func getStorageSummary() -> String { if downloadedModels.isEmpty { return "No models downloaded" } let modelWord = downloadedModels.count == 1 ? "model" : "models" return "\(downloadedModels.count) \(modelWord) • \(totalStorageFormatted)" } /// Gets detailed breakdown of storage usage func getDetailedStorageInfo() -> [String] { var info: [String] = [] if !downloadedModels.isEmpty { info.append("Downloaded Models:") for model in downloadedModels { info.append(" • \(model.displayName): \(model.sizeFormatted)") } info.append("") info.append("Total: \(totalStorageFormatted)") } let updates = getDownloadedUpdates() if !updates.isEmpty { info.append("") info.append("Downloaded Updates:") for update in updates { let size = getUpdateFileSize(at: update) let sizeFormatted = formatBytes(size) info.append(" • \(update.lastPathComponent): \(sizeFormatted)") } } return info } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sapoepsilon/Whispera'

If you have feedback or need assistance with the MCP directory API, please join our Discord server