fix(lab): seal RAV004 spatial replay transport

This commit is contained in:
DCCONSTRUCTIONS
2026-08-30 10:42:41 +03:00
parent f1cbe0061a
commit 757d368c86
11 changed files with 492 additions and 91 deletions
@@ -28,6 +28,7 @@ export function RecordedEvidenceVideoScene({
imageWidth,
imageHeight,
boxes,
overlaySeconds,
semanticOverlay,
pointCloudOverlay,
ariaLabel,
@@ -46,6 +47,7 @@ export function RecordedEvidenceVideoScene({
imageWidth: number;
imageHeight: number;
boxes: readonly RecordedEvidenceBox[];
overlaySeconds?: number;
semanticOverlay?: RecordedEvidenceSemanticOverlay;
pointCloudOverlay?: RecordedEvidencePointCloudOverlayData;
ariaLabel: string;
@@ -77,7 +79,8 @@ export function RecordedEvidenceVideoScene({
const sourceReady = admissionPhase === "ready";
const overlaysPresented = sourceReady
&& presentedSeconds !== null
&& Math.abs(presentedSeconds - playback.currentSeconds) <= 0.25;
&& Math.abs(presentedSeconds - playback.currentSeconds) <= 0.25
&& (overlaySeconds === undefined || Math.abs(presentedSeconds - overlaySeconds) <= 0.25);
const handlePlaybackChange = (next: RecordedObservationPlayback) => {
setPresentedSeconds(next.currentSeconds);
// During a paused operator seek the existing media element can emit its old
@@ -238,7 +238,7 @@ export interface M4ThreatPlaybackProgress {
export interface M4ThreatPlaybackPointPack {
resultId: string;
frameCount: 4489;
frameCount: number;
pointCount: number;
pointOffsets: Uint32Array;
pointsMapXyzM: Float32Array;
@@ -261,7 +261,7 @@ export interface M4ThreatPlaybackChunkDescriptor {
export interface M4ThreatPlaybackManifest {
resultId: string;
frameCount: 4489;
frameCount: number;
pointCount: number;
pointOffsets: Uint32Array;
chunkFrameCount: 24;
@@ -341,7 +341,7 @@ const motion = (value: unknown): M4ThreatMotion => {
};
const resultId = (value: unknown): string => {
const parsed = text(value, "M4.6 result id");
if (!/^m4-threat-replay-[a-f0-9]{64}$/.test(parsed)) {
if (!/^[a-z0-9][a-z0-9-]{0,127}-[a-f0-9]{64}$/.test(parsed)) {
throw new M4ThreatContractError("M4.6 result id: нарушена идентичность.");
}
return parsed;
@@ -1001,7 +1001,10 @@ export async function fetchM4ThreatPlaybackManifest(
exact(manifest.result_id, result, "M4.6 playback result");
exact(manifest.coordinate_frame, "map", "M4.6 playback coordinate frame");
exact(manifest.access, "read-only-sealed-binary-playback", "M4.6 playback access");
const frameCount = exact(integer(manifest.frame_count, "M4.6 playback frames"), 4489, "M4.6 playback frames");
const frameCount = integer(manifest.frame_count, "M4.6 playback frames");
if (frameCount < 1) {
throw new M4ThreatContractError("M4.6 playback frames: пустой playback недопустим.");
}
const pointCount = integer(manifest.point_count, "M4.6 playback points");
const offsetsRaw = array(manifest.point_offsets, "M4.6 playback offsets");
if (offsetsRaw.length !== frameCount + 1) {
@@ -180,6 +180,7 @@ export function M4ReplayThreatVisual({
showReferenceMediaLayers = true,
showSpatialOverlaySummary = true,
playbackTransport = "epoch-stream",
spatialPlaybackTransport = "auto",
recoverTimestampStalls = false,
onActiveSequenceChange,
}: {
@@ -198,6 +199,7 @@ export function M4ReplayThreatVisual({
showReferenceMediaLayers?: boolean;
showSpatialOverlaySummary?: boolean;
playbackTransport?: "segmented" | "epoch-stream";
spatialPlaybackTransport?: "auto" | "sealed-binary" | "json";
recoverTimestampStalls?: boolean;
onActiveSequenceChange?: (sequence: number | null) => void;
}) {
@@ -293,6 +295,7 @@ export function M4ReplayThreatVisual({
currentSeconds: playbackController.playback.currentSeconds,
includeSpatialPoints: evidenceDemand.sourceSpatialPoints,
endpointRoot: timelineEndpointRoot,
spatialPlaybackTransport,
});
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
const [videoLoading, setVideoLoading] = useState(false);
@@ -1143,6 +1146,7 @@ export function M4ReplayThreatVisual({
imageWidth={timeline.imageWidth}
imageHeight={timeline.imageHeight}
boxes={activeBoxes}
overlaySeconds={frame?.sessionSeconds}
semanticOverlay={mediaMode === "video" ? semanticOverlay : undefined}
pointCloudOverlay={mediaMode === "video" ? pointCloudOverlay : undefined}
ariaLabel={`${evidenceLabel} recorded-realtime frame ${frame?.sequence ?? 0}: ${activeBoxes.length} proposals`}
@@ -80,6 +80,7 @@ function FullRouteReviewEvidence({
classifiedSpatialLayer={sealedSpatialGap}
evidenceLabel="RAVNOVES004TREE"
playbackTransport="segmented"
spatialPlaybackTransport="sealed-binary"
recoverTimestampStalls
showReferenceMediaLayers
showSpatialOverlaySummary
@@ -34,14 +34,14 @@ export function m4ThreatChunkWindowStarts(
if (chunkSize < 1 || frameCount < 1) return [];
return [
activeChunkStart,
...Array.from(
{ length: RETAINED_CHUNKS_BEHIND },
(_, index) => activeChunkStart - (index + 1) * chunkSize,
),
...Array.from(
{ length: PREFETCH_CHUNKS_AHEAD },
(_, index) => activeChunkStart + (index + 1) * chunkSize,
),
...Array.from(
{ length: RETAINED_CHUNKS_BEHIND },
(_, index) => activeChunkStart - (index + 1) * chunkSize,
),
].filter((start) => start >= 0 && start < frameCount);
}
@@ -86,12 +86,14 @@ export function useM4ThreatTimelineFrame({
currentSeconds,
includeSpatialPoints = true,
endpointRoot,
spatialPlaybackTransport = "auto",
}: {
resultId: string;
timeline: M4ThreatTimeline | null;
currentSeconds: number;
includeSpatialPoints?: boolean;
endpointRoot?: string;
spatialPlaybackTransport?: "auto" | "sealed-binary" | "json";
}) {
const [chunks, setChunks] = useState<ReadonlyMap<number, M4ThreatTimelineChunk>>(
() => new Map(),
@@ -104,8 +106,10 @@ export function useM4ThreatTimelineFrame({
totalBytes: 0,
});
const [playbackError, setPlaybackError] = useState<string | null>(null);
const binaryPlayback = endpointRoot === undefined
|| endpointRoot === M4_THREAT_TIMELINE_ENDPOINT_ROOT;
const binaryPlayback = spatialPlaybackTransport === "sealed-binary"
|| (spatialPlaybackTransport === "auto" && (
endpointRoot === undefined || endpointRoot === M4_THREAT_TIMELINE_ENDPOINT_ROOT
));
const inFlight = useRef(new Map<number, AbortController>());
const chunksRef = useRef(chunks);
const activeChunkStartRef = useRef<number | null>(null);
@@ -379,14 +379,16 @@ test("M4.6 hydrates a lightweight timeline frame from one retained binary point
});
test("M4.6 source cloud opens from one verified bounded chunk instead of the 105 MiB track", async () => {
const pointOffsets = [0, ...Array(4489).fill(2)];
const playbackResultId = `lab-v1-vegetation-shadow-${"b".repeat(64)}`;
const frameCount = 48;
const pointOffsets = [0, ...Array(frameCount).fill(2)];
const points = new Float32Array([11, 20, 30.25, 12, 19.5, 30]);
const pointBytes = points.buffer;
const pointSha256 = createHash("sha256").update(Buffer.from(pointBytes)).digest("hex");
const endpointRoot = "/api/v1/laboratory/m4-threat/results";
const chunks = Array.from({ length: 188 }, (_, index) => {
const chunks = Array.from({ length: Math.ceil(frameCount / 24) }, (_, index) => {
const start = index * 24;
const count = Math.min(24, 4489 - start);
const count = Math.min(24, frameCount - start);
const pointStart = pointOffsets[start];
const pointStop = pointOffsets[start + count];
const pointCount = pointStop - pointStart;
@@ -396,7 +398,7 @@ test("M4.6 source cloud opens from one verified bounded chunk instead of the 105
count,
point_start: pointStart,
point_count: pointCount,
url: `${endpointRoot}/${resultId}/timeline/playback/chunks/${index}`,
url: `${endpointRoot}/${playbackResultId}/timeline/playback/chunks/${index}`,
media_type: "application/octet-stream",
dtype: "<f4",
shape: [pointCount, 3],
@@ -406,8 +408,8 @@ test("M4.6 source cloud opens from one verified bounded chunk instead of the 105
});
const manifestPayload = {
schema_version: "missioncore.recorded-spatial-playback/v1",
result_id: resultId,
frame_count: 4489,
result_id: playbackResultId,
frame_count: frameCount,
point_count: 2,
point_offsets: pointOffsets,
chunk_frame_count: 24,
@@ -416,7 +418,7 @@ test("M4.6 source cloud opens from one verified bounded chunk instead of the 105
chunks,
track: {
id: "points-map-f32",
url: `${endpointRoot}/${resultId}/timeline/playback/tracks/points-map-f32`,
url: `${endpointRoot}/${playbackResultId}/timeline/playback/tracks/points-map-f32`,
media_type: "application/octet-stream",
dtype: "<f4",
shape: [2, 3],
@@ -434,12 +436,12 @@ test("M4.6 source cloud opens from one verified bounded chunk instead of the 105
if (url.endsWith("/timeline/playback/chunks/0")) return new Response(pointBytes.slice(0));
return new Response(null, { status: 404 });
};
const manifest = await fetchM4ThreatPlaybackManifest(resultId, { fetcher });
const manifest = await fetchM4ThreatPlaybackManifest(playbackResultId, { fetcher });
const chunk = await fetchM4ThreatPlaybackPointChunk(manifest, 0, { fetcher });
assert.deepEqual(requested, [
`${endpointRoot}/${resultId}/timeline/playback`,
`${endpointRoot}/${resultId}/timeline/playback/chunks/0`,
`${endpointRoot}/${playbackResultId}/timeline/playback`,
`${endpointRoot}/${playbackResultId}/timeline/playback/chunks/0`,
]);
assert.equal(chunk.pointCount, 2);
assert.equal(chunk.pointStart, 0);
@@ -769,7 +771,7 @@ test("M4.6 local SLAM surface reprojects registered increments into the active b
});
test("M4.6 spatial buffering keeps the active and one future chunk", () => {
assert.deepEqual(m4ThreatChunkWindowStarts(48, 24, 4489), [48, 24, 72]);
assert.deepEqual(m4ThreatChunkWindowStarts(48, 24, 4489), [48, 72, 24]);
assert.deepEqual(m4ThreatChunkWindowStarts(0, 24, 4489), [0, 24]);
});
@@ -787,7 +789,7 @@ test("M4.6 spatial buffering drops stale in-flight windows across rapid jumps",
}
}
assert.deepEqual([...inFlight.keys()], [4488, 4464]);
assert.deepEqual(aborted, [0, 24, 1488, 1464, 1512]);
assert.deepEqual(aborted, [0, 24, 1488, 1512, 1464]);
});
test("recorded evidence clock advances by selected rate and stops at the sealed end", () => {
@@ -22,6 +22,29 @@ Window structure, switching, seek, buffering and spatial scene code are shared.
## Why the previous LAB failed
### RAV004 did not use the accepted replay data plane
The working M4/Hologravity LAB uses sealed binary numeric tracks, retained scene
state and bounded JSON metadata. RAV004 reused the visual component but silently
left its custom timeline endpoint on the JSON fallback. Each eight-frame spatial
window therefore transferred approximately 1.2-2.9 MB and took 0.87-1.37 s to
produce while representing only about 0.84 s of playback. The next request
aborted and replaced the previous one before spatial state could catch up.
The user screenshot captured the failure exactly: camera frame 366 was active
while the last delivered spatial evidence was frame 230, a 136-frame gap. The
backend also reopened and indexed the 6830-entry semantic ZIP for every requested
mask and proposal frame. This explains why the same canonical viewer was smooth
for Hologravity but stalled for RAV004: the window and interaction code were
shared, but the data-plane contract was not.
RAV004 now publishes the same `missioncore.recorded-spatial-playback/v1`
contract as the accepted LAB: an immutable Float32 map-point track, camera-frame
offsets and 24-frame binary chunks. JSON chunks contain bounded frame metadata
only; retained Local SLAM is reconstructed from exact source increments in the
shared client. The semantic ZIP handle and member index are cached per immutable
artifact instead of being reparsed per frame.
### Video and spatial state had different clocks
The removed RAV004 viewer advanced an animation/host clock even when the browser
@@ -77,8 +100,11 @@ dozens of seconds old. The buffer now loads the active chunk first, the precedin
chunk second and the next chunk as prefetch. The scene selects the latest proven
source increment whose sequence is not later than the active camera frame.
At the final UI check, camera frame 189 causally held spatial frame 184. Before
the fix the same point could hold frame 16.
At the final UI check, playback restarted at frame 1, then ran continuously past
frame 462. At frame 188 the DDRNet mask was frame 188, proposals were frame 187
and spatial state was delivered without buffering; the one-frame proposal delay
is the recorded causal overlay, not stale UI state. Before the transport fix the
user's run had already fallen 136 frames behind by camera frame 366.
## Capability ledger
@@ -104,12 +130,17 @@ full-TGS result is also not reused because it has a different source identity an
Measured on the canonical local service and current immutable artifacts:
- replay launch POST: 3.55 s on first opening;
- timeline metadata: 0.02 s warm;
- active spatial chunk, eight camera frames: 35.17 s first process-local RRD
index build, 0.67 s warm, approximately 3.81 MB;
- UI replay: passed the previously deterministic 11.422 s decoder stop, then
continued to 59 s with media and timeline advancing together;
- operator reset seek: 16.4 s to 0 s, one mounted media worker, successful;
- first binary playback-manifest build after a service restart: 32.33 s while
the process-local RRD point track is materialized; warm manifest: 0.18-0.20 s;
- full retained point track: 3,893,445 Float32 map points, 46,721,340 bytes,
divided into 285 immutable 24-frame chunks;
- representative active binary chunks: 155-224 KB at 9-10 ms;
- representative 24-frame metadata chunks: 24-32 KB at 0.78-1.0 s, covering
about 2.4 s of playback;
- cached semantic-mask reads: 5.6-7.8 ms instead of approximately 80 ms;
- UI replay: reset to frame 1 and ran continuously beyond frame 462 with camera,
semantic overlay and retained spatial state advancing together;
- operator reset seek: successful, one mounted media worker;
- browser console after the acceptance run: no warnings or errors.
The first RRD index is still process-local rather than a persistent disk cache.
@@ -147,13 +178,14 @@ deploy it.
## Acceptance performed
- 44 focused backend tests passed;
- 37 frontend replay, buffering and LAB contract tests passed;
- 12 focused backend spatial/API tests passed;
- 16 focused frontend replay transport/manifest tests passed;
- TypeScript project typecheck passed;
- production Vite build passed (only existing large-chunk warnings);
- `git diff --check` passed;
- live browser run verified the shared controls, disabled unsealed TGS/3D
semantics, continuous media recovery, causal spatial hold and clean console.
- live browser run verified reset seek, the shared controls, disabled unsealed
TGS/3D semantics, continuous playback through the former failing interval,
causal spatial hold and a clean console.
Visual QA: `docs/handoff/2026-08-30_RAV004_CANONICAL_LAB_QA.jpg`.
+94 -9
View File
@@ -75,7 +75,13 @@ def _session_times(batch: Any) -> Any | None:
return batch.column("session_time")
def _point_rows(chunks: list[Any], entity: str, component: str, *, nested: bool = False) -> _TimedPoints:
def _point_rows(
chunks: list[Any],
entity: str,
component: str,
*,
nested: bool = False,
) -> _TimedPoints:
rows: list[tuple[int, np.ndarray]] = []
for chunk in chunks:
if chunk.entity_path != entity:
@@ -436,6 +442,8 @@ def _bounded_local_slam(
def _canonical_lab_spatial_frame_from_index(
index: _CanonicalSpatialIndex,
target_time_ns: int,
*,
include_local_slam: bool = True,
) -> dict[str, object]:
point_index = _latest_index(index.points.times_ns, target_time_ns)
pose_index = _latest_index(index.poses.times_ns, index.points.times_ns[point_index])
@@ -462,12 +470,17 @@ def _canonical_lab_spatial_frame_from_index(
ground_origin,
basis_map_from_body,
)
local_slam, local_slam_source_frames, local_slam_source_points = _bounded_local_slam(
index.points,
index.points.times_ns[point_index],
ground_origin,
basis_map_from_body,
)
if include_local_slam:
local_slam, local_slam_source_frames, local_slam_source_points = _bounded_local_slam(
index.points,
index.points.times_ns[point_index],
ground_origin,
basis_map_from_body,
)
else:
local_slam = np.empty((0, 3), dtype=np.float32)
local_slam_source_frames = 0
local_slam_source_points = 0
return {
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v3",
"target_time_ns": target_time_ns,
@@ -531,6 +544,8 @@ def canonical_lab_spatial_timeline_samples(
frame_times_ns: tuple[int, ...],
start_sequence: int,
frame_count: int,
*,
include_local_slam: bool = True,
) -> tuple[dict[str, object] | None, ...]:
"""Project only new source increments onto a denser camera timeline.
@@ -545,7 +560,10 @@ def canonical_lab_spatial_timeline_samples(
start_sequence < 0
or frame_count < 1
or start_sequence >= len(frame_times_ns)
or any(current <= previous for previous, current in zip(frame_times_ns, frame_times_ns[1:]))
or any(
current <= previous
for previous, current in zip(frame_times_ns, frame_times_ns[1:], strict=False)
)
):
raise ValueError("Recorded LAB timeline sample request is invalid")
stat = recording_path.stat()
@@ -566,8 +584,75 @@ def canonical_lab_spatial_timeline_samples(
else _latest_index(index.points.times_ns, frame_times_ns[sequence - 1])
)
samples.append(
_canonical_lab_spatial_frame_from_index(index, target_time_ns)
_canonical_lab_spatial_frame_from_index(
index,
target_time_ns,
include_local_slam=include_local_slam,
)
if point_index != previous_point_index
else None
)
return tuple(samples)
@lru_cache(maxsize=2)
def _canonical_lab_spatial_playback_points_cached(
recording_path_text: str,
recording_size: int,
recording_mtime_ns: int,
generation_sha256: str,
frame_times_ns: tuple[int, ...],
) -> tuple[np.ndarray, tuple[int, ...]]:
del recording_size, recording_mtime_ns
recording_path = Path(recording_path_text)
stat = recording_path.stat()
index = _load_index(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
)
increments: list[np.ndarray] = []
offsets = [0]
point_count = 0
previous_point_index = -1
for target_time_ns in frame_times_ns:
point_index = _latest_index(index.points.times_ns, target_time_ns)
if point_index != previous_point_index:
increment = np.ascontiguousarray(index.points.values[point_index], dtype="<f4")
increments.append(increment)
point_count += int(increment.shape[0])
offsets.append(point_count)
previous_point_index = point_index
points = (
np.ascontiguousarray(np.concatenate(increments, axis=0), dtype="<f4")
if increments
else np.empty((0, 3), dtype="<f4")
)
points.setflags(write=False)
return points, tuple(offsets)
def canonical_lab_spatial_playback_points(
recording_path: Path,
generation_sha256: str,
frame_times_ns: tuple[int, ...],
) -> tuple[np.ndarray, tuple[int, ...]]:
"""Return one retained map-coordinate point track for a camera timeline."""
if (
not frame_times_ns
or any(
current <= previous
for previous, current in zip(frame_times_ns, frame_times_ns[1:], strict=False)
)
):
raise ValueError("Recorded LAB playback timeline is invalid")
stat = recording_path.stat()
return _canonical_lab_spatial_playback_points_cached(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
frame_times_ns,
)
+248 -44
View File
@@ -26,13 +26,16 @@ from k1link.laboratory.evidence_report import (
)
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA
from k1link.sessions import RecordedCameraFrame, SessionIntegrityError
from k1link.sessions.canonical_lab_spatial import canonical_lab_spatial_timeline_samples
from k1link.sessions.canonical_lab_spatial import (
canonical_lab_spatial_playback_points,
canonical_lab_spatial_timeline_samples,
)
RootProvider = Callable[[], Path | None]
CanonicalRecordingProvider = Callable[[str], tuple[Path, str] | None]
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
_CANONICAL_ROUTE_CHUNK_FRAMES: Final = 8
_CANONICAL_ROUTE_CHUNK_FRAMES: Final = 24
_DEFINITION: Final = LaboratoryEvidenceDefinition(
work_id="lab-v1-vegetation-shadow",
runtime_relative_root=PurePosixPath("lab-v1-vegetation/results"),
@@ -167,19 +170,7 @@ def _build_vegetation_lab_router(
archive_path = candidate.joinpath(*relative.parts)
member = f"masks/frame-{sequence + 1:06d}.png"
try:
before = archive_path.stat()
with zipfile.ZipFile(archive_path) as frozen:
info = frozen.getinfo(member)
if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024:
raise ValueError("Vegetation video mask member is invalid")
payload = frozen.read(info)
after = archive_path.stat()
if (
before.st_size != after.st_size
or before.st_mtime_ns != after.st_mtime_ns
or len(payload) != info.file_size
):
raise ValueError("Vegetation video mask archive changed during read")
payload = _read_cached_mask_member(archive_path, member)
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
raise HTTPException(
status_code=503,
@@ -286,7 +277,7 @@ def _build_vegetation_lab_router(
route, frame_times_ns = _full_route_context(candidate, manifest)
intervals = [
(current - previous) / 1_000_000_000
for previous, current in zip(frame_times_ns, frame_times_ns[1:])
for previous, current in zip(frame_times_ns, frame_times_ns[1:], strict=False)
]
nominal_interval = statistics.median(intervals)
if not math.isfinite(nominal_interval) or nominal_interval <= 0:
@@ -350,7 +341,10 @@ def _build_vegetation_lab_router(
if start >= len(frame_times_ns):
raise HTTPException(status_code=404, detail="Full-route timeline chunk not found")
if canonical_recording_provider is None:
raise HTTPException(status_code=503, detail="Canonical spatial recording is unavailable")
raise HTTPException(
status_code=503,
detail="Canonical spatial recording is unavailable",
)
recording = canonical_recording_provider(str(route["session_id"]))
if recording is None:
raise HTTPException(status_code=409, detail="Canonical spatial recording is not ready")
@@ -362,6 +356,7 @@ def _build_vegetation_lab_router(
frame_times_ns,
start,
count,
include_local_slam=False,
)
except (OSError, ValueError):
raise HTTPException(status_code=503, detail="Canonical spatial chunk failed") from None
@@ -391,6 +386,98 @@ def _build_vegetation_lab_router(
"access": "read-only-bounded-recorded-replay",
}
@router.get("/{result_id}/timeline/playback")
def get_canonical_route_timeline_playback(result_id: str) -> dict[str, object]:
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
points, offsets = _canonical_route_playback(
canonical_recording_provider,
route,
frame_times_ns,
)
points_view = memoryview(points).cast("B")
return {
"schema_version": "missioncore.recorded-spatial-playback/v1",
"result_id": result_id,
"frame_count": len(frame_times_ns),
"point_count": int(points.shape[0]),
"point_offsets": list(offsets),
"chunk_frame_count": _CANONICAL_ROUTE_CHUNK_FRAMES,
"resident_chunk_count_max": 4,
"forward_prefetch_chunk_count": 1,
"chunks": _canonical_route_playback_chunk_catalog(
prefix,
result_id,
points_view,
offsets,
),
"track": {
"id": "points-map-f32",
"url": f"{prefix}/{result_id}/timeline/playback/tracks/points-map-f32",
"media_type": "application/octet-stream",
"dtype": "<f4",
"shape": [int(points.shape[0]), 3],
"bytes": int(points.nbytes),
"sha256": hashlib.sha256(points_view).hexdigest(),
},
"coordinate_frame": "map",
"ground_truth": False,
"authority": "replay-simulated",
"access": "read-only-sealed-binary-playback",
}
@router.get("/{result_id}/timeline/playback/chunks/{chunk_index}")
def get_canonical_route_timeline_playback_chunk(
result_id: str,
chunk_index: int,
) -> Response:
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
points, offsets = _canonical_route_playback(
canonical_recording_provider,
route,
frame_times_ns,
)
points_view = memoryview(points).cast("B")
descriptor = _canonical_route_playback_chunk_descriptor(
prefix,
result_id,
points_view,
offsets,
chunk_index,
)
if descriptor is None:
raise HTTPException(status_code=404, detail="Full-route playback chunk not found")
point_start = int(descriptor["point_start"])
byte_length = int(descriptor["bytes"])
byte_start = point_start * 3 * 4
payload = bytes(points_view[byte_start : byte_start + byte_length])
return Response(
content=payload,
media_type="application/octet-stream",
headers=_immutable_binary_headers(byte_length, str(descriptor["sha256"])),
)
@router.get("/{result_id}/timeline/playback/tracks/points-map-f32")
def get_canonical_route_timeline_playback_track(result_id: str) -> Response:
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
points, _ = _canonical_route_playback(
canonical_recording_provider,
route,
frame_times_ns,
)
payload = memoryview(points).cast("B")
digest = hashlib.sha256(payload).hexdigest()
return Response(
content=bytes(payload),
media_type="application/octet-stream",
headers=_immutable_binary_headers(payload.nbytes, digest),
)
@router.get("/{result_id}/timeline/frames/{sequence}/camera")
def get_canonical_route_camera(result_id: str, sequence: int) -> Response:
if camera_frame_provider is None:
@@ -403,7 +490,10 @@ def _build_vegetation_lab_router(
try:
camera = camera_frame_provider(str(route["session_id"]), sequence)
except (OSError, SessionIntegrityError, ValueError):
raise HTTPException(status_code=503, detail="Full-route camera frame unavailable") from None
raise HTTPException(
status_code=503,
detail="Full-route camera frame unavailable",
) from None
if camera.width != route["width"] or camera.height != route["height"]:
raise HTTPException(status_code=503, detail="Full-route camera dimensions changed")
return Response(
@@ -495,15 +585,111 @@ def _full_route_context(
values = np.frombuffer(payload, dtype="<u8")
frame_times_ns = tuple(int(value) for value in values)
except (OSError, ValueError):
raise HTTPException(status_code=503, detail="Full-route timeline verification failed") from None
raise HTTPException(
status_code=503,
detail="Full-route timeline verification failed",
) from None
if (
len(frame_times_ns) != route["frame_count"]
or any(current <= previous for previous, current in zip(frame_times_ns, frame_times_ns[1:]))
or any(
current <= previous
for previous, current in zip(frame_times_ns, frame_times_ns[1:], strict=False)
)
):
raise HTTPException(status_code=503, detail="Full-route timeline order changed")
return route, frame_times_ns
def _canonical_route_playback(
provider: CanonicalRecordingProvider | None,
route: dict[str, Any],
frame_times_ns: tuple[int, ...],
) -> tuple[np.ndarray, tuple[int, ...]]:
if provider is None:
raise HTTPException(status_code=503, detail="Canonical spatial recording is unavailable")
recording = provider(str(route["session_id"]))
if recording is None:
raise HTTPException(status_code=409, detail="Canonical spatial recording is not ready")
recording_path, generation_sha256 = recording
try:
return canonical_lab_spatial_playback_points(
recording_path,
generation_sha256,
frame_times_ns,
)
except (OSError, ValueError):
raise HTTPException(status_code=503, detail="Canonical spatial playback failed") from None
def _canonical_route_playback_chunk_descriptor(
endpoint_prefix: str,
result_id: str,
points_view: memoryview,
offsets: tuple[int, ...],
chunk_index: int,
) -> dict[str, object] | None:
frame_count = len(offsets) - 1
start = chunk_index * _CANONICAL_ROUTE_CHUNK_FRAMES
if chunk_index < 0 or start >= frame_count:
return None
count = min(_CANONICAL_ROUTE_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"{endpoint_prefix}/{result_id}/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(),
}
def _canonical_route_playback_chunk_catalog(
endpoint_prefix: str,
result_id: str,
points_view: memoryview,
offsets: tuple[int, ...],
) -> list[dict[str, object]]:
frame_count = len(offsets) - 1
chunk_count = (
frame_count + _CANONICAL_ROUTE_CHUNK_FRAMES - 1
) // _CANONICAL_ROUTE_CHUNK_FRAMES
return [
descriptor
for chunk_index in range(chunk_count)
if (
descriptor := _canonical_route_playback_chunk_descriptor(
endpoint_prefix,
result_id,
points_view,
offsets,
chunk_index,
)
)
is not None
]
def _immutable_binary_headers(byte_length: int, sha256: str) -> dict[str, str]:
return {
"Cache-Control": "private, max-age=31536000, immutable",
"Content-Encoding": "identity",
"Content-Length": str(byte_length),
"ETag": f'"{sha256}"',
"X-Content-Type-Options": "nosniff",
"X-Uncompressed-Content-Length": str(byte_length),
}
def _canonical_timeline_frame(
*,
result_id: str,
@@ -518,7 +704,6 @@ def _canonical_timeline_frame(
points = [] if spatial is None or not include_points else spatial["source_points_body_xyz_m"]
point_count = 0 if spatial is None else int(spatial["source_point_count"])
body_frame = None if spatial is None else spatial["body_frame"]
local_slam = [] if spatial is None else spatial["local_slam_body_xyz_m"]
return {
"schema_version": "missioncore.recorded-spatial-evidence-frame/v1",
"sequence": sequence,
@@ -534,11 +719,6 @@ def _canonical_timeline_frame(
"point_cloud_source_count": point_count,
"point_cloud_sample_count": point_count if not include_points else len(points),
"point_cloud_layer": "current-increment",
"local_slam_body_xyz_m": local_slam,
"local_slam_source_frame_count": 0
if spatial is None else spatial["local_slam_source_frame_count"],
"local_slam_source_point_count": 0
if spatial is None else spatial["local_slam_source_point_count"],
"rolling_map_component_count": 0,
"metric_obstacles": [],
"camera_proposals": _semantic_component_proposals(candidate, route, sequence),
@@ -585,12 +765,14 @@ def _semantic_component_proposals_cached(
archive_mtime_ns: int,
sequence: int,
) -> tuple[dict[str, object], ...]:
del archive_size, archive_mtime_ns
archive_path = Path(archive_path_text)
member = f"masks/frame-{sequence + 1:06d}.png"
try:
with zipfile.ZipFile(archive_path) as frozen:
payload = frozen.read(member)
frozen = _cached_zip_archive(
archive_path_text,
archive_size,
archive_mtime_ns,
)
payload = frozen.read(member)
with Image.open(io.BytesIO(payload)) as image:
mask = np.asarray(image.convert("L"), dtype=np.uint8)
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
@@ -733,7 +915,10 @@ def _route_tgs_anchor_payload(path: Path, source_sequence: int) -> dict[str, obj
selected_points = np.ascontiguousarray(points[start:end], dtype=np.float32)
selected_states = np.ascontiguousarray(states[slot], dtype=np.uint8)
selected_z_bounds = np.ascontiguousarray(z_bounds[slot], dtype=np.float32)
if not np.isfinite(selected_points).all() or not np.isin(selected_states, [0, 1, 2, 3]).all():
if (
not np.isfinite(selected_points).all()
or not np.isin(selected_states, [0, 1, 2, 3]).all()
):
raise ValueError("Route TGS payload changed")
result = {
"schema_version": "missioncore.lab-v1-route-tgs-anchor/v1",
@@ -762,19 +947,7 @@ def _route_tgs_anchor_payload(path: Path, source_sequence: int) -> dict[str, obj
def _zip_mask_response(archive_path: Path, sequence: int) -> Response:
member = f"masks/frame-{sequence + 1:06d}.png"
try:
before = archive_path.stat()
with zipfile.ZipFile(archive_path) as frozen:
info = frozen.getinfo(member)
if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024:
raise ValueError("Semantic mask member is invalid")
payload = frozen.read(info)
after = archive_path.stat()
if (
before.st_size != after.st_size
or before.st_mtime_ns != after.st_mtime_ns
or len(payload) != info.file_size
):
raise ValueError("Semantic mask archive changed during read")
payload = _read_cached_mask_member(archive_path, member)
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
raise HTTPException(
status_code=503,
@@ -792,6 +965,37 @@ def _zip_mask_response(archive_path: Path, sequence: int) -> Response:
)
@lru_cache(maxsize=8)
def _cached_zip_archive(
archive_path_text: str,
archive_size: int,
archive_mtime_ns: int,
) -> zipfile.ZipFile:
del archive_size, archive_mtime_ns
return zipfile.ZipFile(archive_path_text)
def _read_cached_mask_member(archive_path: Path, member: str) -> bytes:
before = archive_path.stat()
frozen = _cached_zip_archive(
str(archive_path),
before.st_size,
before.st_mtime_ns,
)
info = frozen.getinfo(member)
if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024:
raise ValueError("Semantic mask member is invalid")
payload = frozen.read(info)
after = archive_path.stat()
if (
before.st_size != after.st_size
or before.st_mtime_ns != after.st_mtime_ns
or len(payload) != info.file_size
):
raise ValueError("Semantic mask archive changed during read")
return payload
def _configured_root(provider: RootProvider) -> Path | None:
candidate = provider()
if candidate is None:
+46 -3
View File
@@ -3,14 +3,17 @@ from __future__ import annotations
import numpy as np
import pytest
import k1link.sessions.canonical_lab_spatial as spatial_module
from k1link.sessions.canonical_lab_spatial import (
_TimedPoints,
_TimedPoses,
_bounded_local_slam,
_estimate_sensor_height,
_CanonicalSpatialIndex,
_estimate_local_sensor_height,
_estimate_sensor_height,
_gravity_stable_basis_map_from_body,
_ground_origin_map,
_TimedPoints,
_TimedPoses,
canonical_lab_spatial_playback_points,
)
@@ -127,3 +130,43 @@ def test_sensor_height_tracks_current_source_window_instead_of_fixed_mount() ->
assert high == pytest.approx(1.05, abs=0.03)
assert low_samples >= 3 and high_samples >= 3
assert low_source == high_source == "local-source-cloud-ground-quantile-median"
def test_playback_track_binds_sparse_map_increments_to_dense_camera_timeline(
tmp_path,
monkeypatch,
) -> None:
recording = tmp_path / "recording.rrd"
recording.write_bytes(b"sealed")
points = _TimedPoints(
times_ns=(10, 20),
values=(
np.asarray([[1.0, 2.0, 3.0]], dtype=np.float32),
np.asarray([[4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float32),
),
)
empty_poses = _TimedPoses(times_ns=(), translations=(), quaternions_xyzw=())
index = _CanonicalSpatialIndex(
points=points,
poses=empty_poses,
trajectories=_TimedPoints(times_ns=(), values=()),
sensor_height_m=0.4,
sensor_height_sample_count=0,
sensor_height_mad_m=0.0,
)
monkeypatch.setattr(spatial_module, "_load_index", lambda *_args: index)
track, offsets = canonical_lab_spatial_playback_points(
recording,
"a" * 64,
(10, 15, 20, 25),
)
assert offsets == (0, 1, 1, 3, 3)
assert track.tolist() == [
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
]
assert track.dtype == np.dtype("<f4")
assert not track.flags.writeable
+20
View File
@@ -22,6 +22,7 @@ from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
from k1link.laboratory.vegetation_policy_review import seal_vegetation_policy_review
from k1link.laboratory.vegetation_shadow_lab import seal_vegetation_shadow_lab
from k1link.web.vegetation_shadow_lab_api import (
_canonical_route_playback_chunk_descriptor,
_mask_component_boxes,
_route_tgs_anchor_payload,
build_vegetation_shadow_lab_router,
@@ -42,6 +43,25 @@ def test_semantic_component_boxes_keep_distinct_objects_separate() -> None:
]
def test_route_playback_chunk_descriptor_seals_only_requested_binary_window() -> None:
points = np.arange(18, dtype="<f4").reshape(6, 3)
descriptor = _canonical_route_playback_chunk_descriptor(
"/api/v1/laboratory/vegetation-shadow",
f"lab-v1-vegetation-shadow-{'a' * 64}",
memoryview(points).cast("B"),
(0, 1, 1, 3, 6),
0,
)
assert descriptor is not None
assert descriptor["start"] == 0
assert descriptor["count"] == 4
assert descriptor["point_count"] == 6
assert descriptor["bytes"] == points.nbytes
assert descriptor["shape"] == [6, 3]
assert len(str(descriptor["sha256"])) == 64
def test_route_tgs_anchor_payload_preserves_metric_evidence(tmp_path: Path) -> None:
path = tmp_path / "tgs-evidence.npz"
point_counts = np.arange(1, 11, dtype=np.int64)