Files
winnow/winnow/cli.py
T
flanandClaude Sonnet 4.6 f50d3fe1ab refactor: rename logging.py, in-process scheduler, trim .gitignore
- winnow/logging.py → winnow/log_config.py: avoids shadowing the stdlib
  logging module; log file renamed from immich_export.log to winnow.log
- scheduler.py: run main() in-process instead of subprocess.run so
  InsightFace and SigLIP models stay resident in memory across scheduled
  runs (hundreds of MB load, previously reloaded every run)
- .gitignore: replaced 200-line boilerplate with ~30 project-relevant
  patterns; removed Django/Flask/Redis/RabbitMQ/Scrapy/etc. noise
- .python-version: untracked (redundant with requires-python in
  pyproject.toml; kept in .gitignore for local pyenv users)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 05:30:37 +00:00

96 lines
3.2 KiB
Python

"""Interactive CLI for winnow."""
import logging
import os
from rich import print as rprint
from rich.prompt import Confirm
from .config import Config, ConfigManager
from .executor import execute_jobs, upload_to_frigate
from .immich_api import get_people
from .jobs import _show_preview, auto_configure, interactive_configure
from .log_config import console, setup_logging
from .upload_tracker import get_person_summary, reset_person
logger = logging.getLogger(__name__)
def main() -> None:
"""Entry point for winnow CLI."""
try:
setup_logging(verbose=False)
console.print(r"""
[bold blue]winnow[/bold blue]
[dim]Immich -> Frigate Training Data Curator[/dim]
""")
ConfigManager.get().interactive_setup()
try:
Config.validate()
except ValueError as e:
rprint(f"[bold red]Configuration Error:[/bold red] {e}")
return
rprint(f"Server: [dim]{Config.IMMICH_URL}[/dim]")
rprint(f"Output: [dim]{Config.OUTPUT_DIR}[/dim]")
# Handle RESET_PERSON before anything else
reset_person_name = os.environ.get("RESET_PERSON", "").strip()
if reset_person_name:
reset_person(reset_person_name)
rprint(f"[bold yellow]Reset tracking data for: {reset_person_name}[/bold yellow]")
# Show per-person tracker summary if data exists
summary = get_person_summary()
if summary:
rprint("\n[dim]Tracker summary:[/dim]")
for person_name, counts in summary.items():
frigate_part = (
f", {counts['frigate_count']} in Frigate"
if counts.get("frigate_count") is not None
else ""
)
rprint(
f" [dim]{person_name}: {counts['uploaded']} uploaded,"
f" {counts['rejected']} rejected{frigate_part}[/dim]"
)
people = get_people()
if not people:
rprint("[bold red]Could not fetch people from Immich. Check URL/Key.[/bold red]")
return
# Check for non-interactive mode
auto_mode = os.environ.get("AUTO_MODE", "false").lower() == "true"
dry_run = os.environ.get("DRY_RUN", "false").lower() in ("true", "1", "yes")
if dry_run:
rprint("[bold yellow]DRY RUN — no images will be downloaded or uploaded[/bold yellow]")
if auto_mode:
rprint("[bold cyan]Running in AUTO mode (non-interactive)[/bold cyan]")
jobs = auto_configure(people)
else:
jobs = interactive_configure(people)
if jobs:
_show_preview(jobs)
if dry_run:
rprint("\n[bold yellow]Dry run complete — skipping execute and upload.[/bold yellow]")
elif auto_mode or Confirm.ask(f"Ready to process {sum(j['limit'] for j in jobs)} images?"):
execute_jobs(jobs)
upload_to_frigate(jobs)
rprint("\n[bold green]Done! Happy Training.[/bold green]")
else:
rprint("[yellow]No jobs configured.[/yellow]")
except KeyboardInterrupt:
rprint("\n[bold red]Aborted by user.[/bold red]")
if __name__ == "__main__":
main()