feat(ui): add M4.8S replay with LiDAR overlay
This commit is contained in:
@@ -9,6 +9,10 @@ import {
|
||||
RecordedEvidenceSemanticMaskOverlay,
|
||||
type RecordedEvidenceSemanticOverlay,
|
||||
} from "./RecordedEvidenceSemanticMaskOverlay";
|
||||
import {
|
||||
RecordedEvidencePointCloudOverlay,
|
||||
type RecordedEvidencePointCloudOverlayData,
|
||||
} from "./RecordedEvidencePointCloudOverlay";
|
||||
|
||||
export function RecordedEvidenceImageScene({
|
||||
src,
|
||||
@@ -16,6 +20,7 @@ export function RecordedEvidenceImageScene({
|
||||
imageHeight,
|
||||
boxes,
|
||||
semanticOverlay,
|
||||
pointCloudOverlay,
|
||||
ariaLabel,
|
||||
}: {
|
||||
src: string;
|
||||
@@ -23,6 +28,7 @@ export function RecordedEvidenceImageScene({
|
||||
imageHeight: number;
|
||||
boxes: readonly RecordedEvidenceBox[];
|
||||
semanticOverlay?: RecordedEvidenceSemanticOverlay;
|
||||
pointCloudOverlay?: RecordedEvidencePointCloudOverlayData;
|
||||
ariaLabel: string;
|
||||
}) {
|
||||
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
|
||||
@@ -47,6 +53,13 @@ export function RecordedEvidenceImageScene({
|
||||
imageHeight={imageHeight}
|
||||
/>
|
||||
) : null}
|
||||
{pointCloudOverlay ? (
|
||||
<RecordedEvidencePointCloudOverlay
|
||||
imageWidth={imageWidth}
|
||||
imageHeight={imageHeight}
|
||||
overlay={pointCloudOverlay}
|
||||
/>
|
||||
) : null}
|
||||
<RecordedEvidenceBoxOverlay
|
||||
imageWidth={imageWidth}
|
||||
imageHeight={imageHeight}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export type RecordedEvidenceProjectedPoint = readonly [number, number, number];
|
||||
|
||||
export interface RecordedEvidencePointCloudOverlayData {
|
||||
pointsXyd: readonly RecordedEvidenceProjectedPoint[];
|
||||
sourcePointCount: number;
|
||||
projectedPointCount: number;
|
||||
projection: "factory-kb4-exact";
|
||||
ariaLabel: string;
|
||||
}
|
||||
|
||||
const DEPTH_BUCKETS = 64;
|
||||
|
||||
function depthColor(depthM: number): string {
|
||||
const normalized = Math.max(0, Math.min(1, (depthM - 0.5) / 24));
|
||||
const bucket = Math.round(normalized * (DEPTH_BUCKETS - 1));
|
||||
const hue = 18 + bucket / (DEPTH_BUCKETS - 1) * 190;
|
||||
return `hsla(${hue}, 96%, 62%, 0.86)`;
|
||||
}
|
||||
|
||||
export function RecordedEvidencePointCloudOverlay({
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
overlay,
|
||||
}: {
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
overlay: RecordedEvidencePointCloudOverlayData;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
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);
|
||||
|
||||
const scale = Math.min(width / imageWidth, height / imageHeight);
|
||||
const offsetX = (width - imageWidth * scale) / 2;
|
||||
const offsetY = (height - imageHeight * scale) / 2;
|
||||
const radius = Math.max(0.8, Math.min(2.2, scale * 1.45));
|
||||
for (const [imageX, imageY, depthM] of overlay.pointsXyd) {
|
||||
context.beginPath();
|
||||
context.arc(
|
||||
offsetX + imageX * scale,
|
||||
offsetY + imageY * scale,
|
||||
radius,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
);
|
||||
context.fillStyle = depthColor(depthM);
|
||||
context.fill();
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(render);
|
||||
observer.observe(host);
|
||||
render();
|
||||
return () => observer.disconnect();
|
||||
}, [imageHeight, imageWidth, overlay]);
|
||||
|
||||
return <canvas ref={canvasRef} role="img" aria-label={overlay.ariaLabel} />;
|
||||
}
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
RecordedEvidenceSemanticMaskOverlay,
|
||||
type RecordedEvidenceSemanticOverlay,
|
||||
} from "./RecordedEvidenceSemanticMaskOverlay";
|
||||
import {
|
||||
RecordedEvidencePointCloudOverlay,
|
||||
type RecordedEvidencePointCloudOverlayData,
|
||||
} from "./RecordedEvidencePointCloudOverlay";
|
||||
|
||||
export type { RecordedEvidenceBox, RecordedEvidenceBoxTone };
|
||||
|
||||
@@ -22,6 +26,7 @@ export function RecordedEvidenceVideoScene({
|
||||
imageHeight,
|
||||
boxes,
|
||||
semanticOverlay,
|
||||
pointCloudOverlay,
|
||||
ariaLabel,
|
||||
interactive = true,
|
||||
segmentSequence,
|
||||
@@ -35,6 +40,7 @@ export function RecordedEvidenceVideoScene({
|
||||
imageHeight: number;
|
||||
boxes: readonly RecordedEvidenceBox[];
|
||||
semanticOverlay?: RecordedEvidenceSemanticOverlay;
|
||||
pointCloudOverlay?: RecordedEvidencePointCloudOverlayData;
|
||||
ariaLabel: string;
|
||||
interactive?: boolean;
|
||||
segmentSequence?: number;
|
||||
@@ -61,6 +67,13 @@ export function RecordedEvidenceVideoScene({
|
||||
imageHeight={imageHeight}
|
||||
/>
|
||||
) : null}
|
||||
{pointCloudOverlay ? (
|
||||
<RecordedEvidencePointCloudOverlay
|
||||
imageWidth={imageWidth}
|
||||
imageHeight={imageHeight}
|
||||
overlay={pointCloudOverlay}
|
||||
/>
|
||||
) : null}
|
||||
<RecordedEvidenceBoxOverlay
|
||||
imageWidth={imageWidth}
|
||||
imageHeight={imageHeight}
|
||||
|
||||
@@ -42,10 +42,12 @@ import {
|
||||
fetchM48LifecycleResult,
|
||||
} from "./m48ObjectCentricQuality";
|
||||
import { fetchM48SmallStaticRegression } from "./m48SmallStaticRegression";
|
||||
import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
||||
|
||||
export type AdvancedLaboratoryWorkId =
|
||||
| "m48-object-centric-quality"
|
||||
| "m48-small-static-passage-regression"
|
||||
| "m48s-fixed-class-detector"
|
||||
| "m47-reference-graph-shadow"
|
||||
| "m4-replay-threat"
|
||||
| "l3-pointpillars-visual-audit"
|
||||
@@ -90,6 +92,7 @@ export interface AdvancedLaboratoryIndexItem {
|
||||
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
||||
"m48-object-centric-quality",
|
||||
"m48-small-static-passage-regression",
|
||||
"m48s-fixed-class-detector",
|
||||
"m47-reference-graph-shadow",
|
||||
"m4-replay-threat",
|
||||
"l3-pointpillars-visual-audit",
|
||||
@@ -129,6 +132,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
||||
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
|
||||
"m48-object-centric-quality": "m48-object-quality-(?:pack|result)",
|
||||
"m48-small-static-passage-regression": "m48-small-static-passage-regression",
|
||||
"m48s-fixed-class-detector": "m48s-fixed-class-detector-lab",
|
||||
"m47-reference-graph-shadow": "m47-reference-graph-lab",
|
||||
"m4-replay-threat": "m4-threat-replay",
|
||||
"l3-pointpillars-visual-audit": "l3-pointpillars-visual-audit",
|
||||
@@ -176,6 +180,7 @@ export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
|
||||
m47Graph: null,
|
||||
m48: null,
|
||||
m48SmallStatic: null,
|
||||
m48s: null,
|
||||
m4Threat: null,
|
||||
l3: null,
|
||||
l31: null,
|
||||
@@ -302,6 +307,7 @@ export function advancedLaboratoryResultAvailable(
|
||||
): boolean {
|
||||
return workId === "m48-object-centric-quality" ? results.m48 !== null
|
||||
: workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== null
|
||||
: workId === "m48s-fixed-class-detector" ? results.m48s !== null
|
||||
: workId === "m47-reference-graph-shadow" ? results.m47Graph !== null
|
||||
: workId === "m4-replay-threat" ? results.m4Threat !== null
|
||||
: workId === "l3-pointpillars-visual-audit" ? results.l3 !== null
|
||||
@@ -357,6 +363,9 @@ export async function fetchAdvancedLaboratoryResult(
|
||||
} else if (workId === "m48-small-static-passage-regression") {
|
||||
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8R1 regression identity не выбрана.");
|
||||
results.m48SmallStatic = await fetchM48SmallStaticRegression(resultId, { fetcher, signal });
|
||||
} else if (workId === "m48s-fixed-class-detector") {
|
||||
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8S LAB identity не выбрана.");
|
||||
results.m48s = await fetchM48SFixedClassDetectorResult(resultId, { fetcher, signal });
|
||||
} else if (workId === "m47-reference-graph-shadow") {
|
||||
if (!resultId) {
|
||||
throw new AdvancedLaboratoryContractError("M4.7 LAB identity не выбрана.");
|
||||
|
||||
@@ -36,11 +36,13 @@ import type { M4ThreatReplayResult } from "./m4ReplayThreat";
|
||||
import type { M47ReferenceGraphLabResult } from "./m47ReferenceGraph";
|
||||
import type { M48AdvancedResult } from "./m48ObjectCentricQuality";
|
||||
import type { M48SmallStaticRegressionResult } from "./m48SmallStaticRegression";
|
||||
import type { M48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
||||
|
||||
export interface AdvancedLaboratoryResults {
|
||||
m47Graph: M47ReferenceGraphLabResult | null;
|
||||
m48: M48AdvancedResult | null;
|
||||
m48SmallStatic: M48SmallStaticRegressionResult | null;
|
||||
m48s: M48SFixedClassDetectorResult | null;
|
||||
m4Threat: M4ThreatReplayResult | null;
|
||||
l3: L3PointPillarsVisualAuditResult | null;
|
||||
l31: L31PointPillarsRavnovesResult | null;
|
||||
|
||||
@@ -967,7 +967,7 @@ export async function fetchAdvancedLaboratoryResults({
|
||||
const e39 = settledCatalogValue(settled[7]);
|
||||
const e40 = settledCatalogValue(settled[8]);
|
||||
return {
|
||||
m47Graph: null, m48: null, m48SmallStatic: null, m4Threat: null,
|
||||
m47Graph: null, m48: null, m48SmallStatic: null, m48s: null, m4Threat: null,
|
||||
l3: null, l31: null,
|
||||
l32: null,
|
||||
l33: null,
|
||||
|
||||
@@ -0,0 +1,569 @@
|
||||
import type { LaboratoryFetch } from "./advancedResults";
|
||||
|
||||
const RESULT_ID = /^m48s-fixed-class-detector-lab-[a-f0-9]{64}$/;
|
||||
const FRAME_ID = /^[0-9]{6}$/;
|
||||
const MODES = ["source", "yolox", "dfine", "rf-detr"] as const;
|
||||
const DETECTOR_MODES = ["yolox", "dfine", "rf-detr"] as const;
|
||||
const RESULT_STATUSES = [
|
||||
"detector-load-passed-reference-graph-shadow-only",
|
||||
"complete-reference-graph-shadow-passed-production-not-authorized",
|
||||
] as const;
|
||||
|
||||
export type M48SDetectorMode = typeof MODES[number];
|
||||
export type M48SDetectorModel = typeof DETECTOR_MODES[number];
|
||||
|
||||
export interface M48SAuthority {
|
||||
actuationAllowed: false;
|
||||
candidateAccepted: false;
|
||||
commandsEnabled: false;
|
||||
groundTruth: false;
|
||||
navigationOrSafetyAccepted: false;
|
||||
}
|
||||
|
||||
export interface M48SMethodComponent {
|
||||
kind: "source" | "tool" | "model" | "algorithm" | "runtime";
|
||||
name: string;
|
||||
version: string;
|
||||
role: string;
|
||||
identitySha256: string;
|
||||
}
|
||||
|
||||
export interface M48SMethod {
|
||||
completeness: "complete";
|
||||
executionClass: "ai-inference";
|
||||
pipelineId: string;
|
||||
components: readonly M48SMethodComponent[];
|
||||
}
|
||||
|
||||
export interface M48SDetectorCandidate {
|
||||
id: M48SDetectorModel;
|
||||
label: string;
|
||||
providerId: string;
|
||||
capacityFps: number;
|
||||
p95Ms: number;
|
||||
frame253DogDetected: boolean;
|
||||
frame253DogScore: number | null;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export interface M48SFrameDescriptor {
|
||||
frameId: string;
|
||||
sourceSequence: number;
|
||||
counts: Readonly<Record<M48SDetectorModel, number>>;
|
||||
}
|
||||
|
||||
export interface M48SIntegratedWorldState {
|
||||
durationSeconds: number;
|
||||
sourceFramesAdmitted: number;
|
||||
deliveredWorldStates: number;
|
||||
supersededFrames: number;
|
||||
effectiveWorldStateFps: number;
|
||||
worldStateCompletionAgeP95Ms: number;
|
||||
worldStateCompletionAgeP99Ms: number;
|
||||
worldStateCompletionAgeMaximumMs: number;
|
||||
localObstacleMapOutputAgeP95Ms: number;
|
||||
queueHighWatermarks: Readonly<Record<"detector" | "geometry" | "temporal" | "rolling" | "threat", number>>;
|
||||
queueCapacity: number;
|
||||
gpuUtilizationMeanPercent: number;
|
||||
gpuUtilizationMaximumPercent: number;
|
||||
gpuMemoryMaximumMib: number;
|
||||
gpuPowerMaximumW: number;
|
||||
gpuTemperatureMaximumC: number;
|
||||
uniqueComponentCount: number;
|
||||
multiFrameComponentCount: number;
|
||||
maximumComponentPublications: number;
|
||||
duplicateComponentIdsWithinFrame: number;
|
||||
advisoryFamilyCounts: Readonly<Record<"animal" | "generic-obstacle" | "light-road-user" | "person" | "vehicle", number>>;
|
||||
semanticHintCounts: Readonly<Record<string, number>>;
|
||||
motionCounts: Readonly<Record<"moving" | "stationary" | "unknown", number>>;
|
||||
additionalInferencePasses: number;
|
||||
failures: number;
|
||||
}
|
||||
|
||||
export interface M48SFixedClassDetectorResult {
|
||||
resultId: string;
|
||||
createdAtUtc: string;
|
||||
status: typeof RESULT_STATUSES[number];
|
||||
boundedQuestionAccepted: true;
|
||||
groundTruth: false;
|
||||
source: {
|
||||
sourceSessionId: "RAVNOVES00";
|
||||
cameraSourceId: "sensor.camera.right";
|
||||
cameraRaster: readonly [800, 600];
|
||||
evidenceFrameCount: number;
|
||||
};
|
||||
configuration: {
|
||||
comparisonThreshold: 0.5;
|
||||
displayModes: readonly M48SDetectorMode[];
|
||||
singleInferencePerFrame: true;
|
||||
geometryOwnsStaticOccupancy: true;
|
||||
unknownStationaryResponse: "route-around";
|
||||
unknownMovingResponse: "conservative-risk";
|
||||
};
|
||||
method: M48SMethod;
|
||||
metrics: {
|
||||
candidates: readonly M48SDetectorCandidate[];
|
||||
detectorLoad: {
|
||||
durationSeconds: number;
|
||||
sourceFramesConsumed: number;
|
||||
sourceFrameReplacements: number;
|
||||
effectiveConsumedFps: number;
|
||||
endToEndP95Ms: number;
|
||||
completionAgeP95Ms: number;
|
||||
gpuUtilizationMeanPercent: number;
|
||||
gpuUtilizationMaximumPercent: number;
|
||||
gpuMemoryMaximumMib: number;
|
||||
queueMaximumDepth: number;
|
||||
queueCapacity: number;
|
||||
failures: number;
|
||||
};
|
||||
integratedWorldState: M48SIntegratedWorldState | null;
|
||||
};
|
||||
decision: {
|
||||
selectedCandidate: "rf-detr";
|
||||
readyForReferenceGraphShadow: true;
|
||||
integratedWorldStateGateEvaluated: boolean;
|
||||
integratedWorldStateGatePassed: boolean;
|
||||
detectorReplacementAuthorized: false;
|
||||
productionAccepted: false;
|
||||
};
|
||||
limitations: readonly string[];
|
||||
authority: M48SAuthority;
|
||||
frames: readonly M48SFrameDescriptor[];
|
||||
}
|
||||
|
||||
export interface M48SDetection {
|
||||
label: string;
|
||||
score: number;
|
||||
bboxXyxy: readonly [number, number, number, number];
|
||||
}
|
||||
|
||||
export interface M48SDetectorFrame {
|
||||
resultId: string;
|
||||
frameId: string;
|
||||
sourceSequence: number;
|
||||
cameraUrl: string;
|
||||
imageWidth: 800;
|
||||
imageHeight: 600;
|
||||
comparisonThreshold: 0.5;
|
||||
detections: Readonly<Record<M48SDetectorModel, readonly M48SDetection[]>>;
|
||||
groundTruthAvailable: false;
|
||||
authority: M48SAuthority;
|
||||
}
|
||||
|
||||
export class M48SFixedClassDetectorContractError extends Error {}
|
||||
|
||||
function objectValue(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new M48SFixedClassDetectorContractError(`${label}: ожидался объект.`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function arrayValue(value: unknown, label: string): readonly unknown[] {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new M48SFixedClassDetectorContractError(`${label}: ожидался список.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function exact(value: unknown, expected: string | number | boolean, label: string): void {
|
||||
if (value !== expected) {
|
||||
throw new M48SFixedClassDetectorContractError(`${label}: нарушен контракт.`);
|
||||
}
|
||||
}
|
||||
|
||||
function textValue(value: unknown, label: string): string {
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
throw new M48SFixedClassDetectorContractError(`${label}: ожидалась строка.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function numberValue(value: unknown, label: string): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
throw new M48SFixedClassDetectorContractError(`${label}: ожидалось число.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function integerValue(value: unknown, label: string): number {
|
||||
const result = numberValue(value, label);
|
||||
if (!Number.isInteger(result) || result < 0) {
|
||||
throw new M48SFixedClassDetectorContractError(`${label}: ожидалось целое число.`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function booleanValue(value: unknown, label: string): boolean {
|
||||
if (typeof value !== "boolean") {
|
||||
throw new M48SFixedClassDetectorContractError(`${label}: ожидался флаг.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function enumValue<T extends string>(value: unknown, allowed: readonly T[], label: string): T {
|
||||
if (typeof value !== "string" || !allowed.includes(value as T)) {
|
||||
throw new M48SFixedClassDetectorContractError(`${label}: неизвестное значение.`);
|
||||
}
|
||||
return value as T;
|
||||
}
|
||||
|
||||
function authorityValue(value: unknown, label: string): M48SAuthority {
|
||||
const authority = objectValue(value, label);
|
||||
exact(authority.actuation_allowed, false, `${label}.actuation_allowed`);
|
||||
exact(authority.candidate_accepted, false, `${label}.candidate_accepted`);
|
||||
exact(authority.commands_enabled, false, `${label}.commands_enabled`);
|
||||
exact(authority.ground_truth, false, `${label}.ground_truth`);
|
||||
exact(
|
||||
authority.navigation_or_safety_accepted,
|
||||
false,
|
||||
`${label}.navigation_or_safety_accepted`,
|
||||
);
|
||||
return {
|
||||
actuationAllowed: false,
|
||||
candidateAccepted: false,
|
||||
commandsEnabled: false,
|
||||
groundTruth: false,
|
||||
navigationOrSafetyAccepted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function methodValue(value: unknown): M48SMethod {
|
||||
const method = objectValue(value, "M4.8S.method");
|
||||
exact(method.schema_version, "missioncore.laboratory-method/v1", "M4.8S.method.schema_version");
|
||||
exact(method.completeness, "complete", "M4.8S.method.completeness");
|
||||
exact(method.execution_class, "ai-inference", "M4.8S.method.execution_class");
|
||||
return {
|
||||
completeness: "complete",
|
||||
executionClass: "ai-inference",
|
||||
pipelineId: textValue(method.pipeline_id, "M4.8S.method.pipeline_id"),
|
||||
components: arrayValue(method.components, "M4.8S.method.components").map((value, index) => {
|
||||
const component = objectValue(value, `M4.8S.method.components[${index}]`);
|
||||
return {
|
||||
kind: enumValue(
|
||||
component.kind,
|
||||
["source", "tool", "model", "algorithm", "runtime"] as const,
|
||||
`M4.8S.method.components[${index}].kind`,
|
||||
),
|
||||
name: textValue(component.name, `M4.8S.method.components[${index}].name`),
|
||||
version: textValue(component.version, `M4.8S.method.components[${index}].version`),
|
||||
role: textValue(component.role, `M4.8S.method.components[${index}].role`),
|
||||
identitySha256: textValue(
|
||||
component.identity_sha256,
|
||||
`M4.8S.method.components[${index}].identity_sha256`,
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function countsValue(value: unknown, label: string): Readonly<Record<M48SDetectorModel, number>> {
|
||||
const counts = objectValue(value, label);
|
||||
return {
|
||||
yolox: integerValue(counts.yolox, `${label}.yolox`),
|
||||
dfine: integerValue(counts.dfine, `${label}.dfine`),
|
||||
"rf-detr": integerValue(counts["rf-detr"], `${label}.rf-detr`),
|
||||
};
|
||||
}
|
||||
|
||||
function descriptorValue(value: unknown): M48SFrameDescriptor {
|
||||
const frame = objectValue(value, "M4.8S.frame");
|
||||
const frameId = textValue(frame.frame_id, "M4.8S.frame.frame_id");
|
||||
if (!FRAME_ID.test(frameId)) {
|
||||
throw new M48SFixedClassDetectorContractError("M4.8S.frame.frame_id: нарушена идентичность.");
|
||||
}
|
||||
return {
|
||||
frameId,
|
||||
sourceSequence: integerValue(frame.source_sequence, "M4.8S.frame.source_sequence"),
|
||||
counts: countsValue(frame.counts, "M4.8S.frame.counts"),
|
||||
};
|
||||
}
|
||||
|
||||
function candidateValue(value: unknown): M48SDetectorCandidate {
|
||||
const candidate = objectValue(value, "M4.8S.candidate");
|
||||
const score = candidate.frame_253_dog_score;
|
||||
if (score !== null && (typeof score !== "number" || !Number.isFinite(score))) {
|
||||
throw new M48SFixedClassDetectorContractError("M4.8S.candidate.frame_253_dog_score: нарушен контракт.");
|
||||
}
|
||||
return {
|
||||
id: enumValue(candidate.id, DETECTOR_MODES, "M4.8S.candidate.id"),
|
||||
label: textValue(candidate.label, "M4.8S.candidate.label"),
|
||||
providerId: textValue(candidate.provider_id, "M4.8S.candidate.provider_id"),
|
||||
capacityFps: numberValue(candidate.capacity_fps, "M4.8S.candidate.capacity_fps"),
|
||||
p95Ms: numberValue(candidate.p95_ms, "M4.8S.candidate.p95_ms"),
|
||||
frame253DogDetected: booleanValue(
|
||||
candidate.frame_253_dog_detected,
|
||||
"M4.8S.candidate.frame_253_dog_detected",
|
||||
),
|
||||
frame253DogScore: score,
|
||||
selected: booleanValue(candidate.selected, "M4.8S.candidate.selected"),
|
||||
};
|
||||
}
|
||||
|
||||
function integerRecordValue(value: unknown, label: string): Readonly<Record<string, number>> {
|
||||
const record = objectValue(value, label);
|
||||
return Object.fromEntries(
|
||||
Object.entries(record).map(([key, item]) => [key, integerValue(item, `${label}.${key}`)]),
|
||||
);
|
||||
}
|
||||
|
||||
function integratedWorldStateValue(value: unknown): M48SIntegratedWorldState {
|
||||
const integrated = objectValue(value, "M4.8S.metrics.integrated_world_state");
|
||||
const queues = integerRecordValue(
|
||||
integrated.queue_high_watermarks,
|
||||
"M4.8S.integrated.queue_high_watermarks",
|
||||
);
|
||||
const advisory = integerRecordValue(
|
||||
integrated.advisory_family_counts,
|
||||
"M4.8S.integrated.advisory_family_counts",
|
||||
);
|
||||
const motion = integerRecordValue(
|
||||
integrated.motion_counts,
|
||||
"M4.8S.integrated.motion_counts",
|
||||
);
|
||||
for (const key of ["detector", "geometry", "temporal", "rolling", "threat"] as const) {
|
||||
if (!(key in queues)) {
|
||||
throw new M48SFixedClassDetectorContractError(`M4.8S.integrated.queue_high_watermarks.${key}: отсутствует.`);
|
||||
}
|
||||
}
|
||||
for (const key of ["animal", "generic-obstacle", "light-road-user", "person", "vehicle"] as const) {
|
||||
if (!(key in advisory)) {
|
||||
throw new M48SFixedClassDetectorContractError(`M4.8S.integrated.advisory_family_counts.${key}: отсутствует.`);
|
||||
}
|
||||
}
|
||||
for (const key of ["moving", "stationary", "unknown"] as const) {
|
||||
if (!(key in motion)) {
|
||||
throw new M48SFixedClassDetectorContractError(`M4.8S.integrated.motion_counts.${key}: отсутствует.`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
durationSeconds: numberValue(integrated.duration_seconds, "M4.8S.integrated.duration_seconds"),
|
||||
sourceFramesAdmitted: integerValue(integrated.source_frames_admitted, "M4.8S.integrated.source_frames_admitted"),
|
||||
deliveredWorldStates: integerValue(integrated.delivered_world_states, "M4.8S.integrated.delivered_world_states"),
|
||||
supersededFrames: integerValue(integrated.superseded_frames, "M4.8S.integrated.superseded_frames"),
|
||||
effectiveWorldStateFps: numberValue(integrated.effective_world_state_fps, "M4.8S.integrated.effective_world_state_fps"),
|
||||
worldStateCompletionAgeP95Ms: numberValue(integrated.world_state_completion_age_p95_ms, "M4.8S.integrated.world_state_completion_age_p95_ms"),
|
||||
worldStateCompletionAgeP99Ms: numberValue(integrated.world_state_completion_age_p99_ms, "M4.8S.integrated.world_state_completion_age_p99_ms"),
|
||||
worldStateCompletionAgeMaximumMs: numberValue(integrated.world_state_completion_age_maximum_ms, "M4.8S.integrated.world_state_completion_age_maximum_ms"),
|
||||
localObstacleMapOutputAgeP95Ms: numberValue(integrated.local_obstacle_map_output_age_p95_ms, "M4.8S.integrated.local_obstacle_map_output_age_p95_ms"),
|
||||
queueHighWatermarks: queues as M48SIntegratedWorldState["queueHighWatermarks"],
|
||||
queueCapacity: integerValue(integrated.queue_capacity, "M4.8S.integrated.queue_capacity"),
|
||||
gpuUtilizationMeanPercent: numberValue(integrated.gpu_utilization_mean_percent, "M4.8S.integrated.gpu_utilization_mean_percent"),
|
||||
gpuUtilizationMaximumPercent: numberValue(integrated.gpu_utilization_maximum_percent, "M4.8S.integrated.gpu_utilization_maximum_percent"),
|
||||
gpuMemoryMaximumMib: numberValue(integrated.gpu_memory_maximum_mib, "M4.8S.integrated.gpu_memory_maximum_mib"),
|
||||
gpuPowerMaximumW: numberValue(integrated.gpu_power_maximum_w, "M4.8S.integrated.gpu_power_maximum_w"),
|
||||
gpuTemperatureMaximumC: numberValue(integrated.gpu_temperature_maximum_c, "M4.8S.integrated.gpu_temperature_maximum_c"),
|
||||
uniqueComponentCount: integerValue(integrated.unique_component_count, "M4.8S.integrated.unique_component_count"),
|
||||
multiFrameComponentCount: integerValue(integrated.multi_frame_component_count, "M4.8S.integrated.multi_frame_component_count"),
|
||||
maximumComponentPublications: integerValue(integrated.maximum_component_publications, "M4.8S.integrated.maximum_component_publications"),
|
||||
duplicateComponentIdsWithinFrame: integerValue(integrated.duplicate_component_ids_within_frame, "M4.8S.integrated.duplicate_component_ids_within_frame"),
|
||||
advisoryFamilyCounts: advisory as M48SIntegratedWorldState["advisoryFamilyCounts"],
|
||||
semanticHintCounts: integerRecordValue(integrated.semantic_hint_counts, "M4.8S.integrated.semantic_hint_counts"),
|
||||
motionCounts: motion as M48SIntegratedWorldState["motionCounts"],
|
||||
additionalInferencePasses: integerValue(integrated.additional_inference_passes, "M4.8S.integrated.additional_inference_passes"),
|
||||
failures: integerValue(integrated.failures, "M4.8S.integrated.failures"),
|
||||
};
|
||||
}
|
||||
|
||||
function parseResult(value: unknown, resultId: string): M48SFixedClassDetectorResult {
|
||||
const payload = objectValue(value, "M4.8S");
|
||||
exact(
|
||||
payload.schema_version,
|
||||
"missioncore.m48s-fixed-class-detector-result-view/v1",
|
||||
"M4.8S.schema_version",
|
||||
);
|
||||
exact(payload.result_id, resultId, "M4.8S.result_id");
|
||||
const status = enumValue(payload.status, RESULT_STATUSES, "M4.8S.status");
|
||||
const integratedGate = status === "complete-reference-graph-shadow-passed-production-not-authorized";
|
||||
exact(payload.bounded_question_accepted, true, "M4.8S.bounded_question_accepted");
|
||||
exact(payload.ground_truth, false, "M4.8S.ground_truth");
|
||||
exact(payload.access, "read-only", "M4.8S.access");
|
||||
const source = objectValue(payload.source, "M4.8S.source");
|
||||
const configuration = objectValue(payload.configuration, "M4.8S.configuration");
|
||||
const metrics = objectValue(payload.metrics, "M4.8S.metrics");
|
||||
const load = objectValue(metrics.detector_load, "M4.8S.metrics.detector_load");
|
||||
const decision = objectValue(payload.decision, "M4.8S.decision");
|
||||
const cameraRaster = arrayValue(source.camera_raster, "M4.8S.source.camera_raster");
|
||||
if (cameraRaster.length !== 2) {
|
||||
throw new M48SFixedClassDetectorContractError("M4.8S.source.camera_raster: нарушен размер.");
|
||||
}
|
||||
exact(cameraRaster[0], 800, "M4.8S.source.camera_raster[0]");
|
||||
exact(cameraRaster[1], 600, "M4.8S.source.camera_raster[1]");
|
||||
const displayModes = arrayValue(
|
||||
configuration.display_modes,
|
||||
"M4.8S.configuration.display_modes",
|
||||
).map((mode, index) => enumValue(mode, MODES, `M4.8S.configuration.display_modes[${index}]`));
|
||||
if (displayModes.join(",") !== MODES.join(",")) {
|
||||
throw new M48SFixedClassDetectorContractError("M4.8S.configuration.display_modes: изменён контракт.");
|
||||
}
|
||||
exact(source.source_session_id, "RAVNOVES00", "M4.8S.source.source_session_id");
|
||||
exact(source.camera_source_id, "sensor.camera.right", "M4.8S.source.camera_source_id");
|
||||
exact(configuration.comparison_threshold, 0.5, "M4.8S.configuration.comparison_threshold");
|
||||
exact(configuration.single_inference_per_frame, true, "M4.8S.configuration.single_inference_per_frame");
|
||||
exact(configuration.geometry_owns_static_occupancy, true, "M4.8S.configuration.geometry_owns_static_occupancy");
|
||||
exact(configuration.unknown_stationary_response, "route-around", "M4.8S.configuration.unknown_stationary_response");
|
||||
exact(configuration.unknown_moving_response, "conservative-risk", "M4.8S.configuration.unknown_moving_response");
|
||||
exact(decision.selected_candidate, "rf-detr", "M4.8S.decision.selected_candidate");
|
||||
exact(decision.ready_for_reference_graph_shadow, true, "M4.8S.decision.ready_for_reference_graph_shadow");
|
||||
exact(decision.integrated_world_state_gate_evaluated, integratedGate, "M4.8S.decision.integrated_world_state_gate_evaluated");
|
||||
if (integratedGate) {
|
||||
exact(decision.integrated_world_state_gate_passed, true, "M4.8S.decision.integrated_world_state_gate_passed");
|
||||
exact(decision.detector_replacement_authorized, false, "M4.8S.decision.detector_replacement_authorized");
|
||||
}
|
||||
exact(decision.production_accepted, false, "M4.8S.decision.production_accepted");
|
||||
const frames = arrayValue(payload.frames, "M4.8S.frames").map(descriptorValue);
|
||||
const evidenceFrameCount = integerValue(source.evidence_frame_count, "M4.8S.source.evidence_frame_count");
|
||||
if (frames.length !== evidenceFrameCount || new Set(frames.map((frame) => frame.frameId)).size !== frames.length) {
|
||||
throw new M48SFixedClassDetectorContractError("M4.8S.frames: каталог изменён.");
|
||||
}
|
||||
return {
|
||||
resultId,
|
||||
createdAtUtc: textValue(payload.created_at_utc, "M4.8S.created_at_utc"),
|
||||
status,
|
||||
boundedQuestionAccepted: true,
|
||||
groundTruth: false,
|
||||
source: {
|
||||
sourceSessionId: "RAVNOVES00",
|
||||
cameraSourceId: "sensor.camera.right",
|
||||
cameraRaster: [800, 600],
|
||||
evidenceFrameCount,
|
||||
},
|
||||
configuration: {
|
||||
comparisonThreshold: 0.5,
|
||||
displayModes,
|
||||
singleInferencePerFrame: true,
|
||||
geometryOwnsStaticOccupancy: true,
|
||||
unknownStationaryResponse: "route-around",
|
||||
unknownMovingResponse: "conservative-risk",
|
||||
},
|
||||
method: methodValue(payload.method),
|
||||
metrics: {
|
||||
candidates: arrayValue(metrics.candidates, "M4.8S.metrics.candidates").map(candidateValue),
|
||||
detectorLoad: {
|
||||
durationSeconds: numberValue(load.duration_seconds, "M4.8S.load.duration_seconds"),
|
||||
sourceFramesConsumed: integerValue(load.source_frames_consumed, "M4.8S.load.source_frames_consumed"),
|
||||
sourceFrameReplacements: integerValue(load.source_frame_replacements, "M4.8S.load.source_frame_replacements"),
|
||||
effectiveConsumedFps: numberValue(load.effective_consumed_fps, "M4.8S.load.effective_consumed_fps"),
|
||||
endToEndP95Ms: numberValue(load.end_to_end_p95_ms, "M4.8S.load.end_to_end_p95_ms"),
|
||||
completionAgeP95Ms: numberValue(load.completion_age_p95_ms, "M4.8S.load.completion_age_p95_ms"),
|
||||
gpuUtilizationMeanPercent: numberValue(load.gpu_utilization_mean_percent, "M4.8S.load.gpu_utilization_mean_percent"),
|
||||
gpuUtilizationMaximumPercent: numberValue(load.gpu_utilization_maximum_percent, "M4.8S.load.gpu_utilization_maximum_percent"),
|
||||
gpuMemoryMaximumMib: numberValue(load.gpu_memory_maximum_mib, "M4.8S.load.gpu_memory_maximum_mib"),
|
||||
queueMaximumDepth: integerValue(load.queue_maximum_depth, "M4.8S.load.queue_maximum_depth"),
|
||||
queueCapacity: integerValue(load.queue_capacity, "M4.8S.load.queue_capacity"),
|
||||
failures: integerValue(load.failures, "M4.8S.load.failures"),
|
||||
},
|
||||
integratedWorldState: integratedGate
|
||||
? integratedWorldStateValue(metrics.integrated_world_state)
|
||||
: null,
|
||||
},
|
||||
decision: {
|
||||
selectedCandidate: "rf-detr",
|
||||
readyForReferenceGraphShadow: true,
|
||||
integratedWorldStateGateEvaluated: integratedGate,
|
||||
integratedWorldStateGatePassed: integratedGate,
|
||||
detectorReplacementAuthorized: false,
|
||||
productionAccepted: false,
|
||||
},
|
||||
limitations: arrayValue(payload.limitations, "M4.8S.limitations").map((item, index) => textValue(item, `M4.8S.limitations[${index}]`)),
|
||||
authority: authorityValue(payload.authority, "M4.8S.authority"),
|
||||
frames,
|
||||
};
|
||||
}
|
||||
|
||||
function detectionValue(value: unknown, label: string): M48SDetection {
|
||||
const detection = objectValue(value, label);
|
||||
const bbox = arrayValue(detection.bbox_xyxy, `${label}.bbox_xyxy`).map((item, index) => numberValue(item, `${label}.bbox_xyxy[${index}]`));
|
||||
if (
|
||||
bbox.length !== 4
|
||||
|| bbox[0]! < 0
|
||||
|| bbox[1]! < 0
|
||||
|| bbox[2]! > 800
|
||||
|| bbox[3]! > 600
|
||||
|| bbox[0]! >= bbox[2]!
|
||||
|| bbox[1]! >= bbox[3]!
|
||||
) {
|
||||
throw new M48SFixedClassDetectorContractError(`${label}.bbox_xyxy: рамка недопустима.`);
|
||||
}
|
||||
const score = numberValue(detection.score, `${label}.score`);
|
||||
if (score < 0.5 || score > 1) {
|
||||
throw new M48SFixedClassDetectorContractError(`${label}.score: нарушен порог.`);
|
||||
}
|
||||
return {
|
||||
label: textValue(detection.label, `${label}.label`),
|
||||
score,
|
||||
bboxXyxy: bbox as unknown as readonly [number, number, number, number],
|
||||
};
|
||||
}
|
||||
|
||||
function parseFrame(value: unknown, resultId: string, frameId: string): M48SDetectorFrame {
|
||||
const payload = objectValue(value, "M4.8S frame");
|
||||
exact(payload.schema_version, "missioncore.m48s-fixed-class-detector-frame/v1", "M4.8S frame.schema_version");
|
||||
exact(payload.result_id, resultId, "M4.8S frame.result_id");
|
||||
exact(payload.frame_id, frameId, "M4.8S frame.frame_id");
|
||||
exact(payload.comparison_threshold, 0.5, "M4.8S frame.comparison_threshold");
|
||||
exact(payload.ground_truth_available, false, "M4.8S frame.ground_truth_available");
|
||||
exact(payload.access, "read-only", "M4.8S frame.access");
|
||||
const camera = objectValue(payload.camera, "M4.8S frame.camera");
|
||||
exact(camera.media_type, "image/jpeg", "M4.8S frame.camera.media_type");
|
||||
exact(camera.width, 800, "M4.8S frame.camera.width");
|
||||
exact(camera.height, 600, "M4.8S frame.camera.height");
|
||||
exact(camera.exact_source_frame, true, "M4.8S frame.camera.exact_source_frame");
|
||||
const detections = objectValue(payload.detections, "M4.8S frame.detections");
|
||||
const parseModel = (model: M48SDetectorModel): readonly M48SDetection[] => arrayValue(
|
||||
detections[model],
|
||||
`M4.8S frame.detections.${model}`,
|
||||
).map((item, index) => detectionValue(item, `M4.8S frame.detections.${model}[${index}]`));
|
||||
return {
|
||||
resultId,
|
||||
frameId,
|
||||
sourceSequence: integerValue(payload.source_sequence, "M4.8S frame.source_sequence"),
|
||||
cameraUrl: `/api/v1/laboratory/m48s/fixed-class-detector/${encodeURIComponent(resultId)}/frames/${encodeURIComponent(frameId)}/camera`,
|
||||
imageWidth: 800,
|
||||
imageHeight: 600,
|
||||
comparisonThreshold: 0.5,
|
||||
detections: {
|
||||
yolox: parseModel("yolox"),
|
||||
dfine: parseModel("dfine"),
|
||||
"rf-detr": parseModel("rf-detr"),
|
||||
},
|
||||
groundTruthAvailable: false,
|
||||
authority: authorityValue(payload.authority, "M4.8S frame.authority"),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchM48SFixedClassDetectorResult(
|
||||
resultId: string,
|
||||
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||
): Promise<M48SFixedClassDetectorResult> {
|
||||
if (!RESULT_ID.test(resultId)) {
|
||||
throw new M48SFixedClassDetectorContractError("M4.8S result identity недопустима.");
|
||||
}
|
||||
const response = await fetcher(
|
||||
`/api/v1/laboratory/m48s/fixed-class-detector/${encodeURIComponent(resultId)}`,
|
||||
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new M48SFixedClassDetectorContractError(`M4.8S недоступен: HTTP ${response.status}.`);
|
||||
}
|
||||
return parseResult(await response.json(), resultId);
|
||||
}
|
||||
|
||||
export async function fetchM48SFixedClassDetectorFrame(
|
||||
resultId: string,
|
||||
frameId: string,
|
||||
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||
): Promise<M48SDetectorFrame> {
|
||||
if (!RESULT_ID.test(resultId) || !FRAME_ID.test(frameId)) {
|
||||
throw new M48SFixedClassDetectorContractError("M4.8S frame identity недопустима.");
|
||||
}
|
||||
const response = await fetcher(
|
||||
`/api/v1/laboratory/m48s/fixed-class-detector/${encodeURIComponent(resultId)}/frames/${encodeURIComponent(frameId)}`,
|
||||
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new M48SFixedClassDetectorContractError(`M4.8S frame недоступен: HTTP ${response.status}.`);
|
||||
}
|
||||
return parseFrame(await response.json(), resultId, frameId);
|
||||
}
|
||||
@@ -133,6 +133,8 @@ export interface M4ThreatTimelineFrame {
|
||||
sessionSeconds: number;
|
||||
sourceAvailable: boolean;
|
||||
spatialAvailable: boolean;
|
||||
worldStateAvailable: boolean;
|
||||
terminalOutcome: "delivered" | "superseded";
|
||||
bodyFrame: {
|
||||
originMapXyzM: M4Point3;
|
||||
basisMapFromBody: M4Matrix3;
|
||||
@@ -141,6 +143,11 @@ export interface M4ThreatTimelineFrame {
|
||||
pointCloudSourceCount: number;
|
||||
pointCloudSampleCount: number;
|
||||
pointCloudLayer: "current-increment";
|
||||
cameraProjectedPointsXyd: readonly (readonly [number, number, number])[];
|
||||
cameraProjectedSourceCount: number;
|
||||
cameraProjectedPointCount: number;
|
||||
cameraProjectedSampleCount: number;
|
||||
cameraProjection: "factory-kb4-exact" | null;
|
||||
rollingMapComponentCount: number;
|
||||
metricObstacles: readonly M4ThreatMetricVisual[];
|
||||
cameraProposals: readonly M4ThreatCameraProposal[];
|
||||
@@ -163,6 +170,11 @@ export interface M4ThreatTimeline {
|
||||
pointSampleLimit: number;
|
||||
maximumSourcePointsPerFrame: number;
|
||||
pointDelivery: "exact-current-increment";
|
||||
cameraPointDelivery: "factory-kb4-projected-current-increment" | null;
|
||||
cameraPointSampleLimit: number;
|
||||
worldStateDelivery: "source-paced-latest-wins" | null;
|
||||
worldStateFrameCount: number;
|
||||
supersededFrameCount: number;
|
||||
sourceRepresentationId: "registered-map-increment-v1";
|
||||
localSurfaceVisualization: {
|
||||
derivation: "bounded-registered-increment-accumulation";
|
||||
@@ -186,6 +198,7 @@ export interface M4ThreatTimelineChunk {
|
||||
}
|
||||
|
||||
type LaboratoryFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
export const M4_THREAT_TIMELINE_ENDPOINT_ROOT = "/api/v1/laboratory/m4-threat/results";
|
||||
class M4ThreatContractError extends Error {}
|
||||
const object = (value: unknown, label: string): Record<string, unknown> => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
@@ -553,10 +566,18 @@ export async function fetchM4ThreatVisual(
|
||||
|
||||
export async function fetchM4ThreatTimeline(
|
||||
result: string,
|
||||
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||
{
|
||||
fetcher = fetch,
|
||||
signal,
|
||||
endpointRoot = M4_THREAT_TIMELINE_ENDPOINT_ROOT,
|
||||
}: {
|
||||
fetcher?: LaboratoryFetch;
|
||||
signal?: AbortSignal;
|
||||
endpointRoot?: string;
|
||||
} = {},
|
||||
): Promise<M4ThreatTimeline> {
|
||||
const response = await fetcher(
|
||||
`/api/v1/laboratory/m4-threat/results/${result}/timeline`,
|
||||
`${endpointRoot}/${result}/timeline`,
|
||||
{ headers: { Accept: "application/json" }, signal },
|
||||
);
|
||||
if (!response.ok) throw new M4ThreatContractError(`M4.6 timeline: HTTP ${response.status}.`);
|
||||
@@ -626,6 +647,29 @@ export async function fetchM4ThreatTimeline(
|
||||
"exact-current-increment",
|
||||
"M4.6 point delivery",
|
||||
),
|
||||
cameraPointDelivery: payload.camera_point_delivery === undefined
|
||||
? null
|
||||
: exact(
|
||||
payload.camera_point_delivery,
|
||||
"factory-kb4-projected-current-increment",
|
||||
"M4.6 camera point delivery",
|
||||
),
|
||||
cameraPointSampleLimit: payload.camera_point_sample_limit === undefined
|
||||
? 0
|
||||
: integer(payload.camera_point_sample_limit, "M4.6 camera point limit"),
|
||||
worldStateDelivery: payload.world_state_delivery === undefined
|
||||
? null
|
||||
: exact(
|
||||
payload.world_state_delivery,
|
||||
"source-paced-latest-wins",
|
||||
"M4.6 world-state delivery",
|
||||
),
|
||||
worldStateFrameCount: payload.world_state_frame_count === undefined
|
||||
? frameCount
|
||||
: integer(payload.world_state_frame_count, "M4.6 world-state frames"),
|
||||
supersededFrameCount: payload.superseded_frame_count === undefined
|
||||
? 0
|
||||
: integer(payload.superseded_frame_count, "M4.6 superseded frames"),
|
||||
sourceRepresentationId: "registered-map-increment-v1",
|
||||
localSurfaceVisualization: {
|
||||
derivation: exact(
|
||||
@@ -668,14 +712,22 @@ export async function fetchM4ThreatTimelineChunk(
|
||||
result: string,
|
||||
startSequence: number,
|
||||
frameCount: number,
|
||||
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||
{
|
||||
fetcher = fetch,
|
||||
signal,
|
||||
endpointRoot = M4_THREAT_TIMELINE_ENDPOINT_ROOT,
|
||||
}: {
|
||||
fetcher?: LaboratoryFetch;
|
||||
signal?: AbortSignal;
|
||||
endpointRoot?: string;
|
||||
} = {},
|
||||
): Promise<M4ThreatTimelineChunk> {
|
||||
const params = new URLSearchParams({
|
||||
start: String(startSequence),
|
||||
count: String(frameCount),
|
||||
});
|
||||
const response = await fetcher(
|
||||
`/api/v1/laboratory/m4-threat/results/${result}/timeline/chunk?${params}`,
|
||||
`${endpointRoot}/${result}/timeline/chunk?${params}`,
|
||||
{ headers: { Accept: "application/json" }, signal },
|
||||
);
|
||||
if (!response.ok) throw new M4ThreatContractError(`M4.6 timeline chunk: HTTP ${response.status}.`);
|
||||
@@ -692,7 +744,7 @@ export async function fetchM4ThreatTimelineChunk(
|
||||
throw new M4ThreatContractError("M4.6 timeline chunk start: нарушен контракт.");
|
||||
}
|
||||
const frames = array(payload.frames, "M4.6 timeline frames").map((raw, offset) =>
|
||||
parseTimelineFrame(raw, result, parsedStart + offset));
|
||||
parseTimelineFrame(raw, result, parsedStart + offset, endpointRoot));
|
||||
const parsedCount = integer(payload.frame_count, "M4.6 timeline chunk count");
|
||||
if (parsedCount !== frames.length || parsedCount > frameCount) {
|
||||
throw new M4ThreatContractError("M4.6 timeline chunk count: нарушен контракт.");
|
||||
@@ -712,6 +764,7 @@ function parseTimelineFrame(
|
||||
value: unknown,
|
||||
result: string,
|
||||
expectedSequence: number,
|
||||
endpointRoot: string,
|
||||
): M4ThreatTimelineFrame {
|
||||
const item = object(value, "M4.6 timeline frame");
|
||||
exact(
|
||||
@@ -742,7 +795,7 @@ function parseTimelineFrame(
|
||||
throw new M4ThreatContractError("M4.6 timeline body basis: нарушен размер.");
|
||||
}
|
||||
const cameraUrl = text(item.camera_url, "M4.6 timeline camera URL");
|
||||
if (!cameraUrl.includes(`/results/${result}/timeline/frames/${sequence}/camera`)) {
|
||||
if (!cameraUrl.includes(`${endpointRoot}/${result}/timeline/frames/${sequence}/camera`)) {
|
||||
throw new M4ThreatContractError("M4.6 timeline camera URL: нарушена идентичность.");
|
||||
}
|
||||
return {
|
||||
@@ -752,6 +805,16 @@ function parseTimelineFrame(
|
||||
sessionSeconds: number(item.session_seconds, "M4.6 timeline time"),
|
||||
sourceAvailable: typeof item.source_available === "boolean" && item.source_available,
|
||||
spatialAvailable,
|
||||
worldStateAvailable: item.world_state_available === undefined
|
||||
? true
|
||||
: typeof item.world_state_available === "boolean" && item.world_state_available,
|
||||
terminalOutcome: item.terminal_outcome === undefined
|
||||
? "delivered"
|
||||
: exact(
|
||||
item.terminal_outcome,
|
||||
item.world_state_available === false ? "superseded" : "delivered",
|
||||
"M4.6 terminal outcome",
|
||||
),
|
||||
bodyFrame: bodyFrame === null || basis === null
|
||||
? null
|
||||
: {
|
||||
@@ -771,6 +834,23 @@ function parseTimelineFrame(
|
||||
"current-increment",
|
||||
"M4.6 timeline point layer",
|
||||
),
|
||||
cameraProjectedPointsXyd: item.camera_projected_points_xyd === undefined
|
||||
? []
|
||||
: array(item.camera_projected_points_xyd, "M4.6 camera points").map(
|
||||
(point) => vector(point, 3, "M4.6 camera point") as [number, number, number],
|
||||
),
|
||||
cameraProjectedSourceCount: item.camera_projected_source_count === undefined
|
||||
? 0
|
||||
: integer(item.camera_projected_source_count, "M4.6 camera point source count"),
|
||||
cameraProjectedPointCount: item.camera_projected_point_count === undefined
|
||||
? 0
|
||||
: integer(item.camera_projected_point_count, "M4.6 projected point count"),
|
||||
cameraProjectedSampleCount: item.camera_projected_sample_count === undefined
|
||||
? 0
|
||||
: integer(item.camera_projected_sample_count, "M4.6 projected point sample count"),
|
||||
cameraProjection: item.camera_projection === undefined
|
||||
? null
|
||||
: exact(item.camera_projection, "factory-kb4-exact", "M4.6 camera projection"),
|
||||
rollingMapComponentCount: integer(
|
||||
item.rolling_map_component_count,
|
||||
"M4.6 rolling components",
|
||||
|
||||
@@ -44,6 +44,7 @@ import { M4ReplayThreatResultView } from "./M4ReplayThreatResult";
|
||||
import { M47ReferenceGraphResultView } from "./M47ReferenceGraphResult";
|
||||
import { M48ObjectCentricQualityResultView } from "./M48ObjectCentricQualityResult";
|
||||
import { M48SmallStaticPassageRegressionResultView } from "./M48SmallStaticPassageRegressionResult";
|
||||
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
|
||||
|
||||
export { isAdvancedLaboratoryWorkId };
|
||||
export type { AdvancedLaboratoryWorkId };
|
||||
@@ -92,6 +93,9 @@ export function AdvancedLaboratoryResult({
|
||||
if (workId === "m48-small-static-passage-regression" && results.m48SmallStatic) {
|
||||
return <M48SmallStaticPassageRegressionResultView rigLabel={rigLabel} result={results.m48SmallStatic} />;
|
||||
}
|
||||
if (workId === "m48s-fixed-class-detector" && results.m48s) {
|
||||
return <M48SFixedClassDetectorResultView rigLabel={rigLabel} result={results.m48s} />;
|
||||
}
|
||||
if (workId === "m47-reference-graph-shadow" && results.m47Graph) {
|
||||
return <M47ReferenceGraphResultView rigLabel={rigLabel} result={results.m47Graph} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type { M48SFixedClassDetectorResult } from "../../core/laboratory/m48sFixedClassDetector";
|
||||
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
|
||||
|
||||
function decimal(value: number, digits = 1): string {
|
||||
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
||||
}
|
||||
|
||||
export function M48SFixedClassDetectorResultView({
|
||||
rigLabel,
|
||||
result,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
result: M48SFixedClassDetectorResult;
|
||||
}) {
|
||||
const selected = result.metrics.candidates.find((candidate) => candidate.selected);
|
||||
const load = result.metrics.detectorLoad;
|
||||
const integrated = result.metrics.integratedWorldState;
|
||||
const status = integrated
|
||||
? "Полный RF-DETR reference graph выдержал realtime shadow"
|
||||
: "RF-DETR-L выдержал detector-only realtime shadow";
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="M4.8S · fixed-class semantics риск-объектов"
|
||||
description="Сравнение трёх готовых COCO-детекторов на точных кадрах RAVNOVES00, 30-минутная квалификация RF-DETR-L и полный source-paced прогон RF-DETR → geometry → temporal → motion → rolling map → threat на Worker 006. Статические препятствия остаются в геометрическом контуре; классы используются только там, где меняется ожидаемое поведение."
|
||||
status={status}
|
||||
statusTone="success"
|
||||
facts={[
|
||||
{ label: "Источник", value: `${rigLabel} RIGHT · raw KB4 · ${result.source.evidenceFrameCount} diagnostic frames` },
|
||||
{ label: "Сравнение", value: "YOLOX-S · D-FINE-S · RF-DETR-L · единый threshold 0.50" },
|
||||
{ label: "Worker", value: "Worker 006 · RTX 4090 · TensorRT 11 + isolated Triton" },
|
||||
{ label: "Authority", value: "SHADOW ONLY · commands OFF · actuation OFF · production NO" },
|
||||
]}
|
||||
brief={{
|
||||
question: "Можно ли заменить слабую class-семантику YOLOX готовой моделью, не потеряв realtime на предельном Worker с RTX 4090?",
|
||||
approach: `YOLOX-S, D-FINE-S и RF-DETR-L сравнили на одинаковых ${result.source.evidenceFrameCount} raw-KB4 кадрах с порогом 0.50. RF-DETR-L отдельно квалифицировали ${decimal(load.durationSeconds / 60, 0)} минут, затем встроили в полный reference graph без дополнительного inference-прохода.`,
|
||||
principalResult: integrated
|
||||
? `Полный граф доставил ${integrated.deliveredWorldStates.toLocaleString("ru-RU")} world states при ${decimal(integrated.effectiveWorldStateFps, 3)} FPS и p95 ${decimal(integrated.worldStateCompletionAgeP95Ms, 3)} ms; ${integrated.supersededFrames} входных кадров штатно вытеснены latest-wins очередью.`
|
||||
: `RF-DETR-L выбран из трёх кандидатов и обработал ${load.sourceFramesConsumed.toLocaleString("ru-RU")} кадров detector-only без замен и ошибок.`,
|
||||
limitation: "Прогон доказывает runtime envelope, а не истинность классов, качество track identity, корректность risk policy или безопасность движения. Production authority и команды отключены.",
|
||||
}}
|
||||
method={{
|
||||
completeness: result.method.completeness,
|
||||
executionClass: result.method.executionClass,
|
||||
pipelineId: result.method.pipelineId,
|
||||
components: result.method.components.map((component) => ({
|
||||
...component,
|
||||
identitySha256: component.identitySha256,
|
||||
})),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="M4.8S VISUAL EVIDENCE · FULL REFERENCE GRAPH REPLAY"
|
||||
title="Полное видео: RF-DETR классы, LiDAR, 3D/PLAN и world-state на общем таймлайне"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<M4ReplayThreatVisual
|
||||
resultId={result.resultId}
|
||||
timelineEndpointRoot="/api/v1/laboratory/m48s/fixed-class-detector"
|
||||
evidenceLabel="M4.8S RF-DETR GRAPH"
|
||||
/>
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title={integrated
|
||||
? "Что дал прогон: полный world-state graph проходит realtime envelope"
|
||||
: "Что дал прогон: RF-DETR-L проходит detector-only realtime envelope"}
|
||||
status={status}
|
||||
statusTone="success"
|
||||
metrics={integrated ? [
|
||||
{ label: "Complete graph", value: `${decimal(integrated.effectiveWorldStateFps, 3)} FPS`, hint: "target ≥ 9.5 FPS · source-paced" },
|
||||
{ label: "World-state age p95", value: `${decimal(integrated.worldStateCompletionAgeP95Ms, 3)} ms`, hint: `p99 ${decimal(integrated.worldStateCompletionAgeP99Ms, 3)} ms · target ≤ 175 ms` },
|
||||
{ label: "Delivered / superseded", value: `${integrated.deliveredWorldStates.toLocaleString("ru-RU")} / ${integrated.supersededFrames}`, hint: `${integrated.failures} failures · queues ${Math.max(...Object.values(integrated.queueHighWatermarks))}/${integrated.queueCapacity}` },
|
||||
{ label: "GPU / VRAM peak", value: `${decimal(integrated.gpuUtilizationMaximumPercent, 0)}% / ${decimal(integrated.gpuMemoryMaximumMib / 1024)} GiB`, hint: `GPU mean ${decimal(integrated.gpuUtilizationMeanPercent)}% · ${decimal(integrated.gpuPowerMaximumW)} W` },
|
||||
] : [
|
||||
{ label: "Detector capacity", value: `${decimal(selected?.capacityFps ?? 0)} FPS`, hint: "RF-DETR-L TensorRT/Triton" },
|
||||
{ label: "Completion age p95", value: `${decimal(load.completionAgeP95Ms)} ms`, hint: "detector-only · target ≤ 175 ms" },
|
||||
{ label: "Consumed / replaced", value: `${load.sourceFramesConsumed.toLocaleString("ru-RU")} / ${load.sourceFrameReplacements}`, hint: `${decimal(load.effectiveConsumedFps, 3)} source FPS · ${load.failures} failures` },
|
||||
{ label: "GPU / VRAM peak", value: `${decimal(load.gpuUtilizationMaximumPercent, 0)}% / ${decimal(load.gpuMemoryMaximumMib / 1024)} GiB`, hint: `GPU mean ${decimal(load.gpuUtilizationMeanPercent)}% · queue ${load.queueMaximumDepth}/${load.queueCapacity}` },
|
||||
]}
|
||||
conclusion={{
|
||||
proved: integrated
|
||||
? `На Worker 006 полный граф обработал ${integrated.sourceFramesAdmitted.toLocaleString("ru-RU")} входных кадров, доставил ${integrated.deliveredWorldStates.toLocaleString("ru-RU")} состояний без ошибок, удержал все очереди в пределах ${integrated.queueCapacity} и p95 ${decimal(integrated.worldStateCompletionAgeP95Ms, 3)} ms. Advisory сформировал публикации по семействам: geometry-only (${integrated.advisoryFamilyCounts["generic-obstacle"].toLocaleString("ru-RU")}), люди (${integrated.advisoryFamilyCounts.person.toLocaleString("ru-RU")}), животные (${integrated.advisoryFamilyCounts.animal.toLocaleString("ru-RU")}) и транспорт (${integrated.advisoryFamilyCounts.vehicle.toLocaleString("ru-RU")}); это не количество уникальных физических объектов и не потребовало второго inference.`
|
||||
: `RF-DETR-L ${decimal(load.durationSeconds / 60, 0)} минут устойчиво потреблял source-paced поток около 10 FPS: ${load.sourceFramesConsumed.toLocaleString("ru-RU")} кадров, 0 замен, 0 ошибок, completion-age p95 ${decimal(load.completionAgeP95Ms, 3)} ms.`,
|
||||
notProved: "Не доказаны unbiased precision/recall классов, независимое качество track identity и risk policy, поведение planner или collision safety. Кадровые рамки не заменяют геометрическую occupancy-карту.",
|
||||
decision: "Сохранить RF-DETR-L как risk-semantic shadow provider полного reference graph. Не классифицировать миллионы статических форм: неизвестное неподвижное препятствие остаётся geometry-owned и объезжается; классы сохраняются для людей, животных и транспорта. Production switch не разрешён.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||
import {
|
||||
RecordedEvidenceBoxOverlay,
|
||||
type RecordedEvidenceBox,
|
||||
type RecordedEvidenceBoxTone,
|
||||
} from "../../components/laboratory/RecordedEvidenceBoxOverlay";
|
||||
import {
|
||||
fetchM48SFixedClassDetectorFrame,
|
||||
type M48SDetectorFrame,
|
||||
type M48SDetectorMode,
|
||||
type M48SFixedClassDetectorResult,
|
||||
} from "../../core/laboratory/m48sFixedClassDetector";
|
||||
|
||||
const MODES = [
|
||||
{ value: "source", label: "SOURCE" },
|
||||
{ value: "yolox", label: "YOLOX" },
|
||||
{ value: "dfine", label: "D-FINE" },
|
||||
{ value: "rf-detr", label: "RF-DETR" },
|
||||
] as const;
|
||||
|
||||
const ANIMAL_LABELS = new Set([
|
||||
"bird",
|
||||
"cat",
|
||||
"dog",
|
||||
"horse",
|
||||
"sheep",
|
||||
"cow",
|
||||
"elephant",
|
||||
"bear",
|
||||
"zebra",
|
||||
"giraffe",
|
||||
]);
|
||||
const VULNERABLE_ROAD_USERS = new Set(["person", "bicycle", "motorcycle", "skateboard"]);
|
||||
|
||||
function message(error: unknown): string {
|
||||
return error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
: "M4.8S visual evidence недоступно.";
|
||||
}
|
||||
|
||||
function toneForLabel(label: string): RecordedEvidenceBoxTone {
|
||||
if (ANIMAL_LABELS.has(label)) return "danger";
|
||||
if (VULNERABLE_ROAD_USERS.has(label)) return "warning";
|
||||
return "accent";
|
||||
}
|
||||
|
||||
function DetectorScene({ frame, mode }: { frame: M48SDetectorFrame; mode: M48SDetectorMode }) {
|
||||
const boxes = useMemo<readonly RecordedEvidenceBox[]>(() => {
|
||||
if (mode === "source") return [];
|
||||
return frame.detections[mode].map((detection) => ({
|
||||
boxXyxy: detection.bboxXyxy,
|
||||
label: `${detection.label} · ${detection.score.toFixed(2)}`,
|
||||
tone: toneForLabel(detection.label),
|
||||
}));
|
||||
}, [frame, mode]);
|
||||
|
||||
return (
|
||||
<div className="m48-atlas-visual__scene">
|
||||
<img src={frame.cameraUrl} alt="" draggable={false} />
|
||||
<RecordedEvidenceBoxOverlay
|
||||
imageWidth={frame.imageWidth}
|
||||
imageHeight={frame.imageHeight}
|
||||
boxes={boxes}
|
||||
ariaLabel={`M4.8S ${mode} fixed-class detections`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function M48SFixedClassDetectorVisual({
|
||||
result,
|
||||
}: {
|
||||
result: M48SFixedClassDetectorResult;
|
||||
}) {
|
||||
const preferredIndex = Math.max(
|
||||
0,
|
||||
result.frames.findIndex((frame) => frame.frameId === "000253"),
|
||||
);
|
||||
const [index, setIndex] = useState(preferredIndex);
|
||||
const [frame, setFrame] = useState<M48SDetectorFrame | null>(null);
|
||||
const [mode, setMode] = useState<M48SDetectorMode>("rf-detr");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const selected = result.frames[index] ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
setFrame(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
void fetchM48SFixedClassDetectorFrame(result.resultId, selected.frameId, {
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then((next) => !controller.signal.aborted && setFrame(next))
|
||||
.catch((caught: unknown) => !controller.signal.aborted && setError(message(caught)))
|
||||
.finally(() => !controller.signal.aborted && setLoading(false));
|
||||
return () => controller.abort();
|
||||
}, [result.resultId, selected]);
|
||||
|
||||
const count = frame && mode !== "source" ? frame.detections[mode].length : 0;
|
||||
return (
|
||||
<LaboratoryEvidenceViewer
|
||||
label="M4.8S fixed-class detector comparison"
|
||||
className="m48-atlas-visual"
|
||||
mode={mode}
|
||||
modes={MODES}
|
||||
expanded={expanded}
|
||||
onModeChange={setMode}
|
||||
onExpandedChange={setExpanded}
|
||||
actions={(
|
||||
<>
|
||||
<IconButton
|
||||
label="Предыдущий кадр M4.8S"
|
||||
disabled={!result.frames.length}
|
||||
onClick={() => setIndex((current) => (
|
||||
current - 1 + result.frames.length
|
||||
) % result.frames.length)}
|
||||
>
|
||||
<Icon name="chevron-left" size={16} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
label="Следующий кадр M4.8S"
|
||||
disabled={!result.frames.length}
|
||||
onClick={() => setIndex((current) => (current + 1) % result.frames.length)}
|
||||
>
|
||||
<Icon name="chevron-right" size={16} />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
overlay={selected ? (
|
||||
<div className="m48-atlas-visual__case">
|
||||
<StatusBadge tone="warning">SHADOW ONLY</StatusBadge>
|
||||
<strong>RAVNOVES00 · frame {selected.frameId} · {mode.toUpperCase()}</strong>
|
||||
<small>
|
||||
{count} risk detections · threshold 0.50 · independent ground truth отсутствует
|
||||
</small>
|
||||
</div>
|
||||
) : null}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="m48-atlas-visual__state" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
Загружаем точный camera-кадр
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="m48-atlas-visual__state" role="alert">
|
||||
<Icon name="alert" size={18} />
|
||||
{error}
|
||||
</div>
|
||||
) : frame ? (
|
||||
<DetectorScene frame={frame} mode={mode} />
|
||||
) : (
|
||||
<div className="m48-atlas-visual__state" role="alert">
|
||||
<Icon name="alert" size={18} />
|
||||
Каталог кадров M4.8S пуст.
|
||||
</div>
|
||||
)}
|
||||
</LaboratoryEvidenceViewer>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
|
||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||
import { RecordedEvidenceImageScene } from "../../components/laboratory/RecordedEvidenceImageScene";
|
||||
import type { RecordedEvidencePointCloudOverlayData } from "../../components/laboratory/RecordedEvidencePointCloudOverlay";
|
||||
import type {
|
||||
RecordedEvidenceSemanticClass,
|
||||
RecordedEvidenceSemanticOverlay,
|
||||
@@ -58,10 +59,14 @@ function toneForProposal(proposal: M4ThreatCameraProposal): RecordedEvidenceBox[
|
||||
}
|
||||
|
||||
function proposalLabel(proposal: M4ThreatCameraProposal): string {
|
||||
const decision = proposal.threatDecision ?? "unknown";
|
||||
if (proposal.rangeM === null) return decision;
|
||||
const decision = proposal.threatDecision
|
||||
?? (proposal.occupiedSupport ? "geometry-supported" : "camera-only");
|
||||
const semantic = proposal.semanticHint ?? "object";
|
||||
if (proposal.rangeM === null) {
|
||||
return `${semantic} · ${proposal.objectness.toFixed(2)} · ${decision}`;
|
||||
}
|
||||
const range = `${proposal.rangeM.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} м`;
|
||||
return `${range} · ${decision}`;
|
||||
return `${semantic} · ${proposal.objectness.toFixed(2)} · ${range} · ${decision}`;
|
||||
}
|
||||
|
||||
function boxes(proposals: readonly M4ThreatCameraProposal[]): readonly RecordedEvidenceBox[] {
|
||||
@@ -104,10 +109,14 @@ export function M4ReplayThreatVisual({
|
||||
resultId,
|
||||
semantic,
|
||||
reviewAnchors = EMPTY_REVIEW_ANCHORS,
|
||||
timelineEndpointRoot,
|
||||
evidenceLabel = "M4.6",
|
||||
}: {
|
||||
resultId: string;
|
||||
semantic?: M4ReplayThreatSemanticLayer;
|
||||
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
|
||||
timelineEndpointRoot?: string;
|
||||
evidenceLabel?: string;
|
||||
}) {
|
||||
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
|
||||
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(null);
|
||||
@@ -116,6 +125,7 @@ export function M4ReplayThreatVisual({
|
||||
const [showRollingMap, setShowRollingMap] = useState(true);
|
||||
const [showMediaSemantic, setShowMediaSemantic] = useState(true);
|
||||
const [showSpatialSemantic, setShowSpatialSemantic] = useState(true);
|
||||
const [showMediaPoints, setShowMediaPoints] = useState(false);
|
||||
const [splitPrimarySize, setSplitPrimarySize] = useState(50);
|
||||
const [splitOrientation, setSplitOrientation] = useState<SplitPaneOrientation>(() => (
|
||||
typeof window !== "undefined" && window.matchMedia("(max-width: 900px)").matches
|
||||
@@ -125,7 +135,7 @@ export function M4ReplayThreatVisual({
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0);
|
||||
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
|
||||
const metadata = useM4ThreatTimelineMetadata(resultId);
|
||||
const metadata = useM4ThreatTimelineMetadata(resultId, timelineEndpointRoot);
|
||||
const playbackRange = useMemo(() => metadata.timeline ? ({
|
||||
startSeconds: metadata.timeline.timelineStartSeconds,
|
||||
endSeconds: metadata.timeline.timelineEndSeconds,
|
||||
@@ -139,6 +149,7 @@ export function M4ReplayThreatVisual({
|
||||
resultId,
|
||||
timeline: metadata.timeline,
|
||||
currentSeconds: playbackController.playback.currentSeconds,
|
||||
endpointRoot: timelineEndpointRoot,
|
||||
});
|
||||
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
|
||||
const [videoLoading, setVideoLoading] = useState(false);
|
||||
@@ -362,6 +373,16 @@ export function M4ReplayThreatVisual({
|
||||
ariaLabel: `E47 semantic mask frame ${frame.sequence + 1}`,
|
||||
}
|
||||
: undefined;
|
||||
const pointCloudOverlay: RecordedEvidencePointCloudOverlayData | undefined =
|
||||
showMediaPoints && frame?.cameraProjection === "factory-kb4-exact"
|
||||
? {
|
||||
pointsXyd: frame.cameraProjectedPointsXyd,
|
||||
sourcePointCount: frame.cameraProjectedSourceCount,
|
||||
projectedPointCount: frame.cameraProjectedPointCount,
|
||||
projection: "factory-kb4-exact",
|
||||
ariaLabel: `${evidenceLabel} LiDAR projection: ${frame.cameraProjectedSampleCount} points`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const handleMediaModeChange = (next: M4ThreatMediaSelection) => {
|
||||
if (next === "none") return;
|
||||
@@ -408,21 +429,35 @@ export function M4ReplayThreatVisual({
|
||||
</div>
|
||||
);
|
||||
|
||||
const mediaLayerControls = semantic ? (
|
||||
const mediaLayerControls = semantic || metadata.timeline?.cameraPointDelivery ? (
|
||||
<div
|
||||
className="m4-replay-threat-visual__pane-layer-controls"
|
||||
role="group"
|
||||
aria-label="Слои камеры и видео"
|
||||
>
|
||||
<Button
|
||||
size="compact"
|
||||
shape="pill"
|
||||
variant={showMediaSemantic ? "primary" : "secondary"}
|
||||
aria-pressed={showMediaSemantic}
|
||||
onClick={() => setShowMediaSemantic((visible) => !visible)}
|
||||
>
|
||||
SEMANTICS
|
||||
</Button>
|
||||
{semantic ? (
|
||||
<Button
|
||||
size="compact"
|
||||
shape="pill"
|
||||
variant={showMediaSemantic ? "primary" : "secondary"}
|
||||
aria-pressed={showMediaSemantic}
|
||||
onClick={() => setShowMediaSemantic((visible) => !visible)}
|
||||
>
|
||||
SEMANTICS
|
||||
</Button>
|
||||
) : null}
|
||||
{metadata.timeline?.cameraPointDelivery ? (
|
||||
<Button
|
||||
size="compact"
|
||||
shape="pill"
|
||||
variant={showMediaPoints ? "primary" : "secondary"}
|
||||
aria-pressed={showMediaPoints}
|
||||
title="Exact LiDAR increment · factory KB4 camera projection"
|
||||
onClick={() => setShowMediaPoints((visible) => !visible)}
|
||||
>
|
||||
POINTS
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
@@ -570,6 +605,12 @@ export function M4ReplayThreatVisual({
|
||||
{spatialFrame
|
||||
? `${spatialFrame.pointCloudSampleCount}/${spatialFrame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
|
||||
: "квалифицированный spatial frame ещё не получен"}
|
||||
{frame.worldStateAvailable
|
||||
? " · world-state delivered"
|
||||
: ` · world-state gap (${frame.terminalOutcome})`}
|
||||
{pointCloudOverlay
|
||||
? ` · camera points ${frame.cameraProjectedSampleCount}/${frame.cameraProjectedPointCount}`
|
||||
: ""}
|
||||
{semantic && spatialSemanticFrame
|
||||
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
|
||||
: semantic ? " · semantic buffer" : ""}
|
||||
@@ -595,7 +636,7 @@ export function M4ReplayThreatVisual({
|
||||
content = (
|
||||
<div className="l3-visual-audit__state" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
<span>Открываем recorded-realtime timeline M4.6</span>
|
||||
<span>Открываем recorded-realtime timeline {evidenceLabel}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
@@ -628,7 +669,8 @@ export function M4ReplayThreatVisual({
|
||||
imageHeight={timeline.imageHeight}
|
||||
boxes={activeBoxes}
|
||||
semanticOverlay={mediaMode === "video" ? semanticOverlay : undefined}
|
||||
ariaLabel={`M4.6 recorded-realtime frame ${frame?.sequence ?? 0}: ${activeBoxes.length} proposals`}
|
||||
pointCloudOverlay={mediaMode === "video" ? pointCloudOverlay : undefined}
|
||||
ariaLabel={`${evidenceLabel} recorded-realtime frame ${frame?.sequence ?? 0}: ${activeBoxes.length} proposals`}
|
||||
interactive={false}
|
||||
segmentSequence={
|
||||
timelineFrame.activeSequence === null
|
||||
@@ -655,7 +697,8 @@ export function M4ReplayThreatVisual({
|
||||
imageHeight={timeline.imageHeight}
|
||||
boxes={activeBoxes}
|
||||
semanticOverlay={semanticOverlay}
|
||||
ariaLabel={`M4.6 exact camera frame ${frame.sequence}: ${activeBoxes.length} proposals`}
|
||||
pointCloudOverlay={pointCloudOverlay}
|
||||
ariaLabel={`${evidenceLabel} exact camera frame ${frame.sequence}: ${activeBoxes.length} proposals`}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
@@ -689,7 +732,7 @@ export function M4ReplayThreatVisual({
|
||||
corridor={timeline.corridor}
|
||||
occupiedVoxelSizeM={timeline.occupiedVoxelSizeM}
|
||||
mode={spatialMode}
|
||||
label="M4.6 exact current increment, bounded local SLAM surface and rolling occupancy"
|
||||
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
|
||||
showCurrentIncrement={showCurrentIncrement}
|
||||
showLocalSurface={showLocalSurface}
|
||||
showRollingMap={showRollingMap}
|
||||
@@ -791,7 +834,7 @@ export function M4ReplayThreatVisual({
|
||||
<LaboratoryEvidenceViewer
|
||||
label={semantic
|
||||
? "E47 semantic + SLAM diagnostic replay"
|
||||
: "M4.6 dual-evidence recorded-realtime replay"}
|
||||
: `${evidenceLabel} recorded-realtime replay`}
|
||||
className="m4-replay-threat-evidence-viewer"
|
||||
mode={mediaMode ?? "none"}
|
||||
modes={[
|
||||
|
||||
@@ -77,6 +77,13 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
|
||||
experimentName: "M4.8 · small static passage regression",
|
||||
variantName: "M4.8R1 · Worker 006 small-static assisted baseline",
|
||||
},
|
||||
"m48s-fixed-class-detector": {
|
||||
profileId: "rig-ravnoves-perception-gate-v1",
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 perception gate`,
|
||||
experimentId: "m48s-fixed-class-risk-detector",
|
||||
experimentName: "RAVNOVES00 fixed-class risk detector",
|
||||
variantName: "M4.8S · RF-DETR-L TensorRT/Triton shadow",
|
||||
},
|
||||
"m47-reference-graph-shadow": {
|
||||
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`,
|
||||
|
||||
@@ -21,6 +21,7 @@ function mergeResults(
|
||||
m47Graph: next.m47Graph ?? current.m47Graph,
|
||||
m48: next.m48 ?? current.m48,
|
||||
m48SmallStatic: next.m48SmallStatic ?? current.m48SmallStatic,
|
||||
m48s: next.m48s ?? current.m48s,
|
||||
m4Threat: next.m4Threat ?? current.m4Threat,
|
||||
l3: next.l3 ?? current.l3,
|
||||
l31: next.l31 ?? current.l31,
|
||||
|
||||
@@ -41,7 +41,7 @@ export function cancelM4ThreatChunkRequestsOutsideWindow<T extends { abort(): vo
|
||||
}
|
||||
}
|
||||
|
||||
export function useM4ThreatTimelineMetadata(resultId: string) {
|
||||
export function useM4ThreatTimelineMetadata(resultId: string, endpointRoot?: string) {
|
||||
const [timeline, setTimeline] = useState<M4ThreatTimeline | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -49,7 +49,7 @@ export function useM4ThreatTimelineMetadata(resultId: string) {
|
||||
const controller = new AbortController();
|
||||
setTimeline(null);
|
||||
setError(null);
|
||||
void fetchM4ThreatTimeline(resultId, { signal: controller.signal })
|
||||
void fetchM4ThreatTimeline(resultId, { signal: controller.signal, endpointRoot })
|
||||
.then((next) => {
|
||||
if (!controller.signal.aborted) setTimeline(next);
|
||||
})
|
||||
@@ -59,7 +59,7 @@ export function useM4ThreatTimelineMetadata(resultId: string) {
|
||||
}
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [resultId]);
|
||||
}, [endpointRoot, resultId]);
|
||||
|
||||
return { timeline, loading: !timeline && !error, error };
|
||||
}
|
||||
@@ -68,10 +68,12 @@ export function useM4ThreatTimelineFrame({
|
||||
resultId,
|
||||
timeline,
|
||||
currentSeconds,
|
||||
endpointRoot,
|
||||
}: {
|
||||
resultId: string;
|
||||
timeline: M4ThreatTimeline | null;
|
||||
currentSeconds: number;
|
||||
endpointRoot?: string;
|
||||
}) {
|
||||
const [chunks, setChunks] = useState<ReadonlyMap<number, M4ThreatTimelineChunk>>(
|
||||
() => new Map(),
|
||||
@@ -124,6 +126,7 @@ export function useM4ThreatTimelineFrame({
|
||||
inFlight.current.set(start, controller);
|
||||
void fetchM4ThreatTimelineChunk(resultId, start, chunkSize, {
|
||||
signal: controller.signal,
|
||||
endpointRoot,
|
||||
})
|
||||
.then((chunk) => {
|
||||
if (controller.signal.aborted) return;
|
||||
@@ -151,7 +154,7 @@ export function useM4ThreatTimelineFrame({
|
||||
if (inFlight.current.get(start) === controller) inFlight.current.delete(start);
|
||||
});
|
||||
}
|
||||
}, [activeChunkStart, chunkSize, resultId, timeline]);
|
||||
}, [activeChunkStart, chunkSize, endpointRoot, resultId, timeline]);
|
||||
|
||||
const activeFrame: M4ThreatTimelineFrame | null = useMemo(() => {
|
||||
if (activeSequence === null || activeChunkStart === null) return null;
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, before, test } from "node:test";
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let fetchM48SFixedClassDetectorResult;
|
||||
let fetchM48SFixedClassDetectorFrame;
|
||||
|
||||
const resultId = `m48s-fixed-class-detector-lab-${"b".repeat(64)}`;
|
||||
const authority = {
|
||||
actuation_allowed: false,
|
||||
candidate_accepted: false,
|
||||
commands_enabled: false,
|
||||
ground_truth: false,
|
||||
navigation_or_safety_accepted: false,
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({
|
||||
fetchM48SFixedClassDetectorResult,
|
||||
fetchM48SFixedClassDetectorFrame,
|
||||
} = await server.ssrLoadModule("/src/core/laboratory/m48sFixedClassDetector.ts"));
|
||||
});
|
||||
|
||||
after(async () => server?.close());
|
||||
|
||||
function response(value) {
|
||||
return { ok: true, status: 200, json: async () => value };
|
||||
}
|
||||
|
||||
function resultPayload() {
|
||||
const candidate = (id, overrides = {}) => ({
|
||||
id,
|
||||
label: id,
|
||||
provider_id: `${id}/v1`,
|
||||
capacity_fps: 40,
|
||||
p95_ms: 35,
|
||||
frame_253_dog_detected: false,
|
||||
frame_253_dog_score: null,
|
||||
selected: false,
|
||||
...overrides,
|
||||
});
|
||||
return {
|
||||
schema_version: "missioncore.m48s-fixed-class-detector-result-view/v1",
|
||||
result_id: resultId,
|
||||
created_at_utc: "2026-08-25T11:06:28Z",
|
||||
status: "complete-reference-graph-shadow-passed-production-not-authorized",
|
||||
bounded_question_accepted: true,
|
||||
ground_truth: false,
|
||||
source: {
|
||||
source_session_id: "RAVNOVES00",
|
||||
camera_source_id: "sensor.camera.right",
|
||||
camera_raster: [800, 600],
|
||||
evidence_frame_count: 1,
|
||||
},
|
||||
configuration: {
|
||||
comparison_threshold: 0.5,
|
||||
display_modes: ["source", "yolox", "dfine", "rf-detr"],
|
||||
single_inference_per_frame: true,
|
||||
geometry_owns_static_occupancy: true,
|
||||
unknown_stationary_response: "route-around",
|
||||
unknown_moving_response: "conservative-risk",
|
||||
},
|
||||
method: {
|
||||
schema_version: "missioncore.laboratory-method/v1",
|
||||
completeness: "complete",
|
||||
execution_class: "ai-inference",
|
||||
pipeline_id: "raw-kb4-fixed-class-risk-detector-tournament/v1",
|
||||
components: [{
|
||||
kind: "model",
|
||||
name: "RF-DETR-L COCO",
|
||||
version: "trt11-fp16",
|
||||
role: "selected fixed-class risk detector",
|
||||
identity_sha256: "9".repeat(64),
|
||||
}],
|
||||
},
|
||||
metrics: {
|
||||
candidates: [
|
||||
candidate("yolox"),
|
||||
candidate("dfine"),
|
||||
candidate("rf-detr", {
|
||||
label: "RF-DETR-L",
|
||||
capacity_fps: 42.496232,
|
||||
frame_253_dog_detected: true,
|
||||
frame_253_dog_score: 0.741674,
|
||||
selected: true,
|
||||
}),
|
||||
],
|
||||
detector_load: {
|
||||
duration_seconds: 1800.019643,
|
||||
source_frames_consumed: 18008,
|
||||
source_frame_replacements: 0,
|
||||
effective_consumed_fps: 10.004444,
|
||||
end_to_end_p95_ms: 32.41534,
|
||||
completion_age_p95_ms: 40.620542,
|
||||
gpu_utilization_mean_percent: 51.407556,
|
||||
gpu_utilization_maximum_percent: 65,
|
||||
gpu_memory_maximum_mib: 9556,
|
||||
queue_maximum_depth: 1,
|
||||
queue_capacity: 2,
|
||||
failures: 0,
|
||||
},
|
||||
integrated_world_state: {
|
||||
duration_seconds: 458.900859,
|
||||
source_frames_admitted: 4489,
|
||||
delivered_world_states: 4481,
|
||||
superseded_frames: 8,
|
||||
effective_world_state_fps: 9.764636,
|
||||
world_state_completion_age_p95_ms: 74.733648,
|
||||
world_state_completion_age_p99_ms: 102.62048,
|
||||
world_state_completion_age_maximum_ms: 669.142137,
|
||||
local_obstacle_map_output_age_p95_ms: 70.635141,
|
||||
queue_high_watermarks: { detector: 2, geometry: 2, temporal: 2, rolling: 2, threat: 2 },
|
||||
queue_capacity: 2,
|
||||
gpu_utilization_mean_percent: 50.903371,
|
||||
gpu_utilization_maximum_percent: 60,
|
||||
gpu_memory_maximum_mib: 9576,
|
||||
gpu_power_maximum_w: 153.51,
|
||||
gpu_temperature_maximum_c: 39,
|
||||
unique_component_count: 44979,
|
||||
multi_frame_component_count: 15887,
|
||||
maximum_component_publications: 219,
|
||||
duplicate_component_ids_within_frame: 0,
|
||||
advisory_family_counts: {
|
||||
animal: 63,
|
||||
"generic-obstacle": 123919,
|
||||
"light-road-user": 329,
|
||||
person: 5600,
|
||||
vehicle: 55818,
|
||||
},
|
||||
semantic_hint_counts: { dog: 60, person: 5600, "geometry-only": 123919 },
|
||||
motion_counts: { moving: 12252, stationary: 1433, unknown: 172044 },
|
||||
additional_inference_passes: 0,
|
||||
failures: 0,
|
||||
},
|
||||
},
|
||||
decision: {
|
||||
selected_candidate: "rf-detr",
|
||||
ready_for_reference_graph_shadow: true,
|
||||
integrated_world_state_gate_evaluated: true,
|
||||
integrated_world_state_gate_passed: true,
|
||||
detector_replacement_authorized: false,
|
||||
production_accepted: false,
|
||||
},
|
||||
limitations: ["No independent semantic ground truth."],
|
||||
authority,
|
||||
frames: [{
|
||||
frame_id: "000253",
|
||||
source_sequence: 253,
|
||||
counts: { yolox: 4, dfine: 7, "rf-detr": 8 },
|
||||
}],
|
||||
access: "read-only",
|
||||
};
|
||||
}
|
||||
|
||||
test("M4.8S result exposes complete graph load without production authority", async () => {
|
||||
const result = await fetchM48SFixedClassDetectorResult(resultId, {
|
||||
fetcher: async (url) => {
|
||||
assert.equal(
|
||||
url,
|
||||
`/api/v1/laboratory/m48s/fixed-class-detector/${resultId}`,
|
||||
);
|
||||
return response(resultPayload());
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.metrics.detectorLoad.sourceFramesConsumed, 18008);
|
||||
assert.equal(result.metrics.detectorLoad.completionAgeP95Ms, 40.620542);
|
||||
assert.equal(result.metrics.candidates[2].frame253DogScore, 0.741674);
|
||||
assert.equal(result.metrics.integratedWorldState.deliveredWorldStates, 4481);
|
||||
assert.equal(result.metrics.integratedWorldState.worldStateCompletionAgeP95Ms, 74.733648);
|
||||
assert.equal(result.metrics.integratedWorldState.additionalInferencePasses, 0);
|
||||
assert.equal(result.decision.integratedWorldStateGatePassed, true);
|
||||
assert.equal(result.decision.productionAccepted, false);
|
||||
assert.equal(result.authority.navigationOrSafetyAccepted, false);
|
||||
});
|
||||
|
||||
test("M4.8S frame binds exact camera endpoint and risk-only boxes", async () => {
|
||||
const frame = await fetchM48SFixedClassDetectorFrame(resultId, "000253", {
|
||||
fetcher: async () => response({
|
||||
schema_version: "missioncore.m48s-fixed-class-detector-frame/v1",
|
||||
result_id: resultId,
|
||||
frame_id: "000253",
|
||||
source_sequence: 253,
|
||||
camera: {
|
||||
media_type: "image/jpeg",
|
||||
width: 800,
|
||||
height: 600,
|
||||
exact_source_frame: true,
|
||||
},
|
||||
comparison_threshold: 0.5,
|
||||
detections: {
|
||||
yolox: [],
|
||||
dfine: [{ label: "skateboard", score: 0.782227, bbox_xyxy: [236, 334, 274, 372] }],
|
||||
"rf-detr": [{ label: "dog", score: 0.741674, bbox_xyxy: [235, 336, 273, 376] }],
|
||||
},
|
||||
ground_truth_available: false,
|
||||
authority,
|
||||
access: "read-only",
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(frame.detections["rf-detr"][0].label, "dog");
|
||||
assert.equal(
|
||||
frame.cameraUrl,
|
||||
`/api/v1/laboratory/m48s/fixed-class-detector/${resultId}/frames/000253/camera`,
|
||||
);
|
||||
assert.equal(frame.groundTruthAvailable, false);
|
||||
});
|
||||
|
||||
test("M4.8S adapter rejects any navigation authority escalation", async () => {
|
||||
await assert.rejects(
|
||||
fetchM48SFixedClassDetectorResult(resultId, {
|
||||
fetcher: async () => response({
|
||||
...resultPayload(),
|
||||
authority: { ...authority, navigation_or_safety_accepted: true },
|
||||
}),
|
||||
}),
|
||||
/navigation_or_safety_accepted/,
|
||||
);
|
||||
});
|
||||
@@ -326,6 +326,100 @@ test("M4.6 timeline keeps only a compact index and decodes bounded spatial chunk
|
||||
assert.equal(selectM4ThreatTimelineFrame(chunk.frames, 35.50).sequence, 1);
|
||||
});
|
||||
|
||||
test("M4.8S timeline binds factory-KB4 camera points through its exact endpoint", async () => {
|
||||
const replayResultId = `m48s-fixed-class-detector-lab-${"b".repeat(64)}`;
|
||||
const endpointRoot = "/api/v1/laboratory/m48s/fixed-class-detector";
|
||||
const frameTimesNs = Array.from(
|
||||
{ length: 4489 },
|
||||
(_, index) => 35_421_857_292 + index * 100_000_000,
|
||||
);
|
||||
let requested = "";
|
||||
const timeline = await fetchM4ThreatTimeline(replayResultId, {
|
||||
endpointRoot,
|
||||
fetcher: async (input) => {
|
||||
requested = String(input);
|
||||
return new Response(JSON.stringify({
|
||||
schema_version: "missioncore.recorded-spatial-evidence-timeline/v1",
|
||||
result_id: replayResultId,
|
||||
recorded_source: {
|
||||
session_id: "20260720T065719Z_viewer_live",
|
||||
source_id: "RAVNOVES00",
|
||||
representation_id: "registered-map-increment-v1",
|
||||
synchronization: "host-arrival-best-effort",
|
||||
},
|
||||
image_width: 800,
|
||||
image_height: 600,
|
||||
frame_count: 4489,
|
||||
frame_times_ns: frameTimesNs,
|
||||
timeline_start_seconds: 35.421857292,
|
||||
timeline_end_seconds: 484.221857292,
|
||||
nominal_frame_interval_seconds: 0.1,
|
||||
nominal_rate_hz: 10,
|
||||
max_chunk_frames: 24,
|
||||
point_sample_limit: 4096,
|
||||
maximum_source_points_per_frame: 3092,
|
||||
point_delivery: "exact-current-increment",
|
||||
camera_point_delivery: "factory-kb4-projected-current-increment",
|
||||
camera_point_sample_limit: 4096,
|
||||
world_state_delivery: "source-paced-latest-wins",
|
||||
world_state_frame_count: 4481,
|
||||
superseded_frame_count: 8,
|
||||
local_surface_visualization: {
|
||||
derivation: "bounded-registered-increment-accumulation",
|
||||
window_seconds: 2,
|
||||
voxel_size_m: 0.1,
|
||||
radius_m: 12,
|
||||
point_limit: 20000,
|
||||
authority: "visual-derived",
|
||||
},
|
||||
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,
|
||||
occupied_voxel_size_m: 0.45,
|
||||
prediction_horizon_seconds: 5,
|
||||
},
|
||||
authority: "replay-simulated",
|
||||
}), { status: 200 });
|
||||
},
|
||||
});
|
||||
assert.equal(requested, `${endpointRoot}/${replayResultId}/timeline`);
|
||||
assert.equal(timeline.cameraPointDelivery, "factory-kb4-projected-current-increment");
|
||||
assert.equal(timeline.worldStateFrameCount, 4481);
|
||||
assert.equal(timeline.supersededFrameCount, 8);
|
||||
|
||||
const chunk = await fetchM4ThreatTimelineChunk(replayResultId, 1, 1, {
|
||||
endpointRoot,
|
||||
fetcher: async (input) => {
|
||||
requested = String(input);
|
||||
return new Response(JSON.stringify({
|
||||
schema_version: "missioncore.recorded-spatial-evidence-chunk/v1",
|
||||
result_id: replayResultId,
|
||||
start_sequence: 1,
|
||||
frame_count: 1,
|
||||
next_sequence: 2,
|
||||
frames: [timelineFrame(1, 35.521857292, {
|
||||
world_state_available: false,
|
||||
terminal_outcome: "superseded",
|
||||
camera_projected_points_xyd: [[100.5, 200.25, 3.75]],
|
||||
camera_projected_source_count: 847,
|
||||
camera_projected_point_count: 1,
|
||||
camera_projected_sample_count: 1,
|
||||
camera_projection: "factory-kb4-exact",
|
||||
camera_url: `${endpointRoot}/${replayResultId}/timeline/frames/1/camera`,
|
||||
})],
|
||||
authority: "replay-simulated",
|
||||
}), { status: 200 });
|
||||
},
|
||||
});
|
||||
assert.match(requested, new RegExp(`^${endpointRoot}/${replayResultId}/timeline/chunk`));
|
||||
assert.equal(chunk.frames[0].worldStateAvailable, false);
|
||||
assert.equal(chunk.frames[0].terminalOutcome, "superseded");
|
||||
assert.deepEqual(chunk.frames[0].cameraProjectedPointsXyd[0], [100.5, 200.25, 3.75]);
|
||||
assert.equal(chunk.frames[0].cameraProjection, "factory-kb4-exact");
|
||||
});
|
||||
|
||||
test("M4.6 local SLAM surface reprojects registered increments into the active body frame", () => {
|
||||
const frames = [
|
||||
timelineFrame(0, 10, {
|
||||
@@ -453,11 +547,12 @@ test("recorded VIDEO clock cannot reverse an explicit operator pause", () => {
|
||||
});
|
||||
|
||||
test("M4.6 viewer keeps media and spatial panes on one playback clock", async () => {
|
||||
const [visual, visualCss, imageScene, videoScene, metricScene] = await Promise.all([
|
||||
const [visual, visualCss, imageScene, videoScene, pointOverlay, metricScene] = await Promise.all([
|
||||
readFile(new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../src/styles/m4-replay-threat.css", import.meta.url), "utf8"),
|
||||
readFile(new URL("../src/components/laboratory/RecordedEvidenceImageScene.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../src/components/laboratory/RecordedEvidenceVideoScene.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../src/components/laboratory/RecordedEvidencePointCloudOverlay.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../src/components/laboratory/LaboratoryMetricEvidenceScene.tsx", import.meta.url), "utf8"),
|
||||
]);
|
||||
assert.match(visual, /<RecordedEvidenceVideoScene/);
|
||||
@@ -474,6 +569,8 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
|
||||
assert.match(visual, /label: "CAMERA"/);
|
||||
assert.match(visual, /label: "3D"/);
|
||||
assert.match(visual, /label: "PLAN"/);
|
||||
assert.match(visual, />\s*POINTS\s*</);
|
||||
assert.match(visual, /pointCloudOverlay=/);
|
||||
assert.match(visual, /mediaMode/);
|
||||
assert.match(visual, /spatialMode/);
|
||||
assert.match(visual, /current === next \? null : next/);
|
||||
@@ -504,6 +601,9 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
|
||||
assert.match(visualCss, /bottom: auto/);
|
||||
assert.match(videoScene, /<RecordedFmp4Player/);
|
||||
assert.match(imageScene, /<RecordedEvidenceBoxOverlay/);
|
||||
assert.match(imageScene, /<RecordedEvidencePointCloudOverlay/);
|
||||
assert.match(videoScene, /<RecordedEvidencePointCloudOverlay/);
|
||||
assert.match(pointOverlay, /factory-kb4-exact/);
|
||||
assert.match(metricScene, /OrbitControls/);
|
||||
assert.match(visual, /LOCAL SLAM/);
|
||||
assert.match(visual, /showLocalSurface/);
|
||||
|
||||
Reference in New Issue
Block a user