Spaces:
Running on Zero
Running on Zero
UI: pin verdict at top with placeholder; auto-delete source+intermediates after audit (opt-out checkbox)
Browse files- legal_doc_redteam/zerogpu_gui.py +108 -24
legal_doc_redteam/zerogpu_gui.py
CHANGED
|
@@ -118,6 +118,19 @@ def bind_vlm_fn(vlm_fn: Callable[[Any, str], str], *, model_id: str) -> None:
|
|
| 118 |
_BOUND_VLM_MODEL_ID = model_id
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
def run_full_audit(
|
| 122 |
file_path: str | None,
|
| 123 |
dpi: int | float,
|
|
@@ -129,11 +142,12 @@ def run_full_audit(
|
|
| 129 |
reviewer_model_id: str,
|
| 130 |
reasoning_effort: str,
|
| 131 |
hf_token: str,
|
|
|
|
| 132 |
progress: gr.Progress = gr.Progress(track_tqdm=False),
|
| 133 |
) -> tuple[str, list[list[str]], list[list[str | int | float]], dict[str, Any], str | None]:
|
| 134 |
if not file_path:
|
| 135 |
return (
|
| 136 |
-
|
| 137 |
[],
|
| 138 |
[],
|
| 139 |
{},
|
|
@@ -223,18 +237,60 @@ def run_full_audit(
|
|
| 223 |
"countermeasures": audit_report,
|
| 224 |
"ocr_integrity": ocr_report,
|
| 225 |
}
|
| 226 |
-
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
return (
|
| 230 |
-
|
| 231 |
controls_as_rows(audit_report),
|
| 232 |
report_table_rows(ocr_report),
|
| 233 |
combined_report,
|
| 234 |
-
|
| 235 |
)
|
| 236 |
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
def _runs_base_dir() -> Path:
|
| 239 |
override = os.environ.get("LEGAL_DOC_REDTEAM_RUNS_DIR")
|
| 240 |
if override:
|
|
@@ -256,9 +312,9 @@ def _allocate_work_dir() -> Path:
|
|
| 256 |
|
| 257 |
|
| 258 |
def cleanup_old_runs(retention_hours: int = DEFAULT_RUN_RETENTION_HOURS) -> int:
|
| 259 |
-
"""Prune per-run audit directories
|
| 260 |
|
| 261 |
-
Returns the number of
|
| 262 |
long-running public Space does not accrete data forever.
|
| 263 |
"""
|
| 264 |
|
|
@@ -268,14 +324,22 @@ def cleanup_old_runs(retention_hours: int = DEFAULT_RUN_RETENTION_HOURS) -> int:
|
|
| 268 |
cutoff = time.time() - max(0, retention_hours) * 3600
|
| 269 |
removed = 0
|
| 270 |
for entry in base.iterdir():
|
| 271 |
-
if not entry.is_dir() or not entry.name.startswith("audit_"):
|
| 272 |
-
continue
|
| 273 |
try:
|
| 274 |
-
if entry.
|
| 275 |
-
|
| 276 |
-
|
|
|
|
| 277 |
except OSError:
|
| 278 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
return removed
|
| 280 |
|
| 281 |
|
|
@@ -336,6 +400,15 @@ def build_app(
|
|
| 336 |
f"_Public Space safeguards: uploads are capped at {DEFAULT_MAX_UPLOAD_MB} MB, "
|
| 337 |
f"per-run audit data is pruned after {DEFAULT_RUN_RETENTION_HOURS} h._"
|
| 338 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
with gr.Row():
|
| 340 |
with gr.Column(scale=2):
|
| 341 |
file_in = gr.File(
|
|
@@ -354,12 +427,19 @@ def build_app(
|
|
| 354 |
".text",
|
| 355 |
],
|
| 356 |
)
|
| 357 |
-
run_btn = gr.Button("Audit document", variant="primary")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
with gr.Column(scale=1):
|
| 359 |
dpi = gr.Number(value=180, precision=0, label="Render DPI")
|
| 360 |
max_pages = gr.Number(value=8, precision=0, label="Max pages")
|
| 361 |
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
| 363 |
cpu_engines = gr.CheckboxGroup(
|
| 364 |
choices=CPU_OCR_ENGINES,
|
| 365 |
value=cpu_defaults,
|
|
@@ -370,7 +450,7 @@ def build_app(
|
|
| 370 |
"`tesseract` system binary; the Space includes it via `packages.txt`._"
|
| 371 |
)
|
| 372 |
|
| 373 |
-
with gr.Accordion("Vision LLM OCR (GPU)", open=
|
| 374 |
vlm_backend = gr.Radio(
|
| 375 |
choices=["none", "local_transformers", "hf_inference"],
|
| 376 |
value=default_vlm_backend,
|
|
@@ -388,7 +468,7 @@ def build_app(
|
|
| 388 |
"handles hard PDFs better. `PaddlePaddle/PaddleOCR-VL` is the most compact._"
|
| 389 |
)
|
| 390 |
|
| 391 |
-
with gr.Accordion("Reasoning verdict", open=
|
| 392 |
reviewer = gr.Radio(
|
| 393 |
choices=["deterministic", "local_transformers", "hf_inference"],
|
| 394 |
value=default_reviewer_backend,
|
|
@@ -404,14 +484,17 @@ def build_app(
|
|
| 404 |
label="Reasoning effort",
|
| 405 |
)
|
| 406 |
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
|
|
|
| 413 |
|
| 414 |
-
|
|
|
|
|
|
|
| 415 |
with gr.Accordion("Countermeasures detector matrix", open=False):
|
| 416 |
audit_table = gr.Dataframe(
|
| 417 |
headers=CONTROL_HEADERS,
|
|
@@ -426,7 +509,7 @@ def build_app(
|
|
| 426 |
)
|
| 427 |
with gr.Accordion("Full JSON report", open=False):
|
| 428 |
json_out = gr.JSON()
|
| 429 |
-
verdict_file = gr.File(label="Verdict markdown", interactive=False)
|
| 430 |
|
| 431 |
run_btn.click(
|
| 432 |
run_full_audit,
|
|
@@ -441,6 +524,7 @@ def build_app(
|
|
| 441 |
reasoning_model,
|
| 442 |
reasoning_effort,
|
| 443 |
hf_token,
|
|
|
|
| 444 |
],
|
| 445 |
outputs=[verdict_md, audit_table, ocr_table, json_out, verdict_file],
|
| 446 |
)
|
|
|
|
| 118 |
_BOUND_VLM_MODEL_ID = model_id
|
| 119 |
|
| 120 |
|
| 121 |
+
_VERDICT_PLACEHOLDER = (
|
| 122 |
+
"## Upload a document and click **Audit document** to begin.\n\n"
|
| 123 |
+
"_The verdict will appear right here as soon as the audit finishes._"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
_CLEANUP_NOTICE = (
|
| 127 |
+
"\n\n---\n"
|
| 128 |
+
"_🔒 Source file and intermediate artefacts (rendered page images, "
|
| 129 |
+
"converted PDFs, temporary JSON) have been deleted from the server. "
|
| 130 |
+
"Only this verdict markdown and the download below remain._"
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
def run_full_audit(
|
| 135 |
file_path: str | None,
|
| 136 |
dpi: int | float,
|
|
|
|
| 142 |
reviewer_model_id: str,
|
| 143 |
reasoning_effort: str,
|
| 144 |
hf_token: str,
|
| 145 |
+
cleanup_after_audit: bool = True,
|
| 146 |
progress: gr.Progress = gr.Progress(track_tqdm=False),
|
| 147 |
) -> tuple[str, list[list[str]], list[list[str | int | float]], dict[str, Any], str | None]:
|
| 148 |
if not file_path:
|
| 149 |
return (
|
| 150 |
+
_VERDICT_PLACEHOLDER,
|
| 151 |
[],
|
| 152 |
[],
|
| 153 |
{},
|
|
|
|
| 237 |
"countermeasures": audit_report,
|
| 238 |
"ocr_integrity": ocr_report,
|
| 239 |
}
|
| 240 |
+
verdict_text = render_markdown(summary)
|
| 241 |
+
|
| 242 |
+
if cleanup_after_audit:
|
| 243 |
+
download_path = _persist_verdict_for_download(verdict_text)
|
| 244 |
+
_scrub_audit_artifacts(work_dir=work_dir, source_path=source)
|
| 245 |
+
verdict_text = verdict_text + _CLEANUP_NOTICE
|
| 246 |
+
verdict_path: str | None = str(download_path) if download_path else None
|
| 247 |
+
else:
|
| 248 |
+
md_path = work_dir / "integrity_verdict.md"
|
| 249 |
+
md_path.write_text(verdict_text, encoding="utf-8")
|
| 250 |
+
verdict_path = str(md_path)
|
| 251 |
|
| 252 |
return (
|
| 253 |
+
verdict_text,
|
| 254 |
controls_as_rows(audit_report),
|
| 255 |
report_table_rows(ocr_report),
|
| 256 |
combined_report,
|
| 257 |
+
verdict_path,
|
| 258 |
)
|
| 259 |
|
| 260 |
|
| 261 |
+
def _downloads_dir() -> Path:
|
| 262 |
+
base = _runs_base_dir() / "downloads"
|
| 263 |
+
base.mkdir(parents=True, exist_ok=True)
|
| 264 |
+
return base
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def _persist_verdict_for_download(verdict_text: str) -> Path | None:
|
| 268 |
+
"""Write the verdict markdown to a separate downloads dir so the work
|
| 269 |
+
dir can be wiped while the Download File component still resolves."""
|
| 270 |
+
|
| 271 |
+
try:
|
| 272 |
+
out_path = _downloads_dir() / f"verdict_{uuid.uuid4().hex[:10]}.md"
|
| 273 |
+
out_path.write_text(verdict_text, encoding="utf-8")
|
| 274 |
+
return out_path
|
| 275 |
+
except OSError:
|
| 276 |
+
return None
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def _scrub_audit_artifacts(*, work_dir: Path, source_path: Path) -> None:
|
| 280 |
+
"""Best-effort delete of the per-run work dir and the Gradio upload."""
|
| 281 |
+
|
| 282 |
+
try:
|
| 283 |
+
if work_dir.exists():
|
| 284 |
+
shutil.rmtree(work_dir, ignore_errors=True)
|
| 285 |
+
except Exception:
|
| 286 |
+
pass
|
| 287 |
+
try:
|
| 288 |
+
if source_path.exists():
|
| 289 |
+
Path(source_path).unlink(missing_ok=True)
|
| 290 |
+
except Exception:
|
| 291 |
+
pass
|
| 292 |
+
|
| 293 |
+
|
| 294 |
def _runs_base_dir() -> Path:
|
| 295 |
override = os.environ.get("LEGAL_DOC_REDTEAM_RUNS_DIR")
|
| 296 |
if override:
|
|
|
|
| 312 |
|
| 313 |
|
| 314 |
def cleanup_old_runs(retention_hours: int = DEFAULT_RUN_RETENTION_HOURS) -> int:
|
| 315 |
+
"""Prune per-run audit directories and stale verdict downloads.
|
| 316 |
|
| 317 |
+
Returns the number of items removed. Intended for startup, so a
|
| 318 |
long-running public Space does not accrete data forever.
|
| 319 |
"""
|
| 320 |
|
|
|
|
| 324 |
cutoff = time.time() - max(0, retention_hours) * 3600
|
| 325 |
removed = 0
|
| 326 |
for entry in base.iterdir():
|
|
|
|
|
|
|
| 327 |
try:
|
| 328 |
+
if entry.is_dir() and entry.name.startswith("audit_"):
|
| 329 |
+
if entry.stat().st_mtime < cutoff:
|
| 330 |
+
shutil.rmtree(entry, ignore_errors=True)
|
| 331 |
+
removed += 1
|
| 332 |
except OSError:
|
| 333 |
continue
|
| 334 |
+
downloads = base / "downloads"
|
| 335 |
+
if downloads.exists():
|
| 336 |
+
for entry in downloads.iterdir():
|
| 337 |
+
try:
|
| 338 |
+
if entry.is_file() and entry.stat().st_mtime < cutoff:
|
| 339 |
+
entry.unlink(missing_ok=True)
|
| 340 |
+
removed += 1
|
| 341 |
+
except OSError:
|
| 342 |
+
continue
|
| 343 |
return removed
|
| 344 |
|
| 345 |
|
|
|
|
| 400 |
f"_Public Space safeguards: uploads are capped at {DEFAULT_MAX_UPLOAD_MB} MB, "
|
| 401 |
f"per-run audit data is pruned after {DEFAULT_RUN_RETENTION_HOURS} h._"
|
| 402 |
)
|
| 403 |
+
|
| 404 |
+
# ----------------------------------------------------------------
|
| 405 |
+
# B1: verdict pinned at the top, gets populated in place after Audit
|
| 406 |
+
# ----------------------------------------------------------------
|
| 407 |
+
verdict_md = gr.Markdown(value=_VERDICT_PLACEHOLDER)
|
| 408 |
+
|
| 409 |
+
# ----------------------------------------------------------------
|
| 410 |
+
# Inputs row (always visible)
|
| 411 |
+
# ----------------------------------------------------------------
|
| 412 |
with gr.Row():
|
| 413 |
with gr.Column(scale=2):
|
| 414 |
file_in = gr.File(
|
|
|
|
| 427 |
".text",
|
| 428 |
],
|
| 429 |
)
|
| 430 |
+
run_btn = gr.Button("Audit document", variant="primary", size="lg")
|
| 431 |
+
cleanup_after = gr.Checkbox(
|
| 432 |
+
value=True,
|
| 433 |
+
label="Delete uploaded file and intermediate artefacts after the report is generated (recommended)",
|
| 434 |
+
)
|
| 435 |
with gr.Column(scale=1):
|
| 436 |
dpi = gr.Number(value=180, precision=0, label="Render DPI")
|
| 437 |
max_pages = gr.Number(value=8, precision=0, label="Max pages")
|
| 438 |
|
| 439 |
+
# ----------------------------------------------------------------
|
| 440 |
+
# Configuration accordions — collapsed by default per B1
|
| 441 |
+
# ----------------------------------------------------------------
|
| 442 |
+
with gr.Accordion("CPU OCR engines", open=False):
|
| 443 |
cpu_engines = gr.CheckboxGroup(
|
| 444 |
choices=CPU_OCR_ENGINES,
|
| 445 |
value=cpu_defaults,
|
|
|
|
| 450 |
"`tesseract` system binary; the Space includes it via `packages.txt`._"
|
| 451 |
)
|
| 452 |
|
| 453 |
+
with gr.Accordion("Vision LLM OCR (GPU)", open=False):
|
| 454 |
vlm_backend = gr.Radio(
|
| 455 |
choices=["none", "local_transformers", "hf_inference"],
|
| 456 |
value=default_vlm_backend,
|
|
|
|
| 468 |
"handles hard PDFs better. `PaddlePaddle/PaddleOCR-VL` is the most compact._"
|
| 469 |
)
|
| 470 |
|
| 471 |
+
with gr.Accordion("Reasoning verdict", open=False):
|
| 472 |
reviewer = gr.Radio(
|
| 473 |
choices=["deterministic", "local_transformers", "hf_inference"],
|
| 474 |
value=default_reviewer_backend,
|
|
|
|
| 484 |
label="Reasoning effort",
|
| 485 |
)
|
| 486 |
|
| 487 |
+
with gr.Accordion("Optional HF token (only for hf_inference)", open=False):
|
| 488 |
+
hf_token = gr.Textbox(
|
| 489 |
+
value="",
|
| 490 |
+
label="HF token (leave empty to use the Space's own HF_TOKEN secret)",
|
| 491 |
+
type="password",
|
| 492 |
+
visible=expose_hf_token,
|
| 493 |
+
)
|
| 494 |
|
| 495 |
+
# ----------------------------------------------------------------
|
| 496 |
+
# Detail accordions — collapsed by default
|
| 497 |
+
# ----------------------------------------------------------------
|
| 498 |
with gr.Accordion("Countermeasures detector matrix", open=False):
|
| 499 |
audit_table = gr.Dataframe(
|
| 500 |
headers=CONTROL_HEADERS,
|
|
|
|
| 509 |
)
|
| 510 |
with gr.Accordion("Full JSON report", open=False):
|
| 511 |
json_out = gr.JSON()
|
| 512 |
+
verdict_file = gr.File(label="Verdict markdown (download)", interactive=False)
|
| 513 |
|
| 514 |
run_btn.click(
|
| 515 |
run_full_audit,
|
|
|
|
| 524 |
reasoning_model,
|
| 525 |
reasoning_effort,
|
| 526 |
hf_token,
|
| 527 |
+
cleanup_after,
|
| 528 |
],
|
| 529 |
outputs=[verdict_md, audit_table, ocr_table, json_out, verdict_file],
|
| 530 |
)
|