Spaces:
Running on Zero
Running on Zero
File size: 40,441 Bytes
370fb2a 768b35e 971eaa4 370fb2a d5ca727 93c1716 d5ca727 002abca d5ca727 002abca d5ca727 002abca 93c1716 d5ca727 002abca d5ca727 e45ca08 d5ca727 f27ada2 93c1716 d5ca727 93c1716 d5ca727 002abca 370fb2a 971eaa4 370fb2a 93c1716 370fb2a 93c1716 1eb359a e45ca08 93c1716 971eaa4 0380074 2ec5490 93c1716 d5ca727 0752ee5 d5ca727 0752ee5 d5ca727 370fb2a d5ca727 370fb2a d5ca727 2ec5490 93c1716 0380074 93c1716 370fb2a 93c1716 002abca 1eb359a 002abca f8a85c2 002abca 370fb2a 93c1716 1eb359a 0380074 971eaa4 93c1716 0380074 1eb359a 971eaa4 93c1716 1eb359a 93c1716 1eb359a 971eaa4 1eb359a d5ca727 1eb359a 971eaa4 93c1716 370fb2a 93c1716 971eaa4 93c1716 971eaa4 93c1716 6a704b2 93c1716 370fb2a 93c1716 e45ca08 93c1716 370fb2a 93c1716 d03188b 93c1716 002abca 93c1716 002abca 93c1716 002abca 93c1716 002abca 93c1716 002abca 93c1716 002abca 93c1716 0380074 93c1716 afb9940 93c1716 afb9940 93c1716 d5ca727 93c1716 d5ca727 93c1716 2ec5490 93c1716 2ec5490 971eaa4 93c1716 d03188b e45ca08 971eaa4 0380074 971eaa4 93c1716 afb9940 93c1716 002abca 93c1716 afb9940 93c1716 370fb2a 93c1716 d03188b d5ca727 370fb2a d5ca727 1eb359a 93c1716 2ec5490 d5ca727 2ec5490 d5ca727 2ec5490 d5ca727 2ec5490 d5ca727 2ec5490 93c1716 370fb2a 971eaa4 768b35e 93c1716 971eaa4 d5ca727 370fb2a 971eaa4 370fb2a 971eaa4 | 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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 | import gradio as gr
import spaces
PAGE = r"""
<div id="sam-browser-app" class="sam-app">
<style>
#sam-browser-app{--sam-accent:#7c6cf2;--sam-accent2:#a89dff;max-width:1120px;margin:0 auto;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--body-text-color)}
#sam-browser-app *{box-sizing:border-box}
#sam-browser-app .hero{padding:26px 0 20px}
#sam-browser-app .eyebrow{font-size:12px;font-weight:800;letter-spacing:.13em;text-transform:uppercase;color:var(--sam-accent2);margin-bottom:9px}
#sam-browser-app h1{font-size:clamp(28px,4vw,40px);letter-spacing:-.04em;line-height:1.12;margin:0 0 12px;font-weight:780}
#sam-browser-app h2{font-size:18px;letter-spacing:-.02em;margin:0;font-weight:750}
#sam-browser-app p{line-height:1.55}
#sam-browser-app .subhead{font-size:15px;max-width:760px;color:var(--body-text-color);opacity:.76;margin:0}
#sam-browser-app .pill{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--border-color-primary);border-radius:999px;padding:7px 11px;font-size:12px;font-weight:650;margin-top:16px}
#sam-browser-app .dot{width:7px;height:7px;border-radius:50%;background:#3dbb86;display:inline-block}
#sam-browser-app .panel{border:1px solid var(--border-color-primary);border-radius:18px;padding:clamp(16px,3vw,24px);margin:15px 0;background:var(--background-fill-primary);box-shadow:0 5px 22px rgba(0,0,0,.035)}
#sam-browser-app .section-head{display:flex;align-items:center;gap:12px;margin-bottom:19px}
#sam-browser-app .step{display:grid;place-items:center;flex:0 0 34px;height:34px;border-radius:11px;background:color-mix(in srgb,var(--sam-accent) 14%,transparent);color:var(--sam-accent2);font-weight:800}
#sam-browser-app .muted{opacity:.68;font-size:13px;line-height:1.5}
#sam-browser-app label{display:block;margin:0 0 7px;font-size:13px;font-weight:700}
#sam-browser-app input[type=file],#sam-browser-app input[type=text],#sam-browser-app input[type=number],#sam-browser-app select{width:100%;min-width:0;padding:12px 13px;border-radius:10px;border:1px solid var(--border-color-primary);background:var(--input-background-fill);color:var(--body-text-color);font:inherit;font-size:14px}
#sam-browser-app input[type=file]{padding:10px}
#sam-browser-app .file-wrap{padding:16px;border:1px dashed var(--border-color-primary);border-radius:14px;background:var(--background-fill-secondary)}
#sam-browser-app .controls{display:grid;grid-template-columns:minmax(0,2fr) minmax(150px,1fr) minmax(110px,.7fr);gap:14px;margin-top:18px}
#sam-browser-app .field{min-width:0}
#sam-browser-app .actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:20px}
#sam-browser-app button{padding:12px 17px;border:0;border-radius:11px;background:var(--sam-accent);color:white;cursor:pointer;font-weight:750;font-size:14px;transition:filter .15s,transform .15s}
#sam-browser-app button:hover:not(:disabled){filter:brightness(1.08);transform:translateY(-1px)}
#sam-browser-app button:disabled{opacity:.55;cursor:wait;transform:none}
#sam-browser-app .secondary{background:var(--button-secondary-background-fill);color:var(--button-secondary-text-color);border:1px solid var(--border-color-primary)}
#sam-browser-app .status{display:flex;align-items:center;gap:9px;min-height:42px;padding:10px 13px;border-radius:11px;background:var(--background-fill-secondary);font-size:13px;flex:1;min-width:180px}
#sam-browser-app .status-dot{width:8px;height:8px;flex:0 0 8px;border-radius:50%;background:#a1a1aa}
#sam-browser-app .status[data-state=busy] .status-dot{background:#e9a23b;box-shadow:0 0 0 4px color-mix(in srgb,#e9a23b 17%,transparent)}
#sam-browser-app .status[data-state=success] .status-dot{background:#35b67a}
#sam-browser-app .status[data-state=error] .status-dot{background:#e05a68}
#sam-browser-app .audio-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}
#sam-browser-app .audio-card{min-width:0;border:1px solid var(--border-color-primary);border-radius:15px;padding:15px;background:var(--background-fill-secondary)}
#sam-browser-app .audio-card-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:13px}
#sam-browser-app .audio-title{font-size:14px;font-weight:800;letter-spacing:-.01em}
#sam-browser-app .tag{font-size:10px;letter-spacing:.08em;text-transform:uppercase;font-weight:800;border-radius:6px;padding:5px 7px;background:color-mix(in srgb,var(--sam-accent) 15%,transparent);color:var(--sam-accent2)}
#sam-browser-app .tag.neutral{background:var(--border-color-primary);color:var(--body-text-color)}
#sam-browser-app .wave-wrap{position:relative;width:100%;height:100px;border-radius:9px;overflow:hidden;background:color-mix(in srgb,var(--background-fill-primary) 78%,transparent)}
#sam-browser-app canvas{display:block;width:100%;height:100px;cursor:pointer;touch-action:pan-y}
#sam-browser-app .wave-empty[hidden]{display:none}
#sam-browser-app .wave-empty{position:absolute;inset:0;display:grid;place-items:center;text-align:center;padding:10px;color:var(--body-text-color);opacity:.45;font-size:12px;pointer-events:none}
#sam-browser-app .audio-meta{display:flex;justify-content:space-between;gap:8px;font-size:11px;opacity:.62;margin:9px 0 7px;font-variant-numeric:tabular-nums}
#sam-browser-app audio{display:block;width:100%;height:40px;margin:7px 0 11px}
#sam-browser-app .download{display:inline-flex;align-items:center;gap:7px;text-decoration:none;font-size:12px;font-weight:750;color:var(--sam-accent2);padding:7px 0}
#sam-browser-app .download[hidden]{display:none}
#sam-browser-app .empty-results{padding:24px 14px;border:1px dashed var(--border-color-primary);border-radius:14px;text-align:center;color:var(--body-text-color);opacity:.65;font-size:13px}
#sam-browser-app .privacy{display:flex;gap:10px;align-items:flex-start;padding:12px 14px;border-radius:11px;background:color-mix(in srgb,#35b67a 8%,transparent);font-size:12px;line-height:1.5;margin-top:16px}
#sam-browser-app .privacy svg{flex:0 0 17px;margin-top:1px}
#sam-browser-app .footer-note{font-size:12px;opacity:.58;text-align:center;margin:18px 0 8px;line-height:1.5}
#sam-browser-app .credits{font-size:12px;opacity:.7;text-align:center;margin:0 0 28px;line-height:1.8}
#sam-browser-app .credits a{color:var(--sam-accent2);text-decoration:none;font-weight:650}
#sam-browser-app .credits a:hover{text-decoration:underline}
@media(max-width:820px){#sam-browser-app .audio-grid{grid-template-columns:1fr}#sam-browser-app .controls{grid-template-columns:1fr 1fr}#sam-browser-app .controls .prompt-field{grid-column:1/-1}}
@media(max-width:480px){#sam-browser-app .controls{grid-template-columns:1fr}#sam-browser-app .controls .prompt-field{grid-column:auto}#sam-browser-app .actions{align-items:stretch}#sam-browser-app .actions button{width:100%}#sam-browser-app .status{flex-basis:100%}}
</style>
<header class="hero">
<div class="eyebrow">Audio separation studio</div>
<h1>SAM Audio <span style="color:var(--sam-accent2)">Studio</span></h1>
<p class="subhead">Separate speech, music, or other sounds from a recording using Meta's SAM Audio, running locally right in your browser.<br><br>Once downloaded, the model takes up about 2.7 GB in the browser cache.</p>
<div class="pill"><span class="dot"></span> Local browser processing <span style="opacity:.45">·</span> ONNX Runtime</div>
</header>
<section class="panel">
<div class="section-head"><div class="step">01</div><div><h2>Source & settings</h2><div class="muted">Choose a recording and describe what you want to isolate.</div></div></div>
<div class="file-wrap"><label for="sam-audio-file">Audio file</label><input id="sam-audio-file" type="file" accept=".wav,.mp3,.flac,.ogg,.m4a,audio/wav,audio/mpeg,audio/flac,audio/ogg,audio/mp4"><div class="muted" style="margin-top:8px">WAV, MP3, FLAC, OGG, or M4A · Maximum duration: 2 minutes</div></div>
<div class="controls">
<div class="field prompt-field"><label for="sam-prompt">What should be isolated?</label><input id="sam-prompt" type="text" value="a person speaking" placeholder="e.g. a person speaking"></div>
<div class="field"><label for="sam-provider">Processing device</label><select id="sam-provider"><option value="auto" selected>Automatic</option><option value="webgpu">GPU · WebGPU</option><option value="wasm">CPU · WASM</option></select></div>
<div class="field"><label for="sam-steps">ODE steps</label><input id="sam-steps" type="number" min="1" max="16" value="8"></div>
</div>
<div class="muted" style="margin-top:9px">Automatic mode uses CPU on phones/tablets and WebGPU on desktop when available. More ODE steps can improve quality but take longer.</div>
<div class="actions"><button id="sam-run">▶ Separate audio</button><button id="sam-clear-cache" class="secondary" type="button">Clear model cache</button><div id="sam-status-box" class="status" data-state="idle"><span class="status-dot"></span><span id="sam-status" aria-live="polite">Ready when you are</span></div></div>
<div class="privacy"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg><div><strong>Your audio stays on your device.</strong> Inference runs locally in this browser. The Space hosts the interface and model downloads; your selected audio is not uploaded for processing.</div></div>
</section>
<section class="panel">
<div class="section-head"><div class="step">02</div><div><h2>Listen & compare</h2><div class="muted">Use the waveform to seek through each track. Download outputs as WAV files.</div></div></div>
<div class="audio-grid">
<article class="audio-card"><div class="audio-card-head"><span class="audio-title">Original audio</span><span class="tag neutral">Input</span></div><div class="wave-wrap"><canvas id="sam-input-wave" aria-label="Original audio waveform"></canvas><div class="wave-empty" id="sam-input-empty">Choose an audio file to preview its waveform</div></div><div class="audio-meta"><span id="sam-input-duration">No audio loaded</span><span id="sam-input-format">Source</span></div><audio id="sam-input-player" controls preload="metadata"></audio><a class="download" id="sam-input-download" download hidden>↓ Download original</a></article>
<article class="audio-card"><div class="audio-card-head"><span class="audio-title">Target</span><span class="tag">Isolated</span></div><div class="wave-wrap"><canvas id="sam-target-wave" aria-label="Target audio waveform"></canvas><div class="wave-empty" id="sam-target-empty">Run separation to create the target</div></div><div class="audio-meta"><span id="sam-target-duration">Waiting for result</span><span>48 kHz · WAV</span></div><audio id="sam-target-player" controls preload="metadata"></audio><a class="download" id="sam-target-download" download="target.wav" hidden>↓ Download target.wav</a></article>
<article class="audio-card"><div class="audio-card-head"><span class="audio-title">Residual</span><span class="tag neutral">Everything else</span></div><div class="wave-wrap"><canvas id="sam-residual-wave" aria-label="Residual audio waveform"></canvas><div class="wave-empty" id="sam-residual-empty">Run separation to create the residual</div></div><div class="audio-meta"><span id="sam-residual-duration">Waiting for result</span><span>48 kHz · WAV</span></div><audio id="sam-residual-player" controls preload="metadata"></audio><a class="download" id="sam-residual-download" download="residual.wav" hidden>↓ Download residual.wav</a></article>
</div>
</section>
<p class="footer-note">First use downloads model files and may take several minutes on CPU.</p>
<div class="credits">Credits: <a href="https://github.com/facebookresearch/sam-audio" target="_blank" rel="noopener noreferrer">Meta's SAM Audio</a> <span aria-hidden="true">·</span> ONNX-converted models by <a href="https://huggingface.co/matbee/sam-audio-small-onnx" target="_blank" rel="noopener noreferrer">matbee</a></div>
</div>
"""
CLIENT_JS = r"""
async () => {
const root = document.getElementById("sam-browser-app");
if (!root || root.dataset.initialized === "yes") return;
root.dataset.initialized = "yes";
const $ = (id) => root.querySelector("#" + id);
const MODEL_BASE = "https://huggingface.co/matbee/sam-audio-small-onnx/resolve/main";
const CACHE_NAME = "sam-audio-webgpu-models-v1";
const MAX_AUDIO_SECONDS = 120;
const MODEL_FILES = {
encoder: "dacvae_encoder.onnx",
decoder: "dacvae_decoder.onnx",
t5: "t5_encoder.onnx",
dit: "dit_single_step.onnx"
};
let ort = null;
let tokenizer = null;
let sessions = null;
let sessionsProvider = null;
let runtimeProvider = null;
let lastAttemptProvider = null;
let previousUrls = [];
function setLog(message) {
// Compact user-facing progress message; technical diagnostics are intentionally hidden.
if (message) setStatus(String(message).split("\n")[0]);
}
function setStatus(message, state = "busy") {
$("sam-status").textContent = message;
$("sam-status-box").dataset.state = state;
}
const waveforms = {};
const colors = {input: "#8b879b", target: "#8b7cf6", residual: "#40b995"};
function formatDuration(seconds) {
if (!Number.isFinite(seconds) || seconds < 0) return "—";
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return mins ? `${mins}:${String(secs).padStart(2, "0")}` : `0:${String(secs).padStart(2, "0")}`;
}
function drawWaveform(canvasId, samples, kind) {
const canvas = $(canvasId);
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
if (!rect.width || !rect.height) return;
const dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, 2));
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
const ctx = canvas.getContext("2d");
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const width = rect.width, height = rect.height, mid = height / 2;
ctx.clearRect(0, 0, width, height);
ctx.strokeStyle = "rgba(128,128,150,.13)"; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, mid); ctx.lineTo(width, mid); ctx.stroke();
if (!samples || !samples.length) return;
const bars = Math.max(1, Math.floor(width / 3));
const stride = Math.max(1, Math.floor(samples.length / bars));
const color = colors[kind] || colors.input;
ctx.strokeStyle = color; ctx.lineWidth = Math.max(1, Math.min(2, width / bars * .7)); ctx.lineCap = "round";
ctx.beginPath();
for (let x = 0; x < bars; x++) {
const start = x * stride;
const end = Math.min(samples.length, start + stride);
let peak = 0;
for (let i = start; i < end; i++) peak = Math.max(peak, Math.abs(samples[i] || 0));
const amplitude = Math.max(1.25, peak * (height * .43));
const px = (x + .5) * width / bars;
ctx.moveTo(px, mid - amplitude); ctx.lineTo(px, mid + amplitude);
}
ctx.stroke();
waveforms[canvasId] = {samples, kind};
updateWaveProgress(canvasId);
}
function updateWaveProgress(canvasId) {
const canvas = $(canvasId);
const player = $(canvasId.replace("-wave", "-player"));
const entry = waveforms[canvasId];
if (!canvas || !entry || !player || !player.duration) return;
const rect = canvas.getBoundingClientRect();
const dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, 2));
const ctx = canvas.getContext("2d");
const x = Math.max(0, Math.min(rect.width, player.currentTime / player.duration * rect.width));
ctx.save(); ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.fillStyle = "rgba(255,255,255,.13)"; ctx.fillRect(0, 0, x, rect.height);
ctx.restore();
}
function attachWavePlayer(canvasId, playerId) {
const canvas = $(canvasId), player = $(playerId);
if (!canvas || !player) return;
player.addEventListener("timeupdate", () => drawWaveform(canvasId, waveforms[canvasId]?.samples, waveforms[canvasId]?.kind || "input"));
canvas.addEventListener("click", event => {
if (!Number.isFinite(player.duration) || !player.duration) return;
const rect = canvas.getBoundingClientRect();
player.currentTime = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width)) * player.duration;
});
}
function setWave(id, samples, kind, durationId, emptyId) {
const empty = $(emptyId);
if (samples && samples.length) {
if (empty) empty.hidden = true;
drawWaveform(id, samples, kind);
const duration = $(durationId);
if (duration) duration.textContent = formatDuration(samples.length / 48000);
} else {
if (empty) empty.hidden = false;
const canvas = $(id), ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
delete waveforms[id];
}
}
function isMobileDevice() {
// Prefer CPU on phones/tablets: mobile WebGPU drivers may expose the API
// but fail during large model inference. Desktop users can still override manually.
return /Android|iPhone|iPad|iPod|Mobile|Tablet/i.test(navigator.userAgent) ||
(navigator.userAgentData?.mobile === true);
}
async function chooseProvider() {
const requested = $("sam-provider").value;
if (requested === "webgpu" || requested === "wasm") return requested;
if (isMobileDevice()) {
setLog("Automatic device selection: mobile device detected. Using CPU (WASM) for compatibility.");
return "wasm";
}
try {
if (navigator.gpu) {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) {
setLog("Automatic device selection: desktop WebGPU adapter found. Using GPU (WebGPU).");
return "webgpu";
}
}
} catch (error) {
setLog("WebGPU adapter check failed. Using CPU (WASM).\n" + String(error));
}
setLog("Automatic device selection: WebGPU is unavailable. Using CPU (WASM).");
return "wasm";
}
async function loadOrt() {
if (!ort) {
ort = await import("https://cdn.jsdelivr.net/npm/onnxruntime-web@1.23.2/+esm");
if (ort.env?.wasm) {
// Let ONNX Runtime choose its supported thread count. Multi-threading
// is enabled only when the browser provides the required isolation.
ort.env.wasm.numThreads = 0;
ort.env.wasm.proxy = false;
}
if (ort.env) ort.env.logLevel = "warning";
}
return ort;
}
async function loadTokenizer() {
if (!tokenizer) {
const transformers = await import("https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.2");
// Let Transformers.js persist tokenizer assets in its browser cache when supported.
transformers.env.useBrowserCache = true;
// Use the Transformers.js-compatible T5 tokenizer repository.
tokenizer = await transformers.AutoTokenizer.from_pretrained("Xenova/t5-base");
}
return tokenizer;
}
async function fetchCachedBytes(url, label) {
// Cache Storage persists across page reloads. If unavailable or quota is exceeded,
// continue with a normal network download rather than failing inference.
let cache = null;
if ("caches" in window) {
try { cache = await caches.open(CACHE_NAME); } catch (_) {}
}
if (cache) {
try {
const cached = await cache.match(url);
if (cached) {
setLog("Using browser cache: " + label + "\nNo network download needed for this file.");
return new Uint8Array(await cached.arrayBuffer());
}
} catch (_) {}
}
setLog("Downloading " + label + "…\nThe file will be cached for future runs if browser storage permits.");
const response = await fetch(url);
if (!response.ok) throw new Error("Download failed (" + response.status + ") for " + url);
if (cache) {
try {
// Cache a clone so the original response remains available for the ONNX runtime.
await cache.put(url, response.clone());
} catch (error) {
setLog("Downloaded " + label + ", but the browser could not persist it in cache (quota/private mode). Continuing without persistent cache.\n" + String(error));
}
}
return new Uint8Array(await response.arrayBuffer());
}
async function configureRuntime(provider) {
const runtime = await loadOrt();
if (runtimeProvider !== null && runtimeProvider !== provider) {
throw new Error(
"The CPU worker and GPU modes require different ONNX Runtime initialization. " +
"Reload this page before switching between CPU and GPU, then select the desired mode before the first inference."
);
}
if (runtimeProvider === null) {
if (runtime.env?.wasm) {
// In CPU mode, run WASM inference in a proxy Web Worker so heavy
// inference does not block the browser's UI thread.
// WebGPU and the WASM proxy worker cannot be used together.
runtime.env.wasm.proxy = provider === "wasm";
runtime.env.wasm.numThreads = 0; // automatic, subject to browser support
}
runtimeProvider = provider;
}
return runtime;
}
async function createSession(filename, provider) {
const url = MODEL_BASE + "/" + filename;
const runtime = await configureRuntime(provider);
const modelBytes = await fetchCachedBytes(url, filename);
const dataUrl = url + ".data";
const externalBytes = await fetchCachedBytes(dataUrl, filename + ".data (external weights)");
return runtime.InferenceSession.create(modelBytes, {
executionProviders: [provider],
graphOptimizationLevel: "all",
externalData: [{ path: filename + ".data", data: externalBytes.buffer }]
});
}
async function clearModelCache() {
if (sessions) {
for (const session of Object.values(sessions)) {
try { await session.release(); } catch (_) {}
}
sessions = null;
sessionsProvider = null;
}
if ("caches" in window) {
try { await caches.delete(CACHE_NAME); } catch (_) {}
}
setStatus("Model cache cleared · reload before next run", "success");
}
async function loadModels(provider) {
if (sessions && sessionsProvider === provider) return sessions;
if (sessions) {
for (const session of Object.values(sessions)) {
try { await session.release(); } catch (_) {}
}
sessions = null;
sessionsProvider = null;
}
const loaded = {};
const order = [
["encoder", "dacvae_encoder.onnx"],
["t5", "t5_encoder.onnx"],
["dit", "dit_single_step.onnx"],
["decoder", "dacvae_decoder.onnx"]
];
try {
for (const [key, filename] of order) {
setStatus("Loading " + filename + "…");
setLog("Loading model component: " + filename + "\nThe browser may download a large external .onnx.data file. Keep this tab open.");
loaded[key] = await createSession(filename, provider);
setLog("Loaded " + filename + "\nInputs: " + loaded[key].inputNames.join(", ") + "\nOutputs: " + loaded[key].outputNames.join(", "));
}
await loadTokenizer();
sessions = loaded;
sessionsProvider = provider;
return sessions;
} catch (error) {
for (const session of Object.values(loaded)) {
try { await session.release(); } catch (_) {}
}
throw error;
}
}
function getTensorMetadata(session, name) {
try { return session.inputMetadata?.[name] || {}; } catch (_) { return {}; }
}
function floatTensor(data, dims) {
// IMPORTANT: with ort.env.wasm.proxy=true, ONNX Runtime transfers input
// ArrayBuffers to its worker. Passing the original typed array detaches it,
// leaving length 0 for later ODE steps. Always pass a fresh copy.
const copiedData = data instanceof Float32Array ? new Float32Array(data) : Float32Array.from(data);
return new ort.Tensor("float32", copiedData, dims);
}
function int64Tensor(data, dims) {
const values = Array.from(data, value => BigInt(value));
return new ort.Tensor("int64", BigInt64Array.from(values), dims);
}
function boolTensor(data, dims) {
return new ort.Tensor("bool", Uint8Array.from(data, value => value ? 1 : 0), dims);
}
async function decodeAudio(file) {
const bytes = await file.arrayBuffer();
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
const context = new AudioContextClass();
try {
const decoded = await context.decodeAudioData(bytes.slice(0));
if (decoded.duration > MAX_AUDIO_SECONDS) {
throw new Error("Audio must be 2 minutes or shorter. Selected file is " + formatDuration(decoded.duration) + ".");
}
const offline = new OfflineAudioContext(1, Math.ceil(decoded.duration * 48000), 48000);
const source = offline.createBufferSource();
const mono = offline.createBuffer(1, decoded.length, decoded.sampleRate);
const channels = [];
for (let i = 0; i < decoded.numberOfChannels; i++) channels.push(decoded.getChannelData(i));
const monoData = mono.getChannelData(0);
for (let i = 0; i < monoData.length; i++) {
let sum = 0;
for (const channel of channels) sum += channel[i];
monoData[i] = sum / channels.length;
}
source.buffer = mono;
source.connect(offline.destination);
source.start();
const resampled = await offline.startRendering();
return {samples: new Float32Array(resampled.getChannelData(0)), sampleRate: 48000};
} finally {
await context.close().catch(() => {});
}
}
async function runSession(session, outputName, feeds) {
const outputs = await session.run(feeds);
const tensor = outputs[outputName] || outputs[session.outputNames[0]];
if (!tensor) throw new Error("Expected output not found: " + outputName);
return tensor.data;
}
async function encodeAudio(samples) {
const session = sessions.encoder;
// In WASM proxy mode, ONNX Runtime may transfer/detach the tensor's
// ArrayBuffer when sending it to the worker. Never give it audio.samples
// directly: keep the decoded source intact for duration checks and output trimming.
const inputSamples = new Float32Array(samples);
const input = new ort.Tensor("float32", inputSamples, [1, 1, inputSamples.length]);
const out = await session.run({audio: input});
const tensor = out.latent_features || out[session.outputNames[0]];
return {data: new Float32Array(tensor.data), dims: tensor.dims};
}
async function encodeText(text) {
const tok = await loadTokenizer();
const encoded = await tok(text, {
padding: "max_length",
truncation: true,
max_length: 77
});
const idsRaw = encoded.input_ids?.data ?? encoded.input_ids;
const maskRaw = encoded.attention_mask?.data ?? encoded.attention_mask;
const ids = Array.from(idsRaw);
const mask = Array.from(maskRaw);
const idsTensor = int64Tensor(ids, [1, ids.length]);
const maskTensor = int64Tensor(mask, [1, mask.length]);
const out = await sessions.t5.run({input_ids: idsTensor, attention_mask: maskTensor});
const tensor = out.hidden_states || out[sessions.t5.outputNames[0]];
return {
features: new Float32Array(tensor.data),
featureDims: tensor.dims,
mask: Uint8Array.from(mask, value => value ? 1 : 0),
maskDims: [1, mask.length]
};
}
async function runDit(noisy, timeValue, audioFeatures, text, sequenceLength) {
const batch = 1;
const zerosInt = new BigInt64Array(sequenceLength);
const mask = new Uint8Array(sequenceLength).fill(1);
const video = new Float32Array(1024 * sequenceLength);
const feeds = {
noisy_audio: floatTensor(noisy, [batch, sequenceLength, 256]),
time: floatTensor(new Float32Array([timeValue]), [1]),
audio_features: floatTensor(audioFeatures, [batch, sequenceLength, 256]),
text_features: floatTensor(text.features, text.featureDims),
text_mask: boolTensor(text.mask, text.maskDims),
masked_video_features: floatTensor(video, [batch, 1024, sequenceLength]),
anchor_ids: new ort.Tensor("int64", zerosInt, [batch, sequenceLength]),
anchor_alignment: new ort.Tensor("int64", new BigInt64Array(sequenceLength), [batch, sequenceLength]),
audio_pad_mask: boolTensor(mask, [batch, sequenceLength])
};
const outputs = await sessions.dit.run(feeds);
const tensor = outputs.velocity || outputs[sessions.dit.outputNames[0]];
return new Float32Array(tensor.data);
}
async function runOde(initial, audioFeatures, text, steps, sequenceLength) {
let x = new Float32Array(initial);
const dt = 1 / steps;
for (let i = 0; i < steps; i++) {
setStatus("Denoising step " + (i + 1) + "/" + steps + " (1/2)…");
const t = i * dt;
const k1 = await runDit(x, t, audioFeatures, text, sequenceLength);
const midpoint = new Float32Array(x.length);
for (let j = 0; j < x.length; j++) midpoint[j] = x[j] + (dt / 2) * k1[j];
setStatus("Denoising step " + (i + 1) + "/" + steps + " (2/2)…");
const k2 = await runDit(midpoint, t + dt / 2, audioFeatures, text, sequenceLength);
for (let j = 0; j < x.length; j++) x[j] += dt * k2[j];
// Yield so status/progress updates can paint between long CPU steps.
await new Promise(resolve => setTimeout(resolve, 0));
}
return x;
}
async function decodeLatent(latent, latentChannels, timeSteps) {
const session = sessions.decoder;
const chunkSize = 25;
const hopLength = 1920;
const chunks = [];
for (let start = 0; start < timeSteps; start += chunkSize) {
const actual = Math.min(chunkSize, timeSteps - start);
const chunk = new Float32Array(latentChannels * chunkSize);
for (let c = 0; c < latentChannels; c++) {
for (let t = 0; t < actual; t++) {
chunk[c * chunkSize + t] = latent[c * timeSteps + start + t];
}
}
const out = await session.run({
latent_features: floatTensor(chunk, [1, latentChannels, chunkSize])
});
const tensor = out.waveform || out[session.outputNames[0]];
const data = tensor.data;
const count = actual * hopLength;
chunks.push(new Float32Array(data.slice(0, count)));
setStatus("Decoding audio " + Math.min(start + chunkSize, timeSteps) + "/" + timeSteps + " latent frames…");
}
const totalLength = chunks.reduce((sum, part) => sum + part.length, 0);
const output = new Float32Array(totalLength);
let offset = 0;
for (const chunk of chunks) { output.set(chunk, offset); offset += chunk.length; }
return output;
}
function wavBlob(samples, sampleRate) {
const buffer = new ArrayBuffer(44 + samples.length * 2);
const view = new DataView(buffer);
function writeString(offset, value) { for (let i = 0; i < value.length; i++) view.setUint8(offset + i, value.charCodeAt(i)); }
writeString(0, "RIFF"); view.setUint32(4, 36 + samples.length * 2, true);
writeString(8, "WAVE"); writeString(12, "fmt "); view.setUint32(16, 16, true);
view.setUint16(20, 1, true); view.setUint16(22, 1, true); view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * 2, true); view.setUint16(32, 2, true); view.setUint16(34, 16, true);
writeString(36, "data"); view.setUint32(40, samples.length * 2, true);
for (let i = 0; i < samples.length; i++) {
const sample = Math.max(-1, Math.min(1, samples[i]));
view.setInt16(44 + i * 2, sample < 0 ? sample * 32768 : sample * 32767, true);
}
return new Blob([buffer], {type:"audio/wav"});
}
function showOutput(playerId, linkId, samples, sampleRate, filename, waveId, kind, durationId, emptyId) {
const url = URL.createObjectURL(wavBlob(samples, sampleRate));
previousUrls.push(url);
const player = $(playerId);
player.src = url;
player.load();
const link = $(linkId); link.href = url; link.download = filename; link.hidden = false;
setWave(waveId, samples, kind, durationId, emptyId);
}
function showInput(file, samples) {
const url = URL.createObjectURL(file);
previousUrls.push(url);
const player = $("sam-input-player"); player.src = url; player.load();
const link = $("sam-input-download"); link.href = url; link.download = file.name; link.hidden = false; link.textContent = "↓ Download original";
$("sam-input-format").textContent = file.name.length > 24 ? file.name.slice(0, 21) + "…" : file.name;
setWave("sam-input-wave", samples, "input", "sam-input-duration", "sam-input-empty");
}
async function separate(providerOverride = null) {
const file = $("sam-audio-file").files?.[0];
const prompt = $("sam-prompt").value.trim();
const steps = Number.parseInt($("sam-steps").value, 10);
const provider = providerOverride || await chooseProvider();
lastAttemptProvider = provider;
const deviceLabel = provider === "wasm" ? "CPU (WASM)" : "GPU (WebGPU)";
if (!file) throw new Error("Choose an audio file first.");
if (!prompt) throw new Error("Enter a text prompt.");
if (!Number.isInteger(steps) || steps < 1 || steps > 16) throw new Error("ODE steps must be between 1 and 16.");
const audio = await decodeAudio(file);
const expectedSamples = audio.samples.length;
if (expectedSamples > 48000 * MAX_AUDIO_SECONDS) throw new Error("Audio must be 2 minutes or shorter.");
setStatus("Loading models for " + deviceLabel + "…");
if (provider === "wasm") {
setLog("CPU mode selected. ONNX Runtime will use WebAssembly in a background worker where supported, keeping the page responsive. This may take longer than GPU inference.");
}
await loadModels(provider);
setStatus("Encoding audio on " + deviceLabel + "…");
const encoded = await encodeAudio(audio.samples);
if (encoded.dims.length !== 3) throw new Error("Unexpected encoder output dimensions: " + JSON.stringify(encoded.dims));
const [batch, latentChannels, timeSteps] = encoded.dims;
if (batch !== 1 || latentChannels !== 128) {
throw new Error("Unexpected audio latent shape " + JSON.stringify(encoded.dims) + "; expected [1, 128, time].");
}
setStatus("Encoding text…");
const text = await encodeText(prompt);
const mixture = new Float32Array(timeSteps * 256);
for (let t = 0; t < timeSteps; t++) {
for (let c = 0; c < latentChannels; c++) {
const value = encoded.data[c * timeSteps + t];
mixture[t * 256 + c] = value;
mixture[t * 256 + latentChannels + c] = value;
}
}
const initial = new Float32Array(timeSteps * 256);
// Standard normal noise, matching the reference implementation's randn initialization.
for (let i = 0; i < initial.length; i += 2) {
const u1 = Math.max(Number.EPSILON, Math.random());
const u2 = Math.random();
const radius = Math.sqrt(-2 * Math.log(u1));
initial[i] = radius * Math.cos(2 * Math.PI * u2);
if (i + 1 < initial.length) initial[i + 1] = radius * Math.sin(2 * Math.PI * u2);
}
const result = await runOde(initial, mixture, text, steps, timeSteps);
const targetLatent = new Float32Array(latentChannels * timeSteps);
const residualLatent = new Float32Array(latentChannels * timeSteps);
for (let t = 0; t < timeSteps; t++) {
for (let c = 0; c < latentChannels; c++) {
targetLatent[c * timeSteps + t] = result[t * 256 + c];
residualLatent[c * timeSteps + t] = result[t * 256 + latentChannels + c];
}
}
setStatus("Decoding target…");
const target = await decodeLatent(targetLatent, latentChannels, timeSteps);
setStatus("Decoding residual…");
const residual = await decodeLatent(residualLatent, latentChannels, timeSteps);
if (audio.samples.length !== expectedSamples) {
throw new Error("Internal audio buffer was unexpectedly detached or resized: expected " + expectedSamples + " samples, got " + audio.samples.length + ".");
}
showOutput("sam-target-player", "sam-target-download", target.slice(0, expectedSamples), 48000, "target.wav", "sam-target-wave", "target", "sam-target-duration", "sam-target-empty");
showOutput("sam-residual-player", "sam-residual-download", residual.slice(0, expectedSamples), 48000, "residual.wav", "sam-residual-wave", "residual", "sam-residual-duration", "sam-residual-empty");
setStatus("Separation complete · " + deviceLabel, "success");
}
attachWavePlayer("sam-input-wave", "sam-input-player");
attachWavePlayer("sam-target-wave", "sam-target-player");
attachWavePlayer("sam-residual-wave", "sam-residual-player");
$("sam-audio-file").addEventListener("change", async () => {
const file = $("sam-audio-file").files?.[0];
if (!file) return;
for (const url of previousUrls) URL.revokeObjectURL(url);
previousUrls = [];
for (const [playerId, linkId, waveId, durationId, emptyId, waitingText] of [
["sam-input-player", "sam-input-download", "sam-input-wave", "sam-input-duration", "sam-input-empty", "No audio loaded"],
["sam-target-player", "sam-target-download", "sam-target-wave", "sam-target-duration", "sam-target-empty", "Waiting for result"],
["sam-residual-player", "sam-residual-download", "sam-residual-wave", "sam-residual-duration", "sam-residual-empty", "Waiting for result"]
]) {
const player = $(playerId); player.pause(); player.removeAttribute("src"); player.load();
const link = $(linkId); link.removeAttribute("href"); link.hidden = true;
$(durationId).textContent = waitingText;
$(emptyId).hidden = false;
const canvas = $(waveId); const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height);
delete waveforms[waveId];
}
try {
setStatus("Preparing audio preview…", "busy");
const preview = await decodeAudio(file);
showInput(file, preview.samples);
setStatus("Source ready · " + formatDuration(preview.samples.length / preview.sampleRate), "idle");
} catch (error) {
setStatus("Could not preview this audio: " + String(error?.message || error), "error");
}
});
window.addEventListener("resize", () => {
for (const [id, entry] of Object.entries(waveforms)) drawWaveform(id, entry.samples, entry.kind);
});
$("sam-clear-cache").addEventListener("click", async () => {
const button = $("sam-clear-cache");
button.disabled = true;
try { await clearModelCache(); }
finally { button.disabled = false; }
});
$("sam-run").addEventListener("click", async () => {
const button = $("sam-run");
button.disabled = true;
try {
await separate();
} catch (error) {
const message = String(error?.message || error);
const isWebGpuBufferFailure = lastAttemptProvider === "webgpu" &&
/createBuffer failed.*mappedAtCreation|mappedAtCreation.*too large/i.test(message);
if (isWebGpuBufferFailure) {
const retryOnCpu = window.confirm(
"WebGPU failed to allocate a GPU buffer on this device. This can be a browser/driver or GPU-memory limitation.\n\nRetry the same audio using CPU (WASM)? This may be slower."
);
if (retryOnCpu) {
$("sam-provider").value = "wasm";
setStatus("Retrying on CPU…");
setLog("WebGPU buffer allocation failed. Retrying locally with CPU (WASM).\nThe audio is not uploaded to the server.");
try {
await separate("wasm");
setStatus("Separation complete · CPU fallback used", "success");
} catch (cpuError) {
setStatus("CPU retry failed: " + String(cpuError?.message || cpuError), "error");
}
} else {
setStatus("GPU allocation failed · try CPU (WASM)", "error");
}
} else {
setStatus("Separation failed: " + message, "error");
}
} finally {
button.disabled = false;
}
});
}
"""
@spaces.GPU
def zerogpu_startup_marker():
return "No server inference; browser WebGPU prototype."
with gr.Blocks(title="SAM Audio Studio", theme=gr.themes.Soft(primary_hue="violet", secondary_hue="slate"), css="footer {visibility: hidden}") as demo:
gr.HTML(PAGE)
demo.load(fn=None, inputs=None, outputs=None, js=CLIENT_JS)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False) |