feat(perception): add mixed-route vegetation review

This commit is contained in:
DCCONSTRUCTIONS
2026-08-28 23:13:48 +03:00
parent e10b96b546
commit af1e530162
12 changed files with 2332 additions and 17 deletions
@@ -79,6 +79,33 @@ export interface VegetationRouteVideo {
validFovMaskSha256: string | null;
}
export interface VegetationMixedRouteCase {
caseId: string;
phase: "rural" | "transition" | "urban";
sourceSequence: number;
sessionSeconds: number;
assets: Readonly<Record<"source" | "city" | "vegetation" | "tgs", string>>;
tgs: {
groundCells: number;
occupiedCells: number;
rejectedCells: number;
unobservedCells: number;
};
}
export interface VegetationMixedRouteReview {
sourceId: "RAVNOVES004TREE";
sessionId: string;
packId: string;
frameCount: 10;
models: {
city: { name: string; inferenceFps: number; endToEndP95Ms: number };
vegetation: { name: string; latencyP95Ms: number };
tgs: { name: string; latencyP95Ms: number; cellSizeM: number; radiusM: number };
};
cases: readonly VegetationMixedRouteCase[];
}
export interface VegetationShadowResult {
resultId: string;
createdAtUtc: string;
@@ -88,6 +115,7 @@ export interface VegetationShadowResult {
routeCases: readonly VegetationVisualCase[];
validationCases: readonly VegetationVisualCase[];
routeVideo: VegetationRouteVideo | null;
routeReview: VegetationMixedRouteReview | null;
limitations: readonly string[];
visualShadowReady: true;
missionPolicyReadyForConfiguration: true;
@@ -423,6 +451,100 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null {
};
}
function mixedRouteReviewValue(
value: unknown,
resultId: string,
endpointRoot: string,
): VegetationMixedRouteReview | null {
if (value === null || value === undefined) return null;
const row = objectValue(value, "vegetation.route_review");
exact(row.source_id, "RAVNOVES004TREE", "vegetation.route_review.source_id");
exact(row.frame_count, 10, "vegetation.route_review.frame_count");
exact(row.ground_truth, false, "vegetation.route_review.ground_truth");
exact(
row.selection_policy,
"same-scene-camera-lidar-aligned-review-islands/v1",
"vegetation.route_review.selection_policy",
);
const packId = textValue(row.pack_id, "vegetation.route_review.pack_id");
if (!/^mixed-route-review-pack-[a-f0-9]{64}$/.test(packId)) {
throw new VegetationShadowContractError("vegetation.route_review.pack_id: identity invalid.");
}
const models = objectValue(row.models, "vegetation.route_review.models");
const city = objectValue(models.city, "vegetation.route_review.models.city");
const vegetation = objectValue(models.vegetation, "vegetation.route_review.models.vegetation");
const tgsModel = objectValue(models.tgs, "vegetation.route_review.models.tgs");
exact(city.frames, 10, "vegetation.route_review.models.city.frames");
exact(vegetation.frames, 10, "vegetation.route_review.models.vegetation.frames");
exact(tgsModel.frames, 10, "vegetation.route_review.models.tgs.frames");
const cases = arrayValue(row.cases, "vegetation.route_review.cases").map((raw, index) => {
const item = objectValue(raw, `vegetation.route_review.cases[${index}]`);
const caseId = textValue(item.case_id, `vegetation.route_review.cases[${index}].case_id`);
if (caseId !== `route-${String(index + 1).padStart(2, "0")}`) {
throw new VegetationShadowContractError("vegetation.route_review.case order changed.");
}
const phaseValue = item.phase;
if (phaseValue !== "rural" && phaseValue !== "transition" && phaseValue !== "urban") {
throw new VegetationShadowContractError("vegetation.route_review.phase changed.");
}
const phase: VegetationMixedRouteCase["phase"] = phaseValue;
const assets = objectValue(item.assets, `vegetation.route_review.cases[${index}].assets`);
const projected = Object.fromEntries(["source", "city", "vegetation", "tgs"].map((key) => {
const descriptor = objectValue(assets[key], `vegetation.route_review.assets.${key}`);
const path = textValue(descriptor.path, `vegetation.route_review.assets.${key}.path`);
const digest = textValue(descriptor.sha256, `vegetation.route_review.assets.${key}.sha256`);
if (!SHA256.test(digest) || !path.startsWith(`route-review/${caseId}/`)) {
throw new VegetationShadowContractError(`vegetation.route_review.assets.${key}: proof invalid.`);
}
return [key, `${endpointRoot}/${encodeURIComponent(resultId)}/assets/${path
.split("/").map(encodeURIComponent).join("/")}`];
})) as Record<"source" | "city" | "vegetation" | "tgs", string>;
const tgs = objectValue(item.tgs, `vegetation.route_review.cases[${index}].tgs`);
const groundCells = integerValue(tgs.ground_cells, "vegetation.route_review.tgs.ground");
const occupiedCells = integerValue(tgs.occupied_cells, "vegetation.route_review.tgs.occupied");
const rejectedCells = integerValue(tgs.rejected_cells, "vegetation.route_review.tgs.rejected");
const unobservedCells = integerValue(tgs.unobserved_cells, "vegetation.route_review.tgs.unobserved");
if (groundCells + occupiedCells + rejectedCells + unobservedCells !== 2244) {
throw new VegetationShadowContractError("vegetation.route_review.tgs cell accounting changed.");
}
return {
caseId,
phase,
sourceSequence: integerValue(item.source_sequence, "vegetation.route_review.source_sequence"),
sessionSeconds: numberValue(item.session_seconds, "vegetation.route_review.session_seconds"),
assets: projected,
tgs: { groundCells, occupiedCells, rejectedCells, unobservedCells },
};
});
if (cases.length !== 10) {
throw new VegetationShadowContractError("vegetation.route_review.cases: expected 10 aligned islands.");
}
return {
sourceId: "RAVNOVES004TREE",
sessionId: textValue(row.session_id, "vegetation.route_review.session_id"),
packId,
frameCount: 10,
models: {
city: {
name: textValue(city.name, "vegetation.route_review.models.city.name"),
inferenceFps: numberValue(city.inference_fps, "vegetation.route_review.models.city.fps"),
endToEndP95Ms: numberValue(city.end_to_end_p95_ms, "vegetation.route_review.models.city.p95"),
},
vegetation: {
name: textValue(vegetation.name, "vegetation.route_review.models.vegetation.name"),
latencyP95Ms: numberValue(vegetation.latency_p95_ms, "vegetation.route_review.models.vegetation.p95"),
},
tgs: {
name: textValue(tgsModel.name, "vegetation.route_review.models.tgs.name"),
latencyP95Ms: numberValue(tgsModel.latency_p95_ms, "vegetation.route_review.models.tgs.p95"),
cellSizeM: numberValue(tgsModel.cell_size_m, "vegetation.route_review.models.tgs.cell"),
radiusM: numberValue(tgsModel.radius_m, "vegetation.route_review.models.tgs.radius"),
},
},
cases,
};
}
function parseResult(
value: unknown,
resultId: string,
@@ -466,7 +588,11 @@ function parseResult(
.map((item) => visualCaseValue(item, resultId, "ravnoves", endpointRoot));
const validationCases = arrayValue(catalogs.goose, "vegetation.catalogs.goose")
.map((item) => visualCaseValue(item, resultId, "goose", endpointRoot));
if (routeCases.length !== 0 || validationCases.length !== 12) {
const routeReview = mixedRouteReviewValue(payload.route_review, resultId, endpointRoot);
if (
routeCases.length !== 0
|| (routeReview ? validationCases.length !== 0 : validationCases.length !== 12)
) {
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer.");
}
return {
@@ -478,6 +604,7 @@ function parseResult(
routeCases,
validationCases,
routeVideo: routeVideoValue(payload.route_video),
routeReview,
limitations: arrayValue(payload.limitations, "vegetation.limitations")
.map((item, index) => textValue(item, `vegetation.limitations[${index}]`)),
visualShadowReady: true,