diff --git a/.env.example b/.env.example index 3daa6d6..9447bfa 100644 --- a/.env.example +++ b/.env.example @@ -34,7 +34,7 @@ STRATEGY=adaptive # ── Caching & Models ────────────────────────────────────────────────────────── # FORCE_CPU=true # Disable GPU, fall back to CPU # ENABLE_CACHE=false # Disable embedding cache (default: true) -CACHE_DIR=/app/.if_cache +DATA_DIR=/app/data INSIGHTFACE_HOME=/models/.insightface # ── Tracker overrides (one-shot — remove after use) ─────────────────────────── diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f9687c..8a486b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.1] - 2026-06-14 + +### Changed + +- **`CACHE_DIR` renamed to `DATA_DIR`**: the environment variable that sets the path for the embedding cache and SQLite tracker database is now called `DATA_DIR` (default: `data`; Docker default: `/app/data`). The old `CACHE_DIR` still works with a startup deprecation warning — rename it to `DATA_DIR` in your `.env` or `compose.yml` to silence the warning. The container-side default path changes from `/app/.if_cache` to `/app/data`; update your volume mount accordingly. + ## [0.5.0] - 2026-06-14 ### Changed diff --git a/README.md b/README.md index 84a89bd..a57fae6 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Immich library │ ▼ 2. Filter by recency (YEARS_FILTER) and skip already-uploaded - and rejected assets (persistent tracker in CACHE_DIR) + and rejected assets (persistent tracker in DATA_DIR) │ ▼ 3. Quality filter — download preview thumbnails and reject: @@ -74,7 +74,7 @@ Immich library ↳ at cap + QUALITY_REPLACEMENT=false — skip this person ``` -Uploaded and rejected asset IDs are persisted across runs in a SQLite database (`winnow_tracker.db` in `CACHE_DIR`). The same image is never processed twice; rejected assets are permanently skipped unless `RETRY_REJECTED=true`. +Uploaded and rejected asset IDs are persisted across runs in a SQLite database (`winnow_tracker.db` in `DATA_DIR`). The same image is never processed twice; rejected assets are permanently skipped unless `RETRY_REJECTED=true`. --- @@ -103,7 +103,7 @@ services: - CRON_SCHEDULE=0 3 * * 0 volumes: - /path/to/models:/models # INSIGHTFACE_HOME — persists Buffalo_L model (~300 MB) - - /path/to/cache:/app/.if_cache + - /path/to/data:/app/data - /path/to/output:/app/frigate_train deploy: resources: @@ -212,7 +212,7 @@ These defaults are tuned for Frigate's ArcFace requirements. winnow will warn on | `FORCE_CPU` | `false` | Disable GPU — fall back to CPU for all inference | | `OPENVINO_DEVICE` | `CPU` | Intel variant only: set `GPU` to use Arc or iGPU; default runs on CPU | | `ENABLE_CACHE` | `true` | Cache computed embeddings to disk (speeds up re-runs on the same library) | -| `CACHE_DIR` | `.if_cache` | Path for embedding cache and upload tracker database (`winnow_tracker.db`) | +| `DATA_DIR` | `data` | Path for embedding cache and upload tracker database (`winnow_tracker.db`) | | `INSIGHTFACE_HOME` | *(system)* | InsightFace model cache path (Buffalo_L) | ### Output diff --git a/compose.yml b/compose.yml index d41ac74..d1ce394 100644 --- a/compose.yml +++ b/compose.yml @@ -37,7 +37,7 @@ services: # - FORCE_CPU=true # Disable GPU, fall back to CPU # - OPENVINO_DEVICE=GPU # Intel variant only: use Arc/iGPU instead of CPU (default: CPU) # - ENABLE_CACHE=false # Disable embedding cache (default: true) - - CACHE_DIR=/app/.if_cache + - DATA_DIR=/app/data - INSIGHTFACE_HOME=/models/.insightface # ── Tracker overrides (one-shot, remove after use) ──────────────────── @@ -57,7 +57,7 @@ services: volumes: # Replace with absolute paths on your host, e.g. /opt/winnow/models - /path/to/winnow/models:/models - - /path/to/winnow/cache:/app/.if_cache + - /path/to/winnow/data:/app/data - /path/to/winnow/output:/app/frigate_train restart: unless-stopped diff --git a/pyproject.toml b/pyproject.toml index 5ccbf97..21a97cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "winnow" -version = "0.5.0" +version = "0.5.1" description = "Selects diverse, high-quality photos from Immich as training data for Frigate face recognition." license = "AGPL-3.0-or-later" requires-python = ">=3.13" diff --git a/tests/test_upload_tracker.py b/tests/test_upload_tracker.py index 7b57ae4..827b42d 100644 --- a/tests/test_upload_tracker.py +++ b/tests/test_upload_tracker.py @@ -7,7 +7,7 @@ import pytest @pytest.fixture(autouse=True) def isolated_cache(monkeypatch, tmp_path): """Point tracker at a temp directory so tests don't touch real cache files.""" - monkeypatch.setenv("CACHE_DIR", str(tmp_path)) + monkeypatch.setenv("DATA_DIR", str(tmp_path)) from winnow.config import _Config _Config.reset() # Also reset the SQLite connection so the next call opens the new path diff --git a/uv.lock b/uv.lock index 22056b0..1f5f7d2 100644 --- a/uv.lock +++ b/uv.lock @@ -862,7 +862,7 @@ wheels = [ [[package]] name = "winnow" -version = "0.5.0" +version = "0.5.1" source = { editable = "." } dependencies = [ { name = "croniter" }, diff --git a/winnow/config.py b/winnow/config.py index 85c8549..5172950 100644 --- a/winnow/config.py +++ b/winnow/config.py @@ -47,7 +47,7 @@ class _Config: ENABLE_FACE_ALIGNMENT: bool ENABLE_CACHE: bool - CACHE_DIR: str + DATA_DIR: str def __new__(cls) -> "_Config": if cls._instance is None: @@ -99,7 +99,17 @@ class _Config: self.USE_FULL_RESOLUTION = os.getenv("USE_FULL_RESOLUTION", "true").lower() in ("true", "1", "yes") self.ENABLE_FACE_ALIGNMENT = os.getenv("ENABLE_FACE_ALIGNMENT", "true").lower() in ("true", "1", "yes") self.ENABLE_CACHE = os.getenv("ENABLE_CACHE", "true").lower() in ("true", "1", "yes") - self.CACHE_DIR = os.getenv("CACHE_DIR", ".if_cache") + _data_dir = os.getenv("DATA_DIR") + _cache_dir_legacy = os.getenv("CACHE_DIR") + if _data_dir: + self.DATA_DIR = _data_dir + elif _cache_dir_legacy: + logging.warning( + "CACHE_DIR is deprecated — rename it to DATA_DIR in your .env or compose.yml" + ) + self.DATA_DIR = _cache_dir_legacy + else: + self.DATA_DIR = "data" # Fall back to config file only when the env var is genuinely absent (None). # An explicitly empty env var (IMMICH_URL="") takes priority over the file. diff --git a/winnow/embeddings.py b/winnow/embeddings.py index a1f470a..3a78437 100644 --- a/winnow/embeddings.py +++ b/winnow/embeddings.py @@ -214,7 +214,7 @@ def get_embedding( from .config import Config use_cache = Config.ENABLE_CACHE and asset_id is not None - cache = get_cache(Config.CACHE_DIR) if use_cache else None + cache = get_cache(Config.DATA_DIR) if use_cache else None if cache: cached = cache.get(asset_id, "insightface") diff --git a/winnow/upload_tracker.py b/winnow/upload_tracker.py index 4e6e55a..9e2eaf9 100644 --- a/winnow/upload_tracker.py +++ b/winnow/upload_tracker.py @@ -1,6 +1,6 @@ """Persistent tracker for Immich asset IDs uploaded/rejected by Frigate. -Uses a local SQLite database (frigate_tracker.db) in CACHE_DIR. +Uses a local SQLite database (frigate_tracker.db) in DATA_DIR. Schema ------ @@ -53,7 +53,7 @@ CREATE TABLE IF NOT EXISTS person_metadata ( ); """ -# Module-level connection state — re-opened when CACHE_DIR changes (test isolation) +# Module-level connection state — re-opened when DATA_DIR changes (test isolation) _conn: sqlite3.Connection | None = None _conn_path: str | None = None @@ -61,15 +61,15 @@ _conn_path: str | None = None def _get_conn() -> sqlite3.Connection: """Return (or create) the module-level SQLite connection. - Re-opens the connection when Config.CACHE_DIR has changed — this provides + Re-opens the connection when Config.DATA_DIR has changed — this provides test isolation when the isolated_cache fixture sets a new tmp directory and calls _Config.reset(). """ global _conn, _conn_path from .config import Config - cache_dir = Config.CACHE_DIR - db_path = str(Path(cache_dir) / _DB_NAME) + data_dir = Config.DATA_DIR + db_path = str(Path(data_dir) / _DB_NAME) if _conn is not None and _conn_path != db_path: try: @@ -79,7 +79,7 @@ def _get_conn() -> sqlite3.Connection: _conn = None if _conn is None: - Path(cache_dir).mkdir(parents=True, exist_ok=True) + Path(data_dir).mkdir(parents=True, exist_ok=True) _conn = sqlite3.connect(db_path, check_same_thread=False) _conn.row_factory = sqlite3.Row _conn.execute("PRAGMA journal_mode=WAL") @@ -87,7 +87,7 @@ def _get_conn() -> sqlite3.Connection: _conn.executescript(_DDL) _conn.commit() _conn_path = db_path - _maybe_migrate(cache_dir, _conn) + _maybe_migrate(data_dir, _conn) return _conn @@ -96,9 +96,9 @@ def _get_conn() -> sqlite3.Connection: # JSON → SQLite migration # --------------------------------------------------------------------------- -def _maybe_migrate(cache_dir: str, conn: sqlite3.Connection) -> None: +def _maybe_migrate(data_dir: str, conn: sqlite3.Connection) -> None: """If the old JSON files exist and DB is empty, migrate and rename them.""" - base = Path(cache_dir) + base = Path(data_dir) upload_json = base / _UPLOAD_JSON reject_json = base / _REJECT_JSON @@ -110,7 +110,7 @@ def _maybe_migrate(cache_dir: str, conn: sqlite3.Connection) -> None: # (e.g. a PermissionError on the second rename would have left the first file's # data committed but the second file un-renamed and un-migrated). - logger.info("Migrating JSON tracker files to SQLite in %s", cache_dir) + logger.info("Migrating JSON tracker files to SQLite in %s", data_dir) try: with conn: