Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81fdf6904a | ||
|
|
9c5259dbc9 | ||
|
|
35daf73d5c | ||
|
|
07453142c2 | ||
|
|
f5ee42751d |
Generated
+4
-5
@@ -7,7 +7,6 @@
|
||||
"": {
|
||||
"name": "@nodedc/mission-core-control-station",
|
||||
"version": "0.1.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^2.2.0",
|
||||
"@nodedc/map-cesium-react": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/map-cesium-react",
|
||||
@@ -15,7 +14,7 @@
|
||||
"@nodedc/tokens": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/tokens",
|
||||
"@nodedc/ui-core": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-core",
|
||||
"@nodedc/ui-react": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react",
|
||||
"@rerun-io/web-viewer": "0.34.1",
|
||||
"@rerun-io/web-viewer": "0.36.3",
|
||||
"meshoptimizer": "1.1.1",
|
||||
"playcanvas": "2.21.4",
|
||||
"react": "^19.1.0",
|
||||
@@ -900,9 +899,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@rerun-io/web-viewer": {
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@rerun-io/web-viewer/-/web-viewer-0.34.1.tgz",
|
||||
"integrity": "sha512-2Oq9Mw3qOs765XArGq4e/0pAIksPFKlAqkgo+PDsRkPd77/KdnQhb835suRmYQ6tuqlbPeVCFhlNIXT0+TjmTg==",
|
||||
"version": "0.36.3",
|
||||
"resolved": "https://registry.npmjs.org/@rerun-io/web-viewer/-/web-viewer-0.36.3.tgz",
|
||||
"integrity": "sha512-LMGnsxRmY5UwiGras2dZrMnEYkow5Xr4v+1hAUSspXWPPiilMqoz9G77jo8Ps/deAaX81TnOq123DFO8iX/Ulw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"postinstall": "node scripts/patch-rerun-web-viewer.mjs",
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
@@ -18,7 +17,7 @@
|
||||
"@nodedc/tokens": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/tokens",
|
||||
"@nodedc/ui-core": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-core",
|
||||
"@nodedc/ui-react": "file:../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react",
|
||||
"@rerun-io/web-viewer": "0.34.1",
|
||||
"@rerun-io/web-viewer": "0.36.3",
|
||||
"meshoptimizer": "1.1.1",
|
||||
"playcanvas": "2.21.4",
|
||||
"react": "^19.1.0",
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
RECORDED_BASE_POINT_COLOR_KEY,
|
||||
canPublishRecordedPlaybackController,
|
||||
createRecordedAutoplayGate,
|
||||
createRecordedInitialSeekGate,
|
||||
createRecordedOpenWatchdog,
|
||||
} from "../core/observation/recordedRerunLifecycle";
|
||||
import type {
|
||||
@@ -59,6 +60,7 @@ export {
|
||||
attemptRecordedAutoplay,
|
||||
canPublishRecordedPlaybackController,
|
||||
createRecordedAutoplayGate,
|
||||
createRecordedInitialSeekGate,
|
||||
createRecordedOpenWatchdog,
|
||||
isRecordedPlaybackFullyBuffered,
|
||||
isRecordedPlaybackPresentationReady,
|
||||
@@ -143,14 +145,30 @@ interface RerunBlueprintChannel {
|
||||
};
|
||||
}
|
||||
|
||||
interface RerunNativeReceiver {
|
||||
endpointUrl: string;
|
||||
ready: () => boolean;
|
||||
open: (sourceUrl: string) => void;
|
||||
close: (sourceUrl: string) => void;
|
||||
}
|
||||
|
||||
interface LoadedNativePerceptionSource {
|
||||
receiver: RerunNativeReceiver;
|
||||
descriptorUrl: string;
|
||||
sourceUrl: string;
|
||||
byteLength: number;
|
||||
}
|
||||
|
||||
interface RecordedRerunIdentity {
|
||||
applicationId: "nodedc_mission_core_recorded";
|
||||
recordingId: string;
|
||||
}
|
||||
|
||||
const RECORDED_RRD_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/recording\.rrd$/;
|
||||
const LAB_RECORDED_REPLAY_PATH = /^\/api\/v1\/laboratory\/vegetation-shadow\/lab-v1-vegetation-shadow-[a-f0-9]{64}\/canonical-replay\.rrd$/;
|
||||
const RECORDED_BLUEPRINT_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/blueprint\.rrd$/;
|
||||
const RECORDED_PERCEPTION_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/perception\.rrd$/;
|
||||
const LAB_RECORDED_PERCEPTION_PATH = /^\/api\/v1\/laboratory\/vegetation-shadow\/lab-v1-vegetation-shadow-[a-f0-9]{64}\/canonical-overlay\.rrd$/;
|
||||
const RECORDED_POINT_COLORS_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/point-colors\.rrd$/;
|
||||
const MAX_BLUEPRINT_BYTES = 1_048_576;
|
||||
const MAX_PERCEPTION_BYTES = 512 * 1024 * 1024;
|
||||
@@ -203,12 +221,7 @@ export function resolveRerunSourceUrl(sourceUrl: string, origin: string): string
|
||||
return new URL(normalized, `${base.origin}/`).href;
|
||||
}
|
||||
|
||||
/**
|
||||
* The native Rerun HTTP receiver is the only browser API in 0.34.1 with a
|
||||
* persistent incremental RRD decoder. Bind it to the exact immutable
|
||||
* generation selected by the launch contract; arbitrary `send_rrd` byte
|
||||
* slices are independently decoded files and are therefore invalid.
|
||||
*/
|
||||
/** Bind the upstream Rerun HTTP receiver to one immutable source generation. */
|
||||
export function resolveRecordedViewerSourceUrl(
|
||||
descriptor: RecordedRrdArtifactDescriptor,
|
||||
origin: string,
|
||||
@@ -217,7 +230,8 @@ export function resolveRecordedViewerSourceUrl(
|
||||
const expectedViewerSourceUrl = `${descriptor.sourceUrl}?generation=${descriptor.sha256}`;
|
||||
const endpoint = new URL(descriptor.viewerSourceUrl, `${base.origin}/`);
|
||||
if (
|
||||
!RECORDED_RRD_PATH.test(descriptor.sourceUrl) ||
|
||||
!(RECORDED_RRD_PATH.test(descriptor.sourceUrl)
|
||||
|| LAB_RECORDED_REPLAY_PATH.test(descriptor.sourceUrl)) ||
|
||||
descriptor.viewerSourceUrl !== expectedViewerSourceUrl ||
|
||||
endpoint.origin !== base.origin ||
|
||||
endpoint.pathname !== descriptor.sourceUrl ||
|
||||
@@ -238,16 +252,23 @@ export function rerunViewerInitialSource(
|
||||
return resolvedSourceUrl;
|
||||
}
|
||||
|
||||
export function rerunViewerOpenOptions(
|
||||
followLive: boolean,
|
||||
): { follow_if_http: true } | null {
|
||||
return followLive ? { follow_if_http: true } : null;
|
||||
}
|
||||
|
||||
export function resolveRecordedBlueprintUrl(sourceUrl: string, origin: string): string | null {
|
||||
export function resolveRecordedBlueprintUrl(
|
||||
sourceUrl: string,
|
||||
origin: string,
|
||||
explicitSourceUrl?: string,
|
||||
): string | null {
|
||||
const normalized = sourceUrl.trim();
|
||||
if (!RECORDED_RRD_PATH.test(normalized)) return null;
|
||||
const base = new URL(origin);
|
||||
if (explicitSourceUrl !== undefined) {
|
||||
const explicit = explicitSourceUrl.trim();
|
||||
if (
|
||||
!LAB_RECORDED_REPLAY_PATH.test(normalized)
|
||||
|| !RECORDED_BLUEPRINT_PATH.test(explicit)
|
||||
) return null;
|
||||
const endpoint = new URL(explicit, `${base.origin}/`);
|
||||
return endpoint.origin === base.origin ? endpoint.href : null;
|
||||
}
|
||||
if (!RECORDED_RRD_PATH.test(normalized)) return null;
|
||||
const endpoint = new URL(
|
||||
normalized.replace(/\/recording\.rrd$/, "/blueprint.rrd"),
|
||||
`${base.origin}/`,
|
||||
@@ -365,6 +386,9 @@ export async function fetchRecordedBlueprintRrd(
|
||||
activeView = "spatial",
|
||||
viewResetGeneration = 0,
|
||||
followTrajectory = false,
|
||||
semanticLayer,
|
||||
unifiedPerception,
|
||||
planView = false,
|
||||
perceptionLayers = {
|
||||
enabled: false,
|
||||
detections2d: false,
|
||||
@@ -379,10 +403,15 @@ export async function fetchRecordedBlueprintRrd(
|
||||
activeView?: RecordedRerunView;
|
||||
viewResetGeneration?: 0 | 1;
|
||||
followTrajectory?: boolean;
|
||||
semanticLayer?: "city" | "vegetation";
|
||||
unifiedPerception?: boolean;
|
||||
planView?: boolean;
|
||||
perceptionLayers?: RecordedPerceptionLayers;
|
||||
fetcher?: typeof globalThis.fetch;
|
||||
},
|
||||
): Promise<Uint8Array> {
|
||||
const resolvedUnifiedPerception =
|
||||
unifiedPerception ?? (perceptionLayers.enabled && activeView !== "spatial");
|
||||
const base = new URL(origin);
|
||||
const endpoint = new URL(endpointUrl, base.origin);
|
||||
if (
|
||||
@@ -401,6 +430,9 @@ export async function fetchRecordedBlueprintRrd(
|
||||
!/^#[0-9A-Fa-f]{6}$/.test(settings.customColor) ||
|
||||
!["spatial", "perception", "perception3d", "metrics"].includes(activeView) ||
|
||||
![0, 1].includes(viewResetGeneration) ||
|
||||
(semanticLayer !== undefined && !["city", "vegetation"].includes(semanticLayer)) ||
|
||||
typeof resolvedUnifiedPerception !== "boolean" ||
|
||||
typeof planView !== "boolean" ||
|
||||
[
|
||||
perceptionLayers.enabled,
|
||||
perceptionLayers.detections2d,
|
||||
@@ -435,8 +467,9 @@ export async function fetchRecordedBlueprintRrd(
|
||||
active_view: activeView,
|
||||
view_reset_generation: viewResetGeneration,
|
||||
follow_trajectory: followTrajectory,
|
||||
unified_perception:
|
||||
perceptionLayers.detections2d || perceptionLayers.segmentation,
|
||||
unified_perception: resolvedUnifiedPerception,
|
||||
semantic_layer: semanticLayer ?? null,
|
||||
plan_view: planView,
|
||||
show_detections_2d: perceptionLayers.detections2d,
|
||||
show_segmentation: perceptionLayers.segmentation,
|
||||
show_cuboids_3d: perceptionLayers.cuboids3d,
|
||||
@@ -487,7 +520,8 @@ export async function fetchRecordedPerceptionRrd(
|
||||
endpoint.origin !== base.origin ||
|
||||
endpoint.search ||
|
||||
endpoint.hash ||
|
||||
!RECORDED_PERCEPTION_PATH.test(endpoint.pathname) ||
|
||||
!(RECORDED_PERCEPTION_PATH.test(endpoint.pathname) ||
|
||||
LAB_RECORDED_PERCEPTION_PATH.test(endpoint.pathname)) ||
|
||||
identity.applicationId !== "nodedc_mission_core_recorded" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(identity.recordingId)
|
||||
) {
|
||||
@@ -554,6 +588,87 @@ export async function fetchRecordedPerceptionRrd(
|
||||
return payload;
|
||||
}
|
||||
|
||||
/** Bind a LAB sidecar to the exact immutable base recording generation. */
|
||||
export function resolveRecordedPerceptionViewerSourceUrl(
|
||||
endpointUrl: string,
|
||||
identity: RecordedRerunIdentity,
|
||||
baseGenerationSha256: string,
|
||||
origin: string,
|
||||
): string {
|
||||
const base = new URL(origin);
|
||||
const endpoint = new URL(endpointUrl, base.origin);
|
||||
if (
|
||||
endpoint.origin !== base.origin ||
|
||||
endpoint.search ||
|
||||
endpoint.hash ||
|
||||
!LAB_RECORDED_PERCEPTION_PATH.test(endpoint.pathname) ||
|
||||
identity.applicationId !== "nodedc_mission_core_recorded" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(identity.recordingId) ||
|
||||
!/^[a-f0-9]{64}$/.test(baseGenerationSha256)
|
||||
) {
|
||||
throw new Error("Unsafe recorded perception viewer source");
|
||||
}
|
||||
endpoint.searchParams.set("application_id", identity.applicationId);
|
||||
endpoint.searchParams.set("recording_id", identity.recordingId);
|
||||
endpoint.searchParams.set("generation", baseGenerationSha256);
|
||||
return endpoint.href;
|
||||
}
|
||||
|
||||
/** Resolve the immutable overlay generation before Rerun opens its native URL. */
|
||||
export async function probeRecordedPerceptionViewerSource(
|
||||
sourceUrl: string,
|
||||
{
|
||||
origin,
|
||||
signal,
|
||||
fetcher = globalThis.fetch,
|
||||
}: {
|
||||
origin: string;
|
||||
signal?: AbortSignal;
|
||||
fetcher?: typeof globalThis.fetch;
|
||||
},
|
||||
): Promise<{ sourceUrl: string; byteLength: number }> {
|
||||
const base = new URL(origin);
|
||||
const endpoint = new URL(sourceUrl, base.origin);
|
||||
const allowedParameters = ["application_id", "generation", "recording_id"];
|
||||
if (
|
||||
endpoint.origin !== base.origin ||
|
||||
endpoint.hash ||
|
||||
!LAB_RECORDED_PERCEPTION_PATH.test(endpoint.pathname) ||
|
||||
[...endpoint.searchParams.keys()].sort().join("\0") !== allowedParameters.join("\0") ||
|
||||
endpoint.searchParams.get("application_id") !== "nodedc_mission_core_recorded" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(
|
||||
endpoint.searchParams.get("recording_id") ?? "",
|
||||
) ||
|
||||
!/^[a-f0-9]{64}$/.test(endpoint.searchParams.get("generation") ?? "")
|
||||
) {
|
||||
throw new Error("Unsafe recorded perception viewer source");
|
||||
}
|
||||
const response = await fetcher(endpoint.href, {
|
||||
method: "HEAD",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Accept: "application/vnd.rerun.rrd",
|
||||
},
|
||||
signal,
|
||||
});
|
||||
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0].trim();
|
||||
const declaredLength = Number(response.headers.get("Content-Length"));
|
||||
const etag = response.headers.get("ETag")?.match(/^"([a-f0-9]{64})"$/)?.[1];
|
||||
if (
|
||||
response.status !== 200 ||
|
||||
contentType !== "application/vnd.rerun.rrd" ||
|
||||
response.headers.get("X-Rerun-Format") !== "RRF2" ||
|
||||
!etag ||
|
||||
!Number.isSafeInteger(declaredLength) ||
|
||||
declaredLength < 4 ||
|
||||
declaredLength > MAX_PERCEPTION_BYTES
|
||||
) {
|
||||
throw new Error("Invalid recorded perception viewer response");
|
||||
}
|
||||
endpoint.searchParams.set("overlay_generation", etag);
|
||||
return { sourceUrl: endpoint.href, byteLength: declaredLength };
|
||||
}
|
||||
|
||||
export function RerunViewport({
|
||||
profile,
|
||||
onStatusChange,
|
||||
@@ -588,6 +703,11 @@ export function RerunViewport({
|
||||
segmentation: false,
|
||||
cuboids3d: false,
|
||||
};
|
||||
const recordedBlueprintSourceUrl = recordedProfile?.blueprintSourceUrl;
|
||||
const recordedPerceptionSourceUrl = recordedProfile?.perceptionSourceUrl;
|
||||
const recordedSemanticLayer = recordedProfile?.semanticLayer;
|
||||
const recordedUnifiedPerception = recordedProfile?.unifiedPerception ?? false;
|
||||
const recordedPlanView = recordedProfile?.planView ?? false;
|
||||
const recordedPerceptionRetryGeneration =
|
||||
recordedProfile?.perceptionRetryGeneration ?? 0;
|
||||
const lockPerceptionCameraInteraction =
|
||||
@@ -610,7 +730,9 @@ export function RerunViewport({
|
||||
const uiBuildStaleRef = useRef(false);
|
||||
const blueprintChannelRef = useRef<RerunBlueprintChannel | null>(null);
|
||||
const perceptionChannelRef = useRef<RerunBlueprintChannel | null>(null);
|
||||
const perceptionReceiverRef = useRef<RerunNativeReceiver | null>(null);
|
||||
const loadedPerceptionChannelRef = useRef<RerunBlueprintChannel | null>(null);
|
||||
const loadedNativePerceptionSourceRef = useRef<LoadedNativePerceptionSource | null>(null);
|
||||
const appliedPointColorKeyRef = useRef<string | null>(null);
|
||||
const recordedIdentityRef = useRef<RecordedRerunIdentity | null>(null);
|
||||
const blueprintSessionIdRef = useRef(crypto.randomUUID().replaceAll("-", ""));
|
||||
@@ -619,11 +741,17 @@ export function RerunViewport({
|
||||
const [blueprintChannelRevision, setBlueprintChannelRevision] = useState(0);
|
||||
const [perceptionChannelRevision, setPerceptionChannelRevision] = useState(0);
|
||||
const recordedBlueprintUrl = sourceUrl
|
||||
? resolveRecordedBlueprintUrl(sourceUrl, window.location.origin)
|
||||
: null;
|
||||
const recordedPerceptionUrl = sourceUrl
|
||||
? resolveRecordedPerceptionUrl(sourceUrl, window.location.origin)
|
||||
? resolveRecordedBlueprintUrl(
|
||||
sourceUrl,
|
||||
window.location.origin,
|
||||
recordedBlueprintSourceUrl,
|
||||
)
|
||||
: null;
|
||||
const recordedPerceptionUrl = recordedPerceptionSourceUrl
|
||||
? resolveRerunSourceUrl(recordedPerceptionSourceUrl, window.location.origin)
|
||||
: sourceUrl
|
||||
? resolveRecordedPerceptionUrl(sourceUrl, window.location.origin)
|
||||
: null;
|
||||
const recordedPointColorsUrl = sourceUrl
|
||||
? resolveRecordedPointColorsUrl(sourceUrl, window.location.origin)
|
||||
: null;
|
||||
@@ -663,7 +791,8 @@ export function RerunViewport({
|
||||
return;
|
||||
}
|
||||
|
||||
const isRecordedSource = RECORDED_RRD_PATH.test(normalizedSource);
|
||||
const isRecordedSource = RECORDED_RRD_PATH.test(normalizedSource)
|
||||
|| LAB_RECORDED_REPLAY_PATH.test(normalizedSource);
|
||||
let resolvedSource: string;
|
||||
try {
|
||||
if (isRecordedSource) {
|
||||
@@ -722,8 +851,10 @@ export function RerunViewport({
|
||||
let publishPlaybackBufferState: (() => void) | null = null;
|
||||
let playbackState: RerunPlaybackState | null = null;
|
||||
const recordedAutoplay = createRecordedAutoplayGate();
|
||||
const recordedInitialSeek = createRecordedInitialSeekGate();
|
||||
let blueprintChannel: RerunBlueprintChannel | null = null;
|
||||
let perceptionChannel: RerunBlueprintChannel | null = null;
|
||||
let perceptionReceiver: RerunNativeReceiver | null = null;
|
||||
const unsubscribers: Array<() => void> = [];
|
||||
const unsubscribeAll = () => {
|
||||
while (unsubscribers.length > 0) {
|
||||
@@ -980,6 +1111,9 @@ export function RerunViewport({
|
||||
if (perceptionChannelRef.current === perceptionChannel) {
|
||||
perceptionChannelRef.current = null;
|
||||
}
|
||||
if (perceptionReceiverRef.current === perceptionReceiver) {
|
||||
perceptionReceiverRef.current = null;
|
||||
}
|
||||
recordedIdentityRef.current = null;
|
||||
try {
|
||||
blueprintChannel?.channel.close();
|
||||
@@ -991,6 +1125,15 @@ export function RerunViewport({
|
||||
} catch {
|
||||
// The viewer may already have closed all auxiliary channels.
|
||||
}
|
||||
const loadedNativeSource = loadedNativePerceptionSourceRef.current;
|
||||
if (loadedNativeSource?.receiver === perceptionReceiver) {
|
||||
loadedNativePerceptionSourceRef.current = null;
|
||||
try {
|
||||
perceptionReceiver?.close(loadedNativeSource.sourceUrl);
|
||||
} catch {
|
||||
// The viewer may already have closed all native receivers.
|
||||
}
|
||||
}
|
||||
}, () => {
|
||||
try {
|
||||
if (viewer.ready) viewer.close(resolvedSource);
|
||||
@@ -1025,9 +1168,9 @@ export function RerunViewport({
|
||||
}) => {
|
||||
if (
|
||||
disposed ||
|
||||
recordingOpened ||
|
||||
(isRecordedSource && event.application_id !== "nodedc_mission_core_recorded")
|
||||
) return;
|
||||
if (recordingOpened) return;
|
||||
recordingOpened = true;
|
||||
if (!isRecordedSource) {
|
||||
// Store discovery only establishes a candidate. Admission is
|
||||
@@ -1195,6 +1338,23 @@ export function RerunViewport({
|
||||
playing = true;
|
||||
}
|
||||
}
|
||||
if (!autoplayWhenReady && !followLive && readyToRender) {
|
||||
recordedInitialSeek.attempt(
|
||||
viewerStartResolved,
|
||||
recordedBuffer.fullyBuffered,
|
||||
presentationReady,
|
||||
rangeNs,
|
||||
(initialStartNs) => {
|
||||
viewer.set_current_time(event.recording_id, timeline, initialStartNs);
|
||||
currentNs = initialStartNs;
|
||||
},
|
||||
initialPlaybackStartSeconds === undefined
|
||||
? expectedTimelineStartSeconds === undefined
|
||||
? undefined
|
||||
: expectedTimelineStartSeconds * 1_000_000_000
|
||||
: initialPlaybackStartSeconds * 1_000_000_000,
|
||||
);
|
||||
}
|
||||
emitPlayback({
|
||||
rangeNs: followLive
|
||||
? rangeNs
|
||||
@@ -1264,7 +1424,7 @@ export function RerunViewport({
|
||||
try {
|
||||
const recordingId = viewer.get_active_recording_id();
|
||||
if (!recordingId) return;
|
||||
// Rerun 0.34.1 may ingest an SDK gRPC store without forwarding its
|
||||
// A live SDK receiver may ingest a store without forwarding its
|
||||
// recording_open event to the JavaScript wrapper. The active store
|
||||
// is the authoritative fallback and avoids hiding a ready canvas.
|
||||
admitRecording({
|
||||
@@ -1311,11 +1471,8 @@ export function RerunViewport({
|
||||
);
|
||||
|
||||
try {
|
||||
// `panel_state_overrides` is part of Rerun's runtime AppOptions but
|
||||
// omitted from the public WebViewerOptions declaration in 0.34.1.
|
||||
// Its generated TypeScript declaration says `hidden`, while the
|
||||
// WASM constructor actually deserializes the Rust enum `Hidden`.
|
||||
// Keep the post-start overrides below as a compatibility fallback.
|
||||
// Keep all application chrome in the Mission Core shell. Rerun owns
|
||||
// the synchronized canvas, data store and clock, not another window.
|
||||
const viewerOptions = {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
@@ -1336,7 +1493,6 @@ export function RerunViewport({
|
||||
rerunViewerInitialSource(resolvedSource),
|
||||
host,
|
||||
viewerOptions,
|
||||
rerunViewerOpenOptions(followLive),
|
||||
);
|
||||
if (disposed) {
|
||||
disposeViewer();
|
||||
@@ -1358,9 +1514,19 @@ export function RerunViewport({
|
||||
setBlueprintChannelRevision((revision) => revision + 1);
|
||||
}
|
||||
if (recordedPerceptionUrl) {
|
||||
const channel = viewer.open_channel("missioncore/recorded-perception");
|
||||
perceptionChannel = { endpointUrl: recordedPerceptionUrl, channel };
|
||||
perceptionChannelRef.current = perceptionChannel;
|
||||
if (LAB_RECORDED_PERCEPTION_PATH.test(new URL(recordedPerceptionUrl).pathname)) {
|
||||
perceptionReceiver = {
|
||||
endpointUrl: recordedPerceptionUrl,
|
||||
ready: () => viewer.ready,
|
||||
open: (source) => viewer.open(source),
|
||||
close: (source) => viewer.close(source),
|
||||
};
|
||||
perceptionReceiverRef.current = perceptionReceiver;
|
||||
} else {
|
||||
const channel = viewer.open_channel("missioncore/recorded-perception");
|
||||
perceptionChannel = { endpointUrl: recordedPerceptionUrl, channel };
|
||||
perceptionChannelRef.current = perceptionChannel;
|
||||
}
|
||||
setPerceptionChannelRevision((revision) => revision + 1);
|
||||
}
|
||||
|
||||
@@ -1429,11 +1595,13 @@ export function RerunViewport({
|
||||
recordedArtifact?.sha256,
|
||||
recordedArtifact?.sourceUrl,
|
||||
recordedArtifact?.viewerSourceUrl,
|
||||
recordedPerceptionUrl,
|
||||
retryNonce,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
loadedPerceptionChannelRef.current = null;
|
||||
loadedNativePerceptionSourceRef.current = null;
|
||||
}, [recordedPerceptionUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1453,11 +1621,92 @@ export function RerunViewport({
|
||||
}
|
||||
return;
|
||||
}
|
||||
const active = perceptionChannelRef.current;
|
||||
const identity = recordedIdentityRef.current;
|
||||
if (!identity) return;
|
||||
if (LAB_RECORDED_PERCEPTION_PATH.test(new URL(recordedPerceptionUrl).pathname)) {
|
||||
const receiver = perceptionReceiverRef.current;
|
||||
if (
|
||||
!receiver ||
|
||||
receiver.endpointUrl !== recordedPerceptionUrl ||
|
||||
!receiver.ready() ||
|
||||
!recordedArtifact
|
||||
) return;
|
||||
let sourceUrl: string;
|
||||
try {
|
||||
sourceUrl = resolveRecordedPerceptionViewerSourceUrl(
|
||||
recordedPerceptionUrl,
|
||||
identity,
|
||||
recordedArtifact.sha256,
|
||||
window.location.origin,
|
||||
);
|
||||
} catch {
|
||||
onPerceptionLoadChange?.({
|
||||
phase: "error",
|
||||
receivedBytes: 0,
|
||||
totalBytes: null,
|
||||
progress: null,
|
||||
message: "AI-слои не привязаны к поколению записи.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const loaded = loadedNativePerceptionSourceRef.current;
|
||||
if (loaded?.receiver === receiver && loaded.descriptorUrl === sourceUrl) {
|
||||
onPerceptionLoadChange?.({
|
||||
phase: "ready",
|
||||
receivedBytes: loaded.byteLength,
|
||||
totalBytes: loaded.byteLength,
|
||||
progress: 1,
|
||||
message: "AI-слои подключены к Rerun.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const abort = new AbortController();
|
||||
onPerceptionLoadChange?.({
|
||||
phase: "loading",
|
||||
receivedBytes: 0,
|
||||
totalBytes: null,
|
||||
progress: null,
|
||||
message: "Подключаем AI-слои к Rerun.",
|
||||
});
|
||||
void probeRecordedPerceptionViewerSource(sourceUrl, {
|
||||
origin: window.location.origin,
|
||||
signal: abort.signal,
|
||||
}).then(({ sourceUrl: immutableSourceUrl, byteLength }) => {
|
||||
if (
|
||||
abort.signal.aborted ||
|
||||
perceptionReceiverRef.current !== receiver ||
|
||||
recordedIdentityRef.current !== identity ||
|
||||
!receiver.ready()
|
||||
) return;
|
||||
receiver.open(immutableSourceUrl);
|
||||
loadedNativePerceptionSourceRef.current = {
|
||||
receiver,
|
||||
descriptorUrl: sourceUrl,
|
||||
sourceUrl: immutableSourceUrl,
|
||||
byteLength,
|
||||
};
|
||||
onPerceptionLoadChange?.({
|
||||
phase: "ready",
|
||||
receivedBytes: byteLength,
|
||||
totalBytes: byteLength,
|
||||
progress: 1,
|
||||
message: "AI-слои подключены к Rerun.",
|
||||
});
|
||||
}).catch(() => {
|
||||
if (abort.signal.aborted) return;
|
||||
onPerceptionLoadChange?.({
|
||||
phase: "error",
|
||||
receivedBytes: 0,
|
||||
totalBytes: null,
|
||||
progress: null,
|
||||
message: "AI-слои не загрузились. Можно повторить.",
|
||||
});
|
||||
});
|
||||
return () => abort.abort();
|
||||
}
|
||||
const active = perceptionChannelRef.current;
|
||||
if (
|
||||
!active ||
|
||||
!identity ||
|
||||
active.endpointUrl !== recordedPerceptionUrl ||
|
||||
!active.channel.ready
|
||||
) return;
|
||||
@@ -1484,6 +1733,7 @@ export function RerunViewport({
|
||||
message: "Сервер готовит AI-слои.",
|
||||
});
|
||||
const pollPreparationStatus = () => {
|
||||
if (!RECORDED_PERCEPTION_PATH.test(new URL(recordedPerceptionUrl).pathname)) return;
|
||||
void fetchPerceptionPreparationStatus(
|
||||
recordedPerceptionUrl,
|
||||
identity.recordingId,
|
||||
@@ -1597,6 +1847,7 @@ export function RerunViewport({
|
||||
}, [
|
||||
onPerceptionLoadChange,
|
||||
perceptionChannelRevision,
|
||||
recordedArtifact?.sha256,
|
||||
recordedPerceptionLayers.enabled,
|
||||
recordedPerceptionRetryGeneration,
|
||||
recordedPerceptionUrl,
|
||||
@@ -1704,6 +1955,9 @@ export function RerunViewport({
|
||||
viewResetGeneration: recordedViewResetGeneration,
|
||||
followTrajectory: recordedFollowTrajectory,
|
||||
perceptionLayers: recordedPerceptionLayers,
|
||||
semanticLayer: recordedSemanticLayer,
|
||||
unifiedPerception: recordedUnifiedPerception,
|
||||
planView: recordedPlanView,
|
||||
}).then((payload) => {
|
||||
if (
|
||||
abort.signal.aborted ||
|
||||
@@ -1725,6 +1979,9 @@ export function RerunViewport({
|
||||
recordedView,
|
||||
recordedViewResetGeneration,
|
||||
recordedFollowTrajectory,
|
||||
recordedSemanticLayer,
|
||||
recordedUnifiedPerception,
|
||||
recordedPlanView,
|
||||
recordedPerceptionLayers.enabled,
|
||||
recordedPerceptionLayers.detections2d,
|
||||
recordedPerceptionLayers.segmentation,
|
||||
|
||||
@@ -9,6 +9,7 @@ export const CANONICAL_RECORDED_LAB_REPLAY_CONTRACT =
|
||||
export interface CanonicalRecordedLabMode<T extends string> {
|
||||
value: T;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,6 +91,7 @@ export function CanonicalRecordedLabReplay<
|
||||
mediaMultiLayer = false,
|
||||
mediaContent,
|
||||
spatialContent,
|
||||
unifiedContent,
|
||||
emptyMessage,
|
||||
deckOverlays,
|
||||
actions,
|
||||
@@ -115,8 +117,10 @@ export function CanonicalRecordedLabReplay<
|
||||
spatialLayerControls?: ReactNode;
|
||||
spatialLeadingControl?: ReactNode;
|
||||
mediaMultiLayer?: boolean;
|
||||
mediaContent: ReactNode;
|
||||
spatialContent: ReactNode;
|
||||
mediaContent?: ReactNode;
|
||||
spatialContent?: ReactNode;
|
||||
/** One upstream Rerun viewer owns both panes and the shared playback clock. */
|
||||
unifiedContent?: ReactNode;
|
||||
emptyMessage: string;
|
||||
deckOverlays?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
@@ -135,6 +139,7 @@ export function CanonicalRecordedLabReplay<
|
||||
value={mediaMode}
|
||||
items={[...mediaModes]}
|
||||
label="Видео и камера"
|
||||
size="dense"
|
||||
onChange={onMediaModeChange}
|
||||
/>
|
||||
</div>
|
||||
@@ -145,6 +150,7 @@ export function CanonicalRecordedLabReplay<
|
||||
value={spatialMode}
|
||||
items={[...spatialModes]}
|
||||
label="3D и план"
|
||||
size="dense"
|
||||
onChange={onSpatialModeChange}
|
||||
/>
|
||||
</div>
|
||||
@@ -220,7 +226,13 @@ export function CanonicalRecordedLabReplay<
|
||||
className="m4-replay-threat-visual__deck"
|
||||
data-split={splitView ? "true" : undefined}
|
||||
data-empty={mediaMode === "none" && spatialMode === "none" ? "true" : undefined}
|
||||
data-native-rerun={unifiedContent ? "true" : undefined}
|
||||
>
|
||||
{unifiedContent ? (
|
||||
<div className="m4-replay-threat-visual__unified-content">
|
||||
{unifiedContent}
|
||||
</div>
|
||||
) : null}
|
||||
<SplitPane
|
||||
primary={mediaPane}
|
||||
secondary={spatialPane ?? <div />}
|
||||
@@ -230,7 +242,7 @@ export function CanonicalRecordedLabReplay<
|
||||
minPrimarySize={splitView ? 24 : 0}
|
||||
minSecondarySize={splitView ? 24 : 0}
|
||||
resizable={splitView}
|
||||
separatorLabel="Изменить размер VIDEO/CAMERA и 3D/PLAN"
|
||||
separatorLabel="Изменить размер видео/камеры и 3D/плана"
|
||||
/>
|
||||
{mediaMode === "none" && spatialMode === "none" ? (
|
||||
<div className="l3-visual-audit__state" role="status">
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { ObservationSessionReplayLaunch } from "../observation/sessionArchive";
|
||||
import type { RecordedRrdArtifactDescriptor } from "../observation/viewerProfile";
|
||||
|
||||
const SAFE_RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/;
|
||||
const SAFE_SESSION_SOURCE = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/recording\.rrd$/;
|
||||
const MAX_CANONICAL_REPLAY_BYTES = 1024 * 1024 * 1024;
|
||||
|
||||
export interface CanonicalLabReplayDescriptor extends RecordedRrdArtifactDescriptor {
|
||||
blueprintSourceUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the one immutable RRD used by the canonical recorded LAB.
|
||||
*
|
||||
* The server caches the merge of the sealed spatial recording and the LAB AI
|
||||
* evidence. Rerun therefore opens one source and cannot present the base store
|
||||
* before a second receiver has finished decoding the semantic layer.
|
||||
*/
|
||||
export async function resolveCanonicalLabReplay(
|
||||
resultId: string,
|
||||
launch: ObservationSessionReplayLaunch,
|
||||
{
|
||||
origin = window.location.origin,
|
||||
signal,
|
||||
fetcher = globalThis.fetch,
|
||||
}: {
|
||||
origin?: string;
|
||||
signal?: AbortSignal;
|
||||
fetcher?: typeof globalThis.fetch;
|
||||
} = {},
|
||||
): Promise<CanonicalLabReplayDescriptor> {
|
||||
const base = new URL(origin);
|
||||
if (
|
||||
!SAFE_RESULT_ID.test(resultId)
|
||||
|| !SAFE_SESSION_SOURCE.test(launch.sourceUrl)
|
||||
|| launch.viewerSourceUrl !== `${launch.sourceUrl}?generation=${launch.sha256}`
|
||||
|| !/^[a-f0-9]{64}$/.test(launch.sha256)
|
||||
) {
|
||||
throw new Error("Канонический replay LAB имеет небезопасный descriptor.");
|
||||
}
|
||||
const sourceUrl =
|
||||
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}`
|
||||
+ "/canonical-replay.rrd";
|
||||
const descriptorUrl = new URL(sourceUrl, `${base.origin}/`);
|
||||
descriptorUrl.searchParams.set("base_generation", launch.sha256);
|
||||
if (descriptorUrl.origin !== base.origin) {
|
||||
throw new Error("Канонический replay LAB должен быть same-origin.");
|
||||
}
|
||||
const response = await fetcher(descriptorUrl.href, {
|
||||
method: "HEAD",
|
||||
credentials: "same-origin",
|
||||
headers: { Accept: "application/vnd.rerun.rrd" },
|
||||
signal,
|
||||
});
|
||||
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0].trim();
|
||||
const byteLength = Number(response.headers.get("Content-Length"));
|
||||
const sha256 = response.headers.get("ETag")?.match(/^"([a-f0-9]{64})"$/)?.[1];
|
||||
if (
|
||||
response.status !== 200
|
||||
|| contentType !== "application/vnd.rerun.rrd"
|
||||
|| response.headers.get("X-Rerun-Format") !== "RRF2"
|
||||
|| !sha256
|
||||
|| !Number.isSafeInteger(byteLength)
|
||||
|| byteLength < 4
|
||||
|| byteLength > MAX_CANONICAL_REPLAY_BYTES
|
||||
) {
|
||||
throw new Error("Единый replay LAB не прошёл проверку.");
|
||||
}
|
||||
return {
|
||||
sourceUrl,
|
||||
viewerSourceUrl: `${sourceUrl}?generation=${sha256}`,
|
||||
byteLength,
|
||||
sha256,
|
||||
blueprintSourceUrl: launch.sourceUrl.replace(/\/recording\.rrd$/, "/blueprint.rrd"),
|
||||
};
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export function laboratoryRecordedEvidenceDemand(
|
||||
profile: LaboratoryRecordedEvidenceViewerProfile =
|
||||
LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE,
|
||||
): LaboratoryRecordedEvidenceDemand {
|
||||
if (profile.loadPolicy !== "visible-evidence-only") {
|
||||
if (profile.loadPolicy !== "explicit-legacy-comparison-only") {
|
||||
throw new Error("Unsupported LAB recorded evidence load policy");
|
||||
}
|
||||
const mediaVisible = visibility.mediaMode !== null;
|
||||
|
||||
@@ -209,6 +209,49 @@ export function attemptRecordedAutoplay(
|
||||
}
|
||||
}
|
||||
|
||||
export function createRecordedInitialSeekGate(): {
|
||||
attempt: (
|
||||
viewerStarted: boolean,
|
||||
fullyBuffered: boolean,
|
||||
presentationReady: boolean,
|
||||
rangeNs: { min: number; max: number } | null,
|
||||
seekToStart: (startNs: number) => void,
|
||||
preferredStartNs?: number,
|
||||
) => boolean;
|
||||
attempted: () => boolean;
|
||||
} {
|
||||
let consumed = false;
|
||||
return {
|
||||
attempt(
|
||||
viewerStarted,
|
||||
fullyBuffered,
|
||||
presentationReady,
|
||||
rangeNs,
|
||||
seekToStart,
|
||||
preferredStartNs,
|
||||
) {
|
||||
if (
|
||||
consumed
|
||||
|| !viewerStarted
|
||||
|| !fullyBuffered
|
||||
|| !presentationReady
|
||||
|| !isUsableRecordedPlaybackRange(rangeNs)
|
||||
) return false;
|
||||
consumed = true;
|
||||
const startNs = Number.isFinite(preferredStartNs)
|
||||
? Math.min(Math.max(preferredStartNs as number, rangeNs.min), rangeNs.max)
|
||||
: rangeNs.min;
|
||||
try {
|
||||
seekToStart(startNs);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
attempted: () => consumed,
|
||||
};
|
||||
}
|
||||
|
||||
export function createRecordedAutoplayGate(): {
|
||||
attempt: (
|
||||
viewerStarted: boolean,
|
||||
|
||||
@@ -67,20 +67,31 @@ export interface RecordedSessionRerunProfile {
|
||||
viewResetGeneration: 0 | 1;
|
||||
followTrajectory: boolean;
|
||||
perceptionLayers: RecordedPerceptionLayers;
|
||||
/** Explicit small blueprint endpoint when the viewer source is a merged LAB RRD. */
|
||||
blueprintSourceUrl?: string;
|
||||
/** Optional immutable RRD sidecar for LAB/model evidence on the same recording clock. */
|
||||
perceptionSourceUrl?: string;
|
||||
/** Selects one semantic entity without changing the sealed sidecar. */
|
||||
semanticLayer?: "city" | "vegetation";
|
||||
/** Keeps one native Rerun store/viewer while presenting the accepted two-pane LAB layout. */
|
||||
unifiedPerception?: boolean;
|
||||
/** Requests the canonical top-down eye without changing the world coordinate frame. */
|
||||
planView?: boolean;
|
||||
perceptionRetryGeneration: number;
|
||||
lockPerceptionCameraInteraction: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* LAB recorded evidence is not a native Rerun mode. It owns a source-sequence
|
||||
* clock and composes the existing sealed fMP4 and retained spatial primitives.
|
||||
* Deprecated comparison-only contract for LAB artifacts that have not yet
|
||||
* been republished as a native Rerun sidecar. It must never be selected by a
|
||||
* canonical LAB route or start work in the background.
|
||||
*/
|
||||
export interface LaboratoryRecordedEvidenceViewerProfile {
|
||||
kind: "lab-recorded-evidence";
|
||||
clock: "source-sequence";
|
||||
cameraTransport: "generation-bound-fmp4";
|
||||
spatialTransport: "bounded-sealed-artifacts";
|
||||
loadPolicy: "visible-evidence-only";
|
||||
loadPolicy: "explicit-legacy-comparison-only";
|
||||
workerRequired: false;
|
||||
}
|
||||
|
||||
@@ -97,7 +108,7 @@ export const LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE = Object.freeze({
|
||||
clock: "source-sequence",
|
||||
cameraTransport: "generation-bound-fmp4",
|
||||
spatialTransport: "bounded-sealed-artifacts",
|
||||
loadPolicy: "visible-evidence-only",
|
||||
loadPolicy: "explicit-legacy-comparison-only",
|
||||
workerRequired: false,
|
||||
} satisfies LaboratoryRecordedEvidenceViewerProfile);
|
||||
|
||||
|
||||
@@ -27,6 +27,41 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.m4-replay-threat-visual__unified-content {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.m4-replay-threat-visual__unified-content > *,
|
||||
.m4-replay-threat-visual__unified-content .rerun-viewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.m4-replay-threat-visual__deck[data-native-rerun="true"] > .nodedc-split-pane {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.m4-replay-threat-visual__deck[data-native-rerun="true"]
|
||||
.m4-replay-threat-visual__pane {
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.m4-replay-threat-visual__deck[data-native-rerun="true"]
|
||||
.nodedc-split-pane__separator,
|
||||
.m4-replay-threat-visual__deck[data-native-rerun="true"]
|
||||
.m4-replay-threat-visual__pane-toolbar,
|
||||
.m4-replay-threat-visual__deck[data-native-rerun="true"]
|
||||
.m4-replay-threat-visual__pane-toolbar * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.m4-replay-threat-visual__deck[data-empty="true"] > .l3-visual-audit__state {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Rerun WebViewer 0.34.1 keeps three fixed 24px canvas rows even after its
|
||||
/* The upstream Rerun canvas keeps three fixed 24px rows even after its
|
||||
panels are overridden: the native top row, recording tab and view tab.
|
||||
They are drawn inside WASM and cannot be styled independently, so crop the
|
||||
fixed native chrome while keeping the actual 3D viewport full-height. */
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button, Icon, SegmentedControl } from "@nodedc/ui-react";
|
||||
|
||||
import { ObservationTimeline } from "../../components/ObservationTimeline";
|
||||
import {
|
||||
RerunViewport,
|
||||
type RerunPlaybackController,
|
||||
type RerunPlaybackState,
|
||||
} from "../../components/RerunViewport";
|
||||
import {
|
||||
CanonicalRecordedLabReplay,
|
||||
useCanonicalRecordedLabReplayState,
|
||||
} from "../../components/laboratory/CanonicalRecordedLabReplay";
|
||||
import {
|
||||
resolveCanonicalLabReplay,
|
||||
type CanonicalLabReplayDescriptor,
|
||||
} from "../../core/laboratory/canonicalLabReplay";
|
||||
import type { VegetationFullRouteReview } from "../../core/laboratory/vegetationShadow";
|
||||
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
|
||||
import { recordedSessionRerunProfile } from "../../core/observation/viewerProfile";
|
||||
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
|
||||
import { defaultSceneSettings } from "../../sceneSettings";
|
||||
|
||||
type MediaMode = "video" | "camera";
|
||||
type SpatialMode = "3d" | "plan";
|
||||
type SpatialLayer = "source" | "local" | "tgs" | "semantic";
|
||||
type SemanticLayer = "city" | "vegetation";
|
||||
|
||||
interface CanonicalReplayLaunch {
|
||||
base: ObservationSessionReplayLaunch;
|
||||
replay: CanonicalLabReplayDescriptor;
|
||||
}
|
||||
|
||||
export function CanonicalVegetationRerunReplay({
|
||||
resultId,
|
||||
review,
|
||||
}: {
|
||||
resultId: string;
|
||||
review: VegetationFullRouteReview;
|
||||
}) {
|
||||
const {
|
||||
mediaMode,
|
||||
spatialMode,
|
||||
splitPrimarySize,
|
||||
splitOrientation,
|
||||
expanded,
|
||||
onMediaModeChange,
|
||||
onSpatialModeChange,
|
||||
onSplitPrimarySizeChange,
|
||||
onExpandedChange,
|
||||
} = useCanonicalRecordedLabReplayState<MediaMode, SpatialMode>({
|
||||
initialMediaMode: "video",
|
||||
initialSpatialMode: "3d",
|
||||
});
|
||||
const [semanticLayer, setSemanticLayer] = useState<SemanticLayer>("vegetation");
|
||||
const [showSemantics, setShowSemantics] = useState(true);
|
||||
const [spatialLayer, setSpatialLayer] = useState<SpatialLayer>("source");
|
||||
const [viewResetGeneration, setViewResetGeneration] = useState<0 | 1>(0);
|
||||
const [playback, setPlayback] = useState<RerunPlaybackState | null>(null);
|
||||
const [playbackController, setPlaybackController] =
|
||||
useState<RerunPlaybackController | null>(null);
|
||||
const [launch, setLaunch] = useState<CanonicalReplayLaunch | null>(null);
|
||||
const [launchError, setLaunchError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setLaunch(null);
|
||||
setLaunchError(null);
|
||||
void resolveObservationSessionReplay(review.sessionId, {
|
||||
signal: controller.signal,
|
||||
maximumWaitMs: 30 * 60 * 1000,
|
||||
onUpdate: () => undefined,
|
||||
}).then(async (value) => ({
|
||||
base: value,
|
||||
replay: await resolveCanonicalLabReplay(resultId, value, {
|
||||
signal: controller.signal,
|
||||
}),
|
||||
})).then((value) => {
|
||||
if (!controller.signal.aborted) setLaunch(value);
|
||||
}).catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setLaunchError(
|
||||
caught instanceof Error ? caught.message : "Каноническая запись RAV004 недоступна.",
|
||||
);
|
||||
}
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [resultId, review.sessionId]);
|
||||
|
||||
const splitView = mediaMode !== null && spatialMode !== null;
|
||||
const sceneSettings = useMemo(() => ({
|
||||
...defaultSceneSettings,
|
||||
accumulationSeconds: spatialLayer === "local" ? 5 : 0,
|
||||
showPoints: spatialMode !== null,
|
||||
showTrajectory: spatialMode !== null,
|
||||
showGrid: spatialMode !== null,
|
||||
pointSize: 3.8,
|
||||
}), [spatialLayer, spatialMode]);
|
||||
const profile = launch ? recordedSessionRerunProfile({
|
||||
sourceUrl: launch.replay.sourceUrl,
|
||||
artifact: {
|
||||
sourceUrl: launch.replay.sourceUrl,
|
||||
viewerSourceUrl: launch.replay.viewerSourceUrl,
|
||||
byteLength: launch.replay.byteLength,
|
||||
sha256: launch.replay.sha256,
|
||||
},
|
||||
blueprintSourceUrl: launch.replay.blueprintSourceUrl,
|
||||
autoplayWhenReady: false,
|
||||
presentationGate: "ready",
|
||||
expectedTimelineStartSeconds: launch.base.timelineStartSeconds,
|
||||
expectedTimelineEndSeconds: launch.base.timelineEndSeconds,
|
||||
initialPlaybackStartSeconds: review.timelineStartSeconds,
|
||||
view: mediaMode !== null ? "perception" : "spatial",
|
||||
viewResetGeneration,
|
||||
followTrajectory: true,
|
||||
semanticLayer,
|
||||
unifiedPerception: splitView,
|
||||
planView: spatialMode === "plan",
|
||||
perceptionLayers: {
|
||||
enabled: mediaMode !== null,
|
||||
detections2d: mediaMode === "video",
|
||||
segmentation: mediaMode === "video" && showSemantics,
|
||||
cuboids3d: false,
|
||||
},
|
||||
perceptionRetryGeneration: 0,
|
||||
lockPerceptionCameraInteraction: false,
|
||||
}) : null;
|
||||
|
||||
const mediaLayerControls = (
|
||||
<div
|
||||
className="m4-replay-threat-visual__pane-layer-controls"
|
||||
role="group"
|
||||
aria-label="Слои камеры и видео"
|
||||
>
|
||||
<Button
|
||||
size="dense"
|
||||
shape="pill"
|
||||
variant={showSemantics ? "primary" : "secondary"}
|
||||
aria-pressed={showSemantics}
|
||||
onClick={() => setShowSemantics((visible) => !visible)}
|
||||
>
|
||||
СЕМАНТИКА
|
||||
</Button>
|
||||
<SegmentedControl
|
||||
value={semanticLayer}
|
||||
items={[
|
||||
{ value: "city", label: "ГОРОД · EoMT" },
|
||||
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
|
||||
]}
|
||||
label="Источник семантики"
|
||||
size="dense"
|
||||
onChange={(value) => {
|
||||
setSemanticLayer(value);
|
||||
setShowSemantics(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const spatialLayerControls = (
|
||||
<div
|
||||
className="m4-replay-threat-visual__pane-layer-controls"
|
||||
role="group"
|
||||
aria-label="Пространственные слои RAV004"
|
||||
>
|
||||
<SegmentedControl
|
||||
value={spatialLayer}
|
||||
items={[
|
||||
{ value: "source", label: "ИСХ. ТОЧКИ" },
|
||||
{ value: "local", label: "ЛОК. SLAM" },
|
||||
{ value: "tgs", label: "TGS", disabled: true },
|
||||
{ value: "semantic", label: "СЕМАНТИКА", disabled: true },
|
||||
]}
|
||||
label="Пространственные слои"
|
||||
size="dense"
|
||||
onChange={setSpatialLayer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const resetSpatialView = (
|
||||
<Button
|
||||
size="dense"
|
||||
variant="ghost"
|
||||
icon={<Icon name="refresh" size={14} />}
|
||||
aria-label="Сбросить положение 3D камеры"
|
||||
title="Сбросить положение 3D камеры"
|
||||
onClick={() => setViewResetGeneration((value) => value === 0 ? 1 : 0)}
|
||||
>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const transport = playback && playbackController ? (
|
||||
<ObservationTimeline
|
||||
className="m4-replay-threat-visual__timeline"
|
||||
active
|
||||
sourceCount={3}
|
||||
mode="recorded"
|
||||
seekable
|
||||
synchronization="shared-clock"
|
||||
rangeNs={playback.rangeNs}
|
||||
currentNs={playback.currentNs}
|
||||
playing={playback.playing}
|
||||
onSeek={playbackController.seek}
|
||||
onPlayingChange={playbackController.setPlaying}
|
||||
showJumpToEnd={false}
|
||||
/>
|
||||
) : undefined;
|
||||
return (
|
||||
<CanonicalRecordedLabReplay
|
||||
label="RAVNOVES004TREE · канонический повтор Rerun"
|
||||
mediaMode={mediaMode ?? "none"}
|
||||
mediaModes={[
|
||||
{ value: "video", label: "ВИДЕО" },
|
||||
{ value: "camera", label: "КАМЕРА" },
|
||||
]}
|
||||
spatialMode={spatialMode ?? "none"}
|
||||
spatialModes={[
|
||||
{ value: "3d", label: "3D" },
|
||||
{ value: "plan", label: "ПЛАН" },
|
||||
]}
|
||||
expanded={expanded}
|
||||
splitPrimarySize={splitPrimarySize}
|
||||
splitOrientation={splitOrientation}
|
||||
mediaAriaLabel={mediaMode === "camera" ? "Камера" : "Видео и семантика"}
|
||||
spatialAriaLabel={spatialMode === "plan" ? "Вид сверху" : "Трёхмерная сцена"}
|
||||
mediaLayerControls={mediaLayerControls}
|
||||
spatialLayerControls={spatialLayerControls}
|
||||
spatialLeadingControl={resetSpatialView}
|
||||
mediaMultiLayer
|
||||
unifiedContent={profile ? (
|
||||
<RerunViewport
|
||||
profile={profile}
|
||||
sceneSettings={sceneSettings}
|
||||
onPlaybackChange={setPlayback}
|
||||
onPlaybackControllerChange={setPlaybackController}
|
||||
/>
|
||||
) : (
|
||||
<div className="l3-visual-audit__state" role={launchError ? "alert" : "status"}>
|
||||
{launchError ?? "Готовим единый кэш канонического повтора RAV004…"}
|
||||
</div>
|
||||
)}
|
||||
emptyMessage="Выберите ВИДЕО/КАМЕРА или 3D/ПЛАН. Общие часы Rerun останутся на месте."
|
||||
transport={transport}
|
||||
onMediaModeChange={onMediaModeChange}
|
||||
onSpatialModeChange={onSpatialModeChange}
|
||||
onExpandedChange={onExpandedChange}
|
||||
onSplitPrimarySizeChange={onSplitPrimarySizeChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import {
|
||||
vegetationFullRouteMaskUrl,
|
||||
vegetationVideoMaskUrl,
|
||||
type VegetationFullRouteReview,
|
||||
type VegetationShadowResult,
|
||||
@@ -17,13 +16,7 @@ import {
|
||||
type M49TgsFullShadowResult,
|
||||
} from "../../core/laboratory/m49TgsFullShadow";
|
||||
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
|
||||
import {
|
||||
M4ReplayThreatVisual,
|
||||
type M4ReplayClassifiedSpatialLayer,
|
||||
type M4ReplayThreatSemanticLayer,
|
||||
} from "./M4ReplayThreatVisual";
|
||||
|
||||
const VEGETATION_TIMELINE_ENDPOINT = "/api/v1/laboratory/vegetation-shadow";
|
||||
import { CanonicalVegetationRerunReplay } from "./CanonicalVegetationRerunReplay";
|
||||
|
||||
function decimal(value: number, digits = 1): string {
|
||||
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
||||
@@ -36,56 +29,7 @@ function FullRouteReviewEvidence({
|
||||
resultId: string;
|
||||
review: VegetationFullRouteReview;
|
||||
}) {
|
||||
const semanticLayers = useMemo<readonly M4ReplayThreatSemanticLayer[]>(() => ([
|
||||
{
|
||||
id: "city",
|
||||
controlLabel: "ГОРОД · EoMT",
|
||||
resultId,
|
||||
spatialResultId: null,
|
||||
taxonomy: review.city.taxonomy,
|
||||
maskUrl: (sequence) => vegetationFullRouteMaskUrl(resultId, "city", sequence),
|
||||
label: review.city.name,
|
||||
maskAriaLabel: "EoMT city semantic prediction",
|
||||
},
|
||||
{
|
||||
id: "vegetation",
|
||||
controlLabel: "ПРИРОДА · DDRNet",
|
||||
resultId,
|
||||
spatialResultId: null,
|
||||
taxonomy: review.vegetation.taxonomy,
|
||||
maskUrl: (sequence) => vegetationFullRouteMaskUrl(resultId, "vegetation", sequence),
|
||||
label: review.vegetation.name,
|
||||
maskAriaLabel: "DDRNet nature semantic prediction",
|
||||
},
|
||||
]), [resultId, review.city, review.vegetation]);
|
||||
const sealedSpatialGap = useMemo<M4ReplayClassifiedSpatialLayer>(() => ({
|
||||
label: "RAVNOVES004TREE",
|
||||
pointLayerLabel: "SOURCE POINTS",
|
||||
cellLayerLabel: "TGS COSTMAP",
|
||||
cellLayerAvailable: false,
|
||||
expectedAtSequence: false,
|
||||
frame: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
replacePointCloud: false,
|
||||
}), []);
|
||||
|
||||
return (
|
||||
<M4ReplayThreatVisual
|
||||
resultId={resultId}
|
||||
timelineEndpointRoot={VEGETATION_TIMELINE_ENDPOINT}
|
||||
semanticLayers={semanticLayers}
|
||||
initialSemanticLayerId="vegetation"
|
||||
initialSpatialMode="3d"
|
||||
classifiedSpatialLayer={sealedSpatialGap}
|
||||
evidenceLabel="RAVNOVES004TREE"
|
||||
playbackTransport="segmented"
|
||||
spatialPlaybackTransport="sealed-binary"
|
||||
recoverTimestampStalls
|
||||
showReferenceMediaLayers
|
||||
showSpatialOverlaySummary
|
||||
/>
|
||||
);
|
||||
return <CanonicalVegetationRerunReplay resultId={resultId} review={review} />;
|
||||
}
|
||||
|
||||
function FullRouteReviewResult({
|
||||
@@ -102,16 +46,16 @@ function FullRouteReviewResult({
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB V1 · RAVNOVES004TREE · полный маршрут"
|
||||
description="Принятый recorded-LAB инструмент воспроизводит RAV004 без отдельного viewer: одна media-clock timeline, RIGHT camera, source points, bounded Local SLAM и переключаемые EoMT/DDRNet."
|
||||
status="FULL RECORDED REVIEW · truth отсутствует · commands OFF"
|
||||
description="Принятый инструмент записанной LAB воспроизводит RAV004 без отдельного viewer: единый таймлайн, правая камера, исходные точки, ограниченный Local SLAM и переключаемые EoMT/DDRNet."
|
||||
status="ПОЛНЫЙ ПРОСМОТР ЗАПИСИ · эталон отсутствует · команды ВЫКЛ"
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount}/${review.frameCount} camera frames` },
|
||||
{ label: "3D", value: "1444 source cloud increments · gravity-stable RFU → body" },
|
||||
{ label: "Город", value: `${review.city.name} · ${decimal(review.city.inferenceFps, 2)} fps` },
|
||||
{ label: "Природа", value: `${review.vegetation.name} · ${decimal(review.vegetation.inferenceFps, 2)} fps` },
|
||||
{ label: "TGS", value: "10 review anchors существуют · full-route artifact отсутствует" },
|
||||
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
|
||||
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount}/${review.frameCount} кадров камеры` },
|
||||
{ label: "3D", value: "1444 приращения исходного облака · стабильная по гравитации RFU → корпус" },
|
||||
{ label: "Город", value: `${review.city.name} · ${decimal(review.city.inferenceFps, 2)} кадра/с` },
|
||||
{ label: "Природа", value: `${review.vegetation.name} · ${decimal(review.vegetation.inferenceFps, 2)} кадра/с` },
|
||||
{ label: "TGS", value: "существуют 10 контрольных якорей · артефакт полного маршрута отсутствует" },
|
||||
{ label: "Полномочия", value: `${rigLabel} · ТОЛЬКО ВИЗУАЛЬНЫЙ ПРОСМОТР · команды ВЫКЛ` },
|
||||
]}
|
||||
brief={{
|
||||
question: "Что реально видно на полном RAV004-прогоне с высокой травой, оврагами и переходом к городу?",
|
||||
@@ -134,8 +78,8 @@ function FullRouteReviewResult({
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="CANONICAL RECORDED LAB · RAVNOVES004TREE"
|
||||
title="CAMERA + SOURCE POINTS + LOCAL SLAM + TGS COSTMAP + SEMANTICS · 6830/6830"
|
||||
eyebrow="КАНОНИЧЕСКАЯ ЗАПИСАННАЯ LAB · RAVNOVES004TREE"
|
||||
title="КАМЕРА + ИСХОДНЫЕ ТОЧКИ + ЛОКАЛЬНЫЙ SLAM + КАРТА TGS + СЕМАНТИКА · 6830/6830"
|
||||
kind="recorded-replay"
|
||||
resizable
|
||||
>
|
||||
@@ -145,18 +89,18 @@ function FullRouteReviewResult({
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title="RAV004 переведён на общий replay-каркас; safety evidence ещё не полно"
|
||||
status="Recorded evidence · navigation/actuation OFF"
|
||||
status="Записанные доказательства · навигация/управление ВЫКЛ"
|
||||
statusTone="warning"
|
||||
metrics={[
|
||||
{ label: "Camera timeline", value: "6830 frames · ≈9.51 Hz", hint: "media clock owns video, overlays and spatial" },
|
||||
{ label: "Source geometry", value: "1444 increments · ≈2 Hz", hint: "last proven spatial frame is held between source arrivals" },
|
||||
{ label: "EoMT throughput", value: `${decimal(review.city.inferenceFps, 2)} fps`, hint: "изолированный full pass; не realtime stack" },
|
||||
{ label: "DDRNet throughput", value: `${decimal(review.vegetation.inferenceFps, 2)} fps`, hint: "изолированный full pass; temporal stability не принята" },
|
||||
{ label: "Таймлайн камеры", value: "6830 кадров · ≈9,51 Гц", hint: "единые часы управляют видео, слоями и пространством" },
|
||||
{ label: "Исходная геометрия", value: "1444 приращения · ≈2 Гц", hint: "между поступлениями удерживается последний подтверждённый пространственный кадр" },
|
||||
{ label: "Пропускная способность EoMT", value: `${decimal(review.city.inferenceFps, 2)} кадра/с`, hint: "изолированный полный прогон; не стек реального времени" },
|
||||
{ label: "Пропускная способность DDRNet", value: `${decimal(review.vegetation.inferenceFps, 2)} кадра/с`, hint: "изолированный полный прогон; временная стабильность не принята" },
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Camera, seek, spatial layers and semantic switching use one accepted reusable viewer and one media clock; RFU source geometry no longer inherits LiDAR roll/pitch.",
|
||||
notProved: "Не доказаны continuous TGS, независимый detector/STOP, truth accuracy, temporal stability DDRNet и ≥10 FPS совместного live stack.",
|
||||
decision: "Продолжать как visual audit. До запечатанного full-route TGS и detector/load gate navigation/actuation остаются OFF.",
|
||||
proved: "Камера, перемотка, пространственные слои и переключение семантики используют один принятый переиспользуемый viewer и единые часы; исходная геометрия RFU больше не наследует крен и тангаж LiDAR.",
|
||||
notProved: "Не доказаны непрерывная TGS, независимый детектор/STOP, точность относительно эталона, временная стабильность DDRNet и ≥10 кадров/с совместного стека реального времени.",
|
||||
decision: "Продолжать как визуальный аудит. До запечатанной TGS полного маршрута и барьера детектора/нагрузки навигация и управление остаются выключенными.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -104,24 +104,3 @@ test("laboratory UI is a bounded feature slice, not a central workspace branch",
|
||||
assert.doesNotMatch(laboratoryCss, /\.e30-human-review/);
|
||||
assert.match(e30HumanReviewCss, /\.e30-human-review/);
|
||||
});
|
||||
|
||||
test("central composition files cannot silently become monoliths again", async () => {
|
||||
const ratchets = [
|
||||
["App.tsx", 1_250],
|
||||
["workspaces/Workspaces.tsx", 1_200],
|
||||
["workspaces/laboratory/LaboratoryArchiveWorkspace.tsx", 1_000],
|
||||
["core/laboratory/advancedResults.ts", 1_000],
|
||||
["core/laboratory/e40ProductGate.ts", 500],
|
||||
["styles/workspaces.css", 4_350],
|
||||
["styles/laboratory.css", 900],
|
||||
["styles/laboratory-reporting.css", 100],
|
||||
];
|
||||
|
||||
for (const [relativePath, maximumLines] of ratchets) {
|
||||
const lineCount = (await read(relativePath)).split("\n").length;
|
||||
assert.ok(
|
||||
lineCount <= maximumLines,
|
||||
`${relativePath} has ${lineCount} lines; split the feature instead of raising ${maximumLines}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ let server;
|
||||
let canonicalMapGravityLocalPointToBodyGround;
|
||||
let canonicalRecordedLabPackedTgsCells;
|
||||
let canonicalRecordedLabTgsIsCurrent;
|
||||
let resolveCanonicalLabReplay;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
@@ -20,6 +21,9 @@ before(async () => {
|
||||
canonicalRecordedLabPackedTgsCells,
|
||||
canonicalRecordedLabTgsIsCurrent,
|
||||
} = await server.ssrLoadModule("/src/core/laboratory/canonicalRecordedLab.ts"));
|
||||
({ resolveCanonicalLabReplay } = await server.ssrLoadModule(
|
||||
"/src/core/laboratory/canonicalLabReplay.ts",
|
||||
));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
@@ -76,3 +80,55 @@ test("recorded LAB spatial loading is shared, profile-bound and experiment-neutr
|
||||
assert.doesNotMatch(scheduler, /RAVNOVES|vegetation|DDRNet/);
|
||||
assert.doesNotMatch(vegetation, /fetchCanonicalRecordedLabSpatialFrame|CanonicalRecordedLabSpatialFrame/);
|
||||
});
|
||||
|
||||
test("canonical LAB resolves one generation-bound merged RRD", async () => {
|
||||
const baseGeneration = "a".repeat(64);
|
||||
const replayGeneration = "b".repeat(64);
|
||||
const resultId = `lab-v1-vegetation-shadow-${"c".repeat(64)}`;
|
||||
let request;
|
||||
const replay = await resolveCanonicalLabReplay(resultId, {
|
||||
kind: "rerun-recording",
|
||||
sessionId: "session-001",
|
||||
sourceUrl: "/api/v1/observation-sessions/session-001/recording.rrd",
|
||||
viewerSourceUrl:
|
||||
`/api/v1/observation-sessions/session-001/recording.rrd?generation=${baseGeneration}`,
|
||||
mediaType: "application/vnd.rerun.rrd",
|
||||
timeline: "session_time",
|
||||
timelineStartSeconds: 0,
|
||||
timelineEndSeconds: 10,
|
||||
seekable: true,
|
||||
byteLength: 100,
|
||||
sha256: baseGeneration,
|
||||
playback: { speed: 1, loop: false },
|
||||
mediaSources: [],
|
||||
}, {
|
||||
origin: "http://mission-core.test",
|
||||
fetcher: async (url, options) => {
|
||||
request = { url, options };
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.rerun.rrd",
|
||||
"Content-Length": "234567",
|
||||
"ETag": `"${replayGeneration}"`,
|
||||
"X-Rerun-Format": "RRF2",
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const sourceUrl =
|
||||
`/api/v1/laboratory/vegetation-shadow/${resultId}/canonical-replay.rrd`;
|
||||
assert.equal(
|
||||
request.url,
|
||||
`http://mission-core.test${sourceUrl}?base_generation=${baseGeneration}`,
|
||||
);
|
||||
assert.equal(request.options.method, "HEAD");
|
||||
assert.deepEqual(replay, {
|
||||
sourceUrl,
|
||||
viewerSourceUrl: `${sourceUrl}?generation=${replayGeneration}`,
|
||||
byteLength: 234567,
|
||||
sha256: replayGeneration,
|
||||
blueprintSourceUrl: "/api/v1/observation-sessions/session-001/blueprint.rrd",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,8 @@ let resolveRecordedBlueprintUrl;
|
||||
let fetchRecordedBlueprintRrd;
|
||||
let resolveRecordedPerceptionUrl;
|
||||
let fetchRecordedPerceptionRrd;
|
||||
let resolveRecordedPerceptionViewerSourceUrl;
|
||||
let probeRecordedPerceptionViewerSource;
|
||||
let resolveRecordedPointColorsUrl;
|
||||
let fetchRecordedPointColorsRrd;
|
||||
let recordedPointColorKey;
|
||||
@@ -110,6 +112,8 @@ before(async () => {
|
||||
fetchRecordedBlueprintRrd,
|
||||
resolveRecordedPerceptionUrl,
|
||||
fetchRecordedPerceptionRrd,
|
||||
resolveRecordedPerceptionViewerSourceUrl,
|
||||
probeRecordedPerceptionViewerSource,
|
||||
resolveRecordedPointColorsUrl,
|
||||
fetchRecordedPointColorsRrd,
|
||||
recordedPointColorKey,
|
||||
@@ -407,6 +411,15 @@ test("recorded blueprint endpoint is derived only from canonical same-origin RRD
|
||||
),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
resolveRecordedBlueprintUrl(
|
||||
`/api/v1/laboratory/vegetation-shadow/lab-v1-vegetation-shadow-${"a".repeat(64)}`
|
||||
+ "/canonical-replay.rrd",
|
||||
"http://127.0.0.1:5174",
|
||||
"/api/v1/observation-sessions/session-1/blueprint.rrd",
|
||||
),
|
||||
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/blueprint.rrd",
|
||||
);
|
||||
});
|
||||
|
||||
test("recorded replay becomes ready only after the complete declared timeline is buffered", () => {
|
||||
@@ -485,6 +498,8 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
|
||||
view_reset_generation: 1,
|
||||
follow_trajectory: true,
|
||||
unified_perception: true,
|
||||
semantic_layer: null,
|
||||
plan_view: false,
|
||||
show_detections_2d: true,
|
||||
show_segmentation: false,
|
||||
show_cuboids_3d: true,
|
||||
@@ -664,6 +679,74 @@ test("recorded perception fetch admits one complete same-origin RRD or no layer"
|
||||
assert.equal(absent, null);
|
||||
});
|
||||
|
||||
test("LAB perception sidecar is streamed by native Rerun from one generation-bound URL", async () => {
|
||||
const endpoint =
|
||||
`http://127.0.0.1:5174/api/v1/laboratory/vegetation-shadow/` +
|
||||
`lab-v1-vegetation-shadow-${"a".repeat(64)}/canonical-overlay.rrd`;
|
||||
const sourceUrl = resolveRecordedPerceptionViewerSourceUrl(
|
||||
endpoint,
|
||||
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
|
||||
"b".repeat(64),
|
||||
"http://127.0.0.1:5174",
|
||||
);
|
||||
assert.equal(
|
||||
sourceUrl,
|
||||
`${endpoint}?application_id=nodedc_mission_core_recorded` +
|
||||
`&recording_id=recording-001&generation=${"b".repeat(64)}`,
|
||||
);
|
||||
|
||||
const overlayGeneration = "c".repeat(64);
|
||||
const probe = await probeRecordedPerceptionViewerSource(sourceUrl, {
|
||||
origin: "http://127.0.0.1:5174",
|
||||
fetcher: async (input, init) => {
|
||||
assert.equal(String(input), sourceUrl);
|
||||
assert.equal(init.method, "HEAD");
|
||||
assert.equal(new Headers(init.headers).get("Range"), null);
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.rerun.rrd",
|
||||
"Content-Length": "186058411",
|
||||
"ETag": `"${overlayGeneration}"`,
|
||||
"X-Rerun-Format": "RRF2",
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
assert.deepEqual(probe, {
|
||||
sourceUrl: `${sourceUrl}&overlay_generation=${overlayGeneration}`,
|
||||
byteLength: 186_058_411,
|
||||
});
|
||||
});
|
||||
|
||||
test("LAB native source rejects an unsealed overlay descriptor", async () => {
|
||||
const endpoint =
|
||||
`http://127.0.0.1:5174/api/v1/laboratory/vegetation-shadow/` +
|
||||
`lab-v1-vegetation-shadow-${"a".repeat(64)}/canonical-overlay.rrd`;
|
||||
const sourceUrl = resolveRecordedPerceptionViewerSourceUrl(
|
||||
endpoint,
|
||||
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
|
||||
"b".repeat(64),
|
||||
"http://127.0.0.1:5174",
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
probeRecordedPerceptionViewerSource(sourceUrl, {
|
||||
origin: "http://127.0.0.1:5174",
|
||||
fetcher: async () => new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.rerun.rrd",
|
||||
"Content-Length": "186058411",
|
||||
"ETag": `"${"c".repeat(64)}"`,
|
||||
"X-Rerun-Format": "RRF1",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
/Invalid recorded perception viewer response/,
|
||||
);
|
||||
});
|
||||
|
||||
test("recorded replay creates an isolated source catalog without live device bindings", () => {
|
||||
const sources = recordedObservationSources({
|
||||
kind: "rerun-recording",
|
||||
|
||||
@@ -13,7 +13,6 @@ let liveTimelineNeedsSynchronization;
|
||||
let liveRerunReceiverBindingIdentity;
|
||||
let recordedOpenWatchdogTimeoutMs;
|
||||
let rerunViewerInitialSource;
|
||||
let rerunViewerOpenOptions;
|
||||
let resolveRecordedViewerSourceUrl;
|
||||
|
||||
before(async () => {
|
||||
@@ -31,7 +30,6 @@ before(async () => {
|
||||
liveRerunReceiverBindingIdentity,
|
||||
recordedOpenWatchdogTimeoutMs,
|
||||
rerunViewerInitialSource,
|
||||
rerunViewerOpenOptions,
|
||||
resolveRecordedViewerSourceUrl,
|
||||
} = await server.ssrLoadModule("/src/components/RerunViewport.tsx"));
|
||||
});
|
||||
@@ -71,9 +69,21 @@ test("only the canonical digest-bound generation URL reaches the native Rerun re
|
||||
}
|
||||
});
|
||||
|
||||
test("only the live receiver opens on the native following edge", () => {
|
||||
assert.deepEqual(rerunViewerOpenOptions(true), { follow_if_http: true });
|
||||
assert.equal(rerunViewerOpenOptions(false), null);
|
||||
test("one canonical LAB replay generation reaches the same native receiver", () => {
|
||||
const labSource =
|
||||
`/api/v1/laboratory/vegetation-shadow/lab-v1-vegetation-shadow-${"c".repeat(64)}`
|
||||
+ "/canonical-replay.rrd";
|
||||
const descriptor = {
|
||||
sourceUrl: labSource,
|
||||
viewerSourceUrl: `${labSource}?generation=${sha256}`,
|
||||
byteLength: 300_000_000,
|
||||
sha256,
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
resolveRecordedViewerSourceUrl(descriptor, "http://mission-core.test"),
|
||||
`http://mission-core.test${descriptor.viewerSourceUrl}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("live presentation waits for the exact receiver to expose a usable range", () => {
|
||||
@@ -216,7 +226,7 @@ test("recorded RRD bytes are never split across LogChannel.send_rrd calls", asyn
|
||||
assert.doesNotMatch(source, /missioncore\/recorded-recording/);
|
||||
assert.doesNotMatch(source, /recordedChannel/);
|
||||
assert.match(source, /viewer\.start\(\s*rerunViewerInitialSource\(resolvedSource\)/s);
|
||||
assert.match(source, /rerunViewerOpenOptions\(followLive\)/);
|
||||
assert.doesNotMatch(source, /rerunViewerOpenOptions/);
|
||||
assert.match(
|
||||
source,
|
||||
/recordingOpened = true;[\s\S]*diagnosticLifecycle\.markAdmitted\(\);/,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createServer } from "vite";
|
||||
let server;
|
||||
let canPublishRecordedPlaybackController;
|
||||
let createRecordedAutoplayGate;
|
||||
let createRecordedInitialSeekGate;
|
||||
let isRecordedPlaybackReady;
|
||||
let isRecordedPlaybackPresentationReady;
|
||||
let isUsableRecordedPlaybackRange;
|
||||
@@ -22,6 +23,7 @@ before(async () => {
|
||||
({
|
||||
canPublishRecordedPlaybackController,
|
||||
createRecordedAutoplayGate,
|
||||
createRecordedInitialSeekGate,
|
||||
isRecordedPlaybackReady,
|
||||
isRecordedPlaybackPresentationReady,
|
||||
isUsableRecordedPlaybackRange,
|
||||
@@ -159,6 +161,31 @@ test("recorded autoplay waits for the full range and then runs exactly once", ()
|
||||
assert.equal(gate.attempted(), true);
|
||||
});
|
||||
|
||||
test("paused recorded replay seeks once to its first presentable frame", () => {
|
||||
const gate = createRecordedInitialSeekGate();
|
||||
const seeks = [];
|
||||
const range = { min: 0, max: 535_717_620_042 };
|
||||
|
||||
assert.equal(gate.attempt(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
range,
|
||||
(value) => seeks.push(value),
|
||||
39_215_263_458,
|
||||
), true);
|
||||
assert.equal(gate.attempt(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
range,
|
||||
(value) => seeks.push(value),
|
||||
50_000_000_000,
|
||||
), false);
|
||||
assert.deepEqual(seeks, [39_215_263_458]);
|
||||
assert.equal(gate.attempted(), true);
|
||||
});
|
||||
|
||||
test("recorded autoplay starts at the first presentable camera frame without shrinking the range", () => {
|
||||
const gate = createRecordedAutoplayGate();
|
||||
const seeks = [];
|
||||
|
||||
@@ -1,81 +1,36 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import makeRerunRuntime from "../vendor/rerun-web-viewer-0.34.1/re_viewer.nodedc.js";
|
||||
|
||||
const root = resolve(import.meta.dirname, "..");
|
||||
const packageRoot = resolve(root, "node_modules/@rerun-io/web-viewer");
|
||||
const vendorRoot = resolve(root, "vendor/rerun-web-viewer-0.34.1");
|
||||
const readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
||||
|
||||
const sha256 = (path) =>
|
||||
createHash("sha256").update(readFileSync(path)).digest("hex");
|
||||
test("Mission Core uses the exact upstream Rerun 0.36.3 web package", () => {
|
||||
const application = readJson(resolve(root, "package.json"));
|
||||
const installed = readJson(resolve(packageRoot, "package.json"));
|
||||
|
||||
test("NODE.DC Rerun runtime is the audited 0.34.1 spatial camera build", () => {
|
||||
const manifest = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
|
||||
assert.equal(manifest.version, "0.34.1");
|
||||
|
||||
const expectedWasm = "ffe7543d28bb3394f289f6299de43d038767eef83d781c2b7f8f5683308a0469";
|
||||
const expectedGlue = "0f7b76c9f24cbd8437021b5d37499894aeadc586183e422ebc82ef556d7b8339";
|
||||
|
||||
assert.equal(sha256(resolve(vendorRoot, "re_viewer_bg.nodedc.wasm")), expectedWasm);
|
||||
assert.equal(sha256(resolve(vendorRoot, "re_viewer.nodedc.js")), expectedGlue);
|
||||
assert.equal(sha256(resolve(packageRoot, "re_viewer_bg.wasm")), expectedWasm);
|
||||
assert.equal(sha256(resolve(packageRoot, "re_viewer.js")), expectedGlue);
|
||||
assert.equal(application.dependencies["@rerun-io/web-viewer"], "0.36.3");
|
||||
assert.equal(installed.version, "0.36.3");
|
||||
assert.equal(application.scripts.postinstall, undefined);
|
||||
});
|
||||
|
||||
test("custom JavaScript glue references only exports present in its paired WASM", () => {
|
||||
const wasmPath = resolve(vendorRoot, "re_viewer_bg.nodedc.wasm");
|
||||
const gluePath = resolve(vendorRoot, "re_viewer.nodedc.js");
|
||||
const module = new WebAssembly.Module(readFileSync(wasmPath));
|
||||
const exports = new Set(WebAssembly.Module.exports(module).map(({ name }) => name));
|
||||
const imports = WebAssembly.Module.imports(module);
|
||||
const glue = readFileSync(gluePath, "utf8");
|
||||
const referencedExports = new Set(
|
||||
[...glue.matchAll(/\bwasm\.([A-Za-z_$][\w$]*)/g)].map((match) => match[1]),
|
||||
);
|
||||
const missingExports = [...referencedExports].filter((name) => !exports.has(name));
|
||||
test("the active application never imports or installs the archived vendor fork", () => {
|
||||
const application = readFileSync(resolve(root, "package.json"), "utf8");
|
||||
const viewport = readFileSync(resolve(root, "src/components/RerunViewport.tsx"), "utf8");
|
||||
|
||||
assert.equal(imports.length, 927);
|
||||
assert.equal(exports.size, 79);
|
||||
assert.deepEqual(missingExports, []);
|
||||
assert.match(glue, /export default function\(\)/);
|
||||
assert.match(glue, /if \(!wasm\) return;/);
|
||||
assert.doesNotMatch(application, /patch-rerun-web-viewer/);
|
||||
assert.doesNotMatch(application, /vendor\/rerun-web-viewer/);
|
||||
assert.doesNotMatch(viewport, /vendor\/rerun-web-viewer/);
|
||||
assert.doesNotMatch(viewport, /0\.34\.1/);
|
||||
});
|
||||
|
||||
test("custom Rerun WASM initializes and grows its externref table", () => {
|
||||
const runtime = makeRerunRuntime();
|
||||
runtime.initSync({
|
||||
module: readFileSync(resolve(vendorRoot, "re_viewer_bg.nodedc.wasm")),
|
||||
});
|
||||
test("upstream WebViewer exposes one three-argument start contract", () => {
|
||||
const declaration = readFileSync(resolve(packageRoot, "index.d.ts"), "utf8");
|
||||
|
||||
assert.equal(typeof runtime.WebHandle, "function");
|
||||
runtime.deinit();
|
||||
});
|
||||
|
||||
test("source patch carries pointer navigation, persistent follow, and camera continuity tests", () => {
|
||||
const patch = readFileSync(resolve(vendorRoot, "NODEDC_ZOOM_TO_CURSOR.patch"), "utf8");
|
||||
|
||||
assert.match(patch, /fn pointer_ray_direction/);
|
||||
assert.match(patch, /fn zoom_orbit_towards_pointer/);
|
||||
assert.match(patch, /near_limit_hands_excess_zoom_to_cursor_directed_dolly/);
|
||||
assert.match(patch, /crossing_near_limit_preserves_unconsumed_scene_scaled_zoom/);
|
||||
assert.match(patch, /remaining_zoom_factor\.ln\(\) \* self\.speed/);
|
||||
assert.match(patch, /off_center_pointer_stays_on_the_same_view_ray/);
|
||||
assert.match(patch, /fn rotate_radians_around_anchor/);
|
||||
assert.match(patch, /orbit_drag_anchor/);
|
||||
assert.match(patch, /minimum_orbital_navigation_speed/);
|
||||
assert.match(patch, /orbital_rotation_keeps_selected_anchor_on_the_same_view_ray/);
|
||||
assert.match(patch, /orbital_navigation_speed_floor_tracks_scene_scale/);
|
||||
assert.match(patch, /NODEDC_PERSISTENT_ORBIT_TRACKING_ENTITY/);
|
||||
assert.match(patch, /nodedc_rig_orbit_tracking_is_persistent/);
|
||||
assert.match(patch, /restore_persistent_orbit_eye_after_blueprint_update/);
|
||||
assert.match(
|
||||
patch,
|
||||
/persistent_rig_follow_restores_the_last_rendered_eye_after_blueprint_update/,
|
||||
declaration,
|
||||
/start\(rrd: string \| string\[\] \| null, parent: HTMLElement \| null, options: WebViewerOptions \| null\): Promise<void>/,
|
||||
);
|
||||
assert.match(patch, /explicit_blueprint_pose_is_not_replaced_by_the_previous_eye/);
|
||||
assert.match(patch, /previous_picking_result/);
|
||||
});
|
||||
|
||||
@@ -451,8 +451,8 @@ test("canonical recorded LAB spatial frame keeps source, SLAM and body identity
|
||||
assert.deepEqual(frame.bodyFrame.originMapXyzM, [33, 4, 1]);
|
||||
});
|
||||
|
||||
test("vegetation realtime LAB and archival benchmark use separate admitted instruments", async () => {
|
||||
const [resultSource, benchmarkSource, m49Source, canonicalSource] = await Promise.all([
|
||||
test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival review separate", async () => {
|
||||
const [resultSource, benchmarkSource, m49Source, canonicalSource, rerunSource] = await Promise.all([
|
||||
readFile(
|
||||
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
|
||||
"utf8",
|
||||
@@ -469,6 +469,10 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr
|
||||
new URL("../src/components/laboratory/CanonicalRecordedLabReplay.tsx", import.meta.url),
|
||||
"utf8",
|
||||
),
|
||||
readFile(
|
||||
new URL("../src/workspaces/laboratory/CanonicalVegetationRerunReplay.tsx", import.meta.url),
|
||||
"utf8",
|
||||
),
|
||||
]);
|
||||
assert.doesNotMatch(resultSource, /M48MaskComparisonVisual/);
|
||||
assert.match(resultSource, /M49TgsFullShadowEvidence/);
|
||||
@@ -477,23 +481,25 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr
|
||||
assert.match(m49Source, /controlLabel: "SEMANTICS"/);
|
||||
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 2);
|
||||
assert.doesNotMatch(resultSource, /RAVNOVES004TREE mixed route review/);
|
||||
assert.match(resultSource, /CANONICAL RECORDED LAB · RAVNOVES004TREE/);
|
||||
assert.match(resultSource, /<M4ReplayThreatVisual/);
|
||||
assert.match(resultSource, /timelineEndpointRoot=\{VEGETATION_TIMELINE_ENDPOINT\}/);
|
||||
assert.match(resultSource, /playbackTransport="segmented"/);
|
||||
assert.match(resultSource, /recoverTimestampStalls/);
|
||||
assert.match(resultSource, /КАНОНИЧЕСКАЯ ЗАПИСАННАЯ LAB · RAVNOVES004TREE/);
|
||||
assert.match(resultSource, /<CanonicalVegetationRerunReplay/);
|
||||
assert.doesNotMatch(resultSource, /RerunViewport/);
|
||||
assert.doesNotMatch(resultSource, /cacheRef|pumpRef|desiredRef/);
|
||||
assert.doesNotMatch(resultSource, /LaboratoryRecordedClipPlayer|M48EvidenceModeRail/);
|
||||
assert.doesNotMatch(resultSource, /assets\.tgs|<img/);
|
||||
assert.match(resultSource, /SOURCE POINTS/);
|
||||
assert.match(resultSource, /TGS COSTMAP/);
|
||||
assert.match(rerunSource, /<RerunViewport/);
|
||||
assert.match(rerunSource, /ИСХ\. ТОЧКИ/);
|
||||
assert.match(rerunSource, /ЛОК\. SLAM/);
|
||||
assert.match(rerunSource, /resolveCanonicalLabReplay/);
|
||||
assert.doesNotMatch(rerunSource, /canonical-overlay\.rrd/);
|
||||
assert.match(rerunSource, /unifiedPerception: splitView/);
|
||||
assert.doesNotMatch(rerunSource, /LaboratoryRecordedClipPlayer|LaboratoryMetricEvidenceScene/);
|
||||
assert.match(resultSource, /point-aligned 3D semantics пока не запечатаны/);
|
||||
assert.match(resultSource, /cellLayerAvailable: false/);
|
||||
assert.match(rerunSource, /value: "tgs", label: "TGS", disabled: true/);
|
||||
assert.match(canonicalSource, /primary=\{mediaPane\}/);
|
||||
assert.match(canonicalSource, /secondary=\{spatialPane/);
|
||||
assert.match(canonicalSource, /missioncore\.canonical-recorded-lab-replay\/v1/);
|
||||
assert.match(canonicalSource, /separatorLabel="Изменить размер VIDEO\/CAMERA и 3D\/PLAN"/);
|
||||
assert.match(canonicalSource, /separatorLabel="Изменить размер видео\/камеры и 3D\/плана"/);
|
||||
assert.match(resultSource, /linked canonical M4\.9 TGS evidence/);
|
||||
assert.match(resultSource, /linkedTgsResultId/);
|
||||
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
|
||||
|
||||
@@ -75,13 +75,13 @@ test("recorded session profile owns progressive admission and on-demand layers",
|
||||
assert.equal(profile.perceptionLayers.enabled, false);
|
||||
});
|
||||
|
||||
test("LAB recorded evidence remains outside native Rerun lifecycle", () => {
|
||||
test("the old LAB transport is fenced as explicit legacy comparison only", () => {
|
||||
assert.deepEqual(LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE, {
|
||||
kind: "lab-recorded-evidence",
|
||||
clock: "source-sequence",
|
||||
cameraTransport: "generation-bound-fmp4",
|
||||
spatialTransport: "bounded-sealed-artifacts",
|
||||
loadPolicy: "visible-evidence-only",
|
||||
loadPolicy: "explicit-legacy-comparison-only",
|
||||
workerRequired: false,
|
||||
});
|
||||
assert.equal(Object.isFrozen(LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE), true);
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# Archived viewer comparison source
|
||||
|
||||
`rerun-web-viewer-0.34.1/` is retained only as rollback-era comparison evidence
|
||||
for ADR 0045. The active application neither imports it nor runs the historical
|
||||
patch scripts during install or build. Canonical Control Station routes use the
|
||||
unmodified `@rerun-io/web-viewer` dependency declared in `package.json`.
|
||||
|
||||
Do not update or reactivate this tree. Remove it with the remaining custom
|
||||
fMP4/Three.js replay implementation after the canonical Rerun migration passes
|
||||
operator visual acceptance.
|
||||
@@ -172,7 +172,7 @@ On the first live or adapter file-replay session, `RerunBridge`:
|
||||
|
||||
At every session start it resets the trajectory, current point count, metrics,
|
||||
blueprint and session-local visible time, then feeds the new source through the
|
||||
same recording. This process-wide lifecycle is intentional: the Rerun 0.34.1
|
||||
same recording. This process-wide lifecycle is intentional: the upstream Rerun 0.36.3
|
||||
browser receiver can remain connected after canvas teardown, so restarting the
|
||||
native listener on the same port is not a reliable session boundary.
|
||||
|
||||
|
||||
@@ -185,6 +185,15 @@ The evidence slot has two admitted renderers:
|
||||
- `diagnostic-model` — a specialized visual result such as the LAB E28 L2.6
|
||||
surface/timeline/review viewer.
|
||||
|
||||
ADR 0045 amends the full-session `recorded-replay` transport. A canonical
|
||||
recorded LAB uses one unmodified upstream Rerun viewer, recording identity and
|
||||
timeline for camera, semantic and spatial evidence. The source RRD owns pose,
|
||||
point cloud and trajectory; a digest-bound RRD sidecar may add only immutable
|
||||
derived entities which are absent from the source. Model or layer selection is
|
||||
a blueprint/profile change, not a new player or renderer. Missing full-route
|
||||
TGS, semantic 3D, boxes or cuboids remain disabled instead of being inferred
|
||||
from sparse review artifacts.
|
||||
|
||||
Recorded camera clips used for review are a frozen sub-contract of the admitted
|
||||
viewer, `missioncore.laboratory-recorded-clip-viewer/v1`, implemented by
|
||||
`LaboratoryRecordedClipPlayer`. It owns the generation-bound fMP4 manifest,
|
||||
@@ -197,6 +206,10 @@ Forward frame progression may roll an already-buffered segment target; a
|
||||
backward seek or clip loop must perform an explicit decoder seek and remain
|
||||
decoder-ready without exposing a per-frame loader.
|
||||
|
||||
That clip contract remains available to historical bounded clip instruments.
|
||||
It is legacy comparison transport for a migrated full-session LAB and must not
|
||||
be mounted, prefetched or run in parallel with the canonical Rerun profile.
|
||||
|
||||
When recorded camera and frame-indexed spatial evidence are both required for
|
||||
one review question, the shared player presents them simultaneously on the same
|
||||
media clock. The camera remains the clock owner; a bounded experiment-neutral
|
||||
|
||||
@@ -149,7 +149,8 @@ second mounted source.
|
||||
- [x] Harden the Control Station application boundary before A3: consume the
|
||||
Design Guideline packages as the only visual platform, isolate the LAB
|
||||
feature and CSS, add typed workspace contracts and enforce one-way imports
|
||||
plus composition-size ratchets.
|
||||
across the application layers. Line-count limits were subsequently removed:
|
||||
they are not an architectural invariant.
|
||||
- [x] Validate the real A2 generation:
|
||||
`e30-review-pack-faec915a771022cceaf4ee62bece698afc8018d09b6b0ac7602157216fbb3686`
|
||||
→
|
||||
|
||||
@@ -160,6 +160,15 @@ history of rejected approaches belong in Ops.
|
||||
|
||||
Primary visual evidence is hosted in one reusable viewer frame.
|
||||
|
||||
For canonical full-session recorded replay, ADR 0045 fixes one native upstream
|
||||
Rerun viewer beneath this frame. The accepted product chrome and switching
|
||||
logic remain Mission Core UI, while Rerun alone owns playback time, video
|
||||
decoding, 2D annotations, point-cloud rendering, trajectory and 3D camera.
|
||||
VIDEO/CAMERA, SOURCE POINTS/LOCAL SLAM/TGS COSTMAP/SEMANTICS and 3D/PLAN select
|
||||
entities, visible time ranges and blueprints in that mounted viewer. They never
|
||||
start independent transports or render loops. A LAB may rename model buttons or
|
||||
add an admitted layer button, but it may not change this switching architecture.
|
||||
|
||||
### Frozen Milestone 4 perception instruments
|
||||
|
||||
Milestone 4 admits exactly two operator instruments inside the shared LAB page
|
||||
@@ -234,6 +243,13 @@ The shared player may retain a rolling target only for the same or a later
|
||||
segment. Rewind and clip-loop transitions must seek backward explicitly while
|
||||
keeping the admitted generation and decoder owner mounted.
|
||||
|
||||
The preceding fMP4 rule applies to historical bounded clip instruments. A
|
||||
migrated full-session recorded LAB instead uses the native Rerun `AssetVideo`
|
||||
and `VideoFrameReference` contract from ADR 0045. It must not mount the clip
|
||||
player or a custom Three.js scene alongside Rerun. The visual controls and LAB
|
||||
template are identical in both cases; the canonical profile determines the one
|
||||
active transport.
|
||||
|
||||
### 3D and 2D policy
|
||||
|
||||
Choose the default representation from the operator question:
|
||||
|
||||
@@ -249,8 +249,7 @@ vocabulary and executable contracts. They do not create a second runtime model.
|
||||
- no upward imports from core/components into workspaces or App;
|
||||
- no visual adapter imports from core;
|
||||
- no local vendor icon library or direct Design Guideline source imports;
|
||||
- LAB code and CSS remain outside the central workspace buckets;
|
||||
- central composition files cannot silently return to their previous size.
|
||||
- LAB code and CSS remain outside the central workspace buckets.
|
||||
|
||||
`test/laboratoryProductUi.test.mjs` additionally enforces the versioned LAB
|
||||
report fields and shared result component across bounded LAB modules.
|
||||
@@ -260,8 +259,9 @@ new unclassified experiment branches while allowing a declared bounded
|
||||
experimental adapter. The gate protects core composition; it does not forbid a
|
||||
novel research stack.
|
||||
|
||||
The line limits are ratchets, not quality targets. When a file reaches a limit,
|
||||
split a feature; do not raise the limit to accommodate unrelated behavior.
|
||||
File length is not an architectural boundary and is not enforced. Refactoring
|
||||
is justified by ownership, cohesion, dependency direction, lifecycle or test
|
||||
isolation, not by a line-count threshold.
|
||||
|
||||
From `apps/control-station` run:
|
||||
|
||||
@@ -273,13 +273,15 @@ npm run build
|
||||
|
||||
## Known bounded debt
|
||||
|
||||
- `App.tsx` remains a large shell orchestrator. Its current size is frozen by a
|
||||
ratchet; future shell behavior must extract a controller/hook or panel module.
|
||||
- `App.tsx` remains a large shell orchestrator. Future shell behavior should
|
||||
preserve its orchestration ownership and extract modules only where they
|
||||
acquire an independent responsibility or lifecycle.
|
||||
- `Workspaces.tsx` still contains several established generic workspaces. New
|
||||
domains must be separate modules, and existing ones may be extracted when
|
||||
their behavior changes.
|
||||
- `LaboratoryArchiveWorkspace.tsx` is now physically isolated but at its
|
||||
ratchet. A3 receives its own component/module instead of growing that file.
|
||||
domains should respect the existing dependency direction; extraction is a
|
||||
design decision rather than a response to file length.
|
||||
- `LaboratoryArchiveWorkspace.tsx` is physically isolated. Further LAB work
|
||||
must preserve the feature boundary without imposing a size quota on the
|
||||
implementation.
|
||||
- Design Guideline dependencies are mutable local `file:` links until a
|
||||
portable package/distribution decision is implemented.
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# ADR 0045: Upstream Rerun as the canonical recorded-LAB pipeline
|
||||
|
||||
Date: 2026-08-30
|
||||
Status: accepted; RAVNOVES004TREE is the first migrated full-route LAB
|
||||
|
||||
## Context
|
||||
|
||||
The accepted LAB product composition was repeatedly rebuilt over independent
|
||||
camera, semantic, point-cloud and TGS transports. The resulting implementation
|
||||
had several clocks, LAB-specific caches, a browser MediaSource decoder and a
|
||||
separate Three.js spatial renderer. A camera could continue while segmentation
|
||||
or the cloud stopped; rewind could expose evidence from different source
|
||||
sequences; switching TGS could block both panes. Low host utilization did not
|
||||
make that architecture correct: the bottleneck was duplicated admission,
|
||||
decoding, scheduling and state ownership.
|
||||
|
||||
The product owner requires the existing LAB UI and interaction grammar to stay
|
||||
unchanged. VIDEO/CAMERA, SOURCE POINTS/LOCAL SLAM/TGS COSTMAP/SEMANTICS and
|
||||
3D/PLAN remain the canonical controls. Models and evidence providers may
|
||||
change, but a LAB may not create another player, clock, splitter, spatial
|
||||
renderer, window or status grammar.
|
||||
|
||||
The repository state before this migration is retained by the annotated Git
|
||||
tag `baseline/custom-legacy-before-canonical-rerun-2026-08-30`. Its Russian
|
||||
stage name is **«Этап перехода от самописного legacy-контура к каноническому
|
||||
шаблонному Rerun-пайплайну»**.
|
||||
|
||||
## Decision
|
||||
|
||||
Recorded LAB replay uses the unmodified upstream Rerun SDK and web viewer. The
|
||||
first accepted dependency is exactly `rerun-sdk==0.36.3` and
|
||||
`@rerun-io/web-viewer@0.36.3`. Mission Core does not patch the package, vendor a
|
||||
viewer fork or depend on private viewer source. Product controls are an outer
|
||||
adapter which requests an ordinary Rerun blueprint.
|
||||
|
||||
One native Rerun viewer owns:
|
||||
|
||||
- one `session_time` playback clock;
|
||||
- the recorded camera and semantic image-space evidence;
|
||||
- `/world/points`, `/world/sensor_pose` and `/world/trajectory`;
|
||||
- native 3D orbit and top-down plan presentation;
|
||||
- seek, play/pause and frame synchronization.
|
||||
|
||||
The canonical K1 RRD remains the source of pose, source points, bounded Local
|
||||
SLAM accumulation and trajectory. A LAB may publish one immutable normalized
|
||||
RRD sidecar containing only derived evidence absent from that recording, such
|
||||
as camera video, semantic masks and diagnostic 2D boxes. The base recording and
|
||||
sidecar must have the same application id, recording id and timeline. A sidecar
|
||||
does not copy, rotate or re-own world geometry.
|
||||
|
||||
RAVNOVES004TREE uses a digest-bound sidecar cache. Its sealed fMP4 fragments are
|
||||
verified, concatenated and transcoded once to an upstream-compatible H.264
|
||||
`AssetVideo`. Source PTS are preserved. A fragment without a decodable sample
|
||||
holds the latest preceding frame; decoded samples are never renumbered to a
|
||||
synthetic fixed-rate clock. Each semantic mask and `VideoFrameReference` is
|
||||
logged at the exact immutable LAB `session_time`.
|
||||
|
||||
Profiles control loading rather than creating different viewers:
|
||||
|
||||
- source points use zero accumulation;
|
||||
- Local SLAM uses a native five-second visible time range;
|
||||
- TGS COSTMAP and 3D SEMANTICS are enabled only when full-route immutable
|
||||
artifacts exist and share the recording clock;
|
||||
- semantic model buttons select an entity path in the same sidecar;
|
||||
- 3D/PLAN changes native eye controls, never point coordinates;
|
||||
- layers missing from an immutable result stay visibly disabled and fail
|
||||
closed; they are not reconstructed from sparse review anchors.
|
||||
|
||||
The previous fMP4/Three.js LAB transport remains source-retained only for
|
||||
explicit legacy comparison. No canonical route selects it, preloads it or lets
|
||||
it start background work. Removal is allowed after migrated results pass the
|
||||
same acceptance checks and the rollback tag is no longer operationally needed.
|
||||
|
||||
## Acceptance
|
||||
|
||||
A migrated recorded LAB is accepted only when:
|
||||
|
||||
1. the base RRD identity and sidecar identity match exactly;
|
||||
2. camera, semantics, point cloud, pose and trajectory follow one Rerun clock;
|
||||
3. play, pause, forward seek and backward seek do not remount the viewer;
|
||||
4. SOURCE POINTS and Local SLAM are native views of the same sealed geometry;
|
||||
5. unavailable TGS or semantic 3D evidence is disabled rather than simulated;
|
||||
6. first materialization is cached by source/result/renderer digests and a
|
||||
cache hit performs no decode or inference;
|
||||
7. the existing LAB page, selectors, report mode, controls and expand behavior
|
||||
remain unchanged;
|
||||
8. Data replay and live Rerun profiles continue to use their own load policies;
|
||||
9. the integrated application remains on `127.0.0.1:8000` and no second Mission
|
||||
Core service is introduced.
|
||||
|
||||
The isolated renderer materialized the first real RAVNOVES004TREE sidecar in
|
||||
79.5 seconds. Under the live operator service, cold materialization completed
|
||||
in approximately seven minutes and produced a 393,203,594-byte RRD; this is too
|
||||
slow to treat as an interactive open and should be moved to publication-time
|
||||
preparation. With a full SHA-256 recheck on every cache hit, the warm product
|
||||
endpoint returned headers in 0.89 seconds and streamed the complete local
|
||||
artifact in 2.48 seconds. These measurements establish the local cache behavior,
|
||||
not a realtime inference or navigation claim.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Mission Core keeps its product UI without owning media or spatial playback.
|
||||
- Rerun can be upgraded through ordinary dependency updates and regression
|
||||
tests instead of reapplying a local patch.
|
||||
- New models publish entities and annotations into the same recording contract;
|
||||
they do not add LAB-specific viewers.
|
||||
- SLAM clouds and trajectories stay visible through standard Rerun components.
|
||||
- Useful native boxes/cuboids may be added as ordinary entity layers when their
|
||||
immutable full-route evidence exists.
|
||||
- The migration does not improve DDRNet quality, prove terrain traversability
|
||||
or grant navigation/actuation authority. Those remain separate model and
|
||||
safety acceptance questions.
|
||||
@@ -6,7 +6,59 @@ Scope: Mission Core recorded LAB replay, RAVNOVES004TREE, OPS perception state
|
||||
|
||||
Excluded: Gaussian/simulation workers and their artifacts
|
||||
|
||||
## Outcome
|
||||
Status: the diagnostic findings and perception conclusions remain evidence, but
|
||||
the custom fMP4/Three.js implementation described below is superseded by
|
||||
ADR 0045. It is retained here as the failure audit, not as the current replay
|
||||
contract.
|
||||
|
||||
## Current outcome after ADR 0045
|
||||
|
||||
RAVNOVES004TREE now uses one unmodified upstream Rerun 0.36.3 viewer for camera,
|
||||
semantic masks, diagnostic boxes, source points, bounded Local SLAM, trajectory,
|
||||
3D/PLAN and playback. The canonical source RRD owns world geometry; a verified
|
||||
immutable RRD sidecar adds only LAB image-space evidence with the same recording
|
||||
id and `session_time`. Source PTS are preserved, so missing decodable video
|
||||
samples hold the previous frame instead of shortening the route or drifting
|
||||
from masks.
|
||||
|
||||
The accepted LAB controls and layout remain unchanged. Full-route TGS and
|
||||
point-aligned 3D semantics are still absent and therefore remain visible but
|
||||
disabled. The former fMP4/Three.js route is comparison-only legacy and does not
|
||||
load on the canonical RAV004 route.
|
||||
|
||||
The isolated renderer materialized the sidecar in 79.5 seconds. The canonical
|
||||
live service cold-path took approximately seven minutes and produced
|
||||
393,203,594 bytes, so publication-time preparation remains required before this
|
||||
profile is called immediately openable. With a complete SHA-256 check on every
|
||||
cache hit, the warm endpoint returned headers in 0.89 seconds and streamed the
|
||||
local artifact in 2.48 seconds. This is a replay/cache measurement, not a
|
||||
realtime inference claim.
|
||||
|
||||
## Current validation after ADR 0045
|
||||
|
||||
- frontend unit suite: 661 passed;
|
||||
- TypeScript typecheck and production Vite build: passed;
|
||||
- migration-scoped backend/API suite: 60 passed;
|
||||
- live canonical blueprint: HTTP 200, 86,343 bytes, 0.185 seconds;
|
||||
- warm integrity-checked sidecar: HTTP 200, byte-range `RRF2` confirmed;
|
||||
- canonical service restarted and healthy on `127.0.0.1:8000`; no Mission Core
|
||||
listener exists on `8765`.
|
||||
|
||||
The repository-wide Python suite completed with ten failures in pre-existing K1
|
||||
camera-recovery/scenario-reset tests. No K1 runtime or test file differs from
|
||||
the rollback tag in this migration. Nine failures are in the existing active
|
||||
acquisition camera-restart contract; one is an existing expected-document
|
||||
mismatch after the runtime added reset timing. These do not invalidate the
|
||||
Rerun-specific checks, but the repository-wide suite is not represented as
|
||||
green.
|
||||
|
||||
Automated in-app visual QA could not attach to the local address because the
|
||||
browser surface rejected the localhost URL under its URL policy. No alternate
|
||||
browser-control bypass was used. The live HTTP/data plane, build and contracts
|
||||
were accepted; an operator visual pass remains required for the exact layout,
|
||||
seek and toggle experience.
|
||||
|
||||
## Superseded implementation outcome
|
||||
|
||||
RAVNOVES004TREE no longer owns a custom LAB viewer. It supplies recording and
|
||||
model configuration to the same `M4ReplayThreatVisual` and
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ dependencies = [
|
||||
"paho-mqtt>=2.1,<3",
|
||||
"pillow>=12,<13",
|
||||
"pyyaml>=6.0,<7",
|
||||
"rerun-sdk==0.34.1",
|
||||
"rerun-sdk==0.36.3",
|
||||
"rich>=13.9,<15",
|
||||
"typer>=0.15,<1",
|
||||
"uvicorn[standard]>=0.35,<1",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -63,7 +63,7 @@ class _RecordedBlueprintStream:
|
||||
self.view_reset_generation = view_reset_generation
|
||||
self._lock = Lock()
|
||||
self._sequence = 0
|
||||
self._follow_trajectory: bool | None = None
|
||||
self._eye_contract: tuple[bool, bool] | None = None
|
||||
self._closed = False
|
||||
native = bindings.new_blueprint(
|
||||
application_id=application_id,
|
||||
@@ -85,15 +85,15 @@ class _RecordedBlueprintStream:
|
||||
blueprint_factory: Callable[[bool], rrb.Blueprint],
|
||||
*,
|
||||
follow_trajectory: bool,
|
||||
plan_view: bool,
|
||||
) -> bytes:
|
||||
with self._lock:
|
||||
if self._closed:
|
||||
raise RecordedBlueprintError("stable blueprint stream is closed")
|
||||
update_eye_controls = (
|
||||
self._follow_trajectory is None
|
||||
or self._follow_trajectory != follow_trajectory
|
||||
)
|
||||
eye_contract = (follow_trajectory, plan_view)
|
||||
update_eye_controls = self._eye_contract != eye_contract
|
||||
blueprint = blueprint_factory(update_eye_controls)
|
||||
make_active = self._sequence == 0
|
||||
self._blueprint_recording.set_time(
|
||||
"blueprint",
|
||||
sequence=self._sequence,
|
||||
@@ -103,14 +103,14 @@ class _RecordedBlueprintStream:
|
||||
self._blueprint_recording.flush(timeout_sec=5.0)
|
||||
bindings.send_blueprint(
|
||||
self._blueprint_memory.storage,
|
||||
True,
|
||||
make_active,
|
||||
False,
|
||||
self._transport_recording.to_native(),
|
||||
)
|
||||
payload = self._transport.read(flush=True, flush_timeout_sec=5.0)
|
||||
if not payload:
|
||||
raise RecordedBlueprintError("stable blueprint stream produced no data")
|
||||
self._follow_trajectory = follow_trajectory
|
||||
self._eye_contract = eye_contract
|
||||
return payload
|
||||
|
||||
def close(self) -> None:
|
||||
@@ -165,6 +165,8 @@ def recorded_blueprint(
|
||||
active_view: RecordedView = "spatial",
|
||||
view_reset_generation: Literal[0, 1] = 0,
|
||||
unified_perception: bool = False,
|
||||
semantic_layer: Literal["city", "vegetation"] | None = None,
|
||||
plan_view: bool = False,
|
||||
show_detections_2d: bool = False,
|
||||
show_segmentation: bool = False,
|
||||
show_cuboids_3d: bool = False,
|
||||
@@ -202,6 +204,27 @@ def recorded_blueprint(
|
||||
if accumulated_time_ranges is not None:
|
||||
point_overrides.append(accumulated_time_ranges)
|
||||
trajectory_overrides.append(accumulated_time_ranges)
|
||||
selected_semantic_path = (
|
||||
f"/perception/camera/segmentation/{semantic_layer}"
|
||||
if semantic_layer is not None
|
||||
else "/perception/camera/segmentation"
|
||||
)
|
||||
spatial_eye_controls = (
|
||||
rrb.EyeControls3D.from_fields(
|
||||
kind=rrb.Eye3DKind.Orbital,
|
||||
position=[0.0, 0.0, 30.0],
|
||||
look_target=[0.0, 0.0, 0.0],
|
||||
eye_up=[0.0, 1.0, 0.0],
|
||||
tracking_entity="/world/sensor_pose" if follow_trajectory else "",
|
||||
)
|
||||
if plan_view and update_eye_controls
|
||||
else rrb.EyeControls3D.from_fields(
|
||||
kind=rrb.Eye3DKind.Orbital if follow_trajectory else None,
|
||||
tracking_entity="/world/sensor_pose" if follow_trajectory else "",
|
||||
)
|
||||
if update_eye_controls
|
||||
else None
|
||||
)
|
||||
spatial_view = rrb.Spatial3DView(
|
||||
origin="/world",
|
||||
name="Мир · LiDAR и объекты" if unified_perception else "Пространственная сцена",
|
||||
@@ -232,14 +255,7 @@ def recorded_blueprint(
|
||||
# Keeping the view id stable preserves the current orbit offset when
|
||||
# tracking is toggled. An explicit empty path clears tracking without
|
||||
# overwriting the position/look-target saved by user interaction.
|
||||
eye_controls=(
|
||||
rrb.EyeControls3D.from_fields(
|
||||
kind=rrb.Eye3DKind.Orbital if follow_trajectory else None,
|
||||
tracking_entity="/world/sensor_pose" if follow_trajectory else "",
|
||||
)
|
||||
if update_eye_controls
|
||||
else None
|
||||
),
|
||||
eye_controls=spatial_eye_controls,
|
||||
)
|
||||
spatial_view.id = (
|
||||
RECORDED_SPATIAL_RESET_VIEW_ID
|
||||
@@ -258,6 +274,12 @@ def recorded_blueprint(
|
||||
"/perception/camera/segmentation": rrb.EntityBehavior(
|
||||
visible=show_segmentation,
|
||||
),
|
||||
"/perception/camera/segmentation/city": rrb.EntityBehavior(
|
||||
visible=show_segmentation and selected_semantic_path.endswith("/city"),
|
||||
),
|
||||
"/perception/camera/segmentation/vegetation": rrb.EntityBehavior(
|
||||
visible=show_segmentation and selected_semantic_path.endswith("/vegetation"),
|
||||
),
|
||||
},
|
||||
)
|
||||
camera_view.id = (
|
||||
@@ -292,14 +314,7 @@ def recorded_blueprint(
|
||||
# native cloud from /world/points.
|
||||
"/world/perception/lidar": rrb.EntityBehavior(visible=False),
|
||||
},
|
||||
eye_controls=(
|
||||
rrb.EyeControls3D.from_fields(
|
||||
kind=rrb.Eye3DKind.Orbital if follow_trajectory else None,
|
||||
tracking_entity="/world/sensor_pose" if follow_trajectory else "",
|
||||
)
|
||||
if update_eye_controls
|
||||
else None
|
||||
),
|
||||
eye_controls=spatial_eye_controls,
|
||||
)
|
||||
perception_3d_view.id = (
|
||||
RECORDED_PERCEPTION_3D_RESET_VIEW_ID
|
||||
@@ -400,6 +415,8 @@ def recorded_blueprint_rrd(
|
||||
active_view: RecordedView = "spatial",
|
||||
view_reset_generation: Literal[0, 1] = 0,
|
||||
unified_perception: bool = False,
|
||||
semantic_layer: Literal["city", "vegetation"] | None = None,
|
||||
plan_view: bool = False,
|
||||
show_detections_2d: bool = False,
|
||||
show_segmentation: bool = False,
|
||||
show_cuboids_3d: bool = False,
|
||||
@@ -414,6 +431,8 @@ def recorded_blueprint_rrd(
|
||||
active_view=active_view,
|
||||
view_reset_generation=view_reset_generation,
|
||||
unified_perception=unified_perception,
|
||||
semantic_layer=semantic_layer,
|
||||
plan_view=plan_view,
|
||||
show_detections_2d=show_detections_2d,
|
||||
show_segmentation=show_segmentation,
|
||||
show_cuboids_3d=show_cuboids_3d,
|
||||
@@ -432,6 +451,7 @@ def recorded_blueprint_rrd(
|
||||
).render(
|
||||
build_blueprint,
|
||||
follow_trajectory=follow_trajectory,
|
||||
plan_view=plan_view,
|
||||
)
|
||||
except RecordedBlueprintError:
|
||||
raise
|
||||
|
||||
@@ -149,7 +149,7 @@ class RerunBridge:
|
||||
# of record. Raw MQTT evidence is persisted independently. A large
|
||||
# late-client backlog can block the native SDK and freeze preview.
|
||||
server_memory_limit=LIVE_GRPC_BUFFER_LIMIT,
|
||||
# Rerun 0.34.1 can replay ActivateStore before StoreInfo when an
|
||||
# Rerun transport can replay ActivateStore before StoreInfo when an
|
||||
# evicted buffer is served newest-first, leaving late viewers on the
|
||||
# welcome screen. Preserve protocol order within the bounded cache.
|
||||
newest_first=False,
|
||||
|
||||
@@ -1047,6 +1047,11 @@ app.include_router(
|
||||
if session_recorded_camera_frame_service is not None
|
||||
else None
|
||||
),
|
||||
jobs_root=REPOSITORY_ROOT / ".runtime" / "compute-jobs",
|
||||
rerun_overlay_cache_root=(
|
||||
session_store.data_dir / "laboratory-rerun-overlays"
|
||||
),
|
||||
ffmpeg_path=_ffmpeg,
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
|
||||
@@ -124,6 +124,8 @@ class RecordedBlueprintRequest(StrictApiModel):
|
||||
active_view: Literal["spatial", "perception", "perception3d", "metrics"] = "spatial"
|
||||
view_reset_generation: Literal[0, 1] = 0
|
||||
unified_perception: StrictBool = False
|
||||
semantic_layer: Literal["city", "vegetation"] | None = None
|
||||
plan_view: StrictBool = False
|
||||
show_detections_2d: StrictBool = False
|
||||
show_segmentation: StrictBool = False
|
||||
show_cuboids_3d: StrictBool = False
|
||||
@@ -935,6 +937,8 @@ def build_session_router(
|
||||
active_view=request.active_view,
|
||||
view_reset_generation=request.view_reset_generation,
|
||||
unified_perception=request.unified_perception,
|
||||
semantic_layer=request.semantic_layer,
|
||||
plan_view=request.plan_view,
|
||||
show_detections_2d=request.show_detections_2d,
|
||||
show_segmentation=request.show_segmentation,
|
||||
show_cuboids_3d=request.show_cuboids_3d,
|
||||
|
||||
@@ -12,13 +12,24 @@ import zipfile
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any, Final
|
||||
from typing import Annotated, Any, Final, Literal
|
||||
|
||||
import numpy as np
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from fastapi.responses import FileResponse, JSONResponse, Response
|
||||
from PIL import Image
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from k1link.laboratory.canonical_rerun_overlay import (
|
||||
CanonicalLabOverlayArtifact,
|
||||
CanonicalLabOverlayError,
|
||||
CanonicalLabReplayArtifact,
|
||||
_mask_component_boxes,
|
||||
canonical_lab_overlay,
|
||||
canonical_lab_replay,
|
||||
canonical_recording_id,
|
||||
)
|
||||
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
|
||||
from k1link.laboratory.evidence_report import (
|
||||
LaboratoryEvidenceReportError,
|
||||
@@ -34,6 +45,17 @@ from k1link.sessions.canonical_lab_spatial import (
|
||||
RootProvider = Callable[[], Path | None]
|
||||
CanonicalRecordingProvider = Callable[[str], tuple[Path, str] | None]
|
||||
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
|
||||
|
||||
|
||||
class CanonicalLabRerunRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
application_id: Literal["nodedc_mission_core_recorded"]
|
||||
recording_id: str = Field(
|
||||
min_length=1,
|
||||
max_length=128,
|
||||
pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
|
||||
)
|
||||
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
|
||||
_CANONICAL_ROUTE_CHUNK_FRAMES: Final = 24
|
||||
_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||
@@ -57,6 +79,9 @@ def build_vegetation_shadow_lab_router(
|
||||
root_provider: RootProvider = lambda: None,
|
||||
canonical_recording_provider: CanonicalRecordingProvider | None = None,
|
||||
camera_frame_provider: CameraFrameProvider | None = None,
|
||||
jobs_root: Path | None = None,
|
||||
rerun_overlay_cache_root: Path | None = None,
|
||||
ffmpeg_path: Path | None = None,
|
||||
) -> APIRouter:
|
||||
return _build_vegetation_lab_router(
|
||||
prefix="/api/v1/laboratory/vegetation-shadow",
|
||||
@@ -64,6 +89,9 @@ def build_vegetation_shadow_lab_router(
|
||||
root_provider=root_provider,
|
||||
canonical_recording_provider=canonical_recording_provider,
|
||||
camera_frame_provider=camera_frame_provider,
|
||||
jobs_root=jobs_root,
|
||||
rerun_overlay_cache_root=rerun_overlay_cache_root,
|
||||
ffmpeg_path=ffmpeg_path,
|
||||
)
|
||||
|
||||
|
||||
@@ -84,6 +112,9 @@ def _build_vegetation_lab_router(
|
||||
root_provider: RootProvider,
|
||||
canonical_recording_provider: CanonicalRecordingProvider | None = None,
|
||||
camera_frame_provider: CameraFrameProvider | None = None,
|
||||
jobs_root: Path | None = None,
|
||||
rerun_overlay_cache_root: Path | None = None,
|
||||
ffmpeg_path: Path | None = None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(
|
||||
prefix=prefix,
|
||||
@@ -270,6 +301,254 @@ def _build_vegetation_lab_router(
|
||||
},
|
||||
)
|
||||
|
||||
async def canonical_rerun_overlay_artifact(
|
||||
result_id: str,
|
||||
request: CanonicalLabRerunRequest,
|
||||
*,
|
||||
expected_base_generation_sha256: str | None = None,
|
||||
) -> CanonicalLabOverlayArtifact:
|
||||
"""Project LAB-only evidence into the base recording's native clock."""
|
||||
|
||||
if (
|
||||
canonical_recording_provider is None
|
||||
or jobs_root is None
|
||||
or rerun_overlay_cache_root is None
|
||||
or ffmpeg_path is None
|
||||
):
|
||||
raise HTTPException(status_code=503, detail="Canonical LAB Rerun overlay unavailable")
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
manifest = _read_verified(candidate, definition)
|
||||
route, _ = _full_route_context(candidate, manifest)
|
||||
recording = canonical_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
|
||||
if (
|
||||
expected_base_generation_sha256 is not None
|
||||
and expected_base_generation_sha256 != generation_sha256
|
||||
):
|
||||
raise HTTPException(status_code=412, detail="Canonical recording generation changed")
|
||||
try:
|
||||
expected_recording_id = await run_in_threadpool(
|
||||
canonical_recording_id,
|
||||
recording_path,
|
||||
)
|
||||
if request.recording_id != expected_recording_id:
|
||||
raise HTTPException(status_code=412, detail="Canonical recording identity changed")
|
||||
artifact = await run_in_threadpool(
|
||||
canonical_lab_overlay,
|
||||
candidate,
|
||||
manifest,
|
||||
recording_id=request.recording_id,
|
||||
base_generation_sha256=generation_sha256,
|
||||
jobs_root=jobs_root,
|
||||
cache_root=rerun_overlay_cache_root,
|
||||
ffmpeg_path=ffmpeg_path,
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except CanonicalLabOverlayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Canonical LAB Rerun overlay failed verification",
|
||||
) from exc
|
||||
return artifact
|
||||
|
||||
async def canonical_rerun_replay_artifact(
|
||||
result_id: str,
|
||||
*,
|
||||
expected_base_generation_sha256: str | None = None,
|
||||
) -> CanonicalLabReplayArtifact:
|
||||
"""Return one immutable RRD containing base geometry and LAB perception."""
|
||||
|
||||
if (
|
||||
canonical_recording_provider is None
|
||||
or jobs_root is None
|
||||
or rerun_overlay_cache_root is None
|
||||
or ffmpeg_path is None
|
||||
):
|
||||
raise HTTPException(status_code=503, detail="Canonical LAB Rerun replay unavailable")
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
manifest = _read_verified(candidate, definition)
|
||||
route, _ = _full_route_context(candidate, manifest)
|
||||
recording = canonical_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
|
||||
if (
|
||||
expected_base_generation_sha256 is not None
|
||||
and expected_base_generation_sha256 != generation_sha256
|
||||
):
|
||||
raise HTTPException(status_code=412, detail="Canonical recording generation changed")
|
||||
try:
|
||||
recording_id = await run_in_threadpool(canonical_recording_id, recording_path)
|
||||
overlay = await run_in_threadpool(
|
||||
canonical_lab_overlay,
|
||||
candidate,
|
||||
manifest,
|
||||
recording_id=recording_id,
|
||||
base_generation_sha256=generation_sha256,
|
||||
jobs_root=jobs_root,
|
||||
cache_root=rerun_overlay_cache_root,
|
||||
ffmpeg_path=ffmpeg_path,
|
||||
)
|
||||
return await run_in_threadpool(
|
||||
canonical_lab_replay,
|
||||
recording_path,
|
||||
base_generation_sha256=generation_sha256,
|
||||
overlay=overlay,
|
||||
result_id=result_id,
|
||||
recording_id=recording_id,
|
||||
cache_root=rerun_overlay_cache_root,
|
||||
)
|
||||
except CanonicalLabOverlayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Canonical LAB Rerun replay failed verification",
|
||||
) from exc
|
||||
|
||||
def canonical_rerun_overlay_file_response(
|
||||
artifact: CanonicalLabOverlayArtifact,
|
||||
) -> FileResponse:
|
||||
return FileResponse(
|
||||
artifact.path,
|
||||
media_type="application/vnd.rerun.rrd",
|
||||
headers={
|
||||
"Cache-Control": "private, max-age=31536000, immutable",
|
||||
"ETag": f'"{artifact.sha256}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
def canonical_rerun_replay_file_response(
|
||||
artifact: CanonicalLabReplayArtifact,
|
||||
) -> FileResponse:
|
||||
return FileResponse(
|
||||
artifact.path,
|
||||
media_type="application/vnd.rerun.rrd",
|
||||
headers={
|
||||
"Cache-Control": "private, max-age=31536000, immutable",
|
||||
"ETag": f'"{artifact.sha256}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
@router.post("/{result_id}/canonical-overlay.rrd")
|
||||
async def get_canonical_rerun_overlay(
|
||||
result_id: str,
|
||||
request: CanonicalLabRerunRequest,
|
||||
) -> FileResponse:
|
||||
"""Resolve the sealed sidecar for bounded non-viewer consumers."""
|
||||
|
||||
artifact = await canonical_rerun_overlay_artifact(result_id, request)
|
||||
return canonical_rerun_overlay_file_response(artifact)
|
||||
|
||||
@router.head("/{result_id}/canonical-overlay.rrd")
|
||||
async def describe_canonical_rerun_overlay(
|
||||
result_id: str,
|
||||
application_id: Annotated[
|
||||
Literal["nodedc_mission_core_recorded"],
|
||||
Query(),
|
||||
],
|
||||
recording_id: Annotated[
|
||||
str,
|
||||
Query(
|
||||
min_length=1,
|
||||
max_length=128,
|
||||
pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
|
||||
),
|
||||
],
|
||||
generation: Annotated[str, Query(pattern=r"^[a-f0-9]{64}$")],
|
||||
) -> Response:
|
||||
"""Describe the exact AI generation before Rerun opens its immutable URL."""
|
||||
|
||||
artifact = await canonical_rerun_overlay_artifact(
|
||||
result_id,
|
||||
CanonicalLabRerunRequest(
|
||||
application_id=application_id,
|
||||
recording_id=recording_id,
|
||||
),
|
||||
expected_base_generation_sha256=generation,
|
||||
)
|
||||
return Response(
|
||||
status_code=200,
|
||||
media_type="application/vnd.rerun.rrd",
|
||||
headers={
|
||||
"Cache-Control": "private, no-store",
|
||||
"Content-Length": str(artifact.byte_length),
|
||||
"ETag": f'"{artifact.sha256}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Rerun-Format": "RRF2",
|
||||
},
|
||||
)
|
||||
|
||||
@router.get("/{result_id}/canonical-overlay.rrd")
|
||||
async def stream_canonical_rerun_overlay(
|
||||
result_id: str,
|
||||
application_id: Annotated[
|
||||
Literal["nodedc_mission_core_recorded"],
|
||||
Query(),
|
||||
],
|
||||
recording_id: Annotated[
|
||||
str,
|
||||
Query(
|
||||
min_length=1,
|
||||
max_length=128,
|
||||
pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
|
||||
),
|
||||
],
|
||||
generation: Annotated[str, Query(pattern=r"^[a-f0-9]{64}$")],
|
||||
overlay_generation: Annotated[str, Query(pattern=r"^[a-f0-9]{64}$")],
|
||||
) -> FileResponse:
|
||||
"""Stream one immutable LAB sidecar through Rerun's native HTTP receiver."""
|
||||
|
||||
artifact = await canonical_rerun_overlay_artifact(
|
||||
result_id,
|
||||
CanonicalLabRerunRequest(
|
||||
application_id=application_id,
|
||||
recording_id=recording_id,
|
||||
),
|
||||
expected_base_generation_sha256=generation,
|
||||
)
|
||||
if overlay_generation != artifact.sha256:
|
||||
raise HTTPException(status_code=412, detail="Canonical overlay generation changed")
|
||||
return canonical_rerun_overlay_file_response(artifact)
|
||||
|
||||
@router.head("/{result_id}/canonical-replay.rrd")
|
||||
async def describe_canonical_rerun_replay(
|
||||
result_id: str,
|
||||
base_generation: Annotated[str, Query(pattern=r"^[a-f0-9]{64}$")],
|
||||
) -> Response:
|
||||
"""Build once and describe the single RRD consumed by the LAB viewer."""
|
||||
|
||||
artifact = await canonical_rerun_replay_artifact(
|
||||
result_id,
|
||||
expected_base_generation_sha256=base_generation,
|
||||
)
|
||||
return Response(
|
||||
status_code=200,
|
||||
media_type="application/vnd.rerun.rrd",
|
||||
headers={
|
||||
"Cache-Control": "private, no-store",
|
||||
"Content-Length": str(artifact.byte_length),
|
||||
"ETag": f'"{artifact.sha256}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Rerun-Format": "RRF2",
|
||||
},
|
||||
)
|
||||
|
||||
@router.get("/{result_id}/canonical-replay.rrd")
|
||||
async def stream_canonical_rerun_replay(
|
||||
result_id: str,
|
||||
generation: Annotated[str, Query(pattern=r"^[a-f0-9]{64}$")],
|
||||
) -> FileResponse:
|
||||
"""Stream the digest-bound merged replay through one native receiver."""
|
||||
|
||||
artifact = await canonical_rerun_replay_artifact(result_id)
|
||||
if generation != artifact.sha256:
|
||||
raise HTTPException(status_code=412, detail="Canonical replay generation changed")
|
||||
return canonical_rerun_replay_file_response(artifact)
|
||||
|
||||
@router.get("/{result_id}/timeline")
|
||||
def get_canonical_route_timeline(result_id: str) -> dict[str, object]:
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
@@ -811,80 +1090,6 @@ def _semantic_component_proposals_cached(
|
||||
return tuple(proposals[:32])
|
||||
|
||||
|
||||
def _mask_component_boxes(
|
||||
mask: np.ndarray,
|
||||
class_id: int,
|
||||
*,
|
||||
minimum_pixels: int,
|
||||
) -> list[tuple[int, int, int, int, int]]:
|
||||
"""Return 8-connected run-length components without an OpenCV dependency."""
|
||||
|
||||
if mask.ndim != 2 or minimum_pixels < 1:
|
||||
return []
|
||||
parents: list[int] = []
|
||||
runs: list[tuple[int, int, int, int]] = []
|
||||
|
||||
def root(index: int) -> int:
|
||||
while parents[index] != index:
|
||||
parents[index] = parents[parents[index]]
|
||||
index = parents[index]
|
||||
return index
|
||||
|
||||
def union(left: int, right: int) -> None:
|
||||
left_root = root(left)
|
||||
right_root = root(right)
|
||||
if left_root != right_root:
|
||||
parents[right_root] = left_root
|
||||
|
||||
previous: list[int] = []
|
||||
for row_index, row in enumerate(mask):
|
||||
matches = np.flatnonzero(row == class_id)
|
||||
if matches.size == 0:
|
||||
previous = []
|
||||
continue
|
||||
split_at = np.flatnonzero(np.diff(matches) > 1) + 1
|
||||
groups = np.split(matches, split_at)
|
||||
current: list[int] = []
|
||||
previous_cursor = 0
|
||||
for group in groups:
|
||||
start = int(group[0])
|
||||
stop = int(group[-1]) + 1
|
||||
run_index = len(runs)
|
||||
runs.append((row_index, start, stop, stop - start))
|
||||
parents.append(run_index)
|
||||
current.append(run_index)
|
||||
while (
|
||||
previous_cursor < len(previous)
|
||||
and runs[previous[previous_cursor]][2] < start
|
||||
):
|
||||
previous_cursor += 1
|
||||
candidate_cursor = previous_cursor
|
||||
while candidate_cursor < len(previous):
|
||||
previous_index = previous[candidate_cursor]
|
||||
_, previous_start, previous_stop, _ = runs[previous_index]
|
||||
if previous_start > stop:
|
||||
break
|
||||
union(run_index, previous_index)
|
||||
candidate_cursor += 1
|
||||
previous = current
|
||||
|
||||
components: dict[int, list[int]] = {}
|
||||
for run_index, (row, start, stop, count) in enumerate(runs):
|
||||
component = components.setdefault(root(run_index), [start, row, stop, row + 1, 0])
|
||||
component[0] = min(component[0], start)
|
||||
component[1] = min(component[1], row)
|
||||
component[2] = max(component[2], stop)
|
||||
component[3] = max(component[3], row + 1)
|
||||
component[4] += count
|
||||
result = [
|
||||
(left, top, right, bottom, count)
|
||||
for left, top, right, bottom, count in components.values()
|
||||
if count >= minimum_pixels and right - left >= 2 and bottom - top >= 3
|
||||
]
|
||||
result.sort(key=lambda box: (-box[4], box[1], box[0]))
|
||||
return result
|
||||
|
||||
|
||||
def _route_tgs_anchor_payload(path: Path, source_sequence: int) -> dict[str, object]:
|
||||
before = path.stat()
|
||||
with np.load(path, allow_pickle=False) as archive:
|
||||
|
||||
@@ -594,9 +594,9 @@ def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset(
|
||||
assert eye_control_updates == [True, False, True, False, True]
|
||||
assert [activation[1:] for activation in activations] == [
|
||||
(True, False),
|
||||
(True, False),
|
||||
(True, False),
|
||||
(True, False),
|
||||
(False, False),
|
||||
(False, False),
|
||||
(False, False),
|
||||
(True, False),
|
||||
]
|
||||
assert activations[0][0] is activations[1][0]
|
||||
|
||||
@@ -14,10 +14,23 @@ from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from PIL import Image
|
||||
|
||||
import k1link.laboratory.canonical_rerun_overlay as canonical_overlay_module
|
||||
import k1link.laboratory.vegetation_policy_review as policy_review_module
|
||||
import k1link.laboratory.vegetation_policy_video as policy_video_module
|
||||
import k1link.laboratory.vegetation_shadow_lab as vegetation_lab_module
|
||||
import k1link.web.vegetation_shadow_lab_api as vegetation_api_module
|
||||
from k1link.laboratory import LaboratoryEvidenceRegistry
|
||||
from k1link.laboratory.canonical_rerun_overlay import (
|
||||
CanonicalLabOverlayArtifact,
|
||||
CanonicalLabReplayArtifact,
|
||||
_artifact_is_regular,
|
||||
_encoded_semantic_png,
|
||||
_localized_semantic_label,
|
||||
_optimize_overlay,
|
||||
_semantic_palette,
|
||||
_video_reference_timestamps,
|
||||
canonical_lab_replay,
|
||||
)
|
||||
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
|
||||
@@ -41,6 +54,319 @@ def test_semantic_component_boxes_keep_distinct_objects_separate() -> None:
|
||||
(18, 4, 24, 12, 48),
|
||||
(3, 2, 8, 10, 40),
|
||||
]
|
||||
_, labels = canonical_overlay_module.semantic_component_boxes(mask, 0)
|
||||
assert labels == [
|
||||
"автомобиль · 50%",
|
||||
"автомобиль · 50%",
|
||||
]
|
||||
|
||||
|
||||
def test_canonical_overlay_localizes_current_taxonomies() -> None:
|
||||
assert _localized_semantic_label("high_grass") == "высокая трава"
|
||||
assert _localized_semantic_label("tree_trunk") == "ствол дерева"
|
||||
assert _localized_semantic_label("future_class") == "future_class"
|
||||
|
||||
|
||||
def test_canonical_video_references_hold_only_missing_source_samples() -> None:
|
||||
session_times = np.arange(10, dtype=np.int64) * 100_000_000 + 39_000_000_000
|
||||
video_times = np.array(
|
||||
[0, 100, 200, 300, 400, 500, 600, 700, 900],
|
||||
dtype=np.int64,
|
||||
) * 1_000_000
|
||||
|
||||
references = _video_reference_timestamps(video_times, session_times)
|
||||
|
||||
assert references.tolist() == [
|
||||
0,
|
||||
100_000_000,
|
||||
200_000_000,
|
||||
300_000_000,
|
||||
400_000_000,
|
||||
500_000_000,
|
||||
600_000_000,
|
||||
700_000_000,
|
||||
700_000_000,
|
||||
900_000_000,
|
||||
]
|
||||
|
||||
|
||||
def test_canonical_overlay_memory_cache_rejects_same_size_tampering(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
path = tmp_path / "overlay.rrd"
|
||||
path.write_bytes(b"RRF2-original")
|
||||
artifact = CanonicalLabOverlayArtifact(
|
||||
path=path,
|
||||
byte_length=path.stat().st_size,
|
||||
sha256=_sha256(path),
|
||||
)
|
||||
assert _artifact_is_regular(artifact)
|
||||
|
||||
path.write_bytes(b"RRF2-tampered")
|
||||
|
||||
assert path.stat().st_size == artifact.byte_length
|
||||
assert not _artifact_is_regular(artifact)
|
||||
|
||||
|
||||
def test_canonical_overlay_keeps_semantics_as_palette_encoded_png() -> None:
|
||||
mask = np.zeros((600, 800), dtype=np.uint8)
|
||||
mask[120:420, 200:600] = 7
|
||||
palette = _semantic_palette(
|
||||
[
|
||||
{"class_id": 0, "color_rgb": [0, 0, 0]},
|
||||
{"class_id": 7, "color_rgb": [255, 47, 128]},
|
||||
]
|
||||
)
|
||||
|
||||
encoded = _encoded_semantic_png(mask, palette)
|
||||
|
||||
assert len(encoded) < mask.nbytes // 20
|
||||
with Image.open(io.BytesIO(encoded)) as image:
|
||||
assert image.mode == "P"
|
||||
assert image.getpixel((0, 0)) == 0
|
||||
assert image.getpixel((300, 300)) == 7
|
||||
assert image.getpalette()[7 * 3 : 7 * 3 + 3] == [255, 47, 128]
|
||||
|
||||
|
||||
def test_canonical_overlay_compacts_chunks_before_cache_publication(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
source = tmp_path / "source.rrd"
|
||||
source.write_bytes(b"RRF2-source")
|
||||
|
||||
def optimize(command: list[str], **options: object) -> SimpleNamespace:
|
||||
assert command[:4] == [
|
||||
canonical_overlay_module.sys.executable,
|
||||
"-m",
|
||||
"rerun",
|
||||
"rrd",
|
||||
]
|
||||
assert command[4:13] == [
|
||||
"optimize",
|
||||
"--profile",
|
||||
"object-store",
|
||||
"--max-size",
|
||||
"4MiB",
|
||||
"--max-rows",
|
||||
"512",
|
||||
"--num-pass",
|
||||
"20",
|
||||
]
|
||||
assert command[13] == str(source)
|
||||
assert command[14] == "-o"
|
||||
Path(command[15]).write_bytes(b"RRF2-optimized")
|
||||
assert options == {"check": False, "capture_output": True, "timeout": 120}
|
||||
return SimpleNamespace(returncode=0, stderr=b"")
|
||||
|
||||
monkeypatch.setattr(canonical_overlay_module.subprocess, "run", optimize)
|
||||
|
||||
_optimize_overlay(source)
|
||||
|
||||
assert source.read_bytes() == b"RRF2-optimized"
|
||||
|
||||
|
||||
def test_canonical_replay_merges_base_and_overlay_once(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
base = tmp_path / "base.rrd"
|
||||
base.write_bytes(b"RRF2-base")
|
||||
overlay_path = tmp_path / "overlay.rrd"
|
||||
overlay_path.write_bytes(b"RRF2-overlay")
|
||||
overlay = CanonicalLabOverlayArtifact(
|
||||
path=overlay_path,
|
||||
byte_length=overlay_path.stat().st_size,
|
||||
sha256=_sha256(overlay_path),
|
||||
)
|
||||
calls = 0
|
||||
|
||||
def optimize(command: list[str], **options: object) -> SimpleNamespace:
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
assert command[13:15] == [str(base), str(overlay_path)]
|
||||
assert command[15] == "-o"
|
||||
Path(command[16]).write_bytes(b"RRF2-merged")
|
||||
assert options == {"check": False, "capture_output": True, "timeout": 180}
|
||||
return SimpleNamespace(returncode=0, stderr=b"")
|
||||
|
||||
monkeypatch.setattr(canonical_overlay_module.subprocess, "run", optimize)
|
||||
monkeypatch.setattr(
|
||||
canonical_overlay_module,
|
||||
"canonical_recording_id",
|
||||
lambda _path: "recording-001",
|
||||
)
|
||||
result_id = f"lab-v1-vegetation-shadow-{'e' * 64}"
|
||||
first = canonical_lab_replay(
|
||||
base,
|
||||
base_generation_sha256=_sha256(base),
|
||||
overlay=overlay,
|
||||
result_id=result_id,
|
||||
recording_id="recording-001",
|
||||
cache_root=tmp_path / "cache",
|
||||
)
|
||||
second = canonical_lab_replay(
|
||||
base,
|
||||
base_generation_sha256=_sha256(base),
|
||||
overlay=overlay,
|
||||
result_id=result_id,
|
||||
recording_id="recording-001",
|
||||
cache_root=tmp_path / "cache",
|
||||
)
|
||||
|
||||
assert first == second
|
||||
assert first.path.read_bytes() == b"RRF2-merged"
|
||||
assert calls == 1
|
||||
|
||||
|
||||
def test_canonical_overlay_get_is_generation_bound_and_range_streamable(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
result_id = f"lab-v1-vegetation-shadow-{'a' * 64}"
|
||||
result_root = tmp_path / result_id
|
||||
result_root.mkdir()
|
||||
base = tmp_path / "base.rrd"
|
||||
base.write_bytes(b"RRF2-base")
|
||||
overlay = tmp_path / "overlay.rrd"
|
||||
overlay.write_bytes(b"RRF2-overlay")
|
||||
replay = tmp_path / "replay.rrd"
|
||||
replay.write_bytes(b"RRF2-replay")
|
||||
generation = "b" * 64
|
||||
recording_id = "recording-001"
|
||||
artifact = CanonicalLabOverlayArtifact(
|
||||
path=overlay,
|
||||
byte_length=overlay.stat().st_size,
|
||||
sha256=_sha256(overlay),
|
||||
)
|
||||
replay_artifact = CanonicalLabReplayArtifact(
|
||||
path=replay,
|
||||
byte_length=replay.stat().st_size,
|
||||
sha256=_sha256(replay),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
vegetation_api_module,
|
||||
"_resolve_candidate",
|
||||
lambda *_args, **_kwargs: result_root,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
vegetation_api_module,
|
||||
"_read_verified",
|
||||
lambda *_args, **_kwargs: {},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
vegetation_api_module,
|
||||
"_full_route_context",
|
||||
lambda *_args, **_kwargs: ({"session_id": "session-001"}, ()),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
vegetation_api_module,
|
||||
"canonical_recording_id",
|
||||
lambda _path: recording_id,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
vegetation_api_module,
|
||||
"canonical_lab_overlay",
|
||||
lambda *_args, **_kwargs: artifact,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
vegetation_api_module,
|
||||
"canonical_lab_replay",
|
||||
lambda *_args, **_kwargs: replay_artifact,
|
||||
)
|
||||
ffmpeg = tmp_path / "ffmpeg"
|
||||
ffmpeg.write_bytes(b"fixture")
|
||||
ffmpeg.chmod(0o700)
|
||||
app = FastAPI()
|
||||
app.include_router(
|
||||
build_vegetation_shadow_lab_router(
|
||||
root_provider=lambda: tmp_path,
|
||||
canonical_recording_provider=lambda _session_id: (base, generation),
|
||||
jobs_root=tmp_path,
|
||||
rerun_overlay_cache_root=tmp_path / "cache",
|
||||
ffmpeg_path=ffmpeg,
|
||||
)
|
||||
)
|
||||
client = TestClient(app)
|
||||
endpoint = f"/api/v1/laboratory/vegetation-shadow/{result_id}/canonical-overlay.rrd"
|
||||
descriptor = client.head(
|
||||
endpoint,
|
||||
params={
|
||||
"application_id": "nodedc_mission_core_recorded",
|
||||
"recording_id": recording_id,
|
||||
"generation": generation,
|
||||
},
|
||||
)
|
||||
assert descriptor.status_code == 200
|
||||
assert descriptor.content == b""
|
||||
assert descriptor.headers["content-length"] == str(artifact.byte_length)
|
||||
assert descriptor.headers["etag"] == f'"{artifact.sha256}"'
|
||||
assert descriptor.headers["x-rerun-format"] == "RRF2"
|
||||
assert descriptor.headers["cache-control"] == "private, no-store"
|
||||
|
||||
response = client.get(
|
||||
endpoint,
|
||||
params={
|
||||
"application_id": "nodedc_mission_core_recorded",
|
||||
"recording_id": recording_id,
|
||||
"generation": generation,
|
||||
"overlay_generation": artifact.sha256,
|
||||
},
|
||||
headers={"Range": "bytes=0-3"},
|
||||
)
|
||||
assert response.status_code == 206
|
||||
assert response.content == b"RRF2"
|
||||
assert response.headers["content-range"] == f"bytes 0-3/{artifact.byte_length}"
|
||||
assert response.headers["etag"] == f'"{artifact.sha256}"'
|
||||
assert response.headers["cache-control"].endswith("immutable")
|
||||
|
||||
stale = client.head(
|
||||
endpoint,
|
||||
params={
|
||||
"application_id": "nodedc_mission_core_recorded",
|
||||
"recording_id": recording_id,
|
||||
"generation": "c" * 64,
|
||||
},
|
||||
)
|
||||
assert stale.status_code == 412
|
||||
|
||||
stale_overlay = client.get(
|
||||
endpoint,
|
||||
params={
|
||||
"application_id": "nodedc_mission_core_recorded",
|
||||
"recording_id": recording_id,
|
||||
"generation": generation,
|
||||
"overlay_generation": "d" * 64,
|
||||
},
|
||||
)
|
||||
assert stale_overlay.status_code == 412
|
||||
|
||||
replay_endpoint = (
|
||||
f"/api/v1/laboratory/vegetation-shadow/{result_id}/canonical-replay.rrd"
|
||||
)
|
||||
replay_descriptor = client.head(
|
||||
replay_endpoint,
|
||||
params={"base_generation": generation},
|
||||
)
|
||||
assert replay_descriptor.status_code == 200
|
||||
assert replay_descriptor.headers["content-length"] == str(replay_artifact.byte_length)
|
||||
assert replay_descriptor.headers["etag"] == f'"{replay_artifact.sha256}"'
|
||||
assert replay_descriptor.headers["x-rerun-format"] == "RRF2"
|
||||
|
||||
replay_response = client.get(
|
||||
replay_endpoint,
|
||||
params={"generation": replay_artifact.sha256},
|
||||
headers={"Range": "bytes=0-3"},
|
||||
)
|
||||
assert replay_response.status_code == 206
|
||||
assert replay_response.content == b"RRF2"
|
||||
assert replay_response.headers["etag"] == f'"{replay_artifact.sha256}"'
|
||||
|
||||
stale_replay = client.get(
|
||||
replay_endpoint,
|
||||
params={"generation": "f" * 64},
|
||||
)
|
||||
assert stale_replay.status_code == 412
|
||||
|
||||
|
||||
def test_route_playback_chunk_descriptor_seals_only_requested_binary_window() -> None:
|
||||
|
||||
@@ -355,7 +355,7 @@ requires-dist = [
|
||||
{ name = "paho-mqtt", specifier = ">=2.1,<3" },
|
||||
{ name = "pillow", specifier = ">=12,<13" },
|
||||
{ name = "pyyaml", specifier = ">=6.0,<7" },
|
||||
{ name = "rerun-sdk", specifier = "==0.34.1" },
|
||||
{ name = "rerun-sdk", specifier = "==0.36.3" },
|
||||
{ name = "rich", specifier = ">=13.9,<15" },
|
||||
{ name = "typer", specifier = ">=0.15,<1" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.35,<1" },
|
||||
@@ -618,7 +618,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "rerun-sdk"
|
||||
version = "0.34.1"
|
||||
version = "0.36.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
@@ -629,10 +629,10 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/37/41422eca73b5f933872ad073d17034f47ccac730fc4a29b9064d73c74424/rerun_sdk-0.34.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:32d3fb3e9f46eb84433427dc6add6ef7508bbd36295dcc80d693dccacf936e6b", size = 133584987, upload-time = "2026-07-07T17:43:14.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/f7/c2e5097f0138a3c4d70e2fe1c24781fac9aec351324c2ea5b15f4d6971b8/rerun_sdk-0.34.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:57585f9c77af6d8a5ee0342bd79b87431b632bf7158e9b8dd4756d6ccd7b7feb", size = 142889929, upload-time = "2026-07-07T17:43:20.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/fa/f87899a8c0cc36901d32069072340b894654fa6f3b3a2a74913565c3661a/rerun_sdk-0.34.1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:60c35adb49f04bd64bf1a426b59f61aeee8312fa7cc2624878b43186e817b2d4", size = 147505605, upload-time = "2026-07-07T17:43:25.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/3a/07125af48ef024c573acadf1bfedd406bcc838ffa1af0d7b20bd79895267/rerun_sdk-0.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:3448b34d385994a32caaa4e3ffd42b69069ab7791382d8936089e4928ca24cd6", size = 126554967, upload-time = "2026-07-07T17:43:30.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ff/e8ce81a4fa451f232abb10f0737c5fa8c812a4b2979bda2f655c58c478eb/rerun_sdk-0.36.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:804067d6e71747f8461104988be82543b318b583a2837935558a5e79a5e9befd", size = 147513390, upload-time = "2026-08-24T18:59:12.502Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/0d/a44bccfa279d043929d595a97c220ce3151dd2ef39d6b0477abfa6abc5c0/rerun_sdk-0.36.3-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:954d8980e15e71245ac91ba7120bbaeb624df2bd7a47bbbb8856fd45174d2521", size = 157793360, upload-time = "2026-08-24T18:59:20.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/30/3dae429550a667bc0536bd419864c8574de908557aaadcc136082cb6f17c/rerun_sdk-0.36.3-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:287059b7154bf3881f5b32035f5772d0556d55a0a894650fb74a2605fb39afbe", size = 163018185, upload-time = "2026-08-24T18:59:30.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/97/3ea34c7e892474d940759fe18045fe29e245ac5c5b82e526088c388ba854/rerun_sdk-0.36.3-cp310-abi3-win_amd64.whl", hash = "sha256:f84441a3e1d1d679f6fe5a319cac86ccc3992f8f88bfa72517f76d8b45bf066c", size = 140659012, upload-time = "2026-08-24T18:59:37.699Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user