second-ear / ear /render.py
sevahu97's picture
Upload folder using huggingface_hub
5459c43 verified
Raw
History Blame Contribute Delete
6.77 kB
"""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 = """
<style>
.se-wrap{--se-bg:#101215;--se-panel:#171a1f;--se-line:#252a31;--se-dim:#7c8794;
--se-text:#e6eaef;--se-ok:#4ec9a0;--se-note:#5aa7e0;--se-warn:#e0a44a;--se-crit:#e0574a;
font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,monospace;
background:var(--se-bg);border:1px solid var(--se-line);border-radius:10px;
padding:16px 18px;color:var(--se-text);}
.se-verdict{display:flex;align-items:baseline;gap:12px;margin-bottom:14px;
padding-bottom:12px;border-bottom:1px solid var(--se-line);}
.se-dot{width:9px;height:9px;border-radius:50%;flex:none;box-shadow:0 0 12px currentColor;}
.se-verdict-text{font-size:15px;letter-spacing:-0.01em;line-height:1.3;}
.se-verdict-sub{margin-left:auto;font-size:11px;color:var(--se-dim);white-space:nowrap;}
.se-nums{display:grid;grid-template-columns:repeat(auto-fit,minmax(88px,1fr));
gap:1px;background:var(--se-line);border:1px solid var(--se-line);border-radius:6px;
overflow:hidden;margin-bottom:16px;}
.se-num{background:var(--se-panel);padding:9px 11px;}
.se-num-k{font-size:9px;letter-spacing:0.09em;text-transform:uppercase;color:var(--se-dim);}
.se-num-v{font-size:17px;margin-top:3px;font-variant-numeric:tabular-nums;}
.se-bars{display:flex;flex-direction:column;gap:6px;}
.se-bar-row{display:grid;grid-template-columns:118px 1fr 54px;align-items:center;gap:10px;}
.se-bar-label{font-size:10px;color:var(--se-dim);letter-spacing:0.03em;}
.se-track{position:relative;height:16px;background:#0b0d10;border-radius:3px;overflow:hidden;}
.se-target{position:absolute;top:0;bottom:0;background:rgba(255,255,255,.07);
border-left:1px solid rgba(255,255,255,.16);border-right:1px solid rgba(255,255,255,.16);}
.se-fill{position:absolute;top:3px;bottom:3px;left:0;border-radius:2px;
transition:width 120ms linear;}
.se-bar-val{font-size:11px;text-align:right;font-variant-numeric:tabular-nums;color:var(--se-dim);}
.se-cards{display:flex;flex-direction:column;gap:9px;}
.se-card{background:var(--se-panel);border:1px solid var(--se-line);border-left-width:3px;
border-radius:7px;padding:12px 14px;}
.se-card h4{margin:0 0 5px;font-size:13.5px;font-weight:600;letter-spacing:-0.01em;}
.se-card .ev{font-size:10.5px;color:var(--se-dim);margin-bottom:8px;
font-variant-numeric:tabular-nums;}
.se-card p{margin:0 0 7px;font-size:12.5px;line-height:1.55;
font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;color:#c3cbd4;}
.se-card .move{font-size:12.5px;line-height:1.55;
font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;color:var(--se-text);
border-top:1px dashed var(--se-line);padding-top:7px;}
.se-idle{color:var(--se-dim);font-size:12.5px;padding:26px 0;text-align:center;}
</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}<div class="se-wrap"><div class="se-idle">{html.escape(message)}</div></div>'
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'<div class="se-num"><div class="se-num-k">{html.escape(k)}</div>'
f'<div class="se-num-v">{html.escape(v)}</div></div>'
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'<div class="se-bar-row">'
f'<div class="se-bar-label">{html.escape(dsp.BAND_LABELS[band])}</div>'
f'<div class="se-track">'
f'<div class="se-target" style="left:{t_left:.1f}%;width:{max(t_right - t_left, 1):.1f}%"></div>'
f'<div class="se-fill" style="width:{_pct(val):.1f}%;background:{fill}"></div>'
f'</div>'
f'<div class="se-bar-val">{val:+.1f}</div>'
f'</div>'
)
sub = html.escape(extra) if extra else f"{rep.duration:.1f}s window · {html.escape(genre)}"
return (
f'{STYLE}<div class="se-wrap">'
f'<div class="se-verdict">'
f'<span class="se-dot" style="background:{colour};color:{colour}"></span>'
f'<span class="se-verdict-text">{html.escape(verdict)}</span>'
f'<span class="se-verdict-sub">{sub}</span>'
f'</div>'
f'<div class="se-nums">{num_html}</div>'
f'<div class="se-bars">{"".join(rows)}</div>'
f'</div>'
)
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'<div class="se-card" style="border-left-color:{colour}">'
f'<h4 style="color:{colour}">{html.escape(d.headline)}</h4>'
f'<div class="ev">{html.escape(d.evidence)}</div>'
f'<p>{html.escape(d.why)}</p>'
f'<div class="move"><b>Move:</b> {html.escape(d.move)}</div>'
f'</div>'
)
return (f'{STYLE}<div class="se-wrap" style="background:transparent;border:none;padding:0">'
f'<div class="se-cards">{"".join(out)}</div></div>')