fix+logging: model load logging, CPU fallback, log level audit
Bugs fixed (from high-effort review): - InsightFace CPU fallback now passes providers=['CPUExecutionProvider'] and wraps in _suppress_output() so broken GPU drivers don't cause fallback to try the same broken provider again, and C-extension noise stays suppressed - scheduler.py: BaseException → Exception (KeyboardInterrupt already re-raised; winnow has no sys.exit() calls, so SystemExit would not occur, but Exception is the correct scope) - compose.yml: fix inverted AUTO_MODE comment (docker run -it enables interactive mode via TTY, not non-interactive) Model loading logging (embeddings.py): - InsightFace: disk cache check, "not cached — downloading now (~300 MB)", "loading into memory on GPU/CPU...", "ready on GPU/CPU (Xs)" - SigLIP: same treatment; cache path derived dynamically from model_name via HuggingFace slug convention (models--org--model) so it stays correct if the model variant ever changes Logging level audit (INFO/DEBUG/WARNING/ERROR): - diversity.py: internal algo steps (clustering, medoids, adaptive threshold, auto-stop decision) → DEBUG; final selection summaries stay INFO - immich_api.py: "Fetching assets" and "Retained N assets" → DEBUG (callers already print this to the console via rprint) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -10,8 +10,8 @@ services:
|
|||||||
|
|
||||||
# ── Mode & Strategy ───────────────────────────────────────────────────
|
# ── Mode & Strategy ───────────────────────────────────────────────────
|
||||||
# Auto mode is active by default when no TTY is present (Docker/cron).
|
# Auto mode is active by default when no TTY is present (Docker/cron).
|
||||||
# Run with `docker run -it` or set AUTO_MODE=true to force non-interactive
|
# Set AUTO_MODE=true to force auto mode in an interactive terminal.
|
||||||
# in a terminal.
|
# To run interactively: docker exec -it winnow winnow
|
||||||
# TRAINING_MODE: face = upload to Frigate face recognition API
|
# TRAINING_MODE: face = upload to Frigate face recognition API
|
||||||
# object = save crops to output dir for manual Frigate placement
|
# object = save crops to output dir for manual Frigate placement
|
||||||
- TRAINING_MODE=face
|
- TRAINING_MODE=face
|
||||||
|
|||||||
+1
-1
@@ -45,7 +45,7 @@ while True:
|
|||||||
print("winnow run complete", flush=True)
|
print("winnow run complete", flush=True)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
raise
|
raise
|
||||||
except BaseException as e:
|
except Exception as e:
|
||||||
logger.error(f"winnow run failed: {e}", exc_info=True)
|
logger.error(f"winnow run failed: {e}", exc_info=True)
|
||||||
print(f"winnow run failed: {e}", flush=True)
|
print(f"winnow run failed: {e}", flush=True)
|
||||||
next_run = cron.get_next(float)
|
next_run = cron.get_next(float)
|
||||||
|
|||||||
+5
-5
@@ -368,7 +368,7 @@ def _compute_adaptive_threshold(emb_normed: np.ndarray, entity_type: str) -> flo
|
|||||||
fraction = 0.20 if entity_type == "face" else 0.10
|
fraction = 0.20 if entity_type == "face" else 0.10
|
||||||
threshold = max(0.05, median_dist * fraction)
|
threshold = max(0.05, median_dist * fraction)
|
||||||
|
|
||||||
logger.info(
|
logger.debug(
|
||||||
f"Adaptive threshold: {threshold:.4f} "
|
f"Adaptive threshold: {threshold:.4f} "
|
||||||
f"(median_dist={median_dist:.4f}, fraction={fraction}, type={entity_type})"
|
f"(median_dist={median_dist:.4f}, fraction={fraction}, type={entity_type})"
|
||||||
)
|
)
|
||||||
@@ -410,7 +410,7 @@ def _cluster_aware_selection(
|
|||||||
|
|
||||||
# --- Stage 1: K-Medoids clustering ---
|
# --- Stage 1: K-Medoids clustering ---
|
||||||
k = min(max(5, target // 4), n // 3, n) # e.g., 5-20 clusters
|
k = min(max(5, target // 4), n // 3, n) # e.g., 5-20 clusters
|
||||||
logger.info(f"Clustering {n} embeddings into {k} groups (K-Medoids)...")
|
logger.debug(f"Clustering {n} embeddings into {k} groups (K-Medoids)...")
|
||||||
|
|
||||||
# Compute full cosine distance matrix
|
# Compute full cosine distance matrix
|
||||||
dist_matrix = 1 - emb_normed @ emb_normed.T
|
dist_matrix = 1 - emb_normed @ emb_normed.T
|
||||||
@@ -419,7 +419,7 @@ def _cluster_aware_selection(
|
|||||||
selected = list(medoid_indices)
|
selected = list(medoid_indices)
|
||||||
selected_set = set(selected)
|
selected_set = set(selected)
|
||||||
|
|
||||||
logger.info(f"Selected {len(selected)} cluster medoids as initial picks.")
|
logger.debug(f"Selected {len(selected)} cluster medoids as initial picks.")
|
||||||
|
|
||||||
# --- Stage 2: FPS with hard example weighting ---
|
# --- Stage 2: FPS with hard example weighting ---
|
||||||
min_dists = np.full(n, np.inf)
|
min_dists = np.full(n, np.inf)
|
||||||
@@ -444,8 +444,8 @@ def _cluster_aware_selection(
|
|||||||
break # All points selected
|
break # All points selected
|
||||||
|
|
||||||
if limit == "auto" and best_dist < auto_threshold:
|
if limit == "auto" and best_dist < auto_threshold:
|
||||||
logger.info(
|
logger.debug(
|
||||||
f"Auto-stop: Next best image {best_dist:.3f} away " f"(adaptive threshold {auto_threshold:.4f})."
|
f"Auto-stop: next best image {best_dist:.3f} away (adaptive threshold {auto_threshold:.4f})."
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
+40
-10
@@ -9,8 +9,10 @@ Unified embedding interface for faces and objects.
|
|||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
import warnings
|
import warnings
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -90,9 +92,16 @@ def get_insightface_app():
|
|||||||
import onnxruntime as ort
|
import onnxruntime as ort
|
||||||
from insightface.app import FaceAnalysis
|
from insightface.app import FaceAnalysis
|
||||||
|
|
||||||
|
# Disk cache check — lets the user know whether a download is coming
|
||||||
|
buffalo_path = Path(insightface_home) / "models" / "buffalo_l"
|
||||||
|
if buffalo_path.exists() and any(buffalo_path.iterdir()):
|
||||||
|
logger.info("InsightFace Buffalo_L: found in model cache")
|
||||||
|
else:
|
||||||
|
logger.info("InsightFace Buffalo_L: not cached — downloading now (~300 MB)")
|
||||||
|
|
||||||
# Get providers, excluding TensorRT to avoid noisy errors
|
# Get providers, excluding TensorRT to avoid noisy errors
|
||||||
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.debug(f"ONNX providers available: {providers}")
|
||||||
|
|
||||||
gpu_providers = {
|
gpu_providers = {
|
||||||
"CUDAExecutionProvider",
|
"CUDAExecutionProvider",
|
||||||
@@ -111,12 +120,14 @@ 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"InsightFace Buffalo_L: loading into memory on {device_str}...")
|
||||||
|
|
||||||
|
t0 = time.time()
|
||||||
with _suppress_output():
|
with _suppress_output():
|
||||||
_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))
|
||||||
|
|
||||||
|
logger.info(f"InsightFace Buffalo_L: ready on {device_str} ({time.time() - t0:.1f}s)")
|
||||||
return _insightface_app
|
return _insightface_app
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -125,15 +136,22 @@ def get_insightface_app():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to load InsightFace: {e}")
|
logger.error(f"Failed to load InsightFace: {e}")
|
||||||
if ctx_id == 0:
|
if ctx_id == 0:
|
||||||
logger.warning("Retrying InsightFace on CPU...")
|
logger.warning("InsightFace GPU load failed — retrying on CPU...")
|
||||||
try:
|
try:
|
||||||
from insightface.app import FaceAnalysis
|
from insightface.app import FaceAnalysis
|
||||||
|
|
||||||
_insightface_app = FaceAnalysis(name="buffalo_l", root=insightface_home)
|
t0 = time.time()
|
||||||
|
with _suppress_output():
|
||||||
|
_insightface_app = FaceAnalysis(
|
||||||
|
name="buffalo_l",
|
||||||
|
root=insightface_home,
|
||||||
|
providers=["CPUExecutionProvider"],
|
||||||
|
)
|
||||||
_insightface_app.prepare(ctx_id=-1, det_size=(640, 640))
|
_insightface_app.prepare(ctx_id=-1, det_size=(640, 640))
|
||||||
|
logger.info(f"InsightFace Buffalo_L: ready on CPU (fallback, {time.time() - t0:.1f}s)")
|
||||||
return _insightface_app
|
return _insightface_app
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error(f"CPU fallback failed: {ex}")
|
logger.error(f"InsightFace CPU fallback failed: {ex}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -182,7 +200,18 @@ def get_siglip_model():
|
|||||||
from transformers import AutoImageProcessor, SiglipVisionModel
|
from transformers import AutoImageProcessor, SiglipVisionModel
|
||||||
|
|
||||||
model_name = "google/siglip-base-patch16-224"
|
model_name = "google/siglip-base-patch16-224"
|
||||||
logger.info(f"Loading SigLIP model ({model_name})...")
|
|
||||||
|
# Disk cache check — path derived from model_name using HuggingFace's slug convention
|
||||||
|
hf_home = os.environ.get("HF_HOME", os.path.join(os.path.expanduser("~"), ".cache", "huggingface"))
|
||||||
|
cache_slug = "models--" + model_name.replace("/", "--")
|
||||||
|
model_cache = Path(hf_home) / "hub" / cache_slug
|
||||||
|
if model_cache.exists() and any(model_cache.iterdir()):
|
||||||
|
logger.info(f"SigLIP {model_name}: found in model cache")
|
||||||
|
else:
|
||||||
|
logger.info(f"SigLIP {model_name}: not cached — downloading now (~380 MB)")
|
||||||
|
|
||||||
|
logger.info(f"SigLIP {model_name}: loading into memory...")
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.filterwarnings("ignore", category=FutureWarning)
|
warnings.filterwarnings("ignore", category=FutureWarning)
|
||||||
@@ -196,15 +225,16 @@ def get_siglip_model():
|
|||||||
if not _is_force_cpu():
|
if not _is_force_cpu():
|
||||||
if torch.cuda.is_available():
|
if torch.cuda.is_available():
|
||||||
_siglip_model = _siglip_model.cuda()
|
_siglip_model = _siglip_model.cuda()
|
||||||
logger.info("SigLIP running on CUDA GPU")
|
device_name = "CUDA GPU"
|
||||||
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
||||||
_siglip_model = _siglip_model.to("mps")
|
_siglip_model = _siglip_model.to("mps")
|
||||||
logger.info("SigLIP running on Apple MPS")
|
device_name = "Apple MPS"
|
||||||
else:
|
else:
|
||||||
logger.info("SigLIP running on CPU")
|
device_name = "CPU"
|
||||||
else:
|
else:
|
||||||
logger.info("FORCE_CPU set. SigLIP running on CPU")
|
device_name = "CPU (FORCE_CPU)"
|
||||||
|
|
||||||
|
logger.info(f"SigLIP {model_name}: ready on {device_name} ({time.time() - t0:.1f}s)")
|
||||||
return _siglip_model, _siglip_processor
|
return _siglip_model, _siglip_processor
|
||||||
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ def fetch_all_assets(person: dict) -> list[dict]:
|
|||||||
url = f"{Config.IMMICH_URL}/api/search/metadata"
|
url = f"{Config.IMMICH_URL}/api/search/metadata"
|
||||||
page_size = 1000
|
page_size = 1000
|
||||||
|
|
||||||
logger.info(f"Fetching assets for {name}...")
|
logger.debug(f"Fetching assets for {name}...")
|
||||||
|
|
||||||
assets = []
|
assets = []
|
||||||
for page in range(1, MAX_PAGES + 1):
|
for page in range(1, MAX_PAGES + 1):
|
||||||
@@ -212,6 +212,6 @@ def filter_recent_assets(assets: list[dict], years: int | None = None) -> list[d
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info(f"Retained {len(recent)} assets (filtered {skipped} old assets).")
|
logger.debug(f"Retained {len(recent)} assets (filtered {skipped} old assets).")
|
||||||
return recent
|
return recent
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user