magenta-retry / magentaRT_rt_tester.html
thecollabagepatch's picture
css fixes
ec31c3e
<!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; } /* more breathing room between L/R halves */
.col-6 { min-width: 0; overflow: hidden; } /* clip any tiny visual bleed */
}
/* prevent tiny default margins on sliders from poking into the gutter */
input[type="range"] { margin: 0; }
/* belt-and-suspenders: keep style rows from visually spilling */
.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; /* name | slider | number | X */
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>
<!-- <label class="small"><input id="chkBootstrap" type="checkbox" checked /> Bootstrap fast (ASAP → flip to realtime)</label>
<label class="small">Pre-roll chunks: <input id="numPreroll" type="number" min="0" max="12" step="1" value="3" style="width:60px"></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 txtStyles = $("txtStyles");
// const txtStyleWeights = $("txtStyleWeights");
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; // crossfade length
let pending = []; // decoded AudioBuffers waiting to be scheduled
let playing = false; // have we started playback?
const START_CUSHION = 0.12; // already used
const fade = XFADE_MS / 1000;
function scheduleAudioBuffer(abuf) {
// Overlap-add crossfade scheduling (same as your scheduleWavBytes but taking a decoded buffer)
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;
// Overlap by 'fade' so there’s no dip
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);
}
// Flip server pacing to realtime after we’ve started, if bootstrapping was enabled
// if ($("chkBootstrap").checked) {
// try { ws?.send(JSON.stringify({ type: "update", pace: "realtime" })); } catch {}
// }
}
// Audio chain
let AudioCtx = window.AudioContext || window.webkitAudioContext;
let ctx = null;
let gain = null;
let nextTime = 0;
let scheduled = []; // [{src, when, dur}]
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; // this used to be for pre-roll. it didn't help with realtime on the L4.
if (!playing) {
pending.push(abuf);
queueEl.textContent = `${pending.length} pending, 0.00s scheduled`;
// start once we hit the threshold
if (pending.length >= need) beginPlaybackFromPending();
return;
}
// already playing → schedule immediately
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,
// NEW:
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;
});
// Trim trailing zeros to avoid sending long tails of 0.00s
let end = vals.length;
while (end > 0 && Math.abs(vals[end-1]) < 1e-9) end--;
return vals.slice(0, end).join(",");
}
// Wire sliders
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);
// --- Dynamic Styles UI ---
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");
// keep slider/number in sync + auto-update
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);
// initial sync
sync(true);
}
function stylesCSV() {
// only include rows with non-empty names
return [...styleRows.querySelectorAll(".style-row")]
.map(r => r.querySelector(".style-name").value.trim())
.filter(s => s.length > 0)
.join(", ");
}
function styleWeightsCSV() {
// weights aligned with non-empty names (server defaults to 1.0 if missing)
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(",");
}
// Default row to match your previous "warmup" default
addStyleRow("warmup", 1.0);
// add style on click
btnAddStyle.addEventListener("click", () => addStyleRow("", 1.0));
// --- Wire into your existing param-building ---
// Replace txtStyles/txtStyleWeights usage inside currentParams()
const ORIGINAL_currentParams = currentParams; // keep a copy if needed
currentParams = function () {
return {
temperature: parseFloat(numTemp.value),
topk: parseInt(numTopk.value, 10),
guidance_weight: parsefloatSafe(numGuid.value, 1.1),
// from the dynamic rows:
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"; // arraybuffer for raw, blob for JSON string frames
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(),
// no bootstrap or pre-roll anymore
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") {
// JSON (e.g., chunk with base64, status, errors)
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") {
// meta for previous binary frame
} 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) {
// Could be binary WAV frame if server sent bytes, or a JSON blob
const ab = await ev.data.arrayBuffer();
// Try to sniff if this is JSON (starts with '{')
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") {
// ignore for now
} 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);
// --- Health check & UI gating ---
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>`;
// disable controls
["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>`;
}
}
// --- Model/asset helpers (with base toggle) ---
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();
}
// Default ws URL to current origin if empty/hardcoded
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();
// Use in-memory flags from backend
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) {
// Signal stock model: step="none" as per backend's selector
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);
// Run once on load
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 += ` &nbsp;•&nbsp; 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>