diff --git a/speaktype/App/AppDelegate.swift b/speaktype/App/AppDelegate.swift index 13d3a53..aa1bcfd 100644 --- a/speaktype/App/AppDelegate.swift +++ b/speaktype/App/AppDelegate.swift @@ -45,14 +45,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { let eventSource = CGEventSource(stateID: .hidSystemState) if let keyDown = CGEvent( - keyboardEventSource: eventSource, virtualKey: dummyKeyCode, keyDown: true) - { + keyboardEventSource: eventSource, virtualKey: dummyKeyCode, keyDown: true) { keyDown.post(tap: .cghidEventTap) } if let keyUp = CGEvent( - keyboardEventSource: eventSource, virtualKey: dummyKeyCode, keyDown: false) - { + keyboardEventSource: eventSource, virtualKey: dummyKeyCode, keyDown: false) { keyUp.post(tap: .cghidEventTap) } } @@ -225,8 +223,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } if let rawValue = UserDefaults.standard.string(forKey: "selectedHotkey"), - let option = HotkeyOption(rawValue: rawValue) - { + let option = HotkeyOption(rawValue: rawValue) { return option } diff --git a/speaktype/Constants/Shortcuts+Extensions.swift b/speaktype/Constants/Shortcuts+Extensions.swift index cd2b015..ef06d6e 100644 --- a/speaktype/Constants/Shortcuts+Extensions.swift +++ b/speaktype/Constants/Shortcuts+Extensions.swift @@ -1,6 +1,6 @@ +import AppKit import Foundation import KeyboardShortcuts -import AppKit extension KeyboardShortcuts.Name { static let toggleRecord = Self("toggleRecord", default: .init(.space, modifiers: [.control, .option])) diff --git a/speaktype/Controllers/MiniRecorderWindowController.swift b/speaktype/Controllers/MiniRecorderWindowController.swift index 85e27fe..5a7ba79 100644 --- a/speaktype/Controllers/MiniRecorderWindowController.swift +++ b/speaktype/Controllers/MiniRecorderWindowController.swift @@ -158,8 +158,7 @@ class MiniRecorderWindowController: NSObject { if response == .alertFirstButtonReturn { if let url = URL( string: - "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") - { + "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { NSWorkspace.shared.open(url) } } diff --git a/speaktype/Models/AIModel.swift b/speaktype/Models/AIModel.swift index f93aad9..af6ebf8 100644 --- a/speaktype/Models/AIModel.swift +++ b/speaktype/Models/AIModel.swift @@ -78,7 +78,7 @@ struct AIModel: Identifiable, Equatable { accuracy: 6.0, // ~12% WER expectedSizeBytes: 30_000_000, minimumRAMGB: 2 - ), + ) ] /// Returns the expected minimum size for a given model variant diff --git a/speaktype/Models/AppSettings.swift b/speaktype/Models/AppSettings.swift index c782496..88c67b9 100644 --- a/speaktype/Models/AppSettings.swift +++ b/speaktype/Models/AppSettings.swift @@ -141,4 +141,3 @@ enum WhisperModel: String, Codable, CaseIterable, Identifiable { self == .base || self == .small } } - diff --git a/speaktype/Models/AppVersion.swift b/speaktype/Models/AppVersion.swift index ffebd90..413d70d 100644 --- a/speaktype/Models/AppVersion.swift +++ b/speaktype/Models/AppVersion.swift @@ -85,7 +85,7 @@ extension AppVersion { "GPT-5.2 model support", "Configurable audio resume delay for Bluetooth headphones", "Redesigned Power Mode & Enhancement UI", - "Minor bug fixes and improvements", + "Minor bug fixes and improvements" ], downloadURL: "https://speaktype.app/download/latest", isRequired: false, diff --git a/speaktype/Models/AudioDevice.swift b/speaktype/Models/AudioDevice.swift index 1ba9787..210a318 100644 --- a/speaktype/Models/AudioDevice.swift +++ b/speaktype/Models/AudioDevice.swift @@ -5,8 +5,8 @@ // Created on 2026-01-07. // -import Foundation import AVFoundation +import Foundation /// Represents an audio input device struct AudioDevice: Identifiable, Codable, Equatable { @@ -122,7 +122,6 @@ enum AudioDeviceType: String, Codable, Equatable { extension AudioDevice { // Factory method removed as AVAudioSessionPortDescription is unavailable on macOS - /// System default device static var systemDefault: AudioDevice { AudioDevice( @@ -192,4 +191,3 @@ enum InputMode: String, Codable, CaseIterable, Identifiable { } } } - diff --git a/speaktype/Models/HotkeyConfiguration.swift b/speaktype/Models/HotkeyConfiguration.swift index 3a43637..443b5eb 100644 --- a/speaktype/Models/HotkeyConfiguration.swift +++ b/speaktype/Models/HotkeyConfiguration.swift @@ -5,8 +5,8 @@ // Created on 2026-01-07. // -import Foundation import Carbon.HIToolbox +import Foundation /// Configuration for global keyboard shortcuts struct HotkeyConfiguration: Codable, Equatable, Identifiable { @@ -92,7 +92,7 @@ struct HotkeyConfiguration: Codable, Equatable, Identifiable { (UInt32(kVK_ANSI_Q), [.command]), // Quit (UInt32(kVK_ANSI_W), [.command]), // Close (UInt32(kVK_Tab), [.command]), // Switch apps - (UInt32(kVK_Space), [.command]), // Spotlight + (UInt32(kVK_Space), [.command]) // Spotlight ] return commonConflicts.contains { code, flags in @@ -211,4 +211,3 @@ private func keyCodeToString(_ keyCode: UInt32) -> String { return String(format: "Key%02X", keyCode) } } - diff --git a/speaktype/Models/HotkeyOption.swift b/speaktype/Models/HotkeyOption.swift index bdb3743..b78a208 100644 --- a/speaktype/Models/HotkeyOption.swift +++ b/speaktype/Models/HotkeyOption.swift @@ -1,5 +1,5 @@ -import Foundation import AppKit +import Foundation /// Hotkey options for triggering SpeakType recording enum HotkeyOption: String, Codable, CaseIterable, Identifiable { diff --git a/speaktype/Models/RecordingSession.swift b/speaktype/Models/RecordingSession.swift index 04d903b..ef5ade7 100644 --- a/speaktype/Models/RecordingSession.swift +++ b/speaktype/Models/RecordingSession.swift @@ -5,8 +5,8 @@ // Created on 2026-01-07. // -import Foundation import AVFoundation +import Foundation /// Represents an active or completed audio recording session struct RecordingSession: Identifiable, Equatable { @@ -199,4 +199,3 @@ extension RecordingSession { ) } } - diff --git a/speaktype/Models/TranscriptionResult.swift b/speaktype/Models/TranscriptionResult.swift index 4725c30..ce14d36 100644 --- a/speaktype/Models/TranscriptionResult.swift +++ b/speaktype/Models/TranscriptionResult.swift @@ -109,4 +109,3 @@ extension TranscriptionResult { ) } } - diff --git a/speaktype/Models/TranscriptionState.swift b/speaktype/Models/TranscriptionState.swift index ea6cf50..cfed055 100644 --- a/speaktype/Models/TranscriptionState.swift +++ b/speaktype/Models/TranscriptionState.swift @@ -50,4 +50,3 @@ enum TranscriptionState: String, Codable, Equatable { } } } - diff --git a/speaktype/Services/AudioPlayerService.swift b/speaktype/Services/AudioPlayerService.swift index 1b4d04c..2e835d4 100644 --- a/speaktype/Services/AudioPlayerService.swift +++ b/speaktype/Services/AudioPlayerService.swift @@ -1,6 +1,6 @@ -import Foundation import AVFoundation import Combine +import Foundation /// Service for playing back audio recordings class AudioPlayerService: NSObject, ObservableObject, AVAudioPlayerDelegate { diff --git a/speaktype/Services/AudioRecordingService.swift b/speaktype/Services/AudioRecordingService.swift index a164039..5c14a99 100644 --- a/speaktype/Services/AudioRecordingService.swift +++ b/speaktype/Services/AudioRecordingService.swift @@ -231,7 +231,7 @@ class AudioRecordingService: NSObject, ObservableObject { AVLinearPCMBitDepthKey: 16, AVLinearPCMIsFloatKey: false, AVLinearPCMIsBigEndianKey: false, - AVLinearPCMIsNonInterleaved: false, + AVLinearPCMIsNonInterleaved: false ] assetWriterInput = AVAssetWriterInput(mediaType: .audio, outputSettings: settings) @@ -271,8 +271,7 @@ class AudioRecordingService: NSObject, ObservableObject { // Ensure minimum recording duration to prevent empty/corrupted WAV files if let startTime = currentFileURL?.path.components(separatedBy: "-").last? .replacingOccurrences(of: ".wav", with: ""), - let startTimestamp = Double(startTime) - { + let startTimestamp = Double(startTime) { let duration = Date().timeIntervalSince1970 - startTimestamp if duration < 0.5 { try? await Task.sleep(nanoseconds: UInt64((0.5 - duration) * 1_000_000_000)) @@ -297,8 +296,7 @@ class AudioRecordingService: NSObject, ObservableObject { if let lastChunkInput = self.chunkAssetWriterInput, let lastChunkWriter = self.chunkAssetWriter, let lastChunkURL = self.chunkFileURL, - self.chunkIsSessionStarted - { + self.chunkIsSessionStarted { self.resetChunkWriterState() finishGroup.enter() @@ -494,7 +492,7 @@ extension AudioRecordingService: AVCaptureAudioDataOutputSampleBufferDelegate { AVLinearPCMBitDepthKey: 16, AVLinearPCMIsFloatKey: false, AVLinearPCMIsBigEndianKey: false, - AVLinearPCMIsNonInterleaved: false, + AVLinearPCMIsNonInterleaved: false ] let ci = AVAssetWriterInput(mediaType: .audio, outputSettings: settings) ci.expectsMediaDataInRealTime = true @@ -651,7 +649,7 @@ extension AudioRecordingService: AVCaptureAudioDataOutputSampleBufferDelegate { // Frequency = (Zero Crossings * Sample Rate) / (2 * N) // Note: 'stride' reduces effective sample rate for this calculation, so we adjust let effectiveSampleRate = Float(asbd.mSampleRate) / Float(stride) - let _ = (Float(zeroCrossings) * effectiveSampleRate) / (2.0 * Float(samplesToRead)) + _ = (Float(zeroCrossings) * effectiveSampleRate) / (2.0 * Float(samplesToRead)) // Normalize Frequency for UI (0...1) // Human voice fundamental freq is roughly 85Hz - 255Hz, harmonics go higher. diff --git a/speaktype/Services/HistoryService.swift b/speaktype/Services/HistoryService.swift index 08be90c..562cb92 100644 --- a/speaktype/Services/HistoryService.swift +++ b/speaktype/Services/HistoryService.swift @@ -1,5 +1,5 @@ -import Foundation import Combine +import Foundation import SwiftUI // For IndexSet operations if needed, though Foundation usually covers it, but error says missing import. struct HistoryStatsEntry: Identifiable, Codable, Hashable { @@ -153,8 +153,7 @@ class HistoryService: ObservableObject { items = normalizedItems if normalizedItems.count != decoded.count - || zip(decoded, normalizedItems).contains(where: { $0.transcript != $1.transcript }) - { + || zip(decoded, normalizedItems).contains(where: { $0.transcript != $1.transcript }) { saveHistory() } diff --git a/speaktype/Services/KeychainHelper.swift b/speaktype/Services/KeychainHelper.swift index dd37abe..4114d6a 100644 --- a/speaktype/Services/KeychainHelper.swift +++ b/speaktype/Services/KeychainHelper.swift @@ -109,4 +109,3 @@ class KeychainHelper { } } } - diff --git a/speaktype/Services/LicenseManager+Extensions.swift b/speaktype/Services/LicenseManager+Extensions.swift index f107122..562f346 100644 --- a/speaktype/Services/LicenseManager+Extensions.swift +++ b/speaktype/Services/LicenseManager+Extensions.swift @@ -102,4 +102,3 @@ enum ProFeature { } } } - diff --git a/speaktype/Services/LicenseManager.swift b/speaktype/Services/LicenseManager.swift index 9626e67..23ecd15 100644 --- a/speaktype/Services/LicenseManager.swift +++ b/speaktype/Services/LicenseManager.swift @@ -6,8 +6,8 @@ // License management and validation for Polar.sh integration // -import Foundation import Combine +import Foundation enum LicenseError: LocalizedError { case invalidKey @@ -316,4 +316,3 @@ class LicenseManager: ObservableObject { print("ℹ️ License deactivated locally. Key removed from Keychain.") } } - diff --git a/speaktype/Services/ModelDownloadService.swift b/speaktype/Services/ModelDownloadService.swift index 2ae73ea..7ce6503 100644 --- a/speaktype/Services/ModelDownloadService.swift +++ b/speaktype/Services/ModelDownloadService.swift @@ -1,44 +1,40 @@ -import Foundation import Combine +import Foundation import WhisperKit class ModelDownloadService: ObservableObject { static let shared = ModelDownloadService() - - @Published var downloadProgress: [String: Double] = [:] // Map Model Variant (String) to progress - @Published var downloadError: [String: String] = [:] // Debugging: track errors + + static let storagePathKey = "modelStoragePath" + + @Published var downloadProgress: [String: Double] = [:] + @Published var downloadError: [String: String] = [:] @Published var isDownloading: [String: Bool] = [:] - - private var activeTasks: [String: Task] = [:] // Track running download tasks - + + private var activeTasks: [String: Task] = [:] + + /// Root directory passed as `downloadBase` to WhisperKit/HubApi. + /// Defaults to ~/Library/Application Support/SpeakType/Models per Apple guidelines. + var modelStorageURL: URL { + let custom = UserDefaults.standard.string(forKey: Self.storagePathKey) ?? "" + if !custom.isEmpty { + return URL(fileURLWithPath: custom) + } + let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! + return appSupport.appendingPathComponent("SpeakType/Models") + } + + /// Path where WhisperKit stores individual model folders. + var whisperKitModelsURL: URL { + modelStorageURL.appendingPathComponent("models/argmaxinc/whisperkit-coreml") + } + private init() { - // Force a custom cache directory to avoid "Multiple models found" conflicts - setupCustomCache() - - // Check for already-downloaded models on launch Task { @MainActor in await refreshDownloadedModels() - // Don't auto-select - let user explicitly pick a model which will load it } } - private func setupCustomCache() { - // Use the standard Documents/huggingface location that WhisperKit expects - guard let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } - - let huggingfaceCache = documentsDir.appendingPathComponent("huggingface") - - do { - try FileManager.default.createDirectory(at: huggingfaceCache, withIntermediateDirectories: true) - print("✅ Using standard HuggingFace cache at: \(huggingfaceCache.path)") - } catch { - print("⚠️ Failed to create huggingface directory: \(error)") - } - - // Don't override HF_HUB_CACHE - let WhisperKit use its default behavior - // This ensures compatibility with the standard HuggingFace cache structure - } - // Check which models are already downloaded and update progress dictionary func refreshDownloadedModels() async { print("🔍 Checking for already-downloaded models...") @@ -50,53 +46,37 @@ class ModelDownloadService: ObservableObject { // Verify models actually exist on disk with proper size validation let fileManager = FileManager.default - if let documentsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { - let whisperKitPath = documentsDir.appendingPathComponent("huggingface/models/argmaxinc/whisperkit-coreml") - - if fileManager.fileExists(atPath: whisperKitPath.path) { - if let contents = try? fileManager.contentsOfDirectory(at: whisperKitPath, includingPropertiesForKeys: [.isDirectoryKey]) { - print("📁 Found \(contents.count) items in WhisperKit cache at \(whisperKitPath.path)") - - for item in contents { - let modelName = item.lastPathComponent - - // Skip non-model directories - if modelName == "config.json" || modelName == ".DS_Store" { - continue - } - - // Verify this directory has actual model files (not just empty directory) - if let subContents = try? fileManager.contentsOfDirectory(at: item, includingPropertiesForKeys: [.fileSizeKey]), - !subContents.isEmpty { - - // Check if it has the essential files for a model (must have config.json) - let hasConfigJson = subContents.contains(where: { $0.lastPathComponent == "config.json" }) - let hasModelFiles = subContents.contains(where: { $0.lastPathComponent.hasSuffix(".mlmodelc") }) - - if hasConfigJson && hasModelFiles { - // Calculate total directory size - let directorySize = Self.calculateDirectorySize(at: item) - let expectedSize = AIModel.expectedSize(for: modelName) - - // Model is complete if it's at least 80% of expected size - let minAcceptableSize = Int64(Double(expectedSize) * 0.8) - - if directorySize >= minAcceptableSize { - print("✅ Model \(modelName) verified: \(Self.formatBytes(directorySize)) (expected ~\(Self.formatBytes(expectedSize)))") - foundModels.insert(modelName) - } else { - print("⚠️ Model \(modelName) is INCOMPLETE: \(Self.formatBytes(directorySize)) < \(Self.formatBytes(minAcceptableSize)) minimum") - } - } else { - print("⚠️ Model \(modelName) is incomplete (missing config.json or .mlmodelc files)") - } + let whisperKitPath = whisperKitModelsURL + + if fileManager.fileExists(atPath: whisperKitPath.path), + let contents = try? fileManager.contentsOfDirectory(at: whisperKitPath, includingPropertiesForKeys: [.isDirectoryKey]) { + print("📁 Found \(contents.count) items in WhisperKit cache at \(whisperKitPath.path)") + + for item in contents { + let modelName = item.lastPathComponent + if modelName == "config.json" || modelName == ".DS_Store" { continue } + + if let subContents = try? fileManager.contentsOfDirectory(at: item, includingPropertiesForKeys: [.fileSizeKey]), + !subContents.isEmpty { + let hasConfigJson = subContents.contains(where: { $0.lastPathComponent == "config.json" }) + let hasModelFiles = subContents.contains(where: { $0.lastPathComponent.hasSuffix(".mlmodelc") }) + + if hasConfigJson && hasModelFiles { + let directorySize = Self.calculateDirectorySize(at: item) + let expectedSize = AIModel.expectedSize(for: modelName) + let minAcceptableSize = Int64(Double(expectedSize) * 0.8) + + if directorySize >= minAcceptableSize { + print("✅ Model \(modelName) verified: \(Self.formatBytes(directorySize))") + foundModels.insert(modelName) + } else { + print("⚠️ Model \(modelName) incomplete: \(Self.formatBytes(directorySize)) < \(Self.formatBytes(minAcceptableSize))") } } } - } else { - print("ℹ️ WhisperKit cache directory doesn't exist yet: \(whisperKitPath.path)") - print(" Models will be downloaded on first use.") } + } else { + print("ℹ️ No model storage directory yet — models will be downloaded on first use.") } await MainActor.run { @@ -126,29 +106,21 @@ class ModelDownloadService: ObservableObject { downloadError[variant] = nil print("Starting WhisperKit download for: \(variant)") + let storageURL = modelStorageURL let task = Task { - // Debug: List what WhisperKit sees - // Note: WhisperKit API might differ, but let's try to see if we can get info. - // If fetchAvailableModels exists. - do { - // Determine model variant enum/string - // Note: WhisperKit.download(variant:from:) is the likely API. - // We use the "variant" string to fetch. - // Assuming `WhisperKit.download(variant: variant)` acts as the fetcher. - // Progress callback mock (since we might not have exact API signature yet): - - // Actual API (hypothetical based on search): - // let model = try await WhisperKit(model: variant) - // OR - // try await WhisperKit.download(variant: variant) { progress in ... } - - // likely: download(variant:progressCallback:) - 'from' usually has a default - let _ = try await WhisperKit.download(variant: variant, progressCallback: { progress in - DispatchQueue.main.async { - self.downloadProgress[variant] = progress.fractionCompleted + // Create storage directory only now, on first actual download + try FileManager.default.createDirectory(at: storageURL, withIntermediateDirectories: true) + + _ = try await WhisperKit.download( + variant: variant, + downloadBase: storageURL, + progressCallback: { progress in + DispatchQueue.main.async { + self.downloadProgress[variant] = progress.fractionCompleted + } } - }) + ) // Check if task was cancelled before declaring success if Task.isCancelled { return } @@ -189,11 +161,15 @@ class ModelDownloadService: ObservableObject { // Retry download once do { - let _ = try await WhisperKit.download(variant: variant, progressCallback: { progress in - DispatchQueue.main.async { - self.downloadProgress[variant] = progress.fractionCompleted + _ = try await WhisperKit.download( + variant: variant, + downloadBase: storageURL, + progressCallback: { progress in + DispatchQueue.main.async { + self.downloadProgress[variant] = progress.fractionCompleted + } } - }) + ) if Task.isCancelled { return } @@ -284,11 +260,16 @@ class ModelDownloadService: ObservableObject { deletedCount += cleanupDirectory(tempHf, matchAny: [String(modelName), underscoreVariant]) deletedCount += cleanupDirectory(tempDir, matchAny: [String(modelName), underscoreVariant]) - // 4. Check Documents/huggingface/models/argmaxinc/whisperkit-coreml (standard location) + // 4. Check configured storage location (whisperKitModelsURL) + let configuredModelsPath = whisperKitModelsURL + checkedPaths.append(configuredModelsPath.path) + deletedCount += cleanupDirectory(configuredModelsPath, matchAny: [String(modelName), underscoreVariant]) + + // 5. Check legacy Documents/huggingface location (old default, kept for cleanup) if let documentsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { - let whisperKitModels = documentsDir.appendingPathComponent("huggingface/models/argmaxinc/whisperkit-coreml") - checkedPaths.append(whisperKitModels.path) - deletedCount += cleanupDirectory(whisperKitModels, matchAny: [String(modelName), underscoreVariant]) + let legacyPath = documentsDir.appendingPathComponent("huggingface/models/argmaxinc/whisperkit-coreml") + checkedPaths.append(legacyPath.path) + deletedCount += cleanupDirectory(legacyPath, matchAny: [String(modelName), underscoreVariant]) } print("🗑️ Cleanup complete. Deleted \(deletedCount) items from \(checkedPaths.count) locations") diff --git a/speaktype/Services/PermissionService.swift b/speaktype/Services/PermissionService.swift index 67e39cf..26ffa60 100644 --- a/speaktype/Services/PermissionService.swift +++ b/speaktype/Services/PermissionService.swift @@ -1,6 +1,6 @@ -import Foundation -import AVFoundation import ApplicationServices +import AVFoundation +import Foundation class PermissionService { static let shared = PermissionService() diff --git a/speaktype/Services/TrialManager.swift b/speaktype/Services/TrialManager.swift index 3a8dc54..b40185f 100644 --- a/speaktype/Services/TrialManager.swift +++ b/speaktype/Services/TrialManager.swift @@ -6,8 +6,8 @@ // Manages trial period and post-trial restrictions // -import Foundation import Combine +import Foundation class TrialManager: ObservableObject { static let shared = TrialManager() @@ -150,4 +150,3 @@ enum TrialAction { } } } - diff --git a/speaktype/Services/WhisperService.swift b/speaktype/Services/WhisperService.swift index f19659f..c4055a9 100644 --- a/speaktype/Services/WhisperService.swift +++ b/speaktype/Services/WhisperService.swift @@ -8,7 +8,7 @@ class WhisperService { private static let placeholderPatterns = [ #"\[(?:BLANK_AUDIO|SILENCE)\]"#, #"<\|nospeech\|>"#, - #"\[\s*S\s*\]"#, + #"\[\s*S\s*\]"# ] private static let noiseLabelTerms = [ "applause", @@ -37,7 +37,7 @@ class WhisperService { "unintelligible", "wind", "wind blowing", - "wind noise", + "wind noise" ] private static let bracketedNoisePattern: String = { let escaped = noiseLabelTerms.map(NSRegularExpression.escapedPattern(for:)).joined( @@ -102,8 +102,7 @@ class WhisperService { print("💻 Device RAM: \(ramGB) GB") if let model = AIModel.availableModels.first(where: { $0.variant == variant }), - ramGB < model.minimumRAMGB - { + ramGB < model.minimumRAMGB { print( "⚠️ WARNING: Model \(variant) recommends \(model.minimumRAMGB)GB+ RAM, device has \(ramGB)GB. Loading may fail or be very slow." ) @@ -120,21 +119,20 @@ class WhisperService { } do { - let documentDirectory = FileManager.default.urls( - for: .documentDirectory, in: .userDomainMask - ).first! - let modelFolderPath = documentDirectory.appendingPathComponent( - "huggingface/models/argmaxinc/whisperkit-coreml/\(variant)" - ).path + let storageURL = ModelDownloadService.shared.modelStorageURL + let modelFolderPath = storageURL + .appendingPathComponent("models/argmaxinc/whisperkit-coreml/\(variant)").path // Use WhisperKitConfig with optimized settings let config = WhisperKitConfig( model: variant, + downloadBase: storageURL, modelFolder: modelFolderPath, - computeOptions: ModelComputeOptions(), // Uses GPU + Neural Engine + tokenizerFolder: storageURL, + computeOptions: ModelComputeOptions(), verbose: false, logLevel: .error, - prewarm: true, // Built-in model specialization (replaces manual warmup) + prewarm: true, load: true, download: false // Already downloaded via ModelDownloadService ) diff --git a/speaktype/Utilities/AppLogger.swift b/speaktype/Utilities/AppLogger.swift index bff3ef0..e0e9698 100644 --- a/speaktype/Utilities/AppLogger.swift +++ b/speaktype/Utilities/AppLogger.swift @@ -64,4 +64,3 @@ extension AppLogger { category.info("✅ \(message)") } } - diff --git a/speaktype/Utilities/Constants.swift b/speaktype/Utilities/Constants.swift index f191d89..0476e94 100644 --- a/speaktype/Utilities/Constants.swift +++ b/speaktype/Utilities/Constants.swift @@ -42,4 +42,3 @@ enum Constants { static let userDefaultsSuiteName = "com.speaktype.defaults" } } - diff --git a/speaktype/Utilities/Extensions/Color+Extensions.swift b/speaktype/Utilities/Extensions/Color+Extensions.swift index 68fdfcf..5f00f3d 100644 --- a/speaktype/Utilities/Extensions/Color+Extensions.swift +++ b/speaktype/Utilities/Extensions/Color+Extensions.swift @@ -68,4 +68,3 @@ extension NSColor { ) } } - diff --git a/speaktype/Utilities/Extensions/View+Extensions.swift b/speaktype/Utilities/Extensions/View+Extensions.swift index 3521e78..d32d633 100644 --- a/speaktype/Utilities/Extensions/View+Extensions.swift +++ b/speaktype/Utilities/Extensions/View+Extensions.swift @@ -42,4 +42,3 @@ extension View { } } } - diff --git a/speaktype/Views/Components/MenuBarDashboardView.swift b/speaktype/Views/Components/MenuBarDashboardView.swift index 17d7ff6..263fbf8 100644 --- a/speaktype/Views/Components/MenuBarDashboardView.swift +++ b/speaktype/Views/Components/MenuBarDashboardView.swift @@ -9,7 +9,7 @@ struct MenuBarDashboardView: View { private let statsColumns = [ GridItem(.flexible(), spacing: 10), - GridItem(.flexible(), spacing: 10), + GridItem(.flexible(), spacing: 10) ] private var todayCount: Int { diff --git a/speaktype/Views/Components/ProFeatureGate.swift b/speaktype/Views/Components/ProFeatureGate.swift index e16dd95..bd17401 100644 --- a/speaktype/Views/Components/ProFeatureGate.swift +++ b/speaktype/Views/Components/ProFeatureGate.swift @@ -177,4 +177,3 @@ struct FeatureLockOverlay: View { } */ - diff --git a/speaktype/Views/LicenseView.swift b/speaktype/Views/LicenseView.swift index a998353..af19660 100644 --- a/speaktype/Views/LicenseView.swift +++ b/speaktype/Views/LicenseView.swift @@ -240,4 +240,3 @@ struct LicenseView: View { LicenseView() .environmentObject(LicenseManager()) } - diff --git a/speaktype/Views/Overlays/MiniRecorderView.swift b/speaktype/Views/Overlays/MiniRecorderView.swift index a006456..32b4fd2 100644 --- a/speaktype/Views/Overlays/MiniRecorderView.swift +++ b/speaktype/Views/Overlays/MiniRecorderView.swift @@ -538,8 +538,7 @@ struct MiniRecorderView: View { debugLog("processRecording started with url: \(url.lastPathComponent)") do { // Ensure model is loaded before transcribing - if !whisperService.isInitialized || whisperService.currentModelVariant != selectedModel - { + if !whisperService.isInitialized || whisperService.currentModelVariant != selectedModel { debugLog("Loading model: \(selectedModel)") await MainActor.run { statusMessage = "Warming up model — first use is slower..." } do { diff --git a/speaktype/Views/Screens/Dashboard/DashboardView.swift b/speaktype/Views/Screens/Dashboard/DashboardView.swift index 6255905..1781b66 100644 --- a/speaktype/Views/Screens/Dashboard/DashboardView.swift +++ b/speaktype/Views/Screens/Dashboard/DashboardView.swift @@ -1,5 +1,5 @@ -import AVKit import AppKit +import AVKit import CoreMedia import SwiftUI import UniformTypeIdentifiers @@ -178,8 +178,7 @@ struct DashboardView: View { .onAppear { Task { if !whisperService.isInitialized - || whisperService.currentModelVariant != selectedModel - { + || whisperService.currentModelVariant != selectedModel { try? await whisperService.loadModel(variant: selectedModel) } } @@ -404,7 +403,7 @@ struct ActivityChartCard: View { HStack(alignment: .bottom, spacing: 14) { let maxCount = max(weeklyData.map { $0.count }.max() ?? 1, 1) - ForEach(Array(weeklyData.enumerated()), id: \.offset) { index, data in + ForEach(Array(weeklyData.enumerated()), id: \.offset) { _, data in VStack(spacing: 8) { // Count label on top (only if > 0) Text(data.count > 0 ? "\(data.count)" : "") diff --git a/speaktype/Views/Screens/History/HistoryView.swift b/speaktype/Views/Screens/History/HistoryView.swift index b1fc0bb..858eea4 100644 --- a/speaktype/Views/Screens/History/HistoryView.swift +++ b/speaktype/Views/Screens/History/HistoryView.swift @@ -4,8 +4,8 @@ struct HistoryView: View { @StateObject private var historyService = HistoryService.shared @StateObject private var audioPlayer = AudioPlayerService.shared @State private var showDeleteAlert = false - @State private var itemPendingDeletion: HistoryItem? = nil - @State private var expandedItemId: UUID? = nil + @State private var itemPendingDeletion: HistoryItem? + @State private var expandedItemId: UUID? @State private var showCopyToast = false var body: some View { @@ -49,7 +49,6 @@ struct HistoryView: View { .padding(.horizontal, 24) .padding(.top, 20) - if historyService.items.isEmpty { // Empty state VStack(spacing: 20) { diff --git a/speaktype/Views/Screens/Onboarding/OnboardingView.swift b/speaktype/Views/Screens/Onboarding/OnboardingView.swift index 04a2205..5af2609 100644 --- a/speaktype/Views/Screens/Onboarding/OnboardingView.swift +++ b/speaktype/Views/Screens/Onboarding/OnboardingView.swift @@ -6,7 +6,7 @@ struct OnboardingView: View { @State private var currentPage = 0 var body: some View { - GeometryReader { geometry in + GeometryReader { _ in ZStack { // Background - Match main app exactly Color.bgApp.ignoresSafeArea() @@ -310,7 +310,7 @@ struct PermissionsPage: View { case .notDetermined: // Show native permission prompt - AVCaptureDevice.requestAccess(for: .audio) { granted in + AVCaptureDevice.requestAccess(for: .audio) { _ in DispatchQueue.main.async { self.checkPermissions() } @@ -350,8 +350,7 @@ struct PermissionsPage: View { } func openSettings(for pane: String) { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?\(pane)") - { + if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?\(pane)") { NSWorkspace.shared.open(url) } } @@ -413,7 +412,7 @@ struct FeatureCard: View { LinearGradient( colors: [ Color.bgCard.opacity(0.8), - Color.textSecondary.opacity(0.03), + Color.textSecondary.opacity(0.03) ], startPoint: .top, endPoint: .bottom diff --git a/speaktype/Views/Screens/Onboarding/PermissionsView.swift b/speaktype/Views/Screens/Onboarding/PermissionsView.swift index 89041b7..5b78d95 100644 --- a/speaktype/Views/Screens/Onboarding/PermissionsView.swift +++ b/speaktype/Views/Screens/Onboarding/PermissionsView.swift @@ -1,6 +1,5 @@ -import SwiftUI import AVFoundation - +import SwiftUI struct PermissionsView: View { @State private var micStatus: AVAuthorizationStatus = .notDetermined @@ -51,7 +50,6 @@ struct PermissionsView: View { } ) - } .padding(.horizontal, 40) } diff --git a/speaktype/Views/Screens/Settings/AIModelsView.swift b/speaktype/Views/Screens/Settings/AIModelsView.swift index 05e00b6..fa2e6c9 100644 --- a/speaktype/Views/Screens/Settings/AIModelsView.swift +++ b/speaktype/Views/Screens/Settings/AIModelsView.swift @@ -1,3 +1,4 @@ +import AppKit import SwiftUI /// Screen for managing AI transcription models diff --git a/speaktype/Views/Screens/Settings/AudioInputView.swift b/speaktype/Views/Screens/Settings/AudioInputView.swift index a618c49..4ccc6e9 100644 --- a/speaktype/Views/Screens/Settings/AudioInputView.swift +++ b/speaktype/Views/Screens/Settings/AudioInputView.swift @@ -1,5 +1,5 @@ -import SwiftUI import AVFoundation +import SwiftUI struct AudioInputView: View { @StateObject private var audioRecorder = AudioRecordingService.shared @@ -26,10 +26,6 @@ struct AudioInputView: View { // Input Mode Section Removed - - - - // Available Devices Section VStack(alignment: .leading, spacing: 15) { HStack { @@ -88,8 +84,6 @@ struct AudioInputView: View { } } - - struct DeviceRow: View { let name: String let isActive: Bool diff --git a/speaktype/Views/Screens/Settings/SettingsView.swift b/speaktype/Views/Screens/Settings/SettingsView.swift index 256dd22..6ad8ebc 100644 --- a/speaktype/Views/Screens/Settings/SettingsView.swift +++ b/speaktype/Views/Screens/Settings/SettingsView.swift @@ -399,7 +399,7 @@ struct GeneralSettingsTab: View { ("tl", "Tagalog"), ("tg", "Tajik"), ("ta", "Tamil"), ("tt", "Tatar"), ("te", "Telugu"), ("th", "Thai"), ("bo", "Tibetan"), ("tr", "Turkish"), ("tk", "Turkmen"), ("uk", "Ukrainian"), ("ur", "Urdu"), ("uz", "Uzbek"), - ("vi", "Vietnamese"), ("cy", "Welsh"), ("yi", "Yiddish"), ("yo", "Yoruba"), + ("vi", "Vietnamese"), ("cy", "Welsh"), ("yi", "Yiddish"), ("yo", "Yoruba") ] } @@ -522,8 +522,7 @@ struct PermissionsSettingsTab: View { } private func openSettings(for pane: String) { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?\(pane)") - { + if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?\(pane)") { NSWorkspace.shared.open(url) } } diff --git a/speaktype/Views/Screens/Statistics/StatisticsView.swift b/speaktype/Views/Screens/Statistics/StatisticsView.swift index e458336..bba889d 100644 --- a/speaktype/Views/Screens/Statistics/StatisticsView.swift +++ b/speaktype/Views/Screens/Statistics/StatisticsView.swift @@ -13,7 +13,7 @@ struct StatisticsView: View { @StateObject private var historyService = HistoryService.shared @ObservedObject private var audioRecorder = AudioRecordingService.shared @State private var selectedPeriod: StatisticsPeriod = .week - @State private var timer: Timer? = nil + @State private var timer: Timer? @State private var timeTrigger = Date() var body: some View { @@ -180,7 +180,7 @@ struct StatisticsView: View { .chartXAxis { if selectedPeriod == .year { // For year (monthly view), show months - AxisMarks(values: .automatic) { value in + AxisMarks(values: .automatic) { _ in AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5)) .foregroundStyle(Color.border.opacity(0.2)) AxisValueLabel() @@ -189,7 +189,7 @@ struct StatisticsView: View { } } else if selectedPeriod == .month { // For month view, show every 7 days - AxisMarks(values: .stride(by: .day, count: 7)) { value in + AxisMarks(values: .stride(by: .day, count: 7)) { _ in AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5)) .foregroundStyle(Color.border.opacity(0.2)) AxisValueLabel() @@ -198,7 +198,7 @@ struct StatisticsView: View { } } else { // For week view, show all days - AxisMarks { value in + AxisMarks { _ in AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5)) .foregroundStyle(Color.border.opacity(0.2)) AxisValueLabel() @@ -208,7 +208,7 @@ struct StatisticsView: View { } } .chartYAxis { - AxisMarks(position: .leading, values: .automatic(desiredCount: 5)) { value in + AxisMarks(position: .leading, values: .automatic(desiredCount: 5)) { _ in AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5)) .foregroundStyle(Color.border.opacity(0.2)) AxisValueLabel() diff --git a/speaktype/Views/TranscribeAudioView.swift b/speaktype/Views/TranscribeAudioView.swift index 7d4b4a8..991319e 100644 --- a/speaktype/Views/TranscribeAudioView.swift +++ b/speaktype/Views/TranscribeAudioView.swift @@ -1,6 +1,6 @@ -import SwiftUI import AVFoundation import CoreMedia +import SwiftUI import UniformTypeIdentifiers struct TranscribeAudioView: View {