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