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

89 lines
2.7 KiB
Python

from pathlib import Path
import pytest
from spelunkai_labeling_backend import models
from spelunkai_labeling_backend.db import Database
from spelunkai_labeling_backend.ingest import ingest_session
def _make_frame_files(frames_dir: Path, count: int) -> None:
frames_dir.mkdir(parents=True, exist_ok=True)
for i in range(count):
(frames_dir / f"frame_{i:06d}.png").write_bytes(b"fake")
def _make_database(tmp_path: Path) -> Database:
database = Database(f"sqlite:///{tmp_path / 'test.db'}")
database.init_models()
return database
def test_ingest_session_inserts_all_frames(tmp_path):
frames_dir = tmp_path / "run01_frames"
_make_frame_files(frames_dir, 3)
database = _make_database(tmp_path)
result = ingest_session(
database=database,
frames_dir=frames_dir,
session_name="run01",
image_path_prefix="run01_frames",
width=1280,
height=720,
)
assert result.inserted == 3
assert result.already_existed == 0
assert result.total_seen == 3
db_session = database.session_factory()
try:
frames = db_session.query(models.Frame).order_by(models.Frame.frame_index).all()
assert [f.frame_index for f in frames] == [0, 1, 2]
assert [f.image_path for f in frames] == [
"run01_frames/frame_000000.png",
"run01_frames/frame_000001.png",
"run01_frames/frame_000002.png",
]
assert frames[0].width == 1280
assert frames[0].height == 720
finally:
db_session.close()
def test_ingest_session_is_idempotent_and_picks_up_new_frames(tmp_path):
frames_dir = tmp_path / "run01_frames"
_make_frame_files(frames_dir, 2)
database = _make_database(tmp_path)
ingest_session(database, frames_dir, "run01", "run01_frames", 1280, 720)
_make_frame_files(frames_dir, 3) # adds frame_000002.png
result = ingest_session(database, frames_dir, "run01", "run01_frames", 1280, 720)
assert result.inserted == 1
assert result.already_existed == 2
assert result.total_seen == 3
def test_ingest_session_keeps_sessions_independent(tmp_path):
frames_dir = tmp_path / "run01_frames"
_make_frame_files(frames_dir, 2)
database = _make_database(tmp_path)
ingest_session(database, frames_dir, "run01", "run01_frames", 1280, 720)
result = ingest_session(database, frames_dir, "run02", "run01_frames", 1280, 720)
assert result.inserted == 2
assert result.already_existed == 0
def test_ingest_session_raises_when_no_frames_found(tmp_path):
frames_dir = tmp_path / "empty"
frames_dir.mkdir()
database = _make_database(tmp_path)
with pytest.raises(FileNotFoundError):
ingest_session(database, frames_dir, "run01", "empty", 1280, 720)