fuze-eletrik commited on
Commit
971eaa4
·
verified ·
1 Parent(s): 1eb359a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +39 -23
app.py CHANGED
@@ -1,6 +1,6 @@
1
  import gradio as gr
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>
@@ -18,8 +18,8 @@ PAGE = r"""
18
  #sam-browser-app .out{min-width:0}
19
  @media(max-width:650px){#sam-browser-app .outputs{grid-template-columns:1fr}}
20
  </style>
21
- <h1>SAM Audio Small — Browser WebGPU inference prototype</h1>
22
- <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>
23
  <div class="panel">
24
  <h2>1. Input</h2>
25
  <label for="sam-audio-file">Audio file</label>
@@ -27,6 +27,7 @@ PAGE = r"""
27
  <label for="sam-prompt">Describe the sound to isolate (English prompt recommended)</label>
28
  <input id="sam-prompt" type="text" value="a person speaking" placeholder="a person speaking">
29
  <div class="row">
 
30
  <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>
31
  <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>
32
  </div>
@@ -47,13 +48,14 @@ PAGE = r"""
47
  <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>
48
  <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>
49
  <li>WebGPU supports only a subset of ONNX operators. A session may load successfully but fail when the first inference runs.</li>
 
50
  <li>First run may download several GB and use substantial browser RAM/VRAM. Use a short audio clip first.</li>
51
  <li>ONNX files and external weights are cached in this browser when storage quota allows. The browser may evict cached files, and clearing site data removes them.</li>
52
  </ul>
53
  </div>
54
  </div>
55
  """
