@bobfrankston/msger 0.1.74 → 0.1.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/KNOWN-BUGS.md +121 -0
  2. package/MSGER-API-SUMMARY.md +162 -0
  3. package/MSGER-API.md +376 -0
  4. package/README.md +93 -0
  5. package/SESSION-2025-11-06.md +191 -0
  6. package/SESSION-NOTES.md +678 -0
  7. package/clihandler.d.ts.map +1 -1
  8. package/clihandler.js +62 -2
  9. package/clihandler.js.map +1 -1
  10. package/clihandler.ts +60 -2
  11. package/icon.png +0 -0
  12. package/icon1.png +0 -0
  13. package/msger-native/Cargo.toml +1 -0
  14. package/msger-native/bin/msgernative.exe +0 -0
  15. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Breadcrumbs +12 -118
  16. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/BrowserMetrics/{BrowserMetrics-690552AF-DCD4.pma → BrowserMetrics-690B9AD3-657C.pma} +0 -0
  17. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/BrowserMetrics/{BrowserMetrics-69055373-F88C.pma → BrowserMetrics-690BA05A-501C.pma} +0 -0
  18. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Crashpad/settings.dat +0 -0
  19. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/BrowsingTopicsState +1 -1
  20. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Cache/Cache_Data/data_0 +0 -0
  21. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Cache/Cache_Data/data_1 +0 -0
  22. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/{BrowserMetrics/BrowserMetrics-69055587-A65C.pma → Default/Cache/Cache_Data/data_2} +0 -0
  23. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Cache/Cache_Data/data_3 +0 -0
  24. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Cache/Cache_Data/f_000001 +383 -0
  25. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Cache/Cache_Data/f_000002 +1091 -0
  26. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Cache/Cache_Data/f_000003 +2153 -0
  27. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Cache/Cache_Data/f_000004 +0 -0
  28. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Cache/Cache_Data/f_000005 +626 -0
  29. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Cache/Cache_Data/f_000006 +393 -0
  30. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Cache/Cache_Data/index +0 -0
  31. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Code Cache/js/01241693cfdc32b9_0 +0 -0
  32. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Code Cache/js/0ba1eea781f3552c_0 +0 -0
  33. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Code Cache/js/323aa210eebefe2c_0 +0 -0
  34. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Code Cache/js/4608446ac118e77a_0 +0 -0
  35. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Code Cache/js/6938205dc2f77841_0 +0 -0
  36. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Code Cache/js/6de12299dc89e5f3_0 +0 -0
  37. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Code Cache/js/8f403c112eaa455b_0 +0 -0
  38. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Code Cache/js/9a3aceb491137f07_0 +0 -0
  39. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Code Cache/js/aedb266cbaf9c28f_0 +0 -0
  40. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Code Cache/js/ca526fdda86d0b9d_0 +0 -0
  41. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Code Cache/js/f5d11d783c9fdf69_0 +0 -0
  42. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Code Cache/js/index-dir/the-real-index +0 -0
  43. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Collections/collectionsSQLite +0 -0
  44. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Collections/collectionsSQLite-journal +0 -0
  45. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/DIPS +0 -0
  46. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/DawnGraphiteCache/data_1 +0 -0
  47. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/DawnGraphiteCache/index +0 -0
  48. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/DawnWebGPUCache/data_1 +0 -0
  49. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/DawnWebGPUCache/index +0 -0
  50. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Extension State/LOG +3 -3
  51. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Extension State/LOG.old +3 -3
  52. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Favicons +0 -0
  53. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/GPUCache/data_0 +0 -0
  54. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/GPUCache/data_1 +0 -0
  55. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/GPUCache/data_2 +0 -0
  56. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/GPUCache/index +0 -0
  57. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/History +0 -0
  58. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Local Storage/leveldb/000003.log +0 -0
  59. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Local Storage/leveldb/LOG +3 -3
  60. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Local Storage/leveldb/LOG.old +3 -3
  61. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/MediaDeviceSalts +0 -0
  62. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/MediaDeviceSalts-journal +0 -0
  63. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Network/Network Persistent State +1 -1
  64. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Network/TransportSecurity +1 -0
  65. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Preferences +1 -1
  66. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Service Worker/CacheStorage/14e849fa8522d406112ea607cf7fd6342b71b987/249ee9af-c3df-4a86-89a8-2c51f3370ee0/index +0 -0
  67. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Service Worker/CacheStorage/14e849fa8522d406112ea607cf7fd6342b71b987/249ee9af-c3df-4a86-89a8-2c51f3370ee0/index-dir/the-real-index +0 -0
  68. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Service Worker/CacheStorage/14e849fa8522d406112ea607cf7fd6342b71b987/index.txt +0 -0
  69. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Service Worker/Database/000003.log +0 -0
  70. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Service Worker/Database/CURRENT +1 -0
  71. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Service Worker/Database/LOCK +0 -0
  72. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Service Worker/Database/LOG +3 -0
  73. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Service Worker/Database/LOG.old +3 -0
  74. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Service Worker/Database/MANIFEST-000001 +0 -0
  75. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Service Worker/ScriptCache/2cc80dabc69f58b6_0 +0 -0
  76. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Service Worker/ScriptCache/2cc80dabc69f58b6_1 +0 -0
  77. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Service Worker/ScriptCache/index +0 -0
  78. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Service Worker/ScriptCache/index-dir/the-real-index +0 -0
  79. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Session Storage/000003.log +0 -0
  80. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Session Storage/LOG +3 -3
  81. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Session Storage/LOG.old +3 -3
  82. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Site Characteristics Database/000003.log +0 -0
  83. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Site Characteristics Database/LOG +3 -3
  84. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Site Characteristics Database/LOG.old +3 -3
  85. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Sync Data/LevelDB/LOG +3 -3
  86. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/Sync Data/LevelDB/LOG.old +3 -3
  87. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/WebStorage/QuotaManager +0 -0
  88. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/WebStorage/QuotaManager-journal +0 -0
  89. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/favorites_diagnostic.log +27 -0
  90. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/shared_proto_db/000003.log +0 -0
  91. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/shared_proto_db/LOG +3 -3
  92. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/shared_proto_db/LOG.old +3 -3
  93. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/shared_proto_db/metadata/000003.log +0 -0
  94. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/shared_proto_db/metadata/LOG +3 -3
  95. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Default/shared_proto_db/metadata/LOG.old +3 -3
  96. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/GrShaderCache/data_0 +0 -0
  97. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/GrShaderCache/data_1 +0 -0
  98. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/GrShaderCache/data_3 +0 -0
  99. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/GrShaderCache/f_000003 +0 -0
  100. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/GrShaderCache/f_000004 +0 -0
  101. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/GrShaderCache/index +0 -0
  102. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/GraphiteDawnCache/data_1 +0 -0
  103. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/GraphiteDawnCache/index +0 -0
  104. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/Local State +1 -1
  105. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/RevisitationBloomfilter +0 -0
  106. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/ShaderCache/data_1 +0 -0
  107. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/ShaderCache/index +0 -0
  108. package/msger-native/src/main.rs +343 -37
  109. package/msger-native/src/template.html +103 -28
  110. package/msger-storage-demo.html +290 -0
  111. package/msger.code-workspace +3 -0
  112. package/msgerdefs/README.md +122 -0
  113. package/msgerdefs/msgerdefs.d.ts +322 -0
  114. package/msgerdefs/msgerdefs.d.ts.map +1 -0
  115. package/msgerdefs/msgerdefs.js +110 -0
  116. package/msgerdefs/msgerdefs.js.map +1 -0
  117. package/msgerdefs/msgerdefs.ts +427 -0
  118. package/msgerdefs/package.json +38 -0
  119. package/msgerdefs/samples.html +431 -0
  120. package/msgerdefs/test1.cmd +1 -0
  121. package/msgerdefs/tsconfig.json +17 -0
  122. package/msgernative-linux-x64 +0 -0
  123. package/package.json +5 -1
  124. package/shower.d.ts +2 -0
  125. package/shower.d.ts.map +1 -1
  126. package/shower.js +17 -0
  127. package/shower.js.map +1 -1
  128. package/shower.ts +24 -0
  129. package/test-data-persistence.html +315 -0
  130. package/test-htmlfrom.html +29 -0
  131. package/test-ipc-reach.html +113 -0
  132. package/test-msger-api.html +120 -0
  133. package/test-msger-functions.html +325 -0
  134. package/msger-native/bin/msgernative.exe.WebView2/EBWebView/BrowserMetrics/BrowserMetrics-69055419-C8A0.pma +0 -0
