fix: bugs and code quality in embeddings, jobs, diversity, config
- embeddings: initialize ctx_id=-1 before try block so the except handler cannot NameError; move insightface_home out of try for the same reason - embeddings: replace contextlib.redirect_stdout/stderr (Python-level only) with fd-level dup2 suppression — actually silences C extension noise from InsightFace during model loading - jobs: fix frigate_count==0 falling through `or` chain; use explicit `is not None` check so a real zero is not treated as missing data - diversity: thread person_id through select_diverse_assets → _select_by_embedding → _get_face_bbox / _get_face_confidence / _crop_face_from_thumbnail so group-photo assets embed the target person's face rather than whichever person is listed first - config: replace type() hack for ConfigManager with a proper class Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+6
-1
@@ -161,7 +161,12 @@ class _ConfigAccessor:
|
|||||||
|
|
||||||
|
|
||||||
Config = _ConfigAccessor()
|
Config = _ConfigAccessor()
|
||||||
ConfigManager = type("ConfigManager", (), {"get": staticmethod(lambda: _Config())})
|
|
||||||
|
|
||||||
|
class ConfigManager:
|
||||||
|
@staticmethod
|
||||||
|
def get() -> _Config:
|
||||||
|
return _Config()
|
||||||
|
|
||||||
|
|
||||||
def get_headers() -> dict[str, str]:
|
def get_headers() -> dict[str, str]:
|
||||||
|
|||||||
+17
-9
@@ -29,6 +29,7 @@ def select_diverse_assets(
|
|||||||
entity_name: str,
|
entity_name: str,
|
||||||
selection_mode: str = "smart",
|
selection_mode: str = "smart",
|
||||||
entity_type: str = "face",
|
entity_type: str = "face",
|
||||||
|
person_id: str | None = None,
|
||||||
progress_callback=None,
|
progress_callback=None,
|
||||||
) -> list:
|
) -> list:
|
||||||
"""
|
"""
|
||||||
@@ -59,7 +60,7 @@ def select_diverse_assets(
|
|||||||
return _select_time_spread(assets, limit)
|
return _select_time_spread(assets, limit)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return _select_by_embedding(assets, limit, entity_type, progress_callback)
|
return _select_by_embedding(assets, limit, entity_type, person_id, progress_callback)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Smart Diversity failed: {e}. Falling back to time spread.")
|
logger.error(f"Smart Diversity failed: {e}. Falling back to time spread.")
|
||||||
return _select_time_spread(assets, limit)
|
return _select_time_spread(assets, limit)
|
||||||
@@ -80,9 +81,11 @@ def _fetch_thumbnail(asset_id: str, timeout: int = 10) -> Image.Image | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_face_bbox(asset: dict) -> tuple[float, float, float, float] | None:
|
def _get_face_bbox(asset: dict, person_id: str | None = None) -> tuple[float, float, float, float] | None:
|
||||||
"""Extract face bounding box from asset metadata if available."""
|
"""Extract face bounding box from asset metadata for the given person."""
|
||||||
for person in asset.get("people", []):
|
for person in asset.get("people", []):
|
||||||
|
if person_id and person.get("id") != person_id:
|
||||||
|
continue
|
||||||
faces = person.get("faces", [])
|
faces = person.get("faces", [])
|
||||||
if faces:
|
if faces:
|
||||||
f = faces[0]
|
f = faces[0]
|
||||||
@@ -95,9 +98,11 @@ def _get_face_bbox(asset: dict) -> tuple[float, float, float, float] | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_face_confidence(asset: dict) -> float | None:
|
def _get_face_confidence(asset: dict, person_id: str | None = None) -> float | None:
|
||||||
"""Extract face detection confidence from asset metadata if available."""
|
"""Extract face detection confidence from asset metadata for the given person."""
|
||||||
for person in asset.get("people", []):
|
for person in asset.get("people", []):
|
||||||
|
if person_id and person.get("id") != person_id:
|
||||||
|
continue
|
||||||
faces = person.get("faces", [])
|
faces = person.get("faces", [])
|
||||||
if faces:
|
if faces:
|
||||||
return faces[0].get("score") or faces[0].get("confidence")
|
return faces[0].get("score") or faces[0].get("confidence")
|
||||||
@@ -108,6 +113,7 @@ def _crop_face_from_thumbnail(
|
|||||||
img: Image.Image,
|
img: Image.Image,
|
||||||
asset: dict,
|
asset: dict,
|
||||||
margin: float = 0.25,
|
margin: float = 0.25,
|
||||||
|
person_id: str | None = None,
|
||||||
) -> Image.Image | None:
|
) -> Image.Image | None:
|
||||||
"""Crop the face region from a thumbnail using Immich bbox metadata.
|
"""Crop the face region from a thumbnail using Immich bbox metadata.
|
||||||
|
|
||||||
@@ -118,11 +124,12 @@ def _crop_face_from_thumbnail(
|
|||||||
img: Full preview thumbnail
|
img: Full preview thumbnail
|
||||||
asset: Asset dict with people/faces metadata
|
asset: Asset dict with people/faces metadata
|
||||||
margin: Extra margin around the bbox (fraction, default 25%)
|
margin: Extra margin around the bbox (fraction, default 25%)
|
||||||
|
person_id: If provided, only crop from this person's face data.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Cropped face PIL image, or None if no face metadata available
|
Cropped face PIL image, or None if no face metadata available
|
||||||
"""
|
"""
|
||||||
bbox = _get_face_bbox(asset)
|
bbox = _get_face_bbox(asset, person_id=person_id)
|
||||||
if bbox is None:
|
if bbox is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -169,6 +176,7 @@ def _select_by_embedding(
|
|||||||
assets: list,
|
assets: list,
|
||||||
limit: int | str,
|
limit: int | str,
|
||||||
entity_type: str,
|
entity_type: str,
|
||||||
|
person_id: str | None = None,
|
||||||
progress_callback=None,
|
progress_callback=None,
|
||||||
) -> list:
|
) -> list:
|
||||||
"""Select assets using embedding-based cluster-aware FPS.
|
"""Select assets using embedding-based cluster-aware FPS.
|
||||||
@@ -217,11 +225,11 @@ def _select_by_embedding(
|
|||||||
if img is None:
|
if img is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
confidence = _get_face_confidence(asset)
|
confidence = _get_face_confidence(asset, person_id=person_id)
|
||||||
|
|
||||||
# Quality gate: filter before expensive embedding computation
|
# Quality gate: filter before expensive embedding computation
|
||||||
if entity_type == "face":
|
if entity_type == "face":
|
||||||
face_bbox = _get_face_bbox(asset)
|
face_bbox = _get_face_bbox(asset, person_id=person_id)
|
||||||
quality = assess_quality(
|
quality = assess_quality(
|
||||||
img,
|
img,
|
||||||
face_bbox=face_bbox,
|
face_bbox=face_bbox,
|
||||||
@@ -236,7 +244,7 @@ def _select_by_embedding(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Crop the target person's face before embedding
|
# Crop the target person's face before embedding
|
||||||
face_crop = _crop_face_from_thumbnail(img, asset)
|
face_crop = _crop_face_from_thumbnail(img, asset, person_id=person_id)
|
||||||
embed_img = face_crop if face_crop is not None else img
|
embed_img = face_crop if face_crop is not None else img
|
||||||
else:
|
else:
|
||||||
embed_img = img
|
embed_img = img
|
||||||
|
|||||||
+22
-6
@@ -6,11 +6,11 @@ Unified embedding interface for faces and objects.
|
|||||||
- Caching: Disk-based cache avoids recomputation on reruns
|
- Caching: Disk-based cache avoids recomputation on reruns
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import contextlib
|
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import warnings
|
import warnings
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -20,6 +20,24 @@ from .cache import get_cache
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _suppress_output():
|
||||||
|
"""Suppress stdout/stderr at the file-descriptor level, silencing C extension noise."""
|
||||||
|
devnull_fd = os.open(os.devnull, os.O_WRONLY)
|
||||||
|
saved_out, saved_err = os.dup(1), os.dup(2)
|
||||||
|
try:
|
||||||
|
os.dup2(devnull_fd, 1)
|
||||||
|
os.dup2(devnull_fd, 2)
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
os.dup2(saved_out, 1)
|
||||||
|
os.dup2(saved_err, 2)
|
||||||
|
os.close(devnull_fd)
|
||||||
|
os.close(saved_out)
|
||||||
|
os.close(saved_err)
|
||||||
|
|
||||||
|
|
||||||
# Lazy-loaded singletons
|
# Lazy-loaded singletons
|
||||||
_insightface_app = None
|
_insightface_app = None
|
||||||
_insightface_loaded = False
|
_insightface_loaded = False
|
||||||
@@ -70,6 +88,8 @@ def get_insightface_app():
|
|||||||
# Preload CUDA/cuDNN DLLs BEFORE any ORT InferenceSession is created
|
# Preload CUDA/cuDNN DLLs BEFORE any ORT InferenceSession is created
|
||||||
_preload_cuda_libs()
|
_preload_cuda_libs()
|
||||||
|
|
||||||
|
ctx_id = -1
|
||||||
|
insightface_home = os.environ.get("INSIGHTFACE_HOME", os.path.expanduser("~/.insightface"))
|
||||||
try:
|
try:
|
||||||
import onnxruntime as ort
|
import onnxruntime as ort
|
||||||
from insightface.app import FaceAnalysis
|
from insightface.app import FaceAnalysis
|
||||||
@@ -78,7 +98,6 @@ def get_insightface_app():
|
|||||||
providers = [p for p in ort.get_available_providers() if p != "TensorrtExecutionProvider"]
|
providers = [p for p in ort.get_available_providers() if p != "TensorrtExecutionProvider"]
|
||||||
logger.info(f"Available ONNX providers: {providers}")
|
logger.info(f"Available ONNX providers: {providers}")
|
||||||
|
|
||||||
# Determine device: 0 for GPU, -1 for CPU
|
|
||||||
gpu_providers = {
|
gpu_providers = {
|
||||||
"CUDAExecutionProvider",
|
"CUDAExecutionProvider",
|
||||||
"ROCmExecutionProvider",
|
"ROCmExecutionProvider",
|
||||||
@@ -90,9 +109,7 @@ def get_insightface_app():
|
|||||||
device_str = "GPU" if ctx_id >= 0 else "CPU"
|
device_str = "GPU" if ctx_id >= 0 else "CPU"
|
||||||
logger.info(f"Loading InsightFace Buffalo_L on {device_str} (ctx_id={ctx_id})...")
|
logger.info(f"Loading InsightFace Buffalo_L on {device_str} (ctx_id={ctx_id})...")
|
||||||
|
|
||||||
# Suppress C-level output during model loading
|
with _suppress_output():
|
||||||
with open(os.devnull, "w") as devnull, contextlib.redirect_stdout(devnull), contextlib.redirect_stderr(devnull):
|
|
||||||
insightface_home = os.environ.get("INSIGHTFACE_HOME", os.path.expanduser("~/.insightface"))
|
|
||||||
_insightface_app = FaceAnalysis(name="buffalo_l", root=insightface_home, providers=providers)
|
_insightface_app = FaceAnalysis(name="buffalo_l", root=insightface_home, providers=providers)
|
||||||
_insightface_app.prepare(ctx_id=ctx_id, det_size=(640, 640))
|
_insightface_app.prepare(ctx_id=ctx_id, det_size=(640, 640))
|
||||||
|
|
||||||
@@ -103,7 +120,6 @@ def get_insightface_app():
|
|||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to load InsightFace: {e}")
|
logger.error(f"Failed to load InsightFace: {e}")
|
||||||
# Retry on CPU if GPU failed
|
|
||||||
if ctx_id == 0:
|
if ctx_id == 0:
|
||||||
logger.warning("Retrying InsightFace on CPU...")
|
logger.warning("Retrying InsightFace on CPU...")
|
||||||
try:
|
try:
|
||||||
|
|||||||
+15
-8
@@ -81,7 +81,9 @@ def _resolve_strategy(strategy: str, has_embedding: bool) -> tuple[int | str, st
|
|||||||
return strategy_map.get(strategy, ("auto", "smart"))
|
return strategy_map.get(strategy, ("auto", "smart"))
|
||||||
|
|
||||||
|
|
||||||
def _perform_selection(assets: list, limit: int | str, name: str, selection_mode: str, entity_type: str) -> list:
|
def _perform_selection(
|
||||||
|
assets: list, limit: int | str, name: str, selection_mode: str, entity_type: str, person_id: str | None = None
|
||||||
|
) -> list:
|
||||||
"""Run diversity selection with progress display."""
|
"""Run diversity selection with progress display."""
|
||||||
if selection_mode == "smart":
|
if selection_mode == "smart":
|
||||||
model_display = "InsightFace (face embeddings)" if entity_type == "face" else "SigLIP (visual embeddings)"
|
model_display = "InsightFace (face embeddings)" if entity_type == "face" else "SigLIP (visual embeddings)"
|
||||||
@@ -104,6 +106,7 @@ def _perform_selection(assets: list, limit: int | str, name: str, selection_mode
|
|||||||
name,
|
name,
|
||||||
selection_mode=selection_mode,
|
selection_mode=selection_mode,
|
||||||
entity_type=entity_type,
|
entity_type=entity_type,
|
||||||
|
person_id=person_id,
|
||||||
progress_callback=lambda c, t: progress.update(task, completed=c, total=t),
|
progress_callback=lambda c, t: progress.update(task, completed=c, total=t),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -113,7 +116,9 @@ def _perform_selection(assets: list, limit: int | str, name: str, selection_mode
|
|||||||
|
|
||||||
rprint(f"\n[cyan]Using time-spread selection for {limit} images...[/cyan]")
|
rprint(f"\n[cyan]Using time-spread selection for {limit} images...[/cyan]")
|
||||||
with console.status(f"[bold]Selecting {limit} images evenly distributed over time...[/bold]"):
|
with console.status(f"[bold]Selecting {limit} images evenly distributed over time...[/bold]"):
|
||||||
selected = select_diverse_assets(assets, limit, name, selection_mode="time", entity_type=entity_type)
|
selected = select_diverse_assets(
|
||||||
|
assets, limit, name, selection_mode="time", entity_type=entity_type, person_id=person_id
|
||||||
|
)
|
||||||
rprint(f" [green]Selected {len(selected)} images using time spread.[/green]")
|
rprint(f" [green]Selected {len(selected)} images using time spread.[/green]")
|
||||||
return selected
|
return selected
|
||||||
|
|
||||||
@@ -167,7 +172,9 @@ def _configure_person(person: dict, people: list[dict]) -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Perform selection
|
# Perform selection
|
||||||
selected_assets = _perform_selection(recent_assets, limit, name, selection_mode, entity_type)
|
selected_assets = _perform_selection(
|
||||||
|
recent_assets, limit, name, selection_mode, entity_type, person_id=person["id"]
|
||||||
|
)
|
||||||
|
|
||||||
rprint(f" [green]Queued {len(selected_assets)} images for {name}.[/green]")
|
rprint(f" [green]Queued {len(selected_assets)} images for {name}.[/green]")
|
||||||
return {"person": person, "assets": selected_assets, "limit": len(selected_assets), "config": config}
|
return {"person": person, "assets": selected_assets, "limit": len(selected_assets), "config": config}
|
||||||
@@ -276,10 +283,8 @@ def auto_configure(people: list[dict]) -> list[dict]:
|
|||||||
if frigate_counts is not None:
|
if frigate_counts is not None:
|
||||||
already_uploaded = frigate_counts.get(name, 0)
|
already_uploaded = frigate_counts.get(name, 0)
|
||||||
else:
|
else:
|
||||||
already_uploaded = (
|
fc = person_summary.get("frigate_count")
|
||||||
person_summary.get("frigate_count")
|
already_uploaded = fc if fc is not None else person_summary.get("uploaded", 0)
|
||||||
or person_summary.get("uploaded", 0)
|
|
||||||
)
|
|
||||||
capacity = Config.MAX_AUTO_IMAGES - already_uploaded
|
capacity = Config.MAX_AUTO_IMAGES - already_uploaded
|
||||||
if capacity <= 0:
|
if capacity <= 0:
|
||||||
rprint(
|
rprint(
|
||||||
@@ -301,7 +306,9 @@ def auto_configure(people: list[dict]) -> list[dict]:
|
|||||||
if selection_mode == "skip":
|
if selection_mode == "skip":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
selected_assets = _perform_selection(recent_assets, limit, name, selection_mode, entity_type)
|
selected_assets = _perform_selection(
|
||||||
|
recent_assets, limit, name, selection_mode, entity_type, person_id=person["id"]
|
||||||
|
)
|
||||||
|
|
||||||
if selected_assets:
|
if selected_assets:
|
||||||
rprint(f" [green]Queued {len(selected_assets)} images for {name}.[/green]")
|
rprint(f" [green]Queued {len(selected_assets)} images for {name}.[/green]")
|
||||||
|
|||||||
Reference in New Issue
Block a user