feat(lab): visualize semantic SLAM shadow
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
forwardRef,
|
||||
type CSSProperties,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
@@ -8,6 +9,13 @@ import {
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||||
|
||||
import {
|
||||
recordedEvidenceSemanticCssColor,
|
||||
resolveRecordedEvidenceSemanticRgb,
|
||||
type RecordedEvidenceSemanticClass,
|
||||
type RecordedEvidenceSemanticPaletteEntry,
|
||||
} from "./RecordedEvidenceSemanticMaskOverlay";
|
||||
|
||||
export type LaboratoryMetricPoint3 = readonly [number, number, number];
|
||||
export type LaboratoryMetricDecision = "threat" | "not-threat" | "unknown";
|
||||
export type LaboratoryMetricSceneMode = "3d" | "plan";
|
||||
@@ -114,6 +122,9 @@ LaboratoryMetricEvidenceSceneHandle,
|
||||
showCurrentIncrement: boolean;
|
||||
showLocalSurface: boolean;
|
||||
showRollingMap: boolean;
|
||||
pointSemanticClassIds?: readonly (number | null)[];
|
||||
semanticClasses?: readonly RecordedEvidenceSemanticClass[];
|
||||
semanticPalette?: readonly RecordedEvidenceSemanticPaletteEntry[];
|
||||
}
|
||||
>(function LaboratoryMetricEvidenceScene({
|
||||
pointCloudBodyXyzM,
|
||||
@@ -127,6 +138,9 @@ LaboratoryMetricEvidenceSceneHandle,
|
||||
showCurrentIncrement,
|
||||
showLocalSurface,
|
||||
showRollingMap,
|
||||
pointSemanticClassIds,
|
||||
semanticClasses,
|
||||
semanticPalette,
|
||||
}, ref) {
|
||||
const hostRef = useRef<HTMLDivElement | null>(null);
|
||||
const sceneRef = useRef<THREE.Scene | null>(null);
|
||||
@@ -240,10 +254,40 @@ LaboratoryMetricEvidenceSceneHandle,
|
||||
"position",
|
||||
new THREE.BufferAttribute(positions(pointCloudBodyXyzM), 3),
|
||||
);
|
||||
const hasAlignedSemanticClasses =
|
||||
pointSemanticClassIds !== undefined
|
||||
&& pointSemanticClassIds.length === pointCloudBodyXyzM.length
|
||||
&& semanticClasses !== undefined
|
||||
&& semanticPalette !== undefined;
|
||||
if (hasAlignedSemanticClasses) {
|
||||
const declaredIds = new Set(semanticClasses.map((item) => item.id));
|
||||
const colorsByClassId = new Map<number, readonly [number, number, number]>();
|
||||
for (const entry of semanticPalette) {
|
||||
if (!declaredIds.has(entry.classId)) continue;
|
||||
const rgb = resolveRecordedEvidenceSemanticRgb(host, entry.color);
|
||||
if (rgb) colorsByClassId.set(entry.classId, rgb);
|
||||
}
|
||||
const context = tokenColor(host, "--nodedc-text-muted", [147, 151, 159]);
|
||||
const pointColors = new Float32Array(pointCloudBodyXyzM.length * 3);
|
||||
pointSemanticClassIds.forEach((classId, index) => {
|
||||
const rgb = classId === null ? undefined : colorsByClassId.get(classId);
|
||||
const offset = index * 3;
|
||||
pointColors[offset] = rgb ? rgb[0] / 255 : context.r;
|
||||
pointColors[offset + 1] = rgb ? rgb[1] / 255 : context.g;
|
||||
pointColors[offset + 2] = rgb ? rgb[2] / 255 : context.b;
|
||||
});
|
||||
contextGeometry.setAttribute(
|
||||
"color",
|
||||
new THREE.BufferAttribute(pointColors, 3),
|
||||
);
|
||||
}
|
||||
content.add(new THREE.Points(
|
||||
contextGeometry,
|
||||
new THREE.PointsMaterial({
|
||||
color: tokenColor(host, "--nodedc-text-muted", [147, 151, 159]),
|
||||
color: hasAlignedSemanticClasses
|
||||
? new THREE.Color(1, 1, 1)
|
||||
: tokenColor(host, "--nodedc-text-muted", [147, 151, 159]),
|
||||
vertexColors: hasAlignedSemanticClasses,
|
||||
size: 1.55,
|
||||
sizeAttenuation: false,
|
||||
transparent: true,
|
||||
@@ -321,6 +365,9 @@ LaboratoryMetricEvidenceSceneHandle,
|
||||
occupiedVoxelSizeM,
|
||||
localSurfaceBodyXyzM,
|
||||
pointCloudBodyXyzM,
|
||||
pointSemanticClassIds,
|
||||
semanticClasses,
|
||||
semanticPalette,
|
||||
showCurrentIncrement,
|
||||
showLocalSurface,
|
||||
showRollingMap,
|
||||
@@ -411,6 +458,27 @@ LaboratoryMetricEvidenceSceneHandle,
|
||||
useEffect(resetView, [corridor.forwardLengthM, mode]);
|
||||
useImperativeHandle(ref, () => ({ resetView }));
|
||||
|
||||
const semanticLegendEntries = (() => {
|
||||
if (
|
||||
!showCurrentIncrement
|
||||
|| !pointSemanticClassIds
|
||||
|| pointSemanticClassIds.length !== pointCloudBodyXyzM.length
|
||||
|| !semanticClasses
|
||||
|| !semanticPalette
|
||||
) return [];
|
||||
const presentIds = new Set(pointSemanticClassIds.filter((item): item is number => item !== null));
|
||||
const classesById = new Map(semanticClasses.map((item) => [item.id, item]));
|
||||
return semanticPalette.flatMap((entry) => {
|
||||
const semanticClass = classesById.get(entry.classId);
|
||||
if (!semanticClass || !presentIds.has(entry.classId) || entry.color.kind === "transparent") return [];
|
||||
return [{
|
||||
id: entry.classId,
|
||||
label: semanticClass.label,
|
||||
cssColor: recordedEvidenceSemanticCssColor(entry.color),
|
||||
}];
|
||||
});
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="laboratory-metric-evidence-scene">
|
||||
<div ref={hostRef} className="laboratory-metric-evidence-scene__viewport">
|
||||
@@ -423,6 +491,15 @@ LaboratoryMetricEvidenceSceneHandle,
|
||||
<span data-decision="context">Current increment</span>
|
||||
<span data-decision="local-surface">Local SLAM surface</span>
|
||||
<span data-decision="rolling">Rolling-map occupied</span>
|
||||
{semanticLegendEntries.map((entry) => (
|
||||
<span
|
||||
key={entry.id}
|
||||
data-decision="semantic"
|
||||
style={{ "--laboratory-metric-legend-color": entry.cssColor } as CSSProperties}
|
||||
>
|
||||
{entry.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,18 +5,24 @@ import {
|
||||
RecordedEvidenceBoxOverlay,
|
||||
type RecordedEvidenceBox,
|
||||
} from "./RecordedEvidenceBoxOverlay";
|
||||
import {
|
||||
RecordedEvidenceSemanticMaskOverlay,
|
||||
type RecordedEvidenceSemanticOverlay,
|
||||
} from "./RecordedEvidenceSemanticMaskOverlay";
|
||||
|
||||
export function RecordedEvidenceImageScene({
|
||||
src,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
boxes,
|
||||
semanticOverlay,
|
||||
ariaLabel,
|
||||
}: {
|
||||
src: string;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
boxes: readonly RecordedEvidenceBox[];
|
||||
semanticOverlay?: RecordedEvidenceSemanticOverlay;
|
||||
ariaLabel: string;
|
||||
}) {
|
||||
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
|
||||
@@ -33,12 +39,21 @@ export function RecordedEvidenceImageScene({
|
||||
onError={() => setState("error")}
|
||||
/>
|
||||
{state === "ready" ? (
|
||||
<RecordedEvidenceBoxOverlay
|
||||
imageWidth={imageWidth}
|
||||
imageHeight={imageHeight}
|
||||
boxes={boxes}
|
||||
ariaLabel={ariaLabel}
|
||||
/>
|
||||
<>
|
||||
{semanticOverlay ? (
|
||||
<RecordedEvidenceSemanticMaskOverlay
|
||||
{...semanticOverlay}
|
||||
imageWidth={imageWidth}
|
||||
imageHeight={imageHeight}
|
||||
/>
|
||||
) : null}
|
||||
<RecordedEvidenceBoxOverlay
|
||||
imageWidth={imageWidth}
|
||||
imageHeight={imageHeight}
|
||||
boxes={boxes}
|
||||
ariaLabel={ariaLabel}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="l3-visual-audit__state" role="status">
|
||||
{state === "loading" ? (
|
||||
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export interface RecordedEvidenceSemanticClass {
|
||||
id: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export type RecordedEvidenceSemanticToken =
|
||||
| "--nodedc-accent-rgb"
|
||||
| "--nodedc-danger-rgb"
|
||||
| "--nodedc-foreground-rgb"
|
||||
| "--nodedc-success-rgb"
|
||||
| "--nodedc-text-muted"
|
||||
| "--nodedc-warning-rgb";
|
||||
|
||||
export type RecordedEvidenceSemanticColor =
|
||||
| {
|
||||
kind: "token";
|
||||
token: RecordedEvidenceSemanticToken;
|
||||
}
|
||||
| {
|
||||
kind: "diagnostic";
|
||||
rgb: readonly [number, number, number];
|
||||
}
|
||||
| {
|
||||
kind: "transparent";
|
||||
};
|
||||
|
||||
export interface RecordedEvidenceSemanticPaletteEntry {
|
||||
classId: number;
|
||||
color: RecordedEvidenceSemanticColor;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
export interface RecordedEvidenceSemanticOverlay {
|
||||
src: string;
|
||||
classes: readonly RecordedEvidenceSemanticClass[];
|
||||
palette: readonly RecordedEvidenceSemanticPaletteEntry[];
|
||||
opacity?: number;
|
||||
ariaLabel: string;
|
||||
}
|
||||
|
||||
interface DecodedSemanticMask {
|
||||
key: string;
|
||||
width: number;
|
||||
height: number;
|
||||
classIds: Uint8Array;
|
||||
}
|
||||
|
||||
interface PendingSemanticMask {
|
||||
controller: AbortController;
|
||||
subscribers: number;
|
||||
promise: Promise<DecodedSemanticMask>;
|
||||
}
|
||||
|
||||
const MASK_CACHE_LIMIT = 48;
|
||||
const decodedMaskCache = new Map<string, DecodedSemanticMask>();
|
||||
const pendingMaskCache = new Map<string, PendingSemanticMask>();
|
||||
|
||||
const TOKEN_FALLBACKS: Record<RecordedEvidenceSemanticToken, readonly [number, number, number]> = {
|
||||
"--nodedc-accent-rgb": [232, 56, 126],
|
||||
"--nodedc-danger-rgb": [255, 104, 112],
|
||||
"--nodedc-foreground-rgb": [245, 245, 245],
|
||||
"--nodedc-success-rgb": [181, 255, 90],
|
||||
"--nodedc-text-muted": [147, 151, 159],
|
||||
"--nodedc-warning-rgb": [255, 197, 92],
|
||||
};
|
||||
|
||||
function clampChannel(value: number): number {
|
||||
return Math.max(0, Math.min(255, Math.round(value)));
|
||||
}
|
||||
|
||||
function clampOpacity(value: number | undefined, fallback: number): number {
|
||||
return Math.max(0, Math.min(1, Number.isFinite(value) ? Number(value) : fallback));
|
||||
}
|
||||
|
||||
function semanticMaskKey(src: string, width: number, height: number): string {
|
||||
return `${width}x${height}:${src}`;
|
||||
}
|
||||
|
||||
function rememberMask(mask: DecodedSemanticMask): void {
|
||||
decodedMaskCache.delete(mask.key);
|
||||
decodedMaskCache.set(mask.key, mask);
|
||||
while (decodedMaskCache.size > MASK_CACHE_LIMIT) {
|
||||
const oldest = decodedMaskCache.keys().next().value;
|
||||
if (typeof oldest !== "string") break;
|
||||
decodedMaskCache.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
async function imageSourceFromBlob(
|
||||
blob: Blob,
|
||||
signal: AbortSignal,
|
||||
): Promise<{ source: CanvasImageSource; width: number; height: number; release: () => void }> {
|
||||
if (typeof createImageBitmap === "function") {
|
||||
const bitmap = await createImageBitmap(blob, {
|
||||
colorSpaceConversion: "none",
|
||||
premultiplyAlpha: "none",
|
||||
});
|
||||
if (signal.aborted) {
|
||||
bitmap.close();
|
||||
throw new DOMException("Aborted", "AbortError");
|
||||
}
|
||||
return {
|
||||
source: bitmap,
|
||||
width: bitmap.width,
|
||||
height: bitmap.height,
|
||||
release: () => bitmap.close(),
|
||||
};
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const image = new Image();
|
||||
image.decoding = "async";
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
image.removeEventListener("load", onLoad);
|
||||
image.removeEventListener("error", onError);
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
};
|
||||
const onLoad = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const onError = () => {
|
||||
cleanup();
|
||||
reject(new Error("Semantic mask image decode failed"));
|
||||
};
|
||||
const onAbort = () => {
|
||||
cleanup();
|
||||
reject(new DOMException("Aborted", "AbortError"));
|
||||
};
|
||||
image.addEventListener("load", onLoad, { once: true });
|
||||
image.addEventListener("error", onError, { once: true });
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
image.src = objectUrl;
|
||||
});
|
||||
return {
|
||||
source: image,
|
||||
width: image.naturalWidth,
|
||||
height: image.naturalHeight,
|
||||
release: () => URL.revokeObjectURL(objectUrl),
|
||||
};
|
||||
} catch (error) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function decodeSemanticMask(
|
||||
key: string,
|
||||
src: string,
|
||||
expectedWidth: number,
|
||||
expectedHeight: number,
|
||||
signal: AbortSignal,
|
||||
): Promise<DecodedSemanticMask> {
|
||||
const response = await fetch(src, { cache: "force-cache", signal });
|
||||
if (!response.ok) throw new Error(`Semantic mask request failed: ${response.status}`);
|
||||
const blob = await response.blob();
|
||||
if (signal.aborted) throw new DOMException("Aborted", "AbortError");
|
||||
const decoded = await imageSourceFromBlob(blob, signal);
|
||||
try {
|
||||
if (decoded.width !== expectedWidth || decoded.height !== expectedHeight) {
|
||||
throw new Error(
|
||||
`Semantic mask dimensions ${decoded.width}x${decoded.height} do not match ${expectedWidth}x${expectedHeight}`,
|
||||
);
|
||||
}
|
||||
const decodeCanvas = document.createElement("canvas");
|
||||
decodeCanvas.width = decoded.width;
|
||||
decodeCanvas.height = decoded.height;
|
||||
const context = decodeCanvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!context) throw new Error("Semantic mask canvas is unavailable");
|
||||
context.drawImage(decoded.source, 0, 0);
|
||||
const rgba = context.getImageData(0, 0, decoded.width, decoded.height).data;
|
||||
const classIds = new Uint8Array(decoded.width * decoded.height);
|
||||
for (let sourceOffset = 0, targetOffset = 0; targetOffset < classIds.length; sourceOffset += 4, targetOffset += 1) {
|
||||
const classId = rgba[sourceOffset] ?? 0;
|
||||
if (rgba[sourceOffset + 1] !== classId || rgba[sourceOffset + 2] !== classId) {
|
||||
throw new Error("Semantic mask must be an 8-bit grayscale class-id PNG");
|
||||
}
|
||||
classIds[targetOffset] = classId;
|
||||
}
|
||||
return { key, width: decoded.width, height: decoded.height, classIds };
|
||||
} finally {
|
||||
decoded.release();
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToSemanticMask(
|
||||
src: string,
|
||||
width: number,
|
||||
height: number,
|
||||
): { promise: Promise<DecodedSemanticMask>; release: () => void } {
|
||||
const key = semanticMaskKey(src, width, height);
|
||||
const cached = decodedMaskCache.get(key);
|
||||
if (cached) {
|
||||
decodedMaskCache.delete(key);
|
||||
decodedMaskCache.set(key, cached);
|
||||
return { promise: Promise.resolve(cached), release: () => undefined };
|
||||
}
|
||||
|
||||
let pending = pendingMaskCache.get(key);
|
||||
if (!pending) {
|
||||
const controller = new AbortController();
|
||||
let next: PendingSemanticMask;
|
||||
const promise = decodeSemanticMask(key, src, width, height, controller.signal)
|
||||
.then((mask) => {
|
||||
rememberMask(mask);
|
||||
return mask;
|
||||
})
|
||||
.finally(() => {
|
||||
if (pendingMaskCache.get(key) === next) pendingMaskCache.delete(key);
|
||||
});
|
||||
next = { controller, subscribers: 0, promise };
|
||||
pendingMaskCache.set(key, next);
|
||||
pending = next;
|
||||
}
|
||||
pending.subscribers += 1;
|
||||
let released = false;
|
||||
return {
|
||||
promise: pending.promise,
|
||||
release: () => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
pending!.subscribers -= 1;
|
||||
if (pending!.subscribers > 0 || decodedMaskCache.has(key)) return;
|
||||
pending!.controller.abort();
|
||||
if (pendingMaskCache.get(key) === pending) pendingMaskCache.delete(key);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function recordedEvidenceSemanticCssColor(
|
||||
color: RecordedEvidenceSemanticColor,
|
||||
): string {
|
||||
if (color.kind === "transparent") return "transparent";
|
||||
if (color.kind === "token") return `rgb(var(${color.token}))`;
|
||||
const [red, green, blue] = color.rgb.map(clampChannel);
|
||||
return `rgb(${red} ${green} ${blue})`;
|
||||
}
|
||||
|
||||
export function resolveRecordedEvidenceSemanticRgb(
|
||||
host: HTMLElement,
|
||||
color: RecordedEvidenceSemanticColor,
|
||||
): readonly [number, number, number] | null {
|
||||
if (color.kind === "transparent") return null;
|
||||
if (color.kind === "diagnostic") return color.rgb.map(clampChannel) as [number, number, number];
|
||||
const channels = getComputedStyle(host)
|
||||
.getPropertyValue(color.token)
|
||||
.trim()
|
||||
.match(/[\d.]+/g)
|
||||
?.slice(0, 3)
|
||||
.map(Number);
|
||||
return channels?.length === 3
|
||||
? channels.map(clampChannel) as [number, number, number]
|
||||
: TOKEN_FALLBACKS[color.token];
|
||||
}
|
||||
|
||||
export function RecordedEvidenceSemanticMaskOverlay({
|
||||
src,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
classes,
|
||||
palette,
|
||||
opacity = 0.46,
|
||||
ariaLabel,
|
||||
}: RecordedEvidenceSemanticOverlay & {
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const [mask, setMask] = useState<DecodedSemanticMask | null>(null);
|
||||
const [failure, setFailure] = useState<string | null>(null);
|
||||
const expectedKey = semanticMaskKey(src, imageWidth, imageHeight);
|
||||
const renderMask = failure
|
||||
? null
|
||||
: mask?.key === expectedKey
|
||||
? mask
|
||||
: decodedMaskCache.get(expectedKey) ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
setFailure(null);
|
||||
const subscription = subscribeToSemanticMask(src, imageWidth, imageHeight);
|
||||
let current = true;
|
||||
void subscription.promise.then((decoded) => {
|
||||
if (!current || decoded.key !== expectedKey) return;
|
||||
const declaredClassIds = new Set(classes.map((item) => item.id));
|
||||
const undeclaredClassId = decoded.classIds.find(
|
||||
(classId) => !declaredClassIds.has(classId),
|
||||
);
|
||||
if (undeclaredClassId !== undefined) {
|
||||
setFailure(`Semantic mask содержит необъявленный class ID ${undeclaredClassId}.`);
|
||||
return;
|
||||
}
|
||||
setMask(decoded);
|
||||
}).catch((error: unknown) => {
|
||||
if (!current || (error instanceof DOMException && error.name === "AbortError")) return;
|
||||
setFailure("Semantic mask не прошла проверку или декодирование.");
|
||||
});
|
||||
return () => {
|
||||
current = false;
|
||||
subscription.release();
|
||||
};
|
||||
}, [classes, expectedKey, imageHeight, imageWidth, src]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const host = canvas?.parentElement;
|
||||
if (!canvas || !host) return;
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) return;
|
||||
|
||||
const render = () => {
|
||||
const width = Math.max(host.clientWidth, 1);
|
||||
const height = Math.max(host.clientHeight, 1);
|
||||
const pixelRatio = Math.min(window.devicePixelRatio, 1.5);
|
||||
canvas.width = Math.round(width * pixelRatio);
|
||||
canvas.height = Math.round(height * pixelRatio);
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
context.clearRect(0, 0, width, height);
|
||||
if (!renderMask) return;
|
||||
|
||||
const declaredClassIds = new Set(
|
||||
classes
|
||||
.map((item) => item.id)
|
||||
.filter((classId) => Number.isInteger(classId) && classId >= 0 && classId <= 255),
|
||||
);
|
||||
const resolvedPalette = new Map<number, { rgb: readonly [number, number, number]; alpha: number }>();
|
||||
for (const entry of palette) {
|
||||
if (!declaredClassIds.has(entry.classId)) continue;
|
||||
const rgb = resolveRecordedEvidenceSemanticRgb(host, entry.color);
|
||||
if (!rgb) continue;
|
||||
resolvedPalette.set(entry.classId, {
|
||||
rgb,
|
||||
alpha: clampOpacity(entry.opacity, 1) * clampOpacity(opacity, 0.46),
|
||||
});
|
||||
}
|
||||
|
||||
const colorCanvas = document.createElement("canvas");
|
||||
colorCanvas.width = renderMask.width;
|
||||
colorCanvas.height = renderMask.height;
|
||||
const colorContext = colorCanvas.getContext("2d");
|
||||
if (!colorContext) return;
|
||||
const imageData = colorContext.createImageData(renderMask.width, renderMask.height);
|
||||
for (let sourceOffset = 0, targetOffset = 0; sourceOffset < renderMask.classIds.length; sourceOffset += 1, targetOffset += 4) {
|
||||
const color = resolvedPalette.get(renderMask.classIds[sourceOffset] ?? -1);
|
||||
if (!color) continue;
|
||||
imageData.data[targetOffset] = color.rgb[0];
|
||||
imageData.data[targetOffset + 1] = color.rgb[1];
|
||||
imageData.data[targetOffset + 2] = color.rgb[2];
|
||||
imageData.data[targetOffset + 3] = Math.round(color.alpha * 255);
|
||||
}
|
||||
colorContext.putImageData(imageData, 0, 0);
|
||||
|
||||
const scale = Math.min(width / imageWidth, height / imageHeight);
|
||||
const drawWidth = imageWidth * scale;
|
||||
const drawHeight = imageHeight * scale;
|
||||
const offsetX = (width - drawWidth) / 2;
|
||||
const offsetY = (height - drawHeight) / 2;
|
||||
context.imageSmoothingEnabled = false;
|
||||
context.drawImage(colorCanvas, offsetX, offsetY, drawWidth, drawHeight);
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(render);
|
||||
observer.observe(host);
|
||||
render();
|
||||
return () => observer.disconnect();
|
||||
}, [classes, expectedKey, imageHeight, imageWidth, opacity, palette, renderMask]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="recorded-evidence-semantic-mask-overlay"
|
||||
role="img"
|
||||
aria-label={ariaLabel}
|
||||
aria-busy={!failure && !renderMask}
|
||||
data-state={failure ? "error" : renderMask ? "ready" : "loading"}
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
{failure ? (
|
||||
<div className="recorded-evidence-semantic-mask-overlay__error" role="alert">
|
||||
{failure}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
type RecordedEvidenceBox,
|
||||
type RecordedEvidenceBoxTone,
|
||||
} from "./RecordedEvidenceBoxOverlay";
|
||||
import {
|
||||
RecordedEvidenceSemanticMaskOverlay,
|
||||
type RecordedEvidenceSemanticOverlay,
|
||||
} from "./RecordedEvidenceSemanticMaskOverlay";
|
||||
|
||||
export type { RecordedEvidenceBox, RecordedEvidenceBoxTone };
|
||||
|
||||
@@ -17,6 +21,7 @@ export function RecordedEvidenceVideoScene({
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
boxes,
|
||||
semanticOverlay,
|
||||
ariaLabel,
|
||||
interactive = true,
|
||||
onPlaybackChange,
|
||||
@@ -26,6 +31,7 @@ export function RecordedEvidenceVideoScene({
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
boxes: readonly RecordedEvidenceBox[];
|
||||
semanticOverlay?: RecordedEvidenceSemanticOverlay;
|
||||
ariaLabel: string;
|
||||
interactive?: boolean;
|
||||
onPlaybackChange?: (playback: RecordedObservationPlayback) => void;
|
||||
@@ -39,6 +45,13 @@ export function RecordedEvidenceVideoScene({
|
||||
prepare
|
||||
onPlaybackChange={onPlaybackChange}
|
||||
/>
|
||||
{semanticOverlay ? (
|
||||
<RecordedEvidenceSemanticMaskOverlay
|
||||
{...semanticOverlay}
|
||||
imageWidth={imageWidth}
|
||||
imageHeight={imageHeight}
|
||||
/>
|
||||
) : null}
|
||||
<RecordedEvidenceBoxOverlay
|
||||
imageWidth={imageWidth}
|
||||
imageHeight={imageHeight}
|
||||
|
||||
@@ -35,6 +35,7 @@ import { fetchE46GRectifiedDetectorBakeoff } from "./e46gRectifiedDetectorBakeof
|
||||
import { fetchE46HFullRectifiedFrontReplay } from "./e46hFullRectifiedFrontReplay";
|
||||
import { fetchE46IGroundingDinoFullReplay } from "./e46iGroundingDinoFullReplay";
|
||||
import { fetchE46JRawFisheyeRealtime } from "./e46jRawFisheyeRealtime";
|
||||
import { fetchE47SemanticSlamResult } from "./e47SemanticSlam";
|
||||
import { fetchM4ThreatReplayResult } from "./m4ReplayThreat";
|
||||
|
||||
export type AdvancedLaboratoryWorkId =
|
||||
@@ -63,6 +64,7 @@ export type AdvancedLaboratoryWorkId =
|
||||
| "e46h-full-rectified-front-replay"
|
||||
| "e46i-grounding-dino-full-replay"
|
||||
| "e46j-raw-fisheye-realtime"
|
||||
| "e47-semantic-slam-shadow"
|
||||
| "l34-right-yolox-truth-island-freeze"
|
||||
| "l34a-assisted-yolox-error-audit"
|
||||
| "l34b-nested-box-consolidation-shadow"
|
||||
@@ -103,6 +105,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
||||
"e46h-full-rectified-front-replay",
|
||||
"e46i-grounding-dino-full-replay",
|
||||
"e46j-raw-fisheye-realtime",
|
||||
"e47-semantic-slam-shadow",
|
||||
"l34-right-yolox-truth-island-freeze",
|
||||
"l34a-assisted-yolox-error-audit",
|
||||
"l34b-nested-box-consolidation-shadow",
|
||||
@@ -138,6 +141,7 @@ const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
|
||||
"e46h-full-rectified-front-replay": "e46h-full-rectified-front-replay",
|
||||
"e46i-grounding-dino-full-replay": "e46i-grounding-dino-full-replay",
|
||||
"e46j-raw-fisheye-realtime": "e46j-raw-fisheye-realtime",
|
||||
"e47-semantic-slam-shadow": "e47-semantic-slam",
|
||||
"l34-right-yolox-truth-island-freeze": "l34-right-yolox-truth-island-freeze",
|
||||
"l34a-assisted-yolox-error-audit": "l34a-assisted-yolox-error-audit",
|
||||
"l34b-nested-box-consolidation-shadow": "l34b-nested-box-consolidation-shadow",
|
||||
@@ -180,6 +184,7 @@ export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
|
||||
e46h: null,
|
||||
e46i: null,
|
||||
e46j: null,
|
||||
e47: null,
|
||||
l34: null,
|
||||
l34a: null,
|
||||
l34b: null,
|
||||
@@ -303,6 +308,7 @@ export function advancedLaboratoryResultAvailable(
|
||||
: workId === "e46h-full-rectified-front-replay" ? results.e46h !== null
|
||||
: workId === "e46i-grounding-dino-full-replay" ? results.e46i !== null
|
||||
: workId === "e46j-raw-fisheye-realtime" ? results.e46j !== null
|
||||
: workId === "e47-semantic-slam-shadow" ? results.e47 !== null
|
||||
: workId === "l34-right-yolox-truth-island-freeze" ? results.l34 !== null
|
||||
: workId === "l34a-assisted-yolox-error-audit" ? results.l34a !== null
|
||||
: workId === "l34b-nested-box-consolidation-shadow" ? results.l34b !== null
|
||||
@@ -403,6 +409,8 @@ export async function fetchAdvancedLaboratoryResult(
|
||||
results.e46i = await fetchE46IGroundingDinoFullReplay({ fetcher, signal });
|
||||
} else if (workId === "e46j-raw-fisheye-realtime") {
|
||||
results.e46j = await fetchE46JRawFisheyeRealtime({ fetcher, signal });
|
||||
} else if (workId === "e47-semantic-slam-shadow") {
|
||||
results.e47 = await fetchE47SemanticSlamResult({ fetcher, signal });
|
||||
} else if (workId === "l34-right-yolox-truth-island-freeze") {
|
||||
results.l34 = await fetchL34RightYoloxTruthIsland({ fetcher, signal });
|
||||
} else if (workId === "l34a-assisted-yolox-error-audit") {
|
||||
|
||||
@@ -31,6 +31,7 @@ import type { E46GRectifiedDetectorBakeoffResult } from "./e46gRectifiedDetector
|
||||
import type { E46HFullRectifiedFrontReplayResult } from "./e46hFullRectifiedFrontReplay";
|
||||
import type { E46IGroundingDinoFullReplayResult } from "./e46iGroundingDinoFullReplay";
|
||||
import type { E46JRawFisheyeRealtimeResult } from "./e46jRawFisheyeRealtime";
|
||||
import type { E47SemanticSlamResult } from "./e47SemanticSlam";
|
||||
import type { M4ThreatReplayResult } from "./m4ReplayThreat";
|
||||
|
||||
export interface AdvancedLaboratoryResults {
|
||||
@@ -59,6 +60,7 @@ export interface AdvancedLaboratoryResults {
|
||||
e46h: E46HFullRectifiedFrontReplayResult | null;
|
||||
e46i: E46IGroundingDinoFullReplayResult | null;
|
||||
e46j: E46JRawFisheyeRealtimeResult | null;
|
||||
e47: E47SemanticSlamResult | null;
|
||||
l34: L34RightYoloxTruthIslandResult | null;
|
||||
l34a: L34AAssistedYoloxErrorAuditResult | null;
|
||||
l34b: L34BResult | null;
|
||||
|
||||
@@ -985,9 +985,9 @@ export async function fetchAdvancedLaboratoryResults({
|
||||
e46b: null,
|
||||
e46c: null,
|
||||
e46d: null,
|
||||
e46e: null,
|
||||
e46f: null,
|
||||
e46e: null, e46f: null,
|
||||
e46g: null, e46h: null, e46i: null, e46j: null,
|
||||
e47: null,
|
||||
l34: null,
|
||||
l34a: null,
|
||||
l34b: null,
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
export type E47SemanticDisposition = "labeled" | "ambiguous";
|
||||
|
||||
export interface E47SemanticClass {
|
||||
classId: number;
|
||||
label: string;
|
||||
disposition: E47SemanticDisposition;
|
||||
colorRgb: readonly [number, number, number];
|
||||
}
|
||||
|
||||
export interface E47SemanticSlamResult {
|
||||
resultId: string;
|
||||
createdAtUtc: string;
|
||||
status: "diagnostic-semantic-slam-shadow";
|
||||
profileId: string;
|
||||
baseM4ResultId: string;
|
||||
semanticResultId: string;
|
||||
geometryResultId: string;
|
||||
sourcePackId: string;
|
||||
calibrationContentSha256: string;
|
||||
provider: {
|
||||
providerId: string;
|
||||
modelId: string;
|
||||
modelRevision: string;
|
||||
modelWeightsSha256: string;
|
||||
preprocessId: string;
|
||||
};
|
||||
temporalBinding: {
|
||||
semanticToCamera: "exact-sequence-and-session-time";
|
||||
cameraToLidar: "accepted-e6-nearest-host-arrival-best-effort";
|
||||
clockBasis: "recorded-host-monotonic-arrival";
|
||||
maximumLidarCameraDeltaMs: number;
|
||||
maximumPosePointDeltaMs: number;
|
||||
physicalSynchronizationProven: false;
|
||||
};
|
||||
taxonomy: readonly E47SemanticClass[];
|
||||
metrics: {
|
||||
frames: {
|
||||
total: number;
|
||||
maskAvailable: number;
|
||||
sourceAvailable: number;
|
||||
};
|
||||
points: {
|
||||
total: number;
|
||||
projected: number;
|
||||
labeled: number;
|
||||
ambiguous: number;
|
||||
unprojected: number;
|
||||
absent: number;
|
||||
};
|
||||
observations: {
|
||||
total: number;
|
||||
labeled: number;
|
||||
ambiguous: number;
|
||||
unprojected: number;
|
||||
absent: number;
|
||||
};
|
||||
runtime: {
|
||||
elapsedMs: number;
|
||||
framesPerSecond: number;
|
||||
};
|
||||
};
|
||||
acceptance: {
|
||||
artifactContractPassed: boolean;
|
||||
frameAccountingPassed: boolean;
|
||||
pointAccountingPassed: boolean;
|
||||
observationBindingPassed: boolean;
|
||||
temporalBindingPassed: boolean;
|
||||
independentSemanticTruthPassed: false;
|
||||
providerPromoted: false;
|
||||
};
|
||||
limitations: readonly string[];
|
||||
}
|
||||
|
||||
export interface E47SemanticTimelineFrame {
|
||||
sequence: number;
|
||||
sourcePointCount: number;
|
||||
classIds: readonly number[];
|
||||
statusCodes: readonly number[];
|
||||
counts: {
|
||||
labeled: number;
|
||||
ambiguous: number;
|
||||
unprojected: number;
|
||||
absent: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface E47SemanticTimelineChunk {
|
||||
resultId: string;
|
||||
startSequence: number;
|
||||
frameCount: number;
|
||||
nextSequence: number | null;
|
||||
frames: readonly E47SemanticTimelineFrame[];
|
||||
}
|
||||
|
||||
type LaboratoryFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
export class E47SemanticSlamContractError extends Error {}
|
||||
|
||||
const object = (value: unknown, label: string): Record<string, unknown> => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new E47SemanticSlamContractError(`${label}: ожидался объект.`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
};
|
||||
|
||||
const array = (value: unknown, label: string): readonly unknown[] => {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new E47SemanticSlamContractError(`${label}: ожидался массив.`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const text = (value: unknown, label: string): string => {
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
throw new E47SemanticSlamContractError(`${label}: ожидалась строка.`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const finite = (value: unknown, label: string): number => {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
throw new E47SemanticSlamContractError(`${label}: ожидалось число.`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const integer = (value: unknown, label: string): number => {
|
||||
const parsed = finite(value, label);
|
||||
if (!Number.isInteger(parsed) || parsed < 0) {
|
||||
throw new E47SemanticSlamContractError(`${label}: ожидалось неотрицательное целое.`);
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const exact = <T extends string | number | boolean>(
|
||||
value: unknown,
|
||||
expected: T,
|
||||
label: string,
|
||||
): T => {
|
||||
if (value !== expected) {
|
||||
throw new E47SemanticSlamContractError(`${label}: нарушен контракт.`);
|
||||
}
|
||||
return expected;
|
||||
};
|
||||
|
||||
function resultId(value: unknown): string {
|
||||
const parsed = text(value, "E47 result id");
|
||||
if (!/^e47-semantic-slam-[a-f0-9]{64}$/.test(parsed)) {
|
||||
throw new E47SemanticSlamContractError("E47 result id: нарушена идентичность.");
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function m4ResultId(value: unknown): string {
|
||||
const parsed = text(value, "E47 base M4 result id");
|
||||
if (!/^m4-threat-replay-[a-f0-9]{64}$/.test(parsed)) {
|
||||
throw new E47SemanticSlamContractError("E47 base M4 result id: нарушена идентичность.");
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function sha256(value: unknown, label: string): string {
|
||||
const parsed = text(value, label);
|
||||
if (!/^[a-f0-9]{64}$/.test(parsed)) {
|
||||
throw new E47SemanticSlamContractError(`${label}: ожидался SHA-256.`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function taxonomy(value: unknown): readonly E47SemanticClass[] {
|
||||
const classes = array(value, "E47 taxonomy").map((raw) => {
|
||||
const item = object(raw, "E47 semantic class");
|
||||
const classId = integer(item.class_id, "E47 class id");
|
||||
if (classId > 255) {
|
||||
throw new E47SemanticSlamContractError("E47 class id: вышел за uint8.");
|
||||
}
|
||||
const disposition = text(item.disposition, "E47 class disposition");
|
||||
if (disposition !== "labeled" && disposition !== "ambiguous") {
|
||||
throw new E47SemanticSlamContractError("E47 class disposition: неизвестное значение.");
|
||||
}
|
||||
const rgb = array(item.color_rgb, "E47 class color").map(
|
||||
(channel) => integer(channel, "E47 color channel"),
|
||||
);
|
||||
if (rgb.length !== 3 || rgb.some((channel) => channel > 255)) {
|
||||
throw new E47SemanticSlamContractError("E47 class color: нарушен RGB-контракт.");
|
||||
}
|
||||
return {
|
||||
classId,
|
||||
label: text(item.label, "E47 class label"),
|
||||
disposition: disposition as E47SemanticDisposition,
|
||||
colorRgb: [rgb[0]!, rgb[1]!, rgb[2]!] as const,
|
||||
};
|
||||
});
|
||||
if (!classes.length || new Set(classes.map((item) => item.classId)).size !== classes.length) {
|
||||
throw new E47SemanticSlamContractError("E47 taxonomy: классы отсутствуют или дублируются.");
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
function parseResult(value: unknown): E47SemanticSlamResult {
|
||||
const item = object(value, "E47 result");
|
||||
exact(item.schema_version, "missioncore.e47-semantic-slam-view/v1", "E47 view schema");
|
||||
exact(item.status, "diagnostic-semantic-slam-shadow", "E47 status");
|
||||
exact(item.ground_truth, false, "E47 ground truth");
|
||||
exact(item.semantic_authority, "diagnostic-only", "E47 semantic authority");
|
||||
exact(item.navigation_or_safety_accepted, false, "E47 safety authority");
|
||||
exact(item.actuation_allowed, false, "E47 actuation authority");
|
||||
const provider = object(item.provider, "E47 provider");
|
||||
const temporalBinding = object(item.temporal_binding, "E47 temporal binding");
|
||||
const metrics = object(item.metrics, "E47 metrics");
|
||||
const frames = object(metrics.frames, "E47 frame metrics");
|
||||
const points = object(metrics.points, "E47 point metrics");
|
||||
const observations = object(metrics.observations, "E47 observation metrics");
|
||||
const runtime = object(metrics.runtime, "E47 runtime metrics");
|
||||
const acceptance = object(item.acceptance, "E47 acceptance");
|
||||
const frameMetrics = {
|
||||
total: exact(frames.total, 4489, "E47 frame total"),
|
||||
maskAvailable: integer(frames.mask_available, "E47 mask frames"),
|
||||
sourceAvailable: integer(frames.source_available, "E47 source frames"),
|
||||
};
|
||||
if (
|
||||
frameMetrics.maskAvailable > frameMetrics.total
|
||||
|| frameMetrics.sourceAvailable > frameMetrics.total
|
||||
) {
|
||||
throw new E47SemanticSlamContractError("E47 frame accounting: нарушен контракт.");
|
||||
}
|
||||
const pointMetrics = {
|
||||
total: integer(points.total, "E47 total points"),
|
||||
projected: integer(points.projected, "E47 projected points"),
|
||||
labeled: integer(points.labeled, "E47 labeled points"),
|
||||
ambiguous: integer(points.ambiguous, "E47 ambiguous points"),
|
||||
unprojected: integer(points.unprojected, "E47 unprojected points"),
|
||||
absent: integer(points.absent, "E47 absent points"),
|
||||
};
|
||||
if (
|
||||
pointMetrics.projected !== pointMetrics.labeled + pointMetrics.ambiguous
|
||||
|| pointMetrics.total !== pointMetrics.projected
|
||||
+ pointMetrics.unprojected
|
||||
+ pointMetrics.absent
|
||||
) {
|
||||
throw new E47SemanticSlamContractError("E47 point accounting: нарушен контракт.");
|
||||
}
|
||||
const observationMetrics = {
|
||||
total: integer(observations.total, "E47 total observations"),
|
||||
labeled: integer(observations.labeled, "E47 labeled observations"),
|
||||
ambiguous: integer(observations.ambiguous, "E47 ambiguous observations"),
|
||||
unprojected: integer(observations.unprojected, "E47 unprojected observations"),
|
||||
absent: integer(observations.absent, "E47 absent observations"),
|
||||
};
|
||||
if (
|
||||
observationMetrics.total !== observationMetrics.labeled
|
||||
+ observationMetrics.ambiguous
|
||||
+ observationMetrics.unprojected
|
||||
+ observationMetrics.absent
|
||||
) {
|
||||
throw new E47SemanticSlamContractError("E47 observation accounting: нарушен контракт.");
|
||||
}
|
||||
const runtimeMetrics = {
|
||||
elapsedMs: finite(runtime.elapsed_ms, "E47 elapsed"),
|
||||
framesPerSecond: finite(runtime.frames_per_second, "E47 FPS"),
|
||||
};
|
||||
if (runtimeMetrics.elapsedMs <= 0 || runtimeMetrics.framesPerSecond <= 0) {
|
||||
throw new E47SemanticSlamContractError("E47 runtime accounting: нарушен контракт.");
|
||||
}
|
||||
return {
|
||||
resultId: resultId(item.result_id),
|
||||
createdAtUtc: text(item.created_at_utc, "E47 created at"),
|
||||
status: "diagnostic-semantic-slam-shadow",
|
||||
profileId: text(item.profile_id, "E47 profile"),
|
||||
baseM4ResultId: m4ResultId(item.base_m4_result_id),
|
||||
semanticResultId: text(item.semantic_result_id, "E47 semantic source"),
|
||||
geometryResultId: text(item.geometry_result_id, "E47 geometry source"),
|
||||
sourcePackId: text(item.source_pack_id, "E47 source pack"),
|
||||
calibrationContentSha256: sha256(item.calibration_content_sha256, "E47 calibration"),
|
||||
provider: {
|
||||
providerId: text(provider.provider_id, "E47 provider id"),
|
||||
modelId: text(provider.model_id, "E47 model id"),
|
||||
modelRevision: text(provider.model_revision, "E47 model revision"),
|
||||
modelWeightsSha256: sha256(provider.model_weights_sha256, "E47 model weights"),
|
||||
preprocessId: text(provider.preprocess_id, "E47 preprocess id"),
|
||||
},
|
||||
temporalBinding: {
|
||||
semanticToCamera: exact(
|
||||
temporalBinding.semantic_to_camera,
|
||||
"exact-sequence-and-session-time",
|
||||
"E47 semantic/camera binding",
|
||||
),
|
||||
cameraToLidar: exact(
|
||||
temporalBinding.camera_to_lidar,
|
||||
"accepted-e6-nearest-host-arrival-best-effort",
|
||||
"E47 camera/LiDAR binding",
|
||||
),
|
||||
clockBasis: exact(
|
||||
temporalBinding.clock_basis,
|
||||
"recorded-host-monotonic-arrival",
|
||||
"E47 clock basis",
|
||||
),
|
||||
maximumLidarCameraDeltaMs: finite(
|
||||
temporalBinding.maximum_lidar_camera_delta_ms,
|
||||
"E47 maximum camera/LiDAR delta",
|
||||
),
|
||||
maximumPosePointDeltaMs: finite(
|
||||
temporalBinding.maximum_pose_point_delta_ms,
|
||||
"E47 maximum pose/point delta",
|
||||
),
|
||||
physicalSynchronizationProven: exact(
|
||||
temporalBinding.physical_synchronization_proven,
|
||||
false,
|
||||
"E47 physical synchronization",
|
||||
),
|
||||
},
|
||||
taxonomy: taxonomy(item.taxonomy),
|
||||
metrics: {
|
||||
frames: frameMetrics,
|
||||
points: pointMetrics,
|
||||
observations: observationMetrics,
|
||||
runtime: runtimeMetrics,
|
||||
},
|
||||
acceptance: {
|
||||
artifactContractPassed: exact(
|
||||
acceptance.artifact_contract_passed,
|
||||
true,
|
||||
"E47 artifact contract",
|
||||
),
|
||||
frameAccountingPassed: exact(
|
||||
acceptance.frame_accounting_passed,
|
||||
true,
|
||||
"E47 frame accounting",
|
||||
),
|
||||
pointAccountingPassed: exact(
|
||||
acceptance.point_accounting_passed,
|
||||
true,
|
||||
"E47 point accounting",
|
||||
),
|
||||
observationBindingPassed: exact(
|
||||
acceptance.observation_binding_passed,
|
||||
true,
|
||||
"E47 observation binding",
|
||||
),
|
||||
temporalBindingPassed: exact(
|
||||
acceptance.temporal_binding_passed,
|
||||
true,
|
||||
"E47 temporal binding",
|
||||
),
|
||||
independentSemanticTruthPassed: exact(
|
||||
acceptance.independent_semantic_truth_passed,
|
||||
false,
|
||||
"E47 independent truth",
|
||||
),
|
||||
providerPromoted: exact(
|
||||
acceptance.provider_promoted,
|
||||
false,
|
||||
"E47 provider promotion",
|
||||
),
|
||||
},
|
||||
limitations: array(item.limitations, "E47 limitations").map(
|
||||
(entry) => text(entry, "E47 limitation"),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchE47SemanticSlamResult({
|
||||
fetcher = fetch,
|
||||
signal,
|
||||
}: {
|
||||
fetcher?: LaboratoryFetch;
|
||||
signal?: AbortSignal;
|
||||
} = {}): Promise<E47SemanticSlamResult | null> {
|
||||
const response = await fetcher("/api/v1/laboratory/e47-semantic-slam/results?limit=1", {
|
||||
headers: { Accept: "application/json" },
|
||||
signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new E47SemanticSlamContractError(`E47 LAB недоступен: HTTP ${response.status}.`);
|
||||
}
|
||||
const payload = object(await response.json(), "E47 catalog");
|
||||
exact(
|
||||
payload.schema_version,
|
||||
"missioncore.e47-semantic-slam-catalog/v1",
|
||||
"E47 catalog schema",
|
||||
);
|
||||
const items = array(payload.items, "E47 catalog items");
|
||||
return items.length ? parseResult(items[0]) : null;
|
||||
}
|
||||
|
||||
function parseFrame(
|
||||
value: unknown,
|
||||
expectedSequence: number,
|
||||
declaredTaxonomy: readonly E47SemanticClass[] | undefined,
|
||||
): E47SemanticTimelineFrame {
|
||||
const item = object(value, "E47 semantic frame");
|
||||
exact(item.schema_version, "missioncore.e47-semantic-slam-frame/v1", "E47 frame schema");
|
||||
const sequence = integer(item.sequence, "E47 frame sequence");
|
||||
if (sequence !== expectedSequence) {
|
||||
throw new E47SemanticSlamContractError("E47 frame sequence: нарушен порядок.");
|
||||
}
|
||||
const sourcePointCount = integer(item.source_point_count, "E47 frame source points");
|
||||
const classIds = array(item.class_ids, "E47 frame classes").map((entry) => {
|
||||
const parsed = finite(entry, "E47 frame class");
|
||||
if (!Number.isInteger(parsed) || parsed < -1 || parsed > 255) {
|
||||
throw new E47SemanticSlamContractError("E47 frame class: вышел за контракт.");
|
||||
}
|
||||
return parsed;
|
||||
});
|
||||
const statusCodes = array(item.status_codes, "E47 frame statuses").map((entry) => {
|
||||
const parsed = integer(entry, "E47 frame status");
|
||||
if (parsed > 3) {
|
||||
throw new E47SemanticSlamContractError("E47 frame status: неизвестное значение.");
|
||||
}
|
||||
return parsed;
|
||||
});
|
||||
if (classIds.length !== sourcePointCount || statusCodes.length !== sourcePointCount) {
|
||||
throw new E47SemanticSlamContractError("E47 frame point accounting: нарушен контракт.");
|
||||
}
|
||||
if (classIds.some((classId, index) => {
|
||||
const status = statusCodes[index];
|
||||
return status === 0 || status === 1 ? classId !== -1 : classId < 0;
|
||||
})) {
|
||||
throw new E47SemanticSlamContractError("E47 frame class/status binding: нарушен контракт.");
|
||||
}
|
||||
if (declaredTaxonomy) {
|
||||
const classesById = new Map(declaredTaxonomy.map((item) => [item.classId, item]));
|
||||
if (classIds.some((classId, index) => {
|
||||
const status = statusCodes[index];
|
||||
if (status !== 2 && status !== 3) return false;
|
||||
const semanticClass = classesById.get(classId);
|
||||
return !semanticClass
|
||||
|| (status === 2 && semanticClass.disposition !== "ambiguous")
|
||||
|| (status === 3 && semanticClass.disposition !== "labeled");
|
||||
})) {
|
||||
throw new E47SemanticSlamContractError("E47 frame taxonomy binding: нарушен контракт.");
|
||||
}
|
||||
}
|
||||
const counts = object(item.counts, "E47 frame counts");
|
||||
const parsedCounts = {
|
||||
labeled: integer(counts.labeled, "E47 frame labeled"),
|
||||
ambiguous: integer(counts.ambiguous, "E47 frame ambiguous"),
|
||||
unprojected: integer(counts.unprojected, "E47 frame unprojected"),
|
||||
absent: integer(counts.absent, "E47 frame absent"),
|
||||
};
|
||||
if (Object.values(parsedCounts).reduce((sum, count) => sum + count, 0) !== sourcePointCount) {
|
||||
throw new E47SemanticSlamContractError("E47 frame status accounting: нарушен контракт.");
|
||||
}
|
||||
const actualCounts = {
|
||||
labeled: statusCodes.filter((status) => status === 3).length,
|
||||
ambiguous: statusCodes.filter((status) => status === 2).length,
|
||||
unprojected: statusCodes.filter((status) => status === 1).length,
|
||||
absent: statusCodes.filter((status) => status === 0).length,
|
||||
};
|
||||
if (Object.keys(actualCounts).some(
|
||||
(key) => actualCounts[key as keyof typeof actualCounts]
|
||||
!== parsedCounts[key as keyof typeof parsedCounts],
|
||||
)) {
|
||||
throw new E47SemanticSlamContractError("E47 frame status histogram: нарушен контракт.");
|
||||
}
|
||||
return { sequence, sourcePointCount, classIds, statusCodes, counts: parsedCounts };
|
||||
}
|
||||
|
||||
export async function fetchE47SemanticTimelineChunk(
|
||||
result: string,
|
||||
startSequence: number,
|
||||
frameCount: number,
|
||||
{
|
||||
fetcher = fetch,
|
||||
signal,
|
||||
taxonomy,
|
||||
}: {
|
||||
fetcher?: LaboratoryFetch;
|
||||
signal?: AbortSignal;
|
||||
taxonomy?: readonly E47SemanticClass[];
|
||||
} = {},
|
||||
): Promise<E47SemanticTimelineChunk> {
|
||||
resultId(result);
|
||||
const parameters = new URLSearchParams({
|
||||
start: String(startSequence),
|
||||
count: String(frameCount),
|
||||
});
|
||||
const response = await fetcher(
|
||||
`/api/v1/laboratory/e47-semantic-slam/results/${result}/timeline/chunk?${parameters}`,
|
||||
{ headers: { Accept: "application/json" }, signal },
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new E47SemanticSlamContractError(`E47 timeline chunk: HTTP ${response.status}.`);
|
||||
}
|
||||
const payload = object(await response.json(), "E47 semantic chunk");
|
||||
exact(payload.schema_version, "missioncore.e47-semantic-slam-chunk/v1", "E47 chunk schema");
|
||||
exact(payload.result_id, result, "E47 chunk result");
|
||||
const parsedStart = integer(payload.start_sequence, "E47 chunk start");
|
||||
if (parsedStart !== startSequence) {
|
||||
throw new E47SemanticSlamContractError("E47 chunk start: нарушен контракт.");
|
||||
}
|
||||
const frames = array(payload.frames, "E47 chunk frames").map(
|
||||
(frame, offset) => parseFrame(frame, parsedStart + offset, taxonomy),
|
||||
);
|
||||
const parsedCount = integer(payload.frame_count, "E47 chunk count");
|
||||
if (parsedCount !== frames.length || parsedCount > frameCount) {
|
||||
throw new E47SemanticSlamContractError("E47 chunk frame count: нарушен контракт.");
|
||||
}
|
||||
return {
|
||||
resultId: result,
|
||||
startSequence: parsedStart,
|
||||
frameCount: parsedCount,
|
||||
nextSequence: payload.next_sequence === null
|
||||
? null
|
||||
: integer(payload.next_sequence, "E47 next sequence"),
|
||||
frames,
|
||||
};
|
||||
}
|
||||
|
||||
export function e47SemanticMaskUrl(result: string, sequence: number): string {
|
||||
resultId(result);
|
||||
if (!Number.isInteger(sequence) || sequence < 0 || sequence >= 4489) {
|
||||
throw new E47SemanticSlamContractError("E47 mask sequence: вне recorded replay.");
|
||||
}
|
||||
return `/api/v1/laboratory/e47-semantic-slam/results/${result}/masks/${sequence}`;
|
||||
}
|
||||
@@ -88,6 +88,24 @@
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.recorded-evidence-semantic-mask-overlay__error {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
top: 4.9rem;
|
||||
left: 50%;
|
||||
max-width: min(32rem, calc(100% - 2rem));
|
||||
border: 1px solid rgb(var(--nodedc-warning-rgb) / 0.44);
|
||||
border-radius: var(--nodedc-radius-control-compact);
|
||||
background: var(--nodedc-floating-surface);
|
||||
padding: 0.42rem 0.58rem;
|
||||
color: rgb(var(--nodedc-warning-rgb));
|
||||
font-size: 0.54rem;
|
||||
text-align: center;
|
||||
backdrop-filter: blur(var(--nodedc-blur-control));
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.m4-replay-threat-evidence-viewer .laboratory-metric-evidence-scene__legend,
|
||||
.m4-replay-threat-evidence-viewer .m4-replay-threat-visual__overlay {
|
||||
bottom: 6.2rem;
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
|
||||
.m4-replay-threat-evidence-viewer .laboratory-evidence-viewer__controls {
|
||||
width: calc(100% - 1.2rem);
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.m4-replay-threat-evidence-viewer .l3-visual-audit__actions {
|
||||
@@ -67,7 +68,7 @@
|
||||
}
|
||||
|
||||
.m4-replay-threat-visual__layer-controls .nodedc-segmented__item {
|
||||
padding-inline: 0.72rem;
|
||||
padding-inline: 0.3rem;
|
||||
}
|
||||
|
||||
.m4-replay-threat-evidence-viewer .laboratory-evidence-viewer__transport {
|
||||
@@ -138,7 +139,7 @@
|
||||
width: 0.38rem;
|
||||
height: 0.38rem;
|
||||
border-radius: 50%;
|
||||
background: var(--nodedc-text-muted);
|
||||
background: var(--laboratory-metric-legend-color, var(--nodedc-text-muted));
|
||||
content: "";
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, before, test } from "node:test";
|
||||
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let fetchE47SemanticSlamResult;
|
||||
let fetchE47SemanticTimelineChunk;
|
||||
let e47SemanticMaskUrl;
|
||||
|
||||
const resultId = `e47-semantic-slam-${"a".repeat(64)}`;
|
||||
const baseM4ResultId = `m4-threat-replay-${"b".repeat(64)}`;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({
|
||||
fetchE47SemanticSlamResult,
|
||||
fetchE47SemanticTimelineChunk,
|
||||
e47SemanticMaskUrl,
|
||||
} = await server.ssrLoadModule("/src/core/laboratory/e47SemanticSlam.ts"));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
function resultView(overrides = {}) {
|
||||
return {
|
||||
schema_version: "missioncore.e47-semantic-slam-view/v1",
|
||||
result_id: resultId,
|
||||
created_at_utc: "2026-08-06T10:00:00.000Z",
|
||||
status: "diagnostic-semantic-slam-shadow",
|
||||
profile_id: "ravnoves00-eomt-kb4-slam-shadow/v1",
|
||||
base_m4_result_id: baseM4ResultId,
|
||||
semantic_result_id: `result-${"c".repeat(64)}`,
|
||||
geometry_result_id: `m4-geometry-replay-${"d".repeat(64)}`,
|
||||
source_pack_id: `e10-lidar-pack-${"e".repeat(64)}`,
|
||||
calibration_content_sha256: "f".repeat(64),
|
||||
provider: {
|
||||
provider_id: "eomt-cityscapes-semantic-control/v1",
|
||||
model_id: "tue-mps/cityscapes_semantic_eomt_large_1024",
|
||||
model_revision: "revision-1",
|
||||
model_weights_sha256: "1".repeat(64),
|
||||
preprocess_id: "raw-kb4-valid-fov-semantic/v1",
|
||||
},
|
||||
temporal_binding: {
|
||||
semantic_to_camera: "exact-sequence-and-session-time",
|
||||
camera_to_lidar: "accepted-e6-nearest-host-arrival-best-effort",
|
||||
clock_basis: "recorded-host-monotonic-arrival",
|
||||
maximum_lidar_camera_delta_ms: 100,
|
||||
maximum_pose_point_delta_ms: 100,
|
||||
physical_synchronization_proven: false,
|
||||
},
|
||||
taxonomy: [
|
||||
{
|
||||
class_id: 0,
|
||||
label: "outside_valid_fov",
|
||||
disposition: "ambiguous",
|
||||
color_rgb: [0, 0, 0],
|
||||
},
|
||||
{
|
||||
class_id: 7,
|
||||
label: "paved_road",
|
||||
disposition: "labeled",
|
||||
color_rgb: [128, 64, 128],
|
||||
},
|
||||
],
|
||||
metrics: {
|
||||
frames: { total: 4489, mask_available: 4489, source_available: 4489 },
|
||||
points: {
|
||||
total: 4,
|
||||
projected: 2,
|
||||
labeled: 1,
|
||||
ambiguous: 1,
|
||||
unprojected: 2,
|
||||
absent: 0,
|
||||
},
|
||||
observations: {
|
||||
total: 2,
|
||||
labeled: 1,
|
||||
ambiguous: 0,
|
||||
unprojected: 1,
|
||||
absent: 0,
|
||||
},
|
||||
runtime: { elapsed_ms: 1000, frames_per_second: 4.489 },
|
||||
},
|
||||
acceptance: {
|
||||
artifact_contract_passed: true,
|
||||
frame_accounting_passed: true,
|
||||
point_accounting_passed: true,
|
||||
observation_binding_passed: true,
|
||||
temporal_binding_passed: true,
|
||||
independent_semantic_truth_passed: false,
|
||||
provider_promoted: false,
|
||||
},
|
||||
limitations: ["diagnostic only"],
|
||||
ground_truth: false,
|
||||
semantic_authority: "diagnostic-only",
|
||||
navigation_or_safety_accepted: false,
|
||||
actuation_allowed: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("E47 accepts only a fully accounted diagnostic semantic/SLAM view", async () => {
|
||||
const result = await fetchE47SemanticSlamResult({
|
||||
fetcher: async () => new Response(JSON.stringify({
|
||||
schema_version: "missioncore.e47-semantic-slam-catalog/v1",
|
||||
items: [resultView()],
|
||||
})),
|
||||
});
|
||||
assert.equal(result.resultId, resultId);
|
||||
assert.equal(result.baseM4ResultId, baseM4ResultId);
|
||||
assert.equal(result.metrics.points.projected, 2);
|
||||
assert.equal(result.acceptance.independentSemanticTruthPassed, false);
|
||||
assert.equal(result.temporalBinding.physicalSynchronizationProven, false);
|
||||
assert.equal(result.temporalBinding.maximumLidarCameraDeltaMs, 100);
|
||||
assert.equal(result.acceptance.temporalBindingPassed, true);
|
||||
assert.equal(result.taxonomy[0].disposition, "ambiguous");
|
||||
assert.equal(
|
||||
e47SemanticMaskUrl(resultId, 14),
|
||||
`/api/v1/laboratory/e47-semantic-slam/results/${resultId}/masks/14`,
|
||||
);
|
||||
});
|
||||
|
||||
test("E47 rejects result-level point accounting drift", async () => {
|
||||
await assert.rejects(
|
||||
fetchE47SemanticSlamResult({
|
||||
fetcher: async () => new Response(JSON.stringify({
|
||||
schema_version: "missioncore.e47-semantic-slam-catalog/v1",
|
||||
items: [resultView({
|
||||
metrics: {
|
||||
...resultView().metrics,
|
||||
points: {
|
||||
...resultView().metrics.points,
|
||||
total: 5,
|
||||
},
|
||||
},
|
||||
})],
|
||||
})),
|
||||
}),
|
||||
/point accounting/,
|
||||
);
|
||||
});
|
||||
|
||||
test("E47 chunk preserves unavailable sentinel and verifies the status histogram", async () => {
|
||||
const validFrame = {
|
||||
schema_version: "missioncore.e47-semantic-slam-frame/v1",
|
||||
sequence: 14,
|
||||
source_point_count: 4,
|
||||
class_ids: [7, 0, -1, -1],
|
||||
status_codes: [3, 2, 1, 1],
|
||||
counts: { labeled: 1, ambiguous: 1, unprojected: 2, absent: 0 },
|
||||
};
|
||||
const chunk = await fetchE47SemanticTimelineChunk(resultId, 14, 1, {
|
||||
taxonomy: [
|
||||
{ classId: 0, label: "outside_valid_fov", disposition: "ambiguous", colorRgb: [0, 0, 0] },
|
||||
{ classId: 7, label: "paved_road", disposition: "labeled", colorRgb: [128, 64, 128] },
|
||||
],
|
||||
fetcher: async () => new Response(JSON.stringify({
|
||||
schema_version: "missioncore.e47-semantic-slam-chunk/v1",
|
||||
result_id: resultId,
|
||||
start_sequence: 14,
|
||||
frame_count: 1,
|
||||
next_sequence: 15,
|
||||
frames: [validFrame],
|
||||
})),
|
||||
});
|
||||
assert.deepEqual(chunk.frames[0].classIds, [7, 0, -1, -1]);
|
||||
|
||||
await assert.rejects(
|
||||
fetchE47SemanticTimelineChunk(resultId, 14, 1, {
|
||||
taxonomy: [
|
||||
{ classId: 0, label: "outside_valid_fov", disposition: "ambiguous", colorRgb: [0, 0, 0] },
|
||||
{ classId: 7, label: "paved_road", disposition: "labeled", colorRgb: [128, 64, 128] },
|
||||
],
|
||||
fetcher: async () => new Response(JSON.stringify({
|
||||
schema_version: "missioncore.e47-semantic-slam-chunk/v1",
|
||||
result_id: resultId,
|
||||
start_sequence: 14,
|
||||
frame_count: 1,
|
||||
next_sequence: 15,
|
||||
frames: [{ ...validFrame, class_ids: [7, 0, 7, -1] }],
|
||||
})),
|
||||
}),
|
||||
/class\/status binding/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { test } from "node:test";
|
||||
|
||||
const component = (name) => new URL(`../src/components/laboratory/${name}`, import.meta.url);
|
||||
|
||||
test("semantic evidence mask is decoded, cached, cancelled and object-contained client-side", async () => {
|
||||
const source = await readFile(component("RecordedEvidenceSemanticMaskOverlay.tsx"), "utf8");
|
||||
assert.match(source, /decodedMaskCache/);
|
||||
assert.match(source, /pendingMaskCache/);
|
||||
assert.match(source, /AbortController/);
|
||||
assert.match(source, /getImageData/);
|
||||
assert.match(source, /8-bit grayscale class-id PNG/);
|
||||
assert.match(source, /ResizeObserver/);
|
||||
assert.match(source, /Math\.min\(width \/ imageWidth, height \/ imageHeight\)/);
|
||||
assert.match(source, /decoded\.key !== expectedKey/);
|
||||
assert.match(source, /mask\?\.key === expectedKey/);
|
||||
assert.match(source, /необъявленный class ID/);
|
||||
assert.match(source, /recorded-evidence-semantic-mask-overlay__error/);
|
||||
assert.match(source, /role="alert"/);
|
||||
});
|
||||
|
||||
test("shared recorded scenes place optional semantic masks beneath boxes", async () => {
|
||||
const [imageScene, videoScene] = await Promise.all([
|
||||
readFile(component("RecordedEvidenceImageScene.tsx"), "utf8"),
|
||||
readFile(component("RecordedEvidenceVideoScene.tsx"), "utf8"),
|
||||
]);
|
||||
for (const source of [imageScene, videoScene]) {
|
||||
assert.match(source, /semanticOverlay\?: RecordedEvidenceSemanticOverlay/);
|
||||
assert.ok(
|
||||
source.indexOf("<RecordedEvidenceSemanticMaskOverlay")
|
||||
< source.indexOf("<RecordedEvidenceBoxOverlay"),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("metric evidence keeps missing semantic assignments as context and exposes a taxonomy legend", async () => {
|
||||
const source = await readFile(component("LaboratoryMetricEvidenceScene.tsx"), "utf8");
|
||||
assert.match(source, /pointSemanticClassIds\?: readonly \(number \| null\)\[\]/);
|
||||
assert.match(source, /pointSemanticClassIds\.length === pointCloudBodyXyzM\.length/);
|
||||
assert.match(source, /classId === null \? undefined : colorsByClassId\.get\(classId\)/);
|
||||
assert.match(source, /data-decision="semantic"/);
|
||||
assert.match(source, /recordedEvidenceSemanticCssColor/);
|
||||
});
|
||||
|
||||
test("semantic point alignment is enforced only when M4 exposes the exact spatial increment", async () => {
|
||||
const source = await readFile(
|
||||
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(source, /semantic && frame\?\.spatialAvailable && semanticFrame/);
|
||||
assert.match(source, /\|\| !frame\.spatialAvailable\s*\|\| !semanticFrame/);
|
||||
assert.match(source, /semanticFrame\.sourcePointCount !== frame\.pointCloudSourceCount/);
|
||||
});
|
||||
|
||||
test("M4 mounts semantic mask overlays only for the active VIDEO or CAMERA layer", async () => {
|
||||
const source = await readFile(
|
||||
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/semanticOverlay=\{mode === "video" \? semanticOverlay : undefined\}/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/semanticOverlay=\{mode === "camera" \? semanticOverlay : undefined\}/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user