fix: embedding cache key mismatch, scheduler path/exception, log handler leak

embeddings.py: face embedding cache used 'immich' for lookup but
'insightface' for storage, so the cache was never hit for locally-
computed embeddings. Unified to 'insightface'/'siglip' throughout.
This affects all users since ENABLE_CACHE now defaults to true.

scheduler.py: INSIGHTFACE_HOME=/models/.insightface was having
'.insightface' appended again, making buffalo_l check always report
'will download'. Also catch BaseException (not just Exception) so a
SystemExit from a library call can't silently kill all future runs.

log_config.py: handlers.clear() abandoned open FileHandler fds on
each scheduled main() call. Close each handler properly before removal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 15:10:44 +00:00
co-authored by Claude Sonnet 4.6
parent ac9e9530f3
commit af8bae1c45
3 changed files with 16 additions and 11 deletions
+5 -3
View File
@@ -17,13 +17,13 @@ from winnow.cli import main
SCHEDULE = os.environ["CRON_SCHEDULE"] SCHEDULE = os.environ["CRON_SCHEDULE"]
MODELS_DIR = os.environ.get("HF_HOME", "/models/huggingface") MODELS_DIR = os.environ.get("HF_HOME", "/models/huggingface")
INSIGHTFACE_BASE = os.environ.get("INSIGHTFACE_HOME", "/models") INSIGHTFACE_HOME = os.environ.get("INSIGHTFACE_HOME", "/models/.insightface")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def check_models() -> None: def check_models() -> None:
buffalo = Path(INSIGHTFACE_BASE) / ".insightface" / "models" / "buffalo_l" buffalo = Path(INSIGHTFACE_HOME) / "models" / "buffalo_l"
hf_hub = Path(MODELS_DIR) / "hub" hf_hub = Path(MODELS_DIR) / "hub"
if not buffalo.exists(): if not buffalo.exists():
print(" InsightFace Buffalo_L not found — will download on first run", flush=True) print(" InsightFace Buffalo_L not found — will download on first run", flush=True)
@@ -43,7 +43,9 @@ while True:
try: try:
main() main()
print("winnow run complete", flush=True) print("winnow run complete", flush=True)
except Exception as e: except KeyboardInterrupt:
raise
except BaseException 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)
+7 -6
View File
@@ -287,30 +287,31 @@ def get_embedding(
use_cache = Config.ENABLE_CACHE and asset_id is not None use_cache = Config.ENABLE_CACHE and asset_id is not None
cache = get_cache(Config.CACHE_DIR) if use_cache else None cache = get_cache(Config.CACHE_DIR) if use_cache else None
model_key = "immich" if entity_type == "face" else "siglip" # Use a single consistent cache key per model so lookups and stores always match.
# "immich" was previously used as the face key on the lookup path but "insightface"
# on the store path — meaning the cache was never hit for locally-computed embeddings.
cache_key = "insightface" if entity_type == "face" else "siglip"
# 1. Use Immich embedding if provided # 1. Use Immich embedding if provided
if immich_embedding is not None: if immich_embedding is not None:
if cache: if cache:
cache.put(asset_id, immich_embedding, model_key) cache.put(asset_id, immich_embedding, cache_key)
return immich_embedding return immich_embedding
# 2. Check disk cache # 2. Check disk cache
if cache: if cache:
cached = cache.get(asset_id, model_key) cached = cache.get(asset_id, cache_key)
if cached is not None: if cached is not None:
return cached return cached
# 3. Compute locally # 3. Compute locally
if entity_type == "face": if entity_type == "face":
emb = get_face_embedding(img_pil) emb = get_face_embedding(img_pil)
model_key = "insightface"
else: else:
emb = get_object_embedding(img_pil) emb = get_object_embedding(img_pil)
# Cache the result
if emb is not None and cache: if emb is not None and cache:
cache.put(asset_id, emb, model_key) cache.put(asset_id, emb, cache_key)
return emb return emb
+4 -2
View File
@@ -26,10 +26,12 @@ def setup_logging(verbose: bool = False) -> logging.Logger:
"""Configure logging with Rich console and file output.""" """Configure logging with Rich console and file output."""
level = logging.DEBUG if verbose else logging.INFO level = logging.DEBUG if verbose else logging.INFO
# Configure root logger # Configure root logger; close existing handlers before replacing them
root = logging.getLogger() root = logging.getLogger()
root.setLevel(level) root.setLevel(level)
root.handlers.clear() for h in root.handlers[:]:
h.close()
root.removeHandler(h)
# Rich console handler - uses shared console to avoid breaking progress bars # Rich console handler - uses shared console to avoid breaking progress bars
root.addHandler(RichHandler(rich_tracebacks=True, markup=True, console=console)) root.addHandler(RichHandler(rich_tracebacks=True, markup=True, console=console))