|
|
|
@@ -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);
|
|
|
|
|
}
|