515 lines
18 KiB
Python
515 lines
18 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
import k1link.compute.fusion_epoch as fusion_module
|
|
import k1link.compute.perception_epoch as epoch_module
|
|
import k1link.compute.results as result_module
|
|
from k1link.compute import (
|
|
RecordedCalibratedFusionStore,
|
|
RecordedPerceptionEpochStore,
|
|
RecordedPerceptionOverlayStore,
|
|
prepare_camera_compute_job,
|
|
validate_recorded_calibrated_fusion,
|
|
validate_recorded_perception_epoch_result,
|
|
validate_recorded_perception_result,
|
|
)
|
|
from k1link.sessions import SessionIntegrityError
|
|
from k1link.web.camera_archive import CameraArchiveWriter
|
|
|
|
|
|
def _canonical_json(value: object) -> bytes:
|
|
return json.dumps(
|
|
value,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
|
|
|
|
def _job(tmp_path: Path) -> Any:
|
|
origin_epoch_ns = 1_000_000_000
|
|
origin_monotonic_ns = 2_000_000_000
|
|
session = tmp_path / "session-1"
|
|
session.mkdir()
|
|
|
|
def box(box_type: bytes, payload: bytes = b"") -> bytes:
|
|
return (8 + len(payload)).to_bytes(4, "big") + box_type + payload
|
|
|
|
def full_box(box_type: bytes, payload: bytes = b"", *, flags: int = 0) -> bytes:
|
|
return box(box_type, bytes([0]) + flags.to_bytes(3, "big") + payload)
|
|
|
|
track_id = 1
|
|
tkhd = full_box(b"tkhd", b"\x00" * 8 + track_id.to_bytes(4, "big") + b"\x00" * 4)
|
|
mdhd = full_box(
|
|
b"mdhd",
|
|
b"\x00" * 8 + (1_000).to_bytes(4, "big") + b"\x00" * 4,
|
|
)
|
|
hdlr = full_box(b"hdlr", b"\x00" * 4 + b"vide")
|
|
trak = box(b"trak", tkhd + box(b"mdia", mdhd + hdlr))
|
|
trex = full_box(
|
|
b"trex",
|
|
track_id.to_bytes(4, "big")
|
|
+ (1).to_bytes(4, "big")
|
|
+ (500).to_bytes(4, "big")
|
|
+ b"\x00" * 8,
|
|
)
|
|
init = box(b"ftyp", b"isom") + box(
|
|
b"moov",
|
|
trak + box(b"mvex", trex) + box(b"avcC", b"\x01\x64\x00\x28"),
|
|
)
|
|
tfhd = full_box(b"tfhd", track_id.to_bytes(4, "big"), flags=0x020000)
|
|
tfdt = full_box(b"tfdt", (0).to_bytes(4, "big"))
|
|
trun = full_box(b"trun", (1).to_bytes(4, "big"))
|
|
fragment = box(b"moof", box(b"traf", tfhd + tfdt + trun)) + box(b"mdat", b"frame")
|
|
writer = CameraArchiveWriter(session, "sensor.camera.left", 1)
|
|
writer.append(
|
|
"init",
|
|
init,
|
|
host_epoch_ns=origin_epoch_ns + 100_000_000,
|
|
host_monotonic_ns=origin_monotonic_ns + 100_000_000,
|
|
)
|
|
writer.append(
|
|
"media",
|
|
fragment,
|
|
host_epoch_ns=origin_epoch_ns + 200_000_000,
|
|
host_monotonic_ns=origin_monotonic_ns + 200_000_000,
|
|
)
|
|
writer.close()
|
|
return prepare_camera_compute_job(
|
|
session_root=session,
|
|
source_id="sensor.camera.left",
|
|
codec_epoch=1,
|
|
origin_epoch_ns=origin_epoch_ns,
|
|
origin_monotonic_ns=origin_monotonic_ns,
|
|
output_root=tmp_path / "jobs",
|
|
)
|
|
|
|
|
|
def _result(tmp_path: Path, job: Any) -> Path:
|
|
parameters = {
|
|
"score_threshold": 0.25,
|
|
"nms_threshold": 0.45,
|
|
"input_shape": [1, 3, 640, 640],
|
|
}
|
|
pipeline = {"id": "recorded-camera-coco-detection", "version": 1}
|
|
model = {
|
|
"id": "synthetic",
|
|
"version": 1,
|
|
"sha256": "a" * 64,
|
|
"source_url": "https://example.invalid/model.onnx",
|
|
"license": "test-only",
|
|
"classes": "synthetic",
|
|
}
|
|
identity = {
|
|
"schema_version": "missioncore.compute-result-identity/v1",
|
|
"job_id": job.job_id,
|
|
"input_sha256": job.input_sha256,
|
|
"pipeline": pipeline,
|
|
"model": {key: model[key] for key in ("id", "version", "sha256")},
|
|
"parameters": parameters,
|
|
}
|
|
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
|
result_id = f"result-{identity_sha256}"
|
|
root = tmp_path / "results" / job.job_id / result_id
|
|
root.mkdir(parents=True)
|
|
detections = {
|
|
"schema_version": "missioncore.object-detections/v1",
|
|
"result_id": result_id,
|
|
"job_id": job.job_id,
|
|
"input_sha256": job.input_sha256,
|
|
"timestamp_basis": "session-time-seconds",
|
|
"frames": [
|
|
{
|
|
"frame_index": 0,
|
|
"epoch_seconds": 0.0,
|
|
"session_seconds": job.timeline_start_seconds,
|
|
"detections": [
|
|
{
|
|
"class_id": 0,
|
|
"class_name": "person",
|
|
"score": 0.75,
|
|
"bbox_xyxy": [10.0, 20.0, 30.0, 60.0],
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
detection_payload = json.dumps(detections, indent=2).encode() + b"\n"
|
|
(root / "detections.json").write_bytes(detection_payload)
|
|
result = {
|
|
"schema_version": "missioncore.compute-result/v1",
|
|
"result_id": result_id,
|
|
"identity_sha256": identity_sha256,
|
|
"job_id": job.job_id,
|
|
"input_sha256": job.input_sha256,
|
|
"created_at_utc": "2026-07-19T00:00:00.000Z",
|
|
"pipeline": pipeline,
|
|
"runtime": {"kind": "synthetic"},
|
|
"model": model,
|
|
"parameters": parameters,
|
|
"metrics": {"frames_processed": 1, "detections": 1},
|
|
"artifacts": [
|
|
{
|
|
"kind": "object-detections",
|
|
"path": "detections.json",
|
|
"byte_length": len(detection_payload),
|
|
"sha256": hashlib.sha256(detection_payload).hexdigest(),
|
|
"schema_version": "missioncore.object-detections/v1",
|
|
}
|
|
],
|
|
"warnings": [],
|
|
}
|
|
(root / "result.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
|
|
return root
|
|
|
|
|
|
def _epoch_result(tmp_path: Path, job: Any) -> Path:
|
|
identity = {
|
|
"schema_version": "missioncore.recorded-perception-identity/v2",
|
|
"job_id": job.job_id,
|
|
"input_sha256": job.input_sha256,
|
|
"calibration": {
|
|
"content_identity_sha256": "b" * 64,
|
|
"camera_slot": "camera_1",
|
|
},
|
|
"configuration": {"pipeline": "synthetic-panoptic/v1"},
|
|
"models": {"instance": {"id": "synthetic"}},
|
|
"publication": {
|
|
"video_encoder": "synthetic",
|
|
"video_media_type": "video/mp4",
|
|
"mask_archive_media_type": "application/gzip",
|
|
},
|
|
}
|
|
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
|
result_id = f"result-{identity_sha256}"
|
|
root = tmp_path / "results" / job.job_id / result_id
|
|
root.mkdir(parents=True)
|
|
frame = {
|
|
"schema_version": "missioncore.panoptic-frame/v1",
|
|
"frame_index": 0,
|
|
"sequence": 1,
|
|
"session_seconds": job.timeline_start_seconds,
|
|
"instances": [],
|
|
"semantic_classes": [],
|
|
}
|
|
(root / "frames.jsonl").write_text(
|
|
json.dumps(frame, separators=(",", ":")) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
(root / "perception.mp4").write_bytes(b"synthetic-mp4")
|
|
(root / "masks.tar.gz").write_bytes(b"synthetic-masks")
|
|
(root / "gpu-telemetry.jsonl").write_text("{}\n", encoding="utf-8")
|
|
report = {
|
|
"schema_version": "missioncore.perception-run-report/v1",
|
|
"state": "published",
|
|
"result_id": result_id,
|
|
"input": {"job_id": job.job_id, "input_sha256": job.input_sha256},
|
|
"metrics": {
|
|
"frames_expected": 1,
|
|
"frames_processed": 1,
|
|
"frames_failed": 0,
|
|
"frames_skipped": 0,
|
|
},
|
|
}
|
|
(root / "run-report.json").write_text(json.dumps(report) + "\n", encoding="utf-8")
|
|
artifact_contracts = (
|
|
("panoptic-overlay-video", "perception.mp4", "video/mp4", None),
|
|
("panoptic-mask-archive", "masks.tar.gz", "application/gzip", None),
|
|
(
|
|
"panoptic-frame-metadata",
|
|
"frames.jsonl",
|
|
"application/x-ndjson",
|
|
"missioncore.panoptic-frame/v1",
|
|
),
|
|
("worker-gpu-telemetry", "gpu-telemetry.jsonl", "application/x-ndjson", None),
|
|
(
|
|
"perception-run-report",
|
|
"run-report.json",
|
|
"application/json",
|
|
"missioncore.perception-run-report/v1",
|
|
),
|
|
)
|
|
artifacts = []
|
|
for kind, name, media_type, schema_version in artifact_contracts:
|
|
path = root / name
|
|
descriptor = {
|
|
"kind": kind,
|
|
"path": name,
|
|
"media_type": media_type,
|
|
"byte_length": path.stat().st_size,
|
|
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
}
|
|
if schema_version is not None:
|
|
descriptor["schema_version"] = schema_version
|
|
artifacts.append(descriptor)
|
|
result = {
|
|
"schema_version": "missioncore.recorded-perception-result/v2",
|
|
"result_id": result_id,
|
|
"identity_sha256": identity_sha256,
|
|
"identity": identity,
|
|
"created_at_utc": "2026-07-20T00:00:00.000Z",
|
|
"job_id": job.job_id,
|
|
"input_sha256": job.input_sha256,
|
|
"session_id": job.session_id,
|
|
"source_id": job.source_id,
|
|
"codec_epoch": job.codec_epoch,
|
|
"timestamp_basis": "session-time-seconds",
|
|
"timeline_start_seconds": job.timeline_start_seconds,
|
|
"timeline_end_seconds": job.timeline_end_seconds,
|
|
"frames_processed": job.segment_count,
|
|
"artifacts": artifacts,
|
|
}
|
|
(root / "result.json").write_text(json.dumps(result) + "\n", encoding="utf-8")
|
|
return root
|
|
|
|
|
|
def _fusion_result(tmp_path: Path, job: Any, perception_root: Path) -> Path:
|
|
perception_result_id = perception_root.name
|
|
identity = {
|
|
"schema_version": "missioncore.recorded-calibrated-fusion-identity/v1",
|
|
"job_id": job.job_id,
|
|
"input_sha256": job.input_sha256,
|
|
"perception_result_id": perception_result_id,
|
|
"calibration_sha256": "b" * 64,
|
|
"camera_slot": "camera_1",
|
|
"configuration": {"pipeline": "synthetic-fusion/v1"},
|
|
}
|
|
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
|
fusion_id = f"fusion-{identity_sha256}"
|
|
root = tmp_path / "fusions" / job.job_id / fusion_id
|
|
root.mkdir(parents=True)
|
|
np.savez_compressed(
|
|
root / "fusion.npz",
|
|
frame_times_ns=np.asarray([0], dtype=np.int64),
|
|
point_offsets=np.asarray([0, 2], dtype=np.int64),
|
|
points=np.asarray([[1, 2, 3], [4, 5, 6]], dtype=np.float32),
|
|
point_colors=np.asarray([[1, 2, 3], [4, 5, 6]], dtype=np.uint8),
|
|
box_offsets=np.asarray([0, 1], dtype=np.int64),
|
|
box_centers=np.asarray([[1, 2, 3]], dtype=np.float32),
|
|
box_half_sizes=np.asarray([[0.5, 0.5, 0.5]], dtype=np.float32),
|
|
box_colors=np.asarray([[1, 2, 3, 96]], dtype=np.uint8),
|
|
)
|
|
(root / "box-labels.json").write_text('["person · 2.0 m · 5 pts"]', encoding="utf-8")
|
|
(root / "fusion-frames.jsonl").write_text(
|
|
'{"frame_index":0,"state":"fused"}\n',
|
|
encoding="utf-8",
|
|
)
|
|
artifacts = [
|
|
{
|
|
"name": path.name,
|
|
"byte_length": path.stat().st_size,
|
|
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
}
|
|
for path in (
|
|
root / "fusion.npz",
|
|
root / "box-labels.json",
|
|
root / "fusion-frames.jsonl",
|
|
)
|
|
]
|
|
manifest = {
|
|
"schema_version": "missioncore.recorded-calibrated-fusion/v1",
|
|
"fusion_id": fusion_id,
|
|
"identity_sha256": identity_sha256,
|
|
"identity": identity,
|
|
"created_at_utc": "2026-07-20T01:00:00.000Z",
|
|
"session_id": job.session_id,
|
|
"source_id": job.source_id,
|
|
"frame_count": job.segment_count,
|
|
"timeline_start_seconds": job.timeline_start_seconds,
|
|
"timeline_end_seconds": job.timeline_end_seconds,
|
|
"artifacts": artifacts,
|
|
}
|
|
(root / "manifest.json").write_text(json.dumps(manifest) + "\n", encoding="utf-8")
|
|
return root
|
|
|
|
|
|
def test_result_is_bound_to_job_and_cache_reuses_complete_overlay(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
job = _job(tmp_path)
|
|
result_root = _result(tmp_path, job)
|
|
result = validate_recorded_perception_result(job.job_root, result_root)
|
|
assert result.job.session_id == "session-1"
|
|
assert result.frames[0].detections[0].class_name == "person"
|
|
|
|
calls = 0
|
|
|
|
def render(*_: object, **__: object) -> bytes:
|
|
nonlocal calls
|
|
calls += 1
|
|
return b"RRF2synthetic-overlay"
|
|
|
|
monkeypatch.setattr(result_module, "_render_overlay", render)
|
|
store = RecordedPerceptionOverlayStore(
|
|
jobs_root=tmp_path / "jobs",
|
|
results_root=tmp_path / "results",
|
|
cache_root=tmp_path / "cache",
|
|
ffmpeg_path=Path(__file__),
|
|
ffprobe_path=Path(__file__),
|
|
)
|
|
first = store.render(
|
|
"session-1",
|
|
application_id="nodedc_mission_core_recorded",
|
|
recording_id="recording-1",
|
|
)
|
|
second = store.render(
|
|
"session-1",
|
|
application_id="nodedc_mission_core_recorded",
|
|
recording_id="recording-1",
|
|
)
|
|
assert first == second == b"RRF2synthetic-overlay"
|
|
assert calls == 1
|
|
|
|
|
|
def test_result_rejects_changed_detection_payload(tmp_path: Path) -> None:
|
|
job = _job(tmp_path)
|
|
result_root = _result(tmp_path, job)
|
|
detection_path = result_root / "detections.json"
|
|
detection_path.write_bytes(detection_path.read_bytes().replace(b"0.75", b"0.76"))
|
|
with pytest.raises(SessionIntegrityError, match="artifact identity changed"):
|
|
validate_recorded_perception_result(job.job_root, result_root)
|
|
|
|
|
|
def test_full_epoch_result_publishes_native_panoptic_video(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
job = _job(tmp_path)
|
|
result_root = _epoch_result(tmp_path, job)
|
|
result = validate_recorded_perception_epoch_result(job.job_root, result_root)
|
|
assert result.calibration_slot == "camera_1"
|
|
assert result.artifact("panoptic-overlay-video").byte_length == len(b"synthetic-mp4")
|
|
|
|
monkeypatch.setattr(
|
|
epoch_module.RecordedPerceptionEpochStore,
|
|
"_probe_video",
|
|
lambda *_: (0.5, "h264", 800, 600),
|
|
)
|
|
store = RecordedPerceptionEpochStore(
|
|
jobs_root=tmp_path / "jobs",
|
|
results_root=tmp_path / "results",
|
|
ffprobe_path=Path(__file__),
|
|
)
|
|
video = store.video("session-1")
|
|
assert video is not None
|
|
assert video.result_id == result.result_id
|
|
assert video.public_source_id == "recorded.perception.left"
|
|
assert video.timeline_start_seconds == 0
|
|
assert video.timeline_end_seconds == 0.5
|
|
|
|
|
|
def test_full_epoch_store_reuses_unchanged_validated_result(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
job = _job(tmp_path)
|
|
_epoch_result(tmp_path, job)
|
|
validation_calls = 0
|
|
validate = epoch_module.validate_recorded_perception_epoch_result
|
|
|
|
def count_validation(job_root: Path, result_root: Path) -> Any:
|
|
nonlocal validation_calls
|
|
validation_calls += 1
|
|
return validate(job_root, result_root)
|
|
|
|
monkeypatch.setattr(
|
|
epoch_module,
|
|
"validate_recorded_perception_epoch_result",
|
|
count_validation,
|
|
)
|
|
monkeypatch.setattr(
|
|
epoch_module.RecordedPerceptionEpochStore,
|
|
"_probe_video",
|
|
lambda *_: (0.5, "h264", 800, 600),
|
|
)
|
|
store = RecordedPerceptionEpochStore(
|
|
jobs_root=tmp_path / "jobs",
|
|
results_root=tmp_path / "results",
|
|
ffprobe_path=Path(__file__),
|
|
)
|
|
|
|
assert store.video("session-1") is not None
|
|
assert store.video("session-1") is not None
|
|
assert validation_calls == 1
|
|
|
|
|
|
def test_full_epoch_result_rejects_changed_frame_metadata(tmp_path: Path) -> None:
|
|
job = _job(tmp_path)
|
|
result_root = _epoch_result(tmp_path, job)
|
|
frame_path = result_root / "frames.jsonl"
|
|
frame_path.write_text("{}\n", encoding="utf-8")
|
|
with pytest.raises(SessionIntegrityError, match="artifact identity changed"):
|
|
validate_recorded_perception_epoch_result(job.job_root, result_root)
|
|
|
|
|
|
def test_calibrated_fusion_is_bound_to_full_epoch_and_reuses_render_cache(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
job = _job(tmp_path)
|
|
perception_root = _epoch_result(tmp_path, job)
|
|
fusion_root = _fusion_result(tmp_path, job, perception_root)
|
|
fusion = validate_recorded_calibrated_fusion(
|
|
job.job_root,
|
|
tmp_path / "results",
|
|
fusion_root,
|
|
)
|
|
assert fusion.perception_result_id == perception_root.name
|
|
assert fusion.frame_count == 1
|
|
rendered = fusion_module._render_fusion(
|
|
fusion,
|
|
application_id="nodedc_mission_core_recorded",
|
|
recording_id="recording-render-proof",
|
|
)
|
|
assert rendered.startswith(b"RRF2")
|
|
|
|
calls = 0
|
|
|
|
def render(*_: object, **__: object) -> bytes:
|
|
nonlocal calls
|
|
calls += 1
|
|
return b"RRF2synthetic-fusion"
|
|
|
|
monkeypatch.setattr(fusion_module, "_render_fusion", render)
|
|
store = RecordedCalibratedFusionStore(
|
|
jobs_root=tmp_path / "jobs",
|
|
perception_results_root=tmp_path / "results",
|
|
fusion_results_root=tmp_path / "fusions",
|
|
cache_root=tmp_path / "fusion-cache",
|
|
)
|
|
first = store.render(
|
|
"session-1",
|
|
application_id="nodedc_mission_core_recorded",
|
|
recording_id="recording-1",
|
|
)
|
|
second = store.render(
|
|
"session-1",
|
|
application_id="nodedc_mission_core_recorded",
|
|
recording_id="recording-1",
|
|
)
|
|
assert first == second == b"RRF2synthetic-fusion"
|
|
assert calls == 1
|
|
|
|
|
|
def test_calibrated_fusion_rejects_changed_array_artifact(tmp_path: Path) -> None:
|
|
job = _job(tmp_path)
|
|
perception_root = _epoch_result(tmp_path, job)
|
|
fusion_root = _fusion_result(tmp_path, job, perception_root)
|
|
with (fusion_root / "fusion.npz").open("ab") as stream:
|
|
stream.write(b"changed")
|
|
with pytest.raises(SessionIntegrityError, match="artifact identity changed"):
|
|
validate_recorded_calibrated_fusion(
|
|
job.job_root,
|
|
tmp_path / "results",
|
|
fusion_root,
|
|
)
|