feat(perception): integrate calibrated operator pipeline
Add calibrated K1 projection, recorded and near-live perception qualification, unified Rerun operator layers, bounded replay admission, audited viewer controls, worker experiments, and lab evidence.
This commit is contained in:
@@ -1,11 +1,80 @@
|
||||
"""Host-owned compute handoff contracts."""
|
||||
|
||||
from .annotation_workspace import (
|
||||
ANNOTATION_WORKSPACE_SCHEMA,
|
||||
AnnotationWorkspace,
|
||||
AnnotationWorkspaceError,
|
||||
prepare_annotation_workspace,
|
||||
validate_annotation_workspace,
|
||||
)
|
||||
from .evaluation_pack import (
|
||||
ANNOTATION_CONTRACT_SCHEMA,
|
||||
EVALUATION_PACK_SCHEMA,
|
||||
EvaluationFrameRequest,
|
||||
EvaluationPackFrame,
|
||||
RecordedEvaluationPack,
|
||||
RecordedEvaluationPackError,
|
||||
prepare_recorded_evaluation_pack,
|
||||
validate_recorded_evaluation_pack,
|
||||
)
|
||||
from .fusion_epoch import (
|
||||
RecordedCalibratedFusion,
|
||||
RecordedCalibratedFusionStore,
|
||||
RecordedPerceptionOverlayMux,
|
||||
validate_recorded_calibrated_fusion,
|
||||
)
|
||||
from .integrated_perception import (
|
||||
IntegratedPerceptionOverlayStore,
|
||||
IntegratedPerceptionResult,
|
||||
validate_integrated_perception_result,
|
||||
)
|
||||
from .jobs import (
|
||||
COMPUTE_JOB_SCHEMA,
|
||||
CameraComputeJob,
|
||||
prepare_camera_compute_job,
|
||||
validate_camera_compute_job,
|
||||
)
|
||||
from .live_perception import (
|
||||
LIVE_INGRESS_SCHEMA,
|
||||
LIVE_INGRESS_WIRE_SCHEMA,
|
||||
TELEMETRY_SCHEMA,
|
||||
WORLD_STATE_SCHEMA,
|
||||
LatestWinsQueue,
|
||||
LiveIngressEvent,
|
||||
LivePerceptionIngress,
|
||||
QueueSnapshot,
|
||||
WorldStateProjector,
|
||||
classify_health,
|
||||
)
|
||||
from .live_replay_qualification import (
|
||||
LiveReplayQualificationResult,
|
||||
validate_live_replay_qualification_result,
|
||||
)
|
||||
from .multirate_perception_qualification import (
|
||||
MultiratePerceptionArtifact,
|
||||
MultiratePerceptionQualificationResult,
|
||||
validate_multirate_perception_qualification_result,
|
||||
)
|
||||
from .perception_epoch import (
|
||||
RecordedPerceptionEpochResult,
|
||||
RecordedPerceptionEpochStore,
|
||||
RecordedPerceptionVideo,
|
||||
validate_recorded_perception_epoch_result,
|
||||
)
|
||||
from .qualification import (
|
||||
DEFAULT_QUALIFICATION_FRAME_COUNT,
|
||||
QUALIFICATION_POLICY,
|
||||
QualificationFrame,
|
||||
RecordedQualificationSlice,
|
||||
RecordedQualificationSliceError,
|
||||
prepare_recorded_qualification_slice,
|
||||
validate_recorded_qualification_slice,
|
||||
)
|
||||
from .realtime_tracking_qualification import (
|
||||
RealtimeTrackingArtifact,
|
||||
RealtimeTrackingQualificationResult,
|
||||
validate_realtime_tracking_qualification_result,
|
||||
)
|
||||
from .results import (
|
||||
DetectionFrame,
|
||||
ObjectDetection,
|
||||
@@ -14,16 +83,79 @@ from .results import (
|
||||
RecordedPerceptionResult,
|
||||
validate_recorded_perception_result,
|
||||
)
|
||||
from .tracked_fusion_qualification import (
|
||||
TrackedFusionQualificationResult,
|
||||
validate_tracked_fusion_qualification_result,
|
||||
)
|
||||
from .tracking_qualification import (
|
||||
TrackingQualificationArtifact,
|
||||
TrackingQualificationResult,
|
||||
validate_tracking_qualification_result,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ANNOTATION_CONTRACT_SCHEMA",
|
||||
"ANNOTATION_WORKSPACE_SCHEMA",
|
||||
"AnnotationWorkspace",
|
||||
"AnnotationWorkspaceError",
|
||||
"COMPUTE_JOB_SCHEMA",
|
||||
"DEFAULT_QUALIFICATION_FRAME_COUNT",
|
||||
"EVALUATION_PACK_SCHEMA",
|
||||
"EvaluationFrameRequest",
|
||||
"EvaluationPackFrame",
|
||||
"LatestWinsQueue",
|
||||
"LIVE_INGRESS_SCHEMA",
|
||||
"LIVE_INGRESS_WIRE_SCHEMA",
|
||||
"LiveIngressEvent",
|
||||
"LivePerceptionIngress",
|
||||
"IntegratedPerceptionOverlayStore",
|
||||
"IntegratedPerceptionResult",
|
||||
"LiveReplayQualificationResult",
|
||||
"MultiratePerceptionArtifact",
|
||||
"MultiratePerceptionQualificationResult",
|
||||
"QUALIFICATION_POLICY",
|
||||
"QueueSnapshot",
|
||||
"RecordedCalibratedFusion",
|
||||
"RecordedCalibratedFusionStore",
|
||||
"RecordedEvaluationPack",
|
||||
"RecordedEvaluationPackError",
|
||||
"RecordedPerceptionOverlayMux",
|
||||
"validate_recorded_calibrated_fusion",
|
||||
"CameraComputeJob",
|
||||
"prepare_camera_compute_job",
|
||||
"prepare_annotation_workspace",
|
||||
"prepare_recorded_evaluation_pack",
|
||||
"validate_camera_compute_job",
|
||||
"RecordedPerceptionEpochResult",
|
||||
"RecordedPerceptionEpochStore",
|
||||
"RecordedPerceptionVideo",
|
||||
"QualificationFrame",
|
||||
"RealtimeTrackingArtifact",
|
||||
"RealtimeTrackingQualificationResult",
|
||||
"RecordedQualificationSlice",
|
||||
"RecordedQualificationSliceError",
|
||||
"validate_recorded_perception_epoch_result",
|
||||
"validate_live_replay_qualification_result",
|
||||
"validate_integrated_perception_result",
|
||||
"validate_multirate_perception_qualification_result",
|
||||
"prepare_recorded_qualification_slice",
|
||||
"DetectionFrame",
|
||||
"ObjectDetection",
|
||||
"RecordedPerceptionOverlayError",
|
||||
"RecordedPerceptionOverlayStore",
|
||||
"RecordedPerceptionResult",
|
||||
"TrackingQualificationArtifact",
|
||||
"TrackingQualificationResult",
|
||||
"TrackedFusionQualificationResult",
|
||||
"TELEMETRY_SCHEMA",
|
||||
"WORLD_STATE_SCHEMA",
|
||||
"WorldStateProjector",
|
||||
"classify_health",
|
||||
"validate_recorded_perception_result",
|
||||
"validate_recorded_evaluation_pack",
|
||||
"validate_recorded_qualification_slice",
|
||||
"validate_realtime_tracking_qualification_result",
|
||||
"validate_tracking_qualification_result",
|
||||
"validate_tracked_fusion_qualification_result",
|
||||
"validate_annotation_workspace",
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,777 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import stat
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from k1link.artifacts import utc_now_iso, write_json_atomic
|
||||
from k1link.device_plugins.xgrids_k1.analyze.valid_fov import (
|
||||
validate_k1_valid_fov_mask,
|
||||
)
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
|
||||
from .jobs import validate_camera_compute_job
|
||||
from .qualification import validate_recorded_qualification_slice
|
||||
|
||||
EVALUATION_PACK_SCHEMA = "missioncore.perception-evaluation-pack/v1"
|
||||
EVALUATION_PACK_IDENTITY_SCHEMA = "missioncore.perception-evaluation-pack-identity/v1"
|
||||
ANNOTATION_CONTRACT_SCHEMA = "missioncore.perception-annotation-contract/v1"
|
||||
ANNOTATION_TEMPLATE_SCHEMA = "missioncore.perception-annotation-template/v1"
|
||||
SELECTION_POLICY = "reviewed-anchors-plus-temporal-clips/v1"
|
||||
MIN_EVALUATION_FRAMES = 16
|
||||
MAX_EVALUATION_FRAMES = 128
|
||||
MAX_MANIFEST_BYTES = 32 * 1024 * 1024
|
||||
MAX_TIMELINE_LINE_BYTES = 16 * 1024
|
||||
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
_SAFE_GENERATION = re.compile(r"^evaluation-pack-[a-f0-9]{64}$")
|
||||
_SAFE_GROUP = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
|
||||
|
||||
|
||||
class RecordedEvaluationPackError(RuntimeError):
|
||||
"""A recorded-perception evaluation pack is invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EvaluationFrameRequest:
|
||||
frame_index: int
|
||||
role: str
|
||||
group_id: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EvaluationPackFrame:
|
||||
image_id: int
|
||||
frame_index: int
|
||||
sequence: int
|
||||
segment_sha256: str
|
||||
session_seconds: float
|
||||
role: str
|
||||
group_id: str
|
||||
raw_path: Path
|
||||
valid_fov_fill_path: Path
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RecordedEvaluationPack:
|
||||
generation_id: str
|
||||
root: Path
|
||||
manifest_path: Path
|
||||
annotation_contract_path: Path
|
||||
annotation_template_path: Path
|
||||
job_id: str
|
||||
input_sha256: str
|
||||
qualification_generation_id: str
|
||||
valid_fov_generation_id: str
|
||||
calibration_sha256: str
|
||||
frames: tuple[EvaluationPackFrame, ...]
|
||||
|
||||
|
||||
def prepare_recorded_evaluation_pack(
|
||||
*,
|
||||
job_root: Path,
|
||||
qualification_root: Path,
|
||||
valid_fov_root: Path,
|
||||
decoded_frames_root: Path,
|
||||
timeline_path: Path,
|
||||
output_root: Path,
|
||||
selection: tuple[EvaluationFrameRequest, ...],
|
||||
decoder_version: str,
|
||||
selection_document_sha256: str,
|
||||
producer_files: tuple[tuple[str, str], ...],
|
||||
) -> RecordedEvaluationPack:
|
||||
"""Seal exact raw/fixed-fill frames and an annotation contract for model A/B.
|
||||
|
||||
The pack is immutable. Human annotations are authored in a separate working
|
||||
copy of ``annotation-template.json`` and later sealed as their own generation.
|
||||
This keeps source imagery and the evolving review state from being conflated.
|
||||
"""
|
||||
|
||||
job = validate_camera_compute_job(job_root)
|
||||
qualification = validate_recorded_qualification_slice(
|
||||
qualification_root,
|
||||
job_root=job.job_root,
|
||||
)
|
||||
valid_fov = validate_k1_valid_fov_mask(valid_fov_root)
|
||||
if not isinstance(decoder_version, str) or not 1 <= len(decoder_version) <= 256:
|
||||
raise RecordedEvaluationPackError("decoder version is invalid")
|
||||
if _SHA256.fullmatch(selection_document_sha256) is None:
|
||||
raise RecordedEvaluationPackError("selection document SHA-256 is invalid")
|
||||
producer_identity = _validate_producer_files(producer_files)
|
||||
if (
|
||||
valid_fov.source_id != job.source_id
|
||||
or valid_fov.width != 800
|
||||
or valid_fov.height != 600
|
||||
):
|
||||
raise RecordedEvaluationPackError("valid-FOV does not match the camera job")
|
||||
selected = _validate_selection(selection, job.segment_count, qualification)
|
||||
timestamps = _read_timeline(
|
||||
timeline_path,
|
||||
job.segment_count,
|
||||
job.timeline_start_seconds,
|
||||
job.timeline_end_seconds,
|
||||
)
|
||||
frames_root = decoded_frames_root.expanduser().resolve(strict=True)
|
||||
if not frames_root.is_dir():
|
||||
raise RecordedEvaluationPackError("decoded frame root is not a directory")
|
||||
with Image.open(valid_fov.mask_path) as opened_mask:
|
||||
mask = opened_mask.copy()
|
||||
if mask.mode != "L" or mask.size != (valid_fov.width, valid_fov.height):
|
||||
raise RecordedEvaluationPackError("valid-FOV mask cannot be applied to the frames")
|
||||
|
||||
source_by_index = {frame.frame_index: frame for frame in qualification.frames}
|
||||
job_index = _read_job_index(job.job_root, job.source_id, job.codec_epoch)
|
||||
decoded: list[dict[str, Any]] = []
|
||||
for image_id, request in enumerate(selected, start=1):
|
||||
source = source_by_index.get(request.frame_index)
|
||||
row = job_index[request.frame_index]
|
||||
sequence = _nonnegative_integer(row.get("sequence"), "segment sequence")
|
||||
segment_sha256 = row.get("sha256")
|
||||
if (
|
||||
sequence != request.frame_index + 1
|
||||
or not isinstance(segment_sha256, str)
|
||||
or _SHA256.fullmatch(segment_sha256) is None
|
||||
or (source is not None and source.segment_sha256 != segment_sha256)
|
||||
):
|
||||
raise RecordedEvaluationPackError("selected frame identity changed")
|
||||
source_path = frames_root / f"frame-{request.frame_index:06d}.png"
|
||||
_confined_regular_file(source_path, frames_root)
|
||||
try:
|
||||
with Image.open(source_path) as opened:
|
||||
raw = opened.convert("RGB")
|
||||
except OSError as exc:
|
||||
raise RecordedEvaluationPackError("decoded evaluation frame is unavailable") from exc
|
||||
if raw.size != (valid_fov.width, valid_fov.height):
|
||||
raise RecordedEvaluationPackError("decoded evaluation frame dimensions changed")
|
||||
fill = Image.composite(raw, Image.new("RGB", raw.size, (0, 0, 0)), mask)
|
||||
decoded.append(
|
||||
{
|
||||
"image_id": image_id,
|
||||
"request": request,
|
||||
"sequence": sequence,
|
||||
"segment_sha256": segment_sha256,
|
||||
"session_seconds": timestamps[request.frame_index],
|
||||
"raw": raw,
|
||||
"fill": fill,
|
||||
"raw_rgb_sha256": _pixel_sha256(raw),
|
||||
"fill_rgb_sha256": _pixel_sha256(fill),
|
||||
}
|
||||
)
|
||||
|
||||
identity = {
|
||||
"schema_version": EVALUATION_PACK_IDENTITY_SCHEMA,
|
||||
"job_id": job.job_id,
|
||||
"input_sha256": job.input_sha256,
|
||||
"session_id": job.session_id,
|
||||
"source_id": job.source_id,
|
||||
"codec_epoch": job.codec_epoch,
|
||||
"qualification_generation_id": qualification.generation_id,
|
||||
"qualification_policy": qualification.policy,
|
||||
"valid_fov_generation_id": valid_fov.generation_id,
|
||||
"calibration_sha256": valid_fov.calibration_sha256,
|
||||
"calibration_slot": valid_fov.calibration_slot,
|
||||
"selection_policy": SELECTION_POLICY,
|
||||
"selection_document_sha256": selection_document_sha256,
|
||||
"preprocessing_profile": "fixed-valid-fov-fill/v1",
|
||||
"decoder_version": decoder_version,
|
||||
"producer_files": producer_identity,
|
||||
"resolution": [valid_fov.width, valid_fov.height],
|
||||
"frames": [
|
||||
{
|
||||
"image_id": item["image_id"],
|
||||
"frame_index": item["request"].frame_index,
|
||||
"sequence": item["sequence"],
|
||||
"segment_sha256": item["segment_sha256"],
|
||||
"session_seconds": item["session_seconds"],
|
||||
"role": item["request"].role,
|
||||
"group_id": item["request"].group_id,
|
||||
"raw_rgb_sha256": item["raw_rgb_sha256"],
|
||||
"valid_fov_fill_rgb_sha256": item["fill_rgb_sha256"],
|
||||
}
|
||||
for item in decoded
|
||||
],
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
generation_id = f"evaluation-pack-{identity_sha256}"
|
||||
root = _prepare_private_directory(output_root)
|
||||
final = root / generation_id
|
||||
if final.exists():
|
||||
existing = validate_recorded_evaluation_pack(
|
||||
final,
|
||||
job_root=job.job_root,
|
||||
qualification_root=qualification.root,
|
||||
valid_fov_root=valid_fov.root,
|
||||
)
|
||||
if existing.input_sha256 != job.input_sha256:
|
||||
raise RecordedEvaluationPackError("evaluation generation collides with another input")
|
||||
return existing
|
||||
|
||||
staging = root / f".{generation_id}.{secrets.token_hex(12)}.incomplete"
|
||||
published = False
|
||||
try:
|
||||
(staging / "images" / "raw").mkdir(mode=0o700, parents=True)
|
||||
(staging / "images" / "valid-fov-fill").mkdir(mode=0o700, parents=True)
|
||||
artifacts: list[dict[str, Any]] = []
|
||||
for item in decoded:
|
||||
image_id = int(item["image_id"])
|
||||
frame_index = item["request"].frame_index
|
||||
name = f"image-{image_id:03d}-frame-{frame_index:06d}.png"
|
||||
raw_path = staging / "images" / "raw" / name
|
||||
fill_path = staging / "images" / "valid-fov-fill" / name
|
||||
item["raw"].save(raw_path, format="PNG", optimize=False)
|
||||
item["fill"].save(fill_path, format="PNG", optimize=False)
|
||||
_fsync_file(raw_path)
|
||||
_fsync_file(fill_path)
|
||||
os.chmod(raw_path, 0o600)
|
||||
os.chmod(fill_path, 0o600)
|
||||
artifacts.extend((_artifact(raw_path, staging), _artifact(fill_path, staging)))
|
||||
|
||||
contract = _annotation_contract(identity_sha256)
|
||||
template = _annotation_template(generation_id, identity["frames"])
|
||||
contract_path = staging / "annotation-contract.json"
|
||||
template_path = staging / "annotation-template.json"
|
||||
write_json_atomic(contract_path, contract)
|
||||
write_json_atomic(template_path, template)
|
||||
os.chmod(contract_path, 0o600)
|
||||
os.chmod(template_path, 0o600)
|
||||
artifacts.extend((_artifact(contract_path, staging), _artifact(template_path, staging)))
|
||||
manifest = {
|
||||
"schema_version": EVALUATION_PACK_SCHEMA,
|
||||
"generation_id": generation_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": utc_now_iso(),
|
||||
"artifacts": artifacts,
|
||||
"annotation_state": {
|
||||
"state": "unannotated",
|
||||
"mutable_inside_pack": False,
|
||||
"workflow": (
|
||||
"copy annotation-template.json to a review workspace; seal reviewed "
|
||||
"annotations as a separate content-addressed ground-truth generation"
|
||||
),
|
||||
},
|
||||
}
|
||||
write_json_atomic(staging / "manifest.json", manifest)
|
||||
os.chmod(staging / "manifest.json", 0o600)
|
||||
_fsync_tree(staging)
|
||||
os.replace(staging, final)
|
||||
_fsync_directory(root)
|
||||
published = True
|
||||
finally:
|
||||
if not published and staging.exists():
|
||||
shutil.rmtree(staging)
|
||||
|
||||
return validate_recorded_evaluation_pack(
|
||||
final,
|
||||
job_root=job.job_root,
|
||||
qualification_root=qualification.root,
|
||||
valid_fov_root=valid_fov.root,
|
||||
)
|
||||
|
||||
|
||||
def validate_recorded_evaluation_pack(
|
||||
pack_root: Path,
|
||||
*,
|
||||
job_root: Path,
|
||||
qualification_root: Path,
|
||||
valid_fov_root: Path,
|
||||
) -> RecordedEvaluationPack:
|
||||
root = pack_root.expanduser().resolve(strict=True)
|
||||
if not root.is_dir() or _SAFE_GENERATION.fullmatch(root.name) is None:
|
||||
raise RecordedEvaluationPackError("evaluation pack root is invalid")
|
||||
manifest = _read_json_object(root / "manifest.json", root, MAX_MANIFEST_BYTES)
|
||||
identity = manifest.get("identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != EVALUATION_PACK_SCHEMA
|
||||
or manifest.get("generation_id") != root.name
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("schema_version") != EVALUATION_PACK_IDENTITY_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or _SHA256.fullmatch(identity_sha256) is None
|
||||
or root.name != f"evaluation-pack-{identity_sha256}"
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
):
|
||||
raise RecordedEvaluationPackError("evaluation pack identity is inconsistent")
|
||||
job = validate_camera_compute_job(job_root)
|
||||
qualification = validate_recorded_qualification_slice(
|
||||
qualification_root,
|
||||
job_root=job.job_root,
|
||||
)
|
||||
valid_fov = validate_k1_valid_fov_mask(valid_fov_root)
|
||||
if (
|
||||
identity.get("job_id") != job.job_id
|
||||
or identity.get("input_sha256") != job.input_sha256
|
||||
or identity.get("session_id") != job.session_id
|
||||
or identity.get("source_id") != job.source_id
|
||||
or identity.get("codec_epoch") != job.codec_epoch
|
||||
or identity.get("qualification_generation_id") != qualification.generation_id
|
||||
or identity.get("qualification_policy") != qualification.policy
|
||||
or identity.get("valid_fov_generation_id") != valid_fov.generation_id
|
||||
or identity.get("calibration_sha256") != valid_fov.calibration_sha256
|
||||
or identity.get("calibration_slot") != valid_fov.calibration_slot
|
||||
or identity.get("selection_policy") != SELECTION_POLICY
|
||||
or not isinstance(identity.get("selection_document_sha256"), str)
|
||||
or _SHA256.fullmatch(str(identity["selection_document_sha256"])) is None
|
||||
or identity.get("preprocessing_profile") != "fixed-valid-fov-fill/v1"
|
||||
or not isinstance(identity.get("decoder_version"), str)
|
||||
or not 1 <= len(str(identity["decoder_version"])) <= 256
|
||||
or identity.get("resolution") != [valid_fov.width, valid_fov.height]
|
||||
):
|
||||
raise RecordedEvaluationPackError("evaluation pack bindings changed")
|
||||
producer_files = identity.get("producer_files")
|
||||
if not isinstance(producer_files, list):
|
||||
raise RecordedEvaluationPackError("evaluation producer identity is unavailable")
|
||||
normalized_producer = _validate_producer_files(
|
||||
tuple(
|
||||
(str(row.get("name")), str(row.get("sha256")))
|
||||
for row in producer_files
|
||||
if isinstance(row, dict)
|
||||
)
|
||||
)
|
||||
if producer_files != normalized_producer:
|
||||
raise RecordedEvaluationPackError("evaluation producer identity changed")
|
||||
rows = identity.get("frames")
|
||||
if (
|
||||
not isinstance(rows, list)
|
||||
or not MIN_EVALUATION_FRAMES <= len(rows) <= MAX_EVALUATION_FRAMES
|
||||
):
|
||||
raise RecordedEvaluationPackError("evaluation pack frame set is invalid")
|
||||
artifact_rows = manifest.get("artifacts")
|
||||
if not isinstance(artifact_rows, list) or len(artifact_rows) != len(rows) * 2 + 2:
|
||||
raise RecordedEvaluationPackError("evaluation pack artifact set is invalid")
|
||||
artifacts: dict[str, dict[str, Any]] = {}
|
||||
for artifact in artifact_rows:
|
||||
if not isinstance(artifact, dict) or not isinstance(artifact.get("path"), str):
|
||||
raise RecordedEvaluationPackError("evaluation artifact descriptor is invalid")
|
||||
path_text = str(artifact["path"])
|
||||
if path_text in artifacts:
|
||||
raise RecordedEvaluationPackError("evaluation artifact is duplicated")
|
||||
path = root / path_text
|
||||
metadata = _confined_regular_file(path, root)
|
||||
digest = artifact.get("sha256")
|
||||
if (
|
||||
artifact.get("byte_length") != metadata.st_size
|
||||
or not isinstance(digest, str)
|
||||
or _SHA256.fullmatch(digest) is None
|
||||
or _sha256_file(path) != digest
|
||||
):
|
||||
raise RecordedEvaluationPackError("evaluation artifact changed")
|
||||
artifacts[path_text] = artifact
|
||||
contract_path = root / "annotation-contract.json"
|
||||
template_path = root / "annotation-template.json"
|
||||
contract = _read_json_object(contract_path, root, MAX_MANIFEST_BYTES)
|
||||
template = _read_json_object(template_path, root, MAX_MANIFEST_BYTES)
|
||||
if (
|
||||
contract.get("schema_version") != ANNOTATION_CONTRACT_SCHEMA
|
||||
or contract.get("evaluation_identity_sha256") != identity_sha256
|
||||
or template.get("schema_version") != ANNOTATION_TEMPLATE_SCHEMA
|
||||
or template.get("evaluation_pack_id") != root.name
|
||||
or template.get("images") != rows
|
||||
or "annotation-contract.json" not in artifacts
|
||||
or "annotation-template.json" not in artifacts
|
||||
):
|
||||
raise RecordedEvaluationPackError("annotation contract changed")
|
||||
|
||||
qualification_indices = {frame.frame_index for frame in qualification.frames}
|
||||
job_index = _read_job_index(job.job_root, job.source_id, job.codec_epoch)
|
||||
with Image.open(valid_fov.mask_path) as opened_mask:
|
||||
validation_mask = opened_mask.copy()
|
||||
frames: list[EvaluationPackFrame] = []
|
||||
previous_index = -1
|
||||
temporal_groups: dict[str, list[int]] = {}
|
||||
for position, row in enumerate(rows, start=1):
|
||||
if not isinstance(row, dict):
|
||||
raise RecordedEvaluationPackError("evaluation frame descriptor is invalid")
|
||||
frame_index = row.get("frame_index")
|
||||
image_id = row.get("image_id")
|
||||
role = row.get("role")
|
||||
group_id = row.get("group_id")
|
||||
session_seconds = row.get("session_seconds")
|
||||
if (
|
||||
image_id != position
|
||||
or not isinstance(frame_index, int)
|
||||
or isinstance(frame_index, bool)
|
||||
or not previous_index < frame_index < job.segment_count
|
||||
or role not in {"anchor", "temporal"}
|
||||
or not isinstance(group_id, str)
|
||||
or _SAFE_GROUP.fullmatch(group_id) is None
|
||||
or not isinstance(session_seconds, (int, float))
|
||||
or isinstance(session_seconds, bool)
|
||||
or not math.isfinite(float(session_seconds))
|
||||
or not job.timeline_start_seconds <= float(session_seconds) <= job.timeline_end_seconds
|
||||
):
|
||||
raise RecordedEvaluationPackError("evaluation frame ordering is invalid")
|
||||
if role == "anchor" and frame_index not in qualification_indices:
|
||||
raise RecordedEvaluationPackError("anchor escaped the qualification slice")
|
||||
if role == "temporal":
|
||||
temporal_groups.setdefault(group_id, []).append(frame_index)
|
||||
name = f"image-{image_id:03d}-frame-{frame_index:06d}.png"
|
||||
raw_relative = f"images/raw/{name}"
|
||||
fill_relative = f"images/valid-fov-fill/{name}"
|
||||
if raw_relative not in artifacts or fill_relative not in artifacts:
|
||||
raise RecordedEvaluationPackError("evaluation image artifacts are incomplete")
|
||||
raw_path = root / raw_relative
|
||||
fill_path = root / fill_relative
|
||||
with Image.open(raw_path) as raw_opened, Image.open(fill_path) as fill_opened:
|
||||
raw = raw_opened.convert("RGB")
|
||||
fill = fill_opened.convert("RGB")
|
||||
expected_fill = Image.composite(
|
||||
raw,
|
||||
Image.new("RGB", raw.size, (0, 0, 0)),
|
||||
validation_mask,
|
||||
)
|
||||
expected_segment_sha256 = job_index[frame_index].get("sha256")
|
||||
if (
|
||||
raw.size != (valid_fov.width, valid_fov.height)
|
||||
or fill.size != raw.size
|
||||
or _pixel_sha256(raw) != row.get("raw_rgb_sha256")
|
||||
or _pixel_sha256(fill) != row.get("valid_fov_fill_rgb_sha256")
|
||||
or _pixel_sha256(expected_fill) != row.get("valid_fov_fill_rgb_sha256")
|
||||
or not isinstance(row.get("segment_sha256"), str)
|
||||
or _SHA256.fullmatch(str(row["segment_sha256"])) is None
|
||||
or row.get("segment_sha256") != expected_segment_sha256
|
||||
or row.get("sequence") != frame_index + 1
|
||||
):
|
||||
raise RecordedEvaluationPackError("evaluation frame pixels or identity changed")
|
||||
frames.append(
|
||||
EvaluationPackFrame(
|
||||
image_id=image_id,
|
||||
frame_index=frame_index,
|
||||
sequence=frame_index + 1,
|
||||
segment_sha256=str(row["segment_sha256"]),
|
||||
session_seconds=float(session_seconds),
|
||||
role=role,
|
||||
group_id=group_id,
|
||||
raw_path=raw_path,
|
||||
valid_fov_fill_path=fill_path,
|
||||
)
|
||||
)
|
||||
previous_index = frame_index
|
||||
for group in temporal_groups.values():
|
||||
if len(group) < 3 or any(
|
||||
right != left + 1 for left, right in zip(group, group[1:], strict=False)
|
||||
):
|
||||
raise RecordedEvaluationPackError("temporal group is not a consecutive clip")
|
||||
if len(temporal_groups) < 2:
|
||||
raise RecordedEvaluationPackError("evaluation pack lacks temporal coverage")
|
||||
return RecordedEvaluationPack(
|
||||
generation_id=root.name,
|
||||
root=root,
|
||||
manifest_path=root / "manifest.json",
|
||||
annotation_contract_path=contract_path,
|
||||
annotation_template_path=template_path,
|
||||
job_id=job.job_id,
|
||||
input_sha256=job.input_sha256,
|
||||
qualification_generation_id=qualification.generation_id,
|
||||
valid_fov_generation_id=valid_fov.generation_id,
|
||||
calibration_sha256=valid_fov.calibration_sha256,
|
||||
frames=tuple(frames),
|
||||
)
|
||||
|
||||
|
||||
def _validate_selection(
|
||||
selection: tuple[EvaluationFrameRequest, ...],
|
||||
source_count: int,
|
||||
qualification: Any,
|
||||
) -> tuple[EvaluationFrameRequest, ...]:
|
||||
if not MIN_EVALUATION_FRAMES <= len(selection) <= MAX_EVALUATION_FRAMES:
|
||||
raise RecordedEvaluationPackError("evaluation selection size is outside bounds")
|
||||
qualification_indices = {frame.frame_index for frame in qualification.frames}
|
||||
previous = -1
|
||||
temporal_groups: dict[str, list[int]] = {}
|
||||
anchors = 0
|
||||
for request in selection:
|
||||
if (
|
||||
not isinstance(request, EvaluationFrameRequest)
|
||||
or not isinstance(request.frame_index, int)
|
||||
or isinstance(request.frame_index, bool)
|
||||
or not previous < request.frame_index < source_count
|
||||
or request.role not in {"anchor", "temporal"}
|
||||
or _SAFE_GROUP.fullmatch(request.group_id) is None
|
||||
):
|
||||
raise RecordedEvaluationPackError("evaluation selection is invalid")
|
||||
if request.role == "anchor":
|
||||
if request.frame_index not in qualification_indices:
|
||||
raise RecordedEvaluationPackError("anchor is outside the qualification slice")
|
||||
anchors += 1
|
||||
else:
|
||||
temporal_groups.setdefault(request.group_id, []).append(request.frame_index)
|
||||
previous = request.frame_index
|
||||
if anchors < MIN_EVALUATION_FRAMES:
|
||||
raise RecordedEvaluationPackError("evaluation selection has too few anchors")
|
||||
if len(temporal_groups) < 2:
|
||||
raise RecordedEvaluationPackError("evaluation selection needs at least two temporal clips")
|
||||
for group in temporal_groups.values():
|
||||
if len(group) < 3 or any(
|
||||
right != left + 1 for left, right in zip(group, group[1:], strict=False)
|
||||
):
|
||||
raise RecordedEvaluationPackError("temporal clip frames must be consecutive")
|
||||
return selection
|
||||
|
||||
|
||||
def _annotation_contract(evaluation_identity_sha256: str) -> dict[str, Any]:
|
||||
categories = [
|
||||
(1, "person", "thing"),
|
||||
(2, "bicycle", "thing"),
|
||||
(3, "motorcycle", "thing"),
|
||||
(4, "car", "thing"),
|
||||
(5, "heavy_vehicle", "thing"),
|
||||
(6, "building_structure", "stuff"),
|
||||
(7, "paved_road", "stuff"),
|
||||
(8, "sidewalk_curb", "stuff"),
|
||||
(9, "ground_dirt", "stuff"),
|
||||
(10, "grass_low_vegetation", "stuff"),
|
||||
(11, "tree_woody_vegetation", "stuff"),
|
||||
(12, "sky", "stuff"),
|
||||
(13, "static_obstacle", "thing"),
|
||||
(14, "animal", "thing"),
|
||||
(15, "other_background", "stuff"),
|
||||
]
|
||||
return {
|
||||
"schema_version": ANNOTATION_CONTRACT_SCHEMA,
|
||||
"evaluation_identity_sha256": evaluation_identity_sha256,
|
||||
"task": "2d-panoptic-and-instance-qualification",
|
||||
"categories": [
|
||||
{"id": category_id, "name": name, "kind": kind}
|
||||
for category_id, name, kind in categories
|
||||
],
|
||||
"label_map": {
|
||||
"outside_valid_fov": 0,
|
||||
"ambiguous_or_unresolvable": 255,
|
||||
"metric_category_ids": [category_id for category_id, _name, _kind in categories],
|
||||
},
|
||||
"policy": {
|
||||
"coverage": "label every resolvable pixel inside the fixed valid-FOV mask",
|
||||
"instances": (
|
||||
"annotate every identifiable thing instance; preserve occluded and truncated "
|
||||
"flags; use ambiguous=255 only when class or boundary cannot be resolved"
|
||||
),
|
||||
"lens_exterior": "must remain label 0 and is excluded from all accuracy metrics",
|
||||
"prelabels": (
|
||||
"model-assisted prelabels are permitted only as drafts; every accepted object "
|
||||
"and semantic region requires human review"
|
||||
),
|
||||
"review": "two-pass review: annotator complete, then reviewer accepted",
|
||||
},
|
||||
"required_metrics": {
|
||||
"semantic": ["per_class_iou", "macro_miou_present", "frequency_weighted_iou"],
|
||||
"instance": ["ap_50_95", "ap50", "ap75", "ar100", "per_class_recall"],
|
||||
"safety_proxies": [
|
||||
"person_vehicle_miss_rate",
|
||||
"false_large_instance_rate",
|
||||
"valid_fov_boundary_leakage",
|
||||
],
|
||||
"temporal": ["class_flicker_rate", "instance_id_switches", "mask_iou_jitter"],
|
||||
},
|
||||
"acceptance": {
|
||||
"unreviewed_frames": 0,
|
||||
"pixels_outside_valid_fov_nonzero": 0,
|
||||
"invalid_category_pixels": 0,
|
||||
"duplicate_instance_ids_per_frame": 0,
|
||||
"metric_reporting": "mean, p50, p95 and worst-frame identities where applicable",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _validate_producer_files(files: tuple[tuple[str, str], ...]) -> list[dict[str, str]]:
|
||||
if not 1 <= len(files) <= 8:
|
||||
raise RecordedEvaluationPackError("evaluation producer file set is invalid")
|
||||
normalized: list[dict[str, str]] = []
|
||||
seen: set[str] = set()
|
||||
for name, digest in files:
|
||||
if (
|
||||
not isinstance(name, str)
|
||||
or _SAFE_GROUP.fullmatch(name) is None
|
||||
or name in seen
|
||||
or not isinstance(digest, str)
|
||||
or _SHA256.fullmatch(digest) is None
|
||||
):
|
||||
raise RecordedEvaluationPackError("evaluation producer file identity is invalid")
|
||||
normalized.append({"name": name, "sha256": digest})
|
||||
seen.add(name)
|
||||
return normalized
|
||||
|
||||
|
||||
def _annotation_template(generation_id: str, frames: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": ANNOTATION_TEMPLATE_SCHEMA,
|
||||
"evaluation_pack_id": generation_id,
|
||||
"state": "unannotated",
|
||||
"images": frames,
|
||||
"semantic_masks": [],
|
||||
"instances": [],
|
||||
"reviews": [
|
||||
{
|
||||
"image_id": row["image_id"],
|
||||
"annotation_status": "unannotated",
|
||||
"review_status": "unreviewed",
|
||||
"notes": "",
|
||||
}
|
||||
for row in frames
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _read_timeline(path: Path, count: int, start: float, end: float) -> list[float]:
|
||||
root = path.expanduser().resolve(strict=True).parent
|
||||
resolved = path.expanduser().resolve(strict=True)
|
||||
_confined_regular_file(resolved, root)
|
||||
values: list[float] = []
|
||||
try:
|
||||
with resolved.open("r", encoding="utf-8") as stream:
|
||||
for expected, line in enumerate(stream):
|
||||
if len(line.encode("utf-8")) > MAX_TIMELINE_LINE_BYTES:
|
||||
raise RecordedEvaluationPackError("decoded timeline row is too large")
|
||||
row = json.loads(line)
|
||||
value = row.get("session_seconds") if isinstance(row, dict) else None
|
||||
if row.get("frame_index") != expected or not isinstance(value, (int, float)):
|
||||
raise RecordedEvaluationPackError("decoded timeline order changed")
|
||||
timestamp = float(value)
|
||||
if not math.isfinite(timestamp) or not start <= timestamp <= end:
|
||||
raise RecordedEvaluationPackError("decoded timeline escapes the job")
|
||||
if values and timestamp <= values[-1]:
|
||||
raise RecordedEvaluationPackError("decoded timeline is not monotonic")
|
||||
values.append(timestamp)
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError, AttributeError) as exc:
|
||||
raise RecordedEvaluationPackError("decoded timeline is unavailable") from exc
|
||||
if len(values) != count:
|
||||
raise RecordedEvaluationPackError("decoded timeline frame count changed")
|
||||
return values
|
||||
|
||||
|
||||
def _read_job_index(job_root: Path, source_id: str, codec_epoch: int) -> list[dict[str, Any]]:
|
||||
path = job_root / "input" / "camera" / source_id / f"epoch-{codec_epoch}" / "index.jsonl"
|
||||
rows: list[dict[str, Any]] = []
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
for line in stream:
|
||||
row = json.loads(line)
|
||||
if not isinstance(row, dict):
|
||||
raise RecordedEvaluationPackError("camera index row is invalid")
|
||||
rows.append(row)
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise RecordedEvaluationPackError("camera index is unavailable") from exc
|
||||
if not rows:
|
||||
raise RecordedEvaluationPackError("camera index is empty")
|
||||
return rows
|
||||
|
||||
|
||||
def _nonnegative_integer(value: Any, label: str) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
||||
raise RecordedEvaluationPackError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _pixel_sha256(image: Image.Image) -> str:
|
||||
digest = hashlib.sha256()
|
||||
digest.update(image.width.to_bytes(4, "big"))
|
||||
digest.update(image.height.to_bytes(4, "big"))
|
||||
digest.update(image.mode.encode("ascii"))
|
||||
digest.update(b"\x00")
|
||||
digest.update(image.tobytes())
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _artifact(path: Path, root: Path) -> dict[str, Any]:
|
||||
return {
|
||||
"path": path.relative_to(root).as_posix(),
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256_file(path),
|
||||
}
|
||||
|
||||
|
||||
def _prepare_private_directory(path: Path) -> Path:
|
||||
candidate = path.expanduser()
|
||||
candidate.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
metadata = candidate.lstat()
|
||||
if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
|
||||
raise RecordedEvaluationPackError("evaluation output root must be a real directory")
|
||||
root = candidate.resolve(strict=True)
|
||||
os.chmod(root, 0o700)
|
||||
return root
|
||||
|
||||
|
||||
def _read_json_object(path: Path, root: Path, maximum_bytes: int) -> dict[str, Any]:
|
||||
metadata = _confined_regular_file(path, root)
|
||||
if not 0 < metadata.st_size <= maximum_bytes:
|
||||
raise RecordedEvaluationPackError("evaluation JSON is outside bounds")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise RecordedEvaluationPackError("evaluation JSON is unavailable") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise RecordedEvaluationPackError("evaluation JSON is not an object")
|
||||
return value
|
||||
|
||||
|
||||
def _confined_regular_file(path: Path, root: Path) -> os.stat_result:
|
||||
try:
|
||||
resolved_root = root.resolve(strict=True)
|
||||
resolved = path.resolve(strict=True)
|
||||
metadata = path.lstat()
|
||||
except OSError as exc:
|
||||
raise RecordedEvaluationPackError("evaluation artifact is unavailable") from exc
|
||||
if (
|
||||
stat.S_ISLNK(metadata.st_mode)
|
||||
or not stat.S_ISREG(metadata.st_mode)
|
||||
or not resolved.is_relative_to(resolved_root)
|
||||
):
|
||||
raise RecordedEvaluationPackError("evaluation artifact is not confined")
|
||||
return metadata
|
||||
|
||||
|
||||
def _sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
try:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise SessionIntegrityError("evaluation identity is not canonical JSON") from exc
|
||||
|
||||
|
||||
def _fsync_file(path: Path) -> None:
|
||||
with path.open("rb") as stream:
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
||||
def _fsync_tree(root: Path) -> None:
|
||||
for path in sorted(root.rglob("*"), reverse=True):
|
||||
if path.is_file():
|
||||
_fsync_file(path)
|
||||
elif path.is_dir():
|
||||
_fsync_directory(path)
|
||||
_fsync_directory(root)
|
||||
|
||||
|
||||
def _fsync_directory(path: Path) -> None:
|
||||
flags = os.O_RDONLY
|
||||
if hasattr(os, "O_DIRECTORY"):
|
||||
flags |= os.O_DIRECTORY
|
||||
descriptor = os.open(path, flags)
|
||||
try:
|
||||
os.fsync(descriptor)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
@@ -0,0 +1,542 @@
|
||||
"""Validated calibrated 3D fusion results projected into an opened Rerun recording."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import threading
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Protocol
|
||||
|
||||
import numpy as np
|
||||
import rerun as rr
|
||||
from rerun.components import FillMode
|
||||
|
||||
from k1link.artifacts import write_json_atomic
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
|
||||
from .jobs import CameraComputeJob, validate_camera_compute_job
|
||||
from .perception_epoch import validate_recorded_perception_epoch_result
|
||||
from .results import RecordedPerceptionOverlayError
|
||||
|
||||
FUSION_SCHEMA = "missioncore.recorded-calibrated-fusion/v1"
|
||||
FUSION_IDENTITY_SCHEMA = "missioncore.recorded-calibrated-fusion-identity/v1"
|
||||
SESSION_TIMELINE = "session_time"
|
||||
MAX_JSON_BYTES = 64 * 1024 * 1024
|
||||
MAX_SCAN = 512
|
||||
MAX_POINTS_PER_FRAME = 100_000
|
||||
MAX_BOXES_PER_FRAME = 10_000
|
||||
|
||||
_SAFE_FUSION_ID = re.compile(r"^fusion-[a-f0-9]{64}$")
|
||||
_SAFE_RECORDING_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RecordedCalibratedFusion:
|
||||
fusion_id: str
|
||||
root: Path
|
||||
job: CameraComputeJob
|
||||
perception_result_id: str
|
||||
created_at_utc: str
|
||||
arrays_path: Path
|
||||
labels_path: Path
|
||||
frame_count: int
|
||||
|
||||
|
||||
class _OverlayProvider(Protocol):
|
||||
def render(
|
||||
self,
|
||||
session_id: str,
|
||||
*,
|
||||
application_id: str,
|
||||
recording_id: str,
|
||||
) -> bytes | None: ...
|
||||
|
||||
|
||||
class RecordedCalibratedFusionStore:
|
||||
"""Discover full-epoch fusion and serialize it for the opened recording ID."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
jobs_root: Path,
|
||||
perception_results_root: Path,
|
||||
fusion_results_root: Path,
|
||||
cache_root: Path,
|
||||
) -> None:
|
||||
self.jobs_root = jobs_root.expanduser().absolute()
|
||||
self.perception_results_root = perception_results_root.expanduser().absolute()
|
||||
self.fusion_results_root = fusion_results_root.expanduser().absolute()
|
||||
self.cache_root = cache_root.expanduser().absolute()
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def render(
|
||||
self,
|
||||
session_id: str,
|
||||
*,
|
||||
application_id: str,
|
||||
recording_id: str,
|
||||
) -> bytes | None:
|
||||
if _SAFE_RECORDING_ID.fullmatch(session_id) is None:
|
||||
raise ValueError("observation session id is invalid")
|
||||
if application_id != "nodedc_mission_core_recorded":
|
||||
raise ValueError("recorded fusion application id is invalid")
|
||||
if _SAFE_RECORDING_ID.fullmatch(recording_id) is None:
|
||||
raise ValueError("recorded fusion recording id is invalid")
|
||||
with self._lock:
|
||||
fusion = self._latest(session_id)
|
||||
if fusion is None:
|
||||
return None
|
||||
cache_root = _private_directory(self.cache_root)
|
||||
session_cache = _private_child_directory(cache_root, session_id)
|
||||
fusion_cache = _private_child_directory(session_cache, fusion.fusion_id)
|
||||
output = fusion_cache / f"{recording_id}.rrd"
|
||||
sidecar = output.with_suffix(".rrd.cache.json")
|
||||
cached = _read_cache(output, sidecar, fusion, recording_id)
|
||||
if cached is not None:
|
||||
return cached
|
||||
payload = _render_fusion(
|
||||
fusion,
|
||||
application_id=application_id,
|
||||
recording_id=recording_id,
|
||||
)
|
||||
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.calibrated-fusion-overlay-cache/v1",
|
||||
"fusion_id": fusion.fusion_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) -> RecordedCalibratedFusion | None:
|
||||
try:
|
||||
jobs = sorted(self.jobs_root.iterdir())
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
if len(jobs) > MAX_SCAN:
|
||||
raise RecordedPerceptionOverlayError("compute job catalog is outside bounds")
|
||||
matches: list[RecordedCalibratedFusion] = []
|
||||
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
|
||||
parent = self.fusion_results_root / job.job_id
|
||||
try:
|
||||
candidates = sorted(
|
||||
path
|
||||
for path in parent.iterdir()
|
||||
if path.is_dir()
|
||||
and not path.is_symlink()
|
||||
and _SAFE_FUSION_ID.fullmatch(path.name) is not None
|
||||
)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
if len(candidates) > MAX_SCAN:
|
||||
raise RecordedPerceptionOverlayError("fusion result catalog is outside bounds")
|
||||
for candidate in candidates:
|
||||
try:
|
||||
matches.append(
|
||||
validate_recorded_calibrated_fusion(
|
||||
job_root,
|
||||
self.perception_results_root,
|
||||
candidate,
|
||||
)
|
||||
)
|
||||
except (OSError, SessionIntegrityError):
|
||||
continue
|
||||
if not matches:
|
||||
return None
|
||||
return max(matches, key=lambda value: (value.created_at_utc, value.fusion_id))
|
||||
|
||||
|
||||
class RecordedPerceptionOverlayMux:
|
||||
"""Prefer calibrated full-epoch 3D fusion and retain the legacy fallback."""
|
||||
|
||||
def __init__(self, primary: _OverlayProvider, fallback: _OverlayProvider | None) -> None:
|
||||
self.primary = primary
|
||||
self.fallback = fallback
|
||||
|
||||
def render(
|
||||
self,
|
||||
session_id: str,
|
||||
*,
|
||||
application_id: str,
|
||||
recording_id: str,
|
||||
) -> bytes | None:
|
||||
payload = self.primary.render(
|
||||
session_id,
|
||||
application_id=application_id,
|
||||
recording_id=recording_id,
|
||||
)
|
||||
if payload is not None or self.fallback is None:
|
||||
return payload
|
||||
return self.fallback.render(
|
||||
session_id,
|
||||
application_id=application_id,
|
||||
recording_id=recording_id,
|
||||
)
|
||||
|
||||
|
||||
def validate_recorded_calibrated_fusion(
|
||||
job_root: Path,
|
||||
perception_results_root: Path,
|
||||
fusion_root: Path,
|
||||
) -> RecordedCalibratedFusion:
|
||||
job = validate_camera_compute_job(job_root)
|
||||
root = fusion_root.expanduser().resolve(strict=True)
|
||||
if not root.is_dir() or _SAFE_FUSION_ID.fullmatch(root.name) is None:
|
||||
raise SessionIntegrityError("calibrated fusion root is invalid")
|
||||
manifest = _read_json_object(root / "manifest.json", root)
|
||||
identity = manifest.get("identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != FUSION_SCHEMA
|
||||
or manifest.get("fusion_id") != root.name
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("schema_version") != FUSION_IDENTITY_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or _SHA256.fullmatch(identity_sha256) is None
|
||||
or root.name != f"fusion-{identity_sha256}"
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or identity.get("job_id") != job.job_id
|
||||
or identity.get("input_sha256") != job.input_sha256
|
||||
or manifest.get("session_id") != job.session_id
|
||||
or manifest.get("source_id") != job.source_id
|
||||
or manifest.get("frame_count") != job.segment_count
|
||||
or manifest.get("timeline_start_seconds") != job.timeline_start_seconds
|
||||
or manifest.get("timeline_end_seconds") != job.timeline_end_seconds
|
||||
):
|
||||
raise SessionIntegrityError("calibrated fusion identity is inconsistent")
|
||||
perception_result_id = identity.get("perception_result_id")
|
||||
if not isinstance(perception_result_id, str):
|
||||
raise SessionIntegrityError("calibrated fusion perception binding is invalid")
|
||||
perception = validate_recorded_perception_epoch_result(
|
||||
job_root,
|
||||
perception_results_root / job.job_id / perception_result_id,
|
||||
)
|
||||
if (
|
||||
identity.get("calibration_sha256") != perception.calibration_sha256
|
||||
or identity.get("camera_slot") != perception.calibration_slot
|
||||
):
|
||||
raise SessionIntegrityError("calibrated fusion calibration binding changed")
|
||||
artifacts = manifest.get("artifacts")
|
||||
expected = {"fusion.npz", "box-labels.json", "fusion-frames.jsonl"}
|
||||
if not isinstance(artifacts, list) or len(artifacts) != len(expected):
|
||||
raise SessionIntegrityError("calibrated fusion artifacts are incomplete")
|
||||
paths: dict[str, Path] = {}
|
||||
for artifact in artifacts:
|
||||
if not isinstance(artifact, dict) or artifact.get("name") not in expected:
|
||||
raise SessionIntegrityError("calibrated fusion artifact is invalid")
|
||||
name = str(artifact["name"])
|
||||
if name in paths:
|
||||
raise SessionIntegrityError("calibrated fusion artifact is duplicated")
|
||||
path = root / name
|
||||
metadata = _confined_regular_file(path, root)
|
||||
if (
|
||||
artifact.get("byte_length") != metadata.st_size
|
||||
or not isinstance(artifact.get("sha256"), str)
|
||||
or _SHA256.fullmatch(str(artifact["sha256"])) is None
|
||||
or _sha256(path) != artifact["sha256"]
|
||||
):
|
||||
raise SessionIntegrityError("calibrated fusion artifact identity changed")
|
||||
paths[name] = path
|
||||
labels = _read_labels(paths["box-labels.json"])
|
||||
_validate_arrays(paths["fusion.npz"], job, len(labels))
|
||||
created_at = manifest.get("created_at_utc")
|
||||
if not isinstance(created_at, str) or not 1 <= len(created_at) <= 64:
|
||||
raise SessionIntegrityError("calibrated fusion creation time is invalid")
|
||||
return RecordedCalibratedFusion(
|
||||
fusion_id=root.name,
|
||||
root=root,
|
||||
job=job,
|
||||
perception_result_id=perception_result_id,
|
||||
created_at_utc=created_at,
|
||||
arrays_path=paths["fusion.npz"],
|
||||
labels_path=paths["box-labels.json"],
|
||||
frame_count=job.segment_count,
|
||||
)
|
||||
|
||||
|
||||
def _validate_arrays(path: Path, job: CameraComputeJob, label_count: int) -> None:
|
||||
try:
|
||||
with np.load(path, allow_pickle=False) as arrays:
|
||||
required = {
|
||||
"frame_times_ns",
|
||||
"point_offsets",
|
||||
"points",
|
||||
"point_colors",
|
||||
"box_offsets",
|
||||
"box_centers",
|
||||
"box_half_sizes",
|
||||
"box_colors",
|
||||
}
|
||||
if set(arrays.files) != required:
|
||||
raise SessionIntegrityError("calibrated fusion array set is invalid")
|
||||
frame_times = arrays["frame_times_ns"]
|
||||
point_offsets = arrays["point_offsets"]
|
||||
points = arrays["points"]
|
||||
point_colors = arrays["point_colors"]
|
||||
box_offsets = arrays["box_offsets"]
|
||||
box_centers = arrays["box_centers"]
|
||||
box_half_sizes = arrays["box_half_sizes"]
|
||||
box_colors = arrays["box_colors"]
|
||||
if (
|
||||
frame_times.dtype != np.int64
|
||||
or frame_times.shape != (job.segment_count,)
|
||||
or point_offsets.dtype != np.int64
|
||||
or point_offsets.shape != (job.segment_count + 1,)
|
||||
or box_offsets.dtype != np.int64
|
||||
or box_offsets.shape != (job.segment_count + 1,)
|
||||
or points.dtype != np.float32
|
||||
or points.ndim != 2
|
||||
or points.shape[1:] != (3,)
|
||||
or point_colors.dtype != np.uint8
|
||||
or point_colors.shape != points.shape
|
||||
or box_centers.dtype != np.float32
|
||||
or box_centers.ndim != 2
|
||||
or box_centers.shape[1:] != (3,)
|
||||
or box_half_sizes.dtype != np.float32
|
||||
or box_half_sizes.shape != box_centers.shape
|
||||
or box_colors.dtype != np.uint8
|
||||
or box_colors.shape != (box_centers.shape[0], 4)
|
||||
or label_count != box_centers.shape[0]
|
||||
):
|
||||
raise SessionIntegrityError("calibrated fusion array shapes changed")
|
||||
_validate_offsets(point_offsets, points.shape[0], MAX_POINTS_PER_FRAME)
|
||||
_validate_offsets(box_offsets, box_centers.shape[0], MAX_BOXES_PER_FRAME)
|
||||
if (
|
||||
np.any(np.diff(frame_times) <= 0)
|
||||
or frame_times[0] < round(job.timeline_start_seconds * 1e9) - 1_000_000
|
||||
or frame_times[-1] > round(job.timeline_end_seconds * 1e9) + 1_000_000
|
||||
or not np.isfinite(points).all()
|
||||
or not np.isfinite(box_centers).all()
|
||||
or not np.isfinite(box_half_sizes).all()
|
||||
or np.any(box_half_sizes <= 0)
|
||||
):
|
||||
raise SessionIntegrityError("calibrated fusion arrays are inconsistent")
|
||||
except (OSError, ValueError, KeyError) as exc:
|
||||
raise SessionIntegrityError("calibrated fusion arrays are unavailable") from exc
|
||||
|
||||
|
||||
def _validate_offsets(
|
||||
offsets: np.ndarray[Any, np.dtype[np.int64]],
|
||||
total: int,
|
||||
maximum: int,
|
||||
) -> None:
|
||||
if (
|
||||
offsets[0] != 0
|
||||
or offsets[-1] != total
|
||||
or np.any(np.diff(offsets) < 0)
|
||||
or np.any(np.diff(offsets) > maximum)
|
||||
):
|
||||
raise SessionIntegrityError("calibrated fusion offsets are inconsistent")
|
||||
|
||||
|
||||
def _render_fusion(
|
||||
fusion: RecordedCalibratedFusion,
|
||||
*,
|
||||
application_id: str,
|
||||
recording_id: str,
|
||||
) -> bytes:
|
||||
labels = _read_labels(fusion.labels_path)
|
||||
recording = rr.RecordingStream(application_id, recording_id=recording_id)
|
||||
stream = rr.binary_stream(recording)
|
||||
try:
|
||||
recording.log("/world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True)
|
||||
recording.log(
|
||||
"/world/perception/contract",
|
||||
rr.TextDocument(
|
||||
"Factory-calibrated KB4 mask-to-LiDAR diagnostic. Distances and support-gated "
|
||||
"boxes are not ground-truthed or safety accepted."
|
||||
),
|
||||
static=True,
|
||||
)
|
||||
with np.load(fusion.arrays_path, allow_pickle=False) as arrays:
|
||||
frame_times = arrays["frame_times_ns"]
|
||||
point_offsets = arrays["point_offsets"]
|
||||
points = arrays["points"]
|
||||
point_colors = arrays["point_colors"]
|
||||
box_offsets = arrays["box_offsets"]
|
||||
centers = arrays["box_centers"]
|
||||
half_sizes = arrays["box_half_sizes"]
|
||||
box_colors = arrays["box_colors"]
|
||||
for index, timestamp in enumerate(frame_times):
|
||||
recording.set_time(
|
||||
SESSION_TIMELINE,
|
||||
duration=np.timedelta64(int(timestamp), "ns"),
|
||||
)
|
||||
point_start, point_end = int(point_offsets[index]), int(point_offsets[index + 1])
|
||||
if point_end > point_start:
|
||||
recording.log(
|
||||
"/world/perception/semantic_points",
|
||||
rr.Points3D(
|
||||
points[point_start:point_end],
|
||||
colors=point_colors[point_start:point_end],
|
||||
radii=rr.Radius.ui_points(2.5),
|
||||
),
|
||||
)
|
||||
else:
|
||||
recording.log(
|
||||
"/world/perception/semantic_points",
|
||||
rr.Clear(recursive=False),
|
||||
)
|
||||
box_start, box_end = int(box_offsets[index]), int(box_offsets[index + 1])
|
||||
if box_end > box_start:
|
||||
recording.log(
|
||||
"/world/perception/boxes3d",
|
||||
rr.Boxes3D(
|
||||
centers=centers[box_start:box_end],
|
||||
half_sizes=half_sizes[box_start:box_end],
|
||||
colors=box_colors[box_start:box_end],
|
||||
labels=labels[box_start:box_end],
|
||||
fill_mode=FillMode.Solid,
|
||||
show_labels=True,
|
||||
),
|
||||
)
|
||||
else:
|
||||
recording.log(
|
||||
"/world/perception/boxes3d",
|
||||
rr.Clear(recursive=False),
|
||||
)
|
||||
payload = stream.read(flush=True, flush_timeout_sec=300.0)
|
||||
except Exception as exc:
|
||||
raise RecordedPerceptionOverlayError("failed to serialize calibrated fusion") from exc
|
||||
finally:
|
||||
with suppress(Exception):
|
||||
recording.disconnect()
|
||||
if payload is None or not payload.startswith(b"RRF2"):
|
||||
raise RecordedPerceptionOverlayError("serialized calibrated fusion is invalid")
|
||||
return payload
|
||||
|
||||
|
||||
def _read_cache(
|
||||
output: Path,
|
||||
sidecar: Path,
|
||||
fusion: RecordedCalibratedFusion,
|
||||
recording_id: str,
|
||||
) -> bytes | None:
|
||||
try:
|
||||
value = _read_json_object(sidecar, sidecar.parent)
|
||||
payload = output.read_bytes()
|
||||
except (OSError, SessionIntegrityError):
|
||||
return None
|
||||
if (
|
||||
value.get("schema_version") != "missioncore.calibrated-fusion-overlay-cache/v1"
|
||||
or value.get("fusion_id") != fusion.fusion_id
|
||||
or value.get("recording_id") != recording_id
|
||||
or value.get("byte_length") != len(payload)
|
||||
or value.get("sha256") != hashlib.sha256(payload).hexdigest()
|
||||
or not payload.startswith(b"RRF2")
|
||||
):
|
||||
return None
|
||||
return payload
|
||||
|
||||
|
||||
def _read_labels(path: Path) -> list[str]:
|
||||
metadata = _confined_regular_file(path, path.parent)
|
||||
if not 1 <= metadata.st_size <= MAX_JSON_BYTES:
|
||||
raise SessionIntegrityError("calibrated fusion labels are outside bounds")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("calibrated fusion labels are unavailable") from exc
|
||||
if (
|
||||
not isinstance(value, list)
|
||||
or any(not isinstance(item, str) or not 1 <= len(item) <= 256 for item in value)
|
||||
):
|
||||
raise SessionIntegrityError("calibrated fusion labels are invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _read_json_object(path: Path, root: Path) -> dict[str, Any]:
|
||||
metadata = _confined_regular_file(path, root)
|
||||
if not 0 < metadata.st_size <= MAX_JSON_BYTES:
|
||||
raise SessionIntegrityError("calibrated fusion JSON is outside bounds")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("calibrated fusion JSON is unavailable") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("calibrated fusion JSON is not an object")
|
||||
return value
|
||||
|
||||
|
||||
def _confined_regular_file(path: Path, root: Path) -> os.stat_result:
|
||||
try:
|
||||
resolved_root = root.resolve(strict=True)
|
||||
resolved = path.resolve(strict=True)
|
||||
metadata = path.lstat()
|
||||
except OSError as exc:
|
||||
raise SessionIntegrityError("calibrated fusion artifact is unavailable") from exc
|
||||
if (
|
||||
stat.S_ISLNK(metadata.st_mode)
|
||||
or not stat.S_ISREG(metadata.st_mode)
|
||||
or not resolved.is_relative_to(resolved_root)
|
||||
):
|
||||
raise SessionIntegrityError("calibrated fusion artifact is not confined")
|
||||
return metadata
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
try:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise SessionIntegrityError("calibrated fusion identity cannot be encoded") from exc
|
||||
|
||||
|
||||
def _private_directory(path: Path) -> Path:
|
||||
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
if path.is_symlink() or not path.is_dir():
|
||||
raise RecordedPerceptionOverlayError("fusion cache root is invalid")
|
||||
with suppress(OSError):
|
||||
os.chmod(path, 0o700)
|
||||
return path.resolve(strict=True)
|
||||
|
||||
|
||||
def _private_child_directory(root: Path, name: str) -> Path:
|
||||
child = root / name
|
||||
child.mkdir(mode=0o700, exist_ok=True)
|
||||
if child.is_symlink() or not child.is_dir() or child.resolve(strict=True).parent != root:
|
||||
raise RecordedPerceptionOverlayError("fusion cache directory is invalid")
|
||||
with suppress(OSError):
|
||||
os.chmod(child, 0o700)
|
||||
return child.resolve(strict=True)
|
||||
@@ -0,0 +1,925 @@
|
||||
"""Validate and project accepted LAB E10 integrated perception results."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Protocol, TypeGuard
|
||||
|
||||
import numpy as np
|
||||
import rerun as rr
|
||||
from rerun.components import FillMode
|
||||
|
||||
from k1link.artifacts import write_json_atomic
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
|
||||
from .jobs import CameraComputeJob, validate_camera_compute_job
|
||||
from .results import RecordedPerceptionOverlayError
|
||||
|
||||
RESULT_SCHEMA = "missioncore.e10-integrated-perception-result/v1"
|
||||
IDENTITY_SCHEMA = "missioncore.e10-integrated-perception-identity/v1"
|
||||
REPORT_SCHEMA = "missioncore.e10-integrated-perception-report/v1"
|
||||
PACK_SCHEMA = "missioncore.e10-lidar-replay-pack/v1"
|
||||
SEMANTIC_SCHEMA = "missioncore.e10-semantic-frame/v1"
|
||||
FUSION_SCHEMA = "missioncore.e10-fusion-frame/v1"
|
||||
WORLD_SCHEMA = "missioncore.live-perception-world-state/v1"
|
||||
SESSION_TIMELINE = "session_time"
|
||||
MAX_SCAN = 512
|
||||
MAX_JSON_BYTES = 64 * 1024 * 1024
|
||||
MAX_LINE_BYTES = 4 * 1024 * 1024
|
||||
MAX_SOURCE_BYTES = 512 * 1024 * 1024
|
||||
OVERLAY_RENDERER_VERSION = "4"
|
||||
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_RECORDING_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class IntegratedPerceptionResult:
|
||||
result_id: str
|
||||
result_root: Path
|
||||
job: CameraComputeJob
|
||||
pack_root: Path
|
||||
created_at_utc: str
|
||||
accepted: bool
|
||||
publication_scope: str
|
||||
source_start_frame_index: int
|
||||
frame_count: int
|
||||
timeline_start_seconds: float
|
||||
timeline_end_seconds: float
|
||||
semantic_path: Path
|
||||
fusion_path: Path
|
||||
world_path: Path
|
||||
arrays_path: Path
|
||||
report_path: Path
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _PresentedCuboid:
|
||||
observed_ns: int
|
||||
track_id: int
|
||||
label: str
|
||||
association_group: str
|
||||
distance_m: float
|
||||
support_points: int
|
||||
center: np.ndarray
|
||||
half_size: np.ndarray
|
||||
quaternion: np.ndarray
|
||||
color: np.ndarray
|
||||
|
||||
|
||||
class _CuboidPresentationState:
|
||||
"""Hold accepted cuboids briefly for operator presentation only.
|
||||
|
||||
The persisted world-state remains fail-closed and frame-exact. This bounded
|
||||
latest-at projection only prevents one rejected LiDAR association from
|
||||
visually clearing an otherwise stable tracked object for a single frame.
|
||||
"""
|
||||
|
||||
def __init__(self, hold_ns: int = CUBOID_PRESENTATION_HOLD_NS) -> None:
|
||||
if hold_ns <= 0:
|
||||
raise ValueError("cuboid presentation hold must be positive")
|
||||
self._hold_ns = hold_ns
|
||||
self._latest: dict[int, _PresentedCuboid] = {}
|
||||
|
||||
def update(
|
||||
self,
|
||||
timestamp_ns: int,
|
||||
objects: list[dict[str, Any]],
|
||||
centers: np.ndarray,
|
||||
half_sizes: np.ndarray,
|
||||
quaternions: np.ndarray,
|
||||
colors: np.ndarray,
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, list[str]] | None:
|
||||
accepted = [
|
||||
item
|
||||
for item in objects
|
||||
if str(item.get("cuboid_status", "")).startswith("accepted-")
|
||||
]
|
||||
if not (
|
||||
len(accepted)
|
||||
== len(centers)
|
||||
== len(half_sizes)
|
||||
== len(quaternions)
|
||||
== len(colors)
|
||||
):
|
||||
raise RecordedPerceptionOverlayError(
|
||||
"integrated perception cuboid presentation arrays are inconsistent"
|
||||
)
|
||||
for item, center, half_size, quaternion, color in zip(
|
||||
accepted,
|
||||
centers,
|
||||
half_sizes,
|
||||
quaternions,
|
||||
colors,
|
||||
strict=True,
|
||||
):
|
||||
track_id = item.get("track_id")
|
||||
if not isinstance(track_id, int) or isinstance(track_id, bool):
|
||||
raise RecordedPerceptionOverlayError(
|
||||
"integrated perception cuboid track identity is invalid"
|
||||
)
|
||||
self._latest[track_id] = _PresentedCuboid(
|
||||
observed_ns=timestamp_ns,
|
||||
track_id=track_id,
|
||||
label=str(item.get("label", "object")),
|
||||
association_group=str(item.get("association_group", "object")),
|
||||
distance_m=float(item["distance_smoothed_m"]),
|
||||
support_points=int(item["clustered_points"]),
|
||||
center=np.asarray(center).copy(),
|
||||
half_size=np.asarray(half_size).copy(),
|
||||
quaternion=np.asarray(quaternion).copy(),
|
||||
color=np.asarray(color).copy(),
|
||||
)
|
||||
|
||||
expired = [
|
||||
track_id
|
||||
for track_id, cuboid in self._latest.items()
|
||||
if timestamp_ns - cuboid.observed_ns > self._hold_ns
|
||||
]
|
||||
for track_id in expired:
|
||||
del self._latest[track_id]
|
||||
if not self._latest:
|
||||
return None
|
||||
|
||||
presented = sorted(self._latest.values(), key=lambda cuboid: cuboid.track_id)
|
||||
presented_colors: list[np.ndarray] = []
|
||||
labels: list[str] = []
|
||||
for cuboid in presented:
|
||||
age_ns = max(0, timestamp_ns - cuboid.observed_ns)
|
||||
color = cuboid.color.copy()
|
||||
if age_ns > 0 and color.shape == (4,):
|
||||
fade = min(1.0, age_ns / self._hold_ns)
|
||||
color[3] = max(24, round(float(color[3]) * (1.0 - 0.55 * fade)))
|
||||
presented_colors.append(color)
|
||||
age_label = "" if age_ns == 0 else f" · hold {age_ns / 1_000_000:.0f} ms"
|
||||
labels.append(
|
||||
f"{cuboid.association_group} #{cuboid.track_id} {cuboid.label} · "
|
||||
f"{cuboid.distance_m:.1f} m · {cuboid.support_points} pts{age_label}"
|
||||
)
|
||||
return (
|
||||
np.stack([cuboid.center for cuboid in presented]),
|
||||
np.stack([cuboid.half_size for cuboid in presented]),
|
||||
np.stack([cuboid.quaternion for cuboid in presented]),
|
||||
np.stack(presented_colors),
|
||||
labels,
|
||||
)
|
||||
|
||||
|
||||
class _OverlayProvider(Protocol):
|
||||
def render(
|
||||
self,
|
||||
session_id: str,
|
||||
*,
|
||||
application_id: str,
|
||||
recording_id: str,
|
||||
) -> bytes | None: ...
|
||||
|
||||
|
||||
def validate_integrated_perception_result(
|
||||
job_root: Path,
|
||||
result_root: Path,
|
||||
lidar_packs_root: Path,
|
||||
) -> IntegratedPerceptionResult:
|
||||
job = validate_camera_compute_job(job_root)
|
||||
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 root is invalid")
|
||||
result = _read_object(root / "result.json", root)
|
||||
identity = result.get("identity")
|
||||
identity_sha256 = result.get("identity_sha256")
|
||||
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 identity.get("job_id") != job.job_id
|
||||
or identity.get("input_sha256") != job.input_sha256
|
||||
or identity.get("session_id") != job.session_id
|
||||
or identity.get("source_id") != job.source_id
|
||||
or result.get("ground_truth") is not False
|
||||
or result.get("publication_scope")
|
||||
not in {
|
||||
"recorded-integrated-realtime-qualification-only",
|
||||
"recorded-integrated-semantic-loss-negative-control-only",
|
||||
}
|
||||
or result.get("acceptance_state") not in {"accepted", "rejected"}
|
||||
):
|
||||
raise SessionIntegrityError("integrated perception identity is inconsistent")
|
||||
selection = identity.get("selection")
|
||||
if not isinstance(selection, dict):
|
||||
raise SessionIntegrityError("integrated perception selection is missing")
|
||||
count = selection.get("frame_count")
|
||||
start = selection.get("source_start_frame_index")
|
||||
end = selection.get("source_end_frame_index")
|
||||
timeline_start = selection.get("timeline_start_seconds")
|
||||
timeline_end = selection.get("timeline_end_seconds")
|
||||
if (
|
||||
not isinstance(count, int)
|
||||
or isinstance(count, bool)
|
||||
or count < 2
|
||||
or not isinstance(start, int)
|
||||
or isinstance(start, bool)
|
||||
or not isinstance(end, int)
|
||||
or isinstance(end, bool)
|
||||
or end - start + 1 != count
|
||||
or start < 0
|
||||
or end >= job.segment_count
|
||||
or not _finite(timeline_start)
|
||||
or not _finite(timeline_end)
|
||||
or float(timeline_end) <= float(timeline_start)
|
||||
or result.get("frames_processed") != count
|
||||
):
|
||||
raise SessionIntegrityError("integrated perception selection is invalid")
|
||||
pack_id = identity.get("lidar_pack_id")
|
||||
if not isinstance(pack_id, str) or _SAFE_PACK_ID.fullmatch(pack_id) is None:
|
||||
raise SessionIntegrityError("integrated perception LiDAR pack binding is invalid")
|
||||
pack_root = lidar_packs_root.expanduser().resolve(strict=True) / pack_id
|
||||
_validate_pack(pack_root, job, identity, count, start, end)
|
||||
artifacts = _validate_artifacts(root, result.get("artifacts"))
|
||||
semantic_path = artifacts["e10-semantic-frames"]
|
||||
fusion_path = artifacts["e10-fusion-frames"]
|
||||
world_path = artifacts["e10-world-state"]
|
||||
arrays_path = artifacts["e10-transient-perception"]
|
||||
report_path = artifacts["e10-run-report"]
|
||||
semantic_rows = _read_rows(semantic_path, root, SEMANTIC_SCHEMA)
|
||||
fusion_rows = _read_rows(fusion_path, root, FUSION_SCHEMA)
|
||||
world_rows = _read_rows(world_path, root, WORLD_SCHEMA)
|
||||
if len(fusion_rows) != count or len(world_rows) != count or not semantic_rows:
|
||||
raise SessionIntegrityError("integrated perception frame counts are incomplete")
|
||||
for index, (fusion, world) in enumerate(zip(fusion_rows, world_rows, strict=True)):
|
||||
source_index = start + index
|
||||
if (
|
||||
fusion.get("frame_index") != index
|
||||
or fusion.get("source_frame_index") != source_index
|
||||
or world.get("frame_index") != index
|
||||
or world.get("source_frame_index") != source_index
|
||||
or not isinstance(fusion.get("objects"), list)
|
||||
or not isinstance(world.get("objects"), list)
|
||||
or world.get("object_count") != len(world["objects"])
|
||||
or not isinstance(world.get("delivery"), dict)
|
||||
):
|
||||
raise SessionIntegrityError("integrated perception frame identity changed")
|
||||
semantic_indices: list[int] = []
|
||||
for row in semantic_rows:
|
||||
frame_index = row.get("frame_index")
|
||||
if not isinstance(frame_index, int) or isinstance(frame_index, bool):
|
||||
raise SessionIntegrityError("integrated perception semantic timeline is invalid")
|
||||
semantic_indices.append(frame_index)
|
||||
if (
|
||||
semantic_indices != sorted(set(semantic_indices))
|
||||
or semantic_indices[0] != 0
|
||||
or semantic_indices[-1] >= count
|
||||
):
|
||||
raise SessionIntegrityError("integrated perception semantic timeline is invalid")
|
||||
_validate_arrays(arrays_path, count, semantic_rows, fusion_rows)
|
||||
report = _read_object(report_path, root)
|
||||
acceptance = report.get("acceptance")
|
||||
if (
|
||||
report.get("schema_version") != REPORT_SCHEMA
|
||||
or report.get("result_id") != root.name
|
||||
or report.get("identity") != identity
|
||||
or report.get("ground_truth") is not False
|
||||
or not isinstance(acceptance, dict)
|
||||
or acceptance.get("accepted") is not (result.get("acceptance_state") == "accepted")
|
||||
or acceptance.get("navigation_or_safety_accepted") is not False
|
||||
):
|
||||
raise SessionIntegrityError("integrated perception report is inconsistent")
|
||||
created = result.get("created_at_utc")
|
||||
if not isinstance(created, str) or not 1 <= len(created) <= 64:
|
||||
raise SessionIntegrityError("integrated perception creation time is invalid")
|
||||
return IntegratedPerceptionResult(
|
||||
result_id=root.name,
|
||||
result_root=root,
|
||||
job=job,
|
||||
pack_root=pack_root,
|
||||
created_at_utc=created,
|
||||
accepted=result.get("acceptance_state") == "accepted",
|
||||
publication_scope=str(result["publication_scope"]),
|
||||
source_start_frame_index=start,
|
||||
frame_count=count,
|
||||
timeline_start_seconds=float(timeline_start),
|
||||
timeline_end_seconds=float(timeline_end),
|
||||
semantic_path=semantic_path,
|
||||
fusion_path=fusion_path,
|
||||
world_path=world_path,
|
||||
arrays_path=arrays_path,
|
||||
report_path=report_path,
|
||||
)
|
||||
|
||||
|
||||
class IntegratedPerceptionOverlayStore:
|
||||
"""Prefer the newest accepted E10 result for a recorded session."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
jobs_root: Path,
|
||||
results_root: Path,
|
||||
lidar_packs_root: Path,
|
||||
cache_root: Path,
|
||||
ffmpeg_path: Path,
|
||||
) -> None:
|
||||
self.jobs_root = jobs_root.expanduser().absolute()
|
||||
self.results_root = results_root.expanduser().absolute()
|
||||
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()
|
||||
|
||||
def render(
|
||||
self,
|
||||
session_id: str,
|
||||
*,
|
||||
application_id: str,
|
||||
recording_id: str,
|
||||
) -> bytes | None:
|
||||
if application_id != "nodedc_mission_core_recorded":
|
||||
raise ValueError("integrated perception application id is invalid")
|
||||
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")
|
||||
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:
|
||||
try:
|
||||
jobs = sorted(self.jobs_root.iterdir())
|
||||
candidates = sorted(self.results_root.iterdir())
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
if len(jobs) > MAX_SCAN or len(candidates) > 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:
|
||||
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,
|
||||
)
|
||||
except (OSError, SessionIntegrityError):
|
||||
continue
|
||||
if (
|
||||
value.accepted
|
||||
and value.publication_scope == "recorded-integrated-realtime-qualification-only"
|
||||
):
|
||||
matches.append(value)
|
||||
if not matches:
|
||||
return None
|
||||
return max(matches, key=lambda value: (value.created_at_utc, value.result_id))
|
||||
|
||||
|
||||
def _render(
|
||||
result: IntegratedPerceptionResult,
|
||||
*,
|
||||
application_id: str,
|
||||
recording_id: str,
|
||||
ffmpeg_path: Path,
|
||||
temporary_root: Path,
|
||||
) -> bytes:
|
||||
fusion_rows = _read_rows(result.fusion_path, result.result_root, FUSION_SCHEMA)
|
||||
with np.load(result.arrays_path, allow_pickle=False) as arrays:
|
||||
frame_times = arrays["frame_times_ns"]
|
||||
semantic_indices = arrays["semantic_frame_indices"]
|
||||
semantic_masks = arrays["semantic_masks"]
|
||||
semantic_by_frame = {
|
||||
int(index): mask for index, mask in zip(semantic_indices, semantic_masks, strict=True)
|
||||
}
|
||||
support_offsets = arrays["support_offsets"]
|
||||
support = arrays["support_points"]
|
||||
support_colors = arrays["support_colors"]
|
||||
box_offsets = arrays["box_offsets"]
|
||||
centers = arrays["box_centers"]
|
||||
half_sizes = arrays["box_half_sizes"]
|
||||
quaternions = arrays["box_quaternions"]
|
||||
box_colors = arrays["box_colors"]
|
||||
cuboid_presentation = _CuboidPresentationState()
|
||||
recording = rr.RecordingStream(application_id, recording_id=recording_id)
|
||||
stream = rr.binary_stream(recording)
|
||||
source_path: Path | None = None
|
||||
proxy_path: Path | None = None
|
||||
try:
|
||||
recording.log("/world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True)
|
||||
recording.log(
|
||||
"/world/perception/contract",
|
||||
rr.TextDocument(
|
||||
"LAB E10 integrated source-paced replay. Generic AI and host-arrival "
|
||||
"synchronization are diagnostic, not navigation or safety accepted."
|
||||
),
|
||||
static=True,
|
||||
)
|
||||
source_path = _camera_source_file(
|
||||
result.job, result.source_start_frame_index + result.frame_count, temporary_root
|
||||
)
|
||||
source_end_frame_index = result.source_start_frame_index + result.frame_count - 1
|
||||
proxy_path = _camera_proxy_file(
|
||||
source_path,
|
||||
result.source_start_frame_index,
|
||||
source_end_frame_index,
|
||||
ffmpeg_path,
|
||||
temporary_root,
|
||||
)
|
||||
video = rr.AssetVideo(path=proxy_path)
|
||||
video_timestamps = video.read_frame_timestamps_nanos()
|
||||
if len(video_timestamps) != len(frame_times):
|
||||
raise RecordedPerceptionOverlayError(
|
||||
"integrated perception video frame count changed"
|
||||
)
|
||||
recording.log(
|
||||
"/perception/camera/image",
|
||||
video,
|
||||
static=True,
|
||||
)
|
||||
for index, timestamp in enumerate(frame_times):
|
||||
recording.set_time(SESSION_TIMELINE, duration=np.timedelta64(int(timestamp), "ns"))
|
||||
recording.log(
|
||||
"/perception/camera/image",
|
||||
rr.VideoFrameReference(nanoseconds=int(video_timestamps[index])),
|
||||
)
|
||||
mask = semantic_by_frame.get(index)
|
||||
if mask is not None:
|
||||
recording.log(
|
||||
"/perception/camera/segmentation",
|
||||
rr.SegmentationImage(mask),
|
||||
)
|
||||
objects = fusion_rows[index]["objects"]
|
||||
if objects:
|
||||
recording.log(
|
||||
"/perception/camera/detections",
|
||||
rr.Boxes2D(
|
||||
array=[item["bbox_xyxy"] for item in objects],
|
||||
array_format=rr.Box2DFormat.XYXY,
|
||||
labels=[
|
||||
f"#{item['track_id']} {item['label']} · {float(item['score']):.0%}"
|
||||
for item in objects
|
||||
],
|
||||
show_labels=True,
|
||||
),
|
||||
)
|
||||
else:
|
||||
recording.log("/perception/camera/detections", rr.Clear(recursive=False))
|
||||
point_start, point_end = (
|
||||
int(support_offsets[index]),
|
||||
int(support_offsets[index + 1]),
|
||||
)
|
||||
if point_end > point_start:
|
||||
recording.log(
|
||||
"/world/perception/support",
|
||||
rr.Points3D(
|
||||
support[point_start:point_end],
|
||||
colors=support_colors[point_start:point_end],
|
||||
radii=rr.Radius.ui_points(3.0),
|
||||
),
|
||||
)
|
||||
else:
|
||||
recording.log("/world/perception/support", rr.Clear(recursive=False))
|
||||
box_start, box_end = int(box_offsets[index]), int(box_offsets[index + 1])
|
||||
presented_cuboids = cuboid_presentation.update(
|
||||
int(timestamp),
|
||||
objects,
|
||||
centers[box_start:box_end],
|
||||
half_sizes[box_start:box_end],
|
||||
quaternions[box_start:box_end],
|
||||
box_colors[box_start:box_end],
|
||||
)
|
||||
if presented_cuboids is not None:
|
||||
(
|
||||
presented_centers,
|
||||
presented_half_sizes,
|
||||
presented_quaternions,
|
||||
presented_colors,
|
||||
presented_labels,
|
||||
) = presented_cuboids
|
||||
recording.log(
|
||||
"/world/perception/boxes3d",
|
||||
rr.Boxes3D(
|
||||
centers=presented_centers,
|
||||
half_sizes=presented_half_sizes,
|
||||
quaternions=presented_quaternions,
|
||||
colors=presented_colors,
|
||||
labels=presented_labels,
|
||||
fill_mode=FillMode.Solid,
|
||||
show_labels=True,
|
||||
),
|
||||
)
|
||||
else:
|
||||
recording.log("/world/perception/boxes3d", rr.Clear(recursive=False))
|
||||
payload = stream.read(flush=True, flush_timeout_sec=300.0)
|
||||
except RecordedPerceptionOverlayError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise RecordedPerceptionOverlayError(
|
||||
"failed to serialize integrated perception"
|
||||
) from exc
|
||||
finally:
|
||||
with suppress(Exception):
|
||||
recording.disconnect()
|
||||
if source_path is not None:
|
||||
source_path.unlink(missing_ok=True)
|
||||
if proxy_path is not None:
|
||||
proxy_path.unlink(missing_ok=True)
|
||||
if payload is None or not payload.startswith(b"RRF2"):
|
||||
raise RecordedPerceptionOverlayError("integrated perception Rerun stream is invalid")
|
||||
return payload
|
||||
|
||||
|
||||
def _camera_source_file(job: CameraComputeJob, segment_count: int, root: Path) -> Path:
|
||||
epoch = job.job_root / "input" / "camera" / job.source_id / f"epoch-{job.codec_epoch}"
|
||||
paths = [epoch / "init.mp4"] + [
|
||||
epoch / "segments" / f"{sequence}.m4s" for sequence in range(1, segment_count + 1)
|
||||
]
|
||||
total = sum(path.stat().st_size for path in paths)
|
||||
if total <= 0 or total > MAX_SOURCE_BYTES:
|
||||
raise RecordedPerceptionOverlayError(
|
||||
"integrated perception camera source is outside bounds"
|
||||
)
|
||||
descriptor, name = tempfile.mkstemp(prefix=".e10-camera-", suffix=".mp4", dir=root)
|
||||
path = Path(name)
|
||||
try:
|
||||
with os.fdopen(descriptor, "wb") as output:
|
||||
for source in paths:
|
||||
with source.open("rb") as stream:
|
||||
shutil.copyfileobj(stream, output, length=1024 * 1024)
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
return path
|
||||
except BaseException:
|
||||
path.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
|
||||
def _camera_proxy_file(
|
||||
source: Path,
|
||||
start_frame_index: int,
|
||||
end_frame_index: int,
|
||||
ffmpeg_path: Path,
|
||||
root: Path,
|
||||
) -> Path:
|
||||
descriptor, name = tempfile.mkstemp(prefix=".e10-camera-proxy-", suffix=".mp4", dir=root)
|
||||
os.close(descriptor)
|
||||
path = Path(name)
|
||||
path.unlink(missing_ok=True)
|
||||
frame_filter = (
|
||||
f"select=between(n\\,{start_frame_index}\\,{end_frame_index}),setpts=N/(10*TB)"
|
||||
)
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
str(ffmpeg_path),
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-i",
|
||||
str(source),
|
||||
"-vf",
|
||||
frame_filter,
|
||||
"-an",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"veryfast",
|
||||
"-crf",
|
||||
"28",
|
||||
"-g",
|
||||
"20",
|
||||
"-keyint_min",
|
||||
"20",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
str(path),
|
||||
],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
timeout=300,
|
||||
)
|
||||
if completed.returncode != 0 or not path.is_file() or path.stat().st_size <= 0:
|
||||
raise RecordedPerceptionOverlayError(
|
||||
f"integrated perception video proxy failed: {completed.stderr[-1000:]!r}"
|
||||
)
|
||||
os.chmod(path, 0o600)
|
||||
return path
|
||||
except BaseException:
|
||||
path.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
|
||||
def _validate_pack(
|
||||
root: Path,
|
||||
job: CameraComputeJob,
|
||||
result_identity: dict[str, Any],
|
||||
count: int,
|
||||
start: int,
|
||||
end: int,
|
||||
) -> None:
|
||||
if not root.is_dir() or _SAFE_PACK_ID.fullmatch(root.name) is None:
|
||||
raise SessionIntegrityError("integrated perception LiDAR pack root is invalid")
|
||||
manifest = _read_object(root / "manifest.json", root)
|
||||
identity = manifest.get("identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
arrays_path = root / "lidar-pack.npz"
|
||||
artifact = manifest.get("artifact")
|
||||
if (
|
||||
manifest.get("schema_version") != PACK_SCHEMA
|
||||
or manifest.get("pack_id") != root.name
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("schema_version") != PACK_SCHEMA
|
||||
or identity.get("job_id") != job.job_id
|
||||
or identity.get("input_sha256") != job.input_sha256
|
||||
or identity.get("calibration_sha256")
|
||||
!= result_identity["configuration"]["profile"]["source"]["calibration_sha256"]
|
||||
or identity.get("frame_count") != count
|
||||
or identity.get("source_start_frame_index") != start
|
||||
or identity.get("source_end_frame_index") != end
|
||||
or not isinstance(identity_sha256, str)
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or root.name != f"e10-lidar-pack-{identity_sha256}"
|
||||
or not isinstance(artifact, dict)
|
||||
or artifact.get("path") != arrays_path.name
|
||||
or artifact.get("byte_length") != arrays_path.stat().st_size
|
||||
or artifact.get("sha256") != _sha256(arrays_path)
|
||||
):
|
||||
raise SessionIntegrityError("integrated perception LiDAR pack is inconsistent")
|
||||
|
||||
|
||||
def _validate_artifacts(root: Path, raw: object) -> dict[str, Path]:
|
||||
expected = {
|
||||
"e10-semantic-frames": ("semantic-frames.jsonl", SEMANTIC_SCHEMA),
|
||||
"e10-fusion-frames": ("fusion-frames.jsonl", FUSION_SCHEMA),
|
||||
"e10-world-state": ("world-state.jsonl", WORLD_SCHEMA),
|
||||
"e10-transient-perception": ("transient-perception.npz", None),
|
||||
"worker-gpu-telemetry": ("gpu-telemetry.jsonl", None),
|
||||
"e10-run-report": ("run-report.json", REPORT_SCHEMA),
|
||||
}
|
||||
if not isinstance(raw, list) or len(raw) != len(expected):
|
||||
raise SessionIntegrityError("integrated perception artifact set is incomplete")
|
||||
result = {}
|
||||
for value in raw:
|
||||
kind = value.get("kind") if isinstance(value, dict) else None
|
||||
if not isinstance(kind, str) or kind not in expected or kind in result:
|
||||
raise SessionIntegrityError("integrated perception artifact descriptor is invalid")
|
||||
name, schema = expected[kind]
|
||||
path = root / name
|
||||
metadata = _confined_file(path, root)
|
||||
if (
|
||||
value.get("path") != name
|
||||
or value.get("schema_version") != schema
|
||||
or value.get("byte_length") != metadata.st_size
|
||||
or value.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise SessionIntegrityError("integrated perception artifact identity changed")
|
||||
result[kind] = path
|
||||
return result
|
||||
|
||||
|
||||
def _validate_arrays(
|
||||
path: Path,
|
||||
count: int,
|
||||
semantic_rows: list[dict[str, Any]],
|
||||
fusion_rows: list[dict[str, Any]],
|
||||
) -> None:
|
||||
with np.load(path, allow_pickle=False) as arrays:
|
||||
required = {
|
||||
"frame_times_ns",
|
||||
"semantic_frame_indices",
|
||||
"semantic_masks",
|
||||
"support_offsets",
|
||||
"support_points",
|
||||
"support_colors",
|
||||
"box_offsets",
|
||||
"box_centers",
|
||||
"box_half_sizes",
|
||||
"box_quaternions",
|
||||
"box_colors",
|
||||
}
|
||||
if set(arrays.files) != required:
|
||||
raise SessionIntegrityError("integrated perception array set changed")
|
||||
times = arrays["frame_times_ns"]
|
||||
semantic_indices = arrays["semantic_frame_indices"]
|
||||
masks = arrays["semantic_masks"]
|
||||
support_offsets = arrays["support_offsets"]
|
||||
support = arrays["support_points"]
|
||||
support_colors = arrays["support_colors"]
|
||||
box_offsets = arrays["box_offsets"]
|
||||
centers = arrays["box_centers"]
|
||||
half_sizes = arrays["box_half_sizes"]
|
||||
quaternions = arrays["box_quaternions"]
|
||||
colors = arrays["box_colors"]
|
||||
expected_boxes = sum(
|
||||
sum(
|
||||
str(item.get("cuboid_status", "")).startswith("accepted-")
|
||||
for item in row["objects"]
|
||||
)
|
||||
for row in fusion_rows
|
||||
)
|
||||
if (
|
||||
times.dtype != np.int64
|
||||
or times.shape != (count,)
|
||||
or np.any(np.diff(times) <= 0)
|
||||
or semantic_indices.dtype != np.int64
|
||||
or semantic_indices.shape != (len(semantic_rows),)
|
||||
or not np.array_equal(semantic_indices, [row["frame_index"] for row in semantic_rows])
|
||||
or masks.dtype != np.uint8
|
||||
or masks.shape != (len(semantic_rows), 600, 800)
|
||||
or support_offsets.shape != (count + 1,)
|
||||
or support.shape[1:] != (3,)
|
||||
or support_colors.shape != support.shape
|
||||
or box_offsets.shape != (count + 1,)
|
||||
or centers.shape != (expected_boxes, 3)
|
||||
or half_sizes.shape != centers.shape
|
||||
or quaternions.shape != (expected_boxes, 4)
|
||||
or colors.shape != (expected_boxes, 4)
|
||||
or int(support_offsets[-1]) != support.shape[0]
|
||||
or int(box_offsets[-1]) != expected_boxes
|
||||
or np.any(np.diff(support_offsets) < 0)
|
||||
or np.any(np.diff(box_offsets) < 0)
|
||||
or not np.isfinite(support).all()
|
||||
or not np.isfinite(centers).all()
|
||||
or not np.isfinite(half_sizes).all()
|
||||
or np.any(half_sizes <= 0)
|
||||
):
|
||||
raise SessionIntegrityError("integrated perception arrays are inconsistent")
|
||||
|
||||
|
||||
def _read_rows(path: Path, root: Path, schema: str) -> list[dict[str, Any]]:
|
||||
_confined_file(path, root)
|
||||
rows = []
|
||||
with path.open(encoding="utf-8") as stream:
|
||||
for line in stream:
|
||||
if len(line.encode()) > MAX_LINE_BYTES:
|
||||
raise SessionIntegrityError("integrated perception row is oversized")
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict) or value.get("schema_version") != schema:
|
||||
raise SessionIntegrityError("integrated perception row schema changed")
|
||||
rows.append(value)
|
||||
return rows
|
||||
|
||||
|
||||
def _read_cache(
|
||||
output: Path,
|
||||
sidecar: Path,
|
||||
result: IntegratedPerceptionResult,
|
||||
recording_id: str,
|
||||
) -> bytes | None:
|
||||
try:
|
||||
value = _read_object(sidecar, sidecar.parent)
|
||||
payload = output.read_bytes()
|
||||
except (OSError, SessionIntegrityError):
|
||||
return None
|
||||
if (
|
||||
value.get("schema_version") != "missioncore.e10-overlay-cache/v1"
|
||||
or value.get("renderer_version") != OVERLAY_RENDERER_VERSION
|
||||
or value.get("result_id") != result.result_id
|
||||
or value.get("recording_id") != recording_id
|
||||
or value.get("byte_length") != len(payload)
|
||||
or value.get("sha256") != hashlib.sha256(payload).hexdigest()
|
||||
or not payload.startswith(b"RRF2")
|
||||
):
|
||||
return None
|
||||
return payload
|
||||
|
||||
|
||||
def _read_object(path: Path, root: Path) -> dict[str, Any]:
|
||||
metadata = _confined_file(path, root)
|
||||
if not 0 < metadata.st_size <= MAX_JSON_BYTES:
|
||||
raise SessionIntegrityError("integrated perception JSON is outside bounds")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("integrated perception JSON is unavailable") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("integrated perception JSON is not an object")
|
||||
return value
|
||||
|
||||
|
||||
def _confined_file(path: Path, root: Path) -> os.stat_result:
|
||||
try:
|
||||
resolved_root = root.resolve(strict=True)
|
||||
resolved = path.resolve(strict=True)
|
||||
metadata = path.lstat()
|
||||
except OSError as exc:
|
||||
raise SessionIntegrityError("integrated perception artifact is unavailable") from exc
|
||||
if (
|
||||
stat.S_ISLNK(metadata.st_mode)
|
||||
or not stat.S_ISREG(metadata.st_mode)
|
||||
or not resolved.is_relative_to(resolved_root)
|
||||
):
|
||||
raise SessionIntegrityError("integrated perception artifact is not confined")
|
||||
return metadata
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _finite(value: object) -> TypeGuard[int | float]:
|
||||
return (
|
||||
isinstance(value, int | float)
|
||||
and not isinstance(value, bool)
|
||||
and bool(np.isfinite(value))
|
||||
)
|
||||
|
||||
|
||||
def _private_directory(path: Path) -> Path:
|
||||
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
if path.is_symlink() or not path.is_dir():
|
||||
raise RecordedPerceptionOverlayError("integrated perception cache root is invalid")
|
||||
with suppress(OSError):
|
||||
os.chmod(path, 0o700)
|
||||
return path.resolve(strict=True)
|
||||
|
||||
|
||||
def _private_child(root: Path, name: str) -> Path:
|
||||
child = root / name
|
||||
child.mkdir(mode=0o700, exist_ok=True)
|
||||
if child.is_symlink() or not child.is_dir() or child.resolve(strict=True).parent != root:
|
||||
raise RecordedPerceptionOverlayError("integrated perception cache child is invalid")
|
||||
with suppress(OSError):
|
||||
os.chmod(child, 0o700)
|
||||
return child.resolve(strict=True)
|
||||
@@ -0,0 +1,991 @@
|
||||
"""Bounded latest-wins primitives for derived live perception."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import statistics
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
import zlib
|
||||
from collections import deque
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from hashlib import sha256
|
||||
from typing import Any, Final, Literal
|
||||
|
||||
import numpy as np
|
||||
|
||||
from k1link.data_plane import DecodedPointCloudView, DecodedPoseView
|
||||
|
||||
WORLD_STATE_SCHEMA = "missioncore.live-perception-world-state/v1"
|
||||
TELEMETRY_SCHEMA = "missioncore.live-perception-telemetry/v1"
|
||||
|
||||
HealthState = Literal["healthy", "degraded", "stale", "unavailable"]
|
||||
LiveIngressModality = Literal[
|
||||
"control",
|
||||
"camera-init",
|
||||
"camera-frame",
|
||||
"lidar",
|
||||
"pose",
|
||||
]
|
||||
|
||||
LIVE_INGRESS_SCHEMA: Final = "missioncore.live-perception-ingress/v1"
|
||||
LIVE_INGRESS_WIRE_SCHEMA: Final = "missioncore.live-perception-wire/v1"
|
||||
LIVE_RESULT_WIRE_SCHEMA: Final = "missioncore.live-perception-result-wire/v1"
|
||||
LIVE_RESULT_MAGIC: Final = b"MCPR"
|
||||
LIVE_RESULT_MAX_HEADER_BYTES: Final = 256 * 1024
|
||||
LIVE_RESULT_MAX_PAYLOAD_BYTES: Final = 2 * 1024 * 1024
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LivePerceptionResultFrame:
|
||||
frame_index: int
|
||||
source_frame_index: int
|
||||
session_seconds: float
|
||||
captured_at_epoch_ns: int
|
||||
image_jpeg: bytes
|
||||
segmentation_mask: np.ndarray[Any, np.dtype[np.uint8]] | None
|
||||
objects: tuple[dict[str, Any], ...]
|
||||
delivery: dict[str, Any]
|
||||
|
||||
|
||||
def encode_live_perception_result(
|
||||
*,
|
||||
frame_index: int,
|
||||
source_frame_index: int,
|
||||
session_seconds: float,
|
||||
captured_at_epoch_ns: int,
|
||||
image_jpeg: bytes,
|
||||
segmentation_mask: np.ndarray[Any, Any] | None,
|
||||
objects: Sequence[Mapping[str, Any]],
|
||||
delivery: Mapping[str, Any],
|
||||
) -> bytes:
|
||||
"""Encode one bounded, non-authoritative worker-to-viewer result frame."""
|
||||
|
||||
if (
|
||||
frame_index < 0
|
||||
or source_frame_index < 0
|
||||
or captured_at_epoch_ns < 0
|
||||
or not math.isfinite(session_seconds)
|
||||
or session_seconds < 0
|
||||
or not 4 <= len(image_jpeg) <= 1024 * 1024
|
||||
or not image_jpeg.startswith(b"\xff\xd8")
|
||||
or not image_jpeg.endswith(b"\xff\xd9")
|
||||
):
|
||||
raise ValueError("live perception result identity or image is invalid")
|
||||
normalized_objects = tuple(_normalize_live_result_object(value) for value in objects)
|
||||
if len(normalized_objects) > 128:
|
||||
raise ValueError("live perception result object count exceeds the bound")
|
||||
mask_payload = b""
|
||||
mask_shape: list[int] | None = None
|
||||
if segmentation_mask is not None:
|
||||
mask = np.asarray(segmentation_mask, dtype=np.uint8)
|
||||
if mask.shape != (600, 800):
|
||||
raise ValueError("live perception segmentation shape is invalid")
|
||||
mask_payload = zlib.compress(mask.tobytes(order="C"), level=1)
|
||||
mask_shape = [600, 800]
|
||||
payload = bytes(image_jpeg) + mask_payload
|
||||
if len(payload) > LIVE_RESULT_MAX_PAYLOAD_BYTES:
|
||||
raise ValueError("live perception result payload exceeds the bound")
|
||||
header = {
|
||||
"schema_version": LIVE_RESULT_WIRE_SCHEMA,
|
||||
"frame_index": frame_index,
|
||||
"source_frame_index": source_frame_index,
|
||||
"session_seconds": session_seconds,
|
||||
"captured_at_epoch_ns": captured_at_epoch_ns,
|
||||
"image": {"codec": "jpeg", "byte_length": len(image_jpeg)},
|
||||
"segmentation": (
|
||||
None
|
||||
if mask_shape is None
|
||||
else {
|
||||
"codec": "zlib-uint8-c1",
|
||||
"shape": mask_shape,
|
||||
"byte_length": len(mask_payload),
|
||||
}
|
||||
),
|
||||
"objects": normalized_objects,
|
||||
"delivery": dict(delivery),
|
||||
"payload_bytes": len(payload),
|
||||
"payload_sha256": sha256(payload).hexdigest(),
|
||||
"authority": "shadow-diagnostic-only",
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
encoded_header = json.dumps(
|
||||
header,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
if len(encoded_header) > LIVE_RESULT_MAX_HEADER_BYTES:
|
||||
raise ValueError("live perception result header exceeds the bound")
|
||||
return LIVE_RESULT_MAGIC + struct.pack("!I", len(encoded_header)) + encoded_header + payload
|
||||
|
||||
|
||||
def decode_live_perception_result(encoded: bytes) -> LivePerceptionResultFrame:
|
||||
"""Validate and decode one result frame before it reaches the Rerun bridge."""
|
||||
|
||||
if len(encoded) < 10 or not encoded.startswith(LIVE_RESULT_MAGIC):
|
||||
raise ValueError("live perception result frame is truncated")
|
||||
header_length = struct.unpack("!I", encoded[4:8])[0]
|
||||
if not 2 <= header_length <= LIVE_RESULT_MAX_HEADER_BYTES:
|
||||
raise ValueError("live perception result header length is invalid")
|
||||
boundary = 8 + header_length
|
||||
if boundary > len(encoded):
|
||||
raise ValueError("live perception result header is truncated")
|
||||
try:
|
||||
header = json.loads(encoded[8:boundary])
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise ValueError("live perception result header is invalid") from exc
|
||||
payload = encoded[boundary:]
|
||||
if (
|
||||
not isinstance(header, dict)
|
||||
or header.get("schema_version") != LIVE_RESULT_WIRE_SCHEMA
|
||||
or header.get("authority") != "shadow-diagnostic-only"
|
||||
or header.get("commands_enabled") is not False
|
||||
or header.get("navigation_or_safety_accepted") is not False
|
||||
or header.get("payload_bytes") != len(payload)
|
||||
or len(payload) > LIVE_RESULT_MAX_PAYLOAD_BYTES
|
||||
or header.get("payload_sha256") != sha256(payload).hexdigest()
|
||||
):
|
||||
raise ValueError("live perception result contract is invalid")
|
||||
image = header.get("image")
|
||||
segmentation = header.get("segmentation")
|
||||
objects = header.get("objects")
|
||||
delivery = header.get("delivery")
|
||||
if (
|
||||
not isinstance(image, dict)
|
||||
or image.get("codec") != "jpeg"
|
||||
or not isinstance(image.get("byte_length"), int)
|
||||
or not isinstance(objects, list)
|
||||
or len(objects) > 128
|
||||
or not isinstance(delivery, dict)
|
||||
):
|
||||
raise ValueError("live perception result content descriptor is invalid")
|
||||
image_length = image["byte_length"]
|
||||
if not 4 <= image_length <= min(len(payload), 1024 * 1024):
|
||||
raise ValueError("live perception result image length is invalid")
|
||||
image_jpeg = payload[:image_length]
|
||||
if not image_jpeg.startswith(b"\xff\xd8") or not image_jpeg.endswith(b"\xff\xd9"):
|
||||
raise ValueError("live perception result JPEG is invalid")
|
||||
mask: np.ndarray[Any, np.dtype[np.uint8]] | None = None
|
||||
if segmentation is None:
|
||||
if len(payload) != image_length:
|
||||
raise ValueError("live perception result has an undescribed payload tail")
|
||||
else:
|
||||
if (
|
||||
not isinstance(segmentation, dict)
|
||||
or segmentation.get("codec") != "zlib-uint8-c1"
|
||||
or segmentation.get("shape") != [600, 800]
|
||||
or not isinstance(segmentation.get("byte_length"), int)
|
||||
or segmentation["byte_length"] != len(payload) - image_length
|
||||
):
|
||||
raise ValueError("live perception segmentation descriptor is invalid")
|
||||
try:
|
||||
raw_mask = zlib.decompress(payload[image_length:])
|
||||
except zlib.error as exc:
|
||||
raise ValueError("live perception segmentation payload is invalid") from exc
|
||||
if len(raw_mask) != 600 * 800:
|
||||
raise ValueError("live perception segmentation byte length is invalid")
|
||||
mask = np.frombuffer(raw_mask, dtype=np.uint8).reshape((600, 800)).copy()
|
||||
normalized_objects = tuple(_normalize_live_result_object(value) for value in objects)
|
||||
frame_index = header.get("frame_index")
|
||||
source_frame_index = header.get("source_frame_index")
|
||||
session_seconds = header.get("session_seconds")
|
||||
captured_at_epoch_ns = header.get("captured_at_epoch_ns")
|
||||
if (
|
||||
not isinstance(frame_index, int)
|
||||
or isinstance(frame_index, bool)
|
||||
or frame_index < 0
|
||||
or not isinstance(source_frame_index, int)
|
||||
or isinstance(source_frame_index, bool)
|
||||
or source_frame_index < 0
|
||||
or not isinstance(captured_at_epoch_ns, int)
|
||||
or isinstance(captured_at_epoch_ns, bool)
|
||||
or captured_at_epoch_ns < 0
|
||||
or not isinstance(session_seconds, (int, float))
|
||||
or isinstance(session_seconds, bool)
|
||||
or not math.isfinite(float(session_seconds))
|
||||
or float(session_seconds) < 0
|
||||
):
|
||||
raise ValueError("live perception result time identity is invalid")
|
||||
return LivePerceptionResultFrame(
|
||||
frame_index=frame_index,
|
||||
source_frame_index=source_frame_index,
|
||||
session_seconds=float(session_seconds),
|
||||
captured_at_epoch_ns=captured_at_epoch_ns,
|
||||
image_jpeg=image_jpeg,
|
||||
segmentation_mask=mask,
|
||||
objects=normalized_objects,
|
||||
delivery=dict(delivery),
|
||||
)
|
||||
|
||||
|
||||
def _normalize_live_result_object(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
if not isinstance(value, Mapping):
|
||||
raise ValueError("live perception result object is invalid")
|
||||
track_id = value.get("track_id")
|
||||
label = value.get("label")
|
||||
score = value.get("score")
|
||||
bbox = value.get("bbox_xyxy")
|
||||
if (
|
||||
not isinstance(track_id, int)
|
||||
or isinstance(track_id, bool)
|
||||
or track_id < 0
|
||||
or not isinstance(label, str)
|
||||
or not 1 <= len(label) <= 64
|
||||
or not isinstance(score, (int, float))
|
||||
or isinstance(score, bool)
|
||||
or not math.isfinite(float(score))
|
||||
or not isinstance(bbox, Sequence)
|
||||
or isinstance(bbox, (str, bytes))
|
||||
or len(bbox) != 4
|
||||
):
|
||||
raise ValueError("live perception result object identity is invalid")
|
||||
bbox_values = [float(item) for item in bbox]
|
||||
if not all(math.isfinite(item) for item in bbox_values):
|
||||
raise ValueError("live perception result 2D box is invalid")
|
||||
normalized: dict[str, Any] = {
|
||||
"track_id": track_id,
|
||||
"label": label,
|
||||
"score": float(score),
|
||||
"bbox_xyxy": bbox_values,
|
||||
}
|
||||
distance = value.get("distance_smoothed_m", value.get("distance_median_m"))
|
||||
if distance is not None:
|
||||
if (
|
||||
not isinstance(distance, (int, float))
|
||||
or isinstance(distance, bool)
|
||||
or not math.isfinite(float(distance))
|
||||
or float(distance) < 0
|
||||
):
|
||||
raise ValueError("live perception result distance is invalid")
|
||||
normalized["distance_m"] = float(distance)
|
||||
else:
|
||||
normalized["distance_m"] = None
|
||||
cuboid_fields = (
|
||||
("cuboid_center_map", 3),
|
||||
("cuboid_half_size", 3),
|
||||
("cuboid_quaternion_xyzw", 4),
|
||||
)
|
||||
present = [value.get(name) is not None for name, _ in cuboid_fields]
|
||||
if any(present) and not all(present):
|
||||
raise ValueError("live perception result cuboid is incomplete")
|
||||
for name, length in cuboid_fields:
|
||||
candidate = value.get(name)
|
||||
if candidate is None:
|
||||
normalized[name] = None
|
||||
continue
|
||||
if (
|
||||
not isinstance(candidate, Sequence)
|
||||
or isinstance(candidate, (str, bytes))
|
||||
or len(candidate) != length
|
||||
):
|
||||
raise ValueError("live perception result cuboid geometry is invalid")
|
||||
values = [float(item) for item in candidate]
|
||||
if not all(math.isfinite(item) for item in values):
|
||||
raise ValueError("live perception result cuboid contains non-finite values")
|
||||
normalized[name] = values
|
||||
return normalized
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LiveIngressEvent:
|
||||
"""One raw-first, derived-only event admitted to the shadow transport."""
|
||||
|
||||
ingress_sequence: int
|
||||
session_id: str
|
||||
modality: LiveIngressModality
|
||||
source_id: str
|
||||
source_sequence: int
|
||||
captured_at_epoch_ns: int
|
||||
received_monotonic_ns: int
|
||||
payload: bytes
|
||||
|
||||
def wire_bytes(self) -> bytes:
|
||||
header = json.dumps(
|
||||
{
|
||||
"schema_version": LIVE_INGRESS_WIRE_SCHEMA,
|
||||
"ingress_sequence": self.ingress_sequence,
|
||||
"session_id": self.session_id,
|
||||
"modality": self.modality,
|
||||
"source_id": self.source_id,
|
||||
"source_sequence": self.source_sequence,
|
||||
"captured_at_epoch_ns": self.captured_at_epoch_ns,
|
||||
"received_monotonic_ns": self.received_monotonic_ns,
|
||||
"payload_bytes": len(self.payload),
|
||||
"payload_sha256": sha256(self.payload).hexdigest(),
|
||||
"authority": "shadow-diagnostic-only",
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
ensure_ascii=True,
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
).encode("utf-8")
|
||||
return struct.pack("!I", len(header)) + header + self.payload
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LiveIngressQueueSnapshot:
|
||||
capacity: int
|
||||
depth: int
|
||||
maximum_depth: int
|
||||
published: int
|
||||
consumed: int
|
||||
dropped_overflow: int
|
||||
rejected_oversize: int
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _LiveIngressQueue:
|
||||
capacity: int
|
||||
items: deque[LiveIngressEvent]
|
||||
maximum_depth: int = 0
|
||||
published: int = 0
|
||||
consumed: int = 0
|
||||
dropped_overflow: int = 0
|
||||
rejected_oversize: int = 0
|
||||
|
||||
|
||||
class LivePerceptionIngress:
|
||||
"""Exclusive, bounded fan-out from committed K1 evidence to one AI worker.
|
||||
|
||||
The ingress is deliberately not an acquisition source and has no command
|
||||
surface. Camera and MQTT producers call it only after their raw evidence
|
||||
commit has completed. Separate modality queues prevent camera bursts from
|
||||
evicting pose or LiDAR observations.
|
||||
"""
|
||||
|
||||
_CAPACITIES: Final[dict[LiveIngressModality, int]] = {
|
||||
"control": 4,
|
||||
"camera-init": 1,
|
||||
"camera-frame": 2,
|
||||
"lidar": 2,
|
||||
"pose": 16,
|
||||
}
|
||||
_MAX_PAYLOAD_BYTES: Final[dict[LiveIngressModality, int]] = {
|
||||
"control": 16 * 1024,
|
||||
"camera-init": 1024 * 1024,
|
||||
"camera-frame": 1024 * 1024,
|
||||
"lidar": 2 * 1024 * 1024,
|
||||
"pose": 2 * 1024 * 1024,
|
||||
}
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._condition = threading.Condition()
|
||||
self._queues = {
|
||||
modality: _LiveIngressQueue(capacity, deque())
|
||||
for modality, capacity in self._CAPACITIES.items()
|
||||
}
|
||||
self._ingress_sequence = 0
|
||||
self._session_id: str | None = None
|
||||
self._active = False
|
||||
self._closed = False
|
||||
self._consumer_id: str | None = None
|
||||
|
||||
def begin_session(self, session_id: str) -> None:
|
||||
if not session_id or len(session_id) > 160:
|
||||
raise ValueError("live perception session id is invalid")
|
||||
with self._condition:
|
||||
if self._closed:
|
||||
raise RuntimeError("live perception ingress is closed")
|
||||
if self._active:
|
||||
if self._session_id == session_id:
|
||||
return
|
||||
raise RuntimeError("another live perception session is active")
|
||||
self._session_id = session_id
|
||||
self._active = True
|
||||
self._publish_locked(
|
||||
modality="control",
|
||||
source_id="mission-core",
|
||||
source_sequence=0,
|
||||
captured_at_epoch_ns=time.time_ns(),
|
||||
received_monotonic_ns=time.monotonic_ns(),
|
||||
payload=b'{"event":"session-start"}',
|
||||
)
|
||||
|
||||
def end_session(self, session_id: str) -> None:
|
||||
with self._condition:
|
||||
if not self._active or self._session_id != session_id:
|
||||
return
|
||||
self._publish_locked(
|
||||
modality="control",
|
||||
source_id="mission-core",
|
||||
source_sequence=0,
|
||||
captured_at_epoch_ns=time.time_ns(),
|
||||
received_monotonic_ns=time.monotonic_ns(),
|
||||
payload=b'{"event":"session-end"}',
|
||||
)
|
||||
self._active = False
|
||||
|
||||
def publish(
|
||||
self,
|
||||
*,
|
||||
modality: LiveIngressModality,
|
||||
source_id: str,
|
||||
source_sequence: int,
|
||||
captured_at_epoch_ns: int,
|
||||
received_monotonic_ns: int,
|
||||
payload: bytes,
|
||||
) -> bool:
|
||||
if modality == "control":
|
||||
raise ValueError("control events are owned by the ingress lifecycle")
|
||||
if not source_id or source_sequence < 0:
|
||||
raise ValueError("live perception source identity is invalid")
|
||||
if captured_at_epoch_ns < 0 or received_monotonic_ns < 0:
|
||||
raise ValueError("live perception timestamps must be non-negative")
|
||||
with self._condition:
|
||||
if self._closed or not self._active:
|
||||
return False
|
||||
return self._publish_locked(
|
||||
modality=modality,
|
||||
source_id=source_id,
|
||||
source_sequence=source_sequence,
|
||||
captured_at_epoch_ns=captured_at_epoch_ns,
|
||||
received_monotonic_ns=received_monotonic_ns,
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
def open_consumer(self, consumer_id: str) -> None:
|
||||
if not consumer_id or len(consumer_id) > 128:
|
||||
raise ValueError("live perception consumer id is invalid")
|
||||
with self._condition:
|
||||
if self._closed:
|
||||
raise RuntimeError("live perception ingress is closed")
|
||||
if self._consumer_id is not None and self._consumer_id != consumer_id:
|
||||
raise RuntimeError("live perception ingress already has a consumer")
|
||||
self._consumer_id = consumer_id
|
||||
|
||||
def close_consumer(self, consumer_id: str) -> None:
|
||||
with self._condition:
|
||||
if self._consumer_id == consumer_id:
|
||||
self._consumer_id = None
|
||||
self._condition.notify_all()
|
||||
|
||||
def take_next(
|
||||
self,
|
||||
consumer_id: str,
|
||||
*,
|
||||
timeout: float | None = None,
|
||||
) -> LiveIngressEvent | None:
|
||||
with self._condition:
|
||||
if self._consumer_id != consumer_id:
|
||||
raise RuntimeError("live perception consumer lease is not active")
|
||||
ready = self._condition.wait_for(
|
||||
lambda: any(queue.items for queue in self._queues.values()) or self._closed,
|
||||
timeout=timeout,
|
||||
)
|
||||
if not ready:
|
||||
return None
|
||||
candidates = [
|
||||
(queue.items[0].ingress_sequence, modality, queue)
|
||||
for modality, queue in self._queues.items()
|
||||
if queue.items
|
||||
]
|
||||
if not candidates:
|
||||
return None
|
||||
_, _, selected = min(candidates, key=lambda item: item[0])
|
||||
selected.consumed += 1
|
||||
return selected.items.popleft()
|
||||
|
||||
def close(self) -> None:
|
||||
with self._condition:
|
||||
self._closed = True
|
||||
self._active = False
|
||||
self._condition.notify_all()
|
||||
|
||||
def snapshot(self) -> dict[str, Any]:
|
||||
with self._condition:
|
||||
return {
|
||||
"schema_version": LIVE_INGRESS_SCHEMA,
|
||||
"mode": "shadow-diagnostic-only",
|
||||
"active": self._active,
|
||||
"session_id": self._session_id,
|
||||
"consumer_connected": self._consumer_id is not None,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"closed": self._closed,
|
||||
"queues": {
|
||||
modality: {
|
||||
"capacity": queue.capacity,
|
||||
"depth": len(queue.items),
|
||||
"maximum_depth": queue.maximum_depth,
|
||||
"published": queue.published,
|
||||
"consumed": queue.consumed,
|
||||
"dropped_overflow": queue.dropped_overflow,
|
||||
"rejected_oversize": queue.rejected_oversize,
|
||||
}
|
||||
for modality, queue in self._queues.items()
|
||||
},
|
||||
}
|
||||
|
||||
def _publish_locked(
|
||||
self,
|
||||
*,
|
||||
modality: LiveIngressModality,
|
||||
source_id: str,
|
||||
source_sequence: int,
|
||||
captured_at_epoch_ns: int,
|
||||
received_monotonic_ns: int,
|
||||
payload: bytes,
|
||||
) -> bool:
|
||||
queue = self._queues[modality]
|
||||
if len(payload) > self._MAX_PAYLOAD_BYTES[modality]:
|
||||
queue.rejected_oversize += 1
|
||||
return False
|
||||
session_id = self._session_id
|
||||
if session_id is None:
|
||||
return False
|
||||
self._ingress_sequence += 1
|
||||
event = LiveIngressEvent(
|
||||
ingress_sequence=self._ingress_sequence,
|
||||
session_id=session_id,
|
||||
modality=modality,
|
||||
source_id=source_id,
|
||||
source_sequence=source_sequence,
|
||||
captured_at_epoch_ns=captured_at_epoch_ns,
|
||||
received_monotonic_ns=received_monotonic_ns,
|
||||
payload=bytes(payload),
|
||||
)
|
||||
if len(queue.items) == queue.capacity:
|
||||
queue.items.popleft()
|
||||
queue.dropped_overflow += 1
|
||||
queue.items.append(event)
|
||||
queue.published += 1
|
||||
queue.maximum_depth = max(queue.maximum_depth, len(queue.items))
|
||||
self._condition.notify_all()
|
||||
return True
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class QueueSnapshot:
|
||||
capacity: int
|
||||
depth: int
|
||||
maximum_depth: int
|
||||
published: int
|
||||
consumed: int
|
||||
dropped_overflow: int
|
||||
dropped_superseded: int
|
||||
closed: bool
|
||||
|
||||
@property
|
||||
def dropped_total(self) -> int:
|
||||
return self.dropped_overflow + self.dropped_superseded
|
||||
|
||||
|
||||
class LatestWinsQueue[T]:
|
||||
"""A bounded derived-data queue that never lets old preview work accumulate."""
|
||||
|
||||
def __init__(self, capacity: int) -> None:
|
||||
if capacity < 1:
|
||||
raise ValueError("latest-wins queue capacity must be positive")
|
||||
self._capacity = capacity
|
||||
self._items: deque[T] = deque()
|
||||
self._condition = threading.Condition()
|
||||
self._maximum_depth = 0
|
||||
self._published = 0
|
||||
self._consumed = 0
|
||||
self._dropped_overflow = 0
|
||||
self._dropped_superseded = 0
|
||||
self._closed = False
|
||||
|
||||
def publish(self, item: T) -> None:
|
||||
with self._condition:
|
||||
if self._closed:
|
||||
raise RuntimeError("cannot publish to a closed latest-wins queue")
|
||||
if len(self._items) == self._capacity:
|
||||
self._items.popleft()
|
||||
self._dropped_overflow += 1
|
||||
self._items.append(item)
|
||||
self._published += 1
|
||||
self._maximum_depth = max(self._maximum_depth, len(self._items))
|
||||
self._condition.notify()
|
||||
|
||||
def take_next(self, timeout: float | None = None) -> T | None:
|
||||
with self._condition:
|
||||
ready = self._condition.wait_for(
|
||||
lambda: bool(self._items) or self._closed,
|
||||
timeout=timeout,
|
||||
)
|
||||
if not ready or not self._items:
|
||||
return None
|
||||
item = self._items.popleft()
|
||||
self._consumed += 1
|
||||
return item
|
||||
|
||||
def close(self) -> None:
|
||||
with self._condition:
|
||||
self._closed = True
|
||||
self._condition.notify_all()
|
||||
|
||||
def snapshot(self) -> QueueSnapshot:
|
||||
with self._condition:
|
||||
return QueueSnapshot(
|
||||
capacity=self._capacity,
|
||||
depth=len(self._items),
|
||||
maximum_depth=self._maximum_depth,
|
||||
published=self._published,
|
||||
consumed=self._consumed,
|
||||
dropped_overflow=self._dropped_overflow,
|
||||
dropped_superseded=self._dropped_superseded,
|
||||
closed=self._closed,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LiveSensorBinding:
|
||||
"""One bounded camera→LiDAR→pose match for live diagnostic fusion."""
|
||||
|
||||
state: Literal[
|
||||
"fused-ready",
|
||||
"lidar-unavailable",
|
||||
"lidar-camera-delta-exceeded",
|
||||
"pose-unavailable",
|
||||
"pose-point-delta-exceeded",
|
||||
]
|
||||
point_cloud: DecodedPointCloudView | None
|
||||
pose: DecodedPoseView | None
|
||||
lidar_camera_delta_ms: float | None
|
||||
pose_point_delta_ms: float | None
|
||||
|
||||
|
||||
class LiveSensorSynchronizer:
|
||||
"""Keep a small arrival-time window and bind sensors without back-pressure.
|
||||
|
||||
The synchronizer deliberately uses the already-recorded host arrival clock
|
||||
carried by the shadow wire contract. It does not claim hardware-clock
|
||||
synchronization. A short wait budget lets a LiDAR or pose event that is
|
||||
already in flight reach the receiver while keeping the detector/world-state
|
||||
latency bounded.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
maximum_lidar_camera_delta_ms: float,
|
||||
maximum_pose_point_delta_ms: float,
|
||||
capacity_per_modality: int = 32,
|
||||
retention_seconds: float = 3.0,
|
||||
) -> None:
|
||||
if (
|
||||
maximum_lidar_camera_delta_ms <= 0
|
||||
or maximum_pose_point_delta_ms <= 0
|
||||
or capacity_per_modality < 2
|
||||
or retention_seconds <= 0
|
||||
):
|
||||
raise ValueError("live sensor synchronizer bounds are invalid")
|
||||
self._maximum_lidar_camera_delta_ns = round(
|
||||
maximum_lidar_camera_delta_ms * 1_000_000
|
||||
)
|
||||
self._maximum_pose_point_delta_ns = round(maximum_pose_point_delta_ms * 1_000_000)
|
||||
self._capacity = capacity_per_modality
|
||||
self._retention_ns = round(retention_seconds * 1_000_000_000)
|
||||
self._condition = threading.Condition()
|
||||
self._points: deque[DecodedPointCloudView] = deque()
|
||||
self._poses: deque[DecodedPoseView] = deque()
|
||||
self._published_points = 0
|
||||
self._published_poses = 0
|
||||
self._evicted_points = 0
|
||||
self._evicted_poses = 0
|
||||
self._maximum_point_depth = 0
|
||||
self._maximum_pose_depth = 0
|
||||
|
||||
def publish_point_cloud(self, value: DecodedPointCloudView) -> None:
|
||||
with self._condition:
|
||||
self._points.append(value)
|
||||
self._published_points += 1
|
||||
self._evicted_points += self._prune(self._points)
|
||||
self._maximum_point_depth = max(self._maximum_point_depth, len(self._points))
|
||||
self._condition.notify_all()
|
||||
|
||||
def publish_pose(self, value: DecodedPoseView) -> None:
|
||||
with self._condition:
|
||||
self._poses.append(value)
|
||||
self._published_poses += 1
|
||||
self._evicted_poses += self._prune(self._poses)
|
||||
self._maximum_pose_depth = max(self._maximum_pose_depth, len(self._poses))
|
||||
self._condition.notify_all()
|
||||
|
||||
def bind_camera(
|
||||
self,
|
||||
captured_at_epoch_ns: int,
|
||||
*,
|
||||
wait_seconds: float = 0.0,
|
||||
) -> LiveSensorBinding:
|
||||
if captured_at_epoch_ns < 0 or wait_seconds < 0 or not math.isfinite(wait_seconds):
|
||||
raise ValueError("camera synchronization input is invalid")
|
||||
deadline = time.monotonic() + wait_seconds
|
||||
with self._condition:
|
||||
while True:
|
||||
binding = self._binding(captured_at_epoch_ns)
|
||||
if binding.state == "fused-ready" or wait_seconds == 0:
|
||||
return binding
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
return binding
|
||||
self._condition.wait(timeout=remaining)
|
||||
|
||||
def snapshot(self) -> dict[str, int | float]:
|
||||
with self._condition:
|
||||
return {
|
||||
"capacity_per_modality": self._capacity,
|
||||
"retention_seconds": self._retention_ns / 1_000_000_000,
|
||||
"point_depth": len(self._points),
|
||||
"pose_depth": len(self._poses),
|
||||
"maximum_point_depth": self._maximum_point_depth,
|
||||
"maximum_pose_depth": self._maximum_pose_depth,
|
||||
"published_points": self._published_points,
|
||||
"published_poses": self._published_poses,
|
||||
"evicted_points": self._evicted_points,
|
||||
"evicted_poses": self._evicted_poses,
|
||||
}
|
||||
|
||||
def _binding(self, captured_at_epoch_ns: int) -> LiveSensorBinding:
|
||||
if not self._points:
|
||||
return LiveSensorBinding("lidar-unavailable", None, None, None, None)
|
||||
point = min(
|
||||
self._points,
|
||||
key=lambda value: abs(value.context.captured_at_epoch_ns - captured_at_epoch_ns),
|
||||
)
|
||||
lidar_delta_ns = point.context.captured_at_epoch_ns - captured_at_epoch_ns
|
||||
lidar_delta_ms = lidar_delta_ns / 1_000_000
|
||||
if abs(lidar_delta_ns) > self._maximum_lidar_camera_delta_ns:
|
||||
return LiveSensorBinding(
|
||||
"lidar-camera-delta-exceeded",
|
||||
point,
|
||||
None,
|
||||
lidar_delta_ms,
|
||||
None,
|
||||
)
|
||||
if not self._poses:
|
||||
return LiveSensorBinding(
|
||||
"pose-unavailable",
|
||||
point,
|
||||
None,
|
||||
lidar_delta_ms,
|
||||
None,
|
||||
)
|
||||
pose = min(
|
||||
self._poses,
|
||||
key=lambda value: abs(
|
||||
value.context.captured_at_epoch_ns - point.context.captured_at_epoch_ns
|
||||
),
|
||||
)
|
||||
pose_delta_ns = pose.context.captured_at_epoch_ns - point.context.captured_at_epoch_ns
|
||||
pose_delta_ms = pose_delta_ns / 1_000_000
|
||||
if abs(pose_delta_ns) > self._maximum_pose_point_delta_ns:
|
||||
return LiveSensorBinding(
|
||||
"pose-point-delta-exceeded",
|
||||
point,
|
||||
pose,
|
||||
lidar_delta_ms,
|
||||
pose_delta_ms,
|
||||
)
|
||||
return LiveSensorBinding(
|
||||
"fused-ready",
|
||||
point,
|
||||
pose,
|
||||
lidar_delta_ms,
|
||||
pose_delta_ms,
|
||||
)
|
||||
|
||||
def _prune(self, values: deque[DecodedPointCloudView] | deque[DecodedPoseView]) -> int:
|
||||
removed = 0
|
||||
newest = values[-1].context.captured_at_epoch_ns
|
||||
oldest_allowed = newest - self._retention_ns
|
||||
while values and (
|
||||
len(values) > self._capacity
|
||||
or values[0].context.captured_at_epoch_ns < oldest_allowed
|
||||
):
|
||||
values.popleft()
|
||||
removed += 1
|
||||
return removed
|
||||
|
||||
|
||||
def classify_health(
|
||||
*,
|
||||
source_available: bool,
|
||||
fusion_state: str,
|
||||
result_age_ms: float,
|
||||
stale_after_ms: float,
|
||||
unavailable_after_ms: float,
|
||||
) -> tuple[HealthState, tuple[str, ...]]:
|
||||
"""Classify freshness separately from whether depth was available."""
|
||||
|
||||
if stale_after_ms <= 0 or unavailable_after_ms <= stale_after_ms:
|
||||
raise ValueError("health thresholds are invalid")
|
||||
if not source_available or result_age_ms >= unavailable_after_ms:
|
||||
return "unavailable", ("source-unavailable",)
|
||||
if result_age_ms >= stale_after_ms:
|
||||
return "stale", ("result-age-exceeded",)
|
||||
if fusion_state != "fused":
|
||||
return "degraded", (fusion_state,)
|
||||
return "healthy", ()
|
||||
|
||||
|
||||
class WorldStateProjector:
|
||||
"""Project accepted E6 observations into a control-facing, timestamped state."""
|
||||
|
||||
def __init__(self, *, velocity_history_limit_s: float = 1.0) -> None:
|
||||
if velocity_history_limit_s <= 0:
|
||||
raise ValueError("velocity history limit must be positive")
|
||||
self._velocity_history_limit_s = velocity_history_limit_s
|
||||
self._track_history: dict[
|
||||
int, deque[tuple[float, tuple[float, float, float]]]
|
||||
] = {}
|
||||
|
||||
def project(
|
||||
self,
|
||||
*,
|
||||
frame: Mapping[str, Any],
|
||||
lidar_positions: Mapping[int, Sequence[float]],
|
||||
clearance: Mapping[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
session_seconds = float(frame["session_seconds"])
|
||||
objects: list[dict[str, Any]] = []
|
||||
for raw in frame.get("objects", []):
|
||||
if not str(raw.get("cuboid_status", "")).startswith("accepted-"):
|
||||
continue
|
||||
track_id = int(raw["track_id"])
|
||||
center_map = _vector3(raw["cuboid_center_map"], "cuboid center")
|
||||
half_size = _vector3(raw["cuboid_half_size"], "cuboid half-size")
|
||||
quaternion = _vector4(raw["cuboid_quaternion_xyzw"], "cuboid quaternion")
|
||||
velocity, velocity_status, velocity_residual = self._velocity(
|
||||
track_id, session_seconds, center_map
|
||||
)
|
||||
speed = None
|
||||
if velocity is not None:
|
||||
speed = math.sqrt(sum(component * component for component in velocity))
|
||||
lidar = lidar_positions.get(track_id)
|
||||
objects.append(
|
||||
{
|
||||
"track_id": track_id,
|
||||
"class": str(raw["association_group"]),
|
||||
"detector_label": str(raw["label"]),
|
||||
"confidence": float(raw["score"]),
|
||||
"position_map_m": list(center_map),
|
||||
"position_lidar_m": (
|
||||
None if lidar is None else [float(value) for value in lidar]
|
||||
),
|
||||
"orientation_map_xyzw": list(quaternion),
|
||||
"size_m": [2.0 * value for value in half_size],
|
||||
"range_m": float(raw["distance_smoothed_m"]),
|
||||
"velocity_map_mps": None if velocity is None else list(velocity),
|
||||
"speed_mps": speed,
|
||||
"velocity_status": velocity_status,
|
||||
"velocity_residual_m": velocity_residual,
|
||||
"support_points": int(raw["clustered_points"]),
|
||||
"geometry": "point-supported-visible-surface-envelope",
|
||||
}
|
||||
)
|
||||
self._prune(session_seconds)
|
||||
return {
|
||||
"schema_version": WORLD_STATE_SCHEMA,
|
||||
"frame_index": int(frame["frame_index"]),
|
||||
"source_frame_index": int(frame["source_frame_index"]),
|
||||
"session_seconds": session_seconds,
|
||||
"coordinate_frames": {
|
||||
"world": "k1-map",
|
||||
"sensor_relative": "k1-lidar",
|
||||
"vehicle_body": "unavailable-no-rig-to-vehicle-transform",
|
||||
},
|
||||
"fusion_state": str(frame["state"]),
|
||||
"objects": objects,
|
||||
"object_count": len(objects),
|
||||
"clearance": dict(clearance),
|
||||
}
|
||||
|
||||
def _velocity(
|
||||
self,
|
||||
track_id: int,
|
||||
session_seconds: float,
|
||||
center_map: tuple[float, float, float],
|
||||
) -> tuple[tuple[float, float, float] | None, str, float | None]:
|
||||
history = self._track_history.setdefault(track_id, deque(maxlen=32))
|
||||
if history and session_seconds <= history[-1][0]:
|
||||
return None, "unavailable-nonmonotonic-time", None
|
||||
history.append((session_seconds, center_map))
|
||||
oldest = session_seconds - self._velocity_history_limit_s
|
||||
while history and history[0][0] < oldest:
|
||||
history.popleft()
|
||||
if len(history) < 4 or history[-1][0] - history[0][0] < 0.4:
|
||||
return None, "unavailable-insufficient-history", None
|
||||
slopes: list[tuple[float, float, float]] = []
|
||||
values = list(history)
|
||||
for left, (left_time, left_center) in enumerate(values):
|
||||
for right_time, right_center in values[left + 1 :]:
|
||||
delta = right_time - left_time
|
||||
if delta < 0.2:
|
||||
continue
|
||||
slopes.append(
|
||||
(
|
||||
(right_center[0] - left_center[0]) / delta,
|
||||
(right_center[1] - left_center[1]) / delta,
|
||||
(right_center[2] - left_center[2]) / delta,
|
||||
)
|
||||
)
|
||||
if not slopes:
|
||||
return None, "unavailable-insufficient-baseline", None
|
||||
velocity = (
|
||||
statistics.median(item[0] for item in slopes),
|
||||
statistics.median(item[1] for item in slopes),
|
||||
statistics.median(item[2] for item in slopes),
|
||||
)
|
||||
speed = math.sqrt(sum(component * component for component in velocity))
|
||||
latest_time, latest_center = values[-1]
|
||||
residuals = []
|
||||
for observed_time, observed_center in values:
|
||||
predicted = tuple(
|
||||
latest - component * (latest_time - observed_time)
|
||||
for latest, component in zip(latest_center, velocity, strict=True)
|
||||
)
|
||||
residuals.append(
|
||||
math.sqrt(
|
||||
sum(
|
||||
(observed - expected) ** 2
|
||||
for observed, expected in zip(
|
||||
observed_center, predicted, strict=True
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
residual = statistics.median(residuals)
|
||||
if speed > 20.0:
|
||||
return None, "rejected-speed-bound", residual
|
||||
if residual > 0.75:
|
||||
return None, "rejected-position-residual", residual
|
||||
return velocity, "diagnostic-robust-history", residual
|
||||
|
||||
def _prune(self, session_seconds: float) -> None:
|
||||
oldest = session_seconds - self._velocity_history_limit_s
|
||||
expired = [
|
||||
track_id
|
||||
for track_id, history in self._track_history.items()
|
||||
if not history or history[-1][0] < oldest
|
||||
]
|
||||
for track_id in expired:
|
||||
del self._track_history[track_id]
|
||||
|
||||
|
||||
def wait_until(deadline: float) -> float:
|
||||
"""Wait for a replay deadline and return non-negative scheduling lag seconds."""
|
||||
|
||||
remaining = deadline - time.perf_counter()
|
||||
if remaining > 0:
|
||||
time.sleep(remaining)
|
||||
return max(0.0, time.perf_counter() - deadline)
|
||||
|
||||
|
||||
def _vector3(value: Sequence[Any], label: str) -> tuple[float, float, float]:
|
||||
if len(value) != 3:
|
||||
raise ValueError(f"{label} must contain three values")
|
||||
return float(value[0]), float(value[1]), float(value[2])
|
||||
|
||||
|
||||
def _vector4(value: Sequence[Any], label: str) -> tuple[float, float, float, float]:
|
||||
if len(value) != 4:
|
||||
raise ValueError(f"{label} must contain four values")
|
||||
return float(value[0]), float(value[1]), float(value[2]), float(value[3])
|
||||
@@ -0,0 +1,368 @@
|
||||
"""Validation for immutable LAB E7 replay-as-live qualification results."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
|
||||
from .live_perception import TELEMETRY_SCHEMA, WORLD_STATE_SCHEMA
|
||||
from .tracked_fusion_qualification import (
|
||||
TrackedFusionQualificationResult,
|
||||
validate_tracked_fusion_qualification_result,
|
||||
)
|
||||
|
||||
RESULT_SCHEMA = "missioncore.e7-live-replay-result/v1"
|
||||
IDENTITY_SCHEMA = "missioncore.e7-live-replay-identity/v1"
|
||||
REPORT_SCHEMA = "missioncore.e7-live-replay-run-report/v1"
|
||||
MAX_JSON_BYTES = 64 * 1024 * 1024
|
||||
MAX_LINE_BYTES = 2 * 1024 * 1024
|
||||
|
||||
_SAFE_RESULT_ID = re.compile(r"^e7-live-replay-[a-f0-9]{64}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LiveReplayQualificationResult:
|
||||
result_id: str
|
||||
result_root: Path
|
||||
upstream: TrackedFusionQualificationResult
|
||||
frame_count: int
|
||||
accepted: bool
|
||||
artifacts: tuple[dict[str, Any], ...]
|
||||
|
||||
|
||||
def validate_live_replay_qualification_result(
|
||||
job_root: Path,
|
||||
tracking_root: Path,
|
||||
semantic_root: Path,
|
||||
e6_root: Path,
|
||||
result_root: Path,
|
||||
) -> LiveReplayQualificationResult:
|
||||
upstream = validate_tracked_fusion_qualification_result(
|
||||
job_root,
|
||||
tracking_root,
|
||||
semantic_root,
|
||||
e6_root,
|
||||
)
|
||||
root = result_root.expanduser().resolve(strict=True)
|
||||
if not root.is_dir() or _SAFE_RESULT_ID.fullmatch(root.name) is None:
|
||||
raise SessionIntegrityError("live replay result root is invalid")
|
||||
result = _read_object(root / "result.json", root)
|
||||
identity = result.get("identity")
|
||||
identity_sha256 = result.get("identity_sha256")
|
||||
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"e7-live-replay-{identity_sha256}"
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or identity.get("e6_result_id") != upstream.result_id
|
||||
or identity.get("job_id") != upstream.job.job_id
|
||||
or identity.get("input_sha256") != upstream.job.input_sha256
|
||||
or identity.get("session_id") != upstream.job.session_id
|
||||
or identity.get("source_id") != upstream.job.source_id
|
||||
or identity.get("calibration_sha256") != upstream.semantic.calibration_sha256
|
||||
or identity.get("camera_slot") != upstream.semantic.calibration_slot
|
||||
or not _valid_sha256(identity.get("producer_sha256"))
|
||||
or not _valid_sha256(identity.get("runtime_contract_sha256"))
|
||||
or not _valid_sha256(identity.get("result_validator_sha256"))
|
||||
or not _valid_sha256(identity.get("e6_adapter_sha256"))
|
||||
or result.get("ground_truth") is not False
|
||||
or result.get("publication_scope") != "replay-qualification-only"
|
||||
or result.get("acceptance_state") not in {"accepted", "rejected"}
|
||||
):
|
||||
raise SessionIntegrityError("live replay identity is inconsistent")
|
||||
selection = identity.get("selection")
|
||||
configuration = identity.get("configuration")
|
||||
if not isinstance(selection, dict) or not isinstance(configuration, dict):
|
||||
raise SessionIntegrityError("live replay configuration is missing")
|
||||
start = selection.get("start_frame_index")
|
||||
count = selection.get("frame_count")
|
||||
if (
|
||||
not isinstance(start, int)
|
||||
or not isinstance(count, int)
|
||||
or start < 0
|
||||
or count < 1
|
||||
or start + count > upstream.frame_count
|
||||
or configuration.get("selection") != selection
|
||||
):
|
||||
raise SessionIntegrityError("live replay selection is invalid")
|
||||
metrics = result.get("metrics")
|
||||
if not isinstance(metrics, dict):
|
||||
raise SessionIntegrityError("live replay metrics are invalid")
|
||||
processed = metrics.get("frames_processed")
|
||||
dropped = metrics.get("frames_dropped")
|
||||
queue = metrics.get("queue")
|
||||
health = metrics.get("health_counts")
|
||||
if (
|
||||
metrics.get("frames_attempted") != count
|
||||
or not isinstance(processed, int)
|
||||
or not isinstance(dropped, int)
|
||||
or processed < 1
|
||||
or dropped < 0
|
||||
or processed + dropped != count
|
||||
or not isinstance(queue, dict)
|
||||
or queue.get("published") != count
|
||||
or queue.get("consumed") != processed
|
||||
or queue.get("final_depth") != 0
|
||||
or queue.get("dropped_overflow", 0) + queue.get("dropped_superseded", 0)
|
||||
!= dropped
|
||||
or not isinstance(health, dict)
|
||||
or sum(health.values()) != processed
|
||||
):
|
||||
raise SessionIntegrityError("live replay accounting is inconsistent")
|
||||
artifacts = _validate_artifacts(root, result.get("artifacts"))
|
||||
world_indices = _validate_world_rows(
|
||||
_artifact_path(artifacts, "live-world-state", root),
|
||||
root,
|
||||
expected_count=processed,
|
||||
selection_start=start,
|
||||
selection_count=count,
|
||||
)
|
||||
_validate_telemetry_rows(
|
||||
_artifact_path(artifacts, "live-telemetry", root),
|
||||
root,
|
||||
expected_indices=world_indices,
|
||||
)
|
||||
report = _read_object(_artifact_path(artifacts, "live-run-report", root), root)
|
||||
acceptance = report.get("acceptance")
|
||||
if (
|
||||
report.get("schema_version") != REPORT_SCHEMA
|
||||
or report.get("result_id") != root.name
|
||||
or report.get("identity") != identity
|
||||
or report.get("metrics") != metrics
|
||||
or not isinstance(acceptance, dict)
|
||||
or not isinstance(acceptance.get("accepted"), bool)
|
||||
or acceptance.get("navigation_or_safety_accepted") is not False
|
||||
or (result.get("acceptance_state") == "accepted")
|
||||
!= acceptance.get("accepted")
|
||||
):
|
||||
raise SessionIntegrityError("live replay report is inconsistent")
|
||||
rrd = _artifact_path(artifacts, "live-rerun", root)
|
||||
with rrd.open("rb") as stream:
|
||||
if stream.read(4) != b"RRF2":
|
||||
raise SessionIntegrityError("live replay Rerun stream is invalid")
|
||||
return LiveReplayQualificationResult(
|
||||
result_id=root.name,
|
||||
result_root=root,
|
||||
upstream=upstream,
|
||||
frame_count=processed,
|
||||
accepted=bool(acceptance["accepted"]),
|
||||
artifacts=artifacts,
|
||||
)
|
||||
|
||||
|
||||
def _validate_artifacts(root: Path, raw: object) -> tuple[dict[str, Any], ...]:
|
||||
expected = {
|
||||
"live-world-state": (
|
||||
"world-state.jsonl",
|
||||
"application/x-ndjson",
|
||||
WORLD_STATE_SCHEMA,
|
||||
),
|
||||
"live-telemetry": (
|
||||
"telemetry.jsonl",
|
||||
"application/x-ndjson",
|
||||
TELEMETRY_SCHEMA,
|
||||
),
|
||||
"live-rerun": ("world-state.rrd", "application/vnd.rerun.rrd", None),
|
||||
"live-run-report": ("run-report.json", "application/json", REPORT_SCHEMA),
|
||||
}
|
||||
if not isinstance(raw, list) or len(raw) != len(expected):
|
||||
raise SessionIntegrityError("live replay artifact set is invalid")
|
||||
artifacts: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for value in raw:
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("live replay artifact descriptor is invalid")
|
||||
kind = value.get("kind")
|
||||
if not isinstance(kind, str) or kind in seen or kind not in expected:
|
||||
raise SessionIntegrityError("live replay artifact kind is invalid")
|
||||
seen.add(kind)
|
||||
name, media_type, schema = expected[kind]
|
||||
path = root / name
|
||||
metadata = _confined_regular_file(path, root)
|
||||
if (
|
||||
value.get("path") != name
|
||||
or value.get("media_type") != media_type
|
||||
or value.get("schema_version") != schema
|
||||
or value.get("byte_length") != metadata.st_size
|
||||
or not isinstance(value.get("sha256"), str)
|
||||
or _SHA256.fullmatch(str(value["sha256"])) is None
|
||||
or _sha256(path) != value["sha256"]
|
||||
):
|
||||
raise SessionIntegrityError("live replay artifact identity changed")
|
||||
artifacts.append(value)
|
||||
return tuple(artifacts)
|
||||
|
||||
|
||||
def _validate_world_rows(
|
||||
path: Path,
|
||||
root: Path,
|
||||
*,
|
||||
expected_count: int,
|
||||
selection_start: int,
|
||||
selection_count: int,
|
||||
) -> tuple[int, ...]:
|
||||
indices: list[int] = []
|
||||
previous_time: float | None = None
|
||||
_confined_regular_file(path, root)
|
||||
with path.open(encoding="utf-8") as stream:
|
||||
for line in stream:
|
||||
if len(line.encode()) > MAX_LINE_BYTES:
|
||||
raise SessionIntegrityError("live replay world-state line is oversized")
|
||||
value = json.loads(line)
|
||||
frame_index = value.get("frame_index") if isinstance(value, dict) else None
|
||||
session_seconds = value.get("session_seconds") if isinstance(value, dict) else None
|
||||
objects = value.get("objects") if isinstance(value, dict) else None
|
||||
delivery = value.get("delivery") if isinstance(value, dict) else None
|
||||
coordinate_frames = (
|
||||
value.get("coordinate_frames") if isinstance(value, dict) else None
|
||||
)
|
||||
if (
|
||||
not isinstance(value, dict)
|
||||
or value.get("schema_version") != WORLD_STATE_SCHEMA
|
||||
or not isinstance(frame_index, int)
|
||||
or not selection_start <= frame_index < selection_start + selection_count
|
||||
or (indices and frame_index <= indices[-1])
|
||||
or not isinstance(session_seconds, int | float)
|
||||
or (previous_time is not None and float(session_seconds) <= previous_time)
|
||||
or not isinstance(objects, list)
|
||||
or value.get("object_count") != len(objects)
|
||||
or not isinstance(delivery, dict)
|
||||
or delivery.get("health")
|
||||
not in {"healthy", "degraded", "stale", "unavailable"}
|
||||
or not isinstance(delivery.get("result_age_ms"), int | float)
|
||||
or not isinstance(coordinate_frames, dict)
|
||||
or coordinate_frames.get("sensor_relative") != "k1-lidar"
|
||||
):
|
||||
raise SessionIntegrityError("live replay world-state row is invalid")
|
||||
for item in objects:
|
||||
if not _valid_object(item):
|
||||
raise SessionIntegrityError("live replay world object is invalid")
|
||||
indices.append(frame_index)
|
||||
previous_time = float(session_seconds)
|
||||
if len(indices) != expected_count:
|
||||
raise SessionIntegrityError("live replay world-state count changed")
|
||||
return tuple(indices)
|
||||
|
||||
|
||||
def _valid_object(value: object) -> bool:
|
||||
if not isinstance(value, dict):
|
||||
return False
|
||||
position_map = value.get("position_map_m")
|
||||
position_lidar = value.get("position_lidar_m")
|
||||
size = value.get("size_m")
|
||||
velocity = value.get("velocity_map_mps")
|
||||
return (
|
||||
isinstance(value.get("track_id"), int)
|
||||
and isinstance(value.get("range_m"), int | float)
|
||||
and float(value["range_m"]) >= 0
|
||||
and _finite_vector(position_map, 3)
|
||||
and (position_lidar is None or _finite_vector(position_lidar, 3))
|
||||
and _finite_vector(size, 3)
|
||||
and (velocity is None or _finite_vector(velocity, 3))
|
||||
and isinstance(value.get("velocity_status"), str)
|
||||
and value.get("geometry") == "point-supported-visible-surface-envelope"
|
||||
)
|
||||
|
||||
|
||||
def _finite_vector(value: object, length: int) -> bool:
|
||||
return (
|
||||
isinstance(value, list)
|
||||
and len(value) == length
|
||||
and all(isinstance(item, int | float) and math.isfinite(float(item)) for item in value)
|
||||
)
|
||||
|
||||
|
||||
def _valid_sha256(value: object) -> bool:
|
||||
return isinstance(value, str) and _SHA256.fullmatch(value) is not None
|
||||
|
||||
|
||||
def _validate_telemetry_rows(
|
||||
path: Path,
|
||||
root: Path,
|
||||
*,
|
||||
expected_indices: tuple[int, ...],
|
||||
) -> None:
|
||||
indices: list[int] = []
|
||||
_confined_regular_file(path, root)
|
||||
with path.open(encoding="utf-8") as stream:
|
||||
for line in stream:
|
||||
if len(line.encode()) > MAX_LINE_BYTES:
|
||||
raise SessionIntegrityError("live replay telemetry line is oversized")
|
||||
value = json.loads(line)
|
||||
if (
|
||||
not isinstance(value, dict)
|
||||
or value.get("schema_version") != TELEMETRY_SCHEMA
|
||||
or not isinstance(value.get("frame_index"), int)
|
||||
or not isinstance(value.get("end_to_end_latency_ms"), int | float)
|
||||
or float(value["end_to_end_latency_ms"]) < 0
|
||||
or value.get("health")
|
||||
not in {"healthy", "degraded", "stale", "unavailable"}
|
||||
):
|
||||
raise SessionIntegrityError("live replay telemetry row is invalid")
|
||||
indices.append(int(value["frame_index"]))
|
||||
if tuple(indices) != expected_indices:
|
||||
raise SessionIntegrityError("live replay telemetry binding changed")
|
||||
|
||||
|
||||
def _artifact_path(
|
||||
artifacts: tuple[dict[str, Any], ...], kind: str, root: Path
|
||||
) -> Path:
|
||||
matches = [value for value in artifacts if value.get("kind") == kind]
|
||||
if len(matches) != 1 or not isinstance(matches[0].get("path"), str):
|
||||
raise SessionIntegrityError("live replay artifact lookup is invalid")
|
||||
return root / str(matches[0]["path"])
|
||||
|
||||
|
||||
def _read_object(path: Path, root: Path) -> dict[str, Any]:
|
||||
metadata = _confined_regular_file(path, root)
|
||||
if metadata.st_size > MAX_JSON_BYTES:
|
||||
raise SessionIntegrityError("live replay JSON is oversized")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("live replay JSON is unreadable") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("live replay JSON root must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _confined_regular_file(path: Path, root: Path) -> os.stat_result:
|
||||
resolved = path.resolve(strict=True)
|
||||
if not resolved.is_relative_to(root) or not resolved.is_file():
|
||||
raise SessionIntegrityError("live replay artifact escapes its result root")
|
||||
metadata = resolved.stat()
|
||||
if stat.S_ISLNK(path.lstat().st_mode) or not stat.S_ISREG(metadata.st_mode):
|
||||
raise SessionIntegrityError("live replay artifact must be a regular file")
|
||||
return metadata
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
@@ -0,0 +1,534 @@
|
||||
"""Validation for immutable LAB E9 multirate perception results."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeGuard
|
||||
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
|
||||
from .jobs import CameraComputeJob, validate_camera_compute_job
|
||||
|
||||
RESULT_SCHEMA = "missioncore.e9-multirate-perception-result/v1"
|
||||
IDENTITY_SCHEMA = "missioncore.e9-multirate-perception-identity/v1"
|
||||
REPORT_SCHEMA = "missioncore.e9-multirate-perception-report/v1"
|
||||
PROFILE_SCHEMA = "missioncore.e9-multirate-perception-profile/v1"
|
||||
DETECTOR_FRAME_SCHEMA = "missioncore.e9-multirate-detector-frame/v1"
|
||||
SEMANTIC_FRAME_SCHEMA = "missioncore.e9-multirate-semantic-frame/v1"
|
||||
MERGED_FRAME_SCHEMA = "missioncore.e9-multirate-merged-frame/v1"
|
||||
MAX_JSON_BYTES = 64 * 1024 * 1024
|
||||
MAX_LINE_BYTES = 2 * 1024 * 1024
|
||||
MAX_SAFE_INTEGER = (1 << 53) - 1
|
||||
|
||||
_SAFE_RESULT_ID = re.compile(r"^e9-multirate-perception-[a-f0-9]{64}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MultiratePerceptionArtifact:
|
||||
kind: str
|
||||
path: Path
|
||||
media_type: str
|
||||
byte_length: int
|
||||
sha256: str
|
||||
schema_version: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MultiratePerceptionQualificationResult:
|
||||
result_id: str
|
||||
result_root: Path
|
||||
job: CameraComputeJob
|
||||
expected_detector_frames: int
|
||||
processed_detector_frames: int
|
||||
processed_semantic_frames: int
|
||||
accepted: bool
|
||||
artifacts: tuple[MultiratePerceptionArtifact, ...]
|
||||
|
||||
def artifact(self, kind: str) -> MultiratePerceptionArtifact:
|
||||
matches = tuple(item for item in self.artifacts if item.kind == kind)
|
||||
if len(matches) != 1:
|
||||
raise SessionIntegrityError(f"multirate artifact is unavailable: {kind}")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def validate_multirate_perception_qualification_result(
|
||||
job_root: Path,
|
||||
result_root: Path,
|
||||
) -> MultiratePerceptionQualificationResult:
|
||||
job = validate_camera_compute_job(job_root)
|
||||
root = result_root.expanduser().resolve(strict=True)
|
||||
if not root.is_dir() or _SAFE_RESULT_ID.fullmatch(root.name) is None:
|
||||
raise SessionIntegrityError("multirate result root is invalid")
|
||||
result = _read_object(root / "result.json", root)
|
||||
identity = result.get("identity")
|
||||
identity_sha256 = result.get("identity_sha256")
|
||||
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"e9-multirate-perception-{identity_sha256}"
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or identity.get("job_id") != job.job_id
|
||||
or identity.get("input_sha256") != job.input_sha256
|
||||
or identity.get("session_id") != job.session_id
|
||||
or identity.get("source_id") != job.source_id
|
||||
or result.get("ground_truth") is not False
|
||||
or result.get("publication_scope") != "recorded-multirate-qualification-only"
|
||||
or result.get("acceptance_state") not in {"accepted", "rejected"}
|
||||
):
|
||||
raise SessionIntegrityError("multirate identity is inconsistent")
|
||||
selection = identity.get("selection")
|
||||
configuration = identity.get("configuration")
|
||||
if not isinstance(selection, dict) or not isinstance(configuration, dict):
|
||||
raise SessionIntegrityError("multirate selection is unavailable")
|
||||
expected = selection.get("frame_count")
|
||||
source_start = selection.get("source_start_frame_index")
|
||||
source_end = selection.get("source_end_frame_index")
|
||||
timeline_start = selection.get("timeline_start_seconds")
|
||||
timeline_end = selection.get("timeline_end_seconds")
|
||||
profile = configuration.get("profile")
|
||||
if (
|
||||
not isinstance(expected, int)
|
||||
or isinstance(expected, bool)
|
||||
or expected < 2
|
||||
or not isinstance(source_start, int)
|
||||
or isinstance(source_start, bool)
|
||||
or not isinstance(source_end, int)
|
||||
or isinstance(source_end, bool)
|
||||
or source_start < 0
|
||||
or source_end - source_start + 1 != expected
|
||||
or source_end >= job.segment_count
|
||||
or not _finite_number(timeline_start)
|
||||
or not _finite_number(timeline_end)
|
||||
or float(timeline_start) < job.timeline_start_seconds
|
||||
or float(timeline_end) > job.timeline_end_seconds
|
||||
or float(timeline_end) <= float(timeline_start)
|
||||
or not isinstance(profile, dict)
|
||||
or profile.get("schema_version") != PROFILE_SCHEMA
|
||||
or not isinstance(profile.get("replay"), dict)
|
||||
):
|
||||
raise SessionIntegrityError("multirate selection is invalid")
|
||||
replay = profile["replay"]
|
||||
stride = replay.get("semantic_sample_every_frames")
|
||||
if (
|
||||
not isinstance(stride, int)
|
||||
or isinstance(stride, bool)
|
||||
or stride < 2
|
||||
or replay.get("detector_queue_capacity") not in range(1, 9)
|
||||
or replay.get("semantic_queue_capacity") not in range(1, 5)
|
||||
):
|
||||
raise SessionIntegrityError("multirate scheduling profile is invalid")
|
||||
|
||||
artifacts = _validate_artifacts(root, result.get("artifacts"))
|
||||
report = _read_object(_artifact(artifacts, "multirate-run-report").path, root)
|
||||
metrics = report.get("metrics")
|
||||
acceptance = report.get("acceptance")
|
||||
if (
|
||||
report.get("schema_version") != REPORT_SCHEMA
|
||||
or report.get("result_id") != root.name
|
||||
or report.get("identity") != identity
|
||||
or report.get("ground_truth") is not False
|
||||
or report.get("state") != result.get("acceptance_state")
|
||||
or not isinstance(metrics, dict)
|
||||
or not isinstance(acceptance, dict)
|
||||
or not isinstance(acceptance.get("accepted"), bool)
|
||||
or acceptance.get("navigation_or_safety_accepted") is not False
|
||||
or (result.get("acceptance_state") == "accepted") != acceptance["accepted"]
|
||||
):
|
||||
raise SessionIntegrityError("multirate report is inconsistent")
|
||||
detector = metrics.get("detector")
|
||||
semantic = metrics.get("semantic")
|
||||
if not isinstance(detector, dict) or not isinstance(semantic, dict):
|
||||
raise SessionIntegrityError("multirate metrics are unavailable")
|
||||
detector_processed, detector_dropped = _validate_queue_metrics(
|
||||
detector,
|
||||
expected=expected,
|
||||
capacity=int(replay["detector_queue_capacity"]),
|
||||
expected_key="frames_expected",
|
||||
)
|
||||
semantic_scheduled = ((expected - 1) // stride) + 1
|
||||
semantic_processed, _semantic_dropped = _validate_queue_metrics(
|
||||
semantic,
|
||||
expected=semantic_scheduled,
|
||||
capacity=int(replay["semantic_queue_capacity"]),
|
||||
expected_key="frames_scheduled",
|
||||
)
|
||||
if (
|
||||
result.get("frames_processed") != detector_processed
|
||||
or detector_processed + detector_dropped != expected
|
||||
or not isinstance(semantic.get("binding_status_counts"), dict)
|
||||
or sum(semantic["binding_status_counts"].values()) != detector_processed
|
||||
):
|
||||
raise SessionIntegrityError("multirate result accounting is inconsistent")
|
||||
|
||||
detector_indices = _validate_detector_rows(
|
||||
_artifact(artifacts, "multirate-detector-frames").path,
|
||||
root,
|
||||
expected_count=detector_processed,
|
||||
selection_count=expected,
|
||||
source_start=source_start,
|
||||
timeline_start=float(timeline_start),
|
||||
timeline_end=float(timeline_end),
|
||||
)
|
||||
semantic_indices = _validate_semantic_rows(
|
||||
_artifact(artifacts, "multirate-semantic-frames").path,
|
||||
root,
|
||||
expected_count=semantic_processed,
|
||||
selection_count=expected,
|
||||
source_start=source_start,
|
||||
stride=stride,
|
||||
timeline_start=float(timeline_start),
|
||||
timeline_end=float(timeline_end),
|
||||
)
|
||||
_validate_merged_rows(
|
||||
_artifact(artifacts, "multirate-merged-frames").path,
|
||||
root,
|
||||
expected_indices=detector_indices,
|
||||
source_start=source_start,
|
||||
semantic_indices=set(semantic_indices),
|
||||
)
|
||||
return MultiratePerceptionQualificationResult(
|
||||
result_id=root.name,
|
||||
result_root=root,
|
||||
job=job,
|
||||
expected_detector_frames=expected,
|
||||
processed_detector_frames=detector_processed,
|
||||
processed_semantic_frames=semantic_processed,
|
||||
accepted=bool(acceptance["accepted"]),
|
||||
artifacts=artifacts,
|
||||
)
|
||||
|
||||
|
||||
def _validate_queue_metrics(
|
||||
metrics: dict[str, Any],
|
||||
*,
|
||||
expected: int,
|
||||
capacity: int,
|
||||
expected_key: str,
|
||||
) -> tuple[int, int]:
|
||||
processed = metrics.get("frames_processed")
|
||||
dropped = metrics.get("frames_dropped")
|
||||
queue = metrics.get("queue")
|
||||
if (
|
||||
metrics.get(expected_key) != expected
|
||||
or not isinstance(processed, int)
|
||||
or isinstance(processed, bool)
|
||||
or not 1 <= processed <= expected
|
||||
or not isinstance(dropped, int)
|
||||
or isinstance(dropped, bool)
|
||||
or dropped < 0
|
||||
or processed + dropped != expected
|
||||
or not isinstance(queue, dict)
|
||||
or queue.get("capacity") != capacity
|
||||
or queue.get("published") != expected
|
||||
or queue.get("consumed") != processed
|
||||
or queue.get("dropped_overflow") != dropped
|
||||
or queue.get("final_depth") != 0
|
||||
or queue.get("closed") is not True
|
||||
or not isinstance(queue.get("maximum_depth"), int)
|
||||
or not 0 <= queue["maximum_depth"] <= capacity
|
||||
):
|
||||
raise SessionIntegrityError("multirate queue accounting is inconsistent")
|
||||
return processed, dropped
|
||||
|
||||
|
||||
def _validate_detector_rows(
|
||||
path: Path,
|
||||
root: Path,
|
||||
*,
|
||||
expected_count: int,
|
||||
selection_count: int,
|
||||
source_start: int,
|
||||
timeline_start: float,
|
||||
timeline_end: float,
|
||||
) -> tuple[int, ...]:
|
||||
rows = _read_jsonl(path, root)
|
||||
indices: list[int] = []
|
||||
previous_time = -math.inf
|
||||
for value in rows:
|
||||
index = value.get("frame_index")
|
||||
session_seconds = value.get("session_seconds")
|
||||
if (
|
||||
value.get("schema_version") != DETECTOR_FRAME_SCHEMA
|
||||
or not isinstance(index, int)
|
||||
or isinstance(index, bool)
|
||||
or not 0 <= index < selection_count
|
||||
or (indices and index <= indices[-1])
|
||||
or value.get("source_frame_index") != source_start + index
|
||||
or not _finite_number(session_seconds)
|
||||
or not timeline_start - 0.001 <= float(session_seconds) <= timeline_end + 0.001
|
||||
or float(session_seconds) <= previous_time
|
||||
or not _finite_number(value.get("result_age_ms"))
|
||||
or float(value["result_age_ms"]) < 0
|
||||
or not isinstance(value.get("detections"), list)
|
||||
or not isinstance(value.get("tracks"), list)
|
||||
):
|
||||
raise SessionIntegrityError("multirate detector row is invalid")
|
||||
indices.append(index)
|
||||
previous_time = float(session_seconds)
|
||||
if len(indices) != expected_count:
|
||||
raise SessionIntegrityError("multirate detector row count changed")
|
||||
return tuple(indices)
|
||||
|
||||
|
||||
def _validate_semantic_rows(
|
||||
path: Path,
|
||||
root: Path,
|
||||
*,
|
||||
expected_count: int,
|
||||
selection_count: int,
|
||||
source_start: int,
|
||||
stride: int,
|
||||
timeline_start: float,
|
||||
timeline_end: float,
|
||||
) -> tuple[int, ...]:
|
||||
rows = _read_jsonl(path, root)
|
||||
indices: list[int] = []
|
||||
previous_time = -math.inf
|
||||
for value in rows:
|
||||
index = value.get("frame_index")
|
||||
session_seconds = value.get("session_seconds")
|
||||
class_pixels = value.get("class_pixels")
|
||||
fractions = value.get("class_fractions")
|
||||
if (
|
||||
value.get("schema_version") != SEMANTIC_FRAME_SCHEMA
|
||||
or not isinstance(index, int)
|
||||
or isinstance(index, bool)
|
||||
or not 0 <= index < selection_count
|
||||
or index % stride != 0
|
||||
or (indices and index <= indices[-1])
|
||||
or value.get("source_frame_index") != source_start + index
|
||||
or not _finite_number(session_seconds)
|
||||
or not timeline_start - 0.001 <= float(session_seconds) <= timeline_end + 0.001
|
||||
or float(session_seconds) <= previous_time
|
||||
or not _finite_number(value.get("completion_age_ms"))
|
||||
or float(value["completion_age_ms"]) < 0
|
||||
or not isinstance(value.get("mask_sha256"), str)
|
||||
or _SHA256.fullmatch(value["mask_sha256"]) is None
|
||||
or not isinstance(class_pixels, dict)
|
||||
or not isinstance(fractions, dict)
|
||||
or set(class_pixels) != set(fractions)
|
||||
or any(not isinstance(count, int) or count < 0 for count in class_pixels.values())
|
||||
or any(
|
||||
not _finite_number(fraction) or not 0 <= float(fraction) <= 1
|
||||
for fraction in fractions.values()
|
||||
)
|
||||
):
|
||||
raise SessionIntegrityError("multirate semantic row is invalid")
|
||||
indices.append(index)
|
||||
previous_time = float(session_seconds)
|
||||
if len(indices) != expected_count:
|
||||
raise SessionIntegrityError("multirate semantic row count changed")
|
||||
return tuple(indices)
|
||||
|
||||
|
||||
def _validate_merged_rows(
|
||||
path: Path,
|
||||
root: Path,
|
||||
*,
|
||||
expected_indices: tuple[int, ...],
|
||||
source_start: int,
|
||||
semantic_indices: set[int],
|
||||
) -> None:
|
||||
rows = _read_jsonl(path, root)
|
||||
indices: list[int] = []
|
||||
for value in rows:
|
||||
index = value.get("frame_index")
|
||||
semantic = value.get("semantic")
|
||||
if (
|
||||
value.get("schema_version") != MERGED_FRAME_SCHEMA
|
||||
or not isinstance(index, int)
|
||||
or value.get("source_frame_index") != source_start + index
|
||||
or not isinstance(value.get("tracks"), list)
|
||||
or not _finite_number(value.get("detector_result_age_ms"))
|
||||
or not isinstance(semantic, dict)
|
||||
or semantic.get("status") not in {"fresh", "stale", "unavailable"}
|
||||
):
|
||||
raise SessionIntegrityError("multirate merged row is invalid")
|
||||
semantic_source = semantic.get("source_frame_index")
|
||||
if semantic["status"] == "unavailable":
|
||||
if any(
|
||||
semantic.get(key) is not None
|
||||
for key in (
|
||||
"source_frame_index",
|
||||
"source_age_ms",
|
||||
"completion_age_ms",
|
||||
"mask_sha256",
|
||||
)
|
||||
):
|
||||
raise SessionIntegrityError("unavailable semantic binding has invented data")
|
||||
elif (
|
||||
not isinstance(semantic_source, int)
|
||||
or semantic_source > source_start + index
|
||||
or semantic_source - source_start not in semantic_indices
|
||||
or not _finite_number(semantic.get("source_age_ms"))
|
||||
or float(semantic["source_age_ms"]) < 0
|
||||
or not _finite_number(semantic.get("completion_age_ms"))
|
||||
or not isinstance(semantic.get("mask_sha256"), str)
|
||||
or _SHA256.fullmatch(semantic["mask_sha256"]) is None
|
||||
):
|
||||
raise SessionIntegrityError("semantic binding is inconsistent")
|
||||
indices.append(index)
|
||||
if tuple(indices) != expected_indices:
|
||||
raise SessionIntegrityError("multirate merged binding changed")
|
||||
|
||||
|
||||
def _validate_artifacts(
|
||||
root: Path,
|
||||
raw: object,
|
||||
) -> tuple[MultiratePerceptionArtifact, ...]:
|
||||
expected = {
|
||||
"multirate-detector-frames": (
|
||||
"detector-frames.jsonl",
|
||||
"application/x-ndjson",
|
||||
DETECTOR_FRAME_SCHEMA,
|
||||
),
|
||||
"multirate-semantic-frames": (
|
||||
"semantic-frames.jsonl",
|
||||
"application/x-ndjson",
|
||||
SEMANTIC_FRAME_SCHEMA,
|
||||
),
|
||||
"multirate-merged-frames": (
|
||||
"merged-frames.jsonl",
|
||||
"application/x-ndjson",
|
||||
MERGED_FRAME_SCHEMA,
|
||||
),
|
||||
"worker-gpu-telemetry": ("gpu-telemetry.jsonl", "application/x-ndjson", None),
|
||||
"multirate-run-report": ("run-report.json", "application/json", REPORT_SCHEMA),
|
||||
}
|
||||
if not isinstance(raw, list) or len(raw) != len(expected):
|
||||
raise SessionIntegrityError("multirate artifact set is invalid")
|
||||
artifacts: list[MultiratePerceptionArtifact] = []
|
||||
seen: set[str] = set()
|
||||
for value in raw:
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("multirate artifact descriptor is invalid")
|
||||
kind = value.get("kind")
|
||||
if not isinstance(kind, str) or kind in seen or kind not in expected:
|
||||
raise SessionIntegrityError("multirate artifact kind is invalid")
|
||||
seen.add(kind)
|
||||
expected_path, expected_media_type, expected_schema = expected[kind]
|
||||
path = root / expected_path
|
||||
metadata = _confined_regular_file(path, root)
|
||||
byte_length = value.get("byte_length")
|
||||
digest = value.get("sha256")
|
||||
if (
|
||||
value.get("path") != expected_path
|
||||
or value.get("media_type") != expected_media_type
|
||||
or value.get("schema_version") != expected_schema
|
||||
or not isinstance(byte_length, int)
|
||||
or isinstance(byte_length, bool)
|
||||
or not 0 < byte_length <= MAX_SAFE_INTEGER
|
||||
or metadata.st_size != byte_length
|
||||
or not isinstance(digest, str)
|
||||
or _SHA256.fullmatch(digest) is None
|
||||
or _sha256_file(path) != digest
|
||||
):
|
||||
raise SessionIntegrityError("multirate artifact identity changed")
|
||||
artifacts.append(
|
||||
MultiratePerceptionArtifact(
|
||||
kind=kind,
|
||||
path=path,
|
||||
media_type=expected_media_type,
|
||||
byte_length=byte_length,
|
||||
sha256=digest,
|
||||
schema_version=expected_schema,
|
||||
)
|
||||
)
|
||||
return tuple(artifacts)
|
||||
|
||||
|
||||
def _artifact(
|
||||
artifacts: tuple[MultiratePerceptionArtifact, ...],
|
||||
kind: str,
|
||||
) -> MultiratePerceptionArtifact:
|
||||
matches = tuple(item for item in artifacts if item.kind == kind)
|
||||
if len(matches) != 1:
|
||||
raise SessionIntegrityError(f"multirate artifact is unavailable: {kind}")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _read_jsonl(path: Path, root: Path) -> list[dict[str, Any]]:
|
||||
_confined_regular_file(path, root)
|
||||
rows: list[dict[str, Any]] = []
|
||||
try:
|
||||
with path.open("rb") as stream:
|
||||
for line in stream:
|
||||
if not line or len(line) > MAX_LINE_BYTES:
|
||||
raise SessionIntegrityError("multirate JSONL line is outside bounds")
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("multirate JSONL row is invalid")
|
||||
rows.append(value)
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("multirate JSONL is unavailable") from exc
|
||||
return rows
|
||||
|
||||
|
||||
def _read_object(path: Path, root: Path) -> dict[str, Any]:
|
||||
metadata = _confined_regular_file(path, root)
|
||||
if not 0 < metadata.st_size <= MAX_JSON_BYTES:
|
||||
raise SessionIntegrityError("multirate JSON is outside bounds")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("multirate JSON is unavailable") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("multirate JSON root is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _confined_regular_file(path: Path, root: Path) -> os.stat_result:
|
||||
try:
|
||||
resolved_root = root.resolve(strict=True)
|
||||
resolved = path.resolve(strict=True)
|
||||
metadata = path.lstat()
|
||||
except OSError as exc:
|
||||
raise SessionIntegrityError("multirate artifact is unavailable") from exc
|
||||
if (
|
||||
stat.S_ISLNK(metadata.st_mode)
|
||||
or not stat.S_ISREG(metadata.st_mode)
|
||||
or not resolved.is_relative_to(resolved_root)
|
||||
):
|
||||
raise SessionIntegrityError("multirate artifact is not confined")
|
||||
return metadata
|
||||
|
||||
|
||||
def _sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
try:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise SessionIntegrityError("multirate identity is not canonical JSON") from exc
|
||||
|
||||
|
||||
def _finite_number(value: object) -> TypeGuard[int | float]:
|
||||
return (
|
||||
isinstance(value, (int, float))
|
||||
and not isinstance(value, bool)
|
||||
and math.isfinite(float(value))
|
||||
)
|
||||
@@ -0,0 +1,500 @@
|
||||
"""Validated full-epoch perception results and native recorded-video delivery."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
|
||||
from .jobs import CameraComputeJob, validate_camera_compute_job
|
||||
|
||||
RESULT_SCHEMA = "missioncore.recorded-perception-result/v2"
|
||||
IDENTITY_SCHEMA = "missioncore.recorded-perception-identity/v2"
|
||||
REPORT_SCHEMA = "missioncore.perception-run-report/v1"
|
||||
FRAME_SCHEMA = "missioncore.panoptic-frame/v1"
|
||||
MAX_JSON_BYTES = 64 * 1024 * 1024
|
||||
MAX_FRAME_LINE_BYTES = 1024 * 1024
|
||||
MAX_JOB_SCAN = 512
|
||||
MAX_SAFE_INTEGER = (1 << 53) - 1
|
||||
|
||||
_SAFE_RESULT_ID = re.compile(r"^result-[a-f0-9]{64}$")
|
||||
_SAFE_RECORDING_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PerceptionArtifact:
|
||||
kind: str
|
||||
path: Path
|
||||
media_type: str
|
||||
byte_length: int
|
||||
sha256: str
|
||||
schema_version: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RecordedPerceptionEpochResult:
|
||||
result_id: str
|
||||
result_root: Path
|
||||
job: CameraComputeJob
|
||||
created_at_utc: str
|
||||
calibration_sha256: str
|
||||
calibration_slot: str
|
||||
artifacts: tuple[PerceptionArtifact, ...]
|
||||
|
||||
def artifact(self, kind: str) -> PerceptionArtifact:
|
||||
matches = tuple(item for item in self.artifacts if item.kind == kind)
|
||||
if len(matches) != 1:
|
||||
raise SessionIntegrityError(f"perception result artifact is unavailable: {kind}")
|
||||
return matches[0]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RecordedPerceptionVideo:
|
||||
result_id: str
|
||||
session_id: str
|
||||
source_id: str
|
||||
public_source_id: str
|
||||
label: str
|
||||
path: Path
|
||||
media_type: str
|
||||
byte_length: int
|
||||
sha256: str
|
||||
timeline_start_seconds: float
|
||||
timeline_end_seconds: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _LatestResultCacheEntry:
|
||||
jobs_root_mtime_ns: int
|
||||
result_parent_mtime_ns: int
|
||||
file_fingerprint: tuple[tuple[str, int, int, int, int], ...]
|
||||
result: RecordedPerceptionEpochResult
|
||||
|
||||
|
||||
class RecordedPerceptionEpochStore:
|
||||
"""Discover immutable full-epoch results and expose their panoptic video."""
|
||||
|
||||
def __init__(self, *, jobs_root: Path, results_root: Path, ffprobe_path: Path) -> None:
|
||||
self.jobs_root = jobs_root.expanduser().absolute()
|
||||
self.results_root = results_root.expanduser().absolute()
|
||||
self.ffprobe_path = ffprobe_path.expanduser().resolve(strict=True)
|
||||
self._lock = threading.Lock()
|
||||
self._video_probe_cache: dict[tuple[str, int, int], tuple[float, str, int, int]] = {}
|
||||
self._latest_result_cache: dict[str, _LatestResultCacheEntry] = {}
|
||||
|
||||
def latest(self, session_id: str) -> RecordedPerceptionEpochResult | None:
|
||||
if _SAFE_RECORDING_ID.fullmatch(session_id) is None:
|
||||
raise ValueError("observation session id is invalid")
|
||||
with self._lock:
|
||||
return self._latest_unlocked(session_id)
|
||||
|
||||
def video(
|
||||
self,
|
||||
session_id: str,
|
||||
result_id: str | None = None,
|
||||
) -> RecordedPerceptionVideo | None:
|
||||
if _SAFE_RECORDING_ID.fullmatch(session_id) is None:
|
||||
raise ValueError("observation session id is invalid")
|
||||
if result_id is not None and _SAFE_RESULT_ID.fullmatch(result_id) is None:
|
||||
raise ValueError("perception result id is invalid")
|
||||
with self._lock:
|
||||
result = (
|
||||
self._latest_unlocked(session_id)
|
||||
if result_id is None
|
||||
else self._resolve_unlocked(session_id, result_id)
|
||||
)
|
||||
if result is None:
|
||||
return None
|
||||
artifact = result.artifact("panoptic-overlay-video")
|
||||
duration, codec, width, height = self._probe_video(artifact)
|
||||
expected = result.job.timeline_end_seconds - result.job.timeline_start_seconds
|
||||
if codec != "h264" or width != 800 or height != 600 or abs(duration - expected) > 1:
|
||||
raise SessionIntegrityError("panoptic video stream contract is invalid")
|
||||
suffix = result.job.source_id.removeprefix("sensor.camera.")
|
||||
return RecordedPerceptionVideo(
|
||||
result_id=result.result_id,
|
||||
session_id=result.job.session_id,
|
||||
source_id=result.job.source_id,
|
||||
public_source_id=f"recorded.perception.{suffix}",
|
||||
label=f"Сегментация · камера {suffix}",
|
||||
path=artifact.path,
|
||||
media_type='video/mp4; codecs="avc1.640028"',
|
||||
byte_length=artifact.byte_length,
|
||||
sha256=artifact.sha256,
|
||||
timeline_start_seconds=result.job.timeline_start_seconds,
|
||||
timeline_end_seconds=result.job.timeline_end_seconds,
|
||||
)
|
||||
|
||||
def _latest_unlocked(self, session_id: str) -> RecordedPerceptionEpochResult | None:
|
||||
cached = self._latest_result_cache.get(session_id)
|
||||
if cached is not None:
|
||||
if self._latest_cache_is_current(cached):
|
||||
return cached.result
|
||||
self._latest_result_cache.pop(session_id, None)
|
||||
try:
|
||||
job_roots = sorted(self.jobs_root.iterdir())
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
if len(job_roots) > MAX_JOB_SCAN:
|
||||
raise SessionIntegrityError("compute job catalog is outside bounds")
|
||||
matches: list[RecordedPerceptionEpochResult] = []
|
||||
for job_root in job_roots:
|
||||
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
|
||||
parent = self.results_root / job.job_id
|
||||
try:
|
||||
result_roots = sorted(
|
||||
path
|
||||
for path in parent.iterdir()
|
||||
if path.is_dir()
|
||||
and not path.is_symlink()
|
||||
and _SAFE_RESULT_ID.fullmatch(path.name) is not None
|
||||
)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
for result_root in result_roots:
|
||||
try:
|
||||
matches.append(validate_recorded_perception_epoch_result(job_root, result_root))
|
||||
except (OSError, SessionIntegrityError):
|
||||
continue
|
||||
if not matches:
|
||||
return None
|
||||
latest = max(matches, key=lambda value: (value.created_at_utc, value.result_id))
|
||||
self._latest_result_cache[session_id] = self._latest_cache_entry(latest)
|
||||
return latest
|
||||
|
||||
def _latest_cache_entry(
|
||||
self,
|
||||
result: RecordedPerceptionEpochResult,
|
||||
) -> _LatestResultCacheEntry:
|
||||
result_parent = self.results_root / result.job.job_id
|
||||
return _LatestResultCacheEntry(
|
||||
jobs_root_mtime_ns=self.jobs_root.stat().st_mtime_ns,
|
||||
result_parent_mtime_ns=result_parent.stat().st_mtime_ns,
|
||||
file_fingerprint=_result_file_fingerprint(result),
|
||||
result=result,
|
||||
)
|
||||
|
||||
def _latest_cache_is_current(self, cached: _LatestResultCacheEntry) -> bool:
|
||||
"""Reuse a fully validated immutable publication while it is unchanged.
|
||||
|
||||
New jobs and new result generations invalidate through their parent
|
||||
directory mtimes. In-place changes to the manifest or any served
|
||||
result artifact invalidate through the bounded file fingerprint and
|
||||
force the full hash validation again.
|
||||
"""
|
||||
|
||||
try:
|
||||
result_parent = self.results_root / cached.result.job.job_id
|
||||
return (
|
||||
self.jobs_root.stat().st_mtime_ns == cached.jobs_root_mtime_ns
|
||||
and result_parent.stat().st_mtime_ns == cached.result_parent_mtime_ns
|
||||
and _result_file_fingerprint(cached.result) == cached.file_fingerprint
|
||||
)
|
||||
except (OSError, SessionIntegrityError):
|
||||
return False
|
||||
|
||||
def _resolve_unlocked(
|
||||
self,
|
||||
session_id: str,
|
||||
result_id: str,
|
||||
) -> RecordedPerceptionEpochResult | None:
|
||||
latest = self._latest_unlocked(session_id)
|
||||
if latest is None or latest.result_id != result_id:
|
||||
return None
|
||||
return latest
|
||||
|
||||
def _probe_video(self, artifact: PerceptionArtifact) -> tuple[float, str, int, int]:
|
||||
metadata = _confined_regular_file(artifact.path, artifact.path.parent)
|
||||
key = (artifact.sha256, metadata.st_mtime_ns, metadata.st_size)
|
||||
cached = self._video_probe_cache.get(key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
str(self.ffprobe_path),
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=codec_name,width,height:format=duration",
|
||||
"-of",
|
||||
"json",
|
||||
str(artifact.path),
|
||||
],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
document = json.loads(completed.stdout)
|
||||
stream = document["streams"][0]
|
||||
value = (
|
||||
float(document["format"]["duration"]),
|
||||
str(stream["codec_name"]),
|
||||
int(stream["width"]),
|
||||
int(stream["height"]),
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError, ValueError, KeyError, IndexError) as exc:
|
||||
raise SessionIntegrityError("panoptic video metadata is unavailable") from exc
|
||||
if completed.returncode != 0 or not math.isfinite(value[0]) or value[0] <= 0:
|
||||
raise SessionIntegrityError("panoptic video could not be decoded")
|
||||
self._video_probe_cache = {key: value}
|
||||
return value
|
||||
|
||||
|
||||
def validate_recorded_perception_epoch_result(
|
||||
job_root: Path,
|
||||
result_root: Path,
|
||||
) -> RecordedPerceptionEpochResult:
|
||||
job = validate_camera_compute_job(job_root)
|
||||
root = result_root.expanduser().resolve(strict=True)
|
||||
if not root.is_dir() or _SAFE_RESULT_ID.fullmatch(root.name) is None:
|
||||
raise SessionIntegrityError("full-epoch perception result root is invalid")
|
||||
result = _read_json_object(root / "result.json", root)
|
||||
identity = result.get("identity")
|
||||
identity_sha256 = result.get("identity_sha256")
|
||||
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"result-{identity_sha256}"
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or result.get("job_id") != job.job_id
|
||||
or result.get("input_sha256") != job.input_sha256
|
||||
or result.get("session_id") != job.session_id
|
||||
or result.get("source_id") != job.source_id
|
||||
or result.get("codec_epoch") != job.codec_epoch
|
||||
or result.get("timestamp_basis") != "session-time-seconds"
|
||||
or result.get("timeline_start_seconds") != job.timeline_start_seconds
|
||||
or result.get("timeline_end_seconds") != job.timeline_end_seconds
|
||||
or result.get("frames_processed") != job.segment_count
|
||||
):
|
||||
raise SessionIntegrityError("full-epoch perception result identity is inconsistent")
|
||||
if identity.get("job_id") != job.job_id or identity.get("input_sha256") != job.input_sha256:
|
||||
raise SessionIntegrityError("full-epoch perception identity is not bound to its job")
|
||||
calibration = identity.get("calibration")
|
||||
if (
|
||||
not isinstance(calibration, dict)
|
||||
or not isinstance(calibration.get("content_identity_sha256"), str)
|
||||
or _SHA256.fullmatch(calibration["content_identity_sha256"]) is None
|
||||
or not isinstance(calibration.get("camera_slot"), str)
|
||||
or not calibration["camera_slot"]
|
||||
):
|
||||
raise SessionIntegrityError("full-epoch perception calibration binding is invalid")
|
||||
artifacts = _validate_artifacts(root, result.get("artifacts"))
|
||||
report_artifact = _artifact(artifacts, "perception-run-report")
|
||||
report = _read_json_object(report_artifact.path, root)
|
||||
metrics = report.get("metrics")
|
||||
report_input = report.get("input")
|
||||
if (
|
||||
report.get("schema_version") != REPORT_SCHEMA
|
||||
or report.get("state") != "published"
|
||||
or report.get("result_id") != root.name
|
||||
or not isinstance(metrics, dict)
|
||||
or metrics.get("frames_expected") != job.segment_count
|
||||
or metrics.get("frames_processed") != job.segment_count
|
||||
or metrics.get("frames_failed") != 0
|
||||
or metrics.get("frames_skipped") != 0
|
||||
or not isinstance(report_input, dict)
|
||||
or report_input.get("job_id") != job.job_id
|
||||
or report_input.get("input_sha256") != job.input_sha256
|
||||
):
|
||||
raise SessionIntegrityError("full-epoch perception run report is inconsistent")
|
||||
_validate_frame_metadata(_artifact(artifacts, "panoptic-frame-metadata"), job)
|
||||
created_at_utc = result.get("created_at_utc")
|
||||
if not isinstance(created_at_utc, str) or not 1 <= len(created_at_utc) <= 64:
|
||||
raise SessionIntegrityError("full-epoch perception creation time is invalid")
|
||||
return RecordedPerceptionEpochResult(
|
||||
result_id=root.name,
|
||||
result_root=root,
|
||||
job=job,
|
||||
created_at_utc=created_at_utc,
|
||||
calibration_sha256=calibration["content_identity_sha256"],
|
||||
calibration_slot=calibration["camera_slot"],
|
||||
artifacts=artifacts,
|
||||
)
|
||||
|
||||
|
||||
def _validate_artifacts(root: Path, raw: object) -> tuple[PerceptionArtifact, ...]:
|
||||
expected = {
|
||||
"panoptic-overlay-video": ("perception.mp4", "video/mp4", None),
|
||||
"panoptic-mask-archive": ("masks.tar.gz", "application/gzip", None),
|
||||
"panoptic-frame-metadata": ("frames.jsonl", "application/x-ndjson", FRAME_SCHEMA),
|
||||
"worker-gpu-telemetry": ("gpu-telemetry.jsonl", "application/x-ndjson", None),
|
||||
"perception-run-report": ("run-report.json", "application/json", REPORT_SCHEMA),
|
||||
}
|
||||
if not isinstance(raw, list) or len(raw) != len(expected):
|
||||
raise SessionIntegrityError("full-epoch perception artifact set is invalid")
|
||||
artifacts: list[PerceptionArtifact] = []
|
||||
seen: set[str] = set()
|
||||
for value in raw:
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("full-epoch perception artifact is invalid")
|
||||
kind = value.get("kind")
|
||||
if not isinstance(kind, str) or kind in seen or kind not in expected:
|
||||
raise SessionIntegrityError("full-epoch perception artifact kind is invalid")
|
||||
seen.add(kind)
|
||||
expected_path, expected_media_type, expected_schema = expected[kind]
|
||||
path = root / expected_path
|
||||
metadata = _confined_regular_file(path, root)
|
||||
byte_length = value.get("byte_length")
|
||||
digest = value.get("sha256")
|
||||
if (
|
||||
value.get("path") != expected_path
|
||||
or value.get("media_type") != expected_media_type
|
||||
or value.get("schema_version") != expected_schema
|
||||
or not isinstance(byte_length, int)
|
||||
or isinstance(byte_length, bool)
|
||||
or not 0 < byte_length <= MAX_SAFE_INTEGER
|
||||
or metadata.st_size != byte_length
|
||||
or not isinstance(digest, str)
|
||||
or _SHA256.fullmatch(digest) is None
|
||||
or _sha256_file(path) != digest
|
||||
):
|
||||
raise SessionIntegrityError("full-epoch perception artifact identity changed")
|
||||
artifacts.append(
|
||||
PerceptionArtifact(
|
||||
kind=kind,
|
||||
path=path,
|
||||
media_type=expected_media_type,
|
||||
byte_length=byte_length,
|
||||
sha256=digest,
|
||||
schema_version=expected_schema,
|
||||
)
|
||||
)
|
||||
return tuple(artifacts)
|
||||
|
||||
|
||||
def _artifact(artifacts: tuple[PerceptionArtifact, ...], kind: str) -> PerceptionArtifact:
|
||||
matches = tuple(item for item in artifacts if item.kind == kind)
|
||||
if len(matches) != 1:
|
||||
raise SessionIntegrityError("full-epoch perception artifact is unavailable")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _validate_frame_metadata(artifact: PerceptionArtifact, job: CameraComputeJob) -> None:
|
||||
previous = -math.inf
|
||||
count = 0
|
||||
try:
|
||||
with artifact.path.open("rb") as stream:
|
||||
for expected_index, line in enumerate(stream):
|
||||
if not line or len(line) > MAX_FRAME_LINE_BYTES:
|
||||
raise SessionIntegrityError("panoptic frame metadata line is outside bounds")
|
||||
value = json.loads(line)
|
||||
session_seconds = value.get("session_seconds") if isinstance(value, dict) else None
|
||||
if (
|
||||
not isinstance(value, dict)
|
||||
or value.get("schema_version") != FRAME_SCHEMA
|
||||
or value.get("frame_index") != expected_index
|
||||
or value.get("sequence") != expected_index + 1
|
||||
or not isinstance(session_seconds, (int, float))
|
||||
or isinstance(session_seconds, bool)
|
||||
or not math.isfinite(float(session_seconds))
|
||||
or float(session_seconds) <= previous
|
||||
or float(session_seconds) < job.timeline_start_seconds - 0.001
|
||||
or float(session_seconds) > job.timeline_end_seconds + 0.001
|
||||
or not isinstance(value.get("instances"), list)
|
||||
or not isinstance(value.get("semantic_classes"), list)
|
||||
):
|
||||
raise SessionIntegrityError("panoptic frame metadata is inconsistent")
|
||||
previous = float(session_seconds)
|
||||
count += 1
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("panoptic frame metadata is unavailable") from exc
|
||||
if count != job.segment_count:
|
||||
raise SessionIntegrityError("panoptic frame metadata count changed")
|
||||
|
||||
|
||||
def _read_json_object(path: Path, root: Path) -> dict[str, Any]:
|
||||
metadata = _confined_regular_file(path, root)
|
||||
if not 0 < metadata.st_size <= MAX_JSON_BYTES:
|
||||
raise SessionIntegrityError("full-epoch perception JSON is outside bounds")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("full-epoch perception JSON is unavailable") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("full-epoch perception JSON is not an object")
|
||||
return value
|
||||
|
||||
|
||||
def _confined_regular_file(path: Path, root: Path) -> os.stat_result:
|
||||
try:
|
||||
resolved_root = root.resolve(strict=True)
|
||||
resolved = path.resolve(strict=True)
|
||||
metadata = path.lstat()
|
||||
except OSError as exc:
|
||||
raise SessionIntegrityError("full-epoch perception artifact is unavailable") from exc
|
||||
if (
|
||||
stat.S_ISLNK(metadata.st_mode)
|
||||
or not stat.S_ISREG(metadata.st_mode)
|
||||
or not resolved.is_relative_to(resolved_root)
|
||||
):
|
||||
raise SessionIntegrityError("full-epoch perception artifact is not confined")
|
||||
return metadata
|
||||
|
||||
|
||||
def _result_file_fingerprint(
|
||||
result: RecordedPerceptionEpochResult,
|
||||
) -> tuple[tuple[str, int, int, int, int], ...]:
|
||||
paths = (result.job.manifest_path, result.result_root / "result.json") + tuple(
|
||||
artifact.path for artifact in result.artifacts
|
||||
)
|
||||
values: list[tuple[str, int, int, int, int]] = []
|
||||
for path in paths:
|
||||
root = result.job.job_root if path == result.job.manifest_path else result.result_root
|
||||
metadata = _confined_regular_file(path, root)
|
||||
values.append(
|
||||
(
|
||||
str(path),
|
||||
metadata.st_dev,
|
||||
metadata.st_ino,
|
||||
metadata.st_size,
|
||||
metadata.st_mtime_ns,
|
||||
)
|
||||
)
|
||||
return tuple(values)
|
||||
|
||||
|
||||
def _sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
try:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise SessionIntegrityError("full-epoch perception identity cannot be encoded") from exc
|
||||
@@ -0,0 +1,401 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import stat
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
from k1link.artifacts import utc_now_iso, write_json_atomic
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
|
||||
from .jobs import CameraComputeJob, validate_camera_compute_job
|
||||
|
||||
QUALIFICATION_SLICE_SCHEMA = "missioncore.recorded-qualification-slice/v1"
|
||||
QUALIFICATION_SLICE_IDENTITY_SCHEMA = "missioncore.recorded-qualification-slice-identity/v1"
|
||||
QUALIFICATION_POLICY = "uniform-frame-index-full-epoch/v1"
|
||||
DEFAULT_QUALIFICATION_FRAME_COUNT = 256
|
||||
MAX_QUALIFICATION_FRAME_COUNT = 4096
|
||||
MAX_SLICE_MANIFEST_BYTES = 16 * 1024 * 1024
|
||||
MAX_INDEX_LINE_BYTES = 16 * 1024
|
||||
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
_SAFE_GENERATION = re.compile(r"^qualification-slice-[a-f0-9]{64}$")
|
||||
|
||||
|
||||
class RecordedQualificationSliceError(RuntimeError):
|
||||
"""A deterministic recorded-camera qualification slice is invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class QualificationFrame:
|
||||
frame_index: int
|
||||
sequence: int
|
||||
segment_sha256: str
|
||||
host_epoch_ns: int
|
||||
host_monotonic_ns: int
|
||||
archive_session_monotonic_ns: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RecordedQualificationSlice:
|
||||
generation_id: str
|
||||
root: Path
|
||||
manifest_path: Path
|
||||
job_id: str
|
||||
input_sha256: str
|
||||
policy: str
|
||||
source_frame_count: int
|
||||
frames: tuple[QualificationFrame, ...]
|
||||
|
||||
|
||||
def prepare_recorded_qualification_slice(
|
||||
*,
|
||||
job_root: Path,
|
||||
output_root: Path,
|
||||
sample_count: int = DEFAULT_QUALIFICATION_FRAME_COUNT,
|
||||
) -> RecordedQualificationSlice:
|
||||
"""Seal an evenly distributed, exact-repeat frame-index slice.
|
||||
|
||||
The selection includes both ends of the epoch and is keyed by the complete
|
||||
compute-job input identity. It deliberately selects by decoded frame index;
|
||||
exact PTS are attached by the worker after decoding, while archive arrival
|
||||
timestamps remain diagnostic metadata only.
|
||||
"""
|
||||
|
||||
job = validate_camera_compute_job(job_root)
|
||||
if (
|
||||
not isinstance(sample_count, int)
|
||||
or isinstance(sample_count, bool)
|
||||
or not 1 <= sample_count <= MAX_QUALIFICATION_FRAME_COUNT
|
||||
):
|
||||
raise RecordedQualificationSliceError("qualification sample count is outside bounds")
|
||||
admitted_count = min(sample_count, job.segment_count)
|
||||
selected_indices = _uniform_indices(job.segment_count, admitted_count)
|
||||
index_rows = _read_archive_index(job)
|
||||
frames = tuple(_frame_from_index(index_rows[index], index) for index in selected_indices)
|
||||
|
||||
identity = {
|
||||
"schema_version": QUALIFICATION_SLICE_IDENTITY_SCHEMA,
|
||||
"job_id": job.job_id,
|
||||
"input_sha256": job.input_sha256,
|
||||
"session_id": job.session_id,
|
||||
"source_id": job.source_id,
|
||||
"codec_epoch": job.codec_epoch,
|
||||
"source_frame_count": job.segment_count,
|
||||
"policy": QUALIFICATION_POLICY,
|
||||
"requested_sample_count": sample_count,
|
||||
"admitted_sample_count": len(frames),
|
||||
"selected_frames": [
|
||||
{
|
||||
"frame_index": frame.frame_index,
|
||||
"sequence": frame.sequence,
|
||||
"segment_sha256": frame.segment_sha256,
|
||||
}
|
||||
for frame in frames
|
||||
],
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
generation_id = f"qualification-slice-{identity_sha256}"
|
||||
root = _prepare_private_directory(output_root)
|
||||
final = root / generation_id
|
||||
if final.exists():
|
||||
existing = validate_recorded_qualification_slice(final, job_root=job.job_root)
|
||||
if existing.input_sha256 != job.input_sha256:
|
||||
raise RecordedQualificationSliceError(
|
||||
"qualification generation collides with another input"
|
||||
)
|
||||
return existing
|
||||
|
||||
staging = root / f".{generation_id}.{secrets.token_hex(12)}.incomplete"
|
||||
published = False
|
||||
try:
|
||||
staging.mkdir(mode=0o700)
|
||||
manifest = {
|
||||
"schema_version": QUALIFICATION_SLICE_SCHEMA,
|
||||
"generation_id": generation_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": utc_now_iso(),
|
||||
"source_timeline": {
|
||||
"basis": "session-time-seconds",
|
||||
"start_seconds": job.timeline_start_seconds,
|
||||
"end_seconds": job.timeline_end_seconds,
|
||||
"duration_seconds": job.timeline_end_seconds - job.timeline_start_seconds,
|
||||
},
|
||||
"frames": [
|
||||
{
|
||||
"frame_index": frame.frame_index,
|
||||
"sequence": frame.sequence,
|
||||
"segment_sha256": frame.segment_sha256,
|
||||
"archive_host_epoch_ns": frame.host_epoch_ns,
|
||||
"archive_host_monotonic_ns": frame.host_monotonic_ns,
|
||||
"archive_session_monotonic_ns": frame.archive_session_monotonic_ns,
|
||||
"decoded_session_seconds": None,
|
||||
}
|
||||
for frame in frames
|
||||
],
|
||||
"usage": {
|
||||
"selection_basis": "zero-based decoded frame index",
|
||||
"worker_timestamp_binding": (
|
||||
"resolve exact best_effort_timestamp_time after full stream decode"
|
||||
),
|
||||
"archive_timestamp_status": "host-arrival-best-effort-diagnostic",
|
||||
"comparison_contract": (
|
||||
"all E1 variants must use this exact ordered frame list and the same input "
|
||||
"and calibration generations"
|
||||
),
|
||||
},
|
||||
}
|
||||
write_json_atomic(staging / "manifest.json", manifest)
|
||||
os.chmod(staging / "manifest.json", 0o600)
|
||||
_fsync_directory(staging)
|
||||
os.replace(staging, final)
|
||||
_fsync_directory(root)
|
||||
published = True
|
||||
finally:
|
||||
if not published and staging.exists():
|
||||
shutil.rmtree(staging)
|
||||
|
||||
return validate_recorded_qualification_slice(final, job_root=job.job_root)
|
||||
|
||||
|
||||
def validate_recorded_qualification_slice(
|
||||
slice_root: Path,
|
||||
*,
|
||||
job_root: Path,
|
||||
) -> RecordedQualificationSlice:
|
||||
root = slice_root.expanduser().resolve(strict=True)
|
||||
if not root.is_dir() or _SAFE_GENERATION.fullmatch(root.name) is None:
|
||||
raise RecordedQualificationSliceError("qualification slice root is invalid")
|
||||
manifest = _read_json_object(root / "manifest.json", root)
|
||||
identity = manifest.get("identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != QUALIFICATION_SLICE_SCHEMA
|
||||
or manifest.get("generation_id") != root.name
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("schema_version") != QUALIFICATION_SLICE_IDENTITY_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or _SHA256.fullmatch(identity_sha256) is None
|
||||
or root.name != f"qualification-slice-{identity_sha256}"
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
):
|
||||
raise RecordedQualificationSliceError("qualification slice identity is inconsistent")
|
||||
job = validate_camera_compute_job(job_root)
|
||||
if (
|
||||
identity.get("job_id") != job.job_id
|
||||
or identity.get("input_sha256") != job.input_sha256
|
||||
or identity.get("session_id") != job.session_id
|
||||
or identity.get("source_id") != job.source_id
|
||||
or identity.get("codec_epoch") != job.codec_epoch
|
||||
or identity.get("source_frame_count") != job.segment_count
|
||||
or identity.get("policy") != QUALIFICATION_POLICY
|
||||
):
|
||||
raise RecordedQualificationSliceError("qualification slice is not bound to the job")
|
||||
rows = manifest.get("frames")
|
||||
selected = identity.get("selected_frames")
|
||||
admitted_count = identity.get("admitted_sample_count")
|
||||
requested_count = identity.get("requested_sample_count")
|
||||
if (
|
||||
not isinstance(rows, list)
|
||||
or not isinstance(selected, list)
|
||||
or rows == []
|
||||
or not isinstance(admitted_count, int)
|
||||
or isinstance(admitted_count, bool)
|
||||
or admitted_count != len(rows)
|
||||
or len(selected) != len(rows)
|
||||
or not isinstance(requested_count, int)
|
||||
or isinstance(requested_count, bool)
|
||||
or not 1 <= requested_count <= MAX_QUALIFICATION_FRAME_COUNT
|
||||
or admitted_count != min(requested_count, job.segment_count)
|
||||
):
|
||||
raise RecordedQualificationSliceError("qualification slice frame count is invalid")
|
||||
archive_rows = _read_archive_index(job)
|
||||
frames: list[QualificationFrame] = []
|
||||
previous_index = -1
|
||||
for row, selected_row in zip(rows, selected, strict=True):
|
||||
if not isinstance(row, dict) or not isinstance(selected_row, dict):
|
||||
raise RecordedQualificationSliceError("qualification frame descriptor is invalid")
|
||||
frame_index = row.get("frame_index")
|
||||
if (
|
||||
not isinstance(frame_index, int)
|
||||
or isinstance(frame_index, bool)
|
||||
or not previous_index < frame_index < job.segment_count
|
||||
):
|
||||
raise RecordedQualificationSliceError("qualification frame order is invalid")
|
||||
expected = _frame_from_index(archive_rows[frame_index], frame_index)
|
||||
expected_identity = {
|
||||
"frame_index": expected.frame_index,
|
||||
"sequence": expected.sequence,
|
||||
"segment_sha256": expected.segment_sha256,
|
||||
}
|
||||
expected_row = {
|
||||
**expected_identity,
|
||||
"archive_host_epoch_ns": expected.host_epoch_ns,
|
||||
"archive_host_monotonic_ns": expected.host_monotonic_ns,
|
||||
"archive_session_monotonic_ns": expected.archive_session_monotonic_ns,
|
||||
"decoded_session_seconds": None,
|
||||
}
|
||||
if selected_row != expected_identity or row != expected_row:
|
||||
raise RecordedQualificationSliceError("qualification frame binding changed")
|
||||
frames.append(expected)
|
||||
previous_index = frame_index
|
||||
if tuple(frame.frame_index for frame in frames) != _uniform_indices(
|
||||
job.segment_count,
|
||||
admitted_count,
|
||||
):
|
||||
raise RecordedQualificationSliceError("qualification selection policy changed")
|
||||
return RecordedQualificationSlice(
|
||||
generation_id=root.name,
|
||||
root=root,
|
||||
manifest_path=root / "manifest.json",
|
||||
job_id=job.job_id,
|
||||
input_sha256=job.input_sha256,
|
||||
policy=QUALIFICATION_POLICY,
|
||||
source_frame_count=job.segment_count,
|
||||
frames=tuple(frames),
|
||||
)
|
||||
|
||||
|
||||
def _uniform_indices(source_count: int, sample_count: int) -> tuple[int, ...]:
|
||||
if source_count < 1 or not 1 <= sample_count <= source_count:
|
||||
raise RecordedQualificationSliceError("uniform selection bounds are invalid")
|
||||
if sample_count == 1:
|
||||
return (source_count // 2,)
|
||||
denominator = sample_count - 1
|
||||
indices = tuple(
|
||||
(position * (source_count - 1) + denominator // 2) // denominator
|
||||
for position in range(sample_count)
|
||||
)
|
||||
if len(set(indices)) != sample_count or indices[0] != 0 or indices[-1] != source_count - 1:
|
||||
raise RecordedQualificationSliceError("uniform selection is not exact")
|
||||
return indices
|
||||
|
||||
|
||||
def _read_archive_index(job: CameraComputeJob) -> list[dict[str, Any]]:
|
||||
index_path = (
|
||||
job.job_root
|
||||
/ "input"
|
||||
/ "camera"
|
||||
/ job.source_id
|
||||
/ f"epoch-{job.codec_epoch}"
|
||||
/ "index.jsonl"
|
||||
)
|
||||
try:
|
||||
metadata = index_path.lstat()
|
||||
resolved = index_path.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise RecordedQualificationSliceError("camera archive index is unavailable") from exc
|
||||
if (
|
||||
stat.S_ISLNK(metadata.st_mode)
|
||||
or not stat.S_ISREG(metadata.st_mode)
|
||||
or not resolved.is_relative_to(job.job_root)
|
||||
):
|
||||
raise RecordedQualificationSliceError("camera archive index is not confined")
|
||||
rows: list[dict[str, Any]] = []
|
||||
try:
|
||||
with index_path.open("r", encoding="utf-8") as stream:
|
||||
for expected_index, line in enumerate(stream):
|
||||
if len(line.encode("utf-8")) > MAX_INDEX_LINE_BYTES:
|
||||
raise RecordedQualificationSliceError("camera archive index row is too large")
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict):
|
||||
raise RecordedQualificationSliceError("camera archive index row is invalid")
|
||||
if value.get("sequence") != expected_index + 1 or value.get("kind") != "media":
|
||||
raise RecordedQualificationSliceError("camera archive index order changed")
|
||||
rows.append(value)
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise RecordedQualificationSliceError("camera archive index could not be read") from exc
|
||||
if len(rows) != job.segment_count:
|
||||
raise RecordedQualificationSliceError("camera archive index count changed")
|
||||
return rows
|
||||
|
||||
|
||||
def _frame_from_index(row: dict[str, Any], frame_index: int) -> QualificationFrame:
|
||||
sequence = row.get("sequence")
|
||||
segment_sha256 = row.get("sha256")
|
||||
host_epoch_ns = row.get("host_epoch_ns")
|
||||
host_monotonic_ns = row.get("host_monotonic_ns")
|
||||
session_monotonic_ns = row.get("session_monotonic_ns")
|
||||
integers = (sequence, host_epoch_ns, host_monotonic_ns, session_monotonic_ns)
|
||||
if (
|
||||
not all(
|
||||
isinstance(value, int) and not isinstance(value, bool) and value >= 0
|
||||
for value in integers
|
||||
)
|
||||
or sequence != frame_index + 1
|
||||
or not isinstance(segment_sha256, str)
|
||||
or _SHA256.fullmatch(segment_sha256) is None
|
||||
):
|
||||
raise RecordedQualificationSliceError("camera archive frame identity is invalid")
|
||||
return QualificationFrame(
|
||||
frame_index=frame_index,
|
||||
sequence=sequence,
|
||||
segment_sha256=segment_sha256,
|
||||
host_epoch_ns=cast(int, host_epoch_ns),
|
||||
host_monotonic_ns=cast(int, host_monotonic_ns),
|
||||
archive_session_monotonic_ns=cast(int, session_monotonic_ns),
|
||||
)
|
||||
|
||||
|
||||
def _prepare_private_directory(path: Path) -> Path:
|
||||
candidate = path.expanduser()
|
||||
candidate.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
metadata = candidate.lstat()
|
||||
if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
|
||||
raise RecordedQualificationSliceError("qualification output root must be a real directory")
|
||||
root = candidate.resolve(strict=True)
|
||||
os.chmod(root, 0o700)
|
||||
return root
|
||||
|
||||
|
||||
def _read_json_object(path: Path, root: Path) -> dict[str, Any]:
|
||||
try:
|
||||
metadata = path.lstat()
|
||||
resolved = path.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise RecordedQualificationSliceError("qualification manifest is unavailable") from exc
|
||||
if (
|
||||
stat.S_ISLNK(metadata.st_mode)
|
||||
or not stat.S_ISREG(metadata.st_mode)
|
||||
or not resolved.is_relative_to(root)
|
||||
or not 0 < metadata.st_size <= MAX_SLICE_MANIFEST_BYTES
|
||||
):
|
||||
raise RecordedQualificationSliceError("qualification manifest is outside bounds")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise RecordedQualificationSliceError("qualification manifest could not be read") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise RecordedQualificationSliceError("qualification manifest is not an object")
|
||||
return value
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
try:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise SessionIntegrityError("qualification identity is not canonical JSON") from exc
|
||||
|
||||
|
||||
def _fsync_directory(path: Path) -> None:
|
||||
flags = os.O_RDONLY
|
||||
if hasattr(os, "O_DIRECTORY"):
|
||||
flags |= os.O_DIRECTORY
|
||||
descriptor = os.open(path, flags)
|
||||
try:
|
||||
os.fsync(descriptor)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
@@ -0,0 +1,466 @@
|
||||
"""Validation for immutable LAB E8 source-paced tracking results."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeGuard
|
||||
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
|
||||
from .jobs import CameraComputeJob, validate_camera_compute_job
|
||||
|
||||
RESULT_SCHEMA = "missioncore.e8-realtime-tracking-result/v1"
|
||||
IDENTITY_SCHEMA = "missioncore.e8-realtime-tracking-identity/v1"
|
||||
REPORT_SCHEMA = "missioncore.e8-realtime-tracking-report/v1"
|
||||
FRAME_SCHEMA = "missioncore.e8-realtime-tracking-frame/v1"
|
||||
TELEMETRY_SCHEMA = "missioncore.e8-realtime-tracking-telemetry/v1"
|
||||
PROFILE_SCHEMA = "missioncore.e8-realtime-tracking-profile/v1"
|
||||
MAX_JSON_BYTES = 64 * 1024 * 1024
|
||||
MAX_LINE_BYTES = 2 * 1024 * 1024
|
||||
MAX_SAFE_INTEGER = (1 << 53) - 1
|
||||
|
||||
_SAFE_RESULT_ID = re.compile(r"^e8-realtime-tracking-[a-f0-9]{64}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RealtimeTrackingArtifact:
|
||||
kind: str
|
||||
path: Path
|
||||
media_type: str
|
||||
byte_length: int
|
||||
sha256: str
|
||||
schema_version: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RealtimeTrackingQualificationResult:
|
||||
result_id: str
|
||||
result_root: Path
|
||||
job: CameraComputeJob
|
||||
mode: str
|
||||
expected_frame_count: int
|
||||
processed_frame_count: int
|
||||
dropped_frame_count: int
|
||||
accepted: bool
|
||||
artifacts: tuple[RealtimeTrackingArtifact, ...]
|
||||
|
||||
def artifact(self, kind: str) -> RealtimeTrackingArtifact:
|
||||
matches = tuple(item for item in self.artifacts if item.kind == kind)
|
||||
if len(matches) != 1:
|
||||
raise SessionIntegrityError(f"realtime tracking artifact is unavailable: {kind}")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def validate_realtime_tracking_qualification_result(
|
||||
job_root: Path,
|
||||
result_root: Path,
|
||||
) -> RealtimeTrackingQualificationResult:
|
||||
job = validate_camera_compute_job(job_root)
|
||||
root = result_root.expanduser().resolve(strict=True)
|
||||
if not root.is_dir() or _SAFE_RESULT_ID.fullmatch(root.name) is None:
|
||||
raise SessionIntegrityError("realtime tracking result root is invalid")
|
||||
result = _read_object(root / "result.json", root)
|
||||
identity = result.get("identity")
|
||||
identity_sha256 = result.get("identity_sha256")
|
||||
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"e8-realtime-tracking-{identity_sha256}"
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or identity.get("job_id") != job.job_id
|
||||
or identity.get("input_sha256") != job.input_sha256
|
||||
or identity.get("session_id") != job.session_id
|
||||
or identity.get("source_id") != job.source_id
|
||||
or result.get("ground_truth") is not False
|
||||
or result.get("publication_scope") != "recorded-realtime-qualification-only"
|
||||
or result.get("acceptance_state") not in {"accepted", "rejected"}
|
||||
):
|
||||
raise SessionIntegrityError("realtime tracking identity is inconsistent")
|
||||
|
||||
selection = identity.get("selection")
|
||||
configuration = identity.get("configuration")
|
||||
if not isinstance(selection, dict) or not isinstance(configuration, dict):
|
||||
raise SessionIntegrityError("realtime tracking selection is unavailable")
|
||||
expected = selection.get("frame_count")
|
||||
source_start = selection.get("source_start_frame_index")
|
||||
source_end = selection.get("source_end_frame_index")
|
||||
timeline_start = selection.get("timeline_start_seconds")
|
||||
timeline_end = selection.get("timeline_end_seconds")
|
||||
profile = configuration.get("profile")
|
||||
if (
|
||||
not isinstance(expected, int)
|
||||
or isinstance(expected, bool)
|
||||
or expected < 2
|
||||
or not isinstance(source_start, int)
|
||||
or isinstance(source_start, bool)
|
||||
or not isinstance(source_end, int)
|
||||
or isinstance(source_end, bool)
|
||||
or source_start < 0
|
||||
or source_end - source_start + 1 != expected
|
||||
or source_end >= job.segment_count
|
||||
or not _finite_number(timeline_start)
|
||||
or not _finite_number(timeline_end)
|
||||
or float(timeline_start) < job.timeline_start_seconds
|
||||
or float(timeline_end) > job.timeline_end_seconds
|
||||
or float(timeline_end) <= float(timeline_start)
|
||||
or not isinstance(profile, dict)
|
||||
or profile.get("schema_version") != PROFILE_SCHEMA
|
||||
):
|
||||
raise SessionIntegrityError("realtime tracking selection is invalid")
|
||||
mode = profile.get("mode")
|
||||
realtime = profile.get("realtime")
|
||||
if (
|
||||
mode not in {"pilot", "qualification", "overload-negative-control"}
|
||||
or not isinstance(realtime, dict)
|
||||
or realtime.get("queue_policy") != "bounded-latest-wins"
|
||||
or not isinstance(realtime.get("queue_capacity"), int)
|
||||
or not 1 <= int(realtime["queue_capacity"]) <= 8
|
||||
):
|
||||
raise SessionIntegrityError("realtime tracking profile is invalid")
|
||||
|
||||
artifacts = _validate_artifacts(root, result.get("artifacts"))
|
||||
report = _read_object(_artifact(artifacts, "realtime-tracking-report").path, root)
|
||||
metrics = report.get("metrics")
|
||||
acceptance = report.get("acceptance")
|
||||
if (
|
||||
report.get("schema_version") != REPORT_SCHEMA
|
||||
or report.get("result_id") != root.name
|
||||
or report.get("identity") != identity
|
||||
or report.get("ground_truth") is not False
|
||||
or report.get("state") != result.get("acceptance_state")
|
||||
or not isinstance(metrics, dict)
|
||||
or not isinstance(acceptance, dict)
|
||||
or not isinstance(acceptance.get("accepted"), bool)
|
||||
or acceptance.get("navigation_or_safety_accepted") is not False
|
||||
or (result.get("acceptance_state") == "accepted") != acceptance["accepted"]
|
||||
):
|
||||
raise SessionIntegrityError("realtime tracking report is inconsistent")
|
||||
processed = metrics.get("frames_processed")
|
||||
dropped = metrics.get("frames_dropped")
|
||||
queue = metrics.get("queue")
|
||||
health = metrics.get("health_counts")
|
||||
if (
|
||||
metrics.get("frames_expected") != expected
|
||||
or metrics.get("frames_published") != expected
|
||||
or not isinstance(processed, int)
|
||||
or isinstance(processed, bool)
|
||||
or not 1 <= processed <= expected
|
||||
or not isinstance(dropped, int)
|
||||
or isinstance(dropped, bool)
|
||||
or dropped < 0
|
||||
or processed + dropped != expected
|
||||
or result.get("frames_processed") != processed
|
||||
or metrics.get("frames_failed") != 0
|
||||
or not isinstance(queue, dict)
|
||||
or queue.get("capacity") != realtime["queue_capacity"]
|
||||
or queue.get("published") != expected
|
||||
or queue.get("consumed") != processed
|
||||
or queue.get("dropped_overflow") != dropped
|
||||
or queue.get("final_depth") != 0
|
||||
or not isinstance(queue.get("maximum_depth"), int)
|
||||
or not 0 <= queue["maximum_depth"] <= queue["capacity"]
|
||||
or queue.get("closed") is not True
|
||||
or not isinstance(health, dict)
|
||||
or sum(health.values()) != processed
|
||||
):
|
||||
raise SessionIntegrityError("realtime tracking accounting is inconsistent")
|
||||
|
||||
indices = _validate_realtime_frames(
|
||||
_artifact(artifacts, "realtime-tracking-frames").path,
|
||||
root,
|
||||
expected_count=processed,
|
||||
selection_count=expected,
|
||||
source_start=source_start,
|
||||
timeline_start=float(timeline_start),
|
||||
timeline_end=float(timeline_end),
|
||||
)
|
||||
_validate_realtime_telemetry(
|
||||
_artifact(artifacts, "realtime-tracking-telemetry").path,
|
||||
root,
|
||||
expected_indices=indices,
|
||||
)
|
||||
return RealtimeTrackingQualificationResult(
|
||||
result_id=root.name,
|
||||
result_root=root,
|
||||
job=job,
|
||||
mode=str(mode),
|
||||
expected_frame_count=expected,
|
||||
processed_frame_count=processed,
|
||||
dropped_frame_count=dropped,
|
||||
accepted=bool(acceptance["accepted"]),
|
||||
artifacts=artifacts,
|
||||
)
|
||||
|
||||
|
||||
def _validate_realtime_frames(
|
||||
path: Path,
|
||||
root: Path,
|
||||
*,
|
||||
expected_count: int,
|
||||
selection_count: int,
|
||||
source_start: int,
|
||||
timeline_start: float,
|
||||
timeline_end: float,
|
||||
) -> tuple[int, ...]:
|
||||
_confined_regular_file(path, root)
|
||||
indices: list[int] = []
|
||||
previous_time = -math.inf
|
||||
try:
|
||||
with path.open("rb") as stream:
|
||||
for line in stream:
|
||||
if not line or len(line) > MAX_LINE_BYTES:
|
||||
raise SessionIntegrityError("realtime tracking frame line is outside bounds")
|
||||
value = json.loads(line)
|
||||
frame_index = value.get("frame_index") if isinstance(value, dict) else None
|
||||
session_seconds = value.get("session_seconds") if isinstance(value, dict) else None
|
||||
detections = value.get("detections") if isinstance(value, dict) else None
|
||||
tracks = value.get("tracks") if isinstance(value, dict) else None
|
||||
delivery = value.get("delivery") if isinstance(value, dict) else None
|
||||
if (
|
||||
not isinstance(value, dict)
|
||||
or value.get("schema_version") != FRAME_SCHEMA
|
||||
or not isinstance(frame_index, int)
|
||||
or isinstance(frame_index, bool)
|
||||
or not 0 <= frame_index < selection_count
|
||||
or (indices and frame_index <= indices[-1])
|
||||
or value.get("sequence") != frame_index + 1
|
||||
or value.get("source_frame_index") != source_start + frame_index
|
||||
or value.get("source_sequence") != source_start + frame_index + 1
|
||||
or not _finite_number(session_seconds)
|
||||
or not timeline_start - 0.001 <= float(session_seconds) <= timeline_end + 0.001
|
||||
or float(session_seconds) <= previous_time
|
||||
or not isinstance(detections, list)
|
||||
or not isinstance(tracks, list)
|
||||
or not isinstance(delivery, dict)
|
||||
or delivery.get("health") not in {"healthy", "degraded", "stale", "unavailable"}
|
||||
or not _finite_number(delivery.get("result_age_ms"))
|
||||
or float(delivery["result_age_ms"]) < 0
|
||||
):
|
||||
raise SessionIntegrityError("realtime tracking frame metadata is inconsistent")
|
||||
_validate_observations(detections, tracks)
|
||||
indices.append(frame_index)
|
||||
previous_time = float(session_seconds)
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("realtime tracking frames are unavailable") from exc
|
||||
if len(indices) != expected_count:
|
||||
raise SessionIntegrityError("realtime tracking frame count changed")
|
||||
return tuple(indices)
|
||||
|
||||
|
||||
def _validate_realtime_telemetry(
|
||||
path: Path,
|
||||
root: Path,
|
||||
*,
|
||||
expected_indices: tuple[int, ...],
|
||||
) -> None:
|
||||
_confined_regular_file(path, root)
|
||||
indices: list[int] = []
|
||||
try:
|
||||
with path.open("rb") as stream:
|
||||
for line in stream:
|
||||
if not line or len(line) > MAX_LINE_BYTES:
|
||||
raise SessionIntegrityError("realtime telemetry line is outside bounds")
|
||||
value = json.loads(line)
|
||||
if (
|
||||
not isinstance(value, dict)
|
||||
or value.get("schema_version") != TELEMETRY_SCHEMA
|
||||
or not isinstance(value.get("frame_index"), int)
|
||||
or value.get("health") not in {"healthy", "degraded", "stale", "unavailable"}
|
||||
or not _finite_number(value.get("result_age_ms"))
|
||||
or float(value["result_age_ms"]) < 0
|
||||
or not _finite_number(value.get("processing_ms"))
|
||||
or float(value["processing_ms"]) < 0
|
||||
):
|
||||
raise SessionIntegrityError("realtime tracking telemetry is inconsistent")
|
||||
indices.append(int(value["frame_index"]))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("realtime tracking telemetry is unavailable") from exc
|
||||
if tuple(indices) != expected_indices:
|
||||
raise SessionIntegrityError("realtime tracking telemetry binding changed")
|
||||
|
||||
|
||||
def _validate_observations(detections: list[object], tracks: list[object]) -> None:
|
||||
for detection in detections:
|
||||
if not isinstance(detection, dict):
|
||||
raise SessionIntegrityError("realtime tracking detection is invalid")
|
||||
_validate_box(detection.get("bbox_xyxy"))
|
||||
if (
|
||||
not isinstance(detection.get("class_id"), int)
|
||||
or not isinstance(detection.get("label"), str)
|
||||
or not _finite_number(detection.get("score"))
|
||||
or not 0 <= float(detection["score"]) <= 1
|
||||
):
|
||||
raise SessionIntegrityError("realtime tracking detection fields are invalid")
|
||||
track_ids: set[int] = set()
|
||||
for track in tracks:
|
||||
if not isinstance(track, dict):
|
||||
raise SessionIntegrityError("realtime tracking observation is invalid")
|
||||
_validate_box(track.get("bbox_xyxy"))
|
||||
track_id = track.get("track_id")
|
||||
if (
|
||||
not isinstance(track_id, int)
|
||||
or isinstance(track_id, bool)
|
||||
or track_id < 1
|
||||
or track_id in track_ids
|
||||
or not isinstance(track.get("label"), str)
|
||||
or not _finite_number(track.get("score"))
|
||||
or not isinstance(track.get("hits"), int)
|
||||
or track["hits"] < 1
|
||||
):
|
||||
raise SessionIntegrityError("realtime tracking observation fields are invalid")
|
||||
track_ids.add(track_id)
|
||||
|
||||
|
||||
def _validate_box(value: object) -> None:
|
||||
if (
|
||||
not isinstance(value, list)
|
||||
or len(value) != 4
|
||||
or not all(_finite_number(item) for item in value)
|
||||
or not 0 <= float(value[0]) < float(value[2]) <= 800
|
||||
or not 0 <= float(value[1]) < float(value[3]) <= 600
|
||||
):
|
||||
raise SessionIntegrityError("realtime tracking box is invalid")
|
||||
|
||||
|
||||
def _validate_artifacts(
|
||||
root: Path,
|
||||
raw: object,
|
||||
) -> tuple[RealtimeTrackingArtifact, ...]:
|
||||
expected = {
|
||||
"realtime-tracking-frames": (
|
||||
"frames.jsonl",
|
||||
"application/x-ndjson",
|
||||
FRAME_SCHEMA,
|
||||
),
|
||||
"realtime-tracking-telemetry": (
|
||||
"telemetry.jsonl",
|
||||
"application/x-ndjson",
|
||||
TELEMETRY_SCHEMA,
|
||||
),
|
||||
"worker-gpu-telemetry": ("gpu-telemetry.jsonl", "application/x-ndjson", None),
|
||||
"realtime-tracking-report": (
|
||||
"run-report.json",
|
||||
"application/json",
|
||||
REPORT_SCHEMA,
|
||||
),
|
||||
}
|
||||
if not isinstance(raw, list) or len(raw) != len(expected):
|
||||
raise SessionIntegrityError("realtime tracking artifact set is invalid")
|
||||
artifacts: list[RealtimeTrackingArtifact] = []
|
||||
seen: set[str] = set()
|
||||
for value in raw:
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("realtime tracking artifact descriptor is invalid")
|
||||
kind = value.get("kind")
|
||||
if not isinstance(kind, str) or kind in seen or kind not in expected:
|
||||
raise SessionIntegrityError("realtime tracking artifact kind is invalid")
|
||||
seen.add(kind)
|
||||
expected_path, expected_media_type, expected_schema = expected[kind]
|
||||
path = root / expected_path
|
||||
metadata = _confined_regular_file(path, root)
|
||||
byte_length = value.get("byte_length")
|
||||
digest = value.get("sha256")
|
||||
if (
|
||||
value.get("path") != expected_path
|
||||
or value.get("media_type") != expected_media_type
|
||||
or value.get("schema_version") != expected_schema
|
||||
or not isinstance(byte_length, int)
|
||||
or isinstance(byte_length, bool)
|
||||
or not 0 < byte_length <= MAX_SAFE_INTEGER
|
||||
or metadata.st_size != byte_length
|
||||
or not isinstance(digest, str)
|
||||
or _SHA256.fullmatch(digest) is None
|
||||
or _sha256_file(path) != digest
|
||||
):
|
||||
raise SessionIntegrityError("realtime tracking artifact identity changed")
|
||||
artifacts.append(
|
||||
RealtimeTrackingArtifact(
|
||||
kind=kind,
|
||||
path=path,
|
||||
media_type=expected_media_type,
|
||||
byte_length=byte_length,
|
||||
sha256=digest,
|
||||
schema_version=expected_schema,
|
||||
)
|
||||
)
|
||||
return tuple(artifacts)
|
||||
|
||||
|
||||
def _artifact(
|
||||
artifacts: tuple[RealtimeTrackingArtifact, ...],
|
||||
kind: str,
|
||||
) -> RealtimeTrackingArtifact:
|
||||
matches = tuple(item for item in artifacts if item.kind == kind)
|
||||
if len(matches) != 1:
|
||||
raise SessionIntegrityError(f"realtime tracking artifact is unavailable: {kind}")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _read_object(path: Path, root: Path) -> dict[str, Any]:
|
||||
metadata = _confined_regular_file(path, root)
|
||||
if not 0 < metadata.st_size <= MAX_JSON_BYTES:
|
||||
raise SessionIntegrityError("realtime tracking JSON is outside bounds")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("realtime tracking JSON is unavailable") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("realtime tracking JSON root is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _confined_regular_file(path: Path, root: Path) -> os.stat_result:
|
||||
try:
|
||||
resolved_root = root.resolve(strict=True)
|
||||
resolved = path.resolve(strict=True)
|
||||
metadata = path.lstat()
|
||||
except OSError as exc:
|
||||
raise SessionIntegrityError("realtime tracking artifact is unavailable") from exc
|
||||
if (
|
||||
stat.S_ISLNK(metadata.st_mode)
|
||||
or not stat.S_ISREG(metadata.st_mode)
|
||||
or not resolved.is_relative_to(resolved_root)
|
||||
):
|
||||
raise SessionIntegrityError("realtime tracking artifact is not confined")
|
||||
return metadata
|
||||
|
||||
|
||||
def _sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
try:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise SessionIntegrityError("realtime tracking identity is not canonical JSON") from exc
|
||||
|
||||
|
||||
def _finite_number(value: object) -> TypeGuard[int | float]:
|
||||
return (
|
||||
isinstance(value, (int, float))
|
||||
and not isinstance(value, bool)
|
||||
and math.isfinite(float(value))
|
||||
)
|
||||
@@ -0,0 +1,425 @@
|
||||
"""Validation for immutable LAB E6 tracked LiDAR qualification results."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeGuard
|
||||
|
||||
import numpy as np
|
||||
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
|
||||
from .jobs import CameraComputeJob, validate_camera_compute_job
|
||||
from .perception_epoch import (
|
||||
RecordedPerceptionEpochResult,
|
||||
validate_recorded_perception_epoch_result,
|
||||
)
|
||||
from .tracking_qualification import (
|
||||
TrackingQualificationResult,
|
||||
validate_tracking_qualification_result,
|
||||
)
|
||||
|
||||
RESULT_SCHEMA = "missioncore.e6-tracked-lidar-fusion-result/v1"
|
||||
IDENTITY_SCHEMA = "missioncore.e6-tracked-lidar-fusion-identity/v1"
|
||||
FRAME_SCHEMA = "missioncore.e6-tracked-lidar-frame/v1"
|
||||
REPORT_SCHEMA = "missioncore.e6-tracked-lidar-run-report/v1"
|
||||
MAX_JSON_BYTES = 64 * 1024 * 1024
|
||||
MAX_FRAME_LINE_BYTES = 4 * 1024 * 1024
|
||||
MAX_POINTS_PER_FRAME = 100_000
|
||||
MAX_BOXES_PER_FRAME = 10_000
|
||||
|
||||
_SAFE_RESULT_ID = re.compile(r"^e6-fusion-[a-f0-9]{64}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TrackedFusionQualificationResult:
|
||||
result_id: str
|
||||
result_root: Path
|
||||
job: CameraComputeJob
|
||||
tracking: TrackingQualificationResult
|
||||
semantic: RecordedPerceptionEpochResult
|
||||
frame_count: int
|
||||
artifacts: tuple[dict[str, Any], ...]
|
||||
|
||||
|
||||
def validate_tracked_fusion_qualification_result(
|
||||
job_root: Path,
|
||||
tracking_root: Path,
|
||||
semantic_root: Path,
|
||||
result_root: Path,
|
||||
) -> TrackedFusionQualificationResult:
|
||||
job = validate_camera_compute_job(job_root)
|
||||
tracking = validate_tracking_qualification_result(job_root, tracking_root)
|
||||
semantic = validate_recorded_perception_epoch_result(job_root, semantic_root)
|
||||
root = result_root.expanduser().resolve(strict=True)
|
||||
if not root.is_dir() or _SAFE_RESULT_ID.fullmatch(root.name) is None:
|
||||
raise SessionIntegrityError("tracked fusion result root is invalid")
|
||||
result = _read_object(root / "result.json", root)
|
||||
identity = result.get("identity")
|
||||
identity_sha256 = result.get("identity_sha256")
|
||||
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"e6-fusion-{identity_sha256}"
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or identity.get("job_id") != job.job_id
|
||||
or identity.get("input_sha256") != job.input_sha256
|
||||
or identity.get("tracking_result_id") != tracking.result_id
|
||||
or identity.get("semantic_result_id") != semantic.result_id
|
||||
or identity.get("calibration_sha256") != semantic.calibration_sha256
|
||||
or identity.get("camera_slot") != semantic.calibration_slot
|
||||
or result.get("session_id") != job.session_id
|
||||
or result.get("source_id") != job.source_id
|
||||
or result.get("ground_truth") is not False
|
||||
or result.get("publication_scope") != "qualification-clip-only"
|
||||
or result.get("frames_processed") != tracking.frame_count
|
||||
):
|
||||
raise SessionIntegrityError("tracked fusion identity is inconsistent")
|
||||
clip = identity.get("clip")
|
||||
if (
|
||||
not isinstance(clip, dict)
|
||||
or clip.get("source_start_frame_index") != tracking.source_start_frame_index
|
||||
or clip.get("source_end_frame_index") != tracking.source_end_frame_index
|
||||
or clip.get("clip_frame_count") != tracking.frame_count
|
||||
or clip.get("timeline_start_seconds") != tracking.timeline_start_seconds
|
||||
or clip.get("timeline_end_seconds") != tracking.timeline_end_seconds
|
||||
):
|
||||
raise SessionIntegrityError("tracked fusion clip binding changed")
|
||||
artifacts = _validate_artifacts(root, result.get("artifacts"))
|
||||
labels = _read_labels(_artifact_path(artifacts, "tracked-lidar-box-labels", root))
|
||||
_validate_arrays(
|
||||
_artifact_path(artifacts, "tracked-lidar-arrays", root),
|
||||
frame_count=tracking.frame_count,
|
||||
label_count=len(labels),
|
||||
timeline_start=tracking.timeline_start_seconds,
|
||||
timeline_end=tracking.timeline_end_seconds,
|
||||
)
|
||||
_validate_frames(
|
||||
_artifact_path(artifacts, "tracked-lidar-frame-metadata", root),
|
||||
tracking=tracking,
|
||||
)
|
||||
report = _read_object(
|
||||
_artifact_path(artifacts, "tracked-lidar-run-report", root), root
|
||||
)
|
||||
metrics = report.get("metrics")
|
||||
if (
|
||||
report.get("schema_version") != REPORT_SCHEMA
|
||||
or report.get("result_id") != root.name
|
||||
or report.get("identity") != identity
|
||||
or not isinstance(metrics, dict)
|
||||
or metrics.get("frames_expected") != tracking.frame_count
|
||||
or metrics.get("frames_processed") != tracking.frame_count
|
||||
or metrics.get("frames_fused", 0) + metrics.get("frames_depth_unavailable", 0)
|
||||
!= tracking.frame_count
|
||||
or result.get("metrics") != metrics
|
||||
):
|
||||
raise SessionIntegrityError("tracked fusion report is inconsistent")
|
||||
rrd = _artifact_path(artifacts, "tracked-lidar-rerun", root)
|
||||
if rrd.read_bytes()[:4] != b"RRF2":
|
||||
raise SessionIntegrityError("tracked fusion Rerun stream is invalid")
|
||||
return TrackedFusionQualificationResult(
|
||||
result_id=root.name,
|
||||
result_root=root,
|
||||
job=job,
|
||||
tracking=tracking,
|
||||
semantic=semantic,
|
||||
frame_count=tracking.frame_count,
|
||||
artifacts=artifacts,
|
||||
)
|
||||
|
||||
|
||||
def _validate_artifacts(root: Path, raw: object) -> tuple[dict[str, Any], ...]:
|
||||
expected = {
|
||||
"tracked-lidar-arrays": ("fusion.npz", "application/x-npz", None),
|
||||
"tracked-lidar-frame-metadata": (
|
||||
"fusion-frames.jsonl",
|
||||
"application/x-ndjson",
|
||||
FRAME_SCHEMA,
|
||||
),
|
||||
"tracked-lidar-box-labels": ("box-labels.json", "application/json", None),
|
||||
"tracked-lidar-overlay-video": ("fusion-overlay.mp4", "video/mp4", None),
|
||||
"tracked-lidar-rerun": ("fusion.rrd", "application/vnd.rerun.rrd", None),
|
||||
"tracked-lidar-contact-sheet": ("contact-sheet.png", "image/png", None),
|
||||
"tracked-lidar-run-report": ("run-report.json", "application/json", REPORT_SCHEMA),
|
||||
}
|
||||
if not isinstance(raw, list) or len(raw) != len(expected):
|
||||
raise SessionIntegrityError("tracked fusion artifact set is invalid")
|
||||
artifacts: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for value in raw:
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("tracked fusion artifact descriptor is invalid")
|
||||
kind = value.get("kind")
|
||||
if not isinstance(kind, str) or kind in seen or kind not in expected:
|
||||
raise SessionIntegrityError("tracked fusion artifact kind is invalid")
|
||||
seen.add(kind)
|
||||
name, media_type, schema = expected[kind]
|
||||
path = root / name
|
||||
metadata = _confined_regular_file(path, root)
|
||||
if (
|
||||
value.get("path") != name
|
||||
or value.get("media_type") != media_type
|
||||
or value.get("schema_version") != schema
|
||||
or value.get("byte_length") != metadata.st_size
|
||||
or not isinstance(value.get("sha256"), str)
|
||||
or _SHA256.fullmatch(str(value["sha256"])) is None
|
||||
or _sha256(path) != value["sha256"]
|
||||
):
|
||||
raise SessionIntegrityError("tracked fusion artifact identity changed")
|
||||
artifacts.append(value)
|
||||
return tuple(artifacts)
|
||||
|
||||
|
||||
def _validate_arrays(
|
||||
path: Path,
|
||||
*,
|
||||
frame_count: int,
|
||||
label_count: int,
|
||||
timeline_start: float,
|
||||
timeline_end: float,
|
||||
) -> None:
|
||||
required = {
|
||||
"frame_times_ns",
|
||||
"cloud_offsets",
|
||||
"cloud_points",
|
||||
"support_offsets",
|
||||
"support_points",
|
||||
"support_colors",
|
||||
"box_offsets",
|
||||
"box_centers",
|
||||
"box_half_sizes",
|
||||
"box_quaternions",
|
||||
"box_colors",
|
||||
"box_track_ids",
|
||||
"box_distances_m",
|
||||
"box_support_counts",
|
||||
}
|
||||
try:
|
||||
with np.load(path, allow_pickle=False) as arrays:
|
||||
if set(arrays.files) != required:
|
||||
raise SessionIntegrityError("tracked fusion array set is invalid")
|
||||
times = arrays["frame_times_ns"]
|
||||
cloud_offsets = arrays["cloud_offsets"]
|
||||
cloud_points = arrays["cloud_points"]
|
||||
support_offsets = arrays["support_offsets"]
|
||||
support_points = arrays["support_points"]
|
||||
support_colors = arrays["support_colors"]
|
||||
box_offsets = arrays["box_offsets"]
|
||||
centers = arrays["box_centers"]
|
||||
half_sizes = arrays["box_half_sizes"]
|
||||
quaternions = arrays["box_quaternions"]
|
||||
colors = arrays["box_colors"]
|
||||
track_ids = arrays["box_track_ids"]
|
||||
distances = arrays["box_distances_m"]
|
||||
support_counts = arrays["box_support_counts"]
|
||||
boxes = centers.shape[0]
|
||||
if (
|
||||
times.dtype != np.int64
|
||||
or times.shape != (frame_count,)
|
||||
or cloud_offsets.dtype != np.int64
|
||||
or cloud_offsets.shape != (frame_count + 1,)
|
||||
or cloud_points.dtype != np.float32
|
||||
or cloud_points.ndim != 2
|
||||
or cloud_points.shape[1:] != (3,)
|
||||
or support_offsets.dtype != np.int64
|
||||
or support_offsets.shape != (frame_count + 1,)
|
||||
or support_points.dtype != np.float32
|
||||
or support_points.ndim != 2
|
||||
or support_points.shape[1:] != (3,)
|
||||
or support_colors.dtype != np.uint8
|
||||
or support_colors.shape != support_points.shape
|
||||
or box_offsets.dtype != np.int64
|
||||
or box_offsets.shape != (frame_count + 1,)
|
||||
or centers.dtype != np.float32
|
||||
or centers.ndim != 2
|
||||
or centers.shape[1:] != (3,)
|
||||
or half_sizes.dtype != np.float32
|
||||
or half_sizes.shape != centers.shape
|
||||
or quaternions.dtype != np.float32
|
||||
or quaternions.shape != (boxes, 4)
|
||||
or colors.dtype != np.uint8
|
||||
or colors.shape != (boxes, 4)
|
||||
or track_ids.dtype != np.int64
|
||||
or track_ids.shape != (boxes,)
|
||||
or distances.dtype != np.float32
|
||||
or distances.shape != (boxes,)
|
||||
or support_counts.dtype != np.int64
|
||||
or support_counts.shape != (boxes,)
|
||||
or label_count != boxes
|
||||
):
|
||||
raise SessionIntegrityError("tracked fusion array shapes changed")
|
||||
_validate_offsets(cloud_offsets, cloud_points.shape[0], MAX_POINTS_PER_FRAME)
|
||||
_validate_offsets(support_offsets, support_points.shape[0], MAX_POINTS_PER_FRAME)
|
||||
_validate_offsets(box_offsets, boxes, MAX_BOXES_PER_FRAME)
|
||||
if (
|
||||
np.any(np.diff(times) <= 0)
|
||||
or times[0] < round(timeline_start * 1e9) - 1_000_000
|
||||
or times[-1] > round(timeline_end * 1e9) + 1_000_000
|
||||
or not np.isfinite(cloud_points).all()
|
||||
or not np.isfinite(support_points).all()
|
||||
or not np.isfinite(centers).all()
|
||||
or not np.isfinite(half_sizes).all()
|
||||
or not np.isfinite(quaternions).all()
|
||||
or not np.isfinite(distances).all()
|
||||
or np.any(half_sizes <= 0)
|
||||
or np.any(np.linalg.norm(quaternions, axis=1) < 1e-6)
|
||||
or np.any(track_ids < 1)
|
||||
or np.any(distances <= 0)
|
||||
or np.any(support_counts < 2)
|
||||
):
|
||||
raise SessionIntegrityError("tracked fusion arrays are inconsistent")
|
||||
except (OSError, ValueError, KeyError) as exc:
|
||||
raise SessionIntegrityError("tracked fusion arrays are unavailable") from exc
|
||||
|
||||
|
||||
def _validate_frames(path: Path, *, tracking: TrackingQualificationResult) -> None:
|
||||
count = 0
|
||||
previous = -math.inf
|
||||
try:
|
||||
with path.open("rb") as stream:
|
||||
for frame_index, line in enumerate(stream):
|
||||
if not line or len(line) > MAX_FRAME_LINE_BYTES:
|
||||
raise SessionIntegrityError("tracked fusion frame line is outside bounds")
|
||||
value = json.loads(line)
|
||||
session_seconds = value.get("session_seconds") if isinstance(value, dict) else None
|
||||
objects = value.get("objects") if isinstance(value, dict) else None
|
||||
if (
|
||||
not isinstance(value, dict)
|
||||
or value.get("schema_version") != FRAME_SCHEMA
|
||||
or value.get("frame_index") != frame_index
|
||||
or value.get("source_frame_index")
|
||||
!= tracking.source_start_frame_index + frame_index
|
||||
or not _finite(session_seconds)
|
||||
or float(session_seconds) <= previous
|
||||
or not isinstance(objects, list)
|
||||
or value.get("state")
|
||||
not in {"fused", "depth-unavailable-sync-gate"}
|
||||
or not isinstance(value.get("accepted_cuboids"), int)
|
||||
or value["accepted_cuboids"] < 0
|
||||
):
|
||||
raise SessionIntegrityError("tracked fusion frame metadata changed")
|
||||
accepted = 0
|
||||
for item in objects:
|
||||
if not isinstance(item, dict):
|
||||
raise SessionIntegrityError("tracked fusion object is invalid")
|
||||
status = item.get("cuboid_status")
|
||||
if status == "accepted-point-supported-oriented-p05-p95":
|
||||
accepted += 1
|
||||
if (
|
||||
not _finite(item.get("distance_smoothed_m"))
|
||||
or not isinstance(item.get("clustered_points"), int)
|
||||
or item["clustered_points"] < 2
|
||||
):
|
||||
raise SessionIntegrityError("accepted tracked fusion object is invalid")
|
||||
if accepted != value["accepted_cuboids"]:
|
||||
raise SessionIntegrityError("tracked fusion frame cuboid count changed")
|
||||
previous = float(session_seconds)
|
||||
count += 1
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("tracked fusion frame metadata is unavailable") from exc
|
||||
if count != tracking.frame_count:
|
||||
raise SessionIntegrityError("tracked fusion frame count changed")
|
||||
|
||||
|
||||
def _validate_offsets(
|
||||
offsets: np.ndarray[Any, np.dtype[np.int64]],
|
||||
total: int,
|
||||
maximum: int,
|
||||
) -> None:
|
||||
if (
|
||||
offsets[0] != 0
|
||||
or offsets[-1] != total
|
||||
or np.any(np.diff(offsets) < 0)
|
||||
or np.any(np.diff(offsets) > maximum)
|
||||
):
|
||||
raise SessionIntegrityError("tracked fusion offsets are inconsistent")
|
||||
|
||||
|
||||
def _read_labels(path: Path) -> list[str]:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, list) or any(
|
||||
not isinstance(item, str) or not 1 <= len(item) <= 256 for item in value
|
||||
):
|
||||
raise SessionIntegrityError("tracked fusion labels are invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _artifact_path(
|
||||
artifacts: tuple[dict[str, Any], ...],
|
||||
kind: str,
|
||||
root: Path,
|
||||
) -> Path:
|
||||
matches = [item for item in artifacts if item["kind"] == kind]
|
||||
if len(matches) != 1:
|
||||
raise SessionIntegrityError(f"tracked fusion artifact is unavailable: {kind}")
|
||||
return root / str(matches[0]["path"])
|
||||
|
||||
|
||||
def _read_object(path: Path, root: Path) -> dict[str, Any]:
|
||||
metadata = _confined_regular_file(path, root)
|
||||
if not 0 < metadata.st_size <= MAX_JSON_BYTES:
|
||||
raise SessionIntegrityError("tracked fusion JSON is outside bounds")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("tracked fusion JSON is unavailable") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("tracked fusion JSON root is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _confined_regular_file(path: Path, root: Path) -> os.stat_result:
|
||||
try:
|
||||
resolved_root = root.resolve(strict=True)
|
||||
resolved = path.resolve(strict=True)
|
||||
metadata = path.lstat()
|
||||
except OSError as exc:
|
||||
raise SessionIntegrityError("tracked fusion artifact is unavailable") from exc
|
||||
if (
|
||||
stat.S_ISLNK(metadata.st_mode)
|
||||
or not stat.S_ISREG(metadata.st_mode)
|
||||
or not resolved.is_relative_to(resolved_root)
|
||||
):
|
||||
raise SessionIntegrityError("tracked fusion artifact is not confined")
|
||||
return metadata
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
try:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise SessionIntegrityError("tracked fusion identity cannot be encoded") from exc
|
||||
|
||||
|
||||
def _finite(value: object) -> TypeGuard[int | float]:
|
||||
return (
|
||||
isinstance(value, (int, float))
|
||||
and not isinstance(value, bool)
|
||||
and math.isfinite(float(value))
|
||||
)
|
||||
@@ -0,0 +1,364 @@
|
||||
"""Validation for immutable LAB E5 tracking-qualification results."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeGuard
|
||||
|
||||
from k1link.sessions import SessionIntegrityError
|
||||
|
||||
from .jobs import CameraComputeJob, validate_camera_compute_job
|
||||
|
||||
RESULT_SCHEMA = "missioncore.e5-tracking-result/v1"
|
||||
IDENTITY_SCHEMA = "missioncore.e5-tracking-identity/v1"
|
||||
REPORT_SCHEMA = "missioncore.e5-tracking-report/v1"
|
||||
FRAME_SCHEMA = "missioncore.e5-tracking-frame/v1"
|
||||
MAX_JSON_BYTES = 64 * 1024 * 1024
|
||||
MAX_FRAME_LINE_BYTES = 1024 * 1024
|
||||
MAX_SAFE_INTEGER = (1 << 53) - 1
|
||||
|
||||
_SAFE_RESULT_ID = re.compile(r"^e5-tracking-[a-f0-9]{64}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TrackingQualificationArtifact:
|
||||
kind: str
|
||||
path: Path
|
||||
media_type: str
|
||||
byte_length: int
|
||||
sha256: str
|
||||
schema_version: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TrackingQualificationResult:
|
||||
result_id: str
|
||||
result_root: Path
|
||||
job: CameraComputeJob
|
||||
frame_count: int
|
||||
source_start_frame_index: int
|
||||
source_end_frame_index: int
|
||||
timeline_start_seconds: float
|
||||
timeline_end_seconds: float
|
||||
artifacts: tuple[TrackingQualificationArtifact, ...]
|
||||
|
||||
def artifact(self, kind: str) -> TrackingQualificationArtifact:
|
||||
matches = tuple(item for item in self.artifacts if item.kind == kind)
|
||||
if len(matches) != 1:
|
||||
raise SessionIntegrityError(f"tracking artifact is unavailable: {kind}")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def validate_tracking_qualification_result(
|
||||
job_root: Path,
|
||||
result_root: Path,
|
||||
) -> TrackingQualificationResult:
|
||||
job = validate_camera_compute_job(job_root)
|
||||
root = result_root.expanduser().resolve(strict=True)
|
||||
if not root.is_dir() or _SAFE_RESULT_ID.fullmatch(root.name) is None:
|
||||
raise SessionIntegrityError("tracking result root is invalid")
|
||||
result = _read_object(root / "result.json", root)
|
||||
identity = result.get("identity")
|
||||
identity_sha256 = result.get("identity_sha256")
|
||||
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"e5-tracking-{identity_sha256}"
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or result.get("job_id") != job.job_id
|
||||
or result.get("input_sha256") != job.input_sha256
|
||||
or result.get("session_id") != job.session_id
|
||||
or result.get("source_id") != job.source_id
|
||||
or result.get("timestamp_basis") != "session-time-seconds"
|
||||
or result.get("ground_truth") is not False
|
||||
or result.get("publication_scope") != "qualification-clip-only"
|
||||
or identity.get("job_id") != job.job_id
|
||||
or identity.get("input_sha256") != job.input_sha256
|
||||
):
|
||||
raise SessionIntegrityError("tracking result identity is inconsistent")
|
||||
clip = identity.get("clip")
|
||||
if not isinstance(clip, dict):
|
||||
raise SessionIntegrityError("tracking clip identity is unavailable")
|
||||
frame_count = clip.get("clip_frame_count")
|
||||
start_index = clip.get("source_start_frame_index")
|
||||
end_index = clip.get("source_end_frame_index")
|
||||
timeline_start = clip.get("timeline_start_seconds")
|
||||
timeline_end = clip.get("timeline_end_seconds")
|
||||
if (
|
||||
not isinstance(frame_count, int)
|
||||
or isinstance(frame_count, bool)
|
||||
or frame_count < 2
|
||||
or not isinstance(start_index, int)
|
||||
or isinstance(start_index, bool)
|
||||
or not isinstance(end_index, int)
|
||||
or isinstance(end_index, bool)
|
||||
or start_index < 0
|
||||
or end_index - start_index + 1 != frame_count
|
||||
or end_index >= job.segment_count
|
||||
or not _finite_number(timeline_start)
|
||||
or not _finite_number(timeline_end)
|
||||
or float(timeline_start) < job.timeline_start_seconds
|
||||
or float(timeline_end) > job.timeline_end_seconds
|
||||
or float(timeline_end) <= float(timeline_start)
|
||||
or result.get("frames_processed") != frame_count
|
||||
):
|
||||
raise SessionIntegrityError("tracking clip identity is invalid")
|
||||
artifacts = _validate_artifacts(root, result.get("artifacts"))
|
||||
report = _read_object(_artifact(artifacts, "tracking-run-report").path, root)
|
||||
metrics = report.get("metrics")
|
||||
report_input = report.get("input")
|
||||
if (
|
||||
report.get("schema_version") != REPORT_SCHEMA
|
||||
or report.get("state") != "published-qualification"
|
||||
or report.get("result_id") != root.name
|
||||
or report.get("ground_truth") is not False
|
||||
or not isinstance(metrics, dict)
|
||||
or metrics.get("frames_expected") != frame_count
|
||||
or metrics.get("frames_processed") != frame_count
|
||||
or metrics.get("frames_failed") != 0
|
||||
or metrics.get("frames_skipped") != 0
|
||||
or not isinstance(report_input, dict)
|
||||
or report_input.get("job_id") != job.job_id
|
||||
or report_input.get("input_sha256") != job.input_sha256
|
||||
or report_input.get("source_start_frame_index") != start_index
|
||||
or report_input.get("source_end_frame_index") != end_index
|
||||
):
|
||||
raise SessionIntegrityError("tracking report is inconsistent")
|
||||
_validate_frames(
|
||||
_artifact(artifacts, "tracking-frame-metadata"),
|
||||
frame_count=frame_count,
|
||||
source_start=start_index,
|
||||
timeline_start=float(timeline_start),
|
||||
timeline_end=float(timeline_end),
|
||||
)
|
||||
return TrackingQualificationResult(
|
||||
result_id=root.name,
|
||||
result_root=root,
|
||||
job=job,
|
||||
frame_count=frame_count,
|
||||
source_start_frame_index=start_index,
|
||||
source_end_frame_index=end_index,
|
||||
timeline_start_seconds=float(timeline_start),
|
||||
timeline_end_seconds=float(timeline_end),
|
||||
artifacts=artifacts,
|
||||
)
|
||||
|
||||
|
||||
def _validate_artifacts(
|
||||
root: Path,
|
||||
raw: object,
|
||||
) -> tuple[TrackingQualificationArtifact, ...]:
|
||||
expected = {
|
||||
"tracking-overlay-video": ("tracking.mp4", "video/mp4", None),
|
||||
"tracking-frame-metadata": ("frames.jsonl", "application/x-ndjson", FRAME_SCHEMA),
|
||||
"worker-gpu-telemetry": ("gpu-telemetry.jsonl", "application/x-ndjson", None),
|
||||
"tracking-contact-sheet": ("contact-sheet.png", "image/png", None),
|
||||
"tracking-run-report": ("run-report.json", "application/json", REPORT_SCHEMA),
|
||||
}
|
||||
if not isinstance(raw, list) or len(raw) != len(expected):
|
||||
raise SessionIntegrityError("tracking artifact set is invalid")
|
||||
artifacts: list[TrackingQualificationArtifact] = []
|
||||
seen: set[str] = set()
|
||||
for value in raw:
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("tracking artifact descriptor is invalid")
|
||||
kind = value.get("kind")
|
||||
if not isinstance(kind, str) or kind in seen or kind not in expected:
|
||||
raise SessionIntegrityError("tracking artifact kind is invalid")
|
||||
seen.add(kind)
|
||||
expected_path, expected_media_type, expected_schema = expected[kind]
|
||||
path = root / expected_path
|
||||
metadata = _confined_regular_file(path, root)
|
||||
byte_length = value.get("byte_length")
|
||||
digest = value.get("sha256")
|
||||
if (
|
||||
value.get("path") != expected_path
|
||||
or value.get("media_type") != expected_media_type
|
||||
or value.get("schema_version") != expected_schema
|
||||
or not isinstance(byte_length, int)
|
||||
or isinstance(byte_length, bool)
|
||||
or not 0 < byte_length <= MAX_SAFE_INTEGER
|
||||
or metadata.st_size != byte_length
|
||||
or not isinstance(digest, str)
|
||||
or _SHA256.fullmatch(digest) is None
|
||||
or _sha256_file(path) != digest
|
||||
):
|
||||
raise SessionIntegrityError("tracking artifact identity changed")
|
||||
artifacts.append(
|
||||
TrackingQualificationArtifact(
|
||||
kind=kind,
|
||||
path=path,
|
||||
media_type=expected_media_type,
|
||||
byte_length=byte_length,
|
||||
sha256=digest,
|
||||
schema_version=expected_schema,
|
||||
)
|
||||
)
|
||||
return tuple(artifacts)
|
||||
|
||||
|
||||
def _validate_frames(
|
||||
artifact: TrackingQualificationArtifact,
|
||||
*,
|
||||
frame_count: int,
|
||||
source_start: int,
|
||||
timeline_start: float,
|
||||
timeline_end: float,
|
||||
) -> None:
|
||||
previous = -math.inf
|
||||
count = 0
|
||||
try:
|
||||
with artifact.path.open("rb") as stream:
|
||||
for frame_index, line in enumerate(stream):
|
||||
if not line or len(line) > MAX_FRAME_LINE_BYTES:
|
||||
raise SessionIntegrityError("tracking frame line is outside bounds")
|
||||
value = json.loads(line)
|
||||
session_seconds = value.get("session_seconds") if isinstance(value, dict) else None
|
||||
detections = value.get("detections") if isinstance(value, dict) else None
|
||||
tracks = value.get("tracks") if isinstance(value, dict) else None
|
||||
if (
|
||||
not isinstance(value, dict)
|
||||
or value.get("schema_version") != FRAME_SCHEMA
|
||||
or value.get("frame_index") != frame_index
|
||||
or value.get("sequence") != frame_index + 1
|
||||
or value.get("source_frame_index") != source_start + frame_index
|
||||
or value.get("source_sequence") != source_start + frame_index + 1
|
||||
or not _finite_number(session_seconds)
|
||||
or float(session_seconds) <= previous
|
||||
or float(session_seconds) < timeline_start - 0.001
|
||||
or float(session_seconds) > timeline_end + 0.001
|
||||
or not isinstance(detections, list)
|
||||
or not isinstance(tracks, list)
|
||||
):
|
||||
raise SessionIntegrityError("tracking frame metadata is inconsistent")
|
||||
_validate_observations(detections, tracks)
|
||||
previous = float(session_seconds)
|
||||
count += 1
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("tracking frame metadata is unavailable") from exc
|
||||
if count != frame_count:
|
||||
raise SessionIntegrityError("tracking frame metadata count changed")
|
||||
|
||||
|
||||
def _validate_observations(detections: list[object], tracks: list[object]) -> None:
|
||||
for detection in detections:
|
||||
if not isinstance(detection, dict):
|
||||
raise SessionIntegrityError("tracking detection is invalid")
|
||||
_validate_box(detection.get("bbox_xyxy"))
|
||||
if (
|
||||
not isinstance(detection.get("class_id"), int)
|
||||
or not isinstance(detection.get("label"), str)
|
||||
or not _finite_number(detection.get("score"))
|
||||
or not 0 <= float(detection["score"]) <= 1
|
||||
):
|
||||
raise SessionIntegrityError("tracking detection fields are invalid")
|
||||
track_ids: set[int] = set()
|
||||
for track in tracks:
|
||||
if not isinstance(track, dict):
|
||||
raise SessionIntegrityError("tracking observation is invalid")
|
||||
_validate_box(track.get("bbox_xyxy"))
|
||||
track_id = track.get("track_id")
|
||||
if (
|
||||
not isinstance(track_id, int)
|
||||
or isinstance(track_id, bool)
|
||||
or track_id < 1
|
||||
or track_id in track_ids
|
||||
or not isinstance(track.get("class_id"), int)
|
||||
or not isinstance(track.get("label"), str)
|
||||
or not _finite_number(track.get("score"))
|
||||
or not isinstance(track.get("hits"), int)
|
||||
or track["hits"] < 1
|
||||
):
|
||||
raise SessionIntegrityError("tracking observation fields are invalid")
|
||||
track_ids.add(track_id)
|
||||
|
||||
|
||||
def _validate_box(value: object) -> None:
|
||||
if (
|
||||
not isinstance(value, list)
|
||||
or len(value) != 4
|
||||
or not all(_finite_number(item) for item in value)
|
||||
or not 0 <= float(value[0]) < float(value[2]) <= 800
|
||||
or not 0 <= float(value[1]) < float(value[3]) <= 600
|
||||
):
|
||||
raise SessionIntegrityError("tracking box is invalid")
|
||||
|
||||
|
||||
def _artifact(
|
||||
artifacts: tuple[TrackingQualificationArtifact, ...],
|
||||
kind: str,
|
||||
) -> TrackingQualificationArtifact:
|
||||
matches = tuple(item for item in artifacts if item.kind == kind)
|
||||
if len(matches) != 1:
|
||||
raise SessionIntegrityError(f"tracking artifact is unavailable: {kind}")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _read_object(path: Path, root: Path) -> dict[str, Any]:
|
||||
metadata = _confined_regular_file(path, root)
|
||||
if not 0 < metadata.st_size <= MAX_JSON_BYTES:
|
||||
raise SessionIntegrityError("tracking JSON is outside bounds")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise SessionIntegrityError("tracking JSON is unavailable") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise SessionIntegrityError("tracking JSON root is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _confined_regular_file(path: Path, root: Path) -> os.stat_result:
|
||||
try:
|
||||
resolved_root = root.resolve(strict=True)
|
||||
resolved = path.resolve(strict=True)
|
||||
metadata = path.lstat()
|
||||
except OSError as exc:
|
||||
raise SessionIntegrityError("tracking artifact is unavailable") from exc
|
||||
if (
|
||||
stat.S_ISLNK(metadata.st_mode)
|
||||
or not stat.S_ISREG(metadata.st_mode)
|
||||
or not resolved.is_relative_to(resolved_root)
|
||||
):
|
||||
raise SessionIntegrityError("tracking artifact is not confined")
|
||||
return metadata
|
||||
|
||||
|
||||
def _sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
try:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise SessionIntegrityError("tracking identity is not canonical JSON") from exc
|
||||
|
||||
|
||||
def _finite_number(value: object) -> TypeGuard[int | float]:
|
||||
return (
|
||||
isinstance(value, (int, float))
|
||||
and not isinstance(value, bool)
|
||||
and math.isfinite(float(value))
|
||||
)
|
||||
Reference in New Issue
Block a user