|
|
|
@@ -0,0 +1,516 @@
|
|
|
|
|
export type E47SemanticDisposition = "labeled" | "ambiguous";
|
|
|
|
|
|
|
|
|
|
export interface E47SemanticClass {
|
|
|
|
|
classId: number;
|
|
|
|
|
label: string;
|
|
|
|
|
disposition: E47SemanticDisposition;
|
|
|
|
|
colorRgb: readonly [number, number, number];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface E47SemanticSlamResult {
|
|
|
|
|
resultId: string;
|
|
|
|
|
createdAtUtc: string;
|
|
|
|
|
status: "diagnostic-semantic-slam-shadow";
|
|
|
|
|
profileId: string;
|
|
|
|
|
baseM4ResultId: string;
|
|
|
|
|
semanticResultId: string;
|
|
|
|
|
geometryResultId: string;
|
|
|
|
|
sourcePackId: string;
|
|
|
|
|
calibrationContentSha256: string;
|
|
|
|
|
provider: {
|
|
|
|
|
providerId: string;
|
|
|
|
|
modelId: string;
|
|
|
|
|
modelRevision: string;
|
|
|
|
|
modelWeightsSha256: string;
|
|
|
|
|
preprocessId: string;
|
|
|
|
|
};
|
|
|
|
|
temporalBinding: {
|
|
|
|
|
semanticToCamera: "exact-sequence-and-session-time";
|
|
|
|
|
cameraToLidar: "accepted-e6-nearest-host-arrival-best-effort";
|
|
|
|
|
clockBasis: "recorded-host-monotonic-arrival";
|
|
|
|
|
maximumLidarCameraDeltaMs: number;
|
|
|
|
|
maximumPosePointDeltaMs: number;
|
|
|
|
|
physicalSynchronizationProven: false;
|
|
|
|
|
};
|
|
|
|
|
taxonomy: readonly E47SemanticClass[];
|
|
|
|
|
metrics: {
|
|
|
|
|
frames: {
|
|
|
|
|
total: number;
|
|
|
|
|
maskAvailable: number;
|
|
|
|
|
sourceAvailable: number;
|
|
|
|
|
};
|
|
|
|
|
points: {
|
|
|
|
|
total: number;
|
|
|
|
|
projected: number;
|
|
|
|
|
labeled: number;
|
|
|
|
|
ambiguous: number;
|
|
|
|
|
unprojected: number;
|
|
|
|
|
absent: number;
|
|
|
|
|
};
|
|
|
|
|
observations: {
|
|
|
|
|
total: number;
|
|
|
|
|
labeled: number;
|
|
|
|
|
ambiguous: number;
|
|
|
|
|
unprojected: number;
|
|
|
|
|
absent: number;
|
|
|
|
|
};
|
|
|
|
|
runtime: {
|
|
|
|
|
elapsedMs: number;
|
|
|
|
|
framesPerSecond: number;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
acceptance: {
|
|
|
|
|
artifactContractPassed: boolean;
|
|
|
|
|
frameAccountingPassed: boolean;
|
|
|
|
|
pointAccountingPassed: boolean;
|
|
|
|
|
observationBindingPassed: boolean;
|
|
|
|
|
temporalBindingPassed: boolean;
|
|
|
|
|
independentSemanticTruthPassed: false;
|
|
|
|
|
providerPromoted: false;
|
|
|
|
|
};
|
|
|
|
|
limitations: readonly string[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface E47SemanticTimelineFrame {
|
|
|
|
|
sequence: number;
|
|
|
|
|
sourcePointCount: number;
|
|
|
|
|
classIds: readonly number[];
|
|
|
|
|
statusCodes: readonly number[];
|
|
|
|
|
counts: {
|
|
|
|
|
labeled: number;
|
|
|
|
|
ambiguous: number;
|
|
|
|
|
unprojected: number;
|
|
|
|
|
absent: number;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface E47SemanticTimelineChunk {
|
|
|
|
|
resultId: string;
|
|
|
|
|
startSequence: number;
|
|
|
|
|
frameCount: number;
|
|
|
|
|
nextSequence: number | null;
|
|
|
|
|
frames: readonly E47SemanticTimelineFrame[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type LaboratoryFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
|
|
|
|
|
|
|
|
export class E47SemanticSlamContractError extends Error {}
|
|
|
|
|
|
|
|
|
|
const object = (value: unknown, label: string): Record<string, unknown> => {
|
|
|
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
|
|
|
throw new E47SemanticSlamContractError(`${label}: ожидался объект.`);
|
|
|
|
|
}
|
|
|
|
|
return value as Record<string, unknown>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const array = (value: unknown, label: string): readonly unknown[] => {
|
|
|
|
|
if (!Array.isArray(value)) {
|
|
|
|
|
throw new E47SemanticSlamContractError(`${label}: ожидался массив.`);
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const text = (value: unknown, label: string): string => {
|
|
|
|
|
if (typeof value !== "string" || !value.trim()) {
|
|
|
|
|
throw new E47SemanticSlamContractError(`${label}: ожидалась строка.`);
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const finite = (value: unknown, label: string): number => {
|
|
|
|
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
|
|
|
throw new E47SemanticSlamContractError(`${label}: ожидалось число.`);
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const integer = (value: unknown, label: string): number => {
|
|
|
|
|
const parsed = finite(value, label);
|
|
|
|
|
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
|
|
|
throw new E47SemanticSlamContractError(`${label}: ожидалось неотрицательное целое.`);
|
|
|
|
|
}
|
|
|
|
|
return parsed;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const exact = <T extends string | number | boolean>(
|
|
|
|
|
value: unknown,
|
|
|
|
|
expected: T,
|
|
|
|
|
label: string,
|
|
|
|
|
): T => {
|
|
|
|
|
if (value !== expected) {
|
|
|
|
|
throw new E47SemanticSlamContractError(`${label}: нарушен контракт.`);
|
|
|
|
|
}
|
|
|
|
|
return expected;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function resultId(value: unknown): string {
|
|
|
|
|
const parsed = text(value, "E47 result id");
|
|
|
|
|
if (!/^e47-semantic-slam-[a-f0-9]{64}$/.test(parsed)) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 result id: нарушена идентичность.");
|
|
|
|
|
}
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function m4ResultId(value: unknown): string {
|
|
|
|
|
const parsed = text(value, "E47 base M4 result id");
|
|
|
|
|
if (!/^m4-threat-replay-[a-f0-9]{64}$/.test(parsed)) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 base M4 result id: нарушена идентичность.");
|
|
|
|
|
}
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sha256(value: unknown, label: string): string {
|
|
|
|
|
const parsed = text(value, label);
|
|
|
|
|
if (!/^[a-f0-9]{64}$/.test(parsed)) {
|
|
|
|
|
throw new E47SemanticSlamContractError(`${label}: ожидался SHA-256.`);
|
|
|
|
|
}
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function taxonomy(value: unknown): readonly E47SemanticClass[] {
|
|
|
|
|
const classes = array(value, "E47 taxonomy").map((raw) => {
|
|
|
|
|
const item = object(raw, "E47 semantic class");
|
|
|
|
|
const classId = integer(item.class_id, "E47 class id");
|
|
|
|
|
if (classId > 255) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 class id: вышел за uint8.");
|
|
|
|
|
}
|
|
|
|
|
const disposition = text(item.disposition, "E47 class disposition");
|
|
|
|
|
if (disposition !== "labeled" && disposition !== "ambiguous") {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 class disposition: неизвестное значение.");
|
|
|
|
|
}
|
|
|
|
|
const rgb = array(item.color_rgb, "E47 class color").map(
|
|
|
|
|
(channel) => integer(channel, "E47 color channel"),
|
|
|
|
|
);
|
|
|
|
|
if (rgb.length !== 3 || rgb.some((channel) => channel > 255)) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 class color: нарушен RGB-контракт.");
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
classId,
|
|
|
|
|
label: text(item.label, "E47 class label"),
|
|
|
|
|
disposition: disposition as E47SemanticDisposition,
|
|
|
|
|
colorRgb: [rgb[0]!, rgb[1]!, rgb[2]!] as const,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
if (!classes.length || new Set(classes.map((item) => item.classId)).size !== classes.length) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 taxonomy: классы отсутствуют или дублируются.");
|
|
|
|
|
}
|
|
|
|
|
return classes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseResult(value: unknown): E47SemanticSlamResult {
|
|
|
|
|
const item = object(value, "E47 result");
|
|
|
|
|
exact(item.schema_version, "missioncore.e47-semantic-slam-view/v1", "E47 view schema");
|
|
|
|
|
exact(item.status, "diagnostic-semantic-slam-shadow", "E47 status");
|
|
|
|
|
exact(item.ground_truth, false, "E47 ground truth");
|
|
|
|
|
exact(item.semantic_authority, "diagnostic-only", "E47 semantic authority");
|
|
|
|
|
exact(item.navigation_or_safety_accepted, false, "E47 safety authority");
|
|
|
|
|
exact(item.actuation_allowed, false, "E47 actuation authority");
|
|
|
|
|
const provider = object(item.provider, "E47 provider");
|
|
|
|
|
const temporalBinding = object(item.temporal_binding, "E47 temporal binding");
|
|
|
|
|
const metrics = object(item.metrics, "E47 metrics");
|
|
|
|
|
const frames = object(metrics.frames, "E47 frame metrics");
|
|
|
|
|
const points = object(metrics.points, "E47 point metrics");
|
|
|
|
|
const observations = object(metrics.observations, "E47 observation metrics");
|
|
|
|
|
const runtime = object(metrics.runtime, "E47 runtime metrics");
|
|
|
|
|
const acceptance = object(item.acceptance, "E47 acceptance");
|
|
|
|
|
const frameMetrics = {
|
|
|
|
|
total: exact(frames.total, 4489, "E47 frame total"),
|
|
|
|
|
maskAvailable: integer(frames.mask_available, "E47 mask frames"),
|
|
|
|
|
sourceAvailable: integer(frames.source_available, "E47 source frames"),
|
|
|
|
|
};
|
|
|
|
|
if (
|
|
|
|
|
frameMetrics.maskAvailable > frameMetrics.total
|
|
|
|
|
|| frameMetrics.sourceAvailable > frameMetrics.total
|
|
|
|
|
) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 frame accounting: нарушен контракт.");
|
|
|
|
|
}
|
|
|
|
|
const pointMetrics = {
|
|
|
|
|
total: integer(points.total, "E47 total points"),
|
|
|
|
|
projected: integer(points.projected, "E47 projected points"),
|
|
|
|
|
labeled: integer(points.labeled, "E47 labeled points"),
|
|
|
|
|
ambiguous: integer(points.ambiguous, "E47 ambiguous points"),
|
|
|
|
|
unprojected: integer(points.unprojected, "E47 unprojected points"),
|
|
|
|
|
absent: integer(points.absent, "E47 absent points"),
|
|
|
|
|
};
|
|
|
|
|
if (
|
|
|
|
|
pointMetrics.projected !== pointMetrics.labeled + pointMetrics.ambiguous
|
|
|
|
|
|| pointMetrics.total !== pointMetrics.projected
|
|
|
|
|
+ pointMetrics.unprojected
|
|
|
|
|
+ pointMetrics.absent
|
|
|
|
|
) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 point accounting: нарушен контракт.");
|
|
|
|
|
}
|
|
|
|
|
const observationMetrics = {
|
|
|
|
|
total: integer(observations.total, "E47 total observations"),
|
|
|
|
|
labeled: integer(observations.labeled, "E47 labeled observations"),
|
|
|
|
|
ambiguous: integer(observations.ambiguous, "E47 ambiguous observations"),
|
|
|
|
|
unprojected: integer(observations.unprojected, "E47 unprojected observations"),
|
|
|
|
|
absent: integer(observations.absent, "E47 absent observations"),
|
|
|
|
|
};
|
|
|
|
|
if (
|
|
|
|
|
observationMetrics.total !== observationMetrics.labeled
|
|
|
|
|
+ observationMetrics.ambiguous
|
|
|
|
|
+ observationMetrics.unprojected
|
|
|
|
|
+ observationMetrics.absent
|
|
|
|
|
) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 observation accounting: нарушен контракт.");
|
|
|
|
|
}
|
|
|
|
|
const runtimeMetrics = {
|
|
|
|
|
elapsedMs: finite(runtime.elapsed_ms, "E47 elapsed"),
|
|
|
|
|
framesPerSecond: finite(runtime.frames_per_second, "E47 FPS"),
|
|
|
|
|
};
|
|
|
|
|
if (runtimeMetrics.elapsedMs <= 0 || runtimeMetrics.framesPerSecond <= 0) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 runtime accounting: нарушен контракт.");
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
resultId: resultId(item.result_id),
|
|
|
|
|
createdAtUtc: text(item.created_at_utc, "E47 created at"),
|
|
|
|
|
status: "diagnostic-semantic-slam-shadow",
|
|
|
|
|
profileId: text(item.profile_id, "E47 profile"),
|
|
|
|
|
baseM4ResultId: m4ResultId(item.base_m4_result_id),
|
|
|
|
|
semanticResultId: text(item.semantic_result_id, "E47 semantic source"),
|
|
|
|
|
geometryResultId: text(item.geometry_result_id, "E47 geometry source"),
|
|
|
|
|
sourcePackId: text(item.source_pack_id, "E47 source pack"),
|
|
|
|
|
calibrationContentSha256: sha256(item.calibration_content_sha256, "E47 calibration"),
|
|
|
|
|
provider: {
|
|
|
|
|
providerId: text(provider.provider_id, "E47 provider id"),
|
|
|
|
|
modelId: text(provider.model_id, "E47 model id"),
|
|
|
|
|
modelRevision: text(provider.model_revision, "E47 model revision"),
|
|
|
|
|
modelWeightsSha256: sha256(provider.model_weights_sha256, "E47 model weights"),
|
|
|
|
|
preprocessId: text(provider.preprocess_id, "E47 preprocess id"),
|
|
|
|
|
},
|
|
|
|
|
temporalBinding: {
|
|
|
|
|
semanticToCamera: exact(
|
|
|
|
|
temporalBinding.semantic_to_camera,
|
|
|
|
|
"exact-sequence-and-session-time",
|
|
|
|
|
"E47 semantic/camera binding",
|
|
|
|
|
),
|
|
|
|
|
cameraToLidar: exact(
|
|
|
|
|
temporalBinding.camera_to_lidar,
|
|
|
|
|
"accepted-e6-nearest-host-arrival-best-effort",
|
|
|
|
|
"E47 camera/LiDAR binding",
|
|
|
|
|
),
|
|
|
|
|
clockBasis: exact(
|
|
|
|
|
temporalBinding.clock_basis,
|
|
|
|
|
"recorded-host-monotonic-arrival",
|
|
|
|
|
"E47 clock basis",
|
|
|
|
|
),
|
|
|
|
|
maximumLidarCameraDeltaMs: finite(
|
|
|
|
|
temporalBinding.maximum_lidar_camera_delta_ms,
|
|
|
|
|
"E47 maximum camera/LiDAR delta",
|
|
|
|
|
),
|
|
|
|
|
maximumPosePointDeltaMs: finite(
|
|
|
|
|
temporalBinding.maximum_pose_point_delta_ms,
|
|
|
|
|
"E47 maximum pose/point delta",
|
|
|
|
|
),
|
|
|
|
|
physicalSynchronizationProven: exact(
|
|
|
|
|
temporalBinding.physical_synchronization_proven,
|
|
|
|
|
false,
|
|
|
|
|
"E47 physical synchronization",
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
taxonomy: taxonomy(item.taxonomy),
|
|
|
|
|
metrics: {
|
|
|
|
|
frames: frameMetrics,
|
|
|
|
|
points: pointMetrics,
|
|
|
|
|
observations: observationMetrics,
|
|
|
|
|
runtime: runtimeMetrics,
|
|
|
|
|
},
|
|
|
|
|
acceptance: {
|
|
|
|
|
artifactContractPassed: exact(
|
|
|
|
|
acceptance.artifact_contract_passed,
|
|
|
|
|
true,
|
|
|
|
|
"E47 artifact contract",
|
|
|
|
|
),
|
|
|
|
|
frameAccountingPassed: exact(
|
|
|
|
|
acceptance.frame_accounting_passed,
|
|
|
|
|
true,
|
|
|
|
|
"E47 frame accounting",
|
|
|
|
|
),
|
|
|
|
|
pointAccountingPassed: exact(
|
|
|
|
|
acceptance.point_accounting_passed,
|
|
|
|
|
true,
|
|
|
|
|
"E47 point accounting",
|
|
|
|
|
),
|
|
|
|
|
observationBindingPassed: exact(
|
|
|
|
|
acceptance.observation_binding_passed,
|
|
|
|
|
true,
|
|
|
|
|
"E47 observation binding",
|
|
|
|
|
),
|
|
|
|
|
temporalBindingPassed: exact(
|
|
|
|
|
acceptance.temporal_binding_passed,
|
|
|
|
|
true,
|
|
|
|
|
"E47 temporal binding",
|
|
|
|
|
),
|
|
|
|
|
independentSemanticTruthPassed: exact(
|
|
|
|
|
acceptance.independent_semantic_truth_passed,
|
|
|
|
|
false,
|
|
|
|
|
"E47 independent truth",
|
|
|
|
|
),
|
|
|
|
|
providerPromoted: exact(
|
|
|
|
|
acceptance.provider_promoted,
|
|
|
|
|
false,
|
|
|
|
|
"E47 provider promotion",
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
limitations: array(item.limitations, "E47 limitations").map(
|
|
|
|
|
(entry) => text(entry, "E47 limitation"),
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchE47SemanticSlamResult({
|
|
|
|
|
fetcher = fetch,
|
|
|
|
|
signal,
|
|
|
|
|
}: {
|
|
|
|
|
fetcher?: LaboratoryFetch;
|
|
|
|
|
signal?: AbortSignal;
|
|
|
|
|
} = {}): Promise<E47SemanticSlamResult | null> {
|
|
|
|
|
const response = await fetcher("/api/v1/laboratory/e47-semantic-slam/results?limit=1", {
|
|
|
|
|
headers: { Accept: "application/json" },
|
|
|
|
|
signal,
|
|
|
|
|
});
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new E47SemanticSlamContractError(`E47 LAB недоступен: HTTP ${response.status}.`);
|
|
|
|
|
}
|
|
|
|
|
const payload = object(await response.json(), "E47 catalog");
|
|
|
|
|
exact(
|
|
|
|
|
payload.schema_version,
|
|
|
|
|
"missioncore.e47-semantic-slam-catalog/v1",
|
|
|
|
|
"E47 catalog schema",
|
|
|
|
|
);
|
|
|
|
|
const items = array(payload.items, "E47 catalog items");
|
|
|
|
|
return items.length ? parseResult(items[0]) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseFrame(
|
|
|
|
|
value: unknown,
|
|
|
|
|
expectedSequence: number,
|
|
|
|
|
declaredTaxonomy: readonly E47SemanticClass[] | undefined,
|
|
|
|
|
): E47SemanticTimelineFrame {
|
|
|
|
|
const item = object(value, "E47 semantic frame");
|
|
|
|
|
exact(item.schema_version, "missioncore.e47-semantic-slam-frame/v1", "E47 frame schema");
|
|
|
|
|
const sequence = integer(item.sequence, "E47 frame sequence");
|
|
|
|
|
if (sequence !== expectedSequence) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 frame sequence: нарушен порядок.");
|
|
|
|
|
}
|
|
|
|
|
const sourcePointCount = integer(item.source_point_count, "E47 frame source points");
|
|
|
|
|
const classIds = array(item.class_ids, "E47 frame classes").map((entry) => {
|
|
|
|
|
const parsed = finite(entry, "E47 frame class");
|
|
|
|
|
if (!Number.isInteger(parsed) || parsed < -1 || parsed > 255) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 frame class: вышел за контракт.");
|
|
|
|
|
}
|
|
|
|
|
return parsed;
|
|
|
|
|
});
|
|
|
|
|
const statusCodes = array(item.status_codes, "E47 frame statuses").map((entry) => {
|
|
|
|
|
const parsed = integer(entry, "E47 frame status");
|
|
|
|
|
if (parsed > 3) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 frame status: неизвестное значение.");
|
|
|
|
|
}
|
|
|
|
|
return parsed;
|
|
|
|
|
});
|
|
|
|
|
if (classIds.length !== sourcePointCount || statusCodes.length !== sourcePointCount) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 frame point accounting: нарушен контракт.");
|
|
|
|
|
}
|
|
|
|
|
if (classIds.some((classId, index) => {
|
|
|
|
|
const status = statusCodes[index];
|
|
|
|
|
return status === 0 || status === 1 ? classId !== -1 : classId < 0;
|
|
|
|
|
})) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 frame class/status binding: нарушен контракт.");
|
|
|
|
|
}
|
|
|
|
|
if (declaredTaxonomy) {
|
|
|
|
|
const classesById = new Map(declaredTaxonomy.map((item) => [item.classId, item]));
|
|
|
|
|
if (classIds.some((classId, index) => {
|
|
|
|
|
const status = statusCodes[index];
|
|
|
|
|
if (status !== 2 && status !== 3) return false;
|
|
|
|
|
const semanticClass = classesById.get(classId);
|
|
|
|
|
return !semanticClass
|
|
|
|
|
|| (status === 2 && semanticClass.disposition !== "ambiguous")
|
|
|
|
|
|| (status === 3 && semanticClass.disposition !== "labeled");
|
|
|
|
|
})) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 frame taxonomy binding: нарушен контракт.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const counts = object(item.counts, "E47 frame counts");
|
|
|
|
|
const parsedCounts = {
|
|
|
|
|
labeled: integer(counts.labeled, "E47 frame labeled"),
|
|
|
|
|
ambiguous: integer(counts.ambiguous, "E47 frame ambiguous"),
|
|
|
|
|
unprojected: integer(counts.unprojected, "E47 frame unprojected"),
|
|
|
|
|
absent: integer(counts.absent, "E47 frame absent"),
|
|
|
|
|
};
|
|
|
|
|
if (Object.values(parsedCounts).reduce((sum, count) => sum + count, 0) !== sourcePointCount) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 frame status accounting: нарушен контракт.");
|
|
|
|
|
}
|
|
|
|
|
const actualCounts = {
|
|
|
|
|
labeled: statusCodes.filter((status) => status === 3).length,
|
|
|
|
|
ambiguous: statusCodes.filter((status) => status === 2).length,
|
|
|
|
|
unprojected: statusCodes.filter((status) => status === 1).length,
|
|
|
|
|
absent: statusCodes.filter((status) => status === 0).length,
|
|
|
|
|
};
|
|
|
|
|
if (Object.keys(actualCounts).some(
|
|
|
|
|
(key) => actualCounts[key as keyof typeof actualCounts]
|
|
|
|
|
!== parsedCounts[key as keyof typeof parsedCounts],
|
|
|
|
|
)) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 frame status histogram: нарушен контракт.");
|
|
|
|
|
}
|
|
|
|
|
return { sequence, sourcePointCount, classIds, statusCodes, counts: parsedCounts };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchE47SemanticTimelineChunk(
|
|
|
|
|
result: string,
|
|
|
|
|
startSequence: number,
|
|
|
|
|
frameCount: number,
|
|
|
|
|
{
|
|
|
|
|
fetcher = fetch,
|
|
|
|
|
signal,
|
|
|
|
|
taxonomy,
|
|
|
|
|
}: {
|
|
|
|
|
fetcher?: LaboratoryFetch;
|
|
|
|
|
signal?: AbortSignal;
|
|
|
|
|
taxonomy?: readonly E47SemanticClass[];
|
|
|
|
|
} = {},
|
|
|
|
|
): Promise<E47SemanticTimelineChunk> {
|
|
|
|
|
resultId(result);
|
|
|
|
|
const parameters = new URLSearchParams({
|
|
|
|
|
start: String(startSequence),
|
|
|
|
|
count: String(frameCount),
|
|
|
|
|
});
|
|
|
|
|
const response = await fetcher(
|
|
|
|
|
`/api/v1/laboratory/e47-semantic-slam/results/${result}/timeline/chunk?${parameters}`,
|
|
|
|
|
{ headers: { Accept: "application/json" }, signal },
|
|
|
|
|
);
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new E47SemanticSlamContractError(`E47 timeline chunk: HTTP ${response.status}.`);
|
|
|
|
|
}
|
|
|
|
|
const payload = object(await response.json(), "E47 semantic chunk");
|
|
|
|
|
exact(payload.schema_version, "missioncore.e47-semantic-slam-chunk/v1", "E47 chunk schema");
|
|
|
|
|
exact(payload.result_id, result, "E47 chunk result");
|
|
|
|
|
const parsedStart = integer(payload.start_sequence, "E47 chunk start");
|
|
|
|
|
if (parsedStart !== startSequence) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 chunk start: нарушен контракт.");
|
|
|
|
|
}
|
|
|
|
|
const frames = array(payload.frames, "E47 chunk frames").map(
|
|
|
|
|
(frame, offset) => parseFrame(frame, parsedStart + offset, taxonomy),
|
|
|
|
|
);
|
|
|
|
|
const parsedCount = integer(payload.frame_count, "E47 chunk count");
|
|
|
|
|
if (parsedCount !== frames.length || parsedCount > frameCount) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 chunk frame count: нарушен контракт.");
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
resultId: result,
|
|
|
|
|
startSequence: parsedStart,
|
|
|
|
|
frameCount: parsedCount,
|
|
|
|
|
nextSequence: payload.next_sequence === null
|
|
|
|
|
? null
|
|
|
|
|
: integer(payload.next_sequence, "E47 next sequence"),
|
|
|
|
|
frames,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function e47SemanticMaskUrl(result: string, sequence: number): string {
|
|
|
|
|
resultId(result);
|
|
|
|
|
if (!Number.isInteger(sequence) || sequence < 0 || sequence >= 4489) {
|
|
|
|
|
throw new E47SemanticSlamContractError("E47 mask sequence: вне recorded replay.");
|
|
|
|
|
}
|
|
|
|
|
return `/api/v1/laboratory/e47-semantic-slam/results/${result}/masks/${sequence}`;
|
|
|
|
|
}
|