fix(lab): seal RAV004 spatial replay transport
This commit is contained in:
@@ -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`.
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user