diff --git a/Dockerfile b/Dockerfile index 57ba318..798b3f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -75,7 +75,7 @@ COPY --from=build /usr/local/bin/uv /usr/local/bin/uv # Register every nvidia pip-package lib/ directory with ldconfig so that # onnxruntime-gpu and torch can find libcudnn, libcublas, libcufft, etc. # without a hand-maintained LD_LIBRARY_PATH. Skipped silently on cpu builds. -RUN find /app/.venv/lib/python3.13/site-packages/nvidia -type d -name "lib" \ +RUN find /app/.venv/lib/python3.*/site-packages/nvidia -type d -name "lib" \ 2>/dev/null > /etc/ld.so.conf.d/nvidia-pip.conf && ldconfig || true RUN groupadd -g 568 apps && useradd -u 568 -g apps -m -s /bin/bash appuser \ diff --git a/scheduler.py b/scheduler.py index c3a08a6..c960fbf 100644 --- a/scheduler.py +++ b/scheduler.py @@ -49,4 +49,4 @@ while True: logger.error(f"winnow run failed: {e}", exc_info=True) print(f"winnow run failed: {e}", flush=True) next_run = cron.get_next(float) - time.sleep(60) + time.sleep(max(1, next_run - time.time())) diff --git a/winnow/diversity.py b/winnow/diversity.py index 2c7cae6..1c6e1e0 100644 --- a/winnow/diversity.py +++ b/winnow/diversity.py @@ -105,7 +105,9 @@ def _get_face_confidence(asset: dict, person_id: str | None = None) -> float | N continue faces = person.get("faces", []) if faces: - return faces[0].get("score") or faces[0].get("confidence") + f = faces[0] + score = f.get("score") + return score if score is not None else f.get("confidence") return None @@ -136,8 +138,10 @@ def _crop_face_from_thumbnail( x1, y1, x2, y2 = bbox img_w, img_h = img.size - # Get metadata dimensions to scale bbox + # Get metadata dimensions to scale bbox — must match the same person as _get_face_bbox for person in asset.get("people", []): + if person_id and person.get("id") != person_id: + continue faces = person.get("faces", []) if faces: meta_w = faces[0].get("imageWidth") or img_w diff --git a/winnow/embeddings.py b/winnow/embeddings.py index ff20c80..7be0063 100644 --- a/winnow/embeddings.py +++ b/winnow/embeddings.py @@ -33,8 +33,10 @@ def _suppress_output(): os.dup2(devnull_fd, 2) yield finally: - os.dup2(saved_out, 1) - os.dup2(saved_err, 2) + try: + os.dup2(saved_out, 1) + finally: + os.dup2(saved_err, 2) os.close(devnull_fd) os.close(saved_out) os.close(saved_err) diff --git a/winnow/immich_api.py b/winnow/immich_api.py index 4e87be6..2473813 100644 --- a/winnow/immich_api.py +++ b/winnow/immich_api.py @@ -140,10 +140,11 @@ def fetch_face_data(asset_id: str, person_id: str | None = None) -> FaceData | N face.get("boundingBoxY2", 0), ) + score = face.get("score") return FaceData( embedding=embedding, bbox=bbox, - confidence=face.get("score") or face.get("confidence"), + confidence=score if score is not None else face.get("confidence"), image_width=face.get("imageWidth", 0), image_height=face.get("imageHeight", 0), ) diff --git a/winnow/jobs.py b/winnow/jobs.py index ebf0989..6783c82 100644 --- a/winnow/jobs.py +++ b/winnow/jobs.py @@ -299,10 +299,13 @@ def auto_configure(people: list[dict]) -> list[dict]: has_embedding = is_embedding_available(entity_type) limit, selection_mode = _resolve_strategy(strategy, has_embedding) - # Cap selection to remaining capacity + # Cap selection to remaining capacity. + # For auto mode with partial training, keep "auto" so adaptive stopping + # still runs — just trim the result to the remaining capacity afterward. + auto_cap = None if limit == "auto": if already_uploaded > 0: - limit = capacity # partially filled — select exactly what remains + auto_cap = capacity else: limit = min(limit, capacity) @@ -312,6 +315,8 @@ def auto_configure(people: list[dict]) -> list[dict]: selected_assets = _perform_selection( recent_assets, limit, name, selection_mode, entity_type, person_id=person["id"] ) + if auto_cap is not None: + selected_assets = selected_assets[:auto_cap] if selected_assets: rprint(f" [green]Queued {len(selected_assets)} images for {name}.[/green]")