feat(lab): add recorded realtime spatial playback

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 23:17:48 +03:00
parent aacc6dc43b
commit 9df9f58ab8
23 changed files with 1443 additions and 517 deletions
+1
View File
@@ -767,6 +767,7 @@ app.include_router(
root_provider=lambda: (
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "m4" / "replay-threat"
),
repository_root_provider=lambda: REPOSITORY_ROOT,
camera_frame_provider=(
session_recorded_camera_frame_service.extract
if session_recorded_camera_frame_service is not None
+89 -96
View File
@@ -22,11 +22,15 @@ from k1link.perception.threat_replay import (
ThreatReplayResult,
read_threat_replay_result,
)
from k1link.perception.threat_timeline import (
RECORDED_SPATIAL_MAX_CHUNK_FRAMES,
RecordedThreatTimeline,
RecordedThreatTimelineError,
)
from k1link.sessions import RecordedCameraFrame, SessionIntegrityError
M4_THREAT_CATALOG_SCHEMA: Final = "missioncore.m4-threat-replay-catalog/v1"
M4_THREAT_VIEW_SCHEMA: Final = "missioncore.m4-threat-replay-view/v1"
M4_THREAT_VIDEO_SCHEMA: Final = "missioncore.m4-threat-video-overlay/v1"
M4_THREAT_VISUAL_CATALOG_SCHEMA: Final = "missioncore.m4-threat-visual-catalog/v1"
_RESULT_ID = re.compile(rf"^{THREAT_REPLAY_RESULT_PREFIX}[a-f0-9]{{64}}$")
RootProvider = Callable[[], Path | None]
@@ -36,6 +40,7 @@ CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
def build_m4_threat_replay_router(
*,
root_provider: RootProvider = lambda: None,
repository_root_provider: RootProvider = lambda: None,
camera_frame_provider: CameraFrameProvider | None = None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory/m4-threat", tags=["laboratory"])
@@ -54,6 +59,23 @@ def build_m4_threat_replay_router(
except (ThreatReplayError, OSError, ValueError):
raise HTTPException(status_code=404, detail="M4.6 result не найден") from None
def timeline(result_id: str) -> RecordedThreatTimeline:
frozen = result(result_id)
repository_root = _configured_root(repository_root_provider)
if repository_root is None:
raise HTTPException(status_code=503, detail="M4.6 timeline source недоступен")
try:
return _read_threat_timeline_cached(
str(repository_root),
str(frozen.result_root),
_result_signature(frozen.result_root),
)
except (OSError, ThreatReplayError, RecordedThreatTimelineError, ValueError):
raise HTTPException(
status_code=503,
detail="M4.6 bounded timeline не прошёл проверку",
) from None
@router.get("/results")
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
candidates = _candidates(root_provider)
@@ -134,41 +156,38 @@ def build_m4_threat_replay_router(
sequence = frames[ordinal - 1].get("sequence")
if not isinstance(session_id, str) or not isinstance(sequence, int):
raise HTTPException(status_code=404, detail="M4.6 camera identity не найдена")
try:
camera = camera_frame_provider(session_id, sequence)
except (OSError, SessionIntegrityError, ValueError):
raise HTTPException(
status_code=503,
detail="M4.6 exact camera frame недоступен",
) from None
if camera.width != 800 or camera.height != 600:
raise HTTPException(
status_code=503,
detail="M4.6 camera frame нарушил размерный контракт",
)
return Response(
content=camera.payload,
media_type=camera.media_type,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{camera.sha256}"',
"X-Content-Type-Options": "nosniff",
},
)
return _camera_response(camera_frame_provider, session_id, sequence)
@router.get("/results/{result_id}/video-overlay")
def get_video_overlay(result_id: str) -> dict[str, object]:
frozen = result(result_id)
identity = frozen.manifest["identity"]
assert isinstance(identity, dict)
return copy.deepcopy(
_cached_video_overlay(
result_id,
str(frozen.result_root),
str(identity["frames_sha256"]),
str(identity["source_session_id"]),
)
)
@router.get("/results/{result_id}/timeline")
def get_timeline(result_id: str) -> dict[str, object]:
return copy.deepcopy(timeline(result_id).metadata())
@router.get("/results/{result_id}/timeline/chunk")
def get_timeline_chunk(
result_id: str,
start: int = Query(default=0, ge=0),
count: int = Query(
default=12,
ge=1,
le=RECORDED_SPATIAL_MAX_CHUNK_FRAMES,
),
) -> dict[str, object]:
try:
return timeline(result_id).chunk(start_sequence=start, frame_count=count)
except RecordedThreatTimelineError:
raise HTTPException(
status_code=404,
detail="M4.6 timeline chunk не найден",
) from None
@router.get("/results/{result_id}/timeline/frames/{sequence}/camera")
def get_timeline_camera(result_id: str, sequence: int) -> Response:
if camera_frame_provider is None:
raise HTTPException(status_code=503, detail="M4.6 camera decoder недоступен")
projected = timeline(result_id)
if not 0 <= sequence < len(projected.index.source_times_ns):
raise HTTPException(status_code=404, detail="M4.6 timeline frame не найден")
return _camera_response(camera_frame_provider, projected.profile.session_id, sequence)
return router
@@ -183,53 +202,16 @@ def _read_threat_result_cached(
@lru_cache(maxsize=4)
def _cached_video_overlay(
result_id: str,
def _read_threat_timeline_cached(
repository_root_value: str,
root_value: str,
frames_sha256: str,
source_session_id: str,
) -> dict[str, object]:
root = Path(root_value).resolve(strict=True)
if root.is_symlink() or not root.is_dir() or len(frames_sha256) != 64:
raise ValueError("M4.6 video evidence identity changed")
frames = []
for expected_sequence, row in enumerate(_iter_jsonl(root / "frames.jsonl")):
if (
row.get("schema_version")
not in {THREAT_REPLAY_FRAME_SCHEMA, THREAT_REPLAY_FRAME_SCHEMA_V2}
or row.get("sequence") != expected_sequence
):
raise ValueError("M4.6 video frame order changed")
frames.append(
{
"frame_index": expected_sequence,
"session_seconds": _nonnegative_int(row.get("source_time_ns"), "source time")
/ 1_000_000_000,
"source_available": row["source_available"],
"camera_proposals": copy.deepcopy(row["camera_proposals"]),
"decision_counts": _decision_counts(_array(row.get("assessments"))),
}
)
if len(frames) != 4489:
raise ValueError("M4.6 video frame coverage changed")
return {
"schema_version": M4_THREAT_VIDEO_SCHEMA,
"result_id": result_id,
"recorded_source": {
"session_id": source_session_id,
"source_id": "sensor.camera.right",
"synchronization": "host-arrival-best-effort",
},
"image_width": 800,
"image_height": 600,
"timeline_start_seconds": frames[0]["session_seconds"],
"timeline_end_seconds": frames[-1]["session_seconds"],
"frame_count": len(frames),
"frames": frames,
"ground_truth": False,
"authority": "replay-simulated",
"access": "read-only-replay-simulated-video",
}
signature: tuple[int, ...],
) -> RecordedThreatTimeline:
result = _read_threat_result_cached(root_value, signature)
return RecordedThreatTimeline(
repository_root=Path(repository_root_value),
result=result,
)
def _project_result(result: ThreatReplayResult) -> dict[str, object]:
@@ -261,14 +243,32 @@ def _project_result(result: ThreatReplayResult) -> dict[str, object]:
}
def _decision_counts(raw: list[object]) -> dict[str, int]:
result = {"threat": 0, "not-threat": 0, "unknown": 0}
for item in raw:
assessment = item if isinstance(item, dict) else {}
decision = assessment.get("decision")
if isinstance(decision, str) and decision in result:
result[decision] += 1
return result
def _camera_response(
provider: CameraFrameProvider,
session_id: str,
sequence: int,
) -> Response:
try:
camera = provider(session_id, sequence)
except (OSError, SessionIntegrityError, ValueError):
raise HTTPException(
status_code=503,
detail="M4.6 exact camera frame недоступен",
) from None
if camera.width != 800 or camera.height != 600:
raise HTTPException(
status_code=503,
detail="M4.6 camera frame нарушил размерный контракт",
)
return Response(
content=camera.payload,
media_type=camera.media_type,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{camera.sha256}"',
"X-Content-Type-Options": "nosniff",
},
)
def _configured_root(provider: RootProvider) -> Path | None:
@@ -343,15 +343,8 @@ def _array(value: object) -> list[object]:
return value
def _nonnegative_int(value: object, label: str) -> int:
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
raise ValueError(f"M4.6 {label} is invalid")
return value
__all__ = [
"M4_THREAT_CATALOG_SCHEMA",
"M4_THREAT_VIDEO_SCHEMA",
"M4_THREAT_VIEW_SCHEMA",
"M4_THREAT_VISUAL_CATALOG_SCHEMA",
"build_m4_threat_replay_router",