feat(perception): seal detector replay evidence
This commit is contained in:
@@ -143,11 +143,21 @@ class TritonHttpInferenceBackend:
|
||||
|
||||
def __init__(self, endpoint: str, *, timeout_seconds: float = 60.0) -> None:
|
||||
parsed = urllib.parse.urlsplit(endpoint)
|
||||
if parsed.scheme != "http" or not parsed.hostname or parsed.query or parsed.fragment:
|
||||
if (
|
||||
parsed.scheme != "http"
|
||||
or not parsed.hostname
|
||||
or parsed.username is not None
|
||||
or parsed.password is not None
|
||||
or parsed.query
|
||||
or parsed.fragment
|
||||
):
|
||||
raise YoloxDetectorError("Triton endpoint must be an explicit HTTP origin")
|
||||
if not math.isfinite(timeout_seconds) or timeout_seconds <= 0:
|
||||
raise YoloxDetectorError("Triton timeout must be positive")
|
||||
self.path = f"{parsed.path.rstrip('/')}/v2/models/{YOLOX_MODEL_ID}/infer"
|
||||
self.path = (
|
||||
f"{parsed.path.rstrip('/')}/v2/models/{YOLOX_MODEL_ID}"
|
||||
f"/versions/{YOLOX_MODEL_VERSION}/infer"
|
||||
)
|
||||
self.connection = http.client.HTTPConnection(
|
||||
parsed.hostname,
|
||||
parsed.port or 80,
|
||||
|
||||
@@ -58,6 +58,21 @@ from .detector import (
|
||||
FrozenYoloxDetectorProvider,
|
||||
proposals_from_detections,
|
||||
)
|
||||
from .detector_replay import run_detector_replay
|
||||
from .detector_replay_result import (
|
||||
DETECTOR_REPLAY_FRAME_SCHEMA,
|
||||
DETECTOR_REPLAY_RESULT_SCHEMA,
|
||||
DETECTOR_RUNTIME_IDENTITY_SCHEMA,
|
||||
M4_DETECTOR_REPLAY_GATE,
|
||||
DetectorReplayFrame,
|
||||
DetectorReplayGate,
|
||||
DetectorReplayMetrics,
|
||||
DetectorReplayResult,
|
||||
DetectorReplayResultError,
|
||||
DetectorRuntimeIdentity,
|
||||
read_detector_replay_result,
|
||||
require_m4_detector_replay_acceptance,
|
||||
)
|
||||
from .graph import (
|
||||
GRAPH_RESULT_SCHEMA,
|
||||
REFERENCE_GRAPH_ID,
|
||||
@@ -148,6 +163,19 @@ __all__ = [
|
||||
"DetectorProviderSnapshot",
|
||||
"FrozenYoloxDetectorProvider",
|
||||
"proposals_from_detections",
|
||||
"DETECTOR_REPLAY_FRAME_SCHEMA",
|
||||
"DETECTOR_REPLAY_RESULT_SCHEMA",
|
||||
"DETECTOR_RUNTIME_IDENTITY_SCHEMA",
|
||||
"M4_DETECTOR_REPLAY_GATE",
|
||||
"DetectorReplayFrame",
|
||||
"DetectorReplayGate",
|
||||
"DetectorReplayMetrics",
|
||||
"DetectorReplayResult",
|
||||
"DetectorReplayResultError",
|
||||
"DetectorRuntimeIdentity",
|
||||
"read_detector_replay_result",
|
||||
"require_m4_detector_replay_acceptance",
|
||||
"run_detector_replay",
|
||||
"REFERENCE_GRAPH_CONFIG_SCHEMA",
|
||||
"DetectorProvider",
|
||||
"GeometryAssociationProvider",
|
||||
|
||||
@@ -30,6 +30,9 @@ BASELINE_SOURCE_PACK_ID: Final = (
|
||||
BASELINE_SOURCE_PACK_SHA256: Final = (
|
||||
"0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944"
|
||||
)
|
||||
BASELINE_PREPROCESS_PROFILE_SHA256: Final = (
|
||||
"19c17dbc23f2b1c539eb6b8214e69fa487ea3fec8db9d88768dffc714895fb88"
|
||||
)
|
||||
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
_EXPECTED_EVIDENCE_ROLES: Final = {
|
||||
@@ -72,6 +75,60 @@ _SOURCE_KEYS: Final = {
|
||||
"source_pack_id",
|
||||
"source_pack_artifact_sha256",
|
||||
}
|
||||
_EXPECTED_CALIBRATION: Final[dict[str, object]] = {
|
||||
"slot": "camera_1",
|
||||
"model": "KB4",
|
||||
"sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9",
|
||||
"valid_fov_result_id": (
|
||||
"valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2"
|
||||
),
|
||||
"valid_fov_mask_sha256": (
|
||||
"a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63"
|
||||
),
|
||||
}
|
||||
_EXPECTED_DETECTOR: Final[dict[str, object]] = {
|
||||
"provider_id": "triton-yolox-s-raw-kb4/v1",
|
||||
"model_id": "yolox_s",
|
||||
"model_version": 1,
|
||||
"model_sha256": "c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063",
|
||||
"config_sha256": "5795c737a7935a655961b069e8404d336d891f9762fb6dffb93956a076479604",
|
||||
"preprocess_profile_sha256": BASELINE_PREPROCESS_PROFILE_SHA256,
|
||||
"minimum_score": 0.5,
|
||||
"nms_iou_threshold": 0.45,
|
||||
"runtime": "NVIDIA Triton 2.70.0 ONNX Runtime GPU backend",
|
||||
}
|
||||
_EXPECTED_NON_GOALS: Final = (
|
||||
"semantic-class-quality",
|
||||
"persistent-reidentification",
|
||||
"physical-live-k1",
|
||||
"physical-threat-authority",
|
||||
"navigation-or-command-authority",
|
||||
"second-source-transfer",
|
||||
"second-worker-bootstrap",
|
||||
"ros2-nav2-px4-gazebo-integration",
|
||||
"deepstream-migration",
|
||||
)
|
||||
_EXPECTED_ROLLBACK: Final[dict[str, object]] = {
|
||||
"worker_id": "worker-006",
|
||||
"worker_node": "DESKTOP-OPJ8J04",
|
||||
"container_name": "ndc-mission-core-perception-worker",
|
||||
"container_image": (
|
||||
"nvcr.io/nvidia/tritonserver:26.06-py3@"
|
||||
"sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794"
|
||||
),
|
||||
"worker_package_id": (
|
||||
"e15-worker-package-dbf55ccb75664b778a2e0f5d34284af10ec0971945bbbee67b27bc264b765b51"
|
||||
),
|
||||
"runner_sha256": "86e9b25c80908a520ed483541b708cde1d1c74605e094053fcb62b610ecefb20",
|
||||
"orchestrator_sha256": (
|
||||
"82de50ae83debe26fc5463799b1e1e15217a8db8cc090671ba2993200b8ed95c"
|
||||
),
|
||||
"entrypoint": "python3 /runner/run_e15_shadow_inference.py serve",
|
||||
"observed_container_id": (
|
||||
"db2024d05098a6beb6b73bbf43f02c88ede586a4664b2c91182f436a42b3e3ff"
|
||||
),
|
||||
"observed_at_utc": "2026-08-05T10:30:00Z",
|
||||
}
|
||||
|
||||
|
||||
class BaselineContractError(ValueError):
|
||||
@@ -129,25 +186,37 @@ def load_m4_baseline(path: Path) -> BaselineProfile:
|
||||
raise BaselineContractError("M4 source pack artifact identity changed")
|
||||
if source.get("camera_stream_sha256") != BASELINE_CAMERA_STREAM_SHA256:
|
||||
raise BaselineContractError("M4 camera stream identity changed")
|
||||
modalities = _string_array(source.get("modalities"), "source modalities")
|
||||
if set(modalities) != {"image", "registered-point-increment", "pose"}:
|
||||
raise BaselineContractError("baseline source must bind image, points and pose")
|
||||
if _integer(source.get("frame_count"), "source frame count") != 4489:
|
||||
if source.get("frame_count") != 4489:
|
||||
raise BaselineContractError("baseline source frame count changed")
|
||||
if source.get("duration_seconds") != 448.723:
|
||||
raise BaselineContractError("baseline source duration changed")
|
||||
if source.get("frame_rate") != 10.003944527024467:
|
||||
raise BaselineContractError("baseline source frame rate changed")
|
||||
modalities = _string_array(source.get("modalities"), "source modalities")
|
||||
if modalities != ("image", "registered-point-increment", "pose"):
|
||||
raise BaselineContractError("baseline source must bind image, points and pose")
|
||||
|
||||
calibration = _object(document.get("calibration"), "calibration")
|
||||
if calibration != _EXPECTED_CALIBRATION:
|
||||
raise BaselineContractError("baseline calibration identity changed")
|
||||
detector = _object(document.get("detector"), "detector")
|
||||
if detector != _EXPECTED_DETECTOR:
|
||||
raise BaselineContractError("baseline detector identity changed")
|
||||
|
||||
authority = _object(document.get("authority"), "authority")
|
||||
if authority.get("mode") != "replay-simulated":
|
||||
raise BaselineContractError("M4 authority must remain replay-simulated")
|
||||
for key in (
|
||||
"ground_truth",
|
||||
"physical_live",
|
||||
"physical_collision_accepted",
|
||||
"commands_enabled",
|
||||
"actuation_allowed",
|
||||
"navigation_or_safety_accepted",
|
||||
):
|
||||
if authority.get(key) is not False:
|
||||
raise BaselineContractError(f"baseline authority {key} must remain false")
|
||||
if authority != {
|
||||
"mode": "replay-simulated",
|
||||
"ground_truth": False,
|
||||
"physical_live": False,
|
||||
"physical_collision_accepted": False,
|
||||
"commands_enabled": False,
|
||||
"actuation_allowed": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}:
|
||||
raise BaselineContractError("M4 authority must remain replay-simulated and false")
|
||||
|
||||
if _string_array(document.get("non_goals"), "non-goals") != _EXPECTED_NON_GOALS:
|
||||
raise BaselineContractError("baseline non-goals changed")
|
||||
|
||||
evidence_items = document.get("evidence")
|
||||
if not isinstance(evidence_items, list):
|
||||
@@ -161,15 +230,8 @@ def load_m4_baseline(path: Path) -> BaselineProfile:
|
||||
raise BaselineContractError("baseline evidence paths must be unique")
|
||||
|
||||
rollback = _object(document.get("rollback"), "rollback")
|
||||
if rollback.get("worker_id") != "worker-006":
|
||||
raise BaselineContractError("rollback worker identity changed")
|
||||
if rollback.get("worker_node") != "DESKTOP-OPJ8J04":
|
||||
raise BaselineContractError("rollback worker node changed")
|
||||
entrypoint = rollback.get("entrypoint")
|
||||
if not isinstance(entrypoint, str) or "run_e15_shadow_inference.py serve" not in entrypoint:
|
||||
raise BaselineContractError("rollback E15 process identity is missing")
|
||||
_digest(rollback.get("runner_sha256"), "rollback runner digest")
|
||||
_digest(rollback.get("orchestrator_sha256"), "rollback orchestrator digest")
|
||||
if rollback != _EXPECTED_ROLLBACK:
|
||||
raise BaselineContractError("rollback E15 identity changed")
|
||||
|
||||
return BaselineProfile(path=path, document=document, evidence=evidence)
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Sequential product runner for the frozen detector replay/capacity gate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from threading import Event
|
||||
|
||||
from .baseline import BASELINE_SESSION_ID, BASELINE_SOURCE_ID
|
||||
from .detector import FrozenYoloxDetectorProvider
|
||||
from .detector_replay_result import (
|
||||
M4_DETECTOR_REPLAY_GATE,
|
||||
DetectorReplayFrame,
|
||||
DetectorReplayGate,
|
||||
DetectorReplayResult,
|
||||
DetectorReplayResultError,
|
||||
DetectorRuntimeIdentity,
|
||||
build_detector_replay_metrics,
|
||||
seal_detector_replay_result,
|
||||
)
|
||||
from .providers import SourceProvider
|
||||
from .recorded_source import RECORDED_SOURCE_PROVIDER_ID
|
||||
|
||||
|
||||
def run_detector_replay(
|
||||
*,
|
||||
source: SourceProvider,
|
||||
provider: FrozenYoloxDetectorProvider,
|
||||
runtime: DetectorRuntimeIdentity,
|
||||
output_root: Path,
|
||||
gate: DetectorReplayGate = M4_DETECTOR_REPLAY_GATE,
|
||||
stop_event: Event | None = None,
|
||||
clock_ns: Callable[[], int] = time.perf_counter_ns,
|
||||
created_at_utc: str | None = None,
|
||||
) -> DetectorReplayResult:
|
||||
"""Execute one request per admitted frame and seal even a bounded failed run."""
|
||||
|
||||
if source.provider_id != RECORDED_SOURCE_PROVIDER_ID:
|
||||
raise DetectorReplayResultError("detector replay source provider is not admitted")
|
||||
stop = stop_event or Event()
|
||||
frames: list[DetectorReplayFrame] = []
|
||||
source_failure_code: str | None = None
|
||||
started_ns = int(clock_ns())
|
||||
packets = source.packets(stop)
|
||||
try:
|
||||
for packet in packets:
|
||||
if len(frames) == gate.expected_frames:
|
||||
source_failure_code = "source-frame-count-exceeded"
|
||||
break
|
||||
if (
|
||||
packet.envelope.sequence != len(frames)
|
||||
or packet.envelope.source_id != BASELINE_SOURCE_ID
|
||||
or packet.envelope.session_id != BASELINE_SESSION_ID
|
||||
):
|
||||
source_failure_code = "source-envelope-sequence-or-identity-mismatch"
|
||||
break
|
||||
frame_started_ns = int(clock_ns())
|
||||
try:
|
||||
proposals = provider.detect(packet)
|
||||
except Exception as exc:
|
||||
frames.append(
|
||||
DetectorReplayFrame(
|
||||
sequence=packet.envelope.sequence,
|
||||
envelope=packet.envelope,
|
||||
outcome="failed",
|
||||
proposals=(),
|
||||
duration_ns=max(0, int(clock_ns()) - frame_started_ns),
|
||||
failure_code=type(exc).__name__,
|
||||
)
|
||||
)
|
||||
break
|
||||
frames.append(
|
||||
DetectorReplayFrame(
|
||||
sequence=packet.envelope.sequence,
|
||||
envelope=packet.envelope,
|
||||
outcome="completed",
|
||||
proposals=proposals,
|
||||
duration_ns=max(0, int(clock_ns()) - frame_started_ns),
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
source_failure_code = type(exc).__name__
|
||||
finally:
|
||||
close = getattr(packets, "close", None)
|
||||
if callable(close):
|
||||
try:
|
||||
close()
|
||||
except Exception as exc:
|
||||
if source_failure_code is None:
|
||||
source_failure_code = type(exc).__name__
|
||||
if stop.is_set() and source_failure_code is None:
|
||||
source_failure_code = "execution-cancelled"
|
||||
if len(frames) != gate.expected_frames and source_failure_code is None:
|
||||
source_failure_code = "source-frame-count-mismatch"
|
||||
ended_ns = int(clock_ns())
|
||||
metrics = build_detector_replay_metrics(
|
||||
tuple(frames),
|
||||
provider.snapshot(),
|
||||
run_duration_ns=max(1, ended_ns - started_ns),
|
||||
)
|
||||
return seal_detector_replay_result(
|
||||
output_root=output_root,
|
||||
frames=tuple(frames),
|
||||
metrics=metrics,
|
||||
runtime=runtime,
|
||||
gate=gate,
|
||||
source_failure_code=source_failure_code,
|
||||
created_at_utc=created_at_utc,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["run_detector_replay"]
|
||||
@@ -0,0 +1,111 @@
|
||||
"""Worker entry point for the canonical frozen detector replay gate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
import json
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
from threading import Event
|
||||
|
||||
from k1link.compute.yolox_object_detector import (
|
||||
TritonHttpInferenceBackend,
|
||||
load_valid_fov_mask,
|
||||
)
|
||||
|
||||
from .baseline import load_m4_baseline, verify_m4_baseline
|
||||
from .detector import FrozenYoloxDetectorProvider
|
||||
from .detector_replay import run_detector_replay
|
||||
from .detector_replay_result import (
|
||||
DetectorReplayResultError,
|
||||
DetectorRuntimeIdentity,
|
||||
require_m4_detector_replay_acceptance,
|
||||
)
|
||||
from .recorded_source import (
|
||||
DecodedRecordedSource,
|
||||
PyAvRecordedImageDecoder,
|
||||
RecordedRavnoves00Source,
|
||||
ReplayPacing,
|
||||
)
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run the product-neutral M4 detector replay/capacity gate."
|
||||
)
|
||||
parser.add_argument("--repository-root", type=Path, required=True)
|
||||
parser.add_argument("--video", type=Path, required=True)
|
||||
parser.add_argument("--valid-fov-mask", type=Path, required=True)
|
||||
parser.add_argument("--triton-origin", required=True, type=_loopback_triton_origin)
|
||||
parser.add_argument("--runtime-identity", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _loopback_triton_origin(value: str) -> str:
|
||||
parsed = urllib.parse.urlsplit(value)
|
||||
hostname = parsed.hostname
|
||||
try:
|
||||
loopback = hostname == "localhost" or (
|
||||
hostname is not None and ipaddress.ip_address(hostname).is_loopback
|
||||
)
|
||||
except ValueError:
|
||||
loopback = False
|
||||
if not loopback:
|
||||
raise argparse.ArgumentTypeError("M4 Triton origin must use worker-local loopback")
|
||||
return value
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _arguments()
|
||||
repository_root = args.repository_root.resolve(strict=True)
|
||||
baseline = load_m4_baseline(
|
||||
repository_root / "config/perception/m4-recorded-realtime-baseline-v1.json"
|
||||
)
|
||||
verify_m4_baseline(repository_root, baseline)
|
||||
runtime_value = json.loads(args.runtime_identity.resolve(strict=True).read_text("utf-8"))
|
||||
runtime = DetectorRuntimeIdentity.from_dict(runtime_value)
|
||||
source = DecodedRecordedSource(
|
||||
source=RecordedRavnoves00Source.from_repository(
|
||||
repository_root,
|
||||
pacing=ReplayPacing.UNCAPPED,
|
||||
),
|
||||
decoder=PyAvRecordedImageDecoder(args.video),
|
||||
)
|
||||
backend = TritonHttpInferenceBackend(args.triton_origin)
|
||||
try:
|
||||
result = run_detector_replay(
|
||||
source=source,
|
||||
provider=FrozenYoloxDetectorProvider(
|
||||
mask=load_valid_fov_mask(args.valid_fov_mask),
|
||||
backend=backend,
|
||||
),
|
||||
runtime=runtime,
|
||||
output_root=args.output_root,
|
||||
stop_event=Event(),
|
||||
)
|
||||
finally:
|
||||
backend.close()
|
||||
try:
|
||||
require_m4_detector_replay_acceptance(result)
|
||||
m4_accepted = True
|
||||
except DetectorReplayResultError:
|
||||
m4_accepted = False
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result.result_id,
|
||||
"result_root": str(result.result_root),
|
||||
"accepted": m4_accepted,
|
||||
"metrics": result.metrics.to_dict(),
|
||||
},
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
)
|
||||
return 0 if m4_accepted else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,493 @@
|
||||
"""Strict contracts and accounting for detector replay evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
from k1link.compute.yolox_object_detector import (
|
||||
YOLOX_CONFIG_SHA256,
|
||||
YOLOX_MODEL_SHA256,
|
||||
YOLOX_VALID_FOV_SHA256,
|
||||
)
|
||||
|
||||
from .contracts import ObjectProposal2D, SourceEnvelope
|
||||
from .detector import (
|
||||
FROZEN_YOLOX_MODEL_ID,
|
||||
FROZEN_YOLOX_PREPROCESS_ID,
|
||||
FROZEN_YOLOX_PROVIDER_ID,
|
||||
DetectorProviderSnapshot,
|
||||
)
|
||||
|
||||
DETECTOR_REPLAY_RESULT_SCHEMA: Final = "missioncore.perception-detector-replay-result/v1"
|
||||
DETECTOR_REPLAY_RECEIPT_SCHEMA: Final = "missioncore.perception-detector-replay-receipt/v1"
|
||||
DETECTOR_REPLAY_FRAME_SCHEMA: Final = "missioncore.perception-detector-replay-frame/v1"
|
||||
DETECTOR_RUNTIME_IDENTITY_SCHEMA: Final = "missioncore.perception-runtime-identity/v1"
|
||||
DETECTOR_REPLAY_RESULT_PREFIX: Final = "m4-detector-replay-"
|
||||
DETECTOR_REPLAY_MANIFEST_NAME: Final = "manifest.json"
|
||||
DETECTOR_REPLAY_RECEIPT_NAME: Final = "receipt.json"
|
||||
DETECTOR_REPLAY_FRAMES_NAME: Final = "frames.jsonl"
|
||||
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
_GIT_REVISION = re.compile(r"^[a-f0-9]{40}$")
|
||||
_RESULT_ID = re.compile(r"^m4-detector-replay-[a-f0-9]{64}$")
|
||||
_DOCKER_IMAGE_ID = re.compile(r"^sha256:[a-f0-9]{64}$")
|
||||
_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,255}$")
|
||||
|
||||
|
||||
class DetectorReplayResultError(ValueError):
|
||||
"""Detector replay evidence is incomplete, mutable or internally inconsistent."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DetectorReplayGate:
|
||||
expected_frames: int
|
||||
minimum_end_to_end_fps: float
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.expected_frames < 1:
|
||||
raise DetectorReplayResultError("detector replay expected frames must be positive")
|
||||
if (
|
||||
not math.isfinite(self.minimum_end_to_end_fps)
|
||||
or self.minimum_end_to_end_fps <= 0.0
|
||||
):
|
||||
raise DetectorReplayResultError("detector replay minimum FPS must be positive")
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"expected_frames": self.expected_frames,
|
||||
"minimum_end_to_end_fps": self.minimum_end_to_end_fps,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, value: object) -> DetectorReplayGate:
|
||||
document = _object(value, "detector replay gate")
|
||||
_exact_keys(document, {"expected_frames", "minimum_end_to_end_fps"}, "gate")
|
||||
return cls(
|
||||
expected_frames=_integer(document.get("expected_frames"), "expected frames"),
|
||||
minimum_end_to_end_fps=_number(
|
||||
document.get("minimum_end_to_end_fps"), "minimum end-to-end FPS"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
M4_DETECTOR_REPLAY_GATE: Final = DetectorReplayGate(
|
||||
expected_frames=4489,
|
||||
minimum_end_to_end_fps=10.004,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DetectorRuntimeIdentity:
|
||||
worker_id: str
|
||||
worker_node: str
|
||||
worker_container_id: str
|
||||
worker_image_id: str
|
||||
triton_container_id: str
|
||||
triton_image_id: str
|
||||
triton_model_sha256: str
|
||||
triton_model_config_sha256: str
|
||||
valid_fov_mask_sha256: str
|
||||
artifact_sha256: str
|
||||
code_revision: str
|
||||
source_mount_read_only: bool
|
||||
model_service_reused: bool
|
||||
public_worker_port_added: bool
|
||||
same_host_tensor_transport: bool
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_identifier(self.worker_id, "worker id")
|
||||
_identifier(self.worker_node, "worker node")
|
||||
for value, label in (
|
||||
(self.worker_container_id, "worker container id"),
|
||||
(self.triton_container_id, "Triton container id"),
|
||||
):
|
||||
if _SHA256.fullmatch(value) is None:
|
||||
raise DetectorReplayResultError(f"{label} must be a full digest")
|
||||
for value, label in (
|
||||
(self.worker_image_id, "worker image id"),
|
||||
(self.triton_image_id, "Triton image id"),
|
||||
):
|
||||
if _DOCKER_IMAGE_ID.fullmatch(value) is None:
|
||||
raise DetectorReplayResultError(f"{label} must be a full image digest")
|
||||
if self.triton_model_sha256 != YOLOX_MODEL_SHA256:
|
||||
raise DetectorReplayResultError("runtime Triton model digest changed")
|
||||
if self.triton_model_config_sha256 != YOLOX_CONFIG_SHA256:
|
||||
raise DetectorReplayResultError("runtime Triton model config digest changed")
|
||||
if self.valid_fov_mask_sha256 != YOLOX_VALID_FOV_SHA256:
|
||||
raise DetectorReplayResultError("runtime valid-FOV mask digest changed")
|
||||
if _SHA256.fullmatch(self.artifact_sha256) is None:
|
||||
raise DetectorReplayResultError("worker artifact must be digest-bound")
|
||||
if _GIT_REVISION.fullmatch(self.code_revision) is None:
|
||||
raise DetectorReplayResultError("code revision must be a full Git revision")
|
||||
if not self.source_mount_read_only:
|
||||
raise DetectorReplayResultError("recorded source mount must be read-only")
|
||||
if not self.model_service_reused:
|
||||
raise DetectorReplayResultError("the admitted Triton service must be reused")
|
||||
if self.public_worker_port_added:
|
||||
raise DetectorReplayResultError("detector replay must not add a public worker port")
|
||||
if not self.same_host_tensor_transport:
|
||||
raise DetectorReplayResultError("full detector tensors must remain on the worker host")
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": DETECTOR_RUNTIME_IDENTITY_SCHEMA,
|
||||
"worker_id": self.worker_id,
|
||||
"worker_node": self.worker_node,
|
||||
"worker_container_id": self.worker_container_id,
|
||||
"worker_image_id": self.worker_image_id,
|
||||
"triton_container_id": self.triton_container_id,
|
||||
"triton_image_id": self.triton_image_id,
|
||||
"triton_model_sha256": self.triton_model_sha256,
|
||||
"triton_model_config_sha256": self.triton_model_config_sha256,
|
||||
"valid_fov_mask_sha256": self.valid_fov_mask_sha256,
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"code_revision": self.code_revision,
|
||||
"source_mount_read_only": self.source_mount_read_only,
|
||||
"model_service_reused": self.model_service_reused,
|
||||
"public_worker_port_added": self.public_worker_port_added,
|
||||
"same_host_tensor_transport": self.same_host_tensor_transport,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, value: object) -> DetectorRuntimeIdentity:
|
||||
document = _object(value, "detector runtime identity")
|
||||
fields = {
|
||||
"worker_id",
|
||||
"worker_node",
|
||||
"worker_container_id",
|
||||
"worker_image_id",
|
||||
"triton_container_id",
|
||||
"triton_image_id",
|
||||
"triton_model_sha256",
|
||||
"triton_model_config_sha256",
|
||||
"valid_fov_mask_sha256",
|
||||
"artifact_sha256",
|
||||
"code_revision",
|
||||
"source_mount_read_only",
|
||||
"model_service_reused",
|
||||
"public_worker_port_added",
|
||||
"same_host_tensor_transport",
|
||||
}
|
||||
_exact_keys(document, fields | {"schema_version"}, "runtime identity")
|
||||
if document.get("schema_version") != DETECTOR_RUNTIME_IDENTITY_SCHEMA:
|
||||
raise DetectorReplayResultError("detector runtime identity schema changed")
|
||||
return cls(
|
||||
worker_id=_string(document.get("worker_id"), "worker id"),
|
||||
worker_node=_string(document.get("worker_node"), "worker node"),
|
||||
worker_container_id=_string(
|
||||
document.get("worker_container_id"), "worker container id"
|
||||
),
|
||||
worker_image_id=_string(document.get("worker_image_id"), "worker image id"),
|
||||
triton_container_id=_string(
|
||||
document.get("triton_container_id"), "Triton container id"
|
||||
),
|
||||
triton_image_id=_string(document.get("triton_image_id"), "Triton image id"),
|
||||
triton_model_sha256=_string(
|
||||
document.get("triton_model_sha256"), "Triton model digest"
|
||||
),
|
||||
triton_model_config_sha256=_string(
|
||||
document.get("triton_model_config_sha256"), "Triton model config digest"
|
||||
),
|
||||
valid_fov_mask_sha256=_string(
|
||||
document.get("valid_fov_mask_sha256"), "valid-FOV mask digest"
|
||||
),
|
||||
artifact_sha256=_string(document.get("artifact_sha256"), "artifact digest"),
|
||||
code_revision=_string(document.get("code_revision"), "code revision"),
|
||||
source_mount_read_only=_boolean(
|
||||
document.get("source_mount_read_only"), "source mount read-only"
|
||||
),
|
||||
model_service_reused=_boolean(
|
||||
document.get("model_service_reused"), "model service reused"
|
||||
),
|
||||
public_worker_port_added=_boolean(
|
||||
document.get("public_worker_port_added"), "public worker port added"
|
||||
),
|
||||
same_host_tensor_transport=_boolean(
|
||||
document.get("same_host_tensor_transport"), "same-host tensor transport"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DetectorReplayFrame:
|
||||
sequence: int
|
||||
envelope: SourceEnvelope
|
||||
outcome: str
|
||||
proposals: tuple[ObjectProposal2D, ...]
|
||||
duration_ns: int
|
||||
failure_code: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.sequence < 0 or self.envelope.sequence != self.sequence:
|
||||
raise DetectorReplayResultError("detector replay frame sequence is invalid")
|
||||
if self.outcome not in {"completed", "failed"}:
|
||||
raise DetectorReplayResultError("detector replay frame outcome is invalid")
|
||||
if self.duration_ns < 0:
|
||||
raise DetectorReplayResultError("detector replay frame duration is invalid")
|
||||
if self.outcome == "completed" and self.failure_code is not None:
|
||||
raise DetectorReplayResultError("completed detector frame cannot carry a failure")
|
||||
if self.outcome == "failed" and (
|
||||
self.proposals or self.failure_code is None or not self.failure_code
|
||||
):
|
||||
raise DetectorReplayResultError("failed detector frame must carry one failure code")
|
||||
proposal_ids: set[str] = set()
|
||||
for proposal in self.proposals:
|
||||
if (
|
||||
proposal.source_id != self.envelope.source_id
|
||||
or proposal.frame_id != self.envelope.frame_id
|
||||
or proposal.provider_id != FROZEN_YOLOX_PROVIDER_ID
|
||||
or proposal.model_id != FROZEN_YOLOX_MODEL_ID
|
||||
or proposal.preprocess_id != FROZEN_YOLOX_PREPROCESS_ID
|
||||
or proposal.provider_tracklet is not None
|
||||
or proposal.proposal_id in proposal_ids
|
||||
):
|
||||
raise DetectorReplayResultError("detector replay proposal ownership changed")
|
||||
proposal_ids.add(proposal.proposal_id)
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": DETECTOR_REPLAY_FRAME_SCHEMA,
|
||||
"sequence": self.sequence,
|
||||
"source_envelope": self.envelope.to_dict(),
|
||||
"outcome": self.outcome,
|
||||
"detector_request_count": 1,
|
||||
"class_routing_used": False,
|
||||
"duration_ns": self.duration_ns,
|
||||
"failure_code": self.failure_code,
|
||||
"proposals": [proposal.to_dict() for proposal in self.proposals],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, value: object) -> DetectorReplayFrame:
|
||||
document = _object(value, "detector replay frame")
|
||||
_exact_keys(
|
||||
document,
|
||||
{
|
||||
"schema_version",
|
||||
"sequence",
|
||||
"source_envelope",
|
||||
"outcome",
|
||||
"detector_request_count",
|
||||
"class_routing_used",
|
||||
"duration_ns",
|
||||
"failure_code",
|
||||
"proposals",
|
||||
},
|
||||
"detector replay frame",
|
||||
)
|
||||
if (
|
||||
document.get("schema_version") != DETECTOR_REPLAY_FRAME_SCHEMA
|
||||
or document.get("detector_request_count") != 1
|
||||
or document.get("class_routing_used") is not False
|
||||
):
|
||||
raise DetectorReplayResultError("detector replay frame contract changed")
|
||||
proposals_value = document.get("proposals")
|
||||
if not isinstance(proposals_value, list):
|
||||
raise DetectorReplayResultError("detector replay proposals must be an array")
|
||||
failure = document.get("failure_code")
|
||||
if failure is not None and not isinstance(failure, str):
|
||||
raise DetectorReplayResultError("detector replay failure code is invalid")
|
||||
return cls(
|
||||
sequence=_integer(document.get("sequence"), "frame sequence"),
|
||||
envelope=SourceEnvelope.from_dict(document.get("source_envelope")),
|
||||
outcome=_string(document.get("outcome"), "frame outcome"),
|
||||
proposals=tuple(ObjectProposal2D.from_dict(item) for item in proposals_value),
|
||||
duration_ns=_integer(document.get("duration_ns"), "frame duration"),
|
||||
failure_code=failure,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DetectorReplayMetrics:
|
||||
frame_count: int
|
||||
completed_frame_count: int
|
||||
failed_frame_count: int
|
||||
proposal_count: int
|
||||
zero_proposal_frame_count: int
|
||||
semantic_hint_count: int
|
||||
provider_tracklet_count: int
|
||||
run_duration_ns: int
|
||||
provider_core_duration_ns: int
|
||||
end_to_end_fps: float
|
||||
provider_core_fps: float
|
||||
frame_latency_p50_ms: float
|
||||
frame_latency_p95_ms: float
|
||||
frame_latency_max_ms: float
|
||||
rejected: tuple[tuple[str, int], ...]
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"frame_count": self.frame_count,
|
||||
"completed_frame_count": self.completed_frame_count,
|
||||
"failed_frame_count": self.failed_frame_count,
|
||||
"proposal_count": self.proposal_count,
|
||||
"zero_proposal_frame_count": self.zero_proposal_frame_count,
|
||||
"semantic_hint_count": self.semantic_hint_count,
|
||||
"provider_tracklet_count": self.provider_tracklet_count,
|
||||
"run_duration_ns": self.run_duration_ns,
|
||||
"provider_core_duration_ns": self.provider_core_duration_ns,
|
||||
"end_to_end_fps": self.end_to_end_fps,
|
||||
"provider_core_fps": self.provider_core_fps,
|
||||
"frame_latency_p50_ms": self.frame_latency_p50_ms,
|
||||
"frame_latency_p95_ms": self.frame_latency_p95_ms,
|
||||
"frame_latency_max_ms": self.frame_latency_max_ms,
|
||||
"rejected": [{"reason": key, "count": count} for key, count in self.rejected],
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DetectorReplayResult:
|
||||
result_id: str
|
||||
result_root: Path
|
||||
accepted: bool
|
||||
metrics: DetectorReplayMetrics
|
||||
runtime: DetectorRuntimeIdentity
|
||||
gate: DetectorReplayGate
|
||||
frames: tuple[DetectorReplayFrame, ...]
|
||||
manifest: dict[str, object]
|
||||
receipt: dict[str, object]
|
||||
|
||||
|
||||
def build_detector_replay_metrics(
|
||||
frames: tuple[DetectorReplayFrame, ...],
|
||||
snapshot: DetectorProviderSnapshot,
|
||||
*,
|
||||
run_duration_ns: int,
|
||||
) -> DetectorReplayMetrics:
|
||||
if run_duration_ns <= 0:
|
||||
raise DetectorReplayResultError("detector replay duration must be positive")
|
||||
completed = sum(frame.outcome == "completed" for frame in frames)
|
||||
failed = len(frames) - completed
|
||||
proposals = tuple(proposal for frame in frames for proposal in frame.proposals)
|
||||
latencies_ms = sorted(frame.duration_ns / 1_000_000 for frame in frames)
|
||||
metrics = DetectorReplayMetrics(
|
||||
frame_count=len(frames),
|
||||
completed_frame_count=completed,
|
||||
failed_frame_count=failed,
|
||||
proposal_count=len(proposals),
|
||||
zero_proposal_frame_count=sum(
|
||||
frame.outcome == "completed" and not frame.proposals for frame in frames
|
||||
),
|
||||
semantic_hint_count=sum(
|
||||
proposal.semantic_hint is not None for proposal in proposals
|
||||
),
|
||||
provider_tracklet_count=sum(
|
||||
proposal.provider_tracklet is not None for proposal in proposals
|
||||
),
|
||||
run_duration_ns=run_duration_ns,
|
||||
provider_core_duration_ns=snapshot.core_duration_ns,
|
||||
end_to_end_fps=round(completed * 1_000_000_000 / run_duration_ns, 6),
|
||||
provider_core_fps=(
|
||||
round(completed * 1_000_000_000 / snapshot.core_duration_ns, 6)
|
||||
if snapshot.core_duration_ns > 0
|
||||
else 0.0
|
||||
),
|
||||
frame_latency_p50_ms=round(_percentile(latencies_ms, 0.5), 6),
|
||||
frame_latency_p95_ms=round(_percentile(latencies_ms, 0.95), 6),
|
||||
frame_latency_max_ms=round(max(latencies_ms, default=0.0), 6),
|
||||
rejected=snapshot.rejected,
|
||||
)
|
||||
if (
|
||||
snapshot.input_frames != len(frames)
|
||||
or snapshot.completed_frames != completed
|
||||
or snapshot.failed_frames != failed
|
||||
or snapshot.proposal_count != len(proposals)
|
||||
or snapshot.zero_proposal_frames != metrics.zero_proposal_frame_count
|
||||
):
|
||||
raise DetectorReplayResultError("provider and replay accounting disagree")
|
||||
return metrics
|
||||
|
||||
|
||||
def detector_replay_accepted(
|
||||
metrics: DetectorReplayMetrics,
|
||||
gate: DetectorReplayGate,
|
||||
source_failure_code: str | None,
|
||||
) -> bool:
|
||||
return (
|
||||
source_failure_code is None
|
||||
and metrics.frame_count == gate.expected_frames
|
||||
and metrics.completed_frame_count == gate.expected_frames
|
||||
and metrics.failed_frame_count == 0
|
||||
and metrics.provider_tracklet_count == 0
|
||||
and metrics.end_to_end_fps >= gate.minimum_end_to_end_fps
|
||||
)
|
||||
|
||||
|
||||
def _percentile(values: list[float], fraction: float) -> float:
|
||||
if not values:
|
||||
return 0.0
|
||||
index = (len(values) - 1) * fraction
|
||||
lower = math.floor(index)
|
||||
upper = math.ceil(index)
|
||||
if lower == upper:
|
||||
return values[lower]
|
||||
ratio = index - lower
|
||||
return values[lower] * (1.0 - ratio) + values[upper] * ratio
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, object]:
|
||||
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
|
||||
raise DetectorReplayResultError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _exact_keys(document: dict[str, object], expected: set[str], label: str) -> None:
|
||||
if set(document) != expected:
|
||||
raise DetectorReplayResultError(f"{label} fields are incompatible")
|
||||
|
||||
|
||||
def _string(value: object, label: str) -> str:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise DetectorReplayResultError(f"{label} must be a nonempty string")
|
||||
return value
|
||||
|
||||
|
||||
def _identifier(value: str, label: str) -> None:
|
||||
if _IDENTIFIER.fullmatch(value) is None:
|
||||
raise DetectorReplayResultError(f"{label} is invalid")
|
||||
|
||||
|
||||
def _integer(value: object, label: str) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
||||
raise DetectorReplayResultError(f"{label} must be a nonnegative integer")
|
||||
return value
|
||||
|
||||
|
||||
def _number(value: object, label: str) -> float:
|
||||
if not isinstance(value, int | float) or isinstance(value, bool):
|
||||
raise DetectorReplayResultError(f"{label} must be numeric")
|
||||
converted = float(value)
|
||||
if not math.isfinite(converted):
|
||||
raise DetectorReplayResultError(f"{label} must be finite")
|
||||
return converted
|
||||
|
||||
|
||||
def _boolean(value: object, label: str) -> bool:
|
||||
if not isinstance(value, bool):
|
||||
raise DetectorReplayResultError(f"{label} must be boolean")
|
||||
return value
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DETECTOR_REPLAY_FRAME_SCHEMA",
|
||||
"DETECTOR_REPLAY_FRAMES_NAME",
|
||||
"DETECTOR_REPLAY_MANIFEST_NAME",
|
||||
"DETECTOR_REPLAY_RECEIPT_NAME",
|
||||
"DETECTOR_REPLAY_RECEIPT_SCHEMA",
|
||||
"DETECTOR_REPLAY_RESULT_PREFIX",
|
||||
"DETECTOR_REPLAY_RESULT_SCHEMA",
|
||||
"DETECTOR_RUNTIME_IDENTITY_SCHEMA",
|
||||
"M4_DETECTOR_REPLAY_GATE",
|
||||
"DetectorReplayFrame",
|
||||
"DetectorReplayGate",
|
||||
"DetectorReplayMetrics",
|
||||
"DetectorReplayResult",
|
||||
"DetectorReplayResultError",
|
||||
"DetectorRuntimeIdentity",
|
||||
"build_detector_replay_metrics",
|
||||
"detector_replay_accepted",
|
||||
]
|
||||
@@ -0,0 +1,220 @@
|
||||
"""Atomic publication API for product detector replay evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.yolox_object_detector import (
|
||||
FROZEN_YOLOX_CONFIG,
|
||||
YOLOX_CONFIG_SHA256,
|
||||
YOLOX_MODEL_SHA256,
|
||||
YOLOX_MODEL_VERSION,
|
||||
YOLOX_VALID_FOV_SHA256,
|
||||
)
|
||||
|
||||
from .baseline import (
|
||||
BASELINE_CAMERA_STREAM_SHA256,
|
||||
BASELINE_PREPROCESS_PROFILE_SHA256,
|
||||
BASELINE_PROFILE_ID,
|
||||
BASELINE_RECORDED_JOB_ID,
|
||||
BASELINE_SESSION_ID,
|
||||
BASELINE_SOURCE_ID,
|
||||
)
|
||||
from .contracts import FalseAuthority
|
||||
from .detector import (
|
||||
FROZEN_YOLOX_MODEL_ID,
|
||||
FROZEN_YOLOX_PREPROCESS_ID,
|
||||
FROZEN_YOLOX_PROVIDER_ID,
|
||||
)
|
||||
from .detector_replay_contracts import (
|
||||
DETECTOR_REPLAY_FRAME_SCHEMA,
|
||||
DETECTOR_REPLAY_FRAMES_NAME,
|
||||
DETECTOR_REPLAY_MANIFEST_NAME,
|
||||
DETECTOR_REPLAY_RECEIPT_NAME,
|
||||
DETECTOR_REPLAY_RECEIPT_SCHEMA,
|
||||
DETECTOR_REPLAY_RESULT_PREFIX,
|
||||
DETECTOR_REPLAY_RESULT_SCHEMA,
|
||||
DETECTOR_RUNTIME_IDENTITY_SCHEMA,
|
||||
M4_DETECTOR_REPLAY_GATE,
|
||||
DetectorReplayFrame,
|
||||
DetectorReplayGate,
|
||||
DetectorReplayMetrics,
|
||||
DetectorReplayResult,
|
||||
DetectorReplayResultError,
|
||||
DetectorRuntimeIdentity,
|
||||
build_detector_replay_metrics,
|
||||
detector_replay_accepted,
|
||||
)
|
||||
from .detector_replay_validation import (
|
||||
read_detector_replay_result as _read_detector_replay_result,
|
||||
)
|
||||
from .detector_replay_validation import validate_frame_accounting
|
||||
from .recorded_source import RECORDED_SOURCE_PROVIDER_ID
|
||||
|
||||
|
||||
def seal_detector_replay_result(
|
||||
*,
|
||||
output_root: Path,
|
||||
frames: tuple[DetectorReplayFrame, ...],
|
||||
metrics: DetectorReplayMetrics,
|
||||
runtime: DetectorRuntimeIdentity,
|
||||
gate: DetectorReplayGate,
|
||||
source_failure_code: str | None,
|
||||
created_at_utc: str | None = None,
|
||||
) -> DetectorReplayResult:
|
||||
validate_frame_accounting(frames, metrics)
|
||||
root = output_root.expanduser().absolute()
|
||||
root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = root / f".detector-replay.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
frames_path = staging / DETECTOR_REPLAY_FRAMES_NAME
|
||||
with frames_path.open("wb") as handle:
|
||||
for frame in frames:
|
||||
handle.write(_canonical_json(frame.to_dict()) + b"\n")
|
||||
frames_sha256 = _file_sha256(frames_path)
|
||||
accepted = detector_replay_accepted(metrics, gate, source_failure_code)
|
||||
identity = {
|
||||
"schema_version": DETECTOR_REPLAY_RESULT_SCHEMA,
|
||||
"baseline_profile_id": BASELINE_PROFILE_ID,
|
||||
"source": {
|
||||
"provider_id": RECORDED_SOURCE_PROVIDER_ID,
|
||||
"source_id": BASELINE_SOURCE_ID,
|
||||
"session_id": BASELINE_SESSION_ID,
|
||||
"camera_artifact_id": BASELINE_RECORDED_JOB_ID,
|
||||
"camera_stream_sha256": BASELINE_CAMERA_STREAM_SHA256,
|
||||
},
|
||||
"detector": {
|
||||
"provider_id": FROZEN_YOLOX_PROVIDER_ID,
|
||||
"model_id": FROZEN_YOLOX_MODEL_ID,
|
||||
"model_version": YOLOX_MODEL_VERSION,
|
||||
"model_sha256": YOLOX_MODEL_SHA256,
|
||||
"model_config_sha256": YOLOX_CONFIG_SHA256,
|
||||
"valid_fov_mask_sha256": YOLOX_VALID_FOV_SHA256,
|
||||
"preprocess_id": FROZEN_YOLOX_PREPROCESS_ID,
|
||||
"preprocess_profile_sha256": BASELINE_PREPROCESS_PROFILE_SHA256,
|
||||
"minimum_score": FROZEN_YOLOX_CONFIG.minimum_score,
|
||||
"nms_iou_threshold": FROZEN_YOLOX_CONFIG.nms_iou_threshold,
|
||||
"target_class_ids": list(FROZEN_YOLOX_CONFIG.target_class_ids),
|
||||
"single_inference_per_frame": True,
|
||||
"class_routing_used": False,
|
||||
"provider_tracklets_used": False,
|
||||
},
|
||||
"runtime": runtime.to_dict(),
|
||||
"gate": gate.to_dict(),
|
||||
"metrics": metrics.to_dict(),
|
||||
"source_failure_code": source_failure_code,
|
||||
"frames_sha256": frames_sha256,
|
||||
"accepted": accepted,
|
||||
"authority": FalseAuthority().to_dict(),
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"{DETECTOR_REPLAY_RESULT_PREFIX}{identity_sha256}"
|
||||
created = created_at_utc or datetime.now(UTC).isoformat(timespec="milliseconds").replace(
|
||||
"+00:00", "Z"
|
||||
)
|
||||
receipt = {
|
||||
"schema_version": DETECTOR_REPLAY_RECEIPT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": created,
|
||||
"accepted": accepted,
|
||||
"source_failure_code": source_failure_code,
|
||||
"gate": gate.to_dict(),
|
||||
"metrics": metrics.to_dict(),
|
||||
"authority": FalseAuthority().to_dict(),
|
||||
}
|
||||
receipt_path = staging / DETECTOR_REPLAY_RECEIPT_NAME
|
||||
_write_json(receipt_path, receipt)
|
||||
manifest = {
|
||||
"schema_version": DETECTOR_REPLAY_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": created,
|
||||
"accepted": accepted,
|
||||
"artifacts": [
|
||||
_artifact(receipt_path, "detector-replay-receipt"),
|
||||
_artifact(frames_path, "detector-replay-frames"),
|
||||
],
|
||||
}
|
||||
_write_json(staging / DETECTOR_REPLAY_MANIFEST_NAME, manifest)
|
||||
destination = root / result_id
|
||||
if destination.exists():
|
||||
shutil.rmtree(staging)
|
||||
return read_detector_replay_result(destination)
|
||||
os.replace(staging, destination)
|
||||
return read_detector_replay_result(destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
|
||||
|
||||
def read_detector_replay_result(root: Path) -> DetectorReplayResult:
|
||||
return _read_detector_replay_result(root)
|
||||
|
||||
|
||||
def require_m4_detector_replay_acceptance(result: DetectorReplayResult) -> None:
|
||||
"""Fail unless a result closes the exact M4.3 Worker 006 detector gate."""
|
||||
|
||||
if (
|
||||
not result.accepted
|
||||
or result.gate != M4_DETECTOR_REPLAY_GATE
|
||||
or result.runtime.worker_id != "worker-006"
|
||||
or result.runtime.worker_node != "DESKTOP-OPJ8J04"
|
||||
):
|
||||
raise DetectorReplayResultError("result does not close the M4.3 Worker 006 gate")
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, object]:
|
||||
return {
|
||||
"role": role,
|
||||
"path": path.name,
|
||||
"bytes": path.stat().st_size,
|
||||
"sha256": _file_sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_bytes(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DETECTOR_REPLAY_FRAME_SCHEMA",
|
||||
"DETECTOR_REPLAY_RESULT_SCHEMA",
|
||||
"DETECTOR_RUNTIME_IDENTITY_SCHEMA",
|
||||
"M4_DETECTOR_REPLAY_GATE",
|
||||
"DetectorReplayFrame",
|
||||
"DetectorReplayGate",
|
||||
"DetectorReplayMetrics",
|
||||
"DetectorReplayResult",
|
||||
"DetectorReplayResultError",
|
||||
"DetectorRuntimeIdentity",
|
||||
"build_detector_replay_metrics",
|
||||
"read_detector_replay_result",
|
||||
"require_m4_detector_replay_acceptance",
|
||||
"seal_detector_replay_result",
|
||||
]
|
||||
@@ -0,0 +1,402 @@
|
||||
"""Fail-closed reader and consistency checks for detector replay evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.yolox_object_detector import (
|
||||
FROZEN_YOLOX_CONFIG,
|
||||
YOLOX_CONFIG_SHA256,
|
||||
YOLOX_MODEL_SHA256,
|
||||
YOLOX_MODEL_VERSION,
|
||||
YOLOX_VALID_FOV_SHA256,
|
||||
)
|
||||
|
||||
from .baseline import (
|
||||
BASELINE_CAMERA_STREAM_SHA256,
|
||||
BASELINE_PREPROCESS_PROFILE_SHA256,
|
||||
BASELINE_PROFILE_ID,
|
||||
BASELINE_RECORDED_JOB_ID,
|
||||
BASELINE_SESSION_ID,
|
||||
BASELINE_SOURCE_ID,
|
||||
)
|
||||
from .contracts import FalseAuthority
|
||||
from .detector import (
|
||||
FROZEN_YOLOX_MODEL_ID,
|
||||
FROZEN_YOLOX_PREPROCESS_ID,
|
||||
FROZEN_YOLOX_PROVIDER_ID,
|
||||
)
|
||||
from .detector_replay_contracts import (
|
||||
_RESULT_ID,
|
||||
_SHA256,
|
||||
DETECTOR_REPLAY_FRAMES_NAME,
|
||||
DETECTOR_REPLAY_MANIFEST_NAME,
|
||||
DETECTOR_REPLAY_RECEIPT_NAME,
|
||||
DETECTOR_REPLAY_RECEIPT_SCHEMA,
|
||||
DETECTOR_REPLAY_RESULT_PREFIX,
|
||||
DETECTOR_REPLAY_RESULT_SCHEMA,
|
||||
DetectorReplayFrame,
|
||||
DetectorReplayGate,
|
||||
DetectorReplayMetrics,
|
||||
DetectorReplayResult,
|
||||
DetectorReplayResultError,
|
||||
DetectorRuntimeIdentity,
|
||||
_exact_keys,
|
||||
_integer,
|
||||
_number,
|
||||
_object,
|
||||
_percentile,
|
||||
_string,
|
||||
detector_replay_accepted,
|
||||
)
|
||||
from .recorded_source import RECORDED_SOURCE_PROVIDER_ID
|
||||
|
||||
|
||||
def read_detector_replay_result(root: Path) -> DetectorReplayResult:
|
||||
resolved = root.resolve(strict=True)
|
||||
if resolved.is_symlink() or _RESULT_ID.fullmatch(resolved.name) is None:
|
||||
raise DetectorReplayResultError("detector replay result root is invalid")
|
||||
manifest = _read_json(resolved / DETECTOR_REPLAY_MANIFEST_NAME)
|
||||
_exact_keys(
|
||||
manifest,
|
||||
{
|
||||
"schema_version",
|
||||
"result_id",
|
||||
"identity_sha256",
|
||||
"identity",
|
||||
"created_at_utc",
|
||||
"accepted",
|
||||
"artifacts",
|
||||
},
|
||||
"detector replay manifest",
|
||||
)
|
||||
identity = _object(manifest.get("identity"), "detector replay identity")
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
if (
|
||||
manifest.get("schema_version") != DETECTOR_REPLAY_RESULT_SCHEMA
|
||||
or manifest.get("result_id") != resolved.name
|
||||
or manifest.get("identity_sha256") != identity_sha256
|
||||
or resolved.name != f"{DETECTOR_REPLAY_RESULT_PREFIX}{identity_sha256}"
|
||||
):
|
||||
raise DetectorReplayResultError("detector replay identity changed")
|
||||
_validate_identity(identity)
|
||||
runtime = DetectorRuntimeIdentity.from_dict(identity.get("runtime"))
|
||||
gate = DetectorReplayGate.from_dict(identity.get("gate"))
|
||||
artifacts_value = manifest.get("artifacts")
|
||||
if not isinstance(artifacts_value, list) or len(artifacts_value) != 2:
|
||||
raise DetectorReplayResultError("detector replay artifact inventory changed")
|
||||
artifacts = {
|
||||
_string(_object(item, "artifact").get("role"), "artifact role"): item
|
||||
for item in artifacts_value
|
||||
}
|
||||
if set(artifacts) != {"detector-replay-receipt", "detector-replay-frames"}:
|
||||
raise DetectorReplayResultError("detector replay artifact roles changed")
|
||||
receipt_path = _validated_artifact(
|
||||
resolved, artifacts["detector-replay-receipt"], DETECTOR_REPLAY_RECEIPT_NAME
|
||||
)
|
||||
frames_path = _validated_artifact(
|
||||
resolved, artifacts["detector-replay-frames"], DETECTOR_REPLAY_FRAMES_NAME
|
||||
)
|
||||
if _file_sha256(frames_path) != identity.get("frames_sha256"):
|
||||
raise DetectorReplayResultError("detector replay frame digest changed")
|
||||
receipt = _read_json(receipt_path)
|
||||
_validate_receipt(receipt, manifest, identity)
|
||||
frames = tuple(
|
||||
DetectorReplayFrame.from_dict(value)
|
||||
for value in _read_jsonl(frames_path, "detector replay frames")
|
||||
)
|
||||
metrics = _metrics_from_dict(identity.get("metrics"))
|
||||
validate_frame_accounting(frames, metrics)
|
||||
source_failure = identity.get("source_failure_code")
|
||||
if source_failure is not None and not isinstance(source_failure, str):
|
||||
raise DetectorReplayResultError("detector replay source failure code changed")
|
||||
accepted = detector_replay_accepted(metrics, gate, source_failure)
|
||||
if manifest.get("accepted") is not accepted or identity.get("accepted") is not accepted:
|
||||
raise DetectorReplayResultError("detector replay acceptance changed")
|
||||
return DetectorReplayResult(
|
||||
result_id=resolved.name,
|
||||
result_root=resolved,
|
||||
accepted=accepted,
|
||||
metrics=metrics,
|
||||
runtime=runtime,
|
||||
gate=gate,
|
||||
frames=frames,
|
||||
manifest=manifest,
|
||||
receipt=receipt,
|
||||
)
|
||||
|
||||
|
||||
def validate_frame_accounting(
|
||||
frames: tuple[DetectorReplayFrame, ...], metrics: DetectorReplayMetrics
|
||||
) -> None:
|
||||
if metrics.run_duration_ns <= 0:
|
||||
raise DetectorReplayResultError("detector replay duration must be positive")
|
||||
if any(frame.sequence != index for index, frame in enumerate(frames)):
|
||||
raise DetectorReplayResultError("detector replay frame sequence is incomplete")
|
||||
if any(
|
||||
frame.envelope.source_id != BASELINE_SOURCE_ID
|
||||
or frame.envelope.session_id != BASELINE_SESSION_ID
|
||||
for frame in frames
|
||||
):
|
||||
raise DetectorReplayResultError("detector replay source ownership changed")
|
||||
completed = sum(frame.outcome == "completed" for frame in frames)
|
||||
failed = len(frames) - completed
|
||||
proposals = tuple(proposal for frame in frames for proposal in frame.proposals)
|
||||
latencies_ms = sorted(frame.duration_ns / 1_000_000 for frame in frames)
|
||||
expected_end_to_end_fps = round(
|
||||
completed * 1_000_000_000 / metrics.run_duration_ns, 6
|
||||
)
|
||||
expected_provider_fps = (
|
||||
round(completed * 1_000_000_000 / metrics.provider_core_duration_ns, 6)
|
||||
if metrics.provider_core_duration_ns > 0
|
||||
else 0.0
|
||||
)
|
||||
if (
|
||||
metrics.frame_count != len(frames)
|
||||
or metrics.completed_frame_count != completed
|
||||
or metrics.failed_frame_count != failed
|
||||
or metrics.proposal_count != len(proposals)
|
||||
or metrics.zero_proposal_frame_count
|
||||
!= sum(frame.outcome == "completed" and not frame.proposals for frame in frames)
|
||||
or metrics.semantic_hint_count
|
||||
!= sum(proposal.semantic_hint is not None for proposal in proposals)
|
||||
or metrics.provider_tracklet_count
|
||||
!= sum(proposal.provider_tracklet is not None for proposal in proposals)
|
||||
or metrics.end_to_end_fps != expected_end_to_end_fps
|
||||
or metrics.provider_core_fps != expected_provider_fps
|
||||
or metrics.frame_latency_p50_ms != round(_percentile(latencies_ms, 0.5), 6)
|
||||
or metrics.frame_latency_p95_ms != round(_percentile(latencies_ms, 0.95), 6)
|
||||
or metrics.frame_latency_max_ms != round(max(latencies_ms, default=0.0), 6)
|
||||
):
|
||||
raise DetectorReplayResultError("detector replay metrics and frames disagree")
|
||||
|
||||
|
||||
def _validate_identity(identity: dict[str, object]) -> None:
|
||||
_exact_keys(
|
||||
identity,
|
||||
{
|
||||
"schema_version",
|
||||
"baseline_profile_id",
|
||||
"source",
|
||||
"detector",
|
||||
"runtime",
|
||||
"gate",
|
||||
"metrics",
|
||||
"source_failure_code",
|
||||
"frames_sha256",
|
||||
"accepted",
|
||||
"authority",
|
||||
},
|
||||
"detector replay identity",
|
||||
)
|
||||
source = _object(identity.get("source"), "detector replay source")
|
||||
detector = _object(identity.get("detector"), "detector replay provider")
|
||||
if (
|
||||
identity.get("schema_version") != DETECTOR_REPLAY_RESULT_SCHEMA
|
||||
or identity.get("baseline_profile_id") != BASELINE_PROFILE_ID
|
||||
or source
|
||||
!= {
|
||||
"provider_id": RECORDED_SOURCE_PROVIDER_ID,
|
||||
"source_id": BASELINE_SOURCE_ID,
|
||||
"session_id": BASELINE_SESSION_ID,
|
||||
"camera_artifact_id": BASELINE_RECORDED_JOB_ID,
|
||||
"camera_stream_sha256": BASELINE_CAMERA_STREAM_SHA256,
|
||||
}
|
||||
or detector
|
||||
!= {
|
||||
"provider_id": FROZEN_YOLOX_PROVIDER_ID,
|
||||
"model_id": FROZEN_YOLOX_MODEL_ID,
|
||||
"model_version": YOLOX_MODEL_VERSION,
|
||||
"model_sha256": YOLOX_MODEL_SHA256,
|
||||
"model_config_sha256": YOLOX_CONFIG_SHA256,
|
||||
"valid_fov_mask_sha256": YOLOX_VALID_FOV_SHA256,
|
||||
"preprocess_id": FROZEN_YOLOX_PREPROCESS_ID,
|
||||
"preprocess_profile_sha256": BASELINE_PREPROCESS_PROFILE_SHA256,
|
||||
"minimum_score": FROZEN_YOLOX_CONFIG.minimum_score,
|
||||
"nms_iou_threshold": FROZEN_YOLOX_CONFIG.nms_iou_threshold,
|
||||
"target_class_ids": list(FROZEN_YOLOX_CONFIG.target_class_ids),
|
||||
"single_inference_per_frame": True,
|
||||
"class_routing_used": False,
|
||||
"provider_tracklets_used": False,
|
||||
}
|
||||
or identity.get("authority") != FalseAuthority().to_dict()
|
||||
or _SHA256.fullmatch(_string(identity.get("frames_sha256"), "frame digest")) is None
|
||||
):
|
||||
raise DetectorReplayResultError("detector replay frozen identity changed")
|
||||
|
||||
|
||||
def _validate_receipt(
|
||||
receipt: dict[str, object],
|
||||
manifest: dict[str, object],
|
||||
identity: dict[str, object],
|
||||
) -> None:
|
||||
_exact_keys(
|
||||
receipt,
|
||||
{
|
||||
"schema_version",
|
||||
"result_id",
|
||||
"identity_sha256",
|
||||
"created_at_utc",
|
||||
"accepted",
|
||||
"source_failure_code",
|
||||
"gate",
|
||||
"metrics",
|
||||
"authority",
|
||||
},
|
||||
"detector replay receipt",
|
||||
)
|
||||
if (
|
||||
receipt.get("schema_version") != DETECTOR_REPLAY_RECEIPT_SCHEMA
|
||||
or receipt.get("result_id") != manifest.get("result_id")
|
||||
or receipt.get("identity_sha256") != manifest.get("identity_sha256")
|
||||
or receipt.get("created_at_utc") != manifest.get("created_at_utc")
|
||||
or receipt.get("accepted") != manifest.get("accepted")
|
||||
or receipt.get("source_failure_code") != identity.get("source_failure_code")
|
||||
or receipt.get("gate") != identity.get("gate")
|
||||
or receipt.get("metrics") != identity.get("metrics")
|
||||
or receipt.get("authority") != identity.get("authority")
|
||||
):
|
||||
raise DetectorReplayResultError("detector replay receipt changed")
|
||||
|
||||
|
||||
def _metrics_from_dict(value: object) -> DetectorReplayMetrics:
|
||||
document = _object(value, "detector replay metrics")
|
||||
fields = {
|
||||
"frame_count",
|
||||
"completed_frame_count",
|
||||
"failed_frame_count",
|
||||
"proposal_count",
|
||||
"zero_proposal_frame_count",
|
||||
"semantic_hint_count",
|
||||
"provider_tracklet_count",
|
||||
"run_duration_ns",
|
||||
"provider_core_duration_ns",
|
||||
"end_to_end_fps",
|
||||
"provider_core_fps",
|
||||
"frame_latency_p50_ms",
|
||||
"frame_latency_p95_ms",
|
||||
"frame_latency_max_ms",
|
||||
"rejected",
|
||||
}
|
||||
_exact_keys(document, fields, "detector replay metrics")
|
||||
rejected_value = document.get("rejected")
|
||||
if not isinstance(rejected_value, list):
|
||||
raise DetectorReplayResultError("detector rejected accounting is invalid")
|
||||
rejected: list[tuple[str, int]] = []
|
||||
for item in rejected_value:
|
||||
row = _object(item, "detector rejection")
|
||||
_exact_keys(row, {"reason", "count"}, "detector rejection")
|
||||
rejected.append(
|
||||
(
|
||||
_string(row.get("reason"), "rejection reason"),
|
||||
_integer(row.get("count"), "rejection count"),
|
||||
)
|
||||
)
|
||||
if rejected != sorted(rejected) or len({reason for reason, _ in rejected}) != len(rejected):
|
||||
raise DetectorReplayResultError("detector rejection accounting is not canonical")
|
||||
metrics = DetectorReplayMetrics(
|
||||
frame_count=_integer(document.get("frame_count"), "frame count"),
|
||||
completed_frame_count=_integer(
|
||||
document.get("completed_frame_count"), "completed frame count"
|
||||
),
|
||||
failed_frame_count=_integer(document.get("failed_frame_count"), "failed frame count"),
|
||||
proposal_count=_integer(document.get("proposal_count"), "proposal count"),
|
||||
zero_proposal_frame_count=_integer(
|
||||
document.get("zero_proposal_frame_count"), "zero-proposal frame count"
|
||||
),
|
||||
semantic_hint_count=_integer(
|
||||
document.get("semantic_hint_count"), "semantic hint count"
|
||||
),
|
||||
provider_tracklet_count=_integer(
|
||||
document.get("provider_tracklet_count"), "provider tracklet count"
|
||||
),
|
||||
run_duration_ns=_integer(document.get("run_duration_ns"), "run duration"),
|
||||
provider_core_duration_ns=_integer(
|
||||
document.get("provider_core_duration_ns"), "provider core duration"
|
||||
),
|
||||
end_to_end_fps=_number(document.get("end_to_end_fps"), "end-to-end FPS"),
|
||||
provider_core_fps=_number(document.get("provider_core_fps"), "provider core FPS"),
|
||||
frame_latency_p50_ms=_number(
|
||||
document.get("frame_latency_p50_ms"), "frame p50 latency"
|
||||
),
|
||||
frame_latency_p95_ms=_number(
|
||||
document.get("frame_latency_p95_ms"), "frame p95 latency"
|
||||
),
|
||||
frame_latency_max_ms=_number(
|
||||
document.get("frame_latency_max_ms"), "frame maximum latency"
|
||||
),
|
||||
rejected=tuple(rejected),
|
||||
)
|
||||
if metrics.run_duration_ns <= 0 or any(
|
||||
value < 0.0
|
||||
for value in (
|
||||
metrics.end_to_end_fps,
|
||||
metrics.provider_core_fps,
|
||||
metrics.frame_latency_p50_ms,
|
||||
metrics.frame_latency_p95_ms,
|
||||
metrics.frame_latency_max_ms,
|
||||
)
|
||||
):
|
||||
raise DetectorReplayResultError("detector replay runtime metrics are invalid")
|
||||
return metrics
|
||||
|
||||
|
||||
def _validated_artifact(root: Path, value: object, expected_name: str) -> Path:
|
||||
artifact = _object(value, "detector replay artifact")
|
||||
_exact_keys(artifact, {"role", "path", "bytes", "sha256"}, "artifact")
|
||||
if artifact.get("path") != expected_name:
|
||||
raise DetectorReplayResultError("detector replay artifact path changed")
|
||||
path = (root / expected_name).resolve(strict=True)
|
||||
if path.parent != root or path.is_symlink():
|
||||
raise DetectorReplayResultError("detector replay artifact escapes its result")
|
||||
if (
|
||||
artifact.get("bytes") != path.stat().st_size
|
||||
or artifact.get("sha256") != _file_sha256(path)
|
||||
):
|
||||
raise DetectorReplayResultError("detector replay artifact changed")
|
||||
return path
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, object]:
|
||||
try:
|
||||
value = json.loads(path.read_text("utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise DetectorReplayResultError(f"cannot read detector replay JSON: {path.name}") from exc
|
||||
return _object(value, path.name)
|
||||
|
||||
|
||||
def _read_jsonl(path: Path, label: str) -> tuple[dict[str, object], ...]:
|
||||
documents: list[dict[str, object]] = []
|
||||
try:
|
||||
lines = path.read_text("utf-8").splitlines()
|
||||
except OSError as exc:
|
||||
raise DetectorReplayResultError(f"cannot read {label}") from exc
|
||||
for line_number, line in enumerate(lines, start=1):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise DetectorReplayResultError(f"{label} line {line_number} is invalid") from exc
|
||||
documents.append(_object(value, f"{label} line {line_number}"))
|
||||
return tuple(documents)
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
__all__ = ["read_detector_replay_result", "validate_frame_accounting"]
|
||||
Reference in New Issue
Block a user