104 lines
3.4 KiB
Swift
104 lines
3.4 KiB
Swift
import AVFoundation
|
|
import ApplicationServices
|
|
import SwiftUI
|
|
import SstCore
|
|
import SstWhisper
|
|
|
|
@MainActor
|
|
final class AppModel: ObservableObject {
|
|
@Published var state: DictationState = .idle
|
|
@Published var modelStatus = "Model laden…"
|
|
@Published var modelReady = false
|
|
@Published var lastError: String?
|
|
@Published var micGranted = false
|
|
@Published var accessibilityGranted = false
|
|
@Published var inputMonitoringGranted = false
|
|
|
|
private let transcriber = WhisperKitTranscriber()
|
|
private let hotkey = HotkeyMonitor()
|
|
private let controller: DictationController
|
|
|
|
init() {
|
|
controller = DictationController(
|
|
recorder: AudioRecorder(),
|
|
transcriber: transcriber,
|
|
cleaner: PassthroughCleaner(),
|
|
inserter: TextInserter(pasteboard: SystemPasteboard(), paster: CGEventPaster())
|
|
)
|
|
controller.onStateChange = { [weak self] in
|
|
self?.state = $0
|
|
if $0 == .recording { self?.lastError = nil }
|
|
}
|
|
controller.onError = { [weak self] in self?.lastError = $0 }
|
|
hotkey.onEvent = { [weak self] event in
|
|
guard let self, self.modelReady else { return }
|
|
if event == .pressed, AVCaptureDevice.authorizationStatus(for: .audio) != .authorized {
|
|
self.lastError = "Microfoon-toegang ontbreekt"
|
|
return
|
|
}
|
|
Task { await self.controller.handle(event) }
|
|
}
|
|
Task { await start() }
|
|
}
|
|
|
|
var statusLine: String {
|
|
guard modelReady else { return modelStatus }
|
|
switch state {
|
|
case .idle: return "Klaar — houd Fn ingedrukt om te dicteren"
|
|
case .recording: return "Luistert…"
|
|
case .transcribing: return "Transcriberen…"
|
|
case .inserting: return "Invoegen…"
|
|
}
|
|
}
|
|
|
|
var iconName: String {
|
|
switch state {
|
|
case .idle:
|
|
if lastError != nil { return "exclamationmark.triangle" }
|
|
return modelReady ? "mic" : "mic.slash"
|
|
case .recording: return "mic.fill"
|
|
case .transcribing, .inserting: return "waveform"
|
|
}
|
|
}
|
|
|
|
func refreshPermissions() {
|
|
micGranted = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized
|
|
accessibilityGranted = AXIsProcessTrusted()
|
|
inputMonitoringGranted = CGPreflightListenEventAccess()
|
|
}
|
|
|
|
func retryHotkey() {
|
|
refreshPermissions()
|
|
do {
|
|
try hotkey.start()
|
|
lastError = nil
|
|
} catch {
|
|
lastError = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
private func start() async {
|
|
_ = await AVCaptureDevice.requestAccess(for: .audio)
|
|
if !CGPreflightListenEventAccess() { _ = CGRequestListenEventAccess() }
|
|
if !AXIsProcessTrusted() {
|
|
_ = AXIsProcessTrustedWithOptions(["AXTrustedCheckOptionPrompt": true] as CFDictionary)
|
|
}
|
|
retryHotkey()
|
|
transcriber.onProgress = { [weak self] fraction in
|
|
Task { @MainActor in
|
|
self?.modelStatus = fraction < 1
|
|
? "Model downloaden… \(Int(fraction * 100))%"
|
|
: "Model laden…"
|
|
}
|
|
}
|
|
do {
|
|
try await transcriber.load()
|
|
modelReady = true
|
|
modelStatus = "Klaar"
|
|
} catch {
|
|
modelStatus = "Model laden mislukt"
|
|
lastError = error.localizedDescription
|
|
}
|
|
}
|
|
}
|