"""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}
{html.escape(message)}
' 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'
' f'
' 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}
' f'
{"".join(out)}
')