"""HTML rendering for the live meter strip and the finding cards.
Gradio's stock components can show a number. They cannot show a number
*against the window it is supposed to sit in*, which is the whole point of a
mix meter — so the bars are hand-rolled with the target range drawn as a
ghost behind the fill.
"""
from __future__ import annotations
import html
from . import dsp, knowledge
STYLE = """
"""
_TONE = {"ok": "var(--se-ok)", "note": "var(--se-note)",
"warn": "var(--se-warn)", "critical": "var(--se-crit)"}
def _pct(value: float, lo: float = -30.0, hi: float = 0.0) -> float:
return max(0.0, min(100.0, (value - lo) / (hi - lo) * 100.0))
def _fmt(v: float, unit: str = "", digits: int = 1) -> str:
if v is None or v != v or v <= -119:
return "—"
return f"{v:.{digits}f}{unit}"
def idle(message: str) -> str:
return f'{STYLE}
'
def meters(rep: dsp.Report | None, genre: str, verdict: str, tone: str,
extra: str = "") -> str:
if rep is None:
return idle("waiting for signal…")
colour = _TONE.get(tone, "var(--se-dim)")
windows = knowledge.band_target_windows(genre)
nums = [
("LUFS-S", _fmt(rep.lufs_s)),
("True peak", _fmt(rep.true_peak, " dB", 2)),
("Crest", _fmt(rep.crest, " dB")),
("Corr", f"{rep.stereo.get('correlation', 0):+.2f}"),
("Sub corr", f"{rep.stereo.get('sub_correlation', 0):+.2f}"),
("Width", _fmt(rep.stereo.get("width_db", -99), " dB")),
]
bpm = rep.rhythm.get("bpm", 0.0)
if bpm > 0:
nums.append(("Tempo", f"{bpm:.0f}"))
key = rep.key.get("key", "—")
if key and key != "—":
nums.append(("Key", key))
num_html = "".join(
f'{html.escape(k)}
'
f'
{html.escape(v)}
'
for k, v in nums
)
rows = []
for band in dsp.BAND_ORDER:
val = rep.bands.get(band, -60.0)
lo, hi = windows.get(band, (-20.0, -4.0))
inside = lo <= val <= hi
fill = "var(--se-ok)" if inside else (
"var(--se-warn)" if val > hi else "var(--se-note)")
t_left, t_right = _pct(lo), _pct(hi)
rows.append(
f''
f'
{html.escape(dsp.BAND_LABELS[band])}
'
f'
'
f'
{val:+.1f}
'
f'
'
)
sub = html.escape(extra) if extra else f"{rep.duration:.1f}s window · {html.escape(genre)}"
return (
f'{STYLE}'
f'
'
f''
f'{html.escape(verdict)}'
f'{sub}'
f'
'
f'
{num_html}
'
f'
{"".join(rows)}
'
f'
'
)
def cards(diags: list[knowledge.Diagnosis], limit: int = 6) -> str:
if not diags:
return idle("no findings yet")
out = []
for d in diags[:limit]:
colour = _TONE.get(d.severity, "var(--se-dim)")
out.append(
f''
f'
{html.escape(d.headline)}
'
f'
{html.escape(d.evidence)}
'
f'
{html.escape(d.why)}
'
f'
Move: {html.escape(d.move)}
'
f'
'
)
return (f'{STYLE}')