feat(perception): project recorded results into Rerun

This commit is contained in:
DCCONSTRUCTIONS
2026-07-19 17:39:13 +03:00
parent 31fc4f6567
commit 2b53168149
13 changed files with 1328 additions and 8 deletions
+73
View File
@@ -13,6 +13,7 @@ from pydantic import BaseModel, ConfigDict, Field, StrictBool, field_validator,
from starlette.concurrency import run_in_threadpool
from starlette.types import Receive, Scope, Send
from k1link.compute import RecordedPerceptionOverlayError
from k1link.sessions import (
RECORDED_MEDIA_MANIFEST_SCHEMA,
LayoutConflictError,
@@ -102,6 +103,16 @@ class RecordedBlueprintRequest(StrictApiModel):
point_size: float = Field(default=2.5, strict=True, ge=0.1, le=32.0)
palette: Literal["turbo", "viridis", "plasma", "grayscale", "custom"] = "turbo"
custom_color: str = Field(default="#f7f8f4", pattern=r"^#[0-9A-Fa-f]{6}$")
active_view: Literal["spatial", "perception", "metrics"] = "spatial"
class RecordedPerceptionRequest(StrictApiModel):
application_id: Literal["nodedc_mission_core_recorded"]
recording_id: str = Field(
min_length=1,
max_length=128,
pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
)
class SceneSettingsDocument(StrictApiModel):
@@ -233,6 +244,16 @@ class CatalogRefresher(Protocol):
def __call__(self) -> object: ...
class RecordedPerceptionOverlayProvider(Protocol):
def render(
self,
session_id: str,
*,
application_id: str,
recording_id: str,
) -> bytes | None: ...
def build_session_router(
store: SessionStore,
*,
@@ -241,6 +262,7 @@ def build_session_router(
recording_materializer: RecordingMaterializer | None = None,
recording_preparation_manager: SessionRecordingPreparationManager | None = None,
media_inspector: RecordedMediaInspector | None = None,
perception_overlay_provider: RecordedPerceptionOverlayProvider | None = None,
allow_synchronous_recording_fallback: bool = False,
replay_action_id: str = DEFAULT_REPLAY_ACTION_ID,
) -> APIRouter:
@@ -755,6 +777,7 @@ def build_session_router(
),
application_id=RECORDED_APPLICATION_ID,
recording_id=request.recording_id,
active_view=request.active_view,
)
except SessionNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@@ -780,6 +803,56 @@ def build_session_router(
},
)
@router.post("/api/v1/observation-sessions/{session_id}/perception.rrd")
async def get_observation_session_perception(
session_id: str,
request: RecordedPerceptionRequest,
) -> Response:
if perception_overlay_provider is None:
return Response(status_code=204, headers={"Cache-Control": "no-store"})
try:
await run_in_threadpool(
_prepare_replay,
store,
catalog_refresher,
session_id,
1.0,
False,
False,
)
payload = await run_in_threadpool(
perception_overlay_provider.render,
session_id,
application_id=request.application_id,
recording_id=request.recording_id,
)
except SessionNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except (SessionNotReplayableError, SessionIntegrityError) as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
except RecordedPerceptionOverlayError as exc:
raise HTTPException(
status_code=500,
detail="Не удалось подготовить слой распознавания.",
) from exc
except ValueError as exc:
raise HTTPException(
status_code=422,
detail="Некорректный идентификатор сессии.",
) from exc
if payload is None:
return Response(status_code=204, headers={"Cache-Control": "no-store"})
return Response(
content=payload,
media_type="application/vnd.rerun.rrd",
headers={
"Cache-Control": "private, no-cache, no-transform",
"Content-Length": str(len(payload)),
"X-Content-Type-Options": "nosniff",
"Content-Disposition": 'inline; filename="perception.rrd"',
},
)
@router.get("/api/v1/observation-sessions/{session_id}/media/{artifact_id}/manifest")
def get_recorded_media_manifest(
session_id: str,