feat(lab): seal full RAVNOVES004TREE semantic review
This commit is contained in:
@@ -106,6 +106,39 @@ export interface VegetationMixedRouteReview {
|
||||
cases: readonly VegetationMixedRouteCase[];
|
||||
}
|
||||
|
||||
export interface VegetationFullRouteLayer {
|
||||
name: string;
|
||||
resultId: string;
|
||||
frameCount: 6830;
|
||||
taxonomy: readonly VegetationVideoSemanticClass[];
|
||||
inferenceFps: number;
|
||||
latencyP95Ms: number;
|
||||
peakReservedVramBytes: number;
|
||||
}
|
||||
|
||||
export interface VegetationFullRouteReview {
|
||||
sourceId: "RAVNOVES004TREE";
|
||||
sessionId: "20260828T130511Z_viewer_live";
|
||||
sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0";
|
||||
sourceJobInputSha256: string;
|
||||
sourceStreamSha256: string;
|
||||
recordedMediaSourceId: "recorded.camera.6a3945242828a038";
|
||||
recordedMediaGenerationSha256: string;
|
||||
frameCount: 6830;
|
||||
width: 800;
|
||||
height: 600;
|
||||
timelineStartSeconds: number;
|
||||
timelineEndSeconds: number;
|
||||
frameSourceTimesNs: readonly number[];
|
||||
decodeRepair: {
|
||||
repairedFrameCount: 1;
|
||||
sequence: 6092;
|
||||
method: "duplicate-previous-decoded-frame";
|
||||
};
|
||||
city: VegetationFullRouteLayer;
|
||||
vegetation: VegetationFullRouteLayer;
|
||||
}
|
||||
|
||||
export interface VegetationShadowResult {
|
||||
resultId: string;
|
||||
createdAtUtc: string;
|
||||
@@ -116,6 +149,7 @@ export interface VegetationShadowResult {
|
||||
validationCases: readonly VegetationVisualCase[];
|
||||
routeVideo: VegetationRouteVideo | null;
|
||||
routeReview: VegetationMixedRouteReview | null;
|
||||
routeFullReview: VegetationFullRouteReview | null;
|
||||
limitations: readonly string[];
|
||||
visualShadowReady: true;
|
||||
missionPolicyReadyForConfiguration: true;
|
||||
@@ -545,6 +579,246 @@ function mixedRouteReviewValue(
|
||||
};
|
||||
}
|
||||
|
||||
function fullRouteTaxonomyValue(
|
||||
value: unknown,
|
||||
label: string,
|
||||
schema: string,
|
||||
classCount: number,
|
||||
): readonly VegetationVideoSemanticClass[] {
|
||||
const taxonomy = objectValue(value, `${label}.taxonomy`);
|
||||
exact(taxonomy.schema_version, schema, `${label}.taxonomy.schema`);
|
||||
const classes = arrayValue(taxonomy.classes, `${label}.taxonomy.classes`).map(
|
||||
(raw, expectedId): VegetationVideoSemanticClass => {
|
||||
const item = objectValue(raw, `${label}.taxonomy[${expectedId}]`);
|
||||
const classId = integerValue(item.class_id, `${label}.class_id[${expectedId}]`);
|
||||
if (classId !== expectedId) {
|
||||
throw new VegetationShadowContractError(`${label}: taxonomy order changed.`);
|
||||
}
|
||||
const color = arrayValue(item.color_rgb, `${label}.color[${expectedId}]`)
|
||||
.map((channel, index) => integerValue(channel, `${label}.color[${expectedId}][${index}]`));
|
||||
if (color.length !== 3 || color.some((channel) => channel > 255)) {
|
||||
throw new VegetationShadowContractError(`${label}: taxonomy color invalid.`);
|
||||
}
|
||||
const disposition = item.disposition;
|
||||
if (
|
||||
disposition !== "labeled"
|
||||
&& disposition !== "ambiguous"
|
||||
&& disposition !== "prediction"
|
||||
&& disposition !== "undefined"
|
||||
) {
|
||||
throw new VegetationShadowContractError(`${label}: taxonomy disposition changed.`);
|
||||
}
|
||||
return {
|
||||
classId,
|
||||
label: textValue(item.label, `${label}.label[${expectedId}]`),
|
||||
colorRgb: color as unknown as readonly [number, number, number],
|
||||
disposition,
|
||||
materialClass: item.material_class === null || item.material_class === undefined
|
||||
? null
|
||||
: textValue(item.material_class, `${label}.material[${expectedId}]`),
|
||||
evidenceState: item.evidence_state === null || item.evidence_state === undefined
|
||||
? null
|
||||
: textValue(item.evidence_state, `${label}.evidence[${expectedId}]`),
|
||||
};
|
||||
},
|
||||
);
|
||||
if (classes.length !== classCount) {
|
||||
throw new VegetationShadowContractError(`${label}: taxonomy size changed.`);
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
function fullRouteLayerValue(
|
||||
value: unknown,
|
||||
layer: "city" | "vegetation",
|
||||
): VegetationFullRouteLayer {
|
||||
const label = `vegetation.route_full_review.layers.${layer}`;
|
||||
const row = objectValue(value, label);
|
||||
const resultId = textValue(row.result_id, `${label}.result_id`);
|
||||
const identity = layer === "city"
|
||||
? /^result-[a-f0-9]{64}$/
|
||||
: /^lab-v1-ravnoves-video-ddrnet-[a-f0-9]{64}$/;
|
||||
if (!identity.test(resultId)) {
|
||||
throw new VegetationShadowContractError(`${label}: identity invalid.`);
|
||||
}
|
||||
exact(row.frame_count, 6830, `${label}.frame_count`);
|
||||
const archive = objectValue(row.mask_archive, `${label}.mask_archive`);
|
||||
exact(
|
||||
archive.path,
|
||||
layer === "city" ? "video/eomt-semantic-masks.zip" : "video/ddrnet-semantic-masks.zip",
|
||||
`${label}.mask_archive.path`,
|
||||
);
|
||||
const digest = textValue(archive.sha256, `${label}.mask_archive.sha256`);
|
||||
if (!SHA256.test(digest)) {
|
||||
throw new VegetationShadowContractError(`${label}: archive digest invalid.`);
|
||||
}
|
||||
integerValue(archive.byte_length, `${label}.mask_archive.byte_length`);
|
||||
return {
|
||||
name: textValue(row.name, `${label}.name`),
|
||||
resultId,
|
||||
frameCount: 6830,
|
||||
taxonomy: fullRouteTaxonomyValue(
|
||||
row.taxonomy,
|
||||
label,
|
||||
layer === "city"
|
||||
? "missioncore.recorded-eomt-taxonomy/v1"
|
||||
: "missioncore.lab-v1-vegetation-taxonomy/v1",
|
||||
layer === "city" ? 16 : 64,
|
||||
),
|
||||
inferenceFps: numberValue(row.inference_fps, `${label}.inference_fps`),
|
||||
latencyP95Ms: numberValue(row.latency_p95_ms, `${label}.latency_p95_ms`),
|
||||
peakReservedVramBytes: integerValue(
|
||||
row.peak_reserved_vram_bytes,
|
||||
`${label}.peak_reserved_vram_bytes`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function fullRouteReviewValue(value: unknown): VegetationFullRouteReview | null {
|
||||
if (value === null || value === undefined) return null;
|
||||
const row = objectValue(value, "vegetation.route_full_review");
|
||||
exact(row.source_id, "RAVNOVES004TREE", "vegetation.route_full_review.source_id");
|
||||
exact(
|
||||
row.session_id,
|
||||
"20260828T130511Z_viewer_live",
|
||||
"vegetation.route_full_review.session_id",
|
||||
);
|
||||
exact(
|
||||
row.source_job_id,
|
||||
"recorded-camera-eb2783c5480d56bda07c8af0",
|
||||
"vegetation.route_full_review.source_job_id",
|
||||
);
|
||||
exact(row.frame_count, 6830, "vegetation.route_full_review.frame_count");
|
||||
exact(row.width, 800, "vegetation.route_full_review.width");
|
||||
exact(row.height, 600, "vegetation.route_full_review.height");
|
||||
exact(row.ground_truth, false, "vegetation.route_full_review.ground_truth");
|
||||
const sourceJobInputSha256 = textValue(
|
||||
row.source_job_input_sha256,
|
||||
"vegetation.route_full_review.source_job_input_sha256",
|
||||
);
|
||||
const sourceStreamSha256 = textValue(
|
||||
row.source_stream_sha256,
|
||||
"vegetation.route_full_review.source_stream_sha256",
|
||||
);
|
||||
exact(
|
||||
sourceJobInputSha256,
|
||||
"eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715",
|
||||
"vegetation.route_full_review.source_job_input_sha256",
|
||||
);
|
||||
exact(
|
||||
sourceStreamSha256,
|
||||
"e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac",
|
||||
"vegetation.route_full_review.source_stream_sha256",
|
||||
);
|
||||
exact(
|
||||
row.recorded_media_source_id,
|
||||
"recorded.camera.6a3945242828a038",
|
||||
"vegetation.route_full_review.recorded_media_source_id",
|
||||
);
|
||||
const recordedMediaGenerationSha256 = textValue(
|
||||
row.recorded_media_generation_sha256,
|
||||
"vegetation.route_full_review.recorded_media_generation_sha256",
|
||||
);
|
||||
exact(
|
||||
recordedMediaGenerationSha256,
|
||||
"b073ea1e7babf1c77a664e1a5b95e3702d0e05b0e34c1e85a7c67a6f8b392ded",
|
||||
"vegetation.route_full_review.recorded_media_generation_sha256",
|
||||
);
|
||||
if (
|
||||
!SHA256.test(sourceJobInputSha256)
|
||||
|| !SHA256.test(sourceStreamSha256)
|
||||
|| !SHA256.test(recordedMediaGenerationSha256)
|
||||
) {
|
||||
throw new VegetationShadowContractError("vegetation.route_full_review: source digest invalid.");
|
||||
}
|
||||
const timelineStartSeconds = numberValue(
|
||||
row.timeline_start_seconds,
|
||||
"vegetation.route_full_review.timeline_start_seconds",
|
||||
);
|
||||
const timelineEndSeconds = numberValue(
|
||||
row.timeline_end_seconds,
|
||||
"vegetation.route_full_review.timeline_end_seconds",
|
||||
);
|
||||
if (timelineEndSeconds <= timelineStartSeconds) {
|
||||
throw new VegetationShadowContractError("vegetation.route_full_review: timeline invalid.");
|
||||
}
|
||||
const frameSourceTimesNs = arrayValue(
|
||||
row.frame_source_times_ns,
|
||||
"vegetation.route_full_review.frame_source_times_ns",
|
||||
).map((raw, index) => {
|
||||
const time = integerValue(raw, `vegetation.route_full_review.frame_source_times_ns[${index}]`);
|
||||
if (!Number.isSafeInteger(time)) {
|
||||
throw new VegetationShadowContractError("vegetation.route_full_review: unsafe frame time.");
|
||||
}
|
||||
return time;
|
||||
});
|
||||
if (
|
||||
frameSourceTimesNs.length !== 6830
|
||||
|| frameSourceTimesNs.some((time, index) => index > 0 && time <= frameSourceTimesNs[index - 1]!)
|
||||
) {
|
||||
throw new VegetationShadowContractError("vegetation.route_full_review: frame timeline changed.");
|
||||
}
|
||||
const decodeRepair = objectValue(
|
||||
row.decode_repair,
|
||||
"vegetation.route_full_review.decode_repair",
|
||||
);
|
||||
exact(decodeRepair.repaired_frame_count, 1, "vegetation.route_full_review.decode_repair.count");
|
||||
exact(decodeRepair.sequence, 6092, "vegetation.route_full_review.decode_repair.sequence");
|
||||
exact(
|
||||
decodeRepair.method,
|
||||
"duplicate-previous-decoded-frame",
|
||||
"vegetation.route_full_review.decode_repair.method",
|
||||
);
|
||||
const repairProofs = objectValue(
|
||||
decodeRepair.proofs,
|
||||
"vegetation.route_full_review.decode_repair.proofs",
|
||||
);
|
||||
for (const [key, expectedPath] of Object.entries({
|
||||
eomt: "proofs/decode_repair.json",
|
||||
ddrnet: "proofs/ddrnet_decode_repair.json",
|
||||
})) {
|
||||
const proof = objectValue(
|
||||
repairProofs[key],
|
||||
`vegetation.route_full_review.decode_repair.proofs.${key}`,
|
||||
);
|
||||
exact(
|
||||
proof.path,
|
||||
expectedPath,
|
||||
`vegetation.route_full_review.decode_repair.proofs.${key}.path`,
|
||||
);
|
||||
const digest = textValue(
|
||||
proof.sha256,
|
||||
`vegetation.route_full_review.decode_repair.proofs.${key}.sha256`,
|
||||
);
|
||||
if (!SHA256.test(digest)) {
|
||||
throw new VegetationShadowContractError("vegetation.route_full_review: repair proof invalid.");
|
||||
}
|
||||
}
|
||||
const layers = objectValue(row.layers, "vegetation.route_full_review.layers");
|
||||
return {
|
||||
sourceId: "RAVNOVES004TREE",
|
||||
sessionId: "20260828T130511Z_viewer_live",
|
||||
sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0",
|
||||
sourceJobInputSha256,
|
||||
sourceStreamSha256,
|
||||
recordedMediaSourceId: "recorded.camera.6a3945242828a038",
|
||||
recordedMediaGenerationSha256,
|
||||
frameCount: 6830,
|
||||
width: 800,
|
||||
height: 600,
|
||||
timelineStartSeconds,
|
||||
timelineEndSeconds,
|
||||
frameSourceTimesNs,
|
||||
decodeRepair: {
|
||||
repairedFrameCount: 1,
|
||||
sequence: 6092,
|
||||
method: "duplicate-previous-decoded-frame",
|
||||
},
|
||||
city: fullRouteLayerValue(layers.city, "city"),
|
||||
vegetation: fullRouteLayerValue(layers.vegetation, "vegetation"),
|
||||
};
|
||||
}
|
||||
|
||||
function parseResult(
|
||||
value: unknown,
|
||||
resultId: string,
|
||||
@@ -589,9 +863,10 @@ function parseResult(
|
||||
const validationCases = arrayValue(catalogs.goose, "vegetation.catalogs.goose")
|
||||
.map((item) => visualCaseValue(item, resultId, "goose", endpointRoot));
|
||||
const routeReview = mixedRouteReviewValue(payload.route_review, resultId, endpointRoot);
|
||||
const routeFullReview = fullRouteReviewValue(payload.route_full_review);
|
||||
if (
|
||||
routeCases.length !== 0
|
||||
|| (routeReview ? validationCases.length !== 0 : validationCases.length !== 12)
|
||||
|| (routeReview || routeFullReview ? validationCases.length !== 0 : validationCases.length !== 12)
|
||||
) {
|
||||
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer.");
|
||||
}
|
||||
@@ -605,6 +880,7 @@ function parseResult(
|
||||
validationCases,
|
||||
routeVideo: routeVideoValue(payload.route_video),
|
||||
routeReview,
|
||||
routeFullReview,
|
||||
limitations: arrayValue(payload.limitations, "vegetation.limitations")
|
||||
.map((item, index) => textValue(item, `vegetation.limitations[${index}]`)),
|
||||
visualShadowReady: true,
|
||||
@@ -625,6 +901,23 @@ export function vegetationVideoMaskUrl(resultId: string, sequence: number): stri
|
||||
return `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/masks/${sequence}`;
|
||||
}
|
||||
|
||||
export function vegetationFullRouteMaskUrl(
|
||||
resultId: string,
|
||||
layer: "city" | "vegetation",
|
||||
sequence: number,
|
||||
): string {
|
||||
if (
|
||||
!RESULT_ID.test(resultId)
|
||||
|| (layer !== "city" && layer !== "vegetation")
|
||||
|| !Number.isInteger(sequence)
|
||||
|| sequence < 0
|
||||
|| sequence >= 6830
|
||||
) {
|
||||
throw new VegetationShadowContractError("Vegetation full-route mask identity недопустима.");
|
||||
}
|
||||
return `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/route-masks/${layer}/${sequence}`;
|
||||
}
|
||||
|
||||
export async function fetchVegetationShadowResult(
|
||||
resultId: string,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user