MeysamSh commited on
Commit
0b3fb1f
·
1 Parent(s): 4fcb7f8

change interface to visualize the wavform

Browse files
Files changed (2) hide show
  1. Web/index.html +44 -13
  2. Web/script.js +31 -351
Web/index.html CHANGED
@@ -16,12 +16,43 @@
16
  body { background-color: #f8f9fa; padding: 20px; }
17
  .container { max-width: 900px; margin: 0 auto; background:#fff; padding:30px; border-radius:10px; box-shadow:0 4px 6px rgba(0,0,0,0.1); }
18
  h1 { text-align:center; margin-bottom:20px; color:#333; font-weight:bold; }
19
- .wave-item { margin-bottom: 18px; padding: 12px; border-radius: 8px; background: #fbfbfb; border: 1px solid #eee; }
20
- .wave-controls { display:flex; gap:8px; align-items:center; margin-top:8px; flex-wrap:wrap; }
21
- .wave-canvas { width:100%; height:80px; }
22
- .file-title { font-weight:600; margin-bottom:6px; color:#222; }
23
- .btn-small { padding: .25rem .5rem; font-size:.85rem; }
24
- #recordingsList .list-group-item { display:block; padding:0; border:none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  .metadata, .response { margin-top:20px; padding:15px; border-radius:5px; }
26
  .metadata { background:#f1f3f4; }
27
  .response { background:#e9ecef; }
@@ -53,6 +84,12 @@
53
  <p class="text-center"><strong>Recordings & Uploads:</strong></p>
54
  <ol id="recordingsList" class="list-group"></ol>
55
 
 
 
 
 
 
 
56
  <div class="metadata mt-4">
57
  <h3>File Metadata</h3>
58
  <div class="mb-3 d-flex flex-wrap gap-3">
@@ -70,16 +107,10 @@
70
  <div id="metadata-display"></div>
71
  </div>
72
 
73
- <div class="response mt-4">
74
- <h3>Analysis Results</h3>
75
- <div id="response"></div>
76
- </div>
77
  </div>
78
 
79
- <!-- Recorder.js (must be present in /Web/recorder.js) -->
80
  <script src="/static/recorder.js"></script>
81
-
82
- <!-- Your main script -->
83
  <script src="/static/script.js"></script>
84
  </body>
85
  </html>
 
16
  body { background-color: #f8f9fa; padding: 20px; }
17
  .container { max-width: 900px; margin: 0 auto; background:#fff; padding:30px; border-radius:10px; box-shadow:0 4px 6px rgba(0,0,0,0.1); }
18
  h1 { text-align:center; margin-bottom:20px; color:#333; font-weight:bold; }
19
+
20
+ .wave-item {
21
+ margin-bottom: 18px;
22
+ padding: 12px;
23
+ border-radius: 8px;
24
+ background: #fbfbfb;
25
+ border: 1px solid #eee;
26
+ }
27
+
28
+ .wave-controls {
29
+ display:flex;
30
+ gap:10px;
31
+ align-items:center;
32
+ margin-top:8px;
33
+ flex-wrap:wrap;
34
+ }
35
+
36
+ .remove-btn {
37
+ background: #dc3545;
38
+ border: none;
39
+ padding: 4px 10px;
40
+ color: white;
41
+ border-radius: 5px;
42
+ cursor: pointer;
43
+ font-size: 0.8rem;
44
+ }
45
+
46
+ .remove-btn:hover {
47
+ background: #b52b38;
48
+ }
49
+
50
+ #recordingsList .list-group-item {
51
+ display:block;
52
+ padding:0;
53
+ border:none;
54
+ }
55
+
56
  .metadata, .response { margin-top:20px; padding:15px; border-radius:5px; }
57
  .metadata { background:#f1f3f4; }
58
  .response { background:#e9ecef; }
 
84
  <p class="text-center"><strong>Recordings & Uploads:</strong></p>
85
  <ol id="recordingsList" class="list-group"></ol>
86
 
87
+ <!-- MOVED ABOVE METADATA -->
88
+ <div class="response mt-4">
89
+ <h3>Analysis Results</h3>
90
+ <div id="response"></div>
91
+ </div>
92
+
93
  <div class="metadata mt-4">
94
  <h3>File Metadata</h3>
95
  <div class="mb-3 d-flex flex-wrap gap-3">
 
107
  <div id="metadata-display"></div>
108
  </div>
109
 
 
 
 
 
110
  </div>
111
 
112
+ <!-- Recorder.js -->
113
  <script src="/static/recorder.js"></script>
 
 
114
  <script src="/static/script.js"></script>
115
  </body>
116
  </html>
Web/script.js CHANGED
@@ -1,214 +1,11 @@
1
- // Web/script.js
2
- // Requires: recorder.js included in page and wavesurfer.js (CDN in index.html)
3
 
4
- const uploadButton = document.getElementById('upload-button');
5
- const audioFileInput = document.getElementById('audio-file');
6
- const recordButton = document.getElementById('recordButton');
7
- const stopButton = document.getElementById('stopButton');
8
- const pauseButton = document.getElementById('pauseButton');
9
- const responseDiv = document.getElementById('response');
10
- const metadataDisplay = document.getElementById('metadata-display');
11
- const recordingsList = document.getElementById('recordingsList');
12
-
13
- let gumStream = null;
14
- let rec = null;
15
- let input = null;
16
- let audioContext = null;
17
-
18
- // initialize audio context
19
- function startAudioContext() {
20
- if (!audioContext) {
21
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
22
- } else if (audioContext.state === 'suspended') {
23
- audioContext.resume();
24
- }
25
- }
26
-
27
- // ---- Resampling helpers (kept from your original code) ----
28
- async function resampleAudio(blob, targetSampleRate = 16000) {
29
- return new Promise((resolve, reject) => {
30
- const reader = new FileReader();
31
- reader.onload = async () => {
32
- const aCtx = new (window.OfflineAudioContext || window.webkitOfflineAudioContext || window.AudioContext)(1, 2, targetSampleRate);
33
- const baseCtx = new (window.AudioContext || window.webkitAudioContext)();
34
- try {
35
- const decoded = await baseCtx.decodeAudioData(reader.result.slice(0));
36
- const offline = new OfflineAudioContext(decoded.numberOfChannels, Math.ceil(decoded.length * targetSampleRate / decoded.sampleRate), targetSampleRate);
37
- const src = offline.createBufferSource();
38
- src.buffer = decoded;
39
- src.connect(offline.destination);
40
- src.start(0);
41
- const rendered = await offline.startRendering();
42
- const wavBlob = bufferToWav(rendered);
43
- resolve(wavBlob);
44
- } catch (err) {
45
- reject(err);
46
- }
47
- };
48
- reader.onerror = reject;
49
- reader.readAsArrayBuffer(blob);
50
- });
51
- }
52
-
53
- function bufferToWav(buffer) {
54
- const numChannels = buffer.numberOfChannels;
55
- const sampleRate = buffer.sampleRate;
56
- const length = buffer.length * numChannels;
57
- const samples = new Float32Array(length);
58
- for (let ch = 0; ch < numChannels; ch++) {
59
- const channelData = buffer.getChannelData(ch);
60
- for (let i = 0; i < channelData.length; i++) {
61
- samples[i * numChannels + ch] = channelData[i];
62
- }
63
- }
64
- return encodeWAV(samples, sampleRate, numChannels);
65
- }
66
-
67
- function encodeWAV(samples, sampleRate, numChannels) {
68
- const buffer = new ArrayBuffer(44 + samples.length * 2);
69
- const view = new DataView(buffer);
70
- function writeString(view, offset, string) {
71
- for (let i = 0; i < string.length; i++) {
72
- view.setUint8(offset + i, string.charCodeAt(i));
73
- }
74
- }
75
- writeString(view, 0, 'RIFF');
76
- view.setUint32(4, 36 + samples.length * 2, true);
77
- writeString(view, 8, 'WAVE');
78
- writeString(view, 12, 'fmt ');
79
- view.setUint32(16, 16, true);
80
- view.setUint16(20, 1, true);
81
- view.setUint16(22, numChannels, true);
82
- view.setUint32(24, sampleRate, true);
83
- view.setUint32(28, sampleRate * numChannels * 2, true);
84
- view.setUint16(32, numChannels * 2, true);
85
- view.setUint16(34, 16, true);
86
- writeString(view, 36, 'data');
87
- view.setUint32(40, samples.length * 2, true);
88
- floatTo16BitPCM(view, 44, samples);
89
- return new Blob([view], { type: 'audio/wav' });
90
- }
91
-
92
- function floatTo16BitPCM(view, offset, input) {
93
- for (let i = 0; i < input.length; i++, offset += 2) {
94
- let s = Math.max(-1, Math.min(1, input[i]));
95
- view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
96
- }
97
- }
98
-
99
- // ---------------- Metadata functions (kept with minimal refactor) ----------------
100
- async function fetchMetadata() {
101
- try {
102
- const response = await fetch('/static/metadata.txt');
103
- if (!response.ok) throw new Error('Failed to fetch metadata');
104
- const text = await response.text();
105
- const lines = text.split('\n').map(l => l.trim()).filter(l => l !== '');
106
- if (lines.length < 2) return [];
107
- const headers = lines[0].split(';').map(h => h.trim().toLowerCase());
108
- const metadata = lines.slice(1).map(line => {
109
- const values = line.split(';').map(v => v.trim());
110
- const entry = {};
111
- headers.forEach((h, i) => entry[h] = values[i] || 'N/A');
112
- return entry;
113
- });
114
- return metadata;
115
- } catch (err) {
116
- console.warn('fetchMetadata failed:', err);
117
- return [];
118
- }
119
- }
120
-
121
- function populateFilters() {
122
- const predefinedValues = {
123
- label: ["spoof", "genuine"],
124
- system: ["bonafide"].concat(Array.from({ length: 19 }, (_, i) => `A${String(i + 1).padStart(2, '0')}`)),
125
- codec: ["FLAC", "WAV", "MP3"],
126
- genre: ["male", "female"],
127
- year: ["2020", "2021", "2022", "2023", "2024", "2025"]
128
- };
129
- Object.keys(predefinedValues).forEach(key => {
130
- populateDropdown(`filter-${key}`, predefinedValues[key]);
131
- });
132
- }
133
-
134
- function populateDropdown(id, values) {
135
- const select = document.getElementById(id);
136
- if (!select) return;
137
- select.innerHTML = '<option value="">All</option>';
138
- values.forEach(v => {
139
- const option = document.createElement('option');
140
- option.value = v;
141
- option.textContent = v.charAt(0).toUpperCase() + v.slice(1);
142
- select.appendChild(option);
143
- });
144
- select.addEventListener('change', filterMetadata);
145
- }
146
-
147
- function displayMetadata(files, metadata, filteredOnly = false) {
148
- metadataDisplay.innerHTML = '';
149
- if (!filteredOnly && (!files || files.length === 0)) {
150
- metadataDisplay.innerHTML = '<p>No files selected.</p>';
151
- return;
152
- }
153
- let filteredMetadata;
154
- if (filteredOnly) {
155
- const sLabel = document.getElementById("filter-label").value.toLowerCase();
156
- const sSystem = document.getElementById("filter-system").value.toLowerCase();
157
- const sCodec = document.getElementById("filter-codec").value.toLowerCase();
158
- const sGenre = document.getElementById("filter-genre").value.toLowerCase();
159
- const sYear = document.getElementById("filter-year").value.toLowerCase();
160
- filteredMetadata = metadata.filter(entry =>
161
- (sLabel === "" || (entry.label && entry.label.toLowerCase() === sLabel)) &&
162
- (sSystem === "" || (entry.system && entry.system.toLowerCase() === sSystem)) &&
163
- (sCodec === "" || (entry.codec && entry.codec.toLowerCase() === sCodec)) &&
164
- (sGenre === "" || (entry.genre && entry.genre.toLowerCase() === sGenre)) &&
165
- (sYear === "" || (entry.year && entry.year.toLowerCase() === sYear))
166
- );
167
- } else {
168
- const selectedFiles = Array.from(files).map(f => f.name.trim().toLowerCase());
169
- filteredMetadata = metadata.filter(entry => selectedFiles.includes(entry.filedir.trim().toLowerCase()));
170
- }
171
- if (!filteredMetadata || filteredMetadata.length === 0) {
172
- metadataDisplay.innerHTML = '<p>No metadata found.</p>';
173
- return;
174
- }
175
- const table = document.createElement('table');
176
- table.className = 'table table-striped table-bordered';
177
- const headerRow = document.createElement('tr');
178
- Object.keys(filteredMetadata[0]).forEach(h => {
179
- const th = document.createElement('th'); th.textContent = h.charAt(0).toUpperCase() + h.slice(1); headerRow.appendChild(th);
180
- });
181
- table.appendChild(headerRow);
182
- filteredMetadata.forEach(entry => {
183
- const row = document.createElement('tr');
184
- Object.values(entry).forEach(val => { const td = document.createElement('td'); td.textContent = val; row.appendChild(td); });
185
- table.appendChild(row);
186
- });
187
- metadataDisplay.appendChild(table);
188
- }
189
-
190
- document.addEventListener('DOMContentLoaded', async () => {
191
- populateFilters();
192
- const metadata = await fetchMetadata();
193
- displayMetadata(null, metadata, true);
194
- });
195
-
196
- // ---------------- Waveform / UI helpers ----------------
197
-
198
- // create a unique id
199
- function uid(prefix='id') {
200
- return prefix + '_' + Math.random().toString(36).slice(2,9);
201
- }
202
-
203
- // create waveform card for a blob or file
204
  function createWaveformItem({ blob, filename, filetype, origin = 'upload' }) {
205
  const id = uid('wave');
206
 
207
- // List item
208
  const li = document.createElement('li');
209
  li.className = 'list-group-item';
210
 
211
- // Card wrapper
212
  const wrapper = document.createElement('div');
213
  wrapper.className = 'wave-item';
214
 
@@ -216,12 +13,10 @@ function createWaveformItem({ blob, filename, filetype, origin = 'upload' }) {
216
  title.className = 'file-title';
217
  title.textContent = filename || 'Audio';
218
 
219
- // waveform container
220
  const canvasDiv = document.createElement('div');
221
  canvasDiv.className = 'wave-canvas';
222
  canvasDiv.id = id;
223
 
224
- // controls
225
  const controls = document.createElement('div');
226
  controls.className = 'wave-controls';
227
 
@@ -236,9 +31,18 @@ function createWaveformItem({ blob, filename, filetype, origin = 'upload' }) {
236
  const downloadBtn = document.createElement('a');
237
  downloadBtn.className = 'btn btn-sm btn-outline-success btn-small';
238
  downloadBtn.textContent = 'Download';
239
- downloadBtn.href = URL.createObjectURL(blob);
 
240
  downloadBtn.download = filename || 'audio.wav';
241
 
 
 
 
 
 
 
 
 
242
  const analyzeBtn = document.createElement('button');
243
  analyzeBtn.className = 'btn btn-sm btn-info btn-small';
244
  analyzeBtn.textContent = 'Analyze';
@@ -246,6 +50,10 @@ function createWaveformItem({ blob, filename, filetype, origin = 'upload' }) {
246
  controls.appendChild(playBtn);
247
  controls.appendChild(pauseBtn);
248
  controls.appendChild(downloadBtn);
 
 
 
 
249
  controls.appendChild(analyzeBtn);
250
 
251
  wrapper.appendChild(title);
@@ -253,9 +61,8 @@ function createWaveformItem({ blob, filename, filetype, origin = 'upload' }) {
253
  wrapper.appendChild(controls);
254
 
255
  li.appendChild(wrapper);
256
- recordingsList.prepend(li); // newest first
257
 
258
- // instantiate wavesurfer on the container
259
  const ws = WaveSurfer.create({
260
  container: `#${id}`,
261
  waveColor: '#8ab4f8',
@@ -268,20 +75,22 @@ function createWaveformItem({ blob, filename, filetype, origin = 'upload' }) {
268
 
269
  ws.loadBlob(blob);
270
 
271
- // wire buttons
272
  playBtn.addEventListener('click', () => ws.play());
273
  pauseBtn.addEventListener('click', () => ws.pause());
 
274
  ws.on('finish', () => { playBtn.textContent = 'Play'; });
275
  ws.on('play', () => { playBtn.textContent = 'Playing'; });
276
  ws.on('pause', () => { playBtn.textContent = 'Play'; });
277
 
278
  analyzeBtn.addEventListener('click', async () => {
279
- // send this blob to API for analysis
280
  responseDiv.textContent = 'Analyzing ' + (filename || 'file') + ' ...';
281
  try {
282
  const res = await sendBlobToAPI(blob, filename);
283
  if (Array.isArray(res) && res.length > 0) {
284
- responseDiv.innerHTML = `File: <b>${res[0].filename || filename}</b>, Label: <b>${res[0].label}</b>, Confidence: <b>${res[0].confidence}</b>`;
 
 
 
285
  } else {
286
  responseDiv.textContent = 'No response from API';
287
  }
@@ -290,144 +99,15 @@ function createWaveformItem({ blob, filename, filetype, origin = 'upload' }) {
290
  }
291
  });
292
 
293
- return { listItem: li, wavesurfer: ws };
294
- }
295
-
296
- // send a Blob to backend predict endpoint
297
- async function sendBlobToAPI(blob, filename = 'file.wav') {
298
- const formData = new FormData();
299
- formData.append('files', blob, filename);
300
- const resp = await fetch('http://127.0.0.1:8000/predict/', { method: 'POST', body: formData });
301
- if (!resp.ok) {
302
- const txt = await resp.text();
303
- throw new Error('Server error: ' + resp.status + ' - ' + txt);
304
- }
305
- return resp.json();
306
- }
307
-
308
- // handle file uploads -> create waveform items (and optionally auto-analyze)
309
- audioFileInput.addEventListener('change', async (e) => {
310
- const files = Array.from(e.target.files || []);
311
- if (!files.length) return;
312
- const metadata = await fetchMetadata();
313
- displayMetadata(files, metadata, false);
314
-
315
- for (const f of files) {
316
- // convert to blob (it's already blob/file)
317
- const blob = f;
318
- // create waveform card
319
- createWaveformItem({ blob, filename: f.name, filetype: f.type, origin: 'upload' });
320
- }
321
- });
322
-
323
- // upload & analyze button (sends all current files displayed in list)
324
- uploadButton.addEventListener('click', async () => {
325
- // collect blobs from list items (download link href or wavesurfer backend)
326
- const items = Array.from(recordingsList.querySelectorAll('.wave-item'));
327
- if (items.length === 0) {
328
- alert('No files to upload/analyze. Please upload or record first.');
329
- return;
330
- }
331
-
332
- // gather blobs by reading download links
333
- const blobs = [];
334
- items.forEach(item => {
335
- const dl = item.querySelector('a[download]');
336
- if (dl) {
337
- // reconstruct blob from object URL is not possible; better to re-create from wavesurfer peaks via export?
338
- // Simpler approach: when creating each item we stored blob via closure. We'll instead keep a weak map.
339
- }
340
  });
 
341
 
342
- // Simpler: ask user to select files via the file input for bulk upload; otherwise analyze each displayed item individually:
343
- const list = recordingsList.querySelectorAll('.wave-item');
344
- if (list.length === 0) return;
345
- responseDiv.textContent = 'Uploading and analyzing audio...';
346
- // iterate and click each Analyze button sequentially
347
- for (const item of list) {
348
- const analyzeBtn = item.querySelector('button.btn-info');
349
- if (analyzeBtn) analyzeBtn.click();
350
- // slight delay between requests to avoid flooding
351
- await new Promise(r => setTimeout(r, 500));
352
- }
353
- });
354
-
355
- // ---------------- Recording logic (cleaned, single set of handlers) ----------------
356
- recordButton.addEventListener('click', async () => {
357
- startAudioContext();
358
- const constraints = { audio: true };
359
- try {
360
- gumStream = await navigator.mediaDevices.getUserMedia(constraints);
361
- input = audioContext.createMediaStreamSource(gumStream);
362
- rec = new Recorder(input, { numChannels: 1 });
363
- rec.record();
364
- recordButton.disabled = true;
365
- stopButton.disabled = false;
366
- pauseButton.disabled = false;
367
- pauseButton.textContent = 'Pause';
368
- } catch (err) {
369
- alert('Microphone access denied: ' + (err.message || err));
370
- }
371
- });
372
-
373
- pauseButton.addEventListener('click', () => {
374
- if (!rec) return;
375
- if (rec.recording) {
376
- rec.stop();
377
- pauseButton.textContent = 'Resume';
378
- } else {
379
- rec.record();
380
- pauseButton.textContent = 'Pause';
381
- }
382
- });
383
-
384
- stopButton.addEventListener('click', () => {
385
- if (!rec) return;
386
- stopButton.disabled = true;
387
- pauseButton.disabled = true;
388
- recordButton.disabled = false;
389
- rec.stop();
390
- // stop tracks
391
- if (gumStream && gumStream.getAudioTracks && gumStream.getAudioTracks()[0]) {
392
- gumStream.getAudioTracks()[0].stop();
393
- }
394
-
395
- rec.exportWAV(async (blob) => {
396
- // sanity check
397
- if (!blob || blob.size === 0) {
398
- responseDiv.textContent = 'Error: recorded file is empty.';
399
- return;
400
- }
401
-
402
- // create waveform from original blob (also resample for sending)
403
- try {
404
- // show original waveform
405
- const item = createWaveformItem({ blob, filename: `recording_${new Date().toISOString()}.wav`, origin: 'record' });
406
- // resample to 16kHz for API and attach analyze behavior to that blob (we will override analyze button behavior to use resampled blob)
407
- const resampledBlob = await resampleAudio(blob, 16000);
408
- // patch the analyze button to send resampledBlob instead of original
409
- const analyzeBtn = item.listItem.querySelector('button.btn-info');
410
- analyzeBtn.onclick = async () => {
411
- responseDiv.textContent = 'Analyzing recorded audio...';
412
- try {
413
- const res = await sendBlobToAPI(resampledBlob, analyzeBtn ? analyzeBtn.dataset?.filename : 'recording.wav');
414
- if (Array.isArray(res) && res.length > 0) {
415
- responseDiv.innerHTML = `File: <b>${res[0].filename || 'recording'}</b>, Label: <b>${res[0].label}</b>, Confidence: <b>${res[0].confidence}</b>`;
416
- } else {
417
- responseDiv.textContent = 'No response from API';
418
- }
419
- } catch (err) {
420
- responseDiv.textContent = 'Error: ' + err.message;
421
- }
422
- };
423
- } catch (err) {
424
- console.error('Resample or create waveform failed:', err);
425
- responseDiv.textContent = 'Error processing recording: ' + err.message;
426
- }
427
- });
428
- });
429
-
430
- // ---------------- Utility: attach analyze to uploaded file items so they use original blob ----------------
431
- // Note: createWaveformItem already wires analyze to send the provided blob; so uploaded files will analyze original file
432
-
433
- // End of script
 
1
+ // ... (everything above stays unchanged)
 
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  function createWaveformItem({ blob, filename, filetype, origin = 'upload' }) {
4
  const id = uid('wave');
5
 
 
6
  const li = document.createElement('li');
7
  li.className = 'list-group-item';
8
 
 
9
  const wrapper = document.createElement('div');
10
  wrapper.className = 'wave-item';
11
 
 
13
  title.className = 'file-title';
14
  title.textContent = filename || 'Audio';
15
 
 
16
  const canvasDiv = document.createElement('div');
17
  canvasDiv.className = 'wave-canvas';
18
  canvasDiv.id = id;
19
 
 
20
  const controls = document.createElement('div');
21
  controls.className = 'wave-controls';
22
 
 
31
  const downloadBtn = document.createElement('a');
32
  downloadBtn.className = 'btn btn-sm btn-outline-success btn-small';
33
  downloadBtn.textContent = 'Download';
34
+ const blobUrl = URL.createObjectURL(blob);
35
+ downloadBtn.href = blobUrl;
36
  downloadBtn.download = filename || 'audio.wav';
37
 
38
+ // ----------------------------
39
+ // ⭐ NEW: REMOVE BUTTON
40
+ // ----------------------------
41
+ const removeBtn = document.createElement("button");
42
+ removeBtn.className = "remove-btn";
43
+ removeBtn.innerText = "Remove";
44
+ // ----------------------------
45
+
46
  const analyzeBtn = document.createElement('button');
47
  analyzeBtn.className = 'btn btn-sm btn-info btn-small';
48
  analyzeBtn.textContent = 'Analyze';
 
50
  controls.appendChild(playBtn);
51
  controls.appendChild(pauseBtn);
52
  controls.appendChild(downloadBtn);
53
+
54
+ // ADD REMOVE BUTTON TO CONTROLS
55
+ controls.appendChild(removeBtn);
56
+
57
  controls.appendChild(analyzeBtn);
58
 
59
  wrapper.appendChild(title);
 
61
  wrapper.appendChild(controls);
62
 
63
  li.appendChild(wrapper);
64
+ recordingsList.prepend(li);
65
 
 
66
  const ws = WaveSurfer.create({
67
  container: `#${id}`,
68
  waveColor: '#8ab4f8',
 
75
 
76
  ws.loadBlob(blob);
77
 
 
78
  playBtn.addEventListener('click', () => ws.play());
79
  pauseBtn.addEventListener('click', () => ws.pause());
80
+
81
  ws.on('finish', () => { playBtn.textContent = 'Play'; });
82
  ws.on('play', () => { playBtn.textContent = 'Playing'; });
83
  ws.on('pause', () => { playBtn.textContent = 'Play'; });
84
 
85
  analyzeBtn.addEventListener('click', async () => {
 
86
  responseDiv.textContent = 'Analyzing ' + (filename || 'file') + ' ...';
87
  try {
88
  const res = await sendBlobToAPI(blob, filename);
89
  if (Array.isArray(res) && res.length > 0) {
90
+ responseDiv.innerHTML =
91
+ `File: <b>${res[0].filename || filename}</b>, ` +
92
+ `Label: <b>${res[0].label}</b>, ` +
93
+ `Confidence: <b>${res[0].confidence}</b>`;
94
  } else {
95
  responseDiv.textContent = 'No response from API';
96
  }
 
99
  }
100
  });
101
 
102
+ // -----------------------------------------------------------
103
+ // ⭐ NEW: REMOVE FUNCTIONALITY — ERASES THIS ENTIRE WAVE CARD
104
+ // -----------------------------------------------------------
105
+ removeBtn.addEventListener("click", () => {
106
+ try { ws.destroy(); } catch (e) {}
107
+ URL.revokeObjectURL(blobUrl);
108
+ li.remove();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  });
110
+ // -----------------------------------------------------------
111
 
112
+ return { listItem: li, wavesurfer: ws };
113
+ }