diff --git a/VoiceInk/Whisper/WhisperState+ModelManager.swift b/VoiceInk/Whisper/WhisperState+ModelManager.swift index 4c3f000..e0e515a 100644 --- a/VoiceInk/Whisper/WhisperState+ModelManager.swift +++ b/VoiceInk/Whisper/WhisperState+ModelManager.swift @@ -150,11 +150,17 @@ extension WhisperState { // MARK: - Resource Management func cleanupModelResources() async { - if !isRecording && !isProcessing { + // Only cleanup resources if we're not actively using them + let canCleanup = !isRecording && !isProcessing + + if canCleanup { logger.notice("๐Ÿงน Cleaning up Whisper resources") + // Release any resources held by the model await whisperContext?.releaseResources() whisperContext = nil isModelLoaded = false + } else { + logger.info("Skipping cleanup while recording or processing is active") } } } \ No newline at end of file diff --git a/VoiceInk/Whisper/WhisperState+UI.swift b/VoiceInk/Whisper/WhisperState+UI.swift index 3955964..aca2915 100644 --- a/VoiceInk/Whisper/WhisperState+UI.swift +++ b/VoiceInk/Whisper/WhisperState+UI.swift @@ -25,10 +25,10 @@ extension WhisperState { } func hideRecorderPanel() { - if isRecording { - Task { - await toggleRecord() - } + if recorderType == "notch" { + notchWindowManager?.hide() + } else { + miniWindowManager?.hide() } } @@ -36,33 +36,31 @@ extension WhisperState { func toggleMiniRecorder() async { if isMiniRecorderVisible { - await dismissMiniRecorder() - } else { - Task { + if isRecording { await toggleRecord() - - SoundManager.shared.playStartSound() - - await MainActor.run { - showRecorderPanel() - isMiniRecorderVisible = true - } + } else { + await cancelRecording() } + } else { + SoundManager.shared.playStartSound() + + await MainActor.run { + isMiniRecorderVisible = true + } + + await toggleRecord() } } func dismissMiniRecorder() async { logger.notice("๐Ÿ“ฑ Dismissing \(self.recorderType) recorder") shouldCancelRecording = true + if isRecording { await recorder.stopRecording() } - if recorderType == "notch" { - notchWindowManager?.hide() - } else { - miniWindowManager?.hide() - } + hideRecorderPanel() await MainActor.run { isRecording = false @@ -79,11 +77,8 @@ extension WhisperState { } func cancelRecording() async { - shouldCancelRecording = true SoundManager.shared.playEscSound() - if isRecording { - await recorder.stopRecording() - } + shouldCancelRecording = true await dismissMiniRecorder() } @@ -95,26 +90,12 @@ extension WhisperState { } @objc public func handleToggleMiniRecorder() { - if isMiniRecorderVisible { - Task { - await toggleRecord() - } - } else { - Task { - await toggleRecord() - - SoundManager.shared.playStartSound() - - await MainActor.run { - showRecorderPanel() - isMiniRecorderVisible = true - } - } + Task { + await toggleMiniRecorder() } } @objc func handleLicenseStatusChanged() { - // This will refresh the license state when it changes elsewhere in the app self.licenseViewModel = LicenseViewModel() } } \ No newline at end of file diff --git a/VoiceInk/Whisper/WhisperState.swift b/VoiceInk/Whisper/WhisperState.swift index e86f20e..0956afb 100644 --- a/VoiceInk/Whisper/WhisperState.swift +++ b/VoiceInk/Whisper/WhisperState.swift @@ -117,19 +117,24 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { func toggleRecord() async { if isRecording { logger.notice("๐Ÿ›‘ Stopping recording") + + await MainActor.run { + isRecording = false + isVisualizerActive = false + } + await recorder.stopRecording() - isRecording = false - isVisualizerActive = false + if let recordedFile { let duration = Date().timeIntervalSince(transcriptionStartTime ?? Date()) - await transcribeAudio(recordedFile, duration: duration) + if !shouldCancelRecording { + await transcribeAudio(recordedFile, duration: duration) + } } else { logger.error("โŒ No recorded file found after stopping recording") } } else { - // Validate that a model is selected before allowing recording to start guard currentModel != nil else { - // Show an alert to the user await MainActor.run { let alert = NSAlert() alert.messageText = "No Whisper Model Selected" @@ -141,6 +146,8 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { return } + shouldCancelRecording = false + logger.notice("๐ŸŽ™๏ธ Starting recording") requestRecordPermission { [self] granted in if granted { @@ -152,11 +159,17 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { create: true) .appending(path: "output.wav") - // Start recording setup and window configuration in parallel + self.recordedFile = file + self.transcriptionStartTime = Date() + + await MainActor.run { + self.isRecording = true + self.isVisualizerActive = true + } + async let recordingTask = self.recorder.startRecording(toOutputFile: file, delegate: self) async let windowConfigTask = ActiveWindowService.shared.applyConfigurationForCurrentApp() - // Start model loading in parallel if needed async let modelLoadingTask: Void = { if let currentModel = await self.currentModel, await self.whisperContext == nil { logger.notice("๐Ÿ”„ Loading model in parallel with recording: \(currentModel.name)") @@ -171,29 +184,23 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { } }() - // Wait for recording and window configuration try await recordingTask await windowConfigTask - self.isRecording = true - self.isVisualizerActive = true - self.recordedFile = file - self.transcriptionStartTime = Date() - - // After recording and window config are done, handle enhancement service if let enhancementService = self.enhancementService, enhancementService.isEnhancementEnabled && enhancementService.useScreenCaptureContext { await enhancementService.captureScreenContext() } - // Wait for model loading to complete (this won't block the UI) await modelLoadingTask } catch { - self.messageLog += "\(error.localizedDescription)\n" - self.isRecording = false - self.isVisualizerActive = false + await MainActor.run { + self.messageLog += "\(error.localizedDescription)\n" + self.isRecording = false + self.isVisualizerActive = false + } } } } else { @@ -235,12 +242,28 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { } private func onDidFinishRecording(success: Bool) { - isRecording = false + if !success { + messageLog += "Recording did not finish successfully\n" + } } private func transcribeAudio(_ url: URL, duration: TimeInterval) async { if shouldCancelRecording { return } + await MainActor.run { + isProcessing = true + isTranscribing = true + canTranscribe = false + } + + defer { + if shouldCancelRecording { + Task { + await cleanupModelResources() + } + } + } + guard let currentModel = currentModel else { logger.error("โŒ Cannot transcribe: No model selected") messageLog += "Cannot transcribe: No model selected.\n" @@ -256,7 +279,6 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { logger.error("โŒ Failed to load model: \(currentModel.name) - \(error.localizedDescription)") messageLog += "Failed to load transcription model. Please try again.\n" currentError = .modelLoadFailed - await cleanupModelResources() return } } @@ -270,47 +292,30 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { logger.notice("๐Ÿ”„ Starting transcription with model: \(currentModel.name)") do { - isProcessing = true - isTranscribing = true - canTranscribe = false - let permanentURL = try saveRecordingPermanently(url) let permanentURLString = permanentURL.absoluteString - if shouldCancelRecording { - await cleanupModelResources() - return - } + if shouldCancelRecording { return } messageLog += "Reading wave samples...\n" let data = try readAudioSamples(url) - if shouldCancelRecording { - await cleanupModelResources() - return - } + if shouldCancelRecording { return } messageLog += "Transcribing data using \(currentModel.name) model...\n" messageLog += "Setting prompt: \(whisperPrompt.transcriptionPrompt)\n" await whisperContext.setPrompt(whisperPrompt.transcriptionPrompt) - if shouldCancelRecording { - await cleanupModelResources() - return - } + if shouldCancelRecording { return } await whisperContext.fullTranscribe(samples: data) - if shouldCancelRecording { - await cleanupModelResources() - return - } + if shouldCancelRecording { return } var text = await whisperContext.getTranscription() text = text.trimmingCharacters(in: .whitespacesAndNewlines) logger.notice("โœ… Transcription completed successfully, length: \(text.count) characters") - // Apply word replacements if enabled if UserDefaults.standard.bool(forKey: "IsWordReplacementEnabled") { text = WordReplacementService.shared.applyReplacements(to: text) logger.notice("โœ… Word replacements applied") @@ -320,10 +325,7 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { enhancementService.isEnhancementEnabled, enhancementService.isConfigured { do { - if shouldCancelRecording { - await cleanupModelResources() - return - } + if shouldCancelRecording { return } messageLog += "Enhancing transcription with AI...\n" let enhancedText = try await enhancementService.enhance(text)