fix: surface skip reasons and suppress norm_crop FutureWarning

process_face_mode now returns a descriptive string instead of None for
filtered-out faces ("face too small 45x38px, min 90px", "no face
metadata"), so the executor can print a useful reason rather than the
generic "no usable face data".

Also suppresses the InsightFace norm_crop FutureWarning about deprecated
estimate usage, which was noisy at INFO level on every aligned crop.
This commit is contained in:
2026-06-17 17:18:43 +00:00
parent c4910d82ef
commit b69f776378
2 changed files with 10 additions and 6 deletions
+2 -1
View File
@@ -202,8 +202,9 @@ def execute_jobs(jobs: list[dict]) -> None:
count += 1 count += 1
else: else:
reason = saved if isinstance(saved, str) else "no usable face data"
progress.console.print( progress.console.print(
f"[yellow]Skipped {asset['id']} (no usable face data)[/yellow]" f"[yellow]Skipped {asset['id']} ({reason})[/yellow]"
) )
except Exception as e: except Exception as e:
logger.error("Failed to process asset %s: %s", asset.get("id", "<unknown>"), e) logger.error("Failed to process asset %s: %s", asset.get("id", "<unknown>"), e)
+8 -5
View File
@@ -48,7 +48,9 @@ def align_face(img: Image.Image, landmarks: list[list[float]] | np.ndarray) -> I
if lm.shape != (5, 2): if lm.shape != (5, 2):
logger.debug("Invalid landmark shape: %s, expected (5, 2)", lm.shape) logger.debug("Invalid landmark shape: %s, expected (5, 2)", lm.shape)
return None return None
aligned = norm_crop(img_np, lm) with warnings.catch_warnings():
warnings.filterwarnings("ignore", message=".*estimate.*is deprecated", category=FutureWarning)
aligned = norm_crop(img_np, lm)
return Image.fromarray(aligned) return Image.fromarray(aligned)
except ImportError: except ImportError:
logger.debug("InsightFace not available for face alignment") logger.debug("InsightFace not available for face alignment")
@@ -66,10 +68,11 @@ def process_face_mode(
count: int, count: int,
min_width: int | None = None, min_width: int | None = None,
insightface_app=None, insightface_app=None,
) -> tuple[int, int] | None: ) -> tuple[int, int] | str | None:
"""Crop face based on Immich metadata and save to output directory. """Crop face based on Immich metadata and save to output directory.
Returns (width, height) of the saved crop, or None if no crop was saved. Returns (width, height) of the saved crop, a skip-reason string if the
face was filtered out, or None if no crop was saved for other reasons.
When insightface_app is provided and ENABLE_FACE_ALIGNMENT is True, When insightface_app is provided and ENABLE_FACE_ALIGNMENT is True,
re-detects the face in the Immich bbox region using InsightFace to get re-detects the face in the Immich bbox region using InsightFace to get
precise landmarks for a proper 112x112 aligned crop. Falls back to precise landmarks for a proper 112x112 aligned crop. Falls back to
@@ -89,7 +92,7 @@ def process_face_mode(
if not face_info: if not face_info:
logger.debug("No face info for %s in asset %s", person.get("name"), asset.get("id")) logger.debug("No face info for %s in asset %s", person.get("name"), asset.get("id"))
return None return "no face metadata"
img_w, img_h = img.size img_w, img_h = img.size
meta_w = face_info.get("imageWidth") or 0 meta_w = face_info.get("imageWidth") or 0
@@ -108,7 +111,7 @@ def process_face_mode(
face_w, face_h = x2 - x1, y2 - y1 face_w, face_h = x2 - x1, y2 - y1
if face_w < min_width or face_h < min_width: if face_w < min_width or face_h < min_width:
logger.debug("Face too small (%.1fx%.1f)", face_w, face_h) logger.debug("Face too small (%.1fx%.1f)", face_w, face_h)
return None return f"face too small ({face_w:.0f}x{face_h:.0f}px, min {min_width}px)"
# Re-detect face with InsightFace for landmark-based alignment. # Re-detect face with InsightFace for landmark-based alignment.
# Immich's /api/faces endpoint does not include landmarks, so the # Immich's /api/faces endpoint does not include landmarks, so the