#!/usr/bin/env python3 """Lightweight local web demo (standard library only): paste an LLM-rewritten passage, get the HTR reconstruction, a word-level diff and preservation warnings. Run: .venv/bin/python demo/app.py --model --port 8765""" from __future__ import annotations import argparse import difflib import html import json import sys from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "src")) from htr.inference.pipeline import NOTICE, HTRReconstructor # noqa: E402 PAGE = (Path(__file__).parent / "index.html").read_text() def word_diff(a: str, b: str) -> str: sm = difflib.SequenceMatcher(None, a.split(), b.split()) out = [] for op, i1, i2, j1, j2 in sm.get_opcodes(): if op == "equal": out.append(html.escape(" ".join(a.split()[i1:i2]))) else: if i2 > i1: out.append('' + html.escape(" ".join(a.split()[i1:i2])) + "") if j2 > j1: out.append('' + html.escape(" ".join(b.split()[j1:j2])) + "") return " ".join(out) class H(BaseHTTPRequestHandler): rec: HTRReconstructor = None # type: ignore[assignment] def _send(self, code: int, body: bytes, ctype: str) -> None: self.send_response(code) self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_GET(self): # noqa: N802 if self.path in ("/", "/index.html"): self._send(200, PAGE.replace("{{MODEL}}", html.escape(self.rec.meta.get("name", "htr"))).encode(), "text/html; charset=utf-8") elif self.path == "/health": self._send(200, json.dumps({"ok": True, "model": self.rec.meta.get("name")}).encode(), "application/json") else: self._send(404, b"not found", "text/plain") def do_POST(self): # noqa: N802 if self.path != "/api/reconstruct": return self._send(404, b"not found", "text/plain") n = int(self.headers.get("Content-Length", "0")) req = json.loads(self.rfile.read(n) or b"{}") text = (req.get("text") or "").strip() if not text: return self._send(400, json.dumps({"error": "empty text"}).encode(), "application/json") meta = {k: req.get(k) for k in ("discipline", "pub_year", "section_role") if req.get(k)} if "pub_year" in meta: meta["pub_year"] = int(meta["pub_year"]) res = self.rec.reconstruct(text[:6000], meta, beam=int(req.get("beam", 1))) body = {**res.to_dict(), "diff_html": word_diff(text, res.reconstruction), "notice": NOTICE} self._send(200, json.dumps(body, ensure_ascii=False).encode(), "application/json") def log_message(self, *a): # quiet pass def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--model", required=True) ap.add_argument("--port", type=int, default=8765) ap.add_argument("--host", default="127.0.0.1") ap.add_argument("--device", default="auto") a = ap.parse_args() H.rec = HTRReconstructor.from_pretrained(a.model, device=a.device) print(f"HTR demo on http://{a.host}:{a.port} (model {H.rec.meta.get('name')})") ThreadingHTTPServer((a.host, a.port), H).serve_forever() if __name__ == "__main__": main()