feat(perception): integrate calibrated operator pipeline
Add calibrated K1 projection, recorded and near-live perception qualification, unified Rerun operator layers, bounded replay admission, audited viewer controls, worker experiments, and lab evidence.
This commit is contained in:
+233
-17
@@ -12,10 +12,11 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi import APIRouter, HTTPException, Response
|
||||
from fastapi import APIRouter, HTTPException, Request, Response
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
import k1link.sessions.media as recorded_media_module
|
||||
from k1link.compute import RecordedPerceptionVideo
|
||||
from k1link.device_plugins.xgrids_k1 import xgrids_k1_archive_source
|
||||
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
|
||||
from k1link.sessions import (
|
||||
@@ -166,9 +167,24 @@ async def render_file_response(
|
||||
*,
|
||||
range_header: bytes | None,
|
||||
) -> tuple[dict[str, Any], bytes]:
|
||||
body_iterator = getattr(response, "body_iterator", None)
|
||||
if body_iterator is not None:
|
||||
chunks = [chunk async for chunk in body_iterator]
|
||||
return {
|
||||
"status": response.status_code,
|
||||
"headers": response.raw_headers,
|
||||
}, b"".join(
|
||||
chunk.encode("utf-8") if isinstance(chunk, str) else chunk
|
||||
for chunk in chunks
|
||||
)
|
||||
sent: list[dict[str, Any]] = []
|
||||
request_delivered = False
|
||||
|
||||
async def receive() -> dict[str, Any]:
|
||||
nonlocal request_delivered
|
||||
if request_delivered:
|
||||
return {"type": "http.disconnect"}
|
||||
request_delivered = True
|
||||
return {"type": "http.request", "body": b"", "more_body": False}
|
||||
|
||||
async def send(message: dict[str, Any]) -> None:
|
||||
@@ -1069,6 +1085,11 @@ def test_recorded_blueprint_endpoint_is_small_strict_and_session_scoped(
|
||||
palette="custom",
|
||||
custom_color="#112233",
|
||||
active_view="perception",
|
||||
view_reset_generation=1,
|
||||
unified_perception=True,
|
||||
show_detections_2d=True,
|
||||
show_segmentation=True,
|
||||
show_cuboids_3d=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -1083,6 +1104,11 @@ def test_recorded_blueprint_endpoint_is_small_strict_and_session_scoped(
|
||||
assert observed_settings[0].show_points is False
|
||||
assert observed_settings[0].show_trajectory is True
|
||||
assert observed_kwargs[0]["active_view"] == "perception"
|
||||
assert observed_kwargs[0]["view_reset_generation"] == 1
|
||||
assert observed_kwargs[0]["unified_perception"] is True
|
||||
assert observed_kwargs[0]["show_detections_2d"] is True
|
||||
assert observed_kwargs[0]["show_segmentation"] is True
|
||||
assert observed_kwargs[0]["show_cuboids_3d"] is True
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
RecordedBlueprintRequest.model_validate(
|
||||
@@ -1234,6 +1260,12 @@ def test_session_router_exposes_opaque_recorded_media_manifest_and_ranges(
|
||||
"epochs/{epoch_ordinal}/segments/{segment_sequence}.m4s",
|
||||
"GET",
|
||||
)
|
||||
stream_route = endpoint(
|
||||
router,
|
||||
"/api/v1/observation-sessions/{session_id}/media/{artifact_id}/"
|
||||
"epochs/{epoch_ordinal}/recording.mp4",
|
||||
"GET",
|
||||
)
|
||||
|
||||
replay = asyncio.run(replay_route(session_id=session.name, request=None))
|
||||
source = replay["launch"]["media_sources"][0]
|
||||
@@ -1271,39 +1303,66 @@ def test_session_router_exposes_opaque_recorded_media_manifest_and_ranges(
|
||||
if_match=f'"sha256:{source["manifest_generation_sha256"]}"',
|
||||
)
|
||||
manifest = json.loads(manifest_response.body)
|
||||
assert manifest["schema_version"] == "missioncore.observation-recorded-media/v2"
|
||||
assert manifest["schema_version"] == "missioncore.observation-recorded-media/v3"
|
||||
assert manifest["source_id"] == source["id"]
|
||||
assert manifest["generation_sha256"] == source["manifest_generation_sha256"]
|
||||
assert manifest["byte_length"] == source["byte_length"]
|
||||
assert manifest["timeline_start_seconds"] == source["timeline_start_seconds"]
|
||||
assert manifest["timeline_end_seconds"] == source["timeline_end_seconds"]
|
||||
assert manifest_response.headers["etag"] == (f'"sha256:{manifest["generation_sha256"]}"')
|
||||
init_sha256 = hashlib.sha256(init).hexdigest()
|
||||
segment_sha256 = hashlib.sha256(segment).hexdigest()
|
||||
generation = manifest["generation_sha256"]
|
||||
assert manifest["epochs"] == [
|
||||
{
|
||||
"ordinal": 1,
|
||||
"timeline_start_seconds": 0.0,
|
||||
"timeline_end_seconds": 0.5,
|
||||
"media_type": 'video/mp4; codecs="avc1.640028"',
|
||||
"init_url": manifest["epochs"][0]["init_url"],
|
||||
"init_byte_length": len(init),
|
||||
"init_sha256": init_sha256,
|
||||
"segment_count": 1,
|
||||
"segment_url_prefix": manifest["epochs"][0]["segment_url_prefix"],
|
||||
"segments": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"url": manifest["epochs"][0]["segments"][0]["url"],
|
||||
"byte_length": len(segment),
|
||||
"sha256": segment_sha256,
|
||||
}
|
||||
],
|
||||
"byte_length": len(init) + len(segment),
|
||||
"stream_url": (
|
||||
f"{source['manifest_url'].removesuffix('/manifest')}/epochs/1/recording.mp4"
|
||||
f"?generation={generation}"
|
||||
),
|
||||
}
|
||||
]
|
||||
assert "sensor.camera.private-left" not in json.dumps(manifest)
|
||||
assert_no_local_paths(manifest, repository)
|
||||
|
||||
request = Request({"type": "http", "method": "GET", "headers": []})
|
||||
with pytest.raises(HTTPException) as stale_stream_generation:
|
||||
stream_route(
|
||||
request=request,
|
||||
session_id=session.name,
|
||||
artifact_id=artifact_id,
|
||||
epoch_ordinal=1,
|
||||
generation="0" * 64,
|
||||
range_header="bytes=0-5",
|
||||
)
|
||||
assert stale_stream_generation.value.status_code == 412
|
||||
|
||||
stream_response = stream_route(
|
||||
request=request,
|
||||
session_id=session.name,
|
||||
artifact_id=artifact_id,
|
||||
epoch_ordinal=1,
|
||||
generation=generation,
|
||||
range_header=f"bytes={len(init) - 2}-{len(init) + 3}",
|
||||
)
|
||||
stream_start, stream_body = asyncio.run(
|
||||
render_file_response(stream_response, range_header=None)
|
||||
)
|
||||
stream_headers = {
|
||||
key.decode("latin-1"): value.decode("latin-1")
|
||||
for key, value in stream_start["headers"]
|
||||
}
|
||||
assert stream_start["status"] == 206
|
||||
assert stream_body == init[-2:] + segment[:4]
|
||||
assert stream_headers["content-range"] == (
|
||||
f"bytes {len(init) - 2}-{len(init) + 3}/{len(init) + len(segment)}"
|
||||
)
|
||||
assert stream_headers["content-length"] == "6"
|
||||
assert stream_headers["accept-ranges"] == "bytes"
|
||||
assert stream_headers["etag"] == f'"generation:{generation}:epoch:1"'
|
||||
|
||||
with pytest.raises(HTTPException) as missing_init_generation:
|
||||
init_route(
|
||||
session_id=session.name,
|
||||
@@ -1313,6 +1372,9 @@ def test_session_router_exposes_opaque_recorded_media_manifest_and_ranges(
|
||||
)
|
||||
assert missing_init_generation.value.status_code == 428
|
||||
|
||||
init_sha256 = hashlib.sha256(init).hexdigest()
|
||||
segment_sha256 = hashlib.sha256(segment).hexdigest()
|
||||
|
||||
init_response = init_route(
|
||||
session_id=session.name,
|
||||
artifact_id=artifact_id,
|
||||
@@ -1375,6 +1437,160 @@ def test_session_router_exposes_opaque_recorded_media_manifest_and_ranges(
|
||||
assert confined.value.status_code == 409
|
||||
|
||||
|
||||
def test_replay_exposes_generation_bound_perception_video_with_native_ranges(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
session = make_legacy_session(sessions, "20260720T065719Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
recording_payload = b"recording"
|
||||
|
||||
def export_recording(source: Path, destination: Path) -> dict[str, object]:
|
||||
destination.write_bytes(recording_payload)
|
||||
return {
|
||||
"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
|
||||
"rrd_sha256": hashlib.sha256(recording_payload).hexdigest(),
|
||||
"rrd_bytes": len(recording_payload),
|
||||
"timeline": "session_time",
|
||||
"timeline_start_ns": 0,
|
||||
"timeline_end_ns": 5_000_000_000,
|
||||
}
|
||||
|
||||
video_payload = b"sealed-panoptic-video"
|
||||
video_path = tmp_path / "perception.mp4"
|
||||
video_path.write_bytes(video_payload)
|
||||
video_sha256 = hashlib.sha256(video_payload).hexdigest()
|
||||
result_id = f"result-{'d' * 64}"
|
||||
video = RecordedPerceptionVideo(
|
||||
result_id=result_id,
|
||||
session_id=session.name,
|
||||
source_id="sensor.camera.right",
|
||||
public_source_id="recorded.perception.right",
|
||||
label="Сегментация · камера right",
|
||||
path=video_path,
|
||||
media_type='video/mp4; codecs="avc1.640028"',
|
||||
byte_length=len(video_payload),
|
||||
sha256=video_sha256,
|
||||
timeline_start_seconds=0.5,
|
||||
timeline_end_seconds=4.5,
|
||||
)
|
||||
|
||||
class Provider:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[str, str | None]] = []
|
||||
|
||||
def video(
|
||||
self,
|
||||
session_id: str,
|
||||
requested_result_id: str | None = None,
|
||||
) -> RecordedPerceptionVideo | None:
|
||||
self.calls.append((session_id, requested_result_id))
|
||||
if session_id != video.session_id:
|
||||
return None
|
||||
if requested_result_id is not None and requested_result_id != video.result_id:
|
||||
return None
|
||||
return video
|
||||
|
||||
provider = Provider()
|
||||
router = build_session_router(
|
||||
store,
|
||||
recording_materializer=SessionRecordingMaterializer(
|
||||
store.data_dir,
|
||||
exporter=export_recording,
|
||||
),
|
||||
perception_media_provider=provider,
|
||||
allow_synchronous_recording_fallback=True,
|
||||
)
|
||||
replay_route = endpoint(
|
||||
router,
|
||||
"/api/v1/observation-sessions/{session_id}/replay",
|
||||
"POST",
|
||||
)
|
||||
manifest_route = endpoint(
|
||||
router,
|
||||
"/api/v1/observation-sessions/{session_id}/perception-media/"
|
||||
"{result_id}/manifest",
|
||||
"GET",
|
||||
)
|
||||
stream_route = endpoint(
|
||||
router,
|
||||
"/api/v1/observation-sessions/{session_id}/perception-media/"
|
||||
"{result_id}/recording.mp4",
|
||||
"GET",
|
||||
)
|
||||
|
||||
replay = asyncio.run(replay_route(session_id=session.name, request=None))
|
||||
# Perception is loaded through the on-demand RRD overlay and no longer
|
||||
# blocks replay launch by hashing a duplicate pre-rendered video.
|
||||
assert replay["launch"]["media_sources"] == []
|
||||
assert provider.calls == []
|
||||
|
||||
with pytest.raises(HTTPException) as missing_generation:
|
||||
manifest_route(session_id=session.name, result_id=result_id)
|
||||
assert missing_generation.value.status_code == 428
|
||||
manifest_response = manifest_route(
|
||||
session_id=session.name,
|
||||
result_id=result_id,
|
||||
if_match=f'"sha256:{video_sha256}"',
|
||||
)
|
||||
assert provider.calls == [
|
||||
(session.name, result_id),
|
||||
(session.name, result_id),
|
||||
]
|
||||
manifest = json.loads(manifest_response.body)
|
||||
assert manifest == {
|
||||
"schema_version": "missioncore.observation-recorded-media/v3",
|
||||
"source_id": "recorded.perception.right",
|
||||
"generation_sha256": video_sha256,
|
||||
"byte_length": len(video_payload),
|
||||
"timeline_start_seconds": 0.5,
|
||||
"timeline_end_seconds": 4.5,
|
||||
"synchronization": "host-arrival-best-effort",
|
||||
"epochs": [
|
||||
{
|
||||
"ordinal": 1,
|
||||
"timeline_start_seconds": 0.5,
|
||||
"timeline_end_seconds": 4.5,
|
||||
"media_type": 'video/mp4; codecs="avc1.640028"',
|
||||
"byte_length": len(video_payload),
|
||||
"stream_url": (
|
||||
f"/api/v1/observation-sessions/{session.name}/perception-media/"
|
||||
f"{result_id}/recording.mp4?generation={video_sha256}"
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
request = Request({"type": "http", "method": "GET", "headers": []})
|
||||
with pytest.raises(HTTPException) as stale_generation:
|
||||
stream_route(
|
||||
request=request,
|
||||
session_id=session.name,
|
||||
result_id=result_id,
|
||||
generation="0" * 64,
|
||||
range_header=None,
|
||||
)
|
||||
assert stale_generation.value.status_code == 412
|
||||
response = stream_route(
|
||||
request=request,
|
||||
session_id=session.name,
|
||||
result_id=result_id,
|
||||
generation=video_sha256,
|
||||
range_header="bytes=7-14",
|
||||
)
|
||||
start, body = asyncio.run(render_file_response(response, range_header=b"bytes=7-14"))
|
||||
headers = {
|
||||
key.decode("latin-1"): value.decode("latin-1") for key, value in start["headers"]
|
||||
}
|
||||
assert start["status"] == 206
|
||||
assert body == video_payload[7:15]
|
||||
assert headers["content-range"] == f"bytes 7-14/{len(video_payload)}"
|
||||
assert headers["accept-ranges"] == "bytes"
|
||||
assert headers["etag"] == f'"sha256:{video_sha256}"'
|
||||
|
||||
|
||||
def test_ready_launch_uses_background_prepared_camera_manifest_without_rescan(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
Reference in New Issue
Block a user