wave-roll
Version:
JavaScript Library for Comparative MIDI Piano-Roll Visualization
288 lines (287 loc) • 12.5 kB
JavaScript
import { p as B } from "./index-Bn89Sr5W.js";
function G(i) {
const x = document.createElement("div");
x.style.cssText = "display:flex;flex-direction:column;gap:12px;";
const r = document.createElement("div");
r.style.cssText = "position:fixed;z-index:99999;pointer-events:none;background:rgba(33,37,41,0.95);color:#fff;padding:6px 8px;border-radius:6px;font-size:11px;line-height:1.2;max-width:320px;box-shadow:0 2px 8px rgba(0,0,0,0.25);transform:translate3d(0,0,0);display:none;white-space:pre-line;", document.body.appendChild(r);
let f = !1;
function g(e, t) {
const { innerWidth: c, innerHeight: o } = window;
r.style.left = "0px", r.style.top = "0px";
const a = r.getBoundingClientRect();
let p = e + 12, u = t + 12;
p + a.width + 12 > c && (p = Math.max(12, e - a.width - 12)), u + a.height + 12 > o && (u = Math.max(12, t - a.height - 12)), r.style.transform = `translate(${p}px, ${u}px)`;
}
function T(e, t, d) {
r.textContent = e, r.style.display = "block", f = !0, g(t, d);
}
function v() {
r.style.display = "none", f = !1;
}
function n(e, t) {
e.setAttribute("data-tip", t), e.hasAttribute("title") && e.removeAttribute("title");
}
x.addEventListener("pointermove", (e) => {
const t = e.target, d = t ? t.closest("[data-tip]") : null;
if (d) {
const c = d.getAttribute("data-tip");
if (c) {
!f || r.textContent !== c ? T(
c,
e.clientX,
e.clientY
) : g(e.clientX, e.clientY);
return;
}
}
f && v();
}), x.addEventListener("pointerleave", () => {
f && v();
});
const y = document.createElement("h3");
y.textContent = "Evaluation: based on ", y.style.cssText = "margin:0;font-size:14px;font-weight:600;color:var(--text-primary);";
const P = "https://github.com/mir-evaluation/mir_eval", b = document.createElement("a");
b.href = P, b.textContent = "mir_eval", b.style.cssText = "color:var(--accent);text-decoration:underline;", b.target = "_blank", b.rel = "noopener noreferrer", y.appendChild(b);
const C = document.createElement("div");
C.style.cssText = "display:flex;align-items:center;gap:6px;font-size:12px;", n(
C,
"Select the ground-truth (reference) MIDI file to compare against."
);
const $ = document.createElement("span");
$.textContent = "Reference:", $.style.cssText = "min-width:80px;font-weight:600;";
const s = document.createElement("select");
s.style.cssText = "flex:1;padding:4px 6px;border:1px solid var(--ui-border);border-radius:6px;background:var(--surface);color:var(--text-primary);", n(s, "Choose the reference MIDI file (ground truth).");
const M = document.createElement("option");
M.value = "", M.textContent = "(none)", M.title = "No reference selected", s.appendChild(M);
const I = document.createElement("div");
I.style.cssText = "display:flex;align-items:start;gap:6px;font-size:12px;", n(
I,
"Select a single comparison transcription file to evaluate against the reference (1:1)."
);
const F = document.createElement("span");
F.textContent = "Comparison:", F.style.cssText = "min-width:80px;font-weight:600;padding-top:4px;";
const l = document.createElement("select");
l.multiple = !1, l.style.cssText = "flex:1;padding:4px 6px;border:1px solid var(--ui-border);border-radius:6px;background:var(--surface);color:var(--text-primary);", n(
l,
"Pick one comparison MIDI file (single) produced by a transcription system."
);
const w = document.createElement("div");
w.style.cssText = "display:flex;flex-direction:column;gap:6px;", n(
w,
"Matching tolerances used to pair reference and comparison notes for metric computation."
);
const _ = {
onsetTolerance: "Maximum allowed onset difference (in seconds) for a note to count as a match.",
pitchTolerance: "Maximum allowed pitch difference (in semitones, MIDI notes) for a match.",
offsetRatioTolerance: "Maximum allowed relative offset difference: |est_end - ref_end| / ref_duration. For example, 0.2 allows a 20% deviation.",
offsetMinTolerance: "Minimum absolute offset tolerance (in seconds) to allow for very short notes."
}, S = (e, t, d = "0.01") => {
const c = document.createElement("div");
c.style.cssText = "display:flex;align-items:center;gap:6px;font-size:12px;", n(c, _[t]);
const o = document.createElement("span");
o.textContent = e, o.style.cssText = "min-width:120px;font-weight:600;", n(o, _[t]);
const a = document.createElement("input");
return a.type = "number", a.step = d, a.style.cssText = "flex:1;padding:4px 6px;border:1px solid var(--ui-border);border-radius:6px;background:var(--surface);color:var(--text-primary);", a.value = String(i.stateManager.getState().evaluation[t]), n(a, _[t]), a.addEventListener("change", () => {
i.stateManager.updateEvaluationState({
[t]: parseFloat(a.value)
}), R();
}), c.append(o, a), c;
};
w.append(
S("Onset tol (s):", "onsetTolerance", "0.001"),
S("Pitch tol (semitones):", "pitchTolerance", "0.1"),
S("Offset ratio tol:", "offsetRatioTolerance", "0.01"),
S("Offset min tol (s):", "offsetMinTolerance", "0.001")
);
const k = document.createElement("div");
k.style.cssText = "display:flex;align-items:center;gap:6px;font-size:12px;", n(
k,
"Select the time region used as the anchor for overlap/visualization."
);
const O = document.createElement("span");
O.textContent = "Anchor:", O.style.cssText = "min-width:80px;font-weight:600;", n(
O,
`Intersection: overlap only;
Ref: reference span;
Est: estimated span.`
);
const m = document.createElement("select");
m.style.cssText = "flex:1;padding:4px 6px;border:1px solid var(--ui-border);border-radius:6px;background:var(--surface);color:var(--text-primary);";
const A = {
intersection: "Use the overlapped region as the anchor for visualization.",
ref: "Use the reference note span as the anchor.",
est: "Use the comparison note span as the anchor."
};
["intersection", "ref", "est"].forEach((e) => {
const t = document.createElement("option");
t.value = e, t.textContent = e, t.title = A[e], m.appendChild(t);
}), m.value = i.stateManager.getState().evaluation.anchor, n(
m,
A[m.value]
), m.addEventListener("change", () => {
i.stateManager.updateEvaluationState({
anchor: m.value
}), n(
m,
A[m.value]
);
});
const h = document.createElement("div");
h.style.cssText = "border:1px solid var(--ui-border);border-radius:6px;padding:8px;background:var(--surface-alt);font-size:11px;font-family:monospace;color:var(--text-primary);", n(
h,
"Shows Precision, Recall, F1, and average overlap ratio for the selected files under current tolerances. Based on mir_eval.transcription."
);
const R = () => {
const e = i.stateManager.getState().evaluation, t = i.midiManager.getState().files;
if (h.innerHTML = "", !e.refId || e.estIds.length === 0) {
h.textContent = "Select reference and comparison files to see metrics";
return;
}
const d = t.find((u) => u.id === e.refId);
if (!d) {
h.textContent = "Reference file not found";
return;
}
const c = e.estIds[0], o = t.find((u) => u.id === c);
if (!o) {
h.textContent = "Comparison file not found";
return;
}
if (!d.parsedData || !o.parsedData) {
h.textContent = "MIDI data not loaded";
return;
}
const a = B(
d.parsedData,
o.parsedData,
{
onsetTolerance: e.onsetTolerance,
pitchTolerance: e.pitchTolerance,
offsetRatioTolerance: e.offsetRatioTolerance,
offsetMinTolerance: e.offsetMinTolerance
}
), p = [
`File: ${o.name}`,
`Precision: ${a.precision.toFixed(3)}`,
`Recall: ${a.recall.toFixed(3)}`,
`F1: ${a.f1.toFixed(3)}`,
`Avg Overlap: ${a.avgOverlapRatio.toFixed(3)}`
];
h.innerHTML = p.join("<br>");
}, H = () => {
const e = i.midiManager.getState().files, t = i.stateManager.getState().evaluation;
s.innerHTML = "";
const d = document.createElement("option");
d.value = "", d.textContent = "(none)", s.appendChild(d), l.innerHTML = "", e.forEach((p) => {
const u = document.createElement("option");
u.value = p.id, u.textContent = p.name, u.title = `Reference candidate: ${p.name}`, s.appendChild(u);
const z = document.createElement("option");
z.value = p.id, z.textContent = p.name, z.title = `Comparison candidate: ${p.name}`, l.appendChild(z);
});
let c = t.refId;
!c && e.length > 0 && (c = e[0].id, i.stateManager.updateEvaluationState({
refId: c
}));
let o = t.estIds;
if (o.length === 0 && e.length >= 2 ? (o = [e[1].id], i.stateManager.updateEvaluationState({ estIds: o })) : o.length > 1 && (o = [o[0]], i.stateManager.updateEvaluationState({ estIds: o })), s.value = c || "", l.value = o[0] || "", s.value) {
const p = s.selectedOptions[0];
n(
s,
p ? `Reference: ${p.textContent}` : "Choose the reference MIDI file (ground truth)."
);
} else
n(s, "Choose the reference MIDI file (ground truth).");
const a = l.selectedOptions[0];
a ? n(l, `Comparison: ${a.textContent ?? ""}`) : n(l, "Pick one comparison MIDI file."), R();
};
s.addEventListener("change", () => {
if (i.stateManager.updateEvaluationState({
refId: s.value || null
}), s.value) {
const e = s.selectedOptions[0];
n(
s,
e ? `Reference: ${e.textContent}` : "Choose the reference MIDI file (ground truth)."
);
} else
n(s, "Choose the reference MIDI file (ground truth).");
R();
}), l.addEventListener("change", () => {
const e = l.value;
i.stateManager.updateEvaluationState({ estIds: e ? [e] : [] });
const t = l.selectedOptions[0];
t ? n(l, `Comparison: ${t.textContent ?? ""}`) : n(l, "Pick one comparison MIDI file."), R();
}), H(), i.stateManager.onStateChange(() => {
H();
}), C.append($, s), I.append(F, l), k.append(O, m);
const D = document.createElement("div");
D.style.cssText = "display:flex;align-items:center;gap:6px;font-size:12px;", n(
D,
"When enabled, always render the reference (ground-truth) notes above all other files."
);
const L = document.createElement("span");
L.textContent = "Ref on top:", L.style.cssText = "min-width:80px;font-weight:600;", n(
L,
"Render reference notes above others for clearer visibility in evaluation modes."
);
const E = document.createElement("input");
return E.type = "checkbox", E.checked = i.stateManager.getState().evaluation.refOnTop, E.addEventListener("change", () => {
i.stateManager.updateEvaluationState({
refOnTop: E.checked
});
}), n(
E,
"If checked, reference notes are drawn last (on top)."
), D.append(L, E), x.append(
y,
C,
I,
w,
k,
D,
h
), x;
}
function U(i) {
const x = document.getElementById("evaluation-results-overlay");
if (x) {
x.parentElement || document.body.appendChild(x);
return;
}
const r = document.createElement("div");
r.id = "evaluation-results-overlay", r.style.cssText = `
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
`;
const f = document.createElement("div");
f.style.cssText = `
width: 600px;
max-width: 95%;
max-height: 80vh;
overflow-y: auto;
background: var(--panel-bg);
border-radius: 12px;
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
`;
const g = document.createElement("div");
g.style.cssText = "display:flex;justify-content:space-between;align-items:center;";
const T = document.createElement("h2");
T.textContent = "Evaluation Results", T.style.cssText = "margin:0;font-size:20px;font-weight:700;color:var(--text-primary);";
const v = document.createElement("button");
v.textContent = "✕", v.style.cssText = "border:none;background:transparent;font-size:24px;cursor:pointer;color:var(--text-muted);", v.onclick = () => r.remove(), g.appendChild(T), g.appendChild(v);
const n = G(i);
f.appendChild(g), f.appendChild(n), r.appendChild(f), r.addEventListener("click", (y) => {
y.target === r && r.remove();
}), document.body.appendChild(r);
}
export {
U as openEvaluationResultsModal
};