Spaces:
Sleeping
Sleeping
| """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>') | |