Spaces:
Sleeping
Sleeping
change interface to visualize the wavform
Browse files- Web/index.html +119 -82
- Web/script.js +190 -102
Web/index.html
CHANGED
|
@@ -1,116 +1,153 @@
|
|
| 1 |
-
<!
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
-
<meta charset="
|
| 5 |
-
<meta name="viewport" content="width=device-width,
|
| 6 |
-
<title>
|
| 7 |
-
<link rel="stylesheet" href="/static/styles.css" />
|
| 8 |
|
| 9 |
-
<!-- Bootstrap
|
| 10 |
-
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.
|
| 11 |
|
| 12 |
-
<!--
|
| 13 |
<script src="https://unpkg.com/wavesurfer.js"></script>
|
| 14 |
|
| 15 |
<style>
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
margin-bottom: 18px;
|
| 22 |
-
padding: 12px;
|
| 23 |
-
border-radius: 8px;
|
| 24 |
-
background: #fbfbfb;
|
| 25 |
-
border: 1px solid #eee;
|
| 26 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
|
|
|
|
| 29 |
display:flex;
|
| 30 |
-
gap:
|
| 31 |
align-items:center;
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
.
|
| 37 |
-
background:
|
| 38 |
border: none;
|
| 39 |
-
|
| 40 |
-
color: white;
|
| 41 |
-
border-radius: 5px;
|
| 42 |
-
cursor: pointer;
|
| 43 |
-
font-size: 0.8rem;
|
| 44 |
}
|
| 45 |
|
| 46 |
-
.
|
| 47 |
-
background: #b52b38;
|
| 48 |
-
}
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
border:none;
|
| 54 |
-
}
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
| 59 |
</style>
|
| 60 |
</head>
|
| 61 |
-
|
| 62 |
<body>
|
| 63 |
-
<div class="
|
| 64 |
-
<
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
-
|
|
|
|
| 68 |
<div class="mb-3">
|
| 69 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
</div>
|
| 71 |
-
<button type="button" id="upload-button" class="btn btn-primary w-100">Upload & Analyze</button>
|
| 72 |
-
</form>
|
| 73 |
-
|
| 74 |
-
<hr>
|
| 75 |
-
|
| 76 |
-
<div id="controls" class="mb-4 text-center">
|
| 77 |
-
<button id="recordButton" class="btn btn-success">Record</button>
|
| 78 |
-
<button id="pauseButton" class="btn btn-warning" disabled>Pause</button>
|
| 79 |
-
<button id="stopButton" class="btn btn-danger" disabled>Stop</button>
|
| 80 |
</div>
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
-
<!--
|
| 88 |
-
<div class="
|
| 89 |
-
<
|
| 90 |
-
<
|
| 91 |
</div>
|
| 92 |
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
<div class="
|
| 96 |
-
<
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
<
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
</div>
|
| 107 |
-
<div id="metadata-display"></div>
|
| 108 |
</div>
|
| 109 |
-
|
| 110 |
</div>
|
| 111 |
|
| 112 |
-
<!--
|
| 113 |
-
|
| 114 |
-
<script src="/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
</body>
|
| 116 |
-
</html>
|
|
|
|
| 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" />
|
| 6 |
+
<title>Speech DeepFake detection </title>
|
|
|
|
| 7 |
|
| 8 |
+
<!-- Bootstrap for quick UI -->
|
| 9 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 10 |
|
| 11 |
+
<!-- WaveSurfer -->
|
| 12 |
<script src="https://unpkg.com/wavesurfer.js"></script>
|
| 13 |
|
| 14 |
<style>
|
| 15 |
+
:root{
|
| 16 |
+
--accent:#ff5500; /* SoundCloud-esque orange */
|
| 17 |
+
--muted:#6c757d;
|
| 18 |
+
--card-bg: #ffffff;
|
| 19 |
+
--page-bg: #fbfbfc;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
+
body{
|
| 22 |
+
background: linear-gradient(180deg, #fbfbfc 0%, #f6f7f9 100%);
|
| 23 |
+
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
|
| 24 |
+
padding: 28px;
|
| 25 |
+
color:#222;
|
| 26 |
+
}
|
| 27 |
+
.app {
|
| 28 |
+
max-width: 960px;
|
| 29 |
+
margin: 0 auto;
|
| 30 |
+
background: var(--card-bg);
|
| 31 |
+
border-radius: 12px;
|
| 32 |
+
box-shadow: 0 8px 30px rgba(34,41,47,0.06);
|
| 33 |
+
padding: 26px;
|
| 34 |
+
}
|
| 35 |
+
h1 { color:#111; font-weight:700; margin-bottom:6px; }
|
| 36 |
+
p.lead { color:var(--muted); margin-top:0; }
|
| 37 |
+
|
| 38 |
+
/* Controls */
|
| 39 |
+
.control-row { display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
|
| 40 |
+
#formats { color:var(--muted); font-size:0.95rem; }
|
| 41 |
|
| 42 |
+
/* Wave card (SoundCloud-like compact list) */
|
| 43 |
+
.wave-item{
|
| 44 |
display:flex;
|
| 45 |
+
gap:14px;
|
| 46 |
align-items:center;
|
| 47 |
+
padding:12px;
|
| 48 |
+
border-radius:10px;
|
| 49 |
+
background: linear-gradient(90deg, rgba(255,255,255,0.6), rgba(250,250,251,0.6));
|
| 50 |
+
border:1px solid #eee;
|
| 51 |
+
margin-bottom:12px;
|
| 52 |
}
|
| 53 |
+
.wave-meta { min-width:160px; max-width:220px; }
|
| 54 |
+
.file-title { font-weight:600; color:#111; margin-bottom:6px; }
|
| 55 |
+
.wave-canvas { flex:1; min-width:240px; }
|
| 56 |
+
.wave-controls{ display:flex; gap:8px; align-items:center; }
|
| 57 |
|
| 58 |
+
.btn-play{
|
| 59 |
+
background: var(--accent);
|
| 60 |
border: none;
|
| 61 |
+
color: #fff;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
}
|
| 63 |
|
| 64 |
+
.small-muted { color:var(--muted); font-size:0.9rem; }
|
|
|
|
|
|
|
| 65 |
|
| 66 |
+
/* Analysis & metadata panels */
|
| 67 |
+
.panel { margin-top:18px; padding:12px; border-radius:8px; background:#fafafa; border:1px solid #eee; }
|
| 68 |
+
.response pre{ white-space:pre-wrap; word-break:break-word; }
|
|
|
|
|
|
|
| 69 |
|
| 70 |
+
input[type="file"] { padding:4px; }
|
| 71 |
+
|
| 72 |
+
/* Make waves visually similar to SoundCloud: subtle rounded waves and orange progress */
|
| 73 |
+
.wavesurfer .wave { border-radius:4px; overflow:hidden; }
|
| 74 |
</style>
|
| 75 |
</head>
|
|
|
|
| 76 |
<body>
|
| 77 |
+
<div class="app">
|
| 78 |
+
<div class="d-flex justify-content-between align-items-start mb-3">
|
| 79 |
+
<div>
|
| 80 |
+
<h1>SoundCloud-style Recorder & Analyzer</h1>
|
| 81 |
+
<p class="lead">Record audio, upload WAV/MP3, preview as waveform, play and analyze.</p>
|
| 82 |
+
</div>
|
| 83 |
+
<div class="text-end small-muted">
|
| 84 |
+
<div>Format: <span id="formats">—</span></div>
|
| 85 |
+
<div class="mt-2">Files shown in SoundCloud-like waveform</div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
|
| 89 |
+
<!-- Upload -->
|
| 90 |
+
<div class="panel">
|
| 91 |
<div class="mb-3">
|
| 92 |
+
<label class="form-label">Upload audio files</label>
|
| 93 |
+
<input id="audio-file" type="file" class="form-control" accept="audio/*,.wav" multiple />
|
| 94 |
+
</div>
|
| 95 |
+
<div class="d-grid gap-2">
|
| 96 |
+
<button id="upload-button" type="button" class="btn btn-outline-primary">Upload & Analyze</button>
|
| 97 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
</div>
|
| 99 |
|
| 100 |
+
<!-- Recorder -->
|
| 101 |
+
<div class="panel mt-3">
|
| 102 |
+
<div class="mb-2"><strong>Record audio</strong></div>
|
| 103 |
+
<div class="control-row">
|
| 104 |
+
<button id="recordButton" class="btn btn-success">Start recording</button>
|
| 105 |
+
<button id="pauseButton" class="btn btn-warning" disabled>Pause</button>
|
| 106 |
+
<button id="stopButton" class="btn btn-danger" disabled>Stop</button>
|
| 107 |
+
<div class="small-muted ms-2">Recorded files appear below with waveform preview</div>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
|
| 111 |
+
<!-- Recordings List -->
|
| 112 |
+
<div class="mt-3">
|
| 113 |
+
<h5>Recordings & Uploads</h5>
|
| 114 |
+
<ol id="recordingsList" class="list-unstyled p-0"></ol>
|
| 115 |
</div>
|
| 116 |
|
| 117 |
+
<!-- Analysis / Metadata -->
|
| 118 |
+
<div class="row mt-3">
|
| 119 |
+
<div class="col-md-6">
|
| 120 |
+
<div class="panel response">
|
| 121 |
+
<h6>Analysis Results</h6>
|
| 122 |
+
<div id="response"><small class="small-muted">No analysis yet</small></div>
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="col-md-6">
|
| 126 |
+
<div class="panel metadata">
|
| 127 |
+
<h6>File Metadata</h6>
|
| 128 |
+
<div id="metadata-display"><small class="small-muted">Upload or record to see metadata</small></div>
|
| 129 |
+
</div>
|
| 130 |
</div>
|
|
|
|
| 131 |
</div>
|
|
|
|
| 132 |
</div>
|
| 133 |
|
| 134 |
+
<!-- Use the uploaded recorder.js and script.js files from your workspace -->
|
| 135 |
+
<!-- per your instruction we reference the uploaded file paths -->
|
| 136 |
+
<script src="/mnt/data/recorder.js"></script>
|
| 137 |
+
<script src="/mnt/data/script.js"></script>
|
| 138 |
+
|
| 139 |
+
<!-- small inline initializer to ensure script.js hooks run after DOM loads -->
|
| 140 |
+
<script>
|
| 141 |
+
// The provided script.js expects DOM elements with IDs used above.
|
| 142 |
+
// If your script.js exposes an init() or similar, call it here; otherwise it will run on load.
|
| 143 |
+
// We'll just ensure basic elements exist and (optionally) show sample rate when available.
|
| 144 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 145 |
+
const formatsEl = document.getElementById('formats');
|
| 146 |
+
if (!formatsEl) return;
|
| 147 |
+
|
| 148 |
+
// If script.js attaches the recorder and updates #formats, it will override this.
|
| 149 |
+
formatsEl.textContent = 'Ready — click Record or Upload a file';
|
| 150 |
+
});
|
| 151 |
+
</script>
|
| 152 |
</body>
|
| 153 |
+
</html>
|
Web/script.js
CHANGED
|
@@ -1,113 +1,201 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
|
|
|
| 56 |
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
wrapper.appendChild(controls);
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
progressColor: '#0d6efd',
|
| 70 |
-
height: 80,
|
| 71 |
-
responsive: true,
|
| 72 |
-
normalize: true,
|
| 73 |
-
backend: 'WebAudio'
|
| 74 |
-
});
|
| 75 |
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
-
|
| 82 |
-
ws.on('play', () => { playBtn.textContent = 'Playing'; });
|
| 83 |
-
ws.on('pause', () => { playBtn.textContent = 'Play'; });
|
| 84 |
|
| 85 |
-
|
| 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 |
-
}
|
| 97 |
} catch (err) {
|
| 98 |
-
|
|
|
|
| 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 |
-
}
|
|
|
|
| 1 |
+
/* ============================================================
|
| 2 |
+
SoundCloud-Style Audio Recorder + Waveform Display + Analyzer
|
| 3 |
+
============================================================
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
// Global variables
|
| 7 |
+
let audioContext;
|
| 8 |
+
let gumStream;
|
| 9 |
+
let rec;
|
| 10 |
+
let input;
|
| 11 |
+
|
| 12 |
+
// DOM Elements
|
| 13 |
+
const recordBtn = document.getElementById("recordButton");
|
| 14 |
+
const stopBtn = document.getElementById("stopButton");
|
| 15 |
+
const pauseBtn = document.getElementById("pauseButton");
|
| 16 |
+
const recordingsList = document.getElementById("recordingsList");
|
| 17 |
+
const uploadButton = document.getElementById("upload-button");
|
| 18 |
+
const uploadInput = document.getElementById("audio-file");
|
| 19 |
+
const responseBox = document.getElementById("response");
|
| 20 |
+
const metadataBox = document.getElementById("metadata-display");
|
| 21 |
+
|
| 22 |
+
// ==============================
|
| 23 |
+
// Recording Controls
|
| 24 |
+
// ==============================
|
| 25 |
+
|
| 26 |
+
recordBtn.addEventListener("click", startRecording);
|
| 27 |
+
stopBtn.addEventListener("click", stopRecording);
|
| 28 |
+
pauseBtn.addEventListener("click", pauseRecording);
|
| 29 |
+
|
| 30 |
+
// Start recording
|
| 31 |
+
function startRecording() {
|
| 32 |
+
console.log("Recording started...");
|
| 33 |
+
|
| 34 |
+
recordBtn.disabled = true;
|
| 35 |
+
stopBtn.disabled = false;
|
| 36 |
+
pauseBtn.disabled = false;
|
| 37 |
+
|
| 38 |
+
navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
|
| 39 |
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 40 |
+
document.getElementById("formats").innerText =
|
| 41 |
+
"Sample rate: " + audioContext.sampleRate + " Hz";
|
| 42 |
+
|
| 43 |
+
gumStream = stream;
|
| 44 |
+
input = audioContext.createMediaStreamSource(stream);
|
| 45 |
+
|
| 46 |
+
rec = new Recorder(input, { numChannels: 1 });
|
| 47 |
+
rec.record();
|
| 48 |
+
|
| 49 |
+
}).catch(function (e) {
|
| 50 |
+
console.error("Could not start recording:", e);
|
| 51 |
+
recordBtn.disabled = false;
|
| 52 |
+
stopBtn.disabled = true;
|
| 53 |
+
pauseBtn.disabled = true;
|
| 54 |
+
});
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// Pause recording
|
| 58 |
+
function pauseRecording() {
|
| 59 |
+
if (rec.recording) {
|
| 60 |
+
rec.stop();
|
| 61 |
+
pauseBtn.innerText = "Resume";
|
| 62 |
+
} else {
|
| 63 |
+
rec.record();
|
| 64 |
+
pauseBtn.innerText = "Pause";
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
|
| 68 |
+
// Stop recording
|
| 69 |
+
function stopRecording() {
|
| 70 |
+
console.log("Recording stopped.");
|
| 71 |
|
| 72 |
+
stopBtn.disabled = true;
|
| 73 |
+
recordBtn.disabled = false;
|
| 74 |
+
pauseBtn.disabled = true;
|
| 75 |
+
pauseBtn.innerText = "Pause";
|
| 76 |
|
| 77 |
+
rec.stop();
|
| 78 |
+
gumStream.getAudioTracks()[0].stop();
|
|
|
|
| 79 |
|
| 80 |
+
rec.exportWAV(createWaveformItem);
|
| 81 |
+
}
|
| 82 |
|
| 83 |
+
// ==============================
|
| 84 |
+
// File Upload
|
| 85 |
+
// ==============================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
+
uploadButton.addEventListener("click", () => {
|
| 88 |
+
if (!uploadInput.files.length) {
|
| 89 |
+
alert("Select at least one file.");
|
| 90 |
+
return;
|
| 91 |
+
}
|
| 92 |
+
Array.from(uploadInput.files).forEach((file) => handleUploadedFile(file));
|
| 93 |
+
});
|
| 94 |
+
|
| 95 |
+
function handleUploadedFile(file) {
|
| 96 |
+
const url = URL.createObjectURL(file);
|
| 97 |
+
createWaveformItem(file, url);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// ==============================
|
| 101 |
+
// Create SoundCloud-like Waveform
|
| 102 |
+
// ==============================
|
| 103 |
+
|
| 104 |
+
function createWaveformItem(blob, forcedUrl = null) {
|
| 105 |
+
const url = forcedUrl || URL.createObjectURL(blob);
|
| 106 |
+
|
| 107 |
+
const li = document.createElement("li");
|
| 108 |
+
li.className = "wave-item";
|
| 109 |
+
|
| 110 |
+
const container = document.createElement("div");
|
| 111 |
+
container.className = "wave-canvas";
|
| 112 |
+
|
| 113 |
+
const meta = document.createElement("div");
|
| 114 |
+
meta.className = "wave-meta";
|
| 115 |
+
meta.innerHTML = `<div class="file-title">${blob.name || "recording.wav"}</div>`;
|
| 116 |
+
|
| 117 |
+
// Play button
|
| 118 |
+
const playBtn = document.createElement("button");
|
| 119 |
+
playBtn.className = "btn btn-play btn-sm";
|
| 120 |
+
playBtn.innerText = "Play";
|
| 121 |
+
|
| 122 |
+
// Analyze button
|
| 123 |
+
const analyzeBtn = document.createElement("button");
|
| 124 |
+
analyzeBtn.className = "btn btn-outline-secondary btn-sm";
|
| 125 |
+
analyzeBtn.innerText = "Analyze";
|
| 126 |
+
|
| 127 |
+
const controls = document.createElement("div");
|
| 128 |
+
controls.className = "wave-controls";
|
| 129 |
+
controls.appendChild(playBtn);
|
| 130 |
+
controls.appendChild(analyzeBtn);
|
| 131 |
+
|
| 132 |
+
li.appendChild(meta);
|
| 133 |
+
li.appendChild(container);
|
| 134 |
+
li.appendChild(controls);
|
| 135 |
+
recordingsList.appendChild(li);
|
| 136 |
+
|
| 137 |
+
// Create WaveSurfer
|
| 138 |
+
const wavesurfer = WaveSurfer.create({
|
| 139 |
+
container,
|
| 140 |
+
waveColor: "#bbb",
|
| 141 |
+
progressColor: "#ff5500",
|
| 142 |
+
cursorColor: "#333",
|
| 143 |
+
barWidth: 2,
|
| 144 |
+
height: 70,
|
| 145 |
+
responsive: true,
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
wavesurfer.load(url);
|
| 149 |
+
|
| 150 |
+
// Toggle play/pause
|
| 151 |
+
playBtn.addEventListener("click", () => {
|
| 152 |
+
wavesurfer.playPause();
|
| 153 |
+
playBtn.innerText = wavesurfer.isPlaying() ? "Pause" : "Play";
|
| 154 |
+
});
|
| 155 |
+
|
| 156 |
+
// Analyze
|
| 157 |
+
analyzeBtn.addEventListener("click", () => {
|
| 158 |
+
analyzeAudio(blob);
|
| 159 |
+
});
|
| 160 |
+
|
| 161 |
+
// Metadata
|
| 162 |
+
blob.arrayBuffer().then((buffer) => {
|
| 163 |
+
const ctx = new AudioContext();
|
| 164 |
+
ctx.decodeAudioData(buffer).then((decoded) => {
|
| 165 |
+
metadataBox.innerHTML = `
|
| 166 |
+
<strong>Channels:</strong> ${decoded.numberOfChannels}<br>
|
| 167 |
+
<strong>Duration:</strong> ${decoded.duration.toFixed(2)}s<br>
|
| 168 |
+
<strong>Sample Rate:</strong> ${decoded.sampleRate} Hz
|
| 169 |
+
`;
|
| 170 |
+
});
|
| 171 |
+
});
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// ==============================
|
| 175 |
+
// Audio Analysis API (placeholder)
|
| 176 |
+
// ==============================
|
| 177 |
+
|
| 178 |
+
async function analyzeAudio(blob) {
|
| 179 |
+
responseBox.innerHTML = "<em>Analyzing...</em>";
|
| 180 |
+
|
| 181 |
+
// Convert Blob to File
|
| 182 |
+
const file = new File([blob], blob.name || "audio.wav", { type: blob.type });
|
| 183 |
+
|
| 184 |
+
const formData = new FormData();
|
| 185 |
+
formData.append("file", file);
|
| 186 |
|
| 187 |
+
try {
|
| 188 |
+
// Replace the URL with your real backend endpoint
|
| 189 |
+
const res = await fetch("http://localhost:7860/predict", {
|
| 190 |
+
method: "POST",
|
| 191 |
+
body: formData,
|
| 192 |
+
});
|
| 193 |
|
| 194 |
+
const data = await res.json();
|
|
|
|
|
|
|
| 195 |
|
| 196 |
+
responseBox.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
} catch (err) {
|
| 198 |
+
responseBox.innerHTML = `<span style="color:red;">Analysis failed.</span>`;
|
| 199 |
+
console.error(err);
|
| 200 |
}
|
| 201 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|