- config.py: resolve_data_path (legacy abs + rel) + rel_data_path - all file-opening reads wrapped: preview, autolabel, training, model download, live count, projects.get; training_start_point hack replaced - new writes store paths relative to data/ - legacy stale rows (/home/asus/reTraining/...) resolve without migration - requirements: REQ-187 added; REQ-188 (per-class max box) + REQ-186 copy-line amendment drafted for the next task
93 lines
3.4 KiB
Python
93 lines
3.4 KiB
Python
"""Paths and settings. Everything is environment-driven so the same image runs
|
|
locally and in Docker without code changes (REQ-072)."""
|
|
|
|
import os
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
# huggingface_hub reads HF_TOKEN / HUGGING_FACE_HUB_TOKEN; accept either name in
|
|
# .env so pasting a token under the obvious name just works.
|
|
if os.environ.get("HF_TOKEN") and not os.environ.get("HUGGING_FACE_HUB_TOKEN"):
|
|
os.environ["HUGGING_FACE_HUB_TOKEN"] = os.environ["HF_TOKEN"]
|
|
|
|
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
DATA_DIR = os.path.abspath(os.environ.get("APP_DATA_DIR", os.path.join(REPO_ROOT, "data")))
|
|
PROJECTS_DIR = os.path.join(DATA_DIR, "projects")
|
|
DB_PATH = os.path.join(DATA_DIR, "app.db")
|
|
|
|
|
|
def resolve_data_path(path):
|
|
"""Resolve a DB-stored weight path against today's DATA_DIR (REQ-187)."""
|
|
if not path:
|
|
return path
|
|
if os.path.isfile(path):
|
|
return path
|
|
if not os.path.isabs(path):
|
|
candidate = os.path.join(DATA_DIR, path)
|
|
return candidate if os.path.isfile(candidate) else path
|
|
# legacy absolute path from before the data dir moved: .../data/<rel>
|
|
marker = os.sep + "data" + os.sep
|
|
idx = path.find(marker)
|
|
if idx != -1:
|
|
candidate = os.path.join(DATA_DIR, path[idx + len(marker):])
|
|
if os.path.isfile(candidate):
|
|
return candidate
|
|
return path
|
|
|
|
|
|
def rel_data_path(path):
|
|
"""Store paths under DATA_DIR relative to it (REQ-187); others unchanged."""
|
|
try:
|
|
rel = os.path.relpath(path, DATA_DIR)
|
|
except ValueError:
|
|
return path
|
|
return path if rel.startswith(os.pardir) else rel
|
|
|
|
# Where the video archive is mounted. Projects store a path relative to nothing —
|
|
# they store an absolute one — but this is the default the UI starts browsing from.
|
|
VIDEO_ROOT = os.path.abspath(os.environ.get("VIDEO_ARCHIVE", os.path.join(DATA_DIR, "archive")))
|
|
|
|
|
|
def archive_host_paths() -> dict:
|
|
"""Where the archive sits on the host — for the copy-path buttons (REQ-179).
|
|
|
|
VIDEO_ARCHIVE_HOST may stay relative (portable across machines): compose
|
|
injects the host cwd as ARCHIVE_HOST_PWD (compose always has PWD — a
|
|
Windows-side run yields a drive-letter path, handled below), bare-metal
|
|
resolves against the process cwd (start.sh cds to the repo root)."""
|
|
import re
|
|
host = os.environ.get("VIDEO_ARCHIVE_HOST", "")
|
|
if host and not os.path.isabs(host):
|
|
base = os.environ.get("ARCHIVE_HOST_PWD") or os.getcwd()
|
|
host = os.path.abspath(os.path.join(base, host))
|
|
if not host and VIDEO_ROOT != "/videos":
|
|
host = VIDEO_ROOT # bare-metal with no VIDEO_ARCHIVE_HOST at all
|
|
if not host:
|
|
return {"linux": None, "windows": None}
|
|
linux = host.rstrip("/")
|
|
if re.match(r"^[A-Za-z]:[\\/]", linux):
|
|
return {"linux": None, "windows": linux.replace("/", "\\")}
|
|
distro = os.environ.get("WSL_DISTRO_NAME", "")
|
|
windows = f"\\\\wsl.localhost\\{distro}\\" + linux.lstrip("/").replace("/", "\\") if distro else None
|
|
return {"linux": linux, "windows": windows}
|
|
|
|
|
|
# Vite dev server needs cross-origin access; in Docker nginx proxies /api and
|
|
# this is irrelevant.
|
|
CORS_ORIGINS = [
|
|
origin.strip()
|
|
for origin in os.environ.get("CORS_ORIGINS", "http://localhost:5173").split(",")
|
|
if origin.strip()
|
|
]
|
|
|
|
|
|
def ensure_dirs() -> None:
|
|
os.makedirs(PROJECTS_DIR, exist_ok=True)
|
|
|
|
|
|
def project_dir(slug: str) -> str:
|
|
return os.path.join(PROJECTS_DIR, slug)
|