spelunkai/training/tests/test_client.py
Jonas 07e75247c4 Implement v1 training pipeline: CenterNet-style detector
Add the anchor-free, center-heatmap detector CLAUDE.md §3.3 specifies:
- targets.py: encodes ground-truth boxes into a per-class Gaussian
  heatmap + wh regression target + center mask, using the standard
  CornerNet/CenterNet gaussian-radius formulation.
- model.py: a small conv backbone (stride 4) with heatmap (sigmoid)
  and wh regression heads - exactly the two outputs CLAUDE.md
  specifies, sized with the 66ms/tick budget in mind.
- losses.py: modified focal loss (heatmap) + masked L1 (wh), combined
  with the standard CenterNet wh_weight=0.1.
- client.py / dataset.py: pull a promoted dataset version + its set's
  class list from the labeling backend and turn it into a
  torch.utils.data.Dataset, reading images from local disk (same
  machine as FRAMES_ROOT).
- train.py: wires it into a basic DataLoader -> train loop ->
  per-epoch checkpoint.

The exact loss weighting/architecture sizing is a reasonable, standard
v1 default, not a tuned final answer - CLAUDE.md's own roadmap flags
the exact formulation as still open; this is the starting point to
iterate from.

16/16 unit tests pass on CPU with synthetic data (target/model/loss
correctness, dataset-version parsing). Beyond that, ran a real
end-to-end smoke test: live labeling backend -> promoted dataset
version -> `spelunkai-train` actually training one real epoch against
it and writing a checkpoint. Not verified: multi-epoch convergence on
real data, which needs jai's GPU and real labeled frames.

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

45 lines
1.3 KiB
Python

from spelunkai_training.client import build_class_mapping, parse_dataset_version
def test_parse_dataset_version():
payload = {
"id": 1,
"set_id": 2,
"name": "v1",
"frames": [
{
"frame": {
"id": 10, "session_name": "run01", "frame_index": 0,
"image_path": "run01_frames/frame_000000.png", "width": 1280, "height": 720,
},
"labels": [
{"sub_class_id": 5, "x": 1.0, "y": 2.0, "width": 3.0, "height": 4.0},
],
},
],
}
result = parse_dataset_version(payload)
assert result.id == 1
assert result.set_id == 2
assert result.name == "v1"
assert len(result.frames) == 1
frame = result.frames[0]
assert frame.image_path == "run01_frames/frame_000000.png"
assert len(frame.labels) == 1
assert frame.labels[0].sub_class_id == 5
def test_build_class_mapping_is_stable_and_ordered_by_id():
payload = {
"main_classes": [
{"id": 1, "name": "Enemy", "sub_classes": [{"id": 5, "name": "Snake"}, {"id": 2, "name": "Bat"}]},
{"id": 2, "name": "Hazard", "sub_classes": [{"id": 9, "name": "Spikes"}]},
],
}
mapping = build_class_mapping(payload)
assert mapping == {2: 0, 5: 1, 9: 2}