Add DatasetVersion/DatasetVersionFrame/DatasetVersionLabel and a
promote endpoint (POST /sets/{id}/dataset-versions) that freezes a
set's currently-reviewed frames (or an explicit frame_ids selection)
into a named, immutable snapshot: it copies each label's data at
promotion time rather than referencing the live rows, so later edits
or deletes to those labels can't retroactively change an already
-promoted version. GET /dataset-versions/{id} returns the frozen
frames+labels - this is what the training pipeline will eventually
pull from.
This was the labeling backend's last deliberately-deferred piece from
the original data model (needed its own design pass for snapshot
semantics). 31/31 backend tests pass, including one that promotes a
version, edits and deletes the live label afterward, and asserts the
snapshot is untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
101 lines
3.9 KiB
Markdown
101 lines
3.9 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, 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/<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).
|
|
|
|
## 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.
|