feat(lab): stabilize autonomous TGS playback
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
"""Build and verify autonomous chunked M49 physical-safety playback artifacts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import shutil
|
||||
import struct
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
import numpy as np
|
||||
|
||||
SCHEMA: Final = "missioncore.m49-physical-safety-playback/v1"
|
||||
PROFILE_SCHEMA: Final = "missioncore.m49-physical-safety-shadow-profile/v1"
|
||||
PREFIX: Final = "m49-physical-safety-playback-"
|
||||
CHUNK_MAGIC: Final = b"MCPSCH01"
|
||||
CHUNK_HEADER: Final = struct.Struct("<8sIIIII")
|
||||
SOURCE_TRACKS: Final = (
|
||||
"costmap-cell-centers-xy-m.npy",
|
||||
"costmap-states.npy",
|
||||
"costmap-z-bounds-m.npy",
|
||||
"frames.ndjson",
|
||||
)
|
||||
_HASH_BLOCK_BYTES: Final = 1024 * 1024
|
||||
_MAX_JSON_BYTES: Final = 4 * 1024 * 1024
|
||||
|
||||
|
||||
class M49PhysicalSafetyPlaybackError(RuntimeError):
|
||||
"""The autonomous playback artifact is missing or violates its contract."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class M49PhysicalSafetyPlayback:
|
||||
result_id: str
|
||||
root: Path
|
||||
manifest: dict[str, Any]
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(_HASH_BLOCK_BYTES), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _canonical_sha256(value: object) -> str:
|
||||
content = json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
return hashlib.sha256(content).hexdigest()
|
||||
|
||||
|
||||
def _json(path: Path, label: str) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file() or path.stat().st_size > _MAX_JSON_BYTES:
|
||||
raise M49PhysicalSafetyPlaybackError(f"{label} is unavailable")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
||||
raise M49PhysicalSafetyPlaybackError(f"{label} is invalid") from error
|
||||
if not isinstance(value, dict):
|
||||
raise M49PhysicalSafetyPlaybackError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _artifact(path: Path, *, media_type: str, role: str) -> dict[str, object]:
|
||||
return {
|
||||
"path": path.name,
|
||||
"media_type": media_type,
|
||||
"role": role,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _source_artifacts(source: Path, manifest: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list):
|
||||
raise M49PhysicalSafetyPlaybackError("source artifact catalog is unavailable")
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
for value in artifacts:
|
||||
if not isinstance(value, dict) or not isinstance(value.get("path"), str):
|
||||
raise M49PhysicalSafetyPlaybackError("source artifact catalog is invalid")
|
||||
name = value["path"]
|
||||
if name in (*SOURCE_TRACKS, "report.json"):
|
||||
path = source / name
|
||||
if (
|
||||
path.parent != source
|
||||
or path.is_symlink()
|
||||
or not path.is_file()
|
||||
or value.get("byte_length") != path.stat().st_size
|
||||
or value.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise M49PhysicalSafetyPlaybackError(f"source artifact changed: {name}")
|
||||
result[name] = value
|
||||
if set(result) != {*SOURCE_TRACKS, "report.json"}:
|
||||
raise M49PhysicalSafetyPlaybackError("source spatial tracks are incomplete")
|
||||
return result
|
||||
|
||||
|
||||
def _state_codes(report: dict[str, Any]) -> dict[str, int]:
|
||||
candidates = (
|
||||
report.get("state_codes"),
|
||||
report.get("visual_review", {}).get("state_codes")
|
||||
if isinstance(report.get("visual_review"), dict)
|
||||
else None,
|
||||
)
|
||||
for candidate in candidates:
|
||||
if not isinstance(candidate, dict):
|
||||
continue
|
||||
parsed = {
|
||||
str(name): int(code)
|
||||
for name, code in candidate.items()
|
||||
if isinstance(name, str)
|
||||
and isinstance(code, int)
|
||||
and not isinstance(code, bool)
|
||||
and 0 <= code <= 255
|
||||
}
|
||||
if len(parsed) == len(candidate) and parsed.get("UNOBSERVED") == 0:
|
||||
return parsed
|
||||
raise M49PhysicalSafetyPlaybackError("source state codes are unavailable")
|
||||
|
||||
|
||||
def _frame_rows(path: Path) -> tuple[dict[str, Any], ...]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
for sequence, line in enumerate(stream):
|
||||
value = json.loads(line)
|
||||
if (
|
||||
not isinstance(value, dict)
|
||||
or not isinstance(value.get("sample_available"), bool)
|
||||
or not isinstance(value.get("source_frame_index"), int)
|
||||
or isinstance(value.get("source_frame_index"), bool)
|
||||
or not isinstance(value.get("session_seconds"), (int, float))
|
||||
or isinstance(value.get("session_seconds"), bool)
|
||||
or not math.isfinite(float(value["session_seconds"]))
|
||||
):
|
||||
raise M49PhysicalSafetyPlaybackError(
|
||||
f"source frame catalog changed at sequence {sequence}"
|
||||
)
|
||||
rows.append(value)
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
||||
raise M49PhysicalSafetyPlaybackError("source frame catalog is invalid") from error
|
||||
if not rows:
|
||||
raise M49PhysicalSafetyPlaybackError("source frame catalog is empty")
|
||||
return tuple(rows)
|
||||
|
||||
|
||||
def _source_geometry(report: dict[str, Any]) -> tuple[float, float, str]:
|
||||
configuration = report.get("configuration")
|
||||
if not isinstance(configuration, dict):
|
||||
raise M49PhysicalSafetyPlaybackError("source geometry is unavailable")
|
||||
cell_size = configuration.get("cell_size_m")
|
||||
radius = configuration.get("radius_m")
|
||||
coordinate_frame = configuration.get("coordinate_frame")
|
||||
if (
|
||||
not isinstance(cell_size, (int, float))
|
||||
or isinstance(cell_size, bool)
|
||||
or not math.isfinite(float(cell_size))
|
||||
or float(cell_size) <= 0
|
||||
or not isinstance(radius, (int, float))
|
||||
or isinstance(radius, bool)
|
||||
or not math.isfinite(float(radius))
|
||||
or float(radius) <= 0
|
||||
or coordinate_frame != "map-gravity-local"
|
||||
):
|
||||
raise M49PhysicalSafetyPlaybackError("source geometry changed")
|
||||
return float(cell_size), float(radius), coordinate_frame
|
||||
|
||||
|
||||
def _profile_policy(profile: dict[str, Any], cell_size_m: float) -> dict[str, int]:
|
||||
visual = profile.get("operator_visual")
|
||||
terrain = profile.get("terrain_evidence")
|
||||
if (
|
||||
profile.get("schema_version") != PROFILE_SCHEMA
|
||||
or not isinstance(visual, dict)
|
||||
or not isinstance(terrain, dict)
|
||||
or visual.get("worker_role") != "realtime-only"
|
||||
or visual.get("worker_runtime_dependency") is not False
|
||||
or visual.get("sealed_artifact_required") is not True
|
||||
):
|
||||
raise M49PhysicalSafetyPlaybackError("physical-safety profile changed")
|
||||
playback = visual.get("playback_buffer")
|
||||
admitted_sizes = [
|
||||
terrain.get("accepted_baseline_cell_size_m"),
|
||||
*(terrain.get("challenger_cell_sizes_m") or []),
|
||||
]
|
||||
if (
|
||||
not isinstance(playback, dict)
|
||||
or playback.get("chunk_frame_count") != 32
|
||||
or playback.get("startup_prebuffer_chunk_count") != 2
|
||||
or playback.get("resident_chunk_count_max") != 3
|
||||
or not any(
|
||||
isinstance(value, (int, float))
|
||||
and not isinstance(value, bool)
|
||||
and math.isclose(float(value), cell_size_m, abs_tol=1e-9)
|
||||
for value in admitted_sizes
|
||||
)
|
||||
):
|
||||
raise M49PhysicalSafetyPlaybackError("physical-safety playback policy changed")
|
||||
return {
|
||||
"chunk_frame_count": 32,
|
||||
"startup_prebuffer_chunk_count": 2,
|
||||
"resident_chunk_count_max": 3,
|
||||
"forward_prefetch_chunk_count": 1,
|
||||
}
|
||||
|
||||
|
||||
def _chunk_payload(
|
||||
path: Path,
|
||||
*,
|
||||
start: int,
|
||||
states: np.ndarray,
|
||||
z_bounds: np.ndarray,
|
||||
) -> None:
|
||||
frame_count, cell_count = states.shape
|
||||
state_values = np.ascontiguousarray(states, dtype=np.uint8)
|
||||
z_values = np.ascontiguousarray(z_bounds, dtype="<f4")
|
||||
state_bytes = state_values.nbytes
|
||||
z_bytes = z_values.nbytes
|
||||
padding = (4 - ((CHUNK_HEADER.size + state_bytes) % 4)) % 4
|
||||
with path.open("wb") as stream:
|
||||
stream.write(
|
||||
CHUNK_HEADER.pack(
|
||||
CHUNK_MAGIC,
|
||||
start,
|
||||
frame_count,
|
||||
cell_count,
|
||||
state_bytes,
|
||||
z_bytes,
|
||||
)
|
||||
)
|
||||
stream.write(memoryview(state_values).cast("B"))
|
||||
if padding:
|
||||
stream.write(b"\0" * padding)
|
||||
stream.write(memoryview(z_values).cast("B"))
|
||||
|
||||
|
||||
def seal_m49_physical_safety_playback(
|
||||
*,
|
||||
source_root: Path,
|
||||
destination_root: Path,
|
||||
profile_path: Path,
|
||||
created_at_utc: str | None = None,
|
||||
) -> M49PhysicalSafetyPlayback:
|
||||
"""Sequentially seal local tracks without starting or contacting Worker 006."""
|
||||
|
||||
raw_source = source_root.expanduser().absolute()
|
||||
if raw_source.is_symlink() or not raw_source.is_dir():
|
||||
raise M49PhysicalSafetyPlaybackError("sealed source root is unavailable")
|
||||
source = raw_source.resolve(strict=True)
|
||||
destination = destination_root.expanduser().absolute()
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
if destination.is_symlink():
|
||||
raise M49PhysicalSafetyPlaybackError("destination must not be a symlink")
|
||||
raw_profile = profile_path.expanduser().absolute()
|
||||
if raw_profile.is_symlink() or not raw_profile.is_file():
|
||||
raise M49PhysicalSafetyPlaybackError("physical-safety profile is unavailable")
|
||||
profile_file = raw_profile.resolve(strict=True)
|
||||
|
||||
source_manifest = _json(source / "manifest.json", "source manifest")
|
||||
source_report = _json(source / "report.json", "source report")
|
||||
source_result_id = source_manifest.get("result_id")
|
||||
source_identity = source_manifest.get("identity")
|
||||
source_identity_sha256 = source_manifest.get("identity_sha256")
|
||||
if (
|
||||
not isinstance(source_result_id, str)
|
||||
or source_report.get("result_id") != source_result_id
|
||||
or not isinstance(source_identity, dict)
|
||||
or not isinstance(source_identity_sha256, str)
|
||||
or source_identity_sha256 != _canonical_sha256(source_identity)
|
||||
or not source_result_id.endswith(source_identity_sha256)
|
||||
):
|
||||
raise M49PhysicalSafetyPlaybackError("source result identity changed")
|
||||
source_artifacts = _source_artifacts(source, source_manifest)
|
||||
state_codes = _state_codes(source_report)
|
||||
cell_size_m, radius_m, coordinate_frame = _source_geometry(source_report)
|
||||
profile = _json(profile_file, "physical-safety profile")
|
||||
policy = _profile_policy(profile, cell_size_m)
|
||||
profile_sha256 = _sha256(profile_file)
|
||||
source_manifest_sha256 = _sha256(source / "manifest.json")
|
||||
|
||||
try:
|
||||
centers = np.load(
|
||||
source / "costmap-cell-centers-xy-m.npy", mmap_mode="r", allow_pickle=False
|
||||
)
|
||||
states = np.load(source / "costmap-states.npy", mmap_mode="r", allow_pickle=False)
|
||||
z_bounds = np.load(
|
||||
source / "costmap-z-bounds-m.npy", mmap_mode="r", allow_pickle=False
|
||||
)
|
||||
except (OSError, ValueError) as error:
|
||||
raise M49PhysicalSafetyPlaybackError("source numeric tracks are invalid") from error
|
||||
frames = _frame_rows(source / "frames.ndjson")
|
||||
if (
|
||||
centers.dtype != np.dtype("<f4")
|
||||
or states.dtype != np.dtype("uint8")
|
||||
or z_bounds.dtype != np.dtype("<f4")
|
||||
or centers.ndim != 2
|
||||
or centers.shape[1] != 2
|
||||
or states.ndim != 2
|
||||
or z_bounds.ndim != 3
|
||||
or z_bounds.shape[2] != 2
|
||||
or states.shape[0] != len(frames)
|
||||
or states.shape[1] != centers.shape[0]
|
||||
or z_bounds.shape != (states.shape[0], states.shape[1], 2)
|
||||
or not np.isfinite(centers).all()
|
||||
):
|
||||
raise M49PhysicalSafetyPlaybackError("source numeric shape or dtype changed")
|
||||
frame_count, cell_count = states.shape
|
||||
admitted_codes = np.asarray(sorted(set(state_codes.values())), dtype=np.uint8)
|
||||
|
||||
identity = {
|
||||
"schema_version": SCHEMA,
|
||||
"source_result_id": source_result_id,
|
||||
"source_manifest_sha256": source_manifest_sha256,
|
||||
"source_track_sha256": {
|
||||
name: source_artifacts[name]["sha256"] for name in SOURCE_TRACKS
|
||||
},
|
||||
"profile_id": profile.get("profile_id"),
|
||||
"profile_sha256": profile_sha256,
|
||||
"coordinate_frame": coordinate_frame,
|
||||
"frame_count": frame_count,
|
||||
"cell_count": cell_count,
|
||||
"cell_size_m": cell_size_m,
|
||||
"radius_m": radius_m,
|
||||
"state_codes": state_codes,
|
||||
"playback_policy": policy,
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_accepted": False,
|
||||
},
|
||||
}
|
||||
identity_sha256 = _canonical_sha256(identity)
|
||||
result_id = f"{PREFIX}{identity_sha256}"
|
||||
target = destination / result_id
|
||||
if target.exists():
|
||||
existing = read_m49_physical_safety_playback(target)
|
||||
existing_playback = existing.manifest["playback"]
|
||||
assert isinstance(existing_playback, dict)
|
||||
existing_chunks = existing_playback["chunks"]
|
||||
assert isinstance(existing_chunks, list)
|
||||
for value in (
|
||||
existing_playback["centers"],
|
||||
existing_playback["frames"],
|
||||
*existing_chunks,
|
||||
):
|
||||
assert isinstance(value, dict)
|
||||
verify_m49_physical_safety_artifact(existing, value)
|
||||
return existing
|
||||
|
||||
created = created_at_utc or datetime.now(tz=UTC).isoformat().replace("+00:00", "Z")
|
||||
chunk_frame_count = int(policy["chunk_frame_count"])
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="mission-core-m49-physical-safety-", dir=destination
|
||||
) as raw_staging:
|
||||
staging = Path(raw_staging) / result_id
|
||||
staging.mkdir()
|
||||
centers_path = staging / "centers.f32"
|
||||
np.ascontiguousarray(centers, dtype="<f4").tofile(centers_path)
|
||||
frames_path = staging / "frames.ndjson"
|
||||
shutil.copyfile(source / "frames.ndjson", frames_path)
|
||||
centers_artifact = _artifact(
|
||||
centers_path,
|
||||
media_type="application/octet-stream",
|
||||
role="costmap-cell-centers-f32le",
|
||||
)
|
||||
frames_artifact = _artifact(
|
||||
frames_path,
|
||||
media_type="application/x-ndjson",
|
||||
role="frame-catalog",
|
||||
)
|
||||
chunks: list[dict[str, object]] = []
|
||||
for index, start in enumerate(range(0, frame_count, chunk_frame_count)):
|
||||
count = min(chunk_frame_count, frame_count - start)
|
||||
state_slice = states[start : start + count]
|
||||
if not np.isin(state_slice, admitted_codes).all():
|
||||
raise M49PhysicalSafetyPlaybackError(
|
||||
f"source state code changed in chunk {index}"
|
||||
)
|
||||
for local_index, frame in enumerate(frames[start : start + count]):
|
||||
if frame["sample_available"] is False and (
|
||||
np.any(state_slice[local_index] != 0)
|
||||
or np.isfinite(z_bounds[start + local_index]).any()
|
||||
):
|
||||
raise M49PhysicalSafetyPlaybackError(
|
||||
"missing source sample did not remain fail-closed"
|
||||
)
|
||||
chunk_path = staging / f"chunk-{index:06d}.bin"
|
||||
_chunk_payload(
|
||||
chunk_path,
|
||||
start=start,
|
||||
states=state_slice,
|
||||
z_bounds=z_bounds[start : start + count],
|
||||
)
|
||||
descriptor = _artifact(
|
||||
chunk_path,
|
||||
media_type="application/octet-stream",
|
||||
role="states-u8-and-z-bounds-f32le",
|
||||
)
|
||||
descriptor.update(
|
||||
{
|
||||
"index": index,
|
||||
"start": start,
|
||||
"count": count,
|
||||
"header_bytes": CHUNK_HEADER.size,
|
||||
"format": "mcpsch01-states-u8-aligned-z-bounds-f32le",
|
||||
}
|
||||
)
|
||||
chunks.append(descriptor)
|
||||
|
||||
manifest = {
|
||||
"schema_version": SCHEMA,
|
||||
"result_id": result_id,
|
||||
"created_at_utc": created,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"execution": {
|
||||
"execution_class": "local-sequential-offline",
|
||||
"worker_role": "realtime-only",
|
||||
"worker_runtime_dependency": False,
|
||||
"worker_requests_required": 0,
|
||||
},
|
||||
"playback": {
|
||||
"coordinate_frame": coordinate_frame,
|
||||
"source_pace_hz": profile["operator_visual"]["source_pace_hz"],
|
||||
"frame_count": frame_count,
|
||||
"cell_count": cell_count,
|
||||
"cell_size_m": cell_size_m,
|
||||
"radius_m": radius_m,
|
||||
"state_codes": state_codes,
|
||||
**policy,
|
||||
"centers": {
|
||||
**centers_artifact,
|
||||
"dtype": "<f4",
|
||||
"shape": [cell_count, 2],
|
||||
},
|
||||
"frames": {
|
||||
**frames_artifact,
|
||||
"dtype": "ndjson",
|
||||
"shape": [frame_count],
|
||||
},
|
||||
"chunks": chunks,
|
||||
},
|
||||
"authority": identity["authority"],
|
||||
}
|
||||
(staging / "manifest.json").write_text(
|
||||
json.dumps(manifest, indent=2, sort_keys=True, allow_nan=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
staging.replace(target)
|
||||
return read_m49_physical_safety_playback(target)
|
||||
|
||||
|
||||
def read_m49_physical_safety_playback(root: Path) -> M49PhysicalSafetyPlayback:
|
||||
raw_candidate = root.expanduser().absolute()
|
||||
if raw_candidate.is_symlink() or not raw_candidate.is_dir():
|
||||
raise M49PhysicalSafetyPlaybackError("physical-safety playback root is invalid")
|
||||
candidate = raw_candidate.resolve(strict=True)
|
||||
manifest = _json(candidate / "manifest.json", "physical-safety playback manifest")
|
||||
identity = manifest.get("identity")
|
||||
execution = manifest.get("execution")
|
||||
if (
|
||||
manifest.get("schema_version") != SCHEMA
|
||||
or not isinstance(identity, dict)
|
||||
or not isinstance(execution, dict)
|
||||
or manifest.get("identity_sha256") != _canonical_sha256(identity)
|
||||
or manifest.get("result_id") != candidate.name
|
||||
or candidate.name != f"{PREFIX}{manifest.get('identity_sha256')}"
|
||||
or execution.get("worker_requests_required") != 0
|
||||
or execution.get("worker_runtime_dependency") is not False
|
||||
):
|
||||
raise M49PhysicalSafetyPlaybackError("physical-safety playback identity changed")
|
||||
playback = manifest.get("playback")
|
||||
if not isinstance(playback, dict) or not isinstance(playback.get("chunks"), list):
|
||||
raise M49PhysicalSafetyPlaybackError("physical-safety playback catalog changed")
|
||||
descriptors = [playback.get("centers"), playback.get("frames"), *playback["chunks"]]
|
||||
seen: set[str] = set()
|
||||
for descriptor in descriptors:
|
||||
if not isinstance(descriptor, dict) or not isinstance(descriptor.get("path"), str):
|
||||
raise M49PhysicalSafetyPlaybackError("physical-safety artifact entry changed")
|
||||
name = descriptor["path"]
|
||||
path = candidate / name
|
||||
if (
|
||||
name in seen
|
||||
or path.parent != candidate
|
||||
or path.is_symlink()
|
||||
or not path.is_file()
|
||||
or descriptor.get("byte_length") != path.stat().st_size
|
||||
or not isinstance(descriptor.get("sha256"), str)
|
||||
or len(descriptor["sha256"]) != 64
|
||||
or any(character not in "0123456789abcdef" for character in descriptor["sha256"])
|
||||
):
|
||||
raise M49PhysicalSafetyPlaybackError("physical-safety artifact changed")
|
||||
seen.add(name)
|
||||
return M49PhysicalSafetyPlayback(candidate.name, candidate, manifest)
|
||||
|
||||
|
||||
def verify_m49_physical_safety_artifact(
|
||||
result: M49PhysicalSafetyPlayback,
|
||||
descriptor: dict[str, Any],
|
||||
) -> Path:
|
||||
name = descriptor.get("path")
|
||||
if not isinstance(name, str):
|
||||
raise M49PhysicalSafetyPlaybackError("physical-safety artifact path changed")
|
||||
path = result.root / name
|
||||
if (
|
||||
path.parent != result.root
|
||||
or path.is_symlink()
|
||||
or not path.is_file()
|
||||
or descriptor.get("byte_length") != path.stat().st_size
|
||||
or descriptor.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise M49PhysicalSafetyPlaybackError("physical-safety artifact digest changed")
|
||||
return path
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CHUNK_HEADER",
|
||||
"CHUNK_MAGIC",
|
||||
"M49PhysicalSafetyPlayback",
|
||||
"M49PhysicalSafetyPlaybackError",
|
||||
"PREFIX",
|
||||
"SCHEMA",
|
||||
"read_m49_physical_safety_playback",
|
||||
"seal_m49_physical_safety_playback",
|
||||
"verify_m49_physical_safety_artifact",
|
||||
]
|
||||
@@ -452,6 +452,32 @@ def build_threat_replay(
|
||||
|
||||
|
||||
def read_threat_replay_result(root: Path) -> ThreatReplayResult:
|
||||
"""Read and deeply verify a sealed threat replay result.
|
||||
|
||||
This is the acceptance/build seam. It deliberately walks every ledger row
|
||||
before returning and therefore must not sit on the synchronous LAB open
|
||||
path for a hundreds-of-megabytes recorded result.
|
||||
"""
|
||||
|
||||
return _read_threat_replay_result(root, validate_ledgers=True)
|
||||
|
||||
|
||||
def read_threat_replay_result_metadata(root: Path) -> ThreatReplayResult:
|
||||
"""Read verified result metadata without eagerly parsing the full ledger.
|
||||
|
||||
Artifact bytes are still digest checked against the sealed manifest. The
|
||||
bounded timeline reader validates every requested row before projection;
|
||||
only the redundant all-4489-row JSON accounting pass is deferred.
|
||||
"""
|
||||
|
||||
return _read_threat_replay_result(root, validate_ledgers=False)
|
||||
|
||||
|
||||
def _read_threat_replay_result(
|
||||
root: Path,
|
||||
*,
|
||||
validate_ledgers: bool,
|
||||
) -> ThreatReplayResult:
|
||||
resolved = root.resolve(strict=True)
|
||||
if resolved.is_symlink() or not resolved.name.startswith(THREAT_REPLAY_RESULT_PREFIX):
|
||||
raise ThreatReplayError("threat replay result root is invalid")
|
||||
@@ -498,7 +524,8 @@ def read_threat_replay_result(root: Path) -> ThreatReplayResult:
|
||||
for role, (name, identity_key) in expected.items():
|
||||
path = _validated_artifact(resolved, by_role[role], name)
|
||||
paths[role] = path
|
||||
if identity_key is not None and _file_sha256(path) != identity.get(identity_key):
|
||||
artifact = _object(by_role[role], "threat artifact")
|
||||
if identity_key is not None and artifact.get("sha256") != identity.get(identity_key):
|
||||
raise ThreatReplayError("threat artifact identity changed")
|
||||
report = _read_json(paths["threat-replay-report"])
|
||||
metrics = _object(identity.get("metrics"), "threat metrics")
|
||||
@@ -522,12 +549,13 @@ def read_threat_replay_result(root: Path) -> ThreatReplayResult:
|
||||
or requirements != expected_requirements
|
||||
):
|
||||
raise ThreatReplayError("threat replay report or acceptance changed")
|
||||
_validate_ledgers(
|
||||
paths["threat-replay-frames"],
|
||||
paths["threat-visual-frames"],
|
||||
metrics,
|
||||
is_v2=is_v2,
|
||||
)
|
||||
if validate_ledgers:
|
||||
_validate_ledgers(
|
||||
paths["threat-replay-frames"],
|
||||
paths["threat-visual-frames"],
|
||||
metrics,
|
||||
is_v2=is_v2,
|
||||
)
|
||||
return ThreatReplayResult(
|
||||
result_id=resolved.name,
|
||||
result_root=resolved,
|
||||
|
||||
@@ -13,6 +13,8 @@ from pathlib import Path
|
||||
from threading import RLock
|
||||
from typing import Final
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .geometry import RecordedGeometryStore
|
||||
from .recorded_source import RECORDED_REPRESENTATION_ID
|
||||
from .spatial_evidence import (
|
||||
@@ -78,24 +80,56 @@ class RecordedThreatTimeline:
|
||||
}
|
||||
if any(identity.get(key) != value for key, value in expected_identity.items()):
|
||||
raise RecordedThreatTimelineError("recorded timeline escaped the threat profile")
|
||||
self.store = RecordedGeometryStore.from_repository(self.repository_root)
|
||||
if (
|
||||
self.store.profile.source_pack_id != self.profile.source_pack_id
|
||||
or self.store.profile.source_pack_sha256 != self.profile.source_pack_sha256
|
||||
or self.store.profile.frame_count != _EXPECTED_FRAME_COUNT
|
||||
):
|
||||
raise RecordedThreatTimelineError("recorded timeline geometry identity changed")
|
||||
if self.store.maximum_current_point_count > RECORDED_SPATIAL_POINT_LIMIT:
|
||||
raise RecordedThreatTimelineError(
|
||||
"recorded timeline exact point delivery exceeds its declared bound"
|
||||
)
|
||||
self.body_frames = RecordedReplayBodyFrameResolver(
|
||||
self.store,
|
||||
profile=self.profile.body_frame,
|
||||
self._maximum_source_points_per_frame = _read_source_point_bound(
|
||||
self.repository_root,
|
||||
source_pack_id=self.profile.source_pack_id,
|
||||
source_pack_sha256=self.profile.source_pack_sha256,
|
||||
)
|
||||
self._store: RecordedGeometryStore | None = None
|
||||
self._body_frames: RecordedReplayBodyFrameResolver | None = None
|
||||
self.index = _index_frame_ledger(self.frames_path)
|
||||
self._lock = RLock()
|
||||
|
||||
@property
|
||||
def store(self) -> RecordedGeometryStore:
|
||||
"""Load and deeply verify the large geometry archives on first use.
|
||||
|
||||
Timeline metadata, camera playback and the independent TGS artifact can
|
||||
become visible without waiting for the source cloud archive. Any
|
||||
endpoint that actually delivers source points still crosses the full
|
||||
digest and shape validation in ``RecordedGeometryStore``.
|
||||
"""
|
||||
|
||||
with self._lock:
|
||||
if self._store is None:
|
||||
store = RecordedGeometryStore.from_repository(self.repository_root)
|
||||
if (
|
||||
store.profile.source_pack_id != self.profile.source_pack_id
|
||||
or store.profile.source_pack_sha256 != self.profile.source_pack_sha256
|
||||
or store.profile.frame_count != _EXPECTED_FRAME_COUNT
|
||||
or store.maximum_current_point_count
|
||||
!= self._maximum_source_points_per_frame
|
||||
):
|
||||
raise RecordedThreatTimelineError(
|
||||
"recorded timeline geometry identity changed"
|
||||
)
|
||||
if store.maximum_current_point_count > RECORDED_SPATIAL_POINT_LIMIT:
|
||||
raise RecordedThreatTimelineError(
|
||||
"recorded timeline exact point delivery exceeds its declared bound"
|
||||
)
|
||||
self._store = store
|
||||
return self._store
|
||||
|
||||
@property
|
||||
def body_frames(self) -> RecordedReplayBodyFrameResolver:
|
||||
with self._lock:
|
||||
if self._body_frames is None:
|
||||
self._body_frames = RecordedReplayBodyFrameResolver(
|
||||
self.store,
|
||||
profile=self.profile.body_frame,
|
||||
)
|
||||
return self._body_frames
|
||||
|
||||
def metadata(self) -> dict[str, object]:
|
||||
times = self.index.source_times_ns
|
||||
intervals = [(current - previous) / 1_000_000_000 for previous, current in pairwise(times)]
|
||||
@@ -120,7 +154,7 @@ class RecordedThreatTimeline:
|
||||
"max_chunk_frames": RECORDED_SPATIAL_MAX_CHUNK_FRAMES,
|
||||
"point_sample_limit": RECORDED_SPATIAL_POINT_LIMIT,
|
||||
"point_delivery": "exact-current-increment",
|
||||
"maximum_source_points_per_frame": self.store.maximum_current_point_count,
|
||||
"maximum_source_points_per_frame": self._maximum_source_points_per_frame,
|
||||
"local_surface_visualization": {
|
||||
"derivation": "bounded-registered-increment-accumulation",
|
||||
"window_seconds": RECORDED_LOCAL_SURFACE_WINDOW_SECONDS,
|
||||
@@ -258,6 +292,82 @@ class RecordedThreatTimeline:
|
||||
}
|
||||
|
||||
|
||||
def _read_source_point_bound(
|
||||
repository_root: Path,
|
||||
*,
|
||||
source_pack_id: str,
|
||||
source_pack_sha256: str,
|
||||
) -> int:
|
||||
"""Read the small offsets member without inflating the 70 MiB source pack.
|
||||
|
||||
This is metadata only. The exact archive digest, every array shape and the
|
||||
local-surface binding are still verified lazily by ``RecordedGeometryStore``
|
||||
before any source point is delivered.
|
||||
"""
|
||||
|
||||
base = (
|
||||
repository_root / ".runtime/compute-experiments/e10/lidar-packs"
|
||||
).resolve(strict=True)
|
||||
pack_root = (base / source_pack_id).resolve(strict=True)
|
||||
manifest_path = (pack_root / "manifest.json").resolve(strict=True)
|
||||
pack_path = (pack_root / "lidar-pack.npz").resolve(strict=True)
|
||||
if (
|
||||
pack_root.parent != base
|
||||
or pack_root.is_symlink()
|
||||
or manifest_path.parent != pack_root
|
||||
or manifest_path.is_symlink()
|
||||
or pack_path.parent != pack_root
|
||||
or pack_path.is_symlink()
|
||||
or not pack_path.is_file()
|
||||
):
|
||||
raise RecordedThreatTimelineError("recorded timeline source pack path changed")
|
||||
try:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
if not isinstance(manifest, dict):
|
||||
raise RecordedThreatTimelineError(
|
||||
"recorded timeline source pack manifest is invalid"
|
||||
)
|
||||
identity = manifest.get("identity")
|
||||
artifact = manifest.get("artifact")
|
||||
if not isinstance(identity, dict) or not isinstance(artifact, dict):
|
||||
raise RecordedThreatTimelineError(
|
||||
"recorded timeline source pack manifest is invalid"
|
||||
)
|
||||
point_count = identity.get("point_count")
|
||||
if (
|
||||
manifest.get("pack_id") != source_pack_id
|
||||
or identity.get("frame_count") != _EXPECTED_FRAME_COUNT
|
||||
or not isinstance(point_count, int)
|
||||
or isinstance(point_count, bool)
|
||||
or point_count < 0
|
||||
or artifact.get("path") != "lidar-pack.npz"
|
||||
or artifact.get("sha256") != source_pack_sha256
|
||||
or artifact.get("byte_length") != pack_path.stat().st_size
|
||||
):
|
||||
raise RecordedThreatTimelineError(
|
||||
"recorded timeline source pack identity changed"
|
||||
)
|
||||
with np.load(pack_path, allow_pickle=False) as archive:
|
||||
offsets = np.asarray(archive["cloud_offsets"], dtype=np.int64)
|
||||
except (KeyError, OSError, ValueError, json.JSONDecodeError) as error:
|
||||
raise RecordedThreatTimelineError(
|
||||
"recorded timeline source point offsets are unavailable"
|
||||
) from error
|
||||
if (
|
||||
offsets.shape != (_EXPECTED_FRAME_COUNT + 1,)
|
||||
or int(offsets[0]) != 0
|
||||
or int(offsets[-1]) != point_count
|
||||
or np.any(np.diff(offsets) < 0)
|
||||
):
|
||||
raise RecordedThreatTimelineError("recorded timeline source point offsets changed")
|
||||
maximum = int(np.diff(offsets).max(initial=0))
|
||||
if maximum > RECORDED_SPATIAL_POINT_LIMIT:
|
||||
raise RecordedThreatTimelineError(
|
||||
"recorded timeline exact point delivery exceeds its declared bound"
|
||||
)
|
||||
return maximum
|
||||
|
||||
|
||||
def _index_frame_ledger(path: Path) -> RecordedThreatTimelineIndex:
|
||||
offsets: list[int] = []
|
||||
source_times: list[int] = []
|
||||
|
||||
@@ -133,6 +133,9 @@ from k1link.web.m48s_fixed_class_detector_lab_api import (
|
||||
build_m48s_fixed_class_detector_lab_router,
|
||||
)
|
||||
from k1link.web.m48t_risk_quality_lab_api import build_m48t_risk_quality_lab_router
|
||||
from k1link.web.m49_physical_safety_playback_api import (
|
||||
build_m49_physical_safety_playback_router,
|
||||
)
|
||||
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
|
||||
from k1link.web.m49_tgs_full_shadow_api import build_m49_tgs_full_shadow_router
|
||||
from k1link.web.map_api import (
|
||||
@@ -1016,6 +1019,17 @@ app.include_router(
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_m49_physical_safety_playback_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "m49"
|
||||
/ "physical-safety-playback-results"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_m48s_fixed_class_detector_lab_router(
|
||||
root_provider=lambda: (
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
"""Read-only local API for autonomous M49 physical-safety playback artifacts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from k1link.laboratory.m49_physical_safety_playback import (
|
||||
PREFIX,
|
||||
M49PhysicalSafetyPlayback,
|
||||
M49PhysicalSafetyPlaybackError,
|
||||
read_m49_physical_safety_playback,
|
||||
verify_m49_physical_safety_artifact,
|
||||
)
|
||||
|
||||
RootProvider = Callable[[], Path | None]
|
||||
RESULT_ID: Final = re.compile(rf"^{re.escape(PREFIX)}[a-f0-9]{{64}}$")
|
||||
SOURCE_RESULT_ID: Final = re.compile(r"^m49-tgs-full-shadow-[a-f0-9]{64}$")
|
||||
ENDPOINT_ROOT: Final = "/api/v1/laboratory/m49/physical-safety-playback"
|
||||
|
||||
|
||||
def build_m49_physical_safety_playback_router(
|
||||
*, root_provider: RootProvider = lambda: None
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix=ENDPOINT_ROOT, tags=["laboratory"])
|
||||
|
||||
def sealed(result_id: str) -> M49PhysicalSafetyPlayback:
|
||||
root = _configured_root(root_provider)
|
||||
if root is None or RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="M49 physical-safety playback not found")
|
||||
candidate = root / result_id
|
||||
if candidate.is_symlink() or not candidate.is_dir():
|
||||
raise HTTPException(status_code=404, detail="M49 physical-safety playback not found")
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
if resolved.parent != root:
|
||||
raise ValueError("result escaped configured root")
|
||||
return _read_cached(str(resolved), _signature(resolved))
|
||||
except (M49PhysicalSafetyPlaybackError, OSError, ValueError):
|
||||
raise HTTPException(
|
||||
status_code=404, detail="M49 physical-safety playback not found"
|
||||
) from None
|
||||
|
||||
@router.get("/results")
|
||||
def list_results(
|
||||
limit: int = Query(default=10, ge=1, le=25),
|
||||
source_result_id: str | None = Query(default=None),
|
||||
) -> dict[str, object]:
|
||||
if source_result_id is not None and SOURCE_RESULT_ID.fullmatch(source_result_id) is None:
|
||||
raise HTTPException(status_code=422, detail="M49 source result identity is invalid")
|
||||
root = _configured_root(root_provider)
|
||||
if root is None:
|
||||
return _catalog([], configured=False, invalid_total=0)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid = 0
|
||||
for candidate in sorted(root.iterdir()):
|
||||
if candidate.is_symlink() or not candidate.is_dir() or not RESULT_ID.fullmatch(
|
||||
candidate.name
|
||||
):
|
||||
continue
|
||||
try:
|
||||
summary = _summary(
|
||||
_read_cached(str(candidate.resolve()), _signature(candidate))
|
||||
)
|
||||
if source_result_id is None or summary["source_result_id"] == source_result_id:
|
||||
items.append(summary)
|
||||
except (M49PhysicalSafetyPlaybackError, OSError, ValueError):
|
||||
invalid += 1
|
||||
items.sort(
|
||||
key=lambda value: (str(value["created_at_utc"]), str(value["result_id"])),
|
||||
reverse=True,
|
||||
)
|
||||
return _catalog(items[:limit], configured=True, invalid_total=invalid)
|
||||
|
||||
@router.get("/{result_id}/manifest")
|
||||
def get_manifest(result_id: str) -> dict[str, object]:
|
||||
return _project_manifest(sealed(result_id))
|
||||
|
||||
@router.get("/{result_id}/tracks/{track_id}")
|
||||
def get_track(result_id: str, track_id: str) -> FileResponse:
|
||||
result = sealed(result_id)
|
||||
playback = _playback(result)
|
||||
if track_id == "centers":
|
||||
descriptor = _descriptor(playback.get("centers"), "centers")
|
||||
elif track_id == "frames":
|
||||
descriptor = _descriptor(playback.get("frames"), "frames")
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="M49 physical-safety track not found")
|
||||
return _file_response(result, descriptor)
|
||||
|
||||
@router.get("/{result_id}/chunks/{chunk_index}")
|
||||
def get_chunk(result_id: str, chunk_index: int) -> FileResponse:
|
||||
result = sealed(result_id)
|
||||
chunks = _playback(result).get("chunks")
|
||||
if not isinstance(chunks, list) or chunk_index < 0 or chunk_index >= len(chunks):
|
||||
raise HTTPException(status_code=404, detail="M49 physical-safety chunk not found")
|
||||
descriptor = _descriptor(chunks[chunk_index], f"chunk {chunk_index}")
|
||||
if descriptor.get("index") != chunk_index:
|
||||
raise HTTPException(status_code=503, detail="M49 physical-safety chunk catalog changed")
|
||||
return _file_response(result, descriptor)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _read_cached(root: str, signature: tuple[int, ...]) -> M49PhysicalSafetyPlayback:
|
||||
del signature
|
||||
return read_m49_physical_safety_playback(Path(root))
|
||||
|
||||
|
||||
def _file_response(
|
||||
result: M49PhysicalSafetyPlayback,
|
||||
descriptor: dict[str, Any],
|
||||
) -> FileResponse:
|
||||
try:
|
||||
path = verify_m49_physical_safety_artifact(result, descriptor)
|
||||
except (KeyError, OSError, TypeError, ValueError, M49PhysicalSafetyPlaybackError):
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="M49 physical-safety playback artifact failed verification",
|
||||
) from None
|
||||
return FileResponse(
|
||||
path,
|
||||
media_type=str(descriptor.get("media_type") or "application/octet-stream"),
|
||||
headers={
|
||||
"Cache-Control": "private, max-age=31536000, immutable",
|
||||
"Content-Encoding": "identity",
|
||||
"ETag": f'"{descriptor["sha256"]}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Mission-Core-Worker-Dependency": "none",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _playback(result: M49PhysicalSafetyPlayback) -> dict[str, Any]:
|
||||
playback = result.manifest.get("playback")
|
||||
if not isinstance(playback, dict):
|
||||
raise HTTPException(status_code=503, detail="M49 physical-safety playback changed")
|
||||
return playback
|
||||
|
||||
|
||||
def _descriptor(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise HTTPException(status_code=503, detail=f"M49 physical-safety {label} changed")
|
||||
return value
|
||||
|
||||
|
||||
def _project_manifest(result: M49PhysicalSafetyPlayback) -> dict[str, object]:
|
||||
payload = copy.deepcopy(result.manifest)
|
||||
playback = payload["playback"]
|
||||
assert isinstance(playback, dict)
|
||||
centers = playback["centers"]
|
||||
frames = playback["frames"]
|
||||
chunks = playback["chunks"]
|
||||
assert isinstance(centers, dict) and isinstance(frames, dict) and isinstance(chunks, list)
|
||||
centers["url"] = f"{ENDPOINT_ROOT}/{result.result_id}/tracks/centers"
|
||||
frames["url"] = f"{ENDPOINT_ROOT}/{result.result_id}/tracks/frames"
|
||||
for index, value in enumerate(chunks):
|
||||
assert isinstance(value, dict)
|
||||
value["url"] = f"{ENDPOINT_ROOT}/{result.result_id}/chunks/{index}"
|
||||
payload["access"] = "read-only-sealed-local"
|
||||
return payload
|
||||
|
||||
|
||||
def _summary(result: M49PhysicalSafetyPlayback) -> dict[str, object]:
|
||||
playback = _playback(result)
|
||||
return {
|
||||
"schema_version": "missioncore.m49-physical-safety-playback-summary/v1",
|
||||
"result_id": result.result_id,
|
||||
"created_at_utc": result.manifest.get("created_at_utc"),
|
||||
"source_result_id": result.manifest["identity"]["source_result_id"],
|
||||
"frame_count": playback.get("frame_count"),
|
||||
"cell_count": playback.get("cell_count"),
|
||||
"cell_size_m": playback.get("cell_size_m"),
|
||||
"radius_m": playback.get("radius_m"),
|
||||
"chunk_count": len(playback.get("chunks", [])),
|
||||
"worker_runtime_dependency": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
def _catalog(
|
||||
items: list[dict[str, object]], *, configured: bool, invalid_total: int
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": "missioncore.m49-physical-safety-playback-catalog/v1",
|
||||
"configured": configured,
|
||||
"items": items,
|
||||
"candidate_total": len(items) + invalid_total,
|
||||
"invalid_total": invalid_total,
|
||||
"worker_runtime_dependency": False,
|
||||
"access": "read-only-sealed-local",
|
||||
}
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
return None
|
||||
raw = value.expanduser().absolute()
|
||||
if raw.is_symlink() or not raw.is_dir():
|
||||
return None
|
||||
return raw.resolve(strict=True)
|
||||
|
||||
|
||||
def _signature(root: Path) -> tuple[int, ...]:
|
||||
path = root / "manifest.json"
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError("physical-safety manifest unavailable")
|
||||
stat = path.stat()
|
||||
return (stat.st_size, stat.st_mtime_ns)
|
||||
|
||||
|
||||
__all__ = ["build_m49_physical_safety_playback_router"]
|
||||
@@ -22,7 +22,7 @@ from k1link.perception.threat_replay import (
|
||||
THREAT_REPLAY_VISUAL_SCHEMA_V2,
|
||||
ThreatReplayError,
|
||||
ThreatReplayResult,
|
||||
read_threat_replay_result,
|
||||
read_threat_replay_result_metadata,
|
||||
)
|
||||
from k1link.perception.threat_timeline import (
|
||||
RECORDED_SPATIAL_MAX_CHUNK_FRAMES,
|
||||
@@ -34,6 +34,7 @@ from k1link.sessions import RecordedCameraFrame, SessionIntegrityError
|
||||
M4_THREAT_CATALOG_SCHEMA: Final = "missioncore.m4-threat-replay-catalog/v1"
|
||||
M4_THREAT_VIEW_SCHEMA: Final = "missioncore.m4-threat-replay-view/v1"
|
||||
M4_THREAT_VISUAL_CATALOG_SCHEMA: Final = "missioncore.m4-threat-visual-catalog/v1"
|
||||
M4_THREAT_PLAYBACK_CHUNK_FRAMES: Final = 24
|
||||
_RESULT_ID = re.compile(rf"^{THREAT_REPLAY_RESULT_PREFIX}[a-f0-9]{{64}}$")
|
||||
RootProvider = Callable[[], Path | None]
|
||||
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
|
||||
@@ -212,13 +213,19 @@ def build_m4_threat_replay_router(
|
||||
projected = timeline(result_id)
|
||||
points = projected.store.playback_points_map()
|
||||
offsets = projected.store.playback_point_offsets()
|
||||
content_sha256 = hashlib.sha256(memoryview(points).cast("B")).hexdigest()
|
||||
points_view = memoryview(points).cast("B")
|
||||
content_sha256 = hashlib.sha256(points_view).hexdigest()
|
||||
chunks = _playback_chunk_catalog(result_id, points_view, offsets)
|
||||
return {
|
||||
"schema_version": "missioncore.recorded-spatial-playback/v1",
|
||||
"result_id": result_id,
|
||||
"frame_count": len(offsets) - 1,
|
||||
"point_count": int(points.shape[0]),
|
||||
"point_offsets": list(offsets),
|
||||
"chunk_frame_count": M4_THREAT_PLAYBACK_CHUNK_FRAMES,
|
||||
"resident_chunk_count_max": 4,
|
||||
"forward_prefetch_chunk_count": 1,
|
||||
"chunks": chunks,
|
||||
"track": {
|
||||
"id": "points-map-f32",
|
||||
"url": (
|
||||
@@ -238,6 +245,42 @@ def build_m4_threat_replay_router(
|
||||
"access": "read-only-sealed-binary-playback",
|
||||
}
|
||||
|
||||
@router.get(
|
||||
"/results/{result_id}/timeline/playback/chunks/{chunk_index}",
|
||||
response_class=Response,
|
||||
)
|
||||
def get_timeline_playback_chunk(result_id: str, chunk_index: int) -> Response:
|
||||
projected = timeline(result_id)
|
||||
points = projected.store.playback_points_map()
|
||||
offsets = projected.store.playback_point_offsets()
|
||||
points_view = memoryview(points).cast("B")
|
||||
descriptor = _playback_chunk_descriptor(
|
||||
result_id,
|
||||
points_view,
|
||||
offsets,
|
||||
chunk_index,
|
||||
)
|
||||
if descriptor is None:
|
||||
raise HTTPException(status_code=404, detail="M4.6 playback chunk не найден")
|
||||
point_start = descriptor["point_start"]
|
||||
byte_length = descriptor["bytes"]
|
||||
assert isinstance(point_start, int)
|
||||
assert isinstance(byte_length, int)
|
||||
byte_start = point_start * 3 * 4
|
||||
byte_stop = byte_start + byte_length
|
||||
return Response(
|
||||
content=bytes(points_view[byte_start:byte_stop]),
|
||||
media_type="application/octet-stream",
|
||||
headers={
|
||||
"Cache-Control": "private, max-age=31536000, immutable",
|
||||
"Content-Encoding": "identity",
|
||||
"Content-Length": str(byte_length),
|
||||
"ETag": f'"{descriptor["sha256"]}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Uncompressed-Content-Length": str(byte_length),
|
||||
},
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"/results/{result_id}/timeline/playback/tracks/points-map-f32",
|
||||
response_class=StreamingResponse,
|
||||
@@ -276,13 +319,71 @@ def _binary_chunks(view: memoryview, chunk_size: int = 1024 * 1024) -> Iterator[
|
||||
yield bytes(view[start : start + chunk_size])
|
||||
|
||||
|
||||
def _playback_chunk_catalog(
|
||||
result_id: str,
|
||||
points_view: memoryview,
|
||||
offsets: tuple[int, ...],
|
||||
) -> list[dict[str, object]]:
|
||||
frame_count = len(offsets) - 1
|
||||
chunk_count = (
|
||||
frame_count + M4_THREAT_PLAYBACK_CHUNK_FRAMES - 1
|
||||
) // M4_THREAT_PLAYBACK_CHUNK_FRAMES
|
||||
return [
|
||||
descriptor
|
||||
for chunk_index in range(chunk_count)
|
||||
if (
|
||||
descriptor := _playback_chunk_descriptor(
|
||||
result_id,
|
||||
points_view,
|
||||
offsets,
|
||||
chunk_index,
|
||||
)
|
||||
)
|
||||
is not None
|
||||
]
|
||||
|
||||
|
||||
def _playback_chunk_descriptor(
|
||||
result_id: str,
|
||||
points_view: memoryview,
|
||||
offsets: tuple[int, ...],
|
||||
chunk_index: int,
|
||||
) -> dict[str, object] | None:
|
||||
frame_count = len(offsets) - 1
|
||||
start = chunk_index * M4_THREAT_PLAYBACK_CHUNK_FRAMES
|
||||
if chunk_index < 0 or start >= frame_count:
|
||||
return None
|
||||
count = min(M4_THREAT_PLAYBACK_CHUNK_FRAMES, frame_count - start)
|
||||
point_start = offsets[start]
|
||||
point_stop = offsets[start + count]
|
||||
byte_start = point_start * 3 * 4
|
||||
byte_stop = point_stop * 3 * 4
|
||||
payload = points_view[byte_start:byte_stop]
|
||||
return {
|
||||
"index": chunk_index,
|
||||
"start": start,
|
||||
"count": count,
|
||||
"point_start": point_start,
|
||||
"point_count": point_stop - point_start,
|
||||
"url": (
|
||||
f"/api/v1/laboratory/m4-threat/results/{result_id}"
|
||||
f"/timeline/playback/chunks/{chunk_index}"
|
||||
),
|
||||
"media_type": "application/octet-stream",
|
||||
"dtype": "<f4",
|
||||
"shape": [point_stop - point_start, 3],
|
||||
"bytes": payload.nbytes,
|
||||
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def _read_threat_result_cached(
|
||||
root_value: str,
|
||||
signature: tuple[int, ...],
|
||||
) -> ThreatReplayResult:
|
||||
del signature
|
||||
return read_threat_replay_result(Path(root_value))
|
||||
return read_threat_replay_result_metadata(Path(root_value))
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
|
||||
Reference in New Issue
Block a user