Spaces:
Running on Zero
Running on Zero
Upload app.py
Browse files
app.py
CHANGED
|
@@ -2,206 +2,375 @@ import gradio as gr
|
|
| 2 |
import spaces
|
| 3 |
|
| 4 |
PAGE = r"""
|
| 5 |
-
<div id="sam-
|
| 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 |
-
<div id="sam-
|
| 39 |
-
<div id="sam-
|
| 40 |
-
</
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
</div>
|
| 47 |
"""
|
| 48 |
|
| 49 |
CLIENT_JS = r"""
|
| 50 |
async () => {
|
| 51 |
-
const root = document.getElementById("sam-
|
| 52 |
if (!root || root.dataset.initialized === "yes") return;
|
| 53 |
root.dataset.initialized = "yes";
|
| 54 |
const $ = (id) => root.querySelector("#" + id);
|
| 55 |
-
const
|
| 56 |
-
const
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
let
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
| 68 |
for (const key of ["name", "message", "code", "stack"]) {
|
| 69 |
-
try { if (error[key] !== undefined)
|
| 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
|
| 91 |
-
if (!
|
| 92 |
-
|
| 93 |
-
if (
|
| 94 |
-
if (
|
| 95 |
}
|
| 96 |
-
return
|
| 97 |
-
}
|
| 98 |
-
async function
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 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
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
} catch
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 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
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
const
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 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 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
}
|
| 189 |
-
$("sam-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
}
|
| 194 |
"""
|
| 195 |
|
| 196 |
@spaces.GPU
|
| 197 |
def zerogpu_startup_marker():
|
| 198 |
-
return "
|
| 199 |
|
| 200 |
-
with gr.Blocks(title="SAM Audio WebGPU
|
| 201 |
gr.Markdown(
|
| 202 |
-
"## SAM Audio Small
|
| 203 |
-
"ZeroGPU is
|
| 204 |
-
"The marker function is never called
|
| 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)
|