56
-
57
  CLIENT_JS = r"""
58
  async () => {
59
  const root = document.getElementById("sam-browser-app");
@@ -71,8 +73,8 @@ async () => {
71
  let ort = null;
72
  let tokenizer = null;
73
  let sessions = null;
 
74
  let previousUrls = [];
75
-
76
  function setLog(message) { $("sam-log").textContent = message; }
77
  function setStatus(message) { $("sam-status").textContent = message; }
78
  function errorDetails(error) {
@@ -130,14 +132,14 @@ async () => {
130
  }
131
  return new Uint8Array(await response.arrayBuffer());
132
  }
133
- async function createSession(filename) {
134
  const url = MODEL_BASE + "/" + filename;
135
  const runtime = await loadOrt();
136
  const modelBytes = await fetchCachedBytes(url, filename);
137
  const dataUrl = url + ".data";
138
  const externalBytes = await fetchCachedBytes(dataUrl, filename + ".data (external weights)");
139
  return runtime.InferenceSession.create(modelBytes, {
140
- executionProviders: ["webgpu"],
141
  graphOptimizationLevel: "all",
142
  externalData: [{ path: filename + ".data", data: externalBytes.buffer }]
143
  });
@@ -148,6 +150,7 @@ async () => {
148
  try { await session.release(); } catch (_) {}
149
  }
150
  sessions = null;
 
151
  }
152
  if ("caches" in window) {
153
  try { await caches.delete(CACHE_NAME); } catch (_) {}
@@ -155,8 +158,15 @@ async () => {
155
  setStatus("Cache cleared");
156
  setLog("The browser's SAM Audio ONNX cache was cleared. Reload the page before running inference again.");
157
  }
158
- async function loadModels() {
159
- if (sessions) return sessions;
 
 
 
 
 
 
 
160
  const loaded = {};
161
  const order = [
162
  ["encoder", "dacvae_encoder.onnx"],
@@ -168,11 +178,12 @@ async () => {
168
  for (const [key, filename] of order) {
169
  setStatus("Loading " + filename + "…");
170
  setLog("Loading model component: " + filename + "\nThe browser may download a large external .onnx.data file. Keep this tab open.");
171
- loaded[key] = await createSession(filename);
172
  setLog("Loaded " + filename + "\nInputs: " + loaded[key].inputNames.join(", ") + "\nOutputs: " + loaded[key].outputNames.join(", "));
173
  }
174
  await loadTokenizer();
175
  sessions = loaded;
 
176
  return sessions;
177
  } catch (error) {
178
  for (const session of Object.values(loaded)) {
@@ -301,7 +312,7 @@ async () => {
301
  const chunk = new Float32Array(latentChannels * chunkSize);
302
  for (let c = 0; c < latentChannels; c++) {
303
  for (let t = 0; t < actual; t++) {
304
- chunk[c * chunkSize + t] = latent[c * timeSteps + start + t];
305
  }
306
  }
307
  const out = await session.run({
@@ -330,7 +341,7 @@ async () => {
330
  writeString(36, "data"); view.setUint32(40, samples.length * 2, true);
331
  for (let i = 0; i < samples.length; i++) {
332
  const sample = Math.max(-1, Math.min(1, samples[i]));
333
- view.setInt16(44 + i * 2, sample < 0 ? sample * 32768 : sample * 32767, true);
334
  }
335
  return new Blob([buffer], {type:"audio/wav"});
336
  }
@@ -344,13 +355,17 @@ async () => {
344
  const file = $("sam-audio-file").files?.[0];
345
  const prompt = $("sam-prompt").value.trim();
346
  const steps = Number.parseInt($("sam-steps").value, 10);
 
 
347
  if (!file) throw new Error("Choose an audio file first.");
348
  if (!prompt) throw new Error("Enter a text prompt.");
349
  if (!Number.isInteger(steps) || steps < 1 || steps > 16) throw new Error("ODE steps must be between 1 and 16.");
350
  const audio = await decodeAudio(file);
351
  if (audio.samples.length > 48000 * 20) throw new Error("For this first prototype, use audio no longer than 20 seconds.");
352
- await loadModels();
353
- setStatus("Encoding audio…");
 
 
354
  const encoded = await encodeAudio(audio.samples);
355
  if (encoded.dims.length !== 3) throw new Error("Unexpected encoder output dimensions: " + JSON.stringify(encoded.dims));
356
  const [batch, latentChannels, timeSteps] = encoded.dims;
@@ -373,16 +388,16 @@ async () => {
373
  const u1 = Math.max(Number.EPSILON, Math.random());
374
  const u2 = Math.random();
375
  const radius = Math.sqrt(-2 * Math.log(u1));
376
- initial[i] = radius * Math.cos(2 * Math.PI * u2);
377
- if (i + 1 < initial.length) initial[i + 1] = radius * Math.sin(2 * Math.PI * u2);
378
  }
379
  const result = await runOde(initial, mixture, text, steps, timeSteps);
380
  const targetLatent = new Float32Array(latentChannels * timeSteps);
381
  const residualLatent = new Float32Array(latentChannels * timeSteps);
382
  for (let t = 0; t < timeSteps; t++) {
383
  for (let c = 0; c < latentChannels; c++) {
384
- targetLatent[c * timeSteps + t] = result[t * 256 + c];
385
- residualLatent[c * timeSteps + t] = result[t * 256 + latentChannels + c];
386
  }
387
  }
388
  setStatus("Decoding target…");
@@ -394,6 +409,7 @@ async () => {
394
  showOutput("sam-residual-player", "sam-residual-download", residual.slice(0, expectedSamples), 48000, "residual.wav");
395
  setStatus("Finished");
396
  setLog("Finished browser inference.\nInput: " + file.name + "\nPrompt: " + prompt +
 
397
  "\nODE steps: " + steps + "\nInput samples: " + expectedSamples +
398
  "\nLatent shape: " + JSON.stringify(encoded.dims) +
399
  "\nTarget samples: " + Math.min(target.length, expectedSamples) +
@@ -417,19 +433,19 @@ async () => {
417
  });
418
  }
419
  """
420
-
421
  @spaces.GPU
422
  def zerogpu_startup_marker():
423
  return "No server inference; browser WebGPU prototype."
424
-
425
  with gr.Blocks(title="SAM Audio Browser WebGPU Inference Prototype") as demo:
426
  gr.Markdown(
427
- "## SAM Audio Small — browser WebGPU inference prototype\n"
428
  "ZeroGPU is selected only because of the Space's hosting configuration. "
429
  "The marker function is never called. All model inference is attempted locally in the browser."
430
  )
431
  gr.HTML(PAGE)
432
  demo.load(fn=None, inputs=None, outputs=None, js=CLIENT_JS)
433
-
434
  if __name__ == "__main__":
435
- demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)
 
1
  import gradio as gr
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>
 
18
  #sam-browser-app .out{min-width:0}
