feat(lab): stabilize autonomous TGS playback

This commit is contained in:
DCCONSTRUCTIONS
2026-08-27 20:34:20 +03:00
parent c8593207d3
commit 95a1ef5057
26 changed files with 3326 additions and 253 deletions
+14
View File
@@ -133,6 +133,9 @@ from k1link.web.m48s_fixed_class_detector_lab_api import (
build_m48s_fixed_class_detector_lab_router,
)
from k1link.web.m48t_risk_quality_lab_api import build_m48t_risk_quality_lab_router
from k1link.web.m49_physical_safety_playback_api import (
build_m49_physical_safety_playback_router,
)
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
from k1link.web.m49_tgs_full_shadow_api import build_m49_tgs_full_shadow_router
from k1link.web.map_api import (
@@ -1016,6 +1019,17 @@ app.include_router(
),
)
)
app.include_router(
build_m49_physical_safety_playback_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "m49"
/ "physical-safety-playback-results"
),
)
)
app.include_router(
build_m48s_fixed_class_detector_lab_router(
root_provider=lambda: (
@@ -0,0 +1,221 @@
"""Read-only local API for autonomous M49 physical-safety playback artifacts."""
from __future__ import annotations
import copy
import re
from collections.abc import Callable
from functools import lru_cache
from pathlib import Path
from typing import Any, Final
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import FileResponse
from k1link.laboratory.m49_physical_safety_playback import (
PREFIX,
M49PhysicalSafetyPlayback,
M49PhysicalSafetyPlaybackError,
read_m49_physical_safety_playback,
verify_m49_physical_safety_artifact,
)
RootProvider = Callable[[], Path | None]
RESULT_ID: Final = re.compile(rf"^{re.escape(PREFIX)}[a-f0-9]{{64}}$")
SOURCE_RESULT_ID: Final = re.compile(r"^m49-tgs-full-shadow-[a-f0-9]{64}$")
ENDPOINT_ROOT: Final = "/api/v1/laboratory/m49/physical-safety-playback"
def build_m49_physical_safety_playback_router(
*, root_provider: RootProvider = lambda: None
) -> APIRouter:
router = APIRouter(prefix=ENDPOINT_ROOT, tags=["laboratory"])
def sealed(result_id: str) -> M49PhysicalSafetyPlayback:
root = _configured_root(root_provider)
if root is None or RESULT_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="M49 physical-safety playback not found")
candidate = root / result_id
if candidate.is_symlink() or not candidate.is_dir():
raise HTTPException(status_code=404, detail="M49 physical-safety playback not found")
try:
resolved = candidate.resolve(strict=True)
if resolved.parent != root:
raise ValueError("result escaped configured root")
return _read_cached(str(resolved), _signature(resolved))
except (M49PhysicalSafetyPlaybackError, OSError, ValueError):
raise HTTPException(
status_code=404, detail="M49 physical-safety playback not found"
) from None
@router.get("/results")
def list_results(
limit: int = Query(default=10, ge=1, le=25),
source_result_id: str | None = Query(default=None),
) -> dict[str, object]:
if source_result_id is not None and SOURCE_RESULT_ID.fullmatch(source_result_id) is None:
raise HTTPException(status_code=422, detail="M49 source result identity is invalid")
root = _configured_root(root_provider)
if root is None:
return _catalog([], configured=False, invalid_total=0)
items: list[dict[str, object]] = []
invalid = 0
for candidate in sorted(root.iterdir()):
if candidate.is_symlink() or not candidate.is_dir() or not RESULT_ID.fullmatch(
candidate.name
):
continue
try:
summary = _summary(
_read_cached(str(candidate.resolve()), _signature(candidate))
)
if source_result_id is None or summary["source_result_id"] == source_result_id:
items.append(summary)
except (M49PhysicalSafetyPlaybackError, OSError, ValueError):
invalid += 1
items.sort(
key=lambda value: (str(value["created_at_utc"]), str(value["result_id"])),
reverse=True,
)
return _catalog(items[:limit], configured=True, invalid_total=invalid)
@router.get("/{result_id}/manifest")
def get_manifest(result_id: str) -> dict[str, object]:
return _project_manifest(sealed(result_id))
@router.get("/{result_id}/tracks/{track_id}")
def get_track(result_id: str, track_id: str) -> FileResponse:
result = sealed(result_id)
playback = _playback(result)
if track_id == "centers":
descriptor = _descriptor(playback.get("centers"), "centers")
elif track_id == "frames":
descriptor = _descriptor(playback.get("frames"), "frames")
else:
raise HTTPException(status_code=404, detail="M49 physical-safety track not found")
return _file_response(result, descriptor)
@router.get("/{result_id}/chunks/{chunk_index}")
def get_chunk(result_id: str, chunk_index: int) -> FileResponse:
result = sealed(result_id)
chunks = _playback(result).get("chunks")
if not isinstance(chunks, list) or chunk_index < 0 or chunk_index >= len(chunks):
raise HTTPException(status_code=404, detail="M49 physical-safety chunk not found")
descriptor = _descriptor(chunks[chunk_index], f"chunk {chunk_index}")
if descriptor.get("index") != chunk_index:
raise HTTPException(status_code=503, detail="M49 physical-safety chunk catalog changed")
return _file_response(result, descriptor)
return router
@lru_cache(maxsize=8)
def _read_cached(root: str, signature: tuple[int, ...]) -> M49PhysicalSafetyPlayback:
del signature
return read_m49_physical_safety_playback(Path(root))
def _file_response(
result: M49PhysicalSafetyPlayback,
descriptor: dict[str, Any],
) -> FileResponse:
try:
path = verify_m49_physical_safety_artifact(result, descriptor)
except (KeyError, OSError, TypeError, ValueError, M49PhysicalSafetyPlaybackError):
raise HTTPException(
status_code=503,
detail="M49 physical-safety playback artifact failed verification",
) from None
return FileResponse(
path,
media_type=str(descriptor.get("media_type") or "application/octet-stream"),
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"Content-Encoding": "identity",
"ETag": f'"{descriptor["sha256"]}"',
"X-Content-Type-Options": "nosniff",
"X-Mission-Core-Worker-Dependency": "none",
},
)
def _playback(result: M49PhysicalSafetyPlayback) -> dict[str, Any]:
playback = result.manifest.get("playback")
if not isinstance(playback, dict):
raise HTTPException(status_code=503, detail="M49 physical-safety playback changed")
return playback
def _descriptor(value: object, label: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise HTTPException(status_code=503, detail=f"M49 physical-safety {label} changed")
return value
def _project_manifest(result: M49PhysicalSafetyPlayback) -> dict[str, object]:
payload = copy.deepcopy(result.manifest)
playback = payload["playback"]
assert isinstance(playback, dict)
centers = playback["centers"]
frames = playback["frames"]
chunks = playback["chunks"]
assert isinstance(centers, dict) and isinstance(frames, dict) and isinstance(chunks, list)
centers["url"] = f"{ENDPOINT_ROOT}/{result.result_id}/tracks/centers"
frames["url"] = f"{ENDPOINT_ROOT}/{result.result_id}/tracks/frames"
for index, value in enumerate(chunks):
assert isinstance(value, dict)
value["url"] = f"{ENDPOINT_ROOT}/{result.result_id}/chunks/{index}"
payload["access"] = "read-only-sealed-local"
return payload
def _summary(result: M49PhysicalSafetyPlayback) -> dict[str, object]:
playback = _playback(result)
return {
"schema_version": "missioncore.m49-physical-safety-playback-summary/v1",
"result_id": result.result_id,
"created_at_utc": result.manifest.get("created_at_utc"),
"source_result_id": result.manifest["identity"]["source_result_id"],
"frame_count": playback.get("frame_count"),
"cell_count": playback.get("cell_count"),
"cell_size_m": playback.get("cell_size_m"),
"radius_m": playback.get("radius_m"),
"chunk_count": len(playback.get("chunks", [])),
"worker_runtime_dependency": False,
"navigation_or_safety_accepted": False,
}
def _catalog(
items: list[dict[str, object]], *, configured: bool, invalid_total: int
) -> dict[str, object]:
return {
"schema_version": "missioncore.m49-physical-safety-playback-catalog/v1",
"configured": configured,
"items": items,
"candidate_total": len(items) + invalid_total,
"invalid_total": invalid_total,
"worker_runtime_dependency": False,
"access": "read-only-sealed-local",
}
def _configured_root(provider: RootProvider) -> Path | None:
value = provider()
if value is None:
return None
raw = value.expanduser().absolute()
if raw.is_symlink() or not raw.is_dir():
return None
return raw.resolve(strict=True)
def _signature(root: Path) -> tuple[int, ...]:
path = root / "manifest.json"
if path.is_symlink() or not path.is_file():
raise ValueError("physical-safety manifest unavailable")
stat = path.stat()
return (stat.st_size, stat.st_mtime_ns)
__all__ = ["build_m49_physical_safety_playback_router"]
+104 -3
View File
@@ -22,7 +22,7 @@ from k1link.perception.threat_replay import (
THREAT_REPLAY_VISUAL_SCHEMA_V2,
ThreatReplayError,
ThreatReplayResult,
read_threat_replay_result,
read_threat_replay_result_metadata,
)
from k1link.perception.threat_timeline import (
RECORDED_SPATIAL_MAX_CHUNK_FRAMES,
@@ -34,6 +34,7 @@ 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_VISUAL_CATALOG_SCHEMA: Final = "missioncore.m4-threat-visual-catalog/v1"
M4_THREAT_PLAYBACK_CHUNK_FRAMES: Final = 24
_RESULT_ID = re.compile(rf"^{THREAT_REPLAY_RESULT_PREFIX}[a-f0-9]{{64}}$")
RootProvider = Callable[[], Path | None]
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
@@ -212,13 +213,19 @@ def build_m4_threat_replay_router(
projected = timeline(result_id)
points = projected.store.playback_points_map()
offsets = projected.store.playback_point_offsets()
content_sha256 = hashlib.sha256(memoryview(points).cast("B")).hexdigest()
points_view = memoryview(points).cast("B")
content_sha256 = hashlib.sha256(points_view).hexdigest()
chunks = _playback_chunk_catalog(result_id, points_view, offsets)
return {
"schema_version": "missioncore.recorded-spatial-playback/v1",
"result_id": result_id,
"frame_count": len(offsets) - 1,
"point_count": int(points.shape[0]),
"point_offsets": list(offsets),
"chunk_frame_count": M4_THREAT_PLAYBACK_CHUNK_FRAMES,
"resident_chunk_count_max": 4,
"forward_prefetch_chunk_count": 1,
"chunks": chunks,
"track": {
"id": "points-map-f32",
"url": (
@@ -238,6 +245,42 @@ def build_m4_threat_replay_router(
"access": "read-only-sealed-binary-playback",
}
@router.get(
"/results/{result_id}/timeline/playback/chunks/{chunk_index}",
response_class=Response,
)
def get_timeline_playback_chunk(result_id: str, chunk_index: int) -> Response:
projected = timeline(result_id)
points = projected.store.playback_points_map()
offsets = projected.store.playback_point_offsets()
points_view = memoryview(points).cast("B")
descriptor = _playback_chunk_descriptor(
result_id,
points_view,
offsets,
chunk_index,
)
if descriptor is None:
raise HTTPException(status_code=404, detail="M4.6 playback chunk не найден")
point_start = descriptor["point_start"]
byte_length = descriptor["bytes"]
assert isinstance(point_start, int)
assert isinstance(byte_length, int)
byte_start = point_start * 3 * 4
byte_stop = byte_start + byte_length
return Response(
content=bytes(points_view[byte_start:byte_stop]),
media_type="application/octet-stream",
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"Content-Encoding": "identity",
"Content-Length": str(byte_length),
"ETag": f'"{descriptor["sha256"]}"',
"X-Content-Type-Options": "nosniff",
"X-Uncompressed-Content-Length": str(byte_length),
},
)
@router.get(
"/results/{result_id}/timeline/playback/tracks/points-map-f32",
response_class=StreamingResponse,
@@ -276,13 +319,71 @@ def _binary_chunks(view: memoryview, chunk_size: int = 1024 * 1024) -> Iterator[
yield bytes(view[start : start + chunk_size])
def _playback_chunk_catalog(
result_id: str,
points_view: memoryview,
offsets: tuple[int, ...],
) -> list[dict[str, object]]:
frame_count = len(offsets) - 1
chunk_count = (
frame_count + M4_THREAT_PLAYBACK_CHUNK_FRAMES - 1
) // M4_THREAT_PLAYBACK_CHUNK_FRAMES
return [
descriptor
for chunk_index in range(chunk_count)
if (
descriptor := _playback_chunk_descriptor(
result_id,
points_view,
offsets,
chunk_index,
)
)
is not None
]
def _playback_chunk_descriptor(
result_id: str,
points_view: memoryview,
offsets: tuple[int, ...],
chunk_index: int,
) -> dict[str, object] | None:
frame_count = len(offsets) - 1
start = chunk_index * M4_THREAT_PLAYBACK_CHUNK_FRAMES
if chunk_index < 0 or start >= frame_count:
return None
count = min(M4_THREAT_PLAYBACK_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"/api/v1/laboratory/m4-threat/results/{result_id}"
f"/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(),
}
@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))
return read_threat_replay_result_metadata(Path(root_value))
@lru_cache(maxsize=4)