diff --git a/MyVoxtral/MyVoxtral/Models/TranscriptionManager.swift b/MyVoxtral/MyVoxtral/Models/TranscriptionManager.swift new file mode 100644 index 0000000..76f9312 --- /dev/null +++ b/MyVoxtral/MyVoxtral/Models/TranscriptionManager.swift @@ -0,0 +1,99 @@ +import SwiftUI + +enum RecordingState: Equatable { + case idle + case recording + case error(String) +} + +@MainActor +final class TranscriptionManager: ObservableObject { + @Published var state: RecordingState = .idle + @Published var currentText: String = "" + + private let audioCapture = AudioCapture() + private let wsClient = VoxtralWebSocketClient() + private let settings = AppSettings.shared + private var hasRetried = false + + var isRecording: Bool { state == .recording } + + func toggle() { + if isRecording { + stop() + } else { + start() + } + } + + func start() { + guard settings.hasAPIKey else { + state = .error("No API key set. Open Settings.") + return + } + + currentText = "" + hasRetried = false + + wsClient.onEvent = { [weak self] event in + self?.handleEvent(event) + } + + wsClient.connect(apiKey: settings.apiKey, delayMs: settings.streamingDelayMs) + + audioCapture.onChunk = { [weak self] chunk in + Task { @MainActor in + self?.wsClient.sendAudio(chunk) + } + } + + do { + try audioCapture.start() + state = .recording + } catch { + state = .error("Mic error: \(error.localizedDescription)") + } + } + + func stop() { + audioCapture.stop() + wsClient.flush() + wsClient.disconnect() + state = .idle + + if !currentText.isEmpty { + TranscriptionLogger.append(text: currentText) + } + } + + private func handleEvent(_ event: VoxtralEvent) { + switch event { + case .sessionCreated: + break + case .textDelta(let text): + currentText += text + if settings.outputMode == .cursorInjection { + CursorInjector.typeText(text) + } + case .segment: + break + case .language: + break + case .done(let text): + if currentText.isEmpty { + currentText = text + } + case .error(let message): + if !hasRetried && state == .recording { + hasRetried = true + wsClient.disconnect() + wsClient.connect(apiKey: settings.apiKey, delayMs: settings.streamingDelayMs) + } else { + state = .error(message) + audioCapture.stop() + } + case .unknown: + break + } + } +}