diff --git a/apps/macos/Sources/Clawdbot/MenuContentView.swift b/apps/macos/Sources/Clawdbot/MenuContentView.swift index 1e5be8683..3e9caf4fd 100644 --- a/apps/macos/Sources/Clawdbot/MenuContentView.swift +++ b/apps/macos/Sources/Clawdbot/MenuContentView.swift @@ -526,7 +526,7 @@ struct MenuContent: View { deviceTypes: [.external, .microphone], mediaType: .audio, position: .unspecified) - let connectedDevices = discovery.devices.filter { $0.isConnected } + let connectedDevices = discovery.devices.filter(\.isConnected) self.availableMics = connectedDevices .sorted { lhs, rhs in lhs.localizedName.localizedCaseInsensitiveCompare(rhs.localizedName) == .orderedAscending diff --git a/apps/macos/Sources/Clawdbot/VoiceWakeRuntime.swift b/apps/macos/Sources/Clawdbot/VoiceWakeRuntime.swift index b2ef1d442..d29f7b75e 100644 --- a/apps/macos/Sources/Clawdbot/VoiceWakeRuntime.swift +++ b/apps/macos/Sources/Clawdbot/VoiceWakeRuntime.swift @@ -212,7 +212,7 @@ actor VoiceWakeRuntime { let preferred = config.micID?.isEmpty == false ? config.micID! : "system-default" self.logger.info( "voicewake runtime input preferred=\(preferred, privacy: .public) " + - "\(AudioInputDeviceObserver.defaultInputDeviceSummary(), privacy: .public)") + "\(AudioInputDeviceObserver.defaultInputDeviceSummary(), privacy: .public)") self.logger.info("voicewake runtime started") DiagnosticsFileLog.shared.log(category: "voicewake.runtime", event: "started", fields: [ "locale": config.localeID ?? "", @@ -377,8 +377,8 @@ actor VoiceWakeRuntime { isFinal: Bool, match: WakeWordGateMatch?, usedFallback: Bool, - capturing: Bool - ) { + capturing: Bool) + { guard !transcript.isEmpty else { return } if transcript == self.lastLoggedText, !isFinal { if let last = self.lastLoggedAt, Date().timeIntervalSince(last) < 0.25 { @@ -389,7 +389,7 @@ actor VoiceWakeRuntime { self.lastLoggedAt = Date() let textOnly = WakeWordGate.matchesTextOnly(text: transcript, triggers: triggers) - let timingCount = segments.filter { $0.start > 0 || $0.duration > 0 }.count + let timingCount = segments.count(where: { $0.start > 0 || $0.duration > 0 }) let matchSummary = match.map { "match=true gap=\(String(format: "%.2f", $0.postGap))s cmdLen=\($0.command.count)" } ?? "match=false" @@ -401,9 +401,9 @@ actor VoiceWakeRuntime { self.logger.info( "voicewake runtime transcript='\(transcript, privacy: .public)' textOnly=\(textOnly) " + - "isFinal=\(isFinal) timing=\(timingCount)/\(segments.count) " + - "capturing=\(capturing) fallback=\(usedFallback) " + - "\(matchSummary) segments=[\(segmentSummary, privacy: .public)]") + "isFinal=\(isFinal) timing=\(timingCount)/\(segments.count) " + + "capturing=\(capturing) fallback=\(usedFallback) " + + "\(matchSummary) segments=[\(segmentSummary, privacy: .public)]") } private func noteAudioTap(rms: Double) { @@ -471,8 +471,8 @@ actor VoiceWakeRuntime { lastSeenAt: Date?, lastText: String?, triggers: [String], - config: RuntimeConfig - ) async { + config: RuntimeConfig) async + { guard !Task.isCancelled else { return } guard !self.isCapturing else { return } guard let lastSeenAt, let lastText else { return } @@ -488,8 +488,8 @@ actor VoiceWakeRuntime { private func textOnlyFallbackMatch( transcript: String, triggers: [String], - config: WakeWordGateConfig - ) -> WakeWordGateMatch? { + config: WakeWordGateConfig) -> WakeWordGateMatch? + { guard WakeWordGate.matchesTextOnly(text: transcript, triggers: triggers) else { return nil } guard Self.startsWithTrigger(transcript: transcript, triggers: triggers) else { return nil } let trimmed = Self.trimmedAfterTrigger(transcript, triggers: triggers) @@ -745,13 +745,13 @@ actor VoiceWakeRuntime { private static func startsWithTrigger(transcript: String, triggers: [String]) -> Bool { let tokens = transcript .split(whereSeparator: { $0.isWhitespace }) - .map { normalizeToken(String($0)) } + .map { self.normalizeToken(String($0)) } .filter { !$0.isEmpty } guard !tokens.isEmpty else { return false } for trigger in triggers { let triggerTokens = trigger .split(whereSeparator: { $0.isWhitespace }) - .map { normalizeToken(String($0)) } + .map { self.normalizeToken(String($0)) } .filter { !$0.isEmpty } guard !triggerTokens.isEmpty, tokens.count >= triggerTokens.count else { continue } if zip(triggerTokens, tokens.prefix(triggerTokens.count)).allSatisfy({ $0 == $1 }) { @@ -763,7 +763,7 @@ actor VoiceWakeRuntime { private static func normalizeToken(_ token: String) -> String { token - .trimmingCharacters(in: Self.whitespaceAndPunctuation) + .trimmingCharacters(in: self.whitespaceAndPunctuation) .lowercased() } diff --git a/apps/macos/Sources/Clawdbot/VoiceWakeSettings.swift b/apps/macos/Sources/Clawdbot/VoiceWakeSettings.swift index 9449eeb5d..92be13199 100644 --- a/apps/macos/Sources/Clawdbot/VoiceWakeSettings.swift +++ b/apps/macos/Sources/Clawdbot/VoiceWakeSettings.swift @@ -1,8 +1,8 @@ import AppKit import AVFoundation import Observation -import SwabbleKit import Speech +import SwabbleKit import SwiftUI import UniformTypeIdentifiers @@ -371,9 +371,9 @@ struct VoiceWakeSettings: View { private static func textOnlyCommand(from transcript: String, triggers: [String]) -> String? { guard !transcript.isEmpty else { return nil } - let normalized = normalizeToken(transcript) + let normalized = self.normalizeToken(transcript) guard !normalized.isEmpty else { return nil } - guard startsWithTrigger(transcript: transcript, triggers: triggers) else { return nil } + guard self.startsWithTrigger(transcript: transcript, triggers: triggers) else { return nil } guard WakeWordGate.matchesTextOnly(text: transcript, triggers: triggers) else { return nil } let trimmed = WakeWordGate.stripWake(text: transcript, triggers: triggers) return trimmed.isEmpty ? nil : trimmed @@ -382,13 +382,13 @@ struct VoiceWakeSettings: View { private static func startsWithTrigger(transcript: String, triggers: [String]) -> Bool { let tokens = transcript .split(whereSeparator: { $0.isWhitespace }) - .map { normalizeToken(String($0)) } + .map { self.normalizeToken(String($0)) } .filter { !$0.isEmpty } guard !tokens.isEmpty else { return false } for trigger in triggers { let triggerTokens = trigger .split(whereSeparator: { $0.isWhitespace }) - .map { normalizeToken(String($0)) } + .map { self.normalizeToken(String($0)) } .filter { !$0.isEmpty } guard !triggerTokens.isEmpty, tokens.count >= triggerTokens.count else { continue } if zip(triggerTokens, tokens.prefix(triggerTokens.count)).allSatisfy({ $0 == $1 }) { @@ -400,7 +400,7 @@ struct VoiceWakeSettings: View { private static func normalizeToken(_ token: String) -> String { token - .trimmingCharacters(in: Self.whitespaceAndPunctuation) + .trimmingCharacters(in: self.whitespaceAndPunctuation) .lowercased() } @@ -528,14 +528,14 @@ struct VoiceWakeSettings: View { @MainActor private func loadMicsIfNeeded(force: Bool = false) async { - guard (force || self.availableMics.isEmpty), !self.loadingMics else { return } + guard force || self.availableMics.isEmpty, !self.loadingMics else { return } self.loadingMics = true let discovery = AVCaptureDevice.DiscoverySession( deviceTypes: [.external, .microphone], mediaType: .audio, position: .unspecified) let aliveUIDs = AudioInputDeviceObserver.aliveInputDeviceUIDs() - let connectedDevices = discovery.devices.filter { $0.isConnected } + let connectedDevices = discovery.devices.filter(\.isConnected) let devices = aliveUIDs.isEmpty ? connectedDevices : connectedDevices.filter { aliveUIDs.contains($0.uniqueID) } diff --git a/apps/macos/Sources/Clawdbot/VoiceWakeTester.swift b/apps/macos/Sources/Clawdbot/VoiceWakeTester.swift index 64c51590c..6b10260b5 100644 --- a/apps/macos/Sources/Clawdbot/VoiceWakeTester.swift +++ b/apps/macos/Sources/Clawdbot/VoiceWakeTester.swift @@ -263,7 +263,8 @@ final class VoiceWakeTester { segments: [WakeWordSegment], triggers: [String], match: WakeWordGateMatch?, - isFinal: Bool) { + isFinal: Bool) + { guard !transcript.isEmpty else { return } if transcript == self.lastLoggedText, !isFinal { if let last = self.lastLoggedAt, Date().timeIntervalSince(last) < 0.25 { @@ -276,15 +277,15 @@ final class VoiceWakeTester { let textOnly = WakeWordGate.matchesTextOnly(text: transcript, triggers: triggers) let gaps = Self.debugCandidateGaps(triggers: triggers, segments: segments) let segmentSummary = Self.debugSegments(segments) - let timingCount = segments.filter { $0.start > 0 || $0.duration > 0 }.count + let timingCount = segments.count(where: { $0.start > 0 || $0.duration > 0 }) let matchSummary = match.map { "match=true gap=\(String(format: "%.2f", $0.postGap))s cmdLen=\($0.command.count)" } ?? "match=false" self.logger.info( "voicewake test transcript='\(transcript, privacy: .public)' textOnly=\(textOnly) " + - "isFinal=\(isFinal) timing=\(timingCount)/\(segments.count) " + - "\(matchSummary) gaps=[\(gaps, privacy: .public)] segments=[\(segmentSummary, privacy: .public)]") + "isFinal=\(isFinal) timing=\(timingCount)/\(segments.count) " + + "\(matchSummary) gaps=[\(gaps, privacy: .public)] segments=[\(segmentSummary, privacy: .public)]") } private static func debugSegments(_ segments: [WakeWordSegment]) -> String { @@ -296,9 +297,9 @@ final class VoiceWakeTester { } private static func debugCandidateGaps(triggers: [String], segments: [WakeWordSegment]) -> String { - let tokens = normalizeSegments(segments) + let tokens = self.normalizeSegments(segments) guard !tokens.isEmpty else { return "" } - let triggerTokens = normalizeTriggers(triggers) + let triggerTokens = self.normalizeTriggers(triggers) var gaps: [String] = [] for trigger in triggerTokens { @@ -332,7 +333,7 @@ final class VoiceWakeTester { for trigger in triggers { let tokens = trigger .split(whereSeparator: { $0.isWhitespace }) - .map { normalizeToken(String($0)) } + .map { self.normalizeToken(String($0)) } .filter { !$0.isEmpty } if tokens.isEmpty { continue } output.append(DebugTriggerTokens(tokens: tokens)) @@ -342,7 +343,7 @@ final class VoiceWakeTester { private static func normalizeSegments(_ segments: [WakeWordSegment]) -> [DebugToken] { segments.compactMap { segment in - let normalized = normalizeToken(segment.text) + let normalized = self.normalizeToken(segment.text) guard !normalized.isEmpty else { return nil } return DebugToken( normalized: normalized, @@ -353,7 +354,7 @@ final class VoiceWakeTester { private static func normalizeToken(_ token: String) -> String { token - .trimmingCharacters(in: Self.whitespaceAndPunctuation) + .trimmingCharacters(in: self.whitespaceAndPunctuation) .lowercased() } @@ -363,8 +364,8 @@ final class VoiceWakeTester { private func textOnlyFallbackMatch( transcript: String, triggers: [String], - config: WakeWordGateConfig - ) -> WakeWordGateMatch? { + config: WakeWordGateConfig) -> WakeWordGateMatch? + { guard WakeWordGate.matchesTextOnly(text: transcript, triggers: triggers) else { return nil } guard Self.startsWithTrigger(transcript: transcript, triggers: triggers) else { return nil } let trimmed = WakeWordGate.stripWake(text: transcript, triggers: triggers) @@ -375,13 +376,13 @@ final class VoiceWakeTester { private static func startsWithTrigger(transcript: String, triggers: [String]) -> Bool { let tokens = transcript .split(whereSeparator: { $0.isWhitespace }) - .map { normalizeToken(String($0)) } + .map { self.normalizeToken(String($0)) } .filter { !$0.isEmpty } guard !tokens.isEmpty else { return false } for trigger in triggers { let triggerTokens = trigger .split(whereSeparator: { $0.isWhitespace }) - .map { normalizeToken(String($0)) } + .map { self.normalizeToken(String($0)) } .filter { !$0.isEmpty } guard !triggerTokens.isEmpty, tokens.count >= triggerTokens.count else { continue } if zip(triggerTokens, tokens.prefix(triggerTokens.count)).allSatisfy({ $0 == $1 }) { @@ -418,8 +419,8 @@ final class VoiceWakeTester { private func scheduleSilenceCheck( triggers: [String], - onUpdate: @escaping @Sendable (VoiceWakeTestState) -> Void - ) { + onUpdate: @escaping @Sendable (VoiceWakeTestState) -> Void) + { self.silenceTask?.cancel() let lastSeenAt = self.lastTranscriptAt let lastText = self.lastTranscript @@ -433,8 +434,7 @@ final class VoiceWakeTester { guard let match = self.textOnlyFallbackMatch( transcript: lastText, triggers: triggers, - config: WakeWordGateConfig(triggers: triggers) - ) else { return } + config: WakeWordGateConfig(triggers: triggers)) else { return } self.holdingAfterDetect = true self.detectedText = match.command self.logger.info("voice wake detected (test, silence) (len=\(match.command.count))") @@ -455,7 +455,7 @@ final class VoiceWakeTester { let preferred = (preferredMicID?.isEmpty == false) ? preferredMicID! : "system-default" self.logger.info( "voicewake test input preferred=\(preferred, privacy: .public) " + - "\(AudioInputDeviceObserver.defaultInputDeviceSummary(), privacy: .public)") + "\(AudioInputDeviceObserver.defaultInputDeviceSummary(), privacy: .public)") } private nonisolated static func ensurePermissions() async throws -> Bool {