|
<!doctype html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="utf-8" /> |
|
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
|
<title>MagentaRT Realtime Tester (rt-mode)</title> |
|
<style> |
|
:root { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; } |
|
body { margin: 0; padding: 24px; background: #0b0b0f; color: #e6e6ea; } |
|
h1 { font-size: 20px; margin: 0 0 12px; } |
|
.card { background: #15151c; border: 1px solid #232334; border-radius: 12px; padding: 16px; margin-bottom: 16px; } |
|
label { display: block; font-size: 12px; color: #b0b0bb; margin: 8px 0 6px; } |
|
input[type="text"], textarea, input[type="number"] { width: 100%; padding: 8px 10px; border-radius: 8px; border: 1px solid #2a2a3a; background: #0f0f14; color: #e6e6ea; } |
|
textarea { min-height: 72px; resize: vertical; } |
|
.row { display: grid; grid-template-columns: repeat(12, 1fr); gap: 12px; } |
|
.col-6 { grid-column: span 6; } |
|
.col-4 { grid-column: span 4; } |
|
.col-3 { grid-column: span 3; } |
|
.col-2 { grid-column: span 2; } |
|
.col-12 { grid-column: span 12; } |
|
.btn { appearance: none; padding: 10px 14px; border-radius: 10px; border: 1px solid #2a2a3a; background: #20202a; color: #fff; cursor: pointer; } |
|
.btn:hover { background: #2a2a36; } |
|
.btn[disabled] { opacity: 0.5; cursor: not-allowed; } |
|
.controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } |
|
.small { font-size: 12px; color: #a6a6b3; } |
|
.range-row { display: grid; grid-template-columns: 100px 1fr 60px; gap: 10px; align-items: center; } |
|
input[type="range"] { width: 100%; } |
|
.log { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; background: #0f0f14; border: 1px solid #2a2a3a; padding: 10px; border-radius: 8px; min-height: 160px; white-space: pre-wrap; overflow: auto; } |
|
.ok { color: #9fe870; } |
|
.warn { color: #ffda6b; } |
|
.err { color: #ff8080; } |
|
.badge { padding: 2px 6px; border-radius: 999px; background: #2a2a3a; font-size: 12px; } |
|
.sep { height: 1px; background: #212133; margin: 12px 0; } |
|
|
|
.row > [class^="col-"] { min-width: 0; } |
|
|
|
@media (min-width: 901px) { |
|
.row { column-gap: 24px; } |
|
.col-6 { min-width: 0; overflow: hidden; } |
|
} |
|
|
|
|
|
input[type="range"] { margin: 0; } |
|
|
|
|
|
.style-row { overflow: hidden; } |
|
|
|
.style-list { display: flex; flex-direction: column; gap: 12px; } |
|
|
|
.style-row { |
|
display: grid; |
|
grid-template-columns: minmax(0,1fr) 140px 70px 32px; |
|
gap: 10px; |
|
align-items: center; |
|
} |
|
.style-row .style-name, |
|
.style-row .style-range { min-width: 0; } |
|
.style-row .remove { padding: 6px 8px; line-height: 1; } |
|
@media (max-width: 900px) { |
|
.row { grid-template-columns: 1fr; } |
|
.col-6, .col-12, .col-4, .col-3, .col-2 { grid-column: 1 / -1; } |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<h1>MagentaRT Realtime Tester <span class="badge">rt-mode</span></h1> |
|
|
|
<div class="card"> |
|
<div class="row"> |
|
<div class="col-12"><div class="small">Model selection</div></div> |
|
<div class="col-6"> |
|
<label>Checkpoint repo (HF)</label> |
|
<input id="selRepo" type="text" placeholder="e.g., thepatch/magenta-ft" /> |
|
</div> |
|
<div class="col-3"> |
|
<label>Checkpoint step</label> |
|
<input id="selStep" type="number" min="0" step="1" placeholder="e.g., 1863001" /> |
|
</div> |
|
<div class="col-3"> |
|
<label>Base size</label> |
|
<input id="selSize" type="text" placeholder="large" value="large" /> |
|
</div> |
|
<div class="col-12 controls"> |
|
<label class="small"><input id="chkBase" type="checkbox" /> Use base model (no checkpoint)</label> |
|
<label class="small"><input id="selPrewarm" type="checkbox" checked /> Prewarm before returning</label> |
|
<label class="small"><input id="selStopActive" type="checkbox" checked /> Stop any active jam</label> |
|
<button id="btnSelectModel" class="btn">Select model & warm up</button> |
|
<span id="selStatus" class="small"></span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div id="healthBanner" class="small" style="margin:8px 0 12px;"></div> |
|
|
|
<div class="card"> |
|
<div class="row"> |
|
<div class="col-12"> |
|
<label>WebSocket URL</label> |
|
<input id="wsUrl" type="text" value="wss://thecollabagepatch-magenta-retry.hf.space/ws/jam" /> |
|
</div> |
|
<div class="col-12 controls"> |
|
<button id="btnStart" class="btn">Start</button> |
|
<button id="btnStop" class="btn" disabled>Stop</button> |
|
<button id="btnPing" class="btn">Ping</button> |
|
<label class="small"><input id="chkBinary" type="checkbox" /> Receive binary WAV frames</label> |
|
<label class="small"><input id="chkAutoUpdate" type="checkbox" checked /> Auto-update on slider change (150ms debounce)</label> |
|
<label class="small"><input id="chkLogAudio" type="checkbox" /> Log chunk sizes</label> |
|
<label class="small"><input id="chkRealtime" type="checkbox" checked /> Ask server to pace real-time</label> |
|
|
|
|
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="card"> |
|
<div class="row"> |
|
<div class="col-6"> |
|
<div class="range-row"> |
|
<label>Temperature</label> |
|
<input id="rngTemp" type="range" min="0.10" max="2.00" step="0.01" value="1.10" /> |
|
<input id="numTemp" type="number" min="0.10" max="2.00" step="0.01" value="1.10" /> |
|
</div> |
|
<div class="range-row"> |
|
<label>Guidance</label> |
|
<input id="rngGuid" type="range" min="0.0" max="8.0" step="0.1" value="1.10" /> |
|
<input id="numGuid" type="number" min="0.0" max="8.0" step="0.1" value="1.10" /> |
|
</div> |
|
<div class="range-row"> |
|
<label>TopK</label> |
|
<input id="rngTopk" type="range" min="1" max="256" step="1" value="40" /> |
|
<input id="numTopk" type="number" min="1" max="256" step="1" value="40" /> |
|
</div> |
|
<div class="range-row"> |
|
<label>Volume</label> |
|
<input id="rngVol" type="range" min="0" max="1" step="0.01" value="1" /> |
|
<input id="numVol" type="number" min="0" max="1" step="0.01" value="1" /> |
|
</div> |
|
<div class="sep"></div> |
|
<button id="btnUpdate" class="btn">Send Update Now</button> |
|
</div> |
|
<div class="col-6"> |
|
<label>Styles</label> |
|
<div id="stylesUI"> |
|
<div id="styleRows" class="style-list"></div> |
|
<div class="controls"> |
|
<button id="btnAddStyle" class="btn" type="button">+ Add style</button> |
|
<label class="small" style="margin-left:8px;"> |
|
<input id="chkUseMixStyle" type="checkbox" /> Use current mix as style |
|
</label> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="card" id="finetuneControls" style="display:none;"> |
|
<div class="row"> |
|
<div class="col-12"><div class="small">In-distribution steering</div></div> |
|
|
|
<div class="col-6"> |
|
<div class="range-row"> |
|
<label>Mean</label> |
|
<input id="rngMean" type="range" min="0.0" max="2.0" step="0.01" value="1.00" /> |
|
<input id="numMean" type="number" min="0.0" max="2.0" step="0.01" value="1.00" /> |
|
</div> |
|
|
|
<div class="sep"></div> |
|
|
|
<div class="range-row"> |
|
<label>Centroid 1</label> |
|
<input id="rngC1" type="range" min="0.0" max="2.0" step="0.01" value="0.00" /> |
|
<input id="numC1" type="number" min="0.0" max="2.0" step="0.01" value="0.00" /> |
|
</div> |
|
<div class="range-row"> |
|
<label>Centroid 2</label> |
|
<input id="rngC2" type="range" min="0.0" max="2.0" step="0.01" value="0.00" /> |
|
<input id="numC2" type="number" min="0.0" max="2.0" step="0.01" value="0.00" /> |
|
</div> |
|
<div class="range-row"> |
|
<label>Centroid 3</label> |
|
<input id="rngC3" type="range" min="0.0" max="2.0" step="0.01" value="0.00" /> |
|
<input id="numC3" type="number" min="0.0" max="2.0" step="0.01" value="0.00" /> |
|
</div> |
|
<div class="range-row"> |
|
<label>Centroid 4</label> |
|
<input id="rngC4" type="range" min="0.0" max="2.0" step="0.01" value="0.00" /> |
|
<input id="numC4" type="number" min="0.0" max="2.0" step="0.01" value="0.00" /> |
|
</div> |
|
<div class="range-row"> |
|
<label>Centroid 5</label> |
|
<input id="rngC5" type="range" min="0.0" max="2.0" step="0.01" value="0.00" /> |
|
<input id="numC5" type="number" min="0.0" max="2.0" step="0.01" value="0.00" /> |
|
</div> |
|
</div> |
|
|
|
<div class="col-6"> |
|
<p class="small"> |
|
These steer the style embedding toward your finetune distribution. |
|
<br/>Mean defaults to 1.0 (on); centroids default to 0.0 (off). |
|
<br/>Adjust live while jamming. |
|
</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="card"> |
|
<div class="row"> |
|
<div class="col-6"> |
|
<div class="small">Audio Status</div> |
|
<div id="status">stopped</div> |
|
</div> |
|
<div class="col-6"> |
|
<div class="small">Queue</div> |
|
<div id="queue">0 buffers, 0.00s scheduled</div> |
|
</div> |
|
<div class="col-12"> |
|
<label>Log</label> |
|
<div id="log" class="log"></div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
(() => { |
|
const $ = (id) => document.getElementById(id); |
|
const wsUrl = $("wsUrl"); |
|
const btnStart = $("btnStart"); |
|
const btnStop = $("btnStop"); |
|
const btnPing = $("btnPing"); |
|
const btnUpdate = $("btnUpdate"); |
|
const chkBinary = $("chkBinary"); |
|
const chkAutoUpdate = $("chkAutoUpdate"); |
|
const chkLogAudio = $("chkLogAudio"); |
|
const rngTemp = $("rngTemp"), numTemp = $("numTemp"); |
|
const rngGuid = $("rngGuid"), numGuid = $("numGuid"); |
|
const rngTopk = $("rngTopk"), numTopk = $("numTopk"); |
|
const rngVol = $("rngVol"), numVol = $("numVol"); |
|
|
|
|
|
const chkUseMixStyle = $("chkUseMixStyle"); |
|
const statusEl = $("status"); |
|
const queueEl = $("queue"); |
|
const logEl = $("log"); |
|
|
|
const rngMean = $("rngMean"), numMean = $("numMean"); |
|
const rngC1 = $("rngC1"), numC1 = $("numC1"); |
|
const rngC2 = $("rngC2"), numC2 = $("numC2"); |
|
const rngC3 = $("rngC3"), numC3 = $("numC3"); |
|
const rngC4 = $("rngC4"), numC4 = $("numC4"); |
|
const rngC5 = $("rngC5"), numC5 = $("numC5"); |
|
|
|
const XFADE_MS = 40; |
|
|
|
let pending = []; |
|
let playing = false; |
|
const START_CUSHION = 0.12; |
|
|
|
const fade = XFADE_MS / 1000; |
|
|
|
function scheduleAudioBuffer(abuf) { |
|
|
|
const src = ctx.createBufferSource(); |
|
const g = ctx.createGain(); |
|
src.buffer = abuf; |
|
src.connect(g); g.connect(gain); |
|
|
|
if (nextTime < ctx.currentTime + 0.05) nextTime = ctx.currentTime + START_CUSHION; |
|
const startAt = nextTime; |
|
const dur = abuf.duration; |
|
|
|
|
|
nextTime = startAt + Math.max(0, dur - fade); |
|
|
|
g.gain.setValueAtTime(0.0, startAt); |
|
g.gain.linearRampToValueAtTime(1.0, startAt + fade); |
|
g.gain.setValueAtTime(1.0, startAt + Math.max(0, dur - fade)); |
|
g.gain.linearRampToValueAtTime(0.0, startAt + dur); |
|
|
|
src.start(startAt); |
|
scheduled.push({ src, when: startAt, dur }); |
|
updateQueueUI(); |
|
src.onended = () => { scheduled = scheduled.filter(s => s.src !== src); updateQueueUI(); }; |
|
} |
|
|
|
function beginPlaybackFromPending() { |
|
if (playing) return; |
|
playing = true; |
|
nextTime = ctx.currentTime + START_CUSHION; |
|
while (pending.length) { |
|
const abuf = pending.shift(); |
|
scheduleAudioBuffer(abuf); |
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
let AudioCtx = window.AudioContext || window.webkitAudioContext; |
|
let ctx = null; |
|
let gain = null; |
|
let nextTime = 0; |
|
let scheduled = []; |
|
let ws = null; |
|
let connected = false; |
|
let autoUpdateTimer = null; |
|
|
|
function log(msg, cls) { |
|
const line = document.createElement("div"); |
|
line.textContent = msg; |
|
if (cls) line.className = cls; |
|
logEl.appendChild(line); |
|
logEl.scrollTop = logEl.scrollHeight; |
|
} |
|
|
|
function setStatus(txt) { |
|
statusEl.textContent = txt; |
|
} |
|
|
|
function updateQueueUI() { |
|
const total = scheduled.reduce((acc, s) => acc + s.dur, 0); |
|
queueEl.textContent = `${scheduled.length} buffers, ${total.toFixed(2)}s scheduled`; |
|
} |
|
|
|
function clearSchedule() { |
|
scheduled.forEach(s => { try { s.src.stop(); } catch(e){} }); |
|
scheduled = []; |
|
pending = []; |
|
playing = false; |
|
nextTime = 0; |
|
updateQueueUI(); |
|
} |
|
|
|
function base64ToArrayBuffer(b64) { |
|
const bin = atob(b64); |
|
const len = bin.length; |
|
const buf = new ArrayBuffer(len); |
|
const view = new Uint8Array(buf); |
|
for (let i = 0; i < len; i++) view[i] = bin.charCodeAt(i); |
|
return buf; |
|
} |
|
|
|
|
|
async function scheduleWavBytes(arrayBuffer) { |
|
if (!ctx) return; |
|
try { |
|
const abuf = await ctx.decodeAudioData(arrayBuffer); |
|
if (!abuf) return; |
|
|
|
const need = 0; |
|
if (!playing) { |
|
pending.push(abuf); |
|
queueEl.textContent = `${pending.length} pending, 0.00s scheduled`; |
|
|
|
if (pending.length >= need) beginPlaybackFromPending(); |
|
return; |
|
} |
|
|
|
|
|
scheduleAudioBuffer(abuf); |
|
} catch (e) { |
|
log("decode error: " + e.message, "err"); |
|
} |
|
} |
|
|
|
function currentParams() { |
|
return { |
|
temperature: parseFloat(numTemp.value), |
|
topk: parseInt(numTopk.value, 10), |
|
guidance_weight: parseFloat(numGuid.value), |
|
styles: txtStyles.value, |
|
style_weights: txtStyleWeights.value, |
|
use_current_mix_as_style: !!chkUseMixStyle.checked, |
|
|
|
|
|
mean: parseFloat(numMean.value), |
|
centroid_weights: centroidWeightsCSV(), |
|
}; |
|
} |
|
|
|
function sendUpdate() { |
|
if (!ws || ws.readyState !== 1) return; |
|
const msg = { type: "update", ...currentParams() }; |
|
ws.send(JSON.stringify(msg)); |
|
log("→ update " + JSON.stringify(msg), "small"); |
|
} |
|
|
|
function debouncedUpdate() { |
|
if (!chkAutoUpdate.checked) return; |
|
if (autoUpdateTimer) clearTimeout(autoUpdateTimer); |
|
autoUpdateTimer = setTimeout(sendUpdate, 150); |
|
} |
|
|
|
function linkRangeNumber(range, number, cb) { |
|
const sync = (fromRange) => { |
|
if (fromRange) number.value = range.value; |
|
else range.value = number.value; |
|
cb?.(); |
|
}; |
|
range.addEventListener("input", () => { sync(true); debouncedUpdate(); }); |
|
number.addEventListener("input", () => { sync(false); debouncedUpdate(); }); |
|
sync(true); |
|
} |
|
|
|
function centroidWeightsCSV() { |
|
const vals = [numC1, numC2, numC3, numC4, numC5].map(n => { |
|
const v = parseFloat(n.value); |
|
return Number.isFinite(v) ? v : 0; |
|
}); |
|
|
|
let end = vals.length; |
|
while (end > 0 && Math.abs(vals[end-1]) < 1e-9) end--; |
|
return vals.slice(0, end).join(","); |
|
} |
|
|
|
|
|
linkRangeNumber(rngTemp, numTemp); |
|
linkRangeNumber(rngGuid, numGuid); |
|
linkRangeNumber(rngTopk, numTopk); |
|
linkRangeNumber(rngVol, numVol, () => { if (gain) gain.gain.value = parseFloat(numVol.value); }); |
|
|
|
linkRangeNumber(rngMean, numMean); |
|
linkRangeNumber(rngC1, numC1); |
|
linkRangeNumber(rngC2, numC2); |
|
linkRangeNumber(rngC3, numC3); |
|
linkRangeNumber(rngC4, numC4); |
|
linkRangeNumber(rngC5, numC5); |
|
|
|
|
|
|
|
const styleRows = document.getElementById("styleRows"); |
|
const btnAddStyle = document.getElementById("btnAddStyle"); |
|
|
|
function addStyleRow(name = "", weight = 1.0) { |
|
const row = document.createElement("div"); |
|
row.className = "style-row"; |
|
row.innerHTML = ` |
|
<input class="style-name" type="text" placeholder="e.g. acid house" value="${name}"> |
|
<input class="style-range" type="range" min="0.0" max="2.0" step="0.01" value="${weight}"> |
|
<input class="style-number" type="number" min="0.0" max="2.0" step="0.01" value="${weight}"> |
|
<button class="btn remove" type="button" title="Remove">×</button> |
|
`; |
|
|
|
const nameEl = row.querySelector(".style-name"); |
|
const rangeEl = row.querySelector(".style-range"); |
|
const numEl = row.querySelector(".style-number"); |
|
const remove = row.querySelector(".remove"); |
|
|
|
|
|
const sync = (fromRange) => { |
|
if (fromRange) numEl.value = rangeEl.value; |
|
else rangeEl.value = numEl.value; |
|
}; |
|
rangeEl.addEventListener("input", () => { sync(true); debouncedUpdate(); }); |
|
numEl.addEventListener("input", () => { sync(false); debouncedUpdate(); }); |
|
nameEl.addEventListener("input", () => { debouncedUpdate(); }); |
|
|
|
remove.addEventListener("click", () => { row.remove(); debouncedUpdate(); }); |
|
|
|
styleRows.appendChild(row); |
|
|
|
sync(true); |
|
} |
|
|
|
function stylesCSV() { |
|
|
|
return [...styleRows.querySelectorAll(".style-row")] |
|
.map(r => r.querySelector(".style-name").value.trim()) |
|
.filter(s => s.length > 0) |
|
.join(", "); |
|
} |
|
|
|
function styleWeightsCSV() { |
|
|
|
const out = []; |
|
for (const r of styleRows.querySelectorAll(".style-row")) { |
|
const name = r.querySelector(".style-name").value.trim(); |
|
if (!name) continue; |
|
const val = parseFloat(r.querySelector(".style-number").value); |
|
out.push(Number.isFinite(val) ? val : 1.0); |
|
} |
|
return out.join(","); |
|
} |
|
|
|
|
|
addStyleRow("warmup", 1.0); |
|
|
|
|
|
btnAddStyle.addEventListener("click", () => addStyleRow("", 1.0)); |
|
|
|
|
|
|
|
const ORIGINAL_currentParams = currentParams; |
|
currentParams = function () { |
|
return { |
|
temperature: parseFloat(numTemp.value), |
|
topk: parseInt(numTopk.value, 10), |
|
guidance_weight: parsefloatSafe(numGuid.value, 1.1), |
|
|
|
|
|
styles: stylesCSV(), |
|
style_weights: styleWeightsCSV(), |
|
use_current_mix_as_style: !!chkUseMixStyle.checked, |
|
|
|
mean: parsefloatSafe(numMean?.value, 1.0), |
|
centroid_weights: centroidWeightsCSV(), |
|
}; |
|
}; |
|
|
|
function parsefloatSafe(v, dflt) { |
|
const x = parseFloat(v); |
|
return Number.isFinite(x) ? x : dflt; |
|
} |
|
|
|
async function start() { |
|
if (connected) return; |
|
if (!AudioCtx) { alert("Web Audio API not supported"); return; } |
|
pending = []; |
|
playing = false; |
|
ctx = ctx || new AudioCtx(); |
|
await ctx.resume(); |
|
gain = ctx.createGain(); |
|
gain.gain.value = parseFloat(numVol.value); |
|
gain.connect(ctx.destination); |
|
clearSchedule(); |
|
|
|
ws = new WebSocket(wsUrl.value); |
|
ws.binaryType = chkBinary.checked ? "arraybuffer" : "blob"; |
|
setStatus("connecting..."); |
|
log("connecting " + wsUrl.value); |
|
|
|
ws.onopen = () => { |
|
connected = true; |
|
btnStart.disabled = true; |
|
btnStop.disabled = false; |
|
setStatus("connected"); |
|
|
|
const realtime = $("chkRealtime")?.checked === true; |
|
const binary = $("chkBinary")?.checked === true; |
|
|
|
const msg = { |
|
type: "start", |
|
mode: "rt", |
|
binary_audio: !!binary, |
|
params: { |
|
...currentParams(), |
|
|
|
pace: realtime ? "realtime" : "asap", |
|
}, |
|
}; |
|
|
|
ws.send(JSON.stringify(msg)); |
|
log("→ start " + JSON.stringify(msg), "ok"); |
|
nextTime = ctx.currentTime + 0.12; |
|
}; |
|
|
|
ws.onmessage = async (ev) => { |
|
try { |
|
if (typeof ev.data === "string") { |
|
|
|
const msg = JSON.parse(ev.data); |
|
if (msg.type === "chunk" && msg.audio_base64) { |
|
const buf = base64ToArrayBuffer(msg.audio_base64); |
|
if (chkLogAudio.checked) log(`chunk (b64) ${buf.byteLength} bytes`, "small"); |
|
scheduleWavBytes(buf); |
|
} else if (msg.type === "chunk_meta") { |
|
|
|
} else if (msg.type === "status") { |
|
log("status: " + JSON.stringify(msg), "small"); |
|
} else if (msg.type === "started") { |
|
log("started: " + JSON.stringify(msg), "ok"); |
|
} else if (msg.type === "error") { |
|
log("error: " + msg.error, "err"); |
|
} else { |
|
log("msg: " + JSON.stringify(msg), "small"); |
|
} |
|
} else if (ev.data instanceof Blob) { |
|
|
|
const ab = await ev.data.arrayBuffer(); |
|
|
|
if (ab.byteLength > 0 && new Uint8Array(ab, 0, 1)[0] === 0x7b) { |
|
const txt = new TextDecoder().decode(ab); |
|
const msg = JSON.parse(txt); |
|
if (msg.type === "chunk_meta") { |
|
|
|
} else { |
|
log("blob-json: " + txt, "small"); |
|
} |
|
} else { |
|
if (chkLogAudio.checked) log(`chunk (bin) ${ab.byteLength} bytes`, "small"); |
|
scheduleWavBytes(ab); |
|
} |
|
} else if (ev.data instanceof ArrayBuffer) { |
|
const ab = ev.data; |
|
if (chkLogAudio.checked) log(`chunk (ab) ${ab.byteLength} bytes`, "small"); |
|
scheduleWavBytes(ab); |
|
} |
|
} catch (e) { |
|
log("onmessage error: " + e.message, "err"); |
|
} |
|
}; |
|
|
|
ws.onclose = () => { |
|
connected = false; |
|
btnStart.disabled = false; |
|
btnStop.disabled = true; |
|
setStatus("closed"); |
|
log("connection closed", "warn"); |
|
}; |
|
|
|
ws.onerror = (e) => { |
|
log("ws error", "err"); |
|
}; |
|
} |
|
|
|
function stop() { |
|
if (!connected) return; |
|
try { |
|
ws?.send(JSON.stringify({ type: "stop" })); |
|
} catch {} |
|
ws?.close(); |
|
connected = false; |
|
btnStart.disabled = false; |
|
btnStop.disabled = true; |
|
setStatus("stopped"); |
|
clearSchedule(); |
|
log("stopped", "warn"); |
|
} |
|
|
|
btnStart.addEventListener("click", start); |
|
btnStop.addEventListener("click", stop); |
|
btnPing.addEventListener("click", () => { try { ws?.send(JSON.stringify({ type: "ping"})); } catch {} }); |
|
btnUpdate.addEventListener("click", sendUpdate); |
|
|
|
const banner = document.getElementById("healthBanner"); |
|
async function checkHealthAndGate() { |
|
try { |
|
const r = await fetch("/health", {cache: "no-store"}); |
|
const data = await r.json().catch(() => ({})); |
|
if (!r.ok || data.ok === false) { |
|
const msg = data?.message || "Service unavailable"; |
|
banner.innerHTML = `<div style="padding:10px;border-radius:8px;background:#3a1010;border:1px solid #712020"> |
|
<strong>Not ready:</strong> ${msg}<br><small>Status: ${data?.status || r.status}</small> |
|
</div>`; |
|
|
|
["btnStart","btnSelectModel"].forEach(id => { const el = $(id); if (el) el.disabled = true; }); |
|
} else { |
|
banner.innerHTML = `<div style="padding:10px;border-radius:8px;background:#0f2d17;border:1px solid #1f6a37"> |
|
<strong>Ready</strong> — mode: ${data.mode}, warmed: ${data.warmed} |
|
</div>`; |
|
["btnStart","btnSelectModel"].forEach(id => { const el = $(id); if (el) el.disabled = false; }); |
|
} |
|
} catch (e) { |
|
banner.innerHTML = `<div style="padding:10px;border-radius:8px;background:#3a2a10;border:1px solid #715820"> |
|
Health check failed. |
|
</div>`; |
|
} |
|
} |
|
|
|
|
|
const selRepo = $("selRepo"); |
|
const selStep = $("selStep"); |
|
const selSize = $("selSize"); |
|
const selPrewarm = $("selPrewarm"); |
|
const selStopActive = $("selStopActive"); |
|
const btnSelectModel = $("btnSelectModel"); |
|
const selStatus = $("selStatus"); |
|
const chkBase = $("chkBase"); |
|
|
|
function updateBaseToggleUI() { |
|
const base = !!(chkBase && chkBase.checked); |
|
if (selRepo) { selRepo.disabled = base; selRepo.parentElement.style.opacity = base ? 0.5 : 1; } |
|
if (selStep) { selStep.disabled = base; selStep.parentElement.style.opacity = base ? 0.5 : 1; } |
|
} |
|
if (chkBase) { |
|
chkBase.addEventListener("change", updateBaseToggleUI); |
|
updateBaseToggleUI(); |
|
} |
|
|
|
|
|
try { |
|
const defWs = (location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/ws/jam"; |
|
if (!wsUrl.value || /hf\.space/.test(wsUrl.value)) wsUrl.value = defWs; |
|
} catch {} |
|
|
|
async function refreshFinetuneControls() { |
|
try { |
|
const r = await fetch("/model/config", { cache: "no-store" }); |
|
if (!r.ok) throw new Error("config status " + r.status); |
|
const cfg = await r.json(); |
|
|
|
const show = !!(cfg.mean_loaded || cfg.centroids_loaded); |
|
const el = document.getElementById("finetuneControls"); |
|
if (el) el.style.display = show ? "block" : "none"; |
|
if (!show) log("finetune assets not detected — hiding steering controls", "small"); |
|
} catch (e) { |
|
const el = document.getElementById("finetuneControls"); |
|
if (el) el.style.display = "none"; |
|
log("config fetch failed: " + e.message, "warn"); |
|
} |
|
} |
|
|
|
async function selectModel() { |
|
selStatus.textContent = "selecting..."; |
|
try { |
|
const useBase = !!(chkBase && chkBase.checked); |
|
const payload = { |
|
size: (selSize?.value || undefined), |
|
prewarm: !!(selPrewarm && selPrewarm.checked), |
|
stop_active: !!(selStopActive && selStopActive.checked), |
|
sync_assets: true, |
|
}; |
|
if (useBase) { |
|
|
|
payload["step"] = "none"; |
|
} else { |
|
if (selRepo?.value) payload["repo_id"] = selRepo.value; |
|
if (selStep?.value) payload["step"] = parseInt(selStep.value, 10); |
|
} |
|
const resp = await fetch("/model/select", { |
|
method: "POST", |
|
headers: { "Content-Type": "application/json" }, |
|
body: JSON.stringify(payload) |
|
}); |
|
const data = await resp.json().catch(() => ({})); |
|
if (!resp.ok || data.ok === false) { |
|
const msg = data?.error || (resp.status + " " + resp.statusText); |
|
selStatus.innerHTML = '<span class="err">error: ' + msg + '</span>'; |
|
log("model/select failed: " + msg, "err"); |
|
return; |
|
} |
|
selStatus.innerHTML = '<span class="ok">selected' + (data.warmup_done ? " + warmed" : "") + "</span>"; |
|
log("model/select ok: " + JSON.stringify(data), "ok"); |
|
await refreshFinetuneControls(); |
|
} catch (e) { |
|
selStatus.innerHTML = '<span class="err">error: ' + e.message + "</span>"; |
|
log("model/select exception: " + e.message, "err"); |
|
} |
|
} |
|
|
|
if (btnSelectModel) btnSelectModel.addEventListener("click", selectModel); |
|
|
|
|
|
checkHealthAndGate(); |
|
refreshFinetuneControls(); |
|
refreshModelInfo(); |
|
|
|
async function refreshModelInfo() { |
|
const el = document.getElementById("modelStatus"); |
|
if (!el) return; |
|
try { |
|
const r = await fetch("/model/config", { cache: "no-store" }); |
|
if (!r.ok) throw new Error("status " + r.status); |
|
const cfg = await r.json(); |
|
|
|
const size = cfg.size || cfg.model_size || 'unknown'; |
|
const repo = cfg.ckpt_repo || cfg.repo_id || cfg.repo || (cfg.ckpt && cfg.ckpt.repo) || cfg.active_repo; |
|
const step = cfg.ckpt_step || cfg.step || (cfg.ckpt && cfg.ckpt.step) || cfg.active_step; |
|
const isBase = (!repo) || (step === "none"); |
|
|
|
const hasMean = !!(cfg.mean_loaded || cfg.has_mean_embed || cfg.mean_style_embed || cfg.mean); |
|
const hasCentroids = !!(cfg.centroids_loaded || cfg.has_centroids || cfg.cluster_centroids || cfg.centroids); |
|
|
|
let line = ''; |
|
if (isBase) { |
|
line = `Model: base <strong>${size}</strong>`; |
|
} else { |
|
line = `Model: finetune <strong>${repo}</strong>@<strong>${step}</strong> (base <strong>${size}</strong>)`; |
|
} |
|
line += ` • assets: ${hasMean ? "mean✓" : "mean×"}/${hasCentroids ? "centroids✓" : "centroids×"}`; |
|
|
|
el.innerHTML = `<div style="padding:8px;border-radius:6px;background:#0f1f2a;border:1px solid #214459">${line}</div>`; |
|
} catch (e) { |
|
const el = document.getElementById("modelStatus"); |
|
if (el) el.innerHTML = ''; |
|
} |
|
} |
|
|
|
})(); |
|
</script> |
|
</body> |
|
</html> |
|
|