feat(lab): visualize dual evidence replay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 19:18:14 +03:00
parent b0d0bc8d7f
commit de19229895
17 changed files with 1825 additions and 115 deletions
@@ -0,0 +1,334 @@
import { useEffect, useRef, useState } from "react";
import { Button, Icon } from "@nodedc/ui-react";
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
export type LaboratoryMetricPoint3 = readonly [number, number, number];
export type LaboratoryMetricDecision = "threat" | "not-threat" | "unknown";
export type LaboratoryMetricSceneMode = "3d" | "plan";
export interface LaboratoryMetricObstacleVisual {
id: string;
decision: LaboratoryMetricDecision;
state: "current" | "held" | "expired";
centroidBodyXyzM: LaboratoryMetricPoint3;
cellCentersBodyXyzM: readonly LaboratoryMetricPoint3[];
}
export interface LaboratoryMetricRigVisual {
lengthM: number;
widthM: number;
nominalSensorHeightM: number;
}
export interface LaboratoryMetricCorridorVisual {
forwardLengthM: number;
rearMarginM: number;
halfWidthM: number;
}
function tokenColor(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
): THREE.Color {
const value = getComputedStyle(host).getPropertyValue(token).trim();
if (value.startsWith("#")) return new THREE.Color(value);
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return new THREE.Color(red / 255, green / 255, blue / 255);
}
function disposeRenderable(object: THREE.Object3D): void {
const renderable = object as THREE.Object3D & {
geometry?: THREE.BufferGeometry;
material?: THREE.Material | THREE.Material[];
};
renderable.geometry?.dispose();
const materials = Array.isArray(renderable.material)
? renderable.material
: renderable.material
? [renderable.material]
: [];
materials.forEach((material) => material.dispose());
}
function scenePoint(point: LaboratoryMetricPoint3): LaboratoryMetricPoint3 {
return [point[0], point[2], -point[1]];
}
function positions(points: readonly LaboratoryMetricPoint3[]): Float32Array {
const result = new Float32Array(points.length * 3);
points.forEach((point, index) => {
const [x, y, z] = scenePoint(point);
const offset = index * 3;
result[offset] = x;
result[offset + 1] = y;
result[offset + 2] = z;
});
return result;
}
function decisionColor(
host: HTMLElement,
decision: LaboratoryMetricDecision,
): THREE.Color {
if (decision === "threat") {
return tokenColor(host, "--nodedc-danger-rgb", [255, 104, 112]);
}
if (decision === "not-threat") {
return tokenColor(host, "--nodedc-success-rgb", [181, 255, 90]);
}
return tokenColor(host, "--nodedc-warning-rgb", [255, 197, 92]);
}
export function LaboratoryMetricEvidenceScene({
pointCloudBodyXyzM,
obstacles,
rig,
corridor,
mode,
label,
}: {
pointCloudBodyXyzM: readonly LaboratoryMetricPoint3[];
obstacles: readonly LaboratoryMetricObstacleVisual[];
rig: LaboratoryMetricRigVisual;
corridor: LaboratoryMetricCorridorVisual;
mode: LaboratoryMetricSceneMode;
label: string;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const sceneRef = useRef<THREE.Scene | null>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const controlsRef = useRef<OrbitControls | null>(null);
const contentRef = useRef<THREE.Group | null>(null);
const [renderError, setRenderError] = useState<string | null>(null);
useEffect(() => {
const host = hostRef.current;
if (!host) return;
let renderer: THREE.WebGLRenderer;
try {
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: false,
powerPreference: "high-performance",
});
} catch {
setRenderError("Браузер не смог создать метрическую 3D-сцену.");
return;
}
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setClearColor(tokenColor(host, "--nodedc-canvas", [5, 5, 6]), 1);
renderer.domElement.setAttribute("aria-label", label);
renderer.domElement.setAttribute("role", "img");
host.prepend(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(48, 1, 0.01, 300);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.enablePan = true;
controls.enableZoom = true;
controls.screenSpacePanning = true;
controls.minDistance = 0.4;
controls.maxDistance = 80;
const content = new THREE.Group();
scene.add(content);
sceneRef.current = scene;
cameraRef.current = camera;
controlsRef.current = controls;
contentRef.current = content;
const resize = () => {
const width = Math.max(host.clientWidth, 1);
const height = Math.max(host.clientHeight, 1);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height, false);
};
const observer = new ResizeObserver(resize);
observer.observe(host);
resize();
let animationFrame = 0;
const render = () => {
animationFrame = window.requestAnimationFrame(render);
controls.update();
renderer.render(scene, camera);
};
render();
return () => {
window.cancelAnimationFrame(animationFrame);
observer.disconnect();
controls.dispose();
scene.traverse(disposeRenderable);
renderer.dispose();
renderer.domElement.remove();
sceneRef.current = null;
cameraRef.current = null;
controlsRef.current = null;
contentRef.current = null;
};
}, [label]);
useEffect(() => {
const host = hostRef.current;
const content = contentRef.current;
if (!host || !content) return;
while (content.children.length) {
const child = content.children[0];
if (!child) break;
content.remove(child);
child.traverse(disposeRenderable);
}
const contextGeometry = new THREE.BufferGeometry();
contextGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions(pointCloudBodyXyzM), 3),
);
content.add(new THREE.Points(
contextGeometry,
new THREE.PointsMaterial({
color: tokenColor(host, "--nodedc-text-muted", [147, 151, 159]),
size: 1.7,
sizeAttenuation: false,
transparent: true,
opacity: 0.34,
depthWrite: false,
}),
));
for (const obstacle of obstacles) {
const color = decisionColor(host, obstacle.decision);
const cellsGeometry = new THREE.BufferGeometry();
cellsGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions(obstacle.cellCentersBodyXyzM), 3),
);
content.add(new THREE.Points(
cellsGeometry,
new THREE.PointsMaterial({
color,
size: obstacle.state === "current" ? 4.8 : 3.8,
sizeAttenuation: false,
transparent: true,
opacity: obstacle.state === "current" ? 0.94 : 0.45,
depthWrite: false,
}),
));
const centroid = new THREE.Mesh(
new THREE.SphereGeometry(0.1, 16, 12),
new THREE.MeshBasicMaterial({ color, wireframe: obstacle.state !== "current" }),
);
centroid.position.fromArray(scenePoint(obstacle.centroidBodyXyzM));
centroid.userData.evidenceId = obstacle.id;
content.add(centroid);
}
const corridorLength = rig.lengthM / 2 + corridor.forwardLengthM + corridor.rearMarginM;
const corridorCenterX = (rig.lengthM / 2 + corridor.forwardLengthM - corridor.rearMarginM) / 2;
const corridorMesh = new THREE.Mesh(
new THREE.PlaneGeometry(corridorLength, corridor.halfWidthM * 2),
new THREE.MeshBasicMaterial({
color: tokenColor(host, "--nodedc-accent-rgb", [232, 56, 126]),
transparent: true,
opacity: 0.11,
side: THREE.DoubleSide,
depthWrite: false,
}),
);
corridorMesh.rotation.x = -Math.PI / 2;
corridorMesh.position.set(corridorCenterX, 0.01, 0);
content.add(corridorMesh);
const corridorOutline = new THREE.LineSegments(
new THREE.EdgesGeometry(new THREE.BoxGeometry(corridorLength, 0.01, corridor.halfWidthM * 2)),
new THREE.LineBasicMaterial({
color: tokenColor(host, "--nodedc-accent-rgb", [232, 56, 126]),
transparent: true,
opacity: 0.7,
}),
);
corridorOutline.position.set(corridorCenterX, 0.015, 0);
content.add(corridorOutline);
const body = new THREE.LineSegments(
new THREE.EdgesGeometry(new THREE.BoxGeometry(rig.lengthM, 0.34, rig.widthM)),
new THREE.LineBasicMaterial({
color: tokenColor(host, "--nodedc-foreground-rgb", [245, 245, 245]),
transparent: true,
opacity: 0.86,
}),
);
body.position.y = 0.17;
content.add(body);
const lidar = new THREE.Mesh(
new THREE.CylinderGeometry(0.08, 0.08, 0.08, 20),
new THREE.MeshBasicMaterial({
color: tokenColor(host, "--nodedc-foreground-rgb", [245, 245, 245]),
}),
);
lidar.position.y = rig.nominalSensorHeightM;
content.add(lidar);
const grid = new THREE.GridHelper(
Math.max(20, corridor.forwardLengthM * 2.5),
40,
tokenColor(host, "--nodedc-text-muted", [96, 99, 106]),
tokenColor(host, "--nodedc-glass-outline", [48, 50, 56]),
);
const gridMaterials = Array.isArray(grid.material) ? grid.material : [grid.material];
gridMaterials.forEach((material) => {
material.transparent = true;
material.opacity = 0.15;
material.depthWrite = false;
});
content.add(grid);
}, [corridor, obstacles, pointCloudBodyXyzM, rig]);
const resetView = () => {
const camera = cameraRef.current;
const controls = controlsRef.current;
if (!camera || !controls) return;
controls.target.set(corridor.forwardLengthM * 0.35, 0.6, 0);
if (mode === "plan") {
camera.position.set(corridor.forwardLengthM * 0.35, 15, 0.001);
camera.up.set(0, 0, -1);
} else {
camera.position.set(-4.5, 4.8, 8.5);
camera.up.set(0, 1, 0);
}
camera.updateProjectionMatrix();
controls.update();
};
useEffect(resetView, [corridor.forwardLengthM, mode, obstacles]);
return (
<div className="laboratory-metric-evidence-scene">
<div ref={hostRef} className="laboratory-metric-evidence-scene__viewport">
{renderError ? <p>{renderError}</p> : null}
</div>
<div className="laboratory-metric-evidence-scene__toolbar">
<Button
variant="secondary"
size="compact"
icon={<Icon name="refresh" size={14} />}
onClick={resetView}
>
Сбросить ракурс
</Button>
<span>ЛКМ · вращение · колесо · масштаб · ПКМ · панорама</span>
</div>
<div className="laboratory-metric-evidence-scene__legend">
<span data-decision="threat">Угроза</span>
<span data-decision="not-threat">Вне коридора</span>
<span data-decision="unknown">Неизвестно</span>
<span data-decision="context">LiDAR context</span>
</div>
</div>
);
}
@@ -0,0 +1,134 @@
import { useEffect, useRef } from "react";
import {
RecordedFmp4Player,
type RecordedObservationPlayback,
} from "../RecordedFmp4Player";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
export type RecordedEvidenceBoxTone =
| "accent"
| "danger"
| "success"
| "warning"
| "neutral";
export interface RecordedEvidenceBox {
boxXyxy: readonly [number, number, number, number];
label: string;
tone: RecordedEvidenceBoxTone;
dashed?: boolean;
}
function rgba(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
alpha = 1,
): string {
const channels = getComputedStyle(host)
.getPropertyValue(token)
.trim()
.match(/[\d.]+/g)
?.slice(0, 3)
.map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
function toneColor(host: HTMLElement, tone: RecordedEvidenceBoxTone): string {
if (tone === "danger") return rgba(host, "--nodedc-danger-rgb", [255, 104, 112]);
if (tone === "success") return rgba(host, "--nodedc-success-rgb", [181, 255, 90]);
if (tone === "warning") return rgba(host, "--nodedc-warning-rgb", [255, 197, 92]);
if (tone === "neutral") return rgba(host, "--nodedc-foreground-rgb", [245, 245, 245], 0.7);
return rgba(host, "--nodedc-accent-rgb", [232, 56, 126]);
}
export function RecordedEvidenceVideoScene({
source,
playback,
imageWidth,
imageHeight,
boxes,
ariaLabel,
onPlaybackChange,
}: {
source: ObservationSourceDescriptor;
playback: RecordedObservationPlayback;
imageWidth: number;
imageHeight: number;
boxes: readonly RecordedEvidenceBox[];
ariaLabel: string;
onPlaybackChange: (playback: RecordedObservationPlayback) => void;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const host = hostRef.current;
const canvas = canvasRef.current;
if (!host || !canvas) 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);
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;
for (const item of boxes) {
const [left, top, right, bottom] = item.boxXyxy;
const x = offsetX + left * scale;
const y = offsetY + top * scale;
const boxWidth = (right - left) * scale;
const boxHeight = (bottom - top) * scale;
const stroke = toneColor(host, item.tone);
context.strokeStyle = stroke;
context.lineWidth = Math.max(1.5, 2 * scale);
context.setLineDash(item.dashed ? [5, 4] : []);
context.strokeRect(x, y, boxWidth, boxHeight);
context.setLineDash([]);
const fontSize = Math.max(9, 10 * scale);
context.font = `650 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = Math.min(drawWidth, context.measureText(item.label).width + 8);
const labelHeight = fontSize + 6;
const labelX = Math.min(offsetX + drawWidth - labelWidth, Math.max(offsetX, x));
const labelY = Math.max(offsetY, y - labelHeight);
context.fillStyle = rgba(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.9);
context.fillRect(labelX, labelY, labelWidth, labelHeight);
context.fillStyle = stroke;
context.fillText(item.label, labelX + 4, labelY + fontSize + 1, labelWidth - 8);
}
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [boxes, imageHeight, imageWidth]);
return (
<div className="recorded-evidence-video-scene" ref={hostRef}>
<RecordedFmp4Player
source={source}
playback={playback}
interactive
prepare
onPlaybackChange={onPlaybackChange}
/>
<canvas ref={canvasRef} role="img" aria-label={ariaLabel} />
</div>
);
}
@@ -35,8 +35,10 @@ import { fetchE46GRectifiedDetectorBakeoff } from "./e46gRectifiedDetectorBakeof
import { fetchE46HFullRectifiedFrontReplay } from "./e46hFullRectifiedFrontReplay";
import { fetchE46IGroundingDinoFullReplay } from "./e46iGroundingDinoFullReplay";
import { fetchE46JRawFisheyeRealtime } from "./e46jRawFisheyeRealtime";
import { fetchM4ThreatReplayResult } from "./m4ReplayThreat";
export type AdvancedLaboratoryWorkId =
| "m4-replay-threat"
| "l3-pointpillars-visual-audit"
| "l31-pointpillars-ravnoves"
| "l32-pointpillars-camera-review"
@@ -76,6 +78,7 @@ export interface AdvancedLaboratoryIndexItem {
}
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
"m4-replay-threat",
"l3-pointpillars-visual-audit",
"l31-pointpillars-ravnoves",
"l32-pointpillars-camera-review",
@@ -110,6 +113,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
];
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
"m4-replay-threat": "m4-threat-replay",
"l3-pointpillars-visual-audit": "l3-pointpillars-visual-audit",
"l31-pointpillars-ravnoves": "l31-pointpillars-ravnoves",
"l32-pointpillars-camera-review": "l32-pointpillars-camera-review",
@@ -151,6 +155,7 @@ export function isAdvancedLaboratoryWorkId(
export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
return {
m4Threat: null,
l3: null,
l31: null,
l32: null,
@@ -273,7 +278,8 @@ export function advancedLaboratoryResultAvailable(
workId: AdvancedLaboratoryWorkId,
results: AdvancedLaboratoryResults,
): boolean {
return workId === "l3-pointpillars-visual-audit" ? results.l3 !== null
return workId === "m4-replay-threat" ? results.m4Threat !== null
: workId === "l3-pointpillars-visual-audit" ? results.l3 !== null
: workId === "l31-pointpillars-ravnoves" ? results.l31 !== null
: workId === "l32-pointpillars-camera-review" ? results.l32 !== null
: workId === "l33-camera-first-detector-review" ? results.l33 !== null
@@ -317,7 +323,9 @@ export async function fetchAdvancedLaboratoryResult(
} = {},
): Promise<AdvancedLaboratoryResults> {
const results = emptyAdvancedLaboratoryResults();
if (workId === "l3-pointpillars-visual-audit") {
if (workId === "m4-replay-threat") {
results.m4Threat = await fetchM4ThreatReplayResult({ fetcher, signal });
} else if (workId === "l3-pointpillars-visual-audit") {
results.l3 = await fetchL3PointPillarsVisualAudit({ fetcher, signal });
} else if (workId === "l31-pointpillars-ravnoves") {
results.l31 = await fetchL31PointPillarsRavnoves({ fetcher, signal });
@@ -31,8 +31,10 @@ import type { E46GRectifiedDetectorBakeoffResult } from "./e46gRectifiedDetector
import type { E46HFullRectifiedFrontReplayResult } from "./e46hFullRectifiedFrontReplay";
import type { E46IGroundingDinoFullReplayResult } from "./e46iGroundingDinoFullReplay";
import type { E46JRawFisheyeRealtimeResult } from "./e46jRawFisheyeRealtime";
import type { M4ThreatReplayResult } from "./m4ReplayThreat";
export interface AdvancedLaboratoryResults {
m4Threat: M4ThreatReplayResult | null;
l3: L3PointPillarsVisualAuditResult | null;
l31: L31PointPillarsRavnovesResult | null;
l32: L32PointPillarsCameraReviewResult | null;
@@ -235,7 +235,6 @@ export class AdvancedLaboratoryContractError extends Error {
this.name = "AdvancedLaboratoryContractError";
}
}
export type LaboratoryFetch = (
input: RequestInfo | URL,
init?: RequestInit,
@@ -968,6 +967,7 @@ export async function fetchAdvancedLaboratoryResults({
const e39 = settledCatalogValue(settled[7]);
const e40 = settledCatalogValue(settled[8]);
return {
m4Threat: null,
l3: null, l31: null,
l32: null,
l33: null,
@@ -0,0 +1,460 @@
export type M4ThreatDecision = "threat" | "not-threat" | "unknown";
export type M4ThreatMotion = "moving" | "stationary" | "unknown";
export type M4Point3 = readonly [number, number, number];
export interface M4ThreatReplayResult {
resultId: string;
createdAtUtc: string;
profileId: string;
rigProfileId: string;
corridorProfileId: string;
sourceResultIds: {
detector: string;
geometry: string;
temporal: string;
};
metrics: {
decisions: Record<M4ThreatDecision, number>;
evidence: {
cameraOnly: number;
currentMetric: number;
staleOrHeld: number;
};
fixtures: {
critical: number;
criticalFalseNotThreat: number;
passed: number;
total: number;
};
runtime: {
framesPerSecond: number;
providerLatencyP50Ms: number;
providerLatencyP95Ms: number;
providerLatencyMaxMs: number;
};
reasonCounts: Readonly<Record<string, number>>;
};
configuration: {
virtualBodyM: readonly [number, number];
nominalSensorHeightM: number;
forwardCorridorM: number;
predictionHorizonSeconds: number;
};
limitations: readonly string[];
}
export interface M4ThreatAssessment {
componentId: string;
decision: M4ThreatDecision;
corridorIntersection: "intersects" | "clear" | "unknown";
relativeSpeedMps: number | null;
closestApproachM: number | null;
ttcSeconds: number | null;
reasonCodes: readonly string[];
}
export interface M4ThreatMetricVisual {
componentId: string;
state: "current" | "held" | "expired";
motion: M4ThreatMotion;
centroidBodyXyzM: M4Point3;
cellCentersBodyXyzM: readonly M4Point3[];
assessment: M4ThreatAssessment;
}
export interface M4ThreatCameraProposal {
proposalId: string;
bboxXyxy: readonly [number, number, number, number];
objectness: number;
semanticHint: string | null;
occupiedSupport: boolean;
rangeM: number | null;
threatDecision: M4ThreatDecision | null;
threatReasonCodes: readonly string[];
}
export interface M4ThreatVisualFrame {
resultId: string;
ordinal: number;
sequence: number;
frameId: string;
sourceTimeNs: number;
pointCloudBodyXyzM: readonly M4Point3[];
pointCloudSourceCount: number;
pointCloudSampleCount: number;
metricObstacles: readonly M4ThreatMetricVisual[];
cameraProposals: readonly M4ThreatCameraProposal[];
rig: {
lengthM: number;
widthM: number;
nominalSensorHeightM: number;
};
corridor: {
forwardLengthM: number;
rearMarginM: number;
halfWidthM: number;
predictionHorizonSeconds: number;
};
}
export interface M4ThreatVisualIndexItem {
ordinal: number;
sequence: number;
frameId: string;
sourceTimeNs: number;
metricObstacleCount: number;
cameraProposalCount: number;
pointCloudSampleCount: number;
}
export interface M4ThreatVideoFrame {
frameIndex: number;
sessionSeconds: number;
sourceAvailable: boolean;
cameraProposals: readonly M4ThreatCameraProposal[];
decisionCounts: Record<M4ThreatDecision, number>;
}
export interface M4ThreatVideoOverlay {
resultId: string;
recordedSourceSessionId: "20260720T065719Z_viewer_live";
imageWidth: 800;
imageHeight: 600;
timelineStartSeconds: number;
timelineEndSeconds: number;
frames: readonly M4ThreatVideoFrame[];
}
type LaboratoryFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
class M4ThreatContractError extends Error {}
const object = (value: unknown, label: string): Record<string, unknown> => {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new M4ThreatContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
};
const array = (value: unknown, label: string): readonly unknown[] => {
if (!Array.isArray(value)) throw new M4ThreatContractError(`${label}: ожидался массив.`);
return value;
};
const text = (value: unknown, label: string): string => {
if (typeof value !== "string" || !value.trim()) {
throw new M4ThreatContractError(`${label}: ожидалась строка.`);
}
return value;
};
const number = (value: unknown, label: string): number => {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new M4ThreatContractError(`${label}: ожидалось число.`);
}
return value;
};
const integer = (value: unknown, label: string): number => {
const parsed = number(value, label);
if (!Number.isInteger(parsed) || parsed < 0) {
throw new M4ThreatContractError(`${label}: ожидалось целое.`);
}
return parsed;
};
const exact = <T extends string | number | boolean>(
value: unknown,
expected: T,
label: string,
): T => {
if (value !== expected) throw new M4ThreatContractError(`${label}: нарушен контракт.`);
return expected;
};
const optionalNumber = (value: unknown, label: string): number | null => (
value === null ? null : number(value, label)
);
const vector = (value: unknown, size: number, label: string): number[] => {
const parsed = array(value, label).map((item) => number(item, label));
if (parsed.length !== size) throw new M4ThreatContractError(`${label}: неверная размерность.`);
return parsed;
};
const decision = (value: unknown, label: string): M4ThreatDecision => {
if (value !== "threat" && value !== "not-threat" && value !== "unknown") {
throw new M4ThreatContractError(`${label}: неизвестное решение.`);
}
return value;
};
const motion = (value: unknown): M4ThreatMotion => {
if (value !== "moving" && value !== "stationary" && value !== "unknown") {
throw new M4ThreatContractError("M4.6 motion: неизвестное состояние.");
}
return value;
};
const resultId = (value: unknown): string => {
const parsed = text(value, "M4.6 result id");
if (!/^m4-threat-replay-[a-f0-9]{64}$/.test(parsed)) {
throw new M4ThreatContractError("M4.6 result id: нарушена идентичность.");
}
return parsed;
};
function parseAssessment(value: unknown): M4ThreatAssessment {
const item = object(value, "M4.6 assessment");
const intersection = text(item.corridor_intersection, "M4.6 intersection");
if (intersection !== "intersects" && intersection !== "clear" && intersection !== "unknown") {
throw new M4ThreatContractError("M4.6 intersection: неизвестное состояние.");
}
return {
componentId: text(item.component_id, "M4.6 component"),
decision: decision(item.decision, "M4.6 decision"),
corridorIntersection: intersection,
relativeSpeedMps: optionalNumber(item.relative_speed_mps, "M4.6 relative speed"),
closestApproachM: optionalNumber(item.closest_approach_m, "M4.6 closest approach"),
ttcSeconds: optionalNumber(item.ttc_seconds, "M4.6 TTC"),
reasonCodes: array(item.reason_codes, "M4.6 reasons").map((reason) => text(reason, "M4.6 reason")),
};
}
function parseCameraProposal(value: unknown): M4ThreatCameraProposal {
const item = object(value, "M4.6 camera proposal");
return {
proposalId: text(item.proposal_id, "M4.6 proposal id"),
bboxXyxy: vector(item.bbox_xyxy, 4, "M4.6 bbox") as [number, number, number, number],
objectness: number(item.objectness, "M4.6 objectness"),
semanticHint: item.semantic_hint === null ? null : text(item.semantic_hint, "M4.6 hint"),
occupiedSupport: typeof item.occupied_support === "boolean" ? item.occupied_support : false,
rangeM: optionalNumber(item.range_m, "M4.6 range"),
threatDecision: item.threat_decision === null
? null
: decision(item.threat_decision, "M4.6 camera threat"),
threatReasonCodes: array(item.threat_reason_codes, "M4.6 threat reasons").map(
(reason) => text(reason, "M4.6 threat reason"),
),
};
}
export async function fetchM4ThreatReplayResult({
fetcher = fetch,
signal,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {}): Promise<M4ThreatReplayResult | null> {
const response = await fetcher("/api/v1/laboratory/m4-threat/results?limit=1", {
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) throw new M4ThreatContractError(`M4.6 LAB недоступен: HTTP ${response.status}.`);
const catalog = object(await response.json(), "M4.6 catalog");
exact(catalog.schema_version, "missioncore.m4-threat-replay-catalog/v1", "M4.6 catalog schema");
const items = array(catalog.items, "M4.6 results");
if (!items.length) return null;
const item = object(items[0], "M4.6 result");
exact(item.schema_version, "missioncore.m4-threat-replay-view/v1", "M4.6 view schema");
exact(item.accepted, true, "M4.6 acceptance");
exact(item.authority, "replay-simulated", "M4.6 authority");
exact(item.physical_collision_accepted, false, "M4.6 physical authority");
exact(item.actuation_allowed, false, "M4.6 actuation");
const metrics = object(item.metrics, "M4.6 metrics");
const decisions = object(metrics.decisions, "M4.6 decisions");
const evidence = object(metrics.evidence, "M4.6 evidence");
const fixtures = object(metrics.fixtures, "M4.6 fixtures");
const runtime = object(metrics.runtime, "M4.6 runtime");
const configuration = object(item.configuration, "M4.6 configuration");
const sourceResultIds = object(item.source_result_ids, "M4.6 sources");
return {
resultId: resultId(item.result_id),
createdAtUtc: text(item.created_at_utc, "M4.6 created"),
profileId: text(item.profile_id, "M4.6 profile"),
rigProfileId: text(item.rig_profile_id, "M4.6 rig"),
corridorProfileId: text(item.corridor_profile_id, "M4.6 corridor"),
sourceResultIds: {
detector: text(sourceResultIds.detector, "M4.6 detector"),
geometry: text(sourceResultIds.geometry, "M4.6 geometry"),
temporal: text(sourceResultIds.temporal, "M4.6 temporal"),
},
metrics: {
decisions: {
threat: integer(decisions.threat, "M4.6 threat count"),
"not-threat": integer(decisions["not-threat"], "M4.6 clear count"),
unknown: integer(decisions.unknown, "M4.6 unknown count"),
},
evidence: {
cameraOnly: integer(evidence["camera-only"], "M4.6 camera-only"),
currentMetric: integer(evidence["current-metric"], "M4.6 metric"),
staleOrHeld: integer(evidence["stale-or-held"], "M4.6 stale"),
},
fixtures: {
critical: integer(fixtures.critical, "M4.6 critical fixtures"),
criticalFalseNotThreat: integer(fixtures.critical_false_not_threat, "M4.6 false-safe"),
passed: integer(fixtures.passed, "M4.6 fixtures passed"),
total: integer(fixtures.total, "M4.6 fixtures total"),
},
runtime: {
framesPerSecond: number(runtime.frames_per_second, "M4.6 FPS"),
providerLatencyP50Ms: number(runtime.provider_latency_p50_ms, "M4.6 p50"),
providerLatencyP95Ms: number(runtime.provider_latency_p95_ms, "M4.6 p95"),
providerLatencyMaxMs: number(runtime.provider_latency_max_ms, "M4.6 max"),
},
reasonCounts: Object.fromEntries(
Object.entries(object(metrics.reason_counts, "M4.6 reasons")).map(
([key, value]) => [key, integer(value, `M4.6 ${key}`)],
),
),
},
configuration: {
virtualBodyM: vector(configuration.virtual_body_m, 2, "M4.6 body") as [number, number],
nominalSensorHeightM: number(configuration.nominal_sensor_height_m, "M4.6 height"),
forwardCorridorM: number(configuration.forward_corridor_m, "M4.6 corridor"),
predictionHorizonSeconds: number(configuration.prediction_horizon_seconds, "M4.6 horizon"),
},
limitations: array(item.limitations, "M4.6 limitations").map((value) => text(value, "M4.6 limitation")),
};
}
export async function fetchM4ThreatVisualIndex(
result: string,
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<readonly M4ThreatVisualIndexItem[]> {
const response = await fetcher(`/api/v1/laboratory/m4-threat/results/${result}/visuals`, {
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) throw new M4ThreatContractError(`M4.6 visual index: HTTP ${response.status}.`);
const payload = object(await response.json(), "M4.6 visual index");
exact(payload.schema_version, "missioncore.m4-threat-visual-catalog/v1", "M4.6 visual schema");
exact(payload.result_id, result, "M4.6 visual result");
return array(payload.items, "M4.6 visual items").map((raw) => {
const item = object(raw, "M4.6 visual item");
return {
ordinal: integer(item.ordinal, "M4.6 visual ordinal"),
sequence: integer(item.sequence, "M4.6 visual sequence"),
frameId: text(item.frame_id, "M4.6 visual frame"),
sourceTimeNs: integer(item.source_time_ns, "M4.6 visual time"),
metricObstacleCount: integer(item.metric_obstacle_count, "M4.6 visual metric"),
cameraProposalCount: integer(item.camera_proposal_count, "M4.6 visual camera"),
pointCloudSampleCount: integer(item.point_cloud_sample_count, "M4.6 visual points"),
};
});
}
export async function fetchM4ThreatVisual(
result: string,
ordinal: number,
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<M4ThreatVisualFrame> {
const response = await fetcher(
`/api/v1/laboratory/m4-threat/results/${result}/visuals/${ordinal}`,
{ headers: { Accept: "application/json" }, signal },
);
if (!response.ok) throw new M4ThreatContractError(`M4.6 visual frame: HTTP ${response.status}.`);
const item = object(await response.json(), "M4.6 visual frame");
exact(item.schema_version, "missioncore.perception-threat-visual-frame/v1", "M4.6 frame schema");
exact(item.result_id, result, "M4.6 frame result");
const rig = object(item.rig, "M4.6 visual rig");
const corridor = object(item.corridor, "M4.6 visual corridor");
return {
resultId: result,
ordinal: integer(item.ordinal, "M4.6 ordinal"),
sequence: integer(item.sequence, "M4.6 sequence"),
frameId: text(item.frame_id, "M4.6 frame id"),
sourceTimeNs: integer(item.source_time_ns, "M4.6 frame time"),
pointCloudBodyXyzM: array(item.point_cloud_body_xyz_m, "M4.6 points").map(
(point) => vector(point, 3, "M4.6 point") as [number, number, number],
),
pointCloudSourceCount: integer(item.point_cloud_source_count, "M4.6 source points"),
pointCloudSampleCount: integer(item.point_cloud_sample_count, "M4.6 sample points"),
metricObstacles: array(item.metric_obstacles, "M4.6 metric visuals").map((raw) => {
const value = object(raw, "M4.6 metric visual");
const state = text(value.state, "M4.6 temporal state");
if (state !== "current" && state !== "held" && state !== "expired") {
throw new M4ThreatContractError("M4.6 temporal state: неизвестное состояние.");
}
return {
componentId: text(value.component_id, "M4.6 visual component"),
state,
motion: motion(value.motion),
centroidBodyXyzM: vector(value.centroid_body_xyz_m, 3, "M4.6 centroid") as [number, number, number],
cellCentersBodyXyzM: array(value.cell_centers_body_xyz_m, "M4.6 cells").map(
(point) => vector(point, 3, "M4.6 cell") as [number, number, number],
),
assessment: parseAssessment(value.assessment),
};
}),
cameraProposals: array(item.camera_proposals, "M4.6 camera proposals").map(parseCameraProposal),
rig: {
lengthM: number(rig.length_m, "M4.6 rig length"),
widthM: number(rig.width_m, "M4.6 rig width"),
nominalSensorHeightM: number(rig.nominal_sensor_height_m, "M4.6 sensor height"),
},
corridor: {
forwardLengthM: number(corridor.forward_length_m, "M4.6 forward corridor"),
rearMarginM: number(corridor.rear_margin_m, "M4.6 rear corridor"),
halfWidthM: number(corridor.half_width_m, "M4.6 half width"),
predictionHorizonSeconds: number(corridor.prediction_horizon_seconds, "M4.6 visual horizon"),
},
};
}
export async function fetchM4ThreatVideoOverlay(
result: string,
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<M4ThreatVideoOverlay> {
const response = await fetcher(
`/api/v1/laboratory/m4-threat/results/${result}/video-overlay`,
{ headers: { Accept: "application/json" }, signal },
);
if (!response.ok) throw new M4ThreatContractError(`M4.6 video overlay: HTTP ${response.status}.`);
const payload = object(await response.json(), "M4.6 video overlay");
exact(payload.schema_version, "missioncore.m4-threat-video-overlay/v1", "M4.6 video schema");
exact(payload.result_id, result, "M4.6 video result");
exact(payload.authority, "replay-simulated", "M4.6 video authority");
const recorded = object(payload.recorded_source, "M4.6 recorded source");
exact(
recorded.session_id,
"20260720T065719Z_viewer_live",
"M4.6 recorded session",
);
const frames = array(payload.frames, "M4.6 video frames").map((raw, expectedIndex) => {
const item = object(raw, "M4.6 video frame");
const frameIndex = integer(item.frame_index, "M4.6 video index");
if (frameIndex !== expectedIndex) throw new M4ThreatContractError("M4.6 video order.");
const counts = object(item.decision_counts, "M4.6 video decisions");
return {
frameIndex,
sessionSeconds: number(item.session_seconds, "M4.6 video time"),
sourceAvailable: typeof item.source_available === "boolean" ? item.source_available : false,
cameraProposals: array(item.camera_proposals, "M4.6 video proposals").map(parseCameraProposal),
decisionCounts: {
threat: integer(counts.threat, "M4.6 video threat"),
"not-threat": integer(counts["not-threat"], "M4.6 video clear"),
unknown: integer(counts.unknown, "M4.6 video unknown"),
},
};
});
exact(payload.frame_count, 4489, "M4.6 video frame count");
return {
resultId: result,
recordedSourceSessionId: "20260720T065719Z_viewer_live",
imageWidth: exact(payload.image_width, 800, "M4.6 image width"),
imageHeight: exact(payload.image_height, 600, "M4.6 image height"),
timelineStartSeconds: number(payload.timeline_start_seconds, "M4.6 video start"),
timelineEndSeconds: number(payload.timeline_end_seconds, "M4.6 video end"),
frames,
};
}
export function selectM4ThreatVideoFrame(
frames: readonly M4ThreatVideoFrame[],
seconds: number,
): M4ThreatVideoFrame | null {
if (!frames.length) return null;
let low = 0;
let high = frames.length - 1;
while (low < high) {
const middle = Math.floor((low + high) / 2);
const current = frames[middle];
if (!current || current.sessionSeconds < seconds) low = middle + 1;
else high = middle;
}
const current = frames[low] ?? frames[frames.length - 1] ?? null;
const previous = frames[Math.max(0, low - 1)] ?? null;
if (!current || !previous) return current;
return Math.abs(previous.sessionSeconds - seconds) <= Math.abs(current.sessionSeconds - seconds)
? previous
: current;
}
+1
View File
@@ -9,6 +9,7 @@
@import "./styles/laboratory-reporting.css";
@import "./styles/laboratory-evidence-report.css";
@import "./styles/e34-temporal-layer.css";
@import "./styles/m4-replay-threat.css";
@import "./styles/e35-degradation-recovery.css";
@import "./styles/e30-human-review.css";
@import "./styles/spatial.css";
@@ -46,7 +46,7 @@
user-select: none;
}
.e46c-video-scene {
.recorded-evidence-video-scene {
position: relative;
width: 100%;
height: 100%;
@@ -55,12 +55,12 @@
background: var(--nodedc-canvas);
}
.e46c-video-scene > .recorded-media-player {
.recorded-evidence-video-scene > .recorded-media-player {
position: absolute;
inset: 0;
}
.e46c-video-scene > canvas {
.recorded-evidence-video-scene > canvas {
position: absolute;
z-index: 2;
inset: 0;
@@ -0,0 +1,110 @@
.laboratory-metric-evidence-scene,
.laboratory-metric-evidence-scene__viewport {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.laboratory-metric-evidence-scene {
position: relative;
overflow: hidden;
background: var(--nodedc-canvas);
}
.laboratory-metric-evidence-scene__viewport {
position: absolute;
inset: 0;
}
.laboratory-metric-evidence-scene__viewport canvas {
display: block;
width: 100%;
height: 100%;
cursor: grab;
touch-action: none;
}
.laboratory-metric-evidence-scene__viewport canvas:active {
cursor: grabbing;
}
.laboratory-metric-evidence-scene__viewport p {
position: absolute;
inset: 0;
display: grid;
place-items: center;
margin: 0;
color: var(--nodedc-text-muted);
font-size: 0.62rem;
}
.laboratory-metric-evidence-scene__toolbar {
position: absolute;
z-index: 3;
top: 0.6rem;
left: 0.6rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.laboratory-metric-evidence-scene__toolbar .nodedc-button,
.laboratory-metric-evidence-scene__toolbar > span,
.laboratory-metric-evidence-scene__legend {
background: var(--nodedc-floating-surface);
backdrop-filter: blur(var(--nodedc-blur-control));
}
.laboratory-metric-evidence-scene__toolbar > span {
border-radius: var(--nodedc-radius-control-compact);
padding: 0.43rem 0.55rem;
color: var(--nodedc-text-secondary);
font-size: 0.5rem;
}
.laboratory-metric-evidence-scene__legend {
position: absolute;
z-index: 3;
right: 0.6rem;
bottom: 0.6rem;
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
border-radius: var(--nodedc-radius-control-compact);
padding: 0.42rem 0.55rem;
color: var(--nodedc-text-secondary);
}
.laboratory-metric-evidence-scene__legend span {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.5rem;
}
.laboratory-metric-evidence-scene__legend span::before {
width: 0.38rem;
height: 0.38rem;
border-radius: 50%;
background: var(--nodedc-text-muted);
content: "";
}
.laboratory-metric-evidence-scene__legend span[data-decision="threat"]::before {
background: rgb(var(--nodedc-danger-rgb));
}
.laboratory-metric-evidence-scene__legend span[data-decision="not-threat"]::before {
background: rgb(var(--nodedc-success-rgb));
}
.laboratory-metric-evidence-scene__legend span[data-decision="unknown"]::before {
background: rgb(var(--nodedc-warning-rgb));
}
@media (max-width: 900px) {
.laboratory-metric-evidence-scene__toolbar > span {
display: none;
}
}
@@ -39,6 +39,7 @@ import { E46GRectifiedDetectorBakeoffResultView } from "./E46GRectifiedDetectorB
import { E46HFullRectifiedFrontReplayResultView } from "./E46HFullRectifiedFrontReplayResult";
import { E46IGroundingDinoFullReplayResultView } from "./E46IGroundingDinoFullReplayResult";
import { E46JRawFisheyeRealtimeResultView } from "./E46JRawFisheyeRealtimeResult";
import { M4ReplayThreatResultView } from "./M4ReplayThreatResult";
export { isAdvancedLaboratoryWorkId };
export type { AdvancedLaboratoryWorkId };
@@ -81,6 +82,9 @@ export function AdvancedLaboratoryResult({
failedSessionId: string | null;
replayError: string | null;
}) {
if (workId === "m4-replay-threat" && results.m4Threat) {
return <M4ReplayThreatResultView rigLabel={rigLabel} result={results.m4Threat} />;
}
if (workId === "l3-pointpillars-visual-audit" && results.l3) {
return <L3PointPillarsResult result={results.l3} />;
}
@@ -1,9 +1,12 @@
import { useEffect, useMemo, useRef } from "react";
import { useMemo } from "react";
import {
RecordedFmp4Player,
type RecordedObservationPlayback,
} from "../../components/RecordedFmp4Player";
import {
RecordedEvidenceVideoScene,
type RecordedEvidenceBox,
} from "../../components/laboratory/RecordedEvidenceVideoScene";
import {
selectE46CVideoFrame,
type E46CMotionState,
@@ -11,26 +14,10 @@ import {
} from "../../core/laboratory/e46cFullReplayWorldTracks";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
function color(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
alpha = 1,
): string {
const channels = getComputedStyle(host)
.getPropertyValue(token)
.trim()
.match(/[\d.]+/g)
?.slice(0, 3)
.map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
function stateColor(host: HTMLElement, state: E46CMotionState): string {
if (state === "dynamic") return color(host, "--nodedc-accent-rgb", [232, 56, 126]);
if (state === "static") return color(host, "--nodedc-success-rgb", [181, 255, 90]);
return color(host, "--nodedc-warning-rgb", [255, 197, 92]);
function stateTone(state: E46CMotionState): RecordedEvidenceBox["tone"] {
if (state === "dynamic") return "accent";
if (state === "static") return "success";
return "warning";
}
function stateLabel(state: E46CMotionState): string {
@@ -50,97 +37,40 @@ export function E46CRecordedVideoScene({
playback: RecordedObservationPlayback;
onPlaybackChange: (playback: RecordedObservationPlayback) => void;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const frame = useMemo(
() => selectE46CVideoFrame(overlay.frames, playback.currentSeconds),
[overlay.frames, playback.currentSeconds],
);
useEffect(() => {
const host = hostRef.current;
const canvas = canvasRef.current;
if (!host || !canvas) 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 (!frame) return;
const scale = Math.min(
width / overlay.imageWidth,
height / overlay.imageHeight,
);
const drawWidth = overlay.imageWidth * scale;
const drawHeight = overlay.imageHeight * scale;
const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
for (const item of frame.objects) {
const [left, top, right, bottom] = item.boxXyxy;
const x = offsetX + left * scale;
const y = offsetY + top * scale;
const boxWidth = (right - left) * scale;
const boxHeight = (bottom - top) * scale;
const stroke = stateColor(host, item.motionState);
context.strokeStyle = stroke;
context.lineWidth = Math.max(1.5, 2 * scale);
context.setLineDash(item.cameraEvidenceCurrent ? [] : [5, 4]);
context.strokeRect(x, y, boxWidth, boxHeight);
context.setLineDash([]);
const identity = `S${item.routeTrackId}${
item.worldTrackId === null ? "" : `→W${item.worldTrackId}`
}`;
const label = `${identity} · ${item.displayCategory} · ${stateLabel(
const boxes = useMemo<readonly RecordedEvidenceBox[]>(() => (
frame?.objects.map((item) => {
const identity = `S${item.routeTrackId}${
item.worldTrackId === null ? "" : `→W${item.worldTrackId}`
}`;
return {
boxXyxy: item.boxXyxy,
label: `${identity} · ${item.displayCategory} · ${stateLabel(
item.motionState,
)} · ${Math.round(item.score * 100)}%`;
const fontSize = Math.max(9, 10 * scale);
context.font = `650 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = context.measureText(label).width + 8;
const labelHeight = fontSize + 6;
const labelX = Math.min(
offsetX + drawWidth - labelWidth,
Math.max(offsetX, x),
);
const labelY = Math.max(offsetY, y - labelHeight);
context.fillStyle = color(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.9);
context.fillRect(labelX, labelY, labelWidth, labelHeight);
context.fillStyle = stroke;
context.fillText(label, labelX + 4, labelY + fontSize + 1);
}
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [frame, overlay.imageHeight, overlay.imageWidth]);
)} · ${Math.round(item.score * 100)}%`,
tone: stateTone(item.motionState),
dashed: !item.cameraEvidenceCurrent,
};
}) ?? []
), [frame]);
return (
<div className="e46c-video-scene" ref={hostRef}>
<RecordedFmp4Player
source={source}
playback={playback}
interactive
prepare
onPlaybackChange={onPlaybackChange}
/>
<canvas
ref={canvasRef}
role="img"
aria-label={
frame
? `E46C video frame ${frame.frameIndex}: ${frame.objects.length} route objects`
: "E46C recorded video overlay"
}
/>
</div>
<RecordedEvidenceVideoScene
source={source}
playback={playback}
imageWidth={overlay.imageWidth}
imageHeight={overlay.imageHeight}
boxes={boxes}
ariaLabel={
frame
? `E46C video frame ${frame.frameIndex}: ${frame.objects.length} route objects`
: "E46C recorded video overlay"
}
onPlaybackChange={onPlaybackChange}
/>
);
}
@@ -0,0 +1,138 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { M4ThreatReplayResult } from "../../core/laboratory/m4ReplayThreat";
import { formatNumber } from "../../presentation";
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
export function M4ReplayThreatResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: M4ThreatReplayResult;
}) {
const metrics = result.metrics;
const totalAssessments = Object.values(metrics.decisions).reduce(
(sum, value) => sum + value,
0,
);
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="M4.6 · dual-evidence threat replay"
description="Camera и LiDAR дают независимые доказательства, после чего один source-neutral слой оценивает пересечение виртуального коридора, ближайшее сближение и TTC. Ни один сенсор не назначен first."
status="4489/4489 · replay-simulated · accepted"
statusTone="warning"
facts={[
{
label: "Конфигурация",
value: `${rigLabel} · RIGHT camera + LiDAR geometry · recorded replay`,
},
{
label: "Виртуальный корпус",
value: `${result.configuration.virtualBodyM[0]}×${result.configuration.virtualBodyM[1]} м · LiDAR ${result.configuration.nominalSensorHeightM} м`,
},
{
label: "Коридор",
value: `${result.configuration.forwardCorridorM} м · horizon ${result.configuration.predictionHorizonSeconds} с`,
},
{
label: "Визуал",
value: "4489-frame VIDEO · 32 exact CAMERA/3D/PLAN samples",
},
]}
brief={{
question: "Может ли единый слой обнаруживать потенциальное препятствие по двум независимым источникам, не теряя LiDAR-only объекты и не объявляя camera-only наблюдение безопасным?",
approach: "Все 4489 кадров RAVNOVES00 повторно пропущены через неизменяемые detector, metric geometry и temporal ledgers. Geometry-only объекты получают метрическую оценку; camera-only и stale/held остаются unknown. Отдельная матрица из 9 детерминированных сценариев проверяет статические, сближающиеся и расходящиеся случаи.",
principalResult: `${metrics.evidence.currentMetric.toLocaleString("ru-RU")} current metric и ${metrics.evidence.cameraOnly.toLocaleString("ru-RU")} camera-only наблюдений учтены; ${metrics.reasonCounts["geometry-only-evidence"]?.toLocaleString("ru-RU") ?? "0"} geometry-only оценок не потеряны. Критические fixtures: ${metrics.fixtures.passed}/${metrics.fixtures.total}, ложных safe: ${metrics.fixtures.criticalFalseNotThreat}.`,
limitation: "Корпус и коридор пока виртуальные, replay не является live-проходом или физическим collision test. Постоянная скорость — ограниченная модель, а independent object truth остаётся следующим gate.",
}}
method={{
completeness: "complete",
executionClass: "hybrid",
pipelineId: "dual-evidence-replay-threat/v1",
components: [
{
kind: "source",
name: result.sourceResultIds.detector,
version: "frozen camera proposals",
role: "независимое image-space evidence без safety authority",
identitySha256: result.sourceResultIds.detector.split("-").at(-1) ?? null,
},
{
kind: "source",
name: result.sourceResultIds.geometry,
version: "frozen metric geometry",
role: "LiDAR occupied components и camera association",
identitySha256: result.sourceResultIds.geometry.split("-").at(-1) ?? null,
},
{
kind: "source",
name: result.sourceResultIds.temporal,
version: "frozen temporal object map",
role: "current / held / expired и bounded motion history",
identitySha256: result.sourceResultIds.temporal.split("-").at(-1) ?? null,
},
{
kind: "algorithm",
name: "dual-evidence virtual corridor",
version: result.profileId,
role: "classless corridor intersection, closest approach and TTC",
identitySha256: result.resultId.split("-").at(-1) ?? null,
},
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="M4.6 VISUAL EVIDENCE · VIDEO / CAMERA / 3D / PLAN"
title="Синхронный контроль рамок, расстояний, облака точек и виртуального коридора"
kind="diagnostic-model"
resizable
>
<M4ReplayThreatVisual resultId={result.resultId} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Dual-evidence слой готов к следующей CV-итерации на recorded replay"
status="Replay gate accepted · physical authority withheld"
statusTone="warning"
metrics={[
{
label: "Replay frames",
value: "4489/4489",
hint: `${formatNumber(metrics.runtime.framesPerSecond, 1)} FPS offline`,
},
{
label: "Metric evidence",
value: metrics.evidence.currentMetric.toLocaleString("ru-RU"),
hint: `${metrics.reasonCounts["geometry-only-evidence"]?.toLocaleString("ru-RU") ?? "0"} geometry-only`,
},
{
label: "Threat / clear",
value: `${metrics.decisions.threat.toLocaleString("ru-RU")} / ${metrics.decisions["not-threat"].toLocaleString("ru-RU")}`,
hint: `${totalAssessments.toLocaleString("ru-RU")} assessments accounted`,
},
{
label: "Critical false-safe",
value: String(metrics.fixtures.criticalFalseNotThreat),
hint: `${metrics.fixtures.passed}/${metrics.fixtures.total} deterministic fixtures passed`,
},
]}
conclusion={{
proved: "На неизменяемом RAVNOVES00 каждый metric, stale/held и camera-only объект получил ровно одну консервативную оценку. Geometry-only препятствия участвуют в threat-решении без класса, camera-only и просроченные данные не превращаются в safe. Видео, точные camera samples и метрическое 3D-доказательство доступны в одном viewer.",
notProved: "Не доказаны live realtime, измеренная геометрия физического корпуса, независимая object-level правильность, навигационная или safety-пригодность и выдача команд.",
decision: "Сохранить dual-evidence provider как канонический replay seam и переходить к независимому object-centric gate; физическую геометрию и live/actuation authority не смешивать с дальнейшей CV-разработкой.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,385 @@
import { useEffect, useMemo, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import type { RecordedObservationPlayback } from "../../components/RecordedFmp4Player";
import {
LaboratoryMetricEvidenceScene,
type LaboratoryMetricSceneMode,
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
RecordedEvidenceVideoScene,
type RecordedEvidenceBox,
} from "../../components/laboratory/RecordedEvidenceVideoScene";
import {
fetchM4ThreatVideoOverlay,
fetchM4ThreatVisual,
fetchM4ThreatVisualIndex,
selectM4ThreatVideoFrame,
type M4ThreatCameraProposal,
type M4ThreatVideoOverlay,
type M4ThreatVisualFrame,
type M4ThreatVisualIndexItem,
} from "../../core/laboratory/m4ReplayThreat";
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
import { replayObservationSession } from "../../core/observation/sessionArchive";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
type M4ThreatViewMode = "video" | "camera" | LaboratoryMetricSceneMode;
function toneForProposal(proposal: M4ThreatCameraProposal): RecordedEvidenceBox["tone"] {
if (proposal.threatDecision === "threat") return "danger";
if (proposal.threatDecision === "not-threat") return "success";
if (proposal.threatDecision === "unknown") return "warning";
return proposal.occupiedSupport ? "accent" : "warning";
}
function proposalLabel(proposal: M4ThreatCameraProposal): string {
const decision = proposal.threatDecision ?? "unknown";
if (proposal.rangeM === null) return decision;
const range = `${proposal.rangeM.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} м`;
return `${range} · ${decision}`;
}
function boxes(proposals: readonly M4ThreatCameraProposal[]): readonly RecordedEvidenceBox[] {
return proposals.map((proposal) => ({
boxXyxy: proposal.bboxXyxy,
label: proposalLabel(proposal),
tone: toneForProposal(proposal),
dashed: !proposal.occupiedSupport,
}));
}
function message(error: unknown, fallback: string): string {
return error instanceof Error && error.message.trim() ? error.message : fallback;
}
export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
const [mode, setMode] = useState<M4ThreatViewMode>("video");
const [expanded, setExpanded] = useState(false);
const [index, setIndex] = useState<readonly M4ThreatVisualIndexItem[]>([]);
const [ordinal, setOrdinal] = useState(1);
const [frame, setFrame] = useState<M4ThreatVisualFrame | null>(null);
const [sampleLoading, setSampleLoading] = useState(true);
const [sampleError, setSampleError] = useState<string | null>(null);
const [videoOverlay, setVideoOverlay] = useState<M4ThreatVideoOverlay | null>(null);
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
const [videoLoading, setVideoLoading] = useState(false);
const [videoError, setVideoError] = useState<string | null>(null);
const [videoPlayback, setVideoPlayback] = useState<RecordedObservationPlayback>({
currentSeconds: 0,
playing: false,
});
useEffect(() => {
const controller = new AbortController();
setSampleLoading(true);
setSampleError(null);
void fetchM4ThreatVisualIndex(resultId, { signal: controller.signal })
.then((items) => {
if (!controller.signal.aborted) setIndex(items);
})
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
setSampleError(message(caught, "Индекс визуальных кадров M4.6 недоступен."));
}
});
return () => controller.abort();
}, [resultId]);
useEffect(() => {
const controller = new AbortController();
setSampleLoading(true);
setSampleError(null);
setFrame(null);
void fetchM4ThreatVisual(resultId, ordinal, { signal: controller.signal })
.then((next) => {
if (!controller.signal.aborted) setFrame(next);
})
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
setSampleError(message(caught, "Метрический visual M4.6 недоступен."));
}
})
.finally(() => {
if (!controller.signal.aborted) setSampleLoading(false);
});
return () => controller.abort();
}, [ordinal, resultId]);
useEffect(() => {
if ((mode !== "video" && mode !== "camera") || (videoOverlay && videoSource)) return;
const controller = new AbortController();
setVideoLoading(true);
setVideoError(null);
void (async () => {
const overlay = await fetchM4ThreatVideoOverlay(resultId, {
signal: controller.signal,
});
const replay = await replayObservationSession(overlay.recordedSourceSessionId, {
signal: controller.signal,
});
if (replay.kind !== "ready") {
throw new Error("RIGHT-видео RAVNOVES00 ещё готовится к воспроизведению.");
}
const source = recordedObservationSources(replay.launch).find(
(candidate) =>
candidate.modality === "video" &&
candidate.semanticChannelId === "camera.video.recorded",
);
const delivery = source?.delivery?.kind === "recorded-fmp4-manifest"
? source.delivery
: null;
if (
!source ||
!delivery ||
delivery.timelineStartSeconds !== overlay.timelineStartSeconds ||
delivery.timelineEndSeconds < overlay.timelineEndSeconds
) {
throw new Error("RIGHT-видео не совпало с временным контрактом M4.6.");
}
if (controller.signal.aborted) return;
setVideoOverlay(overlay);
setVideoSource(source);
setVideoPlayback({
currentSeconds: overlay.timelineStartSeconds,
playing: false,
});
})()
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
setVideoError(message(caught, "Видео-доказательство M4.6 недоступно."));
}
})
.finally(() => {
if (!controller.signal.aborted) setVideoLoading(false);
});
return () => controller.abort();
}, [mode, resultId, videoOverlay, videoSource]);
useEffect(() => {
if (mode !== "camera" || !frame) return;
setVideoPlayback({
currentSeconds: frame.sourceTimeNs / 1_000_000_000,
playing: false,
});
}, [frame, mode]);
const activeVideoFrame = useMemo(
() => videoOverlay
? selectM4ThreatVideoFrame(videoOverlay.frames, videoPlayback.currentSeconds)
: null,
[videoOverlay, videoPlayback.currentSeconds],
);
const activeProposals = mode === "camera"
? frame?.cameraProposals ?? []
: activeVideoFrame?.cameraProposals ?? [];
const activeBoxes = useMemo(() => boxes(activeProposals), [activeProposals]);
const selectedItem = index.find((item) => item.ordinal === ordinal) ?? null;
const threatObstacles = frame?.metricObstacles.filter(
(item) => item.assessment.decision === "threat",
) ?? [];
const nearest = frame?.metricObstacles
.map((item) => item.assessment.closestApproachM)
.filter((value): value is number => value !== null)
.sort((left, right) => left - right)[0] ?? null;
const seekVideo = (seconds: number) => {
if (!videoOverlay) return;
setVideoPlayback({
currentSeconds: Math.min(
videoOverlay.timelineEndSeconds,
Math.max(videoOverlay.timelineStartSeconds, seconds),
),
playing: false,
});
};
const navigate = (offset: -1 | 1) => {
const count = Math.max(index.length, 32);
setOrdinal((current) => ((current - 1 + offset + count) % count) + 1);
};
const actions = mode === "video" ? (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Назад на 5 секунд" onClick={() => seekVideo(videoPlayback.currentSeconds - 5)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Вперёд на 5 секунд" onClick={() => seekVideo(videoPlayback.currentSeconds + 5)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Перейти к метрическому sample M4.6"
value={String(ordinal)}
options={(index.length ? index : Array.from({ length: 32 }, (_, position) => ({
ordinal: position + 1,
sequence: position,
frameId: "",
sourceTimeNs: 0,
metricObstacleCount: 0,
cameraProposalCount: 0,
pointCloudSampleCount: 0,
}))).map((item) => ({
value: String(item.ordinal),
label: `${item.ordinal}/32 · frame ${item.sequence} · ${item.metricObstacleCount} metric objects`,
}))}
variant="split"
menuWidth="anchor"
searchable
searchPlaceholder="Найти sample"
onChange={(value) => {
const nextOrdinal = Number(value);
const target = index.find((item) => item.ordinal === nextOrdinal);
setOrdinal(nextOrdinal);
if (target) seekVideo(target.sourceTimeNs / 1_000_000_000);
}}
/>
</div>
) : (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущий sample M4.6" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующий sample M4.6" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать sample M4.6"
value={String(ordinal)}
options={index.map((item) => ({
value: String(item.ordinal),
label: `${item.ordinal}/32 · frame ${item.sequence} · ${item.metricObstacleCount} metric · ${item.cameraProposalCount} camera`,
}))}
variant="split"
menuWidth="anchor"
searchable
searchPlaceholder="Найти sample"
onChange={(value) => setOrdinal(Number(value))}
/>
</div>
);
const overlay = mode === "video" && videoOverlay ? (
<div className="l3-visual-audit__overlay l3-visual-audit__overlay--video">
<div>
<span>RAVNOVES00 · recorded RIGHT</span>
<strong>
+{(videoPlayback.currentSeconds - videoOverlay.timelineStartSeconds).toFixed(1)} с
{activeVideoFrame ? ` · frame ${activeVideoFrame.frameIndex}` : ""}
</strong>
<small>{videoPlayback.playing ? "воспроизведение" : "пауза / seek"}</small>
</div>
<div>
<span>Camera evidence</span>
<strong>{activeVideoFrame?.cameraProposals.length ?? 0} рамок · distance при LiDAR support</strong>
<small>пунктир = camera-only · всегда unknown</small>
</div>
<div>
<span>Replay decision</span>
<strong>
{activeVideoFrame?.decisionCounts.threat ?? 0} threat · {activeVideoFrame?.decisionCounts["not-threat"] ?? 0} clear · {activeVideoFrame?.decisionCounts.unknown ?? 0} unknown
</strong>
<small>REPLAY-SIMULATED · не live и не safety authority</small>
</div>
</div>
) : frame ? (
<div className="l3-visual-audit__overlay">
<div>
<span>RAVNOVES00 · exact replay sample</span>
<strong>frame {frame.sequence} · sample {frame.ordinal}/32</strong>
<small>{(frame.sourceTimeNs / 1_000_000_000).toFixed(3)} с · {selectedItem?.frameId}</small>
</div>
<div>
<span>Dual evidence</span>
<strong>{frame.metricObstacles.length} metric · {frame.cameraProposals.length} camera</strong>
<small>{frame.pointCloudSampleCount}/{frame.pointCloudSourceCount} LiDAR points shown</small>
</div>
<div>
<span>Virtual corridor</span>
<strong>{threatObstacles.length} threat · nearest {nearest === null ? "—" : `${nearest.toFixed(2)} м`}</strong>
<small>{frame.corridor.forwardLengthM} м · body {frame.rig.lengthM}×{frame.rig.widthM} м · REPLAY-SIMULATED</small>
</div>
</div>
) : undefined;
let content;
if (mode === "video" || mode === "camera") {
content = videoLoading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Связываем 4489 решений M4.6 с RIGHT-видео</span>
</div>
) : videoError || !videoOverlay || !videoSource ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{videoError ?? "Видео-доказательство M4.6 недоступно."}</span>
</div>
) : (
<RecordedEvidenceVideoScene
source={videoSource}
playback={videoPlayback}
imageWidth={videoOverlay.imageWidth}
imageHeight={videoOverlay.imageHeight}
boxes={activeBoxes}
ariaLabel={
mode === "camera"
? `M4.6 exact camera sample ${ordinal}: ${activeBoxes.length} proposals`
: `M4.6 full video frame ${activeVideoFrame?.frameIndex ?? 0}: ${activeBoxes.length} proposals`
}
onPlaybackChange={setVideoPlayback}
/>
);
} else {
content = sampleLoading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем синхронное облако точек M4.6</span>
</div>
) : sampleError || !frame ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{sampleError ?? "Метрический visual M4.6 недоступен."}</span>
</div>
) : (
<LaboratoryMetricEvidenceScene
pointCloudBodyXyzM={frame.pointCloudBodyXyzM}
obstacles={frame.metricObstacles.map((obstacle) => ({
id: obstacle.componentId,
decision: obstacle.assessment.decision,
state: obstacle.state,
centroidBodyXyzM: obstacle.centroidBodyXyzM,
cellCentersBodyXyzM: obstacle.cellCentersBodyXyzM,
}))}
rig={frame.rig}
corridor={frame.corridor}
mode={mode}
label={`M4.6 metric point cloud, frame ${frame.sequence}`}
/>
);
}
return (
<div className="l3-visual-audit m4-replay-threat-visual">
<LaboratoryEvidenceViewer
label="M4.6 dual-evidence replay: video, camera and metric 3D"
mode={mode}
modes={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
{content}
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -6,6 +6,7 @@ import type {
import type { ObservationSessionSummary } from "../../core/observation/sessionArchive";
export type LaboratoryProfileId =
| "rig-dual-evidence-virtual-corridor-v1"
| "rig-camera-local-surface-v1"
| "rig-track-geometry-temporal-v1"
| "rig-ravnoves-perception-gate-v1"
@@ -56,6 +57,13 @@ interface KnownWorkDefinition {
const rig = (rigLabel: string): string => rigLabel.trim() || "Сенсорный риг";
const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`>, KnownWorkDefinition>> = {
"m4-replay-threat": {
profileId: "rig-dual-evidence-virtual-corridor-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`,
experimentId: "m4-ravnoves00-dual-evidence-threat",
experimentName: "RAVNOVES00 dual-evidence threat qualification",
variantName: "M4.6 · virtual corridor replay · VIDEO/CAMERA/3D",
},
"e28-local-surface": {
profileId: "rig-camera-local-surface-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera-first + local-surface LiDAR`,
@@ -18,6 +18,7 @@ function mergeResults(
next: AdvancedLaboratoryResults,
): AdvancedLaboratoryResults {
return {
m4Threat: next.m4Threat ?? current.m4Threat,
l3: next.l3 ?? current.l3,
l31: next.l31 ?? current.l31,
l32: next.l32 ?? current.l32,
@@ -91,8 +91,8 @@ test("E46C decodes the complete path-free temporal video overlay", async () => {
assert.equal(selectE46CVideoFrame(overlay.frames, overlay.frames[20].sessionSeconds).frameIndex, 20);
});
test("E46C viewer opens with full VIDEO and reuses the admitted recorded player", async () => {
const [visual, videoScene, player] = await Promise.all([
test("E46C viewer opens with full VIDEO and reuses the shared recorded overlay scene", async () => {
const [visual, videoScene, sharedScene, player] = await Promise.all([
readFile(
new URL(
"../src/workspaces/laboratory/E46CFullReplayWorldTracksVisual.tsx",
@@ -107,14 +107,22 @@ test("E46C viewer opens with full VIDEO and reuses the admitted recorded player"
),
"utf8",
),
readFile(
new URL(
"../src/components/laboratory/RecordedEvidenceVideoScene.tsx",
import.meta.url,
),
"utf8",
),
readFile(new URL("../src/components/RecordedFmp4Player.tsx", import.meta.url), "utf8"),
]);
assert.match(visual, /useState<E46CViewMode>\("video"\)/);
assert.match(visual, /\{ value: "video", label: "VIDEO" \}/);
assert.match(visual, /replayObservationSession\(overlay\.recordedSourceSessionId/);
assert.match(videoScene, /<RecordedFmp4Player/);
assert.match(videoScene, /<RecordedEvidenceVideoScene/);
assert.match(videoScene, /selectE46CVideoFrame/);
assert.match(sharedScene, /<RecordedFmp4Player/);
assert.match(player, /requestVideoFrameCallback/);
assert.match(player, /controls=\{interactive\}/);
});
@@ -0,0 +1,187 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let fetchM4ThreatReplayResult;
let fetchM4ThreatVisual;
let fetchM4ThreatVideoOverlay;
let selectM4ThreatVideoFrame;
const resultId = `m4-threat-replay-${"a".repeat(64)}`;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
fetchM4ThreatReplayResult,
fetchM4ThreatVisual,
fetchM4ThreatVideoOverlay,
selectM4ThreatVideoFrame,
} = await server.ssrLoadModule("/src/core/laboratory/m4ReplayThreat.ts"));
});
after(async () => {
await server?.close();
});
function proposal(overrides = {}) {
return {
proposal_id: "proposal-1",
bbox_xyxy: [100, 120, 240, 360],
objectness: 0.91,
semantic_hint: "person",
occupied_support: false,
range_m: null,
threat_decision: "unknown",
threat_reason_codes: ["camera-only-no-metric-geometry"],
...overrides,
};
}
test("M4.6 decodes accepted dual-evidence result without physical authority", async () => {
const result = await fetchM4ThreatReplayResult({
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.m4-threat-replay-catalog/v1",
items: [{
schema_version: "missioncore.m4-threat-replay-view/v1",
result_id: resultId,
created_at_utc: "2026-08-05T15:36:01.553Z",
status: "accepted",
profile_id: "m4-ravnoves00-virtual-corridor/v1",
rig_profile_id: "virtual-handheld-body-1000x600/v1",
corridor_profile_id: "ravnoves00-forward-corridor-8m/v1",
source_result_ids: {
detector: `m4-detector-replay-${"b".repeat(64)}`,
geometry: `m4-geometry-replay-${"c".repeat(64)}`,
temporal: `m4-temporal-replay-${"d".repeat(64)}`,
},
metrics: {
decisions: { threat: 8010, "not-threat": 6610, unknown: 60832 },
evidence: { "camera-only": 10158, "current-metric": 27299, "stale-or-held": 37995 },
fixtures: { critical: 4, critical_false_not_threat: 0, passed: 9, total: 9 },
runtime: {
frames_per_second: 116.4,
provider_latency_p50_ms: 4.3,
provider_latency_p95_ms: 19.8,
provider_latency_max_ms: 194.3,
},
reason_counts: { "geometry-only-evidence": 21958 },
},
configuration: {
virtual_body_m: [1, 0.6],
nominal_sensor_height_m: 1.25,
forward_corridor_m: 8,
prediction_horizon_seconds: 5,
},
limitations: ["replay only"],
accepted: true,
authority: "replay-simulated",
physical_collision_accepted: false,
actuation_allowed: false,
}],
}), { status: 200 }),
});
assert.equal(result.resultId, resultId);
assert.equal(result.metrics.evidence.currentMetric, 27299);
assert.equal(result.metrics.fixtures.criticalFalseNotThreat, 0);
assert.deepEqual(result.configuration.virtualBodyM, [1, 0.6]);
});
test("M4.6 binds exact CAMERA and metric 3D evidence to one replay frame", async () => {
const frame = await fetchM4ThreatVisual(resultId, 1, {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.perception-threat-visual-frame/v1",
result_id: resultId,
ordinal: 1,
sequence: 20,
frame_id: "frame-000020",
source_time_ns: 37421857292,
point_cloud_body_xyz_m: [[1, 0, 0.1], [2, 0.2, 0.3]],
point_cloud_source_count: 12000,
point_cloud_sample_count: 2,
metric_obstacles: [{
component_id: "temporal-20-1",
state: "current",
motion: "moving",
centroid_body_xyz_m: [2, 0.1, 0.4],
cell_centers_body_xyz_m: [[2, 0.1, 0.4]],
assessment: {
component_id: "temporal-20-1",
decision: "threat",
corridor_intersection: "intersects",
relative_speed_mps: 1.2,
closest_approach_m: 0.4,
ttc_seconds: 1.6,
reason_codes: ["geometry-only-evidence"],
},
}],
camera_proposals: [proposal()],
rig: { length_m: 1, width_m: 0.6, nominal_sensor_height_m: 1.25 },
corridor: {
forward_length_m: 8,
rear_margin_m: 0.5,
half_width_m: 0.5,
prediction_horizon_seconds: 5,
},
}), { status: 200 }),
});
assert.equal(frame.sequence, 20);
assert.equal(frame.metricObstacles[0].assessment.decision, "threat");
assert.equal(frame.cameraProposals[0].threatDecision, "unknown");
assert.equal(frame.pointCloudSampleCount, 2);
});
test("M4.6 full video preserves camera-only unknown and nearest-frame selection", async () => {
const overlay = await fetchM4ThreatVideoOverlay(resultId, {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.m4-threat-video-overlay/v1",
result_id: resultId,
recorded_source: { session_id: "20260720T065719Z_viewer_live" },
image_width: 800,
image_height: 600,
timeline_start_seconds: 35.421857292,
timeline_end_seconds: 484.044857292,
frame_count: 4489,
frames: [
{
frame_index: 0,
session_seconds: 35.421857292,
source_available: true,
camera_proposals: [],
decision_counts: { threat: 0, "not-threat": 0, unknown: 0 },
},
{
frame_index: 1,
session_seconds: 35.521857292,
source_available: true,
camera_proposals: [proposal()],
decision_counts: { threat: 0, "not-threat": 0, unknown: 1 },
},
],
authority: "replay-simulated",
}), { status: 200 }),
});
assert.equal(overlay.frames[1].cameraProposals[0].rangeM, null);
assert.equal(selectM4ThreatVideoFrame(overlay.frames, 35.50).frameIndex, 1);
});
test("M4.6 viewer reuses shared video and metric evidence renderers", async () => {
const [visual, videoScene, metricScene] = await Promise.all([
readFile(new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/RecordedEvidenceVideoScene.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/LaboratoryMetricEvidenceScene.tsx", import.meta.url), "utf8"),
]);
assert.match(visual, /<RecordedEvidenceVideoScene/);
assert.match(visual, /<LaboratoryMetricEvidenceScene/);
assert.match(visual, /label: "VIDEO"/);
assert.match(visual, /label: "CAMERA"/);
assert.match(visual, /label: "3D"/);
assert.match(videoScene, /<RecordedFmp4Player/);
assert.match(metricScene, /OrbitControls/);
});