19
  @media(max-width:650px){#sam-browser-app .outputs{grid-template-columns:1fr}}
20
  </style>
21
+ <h1>SAM Audio Small — Browser inference prototype</h1>
22
+ <p>This is a browser-side prototype. Audio processing runs locally in your browser using either WebGPU (GPU) or WebAssembly (CPU). The Space server only hosts this interface. No audio is uploaded to the Space.</p>
23
  <div class="panel">
24
  <h2>1. Input</h2>
25
  <label for="sam-audio-file">Audio file</label>
 
27
  <label for="sam-prompt">Describe the sound to isolate (English prompt recommended)</label>
28
  <input id="sam-prompt" type="text" value="a person speaking" placeholder="a person speaking">
29
  <div class="row">
30
+ <div style="flex:1;min-width:220px"><label for="sam-provider">Inference device</label><select id="sam-provider" style="box-sizing:border-box;width:100%;padding:10px;border-radius:8px;border:1px solid var(--border-color-primary);background:var(--input-background-fill);color:var(--body-text-color)"><option value="webgpu" selected>GPU — WebGPU</option><option value="wasm">CPU — WebAssembly (WASM)</option></select></div>
31
  <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>
32
  <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>
33
  </div>
 
48
  <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>
49
  <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>
50
  <li>WebGPU supports only a subset of ONNX operators. A session may load successfully but fail when the first inference runs.</li>
51
+ <li>CPU mode uses ONNX Runtime WebAssembly (WASM) and may be much slower than WebGPU. Use a short audio clip first.</li>
52
  <li>First run may download several GB and use substantial browser RAM/VRAM. Use a short audio clip first.</li>
53
  <li>ONNX files and external weights are cached in this browser when storage quota allows. The browser may evict cached files, and clearing site data removes them.</li>
54
  </ul>
55
  </div>
56
  </div>
57
  """
58
+
59
  CLIENT_JS = r"""
60
  async () => {
61
  const root = document.getElementById("sam-browser-app");
 
73
  let ort = null;
74
  let tokenizer = null;
75
  let sessions = null;
76
+ let sessionsProvider = null;
77
  let previousUrls = [];
 
78
  function setLog(message) { $("sam-log").textContent = message; }
79
  function setStatus(message) { $("sam-status").textContent = message; }
80
  function errorDetails(error) {
 
132
  }
133
  return new Uint8Array(await response.arrayBuffer());
134
  }
135
+ async function createSession(filename, provider) {
136
  const url = MODEL_BASE + "/" + filename;
137
  const runtime = await loadOrt();
138
  const modelBytes = await fetchCachedBytes(url, filename);
139
  const dataUrl = url + ".data";
140
  const externalBytes = await fetchCachedBytes(dataUrl, filename + ".data (external weights)");
141
  return runtime.InferenceSession.create(modelBytes, {
142
+ executionProviders: [provider],
143
  graphOptimizationLevel: "all",
144
  externalData: [{ path: filename + ".data", data: externalBytes.buffer }]
145
  });
 
150
  try { await session.release(); } catch (_) {}
151
  }
152
  sessions = null;
153
+ sessionsProvider = null;
154
  }
155
  if ("caches" in window) {
156
  try { await caches.delete(CACHE_NAME); } catch (_) {}
 
158
  setStatus("Cache cleared");
159
  setLog("The browser's SAM Audio ONNX cache was cleared. Reload the page before running inference again.");
160
  }
