feat(perception): add diagnostic semantic SLAM replay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-06 11:26:37 +03:00
parent b7a51e26e6
commit 8eaa3ab497
15 changed files with 4554 additions and 6 deletions
+12
View File
@@ -74,6 +74,7 @@ from k1link.web.e46i_grounding_dino_full_replay_api import (
from k1link.web.e46j_raw_fisheye_realtime_api import (
build_e46j_raw_fisheye_realtime_router,
)
from k1link.web.e47_semantic_slam_api import build_e47_semantic_slam_router
from k1link.web.environment_api import build_environment_router
from k1link.web.l3_pointpillars_visual_api import (
build_l3_pointpillars_visual_router,
@@ -777,6 +778,17 @@ app.include_router(
),
)
)
app.include_router(
build_e47_semantic_slam_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e47"
/ "semantic-slam-results"
),
)
)
app.include_router(
build_e46e_ready_stack_router(
root_provider=lambda: (
+556
View File
@@ -0,0 +1,556 @@
"""Read-only LAB projection of the immutable E47 semantic/SLAM shadow result."""
from __future__ import annotations
import copy
import hashlib
import json
import re
import zipfile
from collections.abc import Callable
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Final
import numpy as np
import numpy.typing as npt
from fastapi import APIRouter, HTTPException, Query, Response
from k1link.perception.semantic_fusion import SemanticEvidenceStatus
from k1link.perception.semantic_slam_replay import (
PUBLICATION_STATUS,
SEMANTIC_SLAM_MANIFEST_NAME,
SEMANTIC_SLAM_MASKS_NAME,
SEMANTIC_SLAM_OBSERVATIONS_NAME,
SEMANTIC_SLAM_POINTS_NAME,
SEMANTIC_SLAM_REPORT_NAME,
SEMANTIC_SLAM_RESULT_PREFIX,
SEMANTIC_SLAM_TAXONOMY_NAME,
SEMANTIC_SLAM_TAXONOMY_SCHEMA,
SemanticSlamReplayError,
SemanticSlamReplayResult,
read_semantic_slam_replay_result,
)
E47_SEMANTIC_SLAM_CATALOG_SCHEMA: Final = "missioncore.e47-semantic-slam-catalog/v1"
E47_SEMANTIC_SLAM_VIEW_SCHEMA: Final = "missioncore.e47-semantic-slam-view/v1"
E47_SEMANTIC_SLAM_CHUNK_SCHEMA: Final = "missioncore.e47-semantic-slam-chunk/v1"
E47_SEMANTIC_SLAM_FRAME_SCHEMA: Final = "missioncore.e47-semantic-slam-frame/v1"
E47_SEMANTIC_SLAM_VIEW_STATUS: Final = "diagnostic-semantic-slam-shadow"
E47_SEMANTIC_SLAM_MAX_CHUNK_FRAMES: Final = 24
_RESULT_ID = re.compile(rf"^{SEMANTIC_SLAM_RESULT_PREFIX}[a-f0-9]{{64}}$")
_EXPECTED_ARTIFACTS: Final = (
SEMANTIC_SLAM_MANIFEST_NAME,
SEMANTIC_SLAM_REPORT_NAME,
SEMANTIC_SLAM_POINTS_NAME,
SEMANTIC_SLAM_OBSERVATIONS_NAME,
SEMANTIC_SLAM_MASKS_NAME,
SEMANTIC_SLAM_TAXONOMY_NAME,
)
_MAX_TAXONOMY_BYTES: Final = 1024 * 1024
_MAX_MASK_BYTES: Final = 16 * 1024 * 1024
RootProvider = Callable[[], Path | None]
Int64Array = npt.NDArray[np.int64]
Int32Array = npt.NDArray[np.int32]
UInt8Array = npt.NDArray[np.uint8]
@dataclass(frozen=True, slots=True)
class _SemanticPointLedger:
frame_offsets: Int64Array
point_labels: UInt8Array
point_status_codes: UInt8Array
frame_source_point_counts: Int32Array
frame_labeled_point_counts: Int32Array
frame_ambiguous_point_counts: Int32Array
frame_unprojected_point_counts: Int32Array
frame_absent_point_counts: Int32Array
@property
def frame_count(self) -> int:
return int(self.frame_offsets.size - 1)
def build_e47_semantic_slam_router(
*,
root_provider: RootProvider = lambda: None,
) -> APIRouter:
"""Expose immutable E47 evidence without granting it safety authority."""
router = APIRouter(
prefix="/api/v1/laboratory/e47-semantic-slam",
tags=["laboratory"],
)
def result(result_id: str) -> SemanticSlamReplayResult:
if _RESULT_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="E47 result не найден")
root = _configured_root(root_provider)
if root is None:
raise HTTPException(status_code=404, detail="E47 result не найден")
candidate = root / result_id
if candidate.is_symlink():
raise HTTPException(status_code=404, detail="E47 result не найден")
try:
path = candidate.resolve(strict=True)
except OSError:
raise HTTPException(status_code=404, detail="E47 result не найден") from None
if path.parent != root or not path.is_dir():
raise HTTPException(status_code=404, detail="E47 result не найден")
try:
return _read_semantic_result_cached(str(path), _result_signature(path))
except (SemanticSlamReplayError, OSError, ValueError, zipfile.BadZipFile):
raise HTTPException(status_code=404, detail="E47 result не найден") from None
def point_ledger(result_id: str) -> tuple[SemanticSlamReplayResult, _SemanticPointLedger]:
frozen = result(result_id)
try:
signature = _result_signature(frozen.result_root)
return frozen, _read_point_ledger_cached(str(frozen.result_root), signature)
except (SemanticSlamReplayError, OSError, ValueError, zipfile.BadZipFile):
raise HTTPException(
status_code=503,
detail="E47 semantic timeline не прошёл проверку",
) from None
@router.get("/results")
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
candidates = _candidates(root_provider)
items: list[dict[str, object]] = []
invalid_total = 0
for candidate in candidates:
if len(items) >= limit:
break
try:
items.append(_project_result(result(candidate.name)))
except (HTTPException, OSError, ValueError, json.JSONDecodeError):
invalid_total += 1
return {
"schema_version": E47_SEMANTIC_SLAM_CATALOG_SCHEMA,
"configured": _configured_root(root_provider) is not None,
"items": items,
"candidate_total": len(candidates),
"invalid_total": invalid_total,
"access": "read-only-diagnostic-shadow",
}
@router.get("/results/{result_id}/timeline/chunk")
def get_timeline_chunk(
result_id: str,
start: int = Query(default=0, ge=0),
count: int = Query(
default=12,
ge=1,
le=E47_SEMANTIC_SLAM_MAX_CHUNK_FRAMES,
),
) -> dict[str, object]:
frozen, ledger = point_ledger(result_id)
if (
not isinstance(start, int)
or isinstance(start, bool)
or not isinstance(count, int)
or isinstance(count, bool)
or start < 0
or not 1 <= count <= E47_SEMANTIC_SLAM_MAX_CHUNK_FRAMES
):
raise HTTPException(status_code=422, detail="Некорректный E47 timeline chunk")
if start >= ledger.frame_count:
raise HTTPException(status_code=404, detail="E47 timeline chunk не найден")
stop = min(start + count, ledger.frame_count)
try:
frames = [_project_frame(ledger, sequence) for sequence in range(start, stop)]
except ValueError:
raise HTTPException(
status_code=503,
detail="E47 semantic timeline не прошёл проверку",
) from None
return {
"schema_version": E47_SEMANTIC_SLAM_CHUNK_SCHEMA,
"result_id": frozen.result_id,
"start_sequence": start,
"frame_count": len(frames),
"next_sequence": stop if stop < ledger.frame_count else None,
"frames": frames,
"access": "read-only-diagnostic-shadow",
}
@router.get("/results/{result_id}/masks/{sequence}")
def get_mask(result_id: str, sequence: int) -> Response:
frozen = result(result_id)
frame_total = _frame_total(frozen)
if (
not isinstance(sequence, int)
or isinstance(sequence, bool)
or not 0 <= sequence < frame_total
):
raise HTTPException(status_code=404, detail="E47 semantic mask не найдена")
try:
signature = _result_signature(frozen.result_root)
frozen = _read_semantic_result_cached(str(frozen.result_root), signature)
payload = _read_mask(frozen, sequence)
if _result_signature(frozen.result_root) != signature:
raise ValueError("E47 result changed during mask read")
except (
SemanticSlamReplayError,
OSError,
KeyError,
ValueError,
RuntimeError,
zipfile.BadZipFile,
):
raise HTTPException(
status_code=503,
detail="E47 semantic mask не прошла проверку",
) from None
digest = hashlib.sha256(payload).hexdigest()
return Response(
content=payload,
media_type="image/png",
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{digest}"',
"X-Content-Type-Options": "nosniff",
},
)
return router
@lru_cache(maxsize=4)
def _read_semantic_result_cached(
root_value: str,
signature: tuple[int, ...],
) -> SemanticSlamReplayResult:
del signature
return read_semantic_slam_replay_result(Path(root_value))
@lru_cache(maxsize=2)
def _read_point_ledger_cached(
root_value: str,
signature: tuple[int, ...],
) -> _SemanticPointLedger:
frozen = _read_semantic_result_cached(root_value, signature)
path = frozen.result_root / SEMANTIC_SLAM_POINTS_NAME
required = {
"frame_offsets",
"point_labels",
"point_status_codes",
"frame_source_point_counts",
"frame_labeled_point_counts",
"frame_ambiguous_point_counts",
"frame_unprojected_point_counts",
"frame_absent_point_counts",
}
with np.load(path, allow_pickle=False) as archive:
if not required.issubset(archive.files):
raise ValueError("E47 semantic point arrays are incomplete")
ledger = _SemanticPointLedger(
frame_offsets=_frozen_int64(archive["frame_offsets"]),
point_labels=_frozen_uint8(archive["point_labels"]),
point_status_codes=_frozen_uint8(archive["point_status_codes"]),
frame_source_point_counts=_frozen_int32(archive["frame_source_point_counts"]),
frame_labeled_point_counts=_frozen_int32(archive["frame_labeled_point_counts"]),
frame_ambiguous_point_counts=_frozen_int32(archive["frame_ambiguous_point_counts"]),
frame_unprojected_point_counts=_frozen_int32(archive["frame_unprojected_point_counts"]),
frame_absent_point_counts=_frozen_int32(archive["frame_absent_point_counts"]),
)
_validate_point_ledger(ledger, _frame_total(frozen))
_validate_class_status_bindings(ledger, _read_taxonomy(frozen))
return ledger
def _project_result(result: SemanticSlamReplayResult) -> dict[str, object]:
if result.status != PUBLICATION_STATUS:
raise ValueError("E47 publication status changed")
identity = _object(result.manifest.get("identity"), "E47 identity")
provider = _object(identity.get("semantic_provider"), "E47 provider")
temporal_binding = _object(
identity.get("temporal_binding"),
"E47 temporal binding",
)
authority = _object(identity.get("authority"), "E47 authority")
if (
authority.get("ground_truth") is not False
or authority.get("semantic_authority") != "diagnostic-only"
or authority.get("navigation_or_safety_accepted") is not False
or authority.get("actuation_allowed") is not False
):
raise ValueError("E47 authority changed")
taxonomy = _read_taxonomy(result)
return {
"schema_version": E47_SEMANTIC_SLAM_VIEW_SCHEMA,
"result_id": result.result_id,
"created_at_utc": result.manifest["created_at_utc"],
"status": E47_SEMANTIC_SLAM_VIEW_STATUS,
"profile_id": identity["profile_id"],
"base_m4_result_id": identity["base_m4_result_id"],
"semantic_result_id": identity["semantic_result_id"],
"geometry_result_id": identity["geometry_result_id"],
"source_pack_id": identity["source_pack_id"],
"calibration_content_sha256": identity["calibration_content_sha256"],
"provider": {
"provider_id": provider["provider_id"],
"model_id": provider["model_id"],
"model_revision": provider["model_revision"],
"model_weights_sha256": provider["model_weights_sha256"],
"preprocess_id": provider["preprocess_id"],
},
"temporal_binding": copy.deepcopy(temporal_binding),
"taxonomy": taxonomy,
"metrics": copy.deepcopy(result.metrics),
"acceptance": {
"artifact_contract_passed": True,
"frame_accounting_passed": True,
"point_accounting_passed": True,
"observation_binding_passed": True,
"temporal_binding_passed": True,
"independent_semantic_truth_passed": False,
"provider_promoted": False,
},
"limitations": copy.deepcopy(result.report["limitations"]),
"ground_truth": False,
"semantic_authority": "diagnostic-only",
"navigation_or_safety_accepted": False,
"actuation_allowed": False,
"access": "read-only-diagnostic-shadow",
}
def _project_frame(ledger: _SemanticPointLedger, sequence: int) -> dict[str, object]:
offset = int(ledger.frame_offsets[sequence])
stop = int(ledger.frame_offsets[sequence + 1])
labels = ledger.point_labels[offset:stop].astype(np.int16)
statuses = ledger.point_status_codes[offset:stop]
unavailable = np.isin(
statuses,
(
int(SemanticEvidenceStatus.ABSENT),
int(SemanticEvidenceStatus.UNPROJECTED),
),
)
labels[unavailable] = -1
counts = {
"labeled": int(ledger.frame_labeled_point_counts[sequence]),
"ambiguous": int(ledger.frame_ambiguous_point_counts[sequence]),
"unprojected": int(ledger.frame_unprojected_point_counts[sequence]),
"absent": int(ledger.frame_absent_point_counts[sequence]),
}
actual_counts = {
"labeled": int(np.count_nonzero(statuses == int(SemanticEvidenceStatus.LABELED))),
"ambiguous": int(np.count_nonzero(statuses == int(SemanticEvidenceStatus.AMBIGUOUS))),
"unprojected": int(np.count_nonzero(statuses == int(SemanticEvidenceStatus.UNPROJECTED))),
"absent": int(np.count_nonzero(statuses == int(SemanticEvidenceStatus.ABSENT))),
}
if counts != actual_counts or sum(counts.values()) != stop - offset:
raise ValueError("E47 frame point accounting changed")
return {
"schema_version": E47_SEMANTIC_SLAM_FRAME_SCHEMA,
"sequence": sequence,
"source_point_count": int(ledger.frame_source_point_counts[sequence]),
"class_ids": labels.tolist(),
"status_codes": statuses.tolist(),
"counts": counts,
}
def _read_taxonomy(result: SemanticSlamReplayResult) -> list[dict[str, object]]:
path = result.result_root / SEMANTIC_SLAM_TAXONOMY_NAME
if not path.is_file() or path.is_symlink() or path.stat().st_size > _MAX_TAXONOMY_BYTES:
raise ValueError("E47 taxonomy is invalid")
payload = path.read_bytes()
identity = _object(result.manifest.get("identity"), "E47 identity")
if hashlib.sha256(payload).hexdigest() != identity.get("taxonomy_sha256"):
raise ValueError("E47 taxonomy identity changed")
document = json.loads(payload)
if not isinstance(document, dict) or set(document) != {"schema_version", "classes"}:
raise ValueError("E47 taxonomy contract changed")
if document.get("schema_version") != SEMANTIC_SLAM_TAXONOMY_SCHEMA:
raise ValueError("E47 taxonomy schema changed")
classes = document.get("classes")
if not isinstance(classes, list) or not classes:
raise ValueError("E47 taxonomy classes are invalid")
for item in classes:
if not isinstance(item, dict) or set(item) != {
"class_id",
"label",
"disposition",
"color_rgb",
}:
raise ValueError("E47 taxonomy class changed")
return copy.deepcopy(classes)
def _read_mask(result: SemanticSlamReplayResult, sequence: int) -> bytes:
archive_path = result.result_root / SEMANTIC_SLAM_MASKS_NAME
if not archive_path.is_file() or archive_path.is_symlink():
raise ValueError("E47 semantic mask archive is invalid")
member_name = f"semantic-masks/frame-{sequence + 1:06d}.png"
with zipfile.ZipFile(archive_path, mode="r") as archive:
info = archive.getinfo(member_name)
if info.is_dir() or not 0 < info.file_size <= _MAX_MASK_BYTES:
raise ValueError("E47 semantic mask member is invalid")
payload = archive.read(info)
if len(payload) != info.file_size or not payload.startswith(b"\x89PNG\r\n\x1a\n"):
raise ValueError("E47 semantic mask payload is invalid")
return payload
def _validate_point_ledger(ledger: _SemanticPointLedger, frame_total: int) -> None:
arrays = (
ledger.frame_source_point_counts,
ledger.frame_labeled_point_counts,
ledger.frame_ambiguous_point_counts,
ledger.frame_unprojected_point_counts,
ledger.frame_absent_point_counts,
)
if (
ledger.frame_offsets.ndim != 1
or ledger.frame_offsets.shape != (frame_total + 1,)
or int(ledger.frame_offsets[0]) != 0
or np.any(np.diff(ledger.frame_offsets) < 0)
or ledger.point_labels.ndim != 1
or ledger.point_status_codes.shape != ledger.point_labels.shape
or int(ledger.frame_offsets[-1]) != ledger.point_labels.size
or any(value.ndim != 1 or value.shape != (frame_total,) for value in arrays)
or np.any(np.asarray(arrays) < 0)
or not np.array_equal(
np.diff(ledger.frame_offsets),
ledger.frame_source_point_counts,
)
):
raise ValueError("E47 semantic point ledger changed")
valid_statuses = {int(status) for status in SemanticEvidenceStatus}
if set(int(value) for value in np.unique(ledger.point_status_codes)) - valid_statuses:
raise ValueError("E47 semantic status changed")
expected_total = (
ledger.frame_labeled_point_counts
+ ledger.frame_ambiguous_point_counts
+ ledger.frame_unprojected_point_counts
+ ledger.frame_absent_point_counts
)
if not np.array_equal(expected_total, ledger.frame_source_point_counts):
raise ValueError("E47 semantic frame accounting changed")
def _validate_class_status_bindings(
ledger: _SemanticPointLedger,
taxonomy: list[dict[str, object]],
) -> None:
dispositions: dict[int, str] = {}
for item in taxonomy:
class_id = item.get("class_id")
disposition = item.get("disposition")
if (
not isinstance(class_id, int)
or isinstance(class_id, bool)
or not 0 <= class_id <= 255
or disposition not in {"labeled", "ambiguous"}
or class_id in dispositions
):
raise ValueError("E47 semantic taxonomy binding changed")
dispositions[class_id] = str(disposition)
unavailable = np.isin(
ledger.point_status_codes,
(
int(SemanticEvidenceStatus.ABSENT),
int(SemanticEvidenceStatus.UNPROJECTED),
),
)
if np.any(ledger.point_labels[unavailable] != 0):
raise ValueError("E47 unavailable semantic point carried a class")
for status, disposition in (
(SemanticEvidenceStatus.AMBIGUOUS, "ambiguous"),
(SemanticEvidenceStatus.LABELED, "labeled"),
):
class_ids = np.unique(ledger.point_labels[ledger.point_status_codes == int(status)])
if any(dispositions.get(int(class_id)) != disposition for class_id in class_ids):
raise ValueError("E47 semantic point status disagrees with taxonomy")
def _frame_total(result: SemanticSlamReplayResult) -> int:
frames = _object(result.metrics.get("frames"), "E47 frame metrics")
value = frames.get("total")
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
raise ValueError("E47 frame count changed")
return value
def _frozen_int64(value: npt.ArrayLike) -> Int64Array:
array = np.array(value, dtype=np.int64, order="C", copy=True)
array.setflags(write=False)
return array
def _frozen_int32(value: npt.ArrayLike) -> Int32Array:
array = np.array(value, dtype=np.int32, order="C", copy=True)
array.setflags(write=False)
return array
def _frozen_uint8(value: npt.ArrayLike) -> UInt8Array:
array = np.array(value, dtype=np.uint8, order="C", copy=True)
array.setflags(write=False)
return array
def _configured_root(provider: RootProvider) -> Path | None:
value = provider()
if value is None:
return None
candidate = value.expanduser().absolute()
if candidate.is_symlink():
return None
try:
root = candidate.resolve(strict=True)
except OSError:
return None
return root if root.is_dir() else None
def _result_signature(root: Path) -> tuple[int, ...]:
signature: list[int] = []
for name in _EXPECTED_ARTIFACTS:
path = root / name
if not path.is_file() or path.is_symlink():
raise ValueError("E47 result artifact is invalid")
stat = path.stat()
signature.extend((stat.st_ino, stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns))
return tuple(signature)
def _candidates(provider: RootProvider) -> list[Path]:
root = _configured_root(provider)
if root is None:
return []
try:
return sorted(
(
item
for item in root.iterdir()
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,
)
except OSError:
return []
def _object(value: object, label: str) -> dict[str, object]:
if not isinstance(value, dict):
raise ValueError(f"{label} is invalid")
return value
__all__ = [
"E47_SEMANTIC_SLAM_CATALOG_SCHEMA",
"E47_SEMANTIC_SLAM_CHUNK_SCHEMA",
"E47_SEMANTIC_SLAM_FRAME_SCHEMA",
"E47_SEMANTIC_SLAM_MAX_CHUNK_FRAMES",
"E47_SEMANTIC_SLAM_VIEW_SCHEMA",
"build_e47_semantic_slam_router",
]