feat(polygon): add full-frame operator review
This commit is contained in:
@@ -63,6 +63,36 @@ export interface GroundFailurePreview {
|
||||
patchworkDisagreement: number[];
|
||||
}
|
||||
|
||||
export interface GroundReviewFrameMetrics {
|
||||
groundIou: number;
|
||||
naturalGroundRecall: number;
|
||||
obstacleNonGroundRecall: number;
|
||||
latencyMs: number;
|
||||
}
|
||||
|
||||
export interface GroundReviewFrameSummary {
|
||||
sequence: number;
|
||||
frameId: string;
|
||||
sourcePointCount: number;
|
||||
pointCount: number;
|
||||
current: GroundReviewFrameMetrics;
|
||||
patchwork: GroundReviewFrameMetrics;
|
||||
groundIouDelta: number;
|
||||
}
|
||||
|
||||
export interface GroundReview {
|
||||
runId: string;
|
||||
identitySha256: string;
|
||||
sourceId: string;
|
||||
frameCount: number;
|
||||
previewPoints: number;
|
||||
frames: GroundReviewFrameSummary[];
|
||||
}
|
||||
|
||||
export interface GroundReviewFrame extends GroundFailurePreview {
|
||||
intensity0To255: number[];
|
||||
}
|
||||
|
||||
export class GroundQualificationContractError extends Error {}
|
||||
|
||||
export class GroundQualificationApiError extends Error {
|
||||
@@ -130,6 +160,14 @@ function boolean(value: unknown, field: string): boolean {
|
||||
return value;
|
||||
}
|
||||
|
||||
function integer(value: unknown, field: string, minimum = 0): number {
|
||||
const result = number(value, field, minimum);
|
||||
if (!Number.isInteger(result)) {
|
||||
throw new GroundQualificationContractError(`${field} должен быть целым числом.`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function aggregate(value: unknown, field: string): QualificationMetricAggregate {
|
||||
const source = record(value, field);
|
||||
const micro = record(source.micro, `${field}.micro`);
|
||||
@@ -316,6 +354,113 @@ export function decodeGroundFailurePreview(payload: unknown): GroundFailurePrevi
|
||||
};
|
||||
}
|
||||
|
||||
function reviewFrameMetrics(value: unknown, field: string): GroundReviewFrameMetrics {
|
||||
const source = record(value, field);
|
||||
return {
|
||||
groundIou: fraction(source.ground_iou, `${field}.ground_iou`),
|
||||
naturalGroundRecall: fraction(
|
||||
source.natural_ground_recall,
|
||||
`${field}.natural_ground_recall`,
|
||||
),
|
||||
obstacleNonGroundRecall: fraction(
|
||||
source.obstacle_non_ground_recall,
|
||||
`${field}.obstacle_non_ground_recall`,
|
||||
),
|
||||
latencyMs: number(source.latency_ms, `${field}.latency_ms`),
|
||||
};
|
||||
}
|
||||
|
||||
export function decodeGroundReview(payload: unknown): GroundReview {
|
||||
const source = record(payload, "ground review");
|
||||
if (
|
||||
source.schema_version !== "missioncore.polygon-ground-review/v1"
|
||||
|| source.access !== "read-only"
|
||||
) {
|
||||
throw new GroundQualificationContractError("Неизвестная схема покадрового просмотра.");
|
||||
}
|
||||
const frameCount = integer(source.frame_count, "frame_count", 1);
|
||||
const frames = array(source.frames, "frames", 2_000).map(
|
||||
(value, index): GroundReviewFrameSummary => {
|
||||
const frame = record(value, `frames[${index}]`);
|
||||
const sequence = integer(frame.sequence, `frames[${index}].sequence`);
|
||||
if (sequence !== index) {
|
||||
throw new GroundQualificationContractError("Кадры просмотра идут не по порядку.");
|
||||
}
|
||||
return {
|
||||
sequence,
|
||||
frameId: id(frame.frame_id, `frames[${index}].frame_id`),
|
||||
sourcePointCount: integer(
|
||||
frame.source_point_count,
|
||||
`frames[${index}].source_point_count`,
|
||||
1,
|
||||
),
|
||||
pointCount: integer(frame.point_count, `frames[${index}].point_count`, 1),
|
||||
current: reviewFrameMetrics(frame.current, `frames[${index}].current`),
|
||||
patchwork: reviewFrameMetrics(
|
||||
frame.patchworkpp,
|
||||
`frames[${index}].patchworkpp`,
|
||||
),
|
||||
groundIouDelta: number(
|
||||
frame.ground_iou_delta,
|
||||
`frames[${index}].ground_iou_delta`,
|
||||
-1,
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
if (frames.length !== frameCount) {
|
||||
throw new GroundQualificationContractError("Индекс просмотра неполный.");
|
||||
}
|
||||
frames.forEach((frame, index) => {
|
||||
if (frame.pointCount > frame.sourcePointCount) {
|
||||
throw new GroundQualificationContractError(
|
||||
`frames[${index}] содержит больше preview-точек, чем source-точек.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
const identitySha256 = text(source.identity_sha256, "identity_sha256", 64);
|
||||
if (!SHA256.test(identitySha256)) {
|
||||
throw new GroundQualificationContractError("Review identity должен содержать SHA-256.");
|
||||
}
|
||||
return {
|
||||
runId: id(source.run_id, "run_id"),
|
||||
identitySha256,
|
||||
sourceId: text(source.source_id, "source_id"),
|
||||
frameCount,
|
||||
previewPoints: integer(source.preview_points, "preview_points", 1),
|
||||
frames,
|
||||
};
|
||||
}
|
||||
|
||||
export function decodeGroundReviewFrame(payload: unknown): GroundReviewFrame {
|
||||
const source = record(payload, "ground review frame");
|
||||
if (
|
||||
source.schema_version !== "missioncore.polygon-ground-review-frame/v1"
|
||||
|| source.access !== "read-only"
|
||||
) {
|
||||
throw new GroundQualificationContractError("Неизвестная схема кадра просмотра.");
|
||||
}
|
||||
const compatible = {
|
||||
...source,
|
||||
schema_version: "missioncore.polygon-ground-failure-preview/v1",
|
||||
};
|
||||
const decoded = decodeGroundFailurePreview(compatible);
|
||||
const intensity0To255 = array(
|
||||
source.intensity_0_255,
|
||||
"intensity_0_255",
|
||||
50_000,
|
||||
).map((value, index) => integer(value, `intensity_0_255[${index}]`));
|
||||
if (
|
||||
intensity0To255.length !== decoded.pointCount
|
||||
|| intensity0To255.some((value) => value > 255)
|
||||
) {
|
||||
throw new GroundQualificationContractError(
|
||||
"intensity_0_255 не совпадает с point_count.",
|
||||
);
|
||||
}
|
||||
return { ...decoded, intensity0To255 };
|
||||
}
|
||||
|
||||
async function requestJson(
|
||||
url: string,
|
||||
signal: AbortSignal | undefined,
|
||||
@@ -391,3 +536,48 @@ export async function fetchGroundFailurePreview(
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchGroundReview(
|
||||
runId: string,
|
||||
{
|
||||
signal,
|
||||
fetcher = globalThis.fetch,
|
||||
}: { signal?: AbortSignal; fetcher?: QualificationFetch } = {},
|
||||
): Promise<GroundReview | null> {
|
||||
if (!SAFE_ID.test(runId)) {
|
||||
throw new GroundQualificationContractError("Недопустимый run_id.");
|
||||
}
|
||||
try {
|
||||
return decodeGroundReview(
|
||||
await requestJson(
|
||||
`/api/v1/polygon/runs/${encodeURIComponent(runId)}/qualification/review`,
|
||||
signal,
|
||||
fetcher,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof GroundQualificationApiError && error.status === 404) return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchGroundReviewFrame(
|
||||
runId: string,
|
||||
frameId: string,
|
||||
{
|
||||
signal,
|
||||
fetcher = globalThis.fetch,
|
||||
}: { signal?: AbortSignal; fetcher?: QualificationFetch } = {},
|
||||
): Promise<GroundReviewFrame> {
|
||||
if (!SAFE_ID.test(runId) || !SAFE_ID.test(frameId)) {
|
||||
throw new GroundQualificationContractError("Недопустимый идентификатор кадра.");
|
||||
}
|
||||
return decodeGroundReviewFrame(
|
||||
await requestJson(
|
||||
`/api/v1/polygon/runs/${encodeURIComponent(runId)}/qualification/review/frames/`
|
||||
+ encodeURIComponent(frameId),
|
||||
signal,
|
||||
fetcher,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user