cronos3k commited on
Commit
a218fb4
·
verified ·
1 Parent(s): 681b5fb

UI: pin verdict at top with placeholder; auto-delete source+intermediates after audit (opt-out checkbox)

Browse files
Files changed (1) hide show
  1. 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
- "Upload a PDF, DOCX, HTML, Markdown, or text file to begin.",
137
  [],
138
  [],
139
  {},
@@ -223,18 +237,60 @@ def run_full_audit(
223
  "countermeasures": audit_report,
224
  "ocr_integrity": ocr_report,
225
  }
226
- md_path = work_dir / "integrity_verdict.md"
227
- md_path.write_text(render_markdown(summary), encoding="utf-8")
 
 
 
 
 
 
 
 
 
228
 
229
  return (
230
- render_markdown(summary),
231
  controls_as_rows(audit_report),
232
  report_table_rows(ocr_report),
233
  combined_report,
234
- str(md_path),
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 older than ``retention_hours``.
260
 
261
- Returns the number of directories removed. Intended for startup, so a
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.stat().st_mtime < cutoff:
275
- shutil.rmtree(entry, ignore_errors=True)
276
- removed += 1
 
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
- with gr.Accordion("CPU OCR engines", open=True):
 
 
 
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=True):
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=True):
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
- hf_token = gr.Textbox(
408
- value="",
409
- label="HF token (optional, used for hf_inference backends)",
410
- type="password",
411
- visible=expose_hf_token,
412
- )
 
413
 
414
- verdict_md = gr.Markdown()
 
 
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
  )