@@ -0,0 +1,626 @@
1
+ // Voice Controller Module - Handles wake word detection and speech recognition
2
+ // @ts-ignore - Module resolution for browser ES modules
3
+ import { TensorFlowWakeWord, TENSORFLOW_WAKE_WORD_CONFIGS } from './tensorflow-wake-word-simple.js';
4
+ export class VoiceController {
5
+ constructor(config, events = {}) {
6
+ this.isListening = false;
7
+ this.speechRecognition = null;
8
+ this.tensorflowModel = null;
9
+ this.audioContext = null;
10
+ this.mediaStream = null;
11
+ this.isInitialized = false;
12
+ this.currentState = 'idle';
13
+ this.wakeWordBuffer = [];
14
+ this.speechBuffer = [];
15
+ this.lastCommandTime = 0;
16
+ this.commandCooldown = 2000;
17
+ this.selectedMicrophoneId = null;
18
+ this.lastWakePhrase = '';
19
+ this.wakePhraseTimeout = 0;
20
+ this.consecutiveErrors = 0;
21
+ this.lastErrorType = '';
22
+ this.backoffDelay = 0;
23
+ this.maxConsecutiveErrors = 5;
24
+ this.isMicrophoneDead = false;
25
+ // Use null coalescing for cleaner backward compatibility
26
+ this.config = {
27
+ ...config,
28
+ timeoutMs: config.timeoutMs ?? config.commandTimeoutMs ?? 5000,
29
+ useTensorFlow: config.useTensorFlow ?? config.enableTensorFlow ?? false,
30
+ useWebSpeech: config.useWebSpeech ?? config.enableWebSpeech ?? true,
31
+ wakeWords: config.wakeWords ?? (config.wakeWord ? [config.wakeWord] : ['Computer']),
32
+ tabletMode: config.tabletMode ?? this.detectTablet()
33
+ };
34
+ this.events = events;
35
+ this.selectedMicrophoneId = localStorage.getItem('wallclock-selected-microphone');
36
+ }
37
+ detectTablet() {
38
+ const userAgent = navigator.userAgent.toLowerCase();
39
+ const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
40
+ const isTabletUA = /tablet|ipad|android(?!.*mobile)/i.test(userAgent);
41
+ const isSmallScreen = window.innerWidth <= 1024; // Typical tablet breakpoint
42
+ return isTouchDevice && (isTabletUA || isSmallScreen);
43
+ }
44
+ isNativeWakePhrase(transcript) {
45
+ // Only match complete wake phrases to avoid false matches
46
+ const wakePatterns = [
47
+ /\b(go|hey)\s+(timer|time)\b/i, // "go timer" or "hey timer" ONLY
48
+ /\b(go|hey)\s*timer\b/i, // Handle missing space: "gotimer" or "heytimer"
49
+ /\b(go|hey)\s+(timer|time)r?\b/i, // Handle "timers" or slight variations
50
+ ];
51
+ const isMatch = wakePatterns.some(pattern => pattern.test(transcript));
52
+ console.log(`[VoiceController] Wake phrase check: "${transcript}" -> ${isMatch ? 'MATCHED' : 'NO MATCH'}`);
53
+ return isMatch;
54
+ }
55
+ async initialize() {
56
+ try {
57
+ this.log('Initializing voice controller...');
58
+ this.log(`Tablet mode: ${this.config.tabletMode}`);
59
+ this.log(`Using TensorFlow: ${this.config.useTensorFlow}`);
60
+ // For non-tablets, prefer native speech recognition
61
+ if (!this.config.tabletMode) {
62
+ this.config.useTensorFlow = false;
63
+ this.config.useWebSpeech = true;
64
+ this.log('Desktop detected: Using native speech recognition');
65
+ }
66
+ // Request microphone permission first
67
+ await this.requestMicrophonePermission();
68
+ // Initialize audio context
69
+ await this.initializeAudioContext();
70
+ // Initialize recognition engines based on config
71
+ if (this.config.useTensorFlow) {
72
+ await this.initializeTensorFlow();
73
+ }
74
+ if (this.config.useWebSpeech) {
75
+ this.initializeWebSpeech();
76
+ }
77
+ this.isInitialized = true;
78
+ this.events.statusChanged?.('Initialized successfully');
79
+ this.log('Voice controller initialized successfully');
80
+ }
81
+ catch (error) {
82
+ this.log(`Failed to initialize voice controller: ${error.message}`);
83
+ this.events.error?.(`Initialization failed: ${error.message}`);
84
+ throw error;
85
+ }
86
+ }
87
+ async getAvailableMicrophones() {
88
+ try {
89
+ this.log('Getting available microphones...');
90
+ const devices = await navigator.mediaDevices.enumerateDevices();
91
+ this.log(`Total devices found: ${devices.length}`);
92
+ const microphones = devices
93
+ .filter(device => device.kind === 'audioinput')
94
+ .map(device => ({
95
+ deviceId: device.deviceId,
96
+ label: device.label || `Microphone ${device.deviceId.substr(0, 8)}`
97
+ }));
98
+ this.log(`Found ${microphones.length} microphones:`);
99
+ microphones.forEach(mic => {
100
+ this.log(` - ${mic.label} (${mic.deviceId})`);
101
+ });
102
+ return microphones;
103
+ }
104
+ catch (error) {
105
+ this.log(`Failed to enumerate microphones: ${error.message}`);
106
+ console.error('[VOICE] Microphone enumeration error:', error);
107
+ return [];
108
+ }
109
+ }
110
+ setSelectedMicrophone(deviceId) {
111
+ this.selectedMicrophoneId = deviceId;
112
+ if (deviceId) {
113
+ localStorage.setItem('wallclock-selected-microphone', deviceId);
114
+ }
115
+ else {
116
+ localStorage.removeItem('wallclock-selected-microphone');
117
+ }
118
+ this.log(`Selected microphone: ${deviceId || 'default'}`);
119
+ }
120
+ getMicrophoneConstraints() {
121
+ const constraints = {
122
+ echoCancellation: true,
123
+ noiseSuppression: true,
124
+ autoGainControl: true,
125
+ sampleRate: this.config.mobileOptimized ? 16000 : 44100,
126
+ channelCount: 1
127
+ };
128
+ // Use optional chaining
129
+ if (this.selectedMicrophoneId) {
130
+ constraints.deviceId = { exact: this.selectedMicrophoneId };
131
+ }
132
+ return constraints;
133
+ }
134
+ async requestMicrophonePermission() {
135
+ try {
136
+ const constraints = {
137
+ audio: this.getMicrophoneConstraints()
138
+ };
139
+ this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
140
+ this.log('Microphone permission granted');
141
+ }
142
+ catch (error) {
143
+ if (error.name === 'NotAllowedError') {
144
+ throw new Error('Microphone permission denied');
145
+ }
146
+ else if (error.name === 'NotFoundError') {
147
+ throw new Error('No microphone found');
148
+ }
149
+ else if (error.name === 'OverconstrainedError') {
150
+ // Retry with default device if selected device fails
151
+ if (this.selectedMicrophoneId) {
152
+ this.log('Selected microphone failed, trying default...');
153
+ this.setSelectedMicrophone('');
154
+ return this.requestMicrophonePermission();
155
+ }
156
+ throw new Error('Microphone constraints not supported');
157
+ }
158
+ else {
159
+ throw new Error(`Microphone access failed: ${error.message}`);
160
+ }
161
+ }
162
+ }
163
+ async initializeAudioContext() {
164
+ try {
165
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
166
+ if (this.audioContext.state === 'suspended') {
167
+ await this.audioContext.resume();
168
+ }
169
+ this.log('Audio context initialized');
170
+ }
171
+ catch (error) {
172
+ throw new Error(`Failed to initialize audio context: ${error.message}`);
173
+ }
174
+ }
175
+ async initializeTensorFlow() {
176
+ try {
177
+ this.log('Initializing TensorFlow.js wake word detection...');
178
+ const tfConfig = this.config.tensorFlowConfig ?? TENSORFLOW_WAKE_WORD_CONFIGS.GO_WAKE_WORD;
179
+ const tfEvents = {
180
+ onWakeWordDetected: (word, confidence) => {
181
+ this.log(`🎯 TensorFlow wake word detected: "${word}" (${confidence.toFixed(3)})`);
182
+ this.events.wakeWordDetected?.();
183
+ },
184
+ onListeningStarted: () => {
185
+ this.log('TensorFlow wake word listening started');
186
+ },
187
+ onListeningStopped: () => {
188
+ this.log('TensorFlow wake word listening stopped');
189
+ },
190
+ onError: (error) => {
191
+ this.log(`TensorFlow error: ${error.message}`);
192
+ this.events.error?.(error.message);
193
+ },
194
+ onLog: (message) => {
195
+ this.events.onLog?.(message);
196
+ }
197
+ };
198
+ this.tensorflowModel = new TensorFlowWakeWord(tfConfig, tfEvents);
199
+ await this.tensorflowModel.initialize();
200
+ await this.tensorflowModel.startListening();
201
+ this.log('TensorFlow wake word detection active');
202
+ }
203
+ catch (error) {
204
+ this.log(`TensorFlow initialization failed: ${error.message ?? error}`);
205
+ this.log(`TensorFlow error type: ${error.constructor.name}`);
206
+ this.events.error?.(error);
207
+ }
208
+ }
209
+ handleSpeechResult(event) {
210
+ console.log('[VoiceController] handleSpeechResult called', event);
211
+ console.log(`[VoiceController] Current state: ${this.currentState}`);
212
+ if (!event.results || event.results.length === 0) {
213
+ console.log('[VoiceController] No results in event');
214
+ this.log('Speech result received but no results available');
215
+ return;
216
+ }
217
+ const result = event.results[event.results.length - 1];
218
+ console.log('[VoiceController] Result:', result);
219
+ if (!result || result.length === 0) {
220
+ console.log('[VoiceController] No alternatives in result');
221
+ this.log('Speech result received but no alternatives available');
222
+ return;
223
+ }
224
+ // Web Speech API structure: result[0] contains the transcript and confidence
225
+ const transcript = result[0].transcript?.trim().toLowerCase() || '';
226
+ const confidence = result[0].confidence ?? 1.0; // Default to 1.0 if not provided
227
+ console.log(`[VoiceController] Transcript: "${transcript}", Confidence: ${confidence}`);
228
+ console.log(`[VoiceController] Is final: ${result.isFinal}`);
229
+ if (transcript) {
230
+ this.log(`Speech result: "${transcript}" (confidence: ${confidence.toFixed(2)}, final: ${result.isFinal})`);
231
+ }
232
+ // For command listening, wait longer before considering a result "final" to allow compound phrases
233
+ if (this.currentState === 'command-listening') {
234
+ // Only process truly final results during command listening to avoid cutting off compound phrases
235
+ if (!result.isFinal) {
236
+ return; // Wait for the final result
237
+ }
238
+ }
239
+ if (result.isFinal && confidence >= this.config.confidenceThreshold) {
240
+ console.log(`[VoiceController] Processing final result, currentState: ${this.currentState}`);
241
+ // Native wake phrase detection (desktop mode)
242
+ if (!this.config.tabletMode) {
243
+ // If we're already in command-listening mode, treat everything as a command
244
+ if (this.currentState === 'command-listening') {
245
+ // Check if this is the same wake phrase we just detected (ignore it)
246
+ if (transcript === this.lastWakePhrase && Date.now() < this.wakePhraseTimeout) {
247
+ console.log(`[VoiceController] Ignoring repeated wake phrase in command mode: "${transcript}"`);
248
+ return;
249
+ }
250
+ console.log(`[VoiceController] In command-listening mode, sending as command: "${transcript}"`);
251
+ this.events.commandReceived?.(transcript, confidence);
252
+ return;
253
+ }
254
+ // Otherwise check for wake phrases
255
+ if (this.isNativeWakePhrase(transcript)) {
256
+ console.log(`[VoiceController] Native wake phrase detected: "${transcript}"`);
257
+ // Check if it's just a wake phrase or includes command
258
+ const hasCommand = /\b(go|hey)\s*(?:timer|time)s?\s+(?:\d|\w+)/i.test(transcript);
259
+ if (hasCommand) {
260
+ console.log('[VoiceController] Complete command detected, sending to commandReceived');
261
+ // Complete command in one phrase
262
+ this.events.commandReceived?.(transcript, confidence);
263
+ }
264
+ else {
265
+ console.log('[VoiceController] Just wake phrase detected, triggering wakeWordDetected');
266
+ // Store the wake phrase and set timeout to ignore it if it comes through again
267
+ this.lastWakePhrase = transcript;
268
+ this.wakePhraseTimeout = Date.now() + 2000; // Ignore for 2 seconds
269
+ // Just wake phrase, trigger wake word detection
270
+ this.events.wakeWordDetected?.();
271
+ }
272
+ return;
273
+ }
274
+ }
275
+ else {
276
+ // Traditional two-stage mode (tablet mode)
277
+ // If we're in command-listening mode, send the transcript as a command
278
+ if (this.currentState === 'command-listening') {
279
+ console.log(`[VoiceController] Sending command: "${transcript}"`);
280
+ this.events.commandReceived?.(transcript, confidence);
281
+ }
282
+ else {
283
+ // Check for wake words only if not in command-listening mode
284
+ const containsWakeWord = this.config.wakeWords.some(word => transcript.includes(word.toLowerCase()));
285
+ if (containsWakeWord) {
286
+ this.events.wakeWordDetected?.();
287
+ const command = this.extractCommand(transcript);
288
+ if (command) {
289
+ this.events.commandReceived?.(command, confidence);
290
+ }
291
+ }
292
+ }
293
+ }
294
+ }
295
+ }
296
+ destroy() {
297
+ this.stopListening();
298
+ this.mediaStream?.getTracks().forEach(track => track.stop());
299
+ this.mediaStream = null;
300
+ if (this.audioContext?.state !== 'closed') {
301
+ this.audioContext?.close();
302
+ this.audioContext = null;
303
+ }
304
+ this.speechRecognition = null;
305
+ this.tensorflowModel = null;
306
+ this.isInitialized = false;
307
+ this.log('Voice controller destroyed');
308
+ }
309
+ getWakeWordLabel() {
310
+ return this.config.wakeWords?.[0] ?? 'Computer';
311
+ }
312
+ isWebSpeechListening() {
313
+ return this.isListening && this.speechRecognition !== null;
314
+ }
315
+ // Add missing methods that clock.ts expects
316
+ getSelectedMicrophoneId() {
317
+ return this.selectedMicrophoneId ?? '';
318
+ }
319
+ hasActiveWakeWordDetection() {
320
+ return this.isListening && this.currentState === 'wake-listening';
321
+ }
322
+ startCommandListening() {
323
+ console.log('[VoiceController] startCommandListening called');
324
+ console.log('[VoiceController] Previous state:', this.currentState);
325
+ console.log('[VoiceController] isListening:', this.isListening);
326
+ console.log('[VoiceController] speechRecognition exists:', !!this.speechRecognition);
327
+ this.currentState = 'command-listening';
328
+ console.log('[VoiceController] Switched to command-listening mode');
329
+ if (!this.isListening && this.speechRecognition) {
330
+ // Speech recognition not running - start it
331
+ try {
332
+ this.isListening = true;
333
+ this.speechRecognition.start();
334
+ this.log('Started listening for commands');
335
+ console.log('[VoiceController] Speech recognition started for commands');
336
+ }
337
+ catch (error) {
338
+ this.log(`Failed to start command listening: ${error.message}`);
339
+ console.error('[VoiceController] Failed to start speech recognition:', error);
340
+ }
341
+ }
342
+ else if (this.isListening) {
343
+ // Speech recognition already running from wake word detection
344
+ // Just fire the listeningStarted event since we're now in command mode
345
+ this.log('Speech recognition already running, switching to command mode');
346
+ console.log('[VoiceController] Speech recognition already active, firing listeningStarted event');
347
+ this.events.listeningStarted?.();
348
+ }
349
+ else {
350
+ console.log('[VoiceController] No speech recognition available');
351
+ }
352
+ }
353
+ shutdown() {
354
+ this.destroy();
355
+ }
356
+ async logAvailableMicrophones() {
357
+ const microphones = await this.getAvailableMicrophones();
358
+ this.log('Available microphones:');
359
+ microphones.forEach((mic, index) => {
360
+ const selected = mic.deviceId === this.selectedMicrophoneId ? ' (SELECTED)' : '';
361
+ this.log(` ${index + 1}. ${mic.label}${selected}`);
362
+ });
363
+ }
364
+ // Add missing Web Speech initialization and handlers
365
+ initializeWebSpeech() {
366
+ if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
367
+ this.log('Web Speech API not supported');
368
+ return;
369
+ }
370
+ const SpeechRecognition = window.SpeechRecognition ?? window.webkitSpeechRecognition;
371
+ this.speechRecognition = new SpeechRecognition();
372
+ // Configure for native wake phrases (no TensorFlow needed)
373
+ if (!this.config.tabletMode) {
374
+ this.speechRecognition.continuous = true; // Always listening for wake phrases
375
+ this.speechRecognition.interimResults = true; // Enable interim results for better responsiveness
376
+ this.speechRecognition.maxAlternatives = 3; // More alternatives for better matching
377
+ this.speechRecognition.lang = 'en-US';
378
+ this.log('Configured for native wake phrase detection');
379
+ }
380
+ else {
381
+ // Traditional two-stage mode for tablets
382
+ this.speechRecognition.continuous = false;
383
+ this.speechRecognition.interimResults = true;
384
+ this.speechRecognition.maxAlternatives = 1;
385
+ this.speechRecognition.lang = 'en-US';
386
+ this.log('Configured for two-stage recognition');
387
+ }
388
+ this.speechRecognition.onstart = () => {
389
+ this.log(`Speech recognition started - currentState: ${this.currentState}`);
390
+ // Only fire listeningStarted event when in command-listening mode
391
+ // Don't fire it when just waiting for wake words
392
+ if (this.currentState === 'command-listening') {
393
+ this.log('Firing listeningStarted event');
394
+ this.events.listeningStarted?.();
395
+ }
396
+ else {
397
+ this.log('Not firing listeningStarted - state is not command-listening');
398
+ }
399
+ };
400
+ this.speechRecognition.onresult = (event) => {
401
+ this.handleSpeechResult(event);
402
+ };
403
+ this.speechRecognition.onerror = (event) => {
404
+ this.handleSpeechError(event);
405
+ };
406
+ this.speechRecognition.onend = () => {
407
+ const willAutoRestart = this.currentState === 'wake-listening' && !this.isMicrophoneDead;
408
+ if (willAutoRestart) {
409
+ // Don't fire listeningEnded or log for natural restarts - this is normal behavior
410
+ this.isListening = false;
411
+ // Apply backoff delay if we have consecutive errors
412
+ const restartDelay = this.backoffDelay > 0 ? this.backoffDelay : 100;
413
+ setTimeout(() => {
414
+ if (!this.isListening && this.currentState === 'wake-listening' && !this.isMicrophoneDead) {
415
+ this.startListening();
416
+ }
417
+ }, restartDelay);
418
+ }
419
+ else {
420
+ // Only fire listeningEnded when actually stopping (not auto-restarting)
421
+ this.log(`Speech recognition ended - currentState: ${this.currentState}, micDead: ${this.isMicrophoneDead}`);
422
+ this.events.listeningEnded?.();
423
+ this.isListening = false;
424
+ }
425
+ };
426
+ this.log('Web Speech API initialized');
427
+ }
428
+ extractCommand(transcript) {
429
+ let command = transcript;
430
+ this.config.wakeWords.forEach(wakeWord => {
431
+ const regex = new RegExp(wakeWord.toLowerCase(), 'gi');
432
+ command = command.replace(regex, '').trim();
433
+ });
434
+ return command;
435
+ }
436
+ handleSpeechError(event) {
437
+ const error = event.error;
438
+ // Track consecutive errors for backoff
439
+ if (error === this.lastErrorType) {
440
+ this.consecutiveErrors++;
441
+ }
442
+ else {
443
+ this.consecutiveErrors = 1;
444
+ this.lastErrorType = error;
445
+ }
446
+ // Log error with consecutive count
447
+ this.log(`Speech recognition error: ${error} (consecutive: ${this.consecutiveErrors})`);
448
+ // Check for microphone death - "aborted" errors in rapid succession
449
+ if (error === 'aborted' && this.consecutiveErrors >= this.maxConsecutiveErrors) {
450
+ this.isMicrophoneDead = true;
451
+ this.isListening = false;
452
+ this.events.error?.('Microphone unavailable - too many consecutive errors. Please check system permissions.');
453
+ this.log('MICROPHONE DEAD: Stopping restart attempts');
454
+ return;
455
+ }
456
+ // Calculate exponential backoff delay
457
+ if (this.consecutiveErrors > 1) {
458
+ this.backoffDelay = Math.min(1000 * Math.pow(2, this.consecutiveErrors - 1), 30000); // Max 30 seconds
459
+ this.log(`Applying backoff delay: ${this.backoffDelay}ms`);
460
+ }
461
+ else {
462
+ this.backoffDelay = 0;
463
+ }
464
+ if (error === 'no-speech') {
465
+ this.log('No speech detected');
466
+ this.consecutiveErrors = 0; // Reset on benign errors
467
+ }
468
+ else if (error === 'network') {
469
+ this.events.error?.('Network error during speech recognition');
470
+ }
471
+ else if (error === 'not-allowed') {
472
+ this.isMicrophoneDead = true;
473
+ this.isListening = false;
474
+ this.events.error?.('Microphone permission denied');
475
+ }
476
+ else {
477
+ this.events.error?.(`Speech recognition error: ${error}`);
478
+ }
479
+ }
480
+ async startListening() {
481
+ if (!this.isInitialized) {
482
+ throw new Error('Voice controller not initialized');
483
+ }
484
+ // Don't restart if microphone is dead
485
+ if (this.isMicrophoneDead) {
486
+ this.log('Microphone is dead - not attempting to restart');
487
+ return;
488
+ }
489
+ if (this.isListening) {
490
+ this.log('Already listening');
491
+ return;
492
+ }
493
+ try {
494
+ this.isListening = true;
495
+ this.currentState = 'wake-listening';
496
+ this.speechRecognition?.start();
497
+ // Reset error tracking on successful start
498
+ this.consecutiveErrors = 0;
499
+ this.lastErrorType = '';
500
+ this.backoffDelay = 0;
501
+ this.log('Started listening for wake words');
502
+ this.events.statusChanged?.('Listening for wake words...');
503
+ }
504
+ catch (error) {
505
+ this.isListening = false;
506
+ this.log(`Failed to start listening: ${error.message}`);
507
+ this.events.error?.(`Failed to start listening: ${error.message}`);
508
+ throw error;
509
+ }
510
+ }
511
+ stopListening() {
512
+ if (!this.isListening) {
513
+ return;
514
+ }
515
+ this.isListening = false;
516
+ this.currentState = 'idle';
517
+ this.speechRecognition?.stop();
518
+ this.log('Stopped listening');
519
+ this.events.statusChanged?.('Stopped listening');
520
+ }
521
+ updateConfig(newConfig) {
522
+ this.config = { ...this.config, ...newConfig };
523
+ this.log('Configuration updated');
524
+ }
525
+ getState() {
526
+ return this.currentState;
527
+ }
528
+ isActive() {
529
+ return this.isListening;
530
+ }
531
+ log(message) {
532
+ const logMessage = typeof message === 'string' ? message : message.message;
533
+ if (this.config.debugMode) {
534
+ console.log(`[VoiceController] ${logMessage}`);
535
+ }
536
+ this.events.onLog?.(logMessage);
537
+ }
538
+ checkBrowserCompatibility() {
539
+ try {
540
+ // Check for WebAssembly support
541
+ if (typeof WebAssembly === 'undefined') {
542
+ this.log('WebAssembly not supported');
543
+ return false;
544
+ }
545
+ // Check for Web Workers support
546
+ if (typeof Worker === 'undefined') {
547
+ this.log('Web Workers not supported');
548
+ return false;
549
+ }
550
+ // Check for Promises support
551
+ if (typeof Promise === 'undefined') {
552
+ this.log('Promises not supported');
553
+ return false;
554
+ }
555
+ // Check for Web Audio API
556
+ if (typeof window !== 'undefined' && !window.AudioContext && !window.webkitAudioContext) {
557
+ this.log('Web Audio API not supported');
558
+ return false;
559
+ }
560
+ // Check for navigator and getUserMedia
561
+ if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
562
+ this.log('MediaDevices API not supported');
563
+ return false;
564
+ }
565
+ this.log('Browser compatibility check passed');
566
+ return true;
567
+ }
568
+ catch (error) {
569
+ this.log(`Compatibility check failed: ${error.message}`);
570
+ return false;
571
+ }
572
+ }
573
+ }
574
+ // Default configurations for easy setup
575
+ export const DEFAULT_VOICE_CONFIG = {
576
+ accessKey: "uurGHRfD6i555eNVTcSIAi6ciiYgq9EiELQMzgCckAA0XU3DiE3aww==",
577
+ wakeWords: ['Computer'],
578
+ timeoutMs: 15000,
579
+ useWebSpeech: true,
580
+ useTensorFlow: false, // Disabled by default - use native approach
581
+ enableAIFallback: false,
582
+ aiService: 'openai',
583
+ confidenceThreshold: 0.0,
584
+ mobileOptimized: true,
585
+ tabletMode: false, // Auto-detected in constructor
586
+ debugMode: true
587
+ };
588
+ // Alternative configurations for experimentation
589
+ export const VOICE_CONFIGS = {
590
+ TENSORFLOW_ONLY: {
591
+ ...DEFAULT_VOICE_CONFIG,
592
+ useTensorFlow: true,
593
+ useWebSpeech: true,
594
+ wakeWords: ['go'],
595
+ tensorFlowConfig: TENSORFLOW_WAKE_WORD_CONFIGS.GO_OPTIMIZED
596
+ },
597
+ // Tablet-optimized configuration for better mobile device performance
598
+ TENSORFLOW_TABLET: {
599
+ ...DEFAULT_VOICE_CONFIG,
600
+ useTensorFlow: true,
601
+ useWebSpeech: true,
602
+ wakeWords: ['go'],
603
+ timeoutMs: 20000, // Longer timeout for tablets (20 seconds vs default 15)
604
+ tensorFlowConfig: TENSORFLOW_WAKE_WORD_CONFIGS.TABLET_OPTIMIZED
605
+ },
606
+ TENSORFLOW_GO_STOP: {
607
+ ...DEFAULT_VOICE_CONFIG,
608
+ useTensorFlow: true,
609
+ useWebSpeech: true,
610
+ wakeWords: ['go', 'stop'],
611
+ tensorFlowConfig: TENSORFLOW_WAKE_WORD_CONFIGS.GO_STOP_WAKE_WORDS
612
+ },
613
+ WEB_SPEECH_ONLY: {
614
+ ...DEFAULT_VOICE_CONFIG,
615
+ wakeWords: ['timer'], // Will match "hey timer" or "go timer"
616
+ useTensorFlow: false
617
+ },
618
+ TENSORFLOW_NUMBERS: {
619
+ ...DEFAULT_VOICE_CONFIG,
620
+ useTensorFlow: true,
621
+ useWebSpeech: true,
622
+ wakeWords: ['three', 'five'],
623
+ tensorFlowConfig: TENSORFLOW_WAKE_WORD_CONFIGS.NUMBER_WAKE_WORDS
624
+ }
625
+ };
626
+ //# sourceMappingURL=voice-controller.js.map