feat(perception): add PointPillars visual audit
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user