refactor(lab): restore canonical RAV004 replay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-30 01:22:44 +03:00
parent 74da6437e9
commit f1cbe0061a
21 changed files with 1181 additions and 719 deletions
@@ -182,6 +182,27 @@ function recordedMediaTimeRangesContain(
return false;
}
export function recordedMediaTimestampStallRecoveryTarget(
currentSeconds: number,
bufferedRanges: readonly (readonly [number, number])[],
skipSeconds = 0.18,
): number | null {
if (!Number.isFinite(currentSeconds) || !Number.isFinite(skipSeconds) || skipSeconds <= 0) {
return null;
}
for (const [startSeconds, endSeconds] of bufferedRanges) {
if (
!Number.isFinite(startSeconds)
|| !Number.isFinite(endSeconds)
|| currentSeconds < startSeconds - 0.05
|| currentSeconds > endSeconds
) continue;
const targetSeconds = Math.min(currentSeconds + skipSeconds, endSeconds - 0.05);
return targetSeconds >= currentSeconds + 0.04 ? targetSeconds : null;
}
return null;
}
export function recordedMediaPresentationState(
state: "loading" | "ready" | "error",
readyGeneration: string | null,
@@ -783,6 +804,7 @@ export function RecordedFmp4Player({
onPlayingRejected,
playbackAuthority = "media",
playbackTransport = "segmented",
recoverTimestampStalls = false,
}: {
source: ObservationSourceDescriptor;
playback?: RecordedObservationPlayback | null;
@@ -797,6 +819,7 @@ export function RecordedFmp4Player({
onPlayingRejected?: () => void;
playbackAuthority?: "media" | "host";
playbackTransport?: "segmented" | "epoch-stream";
recoverTimestampStalls?: boolean;
}) {
const videoRef = useRef<HTMLVideoElement>(null);
const onAdmissionChangeRef = useRef(onAdmissionChange);
@@ -1450,6 +1473,51 @@ export function RecordedFmp4Player({
visualState,
]);
useEffect(() => {
const video = videoRef.current;
if (!video || !recoverTimestampStalls || !playback?.playing || visualState !== "ready") {
return;
}
let lastSeconds = video.currentTime;
let lastProgressAtMs = performance.now();
const interval = window.setInterval(() => {
if (
!playbackPlayingRef.current
|| video.paused
|| video.ended
|| video.seeking
|| video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA
) {
lastSeconds = video.currentTime;
lastProgressAtMs = performance.now();
return;
}
const nowMs = performance.now();
if (video.currentTime >= lastSeconds + 0.02) {
lastSeconds = video.currentTime;
lastProgressAtMs = nowMs;
return;
}
if (nowMs - lastProgressAtMs < 1_250) return;
const bufferedRanges = Array.from(
{ length: video.buffered.length },
(_, index) => [video.buffered.start(index), video.buffered.end(index)] as const,
);
const targetSeconds = recordedMediaTimestampStallRecoveryTarget(
video.currentTime,
bufferedRanges,
);
lastProgressAtMs = nowMs;
if (targetSeconds === null) return;
// Field recordings can contain non-monotonic or corrupt H.264 timestamps.
// If decoded time is frozen despite proven buffered media ahead, skip only
// the broken timestamp interval and return authority to the media clock.
video.currentTime = targetSeconds;
lastSeconds = targetSeconds;
}, 250);
return () => window.clearInterval(interval);
}, [playback?.playing, recoverTimestampStalls, visualState]);
useEffect(() => {
const video = videoRef.current;
if (!video || !segmented || !playback?.playing || visualState !== "ready") return;
@@ -39,6 +39,7 @@ export function RecordedEvidenceVideoScene({
onAdmissionChange,
playbackAuthority = "media",
playbackTransport = "segmented",
recoverTimestampStalls = false,
}: {
source: ObservationSourceDescriptor;
playback: RecordedObservationPlayback;
@@ -56,6 +57,7 @@ export function RecordedEvidenceVideoScene({
onAdmissionChange?: (state: RecordedCameraAdmissionState) => void;
playbackAuthority?: "media" | "host";
playbackTransport?: "segmented" | "epoch-stream";
recoverTimestampStalls?: boolean;
}) {
const generation = source.delivery?.kind === "recorded-fmp4-manifest"
? source.delivery.manifestGenerationSha256
@@ -63,12 +65,29 @@ export function RecordedEvidenceVideoScene({
const [admissionPhase, setAdmissionPhase] = useState<RecordedCameraAdmissionState["phase"]>(
"loading",
);
useEffect(() => setAdmissionPhase("loading"), [generation, source.id]);
const [presentedSeconds, setPresentedSeconds] = useState<number | null>(null);
useEffect(() => {
setAdmissionPhase("loading");
setPresentedSeconds(null);
}, [generation, source.id]);
const handleAdmissionChange = (next: RecordedCameraAdmissionState) => {
setAdmissionPhase(next.phase);
onAdmissionChange?.(next);
};
const sourceReady = admissionPhase === "ready";
const overlaysPresented = sourceReady
&& presentedSeconds !== null
&& Math.abs(presentedSeconds - playback.currentSeconds) <= 0.25;
const handlePlaybackChange = (next: RecordedObservationPlayback) => {
setPresentedSeconds(next.currentSeconds);
// During a paused operator seek the existing media element can emit its old
// timestamp while the requested MSE window is being rebuilt. That stale
// callback must not undo the host target before the decoder reaches it.
if (!playback.playing && Math.abs(next.currentSeconds - playback.currentSeconds) > 0.35) {
return;
}
onPlaybackChange?.(next);
};
return (
<div className="recorded-evidence-video-scene">
<RecordedFmp4Player
@@ -78,27 +97,28 @@ export function RecordedEvidenceVideoScene({
prepare
segmentSequence={segmentSequence}
segmentCount={segmentCount}
onPlaybackChange={onPlaybackChange}
onPlaybackChange={handlePlaybackChange}
onPlayingRejected={onPlayingRejected}
onAdmissionChange={handleAdmissionChange}
playbackAuthority={playbackAuthority}
playbackTransport={playbackTransport}
recoverTimestampStalls={recoverTimestampStalls}
/>
{sourceReady && semanticOverlay ? (
{overlaysPresented && semanticOverlay ? (
<RecordedEvidenceSemanticMaskOverlay
{...semanticOverlay}
imageWidth={imageWidth}
imageHeight={imageHeight}
/>
) : null}
{sourceReady && pointCloudOverlay ? (
{overlaysPresented && pointCloudOverlay ? (
<RecordedEvidencePointCloudOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
overlay={pointCloudOverlay}
/>
) : null}
{sourceReady ? (
{overlaysPresented ? (
<RecordedEvidenceBoxOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
@@ -130,9 +130,9 @@ export function useRecordedEvidencePlayback(
const synchronize = useCallback((next: RecordedObservationPlayback) => {
if (!validRange(range) || !Number.isFinite(next.currentSeconds)) return;
// The canonical LAB host clock is authoritative. Native media callbacks
// are observational only in this mode: a stalled decoder must never stop
// the common timeline or let an independently playing spatial view drift.
// Animation-clock mode is retained only for non-media diagnostics. A
// recorded LAB with video uses the external media clock so spatial and
// overlays never advance past the frame the decoder actually presented.
if (clock === "animation") return;
setPlayback((current) => synchronizeRecordedEvidencePlayback(current, next, range));
}, [clock, range]);
@@ -1,5 +1,5 @@
export const CANONICAL_RECORDED_LAB_TGS_HISTORY_SECONDS = 1;
export const CANONICAL_RECORDED_LAB_SPATIAL_PROFILE = "source-paced-ground-v2";
export const CANONICAL_RECORDED_LAB_SPATIAL_PROFILE = "source-paced-ground-v3";
export interface CanonicalRecordedLabPackedCellEvidence {
centersBodyXyM: Float32Array;
@@ -15,7 +15,7 @@ export interface CanonicalRecordedLabSpatialFrame {
coordinateFrame: "body-ground";
sensorHeight: {
meters: number;
source: "initial-source-cloud-lower-quantile-median";
source: "local-source-cloud-ground-quantile-median" | "session-source-cloud-fallback";
sampleCount: number;
madM: number;
authority: "visual-derived";
@@ -129,7 +129,7 @@ export async function fetchCanonicalRecordedLabSpatialFrame(
const payload = objectValue(await response.json(), "canonical_lab.spatial_frame");
exact(
payload.schema_version,
"missioncore.canonical-recorded-lab-spatial-frame/v2",
"missioncore.canonical-recorded-lab-spatial-frame/v3",
"canonical_lab.spatial_frame.schema_version",
);
exact(payload.coordinate_frame, "body-ground", "canonical_lab.spatial_frame.coordinate_frame");
@@ -179,11 +179,14 @@ export async function fetchCanonicalRecordedLabSpatialFrame(
);
}
const sensorHeight = objectValue(payload.sensor_height, "canonical_lab.spatial_frame.sensor_height");
exact(
sensorHeight.source,
"initial-source-cloud-lower-quantile-median",
"canonical_lab.spatial_frame.sensor_height.source",
);
if (
sensorHeight.source !== "local-source-cloud-ground-quantile-median"
&& sensorHeight.source !== "session-source-cloud-fallback"
) {
throw new CanonicalRecordedLabSpatialContractError(
"canonical_lab.spatial_frame.sensor_height.source: контракт изменён.",
);
}
exact(
sensorHeight.authority,
"visual-derived",
@@ -210,7 +213,7 @@ export async function fetchCanonicalRecordedLabSpatialFrame(
coordinateFrame: "body-ground",
sensorHeight: {
meters: numberValue(sensorHeight.meters, "canonical_lab.spatial_frame.sensor_height.meters"),
source: "initial-source-cloud-lower-quantile-median",
source: sensorHeight.source,
sampleCount: integerValue(
sensorHeight.sample_count,
"canonical_lab.spatial_frame.sensor_height.sample_count",
@@ -154,6 +154,9 @@ export interface M4ThreatTimelineFrame {
pointCloudSourceCount: number;
pointCloudSampleCount: number;
pointCloudLayer: "current-increment";
localSlamBodyXyzM?: readonly M4Point3[];
localSlamSourceFrameCount?: number;
localSlamSourcePointCount?: number;
cameraProjectedPointsXyd: readonly (readonly [number, number, number])[];
cameraProjectedSourceCount: number;
cameraProjectedPointCount: number;
@@ -168,10 +171,11 @@ export interface M4ThreatTimelineFrame {
export interface M4ThreatTimeline {
resultId: string;
recordedSourceSessionId: "20260720T065719Z_viewer_live";
recordedSourceSessionId: string;
recordedSourceId: string;
imageWidth: 800;
imageHeight: 600;
frameCount: 4489;
frameCount: number;
frameTimesNs: readonly number[];
timelineStartSeconds: number;
timelineEndSeconds: number;
@@ -704,12 +708,8 @@ export async function fetchM4ThreatTimeline(
exact(payload.result_id, result, "M4.6 timeline result");
exact(payload.authority, "replay-simulated", "M4.6 timeline authority");
const recorded = object(payload.recorded_source, "M4.6 recorded source");
exact(
recorded.session_id,
"20260720T065719Z_viewer_live",
"M4.6 recorded session",
);
exact(recorded.source_id, "RAVNOVES00", "M4.6 recorded source id");
const recordedSessionId = text(recorded.session_id, "M4.6 recorded session");
const recordedSourceId = text(recorded.source_id, "M4.6 recorded source id");
exact(
recorded.representation_id,
"registered-map-increment-v1",
@@ -720,7 +720,10 @@ export async function fetchM4ThreatTimeline(
"host-arrival-best-effort",
"M4.6 recorded synchronization",
);
const frameCount = exact(payload.frame_count, 4489, "M4.6 timeline frame count");
const frameCount = integer(payload.frame_count, "M4.6 timeline frame count");
if (frameCount < 1) {
throw new M4ThreatContractError("M4.6 timeline frame count: пустой timeline.");
}
const frameTimesNs = array(payload.frame_times_ns, "M4.6 timeline index").map(
(value) => integer(value, "M4.6 timeline time"),
);
@@ -738,7 +741,8 @@ export async function fetchM4ThreatTimeline(
);
return {
resultId: result,
recordedSourceSessionId: "20260720T065719Z_viewer_live",
recordedSourceSessionId: recordedSessionId,
recordedSourceId,
imageWidth: exact(payload.image_width, 800, "M4.6 image width"),
imageHeight: exact(payload.image_height, 600, "M4.6 image height"),
frameCount,
@@ -1334,6 +1338,17 @@ function parseTimelineFrame(
"current-increment",
"M4.6 timeline point layer",
),
localSlamBodyXyzM: item.local_slam_body_xyz_m === undefined
? []
: array(item.local_slam_body_xyz_m, "M4.6 local SLAM points").map(
(point) => vector(point, 3, "M4.6 local SLAM point") as [number, number, number],
),
localSlamSourceFrameCount: item.local_slam_source_frame_count === undefined
? undefined
: integer(item.local_slam_source_frame_count, "M4.6 local SLAM source frames"),
localSlamSourcePointCount: item.local_slam_source_point_count === undefined
? undefined
: integer(item.local_slam_source_point_count, "M4.6 local SLAM source points"),
cameraProjectedPointsXyd: item.camera_projected_points_xyd === undefined
? []
: array(item.camera_projected_points_xyd, "M4.6 camera points").map(
@@ -153,6 +153,7 @@ export interface M4ReplayClassifiedSpatialLayer {
label: string;
pointLayerLabel: string;
cellLayerLabel: string;
cellLayerAvailable?: boolean;
expectedAtSequence: boolean;
frame: M4ReplayClassifiedSpatialFrame | null;
loading: boolean;
@@ -178,6 +179,8 @@ export function M4ReplayThreatVisual({
classifiedSpatialLayer,
showReferenceMediaLayers = true,
showSpatialOverlaySummary = true,
playbackTransport = "epoch-stream",
recoverTimestampStalls = false,
onActiveSequenceChange,
}: {
resultId: string;
@@ -194,6 +197,8 @@ export function M4ReplayThreatVisual({
classifiedSpatialLayer?: M4ReplayClassifiedSpatialLayer;
showReferenceMediaLayers?: boolean;
showSpatialOverlaySummary?: boolean;
playbackTransport?: "segmented" | "epoch-stream";
recoverTimestampStalls?: boolean;
onActiveSequenceChange?: (sequence: number | null) => void;
}) {
const {
@@ -256,7 +261,7 @@ export function M4ReplayThreatVisual({
showMediaSemantic: Boolean(activeSemantic) && showMediaSemantic,
showSpatialSemantic: Boolean(activeSpatialSemantic) && showSpatialSemantic,
showMediaPoints,
classifiedSpatialMode: !classifiedSpatialLayer
classifiedSpatialMode: !classifiedSpatialLayer || classifiedSpatialLayer.cellLayerAvailable === false
? "none"
: classifiedSpatialLayer.replacePointCloud
? "replace-source"
@@ -278,7 +283,7 @@ export function M4ReplayThreatVisual({
endSeconds: metadata.timeline.timelineEndSeconds,
}) : null, [metadata.timeline]);
const playbackController = useRecordedEvidencePlayback(playbackRange, {
clock: "animation",
clock: "external",
});
const seekPlayback = playbackController.seek;
const setPlaybackPlaying = playbackController.setPlaying;
@@ -356,11 +361,19 @@ export function M4ReplayThreatVisual({
useEffect(() => {
lastSpatialFrameRef.current = null;
}, [evidenceDemand.sourceSpatialPoints, resultId]);
if (frame?.spatialAvailable) {
lastSpatialFrameRef.current = { resultId, frame };
const latestAvailableSpatialFrame = [...timelineFrame.availableFrames]
.reverse()
.find((candidate) => (
candidate.spatialAvailable
&& (timelineFrame.activeSequence === null
|| candidate.sequence <= timelineFrame.activeSequence)
)) ?? null;
const currentSpatialFrame = frame?.spatialAvailable ? frame : latestAvailableSpatialFrame;
if (currentSpatialFrame) {
lastSpatialFrameRef.current = { resultId, frame: currentSpatialFrame };
}
const spatialFrame = frame?.spatialAvailable
? frame
const spatialFrame = currentSpatialFrame
? currentSpatialFrame
: lastSpatialFrameRef.current?.resultId === resultId
? lastSpatialFrameRef.current.frame
: null;
@@ -541,14 +554,20 @@ export function M4ReplayThreatVisual({
const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence
? spatialFrame
: null;
const classifiedSpatialFrame = classifiedSpatialLayer?.frame?.sourceSequence === timelineFrame.activeSequence
const hasClassifiedSpatialOutput = Boolean(
classifiedSpatialLayer && classifiedSpatialLayer.cellLayerAvailable !== false,
);
const classifiedSpatialFrame = hasClassifiedSpatialOutput
&& classifiedSpatialLayer?.frame?.sourceSequence === timelineFrame.activeSequence
? classifiedSpatialLayer?.frame ?? null
: null;
const lastClassifiedSpatialFrameRef = useRef<{
resultId: string;
frame: M4ReplayClassifiedSpatialFrame;
} | null>(null);
const incomingClassifiedSpatialFrame = classifiedSpatialLayer?.frame ?? null;
const incomingClassifiedSpatialFrame = hasClassifiedSpatialOutput
? classifiedSpatialLayer?.frame ?? null
: null;
if (incomingClassifiedSpatialFrame && incomingClassifiedSpatialFrame.sampleAvailable !== false) {
lastClassifiedSpatialFrameRef.current = { resultId, frame: incomingClassifiedSpatialFrame };
}
@@ -577,7 +596,9 @@ export function M4ReplayThreatVisual({
? spatialFrame
: null)
: null;
const replaceClassifiedPointCloud = classifiedSpatialLayer?.replacePointCloud ?? true;
const replaceClassifiedPointCloud = hasClassifiedSpatialOutput
? classifiedSpatialLayer?.replacePointCloud ?? true
: false;
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
const mapGravityLocalSensorToBodyGround = useCallback((
point: readonly [number, number, number],
@@ -695,7 +716,12 @@ export function M4ReplayThreatVisual({
.map((item) => item.assessment.closestApproachM)
.filter((value): value is number => value !== null)
.sort((left, right) => left - right)[0] ?? null;
const localSurface = useMemo(() => buildM4LocalSurface(
const localSurface = useMemo(() => spatialFrame?.localSlamBodyXyzM?.length ? ({
pointsBodyXyzM: spatialFrame.localSlamBodyXyzM,
sourceFrameCount: spatialFrame.localSlamSourceFrameCount ?? 0,
sourcePointCount: spatialFrame.localSlamSourcePointCount ?? 0,
voxelCount: spatialFrame.localSlamBodyXyzM.length,
}) : buildM4LocalSurface(
timelineFrame.availableFrames,
spatialFrame,
metadata.timeline?.localSurfaceVisualization ?? {
@@ -843,16 +869,24 @@ export function M4ReplayThreatVisual({
shape="pill"
variant={showRollingMap ? "primary" : "secondary"}
aria-pressed={showRollingMap}
disabled={classifiedSpatialLayer.cellLayerAvailable === false}
title={classifiedSpatialLayer.cellLayerAvailable === false
? `${classifiedSpatialLayer.cellLayerLabel} недоступен: для этой записи нет запечатанного полного результата`
: undefined}
onClick={() => setShowRollingMap((visible) => !visible)}
>
{classifiedSpatialLayer.cellLayerLabel}
</Button>
{semanticSpatialResultId ? (
{activeSpatialSemantic ? (
<Button
size="compact"
shape="pill"
variant={showSpatialSemantic ? "primary" : "secondary"}
aria-pressed={showSpatialSemantic}
disabled={!semanticSpatialResultId}
title={semanticSpatialResultId
? "Point-aligned semantic evidence"
: "Point-aligned 3D semantics отсутствует в запечатанном результате"}
onClick={() => setShowSpatialSemantic((visible) => !visible)}
>
SEMANTICS
@@ -905,12 +939,16 @@ export function M4ReplayThreatVisual({
LOW-STEP
</Button>
) : null}
{semanticSpatialResultId ? (
{activeSpatialSemantic ? (
<Button
size="compact"
shape="pill"
variant={showSpatialSemantic ? "primary" : "secondary"}
aria-pressed={showSpatialSemantic}
disabled={!semanticSpatialResultId}
title={semanticSpatialResultId
? "Point-aligned semantic evidence"
: "Point-aligned 3D semantics отсутствует в запечатанном результате"}
onClick={() => setShowSpatialSemantic((visible) => !visible)}
>
SEMANTICS
@@ -1014,14 +1052,14 @@ export function M4ReplayThreatVisual({
<>
<div>
<span>Spatial evidence</span>
<strong>{classifiedSpatialLayer
<strong>{hasClassifiedSpatialOutput
? classifiedSpatialFrame
? replaceClassifiedPointCloud
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedCellCount.toLocaleString("ru-RU")} cells`
: `${(activeSpatialFrame?.pointCloudSourceCount ?? classifiedSpatialFrame.sourcePointCount ?? 0).toLocaleString("ru-RU")} source points · ${classifiedCellCount.toLocaleString("ru-RU")} TGS cells`
: "TGS spatial buffer"
: `${currentIncrementObstacles.length} current · ${rollingMapObstacles.length} rolling${metadata.timeline.occupancyProvenanceDelivery ? ` · ${lowStepObstacles.length} low-step` : ""}`}</strong>
<small>{classifiedSpatialLayer
<small>{hasClassifiedSpatialOutput
? classifiedSpatialFrame
? classifiedSpatialFrame.sampleAvailable === false
? displayedClassifiedFrameHeld && displayedClassifiedSpatialFrame
@@ -1030,9 +1068,9 @@ export function M4ReplayThreatVisual({
: activeSpatialFrame
? "map-gravity-local · all eligible points accounted · causal rolling 1 s"
: "TGS рассчитан · linked source cloud недоступен для этого кадра"
: classifiedSpatialLayer.error
?? classifiedSpatialLayer.loadingLabel
?? `Открываем ${classifiedSpatialLayer.label}`
: classifiedSpatialLayer?.error
?? classifiedSpatialLayer?.loadingLabel
?? `Открываем ${classifiedSpatialLayer?.label ?? "spatial evidence"}`
: (
<>
{spatialFrame
@@ -1057,13 +1095,13 @@ export function M4ReplayThreatVisual({
)}</small>
</div>
<div>
<span>{classifiedSpatialLayer ? "TGS fail-closed" : "Virtual corridor"}</span>
<strong>{classifiedSpatialLayer
<span>{hasClassifiedSpatialOutput ? "TGS fail-closed" : "Virtual corridor"}</span>
<strong>{hasClassifiedSpatialOutput
? classifiedSpatialFrame
? `${classifiedCellCounts.occupied} occupied · ${classifiedCellCounts.rejected} rejected · ${classifiedCellCounts.unobserved} unobserved`
: classifiedSpatialLayer.loading || displayingBufferedFrame ? "loading" : "unavailable"
: classifiedSpatialLayer?.loading || displayingBufferedFrame ? "loading" : "unavailable"
: `${spatialFrame?.decisionCounts.threat ?? 0} threat · nearest ${nearest === null ? "—" : `${nearest.toFixed(2)} м`}`}</strong>
<small>{classifiedSpatialLayer
<small>{hasClassifiedSpatialOutput
? classifiedSpatialFrame
? `${classifiedCellCounts.ground} ground-support · visual review only · navigation authority OFF`
: "visual review only · navigation authority OFF"
@@ -1116,8 +1154,9 @@ export function M4ReplayThreatVisual({
}
segmentCount={timeline.frameCount}
onPlaybackChange={playbackController.synchronize}
playbackAuthority="host"
playbackTransport="epoch-stream"
playbackAuthority="media"
playbackTransport={playbackTransport}
recoverTimestampStalls={recoverTimestampStalls}
/>
) : videoError ? (
<SpatialState message={videoError} />
@@ -1148,9 +1187,11 @@ export function M4ReplayThreatVisual({
ref={metricSceneRef}
pointCloudBodyXyzM={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? classifiedPointsBody
: classifiedContextSpatialFrame?.pointCloudBodyXyzM ?? []}
: classifiedContextSpatialFrame?.pointCloudBodyXyzM
?? activeSpatialFrame?.pointCloudBodyXyzM
?? []}
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
obstacles={classifiedSpatialLayer ? [] : sceneObstacles}
obstacles={hasClassifiedSpatialOutput ? [] : sceneObstacles}
rig={timeline.rig}
corridor={timeline.corridor}
occupiedVoxelSizeM={displayedClassifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
@@ -1159,7 +1200,7 @@ export function M4ReplayThreatVisual({
showCurrentIncrement={showCurrentIncrement}
showLocalSurface={showLocalSurface}
showRollingMap={showRollingMap}
showLowStep={classifiedSpatialLayer ? false : showLowStep}
showLowStep={hasClassifiedSpatialOutput ? false : showLowStep}
pointSemanticClassIds={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.pointClassIds
: alignedSemanticPointIds}
@@ -1174,7 +1215,7 @@ export function M4ReplayThreatVisual({
classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM}
showClassifiedCells={showRollingMap}
/>
{classifiedSpatialLayer && !displayedClassifiedSpatialFrame ? (
{hasClassifiedSpatialOutput && classifiedSpatialLayer && !displayedClassifiedSpatialFrame ? (
<div className="l3-visual-audit__state" role={classifiedSpatialLayer.error ? "alert" : "status"}>
{classifiedSpatialLayer.loading || displayingBufferedFrame
? <span className="busy-indicator" aria-hidden="true" />
@@ -1,30 +1,5 @@
import {
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
} from "react";
import {
Button,
Icon,
IconButton,
SegmentedControl,
} from "@nodedc/ui-react";
import { useEffect, useMemo, useState } from "react";
import { ObservationTimeline } from "../../components/ObservationTimeline";
import {
CanonicalRecordedLabReplay,
useCanonicalRecordedLabReplayState,
} from "../../components/laboratory/CanonicalRecordedLabReplay";
import {
LaboratoryMetricEvidenceScene,
type LaboratoryMetricEvidenceSceneHandle,
type LaboratoryMetricPackedCellEvidence,
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
import { RecordedEvidenceVideoScene } from "../../components/laboratory/RecordedEvidenceVideoScene";
import { useCanonicalRecordedLabSpatialFrame } from "../../components/laboratory/useCanonicalRecordedLabSpatialFrame";
import { useRecordedEvidencePlayback } from "../../components/laboratory/useRecordedEvidencePlayback";
import {
LaboratoryEvidence,
LaboratoryResultSummary,
@@ -32,23 +7,9 @@ import {
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import {
type RecordedEvidenceSemanticClass,
type RecordedEvidenceSemanticPaletteEntry,
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
import {
canonicalRecordedLabPackedTgsCells,
canonicalRecordedLabTgsIsCurrent,
} from "../../core/laboratory/canonicalRecordedLab";
import {
fetchVegetationShadowResult,
fetchVegetationRouteTgsAnchor,
vegetationFullRouteMaskUrl,
vegetationVideoMaskUrl,
type VegetationFullRouteLayer,
type VegetationFullRouteReview,
type VegetationMixedRouteCase,
type VegetationMixedRouteReview,
type VegetationRouteTgsAnchor,
type VegetationShadowResult,
} from "../../core/laboratory/vegetationShadow";
import {
@@ -56,81 +17,18 @@ import {
type M49TgsFullShadowResult,
} from "../../core/laboratory/m49TgsFullShadow";
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
import {
M4ReplayThreatVisual,
type M4ReplayClassifiedSpatialLayer,
type M4ReplayThreatSemanticLayer,
} from "./M4ReplayThreatVisual";
const VEGETATION_TIMELINE_ENDPOINT = "/api/v1/laboratory/vegetation-shadow";
function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
}
const FULL_ROUTE_SEMANTIC_MODES = [
{ value: "city", label: "ГОРОД · EoMT" },
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
] as const;
type FullRouteMediaMode = "video" | "camera";
type FullRouteSpatialMode = "3d" | "plan";
const FULL_ROUTE_MEDIA_MODES = [
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
] as const;
const FULL_ROUTE_SPATIAL_MODES = [
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
] as const;
function semanticPresentation(layer: VegetationFullRouteLayer): {
classes: readonly RecordedEvidenceSemanticClass[];
palette: readonly RecordedEvidenceSemanticPaletteEntry[];
} {
return {
classes: layer.taxonomy.map((item) => ({ id: item.classId, label: item.label })),
palette: layer.taxonomy.map((item) => ({
classId: item.classId,
color: item.classId === 0
? { kind: "transparent" as const }
: { kind: "diagnostic" as const, rgb: item.colorRgb },
})),
};
}
function causalTgsCase(
cases: readonly VegetationMixedRouteCase[],
sequence: number,
): VegetationMixedRouteCase | null {
if (!cases.length) return null;
return cases.reduce<VegetationMixedRouteCase | null>((latest, candidate) => (
candidate.sourceSequence <= sequence
&& (!latest || candidate.sourceSequence > latest.sourceSequence)
? candidate
: latest
), null);
}
function nearestFullRouteFrameIndex(
frameSourceTimesNs: readonly number[],
sourceTimeNs: number,
): number {
if (!frameSourceTimesNs.length) return 0;
let low = 0;
let high = frameSourceTimesNs.length - 1;
while (low < high) {
const middle = Math.floor((low + high) / 2);
if ((frameSourceTimesNs[middle] ?? 0) < sourceTimeNs) low = middle + 1;
else high = middle;
}
if (low === 0) return 0;
const previous = frameSourceTimesNs[low - 1] ?? frameSourceTimesNs[0] ?? 0;
const current = frameSourceTimesNs[low] ?? previous;
return Math.abs(sourceTimeNs - previous) <= Math.abs(current - sourceTimeNs)
? low - 1
: low;
}
function FullRouteReviewEvidence({
resultId,
review,
@@ -138,425 +36,53 @@ function FullRouteReviewEvidence({
resultId: string;
review: VegetationFullRouteReview;
}) {
const {
mediaMode,
spatialMode,
splitView,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange: handleMediaModeChange,
onSpatialModeChange: handleSpatialModeChange,
onSplitPrimarySizeChange: setSplitPrimarySize,
onExpandedChange: setExpanded,
} = useCanonicalRecordedLabReplayState<FullRouteMediaMode, FullRouteSpatialMode>({
initialMediaMode: "video",
initialSpatialMode: "3d",
});
const [semanticLayer, setSemanticLayer] = useState<"city" | "vegetation">("vegetation");
const [showCameraSemantic, setShowCameraSemantic] = useState(true);
const [showSourcePoints, setShowSourcePoints] = useState(true);
const [showLocalSlam, setShowLocalSlam] = useState(true);
const [showTgs, setShowTgs] = useState(true);
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
const [replayLaunch, setReplayLaunch] = useState<ObservationSessionReplayLaunch | null>(null);
const [videoError, setVideoError] = useState<string | null>(null);
const [linkedReview, setLinkedReview] = useState<VegetationMixedRouteReview | null>(null);
const [linkedReviewError, setLinkedReviewError] = useState<string | null>(null);
const [tgsAnchor, setTgsAnchor] = useState<VegetationRouteTgsAnchor | null>(null);
const [tgsAnchorLoading, setTgsAnchorLoading] = useState(false);
const [tgsAnchorError, setTgsAnchorError] = useState<string | null>(null);
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
const playbackRange = useMemo(() => ({
startSeconds: review.timelineStartSeconds,
endSeconds: review.timelineEndSeconds,
}), [review.timelineEndSeconds, review.timelineStartSeconds]);
const playbackController = useRecordedEvidencePlayback(playbackRange, { clock: "animation" });
const sequenceIndex = nearestFullRouteFrameIndex(
review.frameSourceTimesNs,
Math.round(playbackController.playback.currentSeconds * 1_000_000_000),
);
const sequence = sequenceIndex + 1;
const spatialRequestIndex = Math.floor(sequenceIndex / 5) * 5;
const spatialRequestTimeNs = review.frameSourceTimesNs[spatialRequestIndex]
?? review.frameSourceTimesNs[sequenceIndex]
?? Math.round(playbackController.playback.currentSeconds * 1_000_000_000);
const spatialEvidence = useCanonicalRecordedLabSpatialFrame({
sessionId: review.sessionId,
generationSha256: replayLaunch?.sha256 ?? null,
targetTimeNs: spatialRequestTimeNs,
});
const layer = review[semanticLayer];
const semantic = useMemo(() => semanticPresentation(layer), [layer]);
const prefetchSrcs = useMemo(() => showCameraSemantic
? Array.from({ length: 8 }, (_, offset) => sequenceIndex + offset + 1)
.filter((candidate) => candidate < review.frameCount)
.map((candidate) => vegetationFullRouteMaskUrl(resultId, semanticLayer, candidate))
: [], [resultId, review.frameCount, semanticLayer, sequenceIndex, showCameraSemantic]);
useEffect(() => {
const controller = new AbortController();
setVideoSource(null);
setReplayLaunch(null);
setVideoError(null);
void resolveObservationSessionReplay(review.sessionId, { signal: controller.signal })
.then((launch) => {
const source = recordedObservationSources(launch).find((candidate) => (
candidate.id === review.recordedMediaSourceId
&& candidate.modality === "video"
&& candidate.semanticChannelId === "camera.video.recorded"
&& candidate.delivery?.kind === "recorded-fmp4-manifest"
&& candidate.delivery.manifestGenerationSha256 === review.recordedMediaGenerationSha256
&& candidate.delivery.timelineStartSeconds === review.timelineStartSeconds
&& candidate.delivery.timelineEndSeconds >= review.timelineEndSeconds
));
if (!source) {
throw new Error("RIGHT-видео не совпало с sealed RAVNOVES004TREE timeline.");
}
if (!controller.signal.aborted) {
setVideoSource(source);
setReplayLaunch(launch);
}
})
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
setVideoError(caught instanceof Error ? caught.message : "Записанное видео недоступно.");
}
});
return () => controller.abort();
}, [
review.recordedMediaGenerationSha256,
review.recordedMediaSourceId,
review.sessionId,
review.timelineEndSeconds,
review.timelineStartSeconds,
]);
useEffect(() => {
const controller = new AbortController();
setLinkedReview(null);
setLinkedReviewError(null);
void fetchVegetationShadowResult(review.linkedRouteReviewResultId, {
signal: controller.signal,
}).then((result) => {
if (
!result.routeReview
|| result.routeReview.sourceId !== review.sourceId
|| result.routeReview.sessionId !== review.sessionId
) {
throw new Error("TGS anchors имеют другую source identity.");
}
if (!controller.signal.aborted) setLinkedReview(result.routeReview);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setLinkedReviewError(caught instanceof Error ? caught.message : "TGS anchors недоступны.");
}
});
return () => controller.abort();
}, [review.linkedRouteReviewResultId, review.sessionId, review.sourceId]);
const selectedTgsCase = linkedReview
? causalTgsCase(linkedReview.cases, sequence)
: null;
const selectedTgsTimeNs = selectedTgsCase
? review.frameSourceTimesNs[selectedTgsCase.sourceSequence - 1]
?? Math.round(selectedTgsCase.sessionSeconds * 1_000_000_000)
: spatialRequestTimeNs;
const currentFrameTimeNs = review.frameSourceTimesNs[sequenceIndex]
?? Math.round(playbackController.playback.currentSeconds * 1_000_000_000);
const tgsWithinEvidenceWindow = Boolean(
selectedTgsCase
&& canonicalRecordedLabTgsIsCurrent(currentFrameTimeNs, selectedTgsTimeNs),
);
const tgsReferenceEvidence = useCanonicalRecordedLabSpatialFrame({
sessionId: review.sessionId,
generationSha256: replayLaunch?.sha256 ?? null,
targetTimeNs: selectedTgsTimeNs,
});
useEffect(() => {
if (!showTgs || !selectedTgsCase) {
setTgsAnchor(null);
setTgsAnchorLoading(false);
setTgsAnchorError(null);
return;
}
const controller = new AbortController();
setTgsAnchorLoading(true);
setTgsAnchorError(null);
void fetchVegetationRouteTgsAnchor(
review.linkedRouteReviewResultId,
selectedTgsCase.sourceSequence,
{ signal: controller.signal },
).then((anchor) => {
if (!controller.signal.aborted) setTgsAnchor(anchor);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setTgsAnchorError(caught instanceof Error ? caught.message : "TGS anchor недоступен.");
}
}).finally(() => {
if (!controller.signal.aborted) setTgsAnchorLoading(false);
});
return () => controller.abort();
}, [review.linkedRouteReviewResultId, selectedTgsCase?.sourceSequence, showTgs]);
const packedTgsCells = useMemo<LaboratoryMetricPackedCellEvidence | undefined>(() => {
if (!tgsAnchor || !tgsWithinEvidenceWindow) return undefined;
const currentBody = spatialEvidence.frame?.bodyFrame;
const anchorBody = tgsReferenceEvidence.frame?.bodyFrame;
if (!currentBody || !anchorBody) return undefined;
return canonicalRecordedLabPackedTgsCells(tgsAnchor.costmap, anchorBody, currentBody);
}, [
spatialEvidence.frame?.bodyFrame,
tgsAnchor,
tgsReferenceEvidence.frame?.bodyFrame,
tgsWithinEvidenceWindow,
]);
const semanticOverlay = showCameraSemantic ? {
src: vegetationFullRouteMaskUrl(resultId, semanticLayer, sequenceIndex),
prefetchSrcs,
classes: semantic.classes,
palette: semantic.palette,
opacity: 0.46,
ariaLabel: `${layer.name} semantic prediction frame ${sequence}`,
} : undefined;
const mediaContent = (
<div className="m4-replay-threat-visual__media-layer" data-media={mediaMode ?? "none"}>
{videoSource ? (
<RecordedEvidenceVideoScene
source={videoSource}
playback={playbackController.playback}
imageWidth={review.width}
imageHeight={review.height}
boxes={[]}
semanticOverlay={semanticOverlay}
ariaLabel={`RAVNOVES004TREE recorded frame ${sequence}`}
interactive={false}
segmentSequence={sequence}
segmentCount={review.frameCount}
onPlaybackChange={playbackController.synchronize}
playbackAuthority="host"
playbackTransport="epoch-stream"
/>
) : (
<div className="l3-visual-audit__state" role={videoError ? "alert" : "status"}>
{videoError ?? "Открываем автономный RAVNOVES004TREE source…"}
</div>
)}
</div>
);
const spatialContent = spatialMode ? (
<>
{spatialEvidence.frame ? (
<LaboratoryMetricEvidenceScene
ref={metricSceneRef}
pointCloudBodyXyzM={showSourcePoints
? spatialEvidence.frame.sourcePointsBodyXyzM
: []}
localSurfaceBodyXyzM={showLocalSlam
? spatialEvidence.frame.localSlamBodyXyzM
: []}
obstacles={[]}
rig={{
lengthM: 1,
widthM: 0.8,
nominalSensorHeightM: spatialEvidence.frame.sensorHeight.meters,
}}
corridor={{ forwardLengthM: 12, rearMarginM: 1, halfWidthM: 0.4 }}
occupiedVoxelSizeM={tgsAnchor?.costmap.cellSizeM ?? 0.45}
mode={spatialMode}
label="RAV004 canonical source points, local SLAM and TGS costmap"
showCurrentIncrement={showSourcePoints}
showLocalSurface={showLocalSlam}
showRollingMap={showTgs}
showLowStep={false}
classifiedPackedCells={packedTgsCells}
classifiedCellSizeM={tgsAnchor?.costmap.cellSizeM}
showClassifiedCells={showTgs && Boolean(packedTgsCells)}
/>
) : (
<div className="l3-visual-audit__state" role={spatialEvidence.error ? "alert" : "status"}>
{spatialEvidence.loading ? <span className="busy-indicator" aria-hidden="true" /> : null}
<span>{spatialEvidence.error ?? "Открываем source points и Local SLAM из sealed RRD…"}</span>
</div>
)}
{showTgs && selectedTgsCase ? (
<div className="m4-replay-threat-visual__pane-status" role="status">
{tgsAnchorError ?? linkedReviewError ?? (tgsAnchorLoading
? `Открываем sealed TGS anchor ${selectedTgsCase.sourceSequence}; source/SLAM и общий clock продолжаются.`
: tgsWithinEvidenceWindow
? `TGS anchor ${selectedTgsCase.sourceSequence} из 10; source/SLAM и общий clock продолжаются.`
: `TGS anchor ${selectedTgsCase.sourceSequence} старше доказанного окна 1 с; слой скрыт, playback продолжается.`)}
</div>
) : null}
</>
) : null;
const mediaLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Слои камеры и видео"
>
<Button
size="compact"
shape="pill"
variant={showCameraSemantic ? "primary" : "secondary"}
aria-pressed={showCameraSemantic}
onClick={() => setShowCameraSemantic((visible) => !visible)}
>
SEMANTICS
</Button>
<SegmentedControl
value={semanticLayer}
items={[...FULL_ROUTE_SEMANTIC_MODES]}
label="Источник семантики"
onChange={(value) => {
setSemanticLayer(value);
setShowCameraSemantic(true);
}}
/>
</div>
);
const spatialLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Слои 3D и плана"
>
<Button
size="compact"
shape="pill"
variant={showSourcePoints ? "primary" : "secondary"}
aria-pressed={showSourcePoints}
onClick={() => setShowSourcePoints((visible) => !visible)}
>
SOURCE POINTS
</Button>
<Button
size="compact"
shape="pill"
variant={showLocalSlam ? "primary" : "secondary"}
aria-pressed={showLocalSlam}
onClick={() => setShowLocalSlam((visible) => !visible)}
>
LOCAL SLAM
</Button>
<Button
size="compact"
shape="pill"
variant={showTgs ? "primary" : "secondary"}
aria-pressed={showTgs}
disabled={!linkedReview}
title={linkedReviewError ?? "10 sealed causal TGS anchors; continuous TGS отсутствует"}
onClick={() => setShowTgs((visible) => !visible)}
>
TGS COSTMAP
</Button>
<Button
size="compact"
shape="pill"
variant={showCameraSemantic ? "primary" : "secondary"}
aria-pressed={showCameraSemantic}
title="Recorded semantic layer; camera-aligned prediction, без выдуманной 3D-проекции"
onClick={() => setShowCameraSemantic((visible) => !visible)}
>
SEMANTICS
</Button>
</div>
);
const resetSpatialView = (
<IconButton
label="Сбросить ракурс"
onClick={() => metricSceneRef.current?.resetView()}
>
<Icon name="refresh" size={16} />
</IconButton>
);
const overlayPanePercent = splitView && splitOrientation === "vertical"
? splitPrimarySize
: 100;
const overlay = (
<div
className="l3-visual-audit__overlay m4-replay-threat-visual__overlay"
style={{
"--m4-replay-threat-overlay-pane-width": `${overlayPanePercent}%`,
} as CSSProperties}
>
<div>
<span>RAVNOVES004TREE · recorded realtime</span>
<strong>frame {sequence}/{review.frameCount}</strong>
<small>
+{(playbackController.playback.currentSeconds - review.timelineStartSeconds).toFixed(3)} с
· {playbackController.playback.playing ? "воспроизведение" : "пауза / seek"}
</small>
</div>
<div>
<span>Spatial evidence</span>
<strong>{showTgs && selectedTgsCase && tgsWithinEvidenceWindow
? `TGS anchor ${selectedTgsCase.sourceSequence} · ${selectedTgsCase.tgs.occupiedCells} occupied`
: "source RRD · points + bounded Local SLAM"}</strong>
<small>{showTgs
? "TGS visible only inside sealed 1 s evidence window · playback retained"
: "5 s bounded Local SLAM · ground-rebased recorded source"}</small>
</div>
</div>
);
const transport = (
<ObservationTimeline
className="m4-replay-threat-visual__timeline"
active
sourceCount={4}
mode="recorded"
seekable
synchronization="host-arrival-best-effort"
rangeNs={{
min: Math.round(review.timelineStartSeconds * 1_000_000_000),
max: Math.round(review.timelineEndSeconds * 1_000_000_000),
}}
currentNs={Math.round(playbackController.playback.currentSeconds * 1_000_000_000)}
playing={playbackController.playback.playing}
playbackRate={playbackController.playback.rate ?? 1}
onSeek={(timeNs) => playbackController.seek(timeNs / 1_000_000_000)}
onPlayingChange={playbackController.setPlaying}
onPlaybackRateChange={playbackController.setRate}
showJumpToEnd={false}
/>
);
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 (
<CanonicalRecordedLabReplay
label="RAVNOVES004TREE full recorded review"
mediaMode={mediaMode ?? "none"}
mediaModes={FULL_ROUTE_MEDIA_MODES}
spatialMode={spatialMode ?? "none"}
spatialModes={FULL_ROUTE_SPATIAL_MODES}
expanded={expanded}
splitPrimarySize={splitPrimarySize}
splitOrientation={splitOrientation}
mediaAriaLabel={mediaMode === "camera" ? "Камера" : "Видео"}
spatialAriaLabel={spatialMode === "3d" ? "Трёхмерная сцена" : "Вид сверху"}
mediaLayerControls={mediaLayerControls}
spatialLayerControls={spatialLayerControls}
spatialLeadingControl={resetSpatialView}
mediaMultiLayer
mediaContent={mediaContent}
spatialContent={spatialContent}
emptyMessage="Выберите VIDEO/CAMERA или 3D/PLAN. Общий таймлайн останется на месте."
overlay={overlay}
transport={transport}
trailingActions={!splitView && spatialMode ? resetSpatialView : null}
onMediaModeChange={handleMediaModeChange}
onSpatialModeChange={handleSpatialModeChange}
onExpandedChange={setExpanded}
onSplitPrimarySizeChange={setSplitPrimarySize}
<M4ReplayThreatVisual
resultId={resultId}
timelineEndpointRoot={VEGETATION_TIMELINE_ENDPOINT}
semanticLayers={semanticLayers}
initialSemanticLayerId="vegetation"
initialSpatialMode="3d"
classifiedSpatialLayer={sealedSpatialGap}
evidenceLabel="RAVNOVES004TREE"
playbackTransport="segmented"
recoverTimestampStalls
showReferenceMediaLayers
showSpatialOverlaySummary
/>
);
}
@@ -575,32 +101,32 @@ function FullRouteReviewResult({
summary={(
<LaboratorySummary
title="LAB V1 · RAVNOVES004TREE · полный маршрут"
description="Общий recorded-LAB шаблон воспроизводит запись с травой и оврагами: RIGHT camera, исходное облако, ограниченный Local SLAM, два независимых semantic-слоя и десять реально просчитанных TGS-якорей."
description="Принятый recorded-LAB инструмент воспроизводит RAV004 без отдельного viewer: одна media-clock timeline, RIGHT camera, source points, bounded Local SLAM и переключаемые EoMT/DDRNet."
status="FULL RECORDED REVIEW · truth отсутствует · commands OFF"
statusTone="warning"
facts={[
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount}/${review.frameCount} frames` },
{ label: "3D", value: "1437 source point chunks · 2825 SLAM poses · recorded RRD" },
{ 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 sealed causal anchors · continuous costmap отсутствует" },
{ label: "TGS", value: "10 review anchors существуют · full-route artifact отсутствует" },
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
]}
brief={{
question: "Что реально видно на полном RAV004-прогоне с высокой травой, оврагами и переходом к городу?",
approach: "Одна recorded timeline открывается общим LAB viewer. Camera и RRD синхронизированы; EoMT/DDRNet переключаются на камере, source points и 5-секундный Local SLAM — в 3D, TGS — только в доказанном окне десяти запечатанных якорей.",
principalResult: "RAV004 больше не подменяется RAV00: доступна полная исходная запись и её реальные пространственные слои.",
limitation: "Ручной truth, continuous TGS и point-aligned 3D semantics отсутствуют. Один повреждённый H.264-пакет на позиции 6092 заменён предыдущим декодированным кадром и отражён в proof.",
approach: "RAV004 поставляет только data/provider configuration в тот же M4 recorded viewer. Видеодекодер владеет clock; новые source increments проецируются в gravity-stable forward/left/up frame, Local SLAM ограничен пятью секундами.",
principalResult: "RAV004 больше не имеет отдельной логики окон, таймера, seek, cache или 3D controls. Модели и подписи меняются конфигурацией, архитектура переключения остаётся общей.",
limitation: "Full-route TGS, независимый person/vehicle detector, ручной truth и point-aligned 3D semantics пока не запечатаны. Semantic-derived рамки диагностические и не являются STOP-authority.",
}}
method={{
completeness: "complete",
executionClass: "ai-inference",
pipelineId: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
pipelineId: "canonical-recorded-lab-rav004tree/v3",
components: [
{ kind: "algorithm", name: "Recorded source points + bounded Local SLAM", version: "source-paced-ground-v2", role: "spatial source evidence", identitySha256: null },
{ kind: "algorithm", name: "Canonical recorded replay", version: "media-clock / one viewer", role: "shared camera + spatial transport", identitySha256: null },
{ kind: "algorithm", name: "Recorded source points + bounded Local SLAM", version: "source-paced-ground-v3", role: "gravity-stable spatial evidence", identitySha256: null },
{ kind: "model", name: review.city.name, version: "sealed Worker 006 run", role: "urban semantic review", identitySha256: null },
{ kind: "model", name: review.vegetation.name, version: "GOOSE DDRNet-39", role: "vegetation semantic review", identitySha256: null },
{ kind: "algorithm", name: "Causal TGS", version: "10 linked route anchors", role: "bounded geometric evidence", identitySha256: null },
],
}}
/>
@@ -617,19 +143,19 @@ function FullRouteReviewResult({
)}
result={(
<LaboratoryResultSummary
title="Полный RAV004 visual review восстановлен; управление не авторизовано"
status="Recorded evidence ready · navigation/actuation OFF"
title="RAV004 переведён на общий replay-каркас; safety evidence ещё не полно"
status="Recorded evidence · navigation/actuation OFF"
statusTone="warning"
metrics={[
{ label: "Route masks", value: "6830/6830 × 2", hint: "sealed local archives · Worker не требуется" },
{ label: "EoMT throughput", value: `${decimal(review.city.inferenceFps, 2)} fps`, hint: "изолированный полный прогон" },
{ label: "DDRNet throughput", value: `${decimal(review.vegetation.inferenceFps, 2)} fps`, hint: "изолированный полный прогон" },
{ label: "Spatial evidence", value: "RRD + 10 TGS anchors", hint: "continuous TGS и 3D semantics отсутствуют" },
{ 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 не принята" },
]}
conclusion={{
proved: "Полный RAV004 открывается в каноническом recorded viewer с camera, source points, bounded Local SLAM, EoMT, DDRNet и связанными TGS-якорями.",
notProved: "Не доказаны truth accuracy, временная стабильность DDRNet, continuous negative-obstacle detection и безопасное управление ровером.",
decision: "Использовать как visual audit. Следующий gate — truth-набор овраг/трава/дерево/яма и motion-aware temporal evaluation при целевых ≥10 FPS; navigation/actuation оставить OFF.",
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.",
}}
/>
)}
@@ -754,27 +280,9 @@ export function VegetationShadowResultView({
executionClass: "ai-inference",
pipelineId: "ravnoves-eomt-ddrnet-yolox-causal-tgs-recorded-review/v1",
components: [
{
kind: "model",
name: "EoMT Cityscapes semantic",
version: "sealed E47 archive",
role: "urban semantic review",
identitySha256: null,
},
{
kind: "model",
name: selected.loadedModelName,
version: selected.candidate,
role: "vegetation material candidate",
identitySha256: selected.checkpointSha256,
},
{
kind: "algorithm",
name: "Frozen YOLOX + causal TGS",
version: "linked M4/M4.9 archives",
role: "independent object and geometry veto",
identitySha256: null,
},
{ kind: "model", name: "EoMT Cityscapes semantic", version: "sealed E47 archive", role: "urban semantic review", identitySha256: null },
{ kind: "model", name: selected.loadedModelName, version: selected.candidate, role: "vegetation material candidate", identitySha256: selected.checkpointSha256 },
{ kind: "algorithm", name: "Frozen YOLOX + causal TGS", version: "linked M4/M4.9 archives", role: "independent object and geometry veto", identitySha256: null },
],
}}
/>
@@ -795,26 +303,10 @@ export function VegetationShadowResultView({
status="Semantics advisory · YOLOX/TGS veto cannot be cleared"
statusTone="warning"
metrics={[
{
label: "Route masks",
value: `${route.frameCount}/${route.frameCount}`,
hint: "sealed local playback · Worker для открытия не нужен",
},
{
label: "Semantic sources",
value: "2 independent layers",
hint: "EoMT CITY / DDRNet VEGETATION · display switches, evidence does not fuse",
},
{
label: "Vegetation worker p95",
value: `${decimal(selected.shadowLatencyP95Ms, 2)} ms`,
hint: "изолированный DDRNet inference; не совместный realtime stack",
},
{
label: "Vegetation peak VRAM",
value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} GiB`,
hint: "DDRNet candidate на Worker 006",
},
{ label: "Route masks", value: `${route.frameCount}/${route.frameCount}`, hint: "sealed local playback · Worker для открытия не нужен" },
{ label: "Semantic sources", value: "2 independent layers", hint: "EoMT CITY / DDRNet VEGETATION · display switches, evidence does not fuse" },
{ label: "Vegetation worker p95", value: `${decimal(selected.shadowLatencyP95Ms, 2)} ms`, hint: "изолированный DDRNet inference; не совместный realtime stack" },
{ label: "Vegetation peak VRAM", value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} GiB`, hint: "DDRNet candidate на Worker 006" },
]}
conclusion={{
proved: "На одной recorded timeline доступны городской EoMT, природный DDRNet, YOLOX detections и causal TGS; LAB автономна от Worker.",
@@ -18,6 +18,7 @@ import {
const REQUESTED_CHUNK_FRAMES = 24;
const RETAINED_CHUNK_COUNT = 4;
const RETAINED_CHUNKS_BEHIND = 1;
const PREFETCH_CHUNKS_AHEAD = 1;
const RETAINED_CAMERA_POINT_OVERLAYS = 12;
@@ -31,10 +32,17 @@ export function m4ThreatChunkWindowStarts(
frameCount: number,
): readonly number[] {
if (chunkSize < 1 || frameCount < 1) return [];
return Array.from(
{ length: PREFETCH_CHUNKS_AHEAD + 1 },
(_, index) => activeChunkStart + index * chunkSize,
).filter((start) => start >= 0 && start < frameCount);
return [
activeChunkStart,
...Array.from(
{ length: RETAINED_CHUNKS_BEHIND },
(_, index) => activeChunkStart - (index + 1) * chunkSize,
),
...Array.from(
{ length: PREFETCH_CHUNKS_AHEAD },
(_, index) => activeChunkStart + (index + 1) * chunkSize,
),
].filter((start) => start >= 0 && start < frameCount);
}
export function cancelM4ThreatChunkRequestsOutsideWindow<T extends { abort(): void }>(
@@ -769,7 +769,7 @@ test("M4.6 local SLAM surface reprojects registered increments into the active b
});
test("M4.6 spatial buffering keeps the active and one future chunk", () => {
assert.deepEqual(m4ThreatChunkWindowStarts(48, 24, 4489), [48, 72]);
assert.deepEqual(m4ThreatChunkWindowStarts(48, 24, 4489), [48, 24, 72]);
assert.deepEqual(m4ThreatChunkWindowStarts(0, 24, 4489), [0, 24]);
});
@@ -786,8 +786,8 @@ test("M4.6 spatial buffering drops stale in-flight windows across rapid jumps",
if (!inFlight.has(start)) inFlight.set(start, controller(start));
}
}
assert.deepEqual([...inFlight.keys()], [4488]);
assert.deepEqual(aborted, [0, 24, 1488, 1512]);
assert.deepEqual([...inFlight.keys()], [4488, 4464]);
assert.deepEqual(aborted, [0, 24, 1488, 1464, 1512]);
});
test("recorded evidence clock advances by selected rate and stops at the sealed end", () => {
@@ -863,7 +863,7 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(canonical, /m4-replay-threat-visual__deck/);
assert.match(visual, /lastFrameRef/);
assert.match(visual, /lastSpatialFrameRef/);
assert.match(visual, /const spatialFrame = frame\?\.spatialAvailable/);
assert.match(visual, /const spatialFrame = currentSpatialFrame/);
assert.match(visual, /<ObservationTimeline/);
assert.match(visual, /useM4ThreatTimelineFrame/);
assert.match(visual, /resolveObservationSessionReplay\(timeline\.recordedSourceSessionId/);
@@ -888,7 +888,7 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(canonical, /separatorLabel="Изменить размер VIDEO\/CAMERA и 3D\/PLAN"/);
assert.match(canonical, /secondaryMode=\{\{/);
assert.match(visual, /playback=\{playbackController\.playback\}/);
assert.match(visual, /clock: "animation"/);
assert.match(visual, /clock: "external"/);
assert.match(visual, /useCanonicalRecordedLabReplayState/);
assert.match(canonical, /current === next \? null : next/);
assert.match(visual, /timelineFrame\.activeSequence \+ 1/);
@@ -898,8 +898,9 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
await readFile(new URL("../src/components/laboratory/useRecordedEvidencePlayback.ts", import.meta.url), "utf8"),
/if \(clock === "animation"\) return;/,
);
assert.match(visual, /playbackAuthority="host"/);
assert.match(visual, /playbackTransport="epoch-stream"/);
assert.match(visual, /playbackAuthority="media"/);
assert.match(visual, /playbackTransport = "epoch-stream"/);
assert.match(visual, /playbackTransport=\{playbackTransport\}/);
assert.match(
await readFile(new URL("../src/components/RecordedFmp4Player.tsx", import.meta.url), "utf8"),
/if \(playbackAuthority === "host"\) return;/,
@@ -927,6 +928,7 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(visualCss, /laboratory-metric-evidence-scene__legend/);
assert.match(visualCss, /bottom: auto/);
assert.match(videoScene, /<RecordedFmp4Player/);
assert.match(videoScene, /!playback\.playing && Math\.abs\(next\.currentSeconds - playback\.currentSeconds\) > 0\.35/);
assert.match(imageScene, /<RecordedEvidenceBoxOverlay/);
assert.match(imageScene, /<RecordedEvidencePointCloudOverlay/);
assert.match(videoScene, /<RecordedEvidencePointCloudOverlay/);
@@ -935,13 +937,14 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(metricScene, /OrbitControls/);
assert.match(visual, /LOCAL SLAM/);
assert.match(visual, /showLocalSurface/);
assert.match(visual, /const latestAvailableSpatialFrame = \[\.\.\.timelineFrame\.availableFrames\][\s\S]*candidate\.spatialAvailable[\s\S]*candidate\.sequence <= timelineFrame\.activeSequence/);
assert.match(
visual,
/pointCloudBodyXyzM=\{displayedClassifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: classifiedContextSpatialFrame\?\.pointCloudBodyXyzM \?\? \[\]\}/,
/pointCloudBodyXyzM=\{displayedClassifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: classifiedContextSpatialFrame\?\.pointCloudBodyXyzM[\s\S]*\?\? activeSpatialFrame\?\.pointCloudBodyXyzM[\s\S]*\?\? \[\]\}/,
);
assert.match(
visual,
/const classifiedSpatialFrame = classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence[\s\S]*lastClassifiedSpatialFrameRef[\s\S]*const displayedClassifiedSpatialFrame = classifiedSpatialFrame\s*&&\s*classifiedSpatialFrame\.sampleAvailable !== false/,
/const classifiedSpatialFrame = hasClassifiedSpatialOutput[\s\S]*classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence[\s\S]*lastClassifiedSpatialFrameRef[\s\S]*const displayedClassifiedSpatialFrame = classifiedSpatialFrame\s*&&\s*classifiedSpatialFrame\.sampleAvailable !== false/,
);
assert.doesNotMatch(visual, /classifiedSpatialFrame\?\.sampleAvailable !== false/);
assert.match(visual, /timelineFrame\.availableFrames\.find/);
@@ -13,6 +13,7 @@ let recordedMediaDecodeStartSequence;
let recordedMediaSegmentAppendOrder;
let recordedMediaSegmentSequenceAtTime;
let recordedMediaCanRollTarget;
let recordedMediaTimestampStallRecoveryTarget;
let nextRecordedMediaRandomAccessSequence;
let recordedMediaRecoveryTargetSequence;
let selectRecordedMediaPreparationEpoch;
@@ -32,6 +33,7 @@ before(async () => {
recordedMediaSegmentAppendOrder,
recordedMediaSegmentSequenceAtTime,
recordedMediaCanRollTarget,
recordedMediaTimestampStallRecoveryTarget,
nextRecordedMediaRandomAccessSequence,
recordedMediaRecoveryTargetSequence,
selectRecordedMediaPreparationEpoch,
@@ -270,6 +272,12 @@ test("recorded player preserves forward rolling playback but seeks backward clip
assert.equal(recordedMediaCanRollTarget(20, 21, true, false), false);
});
test("recorded player skips only a proven buffered corrupt timestamp interval", () => {
assert.equal(recordedMediaTimestampStallRecoveryTarget(11.422, [[0, 16.287]]), 11.602);
assert.equal(recordedMediaTimestampStallRecoveryTarget(16.25, [[0, 16.287]]), null);
assert.equal(recordedMediaTimestampStallRecoveryTarget(20, [[0, 16.287]]), null);
});
test("loading and error overlays fully conceal recorded camera pixels", async () => {
const css = await readFile(
new URL("../src/styles/observation.css", import.meta.url),
@@ -407,7 +407,7 @@ test("canonical recorded LAB spatial frame keeps source, SLAM and body identity
fetcher: async (url) => {
requestedUrl = String(url);
return new Response(JSON.stringify({
schema_version: "missioncore.canonical-recorded-lab-spatial-frame/v2",
schema_version: "missioncore.canonical-recorded-lab-spatial-frame/v3",
target_time_ns: 82_770_000_000,
source_time_ns: 82_769_535_708,
pose_time_ns: 82_769_535_708,
@@ -415,13 +415,13 @@ test("canonical recorded LAB spatial frame keeps source, SLAM and body identity
coordinate_frame: "body-ground",
sensor_height: {
meters: 0.32,
source: "initial-source-cloud-lower-quantile-median",
source: "local-source-cloud-ground-quantile-median",
sample_count: 20,
mad_m: 0.03,
authority: "visual-derived",
},
spatial_profile: {
profile_id: "source-paced-ground-v2",
profile_id: "source-paced-ground-v3",
local_slam_history_seconds: 5,
local_slam_radius_m: 30,
local_slam_voxel_size_m: 0.12,
@@ -443,7 +443,7 @@ test("canonical recorded LAB spatial frame keeps source, SLAM and body identity
});
assert.equal(
requestedUrl,
`/api/v1/observation-sessions/session-004/canonical-lab/spatial-frame?generation=${generation}&time_ns=82770000000&profile=source-paced-ground-v2`,
`/api/v1/observation-sessions/session-004/canonical-lab/spatial-frame?generation=${generation}&time_ns=82770000000&profile=source-paced-ground-v3`,
);
assert.equal(frame.sourcePointCount, 2);
assert.equal(frame.localSlamBodyXyzM.length, 2);
@@ -473,32 +473,23 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr
assert.doesNotMatch(resultSource, /M48MaskComparisonVisual/);
assert.match(resultSource, /M49TgsFullShadowEvidence/);
assert.match(resultSource, /semanticOverride/);
assert.match(resultSource, /EoMT CITY \/ DDRNet VEGETATION/);
assert.match(m49Source, /spatialSemantic=\{spatialSemantic\}/);
assert.match(m49Source, /controlLabel: "SEMANTICS"/);
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 2);
assert.doesNotMatch(resultSource, /RAVNOVES004TREE mixed route review/);
assert.match(resultSource, /RAVNOVES004TREE full recorded review/);
assert.match(resultSource, /CanonicalRecordedLabReplay/);
assert.match(resultSource, /RecordedEvidenceVideoScene/);
assert.match(resultSource, /LaboratoryMetricEvidenceScene/);
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.doesNotMatch(resultSource, /RerunViewport/);
assert.match(resultSource, /useCanonicalRecordedLabSpatialFrame/);
assert.doesNotMatch(resultSource, /cacheRef|pumpRef|desiredRef/);
assert.match(resultSource, /useCanonicalRecordedLabReplayState/);
assert.match(resultSource, /playbackTransport="epoch-stream"/);
assert.match(resultSource, /causalTgsCase/);
assert.match(resultSource, /TGS visible only inside sealed 1 s evidence window/);
assert.match(resultSource, /canonicalRecordedLabPackedTgsCells/);
assert.doesNotMatch(resultSource, /LaboratoryRecordedClipPlayer|M48EvidenceModeRail/);
assert.doesNotMatch(resultSource, /assets\.tgs|<img/);
assert.match(resultSource, /SOURCE POINTS/);
assert.match(resultSource, /LOCAL SLAM/);
assert.match(resultSource, /TGS COSTMAP/);
assert.match(resultSource, /onClick=\{\(\) => setShowTgs\(\(visible\) => !visible\)\}/);
assert.match(resultSource, /showClassifiedCells=\{showTgs && Boolean\(packedTgsCells\)\}/);
assert.doesNotMatch(resultSource, /setShowTgs\(false\)/);
assert.doesNotMatch(resultSource, /setPlaying\(false\);[\s\S]{0,160}setShowTgs/);
assert.match(resultSource, /point-aligned 3D semantics пока не запечатаны/);
assert.match(resultSource, /cellLayerAvailable: false/);
assert.match(canonicalSource, /primary=\{mediaPane\}/);
assert.match(canonicalSource, /secondary=\{spatialPane/);
assert.match(canonicalSource, /missioncore\.canonical-recorded-lab-replay\/v1/);
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

@@ -0,0 +1,167 @@
# RAVNOVES004TREE canonical LAB replay audit
Date: 2026-08-30
Scope: Mission Core recorded LAB replay, RAVNOVES004TREE, OPS perception state
Excluded: Gaussian/simulation workers and their artifacts
## Outcome
RAVNOVES004TREE no longer owns a custom LAB viewer. It supplies recording and
model configuration to the same `M4ReplayThreatVisual` and
`CanonicalRecordedLabReplay` implementation used by the accepted recorded LAB.
No new window or status type was added. The stable interaction contract remains:
- media: `SEMANTICS`, model selector, `VIDEO` / `CAMERA`;
- spatial: `SOURCE POINTS`, `LOCAL SLAM`, `TGS COSTMAP`, `SEMANTICS`, `3D` / `PLAN`;
- one timeline, one resizable split and one media-owned playback clock.
Models, result IDs, endpoints, labels and replay transport are configuration.
Window structure, switching, seek, buffering and spatial scene code are shared.
## Why the previous LAB failed
### Video and spatial state had different clocks
The removed RAV004 viewer advanced an animation/host clock even when the browser
decoder stopped. The point cloud therefore continued while the camera frame and
timeline could remain frozen. The shared viewer now uses the decoded media time
as the external clock, and image masks/boxes are rendered only when their time is
within 250 ms of the actually presented video time.
The RAV004 MP4 itself is not clean. An independent `ffmpeg` decode around the
reproducible stop at 11.422 s reported non-monotonic DTS values and corrupt H.264
macroblocks. The RAV004 profile therefore uses the shared segmented MSE transport
and an opt-in timestamp recovery rule. Recovery is allowed only when all of these
conditions are true:
- playback is requested and the media element is not paused, ended or seeking;
- decoded media time has not advanced by 20 ms for at least 1.25 s;
- the browser reports decoded media buffered ahead of the frozen timestamp.
Only then is the broken timestamp interval skipped by 180 ms. The media clock
immediately remains authoritative; the host does not free-run. A stale callback
from the old MSE window is also prevented from undoing an operator seek.
### LiDAR orientation inherited the wrong axes
The RRD declares `/world` as RFU (`Right`, `Forward`, `Up`) and logs
`/world/points` in map space. The earlier adapter treated raw LiDAR quaternion
columns as rover forward/left/up and inherited sensor roll/pitch. That is why the
grid, rover and facade could visibly disagree.
The v3 adapter now uses:
- map `+Z` as gravity/up;
- the smoothed pose-trajectory tangent projected onto the ground as forward;
- `left = up × forward`;
- projected sensor `+Y` only as a fallback when the tangent is unavailable.
This is a deterministic coordinate contract, not a visual angle correction.
### Sensor height was treated as a constant
RAV004 does not have a stable 0.4 m mounting height throughout the recording.
The adapter now estimates the local ground plane from a causal one-second
near-field point window and uses the sealed session estimate only as fallback.
Observed local heights include approximately 0.17 m, 1.24 m, 1.05 m and 0.22 m
at different route positions; a single hand-entered value is therefore invalid.
### Sparse LiDAR frames were held incorrectly
Camera is approximately 9.51 Hz while source points arrive at approximately
2 Hz. A camera frame without a new LiDAR increment used to retain whichever
spatial frame happened to finish loading last; under fast playback this could be
dozens of seconds old. The buffer now loads the active chunk first, the preceding
chunk second and the next chunk as prefetch. The scene selects the latest proven
source increment whose sequence is not later than the active camera frame.
At the final UI check, camera frame 189 causally held spatial frame 184. Before
the fix the same point could hold frame 16.
## Capability ledger
| Layer | RAV004 full route | UI behavior | Authority |
|---|---:|---|---|
| Recorded RIGHT camera | 6830/6830 | `VIDEO` / `CAMERA`, segmented playback | recorded evidence |
| DDRNet semantic mask | 6830/6830 | selectable, opaque enough for review | diagnostic prediction |
| EoMT semantic mask | 6830/6830 | selectable | diagnostic prediction |
| Diagnostic object boxes | derived from connected EoMT mask components | media-time gated | not an independent detector |
| Source points | 1444 increments | `SOURCE POINTS` | recorded geometry |
| Bounded Local SLAM | causal 5 s / 27k-point limit | `LOCAL SLAM` | visual-derived |
| Full-route TGS | **absent** | canonical `TGS COSTMAP` control is visible but disabled | unavailable, fail closed |
| Point-aligned 3D semantics | **absent** | canonical `SEMANTICS` control is visible but disabled | unavailable |
| Independent person/vehicle detector | **absent** | no STOP claim | unavailable |
Ten old TGS review anchors exist, but they are not a continuous route artifact.
They are not repeated or held as if they were full TGS. The accepted RAVNOVES00
full-TGS result is also not reused because it has a different source identity and
4489-frame timeline.
## Performance evidence
Measured on the canonical local service and current immutable artifacts:
- replay launch POST: 3.55 s on first opening;
- timeline metadata: 0.02 s warm;
- active spatial chunk, eight camera frames: 35.17 s first process-local RRD
index build, 0.67 s warm, approximately 3.81 MB;
- UI replay: passed the previously deterministic 11.422 s decoder stop, then
continued to 59 s with media and timeline advancing together;
- operator reset seek: 16.4 s to 0 s, one mounted media worker, successful;
- browser console after the acceptance run: no warnings or errors.
The first RRD index is still process-local rather than a persistent disk cache.
That is an explicit remaining performance gap; warm playback is the admitted
profile, cold restart latency is not yet accepted.
## Nature perception: current OPS stopping point
OPS card `MISSIONCOR-65` defines the intended independent layers as EoMT,
DDRNet, frozen YOLOX and TGS. The current immutable RAV004 artifact proves full
EoMT and DDRNet inference only. It does not prove full TGS, negative-obstacle
handling, an independent person/vehicle STOP layer or combined real-time load.
Isolated full-route measurements:
- DDRNet-39: p95 27.44 ms, 52.67 inference FPS, validation mean IoU 29.715%,
vegetation mean IoU 0.3701;
- EoMT: p95 361.62 ms, approximately 3.01 inference FPS;
- prior accepted RAVNOVES00 TGS: p95 1.694 ms CPU-only, but this is algorithm
performance on another source, not RAV004 proof.
The DDRNet isolated throughput is sufficient for a 10 FPS budget. DDRNet is not
accepted for driving policy because temporal stability and nature quality are
not sufficient: the OPS temporal sample recorded adjacent-frame IoU near 0.195
for high grass and 0.400 for woody vegetation. EoMT does not meet 10 FPS in its
current form. The next evidentiary milestone is therefore not another UI model
toggle; it is synchronized truth for grass/tree/ditch/drop-off, full TGS and
negative-obstacle evidence, frozen independent detector output and a combined
load test at at least 10 FPS.
Worker 006 was audited read-only. Triton and the Gaussian containers were left
untouched. The separate Mission Core perception worker is currently in a restart
loop (404 during model inference startup); this audit did not stop, recreate or
deploy it.
## Acceptance performed
- 44 focused backend tests passed;
- 37 frontend replay, buffering and LAB contract tests passed;
- TypeScript project typecheck passed;
- production Vite build passed (only existing large-chunk warnings);
- `git diff --check` passed;
- live browser run verified the shared controls, disabled unsealed TGS/3D
semantics, continuous media recovery, causal spatial hold and clean console.
Visual QA: `docs/handoff/2026-08-30_RAV004_CANONICAL_LAB_QA.jpg`.
## External coordinate and media references
- Rerun ViewCoordinates: <https://rerun.io/docs/reference/types/datatypes/view_coordinates>
- Rerun transform relation: <https://rerun.io/docs/reference/types/components/transform_relation>
- Rerun transforms: <https://rerun.io/docs/concepts/logging-and-ingestion/transforms>
- Rerun Transform3D: <https://rerun.io/docs/reference/types/archetypes/transform3d>
- WHATWG media element model: <https://html.spec.whatwg.org/multipage/media.html>
- W3C Media Source Extensions: <https://www.w3.org/TR/media-source-2/>
+207 -36
View File
@@ -5,7 +5,7 @@ transport. This adapter reads the immutable recording once, indexes the
recorded source cloud and sensor pose, estimates the session sensor height from
the initial stationary cloud, and returns both the current increment and a
bounded accumulated local-SLAM cloud in a ground-rebased body frame. Camera,
spatial layers and the common timeline can therefore be driven by one host
spatial layers and the common timeline can therefore be driven by one media
clock without a per-LAB coordinate adapter.
"""
@@ -21,7 +21,7 @@ from typing import Any, Final
import numpy as np
import rerun_bindings as rr_bindings
CANONICAL_LAB_SPATIAL_PROFILE: Final = "source-paced-ground-v2"
CANONICAL_LAB_SPATIAL_PROFILE: Final = "source-paced-ground-v3"
_POINT_ENTITY: Final = "/world/points"
_POSE_ENTITY: Final = "/world/sensor_pose"
_TRAJECTORY_ENTITY: Final = "/world/trajectory"
@@ -35,11 +35,15 @@ _HEIGHT_CALIBRATION_MAX_FRAMES: Final = 120
_HEIGHT_NEAR_MIN_RADIUS_M: Final = 1.0
_HEIGHT_NEAR_MAX_RADIUS_M: Final = 6.0
_HEIGHT_LOWER_QUANTILE: Final = 0.025
_LOCAL_HEIGHT_QUANTILE: Final = 0.10
_LOCAL_HEIGHT_HALF_WINDOW_SECONDS: Final = 1.0
_LOCAL_SLAM_HISTORY_SECONDS: Final = 5.0
_LOCAL_SLAM_RADIUS_M: Final = 30.0
_LOCAL_SLAM_VERTICAL_LIMIT_M: Final = 6.0
_LOCAL_SLAM_VOXEL_SIZE_M: Final = 0.12
_LOCAL_SLAM_POINT_LIMIT: Final = 27_000
_FORWARD_HALF_WINDOW_SECONDS: Final = 1.0
_FORWARD_MINIMUM_DISPLACEMENT_M: Final = 0.15
@dataclass(frozen=True)
@@ -233,6 +237,67 @@ def _map_points_to_body(
return body.astype(np.float32)
def _gravity_stable_basis_map_from_body(
poses: _TimedPoses,
target_time_ns: int,
) -> tuple[np.ndarray, str]:
"""Return a right-handed forward/left/up base frame in the RFU map.
Rerun declares this recording map as RFU, while the metric LAB scene
consumes points as forward/left/up. The LiDAR quaternion columns are sensor
right/forward/up and also contain rover or handheld roll/pitch, so they are
not a body basis. Route displacement owns yaw when available; the sensor's
local +Y (Rerun Forward) projected onto map gravity is the stationary
fallback. Map +Z always owns up.
"""
center = _latest_index(poses.times_ns, target_time_ns)
half_window_ns = round(_FORWARD_HALF_WINDOW_SECONDS * 1_000_000_000)
first = _latest_index(poses.times_ns, max(0, target_time_ns - half_window_ns))
last = min(
len(poses.times_ns) - 1,
max(0, bisect_right(poses.times_ns, target_time_ns + half_window_ns) - 1),
)
route = poses.translations[last] - poses.translations[first]
route_xy = np.asarray([route[0], route[1], 0.0], dtype=np.float64)
route_norm = float(np.linalg.norm(route_xy))
sensor_rotation = _rotation_map_from_body(poses.quaternions_xyzw[center])
sensor_forward = np.asarray(
[sensor_rotation[0, 1], sensor_rotation[1, 1], 0.0],
dtype=np.float64,
)
sensor_forward_norm = float(np.linalg.norm(sensor_forward))
if sensor_forward_norm <= 1e-9:
raise ValueError("Recorded LAB sensor forward axis is invalid")
sensor_forward /= sensor_forward_norm
if route_norm >= _FORWARD_MINIMUM_DISPLACEMENT_M:
forward = route_xy / route_norm
if float(np.dot(forward, sensor_forward)) < 0.0:
forward = -forward
forward_source = "smoothed-pose-trajectory-tangent"
else:
forward = sensor_forward
forward_source = "rerun-rfu-sensor-forward-fallback"
up = np.asarray([0.0, 0.0, 1.0], dtype=np.float64)
left = np.cross(up, forward)
left_norm = float(np.linalg.norm(left))
if left_norm <= 1e-9:
raise ValueError("Recorded LAB body left axis is invalid")
left /= left_norm
forward = np.cross(left, up)
forward /= float(np.linalg.norm(forward))
basis = np.column_stack((forward, left, up))
if (
not np.allclose(basis.T @ basis, np.eye(3), atol=1e-7)
or np.linalg.det(basis) < 0.999999
):
raise ValueError("Recorded LAB gravity-stable body basis is invalid")
return basis, forward_source
def _estimate_sensor_height(points: _TimedPoints, poses: _TimedPoses) -> tuple[float, int, float]:
"""Estimate one session mount height from the initial qualified cloud.
@@ -253,17 +318,13 @@ def _estimate_sensor_height(points: _TimedPoints, poses: _TimedPoses) -> tuple[f
estimates: list[float] = []
for point_index in candidates:
pose_index = _latest_index(poses.times_ns, points.times_ns[point_index])
body = _map_points_to_body(
points.values[point_index],
poses.translations[pose_index],
poses.quaternions_xyzw[pose_index],
)
radius = np.linalg.norm(body[:, :2], axis=1)
eligible = body[
delta = points.values[point_index].astype(np.float64) - poses.translations[pose_index]
radius = np.linalg.norm(delta[:, :2], axis=1)
eligible = delta[
(radius >= _HEIGHT_NEAR_MIN_RADIUS_M)
& (radius <= _HEIGHT_NEAR_MAX_RADIUS_M)
& (body[:, 2] >= -2.0)
& (body[:, 2] <= 0.5)
& (delta[:, 2] >= -2.0)
& (delta[:, 2] <= 0.5)
]
if eligible.shape[0] < 100:
continue
@@ -278,12 +339,55 @@ def _estimate_sensor_height(points: _TimedPoints, poses: _TimedPoses) -> tuple[f
return height, len(estimates), mad
def _estimate_local_sensor_height(
points: _TimedPoints,
poses: _TimedPoses,
target_time_ns: int,
fallback_height_m: float,
) -> tuple[float, int, float, str]:
"""Estimate the current gravity-axis height without a fixed camera mount.
RAVNOVES004TREE changes sensor height during the route. A session-wide
constant therefore moves the scene vertically whenever the operator raises
or lowers K1. Use a short source-time window and a conservative near-field
ground quantile; fall back to the sealed session calibration only when the
current cloud has insufficient support.
"""
half_window_ns = round(_LOCAL_HEIGHT_HALF_WINDOW_SECONDS * 1_000_000_000)
first = bisect_right(points.times_ns, max(0, target_time_ns - half_window_ns) - 1)
last = bisect_right(points.times_ns, target_time_ns + half_window_ns)
estimates: list[float] = []
for point_index in range(first, last):
pose_index = _latest_index(poses.times_ns, points.times_ns[point_index])
delta = points.values[point_index].astype(np.float64) - poses.translations[pose_index]
radius = np.linalg.norm(delta[:, :2], axis=1)
eligible = delta[
(radius >= _HEIGHT_NEAR_MIN_RADIUS_M)
& (radius <= _HEIGHT_NEAR_MAX_RADIUS_M)
& (delta[:, 2] >= -2.5)
& (delta[:, 2] <= 0.5)
]
if eligible.shape[0] < 100:
continue
estimate = -float(np.quantile(eligible[:, 2], _LOCAL_HEIGHT_QUANTILE))
if 0.03 <= estimate <= 2.5:
estimates.append(estimate)
if not estimates:
return fallback_height_m, 0, 0.0, "session-source-cloud-fallback"
values = np.asarray(estimates, dtype=np.float64)
height = float(np.median(values))
mad = float(np.median(np.abs(values - height)))
return height, len(estimates), mad, "local-source-cloud-ground-quantile-median"
def _ground_origin_map(
sensor_origin_map: np.ndarray,
basis_map_from_body: np.ndarray,
sensor_height_m: float,
) -> np.ndarray:
return sensor_origin_map - basis_map_from_body[:, 2] * sensor_height_m
# The calibrated height belongs to the map gravity axis. Sensor roll/pitch
# must never tilt the ground origin or the accumulated world cloud.
return sensor_origin_map - np.asarray([0.0, 0.0, sensor_height_m])
def _map_points_to_ground_body(
@@ -329,32 +433,29 @@ def _bounded_local_slam(
return np.ascontiguousarray(local, dtype=np.float32), len(selected), source_count
def canonical_lab_spatial_frame(
recording_path: Path,
generation_sha256: str,
def _canonical_lab_spatial_frame_from_index(
index: _CanonicalSpatialIndex,
target_time_ns: int,
) -> dict[str, object]:
"""Return the current source cloud and bounded Local SLAM on one host time."""
if target_time_ns < 0:
raise ValueError("Recorded LAB target time is invalid")
stat = recording_path.stat()
index = _load_index(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
)
point_index = _latest_index(index.points.times_ns, target_time_ns)
pose_index = _latest_index(index.poses.times_ns, index.points.times_ns[point_index])
trajectory_index = _latest_index(index.trajectories.times_ns, target_time_ns)
translation = index.poses.translations[pose_index]
quaternion = index.poses.quaternions_xyzw[pose_index]
basis_map_from_body = _rotation_map_from_body(quaternion)
sensor_height_m, sensor_height_sample_count, sensor_height_mad_m, height_source = (
_estimate_local_sensor_height(
index.points,
index.poses,
index.points.times_ns[point_index],
index.sensor_height_m,
)
)
basis_map_from_body, forward_source = _gravity_stable_basis_map_from_body(
index.poses,
index.points.times_ns[point_index],
)
ground_origin = _ground_origin_map(
translation,
basis_map_from_body,
index.sensor_height_m,
sensor_height_m,
)
points_body = _map_points_to_ground_body(
index.points.values[point_index],
@@ -368,17 +469,18 @@ def canonical_lab_spatial_frame(
basis_map_from_body,
)
return {
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v2",
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v3",
"target_time_ns": target_time_ns,
"source_time_ns": index.points.times_ns[point_index],
"pose_time_ns": index.poses.times_ns[pose_index],
"trajectory_time_ns": index.trajectories.times_ns[trajectory_index],
"coordinate_frame": "body-ground",
"sensor_height": {
"meters": index.sensor_height_m,
"source": "initial-source-cloud-lower-quantile-median",
"sample_count": index.sensor_height_sample_count,
"mad_m": index.sensor_height_mad_m,
"meters": sensor_height_m,
"source": height_source,
"sample_count": sensor_height_sample_count,
"mad_m": sensor_height_mad_m,
"session_fallback_meters": index.sensor_height_m,
"authority": "visual-derived",
},
"spatial_profile": {
@@ -392,6 +494,8 @@ def canonical_lab_spatial_frame(
"origin_map_xyz_m": ground_origin.tolist(),
"sensor_origin_map_xyz_m": translation.tolist(),
"basis_map_from_body": basis_map_from_body.tolist(),
"up_source": "rerun-rfu-map-gravity-axis",
"forward_source": forward_source,
},
"source_point_count": int(points_body.shape[0]),
"source_points_body_xyz_m": points_body.tolist(),
@@ -400,3 +504,70 @@ def canonical_lab_spatial_frame(
"local_slam_point_count": int(local_slam.shape[0]),
"local_slam_body_xyz_m": local_slam.tolist(),
}
def canonical_lab_spatial_frame(
recording_path: Path,
generation_sha256: str,
target_time_ns: int,
) -> dict[str, object]:
"""Return the current source cloud and bounded Local SLAM on one media time."""
if target_time_ns < 0:
raise ValueError("Recorded LAB target time is invalid")
stat = recording_path.stat()
index = _load_index(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
)
return _canonical_lab_spatial_frame_from_index(index, target_time_ns)
def canonical_lab_spatial_timeline_samples(
recording_path: Path,
generation_sha256: str,
frame_times_ns: tuple[int, ...],
start_sequence: int,
frame_count: int,
) -> tuple[dict[str, object] | None, ...]:
"""Project only new source increments onto a denser camera timeline.
Camera is roughly 10 Hz in RAVNOVES004TREE while the sealed source cloud is
roughly 2 Hz. Returning the same JSON point array for every camera frame
multiplies transfer and parse cost and makes the viewer chase itself. A row
is populated only when its nearest causal source increment changes; the
canonical viewer retains that spatial frame until the next increment.
"""
if (
start_sequence < 0
or frame_count < 1
or start_sequence >= len(frame_times_ns)
or any(current <= previous for previous, current in zip(frame_times_ns, frame_times_ns[1:]))
):
raise ValueError("Recorded LAB timeline sample request is invalid")
stat = recording_path.stat()
index = _load_index(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
)
stop = min(len(frame_times_ns), start_sequence + frame_count)
samples: list[dict[str, object] | None] = []
for sequence in range(start_sequence, stop):
target_time_ns = frame_times_ns[sequence]
point_index = _latest_index(index.points.times_ns, target_time_ns)
previous_point_index = (
-1
if sequence == 0
else _latest_index(index.points.times_ns, frame_times_ns[sequence - 1])
)
samples.append(
_canonical_lab_spatial_frame_from_index(index, target_time_ns)
if point_index != previous_point_index
else None
)
return tuple(samples)
+15
View File
@@ -360,6 +360,15 @@ def _m48_recorded_camera_playback_source(
return session_recorded_camera_frame_service.playback_source(session_id)
def _canonical_lab_recording_source(session_id: str) -> tuple[Path, str] | None:
"""Resolve one already-published immutable RRD without starting new work."""
snapshot = session_recording_preparation_manager.status(session_id)
if snapshot is None or snapshot.state != "ready" or snapshot.recording is None:
return None
return snapshot.recording.path, snapshot.recording.sha256
def refresh_observation_catalog() -> tuple[str, ...]:
"""Discover completed or recoverable local evidence without copying payloads."""
@@ -1032,6 +1041,12 @@ app.include_router(
/ "lab-v1-vegetation"
/ "results"
),
canonical_recording_provider=_canonical_lab_recording_source,
camera_frame_provider=(
session_recorded_camera_frame_service.extract
if session_recorded_camera_frame_service is not None
else None
),
)
)
app.include_router(
+2 -2
View File
@@ -835,11 +835,11 @@ def build_session_router(
session_id: str,
generation: Annotated[str, Query(min_length=64, max_length=64)],
time_ns: Annotated[int, Query(ge=0, le=MAX_SAFE_INTEGER)],
profile: Literal["source-paced-ground-v2"],
profile: Literal["source-paced-ground-v3"],
) -> JSONResponse:
"""Serve one body-frame sample for the canonical recorded-LAB clock.
The camera timeline owns playback. Spatial evidence is sampled from
The camera media clock owns playback. Spatial evidence is sampled from
the same immutable recording instead of starting a second Rerun clock.
"""
+390 -1
View File
@@ -4,7 +4,10 @@ from __future__ import annotations
import copy
import hashlib
import io
import json
import math
import statistics
import zipfile
from collections.abc import Callable
from functools import lru_cache
@@ -14,6 +17,7 @@ from typing import Any, Final
import numpy as np
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse, JSONResponse, Response
from PIL import Image
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
from k1link.laboratory.evidence_report import (
@@ -21,9 +25,14 @@ from k1link.laboratory.evidence_report import (
verify_laboratory_evidence_result,
)
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA
from k1link.sessions import RecordedCameraFrame, SessionIntegrityError
from k1link.sessions.canonical_lab_spatial import canonical_lab_spatial_timeline_samples
RootProvider = Callable[[], Path | None]
CanonicalRecordingProvider = Callable[[str], tuple[Path, str] | None]
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
_CANONICAL_ROUTE_CHUNK_FRAMES: Final = 8
_DEFINITION: Final = LaboratoryEvidenceDefinition(
work_id="lab-v1-vegetation-shadow",
runtime_relative_root=PurePosixPath("lab-v1-vegetation/results"),
@@ -41,12 +50,17 @@ _BENCHMARK_DEFINITION: Final = LaboratoryEvidenceDefinition(
def build_vegetation_shadow_lab_router(
*, root_provider: RootProvider = lambda: None,
*,
root_provider: RootProvider = lambda: None,
canonical_recording_provider: CanonicalRecordingProvider | None = None,
camera_frame_provider: CameraFrameProvider | None = None,
) -> APIRouter:
return _build_vegetation_lab_router(
prefix="/api/v1/laboratory/vegetation-shadow",
definition=_DEFINITION,
root_provider=root_provider,
canonical_recording_provider=canonical_recording_provider,
camera_frame_provider=camera_frame_provider,
)
@@ -65,6 +79,8 @@ def _build_vegetation_lab_router(
prefix: str,
definition: LaboratoryEvidenceDefinition,
root_provider: RootProvider,
canonical_recording_provider: CanonicalRecordingProvider | None = None,
camera_frame_provider: CameraFrameProvider | None = None,
) -> APIRouter:
router = APIRouter(
prefix=prefix,
@@ -263,6 +279,143 @@ def _build_vegetation_lab_router(
},
)
@router.get("/{result_id}/timeline")
def get_canonical_route_timeline(result_id: str) -> dict[str, object]:
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
intervals = [
(current - previous) / 1_000_000_000
for previous, current in zip(frame_times_ns, frame_times_ns[1:])
]
nominal_interval = statistics.median(intervals)
if not math.isfinite(nominal_interval) or nominal_interval <= 0:
raise HTTPException(status_code=503, detail="Full-route timeline cadence is invalid")
return {
"schema_version": "missioncore.recorded-spatial-evidence-timeline/v1",
"result_id": result_id,
"recorded_source": {
"session_id": route["session_id"],
"source_id": route["source_id"],
"representation_id": "registered-map-increment-v1",
"synchronization": "host-arrival-best-effort",
},
"frame_count": len(frame_times_ns),
"frame_times_ns": list(frame_times_ns),
"timeline_start_seconds": frame_times_ns[0] / 1_000_000_000,
"timeline_end_seconds": frame_times_ns[-1] / 1_000_000_000,
"nominal_frame_interval_seconds": nominal_interval,
"nominal_rate_hz": 1.0 / nominal_interval,
"max_chunk_frames": _CANONICAL_ROUTE_CHUNK_FRAMES,
"point_sample_limit": 100_000,
"maximum_source_points_per_frame": 100_000,
"point_delivery": "exact-current-increment",
"world_state_frame_count": len(frame_times_ns),
"superseded_frame_count": 0,
"local_surface_visualization": {
"derivation": "bounded-registered-increment-accumulation",
"window_seconds": 5.0,
"voxel_size_m": 0.12,
"radius_m": 30.0,
"point_limit": 27_000,
"authority": "visual-derived",
},
"image_width": route["width"],
"image_height": route["height"],
"rig": {"length_m": 1.0, "width_m": 0.8, "nominal_sensor_height_m": 0.4},
"corridor": {
"forward_length_m": 8.0,
"rear_margin_m": 0.5,
"occupied_voxel_size_m": 0.45,
"half_width_m": 0.6,
"prediction_horizon_seconds": 8.0,
},
"ground_truth": False,
"authority": "replay-simulated",
"access": "read-only-bounded-recorded-replay",
}
@router.get("/{result_id}/timeline/chunk")
def get_canonical_route_timeline_chunk(
result_id: str,
start: int = 0,
count: int = _CANONICAL_ROUTE_CHUNK_FRAMES,
include_points: bool = True,
) -> dict[str, object]:
if start < 0 or not 1 <= count <= _CANONICAL_ROUTE_CHUNK_FRAMES:
raise HTTPException(status_code=422, detail="Full-route timeline chunk is invalid")
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
if start >= len(frame_times_ns):
raise HTTPException(status_code=404, detail="Full-route timeline chunk not found")
if canonical_recording_provider is None:
raise HTTPException(status_code=503, detail="Canonical spatial recording is unavailable")
recording = canonical_recording_provider(str(route["session_id"]))
if recording is None:
raise HTTPException(status_code=409, detail="Canonical spatial recording is not ready")
recording_path, generation_sha256 = recording
try:
samples = canonical_lab_spatial_timeline_samples(
recording_path,
generation_sha256,
frame_times_ns,
start,
count,
)
except (OSError, ValueError):
raise HTTPException(status_code=503, detail="Canonical spatial chunk failed") from None
stop = start + len(samples)
frames = [
_canonical_timeline_frame(
result_id=result_id,
endpoint_prefix=prefix,
candidate=candidate,
route=route,
sequence=sequence,
source_time_ns=frame_times_ns[sequence],
spatial=sample,
include_points=include_points,
)
for sequence, sample in zip(range(start, stop), samples, strict=True)
]
return {
"schema_version": "missioncore.recorded-spatial-evidence-chunk/v1",
"result_id": result_id,
"start_sequence": start,
"frame_count": len(frames),
"next_sequence": stop if stop < len(frame_times_ns) else None,
"frames": frames,
"ground_truth": False,
"authority": "replay-simulated",
"access": "read-only-bounded-recorded-replay",
}
@router.get("/{result_id}/timeline/frames/{sequence}/camera")
def get_canonical_route_camera(result_id: str, sequence: int) -> Response:
if camera_frame_provider is None:
raise HTTPException(status_code=503, detail="Recorded camera decoder is unavailable")
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
if not 0 <= sequence < len(frame_times_ns):
raise HTTPException(status_code=404, detail="Full-route camera frame not found")
try:
camera = camera_frame_provider(str(route["session_id"]), sequence)
except (OSError, SessionIntegrityError, ValueError):
raise HTTPException(status_code=503, detail="Full-route camera frame unavailable") from None
if camera.width != route["width"] or camera.height != route["height"]:
raise HTTPException(status_code=503, detail="Full-route camera dimensions changed")
return Response(
content=camera.payload,
media_type=camera.media_type,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{camera.sha256}"',
"X-Content-Type-Options": "nosniff",
},
)
@router.get("/{result_id}/route-tgs-anchor/{source_sequence}")
def get_route_tgs_anchor(result_id: str, source_sequence: int) -> JSONResponse:
candidate = _resolve_candidate(root_provider, definition, result_id)
@@ -314,6 +467,242 @@ def _build_vegetation_lab_router(
return router
def _full_route_context(
candidate: Path,
manifest: dict[str, Any],
) -> tuple[dict[str, Any], tuple[int, ...]]:
route = manifest.get("route_full_review")
timeline = route.get("timeline") if isinstance(route, dict) else None
relative_text = timeline.get("path") if isinstance(timeline, dict) else None
if (
not isinstance(route, dict)
or route.get("source_id") != "RAVNOVES004TREE"
or route.get("session_id") != "20260828T130511Z_viewer_live"
or route.get("frame_count") != 6830
or route.get("width") != 800
or route.get("height") != 600
or not isinstance(relative_text, str)
):
raise HTTPException(status_code=404, detail="Full-route canonical timeline not found")
path = candidate.joinpath(*PurePosixPath(relative_text).parts)
try:
payload = path.read_bytes()
if (
len(payload) != timeline.get("byte_length")
or hashlib.sha256(payload).hexdigest() != timeline.get("sha256")
):
raise ValueError("timeline digest changed")
values = np.frombuffer(payload, dtype="<u8")
frame_times_ns = tuple(int(value) for value in values)
except (OSError, ValueError):
raise HTTPException(status_code=503, detail="Full-route timeline verification failed") from None
if (
len(frame_times_ns) != route["frame_count"]
or any(current <= previous for previous, current in zip(frame_times_ns, frame_times_ns[1:]))
):
raise HTTPException(status_code=503, detail="Full-route timeline order changed")
return route, frame_times_ns
def _canonical_timeline_frame(
*,
result_id: str,
endpoint_prefix: str,
candidate: Path,
route: dict[str, Any],
sequence: int,
source_time_ns: int,
spatial: dict[str, object] | None,
include_points: bool,
) -> dict[str, object]:
points = [] if spatial is None or not include_points else spatial["source_points_body_xyz_m"]
point_count = 0 if spatial is None else int(spatial["source_point_count"])
body_frame = None if spatial is None else spatial["body_frame"]
local_slam = [] if spatial is None else spatial["local_slam_body_xyz_m"]
return {
"schema_version": "missioncore.recorded-spatial-evidence-frame/v1",
"sequence": sequence,
"frame_id": f"frame-{sequence:06d}",
"source_time_ns": source_time_ns,
"session_seconds": source_time_ns / 1_000_000_000,
"source_available": spatial is not None,
"spatial_available": spatial is not None,
"world_state_available": True,
"terminal_outcome": "delivered",
"body_frame": body_frame,
"point_cloud_body_xyz_m": points,
"point_cloud_source_count": point_count,
"point_cloud_sample_count": point_count if not include_points else len(points),
"point_cloud_layer": "current-increment",
"local_slam_body_xyz_m": local_slam,
"local_slam_source_frame_count": 0
if spatial is None else spatial["local_slam_source_frame_count"],
"local_slam_source_point_count": 0
if spatial is None else spatial["local_slam_source_point_count"],
"rolling_map_component_count": 0,
"metric_obstacles": [],
"camera_proposals": _semantic_component_proposals(candidate, route, sequence),
"decision_counts": {"threat": 0, "not-threat": 0, "unknown": 0},
"camera_url": (
f"{endpoint_prefix}/{result_id}/timeline/frames/{sequence}/camera"
),
"ground_truth": False,
"authority": "replay-simulated",
}
def _semantic_component_proposals(
candidate: Path,
route: dict[str, Any],
sequence: int,
) -> list[dict[str, object]]:
layers = route.get("layers")
city = layers.get("city") if isinstance(layers, dict) else None
archive = city.get("mask_archive") if isinstance(city, dict) else None
relative = archive.get("path") if isinstance(archive, dict) else None
if not isinstance(relative, str):
return []
archive_path = candidate.joinpath(*PurePosixPath(relative).parts)
try:
stat = archive_path.stat()
except OSError:
return []
return [
dict(proposal)
for proposal in _semantic_component_proposals_cached(
str(archive_path),
stat.st_size,
stat.st_mtime_ns,
sequence,
)
]
@lru_cache(maxsize=256)
def _semantic_component_proposals_cached(
archive_path_text: str,
archive_size: int,
archive_mtime_ns: int,
sequence: int,
) -> tuple[dict[str, object], ...]:
del archive_size, archive_mtime_ns
archive_path = Path(archive_path_text)
member = f"masks/frame-{sequence + 1:06d}.png"
try:
with zipfile.ZipFile(archive_path) as frozen:
payload = frozen.read(member)
with Image.open(io.BytesIO(payload)) as image:
mask = np.asarray(image.convert("L"), dtype=np.uint8)
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
return ()
labels = {
1: "semantic person",
2: "semantic bicycle",
3: "semantic motorcycle",
4: "semantic car",
5: "semantic heavy vehicle",
13: "semantic static obstacle",
14: "semantic animal",
}
proposals: list[dict[str, object]] = []
for class_id, label in labels.items():
minimum_pixels = 80 if class_id == 13 else 24
for component_index, (left, top, right, bottom, pixel_count) in enumerate(
_mask_component_boxes(mask, class_id, minimum_pixels=minimum_pixels)[:12]
):
proposals.append({
"proposal_id": f"semantic-{class_id}-{sequence}-{component_index}",
"bbox_xyxy": [left, top, right, bottom],
"objectness": round(min(0.99, 0.5 + pixel_count / 20_000), 4),
"semantic_hint": label,
"occupied_support": False,
"range_m": None,
"threat_decision": None,
"threat_reason_codes": ["semantic-mask-derived-not-fail-safe-detector"],
})
proposals.sort(
key=lambda proposal: (
-float(proposal["objectness"]),
str(proposal["proposal_id"]),
)
)
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:
+59 -1
View File
@@ -8,6 +8,8 @@ from k1link.sessions.canonical_lab_spatial import (
_TimedPoses,
_bounded_local_slam,
_estimate_sensor_height,
_estimate_local_sensor_height,
_gravity_stable_basis_map_from_body,
_ground_origin_map,
)
@@ -57,7 +59,7 @@ def test_local_slam_accumulates_source_increments_in_ground_body_frame() -> None
),
)
basis = np.eye(3)
ground_origin = _ground_origin_map(np.asarray([0.0, 0.0, 0.0]), basis, 0.32)
ground_origin = _ground_origin_map(np.asarray([0.0, 0.0, 0.0]), 0.32)
local, frame_count, source_count = _bounded_local_slam(
points,
@@ -69,3 +71,59 @@ def test_local_slam_accumulates_source_increments_in_ground_body_frame() -> None
assert frame_count == 3
assert source_count == 3
assert local[:, 2].tolist() == pytest.approx([0.0, 0.0, 0.0], abs=1e-6)
def test_gravity_stable_body_frame_converts_rfu_to_forward_left_up() -> None:
times = (0, 1_000_000_000, 2_000_000_000)
poses = _TimedPoses(
times_ns=times,
translations=(
np.asarray([0.0, 0.0, 0.4]),
np.asarray([0.0, 1.0, 0.5]),
np.asarray([0.0, 2.0, 0.3]),
),
quaternions_xyzw=tuple(
np.asarray([0.25, 0.0, 0.0, np.sqrt(1.0 - 0.25**2)]) for _ in times
),
)
basis, source = _gravity_stable_basis_map_from_body(poses, 1_000_000_000)
assert source == "smoothed-pose-trajectory-tangent"
assert basis[:, 0].tolist() == pytest.approx([0.0, 1.0, 0.0], abs=1e-7)
assert basis[:, 1].tolist() == pytest.approx([-1.0, 0.0, 0.0], abs=1e-7)
assert basis[:, 2].tolist() == pytest.approx([0.0, 0.0, 1.0], abs=1e-7)
assert np.linalg.det(basis) == pytest.approx(1.0, abs=1e-7)
def test_ground_origin_is_projected_only_along_map_gravity() -> None:
origin = _ground_origin_map(np.asarray([4.0, -2.0, 1.25]), 0.32)
assert origin.tolist() == pytest.approx([4.0, -2.0, 0.93], abs=1e-9)
def test_sensor_height_tracks_current_source_window_instead_of_fixed_mount() -> None:
times = tuple(index * 500_000_000 for index in range(8))
points = _TimedPoints(
times_ns=times,
values=tuple(
_calibration_cloud(0.18 if index < 4 else 1.05, index)
for index in range(8)
),
)
poses = _TimedPoses(
times_ns=times,
translations=tuple(np.zeros(3) for _ in times),
quaternions_xyzw=tuple(np.asarray([0.0, 0.0, 0.0, 1.0]) for _ in times),
)
low, low_samples, _, low_source = _estimate_local_sensor_height(
points, poses, 500_000_000, 0.5,
)
high, high_samples, _, high_source = _estimate_local_sensor_height(
points, poses, 3_000_000_000, 0.5,
)
assert low == pytest.approx(0.18, abs=0.03)
assert high == pytest.approx(1.05, abs=0.03)
assert low_samples >= 3 and high_samples >= 3
assert low_source == high_source == "local-source-cloud-ground-quantile-median"
+5 -5
View File
@@ -489,7 +489,7 @@ def test_canonical_lab_spatial_frame_uses_ready_immutable_recording(
assert resolved is not None and resolved.recording is not None
generation = hashlib.sha256(payload).hexdigest()
expected = {
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v2",
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v3",
"target_time_ns": 500_000_000,
"source_time_ns": 499_000_000,
"pose_time_ns": 499_000_000,
@@ -497,13 +497,13 @@ def test_canonical_lab_spatial_frame_uses_ready_immutable_recording(
"coordinate_frame": "body-ground",
"sensor_height": {
"meters": 0.32,
"source": "initial-source-cloud-lower-quantile-median",
"source": "local-source-cloud-ground-quantile-median",
"sample_count": 20,
"mad_m": 0.03,
"authority": "visual-derived",
},
"spatial_profile": {
"profile_id": "source-paced-ground-v2",
"profile_id": "source-paced-ground-v3",
"local_slam_history_seconds": 5.0,
"local_slam_radius_m": 30.0,
"local_slam_voxel_size_m": 0.12,
@@ -544,11 +544,11 @@ def test_canonical_lab_spatial_frame_uses_ready_immutable_recording(
session_id=session.name,
generation=generation,
time_ns=500_000_000,
profile="source-paced-ground-v2",
profile="source-paced-ground-v3",
))
assert json.loads(response.body) == expected
assert response.headers["etag"] == (
f'"{generation}:source-paced-ground-v2:499000000"'
f'"{generation}:source-paced-ground-v3:499000000"'
)
assert response.headers["cache-control"].endswith("immutable")
finally:
+13
View File
@@ -22,6 +22,7 @@ from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
from k1link.laboratory.vegetation_policy_review import seal_vegetation_policy_review
from k1link.laboratory.vegetation_shadow_lab import seal_vegetation_shadow_lab
from k1link.web.vegetation_shadow_lab_api import (
_mask_component_boxes,
_route_tgs_anchor_payload,
build_vegetation_shadow_lab_router,
)
@@ -29,6 +30,18 @@ from k1link.web.vegetation_shadow_lab_api import (
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
def test_semantic_component_boxes_keep_distinct_objects_separate() -> None:
mask = np.zeros((20, 30), dtype=np.uint8)
mask[2:10, 3:8] = 4
mask[4:12, 18:24] = 4
mask[15:17, 3:5] = 4
assert _mask_component_boxes(mask, 4, minimum_pixels=20) == [
(18, 4, 24, 12, 48),
(3, 2, 8, 10, 40),
]
def test_route_tgs_anchor_payload_preserves_metric_evidence(tmp_path: Path) -> None:
path = tmp_path / "tgs-evidence.npz"
point_counts = np.arange(1, 11, dtype=np.int64)