UNPKG

wave-roll

Version:

JavaScript Library for Comparative MIDI Piano-Roll Visualization

288 lines (287 loc) 12.5 kB
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 };