fix(lab): restore canonical vegetation evidence layers
This commit is contained in:
@@ -215,12 +215,24 @@ export function M49TgsFullShadowEvidence({
|
|||||||
controlLabel: semanticOverride.controlLabel ?? "ПРИРОДА · DDRNet",
|
controlLabel: semanticOverride.controlLabel ?? "ПРИРОДА · DDRNet",
|
||||||
}] : []),
|
}] : []),
|
||||||
], [semantic, semanticOverride]);
|
], [semantic, semanticOverride]);
|
||||||
|
const spatialSemantic = useMemo<M4ReplayThreatSemanticLayer | undefined>(() => (
|
||||||
|
semantic ? {
|
||||||
|
id: "spatial-urban",
|
||||||
|
controlLabel: "SEMANTICS",
|
||||||
|
resultId: semantic.resultId,
|
||||||
|
spatialResultId: semantic.resultId,
|
||||||
|
taxonomy: semantic.taxonomy,
|
||||||
|
label: "EoMT Cityscapes semantic · point-aligned E47",
|
||||||
|
maskAriaLabel: "EoMT urban semantic prediction",
|
||||||
|
} : undefined
|
||||||
|
), [semantic]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<M4ReplayThreatVisual
|
<M4ReplayThreatVisual
|
||||||
resultId={result.source.linkedVisualResultId}
|
resultId={result.source.linkedVisualResultId}
|
||||||
semanticLayers={semanticLayers}
|
semanticLayers={semanticLayers}
|
||||||
|
spatialSemantic={spatialSemantic}
|
||||||
initialSemanticLayerId={semanticOverride ? "vegetation" : "urban"}
|
initialSemanticLayerId={semanticOverride ? "vegetation" : "urban"}
|
||||||
showReviewAnchorBoxes={false}
|
showReviewAnchorBoxes={false}
|
||||||
reviewLabel="4 489 source-paced TGS frames"
|
reviewLabel="4 489 source-paced TGS frames"
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ export function M4ReplayThreatVisual({
|
|||||||
resultId,
|
resultId,
|
||||||
semantic,
|
semantic,
|
||||||
semanticLayers,
|
semanticLayers,
|
||||||
|
spatialSemantic,
|
||||||
initialSemanticLayerId,
|
initialSemanticLayerId,
|
||||||
reviewAnchors = EMPTY_REVIEW_ANCHORS,
|
reviewAnchors = EMPTY_REVIEW_ANCHORS,
|
||||||
showReviewAnchorBoxes = true,
|
showReviewAnchorBoxes = true,
|
||||||
@@ -174,6 +175,7 @@ export function M4ReplayThreatVisual({
|
|||||||
resultId: string;
|
resultId: string;
|
||||||
semantic?: M4ReplayThreatSemanticLayer;
|
semantic?: M4ReplayThreatSemanticLayer;
|
||||||
semanticLayers?: readonly M4ReplayThreatSemanticLayer[];
|
semanticLayers?: readonly M4ReplayThreatSemanticLayer[];
|
||||||
|
spatialSemantic?: M4ReplayThreatSemanticLayer;
|
||||||
initialSemanticLayerId?: string;
|
initialSemanticLayerId?: string;
|
||||||
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
|
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
|
||||||
showReviewAnchorBoxes?: boolean;
|
showReviewAnchorBoxes?: boolean;
|
||||||
@@ -235,11 +237,12 @@ export function M4ReplayThreatVisual({
|
|||||||
const activeSemantic = availableSemanticLayers.find(
|
const activeSemantic = availableSemanticLayers.find(
|
||||||
(layer, index) => (layer.id ?? `${layer.resultId}:${index}`) === selectedSemanticLayerId,
|
(layer, index) => (layer.id ?? `${layer.resultId}:${index}`) === selectedSemanticLayerId,
|
||||||
) ?? availableSemanticLayers[0];
|
) ?? availableSemanticLayers[0];
|
||||||
|
const activeSpatialSemantic = spatialSemantic ?? activeSemantic;
|
||||||
const evidenceDemand = useMemo(() => laboratoryRecordedEvidenceDemand({
|
const evidenceDemand = useMemo(() => laboratoryRecordedEvidenceDemand({
|
||||||
mediaMode,
|
mediaMode,
|
||||||
spatialMode,
|
spatialMode,
|
||||||
showMediaSemantic: Boolean(activeSemantic) && showMediaSemantic,
|
showMediaSemantic: Boolean(activeSemantic) && showMediaSemantic,
|
||||||
showSpatialSemantic: Boolean(activeSemantic) && showSpatialSemantic,
|
showSpatialSemantic: Boolean(activeSpatialSemantic) && showSpatialSemantic,
|
||||||
showMediaPoints,
|
showMediaPoints,
|
||||||
classifiedSpatialMode: !classifiedSpatialLayer
|
classifiedSpatialMode: !classifiedSpatialLayer
|
||||||
? "none"
|
? "none"
|
||||||
@@ -248,6 +251,7 @@ export function M4ReplayThreatVisual({
|
|||||||
: "overlay",
|
: "overlay",
|
||||||
}), [
|
}), [
|
||||||
activeSemantic,
|
activeSemantic,
|
||||||
|
activeSpatialSemantic,
|
||||||
classifiedSpatialLayer,
|
classifiedSpatialLayer,
|
||||||
mediaMode,
|
mediaMode,
|
||||||
showMediaPoints,
|
showMediaPoints,
|
||||||
@@ -362,21 +366,21 @@ export function M4ReplayThreatVisual({
|
|||||||
sequence: frame?.sequence ?? null,
|
sequence: frame?.sequence ?? null,
|
||||||
endpointRoot: timelineEndpointRoot,
|
endpointRoot: timelineEndpointRoot,
|
||||||
});
|
});
|
||||||
const semanticSpatialResultId = activeSemantic
|
const semanticSpatialResultId = activeSpatialSemantic
|
||||||
? activeSemantic.spatialResultId === undefined
|
? activeSpatialSemantic.spatialResultId === undefined
|
||||||
? activeSemantic.resultId
|
? activeSpatialSemantic.resultId
|
||||||
: activeSemantic.spatialResultId
|
: activeSpatialSemantic.spatialResultId
|
||||||
: null;
|
: null;
|
||||||
const spatialSemanticTaxonomy = useMemo<readonly E47SemanticClass[]>(
|
const spatialSemanticTaxonomy = useMemo<readonly E47SemanticClass[]>(
|
||||||
() => semanticSpatialResultId && activeSemantic
|
() => semanticSpatialResultId && activeSpatialSemantic
|
||||||
? activeSemantic.taxonomy.map((item) => ({
|
? activeSpatialSemantic.taxonomy.map((item) => ({
|
||||||
classId: item.classId,
|
classId: item.classId,
|
||||||
label: item.label,
|
label: item.label,
|
||||||
disposition: item.disposition === "ambiguous" ? "ambiguous" : "labeled",
|
disposition: item.disposition === "ambiguous" ? "ambiguous" : "labeled",
|
||||||
colorRgb: item.colorRgb,
|
colorRgb: item.colorRgb,
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
[activeSemantic, semanticSpatialResultId],
|
[activeSpatialSemantic, semanticSpatialResultId],
|
||||||
);
|
);
|
||||||
const semanticTimeline = useE47SemanticTimelineFrame({
|
const semanticTimeline = useE47SemanticTimelineFrame({
|
||||||
resultId: semanticSpatialResultId,
|
resultId: semanticSpatialResultId,
|
||||||
@@ -473,6 +477,27 @@ export function M4ReplayThreatVisual({
|
|||||||
})) ?? [],
|
})) ?? [],
|
||||||
[activeSemantic?.taxonomy],
|
[activeSemantic?.taxonomy],
|
||||||
);
|
);
|
||||||
|
const spatialSemanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
|
||||||
|
() => activeSpatialSemantic?.taxonomy.map((item) => ({
|
||||||
|
id: item.classId,
|
||||||
|
label: `semantic: ${item.label}`,
|
||||||
|
})) ?? [],
|
||||||
|
[activeSpatialSemantic?.taxonomy],
|
||||||
|
);
|
||||||
|
const spatialSemanticPalette = useMemo<readonly RecordedEvidenceSemanticPaletteEntry[]>(
|
||||||
|
() => activeSpatialSemantic?.taxonomy.map((item) => ({
|
||||||
|
classId: item.classId,
|
||||||
|
color: item.disposition === "undefined"
|
||||||
|
? { kind: "transparent" as const }
|
||||||
|
: item.disposition === "ambiguous"
|
||||||
|
? { kind: "token" as const, token: "--nodedc-warning-rgb" as const }
|
||||||
|
: { kind: "diagnostic" as const, rgb: item.colorRgb },
|
||||||
|
opacity: item.disposition === "undefined"
|
||||||
|
? 0
|
||||||
|
: item.disposition === "ambiguous" ? 0.52 : 0.92,
|
||||||
|
})) ?? [],
|
||||||
|
[activeSpatialSemantic?.taxonomy],
|
||||||
|
);
|
||||||
const semanticFrame = semanticTimeline.activeFrame?.sequence === frame?.sequence
|
const semanticFrame = semanticTimeline.activeFrame?.sequence === frame?.sequence
|
||||||
? semanticTimeline.activeFrame
|
? semanticTimeline.activeFrame
|
||||||
: null;
|
: null;
|
||||||
@@ -489,7 +514,7 @@ export function M4ReplayThreatVisual({
|
|||||||
&& lastSpatialSemanticFrameRef.current.frame.sequence === spatialFrame?.sequence
|
&& lastSpatialSemanticFrameRef.current.frame.sequence === spatialFrame?.sequence
|
||||||
? lastSpatialSemanticFrameRef.current.frame
|
? lastSpatialSemanticFrameRef.current.frame
|
||||||
: null;
|
: null;
|
||||||
const semanticIntegrityError = activeSemantic && spatialFrame && spatialSemanticFrame && (
|
const semanticIntegrityError = activeSpatialSemantic && spatialFrame && spatialSemanticFrame && (
|
||||||
spatialSemanticFrame.sourcePointCount !== spatialFrame.pointCloudSourceCount
|
spatialSemanticFrame.sourcePointCount !== spatialFrame.pointCloudSourceCount
|
||||||
|| spatialFrame.pointCloudSampleCount !== spatialFrame.pointCloudSourceCount
|
|| spatialFrame.pointCloudSampleCount !== spatialFrame.pointCloudSourceCount
|
||||||
|| spatialFrame.pointCloudBodyXyzM.length !== spatialFrame.pointCloudSourceCount
|
|| spatialFrame.pointCloudBodyXyzM.length !== spatialFrame.pointCloudSourceCount
|
||||||
@@ -498,7 +523,7 @@ export function M4ReplayThreatVisual({
|
|||||||
: null;
|
: null;
|
||||||
const alignedSemanticPointIds = useMemo<readonly (number | null)[] | undefined>(() => {
|
const alignedSemanticPointIds = useMemo<readonly (number | null)[] | undefined>(() => {
|
||||||
if (
|
if (
|
||||||
!activeSemantic
|
!activeSpatialSemantic
|
||||||
|| !showSpatialSemantic
|
|| !showSpatialSemantic
|
||||||
|| !spatialFrame
|
|| !spatialFrame
|
||||||
|| !spatialSemanticFrame
|
|| !spatialSemanticFrame
|
||||||
@@ -508,7 +533,7 @@ export function M4ReplayThreatVisual({
|
|||||||
const status = spatialSemanticFrame.statusCodes[index];
|
const status = spatialSemanticFrame.statusCodes[index];
|
||||||
return status === 2 || status === 3 ? classId : null;
|
return status === 2 || status === 3 ? classId : null;
|
||||||
});
|
});
|
||||||
}, [activeSemantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
|
}, [activeSpatialSemantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
|
||||||
const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence
|
const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence
|
||||||
? spatialFrame
|
? spatialFrame
|
||||||
: null;
|
: null;
|
||||||
@@ -1200,10 +1225,10 @@ export function M4ReplayThreatVisual({
|
|||||||
: alignedSemanticPointIds}
|
: alignedSemanticPointIds}
|
||||||
semanticClasses={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
semanticClasses={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
? displayedClassifiedSpatialFrame.classes
|
? displayedClassifiedSpatialFrame.classes
|
||||||
: semanticClasses}
|
: spatialSemanticClasses}
|
||||||
semanticPalette={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
semanticPalette={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
? displayedClassifiedSpatialFrame.palette
|
? displayedClassifiedSpatialFrame.palette
|
||||||
: semanticPalette}
|
: spatialSemanticPalette}
|
||||||
classifiedCells={classifiedCellsBody}
|
classifiedCells={classifiedCellsBody}
|
||||||
classifiedPackedCells={classifiedPackedCellsBody}
|
classifiedPackedCells={classifiedPackedCellsBody}
|
||||||
classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM}
|
classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
|
|
||||||
|
|
||||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
|
||||||
import { LaboratoryRecordedClipPlayer } from "../../components/laboratory/LaboratoryRecordedClipPlayer";
|
|
||||||
import { RerunViewport } from "../../components/RerunViewport";
|
|
||||||
import {
|
import {
|
||||||
LaboratoryEvidence,
|
LaboratoryEvidence,
|
||||||
LaboratoryResultSummary,
|
LaboratoryResultSummary,
|
||||||
@@ -11,457 +7,22 @@ import {
|
|||||||
LaboratoryWorkTemplate,
|
LaboratoryWorkTemplate,
|
||||||
} from "../../components/laboratory/LaboratoryPresentation";
|
} from "../../components/laboratory/LaboratoryPresentation";
|
||||||
import {
|
import {
|
||||||
RecordedEvidenceSemanticMaskOverlay,
|
|
||||||
type RecordedEvidenceSemanticClass,
|
|
||||||
type RecordedEvidenceSemanticPaletteEntry,
|
|
||||||
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
|
||||||
import {
|
|
||||||
vegetationFullRouteMaskUrl,
|
|
||||||
vegetationVideoMaskUrl,
|
vegetationVideoMaskUrl,
|
||||||
type VegetationFullRouteLayer,
|
|
||||||
type VegetationFullRouteReview,
|
|
||||||
type VegetationMixedRouteReview,
|
|
||||||
type VegetationShadowResult,
|
type VegetationShadowResult,
|
||||||
} from "../../core/laboratory/vegetationShadow";
|
} from "../../core/laboratory/vegetationShadow";
|
||||||
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
|
|
||||||
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
|
|
||||||
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
|
|
||||||
import {
|
|
||||||
recordedSessionRerunProfile,
|
|
||||||
type RerunPlaybackController,
|
|
||||||
} from "../../core/observation/viewerProfile";
|
|
||||||
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
|
|
||||||
import {
|
import {
|
||||||
fetchM49TgsFullShadowResult,
|
fetchM49TgsFullShadowResult,
|
||||||
type M49TgsFullShadowResult,
|
type M49TgsFullShadowResult,
|
||||||
} from "../../core/laboratory/m49TgsFullShadow";
|
} from "../../core/laboratory/m49TgsFullShadow";
|
||||||
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
|
|
||||||
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
|
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
|
||||||
import {
|
|
||||||
M48EvidenceModeRail,
|
|
||||||
type M48BlindEvidenceMode,
|
|
||||||
} from "./annotation/M48EvidenceModeControls";
|
|
||||||
|
|
||||||
function decimal(value: number, digits = 1): string {
|
function decimal(value: number, digits = 1): string {
|
||||||
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIXED_ROUTE_MODES = [
|
|
||||||
{ value: "source", label: "SOURCE" },
|
|
||||||
{ value: "city", label: "ГОРОД · EoMT" },
|
|
||||||
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
|
|
||||||
{ value: "tgs", label: "TGS" },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const FULL_ROUTE_MODES = [
|
|
||||||
{ value: "source", label: "SOURCE" },
|
|
||||||
{ value: "city", label: "ГОРОД · EoMT" },
|
|
||||||
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
|
|
||||||
] 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 FullRouteReviewEvidence({
|
|
||||||
resultId,
|
|
||||||
review,
|
|
||||||
}: {
|
|
||||||
resultId: string;
|
|
||||||
review: VegetationFullRouteReview;
|
|
||||||
}) {
|
|
||||||
const [sequence, setSequence] = useState(1);
|
|
||||||
const [playing, setPlaying] = useState(false);
|
|
||||||
const [playbackRate, setPlaybackRate] = useState(1);
|
|
||||||
const [mode, setMode] = useState<typeof FULL_ROUTE_MODES[number]["value"]>("vegetation");
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
|
||||||
const [evidenceMode, setEvidenceMode] = useState<M48BlindEvidenceMode>("3d");
|
|
||||||
const [cameraVisible, setCameraVisible] = useState(true);
|
|
||||||
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
|
|
||||||
const [replayLaunch, setReplayLaunch] = useState<ObservationSessionReplayLaunch | null>(null);
|
|
||||||
const [videoError, setVideoError] = 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 layer = mode === "source" ? null : review[mode];
|
|
||||||
const semantic = useMemo(() => layer ? semanticPresentation(layer) : null, [layer]);
|
|
||||||
const maskSequence = sequence - 1;
|
|
||||||
const prefetchSrcs = useMemo(() => layer
|
|
||||||
? Array.from({ length: 8 }, (_, offset) => maskSequence + offset + 1)
|
|
||||||
.filter((candidate) => candidate < review.frameCount)
|
|
||||||
.map((candidate) => vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", candidate))
|
|
||||||
: [], [layer, maskSequence, mode, resultId, review.frameCount]);
|
|
||||||
|
|
||||||
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,
|
|
||||||
]);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const controller = spatialControllerRef.current;
|
|
||||||
if (!controller || !activeFrame) return;
|
|
||||||
controller.setPlaying(false);
|
|
||||||
controller.seek(activeFrame.sourceTimeNs);
|
|
||||||
}, [activeFrame]);
|
|
||||||
|
|
||||||
const cameraPresentation = evidenceMode === "camera"
|
|
||||||
? "primary"
|
|
||||||
: cameraVisible ? "companion" : "hidden";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LaboratoryEvidenceViewer
|
|
||||||
label="RAVNOVES004TREE full recorded review"
|
|
||||||
className="m48-atlas-visual"
|
|
||||||
mode={mode}
|
|
||||||
modes={FULL_ROUTE_MODES}
|
|
||||||
expanded={expanded}
|
|
||||||
onModeChange={setMode}
|
|
||||||
onExpandedChange={setExpanded}
|
|
||||||
chromeLayout="stacked"
|
|
||||||
>
|
|
||||||
<div className="m48-evidence-stage">
|
|
||||||
{videoSource ? (
|
|
||||||
<LaboratoryRecordedClipPlayer
|
|
||||||
source={videoSource}
|
|
||||||
segmentCount={review.frameCount}
|
|
||||||
frames={frames}
|
|
||||||
sequence={sequence}
|
|
||||||
playing={playing}
|
|
||||||
playbackRate={playbackRate}
|
|
||||||
cameraPresentation={cameraPresentation}
|
|
||||||
continuousPlayback
|
|
||||||
sourceCount={2}
|
|
||||||
onSequenceChange={setSequence}
|
|
||||||
onPlayingChange={setPlaying}
|
|
||||||
onPlaybackRateChange={setPlaybackRate}
|
|
||||||
alternativeScene={spatialProfile ? (
|
|
||||||
<RerunViewport
|
|
||||||
profile={spatialProfile}
|
|
||||||
onPlaybackControllerChange={handleSpatialControllerChange}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="m4-replay-threat-visual__pane-status" role="status">
|
|
||||||
Открываем sealed RRD и point-cloud evidence…
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
cameraOverlay={(
|
|
||||||
<>
|
|
||||||
<div className="m48-clip-player__pane-label" data-pane="camera">
|
|
||||||
{mode === "source" ? "SOURCE" : `${mode === "city" ? "EoMT CITY" : "DDRNet NATURE"} · КАДР ${sequence}/${review.frameCount}`}
|
|
||||||
</div>
|
|
||||||
{layer && semantic ? (
|
|
||||||
<div className="m48-clip-player__overlay">
|
|
||||||
<RecordedEvidenceSemanticMaskOverlay
|
|
||||||
src={vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", 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 ?? "Открываем автономный recorded source…"}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{videoSource ? (
|
|
||||||
<M48EvidenceModeRail
|
|
||||||
mode={evidenceMode}
|
|
||||||
cameraVisible={cameraVisible}
|
|
||||||
spatialAvailable={Boolean(spatialProfile)}
|
|
||||||
planAvailable={false}
|
|
||||||
onModeChange={setEvidenceMode}
|
|
||||||
onCameraVisibleChange={setCameraVisible}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</LaboratoryEvidenceViewer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FullRouteReviewResult({
|
|
||||||
rigLabel,
|
|
||||||
resultId,
|
|
||||||
review,
|
|
||||||
}: {
|
|
||||||
rigLabel: string;
|
|
||||||
resultId: string;
|
|
||||||
review: VegetationFullRouteReview;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<LaboratoryWorkTemplate
|
|
||||||
summary={(
|
|
||||||
<LaboratorySummary
|
|
||||||
title="LAB V1 · RAVNOVES004TREE · полный маршрут"
|
|
||||||
description="Существующий M4.7-шаблон воспроизводит всю запись и переключает два независимых sealed semantic-слоя: городской EoMT и природный DDRNet. Worker для открытия результата не нужен."
|
|
||||||
status="FULL RECORDED REVIEW · truth отсутствует · commands OFF"
|
|
||||||
statusTone="warning"
|
|
||||||
facts={[
|
|
||||||
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount}/${review.frameCount} frames` },
|
|
||||||
{ label: "Город", value: `${review.city.name} · ${decimal(review.city.inferenceFps, 2)} fps` },
|
|
||||||
{ label: "Природа", value: `${review.vegetation.name} · ${decimal(review.vegetation.inferenceFps, 2)} fps` },
|
|
||||||
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
|
|
||||||
]}
|
|
||||||
brief={{
|
|
||||||
question: "Как оба semantic-кандидата ведут себя на полном переходе от сельской среды к городской?",
|
|
||||||
approach: "Все 6830 позиции одной recorded timeline последовательно прогнаны на Worker 006 и сохранены двумя независимыми архивами масок. В M4.7 переключается только видимый слой.",
|
|
||||||
principalResult: "Полная временная шкала доступна локально в SOURCE / EoMT CITY / DDRNet NATURE без обращения к Worker.",
|
|
||||||
limitation: "Ручной truth отсутствует. Один повреждённый H.264-пакет на позиции 6092 представлен предыдущим декодированным кадром и явно зафиксирован в proof. Полный TGS и кюветы этим прогоном не проверялись.",
|
|
||||||
}}
|
|
||||||
method={{
|
|
||||||
completeness: "complete",
|
|
||||||
executionClass: "ai-inference",
|
|
||||||
pipelineId: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
|
||||||
components: [
|
|
||||||
{ 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 },
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
evidence={(
|
|
||||||
<LaboratoryEvidence
|
|
||||||
eyebrow="M4.7 TEMPLATE · RAVNOVES004TREE FULL VIDEO"
|
|
||||||
title="SOURCE / EoMT CITY / DDRNet NATURE · 6830/6830 · TRUTH отсутствует"
|
|
||||||
kind="recorded-replay"
|
|
||||||
resizable
|
|
||||||
>
|
|
||||||
<FullRouteReviewEvidence resultId={resultId} review={review} />
|
|
||||||
</LaboratoryEvidence>
|
|
||||||
)}
|
|
||||||
result={(
|
|
||||||
<LaboratoryResultSummary
|
|
||||||
title="Полный двухслойный visual review собран; управление не авторизовано"
|
|
||||||
status="Recorded evidence ready · navigation/actuation OFF"
|
|
||||||
statusTone="warning"
|
|
||||||
metrics={[
|
|
||||||
{ label: "Route masks", value: "6830/6830 × 2", hint: "sealed local archives · Worker не требуется" },
|
|
||||||
{ label: "EoMT p95", value: `${decimal(review.city.latencyP95Ms, 2)} ms`, hint: "последовательный изолированный прогон" },
|
|
||||||
{ label: "DDRNet p95", value: `${decimal(review.vegetation.latencyP95Ms, 2)} ms`, hint: "последовательный изолированный прогон" },
|
|
||||||
{ label: "Decode repair", value: "1/6830", hint: "sequence 6092 · previous frame · sealed proof" },
|
|
||||||
]}
|
|
||||||
conclusion={{
|
|
||||||
proved: "Городской EoMT и природный DDRNet воспроизводимо обработали полную запись и доступны в одном существующем M4.7 viewer.",
|
|
||||||
notProved: "Не доказаны truth accuracy, одновременный realtime-load, полный TGS, отрицательные препятствия и безопасное управление ровером.",
|
|
||||||
decision: "Использовать результат только как визуальную диагностику. Navigation/actuation оставить OFF; следующий gate — оценка временной стабильности и независимый person/vehicle STOP.",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MixedRouteReviewEvidence({ review }: { review: VegetationMixedRouteReview }) {
|
|
||||||
const [index, setIndex] = useState(0);
|
|
||||||
const [mode, setMode] = useState<typeof MIXED_ROUTE_MODES[number]["value"]>("vegetation");
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
|
||||||
const item = review.cases[index]!;
|
|
||||||
return (
|
|
||||||
<LaboratoryEvidenceViewer
|
|
||||||
label="RAVNOVES004TREE mixed route review"
|
|
||||||
className="m48-atlas-visual"
|
|
||||||
mode={mode}
|
|
||||||
modes={MIXED_ROUTE_MODES}
|
|
||||||
expanded={expanded}
|
|
||||||
onModeChange={setMode}
|
|
||||||
onExpandedChange={setExpanded}
|
|
||||||
chromeLayout="stacked"
|
|
||||||
actions={(
|
|
||||||
<>
|
|
||||||
<IconButton label="Предыдущая сцена" onClick={() => setIndex((index - 1 + review.cases.length) % review.cases.length)}>
|
|
||||||
<Icon name="chevron-left" size={16} />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton label="Следующая сцена" onClick={() => setIndex((index + 1) % review.cases.length)}>
|
|
||||||
<Icon name="chevron-right" size={16} />
|
|
||||||
</IconButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
overlay={(
|
|
||||||
<div className="m48-atlas-visual__case">
|
|
||||||
<StatusBadge tone={item.phase === "urban" ? "accent" : item.phase === "transition" ? "warning" : "neutral"}>
|
|
||||||
{item.phase.toUpperCase()} · {index + 1}/{review.cases.length}
|
|
||||||
</StatusBadge>
|
|
||||||
<strong>sequence {item.sourceSequence} · +{decimal(item.sessionSeconds, 2)} s</strong>
|
|
||||||
<small>
|
|
||||||
TGS: {item.tgs.groundCells} ground · {item.tgs.occupiedCells} occupied · {item.tgs.unobservedCells} unobserved
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="recorded-evidence-image-scene">
|
|
||||||
<img src={item.assets[mode]} alt="" draggable={false} />
|
|
||||||
</div>
|
|
||||||
</LaboratoryEvidenceViewer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MixedRouteReviewResult({
|
|
||||||
rigLabel,
|
|
||||||
review,
|
|
||||||
}: {
|
|
||||||
rigLabel: string;
|
|
||||||
review: VegetationMixedRouteReview;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<LaboratoryWorkTemplate
|
|
||||||
summary={(
|
|
||||||
<LaboratorySummary
|
|
||||||
title="LAB V1 · RAVNOVES004TREE · село → город"
|
|
||||||
description="Существующий LAB-шаблон показывает 10 синхронных camera/LiDAR сцен одной записи. EoMT и DDRNet остаются независимыми слоями; TGS показывает отдельную геометрию и не может быть очищен семантической маской."
|
|
||||||
status="BOUNDED RECORDED REVIEW · truth отсутствует · commands OFF"
|
|
||||||
statusTone="warning"
|
|
||||||
facts={[
|
|
||||||
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount} camera/LiDAR islands` },
|
|
||||||
{ label: "Переход", value: "5 rural · 1 transition · 4 urban" },
|
|
||||||
{ label: "Слои", value: "SOURCE · EoMT CITY · DDRNet VEGETATION · causal TGS" },
|
|
||||||
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
|
|
||||||
]}
|
|
||||||
brief={{
|
|
||||||
question: "Сохраняются ли городская семантика, растительность и геометрия при переходе из сельской среды в город?",
|
|
||||||
approach: "Выбраны десять соседних с исходными сцен camera-кадров, каждый синхронизирован с LiDAR в пределах 100 мс. Все три вычислительных слоя прогнаны на Worker 006 и запечатаны локально.",
|
|
||||||
principalResult: "Все 10 сцен обработаны EoMT, DDRNet и causal TGS. Слои можно переключать без наложения цветов и без зависимости LAB от воркера.",
|
|
||||||
limitation: "Это bounded islands без ручной truth. DDRNet шумит по подтипам растительности; TGS не доказывает обнаружение кювета или отрицательного препятствия.",
|
|
||||||
}}
|
|
||||||
method={{
|
|
||||||
completeness: "complete",
|
|
||||||
executionClass: "ai-inference",
|
|
||||||
pipelineId: "ravnoves004tree-eomt-ddrnet-causal-tgs-review/v1",
|
|
||||||
components: [
|
|
||||||
{ kind: "model", name: review.models.city.name, version: "sealed Worker run", role: "urban semantic review", identitySha256: null },
|
|
||||||
{ kind: "model", name: review.models.vegetation.name, version: "GOOSE DDRNet-39", role: "vegetation semantic review", identitySha256: null },
|
|
||||||
{ kind: "algorithm", name: review.models.tgs.name, version: "TRAVEL compatibility runner", role: "independent local geometry", identitySha256: null },
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
evidence={(
|
|
||||||
<LaboratoryEvidence
|
|
||||||
eyebrow="M4.7 TEMPLATE · RAVNOVES004TREE"
|
|
||||||
title="SOURCE / ГОРОД / ПРИРОДА / TGS · 10/10 · TRUTH отсутствует"
|
|
||||||
kind="diagnostic-model"
|
|
||||||
resizable
|
|
||||||
>
|
|
||||||
<MixedRouteReviewEvidence review={review} />
|
|
||||||
</LaboratoryEvidence>
|
|
||||||
)}
|
|
||||||
result={(
|
|
||||||
<LaboratoryResultSummary
|
|
||||||
title="Переход село → город воспроизведён; safety gate не закрыт"
|
|
||||||
status="Review ready · navigation/actuation OFF"
|
|
||||||
statusTone="warning"
|
|
||||||
metrics={[
|
|
||||||
{ label: "Aligned scenes", value: "10/10", hint: "camera + LiDAR + pose · автономный archive" },
|
|
||||||
{ label: "EoMT end-to-end p95", value: `${decimal(review.models.city.endToEndP95Ms, 2)} ms`, hint: `${decimal(review.models.city.inferenceFps, 2)} fps в изолированном прогоне` },
|
|
||||||
{ label: "DDRNet inference p95", value: `${decimal(review.models.vegetation.latencyP95Ms, 2)} ms`, hint: "candidate review · не совместный realtime stack" },
|
|
||||||
{ label: "TGS p95", value: `${decimal(review.models.tgs.latencyP95Ms, 2)} ms`, hint: `${review.models.tgs.cellSizeM} m cells · ${review.models.tgs.radiusM} m radius` },
|
|
||||||
]}
|
|
||||||
conclusion={{
|
|
||||||
proved: "Оба semantic слоя и causal TGS воспроизводимо работают на сельской, переходной и городской части новой записи.",
|
|
||||||
notProved: "Не доказаны accuracy без truth, временная стабильность по всему видео, детект кюветов и безопасное совместное realtime-управление ровером.",
|
|
||||||
decision: "Оставить navigation/actuation OFF. Следующий короткий gate — непрерывный realtime-load двух моделей плюс независимый person/vehicle STOP; кюветы проверять отдельной записью.",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) {
|
function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) {
|
||||||
const route = result.routeVideo!;
|
const route = result.routeVideo!;
|
||||||
|
const linkedTgsResultId = route.linkedTgsResultId;
|
||||||
const [tgs, setTgs] = useState<M49TgsFullShadowResult | null>(null);
|
const [tgs, setTgs] = useState<M49TgsFullShadowResult | null>(null);
|
||||||
const [tgsError, setTgsError] = useState<string | null>(null);
|
const [tgsError, setTgsError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -469,8 +30,8 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
setTgs(null);
|
setTgs(null);
|
||||||
setTgsError(null);
|
setTgsError(null);
|
||||||
if (!route.linkedTgsResultId) return () => controller.abort();
|
if (!linkedTgsResultId) return () => controller.abort();
|
||||||
void fetchM49TgsFullShadowResult(route.linkedTgsResultId, {
|
void fetchM49TgsFullShadowResult(linkedTgsResultId, {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
}).then((next) => {
|
}).then((next) => {
|
||||||
if (controller.signal.aborted) return;
|
if (controller.signal.aborted) return;
|
||||||
@@ -484,7 +45,11 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [route.baseM4ResultId, route.linkedTgsResultId]);
|
}, [linkedTgsResultId, route.baseM4ResultId]);
|
||||||
|
|
||||||
|
if (!linkedTgsResultId) {
|
||||||
|
throw new Error("Vegetation LAB result has no linked canonical M4.9 TGS evidence.");
|
||||||
|
}
|
||||||
|
|
||||||
const semantic = {
|
const semantic = {
|
||||||
id: "vegetation",
|
id: "vegetation",
|
||||||
@@ -497,16 +62,14 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
|
|||||||
maskAriaLabel: "DDRNet vegetation material prediction",
|
maskAriaLabel: "DDRNet vegetation material prediction",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
if (route.linkedTgsResultId && tgs) {
|
if (tgsError) {
|
||||||
return (
|
return (
|
||||||
<M49TgsFullShadowEvidence
|
<div className="m4-replay-threat-visual__pane-status" role="alert">
|
||||||
result={tgs}
|
Канонический M4.9 TGS слой недоступен: {tgsError}
|
||||||
semanticOverride={semantic}
|
</div>
|
||||||
evidenceLabel="LAB V1 · EoMT + DDRNet + YOLOX + TGS"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (route.linkedTgsResultId && !tgsError) {
|
if (!tgs) {
|
||||||
return (
|
return (
|
||||||
<div className="m4-replay-threat-visual__pane-status" role="status">
|
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||||
Открываем sealed EoMT, TGS и coarse vegetation timeline…
|
Открываем sealed EoMT, TGS и coarse vegetation timeline…
|
||||||
@@ -514,20 +77,11 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<M49TgsFullShadowEvidence
|
||||||
<M4ReplayThreatVisual
|
result={tgs}
|
||||||
resultId={route.baseM4ResultId}
|
semanticOverride={semantic}
|
||||||
evidenceLabel="LAB V1 · DDRNet"
|
evidenceLabel="LAB V1 · EoMT + DDRNet + YOLOX + TGS"
|
||||||
showReferenceMediaLayers
|
/>
|
||||||
showSpatialOverlaySummary={false}
|
|
||||||
semantic={semantic}
|
|
||||||
/>
|
|
||||||
{tgsError ? (
|
|
||||||
<div className="m4-replay-threat-visual__pane-status" role="alert">
|
|
||||||
TGS слой недоступен: {tgsError}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,18 +92,11 @@ export function VegetationShadowResultView({
|
|||||||
rigLabel: string;
|
rigLabel: string;
|
||||||
result: VegetationShadowResult;
|
result: VegetationShadowResult;
|
||||||
}) {
|
}) {
|
||||||
if (result.routeFullReview) {
|
if (!result.routeVideo?.linkedTgsResultId) {
|
||||||
return (
|
throw new Error(
|
||||||
<FullRouteReviewResult
|
"Vegetation LAB result has no canonical M4 source timeline and linked M4.9 TGS evidence.",
|
||||||
rigLabel={rigLabel}
|
|
||||||
resultId={result.resultId}
|
|
||||||
review={result.routeFullReview}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (result.routeReview) {
|
|
||||||
return <MixedRouteReviewResult rigLabel={rigLabel} review={result.routeReview} />;
|
|
||||||
}
|
|
||||||
const route = result.routeVideo;
|
const route = result.routeVideo;
|
||||||
const selected = result.candidates.find(
|
const selected = result.candidates.find(
|
||||||
(candidate) => candidate.candidate === result.selectedCandidate,
|
(candidate) => candidate.candidate === result.selectedCandidate,
|
||||||
@@ -561,9 +108,7 @@ export function VegetationShadowResultView({
|
|||||||
<LaboratorySummary
|
<LaboratorySummary
|
||||||
title="LAB V1 · карта ровера · город + растительность"
|
title="LAB V1 · карта ровера · город + растительность"
|
||||||
description="Один recorded-контур RAVNOVES00 синхронно показывает городской EoMT, природный DDRNet, frozen YOLOX detections и causal TGS. Семантические маски переключаются, чтобы их цвета не скрывали друг друга; геометрическое veto остаётся независимым."
|
description="Один recorded-контур RAVNOVES00 синхронно показывает городской EoMT, природный DDRNet, frozen YOLOX detections и causal TGS. Семантические маски переключаются, чтобы их цвета не скрывали друг друга; геометрическое veto остаётся независимым."
|
||||||
status={route
|
status="MULTILAYER RECORDED REVIEW · commands OFF · route truth отсутствует"
|
||||||
? "MULTILAYER RECORDED REVIEW · commands OFF · route truth отсутствует"
|
|
||||||
: "ROUTE EVIDENCE MISSING · commands OFF"}
|
|
||||||
statusTone="warning"
|
statusTone="warning"
|
||||||
facts={[
|
facts={[
|
||||||
{ label: "Источник", value: "RAVNOVES00 · sensor.camera.right · 4489 recorded frames" },
|
{ label: "Источник", value: "RAVNOVES00 · sensor.camera.right · 4489 recorded frames" },
|
||||||
@@ -574,14 +119,12 @@ export function VegetationShadowResultView({
|
|||||||
]}
|
]}
|
||||||
brief={{
|
brief={{
|
||||||
question: "Можно ли одновременно видеть городской и природный semantic stack, не теряя независимую геометрическую защиту?",
|
question: "Можно ли одновременно видеть городской и природный semantic stack, не теряя независимую геометрическую защиту?",
|
||||||
approach: "EoMT и DDRNet сохранены как два независимых sealed слоя на одной M4 timeline. В штатном M4.7 viewer пользователь переключает только отображаемую маску; YOLOX и TGS остаются активными слоями evidence.",
|
approach: "EoMT и DDRNet сохранены как два независимых sealed слоя на одной M4 timeline. В штатном M4.9 viewer пользователь переключает только отображаемую маску; YOLOX и TGS остаются активными слоями evidence.",
|
||||||
principalResult: route
|
principalResult: "Оба semantic archive доступны в одном viewer. Это не пиксельный fusion и не единая новая модель: городской и природный ответы остаются раздельными.",
|
||||||
? "Оба semantic archive доступны в одном viewer. Это не пиксельный fusion и не единая новая модель: городской и природный ответы остаются раздельными."
|
|
||||||
: "Route archive для этой immutable identity отсутствует.",
|
|
||||||
limitation: "RAVNOVES00 не имеет ручной truth. DDRNet заметно прыгает между HIGH GRASS, WOODY и UNKNOWN; поэтому subtype нельзя подавать напрямую в planner. Отсутствие класса никогда не означает свободный путь.",
|
limitation: "RAVNOVES00 не имеет ручной truth. DDRNet заметно прыгает между HIGH GRASS, WOODY и UNKNOWN; поэтому subtype нельзя подавать напрямую в planner. Отсутствие класса никогда не означает свободный путь.",
|
||||||
}}
|
}}
|
||||||
method={{
|
method={{
|
||||||
completeness: route ? "complete" : "legacy-partial",
|
completeness: "complete",
|
||||||
executionClass: "ai-inference",
|
executionClass: "ai-inference",
|
||||||
pipelineId: "ravnoves-eomt-ddrnet-yolox-causal-tgs-recorded-review/v1",
|
pipelineId: "ravnoves-eomt-ddrnet-yolox-causal-tgs-recorded-review/v1",
|
||||||
components: [
|
components: [
|
||||||
@@ -610,25 +153,15 @@ export function VegetationShadowResultView({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
evidence={route ? (
|
evidence={(
|
||||||
<LaboratoryEvidence
|
<LaboratoryEvidence
|
||||||
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
|
eyebrow="M4.9 · RAVNOVES00 FULL VIDEO"
|
||||||
title="EoMT CITY / DDRNet VEGETATION + YOLOX + CAUSAL TGS · 4489/4489 · TRUTH отсутствует"
|
title="EoMT CITY / DDRNet VEGETATION + YOLOX + CAUSAL TGS · 4489/4489 · TRUTH отсутствует"
|
||||||
kind="diagnostic-model"
|
kind="diagnostic-model"
|
||||||
resizable
|
resizable
|
||||||
>
|
>
|
||||||
<VegetationRouteEvidence result={result} />
|
<VegetationRouteEvidence result={result} />
|
||||||
</LaboratoryEvidence>
|
</LaboratoryEvidence>
|
||||||
) : (
|
|
||||||
<LaboratoryEvidence
|
|
||||||
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
|
|
||||||
title="ROUTE ARCHIVE отсутствует"
|
|
||||||
kind="diagnostic-model"
|
|
||||||
>
|
|
||||||
<div className="m4-replay-threat-visual__pane-status" role="alert">
|
|
||||||
Для этой immutable identity нет полного route video evidence.
|
|
||||||
</div>
|
|
||||||
</LaboratoryEvidence>
|
|
||||||
)}
|
)}
|
||||||
result={(
|
result={(
|
||||||
<LaboratoryResultSummary
|
<LaboratoryResultSummary
|
||||||
@@ -638,12 +171,12 @@ export function VegetationShadowResultView({
|
|||||||
metrics={[
|
metrics={[
|
||||||
{
|
{
|
||||||
label: "Route masks",
|
label: "Route masks",
|
||||||
value: route ? `${route.frameCount}/${route.frameCount}` : "0/4489",
|
value: `${route.frameCount}/${route.frameCount}`,
|
||||||
hint: "sealed local playback · Worker для открытия не нужен",
|
hint: "sealed local playback · Worker для открытия не нужен",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Semantic sources",
|
label: "Semantic sources",
|
||||||
value: route ? "2 independent layers" : "0",
|
value: "2 independent layers",
|
||||||
hint: "EoMT CITY / DDRNet VEGETATION · display switches, evidence does not fuse",
|
hint: "EoMT CITY / DDRNet VEGETATION · display switches, evidence does not fuse",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ test("M4 keeps independent semantic controls in media and spatial panes", async
|
|||||||
);
|
);
|
||||||
assert.match(source, /showMediaSemantic/);
|
assert.match(source, /showMediaSemantic/);
|
||||||
assert.match(source, /showSpatialSemantic/);
|
assert.match(source, /showSpatialSemantic/);
|
||||||
|
assert.match(source, /activeSpatialSemantic = spatialSemantic \?\? activeSemantic/);
|
||||||
|
assert.match(source, /spatialSemanticClasses/);
|
||||||
|
assert.match(source, /spatialSemanticPalette/);
|
||||||
assert.match(source, /activeSemantic && evidenceDemand\.selectedSemanticMask && frame/);
|
assert.match(source, /activeSemantic && evidenceDemand\.selectedSemanticMask && frame/);
|
||||||
assert.match(source, /Array\.from\(\{ length: 12 \}, \(_, index\) => index \+ 1\)/);
|
assert.match(source, /Array\.from\(\{ length: 12 \}, \(_, index\) => index \+ 1\)/);
|
||||||
assert.match(source, /\|\| !showSpatialSemantic/);
|
assert.match(source, /\|\| !showSpatialSemantic/);
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ test("vegetation GOOSE benchmark opens through its separate archival endpoint",
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("vegetation realtime LAB and archival benchmark use separate admitted instruments", async () => {
|
test("vegetation realtime LAB and archival benchmark use separate admitted instruments", async () => {
|
||||||
const [resultSource, benchmarkSource] = await Promise.all([
|
const [resultSource, benchmarkSource, m49Source] = await Promise.all([
|
||||||
readFile(
|
readFile(
|
||||||
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
|
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
|
||||||
"utf8",
|
"utf8",
|
||||||
@@ -374,21 +374,24 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr
|
|||||||
new URL("../src/workspaces/laboratory/VegetationBenchmarkResult.tsx", import.meta.url),
|
new URL("../src/workspaces/laboratory/VegetationBenchmarkResult.tsx", import.meta.url),
|
||||||
"utf8",
|
"utf8",
|
||||||
),
|
),
|
||||||
|
readFile(
|
||||||
|
new URL("../src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
assert.doesNotMatch(resultSource, /M48MaskComparisonVisual/);
|
assert.doesNotMatch(resultSource, /M48MaskComparisonVisual/);
|
||||||
assert.match(resultSource, /M4ReplayThreatVisual/);
|
|
||||||
assert.match(resultSource, /M49TgsFullShadowEvidence/);
|
assert.match(resultSource, /M49TgsFullShadowEvidence/);
|
||||||
assert.match(resultSource, /semanticOverride/);
|
assert.match(resultSource, /semanticOverride/);
|
||||||
assert.match(resultSource, /EoMT CITY \/ DDRNet VEGETATION/);
|
assert.match(resultSource, /EoMT CITY \/ DDRNet VEGETATION/);
|
||||||
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 4);
|
assert.match(m49Source, /spatialSemantic=\{spatialSemantic\}/);
|
||||||
assert.match(resultSource, /RAVNOVES004TREE mixed route review/);
|
assert.match(m49Source, /controlLabel: "SEMANTICS"/);
|
||||||
assert.match(resultSource, /RAVNOVES004TREE full recorded review/);
|
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 1);
|
||||||
assert.match(resultSource, /LaboratoryRecordedClipPlayer/);
|
assert.doesNotMatch(resultSource, /RAVNOVES004TREE mixed route review/);
|
||||||
assert.match(resultSource, /RerunViewport/);
|
assert.doesNotMatch(resultSource, /RAVNOVES004TREE full recorded review/);
|
||||||
assert.match(resultSource, /M48EvidenceModeRail/);
|
assert.doesNotMatch(resultSource, /LaboratoryRecordedClipPlayer/);
|
||||||
assert.match(resultSource, /planAvailable=\{false\}/);
|
assert.doesNotMatch(resultSource, /RerunViewport/);
|
||||||
assert.match(resultSource, /kind="recorded-replay"/);
|
assert.doesNotMatch(resultSource, /M48EvidenceModeRail/);
|
||||||
assert.match(resultSource, /className="m48-clip-player__overlay"/);
|
assert.match(resultSource, /linked canonical M4\.9 TGS evidence/);
|
||||||
assert.match(resultSource, /linkedTgsResultId/);
|
assert.match(resultSource, /linkedTgsResultId/);
|
||||||
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
|
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
|
||||||
assert.doesNotMatch(benchmarkSource, /M49TgsFullShadowEvidence/);
|
assert.doesNotMatch(benchmarkSource, /M49TgsFullShadowEvidence/);
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ _E37_RESULT_ID = re.compile(r"^e37-ravnoves-acceptance-[a-f0-9]{64}$")
|
|||||||
_E38_RESULT_ID = re.compile(r"^e38-perception-baseline-[a-f0-9]{64}$")
|
_E38_RESULT_ID = re.compile(r"^e38-perception-baseline-[a-f0-9]{64}$")
|
||||||
_E39_RESULT_ID = re.compile(r"^e39-perception-refinement-[a-f0-9]{64}$")
|
_E39_RESULT_ID = re.compile(r"^e39-perception-refinement-[a-f0-9]{64}$")
|
||||||
_E40_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
|
_E40_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
|
||||||
|
_M4_RESULT_ID = re.compile(r"^m4-threat-replay-[a-f0-9]{64}$")
|
||||||
|
_M49_TGS_RESULT_ID = re.compile(r"^m49-tgs-full-shadow-[a-f0-9]{64}$")
|
||||||
|
|
||||||
RootProvider = Callable[[], Path | None]
|
RootProvider = Callable[[], Path | None]
|
||||||
|
|
||||||
@@ -245,6 +247,7 @@ def _advanced_index_item(
|
|||||||
raise ValueError("advanced LAB authority is invalid")
|
raise ValueError("advanced LAB authority is invalid")
|
||||||
if document.get("ground_truth") not in (None, False):
|
if document.get("ground_truth") not in (None, False):
|
||||||
raise ValueError("advanced LAB ground-truth claim is invalid")
|
raise ValueError("advanced LAB ground-truth claim is invalid")
|
||||||
|
_validate_product_publication_shape(document, work_id=work_id)
|
||||||
created_at_utc = document.get("created_at_utc")
|
created_at_utc = document.get("created_at_utc")
|
||||||
if not isinstance(created_at_utc, str) or not created_at_utc.strip():
|
if not isinstance(created_at_utc, str) or not created_at_utc.strip():
|
||||||
raise ValueError("advanced LAB creation time is invalid")
|
raise ValueError("advanced LAB creation time is invalid")
|
||||||
@@ -256,6 +259,30 @@ def _advanced_index_item(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_product_publication_shape(
|
||||||
|
document: dict[str, Any],
|
||||||
|
*,
|
||||||
|
work_id: str,
|
||||||
|
) -> None:
|
||||||
|
if work_id != "lab-v1-vegetation-shadow":
|
||||||
|
return
|
||||||
|
route = document.get("route_video")
|
||||||
|
fusion = route.get("fusion") if isinstance(route, dict) else None
|
||||||
|
if (
|
||||||
|
not isinstance(route, dict)
|
||||||
|
or route.get("view_kind") != "coarse-material-policy-review"
|
||||||
|
or _M4_RESULT_ID.fullmatch(str(route.get("base_m4_result_id", ""))) is None
|
||||||
|
or _M49_TGS_RESULT_ID.fullmatch(str(route.get("linked_tgs_result_id", "")))
|
||||||
|
is None
|
||||||
|
or not isinstance(fusion, dict)
|
||||||
|
or fusion.get("mode") != "synchronised-multilayer-review"
|
||||||
|
or fusion.get("pixel_raster_fusion") is not False
|
||||||
|
or document.get("route_review") is not None
|
||||||
|
or document.get("route_full_review") is not None
|
||||||
|
):
|
||||||
|
raise ValueError("vegetation LAB has no canonical M4/M4.9 publication shape")
|
||||||
|
|
||||||
|
|
||||||
def _advanced_index(
|
def _advanced_index(
|
||||||
specs: tuple[_AdvancedIndexSpec, ...],
|
specs: tuple[_AdvancedIndexSpec, ...],
|
||||||
) -> dict[str, object]:
|
) -> dict[str, object]:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path, PurePosixPath
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
@@ -151,6 +152,72 @@ def test_advanced_index_projects_one_most_mature_lifecycle_phase(tmp_path: Path)
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_advanced_index_skips_noncanonical_vegetation_viewers(tmp_path: Path) -> None:
|
||||||
|
root = tmp_path / "vegetation"
|
||||||
|
|
||||||
|
def publish(digest: str, *, canonical: bool) -> Path:
|
||||||
|
result_id = f"lab-v1-vegetation-shadow-{digest}"
|
||||||
|
candidate = root / result_id
|
||||||
|
candidate.mkdir(parents=True)
|
||||||
|
route_video = {
|
||||||
|
"view_kind": "coarse-material-policy-review",
|
||||||
|
"base_m4_result_id": f"m4-threat-replay-{'1' * 64}",
|
||||||
|
"linked_tgs_result_id": f"m49-tgs-full-shadow-{'2' * 64}",
|
||||||
|
"fusion": {
|
||||||
|
"mode": "synchronised-multilayer-review",
|
||||||
|
"pixel_raster_fusion": False,
|
||||||
|
},
|
||||||
|
} if canonical else None
|
||||||
|
(candidate / "manifest.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"schema_version": "missioncore.lab-v1-vegetation-shadow/v1",
|
||||||
|
"result_id": result_id,
|
||||||
|
"identity_sha256": digest,
|
||||||
|
"identity": {
|
||||||
|
"authority": {
|
||||||
|
"commands_enabled": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"created_at_utc": "2026-08-29T10:00:00Z",
|
||||||
|
"ground_truth": False,
|
||||||
|
"route_video": route_video,
|
||||||
|
"route_review": None,
|
||||||
|
"route_full_review": None if canonical else {"frame_count": 6830},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
canonical = publish("a" * 64, canonical=True)
|
||||||
|
incomplete = publish("b" * 64, canonical=False)
|
||||||
|
os.utime(canonical, ns=(10_000_000_000, 10_000_000_000))
|
||||||
|
os.utime(incomplete, ns=(20_000_000_000, 20_000_000_000))
|
||||||
|
registry = _evidence_registry(
|
||||||
|
root,
|
||||||
|
work_id="lab-v1-vegetation-shadow",
|
||||||
|
result_id_prefix="lab-v1-vegetation-shadow",
|
||||||
|
schema_version="missioncore.lab-v1-vegetation-shadow/v1",
|
||||||
|
)
|
||||||
|
router = build_advanced_laboratory_router(
|
||||||
|
evidence_registry=registry,
|
||||||
|
evidence_runtime_root_provider=lambda: root.parent,
|
||||||
|
)
|
||||||
|
|
||||||
|
index = _endpoint(router, "/api/v1/laboratory/advanced-index")()
|
||||||
|
|
||||||
|
assert index["items"] == [ # type: ignore[index]
|
||||||
|
{
|
||||||
|
"work_id": "lab-v1-vegetation-shadow",
|
||||||
|
"result_id": canonical.name,
|
||||||
|
"created_at_utc": "2026-08-29T10:00:00Z",
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_advanced_index_includes_valid_l31_identity(
|
def test_advanced_index_includes_valid_l31_identity(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
monkeypatch: MonkeyPatch,
|
monkeypatch: MonkeyPatch,
|
||||||
|
|||||||
Reference in New Issue
Block a user