refactor(lab): перевести RAV004 на канонический Rerun pipeline

This commit is contained in:
DCCONSTRUCTIONS
2026-08-30 12:59:16 +03:00
parent e9ffb829c9
commit f5ee42751d
30 changed files with 1425 additions and 288 deletions
+4 -5
View File
@@ -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": {
+1 -2
View File
@@ -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",
@@ -151,6 +151,7 @@ interface RecordedRerunIdentity {
const RECORDED_RRD_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/recording\.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 +204,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,
@@ -238,12 +234,6 @@ 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 {
const normalized = sourceUrl.trim();
if (!RECORDED_RRD_PATH.test(normalized)) return null;
@@ -365,6 +355,9 @@ export async function fetchRecordedBlueprintRrd(
activeView = "spatial",
viewResetGeneration = 0,
followTrajectory = false,
semanticLayer,
unifiedPerception,
planView = false,
perceptionLayers = {
enabled: false,
detections2d: false,
@@ -379,10 +372,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 +399,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 +436,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 +489,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)
) {
@@ -588,6 +591,10 @@ export function RerunViewport({
segmentation: false,
cuboids3d: false,
};
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 =
@@ -621,9 +628,11 @@ export function RerunViewport({
const recordedBlueprintUrl = sourceUrl
? resolveRecordedBlueprintUrl(sourceUrl, window.location.origin)
: null;
const recordedPerceptionUrl = sourceUrl
? resolveRecordedPerceptionUrl(sourceUrl, window.location.origin)
: 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;
@@ -1264,7 +1273,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 +1320,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 +1342,6 @@ export function RerunViewport({
rerunViewerInitialSource(resolvedSource),
host,
viewerOptions,
rerunViewerOpenOptions(followLive),
);
if (disposed) {
disposeViewer();
@@ -1429,6 +1434,7 @@ export function RerunViewport({
recordedArtifact?.sha256,
recordedArtifact?.sourceUrl,
recordedArtifact?.viewerSourceUrl,
recordedPerceptionUrl,
retryNonce,
]);
@@ -1484,6 +1490,7 @@ export function RerunViewport({
message: "Сервер готовит AI-слои.",
});
const pollPreparationStatus = () => {
if (!RECORDED_PERCEPTION_PATH.test(new URL(recordedPerceptionUrl).pathname)) return;
void fetchPerceptionPreparationStatus(
recordedPerceptionUrl,
identity.recordingId,
@@ -1704,6 +1711,9 @@ export function RerunViewport({
viewResetGeneration: recordedViewResetGeneration,
followTrajectory: recordedFollowTrajectory,
perceptionLayers: recordedPerceptionLayers,
semanticLayer: recordedSemanticLayer,
unifiedPerception: recordedUnifiedPerception,
planView: recordedPlanView,
}).then((payload) => {
if (
abort.signal.aborted ||
@@ -1725,6 +1735,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;
@@ -220,7 +224,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 />}
@@ -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;
@@ -67,20 +67,29 @@ export interface RecordedSessionRerunProfile {
viewResetGeneration: 0 | 1;
followTrajectory: boolean;
perceptionLayers: RecordedPerceptionLayers;
/** 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 +106,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;
+1 -1
View File
@@ -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,260 @@
import { useEffect, useMemo, useState } from "react";
import { Button, Icon, SegmentedControl } from "@nodedc/ui-react";
import { ObservationTimeline } from "../../components/ObservationTimeline";
import {
RerunViewport,
type RecordedPerceptionLoadState,
type RerunPlaybackController,
type RerunPlaybackState,
} from "../../components/RerunViewport";
import {
CanonicalRecordedLabReplay,
useCanonicalRecordedLabReplayState,
} from "../../components/laboratory/CanonicalRecordedLabReplay";
import type { VegetationFullRouteReview } from "../../core/laboratory/vegetationShadow";
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";
const EMPTY_PERCEPTION_LOAD: RecordedPerceptionLoadState = {
phase: "idle",
receivedBytes: 0,
totalBytes: null,
progress: null,
message: "",
};
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 [perceptionLoad, setPerceptionLoad] =
useState<RecordedPerceptionLoadState>(EMPTY_PERCEPTION_LOAD);
const [launch, setLaunch] = useState<Awaited<ReturnType<typeof resolveObservationSessionReplay>> | 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((value) => {
if (!controller.signal.aborted) setLaunch(value);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setLaunchError(
caught instanceof Error ? caught.message : "Каноническая запись RAV004 недоступна.",
);
}
});
return () => controller.abort();
}, [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: 2.2,
}), [spatialLayer, spatialMode]);
const profile = launch ? recordedSessionRerunProfile({
sourceUrl: launch.sourceUrl,
artifact: {
sourceUrl: launch.sourceUrl,
viewerSourceUrl: launch.viewerSourceUrl,
byteLength: launch.byteLength,
sha256: launch.sha256,
},
autoplayWhenReady: false,
presentationGate: "ready",
expectedTimelineStartSeconds: launch.timelineStartSeconds,
expectedTimelineEndSeconds: launch.timelineEndSeconds,
initialPlaybackStartSeconds: review.timelineStartSeconds,
view: mediaMode !== null ? "perception" : "spatial",
viewResetGeneration,
followTrajectory: false,
perceptionSourceUrl:
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}` +
"/canonical-overlay.rrd",
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="compact"
shape="pill"
variant={showSemantics ? "primary" : "secondary"}
aria-pressed={showSemantics}
onClick={() => setShowSemantics((visible) => !visible)}
>
SEMANTICS
</Button>
<SegmentedControl
value={semanticLayer}
items={[
{ value: "city", label: "ГОРОД · EoMT" },
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
]}
label="Источник семантики"
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: "SOURCE POINTS" },
{ value: "local", label: "LOCAL SLAM" },
{ value: "tgs", label: "TGS COSTMAP", disabled: true },
{ value: "semantic", label: "SEMANTICS", disabled: true },
]}
label="Пространственные слои"
onChange={setSpatialLayer}
/>
</div>
);
const resetSpatialView = (
<Button
size="compact"
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;
const overlayMessage = launchError
?? (perceptionLoad.phase === "loading" ? perceptionLoad.message : null)
?? (perceptionLoad.phase === "error" ? perceptionLoad.message : null);
return (
<CanonicalRecordedLabReplay
label="RAVNOVES004TREE · upstream Rerun recorded replay"
mediaMode={mediaMode ?? "none"}
mediaModes={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
]}
spatialMode={spatialMode ?? "none"}
spatialModes={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
]}
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}
onPerceptionLoadChange={setPerceptionLoad}
/>
) : (
<div className="l3-visual-audit__state" role={launchError ? "alert" : "status"}>
{launchError ?? "Открываем каноническую запись RAV004…"}
</div>
)}
emptyMessage="Выберите VIDEO/CAMERA или 3D/PLAN. Общий Rerun-clock останется на месте."
deckOverlays={overlayMessage ? (
<div className="m4-replay-threat-visual__buffering" role="status">
{perceptionLoad.phase === "loading" ? (
<span className="busy-indicator" aria-hidden="true" />
) : (
<Icon name="alert" size={14} />
)}
<span>{overlayMessage}</span>
</div>
) : undefined}
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({
@@ -485,6 +485,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,
@@ -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,11 +69,6 @@ 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("live presentation waits for the exact receiver to expose a usable range", () => {
assert.equal(isLiveRerunPresentationReady(false, { min: 1, max: 2 }, 1), false);
assert.equal(isLiveRerunPresentationReady(true, null, 1), false);
@@ -216,7 +209,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\(\);/,
@@ -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/);
@@ -478,18 +482,20 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr
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, /<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, /SOURCE POINTS/);
assert.match(rerunSource, /LOCAL SLAM/);
assert.match(rerunSource, /TGS COSTMAP/);
assert.match(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 COSTMAP", disabled: true/);
assert.match(canonicalSource, /primary=\{mediaPane\}/);
assert.match(canonicalSource, /secondary=\{spatialPane/);
assert.match(canonicalSource, /missioncore\.canonical-recorded-lab-replay\/v1/);
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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.
+13
View File
@@ -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
@@ -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:
@@ -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
View File
@@ -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",
@@ -0,0 +1,626 @@
"""Native Rerun sidecar for immutable recorded laboratory evidence.
The sidecar deliberately contains only evidence missing from the canonical K1
recording: camera video, semantic images and diagnostic 2D boxes. The base RRD
continues to own poses, point clouds and trajectory. Both files use the same
Rerun recording id and ``session_time`` timeline, so the upstream viewer is the
only playback clock and the only spatial renderer.
"""
from __future__ import annotations
import hashlib
import io
import json
import os
import subprocess
import tempfile
import threading
import zipfile
from contextlib import suppress
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from typing import Any, Final
from uuid import uuid4
import numpy as np
import rerun as rr
import rerun_bindings as bindings
from PIL import Image
APPLICATION_ID: Final = "nodedc_mission_core_recorded"
SESSION_TIMELINE: Final = "session_time"
RENDERER_VERSION: Final = "upstream-rerun-0.36.3-v2"
MAX_SOURCE_BYTES: Final = 768 * 1024 * 1024
MAX_OVERLAY_BYTES: Final = 512 * 1024 * 1024
class CanonicalLabOverlayError(RuntimeError):
"""A sealed LAB result could not be projected into a native Rerun sidecar."""
@dataclass(frozen=True, slots=True)
class CanonicalLabOverlayArtifact:
path: Path
byte_length: int
sha256: str
_render_lock = threading.Lock()
_memory_cache: dict[tuple[str, str, str], CanonicalLabOverlayArtifact] = {}
def canonical_recording_id(path: Path) -> str:
"""Read the single data-store identity from a sealed base RRD."""
try:
entries = bindings.RrdReaderInternal(str(path.resolve(strict=True))).store_entries()
matches = [
entry.recording_id
for entry in entries
if entry.kind == "recording" and entry.application_id == APPLICATION_ID
]
except Exception as exc:
raise CanonicalLabOverlayError("canonical recording identity is unavailable") from exc
if len(matches) != 1 or not matches[0] or len(matches[0]) > 128:
raise CanonicalLabOverlayError("canonical recording identity is ambiguous")
return str(matches[0])
def canonical_lab_overlay(
result_root: Path,
manifest: dict[str, Any],
*,
recording_id: str,
base_generation_sha256: str,
jobs_root: Path,
cache_root: Path,
ffmpeg_path: Path,
) -> CanonicalLabOverlayArtifact:
"""Return one cached, digest-bound RRD sidecar for a full-route LAB result."""
root = result_root.expanduser().resolve(strict=True)
jobs = jobs_root.expanduser().resolve(strict=True)
ffmpeg = ffmpeg_path.expanduser().resolve(strict=True)
cache = cache_root.expanduser().absolute()
result_id = str(manifest.get("result_id", ""))
result_path = root / "result.json"
result_sha256 = _sha256(result_path)
if (
root.name != result_id
or not result_id.startswith("lab-v1-vegetation-shadow-")
or len(result_id) != len("lab-v1-vegetation-shadow-") + 64
or not recording_id
or len(recording_id) > 128
or not _is_sha256(base_generation_sha256)
or not ffmpeg.is_file()
or not os.access(ffmpeg, os.X_OK)
):
raise CanonicalLabOverlayError("canonical LAB overlay identity is invalid")
key = (result_id, recording_id, base_generation_sha256)
cached = _memory_cache.get(key)
if cached is not None and _artifact_is_regular(cached):
return cached
with _render_lock:
cached = _memory_cache.get(key)
if cached is not None and _artifact_is_regular(cached):
return cached
cache.mkdir(parents=True, exist_ok=True)
if cache.is_symlink() or not cache.is_dir():
raise CanonicalLabOverlayError("canonical LAB overlay cache is invalid")
identity = hashlib.sha256(
"\0".join(
(RENDERER_VERSION, result_sha256, recording_id, base_generation_sha256)
).encode()
).hexdigest()
output = cache / f"{identity}.rrd"
sidecar = cache / f"{identity}.json"
restored = _restore_cached(
output,
sidecar,
result_id=result_id,
result_sha256=result_sha256,
recording_id=recording_id,
base_generation_sha256=base_generation_sha256,
)
if restored is not None:
_memory_cache[key] = restored
return restored
temporary = cache / f".{identity}.{uuid4().hex}.rrd"
proxy: Path | None = None
source: Path | None = None
try:
route = _full_route(manifest)
source = _verified_camera_source(root, route, jobs)
proxy = _camera_proxy(source, int(route["frame_count"]), ffmpeg, cache)
_render_overlay(temporary, root, route, recording_id, proxy)
stat = temporary.stat()
if stat.st_size < 4 or stat.st_size > MAX_OVERLAY_BYTES:
raise CanonicalLabOverlayError("canonical LAB overlay size is invalid")
digest = _sha256(temporary)
os.chmod(temporary, 0o600)
os.replace(temporary, output)
_write_json_atomic(
sidecar,
{
"schema_version": "missioncore.canonical-lab-rerun-overlay/v1",
"renderer_version": RENDERER_VERSION,
"result_id": result_id,
"result_sha256": result_sha256,
"recording_id": recording_id,
"base_generation_sha256": base_generation_sha256,
"byte_length": stat.st_size,
"sha256": digest,
},
)
artifact = CanonicalLabOverlayArtifact(output, stat.st_size, digest)
_memory_cache[key] = artifact
return artifact
except CanonicalLabOverlayError:
raise
except Exception as exc:
raise CanonicalLabOverlayError("failed to render canonical LAB overlay") from exc
finally:
temporary.unlink(missing_ok=True)
if proxy is not None:
proxy.unlink(missing_ok=True)
if source is not None:
source.unlink(missing_ok=True)
def _full_route(manifest: dict[str, Any]) -> dict[str, Any]:
route = manifest.get("route_full_review")
layers = route.get("layers") if isinstance(route, dict) else None
if (
not isinstance(route, dict)
or route.get("source_id") != "RAVNOVES004TREE"
or route.get("frame_count") != 6830
or route.get("width") != 800
or route.get("height") != 600
or not isinstance(layers, dict)
or set(layers) != {"city", "vegetation"}
):
raise CanonicalLabOverlayError("full-route LAB contract is unavailable")
return route
def _verified_camera_source(root: Path, route: dict[str, Any], jobs_root: Path) -> Path:
source_job_id = route.get("source_job_id")
proof = route.get("proofs", {}).get("job") if isinstance(route.get("proofs"), dict) else None
if (
not isinstance(source_job_id, str)
or not source_job_id.startswith("recorded-camera-")
or not isinstance(proof, dict)
or proof.get("path") != "proofs/job.json"
or not _is_sha256(proof.get("sha256"))
):
raise CanonicalLabOverlayError("camera job binding is invalid")
proof_path = root / "proofs" / "job.json"
job_root = (jobs_root / source_job_id).resolve(strict=True)
job_path = job_root / "job.json"
if (
not job_root.is_relative_to(jobs_root)
or _sha256(proof_path) != proof["sha256"]
or _sha256(job_path) != proof["sha256"]
):
raise CanonicalLabOverlayError("camera job proof changed")
job = json.loads(job_path.read_text(encoding="utf-8"))
source = job.get("input") if isinstance(job, dict) else None
files = source.get("files") if isinstance(source, dict) else None
frame_count = int(route["frame_count"])
if (
not isinstance(source, dict)
or source.get("session_id") != route.get("session_id")
or source.get("source_id") != "sensor.camera.right"
or source.get("segment_count") != frame_count
or source.get("byte_length", 0) > MAX_SOURCE_BYTES
or not isinstance(files, list)
):
raise CanonicalLabOverlayError("camera job source contract changed")
epoch_prefix = PurePosixPath(
"input/camera/sensor.camera.right"
) / f"epoch-{source.get('codec_epoch')}"
required = [epoch_prefix / "init.mp4"] + [
epoch_prefix / "segments" / f"{index}.m4s"
for index in range(1, frame_count + 1)
]
descriptors = {
item.get("path"): item
for item in files
if isinstance(item, dict) and isinstance(item.get("path"), str)
}
temporary_descriptor, temporary_name = tempfile.mkstemp(
prefix=".canonical-lab-source-", suffix=".mp4", dir=root.parent
)
output = Path(temporary_name)
total = 0
try:
with os.fdopen(temporary_descriptor, "wb") as stream:
for relative in required:
descriptor = descriptors.get(str(relative))
path = (job_root / relative).resolve(strict=True)
if (
descriptor is None
or not path.is_relative_to(job_root)
or path.is_symlink()
or descriptor.get("byte_length") != path.stat().st_size
or not _is_sha256(descriptor.get("sha256"))
):
raise CanonicalLabOverlayError("camera fragment contract changed")
digest = hashlib.sha256()
with path.open("rb") as source_stream:
while chunk := source_stream.read(1024 * 1024):
digest.update(chunk)
stream.write(chunk)
total += len(chunk)
if digest.hexdigest() != descriptor["sha256"]:
raise CanonicalLabOverlayError("camera fragment digest changed")
stream.flush()
os.fsync(stream.fileno())
if total <= 0 or total > MAX_SOURCE_BYTES:
raise CanonicalLabOverlayError("camera source size is invalid")
return output
except BaseException:
output.unlink(missing_ok=True)
raise
def _camera_proxy(source: Path, frame_count: int, ffmpeg: Path, root: Path) -> Path:
descriptor, name = tempfile.mkstemp(prefix=".canonical-lab-video-", suffix=".mp4", dir=root)
os.close(descriptor)
output = Path(name)
output.unlink(missing_ok=True)
completed = subprocess.run(
[
str(ffmpeg),
"-hide_banner",
"-loglevel",
"error",
"-i",
str(source),
"-frames:v",
str(frame_count),
"-an",
"-c:v",
"libx264",
"-preset",
"veryfast",
"-crf",
"28",
"-g",
"20",
"-keyint_min",
"20",
"-pix_fmt",
"yuv420p",
"-fps_mode",
"passthrough",
"-movflags",
"+faststart",
str(output),
],
check=False,
capture_output=True,
timeout=900,
)
if completed.returncode != 0 or not output.is_file() or output.stat().st_size <= 0:
output.unlink(missing_ok=True)
raise CanonicalLabOverlayError(
f"canonical LAB video proxy failed: {completed.stderr[-1000:]!r}"
)
os.chmod(output, 0o600)
return output
def _render_overlay(
output: Path,
root: Path,
route: dict[str, Any],
recording_id: str,
proxy: Path,
) -> None:
frame_times = _frame_times(root, route)
layers = route["layers"]
archives: dict[str, zipfile.ZipFile] = {}
recording = rr.RecordingStream(APPLICATION_ID, recording_id=recording_id)
try:
recording.set_sinks(rr.FileSink(output, write_footer=True))
recording.log(
"/perception/camera/image",
rr.AssetVideo(path=proxy),
static=True,
)
video = rr.AssetVideo(path=proxy)
video_timestamps = video.read_frame_timestamps_nanos()
# The sealed fMP4 source has one fragment per LAB tick, but not every
# fragment contains a decodable video sample. Preserve the source PTS
# in the proxy and bind every LAB tick to the latest available sample.
# Re-numbering decoded samples at 10 Hz shortens RAV004 by ~43 seconds
# and is exactly the camera/semantics drift this projection prevents.
video_references = _video_reference_timestamps(video_timestamps, frame_times)
for layer_id in ("city", "vegetation"):
layer = layers[layer_id]
taxonomy = layer.get("taxonomy")
classes = taxonomy.get("classes") if isinstance(taxonomy, dict) else None
if not isinstance(classes, list):
raise CanonicalLabOverlayError("semantic taxonomy is invalid")
context = rr.AnnotationContext(
[
rr.ClassDescription(
info=rr.AnnotationInfo(
id=int(item["class_id"]),
label=str(item["label"]),
color=[*map(int, item["color_rgb"]), 255],
)
)
for item in classes
if isinstance(item, dict)
]
)
recording.log(
f"/perception/camera/segmentation/{layer_id}",
context,
static=True,
)
archive = layer.get("mask_archive")
relative = archive.get("path") if isinstance(archive, dict) else None
if not isinstance(relative, str):
raise CanonicalLabOverlayError("semantic archive is unavailable")
path = (root / PurePosixPath(relative)).resolve(strict=True)
if not path.is_relative_to(root) or path.is_symlink():
raise CanonicalLabOverlayError("semantic archive path is unsafe")
archives[layer_id] = zipfile.ZipFile(path)
for index, timestamp in enumerate(frame_times):
recording.set_time(SESSION_TIMELINE, duration=np.timedelta64(timestamp, "ns"))
recording.log(
"/perception/camera/image",
rr.VideoFrameReference(nanoseconds=int(video_references[index])),
)
masks = {
layer_id: _read_mask(archive, index)
for layer_id, archive in archives.items()
}
for layer_id, mask in masks.items():
recording.log(
f"/perception/camera/segmentation/{layer_id}",
rr.SegmentationImage(mask, opacity=0.72, draw_order=1.0),
)
boxes, labels = semantic_component_boxes(masks["city"], index)
if boxes:
recording.log(
"/perception/camera/detections",
rr.Boxes2D(
array=boxes,
array_format=rr.Box2DFormat.XYXY,
labels=labels,
show_labels=True,
colors=[[255, 210, 55, 255]] * len(boxes),
),
)
else:
recording.log(
"/perception/camera/detections",
rr.Clear(recursive=False),
)
recording.flush(timeout_sec=300.0)
finally:
for archive in archives.values():
with suppress(Exception):
archive.close()
with suppress(Exception):
recording.disconnect()
def _frame_times(root: Path, route: dict[str, Any]) -> np.ndarray:
descriptor = route.get("timeline")
relative = descriptor.get("path") if isinstance(descriptor, dict) else None
if not isinstance(relative, str):
raise CanonicalLabOverlayError("LAB timeline is unavailable")
path = (root / PurePosixPath(relative)).resolve(strict=True)
payload = path.read_bytes()
if (
not path.is_relative_to(root)
or path.is_symlink()
or descriptor.get("byte_length") != len(payload)
or descriptor.get("sha256") != hashlib.sha256(payload).hexdigest()
):
raise CanonicalLabOverlayError("LAB timeline changed")
values = np.frombuffer(payload, dtype="<u8").astype(np.int64, copy=False)
if values.shape != (route["frame_count"],) or np.any(np.diff(values) <= 0):
raise CanonicalLabOverlayError("LAB timeline order changed")
return values
def _video_reference_timestamps(
video_timestamps: np.ndarray,
frame_times: np.ndarray,
) -> np.ndarray:
"""Bind every LAB tick to the latest decodable source video sample."""
relative_frame_times = frame_times - frame_times[0]
if (
len(video_timestamps) < int(len(frame_times) * 0.9)
or np.any(np.diff(video_timestamps) < 0)
or abs(int(video_timestamps[-1]) - int(relative_frame_times[-1]))
> 2_000_000_000
):
raise CanonicalLabOverlayError("video proxy timeline changed")
indices = np.searchsorted(
video_timestamps,
relative_frame_times,
side="right",
) - 1
return video_timestamps[np.clip(indices, 0, len(video_timestamps) - 1)]
def _read_mask(archive: zipfile.ZipFile, sequence: int) -> np.ndarray:
member = f"masks/frame-{sequence + 1:06d}.png"
with Image.open(io.BytesIO(archive.read(member))) as image:
mask = np.asarray(image.convert("L"), dtype=np.uint8).copy()
if mask.shape != (600, 800):
raise CanonicalLabOverlayError("semantic mask dimensions changed")
return mask
def semantic_component_boxes(
mask: np.ndarray,
_sequence: int,
) -> tuple[list[list[int]], list[str]]:
labels = {
1: "person",
2: "bicycle",
3: "motorcycle",
4: "car",
5: "heavy vehicle",
13: "static obstacle",
14: "animal",
}
candidates: list[tuple[float, list[int], str]] = []
for class_id, label in labels.items():
minimum_pixels = 80 if class_id == 13 else 24
for left, top, right, bottom, pixels in _mask_component_boxes(
mask, class_id, minimum_pixels=minimum_pixels
)[:12]:
score = min(0.99, 0.5 + pixels / 20_000)
candidates.append(
(score, [left, top, right, bottom], f"{label} · {score:.0%} · semantic-only")
)
candidates.sort(key=lambda row: (-row[0], row[1][1], row[1][0]))
selected = candidates[:32]
return [row[1] for row in selected], [row[2] for row in selected]
def _mask_component_boxes(
mask: np.ndarray,
class_id: int,
*,
minimum_pixels: int,
) -> list[tuple[int, int, int, int, int]]:
"""Return deterministic 8-connected run-length components."""
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
groups = np.split(matches, np.flatnonzero(np.diff(matches) > 1) + 1)
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 _restore_cached(
output: Path,
sidecar: Path,
**identity: str,
) -> CanonicalLabOverlayArtifact | None:
try:
value = json.loads(sidecar.read_text(encoding="utf-8"))
stat = output.stat()
if (
output.is_symlink()
or sidecar.is_symlink()
or value.get("schema_version") != "missioncore.canonical-lab-rerun-overlay/v1"
or value.get("renderer_version") != RENDERER_VERSION
or any(value.get(key) != expected for key, expected in identity.items())
or value.get("byte_length") != stat.st_size
or not _is_sha256(value.get("sha256"))
or _sha256(output) != value["sha256"]
):
return None
return CanonicalLabOverlayArtifact(output, stat.st_size, value["sha256"])
except (OSError, ValueError, json.JSONDecodeError):
return None
def _artifact_is_regular(artifact: CanonicalLabOverlayArtifact) -> bool:
try:
return (
not artifact.path.is_symlink()
and artifact.path.stat().st_size == artifact.byte_length
and _sha256(artifact.path) == artifact.sha256
)
except OSError:
return False
def _write_json_atomic(path: Path, value: dict[str, Any]) -> None:
temporary = path.with_name(f".{path.name}.{uuid4().hex}.tmp")
try:
with temporary.open("x", encoding="utf-8") as stream:
json.dump(value, stream, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
stream.write("\n")
stream.flush()
os.fsync(stream.fileno())
os.chmod(temporary, 0o600)
os.replace(temporary, path)
finally:
temporary.unlink(missing_ok=True)
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def _is_sha256(value: object) -> bool:
return isinstance(value, str) and len(value) == 64 and all(
character in "0123456789abcdef" for character in value
)
+41 -22
View File
@@ -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,14 +85,13 @@ 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)
self._blueprint_recording.set_time(
"blueprint",
@@ -110,7 +109,7 @@ class _RecordedBlueprintStream:
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 +164,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 +203,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 +254,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 +273,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 +313,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 +414,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 +430,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 +450,7 @@ def recorded_blueprint_rrd(
).render(
build_blueprint,
follow_trajectory=follow_trajectory,
plan_view=plan_view,
)
except RecordedBlueprintError:
raise
+1 -1
View File
@@ -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,
+5
View File
@@ -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(
+4
View File
@@ -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,
+84 -75
View File
@@ -12,13 +12,21 @@ 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 Any, Final, Literal
import numpy as np
from fastapi import APIRouter, HTTPException
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 (
CanonicalLabOverlayError,
_mask_component_boxes,
canonical_lab_overlay,
canonical_recording_id,
)
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
from k1link.laboratory.evidence_report import (
LaboratoryEvidenceReportError,
@@ -34,6 +42,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 +76,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 +86,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 +109,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 +298,61 @@ def _build_vegetation_lab_router(
},
)
@router.post("/{result_id}/canonical-overlay.rrd")
async def get_canonical_rerun_overlay(
result_id: str,
request: CanonicalLabRerunRequest,
) -> FileResponse:
"""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
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 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.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 +894,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:
+46
View File
@@ -18,6 +18,11 @@ 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
from k1link.laboratory import LaboratoryEvidenceRegistry
from k1link.laboratory.canonical_rerun_overlay import (
CanonicalLabOverlayArtifact,
_artifact_is_regular,
_video_reference_timestamps,
)
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
@@ -43,6 +48,47 @@ def test_semantic_component_boxes_keep_distinct_objects_separate() -> None:
]
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_route_playback_chunk_descriptor_seals_only_requested_binary_window() -> None:
points = np.arange(18, dtype="<f4").reshape(6, 3)
descriptor = _canonical_route_playback_chunk_descriptor(
Generated
+6 -6
View File
@@ -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]]