38 changed files with 2866 additions and 355 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",
@@ -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;
+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,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
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
@@ -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:
+12 -10
View File
@@ -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
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",
File diff suppressed because it is too large Load Diff
+43 -23
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,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
+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,
+281 -76
View File
@@ -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:
+3 -3
View File
@@ -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]
+326
View File
@@ -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:
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]]