feat(ui): add M4.8S replay with LiDAR overlay
This commit is contained in:
@@ -44,6 +44,7 @@ import { M4ReplayThreatResultView } from "./M4ReplayThreatResult";
|
||||
import { M47ReferenceGraphResultView } from "./M47ReferenceGraphResult";
|
||||
import { M48ObjectCentricQualityResultView } from "./M48ObjectCentricQualityResult";
|
||||
import { M48SmallStaticPassageRegressionResultView } from "./M48SmallStaticPassageRegressionResult";
|
||||
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
|
||||
|
||||
export { isAdvancedLaboratoryWorkId };
|
||||
export type { AdvancedLaboratoryWorkId };
|
||||
@@ -92,6 +93,9 @@ export function AdvancedLaboratoryResult({
|
||||
if (workId === "m48-small-static-passage-regression" && results.m48SmallStatic) {
|
||||
return <M48SmallStaticPassageRegressionResultView rigLabel={rigLabel} result={results.m48SmallStatic} />;
|
||||
}
|
||||
if (workId === "m48s-fixed-class-detector" && results.m48s) {
|
||||
return <M48SFixedClassDetectorResultView rigLabel={rigLabel} result={results.m48s} />;
|
||||
}
|
||||
if (workId === "m47-reference-graph-shadow" && results.m47Graph) {
|
||||
return <M47ReferenceGraphResultView rigLabel={rigLabel} result={results.m47Graph} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type { M48SFixedClassDetectorResult } from "../../core/laboratory/m48sFixedClassDetector";
|
||||
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
|
||||
|
||||
function decimal(value: number, digits = 1): string {
|
||||
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
||||
}
|
||||
|
||||
export function M48SFixedClassDetectorResultView({
|
||||
rigLabel,
|
||||
result,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
result: M48SFixedClassDetectorResult;
|
||||
}) {
|
||||
const selected = result.metrics.candidates.find((candidate) => candidate.selected);
|
||||
const load = result.metrics.detectorLoad;
|
||||
const integrated = result.metrics.integratedWorldState;
|
||||
const status = integrated
|
||||
? "Полный RF-DETR reference graph выдержал realtime shadow"
|
||||
: "RF-DETR-L выдержал detector-only realtime shadow";
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="M4.8S · fixed-class semantics риск-объектов"
|
||||
description="Сравнение трёх готовых COCO-детекторов на точных кадрах RAVNOVES00, 30-минутная квалификация RF-DETR-L и полный source-paced прогон RF-DETR → geometry → temporal → motion → rolling map → threat на Worker 006. Статические препятствия остаются в геометрическом контуре; классы используются только там, где меняется ожидаемое поведение."
|
||||
status={status}
|
||||
statusTone="success"
|
||||
facts={[
|
||||
{ label: "Источник", value: `${rigLabel} RIGHT · raw KB4 · ${result.source.evidenceFrameCount} diagnostic frames` },
|
||||
{ label: "Сравнение", value: "YOLOX-S · D-FINE-S · RF-DETR-L · единый threshold 0.50" },
|
||||
{ label: "Worker", value: "Worker 006 · RTX 4090 · TensorRT 11 + isolated Triton" },
|
||||
{ label: "Authority", value: "SHADOW ONLY · commands OFF · actuation OFF · production NO" },
|
||||
]}
|
||||
brief={{
|
||||
question: "Можно ли заменить слабую class-семантику YOLOX готовой моделью, не потеряв realtime на предельном Worker с RTX 4090?",
|
||||
approach: `YOLOX-S, D-FINE-S и RF-DETR-L сравнили на одинаковых ${result.source.evidenceFrameCount} raw-KB4 кадрах с порогом 0.50. RF-DETR-L отдельно квалифицировали ${decimal(load.durationSeconds / 60, 0)} минут, затем встроили в полный reference graph без дополнительного inference-прохода.`,
|
||||
principalResult: integrated
|
||||
? `Полный граф доставил ${integrated.deliveredWorldStates.toLocaleString("ru-RU")} world states при ${decimal(integrated.effectiveWorldStateFps, 3)} FPS и p95 ${decimal(integrated.worldStateCompletionAgeP95Ms, 3)} ms; ${integrated.supersededFrames} входных кадров штатно вытеснены latest-wins очередью.`
|
||||
: `RF-DETR-L выбран из трёх кандидатов и обработал ${load.sourceFramesConsumed.toLocaleString("ru-RU")} кадров detector-only без замен и ошибок.`,
|
||||
limitation: "Прогон доказывает runtime envelope, а не истинность классов, качество track identity, корректность risk policy или безопасность движения. Production authority и команды отключены.",
|
||||
}}
|
||||
method={{
|
||||
completeness: result.method.completeness,
|
||||
executionClass: result.method.executionClass,
|
||||
pipelineId: result.method.pipelineId,
|
||||
components: result.method.components.map((component) => ({
|
||||
...component,
|
||||
identitySha256: component.identitySha256,
|
||||
})),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="M4.8S VISUAL EVIDENCE · FULL REFERENCE GRAPH REPLAY"
|
||||
title="Полное видео: RF-DETR классы, LiDAR, 3D/PLAN и world-state на общем таймлайне"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<M4ReplayThreatVisual
|
||||
resultId={result.resultId}
|
||||
timelineEndpointRoot="/api/v1/laboratory/m48s/fixed-class-detector"
|
||||
evidenceLabel="M4.8S RF-DETR GRAPH"
|
||||
/>
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title={integrated
|
||||
? "Что дал прогон: полный world-state graph проходит realtime envelope"
|
||||
: "Что дал прогон: RF-DETR-L проходит detector-only realtime envelope"}
|
||||
status={status}
|
||||
statusTone="success"
|
||||
metrics={integrated ? [
|
||||
{ label: "Complete graph", value: `${decimal(integrated.effectiveWorldStateFps, 3)} FPS`, hint: "target ≥ 9.5 FPS · source-paced" },
|
||||
{ label: "World-state age p95", value: `${decimal(integrated.worldStateCompletionAgeP95Ms, 3)} ms`, hint: `p99 ${decimal(integrated.worldStateCompletionAgeP99Ms, 3)} ms · target ≤ 175 ms` },
|
||||
{ label: "Delivered / superseded", value: `${integrated.deliveredWorldStates.toLocaleString("ru-RU")} / ${integrated.supersededFrames}`, hint: `${integrated.failures} failures · queues ${Math.max(...Object.values(integrated.queueHighWatermarks))}/${integrated.queueCapacity}` },
|
||||
{ label: "GPU / VRAM peak", value: `${decimal(integrated.gpuUtilizationMaximumPercent, 0)}% / ${decimal(integrated.gpuMemoryMaximumMib / 1024)} GiB`, hint: `GPU mean ${decimal(integrated.gpuUtilizationMeanPercent)}% · ${decimal(integrated.gpuPowerMaximumW)} W` },
|
||||
] : [
|
||||
{ label: "Detector capacity", value: `${decimal(selected?.capacityFps ?? 0)} FPS`, hint: "RF-DETR-L TensorRT/Triton" },
|
||||
{ label: "Completion age p95", value: `${decimal(load.completionAgeP95Ms)} ms`, hint: "detector-only · target ≤ 175 ms" },
|
||||
{ label: "Consumed / replaced", value: `${load.sourceFramesConsumed.toLocaleString("ru-RU")} / ${load.sourceFrameReplacements}`, hint: `${decimal(load.effectiveConsumedFps, 3)} source FPS · ${load.failures} failures` },
|
||||
{ label: "GPU / VRAM peak", value: `${decimal(load.gpuUtilizationMaximumPercent, 0)}% / ${decimal(load.gpuMemoryMaximumMib / 1024)} GiB`, hint: `GPU mean ${decimal(load.gpuUtilizationMeanPercent)}% · queue ${load.queueMaximumDepth}/${load.queueCapacity}` },
|
||||
]}
|
||||
conclusion={{
|
||||
proved: integrated
|
||||
? `На Worker 006 полный граф обработал ${integrated.sourceFramesAdmitted.toLocaleString("ru-RU")} входных кадров, доставил ${integrated.deliveredWorldStates.toLocaleString("ru-RU")} состояний без ошибок, удержал все очереди в пределах ${integrated.queueCapacity} и p95 ${decimal(integrated.worldStateCompletionAgeP95Ms, 3)} ms. Advisory сформировал публикации по семействам: geometry-only (${integrated.advisoryFamilyCounts["generic-obstacle"].toLocaleString("ru-RU")}), люди (${integrated.advisoryFamilyCounts.person.toLocaleString("ru-RU")}), животные (${integrated.advisoryFamilyCounts.animal.toLocaleString("ru-RU")}) и транспорт (${integrated.advisoryFamilyCounts.vehicle.toLocaleString("ru-RU")}); это не количество уникальных физических объектов и не потребовало второго inference.`
|
||||
: `RF-DETR-L ${decimal(load.durationSeconds / 60, 0)} минут устойчиво потреблял source-paced поток около 10 FPS: ${load.sourceFramesConsumed.toLocaleString("ru-RU")} кадров, 0 замен, 0 ошибок, completion-age p95 ${decimal(load.completionAgeP95Ms, 3)} ms.`,
|
||||
notProved: "Не доказаны unbiased precision/recall классов, независимое качество track identity и risk policy, поведение planner или collision safety. Кадровые рамки не заменяют геометрическую occupancy-карту.",
|
||||
decision: "Сохранить RF-DETR-L как risk-semantic shadow provider полного reference graph. Не классифицировать миллионы статических форм: неизвестное неподвижное препятствие остаётся geometry-owned и объезжается; классы сохраняются для людей, животных и транспорта. Production switch не разрешён.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||
import {
|
||||
RecordedEvidenceBoxOverlay,
|
||||
type RecordedEvidenceBox,
|
||||
type RecordedEvidenceBoxTone,
|
||||
} from "../../components/laboratory/RecordedEvidenceBoxOverlay";
|
||||
import {
|
||||
fetchM48SFixedClassDetectorFrame,
|
||||
type M48SDetectorFrame,
|
||||
type M48SDetectorMode,
|
||||
type M48SFixedClassDetectorResult,
|
||||
} from "../../core/laboratory/m48sFixedClassDetector";
|
||||
|
||||
const MODES = [
|
||||
{ value: "source", label: "SOURCE" },
|
||||
{ value: "yolox", label: "YOLOX" },
|
||||
{ value: "dfine", label: "D-FINE" },
|
||||
{ value: "rf-detr", label: "RF-DETR" },
|
||||
] as const;
|
||||
|
||||
const ANIMAL_LABELS = new Set([
|
||||
"bird",
|
||||
"cat",
|
||||
"dog",
|
||||
"horse",
|
||||
"sheep",
|
||||
"cow",
|
||||
"elephant",
|
||||
"bear",
|
||||
"zebra",
|
||||
"giraffe",
|
||||
]);
|
||||
const VULNERABLE_ROAD_USERS = new Set(["person", "bicycle", "motorcycle", "skateboard"]);
|
||||
|
||||
function message(error: unknown): string {
|
||||
return error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
: "M4.8S visual evidence недоступно.";
|
||||
}
|
||||
|
||||
function toneForLabel(label: string): RecordedEvidenceBoxTone {
|
||||
if (ANIMAL_LABELS.has(label)) return "danger";
|
||||
if (VULNERABLE_ROAD_USERS.has(label)) return "warning";
|
||||
return "accent";
|
||||
}
|
||||
|
||||
function DetectorScene({ frame, mode }: { frame: M48SDetectorFrame; mode: M48SDetectorMode }) {
|
||||
const boxes = useMemo<readonly RecordedEvidenceBox[]>(() => {
|
||||
if (mode === "source") return [];
|
||||
return frame.detections[mode].map((detection) => ({
|
||||
boxXyxy: detection.bboxXyxy,
|
||||
label: `${detection.label} · ${detection.score.toFixed(2)}`,
|
||||
tone: toneForLabel(detection.label),
|
||||
}));
|
||||
}, [frame, mode]);
|
||||
|
||||
return (
|
||||
<div className="m48-atlas-visual__scene">
|
||||
<img src={frame.cameraUrl} alt="" draggable={false} />
|
||||
<RecordedEvidenceBoxOverlay
|
||||
imageWidth={frame.imageWidth}
|
||||
imageHeight={frame.imageHeight}
|
||||
boxes={boxes}
|
||||
ariaLabel={`M4.8S ${mode} fixed-class detections`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function M48SFixedClassDetectorVisual({
|
||||
result,
|
||||
}: {
|
||||
result: M48SFixedClassDetectorResult;
|
||||
}) {
|
||||
const preferredIndex = Math.max(
|
||||
0,
|
||||
result.frames.findIndex((frame) => frame.frameId === "000253"),
|
||||
);
|
||||
const [index, setIndex] = useState(preferredIndex);
|
||||
const [frame, setFrame] = useState<M48SDetectorFrame | null>(null);
|
||||
const [mode, setMode] = useState<M48SDetectorMode>("rf-detr");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const selected = result.frames[index] ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
setFrame(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
void fetchM48SFixedClassDetectorFrame(result.resultId, selected.frameId, {
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then((next) => !controller.signal.aborted && setFrame(next))
|
||||
.catch((caught: unknown) => !controller.signal.aborted && setError(message(caught)))
|
||||
.finally(() => !controller.signal.aborted && setLoading(false));
|
||||
return () => controller.abort();
|
||||
}, [result.resultId, selected]);
|
||||
|
||||
const count = frame && mode !== "source" ? frame.detections[mode].length : 0;
|
||||
return (
|
||||
<LaboratoryEvidenceViewer
|
||||
label="M4.8S fixed-class detector comparison"
|
||||
className="m48-atlas-visual"
|
||||
mode={mode}
|
||||
modes={MODES}
|
||||
expanded={expanded}
|
||||
onModeChange={setMode}
|
||||
onExpandedChange={setExpanded}
|
||||
actions={(
|
||||
<>
|
||||
<IconButton
|
||||
label="Предыдущий кадр M4.8S"
|
||||
disabled={!result.frames.length}
|
||||
onClick={() => setIndex((current) => (
|
||||
current - 1 + result.frames.length
|
||||
) % result.frames.length)}
|
||||
>
|
||||
<Icon name="chevron-left" size={16} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
label="Следующий кадр M4.8S"
|
||||
disabled={!result.frames.length}
|
||||
onClick={() => setIndex((current) => (current + 1) % result.frames.length)}
|
||||
>
|
||||
<Icon name="chevron-right" size={16} />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
overlay={selected ? (
|
||||
<div className="m48-atlas-visual__case">
|
||||
<StatusBadge tone="warning">SHADOW ONLY</StatusBadge>
|
||||
<strong>RAVNOVES00 · frame {selected.frameId} · {mode.toUpperCase()}</strong>
|
||||
<small>
|
||||
{count} risk detections · threshold 0.50 · independent ground truth отсутствует
|
||||
</small>
|
||||
</div>
|
||||
) : null}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="m48-atlas-visual__state" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
Загружаем точный camera-кадр
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="m48-atlas-visual__state" role="alert">
|
||||
<Icon name="alert" size={18} />
|
||||
{error}
|
||||
</div>
|
||||
) : frame ? (
|
||||
<DetectorScene frame={frame} mode={mode} />
|
||||
) : (
|
||||
<div className="m48-atlas-visual__state" role="alert">
|
||||
<Icon name="alert" size={18} />
|
||||
Каталог кадров M4.8S пуст.
|
||||
</div>
|
||||
)}
|
||||
</LaboratoryEvidenceViewer>
|
||||
);
|
||||
}
|
||||
@@ -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<M4ThreatMediaMode | null>("video");
|
||||
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(null);
|
||||
@@ -116,6 +125,7 @@ export function M4ReplayThreatVisual({
|
||||
const [showRollingMap, setShowRollingMap] = useState(true);
|
||||
const [showMediaSemantic, setShowMediaSemantic] = useState(true);
|
||||
const [showSpatialSemantic, setShowSpatialSemantic] = useState(true);
|
||||
const [showMediaPoints, setShowMediaPoints] = useState(false);
|
||||
const [splitPrimarySize, setSplitPrimarySize] = useState(50);
|
||||
const [splitOrientation, setSplitOrientation] = useState<SplitPaneOrientation>(() => (
|
||||
typeof window !== "undefined" && window.matchMedia("(max-width: 900px)").matches
|
||||
@@ -125,7 +135,7 @@ export function M4ReplayThreatVisual({
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0);
|
||||
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
|
||||
const metadata = useM4ThreatTimelineMetadata(resultId);
|
||||
const metadata = useM4ThreatTimelineMetadata(resultId, timelineEndpointRoot);
|
||||
const playbackRange = useMemo(() => metadata.timeline ? ({
|
||||
startSeconds: metadata.timeline.timelineStartSeconds,
|
||||
endSeconds: metadata.timeline.timelineEndSeconds,
|
||||
@@ -139,6 +149,7 @@ export function M4ReplayThreatVisual({
|
||||
resultId,
|
||||
timeline: metadata.timeline,
|
||||
currentSeconds: playbackController.playback.currentSeconds,
|
||||
endpointRoot: timelineEndpointRoot,
|
||||
});
|
||||
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
|
||||
const [videoLoading, setVideoLoading] = useState(false);
|
||||
@@ -362,6 +373,16 @@ export function M4ReplayThreatVisual({
|
||||
ariaLabel: `E47 semantic mask frame ${frame.sequence + 1}`,
|
||||
}
|
||||
: undefined;
|
||||
const pointCloudOverlay: RecordedEvidencePointCloudOverlayData | undefined =
|
||||
showMediaPoints && frame?.cameraProjection === "factory-kb4-exact"
|
||||
? {
|
||||
pointsXyd: frame.cameraProjectedPointsXyd,
|
||||
sourcePointCount: frame.cameraProjectedSourceCount,
|
||||
projectedPointCount: frame.cameraProjectedPointCount,
|
||||
projection: "factory-kb4-exact",
|
||||
ariaLabel: `${evidenceLabel} LiDAR projection: ${frame.cameraProjectedSampleCount} points`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const handleMediaModeChange = (next: M4ThreatMediaSelection) => {
|
||||
if (next === "none") return;
|
||||
@@ -408,21 +429,35 @@ export function M4ReplayThreatVisual({
|
||||
</div>
|
||||
);
|
||||
|
||||
const mediaLayerControls = semantic ? (
|
||||
const mediaLayerControls = semantic || metadata.timeline?.cameraPointDelivery ? (
|
||||
<div
|
||||
className="m4-replay-threat-visual__pane-layer-controls"
|
||||
role="group"
|
||||
aria-label="Слои камеры и видео"
|
||||
>
|
||||
<Button
|
||||
size="compact"
|
||||
shape="pill"
|
||||
variant={showMediaSemantic ? "primary" : "secondary"}
|
||||
aria-pressed={showMediaSemantic}
|
||||
onClick={() => setShowMediaSemantic((visible) => !visible)}
|
||||
>
|
||||
SEMANTICS
|
||||
</Button>
|
||||
{semantic ? (
|
||||
<Button
|
||||
size="compact"
|
||||
shape="pill"
|
||||
variant={showMediaSemantic ? "primary" : "secondary"}
|
||||
aria-pressed={showMediaSemantic}
|
||||
onClick={() => setShowMediaSemantic((visible) => !visible)}
|
||||
>
|
||||
SEMANTICS
|
||||
</Button>
|
||||
) : null}
|
||||
{metadata.timeline?.cameraPointDelivery ? (
|
||||
<Button
|
||||
size="compact"
|
||||
shape="pill"
|
||||
variant={showMediaPoints ? "primary" : "secondary"}
|
||||
aria-pressed={showMediaPoints}
|
||||
title="Exact LiDAR increment · factory KB4 camera projection"
|
||||
onClick={() => setShowMediaPoints((visible) => !visible)}
|
||||
>
|
||||
POINTS
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
@@ -570,6 +605,12 @@ export function M4ReplayThreatVisual({
|
||||
{spatialFrame
|
||||
? `${spatialFrame.pointCloudSampleCount}/${spatialFrame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
|
||||
: "квалифицированный spatial frame ещё не получен"}
|
||||
{frame.worldStateAvailable
|
||||
? " · world-state delivered"
|
||||
: ` · world-state gap (${frame.terminalOutcome})`}
|
||||
{pointCloudOverlay
|
||||
? ` · camera points ${frame.cameraProjectedSampleCount}/${frame.cameraProjectedPointCount}`
|
||||
: ""}
|
||||
{semantic && spatialSemanticFrame
|
||||
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
|
||||
: semantic ? " · semantic buffer" : ""}
|
||||
@@ -595,7 +636,7 @@ export function M4ReplayThreatVisual({
|
||||
content = (
|
||||
<div className="l3-visual-audit__state" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
<span>Открываем recorded-realtime timeline M4.6</span>
|
||||
<span>Открываем recorded-realtime timeline {evidenceLabel}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
@@ -628,7 +669,8 @@ export function M4ReplayThreatVisual({
|
||||
imageHeight={timeline.imageHeight}
|
||||
boxes={activeBoxes}
|
||||
semanticOverlay={mediaMode === "video" ? semanticOverlay : undefined}
|
||||
ariaLabel={`M4.6 recorded-realtime frame ${frame?.sequence ?? 0}: ${activeBoxes.length} proposals`}
|
||||
pointCloudOverlay={mediaMode === "video" ? pointCloudOverlay : undefined}
|
||||
ariaLabel={`${evidenceLabel} recorded-realtime frame ${frame?.sequence ?? 0}: ${activeBoxes.length} proposals`}
|
||||
interactive={false}
|
||||
segmentSequence={
|
||||
timelineFrame.activeSequence === null
|
||||
@@ -655,7 +697,8 @@ export function M4ReplayThreatVisual({
|
||||
imageHeight={timeline.imageHeight}
|
||||
boxes={activeBoxes}
|
||||
semanticOverlay={semanticOverlay}
|
||||
ariaLabel={`M4.6 exact camera frame ${frame.sequence}: ${activeBoxes.length} proposals`}
|
||||
pointCloudOverlay={pointCloudOverlay}
|
||||
ariaLabel={`${evidenceLabel} exact camera frame ${frame.sequence}: ${activeBoxes.length} proposals`}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
@@ -689,7 +732,7 @@ export function M4ReplayThreatVisual({
|
||||
corridor={timeline.corridor}
|
||||
occupiedVoxelSizeM={timeline.occupiedVoxelSizeM}
|
||||
mode={spatialMode}
|
||||
label="M4.6 exact current increment, bounded local SLAM surface and rolling occupancy"
|
||||
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
|
||||
showCurrentIncrement={showCurrentIncrement}
|
||||
showLocalSurface={showLocalSurface}
|
||||
showRollingMap={showRollingMap}
|
||||
@@ -791,7 +834,7 @@ export function M4ReplayThreatVisual({
|
||||
<LaboratoryEvidenceViewer
|
||||
label={semantic
|
||||
? "E47 semantic + SLAM diagnostic replay"
|
||||
: "M4.6 dual-evidence recorded-realtime replay"}
|
||||
: `${evidenceLabel} recorded-realtime replay`}
|
||||
className="m4-replay-threat-evidence-viewer"
|
||||
mode={mediaMode ?? "none"}
|
||||
modes={[
|
||||
|
||||
@@ -77,6 +77,13 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
|
||||
experimentName: "M4.8 · small static passage regression",
|
||||
variantName: "M4.8R1 · Worker 006 small-static assisted baseline",
|
||||
},
|
||||
"m48s-fixed-class-detector": {
|
||||
profileId: "rig-ravnoves-perception-gate-v1",
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 perception gate`,
|
||||
experimentId: "m48s-fixed-class-risk-detector",
|
||||
experimentName: "RAVNOVES00 fixed-class risk detector",
|
||||
variantName: "M4.8S · RF-DETR-L TensorRT/Triton shadow",
|
||||
},
|
||||
"m47-reference-graph-shadow": {
|
||||
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`,
|
||||
|
||||
@@ -21,6 +21,7 @@ function mergeResults(
|
||||
m47Graph: next.m47Graph ?? current.m47Graph,
|
||||
m48: next.m48 ?? current.m48,
|
||||
m48SmallStatic: next.m48SmallStatic ?? current.m48SmallStatic,
|
||||
m48s: next.m48s ?? current.m48s,
|
||||
m4Threat: next.m4Threat ?? current.m4Threat,
|
||||
l3: next.l3 ?? current.l3,
|
||||
l31: next.l31 ?? current.l31,
|
||||
|
||||
@@ -41,7 +41,7 @@ export function cancelM4ThreatChunkRequestsOutsideWindow<T extends { abort(): vo
|
||||
}
|
||||
}
|
||||
|
||||
export function useM4ThreatTimelineMetadata(resultId: string) {
|
||||
export function useM4ThreatTimelineMetadata(resultId: string, endpointRoot?: string) {
|
||||
const [timeline, setTimeline] = useState<M4ThreatTimeline | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -49,7 +49,7 @@ export function useM4ThreatTimelineMetadata(resultId: string) {
|
||||
const controller = new AbortController();
|
||||
setTimeline(null);
|
||||
setError(null);
|
||||
void fetchM4ThreatTimeline(resultId, { signal: controller.signal })
|
||||
void fetchM4ThreatTimeline(resultId, { signal: controller.signal, endpointRoot })
|
||||
.then((next) => {
|
||||
if (!controller.signal.aborted) setTimeline(next);
|
||||
})
|
||||
@@ -59,7 +59,7 @@ export function useM4ThreatTimelineMetadata(resultId: string) {
|
||||
}
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [resultId]);
|
||||
}, [endpointRoot, resultId]);
|
||||
|
||||
return { timeline, loading: !timeline && !error, error };
|
||||
}
|
||||
@@ -68,10 +68,12 @@ export function useM4ThreatTimelineFrame({
|
||||
resultId,
|
||||
timeline,
|
||||
currentSeconds,
|
||||
endpointRoot,
|
||||
}: {
|
||||
resultId: string;
|
||||
timeline: M4ThreatTimeline | null;
|
||||
currentSeconds: number;
|
||||
endpointRoot?: string;
|
||||
}) {
|
||||
const [chunks, setChunks] = useState<ReadonlyMap<number, M4ThreatTimelineChunk>>(
|
||||
() => new Map(),
|
||||
@@ -124,6 +126,7 @@ export function useM4ThreatTimelineFrame({
|
||||
inFlight.current.set(start, controller);
|
||||
void fetchM4ThreatTimelineChunk(resultId, start, chunkSize, {
|
||||
signal: controller.signal,
|
||||
endpointRoot,
|
||||
})
|
||||
.then((chunk) => {
|
||||
if (controller.signal.aborted) return;
|
||||
@@ -151,7 +154,7 @@ export function useM4ThreatTimelineFrame({
|
||||
if (inFlight.current.get(start) === controller) inFlight.current.delete(start);
|
||||
});
|
||||
}
|
||||
}, [activeChunkStart, chunkSize, resultId, timeline]);
|
||||
}, [activeChunkStart, chunkSize, endpointRoot, resultId, timeline]);
|
||||
|
||||
const activeFrame: M4ThreatTimelineFrame | null = useMemo(() => {
|
||||
if (activeSequence === null || activeChunkStart === null) return null;
|
||||
|
||||
Reference in New Issue
Block a user