Jonas 96b7d42c34 Add bulk frame ingest for recording sessions
Registering thousands of extracted frames one-by-one via POST /frames
doesn't scale, so add `spelunkai-labeling-backend ingest-session`: it
writes Frame rows directly against the database (no server needs to
be running) for all frame_*.png files under a directory already
placed under FRAMES_ROOT. Idempotent per (session_name, frame_index),
so re-running after copying more frames only inserts the new ones.

Restructured the CLI into subcommands (serve / ingest-session) while
keeping `spelunkai-labeling-backend` with no arguments working exactly
as before (defaults to serve), verified against a live run. 22/22
backend tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 13:04:09 +02:00

82 lines
3.1 KiB
Markdown

# 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, and bulk frame
ingest from a recording session (see `labeling/frontend/` for the UI). **Not yet
implemented:** dataset versioning/promotion (`enemy-v1`, `enemy-v2`, ...) —
deferred since it needs its own design pass (snapshot semantics: labels are
editable at any time, but a promoted dataset version must stay reproducible).
Also 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`).
Frame images are served from `FRAMES_ROOT` (env var, default `./frames`) at
`/images/<image_path>`; `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).
## 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.