Spaces:
Sleeping
Sleeping
MathewKasbarian commited on
Commit Β·
280468b
1
Parent(s): c640d68
fix(annotator): execute review-canvas JS via Blocks head and add edit autosave
Browse filesThe canvas <script> was injected with gr.HTML, which Gradio mounts via
innerHTML β browsers never execute scripts inserted that way, so the
Review & Edit canvas was dead in every browser. Serve it through
gr.Blocks(head=...) instead.
Autosave checkpoints every annotated page to data/working_set/autosave/
on each mutating action (canonical label JSON, per-box status preserved
for exact resume; pages with unreviewed machine boxes stay
audit_status=machine). Disabled automatically off-repo (HF Space).
app.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
import base64
|
| 2 |
import hashlib
|
| 3 |
import json
|
|
|
|
|
|
|
| 4 |
import tempfile
|
| 5 |
import uuid
|
| 6 |
import zipfile
|
|
@@ -404,6 +406,33 @@ def detect_page_api(pdf_file, page_number=1, dpi=DETECTION_DPI, conf=0.25):
|
|
| 404 |
# ---------------------------------------------------------------------------
|
| 405 |
_LABEL_DISPLAY_DPI = 100 # the Review tab canvas coordinate space
|
| 406 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
|
| 408 |
def _source_tag(ann, model_version):
|
| 409 |
src = ann.get("source", "unknown")
|
|
@@ -414,45 +443,74 @@ def _source_tag(ann, model_version):
|
|
| 414 |
return src
|
| 415 |
|
| 416 |
|
| 417 |
-
def
|
| 418 |
-
"""
|
| 419 |
|
| 420 |
Coordinates are converted from the 100-DPI canvas space to PDF points so
|
| 421 |
-
labels are DPI-independent and merge with other sources downstream.
|
|
|
|
| 422 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
if rv_pdf is None:
|
| 424 |
return None, "No PDF loaded."
|
| 425 |
pages = {p: anns for p, anns in (rv_annotations or {}).items() if anns}
|
| 426 |
if not pages:
|
| 427 |
return None, "No annotations to export."
|
| 428 |
stem = Path(rv_pdf).stem
|
| 429 |
-
mv = get_model_version()
|
| 430 |
tmp_dir = Path(tempfile.mkdtemp(prefix="labels_"))
|
| 431 |
written = []
|
| 432 |
for page, anns in sorted(pages.items()):
|
| 433 |
-
|
| 434 |
-
for ann in anns:
|
| 435 |
-
entry = {
|
| 436 |
-
"id": ann.get("uid") or str(uuid.uuid4()),
|
| 437 |
-
"x1_pt": round(px_to_pt(ann["x1"], _LABEL_DISPLAY_DPI), 3),
|
| 438 |
-
"y1_pt": round(px_to_pt(ann["y1"], _LABEL_DISPLAY_DPI), 3),
|
| 439 |
-
"x2_pt": round(px_to_pt(ann["x2"], _LABEL_DISPLAY_DPI), 3),
|
| 440 |
-
"y2_pt": round(px_to_pt(ann["y2"], _LABEL_DISPLAY_DPI), 3),
|
| 441 |
-
"source": _source_tag(ann, mv),
|
| 442 |
-
"conf": round(float(ann.get("conf", 1.0)), 4),
|
| 443 |
-
"history": ann.get("history", []),
|
| 444 |
-
}
|
| 445 |
-
(rejected if ann.get("status") == "rejected" else boxes).append(entry)
|
| 446 |
-
data = {
|
| 447 |
-
"doc": stem,
|
| 448 |
-
"pdf": Path(rv_pdf).name,
|
| 449 |
-
"page": int(page),
|
| 450 |
-
"page_type": "",
|
| 451 |
-
"audit_status": "audited",
|
| 452 |
-
"audited_at": datetime.now(UTC).isoformat(),
|
| 453 |
-
"boxes": boxes,
|
| 454 |
-
"rejected_boxes": rejected,
|
| 455 |
-
}
|
| 456 |
out = tmp_dir / f"p{int(page):03d}.json"
|
| 457 |
out.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
| 458 |
written.append(out)
|
|
@@ -498,13 +556,15 @@ def on_rv_import_labels(label_files, rv_pdf, rv_page, rv_annotations):
|
|
| 498 |
"x2": pt_to_px(box["x2_pt"], _LABEL_DISPLAY_DPI),
|
| 499 |
"y2": pt_to_px(box["y2_pt"], _LABEL_DISPLAY_DPI),
|
| 500 |
"conf": float(box.get("conf", 1.0)),
|
| 501 |
-
|
|
|
|
| 502 |
"source": box.get("source", "unknown"),
|
| 503 |
"history": box.get("history", []),
|
| 504 |
}
|
| 505 |
)
|
| 506 |
rv_annotations[page] = anns
|
| 507 |
loaded_pages.append(page)
|
|
|
|
| 508 |
current = rv_annotations.get(rv_page, [])
|
| 509 |
img_rgb = _render_pdf_page(rv_pdf, rv_page, dpi=100) if rv_pdf is not None and rv_page in loaded_pages else None
|
| 510 |
return (
|
|
@@ -663,6 +723,9 @@ def on_canvas_events(events_json, rv_pdf, rv_page, rv_annotations, rv_ledger):
|
|
| 663 |
)
|
| 664 |
rv_ledger = _append_events(rv_ledger, events)
|
| 665 |
|
|
|
|
|
|
|
|
|
|
| 666 |
canvas_json = _build_canvas_json(None, new_anns, selected=selected)
|
| 667 |
return canvas_json, rv_annotations, rv_ledger, _build_review_df(new_anns), selected
|
| 668 |
|
|
@@ -699,6 +762,7 @@ def on_rv_detect(rv_pdf, rv_page, rv_annotations, rv_conf):
|
|
| 699 |
]
|
| 700 |
rv_annotations = dict(rv_annotations)
|
| 701 |
rv_annotations[rv_page] = annotations
|
|
|
|
| 702 |
engine = "YOLO" if using_yolo else "OpenCV"
|
| 703 |
return (
|
| 704 |
_build_canvas_json(img_rgb, annotations),
|
|
@@ -752,6 +816,7 @@ def on_rv_detect_all(rv_pdf, rv_page, rv_total, rv_annotations, rv_conf):
|
|
| 752 |
return _build_canvas_json(None, []), _build_review_df([]), rv_annotations, f"Error: {exc}"
|
| 753 |
img_rgb = _render_pdf_page(rv_pdf, rv_page, dpi=100)
|
| 754 |
anns = all_anns.get(rv_page, [])
|
|
|
|
| 755 |
engine = "YOLO" if using_yolo else "OpenCV"
|
| 756 |
return (
|
| 757 |
_build_canvas_json(img_rgb, anns),
|
|
@@ -791,6 +856,7 @@ def on_rv_update_box(rv_pdf, rv_page, rv_annotations, ledger, selected_idx, x1,
|
|
| 791 |
rv_annotations[rv_page] = new_anns
|
| 792 |
event = _make_event(rv_page, old, "modified", before=before, after=_make_box(new_anns[idx]))
|
| 793 |
ledger = _append_events(ledger, [event])
|
|
|
|
| 794 |
img_rgb = _render_pdf_page(rv_pdf, rv_page, dpi=100)
|
| 795 |
return (
|
| 796 |
_build_canvas_json(img_rgb, new_anns, selected=idx),
|
|
@@ -814,6 +880,7 @@ def on_rv_approve_all(rv_pdf, rv_page, rv_annotations, ledger):
|
|
| 814 |
if a.get("status") != "approved"
|
| 815 |
]
|
| 816 |
ledger = _append_events(ledger, events)
|
|
|
|
| 817 |
return (
|
| 818 |
_build_canvas_json(None, new_anns),
|
| 819 |
_build_review_df(new_anns),
|
|
@@ -836,6 +903,7 @@ def on_rv_reject_all(rv_pdf, rv_page, rv_annotations, ledger):
|
|
| 836 |
if a.get("status") != "rejected"
|
| 837 |
]
|
| 838 |
ledger = _append_events(ledger, events)
|
|
|
|
| 839 |
return (
|
| 840 |
_build_canvas_json(None, new_anns),
|
| 841 |
_build_review_df(new_anns),
|
|
@@ -859,6 +927,7 @@ def on_rv_approve_selected(rv_pdf, rv_page, rv_annotations, ledger, selected_idx
|
|
| 859 |
rv_annotations[rv_page] = new_anns
|
| 860 |
event = _make_event(rv_page, old, "accepted", before=_make_box(old), after=_make_box(new_anns[idx]))
|
| 861 |
ledger = _append_events(ledger, [event])
|
|
|
|
| 862 |
return (
|
| 863 |
_build_canvas_json(None, new_anns, selected=idx),
|
| 864 |
_build_review_df(new_anns),
|
|
@@ -883,6 +952,7 @@ def on_rv_delete_selected(rv_pdf, rv_page, rv_annotations, ledger, selected_idx)
|
|
| 883 |
rv_annotations[rv_page] = new_anns
|
| 884 |
event = _make_event(rv_page, removed, "rejected", before=_make_box(removed), after=None)
|
| 885 |
ledger = _append_events(ledger, [event])
|
|
|
|
| 886 |
new_sel = min(idx, len(new_anns) - 1) if new_anns else -1
|
| 887 |
return (
|
| 888 |
_build_canvas_json(None, new_anns, selected=new_sel),
|
|
@@ -911,6 +981,7 @@ def on_rv_copy_selected(rv_pdf, rv_page, rv_annotations, ledger, selected_idx):
|
|
| 911 |
new_sel = len(new_anns) - 1
|
| 912 |
event = _make_event(rv_page, new_ann, "created", before=None, after=_make_box(new_ann))
|
| 913 |
ledger = _append_events(ledger, [event])
|
|
|
|
| 914 |
return (
|
| 915 |
_build_canvas_json(None, new_anns, selected=new_sel),
|
| 916 |
_build_review_df(new_anns),
|
|
@@ -1250,7 +1321,9 @@ _CANVAS_JS = """
|
|
| 1250 |
</script>
|
| 1251 |
"""
|
| 1252 |
|
| 1253 |
-
|
|
|
|
|
|
|
| 1254 |
gr.Markdown(f"# Takeoff AI Annotator \n_Model: `{_model_version}`_")
|
| 1255 |
|
| 1256 |
shapes_state = gr.State([])
|
|
@@ -1380,6 +1453,12 @@ with gr.Blocks(title="Takeoff AI Annotator") as demo:
|
|
| 1380 |
gr.Markdown(
|
| 1381 |
"`A` approve `C` copy `Del` delete "
|
| 1382 |
"`β β` pages β draw box by dragging on empty canvas"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1383 |
)
|
| 1384 |
|
| 1385 |
# ββ Wire events βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -1581,7 +1660,5 @@ with gr.Blocks(title="Takeoff AI Annotator") as demo:
|
|
| 1581 |
api_name="detect_page",
|
| 1582 |
)
|
| 1583 |
|
| 1584 |
-
gr.HTML(_CANVAS_JS)
|
| 1585 |
-
|
| 1586 |
if __name__ == "__main__":
|
| 1587 |
demo.launch()
|
|
|
|
| 1 |
import base64
|
| 2 |
import hashlib
|
| 3 |
import json
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
import tempfile
|
| 7 |
import uuid
|
| 8 |
import zipfile
|
|
|
|
| 406 |
# ---------------------------------------------------------------------------
|
| 407 |
_LABEL_DISPLAY_DPI = 100 # the Review tab canvas coordinate space
|
| 408 |
|
| 409 |
+
# Autosave: when running from the repo checkout (not the HF Space), every
|
| 410 |
+
# annotation change is checkpointed to disk so a browser refresh/crash loses
|
| 411 |
+
# nothing. Files go to a staging dir, NOT the canonical labels dir β the
|
| 412 |
+
# canonical ground truth is only ever written via merge_labels.py.
|
| 413 |
+
_REPO_WORKING_SET = Path(__file__).resolve().parents[2] / "data" / "working_set"
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def _resolve_autosave_dir():
|
| 417 |
+
env = os.environ.get("FACADE_AUTOSAVE_DIR")
|
| 418 |
+
if env:
|
| 419 |
+
p = Path(env)
|
| 420 |
+
p.mkdir(parents=True, exist_ok=True)
|
| 421 |
+
return p
|
| 422 |
+
if _REPO_WORKING_SET.is_dir():
|
| 423 |
+
p = _REPO_WORKING_SET / "autosave"
|
| 424 |
+
p.mkdir(parents=True, exist_ok=True)
|
| 425 |
+
return p
|
| 426 |
+
return None # HF Space / standalone β autosave off
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
_AUTOSAVE_DIR = _resolve_autosave_dir()
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
def _slugify_doc(stem):
|
| 433 |
+
# must match scripts/annotation/common.py::doc_slug
|
| 434 |
+
return re.sub(r"[^A-Za-z0-9._-]+", "_", stem).strip("_")[:60]
|
| 435 |
+
|
| 436 |
|
| 437 |
def _source_tag(ann, model_version):
|
| 438 |
src = ann.get("source", "unknown")
|
|
|
|
| 443 |
return src
|
| 444 |
|
| 445 |
|
| 446 |
+
def _page_label_data(rv_pdf, page, anns, audit_status):
|
| 447 |
+
"""Build one page's canonical label dict from canvas annotations.
|
| 448 |
|
| 449 |
Coordinates are converted from the 100-DPI canvas space to PDF points so
|
| 450 |
+
labels are DPI-independent and merge with other sources downstream. The
|
| 451 |
+
per-box "status" key is an extra field for exact import round-trips.
|
| 452 |
"""
|
| 453 |
+
mv = get_model_version()
|
| 454 |
+
boxes, rejected = [], []
|
| 455 |
+
for ann in anns:
|
| 456 |
+
entry = {
|
| 457 |
+
"id": ann.get("uid") or str(uuid.uuid4()),
|
| 458 |
+
"x1_pt": round(px_to_pt(ann["x1"], _LABEL_DISPLAY_DPI), 3),
|
| 459 |
+
"y1_pt": round(px_to_pt(ann["y1"], _LABEL_DISPLAY_DPI), 3),
|
| 460 |
+
"x2_pt": round(px_to_pt(ann["x2"], _LABEL_DISPLAY_DPI), 3),
|
| 461 |
+
"y2_pt": round(px_to_pt(ann["y2"], _LABEL_DISPLAY_DPI), 3),
|
| 462 |
+
"source": _source_tag(ann, mv),
|
| 463 |
+
"conf": round(float(ann.get("conf", 1.0)), 4),
|
| 464 |
+
"status": ann.get("status", "pending"),
|
| 465 |
+
"history": ann.get("history", []),
|
| 466 |
+
}
|
| 467 |
+
(rejected if ann.get("status") == "rejected" else boxes).append(entry)
|
| 468 |
+
return {
|
| 469 |
+
"doc": _slugify_doc(Path(rv_pdf).stem),
|
| 470 |
+
"pdf": Path(rv_pdf).name,
|
| 471 |
+
"page": int(page),
|
| 472 |
+
"page_type": "",
|
| 473 |
+
"audit_status": audit_status,
|
| 474 |
+
"audited_at": datetime.now(UTC).isoformat(),
|
| 475 |
+
"boxes": boxes,
|
| 476 |
+
"rejected_boxes": rejected,
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
def _autosave(rv_pdf, rv_annotations):
|
| 481 |
+
"""Checkpoint every annotated page to the autosave dir. Never raises β
|
| 482 |
+
a failed checkpoint must not break the edit that triggered it."""
|
| 483 |
+
if _AUTOSAVE_DIR is None or rv_pdf is None:
|
| 484 |
+
return
|
| 485 |
+
try:
|
| 486 |
+
out_dir = _AUTOSAVE_DIR / _slugify_doc(Path(rv_pdf).stem)
|
| 487 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 488 |
+
for page, anns in (rv_annotations or {}).items():
|
| 489 |
+
if not anns:
|
| 490 |
+
continue
|
| 491 |
+
# A page still holding unreviewed machine boxes is not an audit
|
| 492 |
+
unreviewed = any(
|
| 493 |
+
a.get("status", "pending") == "pending" and not str(a.get("source", "")).startswith("human")
|
| 494 |
+
for a in anns
|
| 495 |
+
)
|
| 496 |
+
data = _page_label_data(rv_pdf, page, anns, "machine" if unreviewed else "audited")
|
| 497 |
+
(out_dir / f"p{int(page):03d}.json").write_text(json.dumps(data, indent=2), encoding="utf-8")
|
| 498 |
+
except Exception as exc:
|
| 499 |
+
print(f"[autosave] checkpoint failed: {exc}")
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
def on_rv_export_labels(rv_pdf, rv_annotations):
|
| 503 |
+
"""Export every page with annotations as canonical label JSONs (zipped)."""
|
| 504 |
if rv_pdf is None:
|
| 505 |
return None, "No PDF loaded."
|
| 506 |
pages = {p: anns for p, anns in (rv_annotations or {}).items() if anns}
|
| 507 |
if not pages:
|
| 508 |
return None, "No annotations to export."
|
| 509 |
stem = Path(rv_pdf).stem
|
|
|
|
| 510 |
tmp_dir = Path(tempfile.mkdtemp(prefix="labels_"))
|
| 511 |
written = []
|
| 512 |
for page, anns in sorted(pages.items()):
|
| 513 |
+
data = _page_label_data(rv_pdf, page, anns, "audited")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
out = tmp_dir / f"p{int(page):03d}.json"
|
| 515 |
out.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
| 516 |
written.append(out)
|
|
|
|
| 556 |
"x2": pt_to_px(box["x2_pt"], _LABEL_DISPLAY_DPI),
|
| 557 |
"y2": pt_to_px(box["y2_pt"], _LABEL_DISPLAY_DPI),
|
| 558 |
"conf": float(box.get("conf", 1.0)),
|
| 559 |
+
# autosave files carry per-box status for exact resume
|
| 560 |
+
"status": "rejected" if status == "rejected" else box.get("status", status),
|
| 561 |
"source": box.get("source", "unknown"),
|
| 562 |
"history": box.get("history", []),
|
| 563 |
}
|
| 564 |
)
|
| 565 |
rv_annotations[page] = anns
|
| 566 |
loaded_pages.append(page)
|
| 567 |
+
_autosave(rv_pdf, rv_annotations)
|
| 568 |
current = rv_annotations.get(rv_page, [])
|
| 569 |
img_rgb = _render_pdf_page(rv_pdf, rv_page, dpi=100) if rv_pdf is not None and rv_page in loaded_pages else None
|
| 570 |
return (
|
|
|
|
| 723 |
)
|
| 724 |
rv_ledger = _append_events(rv_ledger, events)
|
| 725 |
|
| 726 |
+
if action in ("created", "moved", "resized", "deleted"):
|
| 727 |
+
_autosave(rv_pdf, rv_annotations)
|
| 728 |
+
|
| 729 |
canvas_json = _build_canvas_json(None, new_anns, selected=selected)
|
| 730 |
return canvas_json, rv_annotations, rv_ledger, _build_review_df(new_anns), selected
|
| 731 |
|
|
|
|
| 762 |
]
|
| 763 |
rv_annotations = dict(rv_annotations)
|
| 764 |
rv_annotations[rv_page] = annotations
|
| 765 |
+
_autosave(rv_pdf, rv_annotations)
|
| 766 |
engine = "YOLO" if using_yolo else "OpenCV"
|
| 767 |
return (
|
| 768 |
_build_canvas_json(img_rgb, annotations),
|
|
|
|
| 816 |
return _build_canvas_json(None, []), _build_review_df([]), rv_annotations, f"Error: {exc}"
|
| 817 |
img_rgb = _render_pdf_page(rv_pdf, rv_page, dpi=100)
|
| 818 |
anns = all_anns.get(rv_page, [])
|
| 819 |
+
_autosave(rv_pdf, all_anns)
|
| 820 |
engine = "YOLO" if using_yolo else "OpenCV"
|
| 821 |
return (
|
| 822 |
_build_canvas_json(img_rgb, anns),
|
|
|
|
| 856 |
rv_annotations[rv_page] = new_anns
|
| 857 |
event = _make_event(rv_page, old, "modified", before=before, after=_make_box(new_anns[idx]))
|
| 858 |
ledger = _append_events(ledger, [event])
|
| 859 |
+
_autosave(rv_pdf, rv_annotations)
|
| 860 |
img_rgb = _render_pdf_page(rv_pdf, rv_page, dpi=100)
|
| 861 |
return (
|
| 862 |
_build_canvas_json(img_rgb, new_anns, selected=idx),
|
|
|
|
| 880 |
if a.get("status") != "approved"
|
| 881 |
]
|
| 882 |
ledger = _append_events(ledger, events)
|
| 883 |
+
_autosave(rv_pdf, rv_annotations)
|
| 884 |
return (
|
| 885 |
_build_canvas_json(None, new_anns),
|
| 886 |
_build_review_df(new_anns),
|
|
|
|
| 903 |
if a.get("status") != "rejected"
|
| 904 |
]
|
| 905 |
ledger = _append_events(ledger, events)
|
| 906 |
+
_autosave(rv_pdf, rv_annotations)
|
| 907 |
return (
|
| 908 |
_build_canvas_json(None, new_anns),
|
| 909 |
_build_review_df(new_anns),
|
|
|
|
| 927 |
rv_annotations[rv_page] = new_anns
|
| 928 |
event = _make_event(rv_page, old, "accepted", before=_make_box(old), after=_make_box(new_anns[idx]))
|
| 929 |
ledger = _append_events(ledger, [event])
|
| 930 |
+
_autosave(rv_pdf, rv_annotations)
|
| 931 |
return (
|
| 932 |
_build_canvas_json(None, new_anns, selected=idx),
|
| 933 |
_build_review_df(new_anns),
|
|
|
|
| 952 |
rv_annotations[rv_page] = new_anns
|
| 953 |
event = _make_event(rv_page, removed, "rejected", before=_make_box(removed), after=None)
|
| 954 |
ledger = _append_events(ledger, [event])
|
| 955 |
+
_autosave(rv_pdf, rv_annotations)
|
| 956 |
new_sel = min(idx, len(new_anns) - 1) if new_anns else -1
|
| 957 |
return (
|
| 958 |
_build_canvas_json(None, new_anns, selected=new_sel),
|
|
|
|
| 981 |
new_sel = len(new_anns) - 1
|
| 982 |
event = _make_event(rv_page, new_ann, "created", before=None, after=_make_box(new_ann))
|
| 983 |
ledger = _append_events(ledger, [event])
|
| 984 |
+
_autosave(rv_pdf, rv_annotations)
|
| 985 |
return (
|
| 986 |
_build_canvas_json(None, new_anns, selected=new_sel),
|
| 987 |
_build_review_df(new_anns),
|
|
|
|
| 1321 |
</script>
|
| 1322 |
"""
|
| 1323 |
|
| 1324 |
+
# _CANVAS_JS must go through head= β gr.HTML inserts via innerHTML, which never
|
| 1325 |
+
# executes <script> tags, so the canvas engine would silently not run.
|
| 1326 |
+
with gr.Blocks(title="Takeoff AI Annotator", head=_CANVAS_JS) as demo:
|
| 1327 |
gr.Markdown(f"# Takeoff AI Annotator \n_Model: `{_model_version}`_")
|
| 1328 |
|
| 1329 |
shapes_state = gr.State([])
|
|
|
|
| 1453 |
gr.Markdown(
|
| 1454 |
"`A` approve `C` copy `Del` delete "
|
| 1455 |
"`β β` pages β draw box by dragging on empty canvas"
|
| 1456 |
+
+ (
|
| 1457 |
+
f" \nπΎ **Autosave on** β every change is checkpointed to `{_AUTOSAVE_DIR}`; "
|
| 1458 |
+
"a refresh/crash loses nothing (re-import from there to resume)."
|
| 1459 |
+
if _AUTOSAVE_DIR
|
| 1460 |
+
else ""
|
| 1461 |
+
)
|
| 1462 |
)
|
| 1463 |
|
| 1464 |
# ββ Wire events βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 1660 |
api_name="detect_page",
|
| 1661 |
)
|
| 1662 |
|
|
|
|
|
|
|
| 1663 |
if __name__ == "__main__":
|
| 1664 |
demo.launch()
|