chore: rename project to clawdbot
This commit is contained in:
134
apps/macos/Sources/Clawdbot/VoiceSessionCoordinator.swift
Normal file
134
apps/macos/Sources/Clawdbot/VoiceSessionCoordinator.swift
Normal file
@@ -0,0 +1,134 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class VoiceSessionCoordinator {
|
||||
static let shared = VoiceSessionCoordinator()
|
||||
|
||||
enum Source: String { case wakeWord, pushToTalk }
|
||||
|
||||
struct Session {
|
||||
let token: UUID
|
||||
let source: Source
|
||||
var text: String
|
||||
var attributed: NSAttributedString?
|
||||
var isFinal: Bool
|
||||
var sendChime: VoiceWakeChime
|
||||
var autoSendDelay: TimeInterval?
|
||||
}
|
||||
|
||||
private let logger = Logger(subsystem: "com.clawdbot", category: "voicewake.coordinator")
|
||||
private var session: Session?
|
||||
|
||||
// MARK: - API
|
||||
|
||||
func startSession(
|
||||
source: Source,
|
||||
text: String,
|
||||
attributed: NSAttributedString? = nil,
|
||||
forwardEnabled: Bool = false) -> UUID
|
||||
{
|
||||
let token = UUID()
|
||||
self.logger.info("coordinator start token=\(token.uuidString) source=\(source.rawValue) len=\(text.count)")
|
||||
let attributedText = attributed ?? VoiceWakeOverlayController.shared.makeAttributed(from: text)
|
||||
let session = Session(
|
||||
token: token,
|
||||
source: source,
|
||||
text: text,
|
||||
attributed: attributedText,
|
||||
isFinal: false,
|
||||
sendChime: .none,
|
||||
autoSendDelay: nil)
|
||||
self.session = session
|
||||
VoiceWakeOverlayController.shared.startSession(
|
||||
token: token,
|
||||
source: VoiceWakeOverlayController.Source(rawValue: source.rawValue) ?? .wakeWord,
|
||||
transcript: text,
|
||||
attributed: attributedText,
|
||||
forwardEnabled: forwardEnabled,
|
||||
isFinal: false)
|
||||
return token
|
||||
}
|
||||
|
||||
func updatePartial(token: UUID, text: String, attributed: NSAttributedString? = nil) {
|
||||
guard let session, session.token == token else { return }
|
||||
self.session?.text = text
|
||||
self.session?.attributed = attributed
|
||||
VoiceWakeOverlayController.shared.updatePartial(token: token, transcript: text, attributed: attributed)
|
||||
}
|
||||
|
||||
func finalize(
|
||||
token: UUID,
|
||||
text: String,
|
||||
sendChime: VoiceWakeChime,
|
||||
autoSendAfter: TimeInterval?)
|
||||
{
|
||||
guard let session, session.token == token else { return }
|
||||
self.logger
|
||||
.info(
|
||||
"coordinator finalize token=\(token.uuidString) len=\(text.count) autoSendAfter=\(autoSendAfter ?? -1)")
|
||||
self.session?.text = text
|
||||
self.session?.isFinal = true
|
||||
self.session?.sendChime = sendChime
|
||||
self.session?.autoSendDelay = autoSendAfter
|
||||
|
||||
let attributed = VoiceWakeOverlayController.shared.makeAttributed(from: text)
|
||||
VoiceWakeOverlayController.shared.presentFinal(
|
||||
token: token,
|
||||
transcript: text,
|
||||
autoSendAfter: autoSendAfter,
|
||||
sendChime: sendChime,
|
||||
attributed: attributed)
|
||||
}
|
||||
|
||||
func sendNow(token: UUID, reason: String = "explicit") {
|
||||
guard let session, session.token == token else { return }
|
||||
let text = session.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !text.isEmpty else {
|
||||
self.logger.info("coordinator sendNow \(reason) empty -> dismiss")
|
||||
VoiceWakeOverlayController.shared.dismiss(token: token, reason: .empty, outcome: .empty)
|
||||
self.clearSession()
|
||||
return
|
||||
}
|
||||
VoiceWakeOverlayController.shared.beginSendUI(token: token, sendChime: session.sendChime)
|
||||
Task.detached {
|
||||
_ = await VoiceWakeForwarder.forward(transcript: text)
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss(
|
||||
token: UUID,
|
||||
reason: VoiceWakeOverlayController.DismissReason,
|
||||
outcome: VoiceWakeOverlayController.SendOutcome)
|
||||
{
|
||||
guard let session, session.token == token else { return }
|
||||
VoiceWakeOverlayController.shared.dismiss(token: token, reason: reason, outcome: outcome)
|
||||
self.clearSession()
|
||||
}
|
||||
|
||||
func updateLevel(token: UUID, _ level: Double) {
|
||||
guard let session, session.token == token else { return }
|
||||
VoiceWakeOverlayController.shared.updateLevel(token: token, level)
|
||||
}
|
||||
|
||||
func snapshot() -> (token: UUID?, text: String, visible: Bool) {
|
||||
(self.session?.token, self.session?.text ?? "", VoiceWakeOverlayController.shared.isVisible)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func clearSession() {
|
||||
self.session = nil
|
||||
}
|
||||
|
||||
/// Overlay dismiss completion callback (manual X, empty, auto-dismiss after send).
|
||||
/// Ensures the wake-word recognizer is resumed if Voice Wake is enabled.
|
||||
func overlayDidDismiss(token: UUID?) {
|
||||
if let token, self.session?.token == token {
|
||||
self.clearSession()
|
||||
}
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: AppStateStore.shared) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user