feat(perception): add PointPillars visual audit

This commit is contained in:
DCCONSTRUCTIONS
2026-07-31 11:47:04 +03:00
parent 8fe6184c52
commit a2cb50d1ad
16 changed files with 2351 additions and 4 deletions
+14 -1
View File
@@ -10,6 +10,8 @@ from typing import Any, Final
from fastapi import APIRouter, Query
from k1link.web.l3_pointpillars_visual_api import latest_l3_visual_identity
from k1link.compute.e31_source_qualification import (
E31SourceQualification,
E31SourceQualificationError,
@@ -825,12 +827,13 @@ def build_advanced_laboratory_router(
e38_root_provider: RootProvider = lambda: None,
e39_root_provider: RootProvider = lambda: None,
e40_root_provider: RootProvider = lambda: None,
l3_visual_root_provider: RootProvider = lambda: None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
@router.get("/advanced-index")
def list_advanced_results() -> dict[str, object]:
return _advanced_index(
result = _advanced_index(
(
(
"e31-source-binding",
@@ -897,6 +900,16 @@ def build_advanced_laboratory_router(
),
)
)
l3_identity = latest_l3_visual_identity(l3_visual_root_provider)
if l3_identity is not None:
result["items"].append(
{
"work_id": "l3-pointpillars-visual-audit",
**l3_identity,
"access": "read-only",
}
)
return result
@router.get("/e31/results")
def list_e31_results(
+21
View File
@@ -34,6 +34,9 @@ from k1link.sessions import (
SessionStore,
)
from k1link.web.advanced_laboratory_api import build_advanced_laboratory_router
from k1link.web.l3_pointpillars_visual_api import (
build_l3_pointpillars_visual_router,
)
from k1link.web.artifact_health_api import build_artifact_health_router
from k1link.web.compute_contour_api import build_compute_contour_router
from k1link.web.device_plugin_composition import load_installed_device_plugins
@@ -599,6 +602,24 @@ app.include_router(
/ "e40"
/ "results"
),
l3_visual_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "l3"
/ "visual-audits"
),
)
)
app.include_router(
build_l3_pointpillars_visual_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "l3"
/ "visual-audits"
)
)
)
app.include_router(
@@ -0,0 +1,329 @@
"""Read-only projection of sealed L3 PointPillars visual-audit evidence."""
from __future__ import annotations
import copy
import hashlib
import json
import re
from collections.abc import Callable
from pathlib import Path
from typing import Any, Final
from fastapi import APIRouter, HTTPException, Query
RootProvider = Callable[[], Path | None]
VISUAL_AUDIT_SCHEMA: Final = "missioncore.l3-pointpillars-visual-audit/v1"
VISUAL_CATALOG_SCHEMA: Final = (
"missioncore.l3-pointpillars-visual-audit-catalog/v1"
)
VISUAL_FRAME_SCHEMA: Final = "missioncore.l3-pointpillars-visual-frame/v1"
VISUAL_RESULT_SCHEMA: Final = (
"missioncore.l3-pointpillars-visual-audit-result/v1"
)
VISUAL_RESULT_ID: Final = re.compile(
r"^l3-pointpillars-visual-audit-[a-f0-9]{64}$"
)
FRAME_ID: Final = re.compile(r"^[0-9]{6}$")
MAX_JSON_BYTES: Final = 16 * 1024 * 1024
MAX_CANDIDATES: Final = 64
MAX_FRAMES: Final = 24
def build_l3_pointpillars_visual_router(
*,
root_provider: RootProvider = lambda: None,
) -> APIRouter:
router = APIRouter(
prefix="/api/v1/laboratory/l3/pointpillars-visual-audits",
tags=["laboratory"],
)
@router.get("/results")
def list_results(
limit: int = Query(default=1, ge=1, le=10),
) -> dict[str, object]:
root = _configured_root(root_provider)
if root is None:
return _empty_catalog(False)
candidates = _candidates(root)
items: list[dict[str, object]] = []
invalid_total = 0
for candidate in candidates:
try:
items.append(_project_result(candidate))
except RuntimeError:
invalid_total += 1
items.sort(
key=lambda item: (
str(item["created_at_utc"]),
str(item["result_id"]),
),
reverse=True,
)
return {
"schema_version": (
"missioncore.l3-pointpillars-visual-audit-catalog-results/v1"
),
"configured": True,
"items": items[:limit],
"candidate_total": len(candidates),
"invalid_total": invalid_total,
"access": "read-only",
}
@router.get("/{result_id}/frames/{frame_id}")
def get_frame(result_id: str, frame_id: str) -> dict[str, object]:
if not VISUAL_RESULT_ID.fullmatch(result_id):
raise HTTPException(status_code=404, detail="visual audit not found")
if not FRAME_ID.fullmatch(frame_id):
raise HTTPException(status_code=404, detail="visual frame not found")
root = _configured_root(root_provider)
if root is None:
raise HTTPException(status_code=404, detail="visual audit not found")
candidate = root / result_id
try:
result = _load_result(candidate)
descriptor = next(
item
for item in result["catalog"]["frames"]
if item["frame_id"] == frame_id
)
relative = descriptor["detail_path"]
if relative != f"frames/{frame_id}.json":
raise RuntimeError("visual frame path changed")
path = candidate / relative
payload = _read_json(path)
if (
payload.get("schema_version") != VISUAL_FRAME_SCHEMA
or payload.get("frame_id") != frame_id
or descriptor["detail_sha256"] != _sha256(path)
or descriptor["detail_byte_length"] != path.stat().st_size
):
raise RuntimeError("visual frame identity changed")
except (RuntimeError, StopIteration):
raise HTTPException(
status_code=404,
detail="visual frame not found",
) from None
return {**copy.deepcopy(payload), "access": "read-only"}
return router
def latest_l3_visual_identity(
root_provider: RootProvider,
) -> dict[str, str] | None:
"""Return the newest verified identity for the shared LAB index."""
root = _configured_root(root_provider)
if root is None:
return None
valid: list[dict[str, object]] = []
for candidate in _candidates(root):
try:
valid.append(_project_result(candidate))
except RuntimeError:
continue
if not valid:
return None
latest = max(
valid,
key=lambda item: (
str(item["created_at_utc"]),
str(item["result_id"]),
),
)
return {
"result_id": str(latest["result_id"]),
"created_at_utc": str(latest["created_at_utc"]),
}
def _project_result(candidate: Path) -> dict[str, object]:
result = _load_result(candidate)
manifest = result["manifest"]
catalog = result["catalog"]
metrics = manifest["source_metrics"]
return {
"schema_version": VISUAL_RESULT_SCHEMA,
"result_id": manifest["result_id"],
"created_at_utc": manifest["created_at_utc"],
"status": manifest["status"],
"source_run_id": manifest["identity"]["source_run_id"],
"source_frame_results_identity_sha256": manifest["identity"][
"source_frame_results_identity_sha256"
],
"dataset_source_id": manifest["identity"]["dataset_source_id"],
"dataset_release_identity_sha256": manifest["identity"][
"dataset_release_identity_sha256"
],
"metrics": copy.deepcopy(metrics),
"frames": copy.deepcopy(catalog["frames"]),
"matching": copy.deepcopy(manifest["identity"]["matching"]),
"point_sampling": copy.deepcopy(
manifest["identity"]["point_sampling"]
),
"authority": copy.deepcopy(manifest["authority"]),
"access": "read-only",
}
def _load_result(candidate: Path) -> dict[str, Any]:
if (
not candidate.is_dir()
or candidate.is_symlink()
or not VISUAL_RESULT_ID.fullmatch(candidate.name)
):
raise RuntimeError("visual audit candidate is invalid")
manifest_path = candidate / "manifest.json"
manifest = _read_json(manifest_path)
identity = manifest.get("identity")
authority = manifest.get("authority")
catalog_descriptor = manifest.get("catalog")
if (
manifest.get("schema_version") != VISUAL_AUDIT_SCHEMA
or manifest.get("result_id") != candidate.name
or manifest.get("status") != "operator-visual-review-required"
or not isinstance(manifest.get("created_at_utc"), str)
or not isinstance(identity, dict)
or not isinstance(authority, dict)
or authority.get("read_only") is not True
or authority.get("commands_enabled") is not False
or authority.get("navigation_or_safety_accepted") is not False
or manifest.get("identity_sha256")
!= hashlib.sha256(_canonical_json(identity)).hexdigest()
or candidate.name
!= f"l3-pointpillars-visual-audit-{manifest.get('identity_sha256')}"
or not isinstance(catalog_descriptor, dict)
or catalog_descriptor.get("path") != "catalog.json"
or catalog_descriptor.get("role") != "visual-frame-catalog"
):
raise RuntimeError("visual audit manifest is invalid")
catalog_path = candidate / "catalog.json"
if (
catalog_descriptor.get("sha256") != _sha256(catalog_path)
or catalog_descriptor.get("byte_length") != catalog_path.stat().st_size
):
raise RuntimeError("visual audit catalog changed")
catalog = _read_json(catalog_path)
frames = catalog.get("frames")
if (
catalog.get("schema_version") != VISUAL_CATALOG_SCHEMA
or catalog.get("result_id") != candidate.name
or catalog.get("source_run_id") != identity.get("source_run_id")
or not isinstance(frames, list)
or not 1 <= len(frames) <= MAX_FRAMES
or catalog.get("frame_count") != len(frames)
or len({item.get("frame_id") for item in frames if isinstance(item, dict)})
!= len(frames)
or any(not _valid_frame_descriptor(item) for item in frames)
):
raise RuntimeError("visual audit catalog is invalid")
if not isinstance(manifest.get("source_metrics"), dict):
raise RuntimeError("visual audit metrics are unavailable")
return {"manifest": manifest, "catalog": catalog}
def _valid_frame_descriptor(value: object) -> bool:
if not isinstance(value, dict):
return False
frame_id = value.get("frame_id")
counts = (
"prediction_count",
"evaluated_prediction_count",
"outside_shared_range_count",
"truth_count",
"true_positive_count",
"false_positive_count",
"false_negative_count",
"detail_byte_length",
)
return (
isinstance(frame_id, str)
and FRAME_ID.fullmatch(frame_id) is not None
and value.get("detail_path") == f"frames/{frame_id}.json"
and isinstance(value.get("detail_sha256"), str)
and re.fullmatch(r"[a-f0-9]{64}", value["detail_sha256"]) is not None
and all(
isinstance(value.get(key), int)
and not isinstance(value.get(key), bool)
and value[key] >= 0
for key in counts
)
and 0 < value["detail_byte_length"] <= MAX_JSON_BYTES
and isinstance(value.get("inference_ms"), (int, float))
and not isinstance(value.get("inference_ms"), bool)
and 0 < value["inference_ms"] < 60_000
and isinstance(value.get("truth_classes"), list)
)
def _configured_root(provider: RootProvider) -> Path | None:
value = provider()
if value is None:
return None
root = value.expanduser().absolute()
if not root.is_dir() or root.is_symlink():
return None
return root
def _candidates(root: Path) -> list[Path]:
candidates = [
path
for path in root.iterdir()
if path.is_dir()
and not path.is_symlink()
and VISUAL_RESULT_ID.fullmatch(path.name)
]
if len(candidates) > MAX_CANDIDATES:
raise RuntimeError("visual audit candidate bound exceeded")
return candidates
def _empty_catalog(configured: bool) -> dict[str, object]:
return {
"schema_version": (
"missioncore.l3-pointpillars-visual-audit-catalog-results/v1"
),
"configured": configured,
"items": [],
"candidate_total": 0,
"invalid_total": 0,
"access": "read-only",
}
def _read_json(path: Path) -> dict[str, Any]:
try:
if (
not path.is_file()
or path.is_symlink()
or not 0 < path.stat().st_size <= MAX_JSON_BYTES
):
raise RuntimeError(f"{path.name} is unavailable")
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise RuntimeError(f"{path.name} is invalid") from exc
if not isinstance(payload, dict):
raise RuntimeError(f"{path.name} is not an object")
return payload
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _canonical_json(payload: object) -> bytes:
return json.dumps(
payload,
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
).encode("utf-8")