feat(lab): stabilize autonomous TGS playback
This commit is contained in:
@@ -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"]
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user