feat(perception): add dual evidence replay threat

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 19:18:01 +03:00
parent 6b810952a4
commit b0d0bc8d7f
22 changed files with 3122 additions and 14 deletions
+12
View File
@@ -112,6 +112,7 @@ from k1link.web.laboratory_api import build_laboratory_router
from k1link.web.laboratory_report_api import build_laboratory_report_router
from k1link.web.lidar_api import build_lidar_router
from k1link.web.lidar_local_surface_service import K1LocalSurfaceReadService
from k1link.web.m4_threat_replay_api import build_m4_threat_replay_router
from k1link.web.map_api import (
MapGatewayConfiguration,
MapGatewayProxy,
@@ -752,6 +753,17 @@ app.include_router(
),
)
)
app.include_router(
build_m4_threat_replay_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "m4"
/ "replay-threat"
),
)
)
app.include_router(
build_e46e_ready_stack_router(
root_provider=lambda: (
+315
View File
@@ -0,0 +1,315 @@
"""Read-only LAB projection of the canonical M4.6 replay threat result."""
from __future__ import annotations
import copy
import json
import re
from collections.abc import Callable, Iterator
from functools import lru_cache
from pathlib import Path
from typing import Final
from fastapi import APIRouter, HTTPException, Query
from k1link.perception.threat_replay import (
THREAT_REPLAY_FRAME_SCHEMA,
THREAT_REPLAY_RESULT_PREFIX,
THREAT_REPLAY_VISUAL_SCHEMA,
ThreatReplayError,
ThreatReplayResult,
read_threat_replay_result,
)
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]
def build_m4_threat_replay_router(
*,
root_provider: RootProvider = lambda: None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory/m4-threat", tags=["laboratory"])
def result(result_id: str) -> ThreatReplayResult:
if _RESULT_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="M4.6 result не найден")
root = _configured_root(root_provider)
if root is None:
raise HTTPException(status_code=404, detail="M4.6 result не найден")
path = (root / result_id).resolve()
if path.parent != root or path.is_symlink():
raise HTTPException(status_code=404, detail="M4.6 result не найден")
try:
return _read_threat_result_cached(str(path), _result_signature(path))
except (ThreatReplayError, OSError, ValueError):
raise HTTPException(status_code=404, detail="M4.6 result не найден") from None
@router.get("/results")
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
candidates = _candidates(root_provider)
items: list[dict[str, object]] = []
invalid_total = 0
for candidate in candidates:
try:
frozen = result(candidate.name)
if len(items) < limit:
items.append(_project_result(frozen))
except HTTPException:
invalid_total += 1
return {
"schema_version": M4_THREAT_CATALOG_SCHEMA,
"configured": _configured_root(root_provider) is not None,
"items": items,
"candidate_total": len(candidates),
"invalid_total": invalid_total,
"access": "read-only-replay-simulated",
}
@router.get("/results/{result_id}/visuals")
def list_visuals(result_id: str) -> dict[str, object]:
frozen = result(result_id)
frames = _read_jsonl(frozen.result_root / "visual-frames.jsonl")
return {
"schema_version": M4_THREAT_VISUAL_CATALOG_SCHEMA,
"result_id": result_id,
"items": [
{
"ordinal": index + 1,
"sequence": item["sequence"],
"frame_id": item["frame_id"],
"source_time_ns": item["source_time_ns"],
"metric_obstacle_count": len(_array(item.get("metric_obstacles"))),
"camera_proposal_count": len(_array(item.get("camera_proposals"))),
"point_cloud_sample_count": item["point_cloud_sample_count"],
}
for index, item in enumerate(frames)
],
"access": "read-only-replay-simulated",
}
@router.get("/results/{result_id}/visuals/{ordinal}")
def get_visual(result_id: str, ordinal: int) -> dict[str, object]:
frozen = result(result_id)
if not 1 <= ordinal <= 32:
raise HTTPException(status_code=404, detail="M4.6 visual frame не найден")
frames = _read_jsonl(frozen.result_root / "visual-frames.jsonl")
if len(frames) != 32:
raise HTTPException(status_code=404, detail="M4.6 visual frame не найден")
return {
**copy.deepcopy(frames[ordinal - 1]),
"result_id": result_id,
"ordinal": ordinal,
"ground_truth": False,
"access": "read-only-replay-simulated",
}
@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"]),
)
)
return router
@lru_cache(maxsize=4)
def _read_threat_result_cached(
root_value: str,
signature: tuple[int, ...],
) -> ThreatReplayResult:
del signature
return read_threat_replay_result(Path(root_value))
@lru_cache(maxsize=4)
def _cached_video_overlay(
result_id: 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") != THREAT_REPLAY_FRAME_SCHEMA
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",
}
def _project_result(result: ThreatReplayResult) -> dict[str, object]:
identity = result.manifest["identity"]
assert isinstance(identity, dict)
return {
"schema_version": M4_THREAT_VIEW_SCHEMA,
"result_id": result.result_id,
"created_at_utc": result.manifest["created_at_utc"],
"status": result.report["status"],
"profile_id": identity["profile_id"],
"rig_profile_id": identity["rig_profile_id"],
"corridor_profile_id": identity["corridor_profile_id"],
"source_result_ids": {
"detector": identity["detector_result_id"],
"geometry": identity["geometry_result_id"],
"temporal": identity["temporal_result_id"],
},
"metrics": copy.deepcopy(result.metrics),
"configuration": copy.deepcopy(result.report["configuration"]),
"acceptance_requirements": copy.deepcopy(
result.report["acceptance_requirements"]
),
"limitations": copy.deepcopy(result.report["limitations"]),
"accepted": result.accepted,
"ground_truth": False,
"authority": "replay-simulated",
"physical_collision_accepted": False,
"actuation_allowed": False,
"access": "read-only-replay-simulated",
}
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 _configured_root(provider: RootProvider) -> Path | None:
value = provider()
if value is None:
return None
candidate = value.expanduser().absolute()
if candidate.is_symlink():
return None
try:
root = candidate.resolve(strict=True)
except OSError:
return None
return root if root.is_dir() else None
def _result_signature(root: Path) -> tuple[int, ...]:
signature: list[int] = []
for name in (
"manifest.json",
"report.json",
"fixtures.json",
"frames.jsonl",
"visual-frames.jsonl",
):
path = root / name
if not path.is_file() or path.is_symlink():
raise ValueError("M4.6 result artifact is invalid")
stat = path.stat()
signature.extend((stat.st_size, stat.st_mtime_ns))
return tuple(signature)
def _candidates(provider: RootProvider) -> list[Path]:
root = _configured_root(provider)
if root is None:
return []
return sorted(
(
item
for item in root.iterdir()
if item.is_dir()
and not item.is_symlink()
and _RESULT_ID.fullmatch(item.name)
),
key=lambda item: item.stat().st_mtime_ns,
reverse=True,
)
def _read_jsonl(path: Path) -> list[dict[str, object]]:
return list(_iter_jsonl(path))
def _iter_jsonl(path: Path) -> Iterator[dict[str, object]]:
with path.open("r", encoding="utf-8") as handle:
for line in handle:
value = json.loads(line)
if not isinstance(value, dict):
raise ValueError("M4.6 JSONL row is invalid")
if value.get("schema_version") not in {
THREAT_REPLAY_FRAME_SCHEMA,
THREAT_REPLAY_VISUAL_SCHEMA,
}:
raise ValueError("M4.6 JSONL schema is invalid")
yield value
def _array(value: object) -> list[object]:
if not isinstance(value, list):
raise ValueError("M4.6 array is invalid")
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",
]