solidprivacy commited on
Commit
7f793ca
·
verified ·
1 Parent(s): bab06f4

Commit message: Add editable replacement table

Browse files
Files changed (1) hide show
  1. presidio_streamlit.py +125 -12
presidio_streamlit.py CHANGED
@@ -357,22 +357,135 @@ try:
357
  label="De-identified", value=st_anonymize_results.text, height=400
358
  )
359
  # Build stable placeholder exports
 
360
  replacements, report_rows = build_placeholder_replacements(
361
  st_text,
362
  st_analyze_results,
363
  )
364
 
365
- export_text = apply_replacements_to_text(st_text, replacements)
366
-
367
  st.divider()
368
- st.subheader("Export anonymized files")
369
 
370
  st.caption(
371
- "The exports below use stable placeholders such as [PERSON_01] and [LOCATION_01]. "
372
- "For Word uploads, the .docx export tries to preserve the original document structure and styling."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  )
374
 
375
- # Debug info, useful while testing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  if uploaded_file is not None:
377
  st.info(f"Uploaded file detected for export: {uploaded_file.name}")
378
  else:
@@ -387,11 +500,11 @@ try:
387
  key="download_txt",
388
  )
389
 
390
- # CSV replacement report
391
  st.download_button(
392
- label="Download replacement report (.csv)",
393
- data=replacement_report_csv(report_rows),
394
- file_name="replacement_report.csv",
395
  mime="text/csv",
396
  key="download_csv",
397
  )
@@ -399,7 +512,7 @@ try:
399
  # DOCX export
400
  try:
401
  if uploaded_file is not None and uploaded_file.name.lower().endswith(".docx"):
402
- docx_bytes = anonymized_docx_from_original(uploaded_file, replacements)
403
  docx_filename = "anonymized_" + uploaded_file.name
404
  else:
405
  docx_bytes = docx_from_text(export_text)
@@ -427,7 +540,7 @@ try:
427
  )
428
 
429
  except Exception as pdf_error:
430
- st.error(f"Could not create PDF export: {pdf_error}")
431
 
432
 
433
 
 
357
  label="De-identified", value=st_anonymize_results.text, height=400
358
  )
359
  # Build stable placeholder exports
360
+ # Build stable placeholder suggestions from Presidio
361
  replacements, report_rows = build_placeholder_replacements(
362
  st_text,
363
  st_analyze_results,
364
  )
365
 
 
 
366
  st.divider()
367
+ st.subheader("Review replacement table before export")
368
 
369
  st.caption(
370
+ "Untick false positives, change placeholders, or add your own word pairs. "
371
+ "The downloads below are generated from this edited table."
372
+ )
373
+
374
+ # Convert detected replacements into editable table rows
375
+ default_editor_rows = []
376
+
377
+ for row in report_rows:
378
+ default_editor_rows.append(
379
+ {
380
+ "include": True,
381
+ "find": row.get("detected_text", ""),
382
+ "replace_with": row.get("placeholder", ""),
383
+ "entity_type": row.get("entity_type", ""),
384
+ "score": row.get("score", None),
385
+ }
386
+ )
387
+
388
+ # If nothing was detected, still show an empty editable row
389
+ if not default_editor_rows:
390
+ default_editor_rows = [
391
+ {
392
+ "include": True,
393
+ "find": "",
394
+ "replace_with": "",
395
+ "entity_type": "MANUAL",
396
+ "score": None,
397
+ }
398
+ ]
399
+
400
+ replacement_editor_df = pd.DataFrame(default_editor_rows)
401
+
402
+ edited_replacements_df = st.data_editor(
403
+ replacement_editor_df,
404
+ hide_index=True,
405
+ num_rows="dynamic",
406
+ use_container_width=True,
407
+ column_order=["include", "find", "replace_with", "entity_type", "score"],
408
+ column_config={
409
+ "include": st.column_config.CheckboxColumn(
410
+ "Use",
411
+ help="Untick to exclude this replacement from the export.",
412
+ default=True,
413
+ ),
414
+ "find": st.column_config.TextColumn(
415
+ "Find text",
416
+ help="The exact text that should be replaced.",
417
+ ),
418
+ "replace_with": st.column_config.TextColumn(
419
+ "Replace with",
420
+ help="The placeholder to insert.",
421
+ ),
422
+ "entity_type": st.column_config.TextColumn(
423
+ "Entity type",
424
+ help="Presidio entity type or MANUAL.",
425
+ ),
426
+ "score": st.column_config.NumberColumn(
427
+ "Score",
428
+ help="Presidio confidence score, if available.",
429
+ format="%.3f",
430
+ ),
431
+ },
432
+ key="replacement_editor",
433
  )
434
 
435
+ def safe_cell(value):
436
+ if value is None:
437
+ return ""
438
+ try:
439
+ if pd.isna(value):
440
+ return ""
441
+ except Exception:
442
+ pass
443
+ return str(value).strip()
444
+
445
+
446
+ # Build final replacements from edited table
447
+ edited_replacements = {}
448
+ edited_report_rows = []
449
+
450
+ for _, row in edited_replacements_df.iterrows():
451
+ include = bool(row.get("include", False))
452
+ find_text = safe_cell(row.get("find", ""))
453
+ replace_text = safe_cell(row.get("replace_with", ""))
454
+ entity_type = safe_cell(row.get("entity_type", "MANUAL")) or "MANUAL"
455
+ score = row.get("score", None)
456
+
457
+ if not include:
458
+ continue
459
+
460
+ if not find_text or not replace_text:
461
+ continue
462
+
463
+ edited_replacements[find_text] = replace_text
464
+
465
+ edited_report_rows.append(
466
+ {
467
+ "entity_type": entity_type,
468
+ "detected_text": find_text,
469
+ "placeholder": replace_text,
470
+ "score": score if score is not None else "",
471
+ }
472
+ )
473
+
474
+ st.info(f"{len(edited_replacements)} replacement pair(s) will be applied to the exports.")
475
+
476
+ # Apply edited replacements
477
+ export_text = apply_replacements_to_text(st_text, edited_replacements)
478
+
479
+ with st.expander("Preview anonymized text generated from edited table", expanded=False):
480
+ st.text_area(
481
+ label="Preview",
482
+ value=export_text,
483
+ height=300,
484
+ key="edited_export_preview",
485
+ )
486
+
487
+ st.subheader("Export anonymized files")
488
+
489
  if uploaded_file is not None:
490
  st.info(f"Uploaded file detected for export: {uploaded_file.name}")
491
  else:
 
500
  key="download_txt",
501
  )
502
 
503
+ # CSV replacement report / reusable mapping
504
  st.download_button(
505
+ label="Download replacement table (.csv)",
506
+ data=replacement_report_csv(edited_report_rows),
507
+ file_name="replacement_table.csv",
508
  mime="text/csv",
509
  key="download_csv",
510
  )
 
512
  # DOCX export
513
  try:
514
  if uploaded_file is not None and uploaded_file.name.lower().endswith(".docx"):
515
+ docx_bytes = anonymized_docx_from_original(uploaded_file, edited_replacements)
516
  docx_filename = "anonymized_" + uploaded_file.name
517
  else:
518
  docx_bytes = docx_from_text(export_text)
 
540
  )
541
 
542
  except Exception as pdf_error:
543
+ st.error(f"Could not create PDF export: {pdf_error}")
544
 
545
 
546