fix(m4): retain compact obstacle evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 22:16:33 +03:00
parent 13ed096f80
commit aacc6dc43b
24 changed files with 981 additions and 410 deletions
+20 -18
View File
@@ -31,6 +31,7 @@ from k1link.laboratory import (
)
from k1link.sessions import (
MaterializedRecording,
RecordedCameraFrameService,
RecordedMediaInspector,
RecordedMediaManifest,
RecordingPreparationQueueFull,
@@ -191,6 +192,16 @@ session_recorded_media_inspector = RecordedMediaInspector(
)
_ffmpeg = _resolve_media_tool("ffmpeg")
_ffprobe = _resolve_media_tool("ffprobe")
session_recorded_camera_frame_service = (
RecordedCameraFrameService(
session_store,
session_recorded_media_inspector,
ffmpeg_path=_ffmpeg,
cache_root=session_store.data_dir / "camera-frame-cache",
)
if _ffmpeg is not None
else None
)
session_legacy_perception_overlay_store = (
RecordedPerceptionOverlayStore(
jobs_root=REPOSITORY_ROOT / ".runtime" / "compute-jobs",
@@ -586,9 +597,7 @@ app.include_router(
app.include_router(
build_advanced_laboratory_router(
evidence_registry=LABORATORY_EVIDENCE_REGISTRY,
evidence_runtime_root_provider=lambda: (
REPOSITORY_ROOT / ".runtime" / "compute-experiments"
),
evidence_runtime_root_provider=lambda: REPOSITORY_ROOT / ".runtime" / "compute-experiments",
e31_root_provider=lambda: (
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e31" / "source-qualifications"
),
@@ -756,22 +765,19 @@ app.include_router(
app.include_router(
build_m4_threat_replay_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "m4"
/ "replay-threat"
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "m4" / "replay-threat"
),
camera_frame_provider=(
session_recorded_camera_frame_service.extract
if session_recorded_camera_frame_service is not None
else None
),
)
)
app.include_router(
build_e46e_ready_stack_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e46e"
/ "ready-stack-results"
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46e" / "ready-stack-results"
),
)
)
@@ -785,11 +791,7 @@ app.include_router(
/ "dashcam-bakeoff-results"
),
e46e_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e46e"
/ "ready-stack-results"
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46e" / "ready-stack-results"
),
)
)
+50 -13
View File
@@ -10,7 +10,7 @@ from functools import lru_cache
from pathlib import Path
from typing import Final
from fastapi import APIRouter, HTTPException, Query
from fastapi import APIRouter, HTTPException, Query, Response
from k1link.perception.threat_replay import (
THREAT_REPLAY_FRAME_SCHEMA,
@@ -22,20 +22,21 @@ from k1link.perception.threat_replay import (
ThreatReplayResult,
read_threat_replay_result,
)
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"
)
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]
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
def build_m4_threat_replay_router(
*,
root_provider: RootProvider = lambda: None,
camera_frame_provider: CameraFrameProvider | None = None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory/m4-threat", tags=["laboratory"])
@@ -109,10 +110,52 @@ def build_m4_threat_replay_router(
**copy.deepcopy(frames[ordinal - 1]),
"result_id": result_id,
"ordinal": ordinal,
"camera_url": (
f"/api/v1/laboratory/m4-threat/results/{result_id}/visuals/{ordinal}/camera"
),
"ground_truth": False,
"access": "read-only-replay-simulated",
}
@router.get("/results/{result_id}/visuals/{ordinal}/camera")
def get_visual_camera(result_id: str, ordinal: int) -> Response:
if camera_frame_provider is None:
raise HTTPException(status_code=503, detail="M4.6 camera decoder недоступен")
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 не найден")
identity = frozen.manifest.get("identity")
if not isinstance(identity, dict):
raise HTTPException(status_code=404, detail="M4.6 source identity не найдена")
session_id = identity.get("source_session_id")
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",
},
)
@router.get("/results/{result_id}/video-overlay")
def get_video_overlay(result_id: str) -> dict[str, object]:
frozen = result(result_id)
@@ -160,9 +203,7 @@ def _cached_video_overlay(
frames.append(
{
"frame_index": expected_sequence,
"session_seconds": _nonnegative_int(
row.get("source_time_ns"), "source time"
)
"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"]),
@@ -209,9 +250,7 @@ def _project_result(result: ThreatReplayResult) -> dict[str, object]:
},
"metrics": copy.deepcopy(result.metrics),
"configuration": copy.deepcopy(result.report["configuration"]),
"acceptance_requirements": copy.deepcopy(
result.report["acceptance_requirements"]
),
"acceptance_requirements": copy.deepcopy(result.report["acceptance_requirements"]),
"limitations": copy.deepcopy(result.report["limitations"]),
"accepted": result.accepted,
"ground_truth": False,
@@ -271,9 +310,7 @@ def _candidates(provider: RootProvider) -> list[Path]:
(
item
for item in root.iterdir()
if item.is_dir()
and not item.is_symlink()
and _RESULT_ID.fullmatch(item.name)
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,