STT continued
Dialogue mode and state managed for STT
frdel committed
Nov 10, 2024 at 20:57 UTC
1a91334a420f605101e6aecdbd34f1dca33c192b
6 files changed
+757
-259
webui/index.css
-29
@@ -907,35 +907,6 @@ pre {
907
background-color: var(--color-primary-light);
908
}
909
910
-/* MIC BUTTON */
911
-#microphone-button {
912
- background-color: #3270e2;
913
-}
914
-
915
-#microphone-button:hover {
916
- background-color: #4382e8;
917
-}
918
-#microphone-button.recording {
919
- background-color: #ff4136; /* Red color for recording */
920
- transition: background-color 0.3s ease;
921
-}
922
-
923
-.mic-pulse {
924
- animation: pulse 1.5s infinite;
925
-}
926
-
927
-@keyframes pulse {
928
- 0% {
929
- transform: scale(1);
930
- }
931
- 50% {
932
- transform: scale(1.1);
933
- }
934
- 100% {
935
- transform: scale(1);
936
- }
937
-}
938
-
910
.chat-button svg {
911
width: 1.5rem;
912
height: 1.5rem;
webui/index.html
+2
-1
@@ -8,6 +8,7 @@
8
<link rel="stylesheet" href="index.css">
9
<link rel="stylesheet" href="toast.css">
10
<link rel="stylesheet" href="settings.css">
11
+ <link rel="stylesheet" href="speech.css">
12
13
<script>
14
window.safeCall = function (name, ...args) {
@@ -236,7 +237,7 @@
237
</button>
238
239
<!-- Microphone button -->
239
- <button class="chat-button" id="microphone-button" aria-label="Start/Stop recording">
240
+ <button class="chat-button mic-inactive" id="microphone-button" aria-label="Start/Stop recording">
241
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 18" fill="currentColor" width="24" height="24">
242
<path d="m8,12c1.66,0,3-1.34,3-3V3c0-1.66-1.34-3-3-3s-3,1.34-3,3v6c0,1.66,1.34,3,3,3Zm-1,1.9c-2.7-.4-4.8-2.6-5-5.4H0c.2,3.8,3.1,6.9,7,7.5v2h2v-2c3.9-.6,6.8-3.7,7-7.5h-2c-.2,2.8-2.3,5-5,5.4h-2Z"/>
243
</svg>
webui/speech copy.js
new
+337
@@ -0,0 +1,337 @@
1
+import { pipeline, read_audio } from './transformers@3.0.2.js';
2
+import { updateChatInput, sendMessage } from './index.js';
3
+
4
+const microphoneButton = document.getElementById('microphone-button');
5
+let microphoneInput = null;
6
+let isProcessingClick = false;
7
+
8
+class MicrophoneInput {
9
+ /**
10
+ * Voice Input Handler with Whisper Transcription
11
+ *
12
+ * Whisper Model Size Configuration:
13
+ * - 'tiny': Smallest model, fastest, lowest accuracy (~32MB)
14
+ * - Best for: Quick prototyping, low-resource environments
15
+ * - Pros: Very fast, low memory usage
16
+ * - Cons: Lowest transcription accuracy
17
+ *
18
+ * - 'base': Small model, good balance of speed and accuracy (~74MB)
19
+ * - Best for: General-purpose voice input
20
+ * - Pros: Reasonable accuracy, moderate resource usage
21
+ * - Cons: Less accurate than larger models
22
+ *
23
+ * - 'small': Medium-sized model, better accuracy (~244MB)
24
+ * - Best for: More precise transcription needs
25
+ * - Pros: Improved accuracy over base model
26
+ * - Cons: Slower, more memory-intensive
27
+ *
28
+ * - 'medium': Large model with high accuracy (~769MB)
29
+ * - Best for: Professional transcription, multi-language support
30
+ * - Pros: Very high accuracy
31
+ * - Cons: Significant computational resources required
32
+ *
33
+ * - 'large': Largest model, highest accuracy (~1.5GB)
34
+ * - Best for: Professional, multi-language transcription
35
+ * - Pros: Highest possible accuracy
36
+ * - Cons: Slowest, most resource-intensive
37
+ *
38
+ * Recommended Default: 'base' for most web applications
39
+ */
40
+ constructor(updateCallback, options = {}) {
41
+ this.mediaRecorder = null;
42
+ this.audioChunks = [];
43
+ this.isRecording = false;
44
+ this.updateCallback = updateCallback;
45
+ this.isFinalizing = false;
46
+ this.messageSent = false; // move messageSent into class
47
+
48
+ // New properties for silence detection
49
+ this.audioContext = null;
50
+ this.mediaStreamSource = null;
51
+ this.analyserNode = null;
52
+ this.silenceTimer = null;
53
+ this.silenceThreshold = options.silenceThreshold || 0.01; // Adjust as needed
54
+ this.silenceDuration = options.silenceDuration || 2000; // Duration in milliseconds
55
+
56
+ this.options = {
57
+ modelSize: 'tiny',
58
+ language: 'en',
59
+ chunkDuration: 3000,
60
+ ...options
61
+ };
62
+ }
63
+
64
+ async initialize() {
65
+ try {
66
+
67
+ this.transcriber = await pipeline(`automatic-speech-recognition`, `Xenova/whisper-${this.options.modelSize}.${this.options.language}`);
68
+
69
+ const stream = await navigator.mediaDevices.getUserMedia({
70
+ audio: {
71
+ echoCancellation: true,
72
+ noiseSuppression: true,
73
+ channelCount: 1
74
+ }
75
+ });
76
+
77
+ // Configure MediaRecorder
78
+ this.mediaRecorder = new MediaRecorder(stream);
79
+
80
+ // Handle audio data availability
81
+ this.mediaRecorder.ondataavailable = async (event) => {
82
+ if (event.data.size > 0) {
83
+ this.audioChunks.push(event.data);
84
+ // await this.processAudioChunk(event.data);
85
+ }
86
+ };
87
+
88
+ // Handle recording stop
89
+ this.mediaRecorder.onstop = async () => {
90
+ await this.finalizeRecording();
91
+ };
92
+
93
+ // Set up AudioContext and AnalyserNode for silence detection
94
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
95
+ this.mediaStreamSource = this.audioContext.createMediaStreamSource(stream);
96
+ this.analyserNode = this.audioContext.createAnalyser();
97
+ this.analyserNode.minDecibels = -90;
98
+ this.analyserNode.maxDecibels = -10;
99
+ this.analyserNode.smoothingTimeConstant = 0.85;
100
+
101
+ this.mediaStreamSource.connect(this.analyserNode);
102
+ } catch (error) {
103
+ console.error('Microphone initialization error:', error);
104
+ toast('Failed to access microphone. Please check permissions.', 'error');
105
+ }
106
+ }
107
+
108
+ startSilenceDetection() {
109
+ const dataArray = new Uint8Array(this.analyserNode.fftSize);
110
+ const checkSilence = () => {
111
+ this.analyserNode.getByteTimeDomainData(dataArray);
112
+
113
+ let sum = 0;
114
+ for (let i = 0; i < dataArray.length; i++) {
115
+ const amplitude = (dataArray[i] - 128) / 128;
116
+ sum += amplitude * amplitude;
117
+ }
118
+ const rms = Math.sqrt(sum / dataArray.length);
119
+
120
+ if (rms < this.silenceThreshold) {
121
+ if (!this.silenceTimer) {
122
+ this.silenceTimer = setTimeout(() => {
123
+ if (this.isRecording) {
124
+ console.log('Silence detected. Stopping recording.');
125
+ this.stopRecording();
126
+ microphoneButton.classList.remove('recording');
127
+ microphoneButton.classList.remove('mic-pulse');
128
+ }
129
+ }, this.silenceDuration);
130
+ }
131
+ } else {
132
+ if (this.silenceTimer) {
133
+ clearTimeout(this.silenceTimer);
134
+ this.silenceTimer = null;
135
+ }
136
+ }
137
+
138
+ if (this.isRecording) {
139
+ requestAnimationFrame(checkSilence);
140
+ }
141
+ };
142
+
143
+ if (this.isRecording) {
144
+ requestAnimationFrame(checkSilence);
145
+ }
146
+ }
147
+
148
+ startRecording() {
149
+ if (this.mediaRecorder && this.audioContext) {
150
+ this.isRecording = true;
151
+ this.audioChunks = [];
152
+ this.messageSent = false;
153
+ this.mediaRecorder.start(this.options.chunkDuration);
154
+ this.audioContext.resume();
155
+ this.startSilenceDetection();
156
+ }
157
+ }
158
+
159
+ stopRecording() {
160
+ if (this.mediaRecorder && this.isRecording) {
161
+ this.isRecording = false;
162
+ if (!this.isFinalizing) {
163
+ this.isFinalizing = true;
164
+ this.mediaRecorder.stop();
165
+ this.audioContext.suspend();
166
+ if (this.silenceTimer) {
167
+ clearTimeout(this.silenceTimer);
168
+ this.silenceTimer = null;
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+
175
+ async finalizeRecording() {
176
+ if (this.isFinalizing) {
177
+ this.isFinalizing = false;
178
+
179
+ if (this.audioChunks.length > 0) {
180
+
181
+ const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
182
+ const audioUrl = URL.createObjectURL(audioBlob);
183
+ const samplingRate = 16000; // Adjust as needed for the model
184
+ const audioData = await read_audio(audioUrl, samplingRate);
185
+ URL.revokeObjectURL(audioUrl);
186
+
187
+ // Transcribe the audio
188
+ const result = await this.transcriber(audioData);
189
+
190
+ if (result.text) {
191
+ console.log('Final transcription received:', result.text);
192
+ await this.updateCallback(result.text, true);
193
+ } else {
194
+ console.warn('Final transcription returned empty text.');
195
+ }
196
+
197
+
198
+ // Release the object URL after use
199
+
200
+ // const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });
201
+ // this.audioChunks = []; // Clear for next recording
202
+
203
+ // const reader = new FileReader();
204
+ // reader.onloadend = async () => {
205
+ // const base64Data = reader.result.split(',')[1];
206
+
207
+ // try {
208
+ // const response = await fetch('/transcribe', {
209
+ // method: 'POST',
210
+ // headers: {
211
+ // 'Content-Type': 'application/json'
212
+ // },
213
+ // body: JSON.stringify({
214
+ // audio_data: base64Data,
215
+ // model_size: this.options.modelSize,
216
+ // language: this.options.language,
217
+ // is_final: true
218
+ // })
219
+ // });
220
+
221
+ // const result = await response.json();
222
+
223
+ // if (result.text) {
224
+ // console.log('Final transcription received:', result.text);
225
+ // await this.updateCallback(result.text, true);
226
+ // } else {
227
+ // console.warn('Final transcription returned empty text.');
228
+ // }
229
+ // } catch (transcribeError) {
230
+ // console.error('Final transcription error:', transcribeError);
231
+ // toast('Final transcription failed.', 'error');
232
+ // } finally {
233
+ // // Reset the microphone button state
234
+ // microphoneButton.classList.remove('recording');
235
+ // microphoneButton.classList.remove('mic-pulse');
236
+ // microphoneButton.style.backgroundColor = '';
237
+ // }
238
+ // };
239
+ // reader.readAsDataURL(audioBlob);
240
+ }
241
+ }
242
+ }
243
+}
244
+
245
+export default MicrophoneInput;
246
+
247
+async function initializeMicrophoneInput() {
248
+ console.log('Initializing microphone input');
249
+
250
+ microphoneInput = new MicrophoneInput(
251
+ async (text, isFinal) => {
252
+ if (isFinal) {
253
+ console.log('Final transcription callback received:', text);
254
+ updateChatInput(text)
255
+ // chatInput.value = text;
256
+ // adjustTextareaHeight();
257
+
258
+ if (!microphoneInput.messageSent) {
259
+ microphoneInput.messageSent = true;
260
+ console.log('Sending message');
261
+ await sendMessage();
262
+ }
263
+ }
264
+ },
265
+ {
266
+ modelSize: 'tiny',
267
+ language: 'en',
268
+ silenceThreshold: 0.07, // Adjust as needed
269
+ silenceDuration: 2000, // Adjust as needed
270
+ onError: (error) => {
271
+ console.error('Microphone input error:', error);
272
+ toast('Microphone error: ' + error.message, 'error');
273
+ // Reset recording state
274
+ if (microphoneButton.classList.contains('recording')) {
275
+ microphoneButton.classList.remove('recording');
276
+ }
277
+ }
278
+ }
279
+ );
280
+
281
+ await microphoneInput.initialize();
282
+}
283
+
284
+
285
+function toggleRecording() {
286
+ console.log('toggleRecording called, isRecording:', microphoneInput.isRecording);
287
+
288
+ if (microphoneInput.isRecording) {
289
+ microphoneInput.stopRecording();
290
+ microphoneButton.classList.remove('recording');
291
+ // Add pulsing animation class
292
+ microphoneButton.classList.remove('mic-pulse');
293
+ } else {
294
+ microphoneInput.startRecording();
295
+ microphoneButton.classList.add('recording');
296
+ // Add pulsing animation class
297
+ microphoneButton.classList.add('mic-pulse');
298
+ }
299
+
300
+ // Add visual feedback
301
+ microphoneButton.style.backgroundColor = microphoneInput.isRecording ? '#ff4444' : '';
302
+ console.log('New recording state:', microphoneInput.isRecording);
303
+}
304
+
305
+// Some error handling for microphone input
306
+async function requestMicrophonePermission() {
307
+ try {
308
+ await navigator.mediaDevices.getUserMedia({ audio: true });
309
+ return true;
310
+ } catch (err) {
311
+ console.error('Error accessing microphone:', err);
312
+ toast('Microphone access denied. Please enable microphone access in your browser settings.', 'error');
313
+ return false;
314
+ }
315
+}
316
+// microphoneButton click event listener modifier
317
+microphoneButton.addEventListener('click', async () => {
318
+ console.log('Microphone button clicked');
319
+ if (isProcessingClick) {
320
+ console.log('Click already being processed, ignoring');
321
+ return;
322
+ }
323
+ isProcessingClick = true;
324
+
325
+ const hasPermission = await requestMicrophonePermission();
326
+ if (!hasPermission) return;
327
+
328
+ if (!microphoneInput) {
329
+ await initializeMicrophoneInput();
330
+ }
331
+
332
+ await toggleRecording();
333
+
334
+ setTimeout(() => {
335
+ isProcessingClick = false;
336
+ }, 300); // Add a 300ms delay before allowing another click
337
+});
webui/speech.css
new
+55
@@ -0,0 +1,55 @@
1
+/* MIC BUTTON */
2
+#microphone-button {
3
+
4
+}
5
+
6
+#microphone-button:hover {
7
+}
8
+
9
+#microphone-button.recording {
10
+ background-color: #ff4136; /* Red color for recording */
11
+ transition: background-color 0.3s ease;
12
+}
13
+
14
+@keyframes pulse {
15
+ 0% {
16
+ transform: scale(1);
17
+ }
18
+ 50% {
19
+ transform: scale(1.1);
20
+ }
21
+ 100% {
22
+ transform: scale(1);
23
+ }
24
+ }
25
+
26
+.mic-pulse {
27
+ animation: pulse 1.5s infinite;
28
+}
29
+
30
+
31
+.mic-inactive{
32
+ background-color: grey;
33
+}
34
+
35
+.mic-activating{
36
+ background-color: silver;
37
+ animation: pulse 0.8s infinite;
38
+}
39
+
40
+.mic-listening {
41
+ background-color: red;
42
+}
43
+
44
+.mic-recording {
45
+ background-color: green;
46
+}
47
+
48
+.mic-waiting {
49
+ background-color: teal;
50
+}
51
+
52
+.mic-processing {
53
+ background-color: darkcyan;
54
+ animation: pulse 0.8s infinite;
55
+}
\ No newline at end of file
webui/speech.js
+241
-229
@@ -5,66 +5,149 @@ const microphoneButton = document.getElementById('microphone-button');
5
let microphoneInput = null;
6
let isProcessingClick = false;
7
8
+const Status = {
9
+ INACTIVE: 'inactive',
10
+ ACTIVATING: 'activating',
11
+ LISTENING: 'listening',
12
+ RECORDING: 'recording',
13
+ WAITING: 'waiting',
14
+ PROCESSING: 'processing'
15
+};
16
+
17
class MicrophoneInput {
9
- /**
10
- * Voice Input Handler with Whisper Transcription
11
- *
12
- * Whisper Model Size Configuration:
13
- * - 'tiny': Smallest model, fastest, lowest accuracy (~32MB)
14
- * - Best for: Quick prototyping, low-resource environments
15
- * - Pros: Very fast, low memory usage
16
- * - Cons: Lowest transcription accuracy
17
- *
18
- * - 'base': Small model, good balance of speed and accuracy (~74MB)
19
- * - Best for: General-purpose voice input
20
- * - Pros: Reasonable accuracy, moderate resource usage
21
- * - Cons: Less accurate than larger models
22
- *
23
- * - 'small': Medium-sized model, better accuracy (~244MB)
24
- * - Best for: More precise transcription needs
25
- * - Pros: Improved accuracy over base model
26
- * - Cons: Slower, more memory-intensive
27
- *
28
- * - 'medium': Large model with high accuracy (~769MB)
29
- * - Best for: Professional transcription, multi-language support
30
- * - Pros: Very high accuracy
31
- * - Cons: Significant computational resources required
32
- *
33
- * - 'large': Largest model, highest accuracy (~1.5GB)
34
- * - Best for: Professional, multi-language transcription
35
- * - Pros: Highest possible accuracy
36
- * - Cons: Slowest, most resource-intensive
37
- *
38
- * Recommended Default: 'base' for most web applications
39
- */
18
constructor(updateCallback, options = {}) {
19
this.mediaRecorder = null;
20
this.audioChunks = [];
43
- this.isRecording = false;
21
+ this.lastChunk = [];
22
this.updateCallback = updateCallback;
45
- this.isFinalizing = false;
46
- this.messageSent = false; // move messageSent into class
23
+ this.messageSent = false;
24
48
- // New properties for silence detection
25
+ // Audio analysis properties
26
this.audioContext = null;
27
this.mediaStreamSource = null;
28
this.analyserNode = null;
52
- this.silenceTimer = null;
53
- this.silenceThreshold = options.silenceThreshold || 0.01; // Adjust as needed
54
- this.silenceDuration = options.silenceDuration || 2000; // Duration in milliseconds
29
+ this._status = Status.INACTIVE;
30
+
31
+ // Timing properties
32
+ this.lastAudioTime = null;
33
+ this.waitingTimer = null;
34
+ this.silenceStartTime = null;
35
+ this.hasStartedRecording = false;
36
+ this.analysisFrame = null;
37
38
this.options = {
39
modelSize: 'tiny',
40
language: 'en',
59
- chunkDuration: 3000,
41
+ silenceThreshold: 0.15,
42
+ silenceDuration: 1000,
43
+ waitingTimeout: 2000,
44
+ minSpeechDuration: 500,
45
...options
46
};
47
}
48
49
+ get status() {
50
+ return this._status;
51
+ }
52
+
53
+ set status(newStatus) {
54
+ if (this._status === newStatus) return;
55
+
56
+ const oldStatus = this._status;
57
+ this._status = newStatus;
58
+ console.log(`Mic status changed from ${oldStatus} to ${newStatus}`);
59
+
60
+ // Update UI
61
+ microphoneButton.classList.remove(`mic-${oldStatus.toLowerCase()}`);
62
+ microphoneButton.classList.add(`mic-${newStatus.toLowerCase()}`);
63
+ microphoneButton.setAttribute('data-status', newStatus);
64
+
65
+ // Handle state-specific behaviors
66
+ this.handleStatusChange(oldStatus, newStatus);
67
+ }
68
+
69
+ handleStatusChange(oldStatus, newStatus) {
70
+
71
+ //last chunk kept only for transition to recording status
72
+ if (newStatus != Status.RECORDING) { this.lastChunk = null; }
73
+
74
+ switch (newStatus) {
75
+ case Status.INACTIVE:
76
+ this.handleInactiveState();
77
+ break;
78
+ case Status.LISTENING:
79
+ this.handleListeningState();
80
+ break;
81
+ case Status.RECORDING:
82
+ this.handleRecordingState();
83
+ break;
84
+ case Status.WAITING:
85
+ this.handleWaitingState();
86
+ break;
87
+ case Status.PROCESSING:
88
+ this.handleProcessingState();
89
+ break;
90
+ }
91
+ }
92
+
93
+ handleInactiveState() {
94
+ this.stopRecording();
95
+ this.stopAudioAnalysis();
96
+ if (this.waitingTimer) {
97
+ clearTimeout(this.waitingTimer);
98
+ this.waitingTimer = null;
99
+ }
100
+ }
101
+
102
+ handleListeningState() {
103
+ this.stopRecording();
104
+ this.audioChunks = [];
105
+ this.hasStartedRecording = false;
106
+ this.silenceStartTime = null;
107
+ this.lastAudioTime = null;
108
+ this.messageSent = false;
109
+ this.startAudioAnalysis();
110
+ }
111
+
112
+ handleRecordingState() {
113
+ if (!this.hasStartedRecording && this.mediaRecorder.state !== 'recording') {
114
+ this.hasStartedRecording = true;
115
+ this.mediaRecorder.start(1000);
116
+ console.log('Speech started');
117
+ }
118
+ if (this.waitingTimer) {
119
+ clearTimeout(this.waitingTimer);
120
+ this.waitingTimer = null;
121
+ }
122
+ }
123
+
124
+ handleWaitingState() {
125
+ // Don't stop recording during waiting state
126
+ this.waitingTimer = setTimeout(() => {
127
+ if (this.status === Status.WAITING) {
128
+ this.status = Status.PROCESSING;
129
+ }
130
+ }, this.options.waitingTimeout);
131
+ }
132
+
133
+ handleProcessingState() {
134
+ this.stopRecording();
135
+ this.process();
136
+ }
137
+
138
+ stopRecording() {
139
+ if (this.mediaRecorder?.state === 'recording') {
140
+ this.mediaRecorder.stop();
141
+ this.hasStartedRecording = false;
142
+ }
143
+ }
144
+
145
async initialize() {
146
try {
66
-
67
- this.transcriber = await pipeline(`automatic-speech-recognition`, `Xenova/whisper-${this.options.modelSize}.${this.options.language}`);
147
+ this.transcriber = await pipeline(
148
+ 'automatic-speech-recognition',
149
+ `Xenova/whisper-${this.options.modelSize}.${this.options.language}`
150
+ );
151
152
const stream = await navigator.mediaDevices.getUserMedia({
153
audio: {
@@ -74,42 +157,51 @@ class MicrophoneInput {
157
}
158
});
159
77
- // Configure MediaRecorder
160
this.mediaRecorder = new MediaRecorder(stream);
79
-
80
- // Handle audio data availability
81
- this.mediaRecorder.ondataavailable = async (event) => {
82
- if (event.data.size > 0) {
161
+ this.mediaRecorder.ondataavailable = (event) => {
162
+ if (event.data.size > 0 &&
163
+ (this.status === Status.RECORDING || this.status === Status.WAITING)) {
164
+ if (this.lastChunk) {
165
+ this.audioChunks.push(this.lastChunk);
166
+ this.lastChunk = null;
167
+ }
168
this.audioChunks.push(event.data);
84
- // await this.processAudioChunk(event.data);
169
+ console.log('Audio chunk received, total chunks:', this.audioChunks.length);
170
+ }
171
+ else if (this.status === Status.LISTENING) {
172
+ this.lastChunk = event.data;
173
}
174
};
175
88
- // Handle recording stop
89
- this.mediaRecorder.onstop = async () => {
90
- await this.finalizeRecording();
91
- };
92
-
93
- // Set up AudioContext and AnalyserNode for silence detection
94
- this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
95
- this.mediaStreamSource = this.audioContext.createMediaStreamSource(stream);
96
- this.analyserNode = this.audioContext.createAnalyser();
97
- this.analyserNode.minDecibels = -90;
98
- this.analyserNode.maxDecibels = -10;
99
- this.analyserNode.smoothingTimeConstant = 0.85;
100
-
101
- this.mediaStreamSource.connect(this.analyserNode);
176
+ this.setupAudioAnalysis(stream);
177
+ return true;
178
} catch (error) {
179
+
180
console.error('Microphone initialization error:', error);
181
toast('Failed to access microphone. Please check permissions.', 'error');
182
+ return false;
183
}
184
}
185
108
- startSilenceDetection() {
109
- const dataArray = new Uint8Array(this.analyserNode.fftSize);
110
- const checkSilence = () => {
186
+ setupAudioAnalysis(stream) {
187
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
188
+ this.mediaStreamSource = this.audioContext.createMediaStreamSource(stream);
189
+ this.analyserNode = this.audioContext.createAnalyser();
190
+ this.analyserNode.fftSize = 2048;
191
+ this.analyserNode.minDecibels = -90;
192
+ this.analyserNode.maxDecibels = -10;
193
+ this.analyserNode.smoothingTimeConstant = 0.85;
194
+ this.mediaStreamSource.connect(this.analyserNode);
195
+ }
196
+
197
+ startAudioAnalysis() {
198
+ const analyzeFrame = () => {
199
+ if (this.status === Status.INACTIVE) return;
200
+
201
+ const dataArray = new Uint8Array(this.analyserNode.fftSize);
202
this.analyserNode.getByteTimeDomainData(dataArray);
203
204
+ // Calculate RMS volume
205
let sum = 0;
206
for (let i = 0; i < dataArray.length; i++) {
207
const amplitude = (dataArray[i] - 128) / 128;
@@ -117,147 +209,94 @@ class MicrophoneInput {
209
}
210
const rms = Math.sqrt(sum / dataArray.length);
211
120
- if (rms < this.silenceThreshold) {
121
- if (!this.silenceTimer) {
122
- this.silenceTimer = setTimeout(() => {
123
- if (this.isRecording) {
124
- console.log('Silence detected. Stopping recording.');
125
- this.stopRecording();
126
- microphoneButton.classList.remove('recording');
127
- microphoneButton.classList.remove('mic-pulse');
128
- }
129
- }, this.silenceDuration);
212
+ const now = Date.now();
213
+
214
+ // Update status based on audio level
215
+ if (rms > this.options.silenceThreshold) {
216
+ this.lastAudioTime = now;
217
+ this.silenceStartTime = null;
218
+
219
+ if (this.status === Status.LISTENING || this.status === Status.WAITING) {
220
+ this.status = Status.RECORDING;
221
}
131
- } else {
132
- if (this.silenceTimer) {
133
- clearTimeout(this.silenceTimer);
134
- this.silenceTimer = null;
222
+ } else if (this.status === Status.RECORDING) {
223
+ if (!this.silenceStartTime) {
224
+ this.silenceStartTime = now;
225
}
136
- }
226
138
- if (this.isRecording) {
139
- requestAnimationFrame(checkSilence);
227
+ const silenceDuration = now - this.silenceStartTime;
228
+ if (silenceDuration >= this.options.silenceDuration) {
229
+ this.status = Status.WAITING;
230
+ }
231
}
232
+
233
+ this.analysisFrame = requestAnimationFrame(analyzeFrame);
234
};
235
143
- if (this.isRecording) {
144
- requestAnimationFrame(checkSilence);
145
- }
236
+ this.analysisFrame = requestAnimationFrame(analyzeFrame);
237
}
238
148
- startRecording() {
149
- if (this.mediaRecorder && this.audioContext) {
150
- this.isRecording = true;
151
- this.audioChunks = [];
152
- this.messageSent = false;
153
- this.mediaRecorder.start(this.options.chunkDuration);
154
- this.audioContext.resume();
155
- this.startSilenceDetection();
239
+ stopAudioAnalysis() {
240
+ if (this.analysisFrame) {
241
+ cancelAnimationFrame(this.analysisFrame);
242
+ this.analysisFrame = null;
243
}
244
}
245
159
- stopRecording() {
160
- if (this.mediaRecorder && this.isRecording) {
161
- this.isRecording = false;
162
- if (!this.isFinalizing) {
163
- this.isFinalizing = true;
164
- this.mediaRecorder.stop();
165
- this.audioContext.suspend();
166
- if (this.silenceTimer) {
167
- clearTimeout(this.silenceTimer);
168
- this.silenceTimer = null;
169
- }
170
- }
246
+ async process() {
247
+ if (this.audioChunks.length === 0) {
248
+ this.status = Status.LISTENING;
249
+ return;
250
}
172
- }
251
252
+ const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
253
+ const audioUrl = URL.createObjectURL(audioBlob);
254
175
- async finalizeRecording() {
176
- if (this.isFinalizing) {
177
- this.isFinalizing = false;
178
-
179
- if (this.audioChunks.length > 0) {
180
-
181
- const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
182
- const audioUrl = URL.createObjectURL(audioBlob);
183
- const samplingRate = 16000; // Adjust as needed for the model
184
- const audioData = await read_audio(audioUrl, samplingRate);
185
- URL.revokeObjectURL(audioUrl);
186
-
187
- // Transcribe the audio
188
- const result = await this.transcriber(audioData);
189
-
190
- if (result.text) {
191
- console.log('Final transcription received:', result.text);
192
- await this.updateCallback(result.text, true);
193
- } else {
194
- console.warn('Final transcription returned empty text.');
195
- }
196
-
197
-
198
- // Release the object URL after use
199
-
200
- // const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });
201
- // this.audioChunks = []; // Clear for next recording
202
-
203
- // const reader = new FileReader();
204
- // reader.onloadend = async () => {
205
- // const base64Data = reader.result.split(',')[1];
206
-
207
- // try {
208
- // const response = await fetch('/transcribe', {
209
- // method: 'POST',
210
- // headers: {
211
- // 'Content-Type': 'application/json'
212
- // },
213
- // body: JSON.stringify({
214
- // audio_data: base64Data,
215
- // model_size: this.options.modelSize,
216
- // language: this.options.language,
217
- // is_final: true
218
- // })
219
- // });
220
-
221
- // const result = await response.json();
222
-
223
- // if (result.text) {
224
- // console.log('Final transcription received:', result.text);
225
- // await this.updateCallback(result.text, true);
226
- // } else {
227
- // console.warn('Final transcription returned empty text.');
228
- // }
229
- // } catch (transcribeError) {
230
- // console.error('Final transcription error:', transcribeError);
231
- // toast('Final transcription failed.', 'error');
232
- // } finally {
233
- // // Reset the microphone button state
234
- // microphoneButton.classList.remove('recording');
235
- // microphoneButton.classList.remove('mic-pulse');
236
- // microphoneButton.style.backgroundColor = '';
237
- // }
238
- // };
239
- // reader.readAsDataURL(audioBlob);
255
+ try {
256
+ const samplingRate = 16000;
257
+ const audioData = await read_audio(audioUrl, samplingRate);
258
+ const result = await this.transcriber(audioData);
259
+ const text = this.filterResult(result.text || "")
260
+
261
+ if (text) {
262
+ console.log('Transcription:', result.text);
263
+ await this.updateCallback(result.text, true);
264
}
265
+ } catch (error) {
266
+ console.error('Transcription error:', error);
267
+ toast('Transcription failed.', 'error');
268
+ } finally {
269
+ URL.revokeObjectURL(audioUrl);
270
+ this.audioChunks = [];
271
+ this.status = Status.LISTENING;
272
}
273
}
274
+
275
+ filterResult(text) {
276
+ text = text.trim()
277
+ let ok = false
278
+ while (!ok) {
279
+ if (!text) break
280
+ if (text[0] === '{' && text[text.length - 1] === '}') break
281
+ if (text[0] === '(' && text[text.length - 1] === ')') break
282
+ if (text[0] === '[' && text[text.length - 1] === ']') break
283
+ ok = true
284
+ }
285
+ if (ok) return text
286
+ else console.log(`Discarding transcription: ${text}`)
287
+ }
288
}
289
245
-export default MicrophoneInput;
290
247
-async function initializeMicrophoneInput() {
248
- console.log('Initializing microphone input');
291
292
+// Initialize and handle click events
293
+async function initializeMicrophoneInput() {
294
microphoneInput = new MicrophoneInput(
295
async (text, isFinal) => {
296
if (isFinal) {
253
- console.log('Final transcription callback received:', text);
254
- updateChatInput(text)
255
- // chatInput.value = text;
256
- // adjustTextareaHeight();
257
-
297
+ updateChatInput(text);
298
if (!microphoneInput.messageSent) {
299
microphoneInput.messageSent = true;
260
- console.log('Sending message');
300
await sendMessage();
301
}
302
}
@@ -265,42 +304,37 @@ async function initializeMicrophoneInput() {
304
{
305
modelSize: 'tiny',
306
language: 'en',
268
- silenceThreshold: 0.07, // Adjust as needed
269
- silenceDuration: 2000, // Adjust as needed
270
- onError: (error) => {
271
- console.error('Microphone input error:', error);
272
- toast('Microphone error: ' + error.message, 'error');
273
- // Reset recording state
274
- if (microphoneButton.classList.contains('recording')) {
275
- microphoneButton.classList.remove('recording');
276
- }
277
- }
307
+ silenceThreshold: 0.07,
308
+ silenceDuration: 1000,
309
+ waitingTimeout: 1500
310
}
311
);
312
+ microphoneInput.status = Status.ACTIVATING;
313
281
- await microphoneInput.initialize();
314
+ return await microphoneInput.initialize();
315
}
316
317
+microphoneButton.addEventListener('click', async () => {
318
+ if (isProcessingClick) return;
319
+ isProcessingClick = true;
320
285
-function toggleRecording() {
286
- console.log('toggleRecording called, isRecording:', microphoneInput.isRecording);
321
+ const hasPermission = await requestMicrophonePermission();
322
+ if (!hasPermission) return;
323
288
- if (microphoneInput.isRecording) {
289
- microphoneInput.stopRecording();
290
- microphoneButton.classList.remove('recording');
291
- // Add pulsing animation class
292
- microphoneButton.classList.remove('mic-pulse');
293
- } else {
294
- microphoneInput.startRecording();
295
- microphoneButton.classList.add('recording');
296
- // Add pulsing animation class
297
- microphoneButton.classList.add('mic-pulse');
298
- }
324
+ try {
325
+ if (!microphoneInput && !await initializeMicrophoneInput()) {
326
+ return;
327
+ }
328
300
- // Add visual feedback
301
- microphoneButton.style.backgroundColor = microphoneInput.isRecording ? '#ff4444' : '';
302
- console.log('New recording state:', microphoneInput.isRecording);
303
-}
329
+ // Simply toggle between INACTIVE and LISTENING states
330
+ microphoneInput.status =
331
+ (microphoneInput.status === Status.INACTIVE || microphoneInput.status === Status.ACTIVATING) ? Status.LISTENING : Status.INACTIVE;
332
+ } finally {
333
+ setTimeout(() => {
334
+ isProcessingClick = false;
335
+ }, 300);
336
+ }
337
+});
338
339
// Some error handling for microphone input
340
async function requestMicrophonePermission() {
@@ -313,25 +347,3 @@ async function requestMicrophonePermission() {
347
return false;
348
}
349
}
316
-// microphoneButton click event listener modifier
317
-microphoneButton.addEventListener('click', async () => {
318
- console.log('Microphone button clicked');
319
- if (isProcessingClick) {
320
- console.log('Click already being processed, ignoring');
321
- return;
322
- }
323
- isProcessingClick = true;
324
-
325
- const hasPermission = await requestMicrophonePermission();
326
- if (!hasPermission) return;
327
-
328
- if (!microphoneInput) {
329
- await initializeMicrophoneInput();
330
- }
331
-
332
- await toggleRecording();
333
-
334
- setTimeout(() => {
335
- isProcessingClick = false;
336
- }, 300); // Add a 300ms delay before allowing another click
337
-});
webui/test2.html
new
+122
@@ -0,0 +1,122 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
6
+ <title>Agent Zero</title>
7
+
8
+ <script type="module">
9
+ import { pipeline, read_audio } from './transformers@3.0.2.js';
10
+
11
+ let transcriber;
12
+ let mediaRecorder;
13
+ let isRecording = false;
14
+ let audioChunks = [];
15
+ let chunks_to_process = [{ tokens: [], finalized: false }];
16
+
17
+ async function initTranscriber() {
18
+ try {
19
+ transcriber = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny.en');
20
+ } catch (error) {
21
+ console.error("Failed to initialize transcriber:", error);
22
+ }
23
+ }
24
+
25
+ async function toggleRecording() {
26
+ if (isRecording) {
27
+ stopRecording();
28
+ } else {
29
+ startRecording();
30
+ }
31
+ }
32
+
33
+ async function startRecording() {
34
+ isRecording = true;
35
+ document.getElementById("micButton").innerText = "Stop Recording";
36
+
37
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
38
+ mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' });
39
+
40
+ // Handle data as it’s available
41
+ mediaRecorder.ondataavailable = (event) => {
42
+ audioChunks.push(event.data);
43
+
44
+ // Process chunks every 3 seconds
45
+ if (audioChunks.length >= 3) {
46
+ processAudioChunks();
47
+ }
48
+ };
49
+
50
+ // Capture small chunks every second
51
+ mediaRecorder.start(1000);
52
+ }
53
+
54
+ async function processAudioChunks() {
55
+ const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
56
+ const audioUrl = URL.createObjectURL(audioBlob);
57
+
58
+ try {
59
+ const audioData = await read_audio(audioUrl, 16000);
60
+
61
+ if (transcriber) {
62
+ await transcriber(audioData, {
63
+ callback_function: mergeChunksCallback,
64
+ chunk_callback: processChunkCallback,
65
+ return_timestamps: true,
66
+ force_full_sequences: false,
67
+ });
68
+ } else {
69
+ console.warn("Transcriber is not ready yet.");
70
+ }
71
+ } catch (error) {
72
+ console.error("Error during transcription:", error);
73
+ }
74
+
75
+ // Cleanup and reset buffer
76
+ URL.revokeObjectURL(audioUrl);
77
+ audioChunks = [];
78
+ }
79
+
80
+ function processChunkCallback(chunk) {
81
+ let lastChunk = chunks_to_process[chunks_to_process.length - 1];
82
+ Object.assign(lastChunk, chunk);
83
+ lastChunk.finalized = true;
84
+
85
+ if (!chunk.is_last) {
86
+ chunks_to_process.push({ tokens: [], finalized: false });
87
+ }
88
+ }
89
+
90
+ function mergeChunksCallback(item) {
91
+ let lastChunk = chunks_to_process[chunks_to_process.length - 1];
92
+ lastChunk.tokens = [...item[0].output_token_ids];
93
+
94
+ // Merge text chunks and update transcript
95
+ const mergedText = transcriber.tokenizer._decode_asr(chunks_to_process, {
96
+ time_precision: 1.0 / transcriber.processor.feature_extractor.config.chunk_length,
97
+ return_timestamps: true,
98
+ force_full_sequences: false,
99
+ });
100
+
101
+ document.getElementById("transcript").innerText += mergedText + ' ';
102
+ }
103
+
104
+ function stopRecording() {
105
+ isRecording = false;
106
+ document.getElementById("micButton").innerText = "Start Recording";
107
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') {
108
+ mediaRecorder.stop();
109
+ }
110
+ }
111
+
112
+ window.toggleRecording = toggleRecording;
113
+ window.onload = initTranscriber;
114
+ </script>
115
+</head>
116
+
117
+<body>
118
+ <h1>Agent Zero Speech Transcription</h1>
119
+ <button id="micButton" onclick="toggleRecording()">Start Recording</button>
120
+ <p id="transcript">Transcript will appear here...</p>
121
+</body>
122
+</html>