fix(lab): enforce canonical replay runtime

This commit is contained in:
DCCONSTRUCTIONS
2026-08-29 22:42:30 +03:00
parent 525ab74168
commit bd2892140f
17 changed files with 1765 additions and 507 deletions
@@ -781,6 +781,8 @@ export function RecordedFmp4Player({
onAdmissionChange,
onPlaybackChange,
onPlayingRejected,
playbackAuthority = "media",
playbackTransport = "segmented",
}: {
source: ObservationSourceDescriptor;
playback?: RecordedObservationPlayback | null;
@@ -793,6 +795,8 @@ export function RecordedFmp4Player({
onAdmissionChange?: (state: RecordedCameraAdmissionState) => void;
onPlaybackChange?: (playback: RecordedObservationPlayback) => void;
onPlayingRejected?: () => void;
playbackAuthority?: "media" | "host";
playbackTransport?: "segmented" | "epoch-stream";
}) {
const videoRef = useRef<HTMLVideoElement>(null);
const onAdmissionChangeRef = useRef(onAdmissionChange);
@@ -888,7 +892,8 @@ export function RecordedFmp4Player({
&& effectiveSegmentSequence !== requestedSegmentSequence,
);
const segmented = Boolean(
requestedSegmentSequence !== null
playbackTransport === "segmented"
&& requestedSegmentSequence !== null
&& Number.isInteger(requestedSegmentSequence)
&& requestedSegmentSequence >= 1
&&
@@ -1201,6 +1206,11 @@ export function RecordedFmp4Player({
|| segmentedRuntimeRef.current !== runtime
|| runtime.target?.revision !== revision
) return;
// Canonical recorded LABs run one host-owned clock for camera and
// spatial evidence. A transient MSE play() rejection (commonly a
// pause/reset race while the next fragment is admitted) must stay a
// decoder concern: the rolling target will retry and catch up.
if (playbackAuthority === "host") return;
onPlayingRejectedRef.current?.();
setReadyGeneration(null);
setErrorMessage("Запуск записанной камеры отклонён браузером.");
@@ -1311,7 +1321,13 @@ export function RecordedFmp4Player({
if (targetReadyAbortRef.current === targetReadyAbort) targetReadyAbortRef.current = null;
if (runtime.onTargetBuffered === markBuffered) runtime.onTargetBuffered = null;
};
}, [archive?.byteLength, effectiveSegmentSequence, segmented, segmentedRuntimeGeneration]);
}, [
archive?.byteLength,
effectiveSegmentSequence,
playbackAuthority,
segmented,
segmentedRuntimeGeneration,
]);
useEffect(() => {
const video = videoRef.current;
@@ -1407,6 +1423,7 @@ export function RecordedFmp4Player({
if (playback?.playing && !holdingForSegmentRecovery) {
void video.play().catch(() => {
if (playAttemptRevisionRef.current !== playAttemptRevision) return;
if (playbackAuthority === "host") return;
onPlayingRejectedRef.current?.();
setReadyGeneration(null);
setErrorMessage("Запуск записанной камеры отклонён браузером.");
@@ -1427,6 +1444,7 @@ export function RecordedFmp4Player({
epoch,
holdingForSegmentRecovery,
playback?.playing,
playbackAuthority,
playbackRate,
segmented,
visualState,
@@ -1454,6 +1472,7 @@ export function RecordedFmp4Player({
video.playbackRate = playbackRateRef.current;
void video.play().catch(() => {
if (cancelled || playAttemptRevisionRef.current !== playAttemptRevision) return;
if (playbackAuthority === "host") return;
onPlayingRejectedRef.current?.();
});
};
@@ -1465,7 +1484,7 @@ export function RecordedFmp4Player({
cancelled = true;
for (const event of events) video.removeEventListener(event, queueResume);
};
}, [bufferRevision, playback?.playing, segmented, visualState]);
}, [bufferRevision, playback?.playing, playbackAuthority, segmented, visualState]);
useEffect(() => {
const video = videoRef.current;
@@ -0,0 +1,245 @@
import { useCallback, useEffect, useState, type ReactNode } from "react";
import { SegmentedControl, SplitPane, type SplitPaneOrientation } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "./LaboratoryEvidenceViewer";
export const CANONICAL_RECORDED_LAB_REPLAY_CONTRACT =
"missioncore.canonical-recorded-lab-replay/v1";
export interface CanonicalRecordedLabMode<T extends string> {
value: T;
label: string;
}
/**
* The interaction contract from the accepted recorded-LAB instrument.
*
* Keeping pane selection, collapse semantics, responsive split orientation,
* expansion and splitter state here prevents individual experiments from
* quietly growing their own replay behaviour. Experiments provide evidence
* layers; they do not reimplement the laboratory shell.
*/
export function useCanonicalRecordedLabReplayState<
TMedia extends string,
TSpatial extends string,
>({
initialMediaMode,
initialSpatialMode,
}: {
initialMediaMode: TMedia;
initialSpatialMode: TSpatial | null;
}) {
const [mediaMode, setMediaMode] = useState<TMedia | null>(initialMediaMode);
const [spatialMode, setSpatialMode] = useState<TSpatial | null>(initialSpatialMode);
const [splitPrimarySize, setSplitPrimarySize] = useState(50);
const [splitOrientation, setSplitOrientation] = useState<SplitPaneOrientation>(() => (
typeof window !== "undefined" && window.matchMedia("(max-width: 900px)").matches
? "horizontal"
: "vertical"
));
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const query = window.matchMedia("(max-width: 900px)");
const update = () => setSplitOrientation(query.matches ? "horizontal" : "vertical");
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
const onMediaModeChange = useCallback((next: TMedia | "none") => {
if (next === "none") return;
setMediaMode((current) => current === next ? null : next);
}, []);
const onSpatialModeChange = useCallback((next: TSpatial | "none") => {
if (next === "none") return;
setSpatialMode((current) => current === next ? null : next);
}, []);
return {
mediaMode,
spatialMode,
splitView: mediaMode !== null && spatialMode !== null,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange,
onSpatialModeChange,
onSplitPrimarySizeChange: setSplitPrimarySize,
onExpandedChange: setExpanded,
};
}
export function CanonicalRecordedLabReplay<
TMedia extends string,
TSpatial extends string,
>({
label,
mediaMode,
mediaModes,
spatialMode,
spatialModes,
expanded,
splitPrimarySize,
splitOrientation,
mediaAriaLabel,
spatialAriaLabel,
mediaLayerControls,
spatialLayerControls,
spatialLeadingControl,
mediaMultiLayer = false,
mediaContent,
spatialContent,
emptyMessage,
deckOverlays,
actions,
overlay,
transport,
trailingActions,
onMediaModeChange,
onSpatialModeChange,
onExpandedChange,
onSplitPrimarySizeChange,
}: {
label: string;
mediaMode: TMedia;
mediaModes: readonly CanonicalRecordedLabMode<TMedia>[];
spatialMode: TSpatial;
spatialModes: readonly CanonicalRecordedLabMode<TSpatial>[];
expanded: boolean;
splitPrimarySize: number;
splitOrientation: SplitPaneOrientation;
mediaAriaLabel: string;
spatialAriaLabel: string;
mediaLayerControls?: ReactNode;
spatialLayerControls?: ReactNode;
spatialLeadingControl?: ReactNode;
mediaMultiLayer?: boolean;
mediaContent: ReactNode;
spatialContent: ReactNode;
emptyMessage: string;
deckOverlays?: ReactNode;
actions?: ReactNode;
overlay?: ReactNode;
transport?: ReactNode;
trailingActions?: ReactNode;
onMediaModeChange: (mode: TMedia) => void;
onSpatialModeChange: (mode: TSpatial) => void;
onExpandedChange: (expanded: boolean) => void;
onSplitPrimarySizeChange: (size: number) => void;
}) {
const splitView = mediaMode !== "none" && spatialMode !== "none";
const mediaModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="media">
<SegmentedControl
value={mediaMode}
items={[...mediaModes]}
label="Видео и камера"
onChange={onMediaModeChange}
/>
</div>
);
const spatialModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="spatial">
<SegmentedControl
value={spatialMode}
items={[...spatialModes]}
label="3D и план"
onChange={onSpatialModeChange}
/>
</div>
);
const mediaPane = (
<section
className="m4-replay-threat-visual__pane"
data-pane="media"
aria-label={mediaAriaLabel}
hidden={mediaMode === "none"}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="media"
data-multi-semantic={mediaMultiLayer ? "true" : undefined}
>
{mediaLayerControls}
{mediaModeControls}
</div>
) : null}
{mediaContent}
</section>
);
const spatialPane = spatialMode !== "none" ? (
<section
className="m4-replay-threat-visual__pane"
data-pane="spatial"
aria-label={spatialAriaLabel}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="spatial"
>
{spatialLeadingControl}
<div className="m4-replay-threat-visual__spatial-toolbar-end">
{spatialLayerControls}
{spatialModeControls}
</div>
</div>
) : null}
{spatialContent}
</section>
) : null;
return (
<div
className="l3-visual-audit m4-replay-threat-visual"
data-contract={CANONICAL_RECORDED_LAB_REPLAY_CONTRACT}
>
<LaboratoryEvidenceViewer
label={label}
className="m4-replay-threat-evidence-viewer"
mode={mediaMode}
modes={[...mediaModes]}
secondaryMode={{
value: spatialMode,
modes: [...spatialModes],
label: "3D и план",
onChange: onSpatialModeChange,
}}
expanded={expanded}
onModeChange={onMediaModeChange}
onExpandedChange={onExpandedChange}
modeControlsVisible={!splitView}
actions={actions}
overlay={overlay}
transport={transport}
trailingActions={trailingActions}
>
<div
className="m4-replay-threat-visual__deck"
data-split={splitView ? "true" : undefined}
data-empty={mediaMode === "none" && spatialMode === "none" ? "true" : undefined}
>
<SplitPane
primary={mediaPane}
secondary={spatialPane ?? <div />}
primarySize={splitView ? splitPrimarySize : mediaMode !== "none" ? 100 : 0}
onPrimarySizeChange={onSplitPrimarySizeChange}
orientation={splitOrientation}
minPrimarySize={splitView ? 24 : 0}
minSecondarySize={splitView ? 24 : 0}
resizable={splitView}
separatorLabel="Изменить размер VIDEO/CAMERA и 3D/PLAN"
/>
{mediaMode === "none" && spatialMode === "none" ? (
<div className="l3-visual-audit__state" role="status">
{emptyMessage}
</div>
) : null}
{deckOverlays}
</div>
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -81,9 +81,7 @@ export function LaboratoryRecordedClipPlayer({
sourceCount,
cameraRef,
cameraOverlay,
cameraControls,
alternativeScene,
spatialControls,
onSequenceChange,
onPlayingChange,
onPlaybackRateChange,
@@ -99,9 +97,7 @@ export function LaboratoryRecordedClipPlayer({
sourceCount: number;
cameraRef?: RefObject<HTMLDivElement | null>;
cameraOverlay?: ReactNode;
cameraControls?: ReactNode;
alternativeScene?: ReactNode;
spatialControls?: ReactNode;
onSequenceChange: (sequence: number) => void;
onPlayingChange: (playing: boolean) => void;
onPlaybackRateChange: (rate: number) => void;
@@ -177,14 +173,6 @@ export function LaboratoryRecordedClipPlayer({
aria-hidden={cameraPresentation === "primary"}
>
{alternativeScene}
{spatialControls ? (
<div
className="laboratory-recorded-clip-player__pane-controls"
data-pane="spatial"
>
{spatialControls}
</div>
) : null}
</div>
);
const cameraPane = (
@@ -207,14 +195,6 @@ export function LaboratoryRecordedClipPlayer({
/>
) : null}
{cameraPresentation !== "hidden" ? cameraOverlay : null}
{cameraPresentation !== "hidden" && cameraControls ? (
<div
className="laboratory-recorded-clip-player__pane-controls"
data-pane="camera"
>
{cameraControls}
</div>
) : null}
</div>
);
return (
@@ -33,6 +33,8 @@ export function RecordedEvidenceVideoScene({
segmentCount,
onPlaybackChange,
onPlayingRejected,
playbackAuthority = "media",
playbackTransport = "segmented",
}: {
source: ObservationSourceDescriptor;
playback: RecordedObservationPlayback;
@@ -47,6 +49,8 @@ export function RecordedEvidenceVideoScene({
segmentCount?: number;
onPlaybackChange?: (playback: RecordedObservationPlayback) => void;
onPlayingRejected?: () => void;
playbackAuthority?: "media" | "host";
playbackTransport?: "segmented" | "epoch-stream";
}) {
return (
<div className="recorded-evidence-video-scene">
@@ -59,6 +63,8 @@ export function RecordedEvidenceVideoScene({
segmentCount={segmentCount}
onPlaybackChange={onPlaybackChange}
onPlayingRejected={onPlayingRejected}
playbackAuthority={playbackAuthority}
playbackTransport={playbackTransport}
/>
{semanticOverlay ? (
<RecordedEvidenceSemanticMaskOverlay
@@ -130,8 +130,12 @@ 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.
if (clock === "animation") return;
setPlayback((current) => synchronizeRecordedEvidencePlayback(current, next, range));
}, [range]);
}, [clock, range]);
return useMemo(() => ({
playback,
@@ -106,6 +106,36 @@ export interface VegetationMixedRouteReview {
cases: readonly VegetationMixedRouteCase[];
}
export interface VegetationRouteTgsAnchor {
sourceSequence: number;
slot: number;
currentPointsXyzM: readonly (readonly [number, number, number])[];
costmap: {
cellSizeM: 0.45;
centersXyM: readonly (readonly [number, number])[];
stateCodes: readonly number[];
zBoundsM: readonly (readonly [number | null, number | null])[];
};
}
export interface CanonicalRecordedLabSpatialFrame {
targetTimeNs: number;
sourceTimeNs: number;
poseTimeNs: number;
trajectoryTimeNs: number;
sourcePointCount: number;
bodyFrame: {
originMapXyzM: readonly [number, number, number];
basisMapFromBody: readonly [
readonly [number, number, number],
readonly [number, number, number],
readonly [number, number, number],
];
};
sourcePointsBodyXyzM: readonly (readonly [number, number, number])[];
localSlamBodyXyzM: readonly (readonly [number, number, number])[];
}
export interface VegetationFullRouteLayer {
name: string;
resultId: string;
@@ -942,6 +972,172 @@ export function vegetationFullRouteMaskUrl(
return `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/route-masks/${layer}/${sequence}`;
}
export async function fetchVegetationRouteTgsAnchor(
resultId: string,
sourceSequence: number,
{
fetcher = fetch,
signal,
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<VegetationRouteTgsAnchor> {
if (!RESULT_ID.test(resultId) || !Number.isInteger(sourceSequence) || sourceSequence < 1) {
throw new VegetationShadowContractError("Vegetation TGS anchor identity недопустима.");
}
const response = await fetcher(
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}`
+ `/route-tgs-anchor/${sourceSequence}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new VegetationShadowContractError(`Vegetation TGS anchor недоступен: HTTP ${response.status}.`);
}
const payload = objectValue(await response.json(), "vegetation.route_tgs_anchor");
exact(
payload.schema_version,
"missioncore.lab-v1-route-tgs-anchor/v1",
"vegetation.route_tgs_anchor.schema_version",
);
exact(payload.source_sequence, sourceSequence, "vegetation.route_tgs_anchor.source_sequence");
const pointValue = (value: unknown, label: string): readonly number[] => {
const point = arrayValue(value, label).map((item, index) => numberValue(item, `${label}[${index}]`));
if (point.length !== 2 && point.length !== 3) {
throw new VegetationShadowContractError(`${label}: размер изменён.`);
}
return point;
};
const points = arrayValue(payload.current_points_xyz_m, "vegetation.route_tgs_anchor.points")
.map((value, index) => pointValue(value, `vegetation.route_tgs_anchor.points[${index}]`));
const costmap = objectValue(payload.costmap, "vegetation.route_tgs_anchor.costmap");
exact(costmap.cell_size_m, 0.45, "vegetation.route_tgs_anchor.costmap.cell_size_m");
const centers = arrayValue(costmap.centers_xy_m, "vegetation.route_tgs_anchor.costmap.centers")
.map((value, index) => pointValue(value, `vegetation.route_tgs_anchor.costmap.centers[${index}]`));
const stateCodes = arrayValue(costmap.state_codes, "vegetation.route_tgs_anchor.costmap.states")
.map((value, index) => integerValue(value, `vegetation.route_tgs_anchor.costmap.states[${index}]`));
const zBounds = arrayValue(costmap.z_bounds_m, "vegetation.route_tgs_anchor.costmap.z_bounds")
.map((value, index) => {
const row = arrayValue(value, `vegetation.route_tgs_anchor.costmap.z_bounds[${index}]`);
if (row.length !== 2 || row.some((item) => item !== null && (typeof item !== "number" || !Number.isFinite(item)))) {
throw new VegetationShadowContractError("vegetation.route_tgs_anchor.costmap.z_bounds: контракт изменён.");
}
return row as readonly [number | null, number | null];
});
if (
centers.length !== 2244
|| stateCodes.length !== 2244
|| zBounds.length !== 2244
|| stateCodes.some((value) => value > 3)
|| points.some((point) => point.length !== 3)
|| centers.some((point) => point.length !== 2)
) {
throw new VegetationShadowContractError("Vegetation TGS anchor shape изменён.");
}
return {
sourceSequence,
slot: integerValue(payload.slot, "vegetation.route_tgs_anchor.slot"),
currentPointsXyzM: points.map((point) => [point[0]!, point[1]!, point[2]!] as const),
costmap: {
cellSizeM: 0.45,
centersXyM: centers.map((point) => [point[0]!, point[1]!] as const),
stateCodes,
zBoundsM: zBounds,
},
};
}
export async function fetchCanonicalRecordedLabSpatialFrame(
sessionId: string,
generationSha256: string,
targetTimeNs: number,
{
fetcher = fetch,
signal,
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<CanonicalRecordedLabSpatialFrame> {
if (
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(sessionId)
|| !SHA256.test(generationSha256)
|| !Number.isSafeInteger(targetTimeNs)
|| targetTimeNs < 0
) {
throw new VegetationShadowContractError("Canonical LAB spatial identity недопустима.");
}
const query = new URLSearchParams({
generation: generationSha256,
time_ns: String(targetTimeNs),
});
const response = await fetcher(
`/api/v1/observation-sessions/${encodeURIComponent(sessionId)}`
+ `/canonical-lab/spatial-frame?${query.toString()}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new VegetationShadowContractError(
`Canonical LAB spatial frame недоступен: HTTP ${response.status}.`,
);
}
const payload = objectValue(await response.json(), "canonical_lab.spatial_frame");
exact(
payload.schema_version,
"missioncore.canonical-recorded-lab-spatial-frame/v1",
"canonical_lab.spatial_frame.schema_version",
);
exact(payload.target_time_ns, targetTimeNs, "canonical_lab.spatial_frame.target_time_ns");
const pointList = (value: unknown, label: string) => arrayValue(value, label).map(
(entry, index) => {
const point = arrayValue(entry, `${label}[${index}]`).map(
(channel, channelIndex) => numberValue(channel, `${label}[${index}][${channelIndex}]`),
);
if (point.length !== 3) {
throw new VegetationShadowContractError(`${label}[${index}]: размер изменён.`);
}
return [point[0]!, point[1]!, point[2]!] as const;
},
);
const sourcePoints = pointList(
payload.source_points_body_xyz_m,
"canonical_lab.spatial_frame.source_points",
);
const localSlam = pointList(
payload.local_slam_body_xyz_m,
"canonical_lab.spatial_frame.local_slam",
);
const sourcePointCount = integerValue(
payload.source_point_count,
"canonical_lab.spatial_frame.source_point_count",
);
if (sourcePointCount !== sourcePoints.length || sourcePointCount > 100_000 || localSlam.length > 10_000) {
throw new VegetationShadowContractError("Canonical LAB spatial accounting изменён.");
}
const bodyFrame = objectValue(payload.body_frame, "canonical_lab.spatial_frame.body_frame");
const origin = pointList(
[bodyFrame.origin_map_xyz_m],
"canonical_lab.spatial_frame.body_frame.origin",
)[0]!;
const basisRows = pointList(
bodyFrame.basis_map_from_body,
"canonical_lab.spatial_frame.body_frame.basis",
);
if (basisRows.length !== 3) {
throw new VegetationShadowContractError("Canonical LAB spatial basis изменён.");
}
return {
targetTimeNs,
sourceTimeNs: integerValue(payload.source_time_ns, "canonical_lab.spatial_frame.source_time_ns"),
poseTimeNs: integerValue(payload.pose_time_ns, "canonical_lab.spatial_frame.pose_time_ns"),
trajectoryTimeNs: integerValue(
payload.trajectory_time_ns,
"canonical_lab.spatial_frame.trajectory_time_ns",
),
sourcePointCount,
bodyFrame: {
originMapXyzM: origin,
basisMapFromBody: [basisRows[0]!, basisRows[1]!, basisRows[2]!],
},
sourcePointsBodyXyzM: sourcePoints,
localSlamBodyXyzM: localSlam,
};
}
export async function fetchVegetationShadowResult(
resultId: string,
{
@@ -33,29 +33,6 @@
pointer-events: auto;
}
.laboratory-recorded-clip-player__pane-controls {
position: absolute;
z-index: 6;
top: 0.6rem;
display: flex;
max-width: calc(100% - 1.2rem);
flex-wrap: wrap;
align-items: center;
gap: 0.38rem;
border-radius: var(--nodedc-radius-control-pill);
background: var(--nodedc-floating-surface);
padding: 0.28rem;
backdrop-filter: blur(var(--nodedc-blur-control));
}
.laboratory-recorded-clip-player__pane-controls[data-pane="spatial"] {
right: 0.6rem;
}
.laboratory-recorded-clip-player__pane-controls[data-pane="camera"] {
left: 0.6rem;
}
.laboratory-recorded-clip-player__split
> .nodedc-split-pane__separator::before {
background: transparent;
@@ -1,15 +1,25 @@
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
type ReactNode,
} from "react";
import {
Button,
Icon,
IconButton,
Select,
SegmentedControl,
SplitPane,
type SplitPaneOrientation,
} from "@nodedc/ui-react";
import { ObservationTimeline } from "../../components/ObservationTimeline";
import {
CanonicalRecordedLabReplay,
useCanonicalRecordedLabReplayState,
} from "../../components/laboratory/CanonicalRecordedLabReplay";
import {
LaboratoryMetricEvidenceScene,
type LaboratoryMetricCellEvidence,
@@ -53,8 +63,6 @@ import { useE47SemanticTimelineFrame } from "./useE47SemanticTimeline";
import { buildM4StaticObstacleBoxes } from "./m4StaticObstacleBoxes";
type M4ThreatMediaMode = "video" | "camera";
type M4ThreatMediaSelection = M4ThreatMediaMode | "none";
type M4ThreatSpatialSelection = LaboratoryMetricSceneMode | "none";
function toneForProposal(proposal: M4ThreatCameraProposal): RecordedEvidenceBox["tone"] {
if (proposal.threatDecision === "threat") return "danger";
@@ -188,10 +196,21 @@ export function M4ReplayThreatVisual({
showSpatialOverlaySummary?: boolean;
onActiveSequenceChange?: (sequence: number | null) => void;
}) {
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(
const {
mediaMode,
spatialMode,
splitView,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange: handleMediaModeChange,
onSpatialModeChange: handleSpatialModeChange,
onSplitPrimarySizeChange: setSplitPrimarySize,
onExpandedChange: setExpanded,
} = useCanonicalRecordedLabReplayState<M4ThreatMediaMode, LaboratoryMetricSceneMode>({
initialMediaMode: "video",
initialSpatialMode,
);
});
const [showCurrentIncrement, setShowCurrentIncrement] = useState(true);
const [showLocalSurface, setShowLocalSurface] = useState(true);
const [showRollingMap, setShowRollingMap] = useState(true);
@@ -200,13 +219,6 @@ export function M4ReplayThreatVisual({
const [showSpatialSemantic, setShowSpatialSemantic] = useState(true);
const [showMediaPoints, setShowMediaPoints] = useState(false);
const [showStaticObstacles, setShowStaticObstacles] = useState(true);
const [splitPrimarySize, setSplitPrimarySize] = useState(50);
const [splitOrientation, setSplitOrientation] = useState<SplitPaneOrientation>(() => (
typeof window !== "undefined" && window.matchMedia("(max-width: 900px)").matches
? "horizontal"
: "vertical"
));
const [expanded, setExpanded] = useState(false);
const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0);
const availableSemanticLayers = useMemo<readonly M4ReplayThreatSemanticLayer[]>(
() => semanticLayers?.length ? semanticLayers : semantic ? [semantic] : [],
@@ -266,7 +278,7 @@ export function M4ReplayThreatVisual({
endSeconds: metadata.timeline.timelineEndSeconds,
}) : null, [metadata.timeline]);
const playbackController = useRecordedEvidencePlayback(playbackRange, {
clock: mediaMode === "video" ? "external" : "animation",
clock: "animation",
});
const seekPlayback = playbackController.seek;
const setPlaybackPlaying = playbackController.setPlaying;
@@ -286,14 +298,6 @@ export function M4ReplayThreatVisual({
setVideoError(null);
}, [resultId]);
useEffect(() => {
const query = window.matchMedia("(max-width: 900px)");
const update = () => setSplitOrientation(query.matches ? "horizontal" : "vertical");
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
useEffect(() => {
const timeline = metadata.timeline;
if (!evidenceDemand.recordedVideo) {
@@ -739,15 +743,6 @@ export function M4ReplayThreatVisual({
}
: undefined;
const handleMediaModeChange = (next: M4ThreatMediaSelection) => {
if (next === "none") return;
setMediaMode((current) => current === next ? null : next);
};
const handleSpatialModeChange = (next: M4ThreatSpatialSelection) => {
if (next === "none") return;
setSpatialMode((current) => current === next ? null : next);
};
useEffect(() => {
if (
playbackController.playback.playing
@@ -758,36 +753,6 @@ export function M4ReplayThreatVisual({
image.src = frame.cameraUrl;
}, [evidenceDemand.exactCameraFrame, frame?.cameraUrl, playbackController.playback.playing]);
const splitView = mediaMode !== null && spatialMode !== null;
const mediaModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="media">
<SegmentedControl
value={mediaMode ?? "none"}
items={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
]}
label="Видео и камера"
onChange={handleMediaModeChange}
/>
</div>
);
const spatialModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="spatial">
<SegmentedControl
value={spatialMode ?? "none"}
items={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
]}
label="3D и план"
onChange={handleSpatialModeChange}
/>
</div>
);
const mediaLayerControls = activeSemantic
|| (showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery)
|| (showReferenceMediaLayers && metadata.timeline?.cameraObstacleProjectionDelivery) ? (
@@ -1110,7 +1075,12 @@ export function M4ReplayThreatVisual({
) : undefined;
const timeline = metadata.timeline;
let content;
let content: ReactNode = null;
let canonicalContent: {
mediaContent: ReactNode;
spatialContent: ReactNode;
deckOverlays: ReactNode;
} | null = null;
if (metadata.error) {
content = <SpatialState message={metadata.error} />;
} else if (!timeline) {
@@ -1121,23 +1091,8 @@ export function M4ReplayThreatVisual({
</div>
);
} else {
const mediaPane = (
<section
className="m4-replay-threat-visual__pane"
data-pane="media"
aria-label={mediaMode === "camera" ? "Камера" : "Видео"}
hidden={!mediaMode}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="media"
data-multi-semantic={availableSemanticLayers.length > 1 ? "true" : undefined}
>
{mediaLayerControls}
{mediaModeControls}
</div>
) : null}
const mediaContent = (
<>
<div
className="m4-replay-threat-visual__media-layer"
data-media="video"
@@ -1161,7 +1116,8 @@ export function M4ReplayThreatVisual({
}
segmentCount={timeline.frameCount}
onPlaybackChange={playbackController.synchronize}
onPlayingRejected={() => playbackController.setPlaying(false)}
playbackAuthority="host"
playbackTransport="epoch-stream"
/>
) : videoError ? (
<SpatialState message={videoError} />
@@ -1183,27 +1139,11 @@ export function M4ReplayThreatVisual({
ariaLabel={`${evidenceLabel} exact camera frame ${frame.sequence}: ${activeBoxes.length} proposals`}
/>
) : null}
</section>
</>
);
const spatialPane = spatialMode ? (
<section
className="m4-replay-threat-visual__pane"
data-pane="spatial"
aria-label={spatialMode === "3d" ? "Трёхмерная сцена" : "Вид сверху"}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="spatial"
>
{resetSpatialView}
<div className="m4-replay-threat-visual__spatial-toolbar-end">
{spatialLayerControls}
{spatialModeControls}
</div>
</div>
) : null}
const spatialContent = spatialMode ? (
<>
<LaboratoryMetricEvidenceScene
ref={metricSceneRef}
pointCloudBodyXyzM={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
@@ -1265,31 +1205,11 @@ export function M4ReplayThreatVisual({
: `На кадре ${frame.sequence + 1} нет body frame; ждём первый квалифицированный spatial evidence.`}
</div>
) : null}
</section>
</>
) : null;
content = (
<div
className="m4-replay-threat-visual__deck"
data-split={splitView ? "true" : undefined}
data-empty={!mediaMode && !spatialMode ? "true" : undefined}
>
<SplitPane
primary={mediaPane}
secondary={spatialPane ?? <div />}
primarySize={splitView ? splitPrimarySize : mediaMode ? 100 : 0}
onPrimarySizeChange={setSplitPrimarySize}
orientation={splitOrientation}
minPrimarySize={splitView ? 24 : 0}
minSecondarySize={splitView ? 24 : 0}
resizable={splitView}
separatorLabel="Изменить размер VIDEO/CAMERA и 3D/PLAN"
/>
{!mediaMode && !spatialMode ? (
<div className="l3-visual-audit__state" role="status">
Выберите VIDEO/CAMERA или 3D/PLAN. Общий таймлайн останется на месте.
</div>
) : null}
const deckOverlays = (
<>
{timelineFrame.loading || displayingBufferedFrame ? (
<div className="m4-replay-threat-visual__buffering" role="status">
<span className="busy-indicator" aria-hidden="true" />
@@ -1320,8 +1240,9 @@ export function M4ReplayThreatVisual({
<span>{semanticIntegrityError}</span>
</div>
) : null}
</div>
</>
);
canonicalContent = { mediaContent, spatialContent, deckOverlays };
}
const transport = timeline ? (
@@ -1346,38 +1267,59 @@ export function M4ReplayThreatVisual({
/>
) : undefined;
if (!timeline || !canonicalContent) {
return (
<div className="l3-visual-audit m4-replay-threat-visual">
<LaboratoryEvidenceViewer
label={`${evidenceLabel} recorded-realtime replay`}
className="m4-replay-threat-evidence-viewer"
mode="video"
modes={[{ value: "video", label: "VIDEO" }]}
expanded={expanded}
onModeChange={() => undefined}
onExpandedChange={setExpanded}
>
{content}
</LaboratoryEvidenceViewer>
</div>
);
}
return (
<div className="l3-visual-audit m4-replay-threat-visual">
<LaboratoryEvidenceViewer
label={activeSemantic
? activeSemantic.label ?? "Semantic diagnostic replay"
: `${evidenceLabel} recorded-realtime replay`}
className="m4-replay-threat-evidence-viewer"
mode={mediaMode ?? "none"}
modes={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
]}
secondaryMode={{
value: spatialMode ?? "none",
modes: [
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
],
label: "3D и план",
onChange: handleSpatialModeChange,
}}
expanded={expanded}
onModeChange={handleMediaModeChange}
onExpandedChange={setExpanded}
modeControlsVisible={!splitView}
actions={actions}
overlay={overlay}
transport={transport}
trailingActions={trailingActions}
>
{content}
</LaboratoryEvidenceViewer>
</div>
<CanonicalRecordedLabReplay
label={activeSemantic
? activeSemantic.label ?? "Semantic diagnostic replay"
: `${evidenceLabel} recorded-realtime replay`}
mediaMode={mediaMode ?? "none"}
mediaModes={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
]}
spatialMode={spatialMode ?? "none"}
spatialModes={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
]}
expanded={expanded}
splitPrimarySize={splitPrimarySize}
splitOrientation={splitOrientation}
mediaAriaLabel={mediaMode === "camera" ? "Камера" : "Видео"}
spatialAriaLabel={spatialMode === "3d" ? "Трёхмерная сцена" : "Вид сверху"}
mediaLayerControls={mediaLayerControls}
spatialLayerControls={spatialLayerControls}
spatialLeadingControl={resetSpatialView}
mediaMultiLayer={availableSemanticLayers.length > 1}
mediaContent={canonicalContent.mediaContent}
spatialContent={canonicalContent.spatialContent}
emptyMessage="Выберите VIDEO/CAMERA или 3D/PLAN. Общий таймлайн останется на месте."
deckOverlays={canonicalContent.deckOverlays}
actions={actions}
overlay={overlay}
transport={transport}
trailingActions={trailingActions}
onMediaModeChange={handleMediaModeChange}
onSpatialModeChange={handleSpatialModeChange}
onExpandedChange={setExpanded}
onSplitPrimarySizeChange={setSplitPrimarySize}
/>
);
}
@@ -1,9 +1,29 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Button, SegmentedControl } from "@nodedc/ui-react";
import {
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
} from "react";
import {
Button,
Icon,
IconButton,
SegmentedControl,
} from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import { LaboratoryRecordedClipPlayer } from "../../components/laboratory/LaboratoryRecordedClipPlayer";
import { RerunViewport } from "../../components/RerunViewport";
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 { useRecordedEvidencePlayback } from "../../components/laboratory/useRecordedEvidencePlayback";
import {
LaboratoryEvidence,
LaboratoryResultSummary,
@@ -11,18 +31,21 @@ import {
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import {
RecordedEvidenceSemanticMaskOverlay,
type RecordedEvidenceSemanticClass,
type RecordedEvidenceSemanticPaletteEntry,
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
import {
fetchCanonicalRecordedLabSpatialFrame,
fetchVegetationShadowResult,
fetchVegetationRouteTgsAnchor,
vegetationFullRouteMaskUrl,
vegetationVideoMaskUrl,
type CanonicalRecordedLabSpatialFrame,
type VegetationFullRouteLayer,
type VegetationFullRouteReview,
type VegetationMixedRouteCase,
type VegetationMixedRouteReview,
type VegetationRouteTgsAnchor,
type VegetationShadowResult,
} from "../../core/laboratory/vegetationShadow";
import {
@@ -33,16 +56,7 @@ import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
import {
recordedSessionRerunProfile,
type RerunPlaybackController,
} from "../../core/observation/viewerProfile";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
import { defaultSceneSettings, type SceneSettings } from "../../sceneSettings";
import {
M48EvidenceModeRail,
type M48BlindEvidenceMode,
} from "./annotation/M48EvidenceModeControls";
function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
@@ -53,6 +67,19 @@ const FULL_ROUTE_SEMANTIC_MODES = [
{ 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[];
@@ -68,17 +95,125 @@ function semanticPresentation(layer: VegetationFullRouteLayer): {
};
}
function nearestTgsCase(
function causalTgsCase(
cases: readonly VegetationMixedRouteCase[],
sequence: number,
): VegetationMixedRouteCase | null {
return cases.reduce<VegetationMixedRouteCase | null>((nearest, candidate) => (
!nearest
|| Math.abs(candidate.sourceSequence - sequence)
< Math.abs(nearest.sourceSequence - sequence)
if (!cases.length) return null;
return cases.reduce<VegetationMixedRouteCase | null>((latest, candidate) => (
candidate.sourceSequence <= sequence
&& (!latest || candidate.sourceSequence > latest.sourceSequence)
? candidate
: nearest
), null);
: latest
), null) ?? cases.reduce((first, candidate) => (
candidate.sourceSequence < first.sourceSequence ? candidate : first
));
}
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 useCanonicalRavSpatialFrame(
review: VegetationFullRouteReview,
replayLaunch: ObservationSessionReplayLaunch | null,
targetTimeNs: number,
) {
const [frame, setFrame] = useState<CanonicalRecordedLabSpatialFrame | null>(null);
const [error, setError] = useState<string | null>(null);
const desiredRef = useRef<number | null>(null);
const runningRef = useRef(false);
const mountedRef = useRef(true);
const cacheRef = useRef(new Map<number, CanonicalRecordedLabSpatialFrame>());
const pumpRef = useRef<() => void>(() => undefined);
pumpRef.current = () => {
if (runningRef.current || desiredRef.current === null || !replayLaunch) return;
runningRef.current = true;
let settledTimeNs: number | null = null;
void (async () => {
while (mountedRef.current && desiredRef.current !== null) {
const requestedTimeNs = desiredRef.current;
const cached = cacheRef.current.get(requestedTimeNs);
try {
const next = cached ?? await fetchCanonicalRecordedLabSpatialFrame(
review.sessionId,
replayLaunch.sha256,
requestedTimeNs,
);
if (!cached) {
cacheRef.current.set(requestedTimeNs, next);
while (cacheRef.current.size > 12) {
const oldest = cacheRef.current.keys().next().value as number | undefined;
if (oldest === undefined) break;
cacheRef.current.delete(oldest);
}
}
if (!mountedRef.current) break;
setFrame(next);
setError(null);
} catch (caught: unknown) {
if (!mountedRef.current) break;
setError(caught instanceof Error ? caught.message : "Spatial-слои RAV004 недоступны.");
}
settledTimeNs = requestedTimeNs;
if (desiredRef.current === requestedTimeNs) break;
}
})().finally(() => {
runningRef.current = false;
if (
mountedRef.current
&& desiredRef.current !== null
&& desiredRef.current !== settledTimeNs
) {
pumpRef.current();
}
});
};
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
desiredRef.current = null;
};
}, []);
useEffect(() => {
cacheRef.current.clear();
setFrame(null);
setError(null);
}, [replayLaunch?.sha256, review.sessionId]);
useEffect(() => {
if (!replayLaunch) return;
desiredRef.current = targetTimeNs;
const cached = cacheRef.current.get(targetTimeNs);
if (cached) {
setFrame(cached);
setError(null);
return;
}
pumpRef.current();
}, [replayLaunch, targetTimeNs]);
return { frame, error, loading: Boolean(replayLaunch) && !frame && !error };
}
function FullRouteReviewEvidence({
@@ -88,41 +223,57 @@ function FullRouteReviewEvidence({
resultId: string;
review: VegetationFullRouteReview;
}) {
const [sequence, setSequence] = useState(1);
const [playing, setPlaying] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1);
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 [expanded, setExpanded] = useState(false);
const [evidenceMode, setEvidenceMode] = useState<M48BlindEvidenceMode>("3d");
const [cameraVisible, setCameraVisible] = useState(true);
const [sceneSettings, setSceneSettings] = useState<SceneSettings>(() => ({
...defaultSceneSettings,
accumulationSeconds: 12,
showPoints: true,
showTrajectory: 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 spatialControllerRef = useRef<RerunPlaybackController | null>(null);
const frames = useMemo(
() => review.frameSourceTimesNs.map((sourceTimeNs, index) => ({
sequence: index + 1,
sourceTimeNs,
})),
[review.frameSourceTimesNs],
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 = useCanonicalRavSpatialFrame(review, replayLaunch, spatialRequestTimeNs);
const layer = review[semanticLayer];
const semantic = useMemo(() => semanticPresentation(layer), [layer]);
const maskSequence = sequence - 1;
const prefetchSrcs = useMemo(() => showCameraSemantic
? Array.from({ length: 8 }, (_, offset) => maskSequence + offset + 1)
? Array.from({ length: 8 }, (_, offset) => sequenceIndex + offset + 1)
.filter((candidate) => candidate < review.frameCount)
.map((candidate) => vegetationFullRouteMaskUrl(resultId, semanticLayer, candidate))
: [], [maskSequence, resultId, review.frameCount, semanticLayer, showCameraSemantic]);
: [], [resultId, review.frameCount, semanticLayer, sequenceIndex, showCameraSemantic]);
useEffect(() => {
const controller = new AbortController();
@@ -185,246 +336,326 @@ function FullRouteReviewEvidence({
return () => controller.abort();
}, [review.linkedRouteReviewResultId, review.sessionId, review.sourceId]);
const spatialProfile = useMemo(() => replayLaunch ? recordedSessionRerunProfile({
sourceUrl: replayLaunch.viewerSourceUrl,
artifact: {
sourceUrl: replayLaunch.sourceUrl,
viewerSourceUrl: replayLaunch.viewerSourceUrl,
byteLength: replayLaunch.byteLength,
sha256: replayLaunch.sha256,
},
autoplayWhenReady: false,
presentationGate: "ready",
expectedTimelineStartSeconds: replayLaunch.timelineStartSeconds,
expectedTimelineEndSeconds: replayLaunch.timelineEndSeconds,
initialPlaybackStartSeconds: review.timelineStartSeconds,
view: "spatial",
viewResetGeneration: 0,
followTrajectory: false,
perceptionLayers: {
enabled: false,
detections2d: false,
segmentation: false,
cuboids3d: false,
},
perceptionRetryGeneration: 0,
lockPerceptionCameraInteraction: false,
}) : null, [replayLaunch, review.timelineStartSeconds]);
const activeFrame = frames.find((candidate) => candidate.sequence === sequence)
?? frames[0]
?? null;
const activeSourceTimeNsRef = useRef(activeFrame?.sourceTimeNs ?? null);
activeSourceTimeNsRef.current = activeFrame?.sourceTimeNs ?? null;
const handleSpatialControllerChange = useCallback((controller: RerunPlaybackController | null) => {
spatialControllerRef.current = controller;
const sourceTimeNs = activeSourceTimeNsRef.current;
if (!controller || sourceTimeNs === null) return;
controller.setPlaying(false);
controller.seek(sourceTimeNs);
}, []);
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 tgsReferenceEvidence = useCanonicalRavSpatialFrame(
review,
replayLaunch,
selectedTgsTimeNs,
);
useEffect(() => {
const controller = spatialControllerRef.current;
if (!controller || !activeFrame) return;
controller.setPlaying(false);
controller.seek(activeFrame.sourceTimeNs);
}, [activeFrame]);
const selectedTgsCase = linkedReview
? nearestTgsCase(linkedReview.cases, sequence)
: null;
const selectTgsPlan = useCallback(() => {
if (!linkedReview) return;
const item = nearestTgsCase(linkedReview.cases, sequence);
if (!item) return;
setPlaying(false);
setSequence(item.sourceSequence);
setEvidenceMode("plan");
}, [linkedReview, sequence]);
const handleEvidenceModeChange = useCallback((nextMode: M48BlindEvidenceMode) => {
if (nextMode === "plan") {
selectTgsPlan();
if (!showTgs || !selectedTgsCase) {
setTgsAnchor(null);
setTgsAnchorLoading(false);
setTgsAnchorError(null);
return;
}
setEvidenceMode(nextMode);
}, [selectTgsPlan]);
const cameraPresentation = evidenceMode === "camera"
? "primary"
: cameraVisible ? "companion" : "hidden";
const spatialScene = evidenceMode === "plan" ? (
selectedTgsCase ? (
<div className="recorded-evidence-image-scene">
<img
src={selectedTgsCase.assets.tgs}
alt={`TGS costmap · sequence ${selectedTgsCase.sourceSequence}`}
draggable={false}
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) return undefined;
const currentBody = spatialEvidence.frame?.bodyFrame;
const anchorBody = tgsReferenceEvidence.frame?.bodyFrame;
const transformPoint = (point: readonly [number, number, number]) => {
if (!currentBody || !anchorBody) return point;
const map = [0, 1, 2].map((row) => (
anchorBody.originMapXyzM[row]!
+ anchorBody.basisMapFromBody[row]!.reduce(
(sum, coefficient, column) => sum + coefficient * point[column]!,
0,
)
));
const delta = map.map((value, index) => value - currentBody.originMapXyzM[index]!);
return [0, 1, 2].map((column) => (
currentBody.basisMapFromBody.reduce(
(sum, row, rowIndex) => sum + row[column]! * delta[rowIndex]!,
0,
)
)) as [number, number, number];
};
const centers: number[] = [];
const zBounds: number[] = [];
tgsAnchor.costmap.centersXyM.forEach(([x, y], index) => {
const bounds = tgsAnchor.costmap.zBoundsM[index] ?? [null, null];
const center = transformPoint([x, y, 0]);
centers.push(center[0], center[1]);
if (bounds[0] === null || bounds[1] === null) {
zBounds.push(Number.NaN, Number.NaN);
} else {
const bottom = transformPoint([x, y, bounds[0]]);
const top = transformPoint([x, y, bounds[1]]);
zBounds.push(Math.min(bottom[2], top[2]), Math.max(bottom[2], top[2]));
}
});
return {
centersBodyXyM: Float32Array.from(centers),
zBoundsM: Float32Array.from(zBounds),
stateCodes: Uint8Array.from(tgsAnchor.costmap.stateCodes),
};
}, [spatialEvidence.frame?.bodyFrame, tgsAnchor, tgsReferenceEvidence.frame?.bodyFrame]);
const semanticOverlay = showCameraSemantic ? {
src: vegetationFullRouteMaskUrl(resultId, semanticLayer, sequenceIndex),
prefetchSrcs,
classes: semantic.classes,
palette: semantic.palette,
opacity: 0.76,
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="m48-clip-player__pane-label" data-pane="spatial">
TGS COSTMAP · ЯКОРЬ {selectedTgsCase.sourceSequence} · {selectedTgsCase.tgs.occupiedCells} OCCUPIED
) : (
<div className="l3-visual-audit__state" role={videoError ? "alert" : "status"}>
{videoError ?? "Открываем автономный RAVNOVES004TREE source…"}
</div>
</div>
) : (
<div className="m4-replay-threat-visual__pane-status" role="status">
{linkedReviewError ?? "Открываем sealed TGS anchors…"}
</div>
)
) : spatialProfile ? (
<RerunViewport
profile={spatialProfile}
sceneSettings={sceneSettings}
onPlaybackControllerChange={handleSpatialControllerChange}
/>
) : (
<div className="m4-replay-threat-visual__pane-status" role="status">
Открываем sealed RRD, source points и SLAM trajectory
)}
</div>
);
return (
<LaboratoryEvidenceViewer
label="RAVNOVES004TREE full recorded review"
className="m48-atlas-visual"
mode={semanticLayer}
modes={FULL_ROUTE_SEMANTIC_MODES}
expanded={expanded}
onModeChange={setSemanticLayer}
onExpandedChange={setExpanded}
modeControlsVisible={false}
chromeLayout="stacked"
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: 0.4 }}
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 продолжаются.`
: `TGS anchor ${selectedTgsCase.sourceSequence} из 10; source/SLAM и общий clock продолжаются.`)}
</div>
) : null}
</>
) : null;
const mediaLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Слои камеры и видео"
>
<div className="m48-evidence-stage">
{videoSource ? (
<LaboratoryRecordedClipPlayer
source={videoSource}
segmentCount={review.frameCount}
frames={frames}
sequence={sequence}
playing={playing}
playbackRate={playbackRate}
cameraPresentation={cameraPresentation}
continuousPlayback
sourceCount={4}
onSequenceChange={setSequence}
onPlayingChange={setPlaying}
onPlaybackRateChange={setPlaybackRate}
alternativeScene={spatialScene}
cameraControls={(
<>
<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);
}}
/>
</>
)}
spatialControls={(
<>
<Button
size="compact"
shape="pill"
variant={sceneSettings.showPoints ? "primary" : "secondary"}
aria-pressed={sceneSettings.showPoints}
onClick={() => {
setEvidenceMode("3d");
setSceneSettings((settings) => ({
...settings,
showPoints: !settings.showPoints,
}));
}}
>
SOURCE POINTS
</Button>
<Button
size="compact"
shape="pill"
variant={sceneSettings.showTrajectory ? "primary" : "secondary"}
aria-pressed={sceneSettings.showTrajectory}
onClick={() => {
setEvidenceMode("3d");
setSceneSettings((settings) => ({
...settings,
showTrajectory: !settings.showTrajectory,
}));
}}
>
LOCAL SLAM
</Button>
<Button
size="compact"
shape="pill"
variant={evidenceMode === "plan" ? "primary" : "secondary"}
aria-pressed={evidenceMode === "plan"}
disabled={!linkedReview}
title={linkedReviewError ?? "10 sealed causal TGS anchors; continuous TGS отсутствует"}
onClick={selectTgsPlan}
>
TGS COSTMAP
</Button>
<Button
size="compact"
shape="pill"
variant="secondary"
disabled
title="Для RAV004 не опубликован point-aligned 3D semantic archive"
>
SEMANTICS
</Button>
</>
)}
cameraOverlay={(
<>
<div className="m48-clip-player__pane-label" data-pane="camera">
{showCameraSemantic
? `${semanticLayer === "city" ? "EoMT CITY" : "DDRNet NATURE"} · КАДР ${sequence}/${review.frameCount}`
: `SOURCE · КАДР ${sequence}/${review.frameCount}`}
</div>
{showCameraSemantic ? (
<div className="m48-clip-player__overlay">
<RecordedEvidenceSemanticMaskOverlay
src={vegetationFullRouteMaskUrl(resultId, semanticLayer, maskSequence)}
prefetchSrcs={prefetchSrcs}
imageWidth={review.width}
imageHeight={review.height}
classes={semantic.classes}
palette={semantic.palette}
opacity={0.76}
ariaLabel={`${layer.name} semantic prediction`}
/>
</div>
) : null}
</>
)}
/>
) : (
<div className="m4-replay-threat-visual__pane-status" role={videoError ? "alert" : "status"}>
{videoError ?? "Открываем автономный RAVNOVES004TREE source…"}
</div>
)}
{videoSource ? (
<M48EvidenceModeRail
mode={evidenceMode}
cameraVisible={cameraVisible}
spatialAvailable={Boolean(spatialProfile)}
planAvailable={Boolean(linkedReview)}
onModeChange={handleEvidenceModeChange}
onCameraVisibleChange={setCameraVisible}
/>
) : null}
<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>
</LaboratoryEvidenceViewer>
<div>
<span>Spatial evidence</span>
<strong>{showTgs && selectedTgsCase
? `TGS anchor ${selectedTgsCase.sourceSequence} · ${selectedTgsCase.tgs.occupiedCells} occupied`
: "source RRD · points + SLAM"}</strong>
<small>{showTgs
? "latest causal of 10 sealed anchors · continuous playback retained"
: "causal 1 s view · grayscale intensity · recorded source identity"}</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}
/>
);
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}
/>
);
}
@@ -847,8 +847,9 @@ test("recorded VIDEO clock cannot reverse an explicit operator pause", () => {
});
test("M4.6 viewer keeps media and spatial panes on one playback clock", async () => {
const [visual, visualCss, imageScene, videoScene, pointOverlay, metricScene] = await Promise.all([
const [visual, canonical, visualCss, imageScene, videoScene, pointOverlay, metricScene] = await Promise.all([
readFile(new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/CanonicalRecordedLabReplay.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/styles/m4-replay-threat.css", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/RecordedEvidenceImageScene.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/RecordedEvidenceVideoScene.tsx", import.meta.url), "utf8"),
@@ -858,7 +859,8 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(visual, /<RecordedEvidenceVideoScene/);
assert.match(visual, /<RecordedEvidenceImageScene/);
assert.match(visual, /<LaboratoryMetricEvidenceScene/);
assert.match(visual, /m4-replay-threat-visual__deck/);
assert.match(visual, /<CanonicalRecordedLabReplay/);
assert.match(canonical, /m4-replay-threat-visual__deck/);
assert.match(visual, /lastFrameRef/);
assert.match(visual, /lastSpatialFrameRef/);
assert.match(visual, /const spatialFrame = frame\?\.spatialAvailable/);
@@ -876,19 +878,32 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(visual, /pointCloudOverlay=/);
assert.match(visual, /mediaMode/);
assert.match(visual, /spatialMode/);
assert.match(visual, /current === next \? null : next/);
assert.match(visual, /data-split=\{splitView \? "true" : undefined\}/);
assert.match(visual, /<SplitPane/);
assert.match(visual, /primarySize=\{splitView \? splitPrimarySize : mediaMode \? 100 : 0\}/);
assert.match(visual, /resizable=\{splitView\}/);
assert.match(visual, /separatorLabel="Изменить размер VIDEO\/CAMERA и 3D\/PLAN"/);
assert.match(visual, /secondaryMode=\{\{/);
assert.match(canonical, /current === next \? null : next/);
assert.match(canonical, /data-split=\{splitView \? "true" : undefined\}/);
assert.match(canonical, /<SplitPane/);
assert.match(canonical, /primary=\{mediaPane\}/);
assert.match(canonical, /secondary=\{spatialPane/);
assert.match(canonical, /primarySize=\{splitView \? splitPrimarySize : mediaMode !== "none" \? 100 : 0\}/);
assert.match(canonical, /resizable=\{splitView\}/);
assert.match(canonical, /separatorLabel="Изменить размер VIDEO\/CAMERA и 3D\/PLAN"/);
assert.match(canonical, /secondaryMode=\{\{/);
assert.match(visual, /playback=\{playbackController\.playback\}/);
assert.match(visual, /clock: mediaMode === "video" \? "external" : "animation"/);
assert.match(visual, /clock: "animation"/);
assert.match(visual, /useCanonicalRecordedLabReplayState/);
assert.match(canonical, /current === next \? null : next/);
assert.match(visual, /timelineFrame\.activeSequence \+ 1/);
assert.match(visual, /segmentCount=\{timeline\.frameCount\}/);
assert.match(visual, /onPlaybackChange=\{playbackController\.synchronize\}/);
assert.match(visual, /onPlayingRejected=\{\(\) => playbackController\.setPlaying\(false\)\}/);
assert.match(
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(
await readFile(new URL("../src/components/RecordedFmp4Player.tsx", import.meta.url), "utf8"),
/if \(playbackAuthority === "host"\) return;/,
);
assert.match(visual, /currentSeconds: playbackController\.playback\.currentSeconds/);
assert.match(visualCss, /m4-replay-threat-visual__deck > \.nodedc-split-pane/);
assert.match(visualCss, /m4-replay-threat-visual__pane-toolbar\[data-pane-toolbar="media"\]/);
@@ -951,12 +966,18 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
});
test("M4.6 keeps the recorded VIDEO player mounted across media mode toggles", async () => {
const visual = await readFile(
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
"utf8",
);
assert.match(visual, /const mediaPane = \(/);
assert.match(visual, /hidden=\{!mediaMode\}/);
const [visual, canonical] = await Promise.all([
readFile(
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/components/laboratory/CanonicalRecordedLabReplay.tsx", import.meta.url),
"utf8",
),
]);
assert.match(canonical, /const mediaPane = \(/);
assert.match(canonical, /hidden=\{mediaMode === "none"\}/);
assert.match(visual, /data-media="video"/);
assert.match(visual, /hidden=\{mediaMode !== "video"\}/);
assert.match(visual, /\{videoSource \? \(/);
@@ -75,10 +75,16 @@ test("semantic point alignment follows the last qualified spatial increment", as
});
test("M4 keeps independent semantic controls in media and spatial panes", async () => {
const source = await readFile(
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
"utf8",
);
const [source, canonical] = await Promise.all([
readFile(
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/components/laboratory/CanonicalRecordedLabReplay.tsx", import.meta.url),
"utf8",
),
]);
assert.match(source, /showMediaSemantic/);
assert.match(source, /showSpatialSemantic/);
assert.match(source, /activeSpatialSemantic = spatialSemantic \?\? activeSemantic/);
@@ -89,9 +95,9 @@ test("M4 keeps independent semantic controls in media and spatial panes", async
assert.match(source, /\|\| !showSpatialSemantic/);
assert.match(source, /aria-label="Слои камеры и видео"/);
assert.match(source, /aria-label="Слои 3D и плана"/);
assert.match(source, /data-pane-mode="media"/);
assert.match(source, /data-pane-mode="spatial"/);
assert.match(source, /modeControlsVisible=\{!splitView\}/);
assert.match(canonical, /data-pane-mode="media"/);
assert.match(canonical, /data-pane-mode="spatial"/);
assert.match(canonical, /modeControlsVisible=\{!splitView\}/);
assert.match(source, /semanticOverlay=\{mediaMode === "video" \? semanticOverlay : undefined\}/);
});
@@ -6,7 +6,9 @@ import { createServer } from "vite";
let server;
let fetchVegetationBenchmarkResult;
let fetchCanonicalRecordedLabSpatialFrame;
let fetchVegetationShadowResult;
let fetchVegetationRouteTgsAnchor;
let vegetationFullRouteMaskUrl;
before(async () => {
@@ -17,7 +19,9 @@ before(async () => {
});
({
fetchVegetationBenchmarkResult,
fetchCanonicalRecordedLabSpatialFrame,
fetchVegetationShadowResult,
fetchVegetationRouteTgsAnchor,
vegetationFullRouteMaskUrl,
} = await server.ssrLoadModule(
"/src/core/laboratory/vegetationShadow.ts",
@@ -365,8 +369,68 @@ test("vegetation GOOSE benchmark opens through its separate archival endpoint",
assert.equal(result.validationCases.length, 12);
});
test("vegetation route TGS anchor keeps exact sealed metric shapes", async () => {
let requestedUrl = "";
const anchor = await fetchVegetationRouteTgsAnchor(resultId, 409, {
fetcher: async (url) => {
requestedUrl = String(url);
return new Response(JSON.stringify({
schema_version: "missioncore.lab-v1-route-tgs-anchor/v1",
source_sequence: 409,
slot: 1,
current_points_xyz_m: [[1, 2, 3], [4, 5, 6]],
costmap: {
cell_size_m: 0.45,
centers_xy_m: Array.from({ length: 2244 }, (_, index) => [index, -index]),
state_codes: Array.from({ length: 2244 }, (_, index) => index % 4),
z_bounds_m: Array.from({ length: 2244 }, () => [null, null]),
},
}), { status: 200, headers: { "Content-Type": "application/json" } });
},
});
assert.equal(
requestedUrl,
`/api/v1/laboratory/vegetation-shadow/${resultId}/route-tgs-anchor/409`,
);
assert.equal(anchor.sourceSequence, 409);
assert.equal(anchor.currentPointsXyzM.length, 2);
assert.equal(anchor.costmap.centersXyM.length, 2244);
assert.deepEqual(new Set(anchor.costmap.stateCodes), new Set([0, 1, 2, 3]));
});
test("canonical recorded LAB spatial frame keeps source, SLAM and body identity on one clock", async () => {
const generation = "e".repeat(64);
let requestedUrl = "";
const frame = await fetchCanonicalRecordedLabSpatialFrame("session-004", generation, 82_770_000_000, {
fetcher: async (url) => {
requestedUrl = String(url);
return new Response(JSON.stringify({
schema_version: "missioncore.canonical-recorded-lab-spatial-frame/v1",
target_time_ns: 82_770_000_000,
source_time_ns: 82_769_535_708,
pose_time_ns: 82_769_535_708,
trajectory_time_ns: 82_700_000_000,
source_point_count: 2,
source_points_body_xyz_m: [[1, 2, 3], [4, 5, 6]],
local_slam_body_xyz_m: [[0, 0, 0], [1, 0, 0]],
body_frame: {
origin_map_xyz_m: [33, 4, 1],
basis_map_from_body: [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
},
}), { status: 200, headers: { "Content-Type": "application/json" } });
},
});
assert.equal(
requestedUrl,
`/api/v1/observation-sessions/session-004/canonical-lab/spatial-frame?generation=${generation}&time_ns=82770000000`,
);
assert.equal(frame.sourcePointCount, 2);
assert.equal(frame.localSlamBodyXyzM.length, 2);
assert.deepEqual(frame.bodyFrame.originMapXyzM, [33, 4, 1]);
});
test("vegetation realtime LAB and archival benchmark use separate admitted instruments", async () => {
const [resultSource, benchmarkSource, m49Source] = await Promise.all([
const [resultSource, benchmarkSource, m49Source, canonicalSource] = await Promise.all([
readFile(
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
"utf8",
@@ -379,6 +443,10 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr
new URL("../src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/components/laboratory/CanonicalRecordedLabReplay.tsx", import.meta.url),
"utf8",
),
]);
assert.doesNotMatch(resultSource, /M48MaskComparisonVisual/);
assert.match(resultSource, /M49TgsFullShadowEvidence/);
@@ -389,12 +457,28 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 2);
assert.doesNotMatch(resultSource, /RAVNOVES004TREE mixed route review/);
assert.match(resultSource, /RAVNOVES004TREE full recorded review/);
assert.match(resultSource, /LaboratoryRecordedClipPlayer/);
assert.match(resultSource, /RerunViewport/);
assert.match(resultSource, /M48EvidenceModeRail/);
assert.match(resultSource, /CanonicalRecordedLabReplay/);
assert.match(resultSource, /RecordedEvidenceVideoScene/);
assert.match(resultSource, /LaboratoryMetricEvidenceScene/);
assert.doesNotMatch(resultSource, /RerunViewport/);
assert.match(resultSource, /fetchCanonicalRecordedLabSpatialFrame/);
assert.match(resultSource, /useCanonicalRecordedLabReplayState/);
assert.match(resultSource, /playbackTransport="epoch-stream"/);
assert.match(resultSource, /causalTgsCase/);
assert.match(resultSource, /latest causal of 10 sealed anchors/);
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(canonicalSource, /primary=\{mediaPane\}/);
assert.match(canonicalSource, /secondary=\{spatialPane/);
assert.match(canonicalSource, /missioncore\.canonical-recorded-lab-replay\/v1/);
assert.match(canonicalSource, /separatorLabel="Изменить размер VIDEO\/CAMERA и 3D\/PLAN"/);
assert.match(resultSource, /linked canonical M4\.9 TGS evidence/);
assert.match(resultSource, /linkedTgsResultId/);
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
@@ -0,0 +1,259 @@
"""Canonical recorded-LAB spatial adapter for sealed Rerun recordings.
The LAB viewer must not run an independent Rerun transport beside the camera
transport. This adapter reads the immutable recording once, indexes the
recorded source cloud, sensor pose and SLAM trajectory, and returns the latest
source-paced spatial sample in the current body frame. Camera, spatial layers
and the common timeline can therefore be driven by one host clock.
"""
from __future__ import annotations
from bisect import bisect_right
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from threading import Lock
from typing import Any, Final
import numpy as np
import rerun_bindings as rr_bindings
_POINT_ENTITY: Final = "/world/points"
_POSE_ENTITY: Final = "/world/sensor_pose"
_TRAJECTORY_ENTITY: Final = "/world/trajectory"
_POINT_COMPONENT: Final = "Points3D:positions"
_POSE_TRANSLATION_COMPONENT: Final = "Transform3D:translation"
_POSE_QUATERNION_COMPONENT: Final = "Transform3D:quaternion"
_TRAJECTORY_COMPONENT: Final = "LineStrips3D:strips"
_INDEX_LOCK: Final = Lock()
@dataclass(frozen=True)
class _TimedPoints:
times_ns: tuple[int, ...]
values: tuple[np.ndarray, ...]
@dataclass(frozen=True)
class _TimedPoses:
times_ns: tuple[int, ...]
translations: tuple[np.ndarray, ...]
quaternions_xyzw: tuple[np.ndarray, ...]
@dataclass(frozen=True)
class _CanonicalSpatialIndex:
points: _TimedPoints
poses: _TimedPoses
trajectories: _TimedPoints
def _session_times(batch: Any) -> Any | None:
if "session_time" not in batch.schema.names:
return None
return batch.column("session_time")
def _point_rows(chunks: list[Any], entity: str, component: str, *, nested: bool = False) -> _TimedPoints:
rows: list[tuple[int, np.ndarray]] = []
for chunk in chunks:
if chunk.entity_path != entity:
continue
batch = chunk.to_record_batch()
times = _session_times(batch)
if times is None or component not in batch.schema.names:
continue
column = batch.column(component)
for row_index in range(batch.num_rows):
timestamp = int(times[row_index].value)
payload = column[row_index].as_py()
if nested:
payload = payload[0] if payload else []
values = np.asarray(payload, dtype=np.float32)
if values.ndim != 2 or values.shape[1] != 3 or not np.isfinite(values).all():
continue
values.setflags(write=False)
rows.append((timestamp, values))
rows.sort(key=lambda item: item[0])
return _TimedPoints(
times_ns=tuple(timestamp for timestamp, _ in rows),
values=tuple(values for _, values in rows),
)
def _pose_rows(chunks: list[Any]) -> _TimedPoses:
rows: list[tuple[int, np.ndarray, np.ndarray]] = []
for chunk in chunks:
if chunk.entity_path != _POSE_ENTITY:
continue
batch = chunk.to_record_batch()
times = _session_times(batch)
if (
times is None
or _POSE_TRANSLATION_COMPONENT not in batch.schema.names
or _POSE_QUATERNION_COMPONENT not in batch.schema.names
):
continue
translations = batch.column(_POSE_TRANSLATION_COMPONENT)
quaternions = batch.column(_POSE_QUATERNION_COMPONENT)
for row_index in range(batch.num_rows):
translation_values = translations[row_index].as_py()
quaternion_values = quaternions[row_index].as_py()
if len(translation_values) != 1 or len(quaternion_values) != 1:
continue
translation = np.asarray(translation_values[0], dtype=np.float64)
quaternion = np.asarray(quaternion_values[0], dtype=np.float64)
if (
translation.shape != (3,)
or quaternion.shape != (4,)
or not np.isfinite(translation).all()
or not np.isfinite(quaternion).all()
):
continue
norm = float(np.linalg.norm(quaternion))
if norm <= 1e-9:
continue
translation.setflags(write=False)
normalized = quaternion / norm
normalized.setflags(write=False)
rows.append((int(times[row_index].value), translation, normalized))
rows.sort(key=lambda item: item[0])
return _TimedPoses(
times_ns=tuple(timestamp for timestamp, _, _ in rows),
translations=tuple(translation for _, translation, _ in rows),
quaternions_xyzw=tuple(quaternion for _, _, quaternion in rows),
)
@lru_cache(maxsize=4)
def _load_index_cached(
path_text: str,
byte_length: int,
modified_ns: int,
generation_sha256: str,
) -> _CanonicalSpatialIndex:
path = Path(path_text)
stat = path.stat()
if stat.st_size != byte_length or stat.st_mtime_ns != modified_ns:
raise ValueError("Recorded LAB source changed during spatial indexing")
if len(generation_sha256) != 64:
raise ValueError("Recorded LAB generation is invalid")
# Decode only the three canonical entities in one pass. Building a lazy
# store first decodes the complete RRD (including unrelated payloads), and
# then scanning that store once per layer made first-open take more than a
# minute on RAVNOVES004TREE.
chunks = (
rr_bindings.RrdReaderInternal(str(path))
.stream()
.filter(content=[_POINT_ENTITY, _POSE_ENTITY, _TRAJECTORY_ENTITY])
.to_chunks()
)
points = _point_rows(chunks, _POINT_ENTITY, _POINT_COMPONENT)
poses = _pose_rows(chunks)
trajectories = _point_rows(
chunks,
_TRAJECTORY_ENTITY,
_TRAJECTORY_COMPONENT,
nested=True,
)
if not points.times_ns or not poses.times_ns or not trajectories.times_ns:
raise ValueError("Recorded LAB source has no canonical spatial layers")
return _CanonicalSpatialIndex(points=points, poses=poses, trajectories=trajectories)
def _load_index(
path_text: str,
byte_length: int,
modified_ns: int,
generation_sha256: str,
) -> _CanonicalSpatialIndex:
# functools.lru_cache is coherent but intentionally releases its lock
# during a miss. Serialize cold RRD indexing so simultaneous camera/TGS
# admission cannot parse the same 80 MiB recording twice.
with _INDEX_LOCK:
return _load_index_cached(
path_text,
byte_length,
modified_ns,
generation_sha256,
)
def _latest_index(times_ns: tuple[int, ...], target_ns: int) -> int:
return max(0, min(len(times_ns) - 1, bisect_right(times_ns, target_ns) - 1))
def _rotation_map_from_body(quaternion_xyzw: np.ndarray) -> np.ndarray:
x, y, z, w = (float(value) for value in quaternion_xyzw)
return np.asarray(
[
[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
[2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
[2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
],
dtype=np.float64,
)
def _map_points_to_body(
points_map: np.ndarray,
translation_map: np.ndarray,
quaternion_xyzw: np.ndarray,
) -> np.ndarray:
rotation = _rotation_map_from_body(quaternion_xyzw)
# Row vectors: inverse(map_from_body) == right-multiply by map_from_body.
body = (points_map.astype(np.float64) - translation_map) @ rotation
return body.astype(np.float32)
def canonical_lab_spatial_frame(
recording_path: Path,
generation_sha256: str,
target_time_ns: int,
) -> dict[str, object]:
"""Return the latest sealed source cloud and SLAM route 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)
points_body = _map_points_to_body(index.points.values[point_index], translation, quaternion)
trajectory_body = _map_points_to_body(
index.trajectories.values[trajectory_index],
translation,
quaternion,
)
# The canonical local-SLAM layer is bounded around the vehicle. It must
# never turn into the full world-route "blob" seen in the raw Rerun view.
local_mask = (
(np.abs(trajectory_body[:, 0]) <= 30.0)
& (np.abs(trajectory_body[:, 1]) <= 30.0)
& (np.abs(trajectory_body[:, 2]) <= 6.0)
)
local_trajectory = trajectory_body[local_mask]
return {
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v1",
"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],
"body_frame": {
"origin_map_xyz_m": translation.tolist(),
"basis_map_from_body": basis_map_from_body.tolist(),
},
"source_point_count": int(points_body.shape[0]),
"source_points_body_xyz_m": points_body.tolist(),
"local_slam_body_xyz_m": local_trajectory.tolist(),
}
+71
View File
@@ -37,6 +37,7 @@ from k1link.sessions import (
SessionStore,
validate_recorded_media_timeline,
)
from k1link.sessions.canonical_lab_spatial import canonical_lab_spatial_frame
from k1link.sessions.plugin_contract import RecordedPointColorRenderer
from k1link.viewer.recorded import (
APPLICATION_ID as RECORDED_APPLICATION_ID,
@@ -824,6 +825,76 @@ def build_session_router(
**response_kwargs,
)
@router.get(
"/api/v1/observation-sessions/{session_id}/canonical-lab/spatial-frame"
)
async def get_observation_session_canonical_lab_spatial_frame(
session_id: str,
generation: Annotated[str, Query(min_length=64, max_length=64)],
time_ns: Annotated[int, Query(ge=0, le=MAX_SAFE_INTEGER)],
) -> JSONResponse:
"""Serve one body-frame sample for the canonical recorded-LAB clock.
The camera timeline owns playback. Spatial evidence is sampled from
the same immutable recording instead of starting a second Rerun clock.
"""
if SAFE_SHA256.fullmatch(generation) is None:
raise HTTPException(
status_code=412,
detail="Поколение spatial-записи не совпадает.",
)
if recording_preparation_manager is None:
raise HTTPException(
status_code=503,
detail="Сервис canonical LAB spatial playback не настроен.",
)
snapshot = recording_preparation_manager.status(session_id)
if snapshot is None or snapshot.state != "ready" or snapshot.recording is None:
raise HTTPException(
status_code=409,
detail="Запись canonical LAB ещё не подготовлена.",
)
_require_matching_recording_generation(snapshot.recording.sha256, generation)
pinned = recording_preparation_manager.pin_ready(
session_id,
preparation_id=snapshot.preparation_id,
)
if pinned is None:
raise HTTPException(
status_code=412,
detail="Подготовленная spatial-запись была заменена.",
)
pinned_snapshot, release_recording = pinned
try:
recording = pinned_snapshot.recording
if recording is None:
raise HTTPException(
status_code=500,
detail="Подготовленная spatial-запись недоступна.",
)
payload = await run_in_threadpool(
canonical_lab_spatial_frame,
recording.path,
generation,
time_ns,
)
except (OSError, ValueError) as exc:
raise HTTPException(
status_code=503,
detail="Canonical LAB spatial frame не прошёл проверку.",
) from exc
finally:
release_recording()
return JSONResponse(
payload,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{generation}:{payload["source_time_ns"]}"',
"X-Content-Type-Options": "nosniff",
},
)
@router.post("/api/v1/observation-sessions/{session_id}/blueprint.rrd")
async def get_observation_session_blueprint(
session_id: str,
+106 -1
View File
@@ -11,8 +11,9 @@ from functools import lru_cache
from pathlib import Path, PurePosixPath
from typing import Any, Final
import numpy as np
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse, Response
from fastapi.responses import FileResponse, JSONResponse, Response
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
from k1link.laboratory.evidence_report import (
@@ -262,9 +263,113 @@ def _build_vegetation_lab_router(
},
)
@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)
manifest = _read_verified(candidate, definition)
review = manifest.get("route_review")
cases = review.get("cases") if isinstance(review, dict) else None
if (
not isinstance(cases, list)
or review.get("source_id") != "RAVNOVES004TREE"
or review.get("session_id") != "20260828T130511Z_viewer_live"
or not any(
isinstance(item, dict) and item.get("source_sequence") == source_sequence
for item in cases
)
):
raise HTTPException(status_code=404, detail="Route TGS anchor not found")
artifacts = manifest.get("artifacts")
descriptor = next(
(
item
for item in artifacts if isinstance(item, dict)
and item.get("role") == "mixed-route-tgs-evidence"
and item.get("path") == "proofs/tgs-evidence.npz"
and item.get("media_type") == "application/x-npz"
),
None,
) if isinstance(artifacts, list) else None
if descriptor is None:
raise HTTPException(status_code=404, detail="Route TGS anchor not found")
try:
payload = _route_tgs_anchor_payload(
candidate / "proofs" / "tgs-evidence.npz",
source_sequence,
)
except (KeyError, OSError, ValueError):
raise HTTPException(
status_code=503,
detail="Route TGS anchor failed verification",
) from None
return JSONResponse(
payload,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{descriptor.get("sha256", "")}"',
"X-Content-Type-Options": "nosniff",
},
)
return router
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:
source_indices = archive["source_frame_indices"]
offsets = archive["current_increment_point_offsets"]
points = archive["current_increment_points_xyz_m"]
centers = archive["costmap_cell_centers_xy_m"]
states = archive["causal_rolling_1s_costmap_states"]
z_bounds = archive["causal_rolling_1s_costmap_z_bounds_m"]
if (
source_indices.shape != (10,)
or offsets.shape != (11,)
or points.ndim != 2
or points.shape[1] != 3
or centers.shape != (2244, 2)
or states.shape != (10, 2244)
or z_bounds.shape != (10, 2244, 2)
):
raise ValueError("Route TGS evidence shape changed")
matches = np.flatnonzero(source_indices == source_sequence - 1)
if matches.shape != (1,):
raise ValueError("Route TGS source sequence changed")
slot = int(matches[0])
start = int(offsets[slot])
end = int(offsets[slot + 1])
if not 0 <= start <= end <= points.shape[0]:
raise ValueError("Route TGS point offsets changed")
selected_points = np.ascontiguousarray(points[start:end], dtype=np.float32)
selected_states = np.ascontiguousarray(states[slot], dtype=np.uint8)
selected_z_bounds = np.ascontiguousarray(z_bounds[slot], dtype=np.float32)
if not np.isfinite(selected_points).all() or not np.isin(selected_states, [0, 1, 2, 3]).all():
raise ValueError("Route TGS payload changed")
result = {
"schema_version": "missioncore.lab-v1-route-tgs-anchor/v1",
"source_sequence": source_sequence,
"slot": slot,
"current_points_xyz_m": selected_points.astype(float).tolist(),
"costmap": {
"cell_size_m": 0.45,
"centers_xy_m": centers.astype(float).tolist(),
"state_codes": selected_states.astype(int).tolist(),
"z_bounds_m": [
[
float(row[0]) if np.isfinite(row[0]) else None,
float(row[1]) if np.isfinite(row[1]) else None,
]
for row in selected_z_bounds
],
},
}
after = path.stat()
if before.st_size != after.st_size or before.st_mtime_ns != after.st_mtime_ns:
raise ValueError("Route TGS evidence changed during read")
return result
def _zip_mask_response(archive_path: Path, sequence: int) -> Response:
member = f"masks/frame-{sequence + 1:06d}.png"
try:
+75
View File
@@ -17,6 +17,7 @@ from fastapi.responses import FileResponse
from fastapi.routing import APIRoute
import k1link.sessions.media as recorded_media_module
import k1link.web.session_api as session_api_module
from k1link.compute import RecordedPerceptionOverlayArtifact, RecordedPerceptionVideo
from k1link.device_plugins.xgrids_k1 import xgrids_k1_archive_source
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
@@ -458,6 +459,80 @@ def test_completed_recording_get_does_not_hold_delete_for_launch_lease(
manager.close()
def test_canonical_lab_spatial_frame_uses_ready_immutable_recording(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
repository = tmp_path / "repo"
sessions = repository / "sessions"
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
store = SessionStore(repository, data_dir=tmp_path / "data")
store.reconcile_archive(xgrids_k1_archive_source(sessions))
payload = b"sealed-spatial-recording"
def export_recording(source: Path, destination: Path) -> dict[str, object]:
destination.write_bytes(payload)
return {
"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
"rrd_sha256": hashlib.sha256(payload).hexdigest(),
"rrd_bytes": len(payload),
"timeline": "session_time",
"timeline_start_ns": 0,
"timeline_end_ns": 1_000_000_000,
}
materializer = SessionRecordingMaterializer(store.data_dir, exporter=export_recording)
command = store.prepare_replay(session.name)
recording = materializer.materialize(command)
manager = SessionRecordingPreparationManager(materializer)
resolved = manager.resolve_cached(command)
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/v1",
"target_time_ns": 500_000_000,
"source_time_ns": 499_000_000,
"pose_time_ns": 499_000_000,
"trajectory_time_ns": 490_000_000,
"body_frame": {
"origin_map_xyz_m": [0.0, 0.0, 0.0],
"basis_map_from_body": [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
},
"source_point_count": 1,
"source_points_body_xyz_m": [[1.0, 2.0, 3.0]],
"local_slam_body_xyz_m": [[0.0, 0.0, 0.0]],
}
def spatial_frame(path: Path, sha256: str, time_ns: int) -> dict[str, object]:
assert path == recording.path
assert sha256 == generation
assert time_ns == 500_000_000
return expected
monkeypatch.setattr(session_api_module, "canonical_lab_spatial_frame", spatial_frame)
router = build_session_router(
store,
recording_materializer=materializer,
recording_preparation_manager=manager,
)
spatial_route = endpoint(
router,
"/api/v1/observation-sessions/{session_id}/canonical-lab/spatial-frame",
"GET",
)
try:
response = asyncio.run(spatial_route(
session_id=session.name,
generation=generation,
time_ns=500_000_000,
))
assert json.loads(response.body) == expected
assert response.headers["etag"] == f'"{generation}:499000000"'
assert response.headers["cache-control"].endswith("immutable")
finally:
manager.close()
def test_session_router_returns_seekable_recording_and_serves_byte_ranges(
tmp_path: Path,
) -> None:
+38 -1
View File
@@ -21,11 +21,48 @@ from k1link.laboratory import LaboratoryEvidenceRegistry
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 build_vegetation_shadow_lab_router
from k1link.web.vegetation_shadow_lab_api import (
_route_tgs_anchor_payload,
build_vegetation_shadow_lab_router,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
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)
offsets = np.concatenate(([0], np.cumsum(point_counts)))
points = np.arange(int(offsets[-1]) * 3, dtype=np.float32).reshape(-1, 3)
centers = np.arange(2244 * 2, dtype=np.float32).reshape(2244, 2) * 0.45
states = np.tile(np.arange(2244, dtype=np.uint16) % 4, (10, 1)).astype(np.uint8)
z_bounds = np.zeros((10, 2244, 2), dtype=np.float32)
z_bounds[..., 0] = np.nan
z_bounds[..., 1] = 1.25
np.savez(
path,
source_frame_indices=np.array(
[20, 408, 789, 1189, 1609, 1992, 2380, 3190, 4810, 6381],
dtype=np.int64,
),
current_increment_point_offsets=offsets,
current_increment_points_xyz_m=points,
costmap_cell_centers_xy_m=centers,
causal_rolling_1s_costmap_states=states,
causal_rolling_1s_costmap_z_bounds_m=z_bounds,
)
payload = _route_tgs_anchor_payload(path, 409)
assert payload["schema_version"] == "missioncore.lab-v1-route-tgs-anchor/v1"
assert payload["source_sequence"] == 409
assert payload["slot"] == 1
assert len(payload["current_points_xyz_m"]) == 2
assert len(payload["costmap"]["centers_xy_m"]) == 2244
assert set(payload["costmap"]["state_codes"]) == {0, 1, 2, 3}
assert payload["costmap"]["z_bounds_m"][0] == [None, 1.25]
def test_coarse_policy_masks_mark_every_outside_fov_pixel_undefined(
tmp_path: Path,
monkeypatch,