feat(lab): visualize semantic SLAM shadow
This commit is contained in:
@@ -39,6 +39,7 @@ import { E46GRectifiedDetectorBakeoffResultView } from "./E46GRectifiedDetectorB
|
||||
import { E46HFullRectifiedFrontReplayResultView } from "./E46HFullRectifiedFrontReplayResult";
|
||||
import { E46IGroundingDinoFullReplayResultView } from "./E46IGroundingDinoFullReplayResult";
|
||||
import { E46JRawFisheyeRealtimeResultView } from "./E46JRawFisheyeRealtimeResult";
|
||||
import { E47SemanticSlamResultView } from "./E47SemanticSlamResult";
|
||||
import { M4ReplayThreatResultView } from "./M4ReplayThreatResult";
|
||||
|
||||
export { isAdvancedLaboratoryWorkId };
|
||||
@@ -85,6 +86,9 @@ export function AdvancedLaboratoryResult({
|
||||
if (workId === "m4-replay-threat" && results.m4Threat) {
|
||||
return <M4ReplayThreatResultView rigLabel={rigLabel} result={results.m4Threat} />;
|
||||
}
|
||||
if (workId === "e47-semantic-slam-shadow" && results.e47) {
|
||||
return <E47SemanticSlamResultView rigLabel={rigLabel} result={results.e47} />;
|
||||
}
|
||||
if (workId === "l3-pointpillars-visual-audit" && results.l3) {
|
||||
return <L3PointPillarsResult result={results.l3} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type { E47SemanticSlamResult } from "../../core/laboratory/e47SemanticSlam";
|
||||
import { formatNumber } from "../../presentation";
|
||||
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
|
||||
|
||||
export function E47SemanticSlamResultView({
|
||||
rigLabel,
|
||||
result,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
result: E47SemanticSlamResult;
|
||||
}) {
|
||||
const pointProjectedCoverage = result.metrics.points.total
|
||||
? result.metrics.points.projected / result.metrics.points.total
|
||||
: 0;
|
||||
const pointLabeledCoverage = result.metrics.points.total
|
||||
? result.metrics.points.labeled / result.metrics.points.total
|
||||
: 0;
|
||||
const observationLabeledCoverage = result.metrics.observations.total
|
||||
? result.metrics.observations.labeled / result.metrics.observations.total
|
||||
: 0;
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="E47 · semantic mask → KB4 → SLAM shadow"
|
||||
description="Зафиксированные EoMT-маски проецируются заводской KB4-калибровкой на исходные точки SLAM/LiDAR и отдельно агрегируются по уже существующим геометрическим наблюдениям. Это диагностический слой: он не меняет occupancy, motion, threat или safe/unknown."
|
||||
status="Diagnostic contract passed · provider quality gate open"
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
{
|
||||
label: "Конфигурация",
|
||||
value: `${rigLabel} · RIGHT camera + registered SLAM cloud · recorded replay`,
|
||||
},
|
||||
{
|
||||
label: "Semantic control",
|
||||
value: `${result.provider.modelId} · exact revision ${result.provider.modelRevision.slice(0, 12)}`,
|
||||
},
|
||||
{
|
||||
label: "Проекция",
|
||||
value: `factory KB4 · ${result.calibrationContentSha256.slice(0, 12)} · frame-local point IDs`,
|
||||
},
|
||||
{
|
||||
label: "Синхрон",
|
||||
value: `semantic↔camera exact ledger · camera↔LiDAR E6 best-effort ≤${formatNumber(result.temporalBinding.maximumLidarCameraDeltaMs, 0)} ms · HW sync: нет`,
|
||||
},
|
||||
{
|
||||
label: "Покрытие",
|
||||
value: `${result.metrics.frames.maskAvailable}/${result.metrics.frames.total} masks · ${formatNumber(pointProjectedCoverage * 100, 1)}% projected · ${formatNumber(pointLabeledCoverage * 100, 1)}% unambiguous`,
|
||||
},
|
||||
{
|
||||
label: "Визуал",
|
||||
value: "4489-frame VIDEO/CAMERA/3D/PLAN · один recorded clock · semantic layer",
|
||||
},
|
||||
]}
|
||||
brief={{
|
||||
question: "Можно ли добавить плотную семантику камеры к сильной SLAM/LiDAR-геометрии, не превратив классификацию в источник ложного свободного пространства?",
|
||||
approach: "Для всех 4489 кадров переиспользованы неизменяемые EoMT masks, factory KB4 extrinsic/intrinsic и тот же source point index space, на котором построен M4.6. Semantic↔camera сверяется fail-closed по sequence и session-time; camera↔LiDAR сохраняет исходный bounded nearest-arrival E6 contract, а не выдаётся за hardware-sync. Каждая точка получает labeled, ambiguous, unprojected или absent.",
|
||||
principalResult: `${result.metrics.points.labeled.toLocaleString("ru-RU")} точек получили однозначный класс, ${result.metrics.points.ambiguous.toLocaleString("ru-RU")} остались semantic-ambiguous, ${result.metrics.points.unprojected.toLocaleString("ru-RU")} не спроецировались. Из ${result.metrics.observations.total.toLocaleString("ru-RU")} неизменённых geometry observations однозначный класс получили ${formatNumber(observationLabeledCoverage * 100, 1)}%.`,
|
||||
limitation: "EoMT здесь — фиксированный control provider, а не выбранная production-модель. Physical camera↔LiDAR hardware-sync не доказан; принят только E6 nearest-host-arrival best-effort в пределах 100 мс. Semantic/instance truth, obstacle recall и fisheye-specific качество независимо не размечены; отсутствие класса никогда не означает free.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "hybrid",
|
||||
pipelineId: "semantic-slam-diagnostic-shadow/v1",
|
||||
components: [
|
||||
{
|
||||
kind: "source",
|
||||
name: result.semanticResultId,
|
||||
version: result.provider.modelRevision,
|
||||
role: "sealed full-route uint8 semantic masks",
|
||||
identitySha256: result.provider.modelWeightsSha256,
|
||||
},
|
||||
{
|
||||
kind: "source",
|
||||
name: result.sourcePackId,
|
||||
version: "registered map increments + vendor SLAM pose",
|
||||
role: "точный frame-local point index space",
|
||||
identitySha256: result.sourcePackId.split("-").at(-1) ?? null,
|
||||
},
|
||||
{
|
||||
kind: "source",
|
||||
name: result.geometryResultId,
|
||||
version: "immutable M4 geometry observations",
|
||||
role: "неизменяемые obstacle IDs, occupancy и metric geometry",
|
||||
identitySha256: result.geometryResultId.split("-").at(-1) ?? null,
|
||||
},
|
||||
{
|
||||
kind: "algorithm",
|
||||
name: "semantic diagnostic fusion",
|
||||
version: result.profileId,
|
||||
role: "KB4 mask projection + point/observation accounting без safety authority",
|
||||
identitySha256: result.resultId.split("-").at(-1) ?? null,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="E47 VISUAL EVIDENCE · VIDEO / CAMERA / 3D / PLAN"
|
||||
title="Синхронный контроль маски, semantic-точек, геометрии и коридора"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<M4ReplayThreatVisual
|
||||
resultId={result.baseM4ResultId}
|
||||
semantic={{
|
||||
resultId: result.resultId,
|
||||
taxonomy: result.taxonomy,
|
||||
}}
|
||||
/>
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title="Semantic/SLAM seam принят; качество provider ещё не принято"
|
||||
status="Жёлтый: артефакты и проекция доказаны, independent semantic truth отсутствует"
|
||||
statusTone="warning"
|
||||
metrics={[
|
||||
{
|
||||
label: "Semantic masks",
|
||||
value: `${result.metrics.frames.maskAvailable}/${result.metrics.frames.total}`,
|
||||
hint: "exact immutable full-route archive",
|
||||
},
|
||||
{
|
||||
label: "Point labels",
|
||||
value: result.metrics.points.labeled.toLocaleString("ru-RU"),
|
||||
hint: `${result.metrics.points.unprojected.toLocaleString("ru-RU")} unprojected · ${result.metrics.points.ambiguous.toLocaleString("ru-RU")} ambiguous`,
|
||||
},
|
||||
{
|
||||
label: "Observation labels",
|
||||
value: result.metrics.observations.labeled.toLocaleString("ru-RU"),
|
||||
hint: `${result.metrics.observations.ambiguous.toLocaleString("ru-RU")} ambiguous`,
|
||||
},
|
||||
{
|
||||
label: "Derivative build",
|
||||
value: `${formatNumber(result.metrics.runtime.framesPerSecond, 1)} FPS`,
|
||||
hint: `${formatNumber(result.metrics.runtime.elapsedMs / 1000, 1)} s offline`,
|
||||
},
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Одна каноническая модель-независимая форма принимает sealed semantic mask, привязывает её к исходному кадру и к factory KB4, маркирует полный frame-local point space и публикует проверяемое semantic evidence для существующих geometry observations. Текущий M4.6 при этом не изменён.",
|
||||
notProved: "Не доказаны physical hardware-sync, class accuracy, instance separation, удержание отдельных объектов, obstacle recall, перенос на другой маршрут/provider и production latency на Worker 006. Semantic evidence не имеет navigation/safety authority.",
|
||||
decision: "Оставить EoMT как контрольную ветку. Следующий честный A/B — NVIDIA CitySemSegFormer на замороженном truth-island через тот же provider contract; после ручного GT сравнивать качество, а не интерфейс или цвет overlay.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -9,11 +9,20 @@ import {
|
||||
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
|
||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||
import { RecordedEvidenceImageScene } from "../../components/laboratory/RecordedEvidenceImageScene";
|
||||
import type {
|
||||
RecordedEvidenceSemanticClass,
|
||||
RecordedEvidenceSemanticOverlay,
|
||||
RecordedEvidenceSemanticPaletteEntry,
|
||||
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
||||
import {
|
||||
RecordedEvidenceVideoScene,
|
||||
type RecordedEvidenceBox,
|
||||
} from "../../components/laboratory/RecordedEvidenceVideoScene";
|
||||
import { useRecordedEvidencePlayback } from "../../components/laboratory/useRecordedEvidencePlayback";
|
||||
import {
|
||||
e47SemanticMaskUrl,
|
||||
type E47SemanticClass,
|
||||
} from "../../core/laboratory/e47SemanticSlam";
|
||||
import type {
|
||||
M4ThreatCameraProposal,
|
||||
M4ThreatTimelineFrame,
|
||||
@@ -26,6 +35,7 @@ import {
|
||||
useM4ThreatTimelineFrame,
|
||||
useM4ThreatTimelineMetadata,
|
||||
} from "./useM4ThreatTimeline";
|
||||
import { useE47SemanticTimelineFrame } from "./useE47SemanticTimeline";
|
||||
|
||||
type M4ThreatViewMode = "video" | "camera" | LaboratoryMetricSceneMode;
|
||||
|
||||
@@ -65,12 +75,24 @@ function SpatialState({ message: text }: { message: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
||||
export interface M4ReplayThreatSemanticLayer {
|
||||
resultId: string;
|
||||
taxonomy: readonly E47SemanticClass[];
|
||||
}
|
||||
|
||||
export function M4ReplayThreatVisual({
|
||||
resultId,
|
||||
semantic,
|
||||
}: {
|
||||
resultId: string;
|
||||
semantic?: M4ReplayThreatSemanticLayer;
|
||||
}) {
|
||||
const [mode, setMode] = useState<M4ThreatViewMode>("video");
|
||||
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode>("3d");
|
||||
const [showCurrentIncrement, setShowCurrentIncrement] = useState(true);
|
||||
const [showLocalSurface, setShowLocalSurface] = useState(true);
|
||||
const [showRollingMap, setShowRollingMap] = useState(true);
|
||||
const [showSemantic, setShowSemantic] = useState(true);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
|
||||
const metadata = useM4ThreatTimelineMetadata(resultId);
|
||||
@@ -140,6 +162,12 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
||||
}, [resultId]);
|
||||
if (timelineFrame.activeFrame) lastFrameRef.current = timelineFrame.activeFrame;
|
||||
const frame = timelineFrame.activeFrame ?? lastFrameRef.current;
|
||||
const semanticTimeline = useE47SemanticTimelineFrame({
|
||||
resultId: semantic?.resultId ?? null,
|
||||
activeSequence: frame?.sequence ?? timelineFrame.activeSequence,
|
||||
frameCount: metadata.timeline?.frameCount ?? 0,
|
||||
taxonomy: semantic?.taxonomy ?? [],
|
||||
});
|
||||
const displayingBufferedFrame = Boolean(
|
||||
frame
|
||||
&& timelineFrame.activeSequence !== null
|
||||
@@ -173,6 +201,57 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
||||
pointLimit: 20_000,
|
||||
},
|
||||
), [frame, metadata.timeline, timelineFrame.availableFrames]);
|
||||
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
|
||||
() => semantic?.taxonomy.map((item) => ({
|
||||
id: item.classId,
|
||||
label: `semantic: ${item.label}`,
|
||||
})) ?? [],
|
||||
[semantic?.taxonomy],
|
||||
);
|
||||
const semanticPalette = useMemo<readonly RecordedEvidenceSemanticPaletteEntry[]>(
|
||||
() => semantic?.taxonomy.map((item) => ({
|
||||
classId: item.classId,
|
||||
color: item.disposition === "ambiguous"
|
||||
? { kind: "token" as const, token: "--nodedc-warning-rgb" as const }
|
||||
: { kind: "diagnostic" as const, rgb: item.colorRgb },
|
||||
opacity: item.disposition === "ambiguous" ? 0.22 : 0.56,
|
||||
})) ?? [],
|
||||
[semantic?.taxonomy],
|
||||
);
|
||||
const semanticFrame = semanticTimeline.activeFrame?.sequence === frame?.sequence
|
||||
? semanticTimeline.activeFrame
|
||||
: null;
|
||||
const semanticIntegrityError = semantic && frame?.spatialAvailable && semanticFrame && (
|
||||
semanticFrame.sourcePointCount !== frame.pointCloudSourceCount
|
||||
|| frame.pointCloudSampleCount !== frame.pointCloudSourceCount
|
||||
|| frame.pointCloudBodyXyzM.length !== frame.pointCloudSourceCount
|
||||
)
|
||||
? "E47 semantic point index space не совпал с exact current increment M4.6."
|
||||
: null;
|
||||
const alignedSemanticPointIds = useMemo<readonly (number | null)[] | undefined>(() => {
|
||||
if (
|
||||
!semantic
|
||||
|| !showSemantic
|
||||
|| !frame
|
||||
|| !frame.spatialAvailable
|
||||
|| !semanticFrame
|
||||
|| semanticIntegrityError
|
||||
) return undefined;
|
||||
return semanticFrame.classIds.map((classId, index) => {
|
||||
const status = semanticFrame.statusCodes[index];
|
||||
return status === 2 || status === 3 ? classId : null;
|
||||
});
|
||||
}, [frame, semantic, semanticFrame, semanticIntegrityError, showSemantic]);
|
||||
const semanticOverlay: RecordedEvidenceSemanticOverlay | undefined =
|
||||
semantic && showSemantic && frame
|
||||
? {
|
||||
src: e47SemanticMaskUrl(semantic.resultId, frame.sequence),
|
||||
classes: semanticClasses,
|
||||
palette: semanticPalette,
|
||||
opacity: 0.48,
|
||||
ariaLabel: `E47 semantic mask frame ${frame.sequence + 1}`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const seek = (seconds: number) => playbackController.seek(seconds);
|
||||
const handleModeChange = (next: M4ThreatViewMode) => {
|
||||
@@ -205,40 +284,55 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
||||
<Icon name="chevron-right" size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
{mode === "3d" || mode === "plan" ? (
|
||||
{mode === "3d" || mode === "plan" || semantic ? (
|
||||
<div
|
||||
className="nodedc-segmented m4-replay-threat-visual__layer-controls"
|
||||
role="group"
|
||||
aria-label="Слои пространственного evidence"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-segmented__item"
|
||||
data-active={showCurrentIncrement ? "true" : undefined}
|
||||
aria-pressed={showCurrentIncrement}
|
||||
onClick={() => setShowCurrentIncrement((visible) => !visible)}
|
||||
>
|
||||
CURRENT
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-segmented__item"
|
||||
data-active={showLocalSurface ? "true" : undefined}
|
||||
aria-pressed={showLocalSurface}
|
||||
title="Bounded local SLAM surface · visual-derived"
|
||||
onClick={() => setShowLocalSurface((visible) => !visible)}
|
||||
>
|
||||
LOCAL SLAM
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-segmented__item"
|
||||
data-active={showRollingMap ? "true" : undefined}
|
||||
aria-pressed={showRollingMap}
|
||||
onClick={() => setShowRollingMap((visible) => !visible)}
|
||||
>
|
||||
ROLLING
|
||||
</button>
|
||||
{mode === "3d" || mode === "plan" ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-segmented__item"
|
||||
data-active={showCurrentIncrement ? "true" : undefined}
|
||||
aria-pressed={showCurrentIncrement}
|
||||
onClick={() => setShowCurrentIncrement((visible) => !visible)}
|
||||
>
|
||||
CURRENT
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-segmented__item"
|
||||
data-active={showLocalSurface ? "true" : undefined}
|
||||
aria-pressed={showLocalSurface}
|
||||
title="Bounded local SLAM surface · visual-derived"
|
||||
onClick={() => setShowLocalSurface((visible) => !visible)}
|
||||
>
|
||||
LOCAL SLAM
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-segmented__item"
|
||||
data-active={showRollingMap ? "true" : undefined}
|
||||
aria-pressed={showRollingMap}
|
||||
onClick={() => setShowRollingMap((visible) => !visible)}
|
||||
>
|
||||
ROLLING
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
{semantic ? (
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-segmented__item"
|
||||
data-active={showSemantic ? "true" : undefined}
|
||||
aria-pressed={showSemantic}
|
||||
onClick={() => setShowSemantic((visible) => !visible)}
|
||||
>
|
||||
SEMANTICS
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -274,6 +368,9 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
||||
{frame.spatialAvailable
|
||||
? `${frame.pointCloudSampleCount}/${frame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
|
||||
: "body frame / current increment unavailable"}
|
||||
{semantic && semanticFrame
|
||||
? ` · semantic L ${semanticFrame.counts.labeled} · A ${semanticFrame.counts.ambiguous} · U ${semanticFrame.counts.unprojected} · Ø ${semanticFrame.counts.absent}`
|
||||
: semantic ? " · semantic buffer" : ""}
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
@@ -314,6 +411,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
||||
imageWidth={timeline.imageWidth}
|
||||
imageHeight={timeline.imageHeight}
|
||||
boxes={activeBoxes}
|
||||
semanticOverlay={mode === "video" ? semanticOverlay : undefined}
|
||||
ariaLabel={`M4.6 recorded-realtime frame ${frame?.sequence ?? 0}: ${activeBoxes.length} proposals`}
|
||||
interactive={false}
|
||||
/>
|
||||
@@ -337,6 +435,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
||||
imageWidth={timeline.imageWidth}
|
||||
imageHeight={timeline.imageHeight}
|
||||
boxes={activeBoxes}
|
||||
semanticOverlay={mode === "camera" ? semanticOverlay : undefined}
|
||||
ariaLabel={`M4.6 exact camera frame ${frame.sequence}: ${activeBoxes.length} proposals`}
|
||||
/>
|
||||
) : null}
|
||||
@@ -360,6 +459,9 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
||||
showCurrentIncrement={showCurrentIncrement}
|
||||
showLocalSurface={showLocalSurface}
|
||||
showRollingMap={showRollingMap}
|
||||
pointSemanticClassIds={alignedSemanticPointIds}
|
||||
semanticClasses={semanticClasses}
|
||||
semanticPalette={semanticPalette}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -375,6 +477,24 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
||||
<span>{timelineFrame.error}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{semantic && semanticTimeline.loading ? (
|
||||
<div className="m4-replay-threat-visual__buffering" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
<span>Догружаем semantic-point evidence E47</span>
|
||||
</div>
|
||||
) : null}
|
||||
{semanticTimeline.error ? (
|
||||
<div className="m4-replay-threat-visual__buffering" role="alert">
|
||||
<Icon name="alert" size={16} />
|
||||
<span>{semanticTimeline.error}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{semanticIntegrityError ? (
|
||||
<div className="m4-replay-threat-visual__buffering" role="alert">
|
||||
<Icon name="alert" size={16} />
|
||||
<span>{semanticIntegrityError}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{frame && !frame.spatialAvailable && (mode === "3d" || mode === "plan") ? (
|
||||
<div className="m4-replay-threat-visual__buffering" role="status">
|
||||
На этом кадре нет квалифицированного body frame; сцена сохранена.
|
||||
@@ -409,7 +529,9 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
|
||||
return (
|
||||
<div className="l3-visual-audit m4-replay-threat-visual">
|
||||
<LaboratoryEvidenceViewer
|
||||
label="M4.6 dual-evidence recorded-realtime replay"
|
||||
label={semantic
|
||||
? "E47 semantic + SLAM diagnostic replay"
|
||||
: "M4.6 dual-evidence recorded-realtime replay"}
|
||||
className="m4-replay-threat-evidence-viewer"
|
||||
mode={mode}
|
||||
modes={[
|
||||
|
||||
@@ -64,6 +64,13 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
|
||||
experimentName: "RAVNOVES00 dual-evidence threat qualification",
|
||||
variantName: "M4.6 · virtual corridor replay · VIDEO/CAMERA/3D",
|
||||
},
|
||||
"e47-semantic-slam-shadow": {
|
||||
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`,
|
||||
experimentId: "ravnoves00-semantic-slam-shadow-r1",
|
||||
experimentName: "RAVNOVES00 semantic mask → KB4 → SLAM diagnostic shadow",
|
||||
variantName: "E47 · EoMT control · full semantic point projection",
|
||||
},
|
||||
"e28-local-surface": {
|
||||
profileId: "rig-camera-local-surface-v1",
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera-first + local-surface LiDAR`,
|
||||
|
||||
@@ -43,6 +43,7 @@ function mergeResults(
|
||||
e46h: next.e46h ?? current.e46h,
|
||||
e46i: next.e46i ?? current.e46i,
|
||||
e46j: next.e46j ?? current.e46j,
|
||||
e47: next.e47 ?? current.e47,
|
||||
l34: next.l34 ?? current.l34,
|
||||
l34a: next.l34a ?? current.l34a,
|
||||
l34b: next.l34b ?? current.l34b,
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
fetchE47SemanticTimelineChunk,
|
||||
type E47SemanticClass,
|
||||
type E47SemanticTimelineChunk,
|
||||
type E47SemanticTimelineFrame,
|
||||
} from "../../core/laboratory/e47SemanticSlam";
|
||||
|
||||
const CHUNK_SIZE = 24;
|
||||
const RETAINED_CHUNK_COUNT = 8;
|
||||
const PREFETCH_CHUNKS_AHEAD = 2;
|
||||
|
||||
function chunkWindowStarts(activeStart: number, frameCount: number): readonly number[] {
|
||||
return Array.from(
|
||||
{ length: PREFETCH_CHUNKS_AHEAD + 2 },
|
||||
(_, index) => activeStart + (index - 1) * CHUNK_SIZE,
|
||||
).filter((start) => start >= 0 && start < frameCount);
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
: "Semantic point evidence E47 недоступен.";
|
||||
}
|
||||
|
||||
export function useE47SemanticTimelineFrame({
|
||||
resultId,
|
||||
activeSequence,
|
||||
frameCount,
|
||||
taxonomy,
|
||||
}: {
|
||||
resultId: string | null;
|
||||
activeSequence: number | null;
|
||||
frameCount: number;
|
||||
taxonomy: readonly E47SemanticClass[];
|
||||
}) {
|
||||
const [chunks, setChunks] = useState<ReadonlyMap<number, E47SemanticTimelineChunk>>(
|
||||
() => new Map(),
|
||||
);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const chunksRef = useRef(chunks);
|
||||
const inFlight = useRef(new Map<number, AbortController>());
|
||||
const activeStartRef = useRef<number | null>(null);
|
||||
chunksRef.current = chunks;
|
||||
|
||||
useEffect(() => {
|
||||
for (const controller of inFlight.current.values()) controller.abort();
|
||||
inFlight.current.clear();
|
||||
const empty = new Map<number, E47SemanticTimelineChunk>();
|
||||
chunksRef.current = empty;
|
||||
setChunks(empty);
|
||||
setError(null);
|
||||
return () => {
|
||||
for (const controller of inFlight.current.values()) controller.abort();
|
||||
inFlight.current.clear();
|
||||
};
|
||||
}, [resultId]);
|
||||
|
||||
const activeStart = activeSequence === null
|
||||
? null
|
||||
: Math.floor(activeSequence / CHUNK_SIZE) * CHUNK_SIZE;
|
||||
activeStartRef.current = activeStart;
|
||||
|
||||
useEffect(() => {
|
||||
if (!resultId || activeStart === null || frameCount < 1) return;
|
||||
for (const start of chunkWindowStarts(activeStart, frameCount)) {
|
||||
if (chunksRef.current.has(start) || inFlight.current.has(start)) continue;
|
||||
const controller = new AbortController();
|
||||
inFlight.current.set(start, controller);
|
||||
void fetchE47SemanticTimelineChunk(resultId, start, CHUNK_SIZE, {
|
||||
signal: controller.signal,
|
||||
taxonomy,
|
||||
})
|
||||
.then((chunk) => {
|
||||
if (controller.signal.aborted) return;
|
||||
setChunks((current) => {
|
||||
const next = new Map(current);
|
||||
next.set(start, chunk);
|
||||
const center = activeStartRef.current ?? start;
|
||||
const retained = [...next.keys()]
|
||||
.sort((left, right) => Math.abs(left - center) - Math.abs(right - center))
|
||||
.slice(0, RETAINED_CHUNK_COUNT);
|
||||
const bounded = new Map(retained.map((key) => [key, next.get(key)!]));
|
||||
chunksRef.current = bounded;
|
||||
return bounded;
|
||||
});
|
||||
if (start === activeStartRef.current) setError(null);
|
||||
})
|
||||
.catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted && start === activeStartRef.current) {
|
||||
setError(errorMessage(caught));
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (inFlight.current.get(start) === controller) inFlight.current.delete(start);
|
||||
});
|
||||
}
|
||||
}, [activeStart, frameCount, resultId, taxonomy]);
|
||||
|
||||
const activeFrame: E47SemanticTimelineFrame | null = useMemo(() => {
|
||||
if (activeSequence === null || activeStart === null) return null;
|
||||
return chunks.get(activeStart)?.frames.find(
|
||||
(frame) => frame.sequence === activeSequence,
|
||||
) ?? null;
|
||||
}, [activeSequence, activeStart, chunks]);
|
||||
|
||||
return {
|
||||
activeFrame,
|
||||
loading: Boolean(resultId) && activeSequence !== null && !activeFrame && !error,
|
||||
error,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user