fix(replay): serve admitted AI overlays without revalidation

This commit is contained in:
DCCONSTRUCTIONS
2026-07-29 23:13:42 +03:00
parent 8e4891d608
commit cc6838ed24
10 changed files with 990 additions and 173 deletions
+13
View File
@@ -201,6 +201,19 @@ class RecordedPerceptionOverlayMux:
recording_id=recording_id,
)
def status(self, session_id: str, *, recording_id: str) -> dict[str, Any]:
primary_status = getattr(self.primary, "status", None)
if callable(primary_status):
value = primary_status(session_id, recording_id=recording_id)
if isinstance(value, dict):
return value
return {
"state": "idle",
"phase": "idle",
"elapsed_seconds": 0.0,
"byte_length": None,
}
def validate_recorded_calibrated_fusion(
job_root: Path,
+431 -137
View File
@@ -11,7 +11,9 @@ import stat
import subprocess
import tempfile
import threading
from contextlib import suppress
import time
from collections.abc import Iterator
from contextlib import contextmanager, suppress
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Protocol, TypeGuard
@@ -39,10 +41,13 @@ MAX_JSON_BYTES = 64 * 1024 * 1024
MAX_LINE_BYTES = 4 * 1024 * 1024
MAX_SOURCE_BYTES = 512 * 1024 * 1024
OVERLAY_RENDERER_VERSION = "4"
OVERLAY_ADMISSION_SCHEMA = "missioncore.e10-overlay-admission/v1"
OVERLAY_CACHE_SCHEMA = "missioncore.e10-overlay-cache/v1"
CUBOID_PRESENTATION_HOLD_NS = 500_000_000
_SAFE_RESULT_ID = re.compile(r"^e10-integrated-perception-[a-f0-9]{64}$")
_SAFE_PACK_ID = re.compile(r"^e10-lidar-pack-[a-f0-9]{64}$")
_SAFE_JOB_ID = re.compile(r"^recorded-camera-[a-f0-9]{24}$")
_SAFE_RECORDING_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
@@ -67,6 +72,30 @@ class IntegratedPerceptionResult:
report_path: Path
@dataclass(frozen=True, slots=True)
class _ResultDescriptor:
result_id: str
result_root: Path
result_json_sha256: str
session_id: str
job_id: str
created_at_utc: str
@dataclass(frozen=True, slots=True)
class _OverlayAdmission:
session_id: str
result_id: str
result_json_sha256: str
result_created_at_utc: str
@dataclass(slots=True)
class _FlightLock:
lock: threading.Lock
users: int = 0
@dataclass(frozen=True, slots=True)
class _PresentedCuboid:
observed_ns: int
@@ -329,7 +358,13 @@ def validate_integrated_perception_result(
class IntegratedPerceptionOverlayStore:
"""Prefer the newest accepted E10 result for a recorded session."""
"""Serve the newest admitted E10 overlay without revalidating it on replay.
Result validation is an admission concern. Once a complete overlay has been
rendered and sealed, its cache and admission sidecars are sufficient for
the presentation path. A cache miss still fails closed and validates the
exact selected result before rendering.
"""
def __init__(
self,
@@ -345,9 +380,15 @@ class IntegratedPerceptionOverlayStore:
self.lidar_packs_root = lidar_packs_root.expanduser().absolute()
self.cache_root = cache_root.expanduser().absolute()
self.ffmpeg_path = ffmpeg_path.expanduser().absolute()
self._lock = threading.Lock()
self._catalog_signature: tuple[Any, ...] | None = None
self._catalog_lock = threading.Lock()
self._descriptor_catalog_generation: tuple[int, int] | None = None
self._descriptors_by_session: dict[str, tuple[_ResultDescriptor, ...]] = {}
self._latest_by_session: dict[str, IntegratedPerceptionResult | None] = {}
self._flight_guard = threading.Lock()
self._flights: dict[tuple[str, str], _FlightLock] = {}
self._materialization_lock = threading.Lock()
self._status_lock = threading.Lock()
self._status_by_recording: dict[tuple[str, str], dict[str, Any]] = {}
def render(
self,
@@ -363,146 +404,357 @@ class IntegratedPerceptionOverlayStore:
or _SAFE_RECORDING_ID.fullmatch(recording_id) is None
):
raise ValueError("integrated perception recording id is invalid")
with self._lock:
result = self._latest(session_id)
if result is None:
return None
cache = _private_child(
_private_child(_private_directory(self.cache_root), session_id),
result.result_id,
)
output = cache / f"{recording_id}.rrd"
sidecar = output.with_suffix(".rrd.cache.json")
cached = _read_cache(output, sidecar, result, recording_id)
if cached is not None:
return cached
payload = _render(
result,
application_id=application_id,
recording_id=recording_id,
ffmpeg_path=self.ffmpeg_path,
temporary_root=cache,
)
temporary = output.with_name(f".{output.name}.{os.getpid()}.tmp")
try:
with temporary.open("xb") as stream:
stream.write(payload)
stream.flush()
os.fsync(stream.fileno())
os.chmod(temporary, 0o600)
os.replace(temporary, output)
write_json_atomic(
sidecar,
{
"schema_version": "missioncore.e10-overlay-cache/v1",
"renderer_version": OVERLAY_RENDERER_VERSION,
"result_id": result.result_id,
"recording_id": recording_id,
"byte_length": len(payload),
"sha256": hashlib.sha256(payload).hexdigest(),
},
)
finally:
temporary.unlink(missing_ok=True)
return payload
def _latest(self, session_id: str) -> IntegratedPerceptionResult | None:
signature = _directory_catalog_signature(
self.jobs_root,
self.results_root,
self.lidar_packs_root,
)
if signature != self._catalog_signature:
self._catalog_signature = signature
self._latest_by_session.clear()
if session_id in self._latest_by_session:
return self._latest_by_session[session_id]
key = (session_id, recording_id)
self._set_status(key, state="preparing", phase="cache-lookup")
try:
jobs = sorted(self.jobs_root.iterdir())
candidates = sorted(self.results_root.iterdir())
except FileNotFoundError:
self._latest_by_session[session_id] = None
with self._single_flight(key):
cached = self._read_admitted_cache(session_id, recording_id)
if cached is not None:
self._set_status(
key,
state="ready",
phase="ready",
byte_length=len(cached),
)
return cached
self._set_status(key, state="preparing", phase="queued")
with self._materialization_lock:
self._set_status(key, state="preparing", phase="artifact-validation")
result = self._latest(session_id)
if result is None:
self._set_status(key, state="unavailable", phase="unavailable")
return None
self._write_admission(result)
cache = _private_child(
_private_child(_private_directory(self.cache_root), session_id),
result.result_id,
)
output = cache / f"{recording_id}.rrd"
sidecar = output.with_suffix(".rrd.cache.json")
cached = _read_cache(
output,
sidecar,
result_id=result.result_id,
recording_id=recording_id,
)
if cached is not None:
self._set_status(
key,
state="ready",
phase="ready",
byte_length=len(cached),
)
return cached
self._set_status(key, state="preparing", phase="rendering")
payload = _render(
result,
application_id=application_id,
recording_id=recording_id,
ffmpeg_path=self.ffmpeg_path,
temporary_root=cache,
)
self._set_status(key, state="preparing", phase="cache-write")
temporary = output.with_name(f".{output.name}.{os.getpid()}.tmp")
try:
with temporary.open("xb") as stream:
stream.write(payload)
stream.flush()
os.fsync(stream.fileno())
os.chmod(temporary, 0o600)
os.replace(temporary, output)
write_json_atomic(
sidecar,
{
"schema_version": OVERLAY_CACHE_SCHEMA,
"renderer_version": OVERLAY_RENDERER_VERSION,
"session_id": session_id,
"result_id": result.result_id,
"result_created_at_utc": result.created_at_utc,
"recording_id": recording_id,
"byte_length": len(payload),
"sha256": hashlib.sha256(payload).hexdigest(),
},
)
finally:
temporary.unlink(missing_ok=True)
self._set_status(
key,
state="ready",
phase="ready",
byte_length=len(payload),
)
return payload
except BaseException:
self._set_status(key, state="error", phase="error")
raise
def status(self, session_id: str, *, recording_id: str) -> dict[str, Any]:
if (
_SAFE_RECORDING_ID.fullmatch(session_id) is None
or _SAFE_RECORDING_ID.fullmatch(recording_id) is None
):
raise ValueError("integrated perception recording id is invalid")
key = (session_id, recording_id)
with self._status_lock:
value = self._status_by_recording.get(key)
if value is None:
return {
"state": "idle",
"phase": "idle",
"elapsed_seconds": 0.0,
"byte_length": None,
}
elapsed = max(0.0, time.monotonic() - float(value["started_monotonic"]))
return {
"state": value["state"],
"phase": value["phase"],
"elapsed_seconds": round(elapsed, 3),
"byte_length": value.get("byte_length"),
}
def _set_status(
self,
key: tuple[str, str],
*,
state: str,
phase: str,
byte_length: int | None = None,
) -> None:
with self._status_lock:
previous = self._status_by_recording.get(key)
started = (
float(previous["started_monotonic"])
if previous is not None and previous.get("state") == "preparing"
else time.monotonic()
)
self._status_by_recording[key] = {
"state": state,
"phase": phase,
"started_monotonic": started,
"byte_length": byte_length,
}
@contextmanager
def _single_flight(self, key: tuple[str, str]) -> Iterator[None]:
with self._flight_guard:
flight = self._flights.get(key)
if flight is None:
flight = _FlightLock(lock=threading.Lock())
self._flights[key] = flight
flight.users += 1
flight.lock.acquire()
try:
yield
finally:
flight.lock.release()
with self._flight_guard:
flight.users -= 1
if flight.users == 0:
self._flights.pop(key, None)
def _read_admitted_cache(
self,
session_id: str,
recording_id: str,
) -> bytes | None:
cache_root = _private_directory(self.cache_root)
session_cache = _private_child(cache_root, session_id)
admission = self._load_admission(session_cache, session_id)
latest = self._latest_descriptor(session_id)
if latest is None:
return None
if len(jobs) > MAX_SCAN or len(candidates) > MAX_SCAN:
if admission is None or admission.result_id != latest.result_id:
recovered = self._recover_admission_from_cache(
session_cache,
latest,
recording_id,
)
if recovered is not None:
_, payload = recovered
return payload
admission = None
if admission is None or admission.result_id != latest.result_id:
return None
result_cache = _private_child(session_cache, admission.result_id)
output = result_cache / f"{recording_id}.rrd"
sidecar = output.with_suffix(".rrd.cache.json")
return _read_cache(
output,
sidecar,
result_id=admission.result_id,
recording_id=recording_id,
)
def _load_admission(
self,
session_cache: Path,
session_id: str,
) -> _OverlayAdmission | None:
path = session_cache / "admission.json"
try:
value = _read_object(path, session_cache)
except (OSError, SessionIntegrityError):
return None
result_id = value.get("result_id")
result_json_sha256 = value.get("result_json_sha256")
created_at_utc = value.get("result_created_at_utc")
if (
value.get("schema_version") != OVERLAY_ADMISSION_SCHEMA
or value.get("renderer_version") != OVERLAY_RENDERER_VERSION
or value.get("session_id") != session_id
or not isinstance(result_id, str)
or _SAFE_RESULT_ID.fullmatch(result_id) is None
or not isinstance(result_json_sha256, str)
or _SHA256.fullmatch(result_json_sha256) is None
or not isinstance(created_at_utc, str)
):
return None
try:
descriptor = _read_result_descriptor(self.results_root / result_id)
except (OSError, SessionIntegrityError):
return None
if (
descriptor.session_id != session_id
or descriptor.result_json_sha256 != result_json_sha256
or descriptor.created_at_utc != created_at_utc
):
return None
return _OverlayAdmission(
session_id=session_id,
result_id=result_id,
result_json_sha256=result_json_sha256,
result_created_at_utc=created_at_utc,
)
def _recover_admission_from_cache(
self,
session_cache: Path,
descriptor: _ResultDescriptor,
recording_id: str,
) -> tuple[_OverlayAdmission, bytes] | None:
cache = _private_child(session_cache, descriptor.result_id)
output = cache / f"{recording_id}.rrd"
sidecar = output.with_suffix(".rrd.cache.json")
payload = _read_cache(
output,
sidecar,
result_id=descriptor.result_id,
recording_id=recording_id,
)
if payload is None:
return None
admission = _OverlayAdmission(
session_id=descriptor.session_id,
result_id=descriptor.result_id,
result_json_sha256=descriptor.result_json_sha256,
result_created_at_utc=descriptor.created_at_utc,
)
self._write_admission_document(session_cache, admission)
return admission, payload
def _write_admission(self, result: IntegratedPerceptionResult) -> None:
descriptor = _read_result_descriptor(result.result_root)
if (
descriptor.session_id != result.job.session_id
or descriptor.result_id != result.result_id
or descriptor.created_at_utc != result.created_at_utc
):
raise RecordedPerceptionOverlayError(
"integrated perception admission identity changed"
)
cache_root = _private_directory(self.cache_root)
session_cache = _private_child(cache_root, descriptor.session_id)
self._write_admission_document(
session_cache,
_OverlayAdmission(
session_id=descriptor.session_id,
result_id=descriptor.result_id,
result_json_sha256=descriptor.result_json_sha256,
result_created_at_utc=descriptor.created_at_utc,
),
)
@staticmethod
def _write_admission_document(
session_cache: Path,
admission: _OverlayAdmission,
) -> None:
write_json_atomic(
session_cache / "admission.json",
{
"schema_version": OVERLAY_ADMISSION_SCHEMA,
"renderer_version": OVERLAY_RENDERER_VERSION,
"session_id": admission.session_id,
"result_id": admission.result_id,
"result_json_sha256": admission.result_json_sha256,
"result_created_at_utc": admission.result_created_at_utc,
},
)
def _latest_descriptor(self, session_id: str) -> _ResultDescriptor | None:
try:
metadata = self.results_root.stat()
entries = tuple(sorted(self.results_root.iterdir(), key=lambda path: path.name))
except FileNotFoundError:
return None
if len(entries) > MAX_SCAN:
raise RecordedPerceptionOverlayError("integrated perception catalog is outside bounds")
matches = []
for job_root in jobs:
if job_root.is_symlink():
continue
try:
job = validate_camera_compute_job(job_root)
except (OSError, SessionIntegrityError):
continue
if job.session_id != session_id:
continue
for candidate in candidates:
generation = (metadata.st_mtime_ns, len(entries))
with self._catalog_lock:
if generation != self._descriptor_catalog_generation:
self._descriptor_catalog_generation = generation
self._descriptors_by_session.clear()
self._latest_by_session.clear()
existing = self._descriptors_by_session.get(session_id)
if existing is not None:
return existing[0] if existing else None
matches: list[_ResultDescriptor] = []
for candidate in entries:
if candidate.is_symlink() or _SAFE_RESULT_ID.fullmatch(candidate.name) is None:
continue
try:
value = validate_integrated_perception_result(
job_root,
candidate,
self.lidar_packs_root,
)
descriptor = _read_result_descriptor(candidate)
except (OSError, SessionIntegrityError):
continue
if (
value.accepted
and value.publication_scope == "recorded-integrated-realtime-qualification-only"
):
matches.append(value)
if not matches:
self._latest_by_session[session_id] = None
return None
latest = max(matches, key=lambda value: (value.created_at_utc, value.result_id))
self._latest_by_session[session_id] = latest
return latest
def _directory_catalog_signature(*roots: Path) -> tuple[Any, ...]:
"""Track immutable lab catalogs without rehashing every large artifact per request.
Accepted result directories are append-only. Their directory mtimes still
change when an atomic manifest replacement occurs, while a new experiment
changes the parent listing. This cheap signature therefore invalidates the
process-local validation memo without weakening the first full integrity
validation of every catalog generation.
"""
signature: list[Any] = []
for root in roots:
try:
root_stat = root.stat()
entries = sorted(root.iterdir(), key=lambda path: path.name)
except FileNotFoundError:
signature.append((str(root), None))
continue
if len(entries) > MAX_SCAN:
raise RecordedPerceptionOverlayError("integrated perception catalog is outside bounds")
entry_signature = []
for entry in entries:
try:
metadata = entry.lstat()
except FileNotFoundError:
# A concurrent atomic publication will alter the parent mtime
# and be observed on the next request.
continue
entry_signature.append(
(
entry.name,
metadata.st_mode,
metadata.st_size,
metadata.st_mtime_ns,
if descriptor.session_id == session_id:
matches.append(descriptor)
ordered = tuple(
sorted(
matches,
key=lambda value: (value.created_at_utc, value.result_id),
reverse=True,
)
)
signature.append(
(
str(root),
root_stat.st_mtime_ns,
tuple(entry_signature),
)
)
return tuple(signature)
self._descriptors_by_session[session_id] = ordered
return ordered[0] if ordered else None
def _latest(self, session_id: str) -> IntegratedPerceptionResult | None:
latest_descriptor = self._latest_descriptor(session_id)
if latest_descriptor is None:
return None
with self._catalog_lock:
if session_id in self._latest_by_session:
return self._latest_by_session[session_id]
descriptors = self._descriptors_by_session.get(session_id, ())
for descriptor in descriptors:
try:
value = validate_integrated_perception_result(
self.jobs_root / descriptor.job_id,
descriptor.result_root,
self.lidar_packs_root,
)
except (OSError, SessionIntegrityError):
continue
if (
value.accepted
and value.publication_scope == "recorded-integrated-realtime-qualification-only"
):
with self._catalog_lock:
self._latest_by_session[session_id] = value
return value
with self._catalog_lock:
self._latest_by_session[session_id] = None
return None
def _render(
@@ -890,10 +1142,52 @@ def _read_rows(path: Path, root: Path, schema: str) -> list[dict[str, Any]]:
return rows
def _read_result_descriptor(result_root: Path) -> _ResultDescriptor:
root = result_root.expanduser().resolve(strict=True)
if not root.is_dir() or _SAFE_RESULT_ID.fullmatch(root.name) is None:
raise SessionIntegrityError("integrated perception result descriptor is invalid")
result_path = root / "result.json"
result = _read_object(result_path, root)
identity = result.get("identity")
identity_sha256 = result.get("identity_sha256")
created_at_utc = result.get("created_at_utc")
session_id = identity.get("session_id") if isinstance(identity, dict) else None
job_id = identity.get("job_id") if isinstance(identity, dict) else None
if (
result.get("schema_version") != RESULT_SCHEMA
or result.get("result_id") != root.name
or not isinstance(identity, dict)
or identity.get("schema_version") != IDENTITY_SCHEMA
or not isinstance(identity_sha256, str)
or _SHA256.fullmatch(identity_sha256) is None
or root.name != f"e10-integrated-perception-{identity_sha256}"
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
or result.get("acceptance_state") != "accepted"
or result.get("publication_scope")
!= "recorded-integrated-realtime-qualification-only"
or not isinstance(session_id, str)
or _SAFE_RECORDING_ID.fullmatch(session_id) is None
or not isinstance(job_id, str)
or _SAFE_JOB_ID.fullmatch(job_id) is None
or not isinstance(created_at_utc, str)
or not 1 <= len(created_at_utc) <= 64
):
raise SessionIntegrityError("integrated perception result is not admitted")
return _ResultDescriptor(
result_id=root.name,
result_root=root,
result_json_sha256=_sha256(result_path),
session_id=session_id,
job_id=job_id,
created_at_utc=created_at_utc,
)
def _read_cache(
output: Path,
sidecar: Path,
result: IntegratedPerceptionResult,
*,
result_id: str,
recording_id: str,
) -> bytes | None:
try:
@@ -902,9 +1196,9 @@ def _read_cache(
except (OSError, SessionIntegrityError):
return None
if (
value.get("schema_version") != "missioncore.e10-overlay-cache/v1"
value.get("schema_version") != OVERLAY_CACHE_SCHEMA
or value.get("renderer_version") != OVERLAY_RENDERER_VERSION
or value.get("result_id") != result.result_id
or value.get("result_id") != result_id
or value.get("recording_id") != recording_id
or value.get("byte_length") != len(payload)
or value.get("sha256") != hashlib.sha256(payload).hexdigest()
@@ -55,6 +55,7 @@ JS_MAX_SAFE_INTEGER = (1 << 53) - 1
RECORDED_VIEW_POINT_DECIMATION_THRESHOLD = 100_000
RECORDED_VIEW_POINT_STRIDE = 4
RECORDED_VIEW_POINT_FRAME_STRIDE = 5
RECORDED_RRD_IDENTITY_VERSION = "missioncore.recorded-rrd/v1"
# Rerun keys viewer state by these IDs. Reusing them for every settings-only
# blueprint update makes the update overwrite the existing scene instead of
@@ -216,13 +217,26 @@ def export_k1mqtt_to_rrd(
_validate_paths(source, destination)
destination.parent.mkdir(parents=True, exist_ok=True)
recording_id = str(uuid4())
temporary = destination.with_name(f".{destination.name}.{recording_id}.tmp")
source_sha256 = _sha256_file(
source,
cancel_event=cancel_event,
activity_callback=activity_callback,
)
capture_clock = _optional_capture_clock(source, capture_clock_path)
capture_clock_origin = _optional_capture_clock_origin(
source,
capture_clock_origin_path,
)
# Rerun uses this identity to merge independently delivered base,
# blueprint and perception streams. Bind it to immutable source evidence
# so regenerating an unchanged recording does not invalidate every derived
# overlay cache.
recording_id = _stable_recording_id(
source_sha256,
capture_clock,
capture_clock_origin,
)
temporary = destination.with_name(f".{destination.name}.{uuid4()}.tmp")
settings = RerunSceneSettings()
blueprint = _recorded_blueprint(settings)
recording: rr.RecordingStream | None = None
@@ -231,11 +245,6 @@ def export_k1mqtt_to_rrd(
counters = _ExportCounters()
trajectory = _TrajectoryBuffer.empty()
capture_clock = _optional_capture_clock(source, capture_clock_path)
capture_clock_origin = _optional_capture_clock_origin(
source,
capture_clock_origin_path,
)
if (
capture_clock is not None
and capture_clock_origin is not None
@@ -449,6 +458,31 @@ def _validate_paths(source: Path, destination: Path) -> None:
raise RrdExportError("RRD export accepts only native K1MQTT captures")
def _stable_recording_id(
source_sha256: str,
capture_clock: CaptureClockEnvelope | None,
capture_clock_origin: CaptureClockOrigin | None,
) -> str:
identity = hashlib.sha256()
for value in (
RECORDED_RRD_IDENTITY_VERSION,
source_sha256,
str(capture_clock.started_at_epoch_ns) if capture_clock is not None else "-",
str(capture_clock.started_monotonic_ns) if capture_clock is not None else "-",
str(capture_clock.completed_at_epoch_ns) if capture_clock is not None else "-",
str(capture_clock.completed_monotonic_ns) if capture_clock is not None else "-",
str(capture_clock_origin.started_at_epoch_ns)
if capture_clock_origin is not None
else "-",
str(capture_clock_origin.started_monotonic_ns)
if capture_clock_origin is not None
else "-",
):
identity.update(value.encode("ascii"))
identity.update(b"\0")
return f"k1-{identity.hexdigest()}"
def _optional_capture_clock(
source: Path,
explicit_path: Path | None,
+72
View File
@@ -130,6 +130,23 @@ class RecordedPerceptionRequest(StrictApiModel):
)
class RecordedPerceptionPreparationStatus(StrictApiModel):
state: Literal["idle", "preparing", "ready", "unavailable", "error"]
phase: Literal[
"idle",
"cache-lookup",
"queued",
"artifact-validation",
"rendering",
"cache-write",
"ready",
"unavailable",
"error",
]
elapsed_seconds: float = Field(ge=0.0)
byte_length: int | None = Field(default=None, ge=0)
class RecordedPointColorsRequest(StrictApiModel):
application_id: Literal["nodedc_mission_core_recorded"]
recording_id: str = Field(
@@ -915,6 +932,61 @@ def build_session_router(
},
)
@router.get("/api/v1/observation-sessions/{session_id}/perception/status")
async def get_observation_session_perception_status(
session_id: str,
response: Response,
recording_id: Annotated[
str,
Query(
min_length=1,
max_length=128,
pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
),
],
) -> dict[str, Any]:
try:
await run_in_threadpool(
_prepare_replay,
store,
catalog_refresher,
session_id,
1.0,
False,
False,
)
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
status_provider = (
getattr(perception_overlay_provider, "status", None)
if perception_overlay_provider is not None
else None
)
value: object = (
await run_in_threadpool(
status_provider,
session_id,
recording_id=recording_id,
)
if callable(status_provider)
else {
"state": "idle",
"phase": "idle",
"elapsed_seconds": 0.0,
"byte_length": None,
}
)
response.headers["Cache-Control"] = "no-store"
try:
return RecordedPerceptionPreparationStatus.model_validate(value).model_dump()
except ValueError as exc:
raise HTTPException(
status_code=500,
detail="Сервис слоя распознавания вернул некорректный статус.",
) from exc
@router.post("/api/v1/observation-sessions/{session_id}/point-colors.rrd")
async def get_observation_session_point_colors(
session_id: str,