sirlaw-dev commited on
Commit
c1edf13
·
verified ·
1 Parent(s): c92284f

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +450 -450
index.html CHANGED
@@ -1,451 +1,451 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Naija Medical Voice Assistant</title>
8
- <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
9
- <style>
10
- :root {
11
- --primary: #00d2ff;
12
- --secondary: #3a7bd5;
13
- --bg: #0f172a;
14
- --card: #1e293b;
15
- --text: #f8fafc;
16
- --accent: #ef4444;
17
- }
18
-
19
- * {
20
- box-sizing: border-box;
21
- margin: 0;
22
- padding: 0;
23
- font-family: 'Outfit', sans-serif;
24
- }
25
-
26
- body {
27
- background: var(--bg);
28
- background: radial-gradient(circle at top right, #1e293b, #0f172a);
29
- color: var(--text);
30
- min-height: 100vh;
31
- display: flex;
32
- flex-direction: column;
33
- align-items: center;
34
- justify-content: center;
35
- padding: 20px;
36
- }
37
-
38
- .container {
39
- width: 100%;
40
- max-width: 500px;
41
- background: rgba(30, 41, 59, 0.7);
42
- backdrop-filter: blur(10px);
43
- border: 1px solid rgba(255, 255, 255, 0.1);
44
- border-radius: 24px;
45
- padding: 40px;
46
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
47
- text-align: center;
48
- }
49
-
50
- h1 {
51
- font-size: 2rem;
52
- margin-bottom: 8px;
53
- background: linear-gradient(to right, var(--primary), var(--secondary));
54
- -webkit-background-clip: text;
55
- -webkit-text-fill-color: transparent;
56
- }
57
-
58
- p.subtitle {
59
- color: #94a3b8;
60
- margin-bottom: 40px;
61
- font-weight: 300;
62
- }
63
-
64
- .status-badge {
65
- display: inline-block;
66
- padding: 6px 12px;
67
- border-radius: 100px;
68
- background: rgba(255, 255, 255, 0.05);
69
- font-size: 0.8rem;
70
- margin-bottom: 24px;
71
- border: 1px solid rgba(255, 255, 255, 0.1);
72
- }
73
-
74
- .mic-container {
75
- position: relative;
76
- margin: 40px 0;
77
- display: flex;
78
- justify-content: center;
79
- }
80
-
81
- .mic-button {
82
- width: 100px;
83
- height: 100px;
84
- border-radius: 50%;
85
- background: linear-gradient(135deg, var(--primary), var(--secondary));
86
- border: none;
87
- cursor: pointer;
88
- display: flex;
89
- align-items: center;
90
- justify-content: center;
91
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
92
- box-shadow: 0 0 30px rgba(0, 210, 255, 0.3);
93
- z-index: 2;
94
- }
95
-
96
- .mic-button svg {
97
- width: 40px;
98
- height: 40px;
99
- fill: white;
100
- }
101
-
102
- .mic-button:hover {
103
- transform: scale(1.05);
104
- box-shadow: 0 0 50px rgba(0, 210, 255, 0.5);
105
- }
106
-
107
- .mic-button.recording {
108
- background: var(--accent);
109
- box-shadow: 0 0 50px rgba(239, 68, 68, 0.5);
110
- animation: pulse 1.5s infinite;
111
- }
112
-
113
- @keyframes pulse {
114
- 0% {
115
- transform: scale(1);
116
- }
117
-
118
- 50% {
119
- transform: scale(1.1);
120
- }
121
-
122
- 100% {
123
- transform: scale(1);
124
- }
125
- }
126
-
127
- .pulse-rings {
128
- position: absolute;
129
- width: 100px;
130
- height: 100px;
131
- border-radius: 50%;
132
- border: 2px solid var(--primary);
133
- opacity: 0;
134
- pointer-events: none;
135
- }
136
-
137
- .recording .pulse-rings {
138
- animation: ripple 1.5s infinite;
139
- }
140
-
141
- @keyframes ripple {
142
- 0% {
143
- transform: scale(1);
144
- opacity: 0.5;
145
- }
146
-
147
- 100% {
148
- transform: scale(2);
149
- opacity: 0;
150
- }
151
- }
152
-
153
- .chat-box {
154
- margin-top: 30px;
155
- text-align: left;
156
- display: none;
157
- }
158
-
159
- .message {
160
- margin-bottom: 20px;
161
- animation: fadeIn 0.5s ease-out;
162
- }
163
-
164
- @keyframes fadeIn {
165
- from {
166
- opacity: 0;
167
- transform: translateY(10px);
168
- }
169
-
170
- to {
171
- opacity: 1;
172
- transform: translateY(0);
173
- }
174
- }
175
-
176
- .label {
177
- font-size: 0.7rem;
178
- text-transform: uppercase;
179
- letter-spacing: 1px;
180
- color: #64748b;
181
- margin-bottom: 4px;
182
- }
183
-
184
- .content {
185
- background: rgba(255, 255, 255, 0.05);
186
- padding: 12px 16px;
187
- border-radius: 12px;
188
- font-size: 0.95rem;
189
- line-height: 1.5;
190
- border-left: 3px solid var(--primary);
191
- }
192
-
193
- .content.assistant {
194
- border-left-color: #10b981;
195
- }
196
-
197
- .loader {
198
- display: none;
199
- margin: 20px 0;
200
- color: var(--primary);
201
- font-size: 0.9rem;
202
- }
203
-
204
- .loader span {
205
- display: inline-block;
206
- animation: bounce 1.4s infinite ease-in-out both;
207
- }
208
-
209
- .loader span:nth-child(1) {
210
- animation-delay: -0.32s;
211
- }
212
-
213
- .loader span:nth-child(2) {
214
- animation-delay: -0.16s;
215
- }
216
-
217
- @keyframes bounce {
218
-
219
- 0%,
220
- 80%,
221
- 100% {
222
- transform: scale(0);
223
- }
224
-
225
- 40% {
226
- transform: scale(1);
227
- }
228
- }
229
-
230
- audio {
231
- display: block;
232
- margin: 20px auto;
233
- width: 100%;
234
- height: 40px;
235
- }
236
- </style>
237
- </head>
238
-
239
- <body>
240
-
241
- <div class="container">
242
- <div class="status-badge" id="status">System: Ready</div>
243
- <h1>Naija Health AI</h1>
244
- <p class="subtitle">Your empathetic medical assistant</p>
245
-
246
- <div class="mic-container">
247
- <div class="pulse-rings"></div>
248
- <button class="mic-button" id="micBtn" title="Click to Record">
249
- <svg viewBox="0 0 24 24">
250
- <path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" />
251
- <path
252
- d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" />
253
- </svg>
254
- </button>
255
- </div>
256
-
257
- <!-- <div style="margin-bottom: 30px;" style="display: none;">
258
- <input type="file" id="fileInput" accept="audio/*">
259
- <button onclick="document.getElementById('fileInput').click()"
260
- style="background: transparent; border: 1px solid var(--primary); color: var(--primary); padding: 10px 20px; border-radius: 12px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s;">
261
- 📁 Upload Audio File
262
- </button>
263
- </div> -->
264
-
265
- <div class="loader" id="loader">
266
- AI is thinking<span>.</span><span>.</span><span>.</span>
267
- </div>
268
-
269
- <div class="chat-box" id="chatBox">
270
- <div class="message">
271
- <div class="label">You said</div>
272
- <div class="content" id="userMsg">...</div>
273
- </div>
274
- <div class="message">
275
- <div class="label">Assistant</div>
276
- <div class="content assistant" id="assistantMsg">...</div>
277
- </div>
278
- </div>
279
- </div>
280
-
281
- <div class="container" style="margin-top: 20px; padding: 20px;">
282
- <div class="label" style="text-align: center;">Assistant's Voice</div>
283
- <audio id="audioPlayer" controls></audio>
284
- </div>
285
-
286
- <script>
287
- // --- DOM Elements ---
288
- const micBtn = document.getElementById('micBtn');
289
- const status = document.getElementById('status');
290
- const loader = document.getElementById('loader');
291
- const chatBox = document.getElementById('chatBox');
292
- const userMsg = document.getElementById('userMsg');
293
- const assistantMsg = document.getElementById('assistantMsg');
294
- const audioPlayer = document.getElementById('audioPlayer');
295
- // const fileInput = document.getElementById('fileInput');
296
-
297
- // --- Audio Recording Variables ---
298
- let audioContext;
299
- let mediaStreamSource;
300
- let processor;
301
- let isRecording = false;
302
- let recordedChunks = [];
303
-
304
- // --- File Upload Logic ---
305
- // fileInput.onchange = (e) => {
306
- // const file = e.target.files[0];
307
- // if (file) {
308
- // status.innerText = "Processing uploaded file...";
309
- // sendAudio(file);
310
- // }
311
- // };
312
-
313
- // --- Microphone Recording Logic ---
314
-
315
- micBtn.onclick = async () => {
316
- if (isRecording) {
317
- isRecording = false;
318
- if (processor) processor.disconnect();
319
- if (mediaStreamSource) mediaStreamSource.disconnect();
320
- micBtn.classList.remove('recording');
321
- status.innerText = "Processing...";
322
-
323
- // Export the recorded audio chunks to a real WAV Blob
324
- const wavBlob = exportWAV(recordedChunks, audioContext.sampleRate);
325
- sendAudio(wavBlob);
326
- return;
327
- }
328
-
329
- try {
330
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
331
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
332
- mediaStreamSource = audioContext.createMediaStreamSource(stream);
333
-
334
- // Create a ScriptProcessorNode to capture raw audio data
335
- processor = audioContext.createScriptProcessor(4096, 1, 1);
336
- recordedChunks = [];
337
-
338
- processor.onaudioprocess = function (e) {
339
- if (!isRecording) return;
340
- const channelData = e.inputBuffer.getChannelData(0);
341
- // Copy the Float32Array
342
- recordedChunks.push(new Float32Array(channelData));
343
- };
344
-
345
- mediaStreamSource.connect(processor);
346
- processor.connect(audioContext.destination);
347
-
348
- isRecording = true;
349
- micBtn.classList.add('recording');
350
- status.innerText = "Listening...";
351
- } catch (err) {
352
- console.error("Error accessing mic:", err);
353
- alert("Please allow microphone access!");
354
- }
355
- };
356
-
357
- // --- WAV Encoding Logic ---
358
- function exportWAV(chunks, sampleRate) {
359
- let length = 0;
360
- for (let i = 0; i < chunks.length; i++) {
361
- length += chunks[i].length;
362
- }
363
-
364
- const buffer = new Float32Array(length);
365
- let offset = 0;
366
- for (let i = 0; i < chunks.length; i++) {
367
- buffer.set(chunks[i], offset);
368
- offset += chunks[i].length;
369
- }
370
-
371
- const wavBuffer = new ArrayBuffer(44 + buffer.length * 2);
372
- const view = new DataView(wavBuffer);
373
-
374
- writeString(view, 0, 'RIFF');
375
- view.setUint32(4, 36 + buffer.length * 2, true);
376
- writeString(view, 8, 'WAVE');
377
- writeString(view, 12, 'fmt ');
378
- view.setUint32(16, 16, true);
379
- view.setUint16(20, 1, true); // PCM format
380
- view.setUint16(22, 1, true); // Mono channel
381
- view.setUint32(24, sampleRate, true);
382
- view.setUint32(28, sampleRate * 2, true);
383
- view.setUint16(32, 2, true);
384
- view.setUint16(34, 16, true); // 16-bit
385
- writeString(view, 36, 'data');
386
- view.setUint32(40, buffer.length * 2, true);
387
-
388
- floatTo16BitPCM(view, 44, buffer);
389
-
390
- return new Blob([view], { type: 'audio/wav' });
391
- }
392
-
393
- function writeString(view, offset, string) {
394
- for (let i = 0; i < string.length; i++) {
395
- view.setUint8(offset + i, string.charCodeAt(i));
396
- }
397
- }
398
-
399
- function floatTo16BitPCM(output, offset, input) {
400
- for (let i = 0; i < input.length; i++, offset += 2) {
401
- let s = Math.max(-1, Math.min(1, input[i]));
402
- output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
403
- }
404
- }
405
- // --------------------------
406
-
407
- async function sendAudio(blob) {
408
- loader.style.display = 'block';
409
- chatBox.style.display = 'none';
410
-
411
- const formData = new FormData();
412
- formData.append('file', blob, 'recording.wav');
413
-
414
- try {
415
- const response = await fetch('/voice', {
416
- method: 'POST',
417
- body: formData
418
- });
419
-
420
- if (!response.ok) throw new Error("Server error");
421
-
422
- // Get text from headers
423
- userMsg.innerText = response.headers.get('X-Input-Text') || "No text detected";
424
- assistantMsg.innerText = response.headers.get('X-Response-Text') || "No response generated";
425
-
426
- // Get audio blob
427
- const audioBlob = await response.blob();
428
- const audioUrl = URL.createObjectURL(audioBlob);
429
-
430
- loader.style.display = 'none';
431
- chatBox.style.display = 'block';
432
- status.innerText = "Ready";
433
-
434
- audioPlayer.src = audioUrl;
435
- audioPlayer.load(); // Ensure it's loaded
436
-
437
- audioPlayer.play().catch(e => {
438
- console.error("Auto-play failed. Please click play manually:", e);
439
- status.innerText = "Click play to hear response";
440
- });
441
-
442
- } catch (err) {
443
- console.error("Upload failed:", err);
444
- status.innerText = "Error!";
445
- loader.style.display = 'none';
446
- }
447
- }
448
- </script>
449
- </body>
450
-
451
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Naija Medical Voice Assistant</title>
8
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
9
+ <style>
10
+ :root {
11
+ --primary: #00d2ff;
12
+ --secondary: #3a7bd5;
13
+ --bg: #0f172a;
14
+ --card: #1e293b;
15
+ --text: #f8fafc;
16
+ --accent: #ef4444;
17
+ }
18
+
19
+ * {
20
+ box-sizing: border-box;
21
+ margin: 0;
22
+ padding: 0;
23
+ font-family: 'Outfit', sans-serif;
24
+ }
25
+
26
+ body {
27
+ background: var(--bg);
28
+ background: radial-gradient(circle at top right, #1e293b, #0f172a);
29
+ color: var(--text);
30
+ min-height: 100vh;
31
+ display: flex;
32
+ flex-direction: column;
33
+ align-items: center;
34
+ justify-content: center;
35
+ padding: 20px;
36
+ }
37
+
38
+ .container {
39
+ width: 100%;
40
+ max-width: 500px;
41
+ background: rgba(30, 41, 59, 0.7);
42
+ backdrop-filter: blur(10px);
43
+ border: 1px solid rgba(255, 255, 255, 0.1);
44
+ border-radius: 24px;
45
+ padding: 40px;
46
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
47
+ text-align: center;
48
+ }
49
+
50
+ h1 {
51
+ font-size: 2rem;
52
+ margin-bottom: 8px;
53
+ background: linear-gradient(to right, var(--primary), var(--secondary));
54
+ -webkit-background-clip: text;
55
+ -webkit-text-fill-color: transparent;
56
+ }
57
+
58
+ p.subtitle {
59
+ color: #94a3b8;
60
+ margin-bottom: 40px;
61
+ font-weight: 300;
62
+ }
63
+
64
+ .status-badge {
65
+ display: inline-block;
66
+ padding: 6px 12px;
67
+ border-radius: 100px;
68
+ background: rgba(255, 255, 255, 0.05);
69
+ font-size: 0.8rem;
70
+ margin-bottom: 24px;
71
+ border: 1px solid rgba(255, 255, 255, 0.1);
72
+ }
73
+
74
+ .mic-container {
75
+ position: relative;
76
+ margin: 40px 0;
77
+ display: flex;
78
+ justify-content: center;
79
+ }
80
+
81
+ .mic-button {
82
+ width: 100px;
83
+ height: 100px;
84
+ border-radius: 50%;
85
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
86
+ border: none;
87
+ cursor: pointer;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
92
+ box-shadow: 0 0 30px rgba(0, 210, 255, 0.3);
93
+ z-index: 2;
94
+ }
95
+
96
+ .mic-button svg {
97
+ width: 40px;
98
+ height: 40px;
99
+ fill: white;
100
+ }
101
+
102
+ .mic-button:hover {
103
+ transform: scale(1.05);
104
+ box-shadow: 0 0 50px rgba(0, 210, 255, 0.5);
105
+ }
106
+
107
+ .mic-button.recording {
108
+ background: var(--accent);
109
+ box-shadow: 0 0 50px rgba(239, 68, 68, 0.5);
110
+ animation: pulse 1.5s infinite;
111
+ }
112
+
113
+ @keyframes pulse {
114
+ 0% {
115
+ transform: scale(1);
116
+ }
117
+
118
+ 50% {
119
+ transform: scale(1.1);
120
+ }
121
+
122
+ 100% {
123
+ transform: scale(1);
124
+ }
125
+ }
126
+
127
+ .pulse-rings {
128
+ position: absolute;
129
+ width: 100px;
130
+ height: 100px;
131
+ border-radius: 50%;
132
+ border: 2px solid var(--primary);
133
+ opacity: 0;
134
+ pointer-events: none;
135
+ }
136
+
137
+ .recording .pulse-rings {
138
+ animation: ripple 1.5s infinite;
139
+ }
140
+
141
+ @keyframes ripple {
142
+ 0% {
143
+ transform: scale(1);
144
+ opacity: 0.5;
145
+ }
146
+
147
+ 100% {
148
+ transform: scale(2);
149
+ opacity: 0;
150
+ }
151
+ }
152
+
153
+ .chat-box {
154
+ margin-top: 30px;
155
+ text-align: left;
156
+ display: none;
157
+ }
158
+
159
+ .message {
160
+ margin-bottom: 20px;
161
+ animation: fadeIn 0.5s ease-out;
162
+ }
163
+
164
+ @keyframes fadeIn {
165
+ from {
166
+ opacity: 0;
167
+ transform: translateY(10px);
168
+ }
169
+
170
+ to {
171
+ opacity: 1;
172
+ transform: translateY(0);
173
+ }
174
+ }
175
+
176
+ .label {
177
+ font-size: 0.7rem;
178
+ text-transform: uppercase;
179
+ letter-spacing: 1px;
180
+ color: #64748b;
181
+ margin-bottom: 4px;
182
+ }
183
+
184
+ .content {
185
+ background: rgba(255, 255, 255, 0.05);
186
+ padding: 12px 16px;
187
+ border-radius: 12px;
188
+ font-size: 0.95rem;
189
+ line-height: 1.5;
190
+ border-left: 3px solid var(--primary);
191
+ }
192
+
193
+ .content.assistant {
194
+ border-left-color: #10b981;
195
+ }
196
+
197
+ .loader {
198
+ display: none;
199
+ margin: 20px 0;
200
+ color: var(--primary);
201
+ font-size: 0.9rem;
202
+ }
203
+
204
+ .loader span {
205
+ display: inline-block;
206
+ animation: bounce 1.4s infinite ease-in-out both;
207
+ }
208
+
209
+ .loader span:nth-child(1) {
210
+ animation-delay: -0.32s;
211
+ }
212
+
213
+ .loader span:nth-child(2) {
214
+ animation-delay: -0.16s;
215
+ }
216
+
217
+ @keyframes bounce {
218
+
219
+ 0%,
220
+ 80%,
221
+ 100% {
222
+ transform: scale(0);
223
+ }
224
+
225
+ 40% {
226
+ transform: scale(1);
227
+ }
228
+ }
229
+
230
+ audio {
231
+ display: block;
232
+ margin: 20px auto;
233
+ width: 100%;
234
+ height: 40px;
235
+ }
236
+ </style>
237
+ </head>
238
+
239
+ <body>
240
+
241
+ <div class="container">
242
+ <div class="status-badge" id="status">System: Ready</div>
243
+ <h1>Naija Health AI</h1>
244
+ <p class="subtitle">Your empathetic medical assistant</p>
245
+
246
+ <div class="mic-container">
247
+ <div class="pulse-rings"></div>
248
+ <button class="mic-button" id="micBtn" title="Click to Record">
249
+ <svg viewBox="0 0 24 24">
250
+ <path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" />
251
+ <path
252
+ d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" />
253
+ </svg>
254
+ </button>
255
+ </div>
256
+
257
+ <!-- <div style="margin-bottom: 30px;" style="display: none;">
258
+ <input type="file" id="fileInput" accept="audio/*">
259
+ <button onclick="document.getElementById('fileInput').click()"
260
+ style="background: transparent; border: 1px solid var(--primary); color: var(--primary); padding: 10px 20px; border-radius: 12px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s;">
261
+ 📁 Upload Audio File
262
+ </button>
263
+ </div> -->
264
+
265
+ <div class="loader" id="loader">
266
+ AI is thinking<span>.</span><span>.</span><span>.</span>
267
+ </div>
268
+
269
+ <div class="chat-box" id="chatBox">
270
+ <div class="message">
271
+ <div class="label">You said</div>
272
+ <div class="content" id="userMsg">...</div>
273
+ </div>
274
+ <div class="message">
275
+ <div class="label">Assistant</div>
276
+ <div class="content assistant" id="assistantMsg">...</div>
277
+ </div>
278
+ </div>
279
+ </div>
280
+
281
+ <div class="container" style="margin-top: 20px; padding: 20px;">
282
+ <div class="label" style="text-align: center;">Assistant's Voice</div>
283
+ <audio id="audioPlayer" controls></audio>
284
+ </div>
285
+
286
+ <script>
287
+ // --- DOM Elements ---
288
+ const micBtn = document.getElementById('micBtn');
289
+ const status = document.getElementById('status');
290
+ const loader = document.getElementById('loader');
291
+ const chatBox = document.getElementById('chatBox');
292
+ const userMsg = document.getElementById('userMsg');
293
+ const assistantMsg = document.getElementById('assistantMsg');
294
+ const audioPlayer = document.getElementById('audioPlayer');
295
+ // const fileInput = document.getElementById('fileInput');
296
+
297
+ // --- Audio Recording Variables ---
298
+ let audioContext;
299
+ let mediaStreamSource;
300
+ let processor;
301
+ let isRecording = false;
302
+ let recordedChunks = [];
303
+
304
+ // --- File Upload Logic ---
305
+ // fileInput.onchange = (e) => {
306
+ // const file = e.target.files[0];
307
+ // if (file) {
308
+ // status.innerText = "Processing uploaded file...";
309
+ // sendAudio(file);
310
+ // }
311
+ // };
312
+
313
+ // --- Microphone Recording Logic ---
314
+
315
+ micBtn.onclick = async () => {
316
+ if (isRecording) {
317
+ isRecording = false;
318
+ if (processor) processor.disconnect();
319
+ if (mediaStreamSource) mediaStreamSource.disconnect();
320
+ micBtn.classList.remove('recording');
321
+ status.innerText = "Processing...";
322
+
323
+ // Export the recorded audio chunks to a real WAV Blob
324
+ const wavBlob = exportWAV(recordedChunks, audioContext.sampleRate);
325
+ sendAudio(wavBlob);
326
+ return;
327
+ }
328
+
329
+ try {
330
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
331
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
332
+ mediaStreamSource = audioContext.createMediaStreamSource(stream);
333
+
334
+ // Create a ScriptProcessorNode to capture raw audio data
335
+ processor = audioContext.createScriptProcessor(4096, 1, 1);
336
+ recordedChunks = [];
337
+
338
+ processor.onaudioprocess = function (e) {
339
+ if (!isRecording) return;
340
+ const channelData = e.inputBuffer.getChannelData(0);
341
+ // Copy the Float32Array
342
+ recordedChunks.push(new Float32Array(channelData));
343
+ };
344
+
345
+ mediaStreamSource.connect(processor);
346
+ processor.connect(audioContext.destination);
347
+
348
+ isRecording = true;
349
+ micBtn.classList.add('recording');
350
+ status.innerText = "Listening...";
351
+ } catch (err) {
352
+ console.error("Error accessing mic:", err);
353
+ alert("Please allow microphone access!");
354
+ }
355
+ };
356
+
357
+ // --- WAV Encoding Logic ---
358
+ function exportWAV(chunks, sampleRate) {
359
+ let length = 0;
360
+ for (let i = 0; i < chunks.length; i++) {
361
+ length += chunks[i].length;
362
+ }
363
+
364
+ const buffer = new Float32Array(length);
365
+ let offset = 0;
366
+ for (let i = 0; i < chunks.length; i++) {
367
+ buffer.set(chunks[i], offset);
368
+ offset += chunks[i].length;
369
+ }
370
+
371
+ const wavBuffer = new ArrayBuffer(44 + buffer.length * 2);
372
+ const view = new DataView(wavBuffer);
373
+
374
+ writeString(view, 0, 'RIFF');
375
+ view.setUint32(4, 36 + buffer.length * 2, true);
376
+ writeString(view, 8, 'WAVE');
377
+ writeString(view, 12, 'fmt ');
378
+ view.setUint32(16, 16, true);
379
+ view.setUint16(20, 1, true); // PCM format
380
+ view.setUint16(22, 1, true); // Mono channel
381
+ view.setUint32(24, sampleRate, true);
382
+ view.setUint32(28, sampleRate * 2, true);
383
+ view.setUint16(32, 2, true);
384
+ view.setUint16(34, 16, true); // 16-bit
385
+ writeString(view, 36, 'data');
386
+ view.setUint32(40, buffer.length * 2, true);
387
+
388
+ floatTo16BitPCM(view, 44, buffer);
389
+
390
+ return new Blob([view], { type: 'audio/wav' });
391
+ }
392
+
393
+ function writeString(view, offset, string) {
394
+ for (let i = 0; i < string.length; i++) {
395
+ view.setUint8(offset + i, string.charCodeAt(i));
396
+ }
397
+ }
398
+
399
+ function floatTo16BitPCM(output, offset, input) {
400
+ for (let i = 0; i < input.length; i++, offset += 2) {
401
+ let s = Math.max(-1, Math.min(1, input[i]));
402
+ output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
403
+ }
404
+ }
405
+ // --------------------------
406
+
407
+ async function sendAudio(blob) {
408
+ loader.style.display = 'block';
409
+ chatBox.style.display = 'none';
410
+
411
+ const formData = new FormData();
412
+ formData.append('file', blob, 'recording.wav');
413
+
414
+ try {
415
+ const response = await fetch('/voice', {
416
+ method: 'POST',
417
+ body: formData
418
+ });
419
+
420
+ if (!response.ok) throw new Error("Server error");
421
+
422
+ // Get text from headers and decode Yoruba characters
423
+ userMsg.innerText = decodeURIComponent(response.headers.get('X-Input-Text') || "No text detected");
424
+ assistantMsg.innerText = decodeURIComponent(response.headers.get('X-Response-Text') || "No response generated");
425
+
426
+ // Get audio blob
427
+ const audioBlob = await response.blob();
428
+ const audioUrl = URL.createObjectURL(audioBlob);
429
+
430
+ loader.style.display = 'none';
431
+ chatBox.style.display = 'block';
432
+ status.innerText = "Ready";
433
+
434
+ audioPlayer.src = audioUrl;
435
+ audioPlayer.load(); // Ensure it's loaded
436
+
437
+ audioPlayer.play().catch(e => {
438
+ console.error("Auto-play failed. Please click play manually:", e);
439
+ status.innerText = "Click play to hear response";
440
+ });
441
+
442
+ } catch (err) {
443
+ console.error("Upload failed:", err);
444
+ status.innerText = "Error!";
445
+ loader.style.display = 'none';
446
+ }
447
+ }
448
+ </script>
449
+ </body>
450
+
451
  </html>