Spaces:
Sleeping
Sleeping
Download Web/script.js from MeysamSh/Speech-DeepFake-Detection: direct link, hf CLI and curl.
- Browser
- Download file 20.5 kB
-
https://huggingface.co/spaces/MeysamSh/Speech-DeepFake-Detection/resolve/main/Web/script.js
- Command line
-
hf download hf://spaces/MeysamSh/Speech-DeepFake-Detection/Web/script.js
-
curl -L -o script.js https://huggingface.co/spaces/MeysamSh/Speech-DeepFake-Detection/resolve/main/Web/script.js
20.5 kB
| /* ============================================================ | |
| SoundCloud-Style Audio Recorder + Waveform Display + Analyzer | |
| ============================================================ | |
| */ | |
| // Global variables | |
| let audioContext; | |
| let gumStream; | |
| let rec; | |
| let input; | |
| // DOM Elements | |
| const recordBtn = document.getElementById("recordButton"); | |
| const stopBtn = document.getElementById("stopButton"); | |
| const pauseBtn = document.getElementById("pauseButton"); | |
| const recordingsList = document.getElementById("recordingsList"); | |
| const uploadButton = document.getElementById("upload-button"); | |
| const uploadInput = document.getElementById("audio-file"); | |
| const responseBox = document.getElementById("response"); | |
| const metadataBox = document.getElementById("metadata-display"); | |
| // ============================== | |
| // Recording Controls | |
| // ============================== | |
| recordBtn.addEventListener("click", startRecording); | |
| stopBtn.addEventListener("click", stopRecording); | |
| pauseBtn.addEventListener("click", pauseRecording); | |
| // Start recording | |
| function startRecording() { | |
| console.log("Recording started..."); | |
| recordBtn.disabled = true; | |
| stopBtn.disabled = false; | |
| pauseBtn.disabled = false; | |
| navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| document.getElementById("formats").innerText = | |
| "Sample rate: " + audioContext.sampleRate + " Hz"; | |
| gumStream = stream; | |
| input = audioContext.createMediaStreamSource(stream); | |
| rec = new Recorder(input, { numChannels: 1 }); | |
| rec.record(); | |
| }).catch(function (e) { | |
| console.error("Could not start recording:", e); | |
| recordBtn.disabled = false; | |
| stopBtn.disabled = true; | |
| pauseBtn.disabled = true; | |
| }); | |
| } | |
| // Pause recording | |
| function pauseRecording() { | |
| if (rec.recording) { | |
| rec.stop(); | |
| pauseBtn.innerText = "Resume"; | |
| } else { | |
| rec.record(); | |
| pauseBtn.innerText = "Pause"; | |
| } | |
| } | |
| // Stop recording | |
| function stopRecording() { | |
| console.log("Recording stopped."); | |
| stopBtn.disabled = true; | |
| recordBtn.disabled = false; | |
| pauseBtn.disabled = true; | |
| pauseBtn.innerText = "Pause"; | |
| rec.stop(); | |
| gumStream.getAudioTracks()[0].stop(); | |
| rec.exportWAV(createWaveformItem); | |
| } | |
| // ============================== | |
| // File Upload | |
| // ============================== | |
| uploadButton.addEventListener("click", () => { | |
| if (!uploadInput.files.length) { | |
| alert("Select at least one file."); | |
| return; | |
| } | |
| Array.from(uploadInput.files).forEach((file) => handleUploadedFile(file)); | |
| }); | |
| function handleUploadedFile(file) { | |
| const url = URL.createObjectURL(file); | |
| createWaveformItem(file, url); | |
| } | |
| // ============================== | |
| // Create SoundCloud-like Waveform | |
| // ============================== | |
| function createWaveformItem(blob, forcedUrl = null) { | |
| const url = forcedUrl || URL.createObjectURL(blob); | |
| const li = document.createElement("li"); | |
| li.className = "wave-item"; | |
| const container = document.createElement("div"); | |
| container.className = "wave-canvas"; | |
| // Waveform and its temporal analysis share one column, so both use the same time axis | |
| const waveCol = document.createElement("div"); | |
| waveCol.className = "wave-col"; | |
| const timelineEl = document.createElement("div"); | |
| timelineEl.className = "timeline"; | |
| waveCol.appendChild(container); | |
| waveCol.appendChild(timelineEl); | |
| const meta = document.createElement("div"); | |
| meta.className = "wave-meta"; | |
| meta.innerHTML = `<div class="file-title">${blob.name || "recording.wav"}</div>`; | |
| // Play button | |
| const playBtn = document.createElement("button"); | |
| playBtn.className = "btn btn-play btn-sm"; | |
| playBtn.innerText = "Play"; | |
| // Analyze button | |
| const analyzeBtn = document.createElement("button"); | |
| analyzeBtn.className = "btn btn-outline-secondary btn-sm"; | |
| analyzeBtn.innerText = "Analyze"; | |
| const controls = document.createElement("div"); | |
| controls.className = "wave-controls"; | |
| controls.appendChild(analyzeBtn); | |
| controls.appendChild(playBtn); | |
| li.appendChild(controls); | |
| li.appendChild(waveCol); | |
| li.appendChild(meta); | |
| recordingsList.appendChild(li); | |
| // Create WaveSurfer | |
| const wavesurfer = WaveSurfer.create({ | |
| container, | |
| autoCenter: true, | |
| waveColor: "#bbb", | |
| progressColor: "#ff5500", | |
| cursorColor: "#333", | |
| barWidth: 2, | |
| height: 70, | |
| responsive: true, | |
| }); | |
| // Analyze | |
| const item = { blob, wavesurfer, timelineEl, abort: null, timeline: null }; | |
| analyzeBtn.addEventListener("click", () => { | |
| analyzeAudio(item); | |
| }); | |
| // Toggle play/pause | |
| playBtn.addEventListener("click", () => { | |
| wavesurfer.playPause(); | |
| playBtn.innerText = wavesurfer.isPlaying() ? "Pause" : "Play"; | |
| }); | |
| wavesurfer.on("finish", () => { playBtn.innerText = "Play"; }); | |
| // Keep the timeline's playback cursor in sync with the audio | |
| const syncCursor = () => item.timeline && item.timeline.setCursor(wavesurfer.getCurrentTime()); | |
| wavesurfer.on("audioprocess", syncCursor); | |
| wavesurfer.on("seek", syncCursor); | |
| wavesurfer.load(url); | |
| // Metadata | |
| if (metadataBox) blob.arrayBuffer().then((buffer) => { | |
| const ctx = new AudioContext(); | |
| ctx.decodeAudioData(buffer).then((decoded) => { | |
| metadataBox.innerHTML = ` | |
| <strong>Channels:</strong> ${decoded.numberOfChannels}<br> | |
| <strong>Duration:</strong> ${decoded.duration.toFixed(2)}s<br> | |
| <strong>Sample Rate:</strong> ${decoded.sampleRate} Hz | |
| `; | |
| }); | |
| }); | |
| } | |
| // ============================== | |
| // Audio Analysis API (placeholder) | |
| // ============================== | |
| async function analyzeAudio(item) { | |
| const blob = item.blob; | |
| // Check if the response element exists | |
| const responseBox = document.getElementById('response'); | |
| if (!responseBox) return; | |
| responseBox.innerHTML = "<em>Analyzing...</em>"; | |
| // Convert Blob to File | |
| const file = new File([blob], blob.name || "recording.wav", { type: blob.type }); | |
| const formData = new FormData(); | |
| formData.append("files", file); | |
| try { | |
| // Replace the URL with your real backend endpoint | |
| const res = await fetch("/predict/", { | |
| method: "POST", | |
| body: formData, | |
| }); | |
| const data = await res.json(); | |
| let message = "Prediction Failed!"; | |
| if (data[0]?.status === "success") { | |
| const item = data[0]; | |
| const confidencePercent = Math.round(item.confidence * 100); | |
| message = `${item.filename} is ${item.label} with confidence of ${confidencePercent}%`; | |
| } | |
| let messageVolume = "... "; | |
| if (data[0]?.volume === "silent") { | |
| messageVolume = "The audio appears to be silent or contains no detectable speech. Please record/select another file! "; | |
| message = "" | |
| } | |
| if (data[0]?.volume === "augmented") { | |
| messageVolume = "The detected speech volume was low, so a volume augmentation was applied before analysis. "; | |
| } | |
| const volumeColor = "orange"; | |
| let predictionColor = message.includes("Genuine") ? "green" : "red"; | |
| const silent = data[0]?.volume === "silent"; | |
| responseBox.innerHTML = ` | |
| <div style="font-size: 1.1rem; margin-bottom: 6px; color:${volumeColor};"> | |
| ${messageVolume} | |
| </div> | |
| <div style="font-size: 1.25rem; font-weight:600; color:${predictionColor};"> | |
| ${message} | |
| </div> | |
| `; | |
| // Temporal analysis runs after the global score (one CPU-heavy job at a time) | |
| if (data[0]?.status === "success" && !silent) { | |
| runTimeline(item, file); | |
| } | |
| } catch (err) { | |
| responseBox.innerHTML = `<div style="font-size: 1.25rem; font-weight:600; color:red;">/!\ Analysis failed.</div>`; | |
| console.error(err); | |
| } | |
| } | |
| // ============================== | |
| // Temporal Analysis (sliding window) | |
| // ============================== | |
| const SVG_NS = "http://www.w3.org/2000/svg"; | |
| const TL = { | |
| height: 150, | |
| padTop: 10, | |
| padBottom: 22, // room for the time axis labels | |
| ink: "#2b3a55", // the P(genuine) line | |
| genuineTint: "rgba(12, 163, 12, 0.08)", | |
| spoofTint: "rgba(208, 59, 59, 0.08)", | |
| grid: "#e6e6e6", | |
| cursor: "#ff5500", // same orange as the waveform progress | |
| }; | |
| function svgEl(tag, attrs = {}, parent = null) { | |
| const el = document.createElementNS(SVG_NS, tag); | |
| for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v); | |
| if (parent) parent.appendChild(el); | |
| return el; | |
| } | |
| const fmtPct = (p) => `${(p * 100).toFixed(1)}%`; | |
| const fmtSec = (t) => `${t.toFixed(t < 10 ? 2 : 1)} s`; | |
| function niceTimeStep(duration, widthPx) { | |
| const maxTicks = Math.max(2, Math.floor(widthPx / 60)); | |
| const steps = [0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120]; | |
| return steps.find((s) => duration / s <= maxTicks) || 300; | |
| } | |
| // Read an NDJSON stream line by line | |
| async function* readNdjson(response) { | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ""; | |
| while (true) { | |
| const { value, done } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| let nl; | |
| while ((nl = buffer.indexOf("\n")) >= 0) { | |
| const line = buffer.slice(0, nl).trim(); | |
| buffer = buffer.slice(nl + 1); | |
| if (line) yield JSON.parse(line); | |
| } | |
| } | |
| if (buffer.trim()) yield JSON.parse(buffer); | |
| } | |
| async function runTimeline(item, file) { | |
| // A new analysis of the same item replaces the running one | |
| if (item.abort) item.abort.abort(); | |
| item.abort = new AbortController(); | |
| const windowSec = document.getElementById("tl-window")?.value || "4"; | |
| const hopSec = document.getElementById("tl-hop")?.value || "1"; | |
| const chart = createTimelineChart(item); | |
| item.timeline = chart; | |
| chart.setStatus("Temporal analysis: uploading…"); | |
| const formData = new FormData(); | |
| formData.append("file", file); | |
| formData.append("window_sec", windowSec); | |
| formData.append("hop_sec", hopSec); | |
| try { | |
| const res = await fetch("/timeline/", { | |
| method: "POST", | |
| body: formData, | |
| signal: item.abort.signal, | |
| }); | |
| if (!res.ok) { | |
| const err = await res.json().catch(() => ({})); | |
| throw new Error(err.detail || `HTTP ${res.status}`); | |
| } | |
| for await (const msg of readNdjson(res)) { | |
| if (msg.n_windows !== undefined) chart.setHeader(msg); | |
| else chart.addSegment(msg); | |
| } | |
| chart.finish(); | |
| } catch (err) { | |
| if (err.name === "AbortError") return; | |
| console.error(err); | |
| chart.setStatus(`Temporal analysis failed: ${err.message}`); | |
| } | |
| } | |
| function createTimelineChart(item) { | |
| const root = item.timelineEl; | |
| root.replaceChildren(); | |
| root.classList.add("active"); | |
| const status = document.createElement("div"); | |
| status.className = "timeline-status"; | |
| const plot = document.createElement("div"); | |
| plot.className = "timeline-plot"; | |
| const svg = svgEl("svg", { height: TL.height, role: "img" }, plot); | |
| const tip = document.createElement("div"); | |
| tip.className = "timeline-tip"; | |
| plot.appendChild(tip); | |
| // Table view: the same numbers without hovering | |
| const details = document.createElement("details"); | |
| const summary = document.createElement("summary"); | |
| summary.textContent = "Segment values"; | |
| const table = document.createElement("table"); | |
| const thead = table.createTHead().insertRow(); | |
| ["Start", "End", "P(genuine)", "Decision"].forEach((h) => { | |
| const th = document.createElement("th"); | |
| th.textContent = h; | |
| thead.appendChild(th); | |
| }); | |
| const tbody = table.createTBody(); | |
| details.append(summary, table); | |
| root.append(status, plot, details); | |
| const state = { header: null, segments: [], cursor: null, hover: null, done: false }; | |
| const duration = () => { | |
| const d = item.wavesurfer.getDuration(); | |
| return d > 0 ? d : state.header?.duration || 1; | |
| }; | |
| const geom = () => { | |
| const w = plot.clientWidth || 600; | |
| const h = TL.height; | |
| const y0 = TL.padTop, y1 = h - TL.padBottom; | |
| return { | |
| w, h, y0, y1, | |
| x: (t) => (t / duration()) * w, | |
| y: (p) => y1 - p * (y1 - y0), | |
| t: (px) => Math.min(Math.max(px / w, 0), 1) * duration(), | |
| }; | |
| }; | |
| const center = (s) => (s.start + s.end) / 2; | |
| function render() { | |
| const g = geom(); | |
| svg.replaceChildren(); | |
| svg.setAttribute("viewBox", `0 0 ${g.w} ${g.h}`); | |
| // Zones: above 50% = genuine, below = spoof | |
| svgEl("rect", { x: 0, y: g.y0, width: g.w, height: g.y(0.5) - g.y0, fill: TL.genuineTint }, svg); | |
| svgEl("rect", { x: 0, y: g.y(0.5), width: g.w, height: g.y1 - g.y(0.5), fill: TL.spoofTint }, svg); | |
| // Hovered window span | |
| if (state.hover !== null) { | |
| const s = state.segments[state.hover]; | |
| svgEl("rect", { | |
| x: g.x(s.start), y: g.y0, width: Math.max(1, g.x(s.end) - g.x(s.start)), | |
| height: g.y1 - g.y0, fill: "rgba(43, 58, 85, 0.07)", | |
| }, svg); | |
| } | |
| // Grid at 0 / 50 / 100 %, decision threshold dashed | |
| [0, 0.5, 1].forEach((p) => { | |
| svgEl("line", { | |
| x1: 0, x2: g.w, y1: g.y(p), y2: g.y(p), | |
| stroke: p === 0.5 ? "#9aa0a6" : TL.grid, | |
| "stroke-width": 1, "stroke-dasharray": p === 0.5 ? "4 3" : "", | |
| }, svg); | |
| }); | |
| const ylab = (txt, p, dy) => { | |
| const t = svgEl("text", { x: g.w - 4, y: g.y(p) + dy, "text-anchor": "end" }, svg); | |
| t.textContent = txt; | |
| }; | |
| ylab("100%", 1, 11); | |
| ylab("50%", 0.5, -4); | |
| ylab("0%", 0, -4); | |
| const zlab = (txt, y) => { | |
| const t = svgEl("text", { x: 4, y, "font-weight": 600 }, svg); | |
| t.textContent = txt; | |
| }; | |
| zlab("▲ Genuine", g.y0 + 12); | |
| zlab("▼ Spoof", g.y1 - 5); | |
| // Time axis | |
| const step = niceTimeStep(duration(), g.w); | |
| for (let t = 0; t <= duration() + 1e-9; t += step) { | |
| const x = g.x(t); | |
| svgEl("line", { x1: x, x2: x, y1: g.y1, y2: g.y1 + 4, stroke: "#bbb" }, svg); | |
| const lbl = svgEl("text", { | |
| x, y: g.h - 6, | |
| "text-anchor": t === 0 ? "start" : x > g.w - 20 ? "end" : "middle", | |
| }, svg); | |
| lbl.textContent = `${+t.toFixed(2)}s`; | |
| } | |
| // P(genuine) line through window centres; silent windows break the line | |
| let d = ""; | |
| let pen = false; | |
| state.segments.forEach((s) => { | |
| if (s.silent) { pen = false; return; } | |
| d += `${pen ? "L" : "M"}${g.x(center(s)).toFixed(1)},${g.y(s.bonafide_prob).toFixed(1)}`; | |
| pen = true; | |
| }); | |
| if (d) { | |
| svgEl("path", { | |
| d, fill: "none", stroke: TL.ink, "stroke-width": 2, | |
| "stroke-linejoin": "round", "stroke-linecap": "round", | |
| }, svg); | |
| } | |
| // Markers (only when sparse enough to stay readable) | |
| const showDots = state.segments.length <= 60; | |
| state.segments.forEach((s, i) => { | |
| const cx = g.x(center(s)); | |
| if (s.silent) { | |
| svgEl("line", { | |
| x1: cx - 4, x2: cx + 4, y1: g.y1 - 3, y2: g.y1 - 3, | |
| stroke: "#adb5bd", "stroke-width": 2, "stroke-linecap": "round", | |
| }, svg); | |
| return; | |
| } | |
| if (!showDots && i !== state.hover) return; | |
| svgEl("circle", { | |
| cx, cy: g.y(s.bonafide_prob), r: i === state.hover ? 5 : 3.5, | |
| fill: TL.ink, stroke: "#fff", "stroke-width": 2, | |
| }, svg); | |
| }); | |
| // Crosshair on hover | |
| if (state.hover !== null) { | |
| const x = g.x(center(state.segments[state.hover])); | |
| svgEl("line", { x1: x, x2: x, y1: g.y0, y2: g.y1, stroke: TL.ink, "stroke-width": 1, opacity: 0.4 }, svg); | |
| } | |
| // Playback cursor | |
| if (state.cursor !== null) { | |
| const x = g.x(state.cursor); | |
| svgEl("line", { x1: x, x2: x, y1: 0, y2: g.y1, stroke: TL.cursor, "stroke-width": 2 }, svg); | |
| } | |
| svg.setAttribute("aria-label", ariaSummary()); | |
| } | |
| function ariaSummary() { | |
| const scored = state.segments.filter((s) => !s.silent); | |
| if (!scored.length) return "Temporal analysis: no scored segments yet"; | |
| const spoofed = scored.filter((s) => s.bonafide_prob < 0.5).length; | |
| return `Probability of genuine speech over time: ${spoofed} of ${scored.length} segments below 50%`; | |
| } | |
| function updateStatus() { | |
| const h = state.header; | |
| if (!h) return; | |
| const n = state.segments.length; | |
| const cfg = `window ${h.window_sec} s · hop ${h.hop_sec} s`; | |
| status.replaceChildren(); | |
| const strong = document.createElement("strong"); | |
| if (!state.done) { | |
| strong.textContent = `Temporal analysis: ${n} / ${h.n_windows} segments`; | |
| status.append(strong, ` · ${cfg} · computing…`); | |
| return; | |
| } | |
| const scored = state.segments.filter((s) => !s.silent); | |
| const spoofed = scored.filter((s) => s.bonafide_prob < 0.5).length; | |
| const silent = n - scored.length; | |
| strong.textContent = `${spoofed} of ${scored.length} segments look spoofed`; | |
| status.append( | |
| strong, | |
| ` · ${cfg}${silent ? ` · ${silent} silent segment${silent > 1 ? "s" : ""} not scored` : ""}`, | |
| ); | |
| } | |
| function addRow(s) { | |
| const row = tbody.insertRow(); | |
| [ | |
| fmtSec(s.start), | |
| fmtSec(s.end), | |
| s.silent ? "—" : fmtPct(s.bonafide_prob), | |
| s.silent ? "silent" : s.bonafide_prob >= 0.5 ? "Genuine" : "Spoof", | |
| ].forEach((v) => { row.insertCell().textContent = v; }); | |
| } | |
| // Hover: snap to the nearest window centre | |
| function showTip(i) { | |
| const s = state.segments[i]; | |
| const g = geom(); | |
| tip.replaceChildren(); | |
| const value = document.createElement("div"); | |
| value.className = "tip-value"; | |
| value.textContent = s.silent ? "silent" : `${fmtPct(s.bonafide_prob)} genuine`; | |
| const label = document.createElement("div"); | |
| label.className = "tip-label"; | |
| label.textContent = `${fmtSec(s.start)} – ${fmtSec(s.end)}` | |
| + (s.silent ? " · not scored" : ` · ${s.bonafide_prob >= 0.5 ? "Genuine" : "Spoof"}`); | |
| tip.append(value, label); | |
| tip.style.display = "block"; | |
| const x = g.x(center(s)); | |
| const tw = tip.offsetWidth; | |
| tip.style.left = `${Math.min(Math.max(x - tw / 2, 0), g.w - tw)}px`; | |
| tip.style.top = `${Math.max(0, (s.silent ? g.y1 : g.y(s.bonafide_prob)) - tip.offsetHeight - 10)}px`; | |
| } | |
| svg.addEventListener("pointermove", (e) => { | |
| if (!state.segments.length) return; | |
| const t = geom().t(e.offsetX); | |
| let best = 0; | |
| state.segments.forEach((s, i) => { | |
| if (Math.abs(center(s) - t) < Math.abs(center(state.segments[best]) - t)) best = i; | |
| }); | |
| if (best !== state.hover) { | |
| state.hover = best; | |
| render(); | |
| } | |
| showTip(best); | |
| }); | |
| svg.addEventListener("pointerleave", () => { | |
| state.hover = null; | |
| tip.style.display = "none"; | |
| render(); | |
| }); | |
| // Click to play from that point | |
| svg.addEventListener("click", (e) => { | |
| const g = geom(); | |
| item.wavesurfer.seekTo(g.t(e.offsetX) / duration()); | |
| }); | |
| new ResizeObserver(() => render()).observe(plot); | |
| render(); | |
| return { | |
| setStatus(text) { status.textContent = text; }, | |
| setHeader(h) { state.header = h; updateStatus(); render(); }, | |
| addSegment(s) { state.segments.push(s); addRow(s); updateStatus(); render(); }, | |
| finish() { state.done = true; updateStatus(); render(); }, | |
| setCursor(t) { state.cursor = t; render(); }, | |
| }; | |
| } | |