Source code for annie.core.config
"""Runtime configuration for Annie.
All settings are path-agnostic: nothing absolute is baked into the repository.
Defaults come from environment variables (prefixed ``ANNIE_``) so the same code
runs on any machine — set them on your dev box and your private paths never leak
into the public repo — and every folder can still be overridden at runtime from
the Dataset tab's pickers.
The infrastructure layer owns this module; higher layers read :data:`settings`
rather than touching the environment directly.
"""
from __future__ import annotations
import os
from dataclasses import dataclass, field
from pathlib import Path
def _env_path(name: str) -> Path | None:
"""Return ``$name`` as an expanded :class:`~pathlib.Path`, or ``None`` if unset/empty."""
raw = os.environ.get(name, "").strip()
return Path(raw).expanduser() if raw else None
def _env_int(name: str, default: int) -> int:
"""Return ``$name`` parsed as an int, or ``default`` if unset or unparsable."""
raw = os.environ.get(name, "").strip()
try:
return int(raw) if raw else default
except ValueError:
return default
def _env_tuple(name: str) -> tuple[str, ...] | None:
"""Return ``$name`` split on commas into a tuple, or ``None`` if unset/empty."""
raw = os.environ.get(name, "").strip()
if not raw:
return None
return tuple(col.strip() for col in raw.split(",") if col.strip())
def _env_bool(name: str, default: bool = False) -> bool:
"""Return ``$name`` as a bool (``1``/``true``/``yes``/``on`` are true), else ``default``."""
raw = os.environ.get(name, "").strip().lower()
if not raw:
return default
return raw in ("1", "true", "yes", "on")
[docs]
def annie_home() -> Path:
"""The Annie home directory (env ``ANNIE_HOME``), defaulting to ``~/.annie``.
Config-pinned review databases (``annie_<config-stem>.db``) live directly here, so a
saved dataset always reopens against the same DB regardless of where the config file
is stored.
The path is **resolved**, because it is compared against resolved DB paths to decide
whether a config may store its database as a portable bare filename (see
:func:`annie.dataset.datasets.to_config_dict`). An unresolved home would fail that
comparison whenever it contains a symlink — which is the default on macOS, where
``/tmp`` links to ``/private/tmp`` — silently pinning an absolute path instead.
"""
return (_env_path("ANNIE_HOME") or (Path.home() / ".annie")).resolve()
def _default_db_path() -> Path:
"""Resolve the SQLite path for this session.
Priority:
1. ``ANNIE_DB_PATH`` — explicit persistent path (any valid file path).
2. Otherwise: ``<ANNIE_HOME>/annie_env.db`` — a single, stable review database for
environment-seeded sessions, so decisions persist and reload consistently.
"""
explicit = _env_path("ANNIE_DB_PATH")
if explicit is not None:
return explicit
return annie_home() / "annie_env.db"
[docs]
@dataclass(slots=True)
class Settings:
"""User-facing configuration, resolved once at import time.
Attributes:
videos_dir: Folder of source ``.mp4`` files (env ``ANNIE_VIDEO_DIR``).
vdet_dir: Folder of raw-detection ``.vdet`` files (env ``ANNIE_VDET_DIR``).
track_dir: Folder of ``__track{N}.csv`` files (env ``ANNIE_TRACK_DIR``).
participants_file: The protagonist heuristic CSV (``uuid,track_id``); the
Annotator's Export writes corrections to its ``_manual`` sibling
(env ``ANNIE_PROTAGONIST_CSV``).
labels_csv: A labels CSV file to seed as a label source on startup
(env ``ANNIE_LABEL_CSV``).
labels_key: The key column in :attr:`labels_csv` (env ``ANNIE_LABEL_KEY``);
auto-detected from the header when omitted.
labels_values: Comma-separated value columns from :attr:`labels_csv`
(env ``ANNIE_LABEL_VALUES``); defaults to all non-key columns.
db_path: SQLite file for this session's review status (good/bad/notes). Defaults
to ``<ANNIE_HOME>/annie_env.db`` (a stable per-environment database); set
``ANNIE_DB_PATH`` to pin a different path. Loading a saved config switches
this to the config's own ``annie_<stem>.db``.
db_path_is_explicit: ``True`` when ``ANNIE_DB_PATH`` was set, so the UI can
show "existing DB" mode instead of "new session DB" mode on startup.
annie_home: The Annie home directory (env ``ANNIE_HOME``, default ``~/.annie``);
config-pinned review databases live directly here.
logs_dir: Directory that receives per-session log files
(``<ANNIE_HOME>/logs``).
sessions_dir: Directory that receives per-session SQLite databases
(``<ANNIE_HOME>/sessions``).
temp_dir: Scratch directory for rendered preview clips and exports. Defaults
to ``<ANNIE_HOME>/tmp`` (falling back to ``~/.annie/tmp``) so it lands on
the same persistent location as logs and sessions; override with
``ANNIE_TEMP_DIR``.
config_dir: Optional extra directory scanned for user-saved dataset configs
(env ``ANNIE_CONFIG_DIR``). Useful in Docker where the container-side
``data/config/`` is ephemeral; set this to a path on a persistent volume
so saved configs survive restarts.
data_dir: Optional override for the bundled ``data`` directory (env
``ANNIE_DATA_DIR``). Needed for non-editable installs (Docker, PyPI)
where the package lives in ``site-packages`` and cannot locate ``data``
by walking up from ``__file__``; point it at the checked-out ``data``.
video_extensions: Accepted source-video suffixes (lower-case, dotted).
vdet_extension: Suffix identifying a raw-detection file.
track_glob: Glob identifying derived single-track files.
render_max_workers: Maximum concurrent render jobs.
preview_max_workers: Maximum concurrent preview decodes (env
``ANNIE_PREVIEW_WORKERS``; default: CPU count minus two, at least two).
temp_ttl_seconds: Age after which a rendered clip is swept (default 3 minutes).
host: Interface the NiceGUI server binds to.
port: Port the NiceGUI server listens on.
open_browser: Whether to open the default web browser at the app URL on startup
(env ``ANNIE_OPEN_BROWSER``). Off by default so dev, CI, and the headless Docker
server are unaffected; the Windows launcher (which runs the server in a container
and must open the browser on the *host*) sets it, and it is also handy for a
local end-user launch where no terminal-savvy user is watching for the URL.
"""
videos_dir: Path | None = field(default_factory=lambda: _env_path("ANNIE_VIDEO_DIR"))
vdet_dir: Path | None = field(default_factory=lambda: _env_path("ANNIE_VDET_DIR"))
track_dir: Path | None = field(default_factory=lambda: _env_path("ANNIE_TRACK_DIR"))
participants_file: Path | None = field(
default_factory=lambda: _env_path("ANNIE_PROTAGONIST_CSV")
)
labels_csv: Path | None = field(default_factory=lambda: _env_path("ANNIE_LABEL_CSV"))
labels_key: str | None = field(
default_factory=lambda: os.environ.get("ANNIE_LABEL_KEY", "").strip() or None
)
labels_values: tuple[str, ...] | None = field(
default_factory=lambda: _env_tuple("ANNIE_LABEL_VALUES")
)
db_path: Path = field(default_factory=_default_db_path)
db_path_is_explicit: bool = field(default_factory=lambda: bool(_env_path("ANNIE_DB_PATH")))
logs_dir: Path = field(default_factory=lambda: annie_home() / "logs")
sessions_dir: Path = field(default_factory=lambda: annie_home() / "sessions")
temp_dir: Path = field(
default_factory=lambda: _env_path("ANNIE_TEMP_DIR") or annie_home() / "tmp"
)
config_dir: Path | None = field(default_factory=lambda: _env_path("ANNIE_CONFIG_DIR"))
data_dir: Path | None = field(default_factory=lambda: _env_path("ANNIE_DATA_DIR"))
video_extensions: tuple[str, ...] = (".mp4",)
vdet_extension: str = ".vdet"
track_glob: str = "*__track*.csv"
render_max_workers: int = field(default_factory=lambda: _env_int("ANNIE_RENDER_WORKERS", 2))
preview_max_workers: int = field(
default_factory=lambda: _env_int("ANNIE_PREVIEW_WORKERS", max(2, (os.cpu_count() or 2) - 2))
)
temp_ttl_seconds: int = field(default_factory=lambda: _env_int("ANNIE_TEMP_TTL", 180))
host: str = field(default_factory=lambda: os.environ.get("ANNIE_HOST", "127.0.0.1"))
port: int = field(default_factory=lambda: _env_int("ANNIE_PORT", 8080))
open_browser: bool = field(default_factory=lambda: _env_bool("ANNIE_OPEN_BROWSER"))
annie_home: Path = field(default_factory=annie_home)
settings = Settings()
"""The process-wide settings instance.
Mutate its fields (e.g. from the Dataset tab) to change behaviour at runtime
without re-importing.
"""