solidprivacy commited on
Commit
0256326
·
verified ·
1 Parent(s): a59c3b6

Fix remembered replacement table setup bugfix

Browse files
Files changed (1) hide show
  1. presidio_streamlit.py +256 -260
presidio_streamlit.py CHANGED
@@ -363,280 +363,276 @@ try:
363
  st.text_area(
364
  label="De-identified", value=st_anonymize_results.text, height=400
365
  )
366
- # Build stable placeholder exports
367
- # Build stable placeholder suggestions from Presidio
368
- replacements, report_rows = build_placeholder_replacements(
369
- st_text,
370
- st_analyze_results,
371
- )
372
-
373
- st.divider()
374
- st.subheader("Review replacement table before export")
375
-
376
- st.caption(
377
- "Untick false positives, change placeholders, or add your own word pairs. "
378
- "The downloads below are generated from this edited table."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  )
380
-
381
- # Convert detected replacements into editable table rows
382
- default_editor_rows = []
383
-
384
- for row in report_rows:
385
- default_editor_rows.append(
386
- {
387
- "include": True,
388
- "find": row.get("detected_text", ""),
389
- "replace_with": row.get("placeholder", ""),
390
- "entity_type": row.get("entity_type", ""),
391
- "score": row.get("score", None),
392
- }
393
- )
394
-
395
- # If nothing was detected, still show an empty editable row
396
- if not default_editor_rows:
397
- remembered_rows = load_remembered_replacements()
398
-
399
- # Build editable table rows from remembered replacements + Presidio suggestions
400
- remembered_rows = load_remembered_replacements()
401
-
402
- default_editor_rows = []
403
- seen_find_values = set()
404
-
405
- # First load remembered pairs
406
- for row in remembered_rows:
407
- find_text = str(row.get("find", "")).strip()
408
-
409
- if not find_text:
410
- continue
411
-
412
- default_editor_rows.append(
413
- {
414
- "include": row.get("include", True),
415
- "remember": row.get("remember", True),
416
- "find": find_text,
417
- "replace_with": row.get("replace_with", ""),
418
- "entity_type": row.get("entity_type", "REMEMBERED"),
419
- "score": row.get("score", ""),
420
- }
421
- )
422
- seen_find_values.add(find_text)
423
-
424
- # Then add Presidio suggestions
425
- for row in report_rows:
426
- find_text = str(row.get("detected_text", "")).strip()
427
-
428
- if not find_text:
429
- continue
430
-
431
- # Avoid duplicate rows if a remembered replacement already exists
432
- if find_text in seen_find_values:
433
- continue
434
-
435
- default_editor_rows.append(
436
- {
437
- "include": True,
438
- "remember": False,
439
- "find": find_text,
440
- "replace_with": row.get("placeholder", ""),
441
- "entity_type": row.get("entity_type", ""),
442
- "score": row.get("score", None),
443
- }
444
- )
445
-
446
- # If nothing was detected and nothing was remembered, still show an empty editable row
447
- if not default_editor_rows:
448
- default_editor_rows = [
449
- {
450
- "include": True,
451
- "remember": False,
452
- "find": "",
453
- "replace_with": "",
454
- "entity_type": "MANUAL",
455
- "score": None,
456
- }
457
- ]
458
-
459
- replacement_editor_df = pd.DataFrame(default_editor_rows)
460
-
461
- edited_replacements_df = st.data_editor(
462
- replacement_editor_df,
463
- hide_index=True,
464
- num_rows="dynamic",
465
- use_container_width=True,
466
- column_order=["include", "remember", "find", "replace_with", "entity_type", "score"],
467
- column_config={
468
- "remember": st.column_config.CheckboxColumn(
469
- "Remember",
470
- help="Save this replacement pair for future documents/sessions.",
471
- default=False,
472
- ),
473
- "find": st.column_config.TextColumn(
474
- "Find text",
475
- help="The exact text that should be replaced.",
476
- ),
477
- "replace_with": st.column_config.TextColumn(
478
- "Replace with",
479
- help="The placeholder to insert.",
480
- ),
481
- "entity_type": st.column_config.TextColumn(
482
- "Entity type",
483
- help="Presidio entity type or MANUAL.",
484
- ),
485
- "score": st.column_config.NumberColumn(
486
- "Score",
487
- help="Presidio confidence score, if available.",
488
- format="%.3f",
489
- ),
490
- },
491
- key="replacement_editor",
492
  )
493
-
494
- def safe_cell(value):
495
- if value is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  return ""
497
- try:
498
- if pd.isna(value):
499
- return ""
500
- except Exception:
501
- pass
502
- return str(value).strip()
503
-
504
-
505
- # Build final replacements from edited table
506
- edited_replacements = {}
507
- edited_report_rows = []
508
-
509
- for _, row in edited_replacements_df.iterrows():
510
- include = bool(row.get("include", False))
511
- find_text = safe_cell(row.get("find", ""))
512
- replace_text = safe_cell(row.get("replace_with", ""))
513
- entity_type = safe_cell(row.get("entity_type", "MANUAL")) or "MANUAL"
514
- score = row.get("score", None)
515
-
516
- if not include:
517
- continue
518
-
519
- if not find_text or not replace_text:
520
- continue
521
-
522
- edited_replacements[find_text] = replace_text
523
-
524
- edited_report_rows.append(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
  {
 
 
526
  "entity_type": entity_type,
527
- "detected_text": find_text,
528
- "placeholder": replace_text,
529
- "score": score if score is not None else "",
530
  }
531
  )
532
-
533
- st.info(f"{len(edited_replacements)} replacement pair(s) will be applied to the exports.")
534
-
535
- # Apply edited replacements
536
- export_text = apply_replacements_to_text(st_text, edited_replacements)
537
-
538
- with st.expander("Preview anonymized text generated from edited table", expanded=False):
539
- st.text_area(
540
- label="Preview",
541
- value=export_text,
542
- height=300,
543
- key="edited_export_preview",
544
- )
545
-
546
- st.subheader("Export anonymized files")
547
-
548
- if uploaded_file is not None:
549
- st.info(f"Uploaded file detected for export: {uploaded_file.name}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  else:
551
- st.info("No uploaded file detected for export. Exporting from text area only.")
552
-
553
-
554
- remember_rows_to_save = []
555
-
556
- for _, row in edited_replacements_df.iterrows():
557
- include = bool(row.get("include", False))
558
- remember = bool(row.get("remember", False))
559
- find_text = safe_cell(row.get("find", ""))
560
- replace_text = safe_cell(row.get("replace_with", ""))
561
- entity_type = safe_cell(row.get("entity_type", "REMEMBERED")) or "REMEMBERED"
562
-
563
- if include and remember and find_text and replace_text:
564
- remember_rows_to_save.append(
565
- {
566
- "find": find_text,
567
- "replace_with": replace_text,
568
- "entity_type": entity_type,
569
- }
570
- )
571
-
572
- memory_col1, memory_col2 = st.columns(2)
573
-
574
- with memory_col1:
575
- if st.button("Save remembered replacements"):
576
- saved_count = save_remembered_replacements(remember_rows_to_save)
577
- st.success(f"Saved {saved_count} remembered replacement pair(s).")
578
- st.info(f"Memory file: {get_memory_file_path()}")
579
-
580
- with memory_col2:
581
- if st.button("Clear remembered replacements"):
582
- clear_remembered_replacements()
583
- st.warning("Remembered replacements cleared.")
584
-
585
-
586
- # TXT export
587
  st.download_button(
588
- label="Download anonymized text (.txt)",
589
- data=export_text.encode("utf-8"),
590
- file_name="anonymized_text.txt",
591
- mime="text/plain",
592
- key="download_txt",
593
  )
594
-
595
- # CSV replacement report / reusable mapping
 
 
 
 
596
  st.download_button(
597
- label="Download replacement table (.csv)",
598
- data=replacement_report_csv(edited_report_rows),
599
- file_name="replacement_table.csv",
600
- mime="text/csv",
601
- key="download_csv",
602
  )
603
-
604
- # DOCX export
605
- try:
606
- if uploaded_file is not None and uploaded_file.name.lower().endswith(".docx"):
607
- docx_bytes = anonymized_docx_from_original(uploaded_file, edited_replacements)
608
- docx_filename = "anonymized_" + uploaded_file.name
609
- else:
610
- docx_bytes = docx_from_text(export_text)
611
- docx_filename = "anonymized_text.docx"
612
-
613
- st.download_button(
614
- label="Download anonymized Word file (.docx)",
615
- data=docx_bytes,
616
- file_name=docx_filename,
617
- mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
618
- key="download_docx",
619
- )
620
-
621
- except Exception as docx_error:
622
- st.error(f"Could not create DOCX export: {docx_error}")
623
-
624
- # PDF export
625
- try:
626
- st.download_button(
627
- label="Download anonymized PDF (.pdf)",
628
- data=pdf_from_text(export_text),
629
- file_name="anonymized_text.pdf",
630
- mime="application/pdf",
631
- key="download_pdf",
632
- )
633
-
634
- except Exception as pdf_error:
635
- st.error(f"Could not create PDF export: {pdf_error}")
636
-
637
 
 
 
638
 
639
-
640
  elif st_operator == "synthesize":
641
  with col2:
642
  st.subheader(f"OpenAI Generated output")
 
363
  st.text_area(
364
  label="De-identified", value=st_anonymize_results.text, height=400
365
  )
366
+ # Build stable placeholder suggestions from Presidio
367
+ replacements, report_rows = build_placeholder_replacements(
368
+ st_text,
369
+ st_analyze_results,
370
+ )
371
+
372
+ st.divider()
373
+ st.subheader("Review replacement table before export")
374
+
375
+ st.caption(
376
+ "Untick false positives, change placeholders, add your own word pairs, "
377
+ "and tick Remember for pairs you want to reuse in future documents."
378
+ )
379
+
380
+ # Build editable table rows from remembered replacements + Presidio suggestions
381
+ remembered_rows = load_remembered_replacements()
382
+
383
+ default_editor_rows = []
384
+ seen_find_values = set()
385
+
386
+ # First load remembered pairs
387
+ for row in remembered_rows:
388
+ find_text = str(row.get("find", "")).strip()
389
+ replace_with = str(row.get("replace_with", "")).strip()
390
+
391
+ if not find_text or not replace_with:
392
+ continue
393
+
394
+ default_editor_rows.append(
395
+ {
396
+ "include": row.get("include", True),
397
+ "remember": row.get("remember", True),
398
+ "find": find_text,
399
+ "replace_with": replace_with,
400
+ "entity_type": row.get("entity_type", "REMEMBERED"),
401
+ "score": None,
402
+ }
403
  )
404
+ seen_find_values.add(find_text)
405
+
406
+ # Then add Presidio suggestions, unless already covered by a remembered pair
407
+ for row in report_rows:
408
+ find_text = str(row.get("detected_text", "")).strip()
409
+
410
+ if not find_text:
411
+ continue
412
+
413
+ if find_text in seen_find_values:
414
+ continue
415
+
416
+ default_editor_rows.append(
417
+ {
418
+ "include": True,
419
+ "remember": False,
420
+ "find": find_text,
421
+ "replace_with": row.get("placeholder", ""),
422
+ "entity_type": row.get("entity_type", ""),
423
+ "score": row.get("score", None),
424
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  )
426
+
427
+ # If nothing was detected and nothing was remembered, still show an empty editable row
428
+ if not default_editor_rows:
429
+ default_editor_rows = [
430
+ {
431
+ "include": True,
432
+ "remember": False,
433
+ "find": "",
434
+ "replace_with": "",
435
+ "entity_type": "MANUAL",
436
+ "score": None,
437
+ }
438
+ ]
439
+
440
+ replacement_editor_df = pd.DataFrame(default_editor_rows)
441
+
442
+ edited_replacements_df = st.data_editor(
443
+ replacement_editor_df,
444
+ hide_index=True,
445
+ num_rows="dynamic",
446
+ use_container_width=True,
447
+ column_order=["include", "remember", "find", "replace_with", "entity_type", "score"],
448
+ column_config={
449
+ "include": st.column_config.CheckboxColumn(
450
+ "Use",
451
+ help="Untick to exclude this replacement from the export.",
452
+ default=True,
453
+ ),
454
+ "remember": st.column_config.CheckboxColumn(
455
+ "Remember",
456
+ help="Save this replacement pair for future documents/sessions.",
457
+ default=False,
458
+ ),
459
+ "find": st.column_config.TextColumn(
460
+ "Find text",
461
+ help="The exact text that should be replaced.",
462
+ ),
463
+ "replace_with": st.column_config.TextColumn(
464
+ "Replace with",
465
+ help="The placeholder to insert.",
466
+ ),
467
+ "entity_type": st.column_config.TextColumn(
468
+ "Entity type",
469
+ help="Presidio entity type or MANUAL.",
470
+ ),
471
+ "score": st.column_config.NumberColumn(
472
+ "Score",
473
+ help="Presidio confidence score, if available.",
474
+ format="%.3f",
475
+ ),
476
+ },
477
+ key="replacement_editor",
478
+ )
479
+
480
+ def safe_cell(value):
481
+ if value is None:
482
+ return ""
483
+ try:
484
+ if pd.isna(value):
485
  return ""
486
+ except Exception:
487
+ pass
488
+ return str(value).strip()
489
+
490
+ def safe_bool(value):
491
+ if isinstance(value, bool):
492
+ return value
493
+ if value is None:
494
+ return False
495
+ try:
496
+ if pd.isna(value):
497
+ return False
498
+ except Exception:
499
+ pass
500
+ if isinstance(value, (int, float)):
501
+ return bool(value)
502
+ return str(value).strip().lower() in ("true", "1", "yes", "y", "checked")
503
+
504
+ # Build final replacements from edited table
505
+ edited_replacements = {}
506
+ edited_report_rows = []
507
+
508
+ for _, row in edited_replacements_df.iterrows():
509
+ include = safe_bool(row.get("include", False))
510
+ find_text = safe_cell(row.get("find", ""))
511
+ replace_text = safe_cell(row.get("replace_with", ""))
512
+ entity_type = safe_cell(row.get("entity_type", "MANUAL")) or "MANUAL"
513
+ score = row.get("score", None)
514
+
515
+ if not include:
516
+ continue
517
+
518
+ if not find_text or not replace_text:
519
+ continue
520
+
521
+ edited_replacements[find_text] = replace_text
522
+
523
+ edited_report_rows.append(
524
+ {
525
+ "entity_type": entity_type,
526
+ "detected_text": find_text,
527
+ "placeholder": replace_text,
528
+ "score": score if score is not None else "",
529
+ }
530
+ )
531
+
532
+ st.info(f"{len(edited_replacements)} replacement pair(s) will be applied to the exports.")
533
+
534
+ # Apply edited replacements
535
+ export_text = apply_replacements_to_text(st_text, edited_replacements)
536
+
537
+ with st.expander("Preview anonymized text generated from edited table", expanded=False):
538
+ st.text_area(
539
+ label="Preview",
540
+ value=export_text,
541
+ height=300,
542
+ key="edited_export_preview",
543
+ )
544
+
545
+ st.subheader("Remember reusable replacements")
546
+
547
+ remember_rows_to_save = []
548
+
549
+ for _, row in edited_replacements_df.iterrows():
550
+ include = safe_bool(row.get("include", False))
551
+ remember = safe_bool(row.get("remember", False))
552
+ find_text = safe_cell(row.get("find", ""))
553
+ replace_text = safe_cell(row.get("replace_with", ""))
554
+ entity_type = safe_cell(row.get("entity_type", "REMEMBERED")) or "REMEMBERED"
555
+
556
+ if include and remember and find_text and replace_text:
557
+ remember_rows_to_save.append(
558
  {
559
+ "find": find_text,
560
+ "replace_with": replace_text,
561
  "entity_type": entity_type,
 
 
 
562
  }
563
  )
564
+
565
+ memory_col1, memory_col2 = st.columns(2)
566
+
567
+ with memory_col1:
568
+ if st.button("Save remembered replacements"):
569
+ saved_count = save_remembered_replacements(remember_rows_to_save)
570
+ st.success(f"Saved {saved_count} remembered replacement pair(s).")
571
+ st.info(f"Memory file: {get_memory_file_path()}")
572
+
573
+ with memory_col2:
574
+ if st.button("Clear remembered replacements"):
575
+ clear_remembered_replacements()
576
+ st.warning("Remembered replacements cleared.")
577
+
578
+ st.subheader("Export anonymized files")
579
+
580
+ if uploaded_file is not None:
581
+ st.info(f"Uploaded file detected for export: {uploaded_file.name}")
582
+ else:
583
+ st.info("No uploaded file detected for export. Exporting from text area only.")
584
+
585
+ # TXT export
586
+ st.download_button(
587
+ label="Download anonymized text (.txt)",
588
+ data=export_text.encode("utf-8"),
589
+ file_name="anonymized_text.txt",
590
+ mime="text/plain",
591
+ key="download_txt",
592
+ )
593
+
594
+ # CSV replacement report / reusable mapping
595
+ st.download_button(
596
+ label="Download replacement table (.csv)",
597
+ data=replacement_report_csv(edited_report_rows),
598
+ file_name="replacement_table.csv",
599
+ mime="text/csv",
600
+ key="download_csv",
601
+ )
602
+
603
+ # DOCX export
604
+ try:
605
+ if uploaded_file is not None and uploaded_file.name.lower().endswith(".docx"):
606
+ docx_bytes = anonymized_docx_from_original(uploaded_file, edited_replacements)
607
+ docx_filename = "anonymized_" + uploaded_file.name
608
  else:
609
+ docx_bytes = docx_from_text(export_text)
610
+ docx_filename = "anonymized_text.docx"
611
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  st.download_button(
613
+ label="Download anonymized Word file (.docx)",
614
+ data=docx_bytes,
615
+ file_name=docx_filename,
616
+ mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
617
+ key="download_docx",
618
  )
619
+
620
+ except Exception as docx_error:
621
+ st.error(f"Could not create DOCX export: {docx_error}")
622
+
623
+ # PDF export
624
+ try:
625
  st.download_button(
626
+ label="Download anonymized PDF (.pdf)",
627
+ data=pdf_from_text(export_text),
628
+ file_name="anonymized_text.pdf",
629
+ mime="application/pdf",
630
+ key="download_pdf",
631
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
 
633
+ except Exception as pdf_error:
634
+ st.error(f"Could not create PDF export: {pdf_error}")
635
 
 
636
  elif st_operator == "synthesize":
637
  with col2:
638
  st.subheader(f"OpenAI Generated output")