feat: add TranscriptionManager orchestrating audio, WS, and output
This commit is contained in:
parent
99b091abc8
commit
285b833ba9
1 changed files with 99 additions and 0 deletions
99
MyVoxtral/MyVoxtral/Models/TranscriptionManager.swift
Normal file
99
MyVoxtral/MyVoxtral/Models/TranscriptionManager.swift
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue