fuze-eletrik commited on
Commit
93c1716
·
verified ·
1 Parent(s): eddac96

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +344 -175
app.py CHANGED
@@ -2,206 +2,375 @@ import gradio as gr
2
  import spaces
3
 
4
  PAGE = r"""
5
- <div id="sam-webgpu-test" style="font-family:system-ui,sans-serif;max-width:1000px;margin:auto">
6
- <style>
7
- #sam-webgpu-test .card{border:1px solid var(--border-color-primary);border-radius:12px;padding:16px;margin:14px 0}
8
- #sam-webgpu-test .row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
9
- #sam-webgpu-test .log{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--background-fill-secondary);padding:12px;border-radius:8px;font:13px/1.5 ui-monospace,monospace;min-height:28px}
10
- #sam-webgpu-test .muted{opacity:.75;font-size:.9rem}
11
- #sam-webgpu-test .status{font-weight:700}
12
- #sam-webgpu-test .ok{color:#20a464} #sam-webgpu-test .warn{color:#c58a00} #sam-webgpu-test .bad{color:#dc4545}
13
- #sam-webgpu-test button{border:0;border-radius:8px;padding:9px 13px;cursor:pointer;background:var(--button-primary-background-fill);color:var(--button-primary-text-color)}
14
- #sam-webgpu-test button:disabled{opacity:.55;cursor:wait}
15
- #sam-webgpu-test .model{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;padding:12px 0;border-top:1px solid var(--border-color-primary)}
16
- #sam-webgpu-test .model:first-of-type{border-top:0}
17
- #sam-webgpu-test .model-name{font-weight:700}
18
- #sam-webgpu-test .small{display:block;opacity:.72;font-size:.85rem}
19
- #sam-webgpu-test .pill{display:inline-block;border:1px solid var(--border-color-primary);border-radius:99px;padding:2px 8px;font-size:.8rem;margin-top:5px}
20
- @media(max-width:600px){#sam-webgpu-test .model{grid-template-columns:1fr}#sam-webgpu-test .model button{justify-self:start}}
21
- </style>
22
- <h1>SAM Audio Small ONNX — Browser WebGPU diagnostic</h1>
23
- <p>Model loading and session tests run in your browser. The Space server does not process audio or run model inference.</p>
24
- <section class="card">
25
- <h2>1. Browser and WebGPU</h2>
26
- <div class="row"><button id="sam-check-browser">Run browser checks</button><span id="sam-browser-status" class="status muted">Checking…</span></div>
27
- <div id="sam-browser-log" class="log" style="margin-top:10px">Waiting for browser check…</div>
28
- </section>
29
- <section class="card">
30
- <h2>2. ONNX Runtime WebGPU runtime check</h2>
31
- <div class="row"><button id="sam-smoke">Run runtime check</button><span id="sam-smoke-status" class="status muted">Not tested</span></div>
32
- <div id="sam-smoke-log" class="log" style="margin-top:10px">No test run yet.</div>
33
- </section>
34
- <section class="card">
35
- <h2>3. Model and external-weight download diagnostics</h2>
36
- <p>The ONNX graph references a separate weights file. This version explicitly passes that external data URL to ONNX Runtime Web and checks both URLs first. Test one model at a time; leave DiT until last.</p>
37
- <label style="display:flex;gap:8px;align-items:center;margin:10px 0"><input id="sam-strict" type="checkbox" checked> Require WebGPU only (no WASM fallback)</label>
38
- <div id="sam-model-list"></div>
39
- <div id="sam-model-log" class="log" style="margin-top:10px">No model test run yet.</div>
40
- </section>
41
- <section class="card">
42
- <h2>How to report a failure</h2>
43
- <p>Copy the entire text from the result box, including the error name, message, stack, URL checks and HTTP status. A numeric code alone is not enough to identify the cause.</p>
44
- <p class="muted">Model: <a href="https://huggingface.co/matbee/sam-audio-small-onnx/tree/main" target="_blank" rel="noreferrer">matbee/sam-audio-small-onnx</a> · <a href="https://onnxruntime.ai/docs/tutorials/web/large-models.html" target="_blank" rel="noreferrer">ONNX external data documentation</a></p>
45
- </section>
 
 
 
 
 
 
46
  </div>
47
  """
48
 
49
  CLIENT_JS = r"""
50
  async () => {
51
- const root = document.getElementById("sam-webgpu-test");
52
  if (!root || root.dataset.initialized === "yes") return;
53
  root.dataset.initialized = "yes";
54
  const $ = (id) => root.querySelector("#" + id);
55
- const REPO = "https://huggingface.co/matbee/sam-audio-small-onnx/resolve/main";
56
- const MODELS = [
57
- {id:"dacvae_encoder", name:"DACVAE Encoder", file:"dacvae_encoder.onnx", size:"~110 MB"},
58
- {id:"t5_encoder", name:"T5 Text Encoder", file:"t5_encoder.onnx", size:"~440 MB"},
59
- {id:"dacvae_decoder", name:"DACVAE Decoder", file:"dacvae_decoder.onnx", size:"~320 MB"},
60
- {id:"dit_single_step", name:"DiT Denoiser", file:"dit_single_step.onnx", size:"~2 GB"}
61
- ];
62
- let ortModule = null;
63
- const modelPills = new Map();
64
- function status(el, message, kind="muted") { el.textContent=message; el.className="status "+kind; }
65
- function errorText(error) {
66
- if (error === null || error === undefined) return String(error);
67
- const fields = [];
 
 
 
68
  for (const key of ["name", "message", "code", "stack"]) {
69
- try { if (error[key] !== undefined) fields.push(key + ": " + String(error[key])); } catch (_) {}
70
- }
71
- try { fields.push("JSON: " + JSON.stringify(error, Object.getOwnPropertyNames(error))); } catch (_) {}
72
- return fields.length ? fields.join("\n") : String(error);
73
- }
74
- function log(el, message) { el.textContent=message; }
75
- function renderModels() {
76
- const list = $("sam-model-list");
77
- for (const model of MODELS) {
78
- const row=document.createElement("div"); row.className="model";
79
- const info=document.createElement("div");
80
- const name=document.createElement("div"); name.className="model-name"; name.textContent=model.name;
81
- const file=document.createElement("span"); file.className="small"; file.textContent=model.file+" · "+model.size;
82
- const pill=document.createElement("span"); pill.className="pill"; pill.textContent="Not tested";
83
- modelPills.set(model.id,pill);
84
- info.append(name,file,pill);
85
- const button=document.createElement("button"); button.textContent="Test download + session";
86
- button.addEventListener("click",()=>testModel(model,button));
87
- row.append(info,button); list.append(row);
88
  }
 
 
89
  }
90
- async function getOrt() {
91
- if (!ortModule) {
92
- ortModule = await import("https://cdn.jsdelivr.net/npm/onnxruntime-web@1.23.2/+esm");
93
- if (ortModule.env?.wasm) ortModule.env.wasm.numThreads = 1;
94
- if (ortModule.env) ortModule.env.logLevel = "verbose";
95
  }
96
- return ortModule;
97
- }
98
- async function checkBrowser() {
99
- const lines=["Secure context: "+(window.isSecureContext?"yes":"no"),
100
- "navigator.gpu: "+("gpu" in navigator?"available":"missing"),
101
- "User agent: "+navigator.userAgent];
102
- if (!("gpu" in navigator)) {
103
- status($("sam-browser-status"),"WebGPU unavailable","bad");
104
- log($("sam-browser-log"),lines.join("\n")); return;
105
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  try {
107
- const adapter=await navigator.gpu.requestAdapter();
108
- if (!adapter) throw new Error("requestAdapter() returned null.");
109
- const info=adapter.info||{};
110
- lines.push("Adapter: "+(info.description||info.device||info.architecture||"details unavailable"));
111
- lines.push("Vendor: "+(info.vendor||"unknown"));
112
- lines.push("Architecture: "+(info.architecture||"unknown"));
113
- lines.push("Max buffer size: "+adapter.limits.maxBufferSize);
114
- lines.push("Max storage buffer binding size: "+adapter.limits.maxStorageBufferBindingSize);
115
- status($("sam-browser-status"),"WebGPU adapter available","ok");
116
- } catch(error) { status($("sam-browser-status"),"WebGPU check failed","bad"); lines.push(errorText(error)); }
117
- log($("sam-browser-log"),lines.join("\n"));
118
- }
119
- async function smokeTest() {
120
- const button=$("sam-smoke"); button.disabled=true;
121
- status($("sam-smoke-status"),"Running…","warn");
122
- log($("sam-smoke-log"),"Loading ONNX Runtime Web and checking WebGPU adapter…");
123
- try {
124
- const ort=await getOrt();
125
- const adapter=await navigator.gpu?.requestAdapter();
126
- if (!adapter) throw new Error("No WebGPU adapter available.");
127
- status($("sam-smoke-status"),"Runtime loaded; adapter available","ok");
128
- log($("sam-smoke-log"),"PASS\nONNX Runtime Web version: "+(ort.env?.versions?.web||"unknown")+
129
- "\nWebGPU adapter: available\nThis check does not execute a model graph.");
130
- } catch(error) { status($("sam-smoke-status"),"FAIL","bad"); log($("sam-smoke-log"),errorText(error)); }
131
- finally { button.disabled=false; }
132
- }
133
- async function checkUrl(url) {
134
- // GET a tiny byte range rather than HEAD; some CDN/Xet routes handle HEAD differently.
135
- try {
136
- const response = await fetch(url, {method:"GET", headers:{Range:"bytes=0-0"}});
137
- const contentRange=response.headers.get("content-range");
138
- const contentLength=response.headers.get("content-length");
139
- await response.body?.cancel();
140
- return {ok:response.ok,status:response.status,url:response.url,
141
- contentType:response.headers.get("content-type"),contentLength,contentRange};
142
- } catch(error) { return {ok:false,error:errorText(error),url}; }
143
- }
144
- async function testModel(model,button) {
145
- if (button.disabled) return;
146
- button.disabled=true;
147
- const pill=modelPills.get(model.id);
148
- pill.textContent="Checking URLs…"; pill.className="pill";
149
- const modelUrl=REPO+"/"+model.file;
150
- const dataFile=model.file+".data";
151
- const dataUrl=REPO+"/"+dataFile;
152
- log($("sam-model-log"),"Checking model graph and external weights URLs...\n"+modelUrl+"\n"+dataUrl);
153
- let session=null; const start=performance.now();
154
  try {
155
- const [graphCheck,dataCheck]=await Promise.all([checkUrl(modelUrl),checkUrl(dataUrl)]);
156
- log($("sam-model-log"),"URL checks:\nGraph:\n"+JSON.stringify(graphCheck,null,2)+
157
- "\n\nExternal weights:\n"+JSON.stringify(dataCheck,null,2)+
158
- "\n\nLoading ONNX Runtime Web...");
159
- if (!graphCheck.ok) throw new Error("Graph URL request failed: "+JSON.stringify(graphCheck));
160
- if (!dataCheck.ok) throw new Error("External weights URL request failed: "+JSON.stringify(dataCheck));
161
- const ort=await getOrt();
162
- const strict=$("sam-strict").checked;
163
- const providers=strict?["webgpu"]:["webgpu","wasm"];
164
- session=await ort.InferenceSession.create(modelUrl,{
165
- executionProviders:providers,
166
- graphOptimizationLevel:"all",
167
- externalData:[{path:dataFile,data:dataUrl}]
168
- });
169
- const seconds=((performance.now()-start)/1000).toFixed(1);
170
- pill.textContent="PASS · "+seconds+" s"; pill.className="pill ok";
171
- const inputs=(session.inputNames||[]).map(n=>n+": "+JSON.stringify(session.inputMetadata?.[n]||{}));
172
- const outputs=(session.outputNames||[]).map(n=>n+": "+JSON.stringify(session.outputMetadata?.[n]||{}));
173
- log($("sam-model-log"),"PASS — session created for "+model.file+
174
- "\nElapsed: "+seconds+" s\nInputs: "+(session.inputNames||[]).join(", ")+
175
- "\nOutputs: "+(session.outputNames||[]).join(", ")+
176
- "\n\nInput metadata:\n"+inputs.join("\n")+"\n\nOutput metadata:\n"+outputs.join("\n")+
177
- "\n\nSession released. This is not full inference.");
178
- } catch(error) {
179
- pill.textContent="FAIL"; pill.className="pill bad";
180
- log($("sam-model-log"),"FAIL — "+model.file+
181
- "\nElapsed: "+((performance.now()-start)/1000).toFixed(1)+" s\n\n"+
182
- errorText(error)+"\n\nIf URL checks failed, investigate CORS, HTTP status, or the external-data URL. "+
183
- "If URL checks passed, the error may be an ONNX/WebGPU operator, model format, or memory issue.");
184
  } finally {
185
- if(session) await session.release().catch(()=>{});
186
- button.disabled=false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  }
189
- $("sam-check-browser").addEventListener("click",checkBrowser);
190
- $("sam-smoke").addEventListener("click",smokeTest);
191
- renderModels();
192
- await checkBrowser();
 
 
 
 
 
193
  }
194
  """
195
 
196
  @spaces.GPU
197
  def zerogpu_startup_marker():
198
- return "Browser-side WebGPU test only; no server inference."
199
 
200
- with gr.Blocks(title="SAM Audio WebGPU Compatibility Diagnostic") as demo:
201
  gr.Markdown(
202
- "## SAM Audio Small ONNX — browser WebGPU diagnostic\n"
203
- "ZeroGPU is present only to satisfy the Space's selected hosting hardware. "
204
- "The marker function is never called; model checks run locally in your browser."
205
  )
206
  gr.HTML(PAGE)
207
  demo.load(fn=None, inputs=None, outputs=None, js=CLIENT_JS)
 
2
  import spaces
3
 
4
  PAGE = r"""
5
+ <div id="sam-browser-app" style="font-family:system-ui,sans-serif;max-width:1050px;margin:auto">
6
+ <style>
7
+ #sam-browser-app .panel{border:1px solid var(--border-color-primary);border-radius:12px;padding:16px;margin:14px 0}
8
+ #sam-browser-app .muted{opacity:.75;font-size:.9rem}
9
+ #sam-browser-app .row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
10
+ #sam-browser-app label{display:block;margin:8px 0 4px;font-weight:600}
11
+ #sam-browser-app input[type=file],#sam-browser-app input[type=text],#sam-browser-app input[type=number]{box-sizing:border-box;max-width:100%;width:100%;padding:10px;border-radius:8px;border:1px solid var(--border-color-primary);background:var(--input-background-fill);color:var(--body-text-color)}
12
+ #sam-browser-app button{padding:10px 14px;border:0;border-radius:8px;background:var(--button-primary-background-fill);color:var(--button-primary-text-color);cursor:pointer}
13
+ #sam-browser-app button:disabled{opacity:.5;cursor:wait}
14
+ #sam-browser-app .log{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--background-fill-secondary);padding:12px;border-radius:8px;font:13px/1.5 ui-monospace,monospace;min-height:30px}
15
+ #sam-browser-app audio{width:100%;margin-top:8px}
16
+ #sam-browser-app .outputs{display:grid;grid-template-columns:1fr 1fr;gap:14px}
17
+ #sam-browser-app .out{min-width:0}
18
+ @media(max-width:650px){#sam-browser-app .outputs{grid-template-columns:1fr}}
19
+ </style>
20
+ <h1>SAM Audio Small — Browser WebGPU inference prototype</h1>
21
+ <p>This is a browser-side prototype. Audio processing runs on your local machine through WebGPU; the Space server only hosts this interface. No audio is uploaded to the Space.</p>
22
+ <div class="panel">
23
+ <h2>1. Input</h2>
24
+ <label for="sam-audio-file">Audio file</label>
25
+ <input id="sam-audio-file" type="file" accept="audio/*,.wav,.mp3,.flac,.ogg,.m4a">
26
+ <label for="sam-prompt">Describe the sound to isolate (English prompt recommended)</label>
27
+ <input id="sam-prompt" type="text" value="a person speaking" placeholder="a person speaking">
28
+ <div class="row">
29
+ <div style="flex:1;min-width:180px"><label for="sam-steps">ODE steps</label><input id="sam-steps" type="number" min="1" max="16" value="2"></div>
30
+ <div style="flex:2;min-width:220px"><p class="muted">Start with 1–2 steps to validate inference. The reference configuration uses 16 steps and is much slower.</p></div>
31
+ </div>
32
+ <div class="row" style="margin-top:12px"><button id="sam-run">Load models and separate</button><span id="sam-status" class="muted">Ready</span></div>
33
+ <div id="sam-log" class="log" style="margin-top:12px">Choose a short audio file to begin. First run downloads several GB of model files.</div>
34
+ </div>
35
+ <div class="panel">
36
+ <h2>2. Results</h2>
37
+ <div class="outputs">
38
+ <div class="out"><b>Target</b><audio id="sam-target-player" controls></audio><a id="sam-target-download" download="target.wav" hidden>Download target.wav</a></div>
39
+ <div class="out"><b>Residual</b><audio id="sam-residual-player" controls></audio><a id="sam-residual-download" download="residual.wav" hidden>Download residual.wav</a></div>
40
+ </div>
41
+ </div>
42
+ <div class="panel">
43
+ <h2>Important limitations</h2>
44
+ <ul>
45
+ <li>This is an experimental browser port of the repository's reference ONNX pipeline, not yet validated against its Python output.</li>
46
+ <li>It uses a small number of ODE steps by default for a first end-to-end test. Quality may be poor at 1–2 steps.</li>
47
+ <li>The reference implementation in the repository documents a 48 kHz audio model, T5-base text embeddings, a midpoint ODE solver, and the first/second 128 latent channels as target/residual.</li>
48
+ <li>WebGPU supports only a subset of ONNX operators. A session may load successfully but fail when the first inference runs.</li>
49
+ <li>First run may download several GB and use substantial browser RAM/VRAM. Use a short audio clip first.</li>
50
+ </ul>
51
+ </div>
52
  </div>
53
  """
54
 
55
  CLIENT_JS = r"""
56
  async () => {
57
+ const root = document.getElementById("sam-browser-app");
58
  if (!root || root.dataset.initialized === "yes") return;
59
  root.dataset.initialized = "yes";
60
  const $ = (id) => root.querySelector("#" + id);
61
+ const MODEL_BASE = "https://huggingface.co/matbee/sam-audio-small-onnx/resolve/main";
62
+ const MODEL_FILES = {
63
+ encoder: "dacvae_encoder.onnx",
64
+ decoder: "dacvae_decoder.onnx",
65
+ t5: "t5_encoder.onnx",
66
+ dit: "dit_single_step.onnx"
67
+ };
68
+ let ort = null;
69
+ let tokenizer = null;
70
+ let sessions = null;
71
+ let previousUrls = [];
72
+
73
+ function setLog(message) { $("sam-log").textContent = message; }
74
+ function setStatus(message) { $("sam-status").textContent = message; }
75
+ function errorDetails(error) {
76
+ const values = [];
77
  for (const key of ["name", "message", "code", "stack"]) {
78
+ try { if (error?.[key] !== undefined) values.push(key + ": " + String(error[key])); } catch (_) {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  }
80
+ try { values.push("JSON: " + JSON.stringify(error, Object.getOwnPropertyNames(error))); } catch (_) {}
81
+ return values.join("\n") || String(error);
82
  }
83
+ async function loadOrt() {
84
+ if (!ort) {
85
+ ort = await import("https://cdn.jsdelivr.net/npm/onnxruntime-web@1.23.2/+esm");
86
+ if (ort.env?.wasm) ort.env.wasm.numThreads = 1;
87
+ if (ort.env) ort.env.logLevel = "warning";
88
  }
89
+ return ort;
90
+ }
91
+ async function loadTokenizer() {
92
+ if (!tokenizer) {
93
+ const transformers = await import("https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.2");
94
+ tokenizer = await transformers.AutoTokenizer.from_pretrained("google-t5/t5-base");
 
 
 
95
  }
96
+ return tokenizer;
97
+ }
98
+ async function createSession(filename) {
99
+ const url = MODEL_BASE + "/" + filename;
100
+ const dataUrl = url + ".data";
101
+ const runtime = await loadOrt();
102
+ return runtime.InferenceSession.create(url, {
103
+ executionProviders: ["webgpu"],
104
+ graphOptimizationLevel: "all",
105
+ externalData: [{ path: filename + ".data", data: dataUrl }]
106
+ });
107
+ }
108
+ async function loadModels() {
109
+ if (sessions) return sessions;
110
+ const loaded = {};
111
+ const order = [
112
+ ["encoder", "dacvae_encoder.onnx"],
113
+ ["t5", "t5_encoder.onnx"],
114
+ ["dit", "dit_single_step.onnx"],
115
+ ["decoder", "dacvae_decoder.onnx"]
116
+ ];
117
  try {
118
+ for (const [key, filename] of order) {
119
+ setStatus("Loading " + filename + "…");
120
+ setLog("Loading model component: " + filename + "\nThe browser may download a large external .onnx.data file. Keep this tab open.");
121
+ loaded[key] = await createSession(filename);
122
+ setLog("Loaded " + filename + "\nInputs: " + loaded[key].inputNames.join(", ") + "\nOutputs: " + loaded[key].outputNames.join(", "));
123
+ }
124
+ await loadTokenizer();
125
+ sessions = loaded;
126
+ return sessions;
127
+ } catch (error) {
128
+ for (const session of Object.values(loaded)) {
129
+ try { await session.release(); } catch (_) {}
130
+ }
131
+ throw error;
132
+ }
133
+ }
134
+ function getTensorMetadata(session, name) {
135
+ try { return session.inputMetadata?.[name] || {}; } catch (_) { return {}; }
136
+ }
137
+ function floatTensor(data, dims) {
138
+ return new ort.Tensor("float32", data instanceof Float32Array ? data : new Float32Array(data), dims);
139
+ }
140
+ function int64Tensor(data, dims) {
141
+ const values = Array.from(data, value => BigInt(value));
142
+ return new ort.Tensor("int64", BigInt64Array.from(values), dims);
143
+ }
144
+ function boolTensor(data, dims) {
145
+ return new ort.Tensor("bool", Uint8Array.from(data, value => value ? 1 : 0), dims);
146
+ }
147
+ async function decodeAudio(file) {
148
+ const bytes = await file.arrayBuffer();
149
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
150
+ const context = new AudioContextClass();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  try {
152
+ const decoded = await context.decodeAudioData(bytes.slice(0));
153
+ const offline = new OfflineAudioContext(1, Math.ceil(decoded.duration * 48000), 48000);
154
+ const source = offline.createBufferSource();
155
+ const mono = offline.createBuffer(1, decoded.length, decoded.sampleRate);
156
+ const channels = [];
157
+ for (let i = 0; i < decoded.numberOfChannels; i++) channels.push(decoded.getChannelData(i));
158
+ const monoData = mono.getChannelData(0);
159
+ for (let i = 0; i < monoData.length; i++) {
160
+ let sum = 0;
161
+ for (const channel of channels) sum += channel[i];
162
+ monoData[i] = sum / channels.length;
163
+ }
164
+ source.buffer = mono;
165
+ source.connect(offline.destination);
166
+ source.start();
167
+ const resampled = await offline.startRendering();
168
+ return {samples: new Float32Array(resampled.getChannelData(0)), sampleRate: 48000};
 
 
 
 
 
 
 
 
 
 
 
 
169
  } finally {
170
+ await context.close().catch(() => {});
171
+ }
172
+ }
173
+ async function runSession(session, outputName, feeds) {
174
+ const outputs = await session.run(feeds);
175
+ const tensor = outputs[outputName] || outputs[session.outputNames[0]];
176
+ if (!tensor) throw new Error("Expected output not found: " + outputName);
177
+ return tensor.data;
178
+ }
179
+ async function encodeAudio(samples) {
180
+ const session = sessions.encoder;
181
+ const input = new ort.Tensor("float32", samples, [1, 1, samples.length]);
182
+ const out = await session.run({audio: input});
183
+ const tensor = out.latent_features || out[session.outputNames[0]];
184
+ return {data: new Float32Array(tensor.data), dims: tensor.dims};
185
+ }
186
+ async function encodeText(text) {
187
+ const tok = await loadTokenizer();
188
+ const encoded = await tok(text, {
189
+ padding: "max_length",
190
+ truncation: true,
191
+ max_length: 77
192
+ });
193
+ const idsRaw = encoded.input_ids?.data ?? encoded.input_ids;
194
+ const maskRaw = encoded.attention_mask?.data ?? encoded.attention_mask;
195
+ const ids = Array.from(idsRaw);
196
+ const mask = Array.from(maskRaw);
197
+ const idsTensor = int64Tensor(ids, [1, ids.length]);
198
+ const maskTensor = int64Tensor(mask, [1, mask.length]);
199
+ const out = await sessions.t5.run({input_ids: idsTensor, attention_mask: maskTensor});
200
+ const tensor = out.hidden_states || out[sessions.t5.outputNames[0]];
201
+ return {
202
+ features: new Float32Array(tensor.data),
203
+ featureDims: tensor.dims,
204
+ mask: Uint8Array.from(mask, value => value ? 1 : 0),
205
+ maskDims: [1, mask.length]
206
+ };
207
+ }
208
+ async function runDit(noisy, timeValue, audioFeatures, text, sequenceLength) {
209
+ const batch = 1;
210
+ const zerosInt = new BigInt64Array(sequenceLength);
211
+ const mask = new Uint8Array(sequenceLength).fill(1);
212
+ const video = new Float32Array(1024 * sequenceLength);
213
+ const feeds = {
214
+ noisy_audio: floatTensor(noisy, [batch, sequenceLength, 256]),
215
+ time: floatTensor(new Float32Array([timeValue]), [1]),
216
+ audio_features: floatTensor(audioFeatures, [batch, sequenceLength, 256]),
217
+ text_features: floatTensor(text.features, text.featureDims),
218
+ text_mask: boolTensor(text.mask, text.maskDims),
219
+ masked_video_features: floatTensor(video, [batch, 1024, sequenceLength]),
220
+ anchor_ids: new ort.Tensor("int64", zerosInt, [batch, sequenceLength]),
221
+ anchor_alignment: new ort.Tensor("int64", new BigInt64Array(sequenceLength), [batch, sequenceLength]),
222
+ audio_pad_mask: boolTensor(mask, [batch, sequenceLength])
223
+ };
224
+ const outputs = await sessions.dit.run(feeds);
225
+ const tensor = outputs.velocity || outputs[sessions.dit.outputNames[0]];
226
+ return new Float32Array(tensor.data);
227
+ }
228
+ async function runOde(initial, audioFeatures, text, steps, sequenceLength) {
229
+ let x = new Float32Array(initial);
230
+ const dt = 1 / steps;
231
+ for (let i = 0; i < steps; i++) {
232
+ setStatus("Denoising step " + (i + 1) + "/" + steps + " (1/2)…");
233
+ const t = i * dt;
234
+ const k1 = await runDit(x, t, audioFeatures, text, sequenceLength);
235
+ const midpoint = new Float32Array(x.length);
236
+ for (let j = 0; j < x.length; j++) midpoint[j] = x[j] + (dt / 2) * k1[j];
237
+ setStatus("Denoising step " + (i + 1) + "/" + steps + " (2/2)…");
238
+ const k2 = await runDit(midpoint, t + dt / 2, audioFeatures, text, sequenceLength);
239
+ for (let j = 0; j < x.length; j++) x[j] += dt * k2[j];
240
+ setLog("ODE midpoint solver\nStep " + (i + 1) + "/" + steps + " complete\nLatent tensor: [1, " + sequenceLength + ", 256]");
241
+ }
242
+ return x;
243
+ }
244
+ async function decodeLatent(latent, latentChannels, timeSteps) {
245
+ const session = sessions.decoder;
246
+ const chunkSize = 25;
247
+ const hopLength = 1920;
248
+ const chunks = [];
249
+ for (let start = 0; start < timeSteps; start += chunkSize) {
250
+ const actual = Math.min(chunkSize, timeSteps - start);
251
+ const chunk = new Float32Array(latentChannels * chunkSize);
252
+ for (let c = 0; c < latentChannels; c++) {
253
+ for (let t = 0; t < actual; t++) {
254
+ chunk[c * chunkSize + t] = latent[c * timeSteps + start + t];
255
+ }
256
+ }
257
+ const out = await session.run({
258
+ latent_features: floatTensor(chunk, [1, latentChannels, chunkSize])
259
+ });
260
+ const tensor = out.waveform || out[session.outputNames[0]];
261
+ const data = tensor.data;
262
+ const count = actual * hopLength;
263
+ chunks.push(new Float32Array(data.slice(0, count)));
264
+ setStatus("Decoding audio " + Math.min(start + chunkSize, timeSteps) + "/" + timeSteps + " latent frames…");
265
+ }
266
+ const totalLength = chunks.reduce((sum, part) => sum + part.length, 0);
267
+ const output = new Float32Array(totalLength);
268
+ let offset = 0;
269
+ for (const chunk of chunks) { output.set(chunk, offset); offset += chunk.length; }
270
+ return output;
271
+ }
272
+ function wavBlob(samples, sampleRate) {
273
+ const buffer = new ArrayBuffer(44 + samples.length * 2);
274
+ const view = new DataView(buffer);
275
+ function writeString(offset, value) { for (let i = 0; i < value.length; i++) view.setUint8(offset + i, value.charCodeAt(i)); }
276
+ writeString(0, "RIFF"); view.setUint32(4, 36 + samples.length * 2, true);
277
+ writeString(8, "WAVE"); writeString(12, "fmt "); view.setUint32(16, 16, true);
278
+ view.setUint16(20, 1, true); view.setUint16(22, 1, true); view.setUint32(24, sampleRate, true);
279
+ view.setUint32(28, sampleRate * 2, true); view.setUint16(32, 2, true); view.setUint16(34, 16, true);
280
+ writeString(36, "data"); view.setUint32(40, samples.length * 2, true);
281
+ for (let i = 0; i < samples.length; i++) {
282
+ const sample = Math.max(-1, Math.min(1, samples[i]));
283
+ view.setInt16(44 + i * 2, sample < 0 ? sample * 32768 : sample * 32767, true);
284
+ }
285
+ return new Blob([buffer], {type:"audio/wav"});
286
+ }
287
+ function showOutput(playerId, linkId, samples, sampleRate, filename) {
288
+ const url = URL.createObjectURL(wavBlob(samples, sampleRate));
289
+ previousUrls.push(url);
290
+ $(playerId).src = url;
291
+ const link = $(linkId); link.href = url; link.download = filename; link.hidden = false; link.textContent = "Download " + filename;
292
+ }
293
+ async function separate() {
294
+ const file = $("sam-audio-file").files?.[0];
295
+ const prompt = $("sam-prompt").value.trim();
296
+ const steps = Number.parseInt($("sam-steps").value, 10);
297
+ if (!file) throw new Error("Choose an audio file first.");
298
+ if (!prompt) throw new Error("Enter a text prompt.");
299
+ if (!Number.isInteger(steps) || steps < 1 || steps > 16) throw new Error("ODE steps must be between 1 and 16.");
300
+ const audio = await decodeAudio(file);
301
+ if (audio.samples.length > 48000 * 20) throw new Error("For this first prototype, use audio no longer than 20 seconds.");
302
+ await loadModels();
303
+ setStatus("Encoding audio…");
304
+ const encoded = await encodeAudio(audio.samples);
305
+ if (encoded.dims.length !== 3) throw new Error("Unexpected encoder output dimensions: " + JSON.stringify(encoded.dims));
306
+ const [batch, latentChannels, timeSteps] = encoded.dims;
307
+ if (batch !== 1 || latentChannels !== 128) {
308
+ throw new Error("Unexpected audio latent shape " + JSON.stringify(encoded.dims) + "; expected [1, 128, time].");
309
+ }
310
+ setStatus("Encoding text…");
311
+ const text = await encodeText(prompt);
312
+ const mixture = new Float32Array(timeSteps * 256);
313
+ for (let t = 0; t < timeSteps; t++) {
314
+ for (let c = 0; c < latentChannels; c++) {
315
+ const value = encoded.data[c * timeSteps + t];
316
+ mixture[t * 256 + c] = value;
317
+ mixture[t * 256 + latentChannels + c] = value;
318
+ }
319
+ }
320
+ const initial = new Float32Array(timeSteps * 256);
321
+ // Standard normal noise, matching the reference implementation's randn initialization.
322
+ for (let i = 0; i < initial.length; i += 2) {
323
+ const u1 = Math.max(Number.EPSILON, Math.random());
324
+ const u2 = Math.random();
325
+ const radius = Math.sqrt(-2 * Math.log(u1));
326
+ initial[i] = radius * Math.cos(2 * Math.PI * u2);
327
+ if (i + 1 < initial.length) initial[i + 1] = radius * Math.sin(2 * Math.PI * u2);
328
+ }
329
+ const result = await runOde(initial, mixture, text, steps, timeSteps);
330
+ const targetLatent = new Float32Array(latentChannels * timeSteps);
331
+ const residualLatent = new Float32Array(latentChannels * timeSteps);
332
+ for (let t = 0; t < timeSteps; t++) {
333
+ for (let c = 0; c < latentChannels; c++) {
334
+ targetLatent[c * timeSteps + t] = result[t * 256 + c];
335
+ residualLatent[c * timeSteps + t] = result[t * 256 + latentChannels + c];
336
+ }
337
  }
338
+ setStatus("Decoding target…");
339
+ const target = await decodeLatent(targetLatent, latentChannels, timeSteps);
340
+ setStatus("Decoding residual…");
341
+ const residual = await decodeLatent(residualLatent, latentChannels, timeSteps);
342
+ const expectedSamples = audio.samples.length;
343
+ showOutput("sam-target-player", "sam-target-download", target.slice(0, expectedSamples), 48000, "target.wav");
344
+ showOutput("sam-residual-player", "sam-residual-download", residual.slice(0, expectedSamples), 48000, "residual.wav");
345
+ setStatus("Finished");
346
+ setLog("Finished browser inference.\nInput: " + file.name + "\nPrompt: " + prompt +
347
+ "\nODE steps: " + steps + "\nInput samples: " + expectedSamples +
348
+ "\nLatent shape: " + JSON.stringify(encoded.dims) +
349
+ "\nTarget samples: " + Math.min(target.length, expectedSamples) +
350
+ "\nResidual samples: " + Math.min(residual.length, expectedSamples) +
351
+ "\n\nThis is an experimental port. Compare quality and tensor shapes against the Python reference before relying on results.");
352
  }
353
+ $("sam-run").addEventListener("click", async () => {
354
+ const button = $("sam-run");
355
+ button.disabled = true;
356
+ try { await separate(); }
357
+ catch (error) {
358
+ setStatus("Failed");
359
+ setLog("INFERENCE FAILED\n" + errorDetails(error));
360
+ } finally { button.disabled = false; }
361
+ });
362
  }
363
  """
364
 
365
  @spaces.GPU
366
  def zerogpu_startup_marker():
367
+ return "No server inference; browser WebGPU prototype."
368
 
369
+ with gr.Blocks(title="SAM Audio Browser WebGPU Inference Prototype") as demo:
370
  gr.Markdown(
371
+ "## SAM Audio Small — browser WebGPU inference prototype\n"
372
+ "ZeroGPU is selected only because of the Space's hosting configuration. "
373
+ "The marker function is never called. All model inference is attempted locally in the browser."
374
  )
375
  gr.HTML(PAGE)
376
  demo.load(fn=None, inputs=None, outputs=None, js=CLIENT_JS)