fix(lab): seal RAV004 spatial replay transport

This commit is contained in:
DCCONSTRUCTIONS
2026-08-30 10:42:41 +03:00
parent f1cbe0061a
commit 757d368c86
11 changed files with 492 additions and 91 deletions
+248 -44
View File
@@ -26,13 +26,16 @@ from k1link.laboratory.evidence_report import (
)
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA
from k1link.sessions import RecordedCameraFrame, SessionIntegrityError
from k1link.sessions.canonical_lab_spatial import canonical_lab_spatial_timeline_samples
from k1link.sessions.canonical_lab_spatial import (
canonical_lab_spatial_playback_points,
canonical_lab_spatial_timeline_samples,
)
RootProvider = Callable[[], Path | None]
CanonicalRecordingProvider = Callable[[str], tuple[Path, str] | None]
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
_CANONICAL_ROUTE_CHUNK_FRAMES: Final = 8
_CANONICAL_ROUTE_CHUNK_FRAMES: Final = 24
_DEFINITION: Final = LaboratoryEvidenceDefinition(
work_id="lab-v1-vegetation-shadow",
runtime_relative_root=PurePosixPath("lab-v1-vegetation/results"),
@@ -167,19 +170,7 @@ def _build_vegetation_lab_router(
archive_path = candidate.joinpath(*relative.parts)
member = f"masks/frame-{sequence + 1:06d}.png"
try:
before = archive_path.stat()
with zipfile.ZipFile(archive_path) as frozen:
info = frozen.getinfo(member)
if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024:
raise ValueError("Vegetation video mask member is invalid")
payload = frozen.read(info)
after = archive_path.stat()
if (
before.st_size != after.st_size
or before.st_mtime_ns != after.st_mtime_ns
or len(payload) != info.file_size
):
raise ValueError("Vegetation video mask archive changed during read")
payload = _read_cached_mask_member(archive_path, member)
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
raise HTTPException(
status_code=503,
@@ -286,7 +277,7 @@ def _build_vegetation_lab_router(
route, frame_times_ns = _full_route_context(candidate, manifest)
intervals = [
(current - previous) / 1_000_000_000
for previous, current in zip(frame_times_ns, frame_times_ns[1:])
for previous, current in zip(frame_times_ns, frame_times_ns[1:], strict=False)
]
nominal_interval = statistics.median(intervals)
if not math.isfinite(nominal_interval) or nominal_interval <= 0:
@@ -350,7 +341,10 @@ def _build_vegetation_lab_router(
if start >= len(frame_times_ns):
raise HTTPException(status_code=404, detail="Full-route timeline chunk not found")
if canonical_recording_provider is None:
raise HTTPException(status_code=503, detail="Canonical spatial recording is unavailable")
raise HTTPException(
status_code=503,
detail="Canonical spatial recording is unavailable",
)
recording = canonical_recording_provider(str(route["session_id"]))
if recording is None:
raise HTTPException(status_code=409, detail="Canonical spatial recording is not ready")
@@ -362,6 +356,7 @@ def _build_vegetation_lab_router(
frame_times_ns,
start,
count,
include_local_slam=False,
)
except (OSError, ValueError):
raise HTTPException(status_code=503, detail="Canonical spatial chunk failed") from None
@@ -391,6 +386,98 @@ def _build_vegetation_lab_router(
"access": "read-only-bounded-recorded-replay",
}
@router.get("/{result_id}/timeline/playback")
def get_canonical_route_timeline_playback(result_id: str) -> dict[str, object]:
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
points, offsets = _canonical_route_playback(
canonical_recording_provider,
route,
frame_times_ns,
)
points_view = memoryview(points).cast("B")
return {
"schema_version": "missioncore.recorded-spatial-playback/v1",
"result_id": result_id,
"frame_count": len(frame_times_ns),
"point_count": int(points.shape[0]),
"point_offsets": list(offsets),
"chunk_frame_count": _CANONICAL_ROUTE_CHUNK_FRAMES,
"resident_chunk_count_max": 4,
"forward_prefetch_chunk_count": 1,
"chunks": _canonical_route_playback_chunk_catalog(
prefix,
result_id,
points_view,
offsets,
),
"track": {
"id": "points-map-f32",
"url": f"{prefix}/{result_id}/timeline/playback/tracks/points-map-f32",
"media_type": "application/octet-stream",
"dtype": "<f4",
"shape": [int(points.shape[0]), 3],
"bytes": int(points.nbytes),
"sha256": hashlib.sha256(points_view).hexdigest(),
},
"coordinate_frame": "map",
"ground_truth": False,
"authority": "replay-simulated",
"access": "read-only-sealed-binary-playback",
}
@router.get("/{result_id}/timeline/playback/chunks/{chunk_index}")
def get_canonical_route_timeline_playback_chunk(
result_id: str,
chunk_index: int,
) -> Response:
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
points, offsets = _canonical_route_playback(
canonical_recording_provider,
route,
frame_times_ns,
)
points_view = memoryview(points).cast("B")
descriptor = _canonical_route_playback_chunk_descriptor(
prefix,
result_id,
points_view,
offsets,
chunk_index,
)
if descriptor is None:
raise HTTPException(status_code=404, detail="Full-route playback chunk not found")
point_start = int(descriptor["point_start"])
byte_length = int(descriptor["bytes"])
byte_start = point_start * 3 * 4
payload = bytes(points_view[byte_start : byte_start + byte_length])
return Response(
content=payload,
media_type="application/octet-stream",
headers=_immutable_binary_headers(byte_length, str(descriptor["sha256"])),
)
@router.get("/{result_id}/timeline/playback/tracks/points-map-f32")
def get_canonical_route_timeline_playback_track(result_id: str) -> Response:
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
points, _ = _canonical_route_playback(
canonical_recording_provider,
route,
frame_times_ns,
)
payload = memoryview(points).cast("B")
digest = hashlib.sha256(payload).hexdigest()
return Response(
content=bytes(payload),
media_type="application/octet-stream",
headers=_immutable_binary_headers(payload.nbytes, digest),
)
@router.get("/{result_id}/timeline/frames/{sequence}/camera")
def get_canonical_route_camera(result_id: str, sequence: int) -> Response:
if camera_frame_provider is None:
@@ -403,7 +490,10 @@ def _build_vegetation_lab_router(
try:
camera = camera_frame_provider(str(route["session_id"]), sequence)
except (OSError, SessionIntegrityError, ValueError):
raise HTTPException(status_code=503, detail="Full-route camera frame unavailable") from None
raise HTTPException(
status_code=503,
detail="Full-route camera frame unavailable",
) from None
if camera.width != route["width"] or camera.height != route["height"]:
raise HTTPException(status_code=503, detail="Full-route camera dimensions changed")
return Response(
@@ -495,15 +585,111 @@ def _full_route_context(
values = np.frombuffer(payload, dtype="<u8")
frame_times_ns = tuple(int(value) for value in values)
except (OSError, ValueError):
raise HTTPException(status_code=503, detail="Full-route timeline verification failed") from None
raise HTTPException(
status_code=503,
detail="Full-route timeline verification failed",
) from None
if (
len(frame_times_ns) != route["frame_count"]
or any(current <= previous for previous, current in zip(frame_times_ns, frame_times_ns[1:]))
or any(
current <= previous
for previous, current in zip(frame_times_ns, frame_times_ns[1:], strict=False)
)
):
raise HTTPException(status_code=503, detail="Full-route timeline order changed")
return route, frame_times_ns
def _canonical_route_playback(
provider: CanonicalRecordingProvider | None,
route: dict[str, Any],
frame_times_ns: tuple[int, ...],
) -> tuple[np.ndarray, tuple[int, ...]]:
if provider is None:
raise HTTPException(status_code=503, detail="Canonical spatial recording is unavailable")
recording = provider(str(route["session_id"]))
if recording is None:
raise HTTPException(status_code=409, detail="Canonical spatial recording is not ready")
recording_path, generation_sha256 = recording
try:
return canonical_lab_spatial_playback_points(
recording_path,
generation_sha256,
frame_times_ns,
)
except (OSError, ValueError):
raise HTTPException(status_code=503, detail="Canonical spatial playback failed") from None
def _canonical_route_playback_chunk_descriptor(
endpoint_prefix: str,
result_id: str,
points_view: memoryview,
offsets: tuple[int, ...],
chunk_index: int,
) -> dict[str, object] | None:
frame_count = len(offsets) - 1
start = chunk_index * _CANONICAL_ROUTE_CHUNK_FRAMES
if chunk_index < 0 or start >= frame_count:
return None
count = min(_CANONICAL_ROUTE_CHUNK_FRAMES, frame_count - start)
point_start = offsets[start]
point_stop = offsets[start + count]
byte_start = point_start * 3 * 4
byte_stop = point_stop * 3 * 4
payload = points_view[byte_start:byte_stop]
return {
"index": chunk_index,
"start": start,
"count": count,
"point_start": point_start,
"point_count": point_stop - point_start,
"url": f"{endpoint_prefix}/{result_id}/timeline/playback/chunks/{chunk_index}",
"media_type": "application/octet-stream",
"dtype": "<f4",
"shape": [point_stop - point_start, 3],
"bytes": payload.nbytes,
"sha256": hashlib.sha256(payload).hexdigest(),
}
def _canonical_route_playback_chunk_catalog(
endpoint_prefix: str,
result_id: str,
points_view: memoryview,
offsets: tuple[int, ...],
) -> list[dict[str, object]]:
frame_count = len(offsets) - 1
chunk_count = (
frame_count + _CANONICAL_ROUTE_CHUNK_FRAMES - 1
) // _CANONICAL_ROUTE_CHUNK_FRAMES
return [
descriptor
for chunk_index in range(chunk_count)
if (
descriptor := _canonical_route_playback_chunk_descriptor(
endpoint_prefix,
result_id,
points_view,
offsets,
chunk_index,
)
)
is not None
]
def _immutable_binary_headers(byte_length: int, sha256: str) -> dict[str, str]:
return {
"Cache-Control": "private, max-age=31536000, immutable",
"Content-Encoding": "identity",
"Content-Length": str(byte_length),
"ETag": f'"{sha256}"',
"X-Content-Type-Options": "nosniff",
"X-Uncompressed-Content-Length": str(byte_length),
}
def _canonical_timeline_frame(
*,
result_id: str,
@@ -518,7 +704,6 @@ def _canonical_timeline_frame(
points = [] if spatial is None or not include_points else spatial["source_points_body_xyz_m"]
point_count = 0 if spatial is None else int(spatial["source_point_count"])
body_frame = None if spatial is None else spatial["body_frame"]
local_slam = [] if spatial is None else spatial["local_slam_body_xyz_m"]
return {
"schema_version": "missioncore.recorded-spatial-evidence-frame/v1",
"sequence": sequence,
@@ -534,11 +719,6 @@ def _canonical_timeline_frame(
"point_cloud_source_count": point_count,
"point_cloud_sample_count": point_count if not include_points else len(points),
"point_cloud_layer": "current-increment",
"local_slam_body_xyz_m": local_slam,
"local_slam_source_frame_count": 0
if spatial is None else spatial["local_slam_source_frame_count"],
"local_slam_source_point_count": 0
if spatial is None else spatial["local_slam_source_point_count"],
"rolling_map_component_count": 0,
"metric_obstacles": [],
"camera_proposals": _semantic_component_proposals(candidate, route, sequence),
@@ -585,12 +765,14 @@ def _semantic_component_proposals_cached(
archive_mtime_ns: int,
sequence: int,
) -> tuple[dict[str, object], ...]:
del archive_size, archive_mtime_ns
archive_path = Path(archive_path_text)
member = f"masks/frame-{sequence + 1:06d}.png"
try:
with zipfile.ZipFile(archive_path) as frozen:
payload = frozen.read(member)
frozen = _cached_zip_archive(
archive_path_text,
archive_size,
archive_mtime_ns,
)
payload = frozen.read(member)
with Image.open(io.BytesIO(payload)) as image:
mask = np.asarray(image.convert("L"), dtype=np.uint8)
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
@@ -733,7 +915,10 @@ def _route_tgs_anchor_payload(path: Path, source_sequence: int) -> dict[str, obj
selected_points = np.ascontiguousarray(points[start:end], dtype=np.float32)
selected_states = np.ascontiguousarray(states[slot], dtype=np.uint8)
selected_z_bounds = np.ascontiguousarray(z_bounds[slot], dtype=np.float32)
if not np.isfinite(selected_points).all() or not np.isin(selected_states, [0, 1, 2, 3]).all():
if (
not np.isfinite(selected_points).all()
or not np.isin(selected_states, [0, 1, 2, 3]).all()
):
raise ValueError("Route TGS payload changed")
result = {
"schema_version": "missioncore.lab-v1-route-tgs-anchor/v1",
@@ -762,19 +947,7 @@ def _route_tgs_anchor_payload(path: Path, source_sequence: int) -> dict[str, obj
def _zip_mask_response(archive_path: Path, sequence: int) -> Response:
member = f"masks/frame-{sequence + 1:06d}.png"
try:
before = archive_path.stat()
with zipfile.ZipFile(archive_path) as frozen:
info = frozen.getinfo(member)
if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024:
raise ValueError("Semantic mask member is invalid")
payload = frozen.read(info)
after = archive_path.stat()
if (
before.st_size != after.st_size
or before.st_mtime_ns != after.st_mtime_ns
or len(payload) != info.file_size
):
raise ValueError("Semantic mask archive changed during read")
payload = _read_cached_mask_member(archive_path, member)
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
raise HTTPException(
status_code=503,
@@ -792,6 +965,37 @@ def _zip_mask_response(archive_path: Path, sequence: int) -> Response:
)
@lru_cache(maxsize=8)
def _cached_zip_archive(
archive_path_text: str,
archive_size: int,
archive_mtime_ns: int,
) -> zipfile.ZipFile:
del archive_size, archive_mtime_ns
return zipfile.ZipFile(archive_path_text)
def _read_cached_mask_member(archive_path: Path, member: str) -> bytes:
before = archive_path.stat()
frozen = _cached_zip_archive(
str(archive_path),
before.st_size,
before.st_mtime_ns,
)
info = frozen.getinfo(member)
if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024:
raise ValueError("Semantic mask member is invalid")
payload = frozen.read(info)
after = archive_path.stat()
if (
before.st_size != after.st_size
or before.st_mtime_ns != after.st_mtime_ns
or len(payload) != info.file_size
):
raise ValueError("Semantic mask archive changed during read")
return payload
def _configured_root(provider: RootProvider) -> Path | None:
candidate = provider()
if candidate is None: