+ setIndex((current) => (
+ current - 1 + result.frames.length
+ ) % result.frames.length)}
+ >
+
+
+ setIndex((current) => (current + 1) % result.frames.length)}
+ >
+
+
+ >
+ )}
+ overlay={selected ? (
+
+ SHADOW ONLY
+ RAVNOVES00 · frame {selected.frameId} · {mode.toUpperCase()}
+
+ {count} risk detections · threshold 0.50 · independent ground truth отсутствует
+
+
+ ) : null}
+ >
+ {loading ? (
+
+
+ Загружаем точный camera-кадр
+
+ ) : error ? (
+
+
+ {error}
+
+ ) : frame ? (
+
+ ) : (
+
+
+ Каталог кадров M4.8S пуст.
+
+ )}
+
+ );
+}
diff --git a/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx b/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx
index 0f47d33..c160e21 100644
--- a/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx
+++ b/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx
@@ -17,6 +17,7 @@ import {
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import { RecordedEvidenceImageScene } from "../../components/laboratory/RecordedEvidenceImageScene";
+import type { RecordedEvidencePointCloudOverlayData } from "../../components/laboratory/RecordedEvidencePointCloudOverlay";
import type {
RecordedEvidenceSemanticClass,
RecordedEvidenceSemanticOverlay,
@@ -58,10 +59,14 @@ function toneForProposal(proposal: M4ThreatCameraProposal): RecordedEvidenceBox[
}
function proposalLabel(proposal: M4ThreatCameraProposal): string {
- const decision = proposal.threatDecision ?? "unknown";
- if (proposal.rangeM === null) return decision;
+ const decision = proposal.threatDecision
+ ?? (proposal.occupiedSupport ? "geometry-supported" : "camera-only");
+ const semantic = proposal.semanticHint ?? "object";
+ if (proposal.rangeM === null) {
+ return `${semantic} · ${proposal.objectness.toFixed(2)} · ${decision}`;
+ }
const range = `${proposal.rangeM.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} м`;
- return `${range} · ${decision}`;
+ return `${semantic} · ${proposal.objectness.toFixed(2)} · ${range} · ${decision}`;
}
function boxes(proposals: readonly M4ThreatCameraProposal[]): readonly RecordedEvidenceBox[] {
@@ -104,10 +109,14 @@ export function M4ReplayThreatVisual({
resultId,
semantic,
reviewAnchors = EMPTY_REVIEW_ANCHORS,
+ timelineEndpointRoot,
+ evidenceLabel = "M4.6",
}: {
resultId: string;
semantic?: M4ReplayThreatSemanticLayer;
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
+ timelineEndpointRoot?: string;
+ evidenceLabel?: string;
}) {
const [mediaMode, setMediaMode] = useState