161
+ async function loadModels(provider) {
162
+ if (sessions && sessionsProvider === provider) return sessions;
163
+ if (sessions) {
164
+ for (const session of Object.values(sessions)) {
165
+ try { await session.release(); } catch (_) {}
166
+ }
167
+ sessions = null;
168
+ sessionsProvider = null;
169
+ }
170
  const loaded = {};
171
  const order = [
172
  ["encoder", "dacvae_encoder.onnx"],
 
178
  for (const [key, filename] of order) {
179
  setStatus("Loading " + filename + "…");
180
  setLog("Loading model component: " + filename + "\nThe browser may download a large external .onnx.data file. Keep this tab open.");
181
+ loaded[key] = await createSession(filename, provider);
182
  setLog("Loaded " + filename + "\nInputs: " + loaded[key].inputNames.join(", ") + "\nOutputs: " + loaded[key].outputNames.join(", "));
183
  }
184
  await loadTokenizer();
185
  sessions = loaded;
186
+ sessionsProvider = provider;
187
  return sessions;
188
  } catch (error) {
189
  for (const session of Object.values(loaded)) {
 
312
  const chunk = new Float32Array(latentChannels * chunkSize);
313
  for (let c = 0; c < latentChannels; c++) {
314
  for (let t = 0; t < actual; t++) {
315
+ chunk[c \ chunkSize + t] = latent[c \ timeSteps + start + t];
316
  }
317
  }
318
  const out = await session.run({
 
341
  writeString(36, "data"); view.setUint32(40, samples.length * 2, true);
342
  for (let i = 0; i < samples.length; i++) {
343
  const sample = Math.max(-1, Math.min(1, samples[i]));
344
+ view.setInt16(44 + i \ 2, sample < 0 ? sample \ 32768 : sample * 32767, true);
345
  }
346
  return new Blob([buffer], {type:"audio/wav"});
347
  }
 
355
  const file = $("sam-audio-file").files?.[0];
356
  const prompt = $("sam-prompt").value.trim();
357
  const steps = Number.parseInt($("sam-steps").value, 10);
358
+ const provider = $("sam-provider").value;
359
+ const deviceLabel = provider === "wasm" ? "CPU (WASM)" : "GPU (WebGPU)";
360
  if (!file) throw new Error("Choose an audio file first.");
361
  if (!prompt) throw new Error("Enter a text prompt.");
362
  if (!Number.isInteger(steps) || steps < 1 || steps > 16) throw new Error("ODE steps must be between 1 and 16.");
363
  const audio = await decodeAudio(file);
364
  if (audio.samples.length > 48000 * 20) throw new Error("For this first prototype, use audio no longer than 20 seconds.");
365
+ setStatus("Loading models for " + deviceLabel + "…");
366
+ await loadModels(provider);
367
+ const startedAt = performance.now();
368
+ setStatus("Encoding audio on " + deviceLabel + "…");
369
  const encoded = await encodeAudio(audio.samples);
370
  if (encoded.dims.length !== 3) throw new Error("Unexpected encoder output dimensions: " + JSON.stringify(encoded.dims));
371
  const [batch, latentChannels, timeSteps] = encoded.dims;
 
388
  const u1 = Math.max(Number.EPSILON, Math.random());
389
  const u2 = Math.random();
390
  const radius = Math.sqrt(-2 * Math.log(u1));
391
+ initial[i] = radius \ Math.cos(2 \ Math.PI * u2);
392
+ if (i + 1 < initial.length) initial[i + 1] = radius \ Math.sin(2 \ Math.PI * u2);
393
  }
394
  const result = await runOde(initial, mixture, text, steps, timeSteps);
395
  const targetLatent = new Float32Array(latentChannels * timeSteps);
396
  const residualLatent = new Float32Array(latentChannels * timeSteps);
397
  for (let t = 0; t < timeSteps; t++) {
398
  for (let c = 0; c < latentChannels; c++) {
399
+ targetLatent[c \ timeSteps + t] = result[t \ 256 + c];
400
+ residualLatent[c \ timeSteps + t] = result[t \ 256 + latentChannels + c];
401
  }
402
  }
403
  setStatus("Decoding target…");
 
409
  showOutput("sam-residual-player", "sam-residual-download", residual.slice(0, expectedSamples), 48000, "residual.wav");
410
  setStatus("Finished");
411
  setLog("Finished browser inference.\nInput: " + file.name + "\nPrompt: " + prompt +
412
+ "\nDevice: " + deviceLabel + "\nProcessing time: " + ((performance.now() - startedAt) / 1000).toFixed(2) + " s" +
413
  "\nODE steps: " + steps + "\nInput samples: " + expectedSamples +
414
  "\nLatent shape: " + JSON.stringify(encoded.dims) +
415
  "\nTarget samples: " + Math.min(target.length, expectedSamples) +
 
433
  });
434
  }
435
  """
436
+
437
  @spaces.GPU
438
  def zerogpu_startup_marker():
439
  return "No server inference; browser WebGPU prototype."
440
+
441
  with gr.Blocks(title="SAM Audio Browser WebGPU Inference Prototype") as demo:
442
  gr.Markdown(
443
+ "## SAM Audio Small — browser inference prototype\n"
444
  "ZeroGPU is selected only because of the Space's hosting configuration. "
445
  "The marker function is never called. All model inference is attempted locally in the browser."
446
  )
447
  gr.HTML(PAGE)
448
  demo.load(fn=None, inputs=None, outputs=None, js=CLIENT_JS)
449
+
450
  if __name__ == "__main__":
451
+ demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)