Arsh9210 commited on
Commit
1d67081
·
verified ·
1 Parent(s): ec2f0dd

Added inference_scripts_vllm/unified_s2s_scripts/web/app.js

Browse files
inference_scripts_vllm/unified_s2s_scripts/web/app.js ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PCMPlayerWorklet } from "./PCMPlayerWorklet.js";
2
+
3
+ const els = {
4
+ modelSelect: document.getElementById("model-select"),
5
+ modelState: document.getElementById("model-state"),
6
+ autoE2E: document.getElementById("auto-e2e"),
7
+ enableReasoning: document.getElementById("enable-reasoning"),
8
+ audioFile: document.getElementById("audio-file"),
9
+ recordBtn: document.getElementById("record-btn"),
10
+ audioName: document.getElementById("audio-name"),
11
+ promptInput: document.getElementById("prompt-input"),
12
+ textInput: document.getElementById("text-input"),
13
+ transcribeBtn: document.getElementById("transcribe-btn"),
14
+ generateResponseBtn: document.getElementById("generate-response-btn"),
15
+ generateAudioBtn: document.getElementById("generate-audio-btn"),
16
+ clearBtn: document.getElementById("clear-btn"),
17
+ statusBox: document.getElementById("status-box"),
18
+ textOutput: document.getElementById("text-output"),
19
+ audioOutput: document.getElementById("audio-output"),
20
+ audioState: document.getElementById("audio-state"),
21
+ visualizer: document.getElementById("visualizer"),
22
+ };
23
+
24
+ let config = { sample_rate: 16000 };
25
+ let selectedAudioFile = null;
26
+ let recordedAudio = null;
27
+ let mediaRecorder = null;
28
+ let mediaStream = null;
29
+ let audioContext = null;
30
+ let player = null;
31
+ let pendingAudioInfo = null;
32
+ let currentAudioChunks = [];
33
+ let completedAudioUrl = null;
34
+ let activeSocket = null;
35
+ let busy = false;
36
+
37
+ function setStatus(message, isError = false) {
38
+ els.statusBox.value = message || "";
39
+ els.modelState.textContent = message || "Idle";
40
+ els.modelState.classList.toggle("error", isError);
41
+ }
42
+
43
+ function setReadyState(status) {
44
+ els.modelState.textContent = status.status || "Idle";
45
+ els.modelState.classList.toggle("ready", Boolean(status.ready));
46
+ els.modelState.classList.toggle("error", Boolean(status.error));
47
+ if (!busy) els.statusBox.value = status.status || "";
48
+ }
49
+
50
+ function setBusy(value) {
51
+ busy = value;
52
+ els.transcribeBtn.disabled = value;
53
+ els.generateResponseBtn.disabled = value;
54
+ els.generateAudioBtn.disabled = value;
55
+ els.recordBtn.disabled = value && mediaRecorder?.state !== "recording";
56
+ }
57
+
58
+ async function loadConfig() {
59
+ const response = await fetch("/api/config");
60
+ const data = await response.json();
61
+ config = data;
62
+ els.modelSelect.innerHTML = "";
63
+ for (const model of data.models) {
64
+ const option = document.createElement("option");
65
+ option.value = model;
66
+ option.textContent = model;
67
+ option.selected = model === data.default_model;
68
+ els.modelSelect.appendChild(option);
69
+ }
70
+ els.modelSelect.disabled = data.models.length <= 1;
71
+ els.promptInput.value = data.default_prompt || "";
72
+ setReadyState(data);
73
+ }
74
+
75
+ async function pollStatus() {
76
+ try {
77
+ const response = await fetch("/api/status");
78
+ setReadyState(await response.json());
79
+ } catch (error) {
80
+ setStatus(`Status check failed: ${error.message}`, true);
81
+ }
82
+ }
83
+
84
+ async function readNdjson(response, onEvent) {
85
+ if (!response.ok) throw new Error(await response.text());
86
+ const reader = response.body.getReader();
87
+ const decoder = new TextDecoder();
88
+ let buffer = "";
89
+ while (true) {
90
+ const { value, done } = await reader.read();
91
+ buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
92
+ const lines = buffer.split("\n");
93
+ buffer = lines.pop() || "";
94
+ for (const line of lines) {
95
+ if (line.trim()) onEvent(JSON.parse(line));
96
+ }
97
+ if (done) break;
98
+ }
99
+ if (buffer.trim()) onEvent(JSON.parse(buffer));
100
+ }
101
+
102
+ function currentAudioFile() {
103
+ if (selectedAudioFile) return selectedAudioFile;
104
+ if (!recordedAudio) return null;
105
+ return new File([recordedAudio], "recording.webm", { type: recordedAudio.type || "audio/webm" });
106
+ }
107
+
108
+ async function transcribeAudio() {
109
+ const audio = currentAudioFile();
110
+ if (!audio) {
111
+ setStatus("Please record or upload audio first.", true);
112
+ return false;
113
+ }
114
+
115
+ setBusy(true);
116
+ const form = new FormData();
117
+ form.append("audio", audio);
118
+ form.append("model_name", els.modelSelect.value);
119
+ try {
120
+ const response = await fetch("/api/transcribe", { method: "POST", body: form });
121
+ let finalText = "";
122
+ await readNdjson(response, (event) => {
123
+ if (event.type === "status") setStatus(event.status);
124
+ if (event.type === "transcript") {
125
+ finalText = event.text || "";
126
+ els.textInput.value = finalText;
127
+ }
128
+ if (event.type === "complete") {
129
+ finalText = event.text || finalText;
130
+ els.textInput.value = finalText;
131
+ setStatus(event.status);
132
+ }
133
+ if (event.type === "error") throw new Error(event.error);
134
+ });
135
+ return Boolean(finalText);
136
+ } catch (error) {
137
+ setStatus(`Transcription failed: ${error.message}`, true);
138
+ return false;
139
+ } finally {
140
+ setBusy(false);
141
+ }
142
+ }
143
+
144
+ async function generateResponse() {
145
+ const text = els.textInput.value.trim();
146
+ if (!text) {
147
+ setStatus("Please provide text or transcribe audio first.", true);
148
+ return false;
149
+ }
150
+
151
+ setBusy(true);
152
+ els.textOutput.value = "";
153
+ try {
154
+ const response = await fetch("/api/generate-response", {
155
+ method: "POST",
156
+ headers: { "Content-Type": "application/json" },
157
+ body: JSON.stringify({
158
+ model_name: els.modelSelect.value,
159
+ enable_reasoning: els.enableReasoning.checked,
160
+ prompt: els.promptInput.value,
161
+ text,
162
+ }),
163
+ });
164
+ let finalText = "";
165
+ await readNdjson(response, (event) => {
166
+ if (event.type === "status") setStatus(event.status);
167
+ if (event.type === "text") {
168
+ finalText = event.text || "";
169
+ els.textOutput.value = finalText;
170
+ els.textOutput.scrollTop = els.textOutput.scrollHeight;
171
+ }
172
+ if (event.type === "complete") {
173
+ finalText = event.text || finalText;
174
+ els.textOutput.value = finalText;
175
+ setStatus(event.status);
176
+ }
177
+ if (event.type === "error") throw new Error(event.error);
178
+ });
179
+ return Boolean(finalText);
180
+ } catch (error) {
181
+ setStatus(`Response generation failed: ${error.message}`, true);
182
+ return false;
183
+ } finally {
184
+ setBusy(false);
185
+ }
186
+ }
187
+
188
+ async function ensurePlayer() {
189
+ if (!window.isSecureContext) {
190
+ throw new Error("AudioWorklet requires HTTPS or localhost.");
191
+ }
192
+ if (!audioContext) {
193
+ audioContext = new (window.AudioContext || window.webkitAudioContext)({
194
+ sampleRate: config.sample_rate || 16000,
195
+ latencyHint: "interactive",
196
+ });
197
+ if (!audioContext.audioWorklet) throw new Error("AudioWorklet is not supported in this browser.");
198
+ player = new PCMPlayerWorklet(audioContext, { minBufferBeforePlaybackMs: 250 });
199
+ await player.initPromise;
200
+ player.addEventListener("audioEnded", finalizeAudioPlayback);
201
+ }
202
+ if (audioContext.state === "suspended") await audioContext.resume();
203
+ return player;
204
+ }
205
+
206
+ async function generateAudio() {
207
+ const text = els.textOutput.value.trim();
208
+ if (!text) {
209
+ setStatus("Please generate text output first.", true);
210
+ return false;
211
+ }
212
+
213
+ try {
214
+ await ensurePlayer();
215
+ } catch (error) {
216
+ setStatus(`Audio init failed: ${error.message}`, true);
217
+ return false;
218
+ }
219
+
220
+ setBusy(true);
221
+ resetAudioOutput();
222
+ currentAudioChunks = [];
223
+ els.audioState.textContent = "Connecting audio stream...";
224
+
225
+ const wsScheme = window.location.protocol === "https:" ? "wss" : "ws";
226
+ activeSocket = new WebSocket(`${wsScheme}://${window.location.host}/ws/generate-audio`);
227
+ activeSocket.binaryType = "arraybuffer";
228
+
229
+ return new Promise((resolve) => {
230
+ let ok = false;
231
+ activeSocket.onopen = () => {
232
+ activeSocket.send(JSON.stringify({ model_name: els.modelSelect.value, text }));
233
+ };
234
+ activeSocket.onmessage = (event) => {
235
+ if (typeof event.data === "string") {
236
+ const message = JSON.parse(event.data);
237
+ if (message.type === "audio") pendingAudioInfo = message;
238
+ if (message.status) {
239
+ setStatus(message.status);
240
+ els.audioState.textContent = message.status;
241
+ }
242
+ if (message.type === "complete") {
243
+ ok = true;
244
+ player.notifyStreamEnded();
245
+ els.audioState.textContent = "Finishing playback...";
246
+ }
247
+ if (message.type === "error") {
248
+ setStatus(`Audio generation failed: ${message.error}`, true);
249
+ els.audioState.textContent = "Error";
250
+ activeSocket.close();
251
+ }
252
+ return;
253
+ }
254
+ handleAudioBytes(event.data);
255
+ };
256
+ activeSocket.onerror = () => {
257
+ setStatus("Audio stream failed.", true);
258
+ els.audioState.textContent = "Error";
259
+ setBusy(false);
260
+ resolve(false);
261
+ };
262
+ activeSocket.onclose = () => {
263
+ setBusy(false);
264
+ resolve(ok);
265
+ };
266
+ });
267
+ }
268
+
269
+ function handleAudioBytes(data) {
270
+ const sourceRate = pendingAudioInfo?.sample_rate || config.sample_rate || 16000;
271
+ const serverChunk = new Float32Array(data);
272
+ currentAudioChunks.push(new Float32Array(serverChunk));
273
+ const playbackChunk = resampleFloat32(serverChunk, sourceRate, audioContext.sampleRate);
274
+ player.playAudio(playbackChunk);
275
+ pendingAudioInfo = null;
276
+ }
277
+
278
+ function resetAudioOutput() {
279
+ player?.reset();
280
+ pendingAudioInfo = null;
281
+ if (activeSocket && activeSocket.readyState < WebSocket.CLOSING) activeSocket.close();
282
+ activeSocket = null;
283
+ if (completedAudioUrl) URL.revokeObjectURL(completedAudioUrl);
284
+ completedAudioUrl = null;
285
+ els.audioOutput.removeAttribute("src");
286
+ els.audioOutput.load();
287
+ }
288
+
289
+ function finalizeAudioPlayback() {
290
+ if (!currentAudioChunks.length) {
291
+ els.audioState.textContent = "Idle";
292
+ return;
293
+ }
294
+ const totalSamples = currentAudioChunks.reduce((sum, chunk) => sum + chunk.length, 0);
295
+ const combined = new Float32Array(totalSamples);
296
+ let offset = 0;
297
+ for (const chunk of currentAudioChunks) {
298
+ combined.set(chunk, offset);
299
+ offset += chunk.length;
300
+ }
301
+ completedAudioUrl = URL.createObjectURL(float32ToWavBlob(combined, config.sample_rate || 16000));
302
+ els.audioOutput.src = completedAudioUrl;
303
+ els.audioState.textContent = "Complete.";
304
+ }
305
+
306
+ function resampleFloat32(input, sourceRate, targetRate) {
307
+ if (!sourceRate || sourceRate === targetRate) return input;
308
+ const outputLength = Math.max(1, Math.round((input.length * targetRate) / sourceRate));
309
+ const output = new Float32Array(outputLength);
310
+ const ratio = sourceRate / targetRate;
311
+ for (let i = 0; i < outputLength; i++) {
312
+ const position = i * ratio;
313
+ const index = Math.floor(position);
314
+ const fraction = position - index;
315
+ const first = input[index] || 0;
316
+ const second = input[Math.min(index + 1, input.length - 1)] || 0;
317
+ output[i] = first + (second - first) * fraction;
318
+ }
319
+ return output;
320
+ }
321
+
322
+ function float32ToWavBlob(samples, sampleRate) {
323
+ const buffer = new ArrayBuffer(44 + samples.length * 2);
324
+ const view = new DataView(buffer);
325
+ writeAscii(view, 0, "RIFF");
326
+ view.setUint32(4, 36 + samples.length * 2, true);
327
+ writeAscii(view, 8, "WAVE");
328
+ writeAscii(view, 12, "fmt ");
329
+ view.setUint32(16, 16, true);
330
+ view.setUint16(20, 1, true);
331
+ view.setUint16(22, 1, true);
332
+ view.setUint32(24, sampleRate, true);
333
+ view.setUint32(28, sampleRate * 2, true);
334
+ view.setUint16(32, 2, true);
335
+ view.setUint16(34, 16, true);
336
+ writeAscii(view, 36, "data");
337
+ view.setUint32(40, samples.length * 2, true);
338
+ let offset = 44;
339
+ for (const sample of samples) {
340
+ const clamped = Math.max(-1, Math.min(1, sample));
341
+ view.setInt16(offset, clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff, true);
342
+ offset += 2;
343
+ }
344
+ return new Blob([buffer], { type: "audio/wav" });
345
+ }
346
+
347
+ function writeAscii(view, offset, text) {
348
+ for (let i = 0; i < text.length; i++) view.setUint8(offset + i, text.charCodeAt(i));
349
+ }
350
+
351
+ async function runEndToEnd() {
352
+ if (!(await transcribeAudio())) return;
353
+ if (!(await generateResponse())) return;
354
+ await generateAudio();
355
+ }
356
+
357
+ async function startRecording() {
358
+ if (!navigator.mediaDevices?.getUserMedia) {
359
+ setStatus("Microphone recording is not supported in this browser.", true);
360
+ return;
361
+ }
362
+ selectedAudioFile = null;
363
+ recordedAudio = null;
364
+ els.audioFile.value = "";
365
+ els.audioName.textContent = "Recording...";
366
+ mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
367
+ const chunks = [];
368
+ mediaRecorder = new MediaRecorder(mediaStream);
369
+ mediaRecorder.ondataavailable = (event) => {
370
+ if (event.data.size) chunks.push(event.data);
371
+ };
372
+ mediaRecorder.onstop = async () => {
373
+ recordedAudio = new Blob(chunks, { type: mediaRecorder.mimeType || "audio/webm" });
374
+ selectedAudioFile = null;
375
+ mediaStream.getTracks().forEach((track) => track.stop());
376
+ mediaStream = null;
377
+ els.recordBtn.textContent = "Record Audio";
378
+ els.audioName.textContent = `Recorded audio (${Math.round(recordedAudio.size / 1024)} KB).`;
379
+ if (els.autoE2E.checked) await runEndToEnd();
380
+ };
381
+ mediaRecorder.start();
382
+ els.recordBtn.textContent = "Stop Recording";
383
+ }
384
+
385
+ function stopRecording() {
386
+ if (mediaRecorder?.state === "recording") mediaRecorder.stop();
387
+ }
388
+
389
+ function clearAll() {
390
+ selectedAudioFile = null;
391
+ recordedAudio = null;
392
+ els.audioFile.value = "";
393
+ els.audioName.textContent = "No audio selected.";
394
+ els.textInput.value = "";
395
+ els.textOutput.value = "";
396
+ resetAudioOutput();
397
+ currentAudioChunks = [];
398
+ els.audioState.textContent = "Idle";
399
+ setStatus("Cleared.");
400
+ }
401
+
402
+ function drawVisualizer() {
403
+ requestAnimationFrame(drawVisualizer);
404
+ const canvas = els.visualizer;
405
+ const ctx = canvas.getContext("2d");
406
+ const width = canvas.width;
407
+ const height = canvas.height;
408
+ ctx.clearRect(0, 0, width, height);
409
+ ctx.fillStyle = "#101827";
410
+ ctx.fillRect(0, 0, width, height);
411
+
412
+ if (!player?.analyser) {
413
+ ctx.fillStyle = "#334155";
414
+ ctx.fillRect(0, height / 2 - 1, width, 2);
415
+ return;
416
+ }
417
+
418
+ const data = new Uint8Array(player.analyser.frequencyBinCount);
419
+ player.analyser.getByteFrequencyData(data);
420
+ const bars = 80;
421
+ const barWidth = width / bars;
422
+ ctx.fillStyle = "#7fffd4";
423
+ for (let i = 0; i < bars; i++) {
424
+ const value = data[Math.floor((i * data.length) / bars)] / 255;
425
+ const barHeight = Math.max(2, value * height * 0.8);
426
+ ctx.fillRect(i * barWidth + 1, height - barHeight, Math.max(1, barWidth - 2), barHeight);
427
+ }
428
+ }
429
+
430
+ function attachEvents() {
431
+ els.audioFile.addEventListener("change", async () => {
432
+ selectedAudioFile = els.audioFile.files[0] || null;
433
+ recordedAudio = null;
434
+ els.audioName.textContent = selectedAudioFile ? selectedAudioFile.name : "No audio selected.";
435
+ if (selectedAudioFile && els.autoE2E.checked) await runEndToEnd();
436
+ });
437
+ els.recordBtn.addEventListener("click", async () => {
438
+ if (mediaRecorder?.state === "recording") stopRecording();
439
+ else await startRecording();
440
+ });
441
+ els.transcribeBtn.addEventListener("click", transcribeAudio);
442
+ els.generateResponseBtn.addEventListener("click", generateResponse);
443
+ els.generateAudioBtn.addEventListener("click", generateAudio);
444
+ els.clearBtn.addEventListener("click", clearAll);
445
+ }
446
+
447
+ async function init() {
448
+ attachEvents();
449
+ drawVisualizer();
450
+ try {
451
+ await loadConfig();
452
+ window.setInterval(pollStatus, 2000);
453
+ } catch (error) {
454
+ setStatus(`Initialization failed: ${error.message}`, true);
455
+ }
456
+ }
457
+
458
+ init();