From 5bcc5975bce5b85999b5e2a1ef49b9cf1fc3fbe8 Mon Sep 17 00:00:00 2001 From: Holden Date: Mon, 15 Jun 2026 01:35:27 +0000 Subject: [PATCH] =?UTF-8?q?release:=20v0.5.14=20=E2=80=94=20graceful=20fal?= =?UTF-8?q?lback=20for=20invalid=20numeric=20env=20vars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit YEARS_FILTER, MIN_FACE_WIDTH, MIN_FACE_COUNT, MAX_AUTO_IMAGES, BLUR_THRESHOLD, MIN_CONFIDENCE, and FACE_MARGIN used bare int()/float() calls with no error handler. A typo (trailing space, non-numeric value) raised ValueError inside __getattr__, producing a cryptic traceback on the first config access rather than at the validate() step. Values are now parsed by _getenv_int/_getenv_float helpers that warn and fall back to the documented default, matching the existing FRIGATE_SCORE_CEILING pattern. --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- winnow/config.py | 32 +++++++++++++++++++++++++------- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af0012c..b8fc3bc 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.14] - 2026-06-15 + +### Fixed + +- **Invalid env var values for numeric config now warn and use defaults**: `YEARS_FILTER`, `MIN_FACE_WIDTH`, `MIN_FACE_COUNT`, `MAX_AUTO_IMAGES`, `BLUR_THRESHOLD`, `MIN_CONFIDENCE`, and `FACE_MARGIN` all used bare `int()`/`float()` with no error handler. A typo such as `YEARS_FILTER=10 ` (trailing space) or `MIN_FACE_WIDTH=auto` raised `ValueError` from inside `__getattr__`, surfacing as a cryptic traceback on the first config access rather than at the config-validation step where a helpful error is expected. The values are now parsed with module-level `_getenv_int` / `_getenv_float` helpers that log a `WARNING` and fall back to the documented default on parse failure, matching the existing pattern already used for `FRIGATE_SCORE_CEILING`. + ## [0.5.13] - 2026-06-15 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 8941941..e93bfef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "winnow" -version = "0.5.13" +version = "0.5.14" 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/winnow/config.py b/winnow/config.py index dbc1339..3b4860e 100644 --- a/winnow/config.py +++ b/winnow/config.py @@ -12,6 +12,24 @@ from rich.prompt import Prompt _LEGACY_CONFIG_FILE = Path(".immich_config.json") # pre-v0.6: lived in process CWD, not on a volume +def _getenv_int(name: str, default: int) -> int: + val = os.getenv(name, str(default)) + try: + return int(val) + except ValueError: + logging.warning("%s=%r is not a valid integer — using default %s", name, val, default) + return default + + +def _getenv_float(name: str, default: float) -> float: + val = os.getenv(name, str(default)) + try: + return float(val) + except ValueError: + logging.warning("%s=%r is not a valid float — using default %s", name, val, default) + return default + + class _Config: """Singleton configuration with lazy loading via __getattr__. @@ -84,13 +102,13 @@ class _Config: self.IMMICH_URL = os.getenv("IMMICH_URL") self.API_KEY = os.getenv("API_KEY") self.OUTPUT_DIR = os.getenv("OUTPUT_DIR", "./frigate_train") - self.YEARS_FILTER = int(os.getenv("YEARS_FILTER", "10")) - self.MIN_FACE_WIDTH = int(os.getenv("MIN_FACE_WIDTH", "90")) - self.MIN_FACE_COUNT = int(os.getenv("MIN_FACE_COUNT", "3")) + self.YEARS_FILTER = _getenv_int("YEARS_FILTER", 10) + self.MIN_FACE_WIDTH = _getenv_int("MIN_FACE_WIDTH", 90) + self.MIN_FACE_COUNT = _getenv_int("MIN_FACE_COUNT", 3) self.MERGE_DUPLICATE_PEOPLE = os.getenv("MERGE_DUPLICATE_PEOPLE", "false").lower() in ("true", "1", "yes") - self.BLUR_THRESHOLD = float(os.getenv("BLUR_THRESHOLD", "120.0")) - self.MIN_CONFIDENCE = float(os.getenv("MIN_CONFIDENCE", "0.7")) - self.MAX_AUTO_IMAGES = int(os.getenv("MAX_AUTO_IMAGES", "20")) + self.BLUR_THRESHOLD = _getenv_float("BLUR_THRESHOLD", 120.0) + self.MIN_CONFIDENCE = _getenv_float("MIN_CONFIDENCE", 0.7) + self.MAX_AUTO_IMAGES = _getenv_int("MAX_AUTO_IMAGES", 20) self.QUALITY_REPLACEMENT = os.getenv("QUALITY_REPLACEMENT", "true").lower() in ("true", "1", "yes") _ceiling_env = os.getenv("FRIGATE_SCORE_CEILING", "").strip() if _ceiling_env: @@ -102,7 +120,7 @@ class _Config: else: self.FRIGATE_SCORE_CEILING = None self.ENABLE_FRIGATE_SCORES = os.getenv("ENABLE_FRIGATE_SCORES", "true").lower() in ("true", "1", "yes") - self.FACE_MARGIN = float(os.getenv("FACE_MARGIN", "0.15")) + self.FACE_MARGIN = _getenv_float("FACE_MARGIN", 0.15) 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")