feat: student scan filters — skip GPS images and min CPR threshold
New profile options to reduce noise when scanning student accounts: - skip_gps_images: images flagged solely by GPS coordinates are suppressed. GPS data is still extracted and shown in the detail card when the item is flagged by another signal (faces, EXIF author/comment). - min_cpr_count (default 1): only flag a file if it contains at least N distinct CPR numbers. Deduplication is by value. Faces and EXIF PII still trigger flags regardless of CPR count. Both options apply to M365, Google, and file scan paths. Saved in profiles and editable in the Profile Manager editor. Docs, manuals, i18n (DA/EN/DE), CHANGELOG, and VERSION (1.6.14 → 1.6.15) updated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
dfdc46c812
commit
28c9effd17
13 files changed
+104
-14
No files matched your search
+27
-9
@@ -164,8 +164,10 @@ def run_file_scan(source: dict):
|
||||
smb_domain = source.get("smb_domain") or ""
|
||||
keychain_key= source.get("keychain_key") or None
|
||||
smb_password= source.get("smb_password") or None
|
||||
scan_photos = bool(source.get("scan_photos", False))
|
||||
max_mb = int(source.get("max_file_mb", 50))
|
||||
scan_photos = bool(source.get("scan_photos", False))
|
||||
skip_gps_images = bool(source.get("skip_gps_images", False))
|
||||
min_cpr_count = max(1, int(source.get("min_cpr_count", 1)))
|
||||
max_mb = int(source.get("max_file_mb", 50))
|
||||
|
||||
if not FILE_SCANNER_OK:
|
||||
broadcast("scan_error", {"file": label, "error": "file_scanner.py not found"})
|
||||
@@ -243,7 +245,14 @@ def run_file_scan(source: dict):
|
||||
_face_count = _detect_photo_faces(content, rel_path)
|
||||
_exif = _extract_exif(content, rel_path)
|
||||
|
||||
if not cprs and _face_count == 0 and not _exif.get("has_pii"):
|
||||
# Apply filters: distinct CPR threshold and GPS suppression
|
||||
_distinct_cprs = list(dict.fromkeys(cprs)) # preserve order, deduplicate
|
||||
_cpr_qualifies = len(_distinct_cprs) >= min_cpr_count
|
||||
_exif_has_pii = _exif.get("has_pii") and (
|
||||
not skip_gps_images or bool(_exif.get("pii_fields") or _exif.get("author"))
|
||||
)
|
||||
|
||||
if not (_cpr_qualifies and cprs) and _face_count == 0 and not _exif_has_pii:
|
||||
continue
|
||||
|
||||
# Build card metadata
|
||||
@@ -256,9 +265,9 @@ def run_file_scan(source: dict):
|
||||
_sc = _check_special_category(_file_text, cprs)
|
||||
if _face_count > 0 and "biometric" not in _sc:
|
||||
_sc = sorted(_sc + ["biometric"])
|
||||
if _exif.get("gps") and "gps_location" not in _sc:
|
||||
if _exif.get("gps") and not skip_gps_images and "gps_location" not in _sc:
|
||||
_sc = sorted(_sc + ["gps_location"])
|
||||
if _exif.get("has_pii") and "exif_pii" not in _sc:
|
||||
if _exif_has_pii and "exif_pii" not in _sc:
|
||||
_sc = sorted(_sc + ["exif_pii"])
|
||||
|
||||
# Thumbnail for images
|
||||
@@ -389,6 +398,8 @@ def run_scan(options: dict):
|
||||
max_emails = int(scan_opts.get("max_emails", 2000))
|
||||
delta_enabled = bool(scan_opts.get("delta", False))
|
||||
scan_photos = bool(scan_opts.get("scan_photos", False)) # biometric photo scan (#9)
|
||||
skip_gps_images= bool(scan_opts.get("skip_gps_images", False))
|
||||
min_cpr_count = max(1, int(scan_opts.get("min_cpr_count", 1)))
|
||||
|
||||
# Delta token state — loaded once, updated per-source, saved on completion
|
||||
delta_tokens: dict = _load_delta_tokens() if delta_enabled else {}
|
||||
@@ -1079,8 +1090,15 @@ def run_scan(options: dict):
|
||||
_face_count = _detect_photo_faces(content, name)
|
||||
_exif = _extract_exif(content, name)
|
||||
|
||||
# Flag item if CPRs found, faces detected, or EXIF PII found
|
||||
if cprs or _face_count > 0 or _exif.get("has_pii"):
|
||||
# Apply filters: distinct CPR threshold and GPS suppression
|
||||
_distinct_cprs = list(dict.fromkeys(cprs)) # preserve order, deduplicate
|
||||
_cpr_qualifies = len(_distinct_cprs) >= min_cpr_count
|
||||
_exif_has_pii = _exif.get("has_pii") and (
|
||||
not skip_gps_images or bool(_exif.get("pii_fields") or _exif.get("author"))
|
||||
)
|
||||
|
||||
# Flag item if CPRs found (above threshold), faces detected, or EXIF PII found
|
||||
if (_cpr_qualifies and cprs) or _face_count > 0 or _exif_has_pii:
|
||||
# Make thumbnail
|
||||
if ext in {".jpg", ".jpeg", ".png"} and PIL_OK:
|
||||
thumb = _make_thumb(content, name)
|
||||
@@ -1109,9 +1127,9 @@ def run_scan(options: dict):
|
||||
# the category even when no CPR is present in the file.
|
||||
if _face_count > 0 and "biometric" not in _sc:
|
||||
_sc = sorted(_sc + ["biometric"])
|
||||
if _exif.get("gps") and "gps_location" not in _sc:
|
||||
if _exif.get("gps") and not skip_gps_images and "gps_location" not in _sc:
|
||||
_sc = sorted(_sc + ["gps_location"])
|
||||
if _exif.get("has_pii") and "exif_pii" not in _sc:
|
||||
if _exif_has_pii and "exif_pii" not in _sc:
|
||||
_sc = sorted(_sc + ["exif_pii"])
|
||||
meta["_special_category"] = _sc
|
||||
meta["_face_count"] = _face_count
|
||||
|
||||
Reference in new issue
Block a user