chore: remove remaining dead-mode references from executor, jobs, compose, Dockerfile
After removing the object pipeline, several dead 'mode' artifacts remained: - executor.py: unpack `config` from job even though it was no longer read - jobs.py: set `"mode": "face"` in both configure paths (key never consumed) - compose.yml: TRAINING_MODE=face env, OBJECT_CLASS comment, HF_HOME, stale MAX_AUTO_IMAGES default note - Dockerfile: "and object classification" label, /models/huggingface mkdir, HF_HOME ENV
This commit is contained in:
+3
-3
@@ -67,7 +67,7 @@ FROM base-${TARGETARCH}-${VARIANT} AS runtime
|
|||||||
ARG VARIANT=gpu
|
ARG VARIANT=gpu
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
LABEL org.opencontainers.image.title="winnow" \
|
LABEL org.opencontainers.image.title="winnow" \
|
||||||
org.opencontainers.image.description="Selects diverse, high-quality photos from Immich as training data for Frigate face recognition and object classification." \
|
org.opencontainers.image.description="Selects diverse, high-quality photos from Immich as training data for Frigate face recognition." \
|
||||||
org.opencontainers.image.source="https://github.com/sudolulo/winnow" \
|
org.opencontainers.image.source="https://github.com/sudolulo/winnow" \
|
||||||
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
|
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
|
||||||
org.opencontainers.image.version="${VERSION}"
|
org.opencontainers.image.version="${VERSION}"
|
||||||
@@ -116,7 +116,7 @@ https://repositories.intel.com/graphics/ubuntu jammy flex" \
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
RUN groupadd -g 568 apps && useradd -u 568 -g apps -m -s /bin/bash appuser \
|
RUN groupadd -g 568 apps && useradd -u 568 -g apps -m -s /bin/bash appuser \
|
||||||
&& mkdir -p /models/.insightface /models/huggingface \
|
&& mkdir -p /models/.insightface \
|
||||||
&& chown -R appuser:apps /app /models
|
&& chown -R appuser:apps /app /models
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -124,7 +124,7 @@ USER appuser
|
|||||||
# PYTHONPATH=/app makes the winnow package importable from the entry point script.
|
# PYTHONPATH=/app makes the winnow package importable from the entry point script.
|
||||||
# uv sync builds the wheel before winnow/ is COPY'd, so site-packages has only
|
# uv sync builds the wheel before winnow/ is COPY'd, so site-packages has only
|
||||||
# the dist-info. Explicitly adding /app lets Python find winnow/__init__.py there.
|
# the dist-info. Explicitly adding /app lets Python find winnow/__init__.py there.
|
||||||
ENV HF_HOME=/models/huggingface INSIGHTFACE_HOME=/models/.insightface PYTHONPATH=/app
|
ENV INSIGHTFACE_HOME=/models/.insightface PYTHONPATH=/app
|
||||||
|
|
||||||
HEALTHCHECK CMD test -f /app/entrypoint.sh || exit 1
|
HEALTHCHECK CMD test -f /app/entrypoint.sh || exit 1
|
||||||
ENTRYPOINT ["tini", "--", "/app/entrypoint.sh"]
|
ENTRYPOINT ["tini", "--", "/app/entrypoint.sh"]
|
||||||
|
|||||||
+1
-6
@@ -13,13 +13,9 @@ services:
|
|||||||
# Set AUTO_MODE=true to force auto mode in an interactive terminal.
|
# Set AUTO_MODE=true to force auto mode in an interactive terminal.
|
||||||
# To run interactively: docker exec -it winnow winnow
|
# To run interactively: docker exec -it winnow winnow
|
||||||
# - VERBOSE=true # Enable DEBUG-level console output
|
# - VERBOSE=true # Enable DEBUG-level console output
|
||||||
# TRAINING_MODE: face = upload to Frigate face recognition API
|
|
||||||
# object = save crops to output dir for manual Frigate placement
|
|
||||||
- TRAINING_MODE=face
|
|
||||||
# STRATEGY: auto = objective diversity (recommended), standard = 30 imgs, broad = 100 imgs
|
# STRATEGY: auto = objective diversity (recommended), standard = 30 imgs, broad = 100 imgs
|
||||||
- STRATEGY=auto
|
- STRATEGY=auto
|
||||||
# - LIMIT=50 # Custom image count; overrides STRATEGY preset
|
# - LIMIT=50 # Custom image count; overrides STRATEGY preset
|
||||||
# - OBJECT_CLASS=dog # Object label for object mode (e.g. dog, cat, car)
|
|
||||||
|
|
||||||
# ── People Filtering ──────────────────────────────────────────────────
|
# ── People Filtering ──────────────────────────────────────────────────
|
||||||
# - ONLY_PEOPLE=John,Jane # Comma-separated; process only these people
|
# - ONLY_PEOPLE=John,Jane # Comma-separated; process only these people
|
||||||
@@ -35,14 +31,13 @@ services:
|
|||||||
# - USE_FULL_RESOLUTION=true # Use full-res images vs thumbnails (default: true)
|
# - USE_FULL_RESOLUTION=true # Use full-res images vs thumbnails (default: true)
|
||||||
# - MIN_CONFIDENCE=0.7 # Minimum face detection confidence (default: 0.7)
|
# - MIN_CONFIDENCE=0.7 # Minimum face detection confidence (default: 0.7)
|
||||||
# - BLUR_THRESHOLD=100.0 # Laplacian blur threshold; lower = accept more blur (default: 100.0)
|
# - BLUR_THRESHOLD=100.0 # Laplacian blur threshold; lower = accept more blur (default: 100.0)
|
||||||
# - MAX_AUTO_IMAGES=80 # Hard cap on auto-diversity selection (default: 80)
|
# - MAX_AUTO_IMAGES=80 # Hard cap on auto-diversity selection (default: 20)
|
||||||
|
|
||||||
# ── Caching & Models ──────────────────────────────────────────────────
|
# ── Caching & Models ──────────────────────────────────────────────────
|
||||||
# - FORCE_CPU=true # Disable GPU, fall back to CPU
|
# - FORCE_CPU=true # Disable GPU, fall back to CPU
|
||||||
# - OPENVINO_DEVICE=GPU # Intel variant only: use Arc/iGPU instead of CPU (default: CPU)
|
# - OPENVINO_DEVICE=GPU # Intel variant only: use Arc/iGPU instead of CPU (default: CPU)
|
||||||
# - ENABLE_CACHE=false # Disable embedding cache (default: true)
|
# - ENABLE_CACHE=false # Disable embedding cache (default: true)
|
||||||
- CACHE_DIR=/app/.if_cache
|
- CACHE_DIR=/app/.if_cache
|
||||||
- HF_HOME=/models/huggingface
|
|
||||||
- INSIGHTFACE_HOME=/models/.insightface
|
- INSIGHTFACE_HOME=/models/.insightface
|
||||||
|
|
||||||
# ── Tracker overrides (one-shot, remove after use) ────────────────────
|
# ── Tracker overrides (one-shot, remove after use) ────────────────────
|
||||||
|
|||||||
+20
-22
@@ -173,11 +173,11 @@ def execute_jobs(jobs: list[dict]) -> None:
|
|||||||
|
|
||||||
use_full_res = Config.USE_FULL_RESOLUTION
|
use_full_res = Config.USE_FULL_RESOLUTION
|
||||||
|
|
||||||
# Load InsightFace app for landmark-based crop alignment (face mode only).
|
# Load InsightFace app for landmark-based crop alignment.
|
||||||
# The model is already resident from the diversity/embedding phase, so this
|
# The model is already resident from the diversity/embedding phase, so this
|
||||||
# is just a singleton lookup — no load cost.
|
# is just a singleton lookup — no load cost.
|
||||||
insightface_app = None
|
insightface_app = None
|
||||||
if any(j["config"].get("mode", "face") == "face" for j in jobs) and Config.ENABLE_FACE_ALIGNMENT:
|
if Config.ENABLE_FACE_ALIGNMENT:
|
||||||
try:
|
try:
|
||||||
from .embeddings import get_insightface_app
|
from .embeddings import get_insightface_app
|
||||||
|
|
||||||
@@ -196,8 +196,8 @@ def execute_jobs(jobs: list[dict]) -> None:
|
|||||||
overall_task = progress.add_task("[green]Overall Progress", total=grand_total)
|
overall_task = progress.add_task("[green]Overall Progress", total=grand_total)
|
||||||
|
|
||||||
for job in jobs:
|
for job in jobs:
|
||||||
person, assets, config = job["person"], job["assets"], job["config"]
|
person, assets = job["person"], job["assets"]
|
||||||
name, mode = person["name"], config.get("mode", "face")
|
name = person["name"]
|
||||||
|
|
||||||
job_task = progress.add_task(f"Processing {name}...", total=len(assets))
|
job_task = progress.add_task(f"Processing {name}...", total=len(assets))
|
||||||
try:
|
try:
|
||||||
@@ -206,8 +206,7 @@ def execute_jobs(jobs: list[dict]) -> None:
|
|||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
continue
|
continue
|
||||||
# Face crops are transient (uploaded then discarded); wipe before each run.
|
# Face crops are transient (uploaded then discarded); wipe before each run.
|
||||||
# Object crops are the deliverable; preserve them across runs.
|
if os.path.isdir(person_dir):
|
||||||
if mode == "face" and os.path.isdir(person_dir):
|
|
||||||
shutil.rmtree(person_dir)
|
shutil.rmtree(person_dir)
|
||||||
os.makedirs(person_dir, exist_ok=True)
|
os.makedirs(person_dir, exist_ok=True)
|
||||||
|
|
||||||
@@ -219,22 +218,21 @@ def execute_jobs(jobs: list[dict]) -> None:
|
|||||||
count = 0
|
count = 0
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
try:
|
try:
|
||||||
# For face mode, enrich the asset with face bounding box data
|
# Enrich the asset with face bounding box data from the Immich
|
||||||
# from the Immich faces API (not included in search/metadata results)
|
# faces API (not included in search/metadata results).
|
||||||
if mode == "face":
|
asset = _enrich_asset_with_face_data(asset, person)
|
||||||
asset = _enrich_asset_with_face_data(asset, person)
|
# Skip download if detection confidence already disqualifies
|
||||||
# Skip download if detection confidence already disqualifies
|
# the asset — avoids fetching a large image we'll discard.
|
||||||
# the asset — avoids fetching a large image we'll discard.
|
conf = asset.get("face_confidence")
|
||||||
conf = asset.get("face_confidence")
|
if conf is not None and conf < Config.MIN_CONFIDENCE:
|
||||||
if conf is not None and conf < Config.MIN_CONFIDENCE:
|
progress.console.print(
|
||||||
progress.console.print(
|
f"[yellow]Skipped {asset['id']}"
|
||||||
f"[yellow]Skipped {asset['id']}"
|
f" (detection confidence {conf:.2f} < {Config.MIN_CONFIDENCE})[/yellow]"
|
||||||
f" (detection confidence {conf:.2f} < {Config.MIN_CONFIDENCE})[/yellow]"
|
)
|
||||||
)
|
mark_rejected(asset["id"], person_name=name)
|
||||||
mark_rejected(asset["id"], person_name=name)
|
progress.advance(job_task)
|
||||||
progress.advance(job_task)
|
progress.advance(overall_task)
|
||||||
progress.advance(overall_task)
|
continue
|
||||||
continue
|
|
||||||
|
|
||||||
# Use full-resolution for final output when configured
|
# Use full-resolution for final output when configured
|
||||||
if use_full_res:
|
if use_full_res:
|
||||||
|
|||||||
+2
-2
@@ -125,7 +125,7 @@ def _configure_person(person: dict, people: list[dict]) -> dict | None:
|
|||||||
name = person["name"]
|
name = person["name"]
|
||||||
console.print(f"\nSelected: [bold green]{name}[/bold green]")
|
console.print(f"\nSelected: [bold green]{name}[/bold green]")
|
||||||
|
|
||||||
config = {"name": name, "mode": "face", "quality_replacement": Config.QUALITY_REPLACEMENT}
|
config = {"name": name, "quality_replacement": Config.QUALITY_REPLACEMENT}
|
||||||
|
|
||||||
# Fetch and filter assets
|
# Fetch and filter assets
|
||||||
years = IntPrompt.ask("Filter images older than (years)", default=Config.YEARS_FILTER)
|
years = IntPrompt.ask("Filter images older than (years)", default=Config.YEARS_FILTER)
|
||||||
@@ -240,7 +240,7 @@ def auto_configure(people: list[dict]) -> list[dict]:
|
|||||||
jobs = []
|
jobs = []
|
||||||
for person in valid_people:
|
for person in valid_people:
|
||||||
name = person["name"]
|
name = person["name"]
|
||||||
config = {"name": name, "mode": "face"}
|
config = {"name": name}
|
||||||
|
|
||||||
all_assets = fetch_all_assets(person)
|
all_assets = fetch_all_assets(person)
|
||||||
recent_assets = filter_recent_assets(all_assets, years=Config.YEARS_FILTER)
|
recent_assets = filter_recent_assets(all_assets, years=Config.YEARS_FILTER)
|
||||||
|
|||||||
Reference in New Issue
Block a user