From 71f1924f1a8cedbc8fad8756fb3c6e6bd747194c Mon Sep 17 00:00:00 2001 From: Holden Date: Sun, 14 Jun 2026 20:14:27 +0000 Subject: [PATCH] fix: restore original dedup/prompt order in jobs.py, move time import to module level in reconcile.py _build_job no longer calls filter_already_uploaded internally; callers pass pre-filtered assets so there's no double DB hit and the interactive path restores the original prompt order (retry_rejected asked before strategy, so post-dedup count informs the choice). Skip-count rprint restored in auto_configure. Late 'import time' inside reconcile_frigate_mappings moved to module level. --- winnow/jobs.py | 74 +++++++++++++++++++++------------------------ winnow/reconcile.py | 3 +- 2 files changed, 36 insertions(+), 41 deletions(-) diff --git a/winnow/jobs.py b/winnow/jobs.py index 2a986e7..8e332fc 100644 --- a/winnow/jobs.py +++ b/winnow/jobs.py @@ -122,19 +122,16 @@ def _perform_selection( def _build_job( person: dict, - recent_assets: list, + assets: list, limit: int | str, selection_mode: str, - retry_rejected: bool = False, quality_replacement: bool = False, ) -> dict | None: - """Build a job dict from pre-fetched assets and parameters. No I/O.""" - name = person["name"] - new_asset_ids = set(filter_already_uploaded([a["id"] for a in recent_assets], retry_rejected=retry_rejected)) - new_assets = [a for a in recent_assets if a["id"] in new_asset_ids] - if not new_assets: + """Select from pre-filtered assets and build a job dict. No terminal I/O.""" + if not assets: return None - selected = _perform_selection(new_assets, limit, name, selection_mode, person_id=person["id"]) + name = person["name"] + selected = _perform_selection(assets, limit, name, selection_mode, person_id=person["id"]) if not selected: return None return { @@ -150,7 +147,6 @@ def _configure_person(person: dict, people: list[dict]) -> dict | None: name = person["name"] console.print(f"\nSelected: [bold green]{name}[/bold green]") - # Fetch and filter assets years = IntPrompt.ask("Filter images older than (years)", default=Config.YEARS_FILTER) console.print(f"Scanning for {name}...") @@ -160,36 +156,32 @@ def _configure_person(person: dict, people: list[dict]) -> dict | None: rprint(f" Found [bold]{len(all_assets)}[/bold] total, [bold]{len(recent_assets)}[/bold] in range ({years} years).") - # Strategy selection + # Ask before strategy so the post-dedup count can inform the choice + retry_env = os.environ.get("RETRY_REJECTED", "false").lower() in ("true", "1", "yes") + retry_rejected = Confirm.ask("Include previously rejected images?", default=retry_env) + + before_dedup = len(recent_assets) + new_asset_ids = set(filter_already_uploaded([a["id"] for a in recent_assets], retry_rejected=retry_rejected)) + recent_assets = [a for a in recent_assets if a["id"] in new_asset_ids] + skipped = before_dedup - len(recent_assets) + if skipped: + rprint(f" [dim]Skipped {skipped} assets already uploaded to Frigate.[/dim]") + + if not recent_assets: + rprint(" [dim]Skipping (0 new images after dedup).[/dim]") + return None + has_embedding = is_embedding_available() rprint(f"\n[bold cyan]Select Training Strategy for {name}:[/bold cyan]") - limit, selection_mode = _get_strategy_choice(has_embedding) if selection_mode == "skip": return None - # In interactive mode, ask about retry_rejected — use env var as default - retry_env = os.environ.get("RETRY_REJECTED", "false").lower() in ("true", "1", "yes") - retry_rejected = Confirm.ask("Include previously rejected images?", default=retry_env) - - job = _build_job( - person, - recent_assets, - limit, - selection_mode, - retry_rejected=retry_rejected, - quality_replacement=Config.QUALITY_REPLACEMENT, - ) + job = _build_job(person, recent_assets, limit, selection_mode, quality_replacement=Config.QUALITY_REPLACEMENT) if job is None: - rprint(" [dim]Skipping (0 new images after dedup or selection).[/dim]") + rprint(" [dim]Skipping (0 images selected).[/dim]") return None - # Show how many were skipped (dedup info) - new_asset_ids = set(filter_already_uploaded([a["id"] for a in recent_assets], retry_rejected=retry_rejected)) - skipped = len(recent_assets) - len(new_asset_ids) - if skipped: - rprint(f" [dim]Skipped {skipped} assets already uploaded to Frigate.[/dim]") - rprint(f" [green]Queued {job['limit']} images for {name}.[/green]") return job @@ -312,16 +304,20 @@ def auto_configure(people: list[dict]) -> list[dict]: continue retry_rejected = os.environ.get("RETRY_REJECTED", "false").lower() in ("true", "1", "yes") - job = _build_job( - person, - recent_assets, - limit, - selection_mode, - retry_rejected=retry_rejected, - quality_replacement=quality_replacement, - ) + before_dedup = len(recent_assets) + new_asset_ids = set(filter_already_uploaded([a["id"] for a in recent_assets], retry_rejected=retry_rejected)) + recent_assets = [a for a in recent_assets if a["id"] in new_asset_ids] + skipped = before_dedup - len(recent_assets) + if skipped: + rprint(f" [dim]Skipped {skipped} assets already uploaded to Frigate.[/dim]") + + if not recent_assets: + rprint(f" [dim]Skipping {name} (0 new images after dedup).[/dim]") + continue + + job = _build_job(person, recent_assets, limit, selection_mode, quality_replacement=quality_replacement) if job is None: - rprint(f" [dim]Skipping {name} (0 new images after dedup or selection).[/dim]") + rprint(f" [dim]Skipping {name} (0 images selected).[/dim]") continue # Apply auto_cap post-selection if needed diff --git a/winnow/reconcile.py b/winnow/reconcile.py index 3c9e230..d3e9ab9 100644 --- a/winnow/reconcile.py +++ b/winnow/reconcile.py @@ -1,6 +1,7 @@ """Frigate upload post-processing: reconciliation and asset enrichment.""" import logging +import time from .frigate_api import get_frigate_person_files from .immich_api import fetch_face_data @@ -30,8 +31,6 @@ def reconcile_frigate_mappings( diffing. Until then, the external-upload guard keeps mappings correct at the cost of occasionally missing them when another client is active. """ - import time - target = len(uploaded) current_files: set[str] = set()