release: v0.5.14 — graceful fallback for invalid numeric env vars

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.
This commit is contained in:
2026-06-15 01:35:27 +00:00
parent 51f7ed3961
commit 5bcc5975bc
3 changed files with 32 additions and 8 deletions
+6
View File
@@ -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
+1 -1
View File
@@ -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"
+25 -7
View File
@@ -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")