fix(replay): stream sealed perception overlays

This commit is contained in:
DCCONSTRUCTIONS
2026-07-30 00:51:14 +03:00
parent 091ab2671e
commit 2ab9e45548
10 changed files with 380 additions and 44 deletions
+2
View File
@@ -220,6 +220,7 @@ from .realtime_tracking_qualification import (
from .results import (
DetectionFrame,
ObjectDetection,
RecordedPerceptionOverlayArtifact,
RecordedPerceptionOverlayError,
RecordedPerceptionOverlayStore,
RecordedPerceptionResult,
@@ -414,6 +415,7 @@ __all__ = [
"lidar_field_review_catalog_item",
"DetectionFrame",
"ObjectDetection",
"RecordedPerceptionOverlayArtifact",
"RecordedPerceptionOverlayError",
"RecordedPerceptionOverlayStore",
"RecordedPerceptionResult",
+69 -7
View File
@@ -22,7 +22,10 @@ from k1link.sessions import SessionIntegrityError
from .jobs import CameraComputeJob, validate_camera_compute_job
from .perception_epoch import validate_recorded_perception_epoch_result
from .results import RecordedPerceptionOverlayError
from .results import (
RecordedPerceptionOverlayArtifact,
RecordedPerceptionOverlayError,
)
FUSION_SCHEMA = "missioncore.recorded-calibrated-fusion/v1"
FUSION_IDENTITY_SCHEMA = "missioncore.recorded-calibrated-fusion-identity/v1"
@@ -188,18 +191,77 @@ class RecordedPerceptionOverlayMux:
application_id: str,
recording_id: str,
) -> bytes | None:
payload = self.primary.render(
materialized = self.materialize(
session_id,
application_id=application_id,
recording_id=recording_id,
)
if not isinstance(materialized, RecordedPerceptionOverlayArtifact):
return materialized
try:
payload = materialized.path.read_bytes()
except OSError as exc:
raise RecordedPerceptionOverlayError(
"recorded perception cache became unavailable"
) from exc
if (
len(payload) != materialized.byte_length
or hashlib.sha256(payload).hexdigest() != materialized.sha256
):
raise RecordedPerceptionOverlayError(
"recorded perception cache changed after validation"
)
return payload
def materialize(
self,
session_id: str,
*,
application_id: str,
recording_id: str,
) -> RecordedPerceptionOverlayArtifact | bytes | None:
primary_materializer = getattr(self.primary, "materialize", None)
payload = (
primary_materializer(
session_id,
application_id=application_id,
recording_id=recording_id,
)
if callable(primary_materializer)
else self.primary.render(
session_id,
application_id=application_id,
recording_id=recording_id,
)
)
if payload is not None and not isinstance(
payload, (bytes, RecordedPerceptionOverlayArtifact)
):
raise RecordedPerceptionOverlayError(
"recorded perception provider returned an invalid artifact"
)
if payload is not None or self.fallback is None:
return payload
return self.fallback.render(
session_id,
application_id=application_id,
recording_id=recording_id,
)
fallback_materializer = getattr(self.fallback, "materialize", None)
if callable(fallback_materializer):
fallback_payload = fallback_materializer(
session_id,
application_id=application_id,
recording_id=recording_id,
)
else:
fallback_payload = self.fallback.render(
session_id,
application_id=application_id,
recording_id=recording_id,
)
if fallback_payload is not None and not isinstance(
fallback_payload, (bytes, RecordedPerceptionOverlayArtifact)
):
raise RecordedPerceptionOverlayError(
"recorded perception fallback returned an invalid artifact"
)
return fallback_payload
def status(self, session_id: str, *, recording_id: str) -> dict[str, Any]:
primary_status = getattr(self.primary, "status", None)
+73 -20
View File
@@ -26,7 +26,10 @@ from k1link.artifacts import write_json_atomic
from k1link.sessions import SessionIntegrityError
from .jobs import CameraComputeJob, validate_camera_compute_job
from .results import RecordedPerceptionOverlayError
from .results import (
RecordedPerceptionOverlayArtifact,
RecordedPerceptionOverlayError,
)
RESULT_SCHEMA = "missioncore.e10-integrated-perception-result/v1"
IDENTITY_SCHEMA = "missioncore.e10-integrated-perception-identity/v1"
@@ -397,6 +400,39 @@ class IntegratedPerceptionOverlayStore:
application_id: str,
recording_id: str,
) -> bytes | None:
"""Compatibility adapter for in-process consumers that require bytes."""
artifact = self.materialize(
session_id,
application_id=application_id,
recording_id=recording_id,
)
if artifact is None:
return None
try:
payload = artifact.path.read_bytes()
except OSError as exc:
raise RecordedPerceptionOverlayError(
"integrated perception cache became unavailable"
) from exc
if (
len(payload) != artifact.byte_length
or hashlib.sha256(payload).hexdigest() != artifact.sha256
):
raise RecordedPerceptionOverlayError(
"integrated perception cache changed after validation"
)
return payload
def materialize(
self,
session_id: str,
*,
application_id: str,
recording_id: str,
) -> RecordedPerceptionOverlayArtifact | None:
"""Return a verified file-backed overlay without copying it into heap."""
if application_id != "nodedc_mission_core_recorded":
raise ValueError("integrated perception application id is invalid")
if (
@@ -414,7 +450,7 @@ class IntegratedPerceptionOverlayStore:
key,
state="ready",
phase="ready",
byte_length=len(cached),
byte_length=cached.byte_length,
)
return cached
self._set_status(key, state="preparing", phase="queued")
@@ -431,7 +467,7 @@ class IntegratedPerceptionOverlayStore:
)
output = cache / f"{recording_id}.rrd"
sidecar = output.with_suffix(".rrd.cache.json")
cached = _read_cache(
cached = _read_cache_artifact(
output,
sidecar,
result_id=result.result_id,
@@ -442,7 +478,7 @@ class IntegratedPerceptionOverlayStore:
key,
state="ready",
phase="ready",
byte_length=len(cached),
byte_length=cached.byte_length,
)
return cached
self._set_status(key, state="preparing", phase="rendering")
@@ -483,7 +519,17 @@ class IntegratedPerceptionOverlayStore:
phase="ready",
byte_length=len(payload),
)
return payload
published = _read_cache_artifact(
output,
sidecar,
result_id=result.result_id,
recording_id=recording_id,
)
if published is None:
raise RecordedPerceptionOverlayError(
"integrated perception cache publication failed"
)
return published
except BaseException:
self._set_status(key, state="error", phase="error")
raise
@@ -556,7 +602,7 @@ class IntegratedPerceptionOverlayStore:
self,
session_id: str,
recording_id: str,
) -> bytes | None:
) -> RecordedPerceptionOverlayArtifact | None:
cache_root = _private_directory(self.cache_root)
session_cache = _private_child(cache_root, session_id)
admission = self._load_admission(session_cache, session_id)
@@ -570,15 +616,15 @@ class IntegratedPerceptionOverlayStore:
recording_id,
)
if recovered is not None:
_, payload = recovered
return payload
_, artifact = recovered
return artifact
admission = None
if admission is None or admission.result_id != latest.result_id:
return None
result_cache = _private_child(session_cache, admission.result_id)
output = result_cache / f"{recording_id}.rrd"
sidecar = output.with_suffix(".rrd.cache.json")
return _read_cache(
return _read_cache_artifact(
output,
sidecar,
result_id=admission.result_id,
@@ -631,17 +677,17 @@ class IntegratedPerceptionOverlayStore:
session_cache: Path,
descriptor: _ResultDescriptor,
recording_id: str,
) -> tuple[_OverlayAdmission, bytes] | None:
) -> tuple[_OverlayAdmission, RecordedPerceptionOverlayArtifact] | None:
cache = _private_child(session_cache, descriptor.result_id)
output = cache / f"{recording_id}.rrd"
sidecar = output.with_suffix(".rrd.cache.json")
payload = _read_cache(
artifact = _read_cache_artifact(
output,
sidecar,
result_id=descriptor.result_id,
recording_id=recording_id,
)
if payload is None:
if artifact is None:
return None
admission = _OverlayAdmission(
session_id=descriptor.session_id,
@@ -650,7 +696,7 @@ class IntegratedPerceptionOverlayStore:
result_created_at_utc=descriptor.created_at_utc,
)
self._write_admission_document(session_cache, admission)
return admission, payload
return admission, artifact
def _write_admission(self, result: IntegratedPerceptionResult) -> None:
descriptor = _read_result_descriptor(result.result_root)
@@ -1183,16 +1229,19 @@ def _read_result_descriptor(result_root: Path) -> _ResultDescriptor:
)
def _read_cache(
def _read_cache_artifact(
output: Path,
sidecar: Path,
*,
result_id: str,
recording_id: str,
) -> bytes | None:
) -> RecordedPerceptionOverlayArtifact | None:
try:
value = _read_object(sidecar, sidecar.parent)
payload = output.read_bytes()
metadata = _confined_file(output, sidecar.parent)
with output.open("rb") as stream:
magic = stream.read(4)
digest = _sha256(output)
except (OSError, SessionIntegrityError):
return None
if (
@@ -1200,12 +1249,16 @@ def _read_cache(
or value.get("renderer_version") != OVERLAY_RENDERER_VERSION
or value.get("result_id") != result_id
or value.get("recording_id") != recording_id
or value.get("byte_length") != len(payload)
or value.get("sha256") != hashlib.sha256(payload).hexdigest()
or not payload.startswith(b"RRF2")
or value.get("byte_length") != metadata.st_size
or value.get("sha256") != digest
or magic != b"RRF2"
):
return None
return payload
return RecordedPerceptionOverlayArtifact(
path=output.resolve(strict=True),
byte_length=metadata.st_size,
sha256=digest,
)
def _read_object(path: Path, root: Path) -> dict[str, Any]:
+9
View File
@@ -53,6 +53,15 @@ class DetectionFrame:
detections: tuple[ObjectDetection, ...]
@dataclass(frozen=True, slots=True)
class RecordedPerceptionOverlayArtifact:
"""One validated file-backed RRD ready for bounded HTTP streaming."""
path: Path
byte_length: int
sha256: str
@dataclass(frozen=True, slots=True)
class RecordedPerceptionResult:
result_id: str