s2pro-egy / scripts /serve_eval.py
ehabnegm's picture
Phase 1: merged fast-AR LoRA step-1200 + full toolkit + phase-2 handoff
5c2beba verified
Raw
History Blame Contribute Delete
4.68 kB
#!/usr/bin/env python3
"""Tiny eval-comparison server: http://<box>:8080
Auto-discovers /opt/work/eval/<system>/ dirs (timing.jsonl present), shows each
eval sentence with side-by-side audio players + Soniox WER badges. Refresh to
pick up newly evaluated checkpoints.
"""
import json
import re
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
EVAL = Path("/opt/work/eval")
SENTS = Path("/opt/work/scripts/eval_sentences.jsonl")
def discover():
systems = []
for d in sorted(EVAL.iterdir()):
if d.is_dir() and any(d.glob("*.wav")) and d.name != "baseline":
systems.append(d.name)
# baseline first, then stepN numerically
def key(n):
if n.startswith("baseline"):
return (0, 0)
m = re.search(r"(\d+)", n)
return (1, int(m.group(1)) if m else 9e9)
return sorted(systems, key=key)
def reports(systems):
out = {}
for s in systems:
for fn in ("asr_report_soniox.json", "asr_report.json"):
p = EVAL / s / fn
if p.exists():
try:
rep = json.loads(p.read_text(encoding="utf-8"))
out[s] = {
"rows": {r["id"]: r for r in rep.get("rows", []) if "id" in r},
"summary": rep.get("summary", {}),
"engine": rep.get("summary", {}).get("engine", "whisper"),
}
except Exception:
pass
break
return out
def render():
systems = discover()
reps = reports(systems)
sents = [json.loads(l) for l in SENTS.read_text(encoding="utf-8").splitlines() if l.strip()]
# include ids that only exist in system timing files (e.g. mega paragraph)
known = {s["id"] for s in sents}
for s in systems:
tj = EVAL / s / "timing.jsonl"
if tj.exists():
for line in tj.read_text(encoding="utf-8").splitlines():
if line.strip():
r = json.loads(line)
if r.get("id") and r["id"] not in known and r.get("text"):
sents.insert(0, {"id": r["id"], "text": r["text"]})
known.add(r["id"])
head = "".join(
f"<th>{s}<br><small>{('WER %.0f%%' % (100*reps[s]['summary'].get('mean_wer',0))) if s in reps else 'scoring...'}</small></th>"
for s in systems)
rows_html = []
for sent in sents:
sid = sent["id"]
cells = []
for s in systems:
wav = EVAL / s / f"{sid}.wav"
if wav.exists():
badge = ""
r = reps.get(s, {}).get("rows", {}).get(sid)
if r and "wer" in r:
c = "#2a4" if r["wer"] < 0.2 else ("#a82" if r["wer"] < 0.5 else "#a33")
badge = f'<span class="b" style="background:{c}">WER {r["wer"]:.2f}</span>'
cells.append(
f'<td><audio controls preload="none" src="/{s}/{sid}.wav"></audio>{badge}</td>')
else:
cells.append("<td>—</td>")
rows_html.append(
f'<tr><td class="t"><b>{sid}</b><div dir="rtl">{sent["text"]}</div></td>'
+ "".join(cells) + "</tr>")
return f"""<!doctype html><html><head><meta charset="utf-8">
<title>s2-pro Egyptian eval</title>
<style>
body{{font-family:system-ui;background:#111;color:#eee;margin:20px}}
table{{border-collapse:collapse;width:100%}}
td,th{{border:1px solid #333;padding:8px;vertical-align:top}}
th{{background:#222;position:sticky;top:0}}
.t{{max-width:420px}} .t div{{font-size:15px;line-height:1.7}}
audio{{width:230px;display:block}}
.b{{font-size:11px;padding:2px 6px;border-radius:4px;display:inline-block;margin-top:4px}}
h1{{font-size:18px}}
</style></head><body>
<h1>s2-pro Egyptian fine-tune — A/B eval (refresh for new checkpoints)</h1>
<table><tr><th class="t">sentence</th>{head}</tr>
{''.join(rows_html)}
</table></body></html>"""
class H(SimpleHTTPRequestHandler):
def __init__(self, *a, **kw):
super().__init__(*a, directory=str(EVAL), **kw)
def do_GET(self):
if self.path in ("/", "/index.html"):
body = render().encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
else:
super().do_GET()
def log_message(self, *a):
pass
if __name__ == "__main__":
ThreadingHTTPServer(("0.0.0.0", 8080), H).serve_forever()