VoiceWake: add chimes for trigger and send
This commit is contained in:
@@ -37,6 +37,7 @@ let package = Package(
|
|||||||
],
|
],
|
||||||
resources: [
|
resources: [
|
||||||
.copy("Resources/Clawdis.icns"),
|
.copy("Resources/Clawdis.icns"),
|
||||||
|
.copy("Resources/Sounds"),
|
||||||
.copy("Resources/WebChat"),
|
.copy("Resources/WebChat"),
|
||||||
],
|
],
|
||||||
swiftSettings: [
|
swiftSettings: [
|
||||||
|
|||||||
@@ -43,6 +43,18 @@ final class AppState: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Published var voiceWakeChimeEnabled: Bool {
|
||||||
|
didSet { UserDefaults.standard.set(self.voiceWakeChimeEnabled, forKey: voiceWakeChimeEnabledKey) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var voiceWakeTriggerChime: VoiceWakeChime {
|
||||||
|
didSet { self.storeChime(self.voiceWakeTriggerChime, key: voiceWakeTriggerChimeKey) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var voiceWakeSendChime: VoiceWakeChime {
|
||||||
|
didSet { self.storeChime(self.voiceWakeSendChime, key: voiceWakeSendChimeKey) }
|
||||||
|
}
|
||||||
|
|
||||||
@Published var iconAnimationsEnabled: Bool {
|
@Published var iconAnimationsEnabled: Bool {
|
||||||
didSet { UserDefaults.standard.set(self.iconAnimationsEnabled, forKey: iconAnimationsEnabledKey) }
|
didSet { UserDefaults.standard.set(self.iconAnimationsEnabled, forKey: iconAnimationsEnabledKey) }
|
||||||
}
|
}
|
||||||
@@ -140,6 +152,14 @@ final class AppState: ObservableObject {
|
|||||||
self.swabbleEnabled = voiceWakeSupported ? savedVoiceWake : false
|
self.swabbleEnabled = voiceWakeSupported ? savedVoiceWake : false
|
||||||
self.swabbleTriggerWords = UserDefaults.standard
|
self.swabbleTriggerWords = UserDefaults.standard
|
||||||
.stringArray(forKey: swabbleTriggersKey) ?? defaultVoiceWakeTriggers
|
.stringArray(forKey: swabbleTriggersKey) ?? defaultVoiceWakeTriggers
|
||||||
|
self.voiceWakeChimeEnabled = UserDefaults.standard
|
||||||
|
.object(forKey: voiceWakeChimeEnabledKey) as? Bool ?? true
|
||||||
|
self.voiceWakeTriggerChime = Self.loadChime(
|
||||||
|
key: voiceWakeTriggerChimeKey,
|
||||||
|
fallback: .system(name: defaultVoiceWakeChimeName))
|
||||||
|
self.voiceWakeSendChime = Self.loadChime(
|
||||||
|
key: voiceWakeSendChimeKey,
|
||||||
|
fallback: .system(name: defaultVoiceWakeChimeName))
|
||||||
if let storedIconAnimations = UserDefaults.standard.object(forKey: iconAnimationsEnabledKey) as? Bool {
|
if let storedIconAnimations = UserDefaults.standard.object(forKey: iconAnimationsEnabledKey) as? Bool {
|
||||||
self.iconAnimationsEnabled = storedIconAnimations
|
self.iconAnimationsEnabled = storedIconAnimations
|
||||||
} else {
|
} else {
|
||||||
@@ -240,6 +260,21 @@ final class AppState: ObservableObject {
|
|||||||
func setWorking(_ working: Bool) {
|
func setWorking(_ working: Bool) {
|
||||||
self.isWorking = working
|
self.isWorking = working
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Chime persistence
|
||||||
|
|
||||||
|
private static func loadChime(key: String, fallback: VoiceWakeChime) -> VoiceWakeChime {
|
||||||
|
guard let data = UserDefaults.standard.data(forKey: key) else { return fallback }
|
||||||
|
if let decoded = try? JSONDecoder().decode(VoiceWakeChime.self, from: data) {
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
private func storeChime(_ chime: VoiceWakeChime, key: String) {
|
||||||
|
guard let data = try? JSONEncoder().encode(chime) else { return }
|
||||||
|
UserDefaults.standard.set(data, forKey: key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ let pauseDefaultsKey = "clawdis.pauseEnabled"
|
|||||||
let iconAnimationsEnabledKey = "clawdis.iconAnimationsEnabled"
|
let iconAnimationsEnabledKey = "clawdis.iconAnimationsEnabled"
|
||||||
let swabbleEnabledKey = "clawdis.swabbleEnabled"
|
let swabbleEnabledKey = "clawdis.swabbleEnabled"
|
||||||
let swabbleTriggersKey = "clawdis.swabbleTriggers"
|
let swabbleTriggersKey = "clawdis.swabbleTriggers"
|
||||||
|
let voiceWakeChimeEnabledKey = "clawdis.voiceWakeChimeEnabled"
|
||||||
|
let voiceWakeTriggerChimeKey = "clawdis.voiceWakeTriggerChime"
|
||||||
|
let voiceWakeSendChimeKey = "clawdis.voiceWakeSendChime"
|
||||||
let showDockIconKey = "clawdis.showDockIcon"
|
let showDockIconKey = "clawdis.showDockIcon"
|
||||||
let defaultVoiceWakeTriggers = ["clawd", "claude"]
|
let defaultVoiceWakeTriggers = ["clawd", "claude"]
|
||||||
|
let defaultVoiceWakeChimeName = "startrek-computer"
|
||||||
|
let defaultVoiceWakeChimeExtension = "wav"
|
||||||
let voiceWakeMicKey = "clawdis.voiceWakeMicID"
|
let voiceWakeMicKey = "clawdis.voiceWakeMicID"
|
||||||
let voiceWakeLocaleKey = "clawdis.voiceWakeLocaleID"
|
let voiceWakeLocaleKey = "clawdis.voiceWakeLocaleID"
|
||||||
let voiceWakeAdditionalLocalesKey = "clawdis.voiceWakeAdditionalLocaleIDs"
|
let voiceWakeAdditionalLocalesKey = "clawdis.voiceWakeAdditionalLocaleIDs"
|
||||||
|
|||||||
Binary file not shown.
@@ -84,6 +84,9 @@ actor VoicePushToTalk {
|
|||||||
let micID: String?
|
let micID: String?
|
||||||
let localeID: String?
|
let localeID: String?
|
||||||
let forwardConfig: VoiceWakeForwardConfig
|
let forwardConfig: VoiceWakeForwardConfig
|
||||||
|
let chimeEnabled: Bool
|
||||||
|
let triggerChime: VoiceWakeChime
|
||||||
|
let sendChime: VoiceWakeChime
|
||||||
}
|
}
|
||||||
|
|
||||||
func begin() async {
|
func begin() async {
|
||||||
@@ -97,6 +100,9 @@ actor VoicePushToTalk {
|
|||||||
let config = await MainActor.run { self.makeConfig() }
|
let config = await MainActor.run { self.makeConfig() }
|
||||||
self.activeConfig = config
|
self.activeConfig = config
|
||||||
self.isCapturing = true
|
self.isCapturing = true
|
||||||
|
if config.chimeEnabled {
|
||||||
|
await MainActor.run { VoiceWakeChimePlayer.play(config.triggerChime) }
|
||||||
|
}
|
||||||
await VoiceWakeRuntime.shared.pauseForPushToTalk()
|
await VoiceWakeRuntime.shared.pauseForPushToTalk()
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
VoiceWakeOverlayController.shared.showPartial(transcript: "")
|
VoiceWakeOverlayController.shared.showPartial(transcript: "")
|
||||||
@@ -132,6 +138,10 @@ actor VoicePushToTalk {
|
|||||||
forward = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig }
|
forward = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.activeConfig?.chimeEnabled == true, let chime = self.activeConfig?.sendChime {
|
||||||
|
await MainActor.run { VoiceWakeChimePlayer.play(chime) }
|
||||||
|
}
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
VoiceWakeOverlayController.shared.presentFinal(
|
VoiceWakeOverlayController.shared.presentFinal(
|
||||||
transcript: finalText,
|
transcript: finalText,
|
||||||
@@ -213,7 +223,10 @@ actor VoicePushToTalk {
|
|||||||
return Config(
|
return Config(
|
||||||
micID: state.voiceWakeMicID.isEmpty ? nil : state.voiceWakeMicID,
|
micID: state.voiceWakeMicID.isEmpty ? nil : state.voiceWakeMicID,
|
||||||
localeID: state.voiceWakeLocaleID,
|
localeID: state.voiceWakeLocaleID,
|
||||||
forwardConfig: state.voiceWakeForwardConfig)
|
forwardConfig: state.voiceWakeForwardConfig,
|
||||||
|
chimeEnabled: state.voiceWakeChimeEnabled,
|
||||||
|
triggerChime: state.voiceWakeTriggerChime,
|
||||||
|
sendChime: state.voiceWakeSendChime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Test helpers
|
// MARK: - Test helpers
|
||||||
|
|||||||
82
apps/macos/Sources/Clawdis/VoiceWakeChime.swift
Normal file
82
apps/macos/Sources/Clawdis/VoiceWakeChime.swift
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum VoiceWakeChime: Codable, Equatable {
|
||||||
|
case system(name: String)
|
||||||
|
case custom(displayName: String, bookmark: Data)
|
||||||
|
|
||||||
|
var systemName: String? {
|
||||||
|
if case let .system(name) = self {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayLabel: String {
|
||||||
|
switch self {
|
||||||
|
case let .system(name):
|
||||||
|
return VoiceWakeChimeCatalog.displayName(for: name)
|
||||||
|
case let .custom(displayName, _):
|
||||||
|
return displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VoiceWakeChimeCatalog {
|
||||||
|
/// Options shown in the picker; first entry is the default bundled tone.
|
||||||
|
static let systemOptions: [String] = [
|
||||||
|
defaultVoiceWakeChimeName,
|
||||||
|
"Ping",
|
||||||
|
"Pop",
|
||||||
|
"Glass",
|
||||||
|
"Frog",
|
||||||
|
"Submarine",
|
||||||
|
"Funk",
|
||||||
|
"Tink",
|
||||||
|
]
|
||||||
|
|
||||||
|
static func displayName(for raw: String) -> String {
|
||||||
|
if raw == defaultVoiceWakeChimeName { return "Startrek Computer" }
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VoiceWakeChimePlayer {
|
||||||
|
@MainActor
|
||||||
|
static func play(_ chime: VoiceWakeChime) {
|
||||||
|
guard let sound = self.sound(for: chime) else { return }
|
||||||
|
sound.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func sound(for chime: VoiceWakeChime) -> NSSound? {
|
||||||
|
switch chime {
|
||||||
|
case let .system(name):
|
||||||
|
// Prefer bundled tone if present.
|
||||||
|
if let bundled = bundledSound(named: name) {
|
||||||
|
return bundled
|
||||||
|
}
|
||||||
|
return NSSound(named: NSSound.Name(name))
|
||||||
|
|
||||||
|
case let .custom(_, bookmark):
|
||||||
|
var stale = false
|
||||||
|
guard let url = try? URL(
|
||||||
|
resolvingBookmarkData: bookmark,
|
||||||
|
options: [.withoutUI, .withSecurityScope],
|
||||||
|
bookmarkDataIsStale: &stale)
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
let scoped = url.startAccessingSecurityScopedResource()
|
||||||
|
defer { if scoped { url.stopAccessingSecurityScopedResource() } }
|
||||||
|
return NSSound(contentsOf: url, byReference: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func bundledSound(named name: String) -> NSSound? {
|
||||||
|
guard let url = Bundle.main.url(
|
||||||
|
forResource: name,
|
||||||
|
withExtension: defaultVoiceWakeChimeExtension,
|
||||||
|
subdirectory: "Resources/Sounds")
|
||||||
|
else { return nil }
|
||||||
|
return NSSound(contentsOf: url, byReference: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,9 @@ actor VoiceWakeRuntime {
|
|||||||
let triggers: [String]
|
let triggers: [String]
|
||||||
let micID: String?
|
let micID: String?
|
||||||
let localeID: String?
|
let localeID: String?
|
||||||
|
let chimeEnabled: Bool
|
||||||
|
let triggerChime: VoiceWakeChime
|
||||||
|
let sendChime: VoiceWakeChime
|
||||||
}
|
}
|
||||||
|
|
||||||
func refresh(state: AppState) async {
|
func refresh(state: AppState) async {
|
||||||
@@ -48,7 +51,10 @@ actor VoiceWakeRuntime {
|
|||||||
let config = RuntimeConfig(
|
let config = RuntimeConfig(
|
||||||
triggers: sanitizeVoiceWakeTriggers(state.swabbleTriggerWords),
|
triggers: sanitizeVoiceWakeTriggers(state.swabbleTriggerWords),
|
||||||
micID: state.voiceWakeMicID.isEmpty ? nil : state.voiceWakeMicID,
|
micID: state.voiceWakeMicID.isEmpty ? nil : state.voiceWakeMicID,
|
||||||
localeID: state.voiceWakeLocaleID.isEmpty ? nil : state.voiceWakeLocaleID)
|
localeID: state.voiceWakeLocaleID.isEmpty ? nil : state.voiceWakeLocaleID,
|
||||||
|
chimeEnabled: state.voiceWakeChimeEnabled,
|
||||||
|
triggerChime: state.voiceWakeTriggerChime,
|
||||||
|
sendChime: state.voiceWakeSendChime)
|
||||||
return (enabled, config)
|
return (enabled, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +205,9 @@ actor VoiceWakeRuntime {
|
|||||||
|
|
||||||
private func beginCapture(transcript: String, config: RuntimeConfig) async {
|
private func beginCapture(transcript: String, config: RuntimeConfig) async {
|
||||||
self.isCapturing = true
|
self.isCapturing = true
|
||||||
|
if config.chimeEnabled {
|
||||||
|
await MainActor.run { VoiceWakeChimePlayer.play(config.triggerChime) }
|
||||||
|
}
|
||||||
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.committedTranscript = ""
|
||||||
@@ -268,6 +277,9 @@ actor VoiceWakeRuntime {
|
|||||||
committed: finalTranscript,
|
committed: finalTranscript,
|
||||||
volatile: "",
|
volatile: "",
|
||||||
isFinal: true)
|
isFinal: true)
|
||||||
|
if config.chimeEnabled {
|
||||||
|
await MainActor.run { VoiceWakeChimePlayer.play(config.sendChime) }
|
||||||
|
}
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
VoiceWakeOverlayController.shared.presentFinal(
|
VoiceWakeOverlayController.shared.presentFinal(
|
||||||
transcript: finalTranscript,
|
transcript: finalTranscript,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import AppKit
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import Speech
|
import Speech
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
struct VoiceWakeSettings: View {
|
struct VoiceWakeSettings: View {
|
||||||
@ObservedObject var state: AppState
|
@ObservedObject var state: AppState
|
||||||
@@ -71,6 +73,8 @@ struct VoiceWakeSettings: View {
|
|||||||
isTesting: self.$isTesting,
|
isTesting: self.$isTesting,
|
||||||
onToggle: self.toggleTest)
|
onToggle: self.toggleTest)
|
||||||
|
|
||||||
|
self.chimeSection
|
||||||
|
|
||||||
self.triggerTable
|
self.triggerTable
|
||||||
|
|
||||||
Spacer(minLength: 8)
|
Spacer(minLength: 8)
|
||||||
@@ -141,6 +145,35 @@ struct VoiceWakeSettings: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var chimeSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||||
|
Toggle(isOn: self.$state.voiceWakeChimeEnabled) {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Play sounds")
|
||||||
|
.font(.callout.weight(.semibold))
|
||||||
|
Text("Chimes for wake-word and push-to-talk events.")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toggleStyle(.switch)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.chimeRow(
|
||||||
|
title: "Trigger sound",
|
||||||
|
selection: self.$state.voiceWakeTriggerChime)
|
||||||
|
.disabled(!self.state.voiceWakeChimeEnabled)
|
||||||
|
|
||||||
|
self.chimeRow(
|
||||||
|
title: "Send sound",
|
||||||
|
selection: self.$state.voiceWakeSendChime)
|
||||||
|
.disabled(!self.state.voiceWakeChimeEnabled)
|
||||||
|
}
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
|
||||||
private func addWord() {
|
private func addWord() {
|
||||||
self.state.swabbleTriggerWords.append("")
|
self.state.swabbleTriggerWords.append("")
|
||||||
}
|
}
|
||||||
@@ -204,6 +237,73 @@ struct VoiceWakeSettings: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func chimeRow(title: String, selection: Binding<VoiceWakeChime>) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
HStack(alignment: .center, spacing: 8) {
|
||||||
|
Text(title)
|
||||||
|
.font(.callout.weight(.semibold))
|
||||||
|
.frame(width: self.fieldLabelWidth, alignment: .leading)
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
ForEach(VoiceWakeChimeCatalog.systemOptions, id: \.self) { option in
|
||||||
|
Button(VoiceWakeChimeCatalog.displayName(for: option)) {
|
||||||
|
selection.wrappedValue = .system(name: option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text(selection.wrappedValue.displayLabel)
|
||||||
|
Image(systemName: "chevron.down")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.frame(width: self.controlWidth, alignment: .leading)
|
||||||
|
.padding(6)
|
||||||
|
.background(Color(nsColor: .windowBackgroundColor))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.stroke(Color.secondary.opacity(0.25), lineWidth: 1))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Choose file…") {
|
||||||
|
self.chooseCustomChime(for: selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Test") {
|
||||||
|
VoiceWakeChimePlayer.play(selection.wrappedValue)
|
||||||
|
}
|
||||||
|
.keyboardShortcut(.space, modifiers: [.command])
|
||||||
|
}
|
||||||
|
|
||||||
|
if case let .custom(displayName, _) = selection.wrappedValue {
|
||||||
|
Text("Custom: \(displayName)")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func chooseCustomChime(for selection: Binding<VoiceWakeChime>) {
|
||||||
|
let panel = NSOpenPanel()
|
||||||
|
panel.allowedContentTypes = [.audio]
|
||||||
|
panel.allowsMultipleSelection = false
|
||||||
|
panel.canChooseDirectories = false
|
||||||
|
panel.resolvesAliases = true
|
||||||
|
panel.begin { response in
|
||||||
|
guard response == .OK, let url = panel.url else { return }
|
||||||
|
do {
|
||||||
|
let bookmark = try url.bookmarkData(
|
||||||
|
options: [.withSecurityScope],
|
||||||
|
includingResourceValuesForKeys: nil,
|
||||||
|
relativeTo: nil)
|
||||||
|
selection.wrappedValue = .custom(displayName: url.lastPathComponent, bookmark: bookmark)
|
||||||
|
} catch {
|
||||||
|
// Ignore failures; user can retry.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func sanitizedTriggers() -> [String] {
|
private func sanitizedTriggers() -> [String] {
|
||||||
sanitizeVoiceWakeTriggers(self.state.swabbleTriggerWords)
|
sanitizeVoiceWakeTriggers(self.state.swabbleTriggerWords)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ Updated: 2025-12-08 · Owners: mac app
|
|||||||
- **Voice Wake** toggle: enables wake-word runtime.
|
- **Voice Wake** toggle: enables wake-word runtime.
|
||||||
- **Hold Cmd+Fn to talk**: enables the push-to-talk monitor. Disabled on macOS < 26.
|
- **Hold Cmd+Fn to talk**: enables the push-to-talk monitor. Disabled on macOS < 26.
|
||||||
- Language & mic pickers, live level meter, trigger-word table, tester, forward target/command all remain unchanged.
|
- Language & mic pickers, live level meter, trigger-word table, tester, forward target/command all remain unchanged.
|
||||||
|
- **Sounds**: optional chimes on trigger detect and on send; defaults to a bundled `startrek-computer.wav`. You can pick any `NSSound`-loadable file (e.g. MP3/WAV/AIFF) for each event.
|
||||||
|
|
||||||
## Forwarding payload
|
## Forwarding payload
|
||||||
- `VoiceWakeForwarder.prefixedTranscript(_:)` prepends the machine hint before sending. Shared between wake-word and push-to-talk paths.
|
- `VoiceWakeForwarder.prefixedTranscript(_:)` prepends the machine hint before sending. Shared between wake-word and push-to-talk paths.
|
||||||
|
|||||||
Reference in New Issue
Block a user