feat: show partial transcripts with subdued tint

This commit is contained in:
Peter Steinberger
2025-12-08 16:44:00 +01:00
parent 7a0830de15
commit 367526f750

View File

@@ -22,6 +22,8 @@ actor VoiceWakeRuntime {
private var capturedTranscript: String = "" private var capturedTranscript: String = ""
private var isCapturing: Bool = false private var isCapturing: Bool = false
private var heardBeyondTrigger: Bool = false private var heardBeyondTrigger: Bool = false
private var committedTranscript: String = ""
private var volatileTranscript: String = ""
private var cooldownUntil: Date? private var cooldownUntil: Date?
private var currentConfig: RuntimeConfig? private var currentConfig: RuntimeConfig?
@@ -97,7 +99,8 @@ actor VoiceWakeRuntime {
self.recognitionTask = recognizer.recognitionTask(with: request) { [weak self] result, error in self.recognitionTask = recognizer.recognitionTask(with: request) { [weak self] result, error in
guard let self else { return } guard let self else { return }
let transcript = result?.bestTranscription.formattedString let transcript = result?.bestTranscription.formattedString
Task { await self.handleRecognition(transcript: transcript, error: error, config: config) } let isFinal = result?.isFinal ?? false
Task { await self.handleRecognition(transcript: transcript, isFinal: isFinal, error: error, config: config) }
} }
self.logger.info("voicewake runtime started") self.logger.info("voicewake runtime started")
@@ -134,6 +137,7 @@ actor VoiceWakeRuntime {
private func handleRecognition( private func handleRecognition(
transcript: String?, transcript: String?,
isFinal: Bool,
error: Error?, error: Error?,
config: RuntimeConfig) async config: RuntimeConfig) async
{ {
@@ -150,8 +154,18 @@ actor VoiceWakeRuntime {
let trimmed = Self.trimmedAfterTrigger(transcript, triggers: config.triggers) let trimmed = Self.trimmedAfterTrigger(transcript, triggers: config.triggers)
self.capturedTranscript = trimmed self.capturedTranscript = trimmed
self.updateHeardBeyondTrigger(withTrimmed: trimmed) self.updateHeardBeyondTrigger(withTrimmed: trimmed)
let snapshot = self.capturedTranscript if isFinal {
let attributed = Self.makeAttributed(transcript: snapshot, isFinal: false) self.committedTranscript = trimmed
self.volatileTranscript = ""
} else {
self.volatileTranscript = Self.delta(after: self.committedTranscript, current: trimmed)
}
let attributed = Self.makeAttributed(
committed: self.committedTranscript,
volatile: self.volatileTranscript,
isFinal: isFinal)
let snapshot = self.committedTranscript + self.volatileTranscript
await MainActor.run { await MainActor.run {
VoiceWakeOverlayController.shared.showPartial(transcript: snapshot, attributed: attributed) VoiceWakeOverlayController.shared.showPartial(transcript: snapshot, attributed: attributed)
} }
@@ -183,12 +197,17 @@ actor VoiceWakeRuntime {
self.isCapturing = true self.isCapturing = true
let trimmed = Self.trimmedAfterTrigger(transcript, triggers: config.triggers) let trimmed = Self.trimmedAfterTrigger(transcript, triggers: config.triggers)
self.capturedTranscript = trimmed self.capturedTranscript = trimmed
self.committedTranscript = ""
self.volatileTranscript = trimmed
self.captureStartedAt = Date() self.captureStartedAt = Date()
self.cooldownUntil = nil self.cooldownUntil = nil
self.heardBeyondTrigger = !trimmed.isEmpty self.heardBeyondTrigger = !trimmed.isEmpty
let snapshot = self.capturedTranscript let snapshot = self.committedTranscript + self.volatileTranscript
let attributed = Self.makeAttributed(transcript: snapshot, isFinal: false) let attributed = Self.makeAttributed(
committed: self.committedTranscript,
volatile: self.volatileTranscript,
isFinal: false)
await MainActor.run { await MainActor.run {
VoiceWakeOverlayController.shared.showPartial(transcript: snapshot, attributed: attributed) VoiceWakeOverlayController.shared.showPartial(transcript: snapshot, attributed: attributed)
} }
@@ -245,7 +264,10 @@ actor VoiceWakeRuntime {
let forwardConfig = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig } let forwardConfig = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig }
let delay: TimeInterval = (heardBeyondTrigger && !finalTranscript.isEmpty) ? 1.0 : 3.0 let delay: TimeInterval = (heardBeyondTrigger && !finalTranscript.isEmpty) ? 1.0 : 3.0
let finalAttributed = Self.makeAttributed(transcript: finalTranscript, isFinal: true) let finalAttributed = Self.makeAttributed(
committed: finalTranscript,
volatile: "",
isFinal: true)
await MainActor.run { await MainActor.run {
VoiceWakeOverlayController.shared.presentFinal( VoiceWakeOverlayController.shared.presentFinal(
transcript: finalTranscript, transcript: finalTranscript,
@@ -295,7 +317,7 @@ private func restartRecognizer() {
} }
static func _testAttributedColor(isFinal: Bool) -> NSColor { static func _testAttributedColor(isFinal: Bool) -> NSColor {
self.makeAttributed(transcript: "sample", isFinal: isFinal) self.makeAttributed(committed: "sample", volatile: "", isFinal: isFinal)
.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? NSColor ?? .clear .attribute(.foregroundColor, at: 0, effectiveRange: nil) as? NSColor ?? .clear
} }
@@ -304,9 +326,21 @@ private func restartRecognizer() {
} }
#endif #endif
private static func makeAttributed(transcript: String, isFinal: Bool) -> NSAttributedString { private static func delta(after committed: String, current: String) -> String {
let color: NSColor = isFinal ? .labelColor : .secondaryLabelColor if current.hasPrefix(committed) {
let attrs: [NSAttributedString.Key: Any] = [.foregroundColor: color] let start = current.index(current.startIndex, offsetBy: committed.count)
return NSAttributedString(string: transcript, attributes: attrs) return String(current[start...])
}
return current
}
private static func makeAttributed(committed: String, volatile: String, isFinal: Bool) -> NSAttributedString {
let full = NSMutableAttributedString()
let committedAttr: [NSAttributedString.Key: Any] = [.foregroundColor: NSColor.labelColor]
full.append(NSAttributedString(string: committed, attributes: committedAttr))
let volatileColor: NSColor = isFinal ? .labelColor : .secondaryLabelColor
let volatileAttr: [NSAttributedString.Key: Any] = [.foregroundColor: volatileColor]
full.append(NSAttributedString(string: volatile, attributes: volatileAttr))
return full
} }
} }