Spaces:
Sleeping
Sleeping
Simplify JSON submission workflow
Browse files
app.py
CHANGED
|
@@ -32,6 +32,7 @@ DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
| 32 |
FAVICON_PATH = SPACE_ROOT / "assets" / "favicon.svg"
|
| 33 |
HYDRATE_STATUS = maybe_hydrate_from_dataset(DATA_DIR)
|
| 34 |
STORE = SolutionStore(DATA_DIR)
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
@lru_cache(maxsize=1)
|
|
@@ -1540,7 +1541,13 @@ Coordinate convention:
|
|
| 1540 |
- touching is legal; overlap and protrusion are rejected
|
| 1541 |
"""
|
| 1542 |
|
| 1543 |
-
EXAMPLES_MD = f"""###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1544 |
|
| 1545 |
This is the smallest legal record and a good shape for checking the schema:
|
| 1546 |
|
|
@@ -1554,6 +1561,7 @@ This is the smallest legal record and a good shape for checking the schema:
|
|
| 1554 |
- Use container-centered coordinates.
|
| 1555 |
- Use radians for `rotation_radians`.
|
| 1556 |
- Click **Verify** before submitting; the preview is rendered from the same canonical JSON that is archived.
|
|
|
|
| 1557 |
"""
|
| 1558 |
|
| 1559 |
SCHEMA_DOCS_MD = """### Canonical Fields
|
|
@@ -1575,7 +1583,7 @@ Supported shape specs:
|
|
| 1575 |
- `circle`: `radius` or `diameter`
|
| 1576 |
- `rectangle`: `width` and `height`
|
| 1577 |
|
| 1578 |
-
The verifier
|
| 1579 |
""" + f"""
|
| 1580 |
|
| 1581 |
### Four Circles In A Square
|
|
@@ -2571,32 +2579,76 @@ def submit_schema_intro_html() -> str:
|
|
| 2571 |
<div>
|
| 2572 |
<h2>Submit Entry</h2>
|
| 2573 |
<p class="section-note">
|
| 2574 |
-
Paste canonical coordinate JSON
|
| 2575 |
</p>
|
| 2576 |
</div>
|
| 2577 |
</div>
|
| 2578 |
-
<div class="submit-flow" aria-label="Submission steps">
|
| 2579 |
-
<div class="submit-step"><strong>1. Identify</strong><span>Choose a credited author from the searchable list, or type a new name.</span></div>
|
| 2580 |
-
<div class="submit-step"><strong>2. Verify</strong><span>Paste or upload canonical JSON and run the same geometric evaluator used by the leaderboard.</span></div>
|
| 2581 |
-
<div class="submit-step"><strong>3. Submit</strong><span>Accepted records are stored with normalized JSON and rendered directly as vector geometry.</span></div>
|
| 2582 |
-
</div>
|
| 2583 |
</div>
|
| 2584 |
</section>
|
| 2585 |
</main>
|
| 2586 |
"""
|
| 2587 |
|
| 2588 |
|
| 2589 |
-
def load_submission(json_text: str
|
| 2590 |
-
if json_file:
|
| 2591 |
-
return load_solution_json(Path(json_file).read_text())
|
| 2592 |
if not json_text or not json_text.strip():
|
| 2593 |
-
raise ValueError("Paste canonical
|
| 2594 |
return load_solution_json(json_text)
|
| 2595 |
|
| 2596 |
|
| 2597 |
-
def
|
| 2598 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2599 |
return (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2600 |
"### Verification Passed\n\n"
|
| 2601 |
f"- Case: `{result['case']}`\n"
|
| 2602 |
f"- Items: `{result['n']}`\n"
|
|
@@ -2605,12 +2657,17 @@ def result_markdown(result: dict[str, Any]) -> str:
|
|
| 2605 |
f"- Max boundary excess: `{result['max_boundary_excess']:.3e}`\n"
|
| 2606 |
f"- Max pair overlap depth: `{result['max_pair_overlap_depth']:.3e}`\n"
|
| 2607 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2608 |
errors = "\n".join(f"- {e}" for e in result["errors"])
|
| 2609 |
return "### Verification Failed\n\n" + errors
|
| 2610 |
|
| 2611 |
|
| 2612 |
def empty_report_markdown() -> str:
|
| 2613 |
-
return "### Ready To Verify\n\nPaste
|
| 2614 |
|
| 2615 |
|
| 2616 |
def empty_preview_html() -> str:
|
|
@@ -2647,38 +2704,41 @@ def preview_html(solution: dict[str, Any]) -> str:
|
|
| 2647 |
"""
|
| 2648 |
|
| 2649 |
|
| 2650 |
-
def verify_only(json_text: str
|
| 2651 |
try:
|
| 2652 |
-
solution = normalize_solution(load_submission(json_text
|
| 2653 |
-
result = verify_solution(solution, tolerance=
|
|
|
|
| 2654 |
pretty = json.dumps(solution, indent=2, sort_keys=True)
|
| 2655 |
preview = preview_html(solution) if result["ok"] else ""
|
| 2656 |
-
return result_markdown(result), preview, pretty
|
| 2657 |
except Exception as exc:
|
| 2658 |
return f"### Verification Failed\n\n- {exc}", empty_preview_html(), json_text
|
| 2659 |
|
| 2660 |
|
| 2661 |
def submit_solution(
|
| 2662 |
json_text: str,
|
| 2663 |
-
json_file: str | None,
|
| 2664 |
submitter: str,
|
| 2665 |
notes: str,
|
| 2666 |
source_url: str,
|
| 2667 |
-
tolerance: float,
|
| 2668 |
):
|
| 2669 |
try:
|
| 2670 |
-
solution = normalize_solution(load_submission(json_text
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2671 |
record, result = STORE.append_verified_submission(
|
| 2672 |
solution,
|
| 2673 |
submitter=submitter,
|
| 2674 |
notes=notes,
|
| 2675 |
source_url=source_url,
|
| 2676 |
-
tolerance=
|
| 2677 |
)
|
| 2678 |
stored = STORE.solution_for_record(record)
|
| 2679 |
pretty = json.dumps(stored, indent=2, sort_keys=True)
|
| 2680 |
sync_status = record.get("sync_status", "dataset sync disabled")
|
| 2681 |
-
message = result_markdown(result) + f"\n\nSaved as record `{record['id']}`.\n\nDataset sync: `{sync_status}`."
|
| 2682 |
clear_page_caches()
|
| 2683 |
return (
|
| 2684 |
message,
|
|
@@ -2718,24 +2778,23 @@ with gr.Blocks(
|
|
| 2718 |
with gr.Row(elem_classes=["submit-layout"]):
|
| 2719 |
with gr.Column(scale=7, elem_classes=["submit-editor"]):
|
| 2720 |
with gr.Group(elem_classes=["submit-panel"]):
|
| 2721 |
-
gr.HTML('<div class="submit-panel-title">
|
| 2722 |
-
gr.HTML('<p class="submit-panel-note">Paste JSON
|
| 2723 |
json_code = gr.Textbox(
|
| 2724 |
**supported_gradio_kwargs(
|
| 2725 |
gr.Textbox,
|
| 2726 |
value=SAMPLE_JSON,
|
| 2727 |
label="Coordinate JSON",
|
| 2728 |
-
lines=
|
| 2729 |
max_lines=30,
|
| 2730 |
show_copy_button=True,
|
| 2731 |
)
|
| 2732 |
)
|
| 2733 |
-
json_file = gr.File(label="Upload .json", file_types=[".json"], type="filepath")
|
| 2734 |
|
| 2735 |
with gr.Column(scale=5, elem_classes=["submit-meta"]):
|
| 2736 |
with gr.Group(elem_classes=["submit-panel"]):
|
| 2737 |
-
gr.HTML('<div class="submit-panel-title">
|
| 2738 |
-
gr.HTML('<p class="submit-panel-note">
|
| 2739 |
submitter = gr.Dropdown(
|
| 2740 |
**supported_gradio_kwargs(
|
| 2741 |
gr.Dropdown,
|
|
@@ -2747,9 +2806,8 @@ with gr.Blocks(
|
|
| 2747 |
info="Start typing to search existing authors. New names are accepted.",
|
| 2748 |
)
|
| 2749 |
)
|
| 2750 |
-
source_url = gr.Textbox(label="Source URL", placeholder="Optional
|
| 2751 |
-
notes = gr.Textbox(label="Notes", lines=3, placeholder="Optional method
|
| 2752 |
-
tolerance = gr.Number(value=DEFAULT_TOLERANCE, label="Verification tolerance")
|
| 2753 |
with gr.Row(elem_classes=["submit-actions"]):
|
| 2754 |
verify_btn = gr.Button("Verify", variant="secondary")
|
| 2755 |
submit_btn = gr.Button("Submit Entry", variant="primary")
|
|
@@ -2776,16 +2834,14 @@ with gr.Blocks(
|
|
| 2776 |
)
|
| 2777 |
|
| 2778 |
with gr.Group(elem_classes=["submit-docs"]):
|
| 2779 |
-
with gr.Accordion("
|
| 2780 |
-
gr.Markdown(EXAMPLES_MD)
|
| 2781 |
-
with gr.Accordion("Schema And Coordinate Rules", open=False):
|
| 2782 |
-
gr.Markdown(SCHEMA_DOCS_MD)
|
| 2783 |
|
| 2784 |
demo.load(home_page_for_request, outputs=[browse_page], show_progress="hidden")
|
| 2785 |
-
verify_btn.click(verify_only, inputs=[json_code
|
| 2786 |
submit_btn.click(
|
| 2787 |
submit_solution,
|
| 2788 |
-
inputs=[json_code,
|
| 2789 |
outputs=[report, preview, normalized_json, leaderboard_page, submitter],
|
| 2790 |
)
|
| 2791 |
|
|
|
|
| 32 |
FAVICON_PATH = SPACE_ROOT / "assets" / "favicon.svg"
|
| 33 |
HYDRATE_STATUS = maybe_hydrate_from_dataset(DATA_DIR)
|
| 34 |
STORE = SolutionStore(DATA_DIR)
|
| 35 |
+
SUBMISSION_MIN_IMPROVEMENT = 1.0e-4
|
| 36 |
|
| 37 |
|
| 38 |
@lru_cache(maxsize=1)
|
|
|
|
| 1541 |
- touching is legal; overlap and protrusion are rejected
|
| 1542 |
"""
|
| 1543 |
|
| 1544 |
+
EXAMPLES_MD = f"""### How Submission Works
|
| 1545 |
+
|
| 1546 |
+
Paste one canonical coordinate JSON object, click **Verify**, then click **Submit Entry** if it passes.
|
| 1547 |
+
|
| 1548 |
+
The geometry tolerance is fixed by the evaluator and is not user-selectable. For an existing case, a submission must improve the current top metric by at least `0.0001` to be accepted as a new record. In these tables, smaller metric values are better.
|
| 1549 |
+
|
| 1550 |
+
### Minimal Triangle Example
|
| 1551 |
|
| 1552 |
This is the smallest legal record and a good shape for checking the schema:
|
| 1553 |
|
|
|
|
| 1561 |
- Use container-centered coordinates.
|
| 1562 |
- Use radians for `rotation_radians`.
|
| 1563 |
- Click **Verify** before submitting; the preview is rendered from the same canonical JSON that is archived.
|
| 1564 |
+
- Existing cases must beat the current top metric by at least `0.0001`.
|
| 1565 |
"""
|
| 1566 |
|
| 1567 |
SCHEMA_DOCS_MD = """### Canonical Fields
|
|
|
|
| 1583 |
- `circle`: `radius` or `diameter`
|
| 1584 |
- `rectangle`: `width` and `height`
|
| 1585 |
|
| 1586 |
+
The verifier uses a fixed geometry tolerance. Existing cases must improve the current top metric by at least `0.0001` before they can be submitted as records.
|
| 1587 |
""" + f"""
|
| 1588 |
|
| 1589 |
### Four Circles In A Square
|
|
|
|
| 2579 |
<div>
|
| 2580 |
<h2>Submit Entry</h2>
|
| 2581 |
<p class="section-note">
|
| 2582 |
+
Paste one canonical coordinate JSON object. Existing cases must beat the current top metric by at least 0.0001.
|
| 2583 |
</p>
|
| 2584 |
</div>
|
| 2585 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2586 |
</div>
|
| 2587 |
</section>
|
| 2588 |
</main>
|
| 2589 |
"""
|
| 2590 |
|
| 2591 |
|
| 2592 |
+
def load_submission(json_text: str) -> dict[str, Any]:
|
|
|
|
|
|
|
| 2593 |
if not json_text or not json_text.strip():
|
| 2594 |
+
raise ValueError("Paste canonical coordinate JSON.")
|
| 2595 |
return load_solution_json(json_text)
|
| 2596 |
|
| 2597 |
|
| 2598 |
+
def record_metric_float(record: dict[str, Any]) -> float | None:
|
| 2599 |
+
value = record.get("metric_value")
|
| 2600 |
+
if value is None:
|
| 2601 |
+
value = record.get("side")
|
| 2602 |
+
try:
|
| 2603 |
+
metric = float(value)
|
| 2604 |
+
except (TypeError, ValueError):
|
| 2605 |
+
return None
|
| 2606 |
+
return metric if math.isfinite(metric) else None
|
| 2607 |
+
|
| 2608 |
+
|
| 2609 |
+
def current_top_for_case(case: str) -> dict[str, Any] | None:
|
| 2610 |
+
candidates = [record for record in cached_public_records("", False) if str(record.get("case")) == case]
|
| 2611 |
+
candidates = [record for record in candidates if record_metric_float(record) is not None]
|
| 2612 |
+
if not candidates:
|
| 2613 |
+
return None
|
| 2614 |
+
return min(candidates, key=lambda record: record_metric_float(record) or float("inf"))
|
| 2615 |
+
|
| 2616 |
+
|
| 2617 |
+
def submission_gate(result: dict[str, Any]) -> tuple[bool, str]:
|
| 2618 |
+
if not result.get("ok"):
|
| 2619 |
+
return False, "Geometry verification failed."
|
| 2620 |
+
submitted = result.get("side")
|
| 2621 |
+
try:
|
| 2622 |
+
submitted_metric = float(submitted)
|
| 2623 |
+
except (TypeError, ValueError):
|
| 2624 |
+
return False, "The evaluator could not compute a submitted metric."
|
| 2625 |
+
if not math.isfinite(submitted_metric):
|
| 2626 |
+
return False, "The evaluator returned a non-finite metric."
|
| 2627 |
+
|
| 2628 |
+
current = current_top_for_case(str(result.get("case") or ""))
|
| 2629 |
+
if current is None:
|
| 2630 |
+
return True, "No current top record exists for this case; a valid geometry can be submitted."
|
| 2631 |
+
|
| 2632 |
+
current_metric = record_metric_float(current)
|
| 2633 |
+
if current_metric is None:
|
| 2634 |
+
return True, "No comparable current top metric exists; a valid geometry can be submitted."
|
| 2635 |
+
|
| 2636 |
+
improvement = current_metric - submitted_metric
|
| 2637 |
+
if improvement >= SUBMISSION_MIN_IMPROVEMENT:
|
| 2638 |
return (
|
| 2639 |
+
True,
|
| 2640 |
+
f"Improves the current top metric by {improvement:.10f}; required improvement is at least {SUBMISSION_MIN_IMPROVEMENT:.4f}.",
|
| 2641 |
+
)
|
| 2642 |
+
return (
|
| 2643 |
+
False,
|
| 2644 |
+
f"Current top metric is {current_metric:.10f}. Submitted metric is {submitted_metric:.10f}. "
|
| 2645 |
+
f"Improvement is {improvement:.10f}; required improvement is at least {SUBMISSION_MIN_IMPROVEMENT:.4f}.",
|
| 2646 |
+
)
|
| 2647 |
+
|
| 2648 |
+
|
| 2649 |
+
def result_markdown(result: dict[str, Any], gate: tuple[bool, str] | None = None) -> str:
|
| 2650 |
+
if result["ok"]:
|
| 2651 |
+
text = (
|
| 2652 |
"### Verification Passed\n\n"
|
| 2653 |
f"- Case: `{result['case']}`\n"
|
| 2654 |
f"- Items: `{result['n']}`\n"
|
|
|
|
| 2657 |
f"- Max boundary excess: `{result['max_boundary_excess']:.3e}`\n"
|
| 2658 |
f"- Max pair overlap depth: `{result['max_pair_overlap_depth']:.3e}`\n"
|
| 2659 |
)
|
| 2660 |
+
if gate is not None:
|
| 2661 |
+
passed, message = gate
|
| 2662 |
+
status = "Passed" if passed else "Needs improvement"
|
| 2663 |
+
text += f"\n### Record Gate\n\n- Status: `{status}`\n- {message}\n"
|
| 2664 |
+
return text
|
| 2665 |
errors = "\n".join(f"- {e}" for e in result["errors"])
|
| 2666 |
return "### Verification Failed\n\n" + errors
|
| 2667 |
|
| 2668 |
|
| 2669 |
def empty_report_markdown() -> str:
|
| 2670 |
+
return "### Ready To Verify\n\nPaste canonical JSON, then run the verifier. Existing cases must improve the current top metric by at least `0.0001`."
|
| 2671 |
|
| 2672 |
|
| 2673 |
def empty_preview_html() -> str:
|
|
|
|
| 2704 |
"""
|
| 2705 |
|
| 2706 |
|
| 2707 |
+
def verify_only(json_text: str):
|
| 2708 |
try:
|
| 2709 |
+
solution = normalize_solution(load_submission(json_text))
|
| 2710 |
+
result = verify_solution(solution, tolerance=DEFAULT_TOLERANCE).as_dict()
|
| 2711 |
+
gate = submission_gate(result) if result["ok"] else None
|
| 2712 |
pretty = json.dumps(solution, indent=2, sort_keys=True)
|
| 2713 |
preview = preview_html(solution) if result["ok"] else ""
|
| 2714 |
+
return result_markdown(result, gate), preview, pretty
|
| 2715 |
except Exception as exc:
|
| 2716 |
return f"### Verification Failed\n\n- {exc}", empty_preview_html(), json_text
|
| 2717 |
|
| 2718 |
|
| 2719 |
def submit_solution(
|
| 2720 |
json_text: str,
|
|
|
|
| 2721 |
submitter: str,
|
| 2722 |
notes: str,
|
| 2723 |
source_url: str,
|
|
|
|
| 2724 |
):
|
| 2725 |
try:
|
| 2726 |
+
solution = normalize_solution(load_submission(json_text))
|
| 2727 |
+
preflight = verify_solution(solution, tolerance=DEFAULT_TOLERANCE).as_dict()
|
| 2728 |
+
gate_ok, gate_message = submission_gate(preflight)
|
| 2729 |
+
if not gate_ok:
|
| 2730 |
+
return result_markdown(preflight, (gate_ok, gate_message)), preview_html(solution), json.dumps(solution, indent=2, sort_keys=True), gr.update(), gr.update()
|
| 2731 |
record, result = STORE.append_verified_submission(
|
| 2732 |
solution,
|
| 2733 |
submitter=submitter,
|
| 2734 |
notes=notes,
|
| 2735 |
source_url=source_url,
|
| 2736 |
+
tolerance=DEFAULT_TOLERANCE,
|
| 2737 |
)
|
| 2738 |
stored = STORE.solution_for_record(record)
|
| 2739 |
pretty = json.dumps(stored, indent=2, sort_keys=True)
|
| 2740 |
sync_status = record.get("sync_status", "dataset sync disabled")
|
| 2741 |
+
message = result_markdown(result, (gate_ok, gate_message)) + f"\n\nSaved as record `{record['id']}`.\n\nDataset sync: `{sync_status}`."
|
| 2742 |
clear_page_caches()
|
| 2743 |
return (
|
| 2744 |
message,
|
|
|
|
| 2778 |
with gr.Row(elem_classes=["submit-layout"]):
|
| 2779 |
with gr.Column(scale=7, elem_classes=["submit-editor"]):
|
| 2780 |
with gr.Group(elem_classes=["submit-panel"]):
|
| 2781 |
+
gr.HTML('<div class="submit-panel-title">Paste JSON</div>')
|
| 2782 |
+
gr.HTML('<p class="submit-panel-note">Paste one canonical coordinate JSON object. The evaluator uses a fixed tolerance and requires a 0.0001 metric improvement for existing cases.</p>')
|
| 2783 |
json_code = gr.Textbox(
|
| 2784 |
**supported_gradio_kwargs(
|
| 2785 |
gr.Textbox,
|
| 2786 |
value=SAMPLE_JSON,
|
| 2787 |
label="Coordinate JSON",
|
| 2788 |
+
lines=18,
|
| 2789 |
max_lines=30,
|
| 2790 |
show_copy_button=True,
|
| 2791 |
)
|
| 2792 |
)
|
|
|
|
| 2793 |
|
| 2794 |
with gr.Column(scale=5, elem_classes=["submit-meta"]):
|
| 2795 |
with gr.Group(elem_classes=["submit-panel"]):
|
| 2796 |
+
gr.HTML('<div class="submit-panel-title">Author</div>')
|
| 2797 |
+
gr.HTML('<p class="submit-panel-note">Choose an existing credited author or type a new name.</p>')
|
| 2798 |
submitter = gr.Dropdown(
|
| 2799 |
**supported_gradio_kwargs(
|
| 2800 |
gr.Dropdown,
|
|
|
|
| 2806 |
info="Start typing to search existing authors. New names are accepted.",
|
| 2807 |
)
|
| 2808 |
)
|
| 2809 |
+
source_url = gr.Textbox(label="Source URL", placeholder="Optional")
|
| 2810 |
+
notes = gr.Textbox(label="Notes", lines=3, placeholder="Optional method or provenance note")
|
|
|
|
| 2811 |
with gr.Row(elem_classes=["submit-actions"]):
|
| 2812 |
verify_btn = gr.Button("Verify", variant="secondary")
|
| 2813 |
submit_btn = gr.Button("Submit Entry", variant="primary")
|
|
|
|
| 2834 |
)
|
| 2835 |
|
| 2836 |
with gr.Group(elem_classes=["submit-docs"]):
|
| 2837 |
+
with gr.Accordion("Submission Documentation", open=False):
|
| 2838 |
+
gr.Markdown(EXAMPLES_MD + "\n\n" + SCHEMA_DOCS_MD)
|
|
|
|
|
|
|
| 2839 |
|
| 2840 |
demo.load(home_page_for_request, outputs=[browse_page], show_progress="hidden")
|
| 2841 |
+
verify_btn.click(verify_only, inputs=[json_code], outputs=[report, preview, normalized_json])
|
| 2842 |
submit_btn.click(
|
| 2843 |
submit_solution,
|
| 2844 |
+
inputs=[json_code, submitter, notes, source_url],
|
| 2845 |
outputs=[report, preview, normalized_json, leaderboard_page, submitter],
|
| 2846 |
)
|
| 2847 |
|