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:
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.5.13] - 2026-06-15
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "winnow"
|
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."
|
description = "Selects diverse, high-quality photos from Immich as training data for Frigate face recognition."
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|||||||
+25
-7
@@ -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
|
_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:
|
class _Config:
|
||||||
"""Singleton configuration with lazy loading via __getattr__.
|
"""Singleton configuration with lazy loading via __getattr__.
|
||||||
|
|
||||||
@@ -84,13 +102,13 @@ class _Config:
|
|||||||
self.IMMICH_URL = os.getenv("IMMICH_URL")
|
self.IMMICH_URL = os.getenv("IMMICH_URL")
|
||||||
self.API_KEY = os.getenv("API_KEY")
|
self.API_KEY = os.getenv("API_KEY")
|
||||||
self.OUTPUT_DIR = os.getenv("OUTPUT_DIR", "./frigate_train")
|
self.OUTPUT_DIR = os.getenv("OUTPUT_DIR", "./frigate_train")
|
||||||
self.YEARS_FILTER = int(os.getenv("YEARS_FILTER", "10"))
|
self.YEARS_FILTER = _getenv_int("YEARS_FILTER", 10)
|
||||||
self.MIN_FACE_WIDTH = int(os.getenv("MIN_FACE_WIDTH", "90"))
|
self.MIN_FACE_WIDTH = _getenv_int("MIN_FACE_WIDTH", 90)
|
||||||
self.MIN_FACE_COUNT = int(os.getenv("MIN_FACE_COUNT", "3"))
|
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.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.BLUR_THRESHOLD = _getenv_float("BLUR_THRESHOLD", 120.0)
|
||||||
self.MIN_CONFIDENCE = float(os.getenv("MIN_CONFIDENCE", "0.7"))
|
self.MIN_CONFIDENCE = _getenv_float("MIN_CONFIDENCE", 0.7)
|
||||||
self.MAX_AUTO_IMAGES = int(os.getenv("MAX_AUTO_IMAGES", "20"))
|
self.MAX_AUTO_IMAGES = _getenv_int("MAX_AUTO_IMAGES", 20)
|
||||||
self.QUALITY_REPLACEMENT = os.getenv("QUALITY_REPLACEMENT", "true").lower() in ("true", "1", "yes")
|
self.QUALITY_REPLACEMENT = os.getenv("QUALITY_REPLACEMENT", "true").lower() in ("true", "1", "yes")
|
||||||
_ceiling_env = os.getenv("FRIGATE_SCORE_CEILING", "").strip()
|
_ceiling_env = os.getenv("FRIGATE_SCORE_CEILING", "").strip()
|
||||||
if _ceiling_env:
|
if _ceiling_env:
|
||||||
@@ -102,7 +120,7 @@ class _Config:
|
|||||||
else:
|
else:
|
||||||
self.FRIGATE_SCORE_CEILING = None
|
self.FRIGATE_SCORE_CEILING = None
|
||||||
self.ENABLE_FRIGATE_SCORES = os.getenv("ENABLE_FRIGATE_SCORES", "true").lower() in ("true", "1", "yes")
|
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.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_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.ENABLE_CACHE = os.getenv("ENABLE_CACHE", "true").lower() in ("true", "1", "yes")
|
||||||
|
|||||||
Reference in New Issue
Block a user