refactor: rename project to winnow
- Rename Python package directory if_curator/ → winnow/ - Update all imports, entry points, and CLI references - Update pyproject.toml: name, scripts, package list, repository URL - Update Dockerfile, compose.yml, entrypoint.sh, scheduler.py - Update GitHub Actions workflow image names - Update README Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: sudolulo/if-curator-headless
|
IMAGE_NAME: sudolulo/winnow
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
verify-lockfile:
|
verify-lockfile:
|
||||||
|
|||||||
@@ -136,6 +136,6 @@ jobs:
|
|||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/sudolulo/if-curator-headless:latest
|
ghcr.io/sudolulo/winnow:latest
|
||||||
ghcr.io/sudolulo/if-curator-headless:${{ steps.tag.outputs.TAG }}
|
ghcr.io/sudolulo/winnow:${{ steps.tag.outputs.TAG }}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ COPY pyproject.toml uv.lock ./
|
|||||||
RUN uv sync --frozen --no-dev \
|
RUN uv sync --frozen --no-dev \
|
||||||
&& uv cache clean
|
&& uv cache clean
|
||||||
|
|
||||||
COPY if_curator/ if_curator/
|
COPY winnow/ winnow/
|
||||||
COPY entrypoint.sh scheduler.py ./
|
COPY entrypoint.sh scheduler.py ./
|
||||||
RUN chmod +x /app/entrypoint.sh
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[](https://github.com/sudolulo/if_curator_headless/actions/workflows/docker-publish.yml) [](https://github.com/sudolulo/if_curator_headless/actions/workflows/release.yml) [](https://github.com/sudolulo/if_curator_headless/actions/workflows/lint.yml) [](https://github.com/sudolulo/if_curator_headless/actions/workflows/test.yml)
|
[](https://github.com/sudolulo/winnow/actions/workflows/docker-publish.yml) [](https://github.com/sudolulo/winnow/actions/workflows/release.yml) [](https://github.com/sudolulo/winnow/actions/workflows/lint.yml) [](https://github.com/sudolulo/winnow/actions/workflows/test.yml)
|
||||||
|
|
||||||
# if-curator-headless
|
# winnow
|
||||||
|
|
||||||
`if-curator-headless` pulls photos of people and objects from your [Immich](https://immich.app) library, selects the most diverse and highest-quality subset using AI embeddings, and delivers them as training data for [Frigate](https://frigate.video)'s face recognition and object classification models.
|
`winnow` pulls photos of people and objects from your [Immich](https://immich.app) library, selects the most diverse and highest-quality subset using AI embeddings, and delivers them as training data for [Frigate](https://frigate.video)'s face recognition and object classification models.
|
||||||
|
|
||||||
It runs fully headless in Docker, is configured entirely through environment variables, and can run on a schedule — no interactive prompts, no manual steps.
|
It runs fully headless in Docker, is configured entirely through environment variables, and can run on a schedule — no interactive prompts, no manual steps.
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ Frigate's face recognition model (ArcFace) and object classifier are only as goo
|
|||||||
|
|
||||||
If you upload 100 photos from the same week, the model learns the lighting in your living room and the jacket you wore that month. It struggles the moment anything changes. What you actually want is a spread: different years, different lighting conditions, different angles, different contexts.
|
If you upload 100 photos from the same week, the model learns the lighting in your living room and the jacket you wore that month. It struggles the moment anything changes. What you actually want is a spread: different years, different lighting conditions, different angles, different contexts.
|
||||||
|
|
||||||
Finding that spread manually across a library of thousands of photos is not practical. `if-curator-headless` does it automatically.
|
Finding that spread manually across a library of thousands of photos is not practical. `winnow` does it automatically.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -103,8 +103,8 @@ If the embedding model is unavailable, the tool falls back to **time spread**: e
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
if-curator:
|
winnow:
|
||||||
image: ghcr.io/sudolulo/if-curator-headless:latest
|
image: ghcr.io/sudolulo/winnow:latest
|
||||||
environment:
|
environment:
|
||||||
- IMMICH_URL=http://192.168.1.10:2283
|
- IMMICH_URL=http://192.168.1.10:2283
|
||||||
- API_KEY=your-immich-api-key
|
- API_KEY=your-immich-api-key
|
||||||
@@ -204,10 +204,10 @@ The first run after a fresh install downloads the embedding models (~1-2 GB). Su
|
|||||||
## Local Install
|
## Local Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/sudolulo/if_curator_headless.git
|
git clone https://github.com/sudolulo/winnow.git
|
||||||
cd if_curator_headless
|
cd winnow
|
||||||
uv sync
|
uv sync
|
||||||
uv run if-curator
|
uv run winnow
|
||||||
```
|
```
|
||||||
|
|
||||||
Requires Python 3.12+ and [uv](https://astral.sh/uv/). An NVIDIA GPU is strongly recommended — CPU mode works but embedding computation is significantly slower.
|
Requires Python 3.12+ and [uv](https://astral.sh/uv/). An NVIDIA GPU is strongly recommended — CPU mode works but embedding computation is significantly slower.
|
||||||
@@ -225,4 +225,4 @@ Requires Python 3.12+ and [uv](https://astral.sh/uv/). An NVIDIA GPU is strongly
|
|||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
Based on [if-curator](https://github.com/ds-sebastian/if_curator) by Sebastian, licensed MIT.
|
Based on [winnow](https://github.com/ds-sebastian/if_curator) by Sebastian, licensed MIT.
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
if-curator:
|
||||||
|
build: .
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- /code/if-curator/models:/models
|
||||||
|
- /code/if-curator/embeddings:/app/.if_cache
|
||||||
|
- /code/if-curator/output:/app/output
|
||||||
+7
-7
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
if-curator:
|
winnow:
|
||||||
image: ghcr.io/sudolulo/if-curator-headless:latest
|
image: ghcr.io/sudolulo/winnow:latest
|
||||||
container_name: if-curator
|
container_name: winnow
|
||||||
environment:
|
environment:
|
||||||
# ── Required ──────────────────────────────────────────────────────────
|
# ── Required ──────────────────────────────────────────────────────────
|
||||||
- IMMICH_URL=${IMMICH_URL}
|
- IMMICH_URL=${IMMICH_URL}
|
||||||
@@ -52,10 +52,10 @@ services:
|
|||||||
# - CRON_SCHEDULE=0 3 1 * *
|
# - CRON_SCHEDULE=0 3 1 * *
|
||||||
# - CRON_SCHEDULE=*/30 * * * *
|
# - CRON_SCHEDULE=*/30 * * * *
|
||||||
volumes:
|
volumes:
|
||||||
# Replace <pool> with your TrueNAS pool name, e.g. /mnt/tank/if-curator/...
|
# Replace <pool> with your TrueNAS pool name, e.g. /mnt/tank/winnow/...
|
||||||
- /mnt/<pool>/if-curator/models:/models
|
- /mnt/<pool>/winnow/models:/models
|
||||||
- /mnt/<pool>/if-curator/embeddings:/app/.if_cache
|
- /mnt/<pool>/winnow/embeddings:/app/.if_cache
|
||||||
- /mnt/<pool>/if-curator/output:/app/frigate_train
|
- /mnt/<pool>/winnow/output:/app/frigate_train
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ export PYTHONUNBUFFERED=1
|
|||||||
|
|
||||||
# 1. Run the job immediately on startup
|
# 1. Run the job immediately on startup
|
||||||
echo "▶ Running on startup..."
|
echo "▶ Running on startup..."
|
||||||
uv run python -m if_curator.cli
|
uv run python -m winnow.cli
|
||||||
|
|
||||||
# 2. If a schedule exists, start the scheduler
|
# 2. If a schedule exists, start the scheduler
|
||||||
if [ -n "${CRON_SCHEDULE:-}" ]; then
|
if [ -n "${CRON_SCHEDULE:-}" ]; then
|
||||||
|
|||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "if-curator"
|
name = "winnow"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Immich to Frigate training sets"
|
description = "Immich to Frigate training sets"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -30,10 +30,10 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
if-curator = "if_curator.cli:main"
|
winnow = "winnow.cli:main"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Repository = "https://github.com/sudolulo/if_curator_headless"
|
Repository = "https://github.com/sudolulo/winnow"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
override-dependencies = ["onnxruntime-gpu>=1.23.2"]
|
override-dependencies = ["onnxruntime-gpu>=1.23.2"]
|
||||||
@@ -62,7 +62,7 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["if_curator"]
|
packages = ["winnow"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
|
|||||||
+6
-6
@@ -35,7 +35,7 @@ def check_models():
|
|||||||
print(" ✅ HuggingFace models: present", flush=True)
|
print(" ✅ HuggingFace models: present", flush=True)
|
||||||
else:
|
else:
|
||||||
print(" ⬇️ HuggingFace models: not found — will download", flush=True)
|
print(" ⬇️ HuggingFace models: not found — will download", flush=True)
|
||||||
print("🚀 Starting if-curator...", flush=True)
|
print("🚀 Starting winnow...", flush=True)
|
||||||
|
|
||||||
|
|
||||||
NOW = time.time()
|
NOW = time.time()
|
||||||
@@ -45,14 +45,14 @@ next_run = cron.get_next(float)
|
|||||||
while True:
|
while True:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now >= next_run:
|
if now >= next_run:
|
||||||
print(f"\n▶ [{time.strftime('%Y-%m-%d %H:%M:%S')}] Starting if-curator...", flush=True)
|
print(f"\n▶ [{time.strftime('%Y-%m-%d %H:%M:%S')}] Starting winnow...", flush=True)
|
||||||
check_models()
|
check_models()
|
||||||
result = subprocess.run(["uv", "run", "if-curator"], env=RUN_ENV)
|
result = subprocess.run(["uv", "run", "winnow"], env=RUN_ENV)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
logger.error(f"if-curator exited with code {result.returncode}")
|
logger.error(f"winnow exited with code {result.returncode}")
|
||||||
print(f"❌ if-curator failed with exit code {result.returncode}", flush=True)
|
print(f"❌ winnow failed with exit code {result.returncode}", flush=True)
|
||||||
else:
|
else:
|
||||||
print("✅ if-curator completed successfully", flush=True)
|
print("✅ winnow completed successfully", flush=True)
|
||||||
next_run = cron.get_next(float)
|
next_run = cron.get_next(float)
|
||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ def test_config_loads_defaults(monkeypatch):
|
|||||||
monkeypatch.setenv("IMMICH_URL", "http://test:2283")
|
monkeypatch.setenv("IMMICH_URL", "http://test:2283")
|
||||||
monkeypatch.setenv("API_KEY", "test-key")
|
monkeypatch.setenv("API_KEY", "test-key")
|
||||||
|
|
||||||
from if_curator.config import _Config
|
from winnow.config import _Config
|
||||||
|
|
||||||
_Config.reset()
|
_Config.reset()
|
||||||
cfg = _Config()
|
cfg = _Config()
|
||||||
@@ -46,7 +46,7 @@ def test_config_env_overrides(monkeypatch):
|
|||||||
monkeypatch.setenv("ENABLE_FACE_ALIGNMENT", "false")
|
monkeypatch.setenv("ENABLE_FACE_ALIGNMENT", "false")
|
||||||
monkeypatch.setenv("ENABLE_CACHE", "true")
|
monkeypatch.setenv("ENABLE_CACHE", "true")
|
||||||
|
|
||||||
from if_curator.config import _Config
|
from winnow.config import _Config
|
||||||
|
|
||||||
_Config.reset()
|
_Config.reset()
|
||||||
cfg = _Config()
|
cfg = _Config()
|
||||||
@@ -69,7 +69,7 @@ def test_get_headers_returns_api_key(monkeypatch):
|
|||||||
monkeypatch.setenv("IMMICH_URL", "http://test:2283")
|
monkeypatch.setenv("IMMICH_URL", "http://test:2283")
|
||||||
monkeypatch.setenv("API_KEY", "my-secret-key")
|
monkeypatch.setenv("API_KEY", "my-secret-key")
|
||||||
|
|
||||||
from if_curator.config import _Config, get_headers
|
from winnow.config import _Config, get_headers
|
||||||
|
|
||||||
_Config.reset()
|
_Config.reset()
|
||||||
headers = get_headers()
|
headers = get_headers()
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ def _asset(days_ago: int) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def test_filter_recent_keeps_new_assets(monkeypatch):
|
def test_filter_recent_keeps_new_assets(monkeypatch):
|
||||||
from if_curator.immich_api import filter_recent_assets
|
from winnow.immich_api import filter_recent_assets
|
||||||
assets = [_asset(1), _asset(30), _asset(365 * 5)]
|
assets = [_asset(1), _asset(30), _asset(365 * 5)]
|
||||||
result = filter_recent_assets(assets, years=10)
|
result = filter_recent_assets(assets, years=10)
|
||||||
assert len(result) == 3
|
assert len(result) == 3
|
||||||
|
|
||||||
|
|
||||||
def test_filter_recent_removes_old_assets(monkeypatch):
|
def test_filter_recent_removes_old_assets(monkeypatch):
|
||||||
from if_curator.immich_api import filter_recent_assets
|
from winnow.immich_api import filter_recent_assets
|
||||||
old = _asset(365 * 15)
|
old = _asset(365 * 15)
|
||||||
recent = _asset(10)
|
recent = _asset(10)
|
||||||
result = filter_recent_assets([old, recent], years=10)
|
result = filter_recent_assets([old, recent], years=10)
|
||||||
@@ -25,7 +25,7 @@ def test_filter_recent_removes_old_assets(monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
def test_filter_recent_boundary(monkeypatch):
|
def test_filter_recent_boundary(monkeypatch):
|
||||||
from if_curator.immich_api import filter_recent_assets
|
from winnow.immich_api import filter_recent_assets
|
||||||
just_inside = _asset(365 * 10 - 1)
|
just_inside = _asset(365 * 10 - 1)
|
||||||
just_outside = _asset(365 * 10 + 1)
|
just_outside = _asset(365 * 10 + 1)
|
||||||
result = filter_recent_assets([just_inside, just_outside], years=10)
|
result = filter_recent_assets([just_inside, just_outside], years=10)
|
||||||
@@ -34,7 +34,7 @@ def test_filter_recent_boundary(monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
def test_filter_recent_skips_missing_date():
|
def test_filter_recent_skips_missing_date():
|
||||||
from if_curator.immich_api import filter_recent_assets
|
from winnow.immich_api import filter_recent_assets
|
||||||
assets = [{"id": "no-date"}, _asset(5)]
|
assets = [{"id": "no-date"}, _asset(5)]
|
||||||
result = filter_recent_assets(assets, years=10)
|
result = filter_recent_assets(assets, years=10)
|
||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
@@ -42,12 +42,12 @@ def test_filter_recent_skips_missing_date():
|
|||||||
|
|
||||||
|
|
||||||
def test_filter_recent_skips_bad_date():
|
def test_filter_recent_skips_bad_date():
|
||||||
from if_curator.immich_api import filter_recent_assets
|
from winnow.immich_api import filter_recent_assets
|
||||||
assets = [{"id": "bad", "fileCreatedAt": "not-a-date"}, _asset(5)]
|
assets = [{"id": "bad", "fileCreatedAt": "not-a-date"}, _asset(5)]
|
||||||
result = filter_recent_assets(assets, years=10)
|
result = filter_recent_assets(assets, years=10)
|
||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_filter_recent_empty_list():
|
def test_filter_recent_empty_list():
|
||||||
from if_curator.immich_api import filter_recent_assets
|
from winnow.immich_api import filter_recent_assets
|
||||||
assert filter_recent_assets([], years=10) == []
|
assert filter_recent_assets([], years=10) == []
|
||||||
|
|||||||
+8
-8
@@ -11,21 +11,21 @@ def clean_env(monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
def test_resolve_strategy_default_auto():
|
def test_resolve_strategy_default_auto():
|
||||||
from if_curator.jobs import _resolve_strategy
|
from winnow.jobs import _resolve_strategy
|
||||||
limit, mode = _resolve_strategy("auto", has_embedding=True)
|
limit, mode = _resolve_strategy("auto", has_embedding=True)
|
||||||
assert limit == "auto"
|
assert limit == "auto"
|
||||||
assert mode == "smart"
|
assert mode == "smart"
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_strategy_standard():
|
def test_resolve_strategy_standard():
|
||||||
from if_curator.jobs import _resolve_strategy
|
from winnow.jobs import _resolve_strategy
|
||||||
limit, mode = _resolve_strategy("standard", has_embedding=True)
|
limit, mode = _resolve_strategy("standard", has_embedding=True)
|
||||||
assert limit == 30
|
assert limit == 30
|
||||||
assert mode == "smart"
|
assert mode == "smart"
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_strategy_broad():
|
def test_resolve_strategy_broad():
|
||||||
from if_curator.jobs import _resolve_strategy
|
from winnow.jobs import _resolve_strategy
|
||||||
limit, mode = _resolve_strategy("broad", has_embedding=True)
|
limit, mode = _resolve_strategy("broad", has_embedding=True)
|
||||||
assert limit == 100
|
assert limit == 100
|
||||||
assert mode == "smart"
|
assert mode == "smart"
|
||||||
@@ -33,7 +33,7 @@ def test_resolve_strategy_broad():
|
|||||||
|
|
||||||
def test_resolve_strategy_custom_limit_env(monkeypatch):
|
def test_resolve_strategy_custom_limit_env(monkeypatch):
|
||||||
monkeypatch.setenv("LIMIT", "50")
|
monkeypatch.setenv("LIMIT", "50")
|
||||||
from if_curator.jobs import _resolve_strategy
|
from winnow.jobs import _resolve_strategy
|
||||||
limit, mode = _resolve_strategy("auto", has_embedding=True)
|
limit, mode = _resolve_strategy("auto", has_embedding=True)
|
||||||
assert limit == 50
|
assert limit == 50
|
||||||
assert mode == "smart"
|
assert mode == "smart"
|
||||||
@@ -41,13 +41,13 @@ def test_resolve_strategy_custom_limit_env(monkeypatch):
|
|||||||
|
|
||||||
def test_resolve_strategy_limit_overrides_strategy(monkeypatch):
|
def test_resolve_strategy_limit_overrides_strategy(monkeypatch):
|
||||||
monkeypatch.setenv("LIMIT", "25")
|
monkeypatch.setenv("LIMIT", "25")
|
||||||
from if_curator.jobs import _resolve_strategy
|
from winnow.jobs import _resolve_strategy
|
||||||
limit, mode = _resolve_strategy("broad", has_embedding=True)
|
limit, mode = _resolve_strategy("broad", has_embedding=True)
|
||||||
assert limit == 25
|
assert limit == 25
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_strategy_no_embedding_falls_back_to_time():
|
def test_resolve_strategy_no_embedding_falls_back_to_time():
|
||||||
from if_curator.jobs import _resolve_strategy
|
from winnow.jobs import _resolve_strategy
|
||||||
limit, mode = _resolve_strategy("auto", has_embedding=False)
|
limit, mode = _resolve_strategy("auto", has_embedding=False)
|
||||||
assert mode == "time"
|
assert mode == "time"
|
||||||
assert isinstance(limit, int)
|
assert isinstance(limit, int)
|
||||||
@@ -55,14 +55,14 @@ def test_resolve_strategy_no_embedding_falls_back_to_time():
|
|||||||
|
|
||||||
def test_resolve_strategy_no_embedding_respects_limit(monkeypatch):
|
def test_resolve_strategy_no_embedding_respects_limit(monkeypatch):
|
||||||
monkeypatch.setenv("LIMIT", "60")
|
monkeypatch.setenv("LIMIT", "60")
|
||||||
from if_curator.jobs import _resolve_strategy
|
from winnow.jobs import _resolve_strategy
|
||||||
limit, mode = _resolve_strategy("auto", has_embedding=False)
|
limit, mode = _resolve_strategy("auto", has_embedding=False)
|
||||||
assert limit == 60
|
assert limit == 60
|
||||||
assert mode == "time"
|
assert mode == "time"
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_strategy_unknown_falls_back_to_auto():
|
def test_resolve_strategy_unknown_falls_back_to_auto():
|
||||||
from if_curator.jobs import _resolve_strategy
|
from winnow.jobs import _resolve_strategy
|
||||||
limit, mode = _resolve_strategy("unknown-strategy", has_embedding=True)
|
limit, mode = _resolve_strategy("unknown-strategy", has_embedding=True)
|
||||||
assert limit == "auto"
|
assert limit == "auto"
|
||||||
assert mode == "smart"
|
assert mode == "smart"
|
||||||
|
|||||||
@@ -11,41 +11,41 @@ import pytest
|
|||||||
def isolated_cache(monkeypatch, tmp_path):
|
def isolated_cache(monkeypatch, tmp_path):
|
||||||
"""Point tracker at a temp directory so tests don't touch real cache files."""
|
"""Point tracker at a temp directory so tests don't touch real cache files."""
|
||||||
monkeypatch.setenv("CACHE_DIR", str(tmp_path))
|
monkeypatch.setenv("CACHE_DIR", str(tmp_path))
|
||||||
from if_curator.config import _Config
|
from winnow.config import _Config
|
||||||
_Config.reset()
|
_Config.reset()
|
||||||
yield tmp_path
|
yield tmp_path
|
||||||
_Config.reset()
|
_Config.reset()
|
||||||
|
|
||||||
|
|
||||||
def test_filter_returns_all_when_empty():
|
def test_filter_returns_all_when_empty():
|
||||||
from if_curator.upload_tracker import filter_already_uploaded
|
from winnow.upload_tracker import filter_already_uploaded
|
||||||
ids = ["a1", "b2", "c3"]
|
ids = ["a1", "b2", "c3"]
|
||||||
assert filter_already_uploaded(ids) == ids
|
assert filter_already_uploaded(ids) == ids
|
||||||
|
|
||||||
|
|
||||||
def test_mark_uploaded_excludes_from_filter():
|
def test_mark_uploaded_excludes_from_filter():
|
||||||
from if_curator.upload_tracker import filter_already_uploaded, mark_uploaded
|
from winnow.upload_tracker import filter_already_uploaded, mark_uploaded
|
||||||
mark_uploaded("a1", person_name="Alice")
|
mark_uploaded("a1", person_name="Alice")
|
||||||
result = filter_already_uploaded(["a1", "b2"])
|
result = filter_already_uploaded(["a1", "b2"])
|
||||||
assert result == ["b2"]
|
assert result == ["b2"]
|
||||||
|
|
||||||
|
|
||||||
def test_mark_rejected_excludes_from_filter():
|
def test_mark_rejected_excludes_from_filter():
|
||||||
from if_curator.upload_tracker import filter_already_uploaded, mark_rejected
|
from winnow.upload_tracker import filter_already_uploaded, mark_rejected
|
||||||
mark_rejected("x9", person_name="Bob")
|
mark_rejected("x9", person_name="Bob")
|
||||||
result = filter_already_uploaded(["x9", "y8"])
|
result = filter_already_uploaded(["x9", "y8"])
|
||||||
assert result == ["y8"]
|
assert result == ["y8"]
|
||||||
|
|
||||||
|
|
||||||
def test_retry_rejected_includes_rejected():
|
def test_retry_rejected_includes_rejected():
|
||||||
from if_curator.upload_tracker import filter_already_uploaded, mark_rejected
|
from winnow.upload_tracker import filter_already_uploaded, mark_rejected
|
||||||
mark_rejected("x9", person_name="Bob")
|
mark_rejected("x9", person_name="Bob")
|
||||||
result = filter_already_uploaded(["x9", "y8"], retry_rejected=True)
|
result = filter_already_uploaded(["x9", "y8"], retry_rejected=True)
|
||||||
assert "x9" in result
|
assert "x9" in result
|
||||||
|
|
||||||
|
|
||||||
def test_reset_person_clears_records():
|
def test_reset_person_clears_records():
|
||||||
from if_curator.upload_tracker import filter_already_uploaded, mark_uploaded, reset_person
|
from winnow.upload_tracker import filter_already_uploaded, mark_uploaded, reset_person
|
||||||
mark_uploaded("a1", person_name="Alice")
|
mark_uploaded("a1", person_name="Alice")
|
||||||
mark_uploaded("a2", person_name="Alice")
|
mark_uploaded("a2", person_name="Alice")
|
||||||
mark_uploaded("b1", person_name="Bob")
|
mark_uploaded("b1", person_name="Bob")
|
||||||
@@ -55,7 +55,7 @@ def test_reset_person_clears_records():
|
|||||||
|
|
||||||
|
|
||||||
def test_get_person_summary():
|
def test_get_person_summary():
|
||||||
from if_curator.upload_tracker import get_person_summary, mark_rejected, mark_uploaded
|
from winnow.upload_tracker import get_person_summary, mark_rejected, mark_uploaded
|
||||||
mark_uploaded("a1", person_name="Alice")
|
mark_uploaded("a1", person_name="Alice")
|
||||||
mark_uploaded("a2", person_name="Alice")
|
mark_uploaded("a2", person_name="Alice")
|
||||||
mark_rejected("a3", person_name="Alice")
|
mark_rejected("a3", person_name="Alice")
|
||||||
@@ -68,7 +68,7 @@ def test_get_person_summary():
|
|||||||
|
|
||||||
|
|
||||||
def test_duplicate_marks_are_idempotent():
|
def test_duplicate_marks_are_idempotent():
|
||||||
from if_curator.upload_tracker import filter_already_uploaded, mark_uploaded
|
from winnow.upload_tracker import filter_already_uploaded, mark_uploaded
|
||||||
mark_uploaded("dup", person_name="Alice")
|
mark_uploaded("dup", person_name="Alice")
|
||||||
mark_uploaded("dup", person_name="Alice")
|
mark_uploaded("dup", person_name="Alice")
|
||||||
assert filter_already_uploaded(["dup", "new"]) == ["new"]
|
assert filter_already_uploaded(["dup", "new"]) == ["new"]
|
||||||
|
|||||||
@@ -549,61 +549,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "if-curator"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { editable = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "croniter" },
|
|
||||||
{ name = "insightface" },
|
|
||||||
{ name = "numpy" },
|
|
||||||
{ name = "nvidia-cudnn-cu12" },
|
|
||||||
{ name = "onnxruntime-gpu" },
|
|
||||||
{ name = "opencv-python-headless" },
|
|
||||||
{ name = "pillow" },
|
|
||||||
{ name = "python-dotenv" },
|
|
||||||
{ name = "requests" },
|
|
||||||
{ name = "rich" },
|
|
||||||
{ name = "torch", version = "2.12.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "platform_machine != 's390x' and sys_platform == 'darwin'" },
|
|
||||||
{ name = "torch", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux'" },
|
|
||||||
{ name = "torch", version = "2.12.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine == 's390x' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
|
|
||||||
{ name = "torch", version = "2.12.0+cu126", source = { registry = "https://download.pytorch.org/whl/cu126" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
|
||||||
{ name = "transformers" },
|
|
||||||
{ name = "ultralytics" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dev-dependencies]
|
|
||||||
dev = [
|
|
||||||
{ name = "pytest" },
|
|
||||||
{ name = "ruff" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "croniter", specifier = ">=5.0.2" },
|
|
||||||
{ name = "insightface", specifier = ">=0.7.3" },
|
|
||||||
{ name = "numpy", specifier = ">=2.2.6" },
|
|
||||||
{ name = "nvidia-cudnn-cu12", specifier = ">=9.0.0" },
|
|
||||||
{ name = "onnxruntime-gpu", specifier = ">=1.23.2" },
|
|
||||||
{ name = "opencv-python-headless", specifier = ">=4.12.0.88" },
|
|
||||||
{ name = "pillow", specifier = ">=12.1.0" },
|
|
||||||
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
|
||||||
{ name = "requests", specifier = ">=2.32.5" },
|
|
||||||
{ name = "rich", specifier = ">=14.2.0" },
|
|
||||||
{ name = "torch", marker = "sys_platform != 'linux'", specifier = ">=2.6.0", index = "https://download.pytorch.org/whl/cpu" },
|
|
||||||
{ name = "torch", marker = "platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux'", specifier = ">=2.6.0" },
|
|
||||||
{ name = "torch", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'", specifier = ">=2.6.0", index = "https://download.pytorch.org/whl/cpu" },
|
|
||||||
{ name = "torch", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=2.6.0", index = "https://download.pytorch.org/whl/cu126" },
|
|
||||||
{ name = "transformers", specifier = ">=4.57.6" },
|
|
||||||
{ name = "ultralytics", specifier = ">=8.3.252" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
|
||||||
dev = [
|
|
||||||
{ name = "pytest", specifier = ">=8.0" },
|
|
||||||
{ name = "ruff", specifier = ">=0.9.2" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "imageio"
|
name = "imageio"
|
||||||
version = "2.37.3"
|
version = "2.37.3"
|
||||||
@@ -2341,3 +2286,58 @@ sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e
|
|||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "croniter" },
|
||||||
|
{ name = "insightface" },
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "nvidia-cudnn-cu12" },
|
||||||
|
{ name = "onnxruntime-gpu" },
|
||||||
|
{ name = "opencv-python-headless" },
|
||||||
|
{ name = "pillow" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "rich" },
|
||||||
|
{ name = "torch", version = "2.12.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "platform_machine != 's390x' and sys_platform == 'darwin'" },
|
||||||
|
{ name = "torch", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux'" },
|
||||||
|
{ name = "torch", version = "2.12.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine == 's390x' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
|
||||||
|
{ name = "torch", version = "2.12.0+cu126", source = { registry = "https://download.pytorch.org/whl/cu126" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||||
|
{ name = "transformers" },
|
||||||
|
{ name = "ultralytics" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "croniter", specifier = ">=5.0.2" },
|
||||||
|
{ name = "insightface", specifier = ">=0.7.3" },
|
||||||
|
{ name = "numpy", specifier = ">=2.2.6" },
|
||||||
|
{ name = "nvidia-cudnn-cu12", specifier = ">=9.0.0" },
|
||||||
|
{ name = "onnxruntime-gpu", specifier = ">=1.23.2" },
|
||||||
|
{ name = "opencv-python-headless", specifier = ">=4.12.0.88" },
|
||||||
|
{ name = "pillow", specifier = ">=12.1.0" },
|
||||||
|
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
||||||
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
|
{ name = "rich", specifier = ">=14.2.0" },
|
||||||
|
{ name = "torch", marker = "sys_platform != 'linux'", specifier = ">=2.6.0", index = "https://download.pytorch.org/whl/cpu" },
|
||||||
|
{ name = "torch", marker = "platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux'", specifier = ">=2.6.0" },
|
||||||
|
{ name = "torch", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'", specifier = ">=2.6.0", index = "https://download.pytorch.org/whl/cpu" },
|
||||||
|
{ name = "torch", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=2.6.0", index = "https://download.pytorch.org/whl/cu126" },
|
||||||
|
{ name = "transformers", specifier = ">=4.57.6" },
|
||||||
|
{ name = "ultralytics", specifier = ">=8.3.252" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest", specifier = ">=8.0" },
|
||||||
|
{ name = "ruff", specifier = ">=0.9.2" },
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from if_curator.cli import main
|
from winnow.cli import main
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Interactive CLI for if-curator."""
|
"""Interactive CLI for winnow."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -17,12 +17,12 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Entry point for if-curator CLI."""
|
"""Entry point for winnow CLI."""
|
||||||
try:
|
try:
|
||||||
setup_logging(verbose=False)
|
setup_logging(verbose=False)
|
||||||
|
|
||||||
console.print(r"""
|
console.print(r"""
|
||||||
[bold blue]if-curator[/bold blue]
|
[bold blue]winnow[/bold blue]
|
||||||
[dim]Immich -> Frigate Training Data Curator[/dim]
|
[dim]Immich -> Frigate Training Data Curator[/dim]
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Configuration management for if-curator."""
|
"""Configuration management for winnow."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Logging configuration for if-curator."""
|
"""Logging configuration for winnow."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
Reference in New Issue
Block a user