# Labeling Tool — Backend API + data model for the bounding-box labeling tool. See CLAUDE.md §3.2 for the full requirements this is built against. **Status:** core data model + CRUD API implemented (sets, hierarchical classes, frames, labels, per-frame/per-set status), frame image serving, bulk frame ingest from a recording session, and dataset version promotion (see `labeling/frontend/` for the UI). **Not yet implemented:** auth/login (see below) or the active-learning auto-label workflow. ## Stack - **FastAPI** + **SQLAlchemy** (2.0), **SQLite** by default (`./labeling.db`), swappable via the `DATABASE_URL` env var (e.g. to Postgres later without code changes — one Postgres-compatible ORM). - **Multi-user, no auth yet:** labels/status changes take a plain `created_by` / `updated_by` username string, resolved via get-or-create (`users.py`). There's no login flow — attribution only, since there's no UI yet that would need real auth. ## Data model - `LabelSet` — a label set (`Enemy`, `Items`, `Traps`, ...), one per detector model. - `MainClass` / `SubClass` — the per-set Main → Sub class hierarchy (e.g. `Enemy` → `Bat`, `Snake`), created ad hoc via the API, no migration needed to add classes. - `Frame` — one labelable image, identified by `(session_name, frame_index)` — matches the recording tool's frame-extraction output 1:1. - `Label` — one bounding box (`x, y, width, height` in pixel space), scoped to a frame + set + sub-class. - `FrameSetStatus` — per-frame, per-set label state (`unlabeled` / `auto_labeled` / `reviewed`). - `DatasetVersion` / `DatasetVersionFrame` / `DatasetVersionLabel` — a named, immutable snapshot of a set's frames+labels as of promotion time (`enemy-v1`, `enemy-v2`, ...). Labels stay live-editable at any time; a promoted version copies the label data at that moment, so it stays reproducible regardless of later edits or deletions to the live labels. Frame images are served from `FRAMES_ROOT` (env var, default `./frames`) at `/images/`; `Frame.image_path` is always relative to that root. ## Setup ``` python -m venv .venv source .venv/bin/activate pip install -e ".[dev]" ``` ## Run ``` spelunkai-labeling-backend # or: uvicorn spelunkai_labeling_backend.main:create_app --factory --reload ``` Interactive API docs at `http://127.0.0.1:8000/docs` once running. ## Bulk-ingesting a recording session After a session's extracted frames (`recording/`'s `extract-frames` output) have been copied/rsynced onto this machine under `FRAMES_ROOT`, register them all in one shot (writes directly to the database, no server needs to be running): ``` spelunkai-labeling-backend ingest-session \ --frames-dir /path/under/FRAMES_ROOT/run01_frames \ --session-name run01 ``` `--image-path-prefix` defaults to the frames directory's own name (here `run01_frames`) — override it if the directory was copied under a different name. Safe to re-run: frames already registered for that session (by frame index) are skipped. `--width`/`--height` default to 1280x720 (Spelunky Classic HD's fixed capture resolution). ## Promoting a dataset version Once frames are marked `reviewed` (see the frontend, or `PUT /frames/{id}/sets/{id}/status`), freeze them into a named, reproducible version: ``` POST /sets/{set_id}/dataset-versions {"name": "enemy-v1", "description": "first reviewed batch"} ``` Defaults to every currently-`reviewed` frame in that set; pass an explicit `frame_ids` list to promote a different selection instead. Fetch the frozen result (what training will eventually consume) via `GET /dataset-versions/{id}` — it returns each frame plus the exact labels that existed at promotion time, unaffected by any later edits to the live labels. ## Testing ``` pytest ``` Each test gets a fully isolated app + SQLite file via `create_app(database_url=...)` (see `tests/conftest.py`) — no shared state between tests, no real server needed.