fix(viewer): gate recorded perception readiness

This commit is contained in:
DCCONSTRUCTIONS 2026-07-23 12:27:48 +03:00
parent ee15160862
commit 9f712bf353
6 changed files with 347 additions and 40 deletions

View File

@ -5,6 +5,20 @@ import type { RecordedAdmissionPhase } from "../core/observation/recordedSession
export type RerunViewportStatus = "idle" | "loading" | "ready" | "error"; export type RerunViewportStatus = "idle" | "loading" | "ready" | "error";
export type RecordedRerunView = "spatial" | "perception" | "perception3d" | "metrics"; export type RecordedRerunView = "spatial" | "perception" | "perception3d" | "metrics";
export type RecordedPerceptionLoadPhase =
| "idle"
| "loading"
| "ready"
| "unavailable"
| "error";
export interface RecordedPerceptionLoadState {
phase: RecordedPerceptionLoadPhase;
receivedBytes: number;
totalBytes: number | null;
progress: number | null;
message: string;
}
export interface RecordedPerceptionLayers { export interface RecordedPerceptionLayers {
enabled: boolean; enabled: boolean;
@ -67,7 +81,9 @@ export interface RerunViewportProps {
recordedView?: RecordedRerunView; recordedView?: RecordedRerunView;
recordedViewResetGeneration?: 0 | 1; recordedViewResetGeneration?: 0 | 1;
recordedPerceptionLayers?: RecordedPerceptionLayers; recordedPerceptionLayers?: RecordedPerceptionLayers;
onPerceptionAvailabilityChange?: (available: boolean) => void; recordedPerceptionRetryGeneration?: number;
lockPerceptionCameraInteraction?: boolean;
onPerceptionLoadChange?: (state: RecordedPerceptionLoadState) => void;
} }
export interface RecordedRrdArtifactDescriptor { export interface RecordedRrdArtifactDescriptor {
@ -566,10 +582,12 @@ export async function fetchRecordedPerceptionRrd(
origin, origin,
signal, signal,
fetcher = globalThis.fetch, fetcher = globalThis.fetch,
onProgress,
}: { }: {
origin: string; origin: string;
signal?: AbortSignal; signal?: AbortSignal;
fetcher?: typeof globalThis.fetch; fetcher?: typeof globalThis.fetch;
onProgress?: (receivedBytes: number, totalBytes: number) => void;
}, },
): Promise<Uint8Array | null> { ): Promise<Uint8Array | null> {
const base = new URL(origin); const base = new URL(origin);
@ -609,9 +627,32 @@ export async function fetchRecordedPerceptionRrd(
) { ) {
throw new Error("Invalid recorded perception response"); throw new Error("Invalid recorded perception response");
} }
const payload = new Uint8Array(await response.arrayBuffer()); const payload = new Uint8Array(declaredLength);
let receivedBytes = 0;
onProgress?.(receivedBytes, declaredLength);
if (response.body) {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (receivedBytes + value.byteLength > declaredLength) {
await reader.cancel();
throw new Error("Recorded perception response exceeds its declared length");
}
payload.set(value, receivedBytes);
receivedBytes += value.byteLength;
onProgress?.(receivedBytes, declaredLength);
}
} else {
const fallback = new Uint8Array(await response.arrayBuffer());
if (fallback.byteLength <= declaredLength) {
payload.set(fallback);
receivedBytes = fallback.byteLength;
onProgress?.(receivedBytes, declaredLength);
}
}
if ( if (
payload.byteLength !== declaredLength || receivedBytes !== declaredLength ||
payload[0] !== 0x52 || payload[0] !== 0x52 ||
payload[1] !== 0x52 || payload[1] !== 0x52 ||
payload[2] !== 0x46 || payload[2] !== 0x46 ||
@ -644,7 +685,9 @@ export function RerunViewport({
segmentation: false, segmentation: false,
cuboids3d: false, cuboids3d: false,
}, },
onPerceptionAvailabilityChange, recordedPerceptionRetryGeneration = 0,
lockPerceptionCameraInteraction = false,
onPerceptionLoadChange,
}: RerunViewportProps) { }: RerunViewportProps) {
const hostRef = useRef<HTMLDivElement>(null); const hostRef = useRef<HTMLDivElement>(null);
const [status, setStatus] = useState<RerunViewportStatus>(sourceUrl ? "loading" : "idle"); const [status, setStatus] = useState<RerunViewportStatus>(sourceUrl ? "loading" : "idle");
@ -1189,7 +1232,7 @@ export function RerunViewport({
}, [recordedPerceptionUrl]); }, [recordedPerceptionUrl]);
useEffect(() => { useEffect(() => {
if (!recordedPerceptionUrl || !recordedPerceptionLayers.enabled) return; if (!recordedPerceptionUrl) return;
const active = perceptionChannelRef.current; const active = perceptionChannelRef.current;
const identity = recordedIdentityRef.current; const identity = recordedIdentityRef.current;
if ( if (
@ -1199,16 +1242,51 @@ export function RerunViewport({
!active.channel.ready !active.channel.ready
) return; ) return;
if (loadedPerceptionChannelRef.current === active) { if (loadedPerceptionChannelRef.current === active) {
onPerceptionAvailabilityChange?.(true); onPerceptionLoadChange?.({
phase: "ready",
receivedBytes: 0,
totalBytes: null,
progress: 1,
message: "AI-слои готовы.",
});
return; return;
} }
const abort = new AbortController(); const abort = new AbortController();
let lastReportedPercent = -1;
onPerceptionLoadChange?.({
phase: "loading",
receivedBytes: 0,
totalBytes: null,
progress: null,
message: "Сервер готовит AI-слои.",
});
void fetchRecordedPerceptionRrd(recordedPerceptionUrl, identity, { void fetchRecordedPerceptionRrd(recordedPerceptionUrl, identity, {
origin: window.location.origin, origin: window.location.origin,
signal: abort.signal, signal: abort.signal,
onProgress: (receivedBytes, totalBytes) => {
const progress = totalBytes > 0 ? receivedBytes / totalBytes : null;
const percent = progress === null ? -1 : Math.floor(progress * 100);
if (percent === lastReportedPercent && receivedBytes !== totalBytes) return;
lastReportedPercent = percent;
onPerceptionLoadChange?.({
phase: "loading",
receivedBytes,
totalBytes,
progress,
message: progress === null
? "Загружаем AI-слои."
: `Загружаем AI-слои · ${Math.round(progress * 100)}%`,
});
},
}).then((payload) => { }).then((payload) => {
if (payload === null) { if (payload === null) {
onPerceptionAvailabilityChange?.(false); onPerceptionLoadChange?.({
phase: "unavailable",
receivedBytes: 0,
totalBytes: null,
progress: null,
message: "Для этой сессии AI-слои не опубликованы.",
});
return; return;
} }
if ( if (
@ -1221,16 +1299,31 @@ export function RerunViewport({
} }
active.channel.send_rrd(payload); active.channel.send_rrd(payload);
loadedPerceptionChannelRef.current = active; loadedPerceptionChannelRef.current = active;
onPerceptionAvailabilityChange?.(true); onPerceptionLoadChange?.({
}).catch(() => { phase: "ready",
onPerceptionAvailabilityChange?.(false); receivedBytes: payload.byteLength,
totalBytes: payload.byteLength,
progress: 1,
message: "AI-слои готовы.",
});
}).catch((error: unknown) => {
if (abort.signal.aborted) return;
onPerceptionLoadChange?.({
phase: "error",
receivedBytes: 0,
totalBytes: null,
progress: null,
message: error instanceof Error
? "AI-слои не загрузились. Можно повторить."
: "AI-слои недоступны.",
});
// The base recording remains available when no admitted perception layer exists. // The base recording remains available when no admitted perception layer exists.
}); });
return () => abort.abort(); return () => abort.abort();
}, [ }, [
onPerceptionAvailabilityChange, onPerceptionLoadChange,
perceptionChannelRevision, perceptionChannelRevision,
recordedPerceptionLayers.enabled, recordedPerceptionRetryGeneration,
recordedPerceptionUrl, recordedPerceptionUrl,
]); ]);
@ -1292,6 +1385,13 @@ export function RerunViewport({
className="rerun-viewport__canvas" className="rerun-viewport__canvas"
data-presented={recordedArtifact === null || presentationStatus === "ready" ? "true" : "false"} data-presented={recordedArtifact === null || presentationStatus === "ready" ? "true" : "false"}
/> />
{lockPerceptionCameraInteraction && presentationStatus === "ready" ? (
<div
className="rerun-viewport__camera-lock"
aria-hidden="true"
title="Операторское видео зафиксировано"
/>
) : null}
{presentationStatus === "loading" ? ( {presentationStatus === "loading" ? (
<div className="rerun-viewport__notice" role="status"> <div className="rerun-viewport__notice" role="status">
<span className="busy-indicator" aria-hidden="true" /> <span className="busy-indicator" aria-hidden="true" />

View File

@ -39,6 +39,39 @@
border-radius: 999px; border-radius: 999px;
} }
.spatial-toolbar__perception-status,
.spatial-toolbar__perception-retry {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 0.4rem;
border: 1px solid rgb(255 255 255 / 0.07);
border-radius: 999px;
background: rgb(255 255 255 / 0.035);
padding: 0.42rem 0.62rem;
color: var(--nodedc-text-muted);
font: inherit;
font-size: 0.58rem;
white-space: nowrap;
}
.spatial-toolbar__perception-status .busy-indicator {
width: 0.72rem;
height: 0.72rem;
flex-basis: 0.72rem;
}
.spatial-toolbar__perception-retry {
color: var(--nodedc-text-primary);
cursor: pointer;
}
.spatial-toolbar__perception-retry:hover,
.spatial-toolbar__perception-retry:focus-visible {
background: rgb(255 255 255 / 0.09);
outline: none;
}
.spatial-viewport-shell { .spatial-viewport-shell {
position: relative; position: relative;
min-width: 0; min-width: 0;
@ -95,6 +128,17 @@
height: 100% !important; height: 100% !important;
} }
.rerun-viewport__camera-lock {
position: absolute;
z-index: 2;
top: 0;
bottom: 0;
left: 0;
width: calc(46% - 2px);
cursor: default;
touch-action: none;
}
.rerun-viewport:is([data-status="loading"], [data-status="error"]) .rerun-viewport:is([data-status="loading"], [data-status="error"])
.rerun-viewport__canvas { .rerun-viewport__canvas {
visibility: hidden; visibility: hidden;

View File

@ -45,6 +45,7 @@ import {
type RerunPlaybackState, type RerunPlaybackState,
type RerunSelection, type RerunSelection,
type RerunViewportStatus, type RerunViewportStatus,
type RecordedPerceptionLoadState,
} from "../components/RerunViewport"; } from "../components/RerunViewport";
import { import {
capabilityStatusLabel, capabilityStatusLabel,
@ -307,9 +308,14 @@ function SpatialWorkspace({
const [playbackState, setPlaybackState] = useState<RerunPlaybackState | null>(null); const [playbackState, setPlaybackState] = useState<RerunPlaybackState | null>(null);
const [playbackController, setPlaybackController] = useState<RerunPlaybackController | null>(null); const [playbackController, setPlaybackController] = useState<RerunPlaybackController | null>(null);
const [recordedViewResetGeneration, setRecordedViewResetGeneration] = useState<0 | 1>(0); const [recordedViewResetGeneration, setRecordedViewResetGeneration] = useState<0 | 1>(0);
const [perceptionAvailability, setPerceptionAvailability] = useState< const [perceptionLoad, setPerceptionLoad] = useState<RecordedPerceptionLoadState>({
"unknown" | "available" | "unavailable" phase: "idle",
>("unknown"); receivedBytes: 0,
totalBytes: null,
progress: null,
message: "",
});
const [perceptionRetryGeneration, setPerceptionRetryGeneration] = useState(0);
const [showDetections2d, setShowDetections2d] = useState(false); const [showDetections2d, setShowDetections2d] = useState(false);
const [showSegmentation, setShowSegmentation] = useState(false); const [showSegmentation, setShowSegmentation] = useState(false);
const [showCuboids3d, setShowCuboids3d] = useState(false); const [showCuboids3d, setShowCuboids3d] = useState(false);
@ -335,7 +341,9 @@ function SpatialWorkspace({
const mediaSources = observationSources.filter( const mediaSources = observationSources.filter(
(source) => source.capabilities.overlay && source.modality !== "point-cloud", (source) => source.capabilities.overlay && source.modality !== "point-cloud",
); );
const recordedPerceptionSupported = recordedSource && perceptionAvailability !== "unavailable"; const recordedPerceptionSupported =
recordedSource && perceptionLoad.phase !== "unavailable";
const recordedPerceptionReady = recordedSource && perceptionLoad.phase === "ready";
const recordedPerceptionEnabled = const recordedPerceptionEnabled =
showDetections2d || showSegmentation || showCuboids3d; showDetections2d || showSegmentation || showCuboids3d;
// The native recorded camera remains the authoritative original. Only 2D // The native recorded camera remains the authoritative original. Only 2D
@ -421,9 +429,9 @@ function SpatialWorkspace({
(next: RerunPlaybackController | null) => setPlaybackController(next), (next: RerunPlaybackController | null) => setPlaybackController(next),
[], [],
); );
const onPerceptionAvailabilityChange = useCallback((available: boolean) => { const onPerceptionLoadChange = useCallback((next: RecordedPerceptionLoadState) => {
setPerceptionAvailability(available ? "available" : "unavailable"); setPerceptionLoad(next);
if (!available) { if (next.phase === "unavailable" || next.phase === "error") {
setShowDetections2d(false); setShowDetections2d(false);
setShowSegmentation(false); setShowSegmentation(false);
setShowCuboids3d(false); setShowCuboids3d(false);
@ -431,12 +439,19 @@ function SpatialWorkspace({
}, []); }, []);
useEffect(() => { useEffect(() => {
setPerceptionAvailability("unknown"); setPerceptionLoad({
phase: recordedSource ? "loading" : "idle",
receivedBytes: 0,
totalBytes: null,
progress: null,
message: recordedSource ? "Ожидаем канал AI-слоёв." : "",
});
setPerceptionRetryGeneration(0);
setShowDetections2d(false); setShowDetections2d(false);
setShowSegmentation(false); setShowSegmentation(false);
setShowCuboids3d(false); setShowCuboids3d(false);
setRecordedViewResetGeneration(0); setRecordedViewResetGeneration(0);
}, [sourceUrl]); }, [recordedSource, sourceUrl]);
useEffect(() => { useEffect(() => {
if (pointCloudVisible && sourceUrl.trim()) return; if (pointCloudVisible && sourceUrl.trim()) return;
@ -445,7 +460,13 @@ function SpatialWorkspace({
setSelection(null); setSelection(null);
setPlaybackState(null); setPlaybackState(null);
setPlaybackController(null); setPlaybackController(null);
setPerceptionAvailability("unknown"); setPerceptionLoad({
phase: "idle",
receivedBytes: 0,
totalBytes: null,
progress: null,
message: "",
});
setShowDetections2d(false); setShowDetections2d(false);
setShowSegmentation(false); setShowSegmentation(false);
setShowCuboids3d(false); setShowCuboids3d(false);
@ -507,6 +528,7 @@ function SpatialWorkspace({
variant={detections2dActive ? "primary" : "secondary"} variant={detections2dActive ? "primary" : "secondary"}
icon={<Icon name="target" />} icon={<Icon name="target" />}
aria-pressed={detections2dActive} aria-pressed={detections2dActive}
disabled={recordedSource && !recordedPerceptionReady}
onClick={() => recordedSource onClick={() => recordedSource
? setShowDetections2d((current) => !current) ? setShowDetections2d((current) => !current)
: onLivePerceptionLayersChange({ : onLivePerceptionLayersChange({
@ -521,6 +543,7 @@ function SpatialWorkspace({
variant={segmentationActive ? "primary" : "secondary"} variant={segmentationActive ? "primary" : "secondary"}
icon={<Icon name="image" />} icon={<Icon name="image" />}
aria-pressed={segmentationActive} aria-pressed={segmentationActive}
disabled={recordedSource && !recordedPerceptionReady}
onClick={() => recordedSource onClick={() => recordedSource
? setShowSegmentation((current) => !current) ? setShowSegmentation((current) => !current)
: onLivePerceptionLayersChange({ : onLivePerceptionLayersChange({
@ -535,6 +558,7 @@ function SpatialWorkspace({
variant={cuboids3dActive ? "primary" : "secondary"} variant={cuboids3dActive ? "primary" : "secondary"}
icon={<Icon name="apps" />} icon={<Icon name="apps" />}
aria-pressed={cuboids3dActive} aria-pressed={cuboids3dActive}
disabled={recordedSource && !recordedPerceptionReady}
onClick={() => recordedSource onClick={() => recordedSource
? setShowCuboids3d((current) => !current) ? setShowCuboids3d((current) => !current)
: onLivePerceptionLayersChange({ : onLivePerceptionLayersChange({
@ -546,6 +570,22 @@ function SpatialWorkspace({
</Button> </Button>
</div> </div>
) : null} ) : null}
{recordedSource && perceptionLoad.phase === "loading" ? (
<div className="spatial-toolbar__perception-status" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>{perceptionLoad.message || "Готовим AI-слои."}</span>
</div>
) : null}
{recordedSource && perceptionLoad.phase === "error" ? (
<button
type="button"
className="spatial-toolbar__perception-retry"
onClick={() => setPerceptionRetryGeneration((generation) => generation + 1)}
>
<Icon name="refresh" size={14} />
Повторить AI
</button>
) : null}
{recordedSource && presentedViewerStatus === "ready" ? ( {recordedSource && presentedViewerStatus === "ready" ? (
<Button <Button
size="compact" size="compact"
@ -591,12 +631,17 @@ function SpatialWorkspace({
sceneSettings={sceneSettings} sceneSettings={sceneSettings}
recordedViewResetGeneration={recordedViewResetGeneration} recordedViewResetGeneration={recordedViewResetGeneration}
recordedPerceptionLayers={{ recordedPerceptionLayers={{
enabled: recordedPerceptionSupported && recordedPerceptionEnabled, enabled:
recordedPerceptionSupported &&
recordedPerceptionReady &&
recordedPerceptionEnabled,
detections2d: showDetections2d, detections2d: showDetections2d,
segmentation: showSegmentation, segmentation: showSegmentation,
cuboids3d: showCuboids3d, cuboids3d: showCuboids3d,
}} }}
onPerceptionAvailabilityChange={onPerceptionAvailabilityChange} recordedPerceptionRetryGeneration={perceptionRetryGeneration}
lockPerceptionCameraInteraction={unifiedPerception}
onPerceptionLoadChange={onPerceptionLoadChange}
onStatusChange={onStatusChange} onStatusChange={onStatusChange}
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
onPlaybackChange={onPlaybackChange} onPlaybackChange={onPlaybackChange}

View File

@ -375,11 +375,15 @@ test("recorded perception fetch admits one complete same-origin RRD or no layer"
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/perception.rrd", "http://127.0.0.1:5174/api/v1/observation-sessions/session-1/perception.rrd",
); );
const payload = Uint8Array.from([0x52, 0x52, 0x46, 0x32, 0x01]); const payload = Uint8Array.from([0x52, 0x52, 0x46, 0x32, 0x01]);
const progress = [];
const result = await fetchRecordedPerceptionRrd( const result = await fetchRecordedPerceptionRrd(
endpoint, endpoint,
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" }, { applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
{ {
origin: "http://127.0.0.1:5174", origin: "http://127.0.0.1:5174",
onProgress: (receivedBytes, totalBytes) => {
progress.push([receivedBytes, totalBytes]);
},
fetcher: async (_input, init) => { fetcher: async (_input, init) => {
assert.deepEqual(JSON.parse(String(init.body)), { assert.deepEqual(JSON.parse(String(init.body)), {
application_id: "nodedc_mission_core_recorded", application_id: "nodedc_mission_core_recorded",
@ -396,6 +400,8 @@ test("recorded perception fetch admits one complete same-origin RRD or no layer"
}, },
); );
assert.deepEqual([...result], [...payload]); assert.deepEqual([...result], [...payload]);
assert.deepEqual(progress[0], [0, payload.byteLength]);
assert.deepEqual(progress.at(-1), [payload.byteLength, payload.byteLength]);
const absent = await fetchRecordedPerceptionRrd( const absent = await fetchRecordedPerceptionRrd(
endpoint, endpoint,

View File

@ -105,16 +105,10 @@ class _CuboidPresentationState:
colors: np.ndarray, colors: np.ndarray,
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, list[str]] | None: ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, list[str]] | None:
accepted = [ accepted = [
item item for item in objects if str(item.get("cuboid_status", "")).startswith("accepted-")
for item in objects
if str(item.get("cuboid_status", "")).startswith("accepted-")
] ]
if not ( if not (
len(accepted) len(accepted) == len(centers) == len(half_sizes) == len(quaternions) == len(colors)
== len(centers)
== len(half_sizes)
== len(quaternions)
== len(colors)
): ):
raise RecordedPerceptionOverlayError( raise RecordedPerceptionOverlayError(
"integrated perception cuboid presentation arrays are inconsistent" "integrated perception cuboid presentation arrays are inconsistent"
@ -343,6 +337,8 @@ class IntegratedPerceptionOverlayStore:
self.cache_root = cache_root.expanduser().absolute() self.cache_root = cache_root.expanduser().absolute()
self.ffmpeg_path = ffmpeg_path.expanduser().absolute() self.ffmpeg_path = ffmpeg_path.expanduser().absolute()
self._lock = threading.Lock() self._lock = threading.Lock()
self._catalog_signature: tuple[Any, ...] | None = None
self._latest_by_session: dict[str, IntegratedPerceptionResult | None] = {}
def render( def render(
self, self,
@ -402,10 +398,21 @@ class IntegratedPerceptionOverlayStore:
return payload return payload
def _latest(self, session_id: str) -> IntegratedPerceptionResult | None: def _latest(self, session_id: str) -> IntegratedPerceptionResult | None:
signature = _directory_catalog_signature(
self.jobs_root,
self.results_root,
self.lidar_packs_root,
)
if signature != self._catalog_signature:
self._catalog_signature = signature
self._latest_by_session.clear()
if session_id in self._latest_by_session:
return self._latest_by_session[session_id]
try: try:
jobs = sorted(self.jobs_root.iterdir()) jobs = sorted(self.jobs_root.iterdir())
candidates = sorted(self.results_root.iterdir()) candidates = sorted(self.results_root.iterdir())
except FileNotFoundError: except FileNotFoundError:
self._latest_by_session[session_id] = None
return None return None
if len(jobs) > MAX_SCAN or len(candidates) > MAX_SCAN: if len(jobs) > MAX_SCAN or len(candidates) > MAX_SCAN:
raise RecordedPerceptionOverlayError("integrated perception catalog is outside bounds") raise RecordedPerceptionOverlayError("integrated perception catalog is outside bounds")
@ -436,8 +443,57 @@ class IntegratedPerceptionOverlayStore:
): ):
matches.append(value) matches.append(value)
if not matches: if not matches:
self._latest_by_session[session_id] = None
return None return None
return max(matches, key=lambda value: (value.created_at_utc, value.result_id)) latest = max(matches, key=lambda value: (value.created_at_utc, value.result_id))
self._latest_by_session[session_id] = latest
return latest
def _directory_catalog_signature(*roots: Path) -> tuple[Any, ...]:
"""Track immutable lab catalogs without rehashing every large artifact per request.
Accepted result directories are append-only. Their directory mtimes still
change when an atomic manifest replacement occurs, while a new experiment
changes the parent listing. This cheap signature therefore invalidates the
process-local validation memo without weakening the first full integrity
validation of every catalog generation.
"""
signature: list[Any] = []
for root in roots:
try:
root_stat = root.stat()
entries = sorted(root.iterdir(), key=lambda path: path.name)
except FileNotFoundError:
signature.append((str(root), None))
continue
if len(entries) > MAX_SCAN:
raise RecordedPerceptionOverlayError("integrated perception catalog is outside bounds")
entry_signature = []
for entry in entries:
try:
metadata = entry.lstat()
except FileNotFoundError:
# A concurrent atomic publication will alter the parent mtime
# and be observed on the next request.
continue
entry_signature.append(
(
entry.name,
metadata.st_mode,
metadata.st_size,
metadata.st_mtime_ns,
)
)
signature.append(
(
str(root),
root_stat.st_mtime_ns,
tuple(entry_signature),
)
)
return tuple(signature)
def _render( def _render(
@ -630,9 +686,7 @@ def _camera_proxy_file(
os.close(descriptor) os.close(descriptor)
path = Path(name) path = Path(name)
path.unlink(missing_ok=True) path.unlink(missing_ok=True)
frame_filter = ( frame_filter = f"select=between(n\\,{start_frame_index}\\,{end_frame_index}),setpts=N/(10*TB)"
f"select=between(n\\,{start_frame_index}\\,{end_frame_index}),setpts=N/(10*TB)"
)
try: try:
completed = subprocess.run( completed = subprocess.run(
[ [
@ -900,9 +954,7 @@ def _canonical_json(value: object) -> bytes:
def _finite(value: object) -> TypeGuard[int | float]: def _finite(value: object) -> TypeGuard[int | float]:
return ( return (
isinstance(value, int | float) isinstance(value, int | float) and not isinstance(value, bool) and bool(np.isfinite(value))
and not isinstance(value, bool)
and bool(np.isfinite(value))
) )

View File

@ -3,10 +3,16 @@ from __future__ import annotations
import importlib.util import importlib.util
import sys import sys
from pathlib import Path from pathlib import Path
from types import SimpleNamespace
import numpy as np import numpy as np
import pytest
from k1link.compute.integrated_perception import _CuboidPresentationState import k1link.compute.integrated_perception as integrated_module
from k1link.compute.integrated_perception import (
IntegratedPerceptionOverlayStore,
_CuboidPresentationState,
)
def _worker_modules() -> tuple[object, object]: def _worker_modules() -> tuple[object, object]:
@ -35,6 +41,60 @@ def _worker_modules() -> tuple[object, object]:
sys.path.pop(0) sys.path.pop(0)
def test_integrated_overlay_catalog_validation_is_memoized_until_publication(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
jobs_root = tmp_path / "jobs"
results_root = tmp_path / "results"
lidar_packs_root = tmp_path / "lidar-packs"
cache_root = tmp_path / "cache"
for root in (jobs_root, results_root, lidar_packs_root):
root.mkdir()
job_root = jobs_root / "job-1"
job_root.mkdir()
first_result = results_root / f"e10-integrated-perception-{'a' * 64}"
first_result.mkdir()
validation_calls = {"job": 0, "result": 0}
job = SimpleNamespace(session_id="session-1")
def validate_job(_root: Path) -> object:
validation_calls["job"] += 1
return job
def validate_result(_job_root: Path, candidate: Path, _packs: Path) -> object:
validation_calls["result"] += 1
return SimpleNamespace(
accepted=True,
publication_scope="recorded-integrated-realtime-qualification-only",
created_at_utc=candidate.name,
result_id=candidate.name,
)
monkeypatch.setattr(integrated_module, "validate_camera_compute_job", validate_job)
monkeypatch.setattr(
integrated_module,
"validate_integrated_perception_result",
validate_result,
)
store = IntegratedPerceptionOverlayStore(
jobs_root=jobs_root,
results_root=results_root,
lidar_packs_root=lidar_packs_root,
cache_root=cache_root,
ffmpeg_path=tmp_path / "ffmpeg",
)
assert store._latest("session-1") is not None
assert store._latest("session-1") is not None
assert validation_calls == {"job": 1, "result": 1}
(results_root / f"e10-integrated-perception-{'b' * 64}").mkdir()
assert store._latest("session-1") is not None
assert validation_calls == {"job": 2, "result": 3}
def test_e10_profile_pins_integrated_realtime_budget() -> None: def test_e10_profile_pins_integrated_realtime_budget() -> None:
_fusion, runner = _worker_modules() _fusion, runner = _worker_modules()
profile_path = ( profile_path = (