(function () { "use strict"; // The Gradio JS client is loaded as a global from CDN (see index.html). // It exposes `window.gradio` with `Client` and `handle_file`. const { Client, handle_file } = window.gradio; // ------------------------------------------------------------------ // // State // // ------------------------------------------------------------------ // const state = { presets: null, selectedFile: null, isWorking: false, client: null, }; // ------------------------------------------------------------------ // // DOM helpers // // ------------------------------------------------------------------ // const $ = (sel) => document.querySelector(sel); const $$ = (sel) => document.querySelectorAll(sel); function el(tag, attrs = {}, ...children) { const node = document.createElement(tag); for (const [k, v] of Object.entries(attrs)) { if (k === "class") node.className = v; else if (k === "html") node.innerHTML = v; else if (k.startsWith("on") && typeof v === "function") { node.addEventListener(k.slice(2).toLowerCase(), v); } else if (v !== null && v !== undefined) { node.setAttribute(k, v); } } for (const child of children) { if (child === null || child === undefined) continue; node.appendChild(typeof child === "string" ? document.createTextNode(child) : child); } return node; } // ------------------------------------------------------------------ // // Status / metrics // // ------------------------------------------------------------------ // function setStatus(text, kind = "ready") { const box = $("#status"); box.className = `status ${kind}`; box.innerHTML = text; } function setMetrics({ dur = "—", est = "—", stems = "—" } = {}) { $("#metric-dur").textContent = dur; $("#metric-est").textContent = est; $("#metric-stems").textContent = stems; } // ------------------------------------------------------------------ // // Slider value display // // ------------------------------------------------------------------ // function bindSliderDisplay(sliderId, displayId, formatter = (v) => v) { const slider = document.getElementById(sliderId); const display = document.getElementById(displayId); if (!slider || !display) return; const update = () => { display.textContent = formatter(slider.value); }; slider.addEventListener("input", update); update(); } // ------------------------------------------------------------------ // // Dropzone & file input // // ------------------------------------------------------------------ // function initDropzone() { const dz = $("#dropzone"); const input = $("#file-input"); const preview = $("#preview"); const previewWrap = $("#preview-wrap"); const previewName = $("#preview-name"); const previewDur = $("#preview-dur"); const clearBtn = $("#clear-input"); const separateBtn = $("#separate-btn"); function handleFile(file) { if (!file) return; if (!file.type.startsWith("audio/") && !/\.(wav|mp3|flac|m4a|ogg|aac|opus)$/i.test(file.name)) { setStatus(`⚠️ "${file.name}" doesn't look like an audio file. Try anyway?`, "error"); } state.selectedFile = file; const url = URL.createObjectURL(file); preview.src = url; previewName.textContent = file.name; previewDur.textContent = `${(file.size / 1024 / 1024).toFixed(2)} MB`; previewWrap.hidden = false; dz.style.display = "none"; separateBtn.disabled = false; setStatus(`Ready. Click Separate audio to process ${file.name}.`); } function clearFile() { state.selectedFile = null; preview.src = ""; previewWrap.hidden = true; dz.style.display = ""; separateBtn.disabled = true; setStatus("Ready. Upload audio and click Separate."); setMetrics({}); } // Click to browse dz.addEventListener("click", () => input.click()); dz.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); input.click(); } }); // File picker input.addEventListener("change", (e) => { const file = e.target.files && e.target.files[0]; if (file) handleFile(file); }); // Drag & drop ["dragenter", "dragover"].forEach((ev) => { dz.addEventListener(ev, (e) => { e.preventDefault(); dz.classList.add("dragging"); }); }); ["dragleave", "drop"].forEach((ev) => { dz.addEventListener(ev, (e) => { e.preventDefault(); dz.classList.remove("dragging"); }); }); dz.addEventListener("drop", (e) => { const file = e.dataTransfer.files && e.dataTransfer.files[0]; if (file) handleFile(file); }); clearBtn.addEventListener("click", clearFile); // Compute duration once metadata loads preview.addEventListener("loadedmetadata", () => { const s = preview.duration; if (isFinite(s)) { const mm = Math.floor(s / 60).toString().padStart(2, "0"); const ss = Math.floor(s % 60).toString().padStart(2, "0"); previewDur.textContent = `${mm}:${ss} · ${(state.selectedFile.size / 1024 / 1024).toFixed(2)} MB`; } }); } // ------------------------------------------------------------------ // // Preset / model dropdowns // // ------------------------------------------------------------------ // async function loadPresets() { try { const result = await state.client.predict("/presets", {}); const data = result.data[0]; state.presets = data; const useCaseSel = $("#use-case"); useCaseSel.innerHTML = ""; Object.keys(data.presets).forEach((name) => { const opt = el("option", { value: name }, name); if (name === data.default_use_case) opt.selected = true; useCaseSel.appendChild(opt); }); // Load model list for the default architecture await refreshModelDropdown(data.default_use_case); // Trigger initial preset description useCaseSel.dispatchEvent(new Event("change")); } catch (err) { console.error("Failed to load presets:", err); setStatus(`⚠️ Failed to load presets: ${err.message || err}`, "error"); } } async function refreshModelDropdown(useCaseName) { if (!state.presets) return; const preset = state.presets.presets[useCaseName]; if (!preset) return; const arch = preset.architecture; try { const result = await state.client.predict("/models", { architecture: arch }); const models = result.data[0]; const sel = $("#model"); sel.innerHTML = ""; models.forEach((name) => { const opt = el("option", { value: name }, name); if (name === preset.model) opt.selected = true; sel.appendChild(opt); }); } catch (err) { console.error("Failed to load models:", err); } } function onUseCaseChange() { const useCaseName = $("#use-case").value; if (!state.presets) return; const preset = state.presets.presets[useCaseName]; if (!preset) return; $("#preset-desc").textContent = `ℹ️ ${preset.description}`; refreshModelDropdown(useCaseName); } function onModelChange() { // Architecture groups visibility is auto-managed by the backend's // response, but since we don't get the architecture back from /models, // we re-derive it client-side from the preset. const modelName = $("#model").value; const useCaseName = $("#use-case").value; if (!state.presets) return; const preset = state.presets.presets[useCaseName]; if (!preset) return; const arch = (modelName === preset.model) ? preset.architecture : inferArchFromModel(modelName); updateAdvancedGroups(arch); } function inferArchFromModel(name) { if (!name) return "roformer"; if (name.endsWith(".yaml")) return "demucs"; if (name.endsWith(".pth")) return "vr"; if (name.endsWith(".onnx")) return "mdx"; if (name.endsWith(".ckpt")) return "roformer"; return "roformer"; } function updateAdvancedGroups(arch) { $("#adv-rof").hidden = !(arch === "roformer" || arch === "mdx23c"); $("#adv-mdx").hidden = !(arch === "mdx"); $("#adv-vr").hidden = !(arch === "vr"); $("#adv-demucs").hidden = !(arch === "demucs"); } // ------------------------------------------------------------------ // // Advanced toggle // // ------------------------------------------------------------------ // function initAdvancedToggle() { const cb = $("#advanced"); const acc = $("#adv-accordion"); cb.addEventListener("change", () => { acc.hidden = !cb.checked; if (cb.checked) acc.open = true; }); } // ------------------------------------------------------------------ // // History // // ------------------------------------------------------------------ // async function loadHistory() { try { const result = await state.client.predict("/history", {}); const rows = result.data[0] || []; const body = $("#history-body"); body.innerHTML = ""; if (!rows.length) { body.appendChild(el("tr", { class: "empty-row" }, el("td", { colspan: "5" }, "No history yet."))); return; } rows.forEach((r) => { body.appendChild(el("tr", null, el("td", null, r[0] || ""), el("td", null, r[1] || ""), el("td", null, r[2] || ""), el("td", null, r[3] || ""), el("td", null, r[4] || "") )); }); } catch (err) { // Silent — history is best-effort console.warn("History load failed:", err); } } // ------------------------------------------------------------------ // // Separation // // ------------------------------------------------------------------ // async function runSeparation() { if (state.isWorking) return; if (!state.selectedFile) { setStatus("⚠️ Please upload an audio file first.", "error"); return; } state.isWorking = true; const btn = $("#separate-btn"); btn.disabled = true; $(".btn-label", btn).textContent = "Working…"; $(".btn-spinner", btn).hidden = false; setStatus(`Working on ${state.selectedFile.name}… First run downloads the model, so this can take a while.`, "working"); setMetrics({ stems: "…" }); // Build payload const payload = { audio_file: handle_file(state.selectedFile), use_case: $("#use-case").value, model_display_name: $("#model").value, output_format: $("#output-format").value, advanced_mode: $("#advanced").checked, seg_size: parseInt($("#seg-size").value, 10), overlap_rof: parseInt($("#overlap-rof").value, 10), overlap_mdx: parseFloat($("#overlap-mdx").value), pitch_shift: parseInt($("#pitch-shift").value, 10), hop_length: parseInt($("#hop-length").value, 10), enable_denoise: $("#enable-denoise").checked, window_size: parseInt($("#window-size").value, 10), aggression: parseInt($("#aggression").value, 10), enable_tta: $("#enable-tta").checked, enable_post: $("#enable-post").checked, post_thresh: parseFloat($("#post-thresh").value), enable_high_end: $("#enable-high-end").checked, shifts: parseInt($("#shifts").value, 10), segments_enabled: $("#segments-enabled").checked, norm_thresh: parseFloat($("#norm-thresh").value), amp_thresh: parseFloat($("#amp-thresh").value), batch_size: parseInt($("#batch-size").value, 10), }; try { const result = await state.client.predict("/separate", payload); const data = result.data[0]; if (data.error) { setStatus(`❌ ${data.error}`, "error"); setMetrics({ dur: data.duration || "—", est: data.est || "—", stems: "0", }); return; } renderStems(data.stems || []); renderZip(data.zip); const stemNames = (data.stems || []).map((s) => s.label).join(", "); setStatus( `✅ Done. ${data.stems.length} stem(s): ${stemNames} · ` + `Model ${data.model} · Architecture ${data.architecture} · ` + `Device ${data.device}`, "success" ); setMetrics({ dur: data.duration || "—", est: data.est || "—", stems: String(data.stems.length), }); loadHistory(); } catch (err) { console.error("Separation failed:", err); setStatus(`❌ Error: ${err.message || err}`, "error"); } finally { state.isWorking = false; btn.disabled = false; $(".btn-label", btn).textContent = "Separate audio"; $(".btn-spinner", btn).hidden = true; } } function renderStems(stems) { const container = $("#stems"); container.innerHTML = ""; if (!stems.length) { $("#empty-state").classList.remove("hidden"); return; } $("#empty-state").classList.add("hidden"); stems.forEach((stem) => { // stem.file is a FileData — its `.url` is the playable URL. // Some serialization paths leave `url` null and only populate `path`; // in that case we build the URL ourselves via Gradio's /file= endpoint. const url = fileDataUrl(stem.file); if (!url) return; const card = el("div", { class: "stem-card" }, el("div", { class: "stem-header" }, el("div", { class: "stem-label" }, `🎵 ${stem.label}`), el("a", { class: "stem-download", href: url, download: "" }, "download ↓") ), el("audio", { controls: "", preload: "metadata", src: url }) ); container.appendChild(card); }); } function renderZip(zipFileData) { const link = $("#zip-link"); if (!zipFileData) { link.hidden = true; return; } const url = fileDataUrl(zipFileData); if (!url) { link.hidden = true; return; } link.href = url; link.hidden = false; } /** * Resolve a FileData (or string path) to a playable/downloadable URL. * The Gradio wire protocol gives us either: * - { url: "https://...", path: "/abs/path" } — use url directly * - { url: null, path: "/abs/path" } — build via /gradio_api/file= * - "/abs/path" — build via /gradio_api/file= * - "https://..." — use directly */ function fileDataUrl(fileData) { if (!fileData) return null; if (typeof fileData === "string") { if (/^https?:\/\//.test(fileData)) return fileData; // Gradio expects the path UNencoded after "file=" — it does its own // path parsing. Encoding with encodeURIComponent would break it. return `${window.location.origin}/gradio_api/file=${fileData}`; } if (fileData.url) return fileData.url; if (fileData.path) { return `${window.location.origin}/gradio_api/file=${fileData.path}`; } return null; } // ------------------------------------------------------------------ // // Clear // // ------------------------------------------------------------------ // function clearOutputs() { $("#stems").innerHTML = ""; $("#zip-link").hidden = true; $("#empty-state").classList.remove("hidden"); setMetrics({}); setStatus("Ready. Upload audio and click Separate.", "ready"); } // ------------------------------------------------------------------ // // Health badge // // ------------------------------------------------------------------ // async function loadHealth() { try { const res = await fetch("/healthz"); const data = await res.json(); $("#device-badge").textContent = `device: ${data.device || "—"}`; $("#engine-badge").textContent = `engine: ${data.engine_installed ? "ok" : "missing"}`; $("#footer-device").textContent = data.device || "—"; $("#footer-engine").textContent = data.engine_installed ? "installed" : "missing"; } catch (err) { $("#device-badge").textContent = "device: ?"; $("#engine-badge").textContent = "engine: ?"; } } // ------------------------------------------------------------------ // // Init // // ------------------------------------------------------------------ // async function init() { // Bind slider value displays bindSliderDisplay("norm-thresh", "norm-thresh-val"); bindSliderDisplay("amp-thresh", "amp-thresh-val"); bindSliderDisplay("batch-size", "batch-size-val"); bindSliderDisplay("seg-size", "seg-size-val"); bindSliderDisplay("overlap-rof", "overlap-rof-val"); bindSliderDisplay("overlap-mdx", "overlap-mdx-val", (v) => parseFloat(v).toFixed(3)); bindSliderDisplay("pitch-shift", "pitch-shift-val"); bindSliderDisplay("hop-length", "hop-length-val"); bindSliderDisplay("window-size", "window-size-val"); bindSliderDisplay("aggression", "aggression-val"); bindSliderDisplay("post-thresh", "post-thresh-val"); bindSliderDisplay("shifts", "shifts-val"); initDropzone(); initAdvancedToggle(); // Wire up events $("#use-case").addEventListener("change", onUseCaseChange); $("#model").addEventListener("change", onModelChange); $("#separate-btn").addEventListener("click", runSeparation); $("#clear-btn").addEventListener("click", clearOutputs); // Connect Gradio client to the current origin (as the blog shows) try { state.client = await Client.connect(window.location.origin); } catch (err) { console.error("Failed to connect Gradio client:", err); setStatus(`Failed to connect to backend: ${err.message || err}`, "error"); return; } await Promise.all([loadHealth(), loadPresets(), loadHistory()]); // Initial advanced-group visibility (matches default preset's arch) onModelChange(); } // The Gradio client is loaded as an ESM module via a separate