Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88e47fc39a | ||
|
|
cd16ea28a6 | ||
|
|
45019f8952 | ||
|
|
af1e530162 | ||
|
|
e10b96b546 | ||
|
|
a2c3385062 |
@@ -48,9 +48,13 @@ import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
||||
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
|
||||
import { fetchM49TgsFailClosedResult } from "./m49TgsFailClosed";
|
||||
import { fetchM49TgsFullShadowResult } from "./m49TgsFullShadow";
|
||||
import { fetchVegetationShadowResult } from "./vegetationShadow";
|
||||
import {
|
||||
fetchVegetationBenchmarkResult,
|
||||
fetchVegetationShadowResult,
|
||||
} from "./vegetationShadow";
|
||||
|
||||
export type AdvancedLaboratoryWorkId =
|
||||
| "lab-v1-vegetation-benchmark"
|
||||
| "lab-v1-vegetation-shadow"
|
||||
| "m48-object-centric-quality"
|
||||
| "m48-small-static-passage-regression"
|
||||
@@ -102,6 +106,7 @@ export interface AdvancedLaboratoryIndexItem {
|
||||
}
|
||||
|
||||
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
||||
"lab-v1-vegetation-benchmark",
|
||||
"lab-v1-vegetation-shadow",
|
||||
"m48-object-centric-quality",
|
||||
"m48-small-static-passage-regression",
|
||||
@@ -148,6 +153,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
||||
];
|
||||
|
||||
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
|
||||
"lab-v1-vegetation-benchmark": "lab-v1-vegetation-benchmark",
|
||||
"lab-v1-vegetation-shadow": "lab-v1-vegetation-shadow",
|
||||
"m48-object-centric-quality": "m48-object-quality-(?:pack|result)",
|
||||
"m48-small-static-passage-regression": "m48-small-static-passage-regression",
|
||||
@@ -201,6 +207,7 @@ export function isAdvancedLaboratoryWorkId(
|
||||
|
||||
export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
|
||||
return {
|
||||
vegetationBenchmark: null,
|
||||
vegetationShadow: null,
|
||||
m47Graph: null,
|
||||
m48: null,
|
||||
@@ -335,7 +342,8 @@ export function advancedLaboratoryResultAvailable(
|
||||
workId: AdvancedLaboratoryWorkId,
|
||||
results: AdvancedLaboratoryResults,
|
||||
): boolean {
|
||||
return workId === "lab-v1-vegetation-shadow" ? results.vegetationShadow !== null
|
||||
return workId === "lab-v1-vegetation-benchmark" ? results.vegetationBenchmark !== null
|
||||
: workId === "lab-v1-vegetation-shadow" ? results.vegetationShadow !== null
|
||||
: workId === "m48-object-centric-quality" ? results.m48 !== null
|
||||
: workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== null
|
||||
: workId === "m48-static-occupancy-qualification" ? results.m48StaticOccupancy !== null
|
||||
@@ -393,7 +401,10 @@ export async function fetchAdvancedLaboratoryResult(
|
||||
} = {},
|
||||
): Promise<AdvancedLaboratoryResults> {
|
||||
const results = emptyAdvancedLaboratoryResults();
|
||||
if (workId === "lab-v1-vegetation-shadow") {
|
||||
if (workId === "lab-v1-vegetation-benchmark") {
|
||||
if (!resultId) throw new AdvancedLaboratoryContractError("Vegetation benchmark identity не выбрана.");
|
||||
results.vegetationBenchmark = await fetchVegetationBenchmarkResult(resultId, { fetcher, signal });
|
||||
} else if (workId === "lab-v1-vegetation-shadow") {
|
||||
if (!resultId) throw new AdvancedLaboratoryContractError("Vegetation LAB identity не выбрана.");
|
||||
results.vegetationShadow = await fetchVegetationShadowResult(resultId, { fetcher, signal });
|
||||
} else if (workId === "m48-object-centric-quality") {
|
||||
|
||||
@@ -45,6 +45,7 @@ import type { M49TgsFullShadowResult } from "./m49TgsFullShadow";
|
||||
import type { VegetationShadowResult } from "./vegetationShadow";
|
||||
|
||||
export interface AdvancedLaboratoryResults {
|
||||
vegetationBenchmark: VegetationShadowResult | null;
|
||||
vegetationShadow: VegetationShadowResult | null;
|
||||
m47Graph: M47ReferenceGraphLabResult | null;
|
||||
m48: M48AdvancedResult | null;
|
||||
|
||||
@@ -967,6 +967,7 @@ export async function fetchAdvancedLaboratoryResults({
|
||||
const e39 = settledCatalogValue(settled[7]);
|
||||
const e40 = settledCatalogValue(settled[8]);
|
||||
return {
|
||||
vegetationBenchmark: null,
|
||||
vegetationShadow: null,
|
||||
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
|
||||
m48r3StaticOccupancy: null,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { LaboratoryFetch } from "./advancedResults";
|
||||
|
||||
const RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/;
|
||||
const BENCHMARK_RESULT_ID = /^lab-v1-vegetation-benchmark-[a-f0-9]{64}$/;
|
||||
const SHA256 = /^[a-f0-9]{64}$/;
|
||||
const CANDIDATES = ["ddrnet", "ppliteseg"] as const;
|
||||
const ROUTE_MODES = ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"] as const;
|
||||
@@ -75,6 +76,71 @@ export interface VegetationRouteVideo {
|
||||
aggregatePredictionPixels: readonly number[];
|
||||
policyPresets: Readonly<Record<string, Readonly<Record<string, string>>>> | null;
|
||||
fusionMode: "synchronised-multilayer-review" | null;
|
||||
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 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;
|
||||
timelineArtifact: {
|
||||
sha256: string;
|
||||
byteLength: number;
|
||||
};
|
||||
frameSourceTimesNs: readonly number[];
|
||||
decodeRepair: {
|
||||
repairedFrameCount: 1;
|
||||
sequence: 6092;
|
||||
method: "duplicate-previous-decoded-frame";
|
||||
};
|
||||
city: VegetationFullRouteLayer;
|
||||
vegetation: VegetationFullRouteLayer;
|
||||
}
|
||||
|
||||
export interface VegetationShadowResult {
|
||||
@@ -86,6 +152,8 @@ export interface VegetationShadowResult {
|
||||
routeCases: readonly VegetationVisualCase[];
|
||||
validationCases: readonly VegetationVisualCase[];
|
||||
routeVideo: VegetationRouteVideo | null;
|
||||
routeReview: VegetationMixedRouteReview | null;
|
||||
routeFullReview: VegetationFullRouteReview | null;
|
||||
limitations: readonly string[];
|
||||
visualShadowReady: true;
|
||||
missionPolicyReadyForConfiguration: true;
|
||||
@@ -192,6 +260,7 @@ function visualCaseValue(
|
||||
value: unknown,
|
||||
resultId: string,
|
||||
expectedKind: "goose" | "ravnoves",
|
||||
endpointRoot: string,
|
||||
): VegetationVisualCase {
|
||||
const row = objectValue(value, `vegetation.${expectedKind}.case`);
|
||||
exact(row.source_kind, expectedKind, "vegetation.case.source_kind");
|
||||
@@ -210,7 +279,7 @@ function visualCaseValue(
|
||||
if (!SHA256.test(sha256) || !path.startsWith(`visual/${expectedKind}/${caseId}/`)) {
|
||||
throw new VegetationShadowContractError(`vegetation.case.assets.${key}: proof invalid.`);
|
||||
}
|
||||
projected[key] = `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/assets/${path
|
||||
projected[key] = `${endpointRoot}/${encodeURIComponent(resultId)}/assets/${path
|
||||
.split("/")
|
||||
.map(encodeURIComponent)
|
||||
.join("/")}`;
|
||||
@@ -342,10 +411,16 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null {
|
||||
evidenceState,
|
||||
};
|
||||
});
|
||||
const expectedClassCount = viewKind === "coarse-material-policy-review" ? 9 : 64;
|
||||
const expectedClassCount = viewKind === "coarse-material-policy-review" ? 10 : 64;
|
||||
if (classes.length !== expectedClassCount) {
|
||||
throw new VegetationShadowContractError("vegetation.route_video: taxonomy size changed.");
|
||||
}
|
||||
if (
|
||||
viewKind === "coarse-material-policy-review"
|
||||
&& (classes[9]?.disposition !== "undefined" || classes[9]?.evidenceState !== "UNOBSERVED")
|
||||
) {
|
||||
throw new VegetationShadowContractError("vegetation.route_video: valid-FOV class changed.");
|
||||
}
|
||||
const aggregatePredictionPixels = arrayValue(
|
||||
row.aggregate_prediction_pixels,
|
||||
"vegetation.route_video.aggregate_prediction_pixels",
|
||||
@@ -364,7 +439,18 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null {
|
||||
integerValue(maskArchive.byte_length, "vegetation.route_video.mask_archive.byte_length");
|
||||
let policyPresets: VegetationRouteVideo["policyPresets"] = null;
|
||||
let fusionMode: VegetationRouteVideo["fusionMode"] = null;
|
||||
let validFovMaskSha256: string | null = null;
|
||||
if (viewKind === "coarse-material-policy-review") {
|
||||
const validFov = objectValue(row.valid_fov, "vegetation.route_video.valid_fov");
|
||||
exact(validFov.mask_path, "video/valid-fov-mask.png", "vegetation.route_video.valid_fov.path");
|
||||
validFovMaskSha256 = textValue(
|
||||
validFov.mask_sha256,
|
||||
"vegetation.route_video.valid_fov.sha256",
|
||||
);
|
||||
if (!SHA256.test(validFovMaskSha256)) {
|
||||
throw new VegetationShadowContractError("vegetation.route_video: valid-FOV digest invalid.");
|
||||
}
|
||||
exact(validFov.outside_valid_fov_class_id, 9, "vegetation.route_video.valid_fov.class_id");
|
||||
const policy = objectValue(row.policy, "vegetation.route_video.policy");
|
||||
const presets = objectValue(policy.presets, "vegetation.route_video.policy.presets");
|
||||
policyPresets = Object.fromEntries(Object.entries(presets).map(([presetId, rawRules]) => {
|
||||
@@ -399,10 +485,358 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null {
|
||||
aggregatePredictionPixels,
|
||||
policyPresets,
|
||||
fusionMode,
|
||||
validFovMaskSha256,
|
||||
};
|
||||
}
|
||||
|
||||
function parseResult(value: unknown, resultId: string): VegetationShadowResult {
|
||||
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 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 timeline = objectValue(row.timeline, "vegetation.route_full_review.timeline");
|
||||
exact(
|
||||
timeline.path,
|
||||
"video/frame-source-times-ns.bin",
|
||||
"vegetation.route_full_review.timeline.path",
|
||||
);
|
||||
exact(
|
||||
timeline.encoding,
|
||||
"uint64-le-nanoseconds",
|
||||
"vegetation.route_full_review.timeline.encoding",
|
||||
);
|
||||
exact(timeline.frame_count, 6830, "vegetation.route_full_review.timeline.frame_count");
|
||||
const timelineSha256 = textValue(
|
||||
timeline.sha256,
|
||||
"vegetation.route_full_review.timeline.sha256",
|
||||
);
|
||||
if (!SHA256.test(timelineSha256)) {
|
||||
throw new VegetationShadowContractError("vegetation.route_full_review: timeline digest invalid.");
|
||||
}
|
||||
const timelineByteLength = integerValue(
|
||||
timeline.byte_length,
|
||||
"vegetation.route_full_review.timeline.byte_length",
|
||||
);
|
||||
exact(timelineByteLength, 6830 * 8, "vegetation.route_full_review.timeline.byte_length");
|
||||
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,
|
||||
timelineArtifact: { sha256: timelineSha256, byteLength: timelineByteLength },
|
||||
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,
|
||||
endpointRoot: string,
|
||||
): VegetationShadowResult {
|
||||
const payload = objectValue(value, "Vegetation LAB");
|
||||
exact(payload.schema_version, "missioncore.lab-v1-vegetation-shadow/v1", "vegetation.schema");
|
||||
exact(payload.result_id, resultId, "vegetation.result_id");
|
||||
@@ -438,10 +872,15 @@ function parseResult(value: unknown, resultId: string): VegetationShadowResult {
|
||||
"vegetation.authority.camera_semantics_can_clear_rigid_geometry",
|
||||
);
|
||||
const routeCases = arrayValue(catalogs.ravnoves, "vegetation.catalogs.ravnoves")
|
||||
.map((item) => visualCaseValue(item, resultId, "ravnoves"));
|
||||
.map((item) => visualCaseValue(item, resultId, "ravnoves", endpointRoot));
|
||||
const validationCases = arrayValue(catalogs.goose, "vegetation.catalogs.goose")
|
||||
.map((item) => visualCaseValue(item, resultId, "goose"));
|
||||
if (routeCases.length !== 0 || validationCases.length !== 12) {
|
||||
.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 || routeFullReview ? validationCases.length !== 0 : validationCases.length !== 12)
|
||||
) {
|
||||
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer.");
|
||||
}
|
||||
return {
|
||||
@@ -453,6 +892,8 @@ function parseResult(value: unknown, resultId: string): VegetationShadowResult {
|
||||
routeCases,
|
||||
validationCases,
|
||||
routeVideo: routeVideoValue(payload.route_video),
|
||||
routeReview,
|
||||
routeFullReview,
|
||||
limitations: arrayValue(payload.limitations, "vegetation.limitations")
|
||||
.map((item, index) => textValue(item, `vegetation.limitations[${index}]`)),
|
||||
visualShadowReady: true,
|
||||
@@ -473,6 +914,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,
|
||||
{
|
||||
@@ -490,5 +948,74 @@ export async function fetchVegetationShadowResult(
|
||||
if (!response.ok) {
|
||||
throw new VegetationShadowContractError(`Vegetation LAB недоступна: HTTP ${response.status}.`);
|
||||
}
|
||||
return parseResult(await response.json(), resultId);
|
||||
const result = parseResult(
|
||||
await response.json(),
|
||||
resultId,
|
||||
"/api/v1/laboratory/vegetation-shadow",
|
||||
);
|
||||
if (!result.routeFullReview) return result;
|
||||
const timelineResponse = await fetcher(
|
||||
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/route-timeline`,
|
||||
{ method: "GET", headers: { Accept: "application/octet-stream" }, signal },
|
||||
);
|
||||
if (!timelineResponse.ok) {
|
||||
throw new VegetationShadowContractError(
|
||||
`Vegetation LAB timeline недоступна: HTTP ${timelineResponse.status}.`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
timelineResponse.headers.get("etag")
|
||||
!== `"${result.routeFullReview.timelineArtifact.sha256}"`
|
||||
) {
|
||||
throw new VegetationShadowContractError("Vegetation LAB timeline digest изменён.");
|
||||
}
|
||||
const timelinePayload = await timelineResponse.arrayBuffer();
|
||||
if (timelinePayload.byteLength !== result.routeFullReview.timelineArtifact.byteLength) {
|
||||
throw new VegetationShadowContractError("Vegetation LAB timeline size изменён.");
|
||||
}
|
||||
const timelineView = new DataView(timelinePayload);
|
||||
const frameSourceTimesNs = Array.from({ length: result.routeFullReview.frameCount }, (_, index) => {
|
||||
const value = Number(timelineView.getBigUint64(index * 8, true));
|
||||
if (!Number.isSafeInteger(value)) {
|
||||
throw new VegetationShadowContractError("Vegetation LAB timeline содержит unsafe time.");
|
||||
}
|
||||
return value;
|
||||
});
|
||||
if (
|
||||
frameSourceTimesNs[0] !== Math.round(result.routeFullReview.timelineStartSeconds * 1_000_000_000)
|
||||
|| frameSourceTimesNs.some((time, index) => index > 0 && time <= frameSourceTimesNs[index - 1]!)
|
||||
) {
|
||||
throw new VegetationShadowContractError("Vegetation LAB timeline нарушена.");
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
routeFullReview: { ...result.routeFullReview, frameSourceTimesNs },
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchVegetationBenchmarkResult(
|
||||
resultId: string,
|
||||
{
|
||||
fetcher = fetch,
|
||||
signal,
|
||||
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||
): Promise<VegetationShadowResult> {
|
||||
if (!BENCHMARK_RESULT_ID.test(resultId)) {
|
||||
throw new VegetationShadowContractError("Vegetation benchmark identity недопустима.");
|
||||
}
|
||||
const endpointRoot = "/api/v1/laboratory/vegetation-benchmark";
|
||||
const response = await fetcher(
|
||||
`${endpointRoot}/${encodeURIComponent(resultId)}`,
|
||||
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new VegetationShadowContractError(
|
||||
`Vegetation benchmark недоступен: HTTP ${response.status}.`,
|
||||
);
|
||||
}
|
||||
const result = parseResult(await response.json(), resultId, endpointRoot);
|
||||
if (result.routeVideo) {
|
||||
throw new VegetationShadowContractError("Vegetation benchmark содержит route video.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -81,6 +81,21 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"][data-multi-semantic="true"] {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"][data-multi-semantic="true"]
|
||||
> .m4-replay-threat-visual__pane-layer-controls {
|
||||
flex: 1 0 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"][data-multi-semantic="true"]
|
||||
> .m4-replay-threat-visual__pane-mode-controls {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.m4-replay-threat-evidence-viewer[data-mode-controls="content"]:has(
|
||||
.m4-replay-threat-visual__review-controls
|
||||
) .m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"] {
|
||||
|
||||
@@ -51,6 +51,7 @@ import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
|
||||
import { M49TgsFailClosedResultView } from "./M49TgsFailClosedResult";
|
||||
import { M49TgsFullShadowResultView } from "./M49TgsFullShadowResult";
|
||||
import { VegetationShadowResultView } from "./VegetationShadowResult";
|
||||
import { VegetationBenchmarkResultView } from "./VegetationBenchmarkResult";
|
||||
|
||||
export { isAdvancedLaboratoryWorkId };
|
||||
export type { AdvancedLaboratoryWorkId };
|
||||
@@ -93,6 +94,9 @@ export function AdvancedLaboratoryResult({
|
||||
failedSessionId: string | null;
|
||||
replayError: string | null;
|
||||
}) {
|
||||
if (workId === "lab-v1-vegetation-benchmark" && results.vegetationBenchmark) {
|
||||
return <VegetationBenchmarkResultView rigLabel={rigLabel} result={results.vegetationBenchmark} />;
|
||||
}
|
||||
if (workId === "lab-v1-vegetation-shadow" && results.vegetationShadow) {
|
||||
return <VegetationShadowResultView rigLabel={rigLabel} result={results.vegetationShadow} />;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ export function M49TgsFullShadowEvidence({
|
||||
const controller = new AbortController();
|
||||
setSemantic(null);
|
||||
setSemanticError(null);
|
||||
if (semanticOverride) return () => controller.abort();
|
||||
void fetchE47SemanticSlamResult({
|
||||
resultId: result.source.linkedSemanticResultId,
|
||||
signal: controller.signal,
|
||||
@@ -87,7 +86,7 @@ export function M49TgsFullShadowEvidence({
|
||||
if (!controller.signal.aborted) setSemanticError(message(caught));
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId, semanticOverride]);
|
||||
}, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
@@ -201,15 +200,28 @@ export function M49TgsFullShadowEvidence({
|
||||
const handleSequenceChange = useCallback((sequence: number | null) => {
|
||||
setActiveSequence(sequence);
|
||||
}, []);
|
||||
const semanticLayers = useMemo<readonly M4ReplayThreatSemanticLayer[]>(() => [
|
||||
...(semantic ? [{
|
||||
id: "urban",
|
||||
controlLabel: "ГОРОД · EoMT",
|
||||
resultId: semantic.resultId,
|
||||
taxonomy: semantic.taxonomy,
|
||||
label: "EoMT Cityscapes semantic · recorded video",
|
||||
maskAriaLabel: "EoMT urban semantic prediction",
|
||||
}] : []),
|
||||
...(semanticOverride ? [{
|
||||
...semanticOverride,
|
||||
id: semanticOverride.id ?? "vegetation",
|
||||
controlLabel: semanticOverride.controlLabel ?? "ПРИРОДА · DDRNet",
|
||||
}] : []),
|
||||
], [semantic, semanticOverride]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<M4ReplayThreatVisual
|
||||
resultId={result.source.linkedVisualResultId}
|
||||
semantic={semanticOverride ?? (semantic ? {
|
||||
resultId: semantic.resultId,
|
||||
taxonomy: semantic.taxonomy,
|
||||
} : undefined)}
|
||||
semanticLayers={semanticLayers}
|
||||
initialSemanticLayerId={semanticOverride ? "vegetation" : "urban"}
|
||||
showReviewAnchorBoxes={false}
|
||||
reviewLabel="4 489 source-paced TGS frames"
|
||||
evidenceLabel={evidenceLabel}
|
||||
@@ -227,7 +239,7 @@ export function M49TgsFullShadowEvidence({
|
||||
replacePointCloud: false,
|
||||
}}
|
||||
/>
|
||||
{!semanticOverride && semanticError ? (
|
||||
{semanticError ? (
|
||||
<div className="m4-replay-threat-visual__pane-status" role="alert">
|
||||
Semantic overlay недоступен: {semanticError}
|
||||
</div>
|
||||
|
||||
@@ -96,6 +96,8 @@ function SpatialState({ message: text }: { message: string }) {
|
||||
}
|
||||
|
||||
export interface M4ReplayThreatSemanticLayer {
|
||||
id?: string;
|
||||
controlLabel?: string;
|
||||
resultId: string;
|
||||
spatialResultId?: string | null;
|
||||
maskUrl?: (sequence: number) => string;
|
||||
@@ -155,6 +157,8 @@ const EMPTY_REVIEW_ANCHORS: readonly M4ReplayThreatReviewAnchor[] = [];
|
||||
export function M4ReplayThreatVisual({
|
||||
resultId,
|
||||
semantic,
|
||||
semanticLayers,
|
||||
initialSemanticLayerId,
|
||||
reviewAnchors = EMPTY_REVIEW_ANCHORS,
|
||||
showReviewAnchorBoxes = true,
|
||||
reviewLabel = "Контрольные примеры M4.8R1",
|
||||
@@ -168,6 +172,8 @@ export function M4ReplayThreatVisual({
|
||||
}: {
|
||||
resultId: string;
|
||||
semantic?: M4ReplayThreatSemanticLayer;
|
||||
semanticLayers?: readonly M4ReplayThreatSemanticLayer[];
|
||||
initialSemanticLayerId?: string;
|
||||
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
|
||||
showReviewAnchorBoxes?: boolean;
|
||||
reviewLabel?: string;
|
||||
@@ -199,6 +205,35 @@ export function M4ReplayThreatVisual({
|
||||
));
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0);
|
||||
const availableSemanticLayers = useMemo<readonly M4ReplayThreatSemanticLayer[]>(
|
||||
() => semanticLayers?.length ? semanticLayers : semantic ? [semantic] : [],
|
||||
[semantic, semanticLayers],
|
||||
);
|
||||
const semanticLayerIdentity = availableSemanticLayers
|
||||
.map((layer, index) => layer.id ?? `${layer.resultId}:${index}`)
|
||||
.join("|");
|
||||
const [selectedSemanticLayerId, setSelectedSemanticLayerId] = useState(
|
||||
initialSemanticLayerId ?? "",
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!availableSemanticLayers.length) {
|
||||
setSelectedSemanticLayerId("");
|
||||
return;
|
||||
}
|
||||
const selectedStillExists = availableSemanticLayers.some(
|
||||
(layer, index) => (layer.id ?? `${layer.resultId}:${index}`) === selectedSemanticLayerId,
|
||||
);
|
||||
if (selectedStillExists) return;
|
||||
const preferred = initialSemanticLayerId
|
||||
? availableSemanticLayers.find((layer) => layer.id === initialSemanticLayerId)
|
||||
: null;
|
||||
const next = preferred ?? availableSemanticLayers[0]!;
|
||||
const nextIndex = availableSemanticLayers.indexOf(next);
|
||||
setSelectedSemanticLayerId(next.id ?? `${next.resultId}:${nextIndex}`);
|
||||
}, [availableSemanticLayers, initialSemanticLayerId, semanticLayerIdentity, selectedSemanticLayerId]);
|
||||
const activeSemantic = availableSemanticLayers.find(
|
||||
(layer, index) => (layer.id ?? `${layer.resultId}:${index}`) === selectedSemanticLayerId,
|
||||
) ?? availableSemanticLayers[0];
|
||||
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
|
||||
const metadata = useM4ThreatTimelineMetadata(resultId, timelineEndpointRoot);
|
||||
const playbackRange = useMemo(() => metadata.timeline ? ({
|
||||
@@ -298,19 +333,21 @@ export function M4ReplayThreatVisual({
|
||||
sequence: frame?.sequence ?? null,
|
||||
endpointRoot: timelineEndpointRoot,
|
||||
});
|
||||
const semanticSpatialResultId = semantic
|
||||
? semantic.spatialResultId === undefined ? semantic.resultId : semantic.spatialResultId
|
||||
const semanticSpatialResultId = activeSemantic
|
||||
? activeSemantic.spatialResultId === undefined
|
||||
? activeSemantic.resultId
|
||||
: activeSemantic.spatialResultId
|
||||
: null;
|
||||
const spatialSemanticTaxonomy = useMemo<readonly E47SemanticClass[]>(
|
||||
() => semanticSpatialResultId && semantic
|
||||
? semantic.taxonomy.map((item) => ({
|
||||
() => semanticSpatialResultId && activeSemantic
|
||||
? activeSemantic.taxonomy.map((item) => ({
|
||||
classId: item.classId,
|
||||
label: item.label,
|
||||
disposition: item.disposition === "ambiguous" ? "ambiguous" : "labeled",
|
||||
colorRgb: item.colorRgb,
|
||||
}))
|
||||
: [],
|
||||
[semantic, semanticSpatialResultId],
|
||||
[activeSemantic, semanticSpatialResultId],
|
||||
);
|
||||
const semanticTimeline = useE47SemanticTimelineFrame({
|
||||
resultId: semanticSpatialResultId,
|
||||
@@ -386,14 +423,14 @@ export function M4ReplayThreatVisual({
|
||||
[frame, reviewAnchorBoxes, showReferenceMediaLayers, staticObstacleBoxes],
|
||||
);
|
||||
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
|
||||
() => semantic?.taxonomy.map((item) => ({
|
||||
() => activeSemantic?.taxonomy.map((item) => ({
|
||||
id: item.classId,
|
||||
label: `semantic: ${item.label}`,
|
||||
})) ?? [],
|
||||
[semantic?.taxonomy],
|
||||
[activeSemantic?.taxonomy],
|
||||
);
|
||||
const semanticPalette = useMemo<readonly RecordedEvidenceSemanticPaletteEntry[]>(
|
||||
() => semantic?.taxonomy.map((item) => ({
|
||||
() => activeSemantic?.taxonomy.map((item) => ({
|
||||
classId: item.classId,
|
||||
color: item.disposition === "undefined"
|
||||
? { kind: "transparent" as const }
|
||||
@@ -404,7 +441,7 @@ export function M4ReplayThreatVisual({
|
||||
? 0
|
||||
: item.disposition === "ambiguous" ? 0.52 : 0.92,
|
||||
})) ?? [],
|
||||
[semantic?.taxonomy],
|
||||
[activeSemantic?.taxonomy],
|
||||
);
|
||||
const semanticFrame = semanticTimeline.activeFrame?.sequence === frame?.sequence
|
||||
? semanticTimeline.activeFrame
|
||||
@@ -422,7 +459,7 @@ export function M4ReplayThreatVisual({
|
||||
&& lastSpatialSemanticFrameRef.current.frame.sequence === spatialFrame?.sequence
|
||||
? lastSpatialSemanticFrameRef.current.frame
|
||||
: null;
|
||||
const semanticIntegrityError = semantic && spatialFrame && spatialSemanticFrame && (
|
||||
const semanticIntegrityError = activeSemantic && spatialFrame && spatialSemanticFrame && (
|
||||
spatialSemanticFrame.sourcePointCount !== spatialFrame.pointCloudSourceCount
|
||||
|| spatialFrame.pointCloudSampleCount !== spatialFrame.pointCloudSourceCount
|
||||
|| spatialFrame.pointCloudBodyXyzM.length !== spatialFrame.pointCloudSourceCount
|
||||
@@ -431,7 +468,7 @@ export function M4ReplayThreatVisual({
|
||||
: null;
|
||||
const alignedSemanticPointIds = useMemo<readonly (number | null)[] | undefined>(() => {
|
||||
if (
|
||||
!semantic
|
||||
!activeSemantic
|
||||
|| !showSpatialSemantic
|
||||
|| !spatialFrame
|
||||
|| !spatialSemanticFrame
|
||||
@@ -441,7 +478,7 @@ export function M4ReplayThreatVisual({
|
||||
const status = spatialSemanticFrame.statusCodes[index];
|
||||
return status === 2 || status === 3 ? classId : null;
|
||||
});
|
||||
}, [semantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
|
||||
}, [activeSemantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
|
||||
const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence
|
||||
? spatialFrame
|
||||
: null;
|
||||
@@ -610,19 +647,19 @@ export function M4ReplayThreatVisual({
|
||||
},
|
||||
), [metadata.timeline, spatialFrame, timelineFrame.availableFrames]);
|
||||
const semanticOverlay: RecordedEvidenceSemanticOverlay | undefined =
|
||||
semantic && showMediaSemantic && frame
|
||||
activeSemantic && showMediaSemantic && frame
|
||||
? {
|
||||
src: semantic.maskUrl?.(frame.sequence)
|
||||
?? e47SemanticMaskUrl(semantic.resultId, frame.sequence),
|
||||
src: activeSemantic.maskUrl?.(frame.sequence)
|
||||
?? e47SemanticMaskUrl(activeSemantic.resultId, frame.sequence),
|
||||
prefetchSrcs: Array.from({ length: 12 }, (_, index) => index + 1)
|
||||
.map((offset) => frame.sequence + offset)
|
||||
.filter((sequence) => sequence < (metadata.timeline?.frameCount ?? 0))
|
||||
.map((sequence) => semantic.maskUrl?.(sequence)
|
||||
?? e47SemanticMaskUrl(semantic.resultId, sequence)),
|
||||
.map((sequence) => activeSemantic.maskUrl?.(sequence)
|
||||
?? e47SemanticMaskUrl(activeSemantic.resultId, sequence)),
|
||||
classes: semanticClasses,
|
||||
palette: semanticPalette,
|
||||
opacity: 0.9,
|
||||
ariaLabel: `${semantic.maskAriaLabel ?? "Semantic prediction"} frame ${frame.sequence + 1}`,
|
||||
ariaLabel: `${activeSemantic.maskAriaLabel ?? "Semantic prediction"} frame ${frame.sequence + 1}`,
|
||||
}
|
||||
: undefined;
|
||||
const accumulatedCameraPoints = cameraPointOverlay.overlay?.sequence === frame?.sequence
|
||||
@@ -692,7 +729,7 @@ export function M4ReplayThreatVisual({
|
||||
</div>
|
||||
);
|
||||
|
||||
const mediaLayerControls = semantic
|
||||
const mediaLayerControls = activeSemantic
|
||||
|| (showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery)
|
||||
|| (showReferenceMediaLayers && metadata.timeline?.cameraObstacleProjectionDelivery) ? (
|
||||
<div
|
||||
@@ -700,7 +737,7 @@ export function M4ReplayThreatVisual({
|
||||
role="group"
|
||||
aria-label="Слои камеры и видео"
|
||||
>
|
||||
{semantic ? (
|
||||
{activeSemantic ? (
|
||||
<Button
|
||||
size="compact"
|
||||
shape="pill"
|
||||
@@ -711,6 +748,20 @@ export function M4ReplayThreatVisual({
|
||||
SEMANTICS
|
||||
</Button>
|
||||
) : null}
|
||||
{availableSemanticLayers.length > 1 ? (
|
||||
<SegmentedControl
|
||||
value={selectedSemanticLayerId}
|
||||
items={availableSemanticLayers.map((layer, index) => ({
|
||||
value: layer.id ?? `${layer.resultId}:${index}`,
|
||||
label: layer.controlLabel ?? layer.label ?? `SEMANTIC ${index + 1}`,
|
||||
}))}
|
||||
label="Источник семантики"
|
||||
onChange={(value) => {
|
||||
setSelectedSemanticLayerId(value);
|
||||
setShowMediaSemantic(true);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery ? (
|
||||
<Button
|
||||
size="compact"
|
||||
@@ -1022,6 +1073,7 @@ export function M4ReplayThreatVisual({
|
||||
<div
|
||||
className="m4-replay-threat-visual__pane-toolbar"
|
||||
data-pane-toolbar="media"
|
||||
data-multi-semantic={availableSemanticLayers.length > 1 ? "true" : undefined}
|
||||
>
|
||||
{mediaLayerControls}
|
||||
{mediaModeControls}
|
||||
@@ -1238,8 +1290,8 @@ export function M4ReplayThreatVisual({
|
||||
return (
|
||||
<div className="l3-visual-audit m4-replay-threat-visual">
|
||||
<LaboratoryEvidenceViewer
|
||||
label={semantic
|
||||
? semantic.label ?? "Semantic diagnostic replay"
|
||||
label={activeSemantic
|
||||
? activeSemantic.label ?? "Semantic diagnostic replay"
|
||||
: `${evidenceLabel} recorded-realtime replay`}
|
||||
className="m4-replay-threat-evidence-viewer"
|
||||
mode={mediaMode ?? "none"}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type { VegetationShadowResult } from "../../core/laboratory/vegetationShadow";
|
||||
import {
|
||||
M48MaskComparisonVisual,
|
||||
type M48MaskComparisonCase,
|
||||
} from "./M48FailureAtlasVisual";
|
||||
|
||||
function decimal(value: number, digits = 1): string {
|
||||
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
||||
}
|
||||
|
||||
const VEGETATION_LABELS: Readonly<Record<string, string>> = {
|
||||
high_grass: "Высокая трава",
|
||||
low_grass: "Низкая трава",
|
||||
bush: "Куст",
|
||||
tree_trunk: "Ствол дерева",
|
||||
tree_crown: "Крона дерева",
|
||||
hedge: "Живая изгородь",
|
||||
forest: "Лесная растительность",
|
||||
crops: "Посевы",
|
||||
};
|
||||
|
||||
function comparisonCases(result: VegetationShadowResult): readonly M48MaskComparisonCase[] {
|
||||
return result.validationCases.map((item) => {
|
||||
const focus = item.focus!;
|
||||
return {
|
||||
caseId: item.caseId,
|
||||
title: `${VEGETATION_LABELS[focus.className] ?? focus.className} · truth ${decimal(focus.truthFraction * 100, 1)}% кадра`,
|
||||
sourceUrl: item.assets.source,
|
||||
truthUrl: item.assets.truth,
|
||||
predictions: {
|
||||
ddrnet: item.assets.ddrnet,
|
||||
ppliteseg: item.assets.ppliteseg,
|
||||
},
|
||||
errors: {
|
||||
ddrnet: item.assets.ddrnet_error,
|
||||
ppliteseg: item.assets.ppliteseg_error,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function VegetationBenchmarkResultView({
|
||||
rigLabel,
|
||||
result,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
result: VegetationShadowResult;
|
||||
}) {
|
||||
const selected = result.candidates.find(
|
||||
(candidate) => candidate.candidate === result.selectedCandidate,
|
||||
)!;
|
||||
const alternative = result.candidates.find(
|
||||
(candidate) => candidate.candidate !== result.selectedCandidate,
|
||||
)!;
|
||||
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="M4.8 · архивный benchmark растительности"
|
||||
description="Отдельный truth-backed контур GOOSE для сравнения готовых fine-64 весов. Он не является частью RAVNOVES00 realtime LAB и открывается автономно без Worker 006."
|
||||
status="ARCHIVE ANALYSIS · model qualification only · commands OFF"
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
{ label: "Источник", value: "GOOSE validation · 962 размеченных кадра · 12 hard cases" },
|
||||
{ label: "Сравнение", value: "DDRNet-39 vs PPLiteSeg · official fine-64 weights" },
|
||||
{ label: "Кейсы", value: "трава · куст · ствол · крона · изгородь · лес · посевы" },
|
||||
{ label: "Authority", value: `${rigLabel} · MODEL QUALIFICATION ONLY · commands OFF` },
|
||||
]}
|
||||
brief={{
|
||||
question: "Какие готовые веса лучше различают проезжаемую траву, кусты и стволы на размеченных off-road кадрах?",
|
||||
approach: "Обе модели прогнаны на 962 кадрах, а 12 визуальных кейсов выбраны детерминированно по truth-поддержке восьми растительных классов. Viewer показывает source, ручной truth, prediction и error.",
|
||||
principalResult: `${selected.loadedModelName} лидирует по vegetation IoU: ${decimal(selected.vegetationMeanIouPercent, 2)}% против ${decimal(alternative.vegetationMeanIouPercent, 2)}%.`,
|
||||
limitation: "GOOSE — внешний размеченный домен. Результат выбирает стартовые веса, но не доказывает качество на fisheye RAVNOVES00 и не даёт navigation authority.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "ai-inference",
|
||||
pipelineId: "goose-fine64-ready-weights-benchmark-archive/v1",
|
||||
components: result.candidates.map((candidate) => ({
|
||||
kind: "model" as const,
|
||||
name: candidate.loadedModelName,
|
||||
version: candidate.candidate,
|
||||
role: candidate.candidate === result.selectedCandidate
|
||||
? "selected vegetation candidate"
|
||||
: "comparison candidate",
|
||||
identitySha256: candidate.checkpointSha256,
|
||||
})),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="M4.8 · GOOSE VEGETATION HARD CASES"
|
||||
title="TRUTH — ручная разметка · PREDICTION — ответ модели · ERROR — расхождение"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<M48MaskComparisonVisual
|
||||
cases={comparisonCases(result)}
|
||||
initialCandidate={result.selectedCandidate}
|
||||
/>
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title="DDRNet выбран как стартовый vegetation candidate"
|
||||
status={`${selected.loadedModelName} · перенос на ровер не доказан`}
|
||||
statusTone="warning"
|
||||
metrics={[
|
||||
{
|
||||
label: "GOOSE mIoU",
|
||||
value: `${decimal(selected.meanIouPercent, 2)}% / ${decimal(alternative.meanIouPercent, 2)}%`,
|
||||
hint: `${selected.candidate} / ${alternative.candidate} · полный validation split`,
|
||||
},
|
||||
{
|
||||
label: "Vegetation IoU",
|
||||
value: `${decimal(selected.vegetationMeanIouPercent, 2)}% / ${decimal(alternative.vegetationMeanIouPercent, 2)}%`,
|
||||
hint: "grass/vegetation/bush/tree и родственные fine-64 labels",
|
||||
},
|
||||
{
|
||||
label: "Worker shadow p95",
|
||||
value: `${decimal(selected.shadowLatencyP95Ms, 2)} / ${decimal(alternative.shadowLatencyP95Ms, 2)} ms`,
|
||||
hint: "чистый inference · одна тяжёлая модель за раз",
|
||||
},
|
||||
{
|
||||
label: "Peak VRAM",
|
||||
value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} / ${decimal(alternative.peakReservedVramBytes / 1024 ** 3, 2)} GiB`,
|
||||
hint: `${selected.candidate} / ${alternative.candidate} · RTX 4090`,
|
||||
},
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Обе готовые fine-64 модели воспроизводимо запускаются; DDRNet лучше по aggregate vegetation IoU.",
|
||||
notProved: "Не доказаны accuracy на нашем fisheye, temporal stability, collision safety и физическое поведение ровера.",
|
||||
decision: "Хранить как архив квалификации весов. Проверку на RAVNOVES00 вести только в основной многослойной LAB.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||
import { LaboratoryRecordedClipPlayer } from "../../components/laboratory/LaboratoryRecordedClipPlayer";
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
@@ -7,17 +10,25 @@ import {
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import {
|
||||
RecordedEvidenceSemanticMaskOverlay,
|
||||
type RecordedEvidenceSemanticClass,
|
||||
type RecordedEvidenceSemanticPaletteEntry,
|
||||
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
||||
import {
|
||||
vegetationFullRouteMaskUrl,
|
||||
vegetationVideoMaskUrl,
|
||||
type VegetationFullRouteLayer,
|
||||
type VegetationFullRouteReview,
|
||||
type VegetationMixedRouteReview,
|
||||
type VegetationShadowResult,
|
||||
} from "../../core/laboratory/vegetationShadow";
|
||||
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
|
||||
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
|
||||
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
|
||||
import {
|
||||
fetchM49TgsFullShadowResult,
|
||||
type M49TgsFullShadowResult,
|
||||
} from "../../core/laboratory/m49TgsFullShadow";
|
||||
import {
|
||||
M48MaskComparisonVisual,
|
||||
type M48MaskComparisonCase,
|
||||
} from "./M48FailureAtlasVisual";
|
||||
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
|
||||
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
|
||||
|
||||
@@ -25,35 +36,339 @@ function decimal(value: number, digits = 1): string {
|
||||
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
||||
}
|
||||
|
||||
const VEGETATION_LABELS: Readonly<Record<string, string>> = {
|
||||
high_grass: "Высокая трава",
|
||||
low_grass: "Низкая трава",
|
||||
bush: "Куст",
|
||||
tree_trunk: "Ствол дерева",
|
||||
tree_crown: "Крона дерева",
|
||||
hedge: "Живая изгородь",
|
||||
forest: "Лесная растительность",
|
||||
crops: "Посевы",
|
||||
};
|
||||
const MIXED_ROUTE_MODES = [
|
||||
{ value: "source", label: "SOURCE" },
|
||||
{ value: "city", label: "ГОРОД · EoMT" },
|
||||
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
|
||||
{ value: "tgs", label: "TGS" },
|
||||
] as const;
|
||||
|
||||
function comparisonCases(result: VegetationShadowResult): readonly M48MaskComparisonCase[] {
|
||||
return result.validationCases.map((item) => {
|
||||
const focus = item.focus!;
|
||||
return {
|
||||
caseId: item.caseId,
|
||||
title: `${VEGETATION_LABELS[focus.className] ?? focus.className} · truth ${decimal(focus.truthFraction * 100, 1)}% кадра`,
|
||||
sourceUrl: item.assets.source,
|
||||
truthUrl: item.assets.truth,
|
||||
predictions: {
|
||||
ddrnet: item.assets.ddrnet,
|
||||
ppliteseg: item.assets.ppliteseg,
|
||||
},
|
||||
errors: {
|
||||
ddrnet: item.assets.ddrnet_error,
|
||||
ppliteseg: item.assets.ppliteseg_error,
|
||||
},
|
||||
};
|
||||
});
|
||||
const FULL_ROUTE_MODES = [
|
||||
{ value: "source", label: "SOURCE" },
|
||||
{ value: "city", label: "ГОРОД · EoMT" },
|
||||
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
|
||||
] as const;
|
||||
|
||||
function semanticPresentation(layer: VegetationFullRouteLayer): {
|
||||
classes: readonly RecordedEvidenceSemanticClass[];
|
||||
palette: readonly RecordedEvidenceSemanticPaletteEntry[];
|
||||
} {
|
||||
return {
|
||||
classes: layer.taxonomy.map((item) => ({ id: item.classId, label: item.label })),
|
||||
palette: layer.taxonomy.map((item) => ({
|
||||
classId: item.classId,
|
||||
color: item.classId === 0
|
||||
? { kind: "transparent" as const }
|
||||
: { kind: "diagnostic" as const, rgb: item.colorRgb },
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function FullRouteReviewEvidence({
|
||||
resultId,
|
||||
review,
|
||||
}: {
|
||||
resultId: string;
|
||||
review: VegetationFullRouteReview;
|
||||
}) {
|
||||
const [sequence, setSequence] = useState(1);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [playbackRate, setPlaybackRate] = useState(1);
|
||||
const [mode, setMode] = useState<typeof FULL_ROUTE_MODES[number]["value"]>("vegetation");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
|
||||
const [videoError, setVideoError] = useState<string | null>(null);
|
||||
const frames = useMemo(
|
||||
() => review.frameSourceTimesNs.map((sourceTimeNs, index) => ({
|
||||
sequence: index + 1,
|
||||
sourceTimeNs,
|
||||
})),
|
||||
[review.frameSourceTimesNs],
|
||||
);
|
||||
const layer = mode === "source" ? null : review[mode];
|
||||
const semantic = useMemo(() => layer ? semanticPresentation(layer) : null, [layer]);
|
||||
const maskSequence = sequence - 1;
|
||||
const prefetchSrcs = useMemo(() => layer
|
||||
? Array.from({ length: 8 }, (_, offset) => maskSequence + offset + 1)
|
||||
.filter((candidate) => candidate < review.frameCount)
|
||||
.map((candidate) => vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", candidate))
|
||||
: [], [layer, maskSequence, mode, resultId, review.frameCount]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setVideoSource(null);
|
||||
setVideoError(null);
|
||||
void resolveObservationSessionReplay(review.sessionId, { signal: controller.signal })
|
||||
.then((launch) => {
|
||||
const source = recordedObservationSources(launch).find((candidate) => (
|
||||
candidate.id === review.recordedMediaSourceId
|
||||
&& candidate.modality === "video"
|
||||
&& candidate.semanticChannelId === "camera.video.recorded"
|
||||
&& candidate.delivery?.kind === "recorded-fmp4-manifest"
|
||||
&& candidate.delivery.manifestGenerationSha256 === review.recordedMediaGenerationSha256
|
||||
&& candidate.delivery.timelineStartSeconds === review.timelineStartSeconds
|
||||
&& candidate.delivery.timelineEndSeconds >= review.timelineEndSeconds
|
||||
));
|
||||
if (!source) {
|
||||
throw new Error("RIGHT-видео не совпало с sealed RAVNOVES004TREE timeline.");
|
||||
}
|
||||
if (!controller.signal.aborted) setVideoSource(source);
|
||||
})
|
||||
.catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setVideoError(caught instanceof Error ? caught.message : "Записанное видео недоступно.");
|
||||
}
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [
|
||||
review.recordedMediaGenerationSha256,
|
||||
review.recordedMediaSourceId,
|
||||
review.sessionId,
|
||||
review.timelineEndSeconds,
|
||||
review.timelineStartSeconds,
|
||||
]);
|
||||
|
||||
return (
|
||||
<LaboratoryEvidenceViewer
|
||||
label="RAVNOVES004TREE full recorded review"
|
||||
className="m48-atlas-visual"
|
||||
mode={mode}
|
||||
modes={FULL_ROUTE_MODES}
|
||||
expanded={expanded}
|
||||
onModeChange={setMode}
|
||||
onExpandedChange={setExpanded}
|
||||
chromeLayout="stacked"
|
||||
>
|
||||
{videoSource ? (
|
||||
<LaboratoryRecordedClipPlayer
|
||||
source={videoSource}
|
||||
segmentCount={review.frameCount}
|
||||
frames={frames}
|
||||
sequence={sequence}
|
||||
playing={playing}
|
||||
playbackRate={playbackRate}
|
||||
cameraPresentation="primary"
|
||||
continuousPlayback
|
||||
sourceCount={1}
|
||||
onSequenceChange={setSequence}
|
||||
onPlayingChange={setPlaying}
|
||||
onPlaybackRateChange={setPlaybackRate}
|
||||
cameraOverlay={(
|
||||
<>
|
||||
<div className="m48-clip-player__pane-label" data-pane="camera">
|
||||
{mode === "source" ? "SOURCE" : `${mode === "city" ? "EoMT CITY" : "DDRNet NATURE"} · КАДР ${sequence}/${review.frameCount}`}
|
||||
</div>
|
||||
{layer && semantic ? (
|
||||
<div className="m48-clip-player__overlay">
|
||||
<RecordedEvidenceSemanticMaskOverlay
|
||||
src={vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", maskSequence)}
|
||||
prefetchSrcs={prefetchSrcs}
|
||||
imageWidth={review.width}
|
||||
imageHeight={review.height}
|
||||
classes={semantic.classes}
|
||||
palette={semantic.palette}
|
||||
opacity={0.76}
|
||||
ariaLabel={`${layer.name} semantic prediction`}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="m4-replay-threat-visual__pane-status" role={videoError ? "alert" : "status"}>
|
||||
{videoError ?? "Открываем автономный recorded source…"}
|
||||
</div>
|
||||
)}
|
||||
</LaboratoryEvidenceViewer>
|
||||
);
|
||||
}
|
||||
|
||||
function FullRouteReviewResult({
|
||||
rigLabel,
|
||||
resultId,
|
||||
review,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
resultId: string;
|
||||
review: VegetationFullRouteReview;
|
||||
}) {
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB V1 · RAVNOVES004TREE · полный маршрут"
|
||||
description="Существующий M4.7-шаблон воспроизводит всю запись и переключает два независимых sealed semantic-слоя: городской EoMT и природный DDRNet. Worker для открытия результата не нужен."
|
||||
status="FULL RECORDED REVIEW · truth отсутствует · commands OFF"
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount}/${review.frameCount} frames` },
|
||||
{ label: "Город", value: `${review.city.name} · ${decimal(review.city.inferenceFps, 2)} fps` },
|
||||
{ label: "Природа", value: `${review.vegetation.name} · ${decimal(review.vegetation.inferenceFps, 2)} fps` },
|
||||
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
|
||||
]}
|
||||
brief={{
|
||||
question: "Как оба semantic-кандидата ведут себя на полном переходе от сельской среды к городской?",
|
||||
approach: "Все 6830 позиции одной recorded timeline последовательно прогнаны на Worker 006 и сохранены двумя независимыми архивами масок. В M4.7 переключается только видимый слой.",
|
||||
principalResult: "Полная временная шкала доступна локально в SOURCE / EoMT CITY / DDRNet NATURE без обращения к Worker.",
|
||||
limitation: "Ручной truth отсутствует. Один повреждённый H.264-пакет на позиции 6092 представлен предыдущим декодированным кадром и явно зафиксирован в proof. Полный TGS и кюветы этим прогоном не проверялись.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "ai-inference",
|
||||
pipelineId: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
components: [
|
||||
{ kind: "model", name: review.city.name, version: "sealed Worker 006 run", role: "urban semantic review", identitySha256: null },
|
||||
{ kind: "model", name: review.vegetation.name, version: "GOOSE DDRNet-39", role: "vegetation semantic review", identitySha256: null },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="M4.7 TEMPLATE · RAVNOVES004TREE FULL VIDEO"
|
||||
title="SOURCE / EoMT CITY / DDRNet NATURE · 6830/6830 · TRUTH отсутствует"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<FullRouteReviewEvidence resultId={resultId} review={review} />
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title="Полный двухслойный visual review собран; управление не авторизовано"
|
||||
status="Recorded evidence ready · navigation/actuation OFF"
|
||||
statusTone="warning"
|
||||
metrics={[
|
||||
{ label: "Route masks", value: "6830/6830 × 2", hint: "sealed local archives · Worker не требуется" },
|
||||
{ label: "EoMT p95", value: `${decimal(review.city.latencyP95Ms, 2)} ms`, hint: "последовательный изолированный прогон" },
|
||||
{ label: "DDRNet p95", value: `${decimal(review.vegetation.latencyP95Ms, 2)} ms`, hint: "последовательный изолированный прогон" },
|
||||
{ label: "Decode repair", value: "1/6830", hint: "sequence 6092 · previous frame · sealed proof" },
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Городской EoMT и природный DDRNet воспроизводимо обработали полную запись и доступны в одном существующем M4.7 viewer.",
|
||||
notProved: "Не доказаны truth accuracy, одновременный realtime-load, полный TGS, отрицательные препятствия и безопасное управление ровером.",
|
||||
decision: "Использовать результат только как визуальную диагностику. Navigation/actuation оставить OFF; следующий gate — оценка временной стабильности и независимый person/vehicle STOP.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MixedRouteReviewEvidence({ review }: { review: VegetationMixedRouteReview }) {
|
||||
const [index, setIndex] = useState(0);
|
||||
const [mode, setMode] = useState<typeof MIXED_ROUTE_MODES[number]["value"]>("vegetation");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const item = review.cases[index]!;
|
||||
return (
|
||||
<LaboratoryEvidenceViewer
|
||||
label="RAVNOVES004TREE mixed route review"
|
||||
className="m48-atlas-visual"
|
||||
mode={mode}
|
||||
modes={MIXED_ROUTE_MODES}
|
||||
expanded={expanded}
|
||||
onModeChange={setMode}
|
||||
onExpandedChange={setExpanded}
|
||||
chromeLayout="stacked"
|
||||
actions={(
|
||||
<>
|
||||
<IconButton label="Предыдущая сцена" onClick={() => setIndex((index - 1 + review.cases.length) % review.cases.length)}>
|
||||
<Icon name="chevron-left" size={16} />
|
||||
</IconButton>
|
||||
<IconButton label="Следующая сцена" onClick={() => setIndex((index + 1) % review.cases.length)}>
|
||||
<Icon name="chevron-right" size={16} />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
overlay={(
|
||||
<div className="m48-atlas-visual__case">
|
||||
<StatusBadge tone={item.phase === "urban" ? "accent" : item.phase === "transition" ? "warning" : "neutral"}>
|
||||
{item.phase.toUpperCase()} · {index + 1}/{review.cases.length}
|
||||
</StatusBadge>
|
||||
<strong>sequence {item.sourceSequence} · +{decimal(item.sessionSeconds, 2)} s</strong>
|
||||
<small>
|
||||
TGS: {item.tgs.groundCells} ground · {item.tgs.occupiedCells} occupied · {item.tgs.unobservedCells} unobserved
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="recorded-evidence-image-scene">
|
||||
<img src={item.assets[mode]} alt="" draggable={false} />
|
||||
</div>
|
||||
</LaboratoryEvidenceViewer>
|
||||
);
|
||||
}
|
||||
|
||||
function MixedRouteReviewResult({
|
||||
rigLabel,
|
||||
review,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
review: VegetationMixedRouteReview;
|
||||
}) {
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB V1 · RAVNOVES004TREE · село → город"
|
||||
description="Существующий LAB-шаблон показывает 10 синхронных camera/LiDAR сцен одной записи. EoMT и DDRNet остаются независимыми слоями; TGS показывает отдельную геометрию и не может быть очищен семантической маской."
|
||||
status="BOUNDED RECORDED REVIEW · truth отсутствует · commands OFF"
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount} camera/LiDAR islands` },
|
||||
{ label: "Переход", value: "5 rural · 1 transition · 4 urban" },
|
||||
{ label: "Слои", value: "SOURCE · EoMT CITY · DDRNet VEGETATION · causal TGS" },
|
||||
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
|
||||
]}
|
||||
brief={{
|
||||
question: "Сохраняются ли городская семантика, растительность и геометрия при переходе из сельской среды в город?",
|
||||
approach: "Выбраны десять соседних с исходными сцен camera-кадров, каждый синхронизирован с LiDAR в пределах 100 мс. Все три вычислительных слоя прогнаны на Worker 006 и запечатаны локально.",
|
||||
principalResult: "Все 10 сцен обработаны EoMT, DDRNet и causal TGS. Слои можно переключать без наложения цветов и без зависимости LAB от воркера.",
|
||||
limitation: "Это bounded islands без ручной truth. DDRNet шумит по подтипам растительности; TGS не доказывает обнаружение кювета или отрицательного препятствия.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "ai-inference",
|
||||
pipelineId: "ravnoves004tree-eomt-ddrnet-causal-tgs-review/v1",
|
||||
components: [
|
||||
{ kind: "model", name: review.models.city.name, version: "sealed Worker run", role: "urban semantic review", identitySha256: null },
|
||||
{ kind: "model", name: review.models.vegetation.name, version: "GOOSE DDRNet-39", role: "vegetation semantic review", identitySha256: null },
|
||||
{ kind: "algorithm", name: review.models.tgs.name, version: "TRAVEL compatibility runner", role: "independent local geometry", identitySha256: null },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="M4.7 TEMPLATE · RAVNOVES004TREE"
|
||||
title="SOURCE / ГОРОД / ПРИРОДА / TGS · 10/10 · TRUTH отсутствует"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<MixedRouteReviewEvidence review={review} />
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title="Переход село → город воспроизведён; safety gate не закрыт"
|
||||
status="Review ready · navigation/actuation OFF"
|
||||
statusTone="warning"
|
||||
metrics={[
|
||||
{ label: "Aligned scenes", value: "10/10", hint: "camera + LiDAR + pose · автономный archive" },
|
||||
{ label: "EoMT end-to-end p95", value: `${decimal(review.models.city.endToEndP95Ms, 2)} ms`, hint: `${decimal(review.models.city.inferenceFps, 2)} fps в изолированном прогоне` },
|
||||
{ label: "DDRNet inference p95", value: `${decimal(review.models.vegetation.latencyP95Ms, 2)} ms`, hint: "candidate review · не совместный realtime stack" },
|
||||
{ label: "TGS p95", value: `${decimal(review.models.tgs.latencyP95Ms, 2)} ms`, hint: `${review.models.tgs.cellSizeM} m cells · ${review.models.tgs.radiusM} m radius` },
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Оба semantic слоя и causal TGS воспроизводимо работают на сельской, переходной и городской части новой записи.",
|
||||
notProved: "Не доказаны accuracy без truth, временная стабильность по всему видео, детект кюветов и безопасное совместное realtime-управление ровером.",
|
||||
decision: "Оставить navigation/actuation OFF. Следующий короткий gate — непрерывный realtime-load двух моделей плюс независимый person/vehicle STOP; кюветы проверять отдельной записью.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) {
|
||||
@@ -83,16 +398,14 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
|
||||
}, [route.baseM4ResultId, route.linkedTgsResultId]);
|
||||
|
||||
const semantic = {
|
||||
id: "vegetation",
|
||||
controlLabel: "ПРИРОДА · DDRNet",
|
||||
resultId: route.workerResultId,
|
||||
spatialResultId: null,
|
||||
taxonomy: route.taxonomy,
|
||||
maskUrl: (sequence: number) => vegetationVideoMaskUrl(result.resultId, sequence),
|
||||
label: route.viewKind === "coarse-material-policy-review"
|
||||
? "Coarse material evidence · recorded video"
|
||||
: "DDRNet vegetation prediction · recorded video",
|
||||
maskAriaLabel: route.viewKind === "coarse-material-policy-review"
|
||||
? "Coarse material policy evidence"
|
||||
: "DDRNet vegetation prediction",
|
||||
label: "DDRNet coarse vegetation material · recorded video",
|
||||
maskAriaLabel: "DDRNet vegetation material prediction",
|
||||
} as const;
|
||||
|
||||
if (route.linkedTgsResultId && tgs) {
|
||||
@@ -100,19 +413,23 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
|
||||
<M49TgsFullShadowEvidence
|
||||
result={tgs}
|
||||
semanticOverride={semantic}
|
||||
evidenceLabel="LAB V1 · MATERIAL + YOLOX + TGS"
|
||||
evidenceLabel="LAB V1 · EoMT + DDRNet + YOLOX + TGS"
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (route.linkedTgsResultId && !tgsError) {
|
||||
return <div className="m4-replay-threat-visual__pane-status" role="status">Открываем sealed TGS и coarse material timeline…</div>;
|
||||
return (
|
||||
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||
Открываем sealed EoMT, TGS и coarse vegetation timeline…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<M4ReplayThreatVisual
|
||||
resultId={route.baseM4ResultId}
|
||||
evidenceLabel="LAB V1 · DDRNet"
|
||||
showReferenceMediaLayers={route.viewKind === "coarse-material-policy-review"}
|
||||
showReferenceMediaLayers
|
||||
showSpatialOverlaySummary={false}
|
||||
semantic={semantic}
|
||||
/>
|
||||
@@ -132,146 +449,129 @@ export function VegetationShadowResultView({
|
||||
rigLabel: string;
|
||||
result: VegetationShadowResult;
|
||||
}) {
|
||||
if (result.routeFullReview) {
|
||||
return (
|
||||
<FullRouteReviewResult
|
||||
rigLabel={rigLabel}
|
||||
resultId={result.resultId}
|
||||
review={result.routeFullReview}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (result.routeReview) {
|
||||
return <MixedRouteReviewResult rigLabel={rigLabel} review={result.routeReview} />;
|
||||
}
|
||||
const route = result.routeVideo;
|
||||
const selected = result.candidates.find(
|
||||
(candidate) => candidate.candidate === result.selectedCandidate,
|
||||
)!;
|
||||
const alternative = result.candidates.find(
|
||||
(candidate) => candidate.candidate !== result.selectedCandidate,
|
||||
)!;
|
||||
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB V1 · готовые модели растительности"
|
||||
description={result.routeVideo
|
||||
? result.routeVideo.viewKind === "coarse-material-policy-review"
|
||||
? "M4.8 сохраняет truth-backed сравнение моделей, а штатный M4.7 синхронно показывает coarse material evidence, frozen YOLOX vetoes и causal TGS на всей записи RAVNOVES00. Все слои запечатаны локально и открываются без Worker 006."
|
||||
: "M4.8 сохраняет truth-backed сравнение моделей, а штатный M4.7 viewer показывает фактический DDRNet prediction на всей записи RAVNOVES00. Все 4489 масок запечатаны локально и открываются без Worker 006."
|
||||
: "Штатный M4.8-инструмент сравнивает две готовые fine-64 модели на полном GOOSE validation split и на 12 truth-backed hard cases, выбранных только по наличию нужной растительности. Sealed evidence открывается локально без Worker 006."}
|
||||
status={result.routeVideo
|
||||
? result.routeVideo.viewKind === "coarse-material-policy-review"
|
||||
? "MULTILAYER POLICY REVIEW · commands OFF · route truth отсутствует"
|
||||
: "DDRNet full-video prediction ready · route truth отсутствует"
|
||||
: "Truth-backed model comparison · route transfer не принят"}
|
||||
title="LAB V1 · карта ровера · город + растительность"
|
||||
description="Один recorded-контур RAVNOVES00 синхронно показывает городской EoMT, природный DDRNet, frozen YOLOX detections и causal TGS. Семантические маски переключаются, чтобы их цвета не скрывали друг друга; геометрическое veto остаётся независимым."
|
||||
status={route
|
||||
? "MULTILAYER RECORDED REVIEW · commands OFF · route truth отсутствует"
|
||||
: "ROUTE EVIDENCE MISSING · commands OFF"}
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
{ label: "Источник", value: "GOOSE validation · 962 размеченных кадра · 12 vegetation hard cases" },
|
||||
{ label: "Сравнение", value: "DDRNet-39 vs PPLiteSeg · official fine-64 weights" },
|
||||
{ label: "Кейсы", value: "трава · куст · ствол · крона · изгородь · лес · посевы" },
|
||||
...(result.routeVideo ? [{
|
||||
label: "Видео",
|
||||
value: result.routeVideo.viewKind === "coarse-material-policy-review"
|
||||
? "RAVNOVES00 · 4489/4489 coarse masks + YOLOX + TGS · exact sequence"
|
||||
: "RAVNOVES00 · 4489/4489 DDRNet masks · exact recorded sequence",
|
||||
}] : []),
|
||||
{ label: "Authority", value: `${rigLabel} · MODEL QUALIFICATION ONLY · commands OFF` },
|
||||
{ label: "Источник", value: "RAVNOVES00 · sensor.camera.right · 4489 recorded frames" },
|
||||
{ label: "Город", value: "EoMT Cityscapes · sealed E47 semantic archive" },
|
||||
{ label: "Растительность", value: "DDRNet-39 fine-64 → coarse mission-neutral materials" },
|
||||
{ label: "Safety", value: "YOLOX object boxes + causal TGS · semantic masks не снимают veto" },
|
||||
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
|
||||
]}
|
||||
brief={{
|
||||
question: "Какие готовые веса лучше различают проезжаемую траву, кусты и стволы на размеченных off-road кадрах?",
|
||||
approach: "Обе модели последовательно прогнаны в одном изолированном CUDA-runtime на 962 кадрах. 12 визуальных кейсов выбраны детерминированно по truth-поддержке восьми растительных классов; один M4.8 viewer показывает source, truth, prediction и material-error для выбранной модели.",
|
||||
principalResult: `${selected.loadedModelName} лидирует по vegetation IoU: ${decimal(selected.vegetationMeanIouPercent, 2)}% против ${decimal(alternative.vegetationMeanIouPercent, 2)}%. ${result.routeVideo?.viewKind === "coarse-material-policy-review" ? "Fine-64 prediction сведён к mission-neutral материалам; YOLOX и TGS сохраняют независимое veto." : result.routeVideo ? "Его фактическая temporal stability теперь видна на всех 4489 кадрах штатного recorded viewer." : "Ошибки по каждому типу проверяются в одном штатном инструменте."}`,
|
||||
limitation: "GOOSE — внешний размеченный домен; RAVNOVES00 — наш fisheye, но без ручной truth-разметки. Материалы — prediction, а не доказательство проходимости. TGS не проецируется в пиксели без отдельной принятой калибровки.",
|
||||
question: "Можно ли одновременно видеть городской и природный semantic stack, не теряя независимую геометрическую защиту?",
|
||||
approach: "EoMT и DDRNet сохранены как два независимых sealed слоя на одной M4 timeline. В штатном M4.7 viewer пользователь переключает только отображаемую маску; YOLOX и TGS остаются активными слоями evidence.",
|
||||
principalResult: route
|
||||
? "Оба semantic archive доступны в одном viewer. Это не пиксельный fusion и не единая новая модель: городской и природный ответы остаются раздельными."
|
||||
: "Route archive для этой immutable identity отсутствует.",
|
||||
limitation: "RAVNOVES00 не имеет ручной truth. DDRNet заметно прыгает между HIGH GRASS, WOODY и UNKNOWN; поэтому subtype нельзя подавать напрямую в planner. Отсутствие класса никогда не означает свободный путь.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
completeness: route ? "complete" : "legacy-partial",
|
||||
executionClass: "ai-inference",
|
||||
pipelineId: "goose-fine64-ready-weights-to-ravnoves-policy-shadow/v1",
|
||||
components: result.candidates.map((candidate) => ({
|
||||
kind: "model" as const,
|
||||
name: candidate.loadedModelName,
|
||||
version: candidate.candidate,
|
||||
role: candidate.candidate === result.selectedCandidate ? "selected policy provider" : "comparison candidate",
|
||||
identitySha256: candidate.checkpointSha256,
|
||||
})),
|
||||
pipelineId: "ravnoves-eomt-ddrnet-yolox-causal-tgs-recorded-review/v1",
|
||||
components: [
|
||||
{
|
||||
kind: "model",
|
||||
name: "EoMT Cityscapes semantic",
|
||||
version: "sealed E47 archive",
|
||||
role: "urban semantic review",
|
||||
identitySha256: null,
|
||||
},
|
||||
{
|
||||
kind: "model",
|
||||
name: selected.loadedModelName,
|
||||
version: selected.candidate,
|
||||
role: "vegetation material candidate",
|
||||
identitySha256: selected.checkpointSha256,
|
||||
},
|
||||
{
|
||||
kind: "algorithm",
|
||||
name: "Frozen YOLOX + causal TGS",
|
||||
version: "linked M4/M4.9 archives",
|
||||
role: "independent object and geometry veto",
|
||||
identitySha256: null,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<>
|
||||
<LaboratoryEvidence
|
||||
eyebrow="M4.8 · GOOSE VEGETATION HARD CASES"
|
||||
title="ERROR: красный — пропуск · жёлтый — лишнее · фиолетовый — перепутан тип · зелёный — совпадение"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<M48MaskComparisonVisual
|
||||
cases={comparisonCases(result)}
|
||||
initialCandidate={result.selectedCandidate}
|
||||
/>
|
||||
</LaboratoryEvidence>
|
||||
{result.routeVideo ? (
|
||||
<LaboratoryEvidence
|
||||
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
|
||||
title={result.routeVideo.viewKind === "coarse-material-policy-review"
|
||||
? "COARSE MATERIAL + YOLOX VETO + CAUSAL TGS · 4489/4489 · TRUTH отсутствует"
|
||||
: "DDRNet PREDICTION · 4489/4489 кадров · TRUTH для этой записи отсутствует"}
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<VegetationRouteEvidence result={result} />
|
||||
</LaboratoryEvidence>
|
||||
) : null}
|
||||
</>
|
||||
evidence={route ? (
|
||||
<LaboratoryEvidence
|
||||
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
|
||||
title="EoMT CITY / DDRNet VEGETATION + YOLOX + CAUSAL TGS · 4489/4489 · TRUTH отсутствует"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<VegetationRouteEvidence result={result} />
|
||||
</LaboratoryEvidence>
|
||||
) : (
|
||||
<LaboratoryEvidence
|
||||
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
|
||||
title="ROUTE ARCHIVE отсутствует"
|
||||
kind="diagnostic-model"
|
||||
>
|
||||
<div className="m4-replay-threat-visual__pane-status" role="alert">
|
||||
Для этой immutable identity нет полного route video evidence.
|
||||
</div>
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title={result.routeVideo?.viewKind === "coarse-material-policy-review"
|
||||
? "Слои собраны для визуального policy review; управление не авторизовано"
|
||||
: "DDRNet — стартовые веса; перенос на ровер ещё не доказан"}
|
||||
status={result.routeVideo?.viewKind === "coarse-material-policy-review"
|
||||
? "Materials are advisory · YOLOX/TGS veto cannot be cleared"
|
||||
: `${selected.loadedModelName} выбран только как vegetation candidate`}
|
||||
title="Многослойный visual review собран; управление не авторизовано"
|
||||
status="Semantics advisory · YOLOX/TGS veto cannot be cleared"
|
||||
statusTone="warning"
|
||||
metrics={[
|
||||
{
|
||||
label: "GOOSE mIoU",
|
||||
value: `${decimal(selected.meanIouPercent, 2)}% / ${decimal(alternative.meanIouPercent, 2)}%`,
|
||||
hint: `${selected.candidate} / ${alternative.candidate} · полный validation split`,
|
||||
label: "Route masks",
|
||||
value: route ? `${route.frameCount}/${route.frameCount}` : "0/4489",
|
||||
hint: "sealed local playback · Worker для открытия не нужен",
|
||||
},
|
||||
{
|
||||
label: "Vegetation IoU",
|
||||
value: `${decimal(selected.vegetationMeanIouPercent, 2)}% / ${decimal(alternative.vegetationMeanIouPercent, 2)}%`,
|
||||
hint: "агрегация классов grass/vegetation/bush/tree и родственных fine-64 labels",
|
||||
label: "Semantic sources",
|
||||
value: route ? "2 independent layers" : "0",
|
||||
hint: "EoMT CITY / DDRNet VEGETATION · display switches, evidence does not fuse",
|
||||
},
|
||||
{
|
||||
label: "Worker shadow p95",
|
||||
value: `${decimal(selected.shadowLatencyP95Ms, 2)} / ${decimal(alternative.shadowLatencyP95Ms, 2)} ms`,
|
||||
hint: "чистый inference · одна тяжёлая модель за раз",
|
||||
label: "Vegetation worker p95",
|
||||
value: `${decimal(selected.shadowLatencyP95Ms, 2)} ms`,
|
||||
hint: "изолированный DDRNet inference; не совместный realtime stack",
|
||||
},
|
||||
{
|
||||
label: "Cold prewarm",
|
||||
value: `${decimal(selected.shadowPrewarmLatencyMs, 1)} / ${decimal(alternative.shadowPrewarmLatencyMs, 1)} ms`,
|
||||
hint: "один явный inference до допуска кадров; исключён из steady-state p95",
|
||||
label: "Vegetation peak VRAM",
|
||||
value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} GiB`,
|
||||
hint: "DDRNet candidate на Worker 006",
|
||||
},
|
||||
{
|
||||
label: "Worker throughput",
|
||||
value: `${decimal(selected.shadowThroughputFps, 1)} / ${decimal(alternative.shadowThroughputFps, 1)} FPS`,
|
||||
hint: "изолированный Worker 006 · не realtime graph целиком",
|
||||
},
|
||||
{
|
||||
label: "Peak VRAM",
|
||||
value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} / ${decimal(alternative.peakReservedVramBytes / 1024 ** 3, 2)} GiB`,
|
||||
hint: `${selected.candidate} / ${alternative.candidate} · RTX 4090`,
|
||||
},
|
||||
{
|
||||
label: "Hard-case evidence",
|
||||
value: "12 truth-backed cases",
|
||||
hint: "8 vegetation strata · Worker для открытия не требуется",
|
||||
},
|
||||
...(result.routeVideo ? [{
|
||||
label: "Route video",
|
||||
value: "4489/4489 masks",
|
||||
hint: result.routeVideo.viewKind === "coarse-material-policy-review"
|
||||
? "9 coarse states · YOLOX + causal TGS · Worker-independent playback"
|
||||
: "DDRNet prediction · exact sequence · Worker-independent playback",
|
||||
}] : []),
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Обе официальные fine-64 модели воспроизводимо запускаются на Worker 006; DDRNet лучше по aggregate vegetation IoU. Truth-backed hard cases прямо показывают траву, кусты и стволы, а не случайные автомобили и здания.",
|
||||
notProved: "Не доказаны accuracy на нашем fisheye-домене, папоротник как отдельный материал, collision safety и physical-live поведение ровера. Видео позволяет увидеть temporal stability, но без truth не превращает её в метрику качества.",
|
||||
decision: result.routeVideo?.viewKind === "coarse-material-policy-review"
|
||||
? "На одном M4.7 проверить ложные LOW GRASS/HIGH GRASS кандидаты против YOLOX и TGS. До truth-кейсов и integrated load этот слой не подключать к planner/actuation."
|
||||
: "Смотреть полный prediction на видео и собирать конкретные temporal/domain failure cases. DDRNet остаётся diagnostic candidate; LiDAR/TGS fail-closed геометрию не ослаблять.",
|
||||
proved: "На одной recorded timeline доступны городской EoMT, природный DDRNet, YOLOX detections и causal TGS; LAB автономна от Worker.",
|
||||
notProved: "Не доказаны совместный live-runtime EoMT+DDRNet, truth accuracy на fisheye, стабильные vegetation subtypes и безопасное управление ровером.",
|
||||
decision: "Использовать маски только для диагностики. Следующий qualification gate — motion-aware temporal vegetation fusion и отдельный совместный realtime load test; до него planner/actuation остаются OFF.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
+14
-14
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState, type ReactNode } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
|
||||
import type { L34RightYoloxTruthIslandResult } from "../../../core/laboratory/l34RightYoloxTruthIsland";
|
||||
import type { L34DResult } from "../../../core/laboratory/l34dCumulativePostprocessing";
|
||||
@@ -28,19 +28,19 @@ export function useL34AnnotationCapability({
|
||||
}): ReactNode {
|
||||
const [open, setOpen] = useState(false);
|
||||
const openWorkspace = useCallback(() => setOpen(true), []);
|
||||
const available = selectedWorkId === "l34-right-yolox-truth-island-freeze"
|
||||
&& l34Result
|
||||
? { resultId: l34Result.resultId, workflow: "assisted-candidate" as const }
|
||||
: selectedWorkId === "e46-detector-truth-island" && e46Result
|
||||
? { resultId: e46Result.resultId, workflow: "independent-blind" as const }
|
||||
: selectedWorkId === "e46a-ai-engineering-preannotation" && e46aResult
|
||||
? { resultId: e46aResult.resultId, workflow: "engineering-preannotation" as const }
|
||||
: selectedWorkId === "l34d-cumulative-postprocessing-candidate"
|
||||
&& l34dResult
|
||||
? { resultId: l34dResult.resultId, workflow: "prediction-hidden" as const }
|
||||
: selectedWorkId === "l34e-self-review-diagnostic" && l34eResult
|
||||
? { resultId: l34eResult.resultId, workflow: "adjudication" as const }
|
||||
: null;
|
||||
const available = useMemo(() => (
|
||||
selectedWorkId === "l34-right-yolox-truth-island-freeze" && l34Result
|
||||
? { resultId: l34Result.resultId, workflow: "assisted-candidate" as const }
|
||||
: selectedWorkId === "e46-detector-truth-island" && e46Result
|
||||
? { resultId: e46Result.resultId, workflow: "independent-blind" as const }
|
||||
: selectedWorkId === "e46a-ai-engineering-preannotation" && e46aResult
|
||||
? { resultId: e46aResult.resultId, workflow: "engineering-preannotation" as const }
|
||||
: selectedWorkId === "l34d-cumulative-postprocessing-candidate" && l34dResult
|
||||
? { resultId: l34dResult.resultId, workflow: "prediction-hidden" as const }
|
||||
: selectedWorkId === "l34e-self-review-diagnostic" && l34eResult
|
||||
? { resultId: l34eResult.resultId, workflow: "adjudication" as const }
|
||||
: null
|
||||
), [e46Result, e46aResult, l34Result, l34dResult, l34eResult, selectedWorkId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!available) {
|
||||
|
||||
@@ -10,6 +10,7 @@ export type LaboratoryProfileId =
|
||||
| "rig-camera-local-surface-v1"
|
||||
| "rig-track-geometry-temporal-v1"
|
||||
| "rig-ravnoves-perception-gate-v1"
|
||||
| "rig-goose-vegetation-benchmark-v1"
|
||||
| "rig-pointpillars-transfer-v1"
|
||||
| "rig-right-yolox-lidar-range-v1"
|
||||
| "rig-nvidia-ready-stack-v1"
|
||||
@@ -63,12 +64,19 @@ interface KnownWorkDefinition {
|
||||
const rig = (rigLabel: string): string => rigLabel.trim() || "Сенсорный риг";
|
||||
|
||||
const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`>, KnownWorkDefinition>> = {
|
||||
"lab-v1-vegetation-benchmark": {
|
||||
profileId: "rig-goose-vegetation-benchmark-v1",
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} · GOOSE vegetation archive`,
|
||||
experimentId: "lab-v1-vegetation-benchmark-archive",
|
||||
experimentName: "DDRNet vs PPLiteSeg · truth-backed archival comparison",
|
||||
variantName: "M4.8 · GOOSE truth · архивный анализ моделей",
|
||||
},
|
||||
"lab-v1-vegetation-shadow": {
|
||||
profileId: "rig-ravnoves-perception-gate-v1",
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} · GOOSE vegetation qualification`,
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} · RAVNOVES00 rover perception gate`,
|
||||
experimentId: "lab-v1-vegetation-mission-policy",
|
||||
experimentName: "DDRNet vs PPLiteSeg · truth-backed vegetation hard cases",
|
||||
variantName: "LAB V1 · готовые vegetation weights · GOOSE truth",
|
||||
experimentName: "RAVNOVES00 · city + vegetation + TGS review",
|
||||
variantName: "LAB V1 · EoMT + DDRNet + YOLOX + TGS · commands OFF",
|
||||
},
|
||||
"m48-object-centric-quality": {
|
||||
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||
|
||||
@@ -18,6 +18,7 @@ function mergeResults(
|
||||
next: AdvancedLaboratoryResults,
|
||||
): AdvancedLaboratoryResults {
|
||||
return {
|
||||
vegetationBenchmark: next.vegetationBenchmark ?? current.vegetationBenchmark,
|
||||
vegetationShadow: next.vegetationShadow ?? current.vegetationShadow,
|
||||
m47Graph: next.m47Graph ?? current.m47Graph,
|
||||
m48: next.m48 ?? current.m48,
|
||||
@@ -122,6 +123,7 @@ export function useAdvancedLaboratoryCatalog({
|
||||
const indexedResultId = index.find((item) => item.workId === selectedWorkId)?.resultId;
|
||||
if (
|
||||
[
|
||||
"lab-v1-vegetation-benchmark",
|
||||
"lab-v1-vegetation-shadow",
|
||||
"m47-reference-graph-shadow",
|
||||
"m48-object-centric-quality",
|
||||
|
||||
@@ -108,8 +108,13 @@ test("M4.9T5 viewer prefers autonomous chunks and keeps a sealed legacy fallback
|
||||
assert.doesNotMatch(source, /centersXyM\.map\(/);
|
||||
assert.match(source, /fetchE47SemanticSlamResult/);
|
||||
assert.match(source, /next\.baseM4ResultId !== result\.source\.linkedVisualResultId/);
|
||||
assert.match(source, /semantic=\{semanticOverride \?\? \(semantic \? \{/);
|
||||
assert.match(source, /semanticLayers=\{semanticLayers\}/);
|
||||
assert.match(source, /ГОРОД · EoMT/);
|
||||
assert.match(source, /ПРИРОДА · DDRNet/);
|
||||
assert.match(source, /semanticOverride/);
|
||||
assert.doesNotMatch(source, /if \(semanticOverride\) return/);
|
||||
assert.match(visual, /label="Источник семантики"/);
|
||||
assert.match(visual, /availableSemanticLayers\.length > 1/);
|
||||
assert.doesNotMatch(visual, /classifiedSpatialLayer \|\| !showReferenceMediaLayers \? \[\]/);
|
||||
assert.match(
|
||||
visual,
|
||||
|
||||
@@ -5,7 +5,9 @@ import { after, before, test } from "node:test";
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let fetchVegetationBenchmarkResult;
|
||||
let fetchVegetationShadowResult;
|
||||
let vegetationFullRouteMaskUrl;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
@@ -13,7 +15,11 @@ before(async () => {
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({ fetchVegetationShadowResult } = await server.ssrLoadModule(
|
||||
({
|
||||
fetchVegetationBenchmarkResult,
|
||||
fetchVegetationShadowResult,
|
||||
vegetationFullRouteMaskUrl,
|
||||
} = await server.ssrLoadModule(
|
||||
"/src/core/laboratory/vegetationShadow.ts",
|
||||
));
|
||||
});
|
||||
@@ -23,6 +29,7 @@ after(async () => {
|
||||
});
|
||||
|
||||
const resultId = `lab-v1-vegetation-shadow-${"a".repeat(64)}`;
|
||||
const benchmarkResultId = `lab-v1-vegetation-benchmark-${"d".repeat(64)}`;
|
||||
|
||||
function candidate(candidateKey, vegetationIou) {
|
||||
return {
|
||||
@@ -111,21 +118,26 @@ function coarseRouteVideo() {
|
||||
linked_tgs_result_id: `m49-tgs-full-shadow-${"2".repeat(64)}`,
|
||||
taxonomy: {
|
||||
schema_version: "missioncore.lab-v1-terrain-policy-taxonomy/v1",
|
||||
classes: Array.from({ length: 9 }, (_, classId) => ({
|
||||
classes: Array.from({ length: 10 }, (_, classId) => ({
|
||||
class_id: classId,
|
||||
label: `policy-${classId}`,
|
||||
color_rgb: [classId, classId, classId],
|
||||
disposition: classId === 0 ? "ambiguous" : "prediction",
|
||||
material_class: classId === 0 ? null : "grass",
|
||||
evidence_state: classId === 0 ? "UNOBSERVED" : "SUPPORTED_GROUND",
|
||||
disposition: classId === 0 ? "ambiguous" : classId === 9 ? "undefined" : "prediction",
|
||||
material_class: classId === 0 || classId === 9 ? null : "grass",
|
||||
evidence_state: classId === 0 || classId === 9 ? "UNOBSERVED" : "SUPPORTED_GROUND",
|
||||
})),
|
||||
},
|
||||
aggregate_prediction_pixels: Array(9).fill(0),
|
||||
aggregate_prediction_pixels: Array(10).fill(0),
|
||||
mask_archive: {
|
||||
path: "video/coarse-material-policy-masks.zip",
|
||||
sha256: "8".repeat(64),
|
||||
byte_length: 2048,
|
||||
},
|
||||
valid_fov: {
|
||||
mask_path: "video/valid-fov-mask.png",
|
||||
mask_sha256: "7".repeat(64),
|
||||
outside_valid_fov_class_id: 9,
|
||||
},
|
||||
policy: {
|
||||
presets: {
|
||||
urban: { grass: "NO_GO" },
|
||||
@@ -141,6 +153,69 @@ function coarseRouteVideo() {
|
||||
};
|
||||
}
|
||||
|
||||
function fullRouteReview() {
|
||||
const layer = (kind) => ({
|
||||
name: kind === "city" ? "EoMT Cityscapes" : "ddrnet_39",
|
||||
result_id: kind === "city"
|
||||
? `result-${"2".repeat(64)}`
|
||||
: `lab-v1-ravnoves-video-ddrnet-${"3".repeat(64)}`,
|
||||
frame_count: 6830,
|
||||
taxonomy: {
|
||||
schema_version: kind === "city"
|
||||
? "missioncore.recorded-eomt-taxonomy/v1"
|
||||
: "missioncore.lab-v1-vegetation-taxonomy/v1",
|
||||
classes: Array.from({ length: kind === "city" ? 16 : 64 }, (_, classId) => ({
|
||||
class_id: classId,
|
||||
label: classId === 0 ? "undefined" : `${kind}-${classId}`,
|
||||
color_rgb: [classId, classId, classId],
|
||||
disposition: classId === 0 ? "undefined" : "prediction",
|
||||
})),
|
||||
},
|
||||
mask_archive: {
|
||||
path: kind === "city"
|
||||
? "video/eomt-semantic-masks.zip"
|
||||
: "video/ddrnet-semantic-masks.zip",
|
||||
sha256: "4".repeat(64),
|
||||
byte_length: 4096,
|
||||
},
|
||||
inference_fps: 9.5,
|
||||
latency_p95_ms: 101.2,
|
||||
peak_reserved_vram_bytes: 3_000_000_000,
|
||||
});
|
||||
return {
|
||||
source_id: "RAVNOVES004TREE",
|
||||
session_id: "20260828T130511Z_viewer_live",
|
||||
source_job_id: "recorded-camera-eb2783c5480d56bda07c8af0",
|
||||
source_job_input_sha256: "eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715",
|
||||
source_stream_sha256: "e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac",
|
||||
recorded_media_source_id: "recorded.camera.6a3945242828a038",
|
||||
recorded_media_generation_sha256: "b073ea1e7babf1c77a664e1a5b95e3702d0e05b0e34c1e85a7c67a6f8b392ded",
|
||||
frame_count: 6830,
|
||||
width: 800,
|
||||
height: 600,
|
||||
timeline_start_seconds: 39.215263458,
|
||||
timeline_end_seconds: 757.260263458,
|
||||
timeline: {
|
||||
path: "video/frame-source-times-ns.bin",
|
||||
sha256: "5".repeat(64),
|
||||
byte_length: 6830 * 8,
|
||||
encoding: "uint64-le-nanoseconds",
|
||||
frame_count: 6830,
|
||||
},
|
||||
ground_truth: false,
|
||||
decode_repair: {
|
||||
repaired_frame_count: 1,
|
||||
sequence: 6092,
|
||||
method: "duplicate-previous-decoded-frame",
|
||||
proofs: {
|
||||
eomt: { path: "proofs/decode_repair.json", sha256: "7".repeat(64) },
|
||||
ddrnet: { path: "proofs/ddrnet_decode_repair.json", sha256: "8".repeat(64) },
|
||||
},
|
||||
},
|
||||
layers: { city: layer("city"), vegetation: layer("vegetation") },
|
||||
};
|
||||
}
|
||||
|
||||
function labPayload(route = routeVideo()) {
|
||||
return {
|
||||
schema_version: "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
@@ -220,23 +295,100 @@ test("vegetation LAB parses coarse material policy and sealed TGS binding", asyn
|
||||
});
|
||||
assert.equal(result.routeVideo.viewKind, "coarse-material-policy-review");
|
||||
assert.match(result.routeVideo.linkedTgsResultId, /^m49-tgs-full-shadow-/);
|
||||
assert.equal(result.routeVideo.taxonomy.length, 9);
|
||||
assert.equal(result.routeVideo.taxonomy.length, 10);
|
||||
assert.equal(result.routeVideo.taxonomy[0].evidenceState, "UNOBSERVED");
|
||||
assert.equal(result.routeVideo.policyPresets.urban.grass, "NO_GO");
|
||||
assert.equal(result.routeVideo.fusionMode, "synchronised-multilayer-review");
|
||||
});
|
||||
|
||||
test("vegetation LAB reuses the admitted M4.8 and M4.7 instruments", async () => {
|
||||
const resultSource = await readFile(
|
||||
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
|
||||
"utf8",
|
||||
test("vegetation LAB parses the full 004 pass inside the existing result contract", async () => {
|
||||
const payload = {
|
||||
...labPayload(null),
|
||||
catalogs: { goose: [], ravnoves: [] },
|
||||
route_full_review: fullRouteReview(),
|
||||
};
|
||||
const timeline = new ArrayBuffer(6830 * 8);
|
||||
const timelineView = new DataView(timeline);
|
||||
for (let index = 0; index < 6830; index += 1) {
|
||||
timelineView.setBigUint64(
|
||||
index * 8,
|
||||
BigInt(39_215_263_458 + index * 100_000_000),
|
||||
true,
|
||||
);
|
||||
}
|
||||
const result = await fetchVegetationShadowResult(resultId, {
|
||||
fetcher: async (url) => String(url).endsWith("/route-timeline")
|
||||
? new Response(timeline, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
ETag: `"${"5".repeat(64)}"`,
|
||||
},
|
||||
})
|
||||
: new Response(JSON.stringify(payload), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
});
|
||||
assert.equal(result.routeVideo, null);
|
||||
assert.equal(result.routeFullReview.frameCount, 6830);
|
||||
assert.equal(result.routeFullReview.city.taxonomy.length, 16);
|
||||
assert.equal(result.routeFullReview.vegetation.taxonomy.length, 64);
|
||||
assert.equal(result.routeFullReview.decodeRepair.sequence, 6092);
|
||||
assert.equal(result.routeFullReview.frameSourceTimesNs.length, 6830);
|
||||
assert.equal(
|
||||
vegetationFullRouteMaskUrl(resultId, "vegetation", 6829),
|
||||
`/api/v1/laboratory/vegetation-shadow/${resultId}/route-masks/vegetation/6829`,
|
||||
);
|
||||
assert.match(resultSource, /M48MaskComparisonVisual/);
|
||||
});
|
||||
|
||||
test("vegetation GOOSE benchmark opens through its separate archival endpoint", async () => {
|
||||
let requestedUrl = "";
|
||||
const result = await fetchVegetationBenchmarkResult(benchmarkResultId, {
|
||||
fetcher: async (url) => {
|
||||
requestedUrl = String(url);
|
||||
return new Response(JSON.stringify({
|
||||
...labPayload(null),
|
||||
result_id: benchmarkResultId,
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
requestedUrl,
|
||||
`/api/v1/laboratory/vegetation-benchmark/${benchmarkResultId}`,
|
||||
);
|
||||
assert.equal(result.routeVideo, null);
|
||||
assert.equal(result.validationCases.length, 12);
|
||||
});
|
||||
|
||||
test("vegetation realtime LAB and archival benchmark use separate admitted instruments", async () => {
|
||||
const [resultSource, benchmarkSource] = await Promise.all([
|
||||
readFile(
|
||||
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
|
||||
"utf8",
|
||||
),
|
||||
readFile(
|
||||
new URL("../src/workspaces/laboratory/VegetationBenchmarkResult.tsx", import.meta.url),
|
||||
"utf8",
|
||||
),
|
||||
]);
|
||||
assert.doesNotMatch(resultSource, /M48MaskComparisonVisual/);
|
||||
assert.match(resultSource, /M4ReplayThreatVisual/);
|
||||
assert.match(resultSource, /M49TgsFullShadowEvidence/);
|
||||
assert.match(resultSource, /semanticOverride/);
|
||||
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 2);
|
||||
assert.match(resultSource, /EoMT CITY \/ DDRNet VEGETATION/);
|
||||
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 4);
|
||||
assert.match(resultSource, /RAVNOVES004TREE mixed route review/);
|
||||
assert.match(resultSource, /RAVNOVES004TREE full recorded review/);
|
||||
assert.match(resultSource, /LaboratoryRecordedClipPlayer/);
|
||||
assert.match(resultSource, /className="m48-clip-player__overlay"/);
|
||||
assert.match(resultSource, /linkedTgsResultId/);
|
||||
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
|
||||
assert.doesNotMatch(benchmarkSource, /M49TgsFullShadowEvidence/);
|
||||
assert.equal(benchmarkSource.match(/<LaboratoryEvidence\b/g)?.length, 1);
|
||||
assert.doesNotMatch(resultSource, /VegetationRouteVisual|urban\/rural\/off-road presets/);
|
||||
await assert.rejects(
|
||||
access(new URL("../src/workspaces/laboratory/VegetationShadowVisual.tsx", import.meta.url)),
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "lab-v1-vegetation-benchmark",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "lab-v1-vegetation-benchmark/results",
|
||||
"result_id_prefix": "lab-v1-vegetation-benchmark",
|
||||
"document_name": "result.json",
|
||||
"schema_version": "missioncore.lab-v1-vegetation-shadow/v1"
|
||||
}
|
||||
}
|
||||
@@ -212,6 +212,7 @@
|
||||
}
|
||||
],
|
||||
"legacy_work_ids": [
|
||||
"lab-v1-vegetation-benchmark",
|
||||
"m48r3-static-occupancy-shadow",
|
||||
"m47-reference-graph-shadow",
|
||||
"e31-source-binding",
|
||||
|
||||
@@ -282,10 +282,17 @@
|
||||
"lifecycle": "current",
|
||||
"visual_evidence": "available"
|
||||
},
|
||||
{
|
||||
"catalog_id": "lab-v1-vegetation-benchmark",
|
||||
"evidence_id": "lab-v1-vegetation-benchmark-a8944d6c2d1102d81da78bcb4963760c9288db0421d9f2686afcbdd14b610d3d",
|
||||
"signal": "progress",
|
||||
"lifecycle": "current",
|
||||
"visual_evidence": "available"
|
||||
},
|
||||
{
|
||||
"catalog_id": "lab-v1-vegetation-shadow",
|
||||
"evidence_id": "lab-v1-vegetation-shadow-ad4d9fbbb21ff8a270b77f559b4e78dcdaf0455afd61afb5033009623984e554",
|
||||
"signal": "failed",
|
||||
"evidence_id": "lab-v1-vegetation-shadow-d179462134967ace1c5ebd6fbdbdd8659905d390484b9c01ea7930f083bb74d1",
|
||||
"signal": "progress",
|
||||
"lifecycle": "current",
|
||||
"visual_evidence": "available"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"schema_version": "missioncore.lab-v1-ravnoves-source/v1",
|
||||
"profile_id": "ravnoves004tree-full-video-source/v1",
|
||||
"source": {
|
||||
"source_id": "RAVNOVES004TREE/right-e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac",
|
||||
"source_sha256": "e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac",
|
||||
"source_job_id": "recorded-camera-eb2783c5480d56bda07c8af0",
|
||||
"source_job_input_sha256": "eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715",
|
||||
"session_id": "20260828T130511Z_viewer_live",
|
||||
"base_m4_result_id": null,
|
||||
"expected_width": 800,
|
||||
"expected_height": 600,
|
||||
"expected_frame_count": 6830,
|
||||
"timeline_start_seconds": 39.215263458,
|
||||
"timeline_end_seconds": 757.260263458,
|
||||
"frame_indices": [],
|
||||
"crop_contract": "center-600-square-to-512; outside-crop-is-undefined"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"schema_version": "missioncore.mixed-route-tgs-review-profile/v1",
|
||||
"profile_id": "ravnoves004tree-mixed-route-tgs-review/v1",
|
||||
"source": {
|
||||
"source_id": "RAVNOVES004TREE",
|
||||
"session_id": "20260828T130511Z_viewer_live",
|
||||
"review_pack_id": "mixed-route-review-pack-a8d245eb08a9581a994c4ae5ad242fec20f02c7c512c5ca5d3a6dd9464012753",
|
||||
"source_pack_id": "mixed-route-lidar-pack-e3fe195588cc4a2ec17e15af6f46582ed71c9bed643943779c4ed5e565a3c839",
|
||||
"source_pack_sha256": "10c759463da7711fbbe67e70df931597d85ab21325f7f8026e2c945b677e1bc6",
|
||||
"input_coordinate_frame": "map-gravity-local-translation-only"
|
||||
},
|
||||
"tgs": {
|
||||
"max_range_m": 80.0,
|
||||
"min_range_m": 1.0,
|
||||
"resolution_m": 8.0,
|
||||
"num_iterations": 3,
|
||||
"num_lowest_representative_points": 5,
|
||||
"minimum_points": 10,
|
||||
"seed_threshold_m": 0.5,
|
||||
"distance_threshold_m": 0.125,
|
||||
"outlier_threshold_m": 0.3,
|
||||
"normal_threshold": 0.94,
|
||||
"weight_threshold": 200.0,
|
||||
"lcc_normal_similarity": 0.03,
|
||||
"lcc_planar_distance_m": 0.1,
|
||||
"obstacle_height_m": 1.0,
|
||||
"refine_mode": true
|
||||
},
|
||||
"profiles": {
|
||||
"current_increment": {
|
||||
"role": "diagnostic-current-evidence"
|
||||
},
|
||||
"causal_rolling_1s": {
|
||||
"role": "primary-local-evidence",
|
||||
"history_seconds": 1.0,
|
||||
"local_radius_m": 12.0
|
||||
}
|
||||
},
|
||||
"costmap": {
|
||||
"coordinate_frame": "map-gravity-local",
|
||||
"cell_size_m": 0.45,
|
||||
"radius_m": 12.0,
|
||||
"state_priority": [
|
||||
"NONGROUND_OCCUPIED",
|
||||
"UNKNOWN_REJECTED",
|
||||
"GROUND_SUPPORT",
|
||||
"UNOBSERVED"
|
||||
]
|
||||
},
|
||||
"state_codes": {
|
||||
"UNOBSERVED": 0,
|
||||
"GROUND_SUPPORT": 1,
|
||||
"NONGROUND_OCCUPIED": 2,
|
||||
"UNKNOWN_REJECTED": 3
|
||||
},
|
||||
"invariants": {
|
||||
"all_eligible_input_points_accounted": true,
|
||||
"aos_allowed": false,
|
||||
"lidar_orientation_applied_to_tgs_input": false,
|
||||
"map_gravity_axis_preserved": true,
|
||||
"missing_support_means_free": false,
|
||||
"unobserved_cells_are_emitted": true,
|
||||
"camera_projection_is_authoritative": false,
|
||||
"future_frames_used": false,
|
||||
"gpu_allowed": false,
|
||||
"navigation_or_actuation_allowed": false
|
||||
}
|
||||
}
|
||||
@@ -58,12 +58,10 @@ Mission Core backend
|
||||
DC Gaussian Pipeline
|
||||
├─ TUS bundle or archive admission
|
||||
├─ secure ZIP/RAR/7z normalization
|
||||
└─ native Vulkan SplatTransform visual build on Worker 006
|
||||
|
||||
Optional physical-mesh pipeline (separate job; experimental)
|
||||
├─ source-mesh discovery or explicit mesh generation
|
||||
├─ physics-oriented cleanup and geometry budget
|
||||
└─ compressed, digest-bound publication
|
||||
├─ native Vulkan SplatTransform visual build on Worker 006
|
||||
└─ optional Mesh_Files/*.ply source-collision path
|
||||
├─ conservative small-hole repair and topology audit
|
||||
└─ mandatory Draco, digest-bound publication
|
||||
```
|
||||
|
||||
The browser never receives the Worker token. Mission Core does not embed archive-format behavior
|
||||
@@ -81,18 +79,18 @@ SplatTransform GPU command through a confined filesystem spool to the pinned nat
|
||||
on the RTX 4090. It does not install into or share the Python, CUDA, Triton or computer-vision
|
||||
environments on the host.
|
||||
|
||||
Project processing is split at a durable product boundary. The mandatory first job builds only the
|
||||
preview and streamed Gaussian assets required for visual inspection. It never generates collision
|
||||
geometry, so a location can reach `ready` without paying the time, GPU-memory and storage cost of a
|
||||
physical mesh.
|
||||
Project processing never generates collision geometry from Gaussian data by default. When the
|
||||
normalized source contains exactly one PLY below `Mesh_Files`, the same queued build automatically
|
||||
selects the provider's `source` collision profile. The original indexed mesh is retained; only
|
||||
strictly admitted small internal boundary loops on approximately planar Z-up surfaces receive new
|
||||
triangles. Vertices and existing faces are never moved, welded, smoothed, simplified or remeshed.
|
||||
The derived GLB then passes the mandatory Draco publication gate and carries a separate repair
|
||||
report. A source with no admitted PLY remains visual-only and still reaches `ready` normally.
|
||||
|
||||
Physical geometry is an optional second job started only after the visual world is ready. Its first
|
||||
candidate source is a mesh already present in the uploaded export (for example a PLY in
|
||||
`Mesh_Files`); generation from the Gaussian cloud is a fallback experiment, not the default path.
|
||||
The second-stage contract, cleanup method and acceptance gates are intentionally separate from the
|
||||
visual build. When that stage publishes a GLB, simplification still controls decoded physics cost
|
||||
and Draco controls transfer/storage bytes; compression is not treated as a replacement for a
|
||||
physics mesh budget.
|
||||
The conservative repair rejects outer borders, branched boundaries, large or non-planar loops,
|
||||
vertical openings, mixed orientation and failed/self-intersecting triangulations. Ambiguous holes
|
||||
remain open and are counted in the report instead of being silently capped. Explicit mesh
|
||||
generation from Gaussian data remains a later experiment, not a fallback in this ingestion path.
|
||||
|
||||
Visual and collision layers retain independent X/Y/Z correction settings, while PlayCanvas world,
|
||||
camera, navigation and future physics stay in the canonical Y-up coordinate system. Quality, both
|
||||
@@ -115,9 +113,10 @@ Primary implementation references:
|
||||
- encrypted, linked, traversing, duplicate and over-limit archive entries fail closed;
|
||||
- project status is durable and reflects provider state without fabricated percentages;
|
||||
- ready artifacts are imported digest-bound and served from Mission Core same-origin URLs;
|
||||
- the mandatory build requests preview and streamed Gaussian outputs with collision disabled;
|
||||
- a visual project reaches ready state without a collision artifact;
|
||||
- physical mesh preparation is a separate explicit job and never blocks visual inspection;
|
||||
- the build always requests preview and streamed Gaussian outputs;
|
||||
- exactly one `Mesh_Files/*.ply` automatically selects repaired source-mesh collision plus Draco;
|
||||
- archives without that mesh remain visual-only and reach ready without a collision artifact;
|
||||
- generated Gaussian/voxel collision is never used as an implicit fallback;
|
||||
- edit changes project metadata; delete removes both the Mission Core project and terminal provider
|
||||
job;
|
||||
- a ready project mounts direct PlayCanvas Engine and loads Streamed SOG with preview fallback;
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Publish LiDAR/pose evidence aligned to an immutable mixed-route review pack."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from fuse_e6_tracking_lidar import CameraAnchor, _lidar_samples
|
||||
|
||||
from k1link.compute.jobs import validate_camera_compute_job
|
||||
from k1link.device_plugins.xgrids_k1.analyze.calibrated_overlay import (
|
||||
_load_calibration_snapshot,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
|
||||
Kb4ProjectionProfile,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.mqtt.capture import read_capture_clock_origin
|
||||
from k1link.device_plugins.xgrids_k1.protocol.streams import decode_lio_pcl
|
||||
from k1link.device_plugins.xgrids_k1.viewer.replay import iter_replay_messages
|
||||
|
||||
SCHEMA = "missioncore.mixed-route-lidar-pack/v1"
|
||||
REVIEW_SCHEMA = "missioncore.mixed-route-review-pack/v1"
|
||||
MAXIMUM_LIDAR_CAMERA_DELTA_MS = 100.0
|
||||
MAXIMUM_POSE_POINT_DELTA_MS = 100.0
|
||||
CAUSAL_HISTORY_SECONDS = 1.0
|
||||
|
||||
|
||||
class MixedRouteLidarPackError(RuntimeError):
|
||||
"""The recorded route cannot satisfy the selected LiDAR evidence contract."""
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job", type=Path, required=True)
|
||||
parser.add_argument("--session", type=Path, required=True)
|
||||
parser.add_argument("--review-pack", type=Path, required=True)
|
||||
parser.add_argument("--calibration", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _read_review_pack(root: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest_path = resolved / "manifest.json"
|
||||
try:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise MixedRouteLidarPackError("mixed-route review manifest is invalid") from exc
|
||||
identity = manifest.get("identity") if isinstance(manifest, dict) else None
|
||||
timeline = manifest.get("timeline") if isinstance(manifest, dict) else None
|
||||
frames = manifest.get("frames") if isinstance(manifest, dict) else None
|
||||
if (
|
||||
manifest.get("schema_version") != REVIEW_SCHEMA
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("schema_version") != REVIEW_SCHEMA
|
||||
or identity.get("ground_truth") is not False
|
||||
or not isinstance(timeline, dict)
|
||||
or not isinstance(frames, list)
|
||||
or manifest.get("frame_count") != len(frames)
|
||||
or not frames
|
||||
):
|
||||
raise MixedRouteLidarPackError("mixed-route review contract changed")
|
||||
timeline_path = resolved / str(timeline.get("path"))
|
||||
if (
|
||||
not timeline_path.is_file()
|
||||
or timeline.get("sha256") != _sha256(timeline_path)
|
||||
or timeline.get("byte_length") != timeline_path.stat().st_size
|
||||
):
|
||||
raise MixedRouteLidarPackError("mixed-route review timeline changed")
|
||||
rows: list[dict[str, Any]] = []
|
||||
previous_seconds = -1.0
|
||||
with timeline_path.open(encoding="utf-8") as stream:
|
||||
for expected, line in enumerate(stream):
|
||||
try:
|
||||
row = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise MixedRouteLidarPackError("mixed-route timeline JSON is invalid") from exc
|
||||
seconds = row.get("session_seconds") if isinstance(row, dict) else None
|
||||
if (
|
||||
not isinstance(row, dict)
|
||||
or row.get("frame_index") != expected
|
||||
or row.get("sequence") != expected + 1
|
||||
or row.get("source_sequence") != row.get("source_frame_index") + 1
|
||||
or not isinstance(seconds, (int, float))
|
||||
or isinstance(seconds, bool)
|
||||
or float(seconds) <= previous_seconds
|
||||
):
|
||||
raise MixedRouteLidarPackError("mixed-route timeline row changed")
|
||||
rows.append(row)
|
||||
previous_seconds = float(seconds)
|
||||
if len(rows) != len(frames):
|
||||
raise MixedRouteLidarPackError("mixed-route timeline is incomplete")
|
||||
for frame in frames:
|
||||
path = resolved / str(frame.get("path"))
|
||||
if (
|
||||
not path.is_file()
|
||||
or frame.get("byte_length") != path.stat().st_size
|
||||
or frame.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise MixedRouteLidarPackError("mixed-route source frame changed")
|
||||
return manifest, rows
|
||||
|
||||
|
||||
def _causal_history_clouds(
|
||||
raw_path: Path,
|
||||
*,
|
||||
origin_monotonic_ns: int,
|
||||
sample_seconds: list[float],
|
||||
) -> list[np.ndarray]:
|
||||
grouped: list[list[np.ndarray]] = [[] for _ in sample_seconds]
|
||||
last = sample_seconds[-1]
|
||||
for message in iter_replay_messages(raw_path):
|
||||
monotonic_ns = message.received_monotonic_ns
|
||||
if not isinstance(monotonic_ns, int) or monotonic_ns < origin_monotonic_ns:
|
||||
raise MixedRouteLidarPackError("MQTT replay message has no compatible clock")
|
||||
seconds = (monotonic_ns - origin_monotonic_ns) / 1e9
|
||||
if seconds > last:
|
||||
break
|
||||
if not message.topic.endswith("/lio_pcl"):
|
||||
continue
|
||||
matching = [
|
||||
index
|
||||
for index, sample_time in enumerate(sample_seconds)
|
||||
if sample_time - CAUSAL_HISTORY_SECONDS <= seconds <= sample_time
|
||||
]
|
||||
if not matching:
|
||||
continue
|
||||
frame = decode_lio_pcl(message.payload)
|
||||
cloud = np.asarray(
|
||||
[point.scaled_xyz(frame.header.scaler) for point in frame.points],
|
||||
dtype=np.float32,
|
||||
).reshape((-1, 3))
|
||||
if cloud.shape[0] == 0 or not np.isfinite(cloud).all():
|
||||
raise MixedRouteLidarPackError("causal LiDAR history is empty or non-finite")
|
||||
for index in matching:
|
||||
grouped[index].append(cloud)
|
||||
result: list[np.ndarray] = []
|
||||
for clouds in grouped:
|
||||
if not clouds:
|
||||
raise MixedRouteLidarPackError("selected frame has no causal LiDAR history")
|
||||
result.append(np.concatenate(clouds))
|
||||
return result
|
||||
|
||||
|
||||
def prepare(
|
||||
*,
|
||||
job_root: Path,
|
||||
session_root: Path,
|
||||
review_pack_root: Path,
|
||||
calibration_root: Path,
|
||||
output_root: Path,
|
||||
) -> Path:
|
||||
job = validate_camera_compute_job(job_root)
|
||||
session = session_root.resolve(strict=True)
|
||||
if not session.is_dir() or session.name != job.session_id:
|
||||
raise MixedRouteLidarPackError("camera job and observation session differ")
|
||||
review, timeline = _read_review_pack(review_pack_root)
|
||||
review_identity = review["identity"]
|
||||
if (
|
||||
review_identity.get("job_id") != job.job_id
|
||||
or review_identity.get("input_sha256") != job.input_sha256
|
||||
or review_identity.get("session_id") != job.session_id
|
||||
or review_identity.get("source_id") != job.source_id
|
||||
or review_identity.get("codec_epoch") != job.codec_epoch
|
||||
):
|
||||
raise MixedRouteLidarPackError("review pack and camera job differ")
|
||||
|
||||
calibration, calibration_sha256 = _load_calibration_snapshot(
|
||||
calibration_root.resolve(strict=True)
|
||||
)
|
||||
projection = Kb4ProjectionProfile.from_factory_calibration(calibration, job.source_id)
|
||||
capture_root = session / "captures" / "mqtt_live"
|
||||
origin_path = capture_root / "mqtt.timeline.origin.json"
|
||||
origin = read_capture_clock_origin(origin_path)
|
||||
anchors = [
|
||||
CameraAnchor(
|
||||
frame_index=int(row["frame_index"]),
|
||||
source_frame_index=int(row["source_frame_index"]),
|
||||
host_session_seconds=(
|
||||
int(row["host_monotonic_ns"]) - origin.started_monotonic_ns
|
||||
)
|
||||
/ 1e9,
|
||||
video_session_seconds=float(row["session_seconds"]),
|
||||
)
|
||||
for row in timeline
|
||||
]
|
||||
if any(
|
||||
anchor.host_session_seconds != anchor.video_session_seconds
|
||||
for anchor in anchors
|
||||
):
|
||||
raise MixedRouteLidarPackError("review timeline does not use host arrival time")
|
||||
samples = list(
|
||||
_lidar_samples(
|
||||
capture_root / "mqtt.raw.k1mqtt",
|
||||
anchors,
|
||||
origin_monotonic_ns=origin.started_monotonic_ns,
|
||||
maximum_lidar_camera_delta_s=MAXIMUM_LIDAR_CAMERA_DELTA_MS / 1000.0,
|
||||
maximum_pose_point_delta_s=MAXIMUM_POSE_POINT_DELTA_MS / 1000.0,
|
||||
)
|
||||
)
|
||||
if len(samples) != len(anchors):
|
||||
raise MixedRouteLidarPackError("LiDAR sampler did not account for every anchor")
|
||||
|
||||
count = len(anchors)
|
||||
available = np.zeros((count,), dtype=np.bool_)
|
||||
offsets = [0]
|
||||
clouds: list[np.ndarray] = []
|
||||
positions = np.full((count, 3), np.nan, dtype=np.float64)
|
||||
quaternions = np.full((count, 4), np.nan, dtype=np.float64)
|
||||
lidar_delta = np.full((count,), np.nan, dtype=np.float64)
|
||||
pose_delta = np.full((count,), np.nan, dtype=np.float64)
|
||||
sample_seconds: list[float] = []
|
||||
for index, (anchor, sample) in enumerate(zip(anchors, samples, strict=True)):
|
||||
if sample is None:
|
||||
offsets.append(offsets[-1])
|
||||
sample_seconds.append(float("nan"))
|
||||
continue
|
||||
cloud = np.asarray(
|
||||
[
|
||||
point.scaled_xyz(sample.point_frame.header.scaler)
|
||||
for point in sample.point_frame.points
|
||||
],
|
||||
dtype=np.float32,
|
||||
).reshape((-1, 3))
|
||||
if cloud.shape[0] == 0 or not np.isfinite(cloud).all():
|
||||
raise MixedRouteLidarPackError("selected LiDAR sample is empty or non-finite")
|
||||
available[index] = True
|
||||
clouds.append(cloud)
|
||||
offsets.append(offsets[-1] + cloud.shape[0])
|
||||
positions[index] = sample.pose_frame.position_xyz
|
||||
quaternions[index] = sample.pose_frame.orientation_xyzw
|
||||
lidar_delta[index] = (
|
||||
sample.point_session_seconds - anchor.host_session_seconds
|
||||
) * 1000.0
|
||||
pose_delta[index] = (
|
||||
sample.pose_session_seconds - sample.point_session_seconds
|
||||
) * 1000.0
|
||||
sample_seconds.append(sample.point_session_seconds)
|
||||
|
||||
if not available.all() or not np.isfinite(np.asarray(sample_seconds)).all():
|
||||
raise MixedRouteLidarPackError(
|
||||
"every mixed-route review island must have a temporally admissible LiDAR sample"
|
||||
)
|
||||
history_clouds = _causal_history_clouds(
|
||||
capture_root / "mqtt.raw.k1mqtt",
|
||||
origin_monotonic_ns=origin.started_monotonic_ns,
|
||||
sample_seconds=sample_seconds,
|
||||
)
|
||||
history_offsets = [0]
|
||||
for cloud in history_clouds:
|
||||
history_offsets.append(history_offsets[-1] + cloud.shape[0])
|
||||
|
||||
identity = {
|
||||
"schema_version": SCHEMA,
|
||||
"job_id": job.job_id,
|
||||
"input_sha256": job.input_sha256,
|
||||
"session_id": job.session_id,
|
||||
"source_id": job.source_id,
|
||||
"camera_slot": "camera_1",
|
||||
"calibration_sha256": calibration_sha256,
|
||||
"review_pack_id": review["pack_id"],
|
||||
"review_pack_identity_sha256": review["identity_sha256"],
|
||||
"selected_source_frame_indices": [
|
||||
int(row["source_frame_index"]) for row in timeline
|
||||
],
|
||||
"frame_count": count,
|
||||
"available_lidar_frames": int(available.sum()),
|
||||
"point_count": int(offsets[-1]),
|
||||
"causal_history_seconds": CAUSAL_HISTORY_SECONDS,
|
||||
"causal_history_point_count": int(history_offsets[-1]),
|
||||
"temporal_policy": {
|
||||
"binding": "nearest-host-arrival-best-effort",
|
||||
"maximum_lidar_camera_delta_ms": MAXIMUM_LIDAR_CAMERA_DELTA_MS,
|
||||
"maximum_pose_point_delta_ms": MAXIMUM_POSE_POINT_DELTA_MS,
|
||||
"clock_source": "recorded-host-monotonic-arrival",
|
||||
},
|
||||
"projection": {
|
||||
"model": "kb4",
|
||||
"width": projection.width,
|
||||
"height": projection.height,
|
||||
"source_coordinates": "k1-map",
|
||||
"target_camera": job.source_id,
|
||||
},
|
||||
"ground_truth": False,
|
||||
"authority": {
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_allowed": False,
|
||||
},
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
pack_id = f"mixed-route-lidar-pack-{identity_sha256}"
|
||||
parent = output_root.resolve()
|
||||
parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
final = parent / pack_id
|
||||
if final.exists():
|
||||
return final
|
||||
staging = Path(tempfile.mkdtemp(prefix=f".{pack_id}.", dir=parent))
|
||||
published = False
|
||||
try:
|
||||
arrays_path = staging / "lidar-pack.npz"
|
||||
np.savez_compressed(
|
||||
arrays_path,
|
||||
frame_indices=np.arange(count, dtype=np.int64),
|
||||
source_frame_indices=np.asarray(
|
||||
[row["source_frame_index"] for row in timeline], dtype=np.int64
|
||||
),
|
||||
session_seconds=np.asarray(
|
||||
[anchor.video_session_seconds for anchor in anchors], dtype=np.float64
|
||||
),
|
||||
host_session_seconds=np.asarray(
|
||||
[anchor.host_session_seconds for anchor in anchors], dtype=np.float64
|
||||
),
|
||||
lidar_session_seconds=np.asarray(sample_seconds, dtype=np.float64),
|
||||
sample_available=available,
|
||||
cloud_offsets=np.asarray(offsets, dtype=np.int64),
|
||||
cloud_points_map=(
|
||||
np.concatenate(clouds) if clouds else np.empty((0, 3), dtype=np.float32)
|
||||
),
|
||||
pose_positions_map=positions,
|
||||
pose_quaternions_map_from_lidar=quaternions,
|
||||
lidar_camera_delta_ms=lidar_delta,
|
||||
pose_point_delta_ms=pose_delta,
|
||||
causal_history_seconds=np.asarray(
|
||||
[CAUSAL_HISTORY_SECONDS], dtype=np.float64
|
||||
),
|
||||
causal_history_offsets=np.asarray(history_offsets, dtype=np.int64),
|
||||
causal_history_points_map=np.concatenate(history_clouds),
|
||||
intrinsic_fx_fy_cx_cy=np.asarray(
|
||||
projection.intrinsic_fx_fy_cx_cy, dtype=np.float64
|
||||
),
|
||||
distortion_kb4=np.asarray(projection.distortion_kb4, dtype=np.float64),
|
||||
t_camera_from_lidar=np.asarray(projection.t_camera_from_lidar, dtype=np.float64),
|
||||
)
|
||||
manifest = {
|
||||
"schema_version": SCHEMA,
|
||||
"pack_id": pack_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": datetime.now(UTC)
|
||||
.isoformat(timespec="milliseconds")
|
||||
.replace("+00:00", "Z"),
|
||||
"classification": "private-recorded-sensor-review-input",
|
||||
"ground_truth": False,
|
||||
"artifact": {
|
||||
"path": arrays_path.name,
|
||||
"media_type": "application/x-npz",
|
||||
"byte_length": arrays_path.stat().st_size,
|
||||
"sha256": _sha256(arrays_path),
|
||||
},
|
||||
}
|
||||
(staging / "manifest.json").write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
os.replace(staging, final)
|
||||
published = True
|
||||
finally:
|
||||
if not published:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
return final
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _arguments()
|
||||
output = prepare(
|
||||
job_root=args.job,
|
||||
session_root=args.session,
|
||||
review_pack_root=args.review_pack,
|
||||
calibration_root=args.calibration,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
manifest = json.loads((output / "manifest.json").read_text(encoding="utf-8"))
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"pack_id": manifest["pack_id"],
|
||||
"output": str(output),
|
||||
"frames": manifest["identity"]["frame_count"],
|
||||
"lidar_frames": manifest["identity"]["available_lidar_frames"],
|
||||
"points": manifest["identity"]["point_count"],
|
||||
"artifact_sha256": manifest["artifact"]["sha256"],
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -187,12 +187,15 @@ Write-Output "PHASE=e4-preflight-complete"
|
||||
$runToken = [Guid]::NewGuid().ToString("N")
|
||||
$workRoot = Join-Path $tmpRoot ("{0}-e4-{1}" -f $job.job_id, $runToken)
|
||||
$framesRoot = Join-Path $workRoot "frames"
|
||||
$decodedFramesRoot = Join-Path $workRoot "decoded-by-pts"
|
||||
$streamPath = Join-Path $workRoot "camera.mp4"
|
||||
$ptsPath = Join-Path $workRoot "pts.json"
|
||||
$packetsPath = Join-Path $workRoot "packets.csv"
|
||||
$decodeRepairPath = Join-Path $workRoot "decode-repair.json"
|
||||
$timelinePath = Join-Path $workRoot "timeline.jsonl"
|
||||
$publishRoot = Join-Path $derivedRoot (".{0}-e4-{1}.publish" -f $job.job_id, $runToken)
|
||||
$stagingRoot = Join-Path $publishRoot "output"
|
||||
$null = New-Item -ItemType Directory -Path $framesRoot
|
||||
$null = New-Item -ItemType Directory -Path $decodedFramesRoot
|
||||
$null = New-Item -ItemType Directory -Path $publishRoot
|
||||
$totalWatch = [Diagnostics.Stopwatch]::StartNew()
|
||||
$completed = $false
|
||||
@@ -230,29 +233,69 @@ try {
|
||||
|
||||
$extractWatch = [Diagnostics.Stopwatch]::StartNew()
|
||||
Write-Output "PHASE=e4-frame-extraction-start"
|
||||
& ffmpeg -hide_banner -loglevel fatal -i $streamPath -map 0:v:0 -fps_mode passthrough -frames:v $activeFrameCount (Join-Path $framesRoot "frame-%06d.png")
|
||||
& ffprobe -v error -select_streams v:0 -show_packets -show_entries packet=pts,flags -of csv=p=0 -o $packetsPath $streamPath
|
||||
Assert-LastExitCode "LAB E4 packet timestamp probe"
|
||||
$packetRows = @(Get-Content -LiteralPath $packetsPath | Select-Object -First $activeFrameCount)
|
||||
if ($packetRows.Count -ne $activeFrameCount) {
|
||||
throw "LAB E4 packet count differs from the requested camera epoch"
|
||||
}
|
||||
|
||||
& ffmpeg -hide_banner -loglevel error `
|
||||
-hwaccel cuda -hwaccel_output_format cuda -c:v h264_cuvid `
|
||||
-err_detect ignore_err -flags +output_corrupt -copyts `
|
||||
-i $streamPath -map 0:v:0 -vf "hwdownload,format=nv12" `
|
||||
-fps_mode passthrough -enc_time_base demux -frames:v $activeFrameCount `
|
||||
-frame_pts 1 (Join-Path $decodedFramesRoot "frame-%d.png")
|
||||
Assert-LastExitCode "LAB E4 camera extraction"
|
||||
& ffprobe -v error -select_streams v:0 -show_entries frame=best_effort_timestamp_time -of json $streamPath | Set-Content -LiteralPath $ptsPath -Encoding utf8
|
||||
Assert-LastExitCode "LAB E4 camera timestamp probe"
|
||||
|
||||
$decodedCount = @(Get-ChildItem -LiteralPath $decodedFramesRoot -File -Filter "frame-*.png").Count
|
||||
$repairs = @()
|
||||
$packetPts = @()
|
||||
for ($index = 0; $index -lt $activeFrameCount; $index++) {
|
||||
$columns = ([string]$packetRows[$index]).Split(",")
|
||||
if ($columns.Count -lt 2) {
|
||||
throw "LAB E4 packet timestamp row is malformed"
|
||||
}
|
||||
$pts = [int64]::Parse($columns[0].Trim(), [Globalization.CultureInfo]::InvariantCulture)
|
||||
$packetPts += $pts
|
||||
$decodedPath = Join-Path $decodedFramesRoot ("frame-{0}.png" -f $pts)
|
||||
$canonicalPath = Join-Path $framesRoot ("frame-{0:D6}.png" -f ($index + 1))
|
||||
if (Test-Path -LiteralPath $decodedPath -PathType Leaf) {
|
||||
Move-Item -LiteralPath $decodedPath -Destination $canonicalPath
|
||||
continue
|
||||
}
|
||||
if ($index -eq 0 -or $repairs.Count -ge 1) {
|
||||
throw "LAB E4 source contains more than one recoverable decoder gap"
|
||||
}
|
||||
$previousPath = Join-Path $framesRoot ("frame-{0:D6}.png" -f $index)
|
||||
Copy-Item -LiteralPath $previousPath -Destination $canonicalPath
|
||||
$repairs += [ordered]@{
|
||||
sequence = $index + 1
|
||||
packet_pts = $pts
|
||||
method = "duplicate-previous-decoded-frame"
|
||||
}
|
||||
}
|
||||
|
||||
$decodedFrames = @(Get-ChildItem -LiteralPath $framesRoot -File -Filter "frame-*.png")
|
||||
$ptsDocument = Get-Content -LiteralPath $ptsPath -Raw | ConvertFrom-Json
|
||||
$pts = @($ptsDocument.frames)
|
||||
if ($decodedFrames.Count -ne $activeFrameCount -or $pts.Count -lt $activeFrameCount) {
|
||||
if ($decodedFrames.Count -ne $activeFrameCount) {
|
||||
throw "Decoded LAB E4 frame count differs from the requested camera epoch"
|
||||
}
|
||||
$firstEpochSeconds = [double]::Parse(
|
||||
([string]$pts[0].best_effort_timestamp_time).Trim(),
|
||||
[Globalization.CultureInfo]::InvariantCulture
|
||||
)
|
||||
$decodeRepair = [ordered]@{
|
||||
schema_version = "missioncore.recorded-video-decode-repair/v1"
|
||||
decoder = "ffmpeg-h264_cuvid-output-corrupt"
|
||||
packets_requested = $activeFrameCount
|
||||
frames_decoded = $decodedCount
|
||||
repaired_frame_count = $repairs.Count
|
||||
repairs = $repairs
|
||||
}
|
||||
$decodeRepair | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $decodeRepairPath -Encoding utf8
|
||||
|
||||
$firstPacketPts = [int64]$packetPts[0]
|
||||
$previousEpochSeconds = -1.0
|
||||
$timelineWriter = [IO.StreamWriter]::new($timelinePath, $false, [Text.UTF8Encoding]::new($false))
|
||||
try {
|
||||
for ($index = 0; $index -lt $activeFrameCount; $index++) {
|
||||
$epochSeconds = [double]::Parse(
|
||||
([string]$pts[$index].best_effort_timestamp_time).Trim(),
|
||||
[Globalization.CultureInfo]::InvariantCulture
|
||||
) - $firstEpochSeconds
|
||||
$epochSeconds = ([int64]$packetPts[$index] - $firstPacketPts) / 90000.0
|
||||
if ($epochSeconds -le $previousEpochSeconds -or $epochSeconds -gt ($timelineDuration + 0.001)) {
|
||||
throw "Decoded LAB E4 timestamps are not strictly monotonic inside the camera timeline"
|
||||
}
|
||||
@@ -313,6 +356,7 @@ try {
|
||||
Write-Output ("PHASE=e4-inference-start FRAMES={0}" -f $activeFrameCount)
|
||||
& docker @runArgs
|
||||
Assert-LastExitCode "LAB E4 semantic inference"
|
||||
Copy-Item -LiteralPath $decodeRepairPath -Destination (Join-Path $stagingRoot "decode-repair.json")
|
||||
$freeBytesPostInference = Assert-FreeSpace "post-inference"
|
||||
Write-Output "PHASE=e4-inference-complete"
|
||||
|
||||
|
||||
@@ -12,7 +12,18 @@ param(
|
||||
|
||||
[string]$OutputRoot = "D:\NDC_MISSIONCORE\runtime\experiments\lab-v1-vegetation",
|
||||
|
||||
[string]$RavnovesVideo = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8.mp4"
|
||||
[string]$RavnovesVideo = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8.mp4",
|
||||
|
||||
[string]$RavnovesSourceId = "RAVNOVES00/right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
||||
|
||||
[string]$RavnovesSha256 = "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
||||
|
||||
[ValidateRange(1, 1000000)]
|
||||
[int]$RavnovesExpectedFrameCount = 4489,
|
||||
|
||||
[string]$RavnovesBaseM4ResultId = "m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324",
|
||||
|
||||
[string]$RavnovesSourceProfile = ""
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -38,6 +49,31 @@ $configRoot = Join-Path $ToolRoot "config"
|
||||
$benchmarkConfig = Join-Path $configRoot "lab-v1-goose-vegetation-benchmark-v1.json"
|
||||
$policyConfig = Join-Path $configRoot "lab-v1-vegetation-mission-policy-v1.json"
|
||||
$providerMapConfig = Join-Path $configRoot "lab-v1-vegetation-provider-label-map-v1.json"
|
||||
$ravnovesProfileDocument = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($RavnovesSourceProfile)) {
|
||||
$resolvedProfile = (Resolve-Path -LiteralPath $RavnovesSourceProfile).Path
|
||||
if (-not $resolvedProfile.StartsWith($ToolRoot, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "RAVNOVES source profile must stay under ToolRoot"
|
||||
}
|
||||
$ravnovesProfileDocument = Get-Content -LiteralPath $resolvedProfile -Raw | ConvertFrom-Json
|
||||
$source = $ravnovesProfileDocument.source
|
||||
if (
|
||||
$ravnovesProfileDocument.schema_version -ne "missioncore.lab-v1-ravnoves-source/v1" -or
|
||||
$null -eq $source -or
|
||||
[string]::IsNullOrWhiteSpace([string]$source.source_id) -or
|
||||
[string]$source.source_sha256 -notmatch "^[a-f0-9]{64}$" -or
|
||||
[int]$source.expected_width -ne 800 -or
|
||||
[int]$source.expected_height -ne 600 -or
|
||||
[int]$source.expected_frame_count -lt 1 -or
|
||||
[string]$source.crop_contract -ne "center-600-square-to-512; outside-crop-is-undefined"
|
||||
) {
|
||||
throw "RAVNOVES source profile is incompatible"
|
||||
}
|
||||
$RavnovesSourceId = [string]$source.source_id
|
||||
$RavnovesSha256 = [string]$source.source_sha256
|
||||
$RavnovesExpectedFrameCount = [int]$source.expected_frame_count
|
||||
$RavnovesBaseM4ResultId = [string]$source.base_m4_result_id
|
||||
}
|
||||
$datasetRoot = Join-Path $AssetRoot "goose-2d\validation"
|
||||
$checkpointRelative = if ($candidateKey -eq "ddrnet") {
|
||||
"models\goose\ddrnet_class_512.pth"
|
||||
@@ -51,7 +87,6 @@ $expectedCheckpointSha256 = if ($candidateKey -eq "ddrnet") {
|
||||
"6dd412c0c99115e359896c4cab43a8e6bce9e09b843e7fa885fe597b0a6121cd"
|
||||
}
|
||||
$expectedCheckpointBytes = if ($candidateKey -eq "ddrnet") { 259419077 } else { 98208249 }
|
||||
$ravnovesSha256 = "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8"
|
||||
$frameIndices = @(0, 253, 512, 768, 1024, 1536, 2048, 2560, 3072, 3584, 4096, 4488)
|
||||
$dockerConfig = "D:\NDC_MISSIONCORE\datasets\state\lab-v1-vegetation\docker-config"
|
||||
|
||||
@@ -110,6 +145,18 @@ function Invoke-IsolatedRun {
|
||||
[string]$FramesRoot = ""
|
||||
)
|
||||
$containerName = "ndc-lab-v1-goose-$candidateKey-$([Guid]::NewGuid().ToString('N').Substring(0, 10))"
|
||||
$activeConfigRoot = $configRoot
|
||||
if ($RunMode -eq "ravnoves-video" -and $null -ne $ravnovesProfileDocument) {
|
||||
$activeConfigRoot = Join-Path $RunRoot "effective-config"
|
||||
New-Item -ItemType Directory -Path $activeConfigRoot | Out-Null
|
||||
Copy-Item -LiteralPath $policyConfig -Destination $activeConfigRoot
|
||||
Copy-Item -LiteralPath $providerMapConfig -Destination $activeConfigRoot
|
||||
$benchmark = Get-Content -LiteralPath $benchmarkConfig -Raw | ConvertFrom-Json
|
||||
$benchmark.ravnoves = $ravnovesProfileDocument.source
|
||||
$benchmark | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath (
|
||||
Join-Path $activeConfigRoot "lab-v1-goose-vegetation-benchmark-v1.json"
|
||||
) -Encoding utf8
|
||||
}
|
||||
$visualCount = if ($RunMode -eq "ravnoves-video") { 0 } else { 12 }
|
||||
$arguments = @(
|
||||
"run", "--rm", "--name", $containerName,
|
||||
@@ -125,7 +172,7 @@ function Invoke-IsolatedRun {
|
||||
"--env", "HOME=/tmp",
|
||||
"--mount", "type=bind,src=$datasetRoot,dst=/data/goose,readonly",
|
||||
"--mount", "type=bind,src=$checkpoint,dst=/models/candidate.pth,readonly",
|
||||
"--mount", "type=bind,src=$configRoot,dst=/config,readonly",
|
||||
"--mount", "type=bind,src=$activeConfigRoot,dst=/config,readonly",
|
||||
"--mount", "type=bind,src=$RunRoot,dst=/output",
|
||||
$image,
|
||||
"--mode", $RunMode,
|
||||
@@ -147,15 +194,27 @@ function Invoke-IsolatedRun {
|
||||
$tail = @($arguments[$mountIndex..($arguments.Count - 1)])
|
||||
$arguments = $head + @("--mount", "type=bind,src=$FramesRoot,dst=/input,readonly") + $tail
|
||||
}
|
||||
& docker @arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "LAB V1 container failed with exit code $LASTEXITCODE"
|
||||
$dockerExitCode = -1
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
try {
|
||||
# Windows PowerShell exposes native stderr as ErrorRecord objects. Model
|
||||
# libraries legitimately emit warnings there, so merge the stream and
|
||||
# fail only on the native process exit code.
|
||||
$ErrorActionPreference = "Continue"
|
||||
& docker @arguments 2>&1 | ForEach-Object { Write-Output $_ }
|
||||
$dockerExitCode = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($dockerExitCode -ne 0) {
|
||||
throw "LAB V1 container failed with exit code $dockerExitCode"
|
||||
}
|
||||
}
|
||||
|
||||
function Export-RavnovesFrames {
|
||||
param([string]$Destination)
|
||||
Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $ravnovesSha256
|
||||
Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $RavnovesSha256
|
||||
New-Item -ItemType Directory -Path $Destination | Out-Null
|
||||
$expression = ($frameIndices | ForEach-Object { "eq(n\,$_ )" }) -join "+"
|
||||
$temporaryPattern = Join-Path $Destination "selected-%03d.png"
|
||||
@@ -175,14 +234,68 @@ function Export-RavnovesFrames {
|
||||
|
||||
function Export-RavnovesVideoFrames {
|
||||
param([string]$Destination)
|
||||
Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $ravnovesSha256
|
||||
Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $RavnovesSha256
|
||||
New-Item -ItemType Directory -Path $Destination | Out-Null
|
||||
& ffmpeg -hide_banner -loglevel error -i $RavnovesVideo -map 0:v:0 -fps_mode passthrough (Join-Path $Destination "frame-%06d.png")
|
||||
$decodedRoot = "{0}-decoded-by-pts" -f $Destination
|
||||
$packetsPath = "{0}-packets.csv" -f $Destination
|
||||
New-Item -ItemType Directory -Path $decodedRoot | Out-Null
|
||||
& ffprobe -v error -select_streams v:0 -show_packets -show_entries packet=pts,flags -of csv=p=0 -o $packetsPath $RavnovesVideo
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "RAVNOVES full-video packet probe failed"
|
||||
}
|
||||
$packetRows = @(Get-Content -LiteralPath $packetsPath | Select-Object -First $RavnovesExpectedFrameCount)
|
||||
if ($packetRows.Count -ne $RavnovesExpectedFrameCount) {
|
||||
throw "RAVNOVES full-video packet sequence changed"
|
||||
}
|
||||
& ffmpeg -hide_banner -loglevel error `
|
||||
-hwaccel cuda -hwaccel_output_format cuda -c:v h264_cuvid `
|
||||
-err_detect ignore_err -flags +output_corrupt -copyts `
|
||||
-i $RavnovesVideo -map 0:v:0 -vf "hwdownload,format=nv12" `
|
||||
-fps_mode passthrough -enc_time_base demux -frames:v $RavnovesExpectedFrameCount `
|
||||
-frame_pts 1 (Join-Path $decodedRoot "frame-%d.png")
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "RAVNOVES full-video frame extraction failed"
|
||||
}
|
||||
$decodedCount = @(Get-ChildItem -LiteralPath $decodedRoot -File -Filter "frame-*.png").Count
|
||||
$repairs = @()
|
||||
for ($index = 0; $index -lt $RavnovesExpectedFrameCount; $index++) {
|
||||
$columns = ([string]$packetRows[$index]).Split(",")
|
||||
if ($columns.Count -lt 2) {
|
||||
throw "RAVNOVES full-video packet row is malformed"
|
||||
}
|
||||
$pts = [int64]::Parse($columns[0].Trim(), [Globalization.CultureInfo]::InvariantCulture)
|
||||
$decodedPath = Join-Path $decodedRoot ("frame-{0}.png" -f $pts)
|
||||
$canonicalPath = Join-Path $Destination ("frame-{0:D6}.png" -f ($index + 1))
|
||||
if (Test-Path -LiteralPath $decodedPath -PathType Leaf) {
|
||||
Move-Item -LiteralPath $decodedPath -Destination $canonicalPath
|
||||
continue
|
||||
}
|
||||
if ($index -eq 0 -or $repairs.Count -ge 1) {
|
||||
throw "RAVNOVES source contains more than one recoverable decoder gap"
|
||||
}
|
||||
$previousPath = Join-Path $Destination ("frame-{0:D6}.png" -f $index)
|
||||
Copy-Item -LiteralPath $previousPath -Destination $canonicalPath
|
||||
$repairs += [ordered]@{
|
||||
sequence = $index + 1
|
||||
packet_pts = $pts
|
||||
method = "duplicate-previous-decoded-frame"
|
||||
}
|
||||
}
|
||||
Remove-Item -LiteralPath $decodedRoot -Recurse -Force
|
||||
Remove-Item -LiteralPath $packetsPath -Force
|
||||
[ordered]@{
|
||||
schema_version = "missioncore.recorded-video-decode-repair/v1"
|
||||
decoder = "ffmpeg-h264_cuvid-output-corrupt"
|
||||
packets_requested = $RavnovesExpectedFrameCount
|
||||
frames_decoded = $decodedCount
|
||||
repaired_frame_count = $repairs.Count
|
||||
repairs = $repairs
|
||||
} | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (
|
||||
Join-Path (Split-Path $Destination -Parent) "decode-repair.json"
|
||||
) -Encoding utf8
|
||||
$frames = @(Get-ChildItem -LiteralPath $Destination -File -Filter "frame-*.png" | Sort-Object Name)
|
||||
if ($frames.Count -ne 4489 -or $frames[0].Name -ne "frame-000001.png" -or $frames[-1].Name -ne "frame-004489.png") {
|
||||
$lastFrameName = "frame-{0:D6}.png" -f $RavnovesExpectedFrameCount
|
||||
if ($frames.Count -ne $RavnovesExpectedFrameCount -or $frames[0].Name -ne "frame-000001.png" -or $frames[-1].Name -ne $lastFrameName) {
|
||||
throw "RAVNOVES full-video frame sequence changed"
|
||||
}
|
||||
}
|
||||
@@ -244,6 +357,9 @@ try {
|
||||
$framesRoot = Join-Path $runRoot "input-frames"
|
||||
Export-RavnovesVideoFrames -Destination $framesRoot
|
||||
Invoke-IsolatedRun -RunMode "ravnoves-video" -RunRoot $runRoot -Limit 0 -FramesRoot $framesRoot
|
||||
Copy-Item -LiteralPath (Join-Path $runRoot "decode-repair.json") -Destination (
|
||||
Join-Path $runRoot "result\decode-repair.json"
|
||||
)
|
||||
Remove-Item -LiteralPath $framesRoot -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run DDRNet on an immutable mixed-route camera review pack."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import platform
|
||||
import statistics
|
||||
import time
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
from run_goose_vegetation_benchmark import (
|
||||
CLASS_COUNT,
|
||||
expand_mask,
|
||||
infer,
|
||||
load_mapping,
|
||||
load_model,
|
||||
percentile,
|
||||
preprocess,
|
||||
read_json,
|
||||
save_image,
|
||||
sha256,
|
||||
stable_digest,
|
||||
validate_contracts,
|
||||
)
|
||||
|
||||
SCHEMA = "missioncore.mixed-route-ddrnet-islands/v1"
|
||||
PACK_SCHEMA = "missioncore.mixed-route-review-pack/v1"
|
||||
AUTHORITY = {
|
||||
"ground_truth": False,
|
||||
"candidate_accepted": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"camera_semantics_can_clear_rigid_geometry": False,
|
||||
"actuation_allowed": False,
|
||||
}
|
||||
|
||||
|
||||
class MixedRouteDdrnetError(RuntimeError):
|
||||
"""The route pack or DDRNet evidence changed or is incomplete."""
|
||||
|
||||
|
||||
def arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--pack", type=Path, required=True)
|
||||
parser.add_argument("--config", type=Path, required=True)
|
||||
parser.add_argument("--policy", type=Path, required=True)
|
||||
parser.add_argument("--provider-map", type=Path, required=True)
|
||||
parser.add_argument("--checkpoint", type=Path, required=True)
|
||||
parser.add_argument("--dataset-root", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def object_value(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
|
||||
raise MixedRouteDdrnetError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def load_pack(root: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
||||
pack = root.resolve(strict=True)
|
||||
if not pack.is_dir() or pack.is_symlink():
|
||||
raise MixedRouteDdrnetError("mixed-route review pack is unavailable")
|
||||
manifest_path = pack / "manifest.json"
|
||||
manifest = object_value(
|
||||
json.loads(manifest_path.read_text(encoding="utf-8")),
|
||||
"mixed-route manifest",
|
||||
)
|
||||
identity = object_value(manifest.get("identity"), "mixed-route identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
frames = manifest.get("frames")
|
||||
frame_count = manifest.get("frame_count")
|
||||
if (
|
||||
manifest.get("schema_version") != PACK_SCHEMA
|
||||
or identity.get("schema_version") != PACK_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or hashlib.sha256(canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or manifest.get("pack_id") != f"mixed-route-review-pack-{identity_sha256}"
|
||||
or identity.get("ground_truth") is not False
|
||||
or object_value(identity.get("authority"), "mixed-route authority").get(
|
||||
"navigation_or_safety_accepted"
|
||||
)
|
||||
is not False
|
||||
or not isinstance(frame_count, int)
|
||||
or isinstance(frame_count, bool)
|
||||
or not 1 <= frame_count <= 64
|
||||
or not isinstance(frames, list)
|
||||
or len(frames) != frame_count
|
||||
):
|
||||
raise MixedRouteDdrnetError("mixed-route review pack identity changed")
|
||||
timeline_descriptor = object_value(manifest.get("timeline"), "mixed-route timeline")
|
||||
timeline_path = pack / "timeline.jsonl"
|
||||
if (
|
||||
timeline_descriptor.get("path") != timeline_path.name
|
||||
or timeline_path.stat().st_size != timeline_descriptor.get("byte_length")
|
||||
or sha256(timeline_path) != timeline_descriptor.get("sha256")
|
||||
):
|
||||
raise MixedRouteDdrnetError("mixed-route timeline proof changed")
|
||||
rows: list[dict[str, Any]] = []
|
||||
with timeline_path.open(encoding="utf-8") as stream:
|
||||
for expected, line in enumerate(stream):
|
||||
row = object_value(json.loads(line), "mixed-route timeline row")
|
||||
seconds = row.get("session_seconds")
|
||||
if (
|
||||
row.get("frame_index") != expected
|
||||
or row.get("sequence") != expected + 1
|
||||
or not isinstance(row.get("source_sequence"), int)
|
||||
or row.get("source_frame_index") != row["source_sequence"] - 1
|
||||
or not isinstance(seconds, (int, float))
|
||||
or isinstance(seconds, bool)
|
||||
or (rows and float(seconds) <= float(rows[-1]["session_seconds"]))
|
||||
):
|
||||
raise MixedRouteDdrnetError("mixed-route timeline order changed")
|
||||
rows.append(row)
|
||||
if len(rows) != frame_count:
|
||||
raise MixedRouteDdrnetError("mixed-route timeline is incomplete")
|
||||
for expected, (descriptor_raw, row) in enumerate(zip(frames, rows)): # noqa: B905
|
||||
descriptor = object_value(descriptor_raw, "mixed-route frame descriptor")
|
||||
relative = descriptor.get("path")
|
||||
if relative != f"frames/frame-{expected + 1:06d}.png":
|
||||
raise MixedRouteDdrnetError("mixed-route frame path changed")
|
||||
pure = PurePosixPath(relative)
|
||||
path = pack.joinpath(*pure.parts)
|
||||
if (
|
||||
path.is_symlink()
|
||||
or not path.is_file()
|
||||
or not path.resolve().is_relative_to(pack)
|
||||
or path.stat().st_size != descriptor.get("byte_length")
|
||||
or sha256(path) != descriptor.get("sha256")
|
||||
or not isinstance(descriptor.get("source_segment_sha256"), str)
|
||||
or row.get("source_sequence")
|
||||
!= identity["selected_sequences"][expected]
|
||||
):
|
||||
raise MixedRouteDdrnetError("mixed-route frame proof changed")
|
||||
return manifest, rows
|
||||
|
||||
|
||||
def overlay(source: Image.Image, semantic: np.ndarray, palette: np.ndarray) -> Image.Image:
|
||||
if semantic.shape != (600, 800):
|
||||
raise MixedRouteDdrnetError("expanded semantic mask shape changed")
|
||||
base = source.convert("RGBA")
|
||||
colors = Image.fromarray(palette[semantic], mode="RGBA")
|
||||
return Image.alpha_composite(base, colors)
|
||||
|
||||
|
||||
def run() -> int:
|
||||
args = arguments()
|
||||
if not torch.cuda.is_available():
|
||||
raise MixedRouteDdrnetError("CUDA is required for DDRNet islands")
|
||||
if args.output.exists():
|
||||
raise MixedRouteDdrnetError("DDRNet islands output already exists")
|
||||
manifest, timeline = load_pack(args.pack)
|
||||
config = read_json(args.config, "benchmark config")
|
||||
policy = read_json(args.policy, "mission policy")
|
||||
provider_map = read_json(args.provider_map, "provider map")
|
||||
candidate = validate_contracts(config, policy, provider_map, "ddrnet")
|
||||
checkpoint = args.checkpoint.resolve(strict=True)
|
||||
if (
|
||||
checkpoint.is_symlink()
|
||||
or checkpoint.stat().st_size != candidate["checkpoint_size_bytes"]
|
||||
or sha256(checkpoint) != candidate["checkpoint_sha256"]
|
||||
):
|
||||
raise MixedRouteDdrnetError("DDRNet checkpoint identity changed")
|
||||
dataset_root = args.dataset_root.resolve(strict=True)
|
||||
mapping_path = dataset_root / config["dataset"]["mapping_relative_path"]
|
||||
names, palette = load_mapping(mapping_path, config["dataset"]["mapping_sha256"])
|
||||
|
||||
args.output.mkdir(mode=0o700, parents=True, exist_ok=False)
|
||||
mask_root = args.output / "semantic-masks"
|
||||
overlay_root = args.output / "overlay-frames"
|
||||
mask_root.mkdir(mode=0o700)
|
||||
overlay_root.mkdir(mode=0o700)
|
||||
torch.cuda.empty_cache()
|
||||
model, model_name, architecture_failures = load_model("ddrnet", checkpoint)
|
||||
first_path = args.pack / manifest["frames"][0]["path"]
|
||||
with Image.open(first_path) as opened:
|
||||
warm_source = opened.convert("RGB")
|
||||
warm_tensor, _ = preprocess(warm_source)
|
||||
warmup_ms = [infer(model, warm_tensor)[1] for _ in range(3)]
|
||||
torch.cuda.reset_peak_memory_stats()
|
||||
|
||||
latencies_ms: list[float] = []
|
||||
aggregate = np.zeros(CLASS_COUNT, dtype=np.int64)
|
||||
frame_results: list[dict[str, Any]] = []
|
||||
started = time.perf_counter()
|
||||
for index, (descriptor, timeline_row) in enumerate(
|
||||
zip(manifest["frames"], timeline) # noqa: B905 - Worker image uses Python 3.9.
|
||||
):
|
||||
source_path = args.pack / descriptor["path"]
|
||||
with Image.open(source_path) as opened:
|
||||
source = opened.convert("RGB")
|
||||
if source.size != (800, 600):
|
||||
raise MixedRouteDdrnetError("mixed-route source resolution changed")
|
||||
tensor, crop_box = preprocess(source)
|
||||
prediction, latency_ms = infer(model, tensor)
|
||||
expanded = expand_mask(prediction, source.size, crop_box)
|
||||
latencies_ms.append(latency_ms)
|
||||
aggregate += np.bincount(expanded.reshape(-1), minlength=CLASS_COUNT)
|
||||
mask_path = mask_root / f"frame-{index + 1:06d}.png"
|
||||
overlay_path = overlay_root / f"frame-{index + 1:06d}.png"
|
||||
mask_sha256 = save_image(mask_path, expanded, "L")
|
||||
overlay_sha256 = save_image(overlay_path, overlay(source, expanded, palette))
|
||||
present = np.flatnonzero(np.bincount(expanded.reshape(-1), minlength=CLASS_COUNT))
|
||||
frame_results.append(
|
||||
{
|
||||
"frame_index": index,
|
||||
"source_sequence": timeline_row["source_sequence"],
|
||||
"source_frame_index": timeline_row["source_frame_index"],
|
||||
"session_seconds": timeline_row["session_seconds"],
|
||||
"latency_ms": round(latency_ms, 6),
|
||||
"present_classes": [
|
||||
{"class_id": int(class_id), "label": names[int(class_id)]}
|
||||
for class_id in present
|
||||
],
|
||||
"mask": {
|
||||
"path": mask_path.relative_to(args.output).as_posix(),
|
||||
"byte_length": mask_path.stat().st_size,
|
||||
"sha256": mask_sha256,
|
||||
},
|
||||
"overlay": {
|
||||
"path": overlay_path.relative_to(args.output).as_posix(),
|
||||
"byte_length": overlay_path.stat().st_size,
|
||||
"sha256": overlay_sha256,
|
||||
},
|
||||
}
|
||||
)
|
||||
wall_seconds = time.perf_counter() - started
|
||||
if len(frame_results) != manifest["frame_count"]:
|
||||
raise MixedRouteDdrnetError("DDRNet island accounting changed")
|
||||
timing = {
|
||||
"prewarm_inference_count": len(warmup_ms),
|
||||
"prewarm_latency_ms_first": round(warmup_ms[0], 6),
|
||||
"prewarm_latency_ms_last": round(warmup_ms[-1], 6),
|
||||
"inference_wall_seconds": round(wall_seconds, 6),
|
||||
"latency_ms_mean": round(statistics.fmean(latencies_ms), 6),
|
||||
"latency_ms_p50": round(percentile(latencies_ms, 0.5), 6),
|
||||
"latency_ms_p95": round(percentile(latencies_ms, 0.95), 6),
|
||||
"throughput_fps_from_mean_inference": round(
|
||||
1000.0 / statistics.fmean(latencies_ms), 6
|
||||
),
|
||||
}
|
||||
if any(not math.isfinite(float(value)) for value in timing.values()):
|
||||
raise MixedRouteDdrnetError("DDRNet timing is non-finite")
|
||||
result: dict[str, Any] = {
|
||||
"schema_version": SCHEMA,
|
||||
"status": "review-islands-ready-not-accepted",
|
||||
"worker_id": "worker-006",
|
||||
"source": {
|
||||
"pack_id": manifest["pack_id"],
|
||||
"pack_identity_sha256": manifest["identity_sha256"],
|
||||
"job_id": manifest["identity"]["job_id"],
|
||||
"input_sha256": manifest["identity"]["input_sha256"],
|
||||
"session_id": manifest["identity"]["session_id"],
|
||||
"source_id": manifest["identity"]["source_id"],
|
||||
"frame_count": manifest["frame_count"],
|
||||
"ground_truth_available": False,
|
||||
},
|
||||
"candidate": {
|
||||
"candidate_key": "ddrnet",
|
||||
"candidate_id": candidate["candidate_id"],
|
||||
"loaded_model_name": model_name,
|
||||
"architecture_probe_failures": architecture_failures,
|
||||
"checkpoint_size_bytes": checkpoint.stat().st_size,
|
||||
"checkpoint_sha256": sha256(checkpoint),
|
||||
},
|
||||
"taxonomy": {
|
||||
"schema_version": "missioncore.lab-v1-vegetation-taxonomy/v1",
|
||||
"classes": [
|
||||
{
|
||||
"class_id": class_id,
|
||||
"label": names[class_id],
|
||||
"color_rgb": palette[class_id, :3].astype(int).tolist(),
|
||||
"disposition": "undefined" if class_id == 0 else "prediction",
|
||||
}
|
||||
for class_id in range(CLASS_COUNT)
|
||||
],
|
||||
},
|
||||
"aggregate_prediction_pixels": aggregate.tolist(),
|
||||
"frames": frame_results,
|
||||
"timing": timing,
|
||||
"resource": {
|
||||
"hostname": platform.node(),
|
||||
"gpu_name": torch.cuda.get_device_name(0),
|
||||
"peak_allocated_vram_bytes": int(torch.cuda.max_memory_allocated()),
|
||||
"peak_reserved_vram_bytes": int(torch.cuda.max_memory_reserved()),
|
||||
"torch_version": torch.__version__,
|
||||
"cuda_runtime_version": torch.version.cuda,
|
||||
"python_version": platform.python_version(),
|
||||
},
|
||||
"provenance": {
|
||||
"pack_manifest_sha256": sha256(args.pack / "manifest.json"),
|
||||
"config_sha256": sha256(args.config),
|
||||
"policy_sha256": sha256(args.policy),
|
||||
"provider_map_sha256": sha256(args.provider_map),
|
||||
"runner_sha256": sha256(Path(__file__)),
|
||||
},
|
||||
"limitations": [
|
||||
"Selected independently decodable islands are not a complete route timeline.",
|
||||
"RAVNOVES004TREE has no route truth; class colors are model predictions.",
|
||||
"DDRNet evidence cannot clear rigid geometry, person or vehicle vetoes.",
|
||||
],
|
||||
"authority": AUTHORITY,
|
||||
}
|
||||
result["result_id"] = f"mixed-route-ddrnet-islands-{stable_digest(result)}"
|
||||
(args.output / "result.json").write_text(
|
||||
json.dumps(result, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result["result_id"],
|
||||
"frames": len(frame_results),
|
||||
"latency_p95_ms": timing["latency_ms_p95"],
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(run())
|
||||
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seal fail-closed TRAVEL/TGS evidence for mixed-route review islands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from build_tgs_fail_closed_evidence import (
|
||||
TgsEvidenceError,
|
||||
_load_float32,
|
||||
classify_exact_input,
|
||||
costmap_grid,
|
||||
rasterize_costmap,
|
||||
sha256_file,
|
||||
write_deterministic_npz,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = "missioncore.mixed-route-tgs-review-profile/v1"
|
||||
INPUT_SCHEMA = "missioncore.mixed-route-tgs-input/v1"
|
||||
RESULT_SCHEMA = "missioncore.mixed-route-tgs-result/v1"
|
||||
FRAME_COUNT = 10
|
||||
|
||||
|
||||
def _timing(path: Path) -> dict[str, object]:
|
||||
rows: list[dict[str, object]] = []
|
||||
with path.open(encoding="utf-8", newline="") as stream:
|
||||
for raw in csv.DictReader(stream, delimiter="\t"):
|
||||
try:
|
||||
row = {
|
||||
"profile_id": str(raw["profile"]),
|
||||
"slot": int(raw["slot"]),
|
||||
"wall_seconds": float(raw["wall_seconds"]),
|
||||
"max_rss_kib": int(raw["max_rss_kib"]),
|
||||
}
|
||||
except (KeyError, TypeError, ValueError) as exc:
|
||||
raise TgsEvidenceError("TGS timing row is invalid") from exc
|
||||
if (
|
||||
row["profile_id"] not in {"current_increment", "causal_rolling_1s"}
|
||||
or not 0 <= row["slot"] < FRAME_COUNT
|
||||
or not 0 <= row["wall_seconds"] < 60
|
||||
or not 0 < row["max_rss_kib"] < 16 * 1024 * 1024
|
||||
):
|
||||
raise TgsEvidenceError("TGS timing value is invalid")
|
||||
rows.append(row)
|
||||
if len(rows) != FRAME_COUNT * 2:
|
||||
raise TgsEvidenceError("TGS timing is incomplete")
|
||||
seconds = np.asarray([row["wall_seconds"] for row in rows], dtype=np.float64)
|
||||
return {
|
||||
"runs": rows,
|
||||
"wall_seconds_mean": round(float(seconds.mean()), 6),
|
||||
"wall_seconds_p95": round(float(np.percentile(seconds, 95)), 6),
|
||||
"max_rss_kib": max(int(row["max_rss_kib"]) for row in rows),
|
||||
}
|
||||
|
||||
|
||||
def build(run_root: Path, config_path: Path, output_root: Path) -> dict[str, object]:
|
||||
if output_root.exists():
|
||||
raise TgsEvidenceError("mixed-route TGS evidence already exists")
|
||||
config = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
source = config.get("source") if isinstance(config, dict) else None
|
||||
invariants = config.get("invariants") if isinstance(config, dict) else None
|
||||
if (
|
||||
config.get("schema_version") != CONFIG_SCHEMA
|
||||
or not isinstance(source, dict)
|
||||
or not isinstance(invariants, dict)
|
||||
or invariants.get("aos_allowed") is not False
|
||||
or invariants.get("missing_support_means_free") is not False
|
||||
or invariants.get("future_frames_used") is not False
|
||||
or invariants.get("navigation_or_actuation_allowed") is not False
|
||||
or config.get("state_codes")
|
||||
!= {
|
||||
"UNOBSERVED": 0,
|
||||
"GROUND_SUPPORT": 1,
|
||||
"NONGROUND_OCCUPIED": 2,
|
||||
"UNKNOWN_REJECTED": 3,
|
||||
}
|
||||
):
|
||||
raise TgsEvidenceError("mixed-route TGS profile changed")
|
||||
input_manifest_path = run_root / "inputs" / "input-manifest.json"
|
||||
input_manifest = json.loads(input_manifest_path.read_text(encoding="utf-8"))
|
||||
if (
|
||||
input_manifest.get("schema_version") != INPUT_SCHEMA
|
||||
or input_manifest.get("source_pack_id") != source.get("source_pack_id")
|
||||
or input_manifest.get("source_pack_sha256")
|
||||
!= source.get("source_pack_sha256")
|
||||
or input_manifest.get("config_sha256") != sha256_file(config_path)
|
||||
or input_manifest.get("coordinate_frame") != "map-gravity-local"
|
||||
or input_manifest.get("future_frames_used") is not False
|
||||
or input_manifest.get("frame_count") != FRAME_COUNT
|
||||
or len(input_manifest.get("records", [])) != FRAME_COUNT * 2
|
||||
):
|
||||
raise TgsEvidenceError("mixed-route TGS input manifest changed")
|
||||
records = {
|
||||
(str(row["profile_id"]), int(row["slot"])): row
|
||||
for row in input_manifest["records"]
|
||||
}
|
||||
if len(records) != FRAME_COUNT * 2:
|
||||
raise TgsEvidenceError("mixed-route TGS input records are not unique")
|
||||
|
||||
cell_size = float(config["costmap"]["cell_size_m"])
|
||||
radius = float(config["costmap"]["radius_m"])
|
||||
grid = costmap_grid(radius, cell_size)
|
||||
arrays: dict[str, np.ndarray] = {
|
||||
"costmap_cell_indices_xy": grid[:, :2].astype(np.int32),
|
||||
"costmap_cell_centers_xy_m": grid[:, 2:].astype(np.float32),
|
||||
"source_frame_indices": np.asarray(
|
||||
[
|
||||
records[("current_increment", slot)]["source_frame_index"]
|
||||
for slot in range(FRAME_COUNT)
|
||||
],
|
||||
dtype=np.int64,
|
||||
),
|
||||
"session_seconds": np.asarray(
|
||||
[
|
||||
records[("current_increment", slot)]["session_seconds"]
|
||||
for slot in range(FRAME_COUNT)
|
||||
],
|
||||
dtype=np.float64,
|
||||
),
|
||||
}
|
||||
summaries: list[dict[str, object]] = []
|
||||
for profile_id in ("current_increment", "causal_rolling_1s"):
|
||||
all_points: list[np.ndarray] = []
|
||||
all_states: list[np.ndarray] = []
|
||||
offsets = [0]
|
||||
grid_states: list[np.ndarray] = []
|
||||
ground_counts: list[np.ndarray] = []
|
||||
nonground_counts: list[np.ndarray] = []
|
||||
rejected_counts: list[np.ndarray] = []
|
||||
z_bounds_rows: list[np.ndarray] = []
|
||||
for slot in range(FRAME_COUNT):
|
||||
record = records[(profile_id, slot)]
|
||||
native_path = run_root / "inputs" / str(record["relative_path"])
|
||||
if (
|
||||
not native_path.is_file()
|
||||
or native_path.stat().st_size != record["bytes"]
|
||||
or sha256_file(native_path) != record["sha256"]
|
||||
):
|
||||
raise TgsEvidenceError("sealed mixed-route TGS input changed")
|
||||
output = run_root / "outputs" / profile_id
|
||||
points, states = classify_exact_input(
|
||||
_load_float32(native_path, 4),
|
||||
_load_float32(output / f"{slot}_ground.bin", 4),
|
||||
_load_float32(output / f"{slot}_nonground.bin", 4),
|
||||
min_range_m=float(config["tgs"]["min_range_m"]),
|
||||
max_range_m=float(config["tgs"]["max_range_m"]),
|
||||
)
|
||||
grid_state, ground, nonground, rejected, z_bounds = rasterize_costmap(
|
||||
points,
|
||||
states,
|
||||
grid,
|
||||
cell_size_m=cell_size,
|
||||
)
|
||||
all_points.append(points.astype(np.float32, copy=False))
|
||||
all_states.append(states)
|
||||
offsets.append(offsets[-1] + points.shape[0])
|
||||
grid_states.append(grid_state)
|
||||
ground_counts.append(ground)
|
||||
nonground_counts.append(nonground)
|
||||
rejected_counts.append(rejected)
|
||||
z_bounds_rows.append(z_bounds)
|
||||
accounted = (
|
||||
np.count_nonzero(states == 1)
|
||||
+ np.count_nonzero(states == 2)
|
||||
+ np.count_nonzero(states == 3)
|
||||
== points.shape[0]
|
||||
)
|
||||
summaries.append(
|
||||
{
|
||||
"profile_id": profile_id,
|
||||
"slot": slot,
|
||||
"frame_index": int(record["frame_index"]),
|
||||
"source_frame_index": int(record["source_frame_index"]),
|
||||
"source_sequence": int(record["source_sequence"]),
|
||||
"session_seconds": float(record["session_seconds"]),
|
||||
"point_count": int(points.shape[0]),
|
||||
"ground_point_count": int(np.count_nonzero(states == 1)),
|
||||
"nonground_point_count": int(np.count_nonzero(states == 2)),
|
||||
"rejected_point_count": int(np.count_nonzero(states == 3)),
|
||||
"ground_cell_count": int(np.count_nonzero(grid_state == 1)),
|
||||
"nonground_cell_count": int(np.count_nonzero(grid_state == 2)),
|
||||
"rejected_cell_count": int(np.count_nonzero(grid_state == 3)),
|
||||
"unobserved_cell_count": int(np.count_nonzero(grid_state == 0)),
|
||||
"all_points_accounted": bool(accounted),
|
||||
}
|
||||
)
|
||||
arrays[f"{profile_id}_points_xyz_m"] = np.concatenate(all_points)
|
||||
arrays[f"{profile_id}_point_states"] = np.concatenate(all_states)
|
||||
arrays[f"{profile_id}_point_offsets"] = np.asarray(offsets, dtype=np.int64)
|
||||
arrays[f"{profile_id}_costmap_states"] = np.stack(grid_states)
|
||||
arrays[f"{profile_id}_costmap_ground_point_counts"] = np.stack(ground_counts)
|
||||
arrays[f"{profile_id}_costmap_nonground_point_counts"] = np.stack(
|
||||
nonground_counts
|
||||
)
|
||||
arrays[f"{profile_id}_costmap_rejected_point_counts"] = np.stack(
|
||||
rejected_counts
|
||||
)
|
||||
arrays[f"{profile_id}_costmap_z_bounds_m"] = np.stack(z_bounds_rows)
|
||||
if not all(bool(row["all_points_accounted"]) for row in summaries):
|
||||
raise TgsEvidenceError("mixed-route TGS lost an eligible point")
|
||||
|
||||
output_root.mkdir(parents=True)
|
||||
evidence_path = output_root / "evidence.npz"
|
||||
write_deterministic_npz(evidence_path, arrays)
|
||||
timing = _timing(run_root / "tgs-timing.tsv")
|
||||
result = {
|
||||
"schema_version": RESULT_SCHEMA,
|
||||
"status": "passed-review-only",
|
||||
"source": {
|
||||
"source_id": source["source_id"],
|
||||
"session_id": source["session_id"],
|
||||
"review_pack_id": source["review_pack_id"],
|
||||
"source_pack_id": source["source_pack_id"],
|
||||
"source_pack_sha256": source["source_pack_sha256"],
|
||||
},
|
||||
"config_sha256": sha256_file(config_path),
|
||||
"input_manifest_sha256": sha256_file(input_manifest_path),
|
||||
"evidence": {
|
||||
"path": "evidence.npz",
|
||||
"bytes": evidence_path.stat().st_size,
|
||||
"sha256": sha256_file(evidence_path),
|
||||
},
|
||||
"costmap": {
|
||||
"coordinate_frame": "map-gravity-local",
|
||||
"cell_size_m": cell_size,
|
||||
"radius_m": radius,
|
||||
"cell_count": int(grid.shape[0]),
|
||||
},
|
||||
"anchors": summaries,
|
||||
"timing": timing,
|
||||
"summary": {
|
||||
"frame_count": FRAME_COUNT,
|
||||
"anchor_profile_count": len(summaries),
|
||||
"all_eligible_points_accounted": True,
|
||||
"aos_used": False,
|
||||
"primary_profile": "causal_rolling_1s",
|
||||
},
|
||||
"limitations": [
|
||||
"Selected review islands are not a complete route timeline.",
|
||||
(
|
||||
"TGS separates local ground support from non-ground evidence; it does not "
|
||||
"prove ditch or negative-obstacle detection."
|
||||
),
|
||||
"Camera projection is visual evidence only and cannot clear rigid geometry.",
|
||||
],
|
||||
"authority": {
|
||||
"visual_quality_accepted": False,
|
||||
"traversability_accepted": False,
|
||||
"realtime_accepted": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_allowed": False,
|
||||
},
|
||||
}
|
||||
(output_root / "result.json").write_text(
|
||||
json.dumps(result, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--run-root", type=Path, required=True)
|
||||
parser.add_argument("--config", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build(args.run_root, args.config, args.output_root)
|
||||
print(json.dumps(result["summary"], sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare exact mixed-route LiDAR islands for isolated TRAVEL/TGS review."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from prepare_tgs_fail_closed_inputs import TgsInputError, gravity_local_xyzi
|
||||
|
||||
CONFIG_SCHEMA = "missioncore.mixed-route-tgs-review-profile/v1"
|
||||
PACK_SCHEMA = "missioncore.mixed-route-lidar-pack/v1"
|
||||
INPUT_SCHEMA = "missioncore.mixed-route-tgs-input/v1"
|
||||
FRAME_COUNT = 10
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _slice(points: np.ndarray, offsets: np.ndarray, index: int) -> np.ndarray:
|
||||
return points[int(offsets[index]) : int(offsets[index + 1])]
|
||||
|
||||
|
||||
def _validate_offsets(offsets: np.ndarray, point_count: int) -> bool:
|
||||
return bool(
|
||||
offsets.shape == (FRAME_COUNT + 1,)
|
||||
and offsets.dtype == np.int64
|
||||
and int(offsets[0]) == 0
|
||||
and int(offsets[-1]) == point_count
|
||||
and np.all(np.diff(offsets) > 0)
|
||||
)
|
||||
|
||||
|
||||
def prepare(source_root: Path, config_path: Path, output_root: Path) -> dict[str, object]:
|
||||
if output_root.exists():
|
||||
raise TgsInputError("mixed-route TGS output already exists")
|
||||
source = source_root.resolve(strict=True)
|
||||
config = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
manifest = json.loads((source / "manifest.json").read_text(encoding="utf-8"))
|
||||
identity = manifest.get("identity") if isinstance(manifest, dict) else None
|
||||
artifact = manifest.get("artifact") if isinstance(manifest, dict) else None
|
||||
source_config = config.get("source") if isinstance(config, dict) else None
|
||||
invariants = config.get("invariants") if isinstance(config, dict) else None
|
||||
profiles = config.get("profiles") if isinstance(config, dict) else None
|
||||
if (
|
||||
config.get("schema_version") != CONFIG_SCHEMA
|
||||
or not isinstance(source_config, dict)
|
||||
or not isinstance(invariants, dict)
|
||||
or not isinstance(profiles, dict)
|
||||
or set(profiles) != {"current_increment", "causal_rolling_1s"}
|
||||
or source_config.get("input_coordinate_frame")
|
||||
!= "map-gravity-local-translation-only"
|
||||
or invariants.get("lidar_orientation_applied_to_tgs_input") is not False
|
||||
or invariants.get("future_frames_used") is not False
|
||||
or invariants.get("navigation_or_actuation_allowed") is not False
|
||||
or manifest.get("schema_version") != PACK_SCHEMA
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("schema_version") != PACK_SCHEMA
|
||||
or identity.get("session_id") != source_config.get("session_id")
|
||||
or identity.get("review_pack_id") != source_config.get("review_pack_id")
|
||||
or manifest.get("pack_id") != source_config.get("source_pack_id")
|
||||
or not isinstance(artifact, dict)
|
||||
or artifact.get("path") != "lidar-pack.npz"
|
||||
or artifact.get("sha256") != source_config.get("source_pack_sha256")
|
||||
or identity.get("frame_count") != FRAME_COUNT
|
||||
or identity.get("available_lidar_frames") != FRAME_COUNT
|
||||
or identity.get("causal_history_seconds")
|
||||
!= float(profiles["causal_rolling_1s"]["history_seconds"])
|
||||
or identity.get("ground_truth") is not False
|
||||
):
|
||||
raise TgsInputError("mixed-route TGS source contract changed")
|
||||
pack_path = source / "lidar-pack.npz"
|
||||
if (
|
||||
not pack_path.is_file()
|
||||
or pack_path.stat().st_size != artifact.get("byte_length")
|
||||
or sha256_file(pack_path) != artifact.get("sha256")
|
||||
):
|
||||
raise TgsInputError("mixed-route LiDAR pack changed")
|
||||
|
||||
required = {
|
||||
"frame_indices",
|
||||
"source_frame_indices",
|
||||
"session_seconds",
|
||||
"lidar_session_seconds",
|
||||
"sample_available",
|
||||
"cloud_offsets",
|
||||
"cloud_points_map",
|
||||
"pose_positions_map",
|
||||
"lidar_camera_delta_ms",
|
||||
"pose_point_delta_ms",
|
||||
"causal_history_seconds",
|
||||
"causal_history_offsets",
|
||||
"causal_history_points_map",
|
||||
}
|
||||
with np.load(pack_path, allow_pickle=False) as archive:
|
||||
if not required.issubset(archive.files):
|
||||
raise TgsInputError("mixed-route LiDAR pack members changed")
|
||||
arrays = {name: archive[name] for name in required}
|
||||
current_points = arrays["cloud_points_map"]
|
||||
history_points = arrays["causal_history_points_map"]
|
||||
if (
|
||||
arrays["frame_indices"].shape != (FRAME_COUNT,)
|
||||
or arrays["frame_indices"].dtype != np.int64
|
||||
or not np.array_equal(arrays["frame_indices"], np.arange(FRAME_COUNT))
|
||||
or arrays["source_frame_indices"].shape != (FRAME_COUNT,)
|
||||
or arrays["source_frame_indices"].dtype != np.int64
|
||||
or np.any(np.diff(arrays["source_frame_indices"]) <= 0)
|
||||
or arrays["session_seconds"].shape != (FRAME_COUNT,)
|
||||
or arrays["session_seconds"].dtype != np.float64
|
||||
or np.any(np.diff(arrays["session_seconds"]) <= 0)
|
||||
or arrays["lidar_session_seconds"].shape != (FRAME_COUNT,)
|
||||
or arrays["lidar_session_seconds"].dtype != np.float64
|
||||
or arrays["sample_available"].shape != (FRAME_COUNT,)
|
||||
or arrays["sample_available"].dtype != np.bool_
|
||||
or not arrays["sample_available"].all()
|
||||
or current_points.ndim != 2
|
||||
or current_points.shape[1:] != (3,)
|
||||
or current_points.dtype != np.float32
|
||||
or history_points.ndim != 2
|
||||
or history_points.shape[1:] != (3,)
|
||||
or history_points.dtype != np.float32
|
||||
or not np.isfinite(current_points).all()
|
||||
or not np.isfinite(history_points).all()
|
||||
or not _validate_offsets(arrays["cloud_offsets"], current_points.shape[0])
|
||||
or not _validate_offsets(
|
||||
arrays["causal_history_offsets"], history_points.shape[0]
|
||||
)
|
||||
or arrays["pose_positions_map"].shape != (FRAME_COUNT, 3)
|
||||
or arrays["pose_positions_map"].dtype != np.float64
|
||||
or not np.isfinite(arrays["pose_positions_map"]).all()
|
||||
or arrays["causal_history_seconds"].shape != (1,)
|
||||
or float(arrays["causal_history_seconds"][0])
|
||||
!= float(profiles["causal_rolling_1s"]["history_seconds"])
|
||||
or np.any(np.abs(arrays["lidar_camera_delta_ms"]) > 100.0)
|
||||
or np.any(np.abs(arrays["pose_point_delta_ms"]) > 100.0)
|
||||
):
|
||||
raise TgsInputError("mixed-route LiDAR arrays changed")
|
||||
|
||||
records: list[dict[str, object]] = []
|
||||
for profile_id in ("current_increment", "causal_rolling_1s"):
|
||||
for slot in range(FRAME_COUNT):
|
||||
if profile_id == "current_increment":
|
||||
points_map = _slice(
|
||||
current_points, arrays["cloud_offsets"], slot
|
||||
)
|
||||
else:
|
||||
points_map = _slice(
|
||||
history_points, arrays["causal_history_offsets"], slot
|
||||
)
|
||||
radius = float(profiles[profile_id]["local_radius_m"])
|
||||
relative_xy = (
|
||||
points_map[:, :2].astype(np.float64)
|
||||
- arrays["pose_positions_map"][slot, :2]
|
||||
)
|
||||
points_map = points_map[np.linalg.norm(relative_xy, axis=1) <= radius]
|
||||
native = gravity_local_xyzi(
|
||||
points_map, arrays["pose_positions_map"][slot]
|
||||
)
|
||||
if native.shape[0] == 0:
|
||||
raise TgsInputError("mixed-route TGS profile produced an empty cloud")
|
||||
target = (
|
||||
output_root
|
||||
/ "profiles"
|
||||
/ profile_id
|
||||
/ "velodyne"
|
||||
/ f"{slot:06d}.bin"
|
||||
)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(np.ascontiguousarray(native).tobytes())
|
||||
records.append(
|
||||
{
|
||||
"profile_id": profile_id,
|
||||
"slot": slot,
|
||||
"frame_index": slot,
|
||||
"source_frame_index": int(
|
||||
arrays["source_frame_indices"][slot]
|
||||
),
|
||||
"source_sequence": int(
|
||||
arrays["source_frame_indices"][slot]
|
||||
)
|
||||
+ 1,
|
||||
"session_seconds": float(arrays["session_seconds"][slot]),
|
||||
"lidar_session_seconds": float(
|
||||
arrays["lidar_session_seconds"][slot]
|
||||
),
|
||||
"lidar_camera_delta_ms": float(
|
||||
arrays["lidar_camera_delta_ms"][slot]
|
||||
),
|
||||
"pose_point_delta_ms": float(
|
||||
arrays["pose_point_delta_ms"][slot]
|
||||
),
|
||||
"point_count": int(native.shape[0]),
|
||||
"relative_path": target.relative_to(output_root).as_posix(),
|
||||
"bytes": target.stat().st_size,
|
||||
"sha256": sha256_file(target),
|
||||
}
|
||||
)
|
||||
manifest_out = {
|
||||
"schema_version": INPUT_SCHEMA,
|
||||
"source_pack_id": manifest["pack_id"],
|
||||
"source_pack_sha256": artifact["sha256"],
|
||||
"config_sha256": sha256_file(config_path),
|
||||
"coordinate_frame": "map-gravity-local",
|
||||
"transform": "translation-only-preserve-map-gravity-axis",
|
||||
"intensity_policy": "zero-filled-algorithm-compatibility-only",
|
||||
"future_frames_used": False,
|
||||
"frame_count": FRAME_COUNT,
|
||||
"profile_count": 2,
|
||||
"records": records,
|
||||
"authority": {
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_allowed": False,
|
||||
},
|
||||
}
|
||||
manifest_path = output_root / "input-manifest.json"
|
||||
manifest_path.write_text(
|
||||
json.dumps(manifest_out, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return manifest_out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source-root", type=Path, required=True)
|
||||
parser.add_argument("--config", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
manifest = prepare(args.source_root, args.config, args.output_root)
|
||||
print(json.dumps({"ok": True, "records": len(manifest["records"])}, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Publish exact, independently decodable camera islands for mixed-route review."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from k1link.compute.jobs import validate_camera_compute_job
|
||||
from k1link.device_plugins.xgrids_k1.mqtt.capture import read_capture_clock_origin
|
||||
|
||||
SCHEMA = "missioncore.mixed-route-review-pack/v1"
|
||||
MAX_INDEX_LINE_BYTES = 64 * 1024
|
||||
|
||||
|
||||
class MixedRouteReviewPackError(RuntimeError):
|
||||
"""The selected camera evidence cannot be published without ambiguity."""
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _sequences(value: str) -> tuple[int, ...]:
|
||||
try:
|
||||
sequences = tuple(int(item) for item in value.split(","))
|
||||
except ValueError as exc:
|
||||
raise argparse.ArgumentTypeError("sequences must be comma-separated integers") from exc
|
||||
if not sequences or any(item < 1 for item in sequences):
|
||||
raise argparse.ArgumentTypeError("sequences must be positive")
|
||||
if len(set(sequences)) != len(sequences) or tuple(sorted(sequences)) != sequences:
|
||||
raise argparse.ArgumentTypeError("sequences must be unique and increasing")
|
||||
return sequences
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job", type=Path, required=True)
|
||||
parser.add_argument("--session", type=Path, required=True)
|
||||
parser.add_argument("--sequences", type=_sequences, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
parser.add_argument("--ffmpeg", type=Path, required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _read_selected_index(
|
||||
path: Path,
|
||||
sequences: tuple[int, ...],
|
||||
) -> list[dict[str, Any]]:
|
||||
wanted = set(sequences)
|
||||
selected: dict[int, dict[str, Any]] = {}
|
||||
with path.open("rb") as stream:
|
||||
for expected_sequence, line in enumerate(stream, start=1):
|
||||
if len(line) > MAX_INDEX_LINE_BYTES or not line.endswith(b"\n"):
|
||||
raise MixedRouteReviewPackError("camera index line is invalid")
|
||||
if expected_sequence not in wanted:
|
||||
continue
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise MixedRouteReviewPackError("camera index JSON is invalid") from exc
|
||||
if (
|
||||
not isinstance(value, dict)
|
||||
or value.get("schema_version")
|
||||
!= "missioncore.camera-recording-index/v1"
|
||||
or value.get("kind") != "media"
|
||||
or value.get("sequence") != expected_sequence
|
||||
or value.get("path") != f"segments/{expected_sequence}.m4s"
|
||||
or not isinstance(value.get("session_monotonic_ns"), int)
|
||||
or not isinstance(value.get("host_monotonic_ns"), int)
|
||||
or not isinstance(value.get("host_epoch_ns"), int)
|
||||
):
|
||||
raise MixedRouteReviewPackError("selected camera index row changed")
|
||||
selected[expected_sequence] = value
|
||||
if tuple(sorted(selected)) != sequences:
|
||||
raise MixedRouteReviewPackError("selected camera sequence is incomplete")
|
||||
return [selected[sequence] for sequence in sequences]
|
||||
|
||||
|
||||
def _decode_exact_fragment(
|
||||
*,
|
||||
ffmpeg: Path,
|
||||
init_path: Path,
|
||||
segment_path: Path,
|
||||
output_path: Path,
|
||||
) -> None:
|
||||
input_value = f"concat:{init_path}|{segment_path}"
|
||||
completed = subprocess.run(
|
||||
[
|
||||
os.fspath(ffmpeg),
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-nostdin",
|
||||
"-y",
|
||||
"-i",
|
||||
input_value,
|
||||
"-frames:v",
|
||||
"1",
|
||||
os.fspath(output_path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
)
|
||||
if completed.returncode != 0 or not output_path.is_file():
|
||||
detail = completed.stderr.strip().splitlines()[-1:] or ["no decoded frame"]
|
||||
raise MixedRouteReviewPackError(
|
||||
f"selected fragment is not independently decodable: {segment_path.name}: {detail[0]}"
|
||||
)
|
||||
with Image.open(output_path) as image:
|
||||
if image.mode != "RGB" or image.size != (800, 600):
|
||||
raise MixedRouteReviewPackError("selected camera frame shape changed")
|
||||
|
||||
|
||||
def prepare(
|
||||
*,
|
||||
job_root: Path,
|
||||
session_root: Path,
|
||||
sequences: tuple[int, ...],
|
||||
output_root: Path,
|
||||
ffmpeg_path: Path,
|
||||
) -> Path:
|
||||
job = validate_camera_compute_job(job_root)
|
||||
session = session_root.resolve(strict=True)
|
||||
if not session.is_dir() or session.name != job.session_id:
|
||||
raise MixedRouteReviewPackError("camera job and observation session differ")
|
||||
capture_root = session / "captures" / "mqtt_live"
|
||||
origin_path = capture_root / "mqtt.timeline.origin.json"
|
||||
origin = read_capture_clock_origin(origin_path)
|
||||
if sequences[-1] > job.segment_count:
|
||||
raise MixedRouteReviewPackError("selected sequence escapes the camera epoch")
|
||||
ffmpeg = ffmpeg_path.resolve(strict=True)
|
||||
if not ffmpeg.is_file():
|
||||
raise MixedRouteReviewPackError("ffmpeg is unavailable")
|
||||
epoch_root = (
|
||||
job.job_root
|
||||
/ "input"
|
||||
/ "camera"
|
||||
/ job.source_id
|
||||
/ f"epoch-{job.codec_epoch}"
|
||||
)
|
||||
selected = _read_selected_index(epoch_root / "index.jsonl", sequences)
|
||||
identity = {
|
||||
"schema_version": SCHEMA,
|
||||
"job_id": job.job_id,
|
||||
"input_sha256": job.input_sha256,
|
||||
"session_id": job.session_id,
|
||||
"source_id": job.source_id,
|
||||
"codec_epoch": job.codec_epoch,
|
||||
"clock_origin": {
|
||||
"artifact_sha256": _sha256(origin_path),
|
||||
"started_epoch_ns": origin.started_at_epoch_ns,
|
||||
"started_monotonic_ns": origin.started_monotonic_ns,
|
||||
},
|
||||
"selected_sequences": list(sequences),
|
||||
"selection_policy": "exact-independently-decodable-fragments/v1",
|
||||
"ground_truth": False,
|
||||
"authority": {
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_allowed": False,
|
||||
},
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
pack_id = f"mixed-route-review-pack-{identity_sha256}"
|
||||
parent = output_root.resolve()
|
||||
parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
final = parent / pack_id
|
||||
if final.exists():
|
||||
return final
|
||||
staging = Path(tempfile.mkdtemp(prefix=f".{pack_id}.", dir=parent))
|
||||
published = False
|
||||
try:
|
||||
frames_root = staging / "frames"
|
||||
frames_root.mkdir(mode=0o700)
|
||||
timeline_rows: list[dict[str, Any]] = []
|
||||
artifacts: list[dict[str, Any]] = []
|
||||
for frame_index, (sequence, row) in enumerate(
|
||||
zip(sequences, selected, strict=True)
|
||||
):
|
||||
output_path = frames_root / f"frame-{frame_index + 1:06d}.png"
|
||||
segment_path = epoch_root / "segments" / f"{sequence}.m4s"
|
||||
_decode_exact_fragment(
|
||||
ffmpeg=ffmpeg,
|
||||
init_path=epoch_root / "init.mp4",
|
||||
segment_path=segment_path,
|
||||
output_path=output_path,
|
||||
)
|
||||
host_monotonic_ns = int(row["host_monotonic_ns"])
|
||||
if host_monotonic_ns < origin.started_monotonic_ns:
|
||||
raise MixedRouteReviewPackError("selected frame predates the session clock origin")
|
||||
session_seconds = (
|
||||
host_monotonic_ns - origin.started_monotonic_ns
|
||||
) / 1e9
|
||||
timeline_rows.append(
|
||||
{
|
||||
"frame_index": frame_index,
|
||||
"sequence": frame_index + 1,
|
||||
"source_frame_index": sequence - 1,
|
||||
"source_sequence": sequence,
|
||||
"session_seconds": session_seconds,
|
||||
"host_monotonic_ns": row["host_monotonic_ns"],
|
||||
"host_epoch_ns": row["host_epoch_ns"],
|
||||
}
|
||||
)
|
||||
artifacts.append(
|
||||
{
|
||||
"path": output_path.relative_to(staging).as_posix(),
|
||||
"byte_length": output_path.stat().st_size,
|
||||
"sha256": _sha256(output_path),
|
||||
"source_segment_sha256": row["sha256"],
|
||||
}
|
||||
)
|
||||
timeline_path = staging / "timeline.jsonl"
|
||||
timeline_path.write_text(
|
||||
"".join(
|
||||
json.dumps(
|
||||
row,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
+ "\n"
|
||||
for row in timeline_rows
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
manifest = {
|
||||
"schema_version": SCHEMA,
|
||||
"pack_id": pack_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": datetime.now(UTC)
|
||||
.isoformat(timespec="milliseconds")
|
||||
.replace("+00:00", "Z"),
|
||||
"frame_count": len(sequences),
|
||||
"timeline": {
|
||||
"path": timeline_path.name,
|
||||
"byte_length": timeline_path.stat().st_size,
|
||||
"sha256": _sha256(timeline_path),
|
||||
},
|
||||
"frames": artifacts,
|
||||
}
|
||||
(staging / "manifest.json").write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
os.replace(staging, final)
|
||||
published = True
|
||||
finally:
|
||||
if not published:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
return final
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _arguments()
|
||||
result = prepare(
|
||||
job_root=args.job,
|
||||
session_root=args.session,
|
||||
sequences=args.sequences,
|
||||
output_root=args.output_root,
|
||||
ffmpeg_path=args.ffmpeg,
|
||||
)
|
||||
print(json.dumps({"pack_id": result.name, "output": os.fspath(result)}, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seal the complete RAVNOVES004TREE semantic pass into existing LAB V1."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.laboratory.mixed_route_vegetation_review import (
|
||||
seal_mixed_route_full_video_review,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--base-lab-root", type=Path, required=True)
|
||||
parser.add_argument("--job-root", type=Path, required=True)
|
||||
parser.add_argument("--recorded-media-preparation", type=Path, required=True)
|
||||
parser.add_argument("--eomt-root", type=Path, required=True)
|
||||
parser.add_argument("--eomt-profile", type=Path, required=True)
|
||||
parser.add_argument("--ddrnet-root", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
print(
|
||||
seal_mixed_route_full_video_review(
|
||||
base_lab_root=args.base_lab_root,
|
||||
job_root=args.job_root,
|
||||
recorded_media_preparation_path=args.recorded_media_preparation,
|
||||
eomt_root=args.eomt_root,
|
||||
eomt_profile_path=args.eomt_profile,
|
||||
ddrnet_root=args.ddrnet_root,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,901 @@
|
||||
"""Seal RAVNOVES004TREE mixed-route review into the existing vegetation LAB."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import tarfile
|
||||
import tempfile
|
||||
import zipfile
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from k1link.compute.jobs import validate_camera_compute_job
|
||||
|
||||
from k1link.laboratory.vegetation_shadow_lab import (
|
||||
LAB_SCHEMA,
|
||||
RESULT_PREFIX,
|
||||
VegetationShadowLabError,
|
||||
canonical_json,
|
||||
sha256_path,
|
||||
)
|
||||
|
||||
REVIEW_SCHEMA = "missioncore.mixed-route-review-pack/v1"
|
||||
DDRNET_SCHEMA = "missioncore.mixed-route-ddrnet-islands/v1"
|
||||
TGS_SCHEMA = "missioncore.mixed-route-tgs-result/v1"
|
||||
FRAME_COUNT = 10
|
||||
PHASES = (
|
||||
"rural",
|
||||
"rural",
|
||||
"rural",
|
||||
"rural",
|
||||
"rural",
|
||||
"transition",
|
||||
"urban",
|
||||
"urban",
|
||||
"urban",
|
||||
"urban",
|
||||
)
|
||||
TGS_COLORS = {
|
||||
0: (5, 7, 9),
|
||||
1: (132, 188, 86),
|
||||
2: (235, 112, 122),
|
||||
3: (150, 154, 163),
|
||||
}
|
||||
FULL_ROUTE_SOURCE_ID = "RAVNOVES004TREE"
|
||||
FULL_ROUTE_FRAME_COUNT = 6830
|
||||
FULL_ROUTE_JOB_ID = "recorded-camera-eb2783c5480d56bda07c8af0"
|
||||
FULL_ROUTE_INPUT_SHA256 = (
|
||||
"eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715"
|
||||
)
|
||||
FULL_ROUTE_STREAM_SHA256 = (
|
||||
"e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac"
|
||||
)
|
||||
|
||||
|
||||
def _read_json(path: Path, label: str) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise VegetationShadowLabError(f"{label} is invalid") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise VegetationShadowLabError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _artifact(
|
||||
source: Path,
|
||||
staging: Path,
|
||||
relative: str,
|
||||
artifacts: list[dict[str, object]],
|
||||
*,
|
||||
role: str,
|
||||
media_type: str,
|
||||
) -> dict[str, object]:
|
||||
if source.is_symlink() or not source.is_file():
|
||||
raise VegetationShadowLabError(f"mixed-route artifact is unavailable: {relative}")
|
||||
target = staging / relative
|
||||
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
shutil.copyfile(source, target)
|
||||
descriptor = {
|
||||
"role": role,
|
||||
"path": relative,
|
||||
"byte_length": target.stat().st_size,
|
||||
"sha256": sha256_path(target),
|
||||
"media_type": media_type,
|
||||
}
|
||||
artifacts.append(descriptor)
|
||||
return descriptor
|
||||
|
||||
|
||||
def _image_proof(descriptor: dict[str, object]) -> dict[str, object]:
|
||||
return {"path": descriptor["path"], "sha256": descriptor["sha256"]}
|
||||
|
||||
|
||||
def _mask_archive_descriptor(
|
||||
path: Path,
|
||||
relative: str,
|
||||
artifacts: list[dict[str, object]],
|
||||
*,
|
||||
role: str,
|
||||
) -> dict[str, object]:
|
||||
descriptor = {
|
||||
"role": role,
|
||||
"path": relative,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": sha256_path(path),
|
||||
"media_type": "application/zip",
|
||||
}
|
||||
artifacts.append(descriptor)
|
||||
return descriptor
|
||||
|
||||
|
||||
def _repack_eomt_masks(source: Path, destination: Path, frame_count: int) -> None:
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
expected = [f"semantic-masks/frame-{sequence + 1:06d}.png" for sequence in range(frame_count)]
|
||||
try:
|
||||
with (
|
||||
tarfile.open(source, mode="r:gz") as archive,
|
||||
zipfile.ZipFile(
|
||||
destination,
|
||||
mode="x",
|
||||
compression=zipfile.ZIP_STORED,
|
||||
allowZip64=True,
|
||||
) as output,
|
||||
):
|
||||
members = [member for member in archive.getmembers() if member.isfile()]
|
||||
if [member.name.removeprefix("./") for member in members] != expected:
|
||||
raise VegetationShadowLabError("full-route EoMT mask sequence changed")
|
||||
for member, expected_name in zip(members, expected, strict=True):
|
||||
if member.size < 8 or member.size > 1024 * 1024:
|
||||
raise VegetationShadowLabError("full-route EoMT mask size changed")
|
||||
stream = archive.extractfile(member)
|
||||
if stream is None:
|
||||
raise VegetationShadowLabError("full-route EoMT mask is unavailable")
|
||||
output.writestr(
|
||||
f"masks/{Path(expected_name).name}",
|
||||
stream.read(),
|
||||
)
|
||||
except (OSError, tarfile.TarError, zipfile.BadZipFile) as exc:
|
||||
destination.unlink(missing_ok=True)
|
||||
raise VegetationShadowLabError("full-route EoMT archive is invalid") from exc
|
||||
|
||||
|
||||
def _validate_zip_masks(path: Path, frame_count: int) -> None:
|
||||
expected = [f"masks/frame-{sequence + 1:06d}.png" for sequence in range(frame_count)]
|
||||
try:
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
members = archive.infolist()
|
||||
if (
|
||||
[member.filename for member in members] != expected
|
||||
or any(
|
||||
member.is_dir() or member.file_size < 8 or member.file_size > 1024 * 1024
|
||||
for member in members
|
||||
)
|
||||
):
|
||||
raise VegetationShadowLabError("full-route semantic mask sequence changed")
|
||||
except (OSError, zipfile.BadZipFile) as exc:
|
||||
raise VegetationShadowLabError("full-route semantic archive is invalid") from exc
|
||||
|
||||
|
||||
def _full_route_frame_times(media: dict[str, Any], frame_count: int) -> list[int]:
|
||||
epochs = media.get("epochs")
|
||||
start = media.get("timeline_start_seconds")
|
||||
end = media.get("timeline_end_seconds")
|
||||
if (
|
||||
not isinstance(epochs, list)
|
||||
or len(epochs) != 1
|
||||
or not isinstance(start, (int, float))
|
||||
or not isinstance(end, (int, float))
|
||||
):
|
||||
raise VegetationShadowLabError("recorded media timeline changed")
|
||||
epoch = epochs[0]
|
||||
segments = epoch.get("segments") if isinstance(epoch, dict) else None
|
||||
if not isinstance(segments, list) or len(segments) != frame_count:
|
||||
raise VegetationShadowLabError("recorded media segment count changed")
|
||||
starts = [float(start)]
|
||||
previous_end = 0.0
|
||||
for sequence, raw in enumerate(segments, start=1):
|
||||
if (
|
||||
not isinstance(raw, dict)
|
||||
or raw.get("sequence") != sequence
|
||||
or not isinstance(raw.get("end_time_seconds"), (int, float))
|
||||
or float(raw["end_time_seconds"]) <= previous_end
|
||||
):
|
||||
raise VegetationShadowLabError("recorded media segment timeline changed")
|
||||
if sequence < frame_count:
|
||||
starts.append(float(start) + float(raw["end_time_seconds"]))
|
||||
previous_end = float(raw["end_time_seconds"])
|
||||
if abs((float(start) + previous_end) - float(end)) > 0.001:
|
||||
raise VegetationShadowLabError("recorded media duration changed")
|
||||
return [round(value * 1_000_000_000) for value in starts]
|
||||
|
||||
|
||||
def _eomt_taxonomy(profile: dict[str, Any]) -> dict[str, object]:
|
||||
taxonomy = profile.get("target_taxonomy")
|
||||
if not isinstance(taxonomy, dict) or set(taxonomy) != {str(index) for index in range(16)}:
|
||||
raise VegetationShadowLabError("EoMT target taxonomy changed")
|
||||
classes = []
|
||||
for class_id in range(16):
|
||||
digest = hashlib.sha256(f"mission-core-segment-{class_id}".encode()).digest()
|
||||
classes.append(
|
||||
{
|
||||
"class_id": class_id,
|
||||
"label": taxonomy[str(class_id)],
|
||||
"color_rgb": [64 + digest[index] % 176 for index in range(3)],
|
||||
"disposition": "undefined" if class_id == 0 else "prediction",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"schema_version": "missioncore.recorded-eomt-taxonomy/v1",
|
||||
"classes": classes,
|
||||
}
|
||||
|
||||
|
||||
def _render_tgs_costmaps(tgs_root: Path, destination: Path) -> list[Path]:
|
||||
result = _read_json(tgs_root / "result.json", "mixed-route TGS result")
|
||||
evidence = result.get("evidence")
|
||||
costmap = result.get("costmap")
|
||||
if (
|
||||
result.get("schema_version") != TGS_SCHEMA
|
||||
or result.get("status") != "passed-review-only"
|
||||
or not isinstance(evidence, dict)
|
||||
or not isinstance(costmap, dict)
|
||||
or result.get("summary", {}).get("frame_count") != FRAME_COUNT
|
||||
or result.get("authority", {}).get("actuation_allowed") is not False
|
||||
):
|
||||
raise VegetationShadowLabError("mixed-route TGS contract changed")
|
||||
evidence_path = tgs_root / str(evidence.get("path"))
|
||||
if (
|
||||
not evidence_path.is_file()
|
||||
or evidence.get("bytes") != evidence_path.stat().st_size
|
||||
or evidence.get("sha256") != sha256_path(evidence_path)
|
||||
):
|
||||
raise VegetationShadowLabError("mixed-route TGS evidence changed")
|
||||
with np.load(evidence_path, allow_pickle=False) as archive:
|
||||
centers = archive["costmap_cell_centers_xy_m"]
|
||||
states = archive["causal_rolling_1s_costmap_states"]
|
||||
if centers.shape != (2244, 2) or states.shape != (FRAME_COUNT, 2244):
|
||||
raise VegetationShadowLabError("mixed-route TGS costmap shape changed")
|
||||
radius = float(costmap["radius_m"])
|
||||
cell_size = float(costmap["cell_size_m"])
|
||||
size = 600
|
||||
scale = size / (radius * 2.0)
|
||||
outputs: list[Path] = []
|
||||
destination.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
for slot in range(FRAME_COUNT):
|
||||
image = Image.new("RGB", (size, size), TGS_COLORS[0])
|
||||
draw = ImageDraw.Draw(image)
|
||||
half = cell_size * scale / 2.0
|
||||
for center, state in zip(centers, states[slot], strict=True):
|
||||
x = (float(center[0]) + radius) * scale
|
||||
y = (radius - float(center[1])) * scale
|
||||
draw.rectangle((x - half, y - half, x + half, y + half), fill=TGS_COLORS[int(state)])
|
||||
rover_w = 0.8 * scale
|
||||
rover_l = 1.0 * scale
|
||||
cx = size / 2.0
|
||||
cy = size / 2.0
|
||||
draw.rectangle(
|
||||
(cx - rover_w / 2, cy - rover_l / 2, cx + rover_w / 2, cy + rover_l / 2),
|
||||
outline=(255, 255, 255),
|
||||
width=3,
|
||||
)
|
||||
path = destination / f"frame-{slot + 1:06d}.png"
|
||||
image.save(path, format="PNG", optimize=True)
|
||||
outputs.append(path)
|
||||
return outputs
|
||||
|
||||
|
||||
def seal_mixed_route_vegetation_review(
|
||||
*,
|
||||
base_lab_root: Path,
|
||||
review_pack_root: Path,
|
||||
eomt_root: Path,
|
||||
ddrnet_root: Path,
|
||||
tgs_root: Path,
|
||||
output_root: Path,
|
||||
) -> Path:
|
||||
base_root = base_lab_root.resolve(strict=True)
|
||||
base = _read_json(base_root / "result.json", "base vegetation LAB")
|
||||
base_identity = base.get("identity")
|
||||
if (
|
||||
base.get("schema_version") != LAB_SCHEMA
|
||||
or not isinstance(base_identity, dict)
|
||||
or hashlib.sha256(canonical_json(base_identity)).hexdigest()
|
||||
!= base.get("identity_sha256")
|
||||
or base.get("result_id") != base_root.name
|
||||
or not base_root.name.startswith(RESULT_PREFIX)
|
||||
or base.get("authority", {}).get("commands_enabled") is not False
|
||||
):
|
||||
raise VegetationShadowLabError("base vegetation LAB proof changed")
|
||||
|
||||
pack_root = review_pack_root.resolve(strict=True)
|
||||
pack = _read_json(pack_root / "manifest.json", "mixed-route review pack")
|
||||
timeline_path = pack_root / str(pack.get("timeline", {}).get("path"))
|
||||
if (
|
||||
pack.get("schema_version") != REVIEW_SCHEMA
|
||||
or pack.get("frame_count") != FRAME_COUNT
|
||||
or pack.get("identity", {}).get("session_id") != "20260828T130511Z_viewer_live"
|
||||
or pack.get("identity", {}).get("ground_truth") is not False
|
||||
or not timeline_path.is_file()
|
||||
or pack.get("timeline", {}).get("sha256") != sha256_path(timeline_path)
|
||||
):
|
||||
raise VegetationShadowLabError("mixed-route review pack changed")
|
||||
timeline = [json.loads(line) for line in timeline_path.read_text(encoding="utf-8").splitlines()]
|
||||
if len(timeline) != FRAME_COUNT:
|
||||
raise VegetationShadowLabError("mixed-route timeline is incomplete")
|
||||
|
||||
eomt = _read_json(eomt_root / "run-report.partial.json", "mixed-route EoMT result")
|
||||
ddrnet = _read_json(ddrnet_root / "result.json", "mixed-route DDRNet result")
|
||||
tgs = _read_json(tgs_root / "result.json", "mixed-route TGS result")
|
||||
if (
|
||||
eomt.get("input", {}).get("frames_admitted") != FRAME_COUNT
|
||||
or eomt.get("metrics", {}).get("frames_processed") != FRAME_COUNT
|
||||
or eomt.get("ground_truth") is not False
|
||||
or ddrnet.get("schema_version") != DDRNET_SCHEMA
|
||||
or ddrnet.get("source", {}).get("pack_id") != pack["pack_id"]
|
||||
or len(ddrnet.get("frames", [])) != FRAME_COUNT
|
||||
or ddrnet.get("authority", {}).get("candidate_accepted") is not False
|
||||
or tgs.get("schema_version") != TGS_SCHEMA
|
||||
or tgs.get("source", {}).get("review_pack_id") != pack["pack_id"]
|
||||
or tgs.get("summary", {}).get("frame_count") != FRAME_COUNT
|
||||
):
|
||||
raise VegetationShadowLabError("mixed-route model identities differ")
|
||||
|
||||
output_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
temporary = Path(tempfile.mkdtemp(prefix=".mixed-route-vegetation-", dir=output_root))
|
||||
artifacts: list[dict[str, object]] = []
|
||||
try:
|
||||
tgs_images = _render_tgs_costmaps(tgs_root, temporary / ".tgs-render")
|
||||
cases: list[dict[str, object]] = []
|
||||
tgs_anchors = {
|
||||
int(row["slot"]): row
|
||||
for row in tgs["anchors"]
|
||||
if row.get("profile_id") == "causal_rolling_1s"
|
||||
}
|
||||
for slot, row in enumerate(timeline):
|
||||
case_id = f"route-{slot + 1:02d}"
|
||||
relative_root = f"route-review/{case_id}"
|
||||
source_descriptor = _artifact(
|
||||
pack_root / "frames" / f"frame-{slot + 1:06d}.png",
|
||||
temporary,
|
||||
f"{relative_root}/source.png",
|
||||
artifacts,
|
||||
role="mixed-route-source-frame",
|
||||
media_type="image/png",
|
||||
)
|
||||
city_descriptor = _artifact(
|
||||
eomt_root / "overlay-frames" / f"frame-{slot + 1:06d}.png",
|
||||
temporary,
|
||||
f"{relative_root}/city.png",
|
||||
artifacts,
|
||||
role="mixed-route-eomt-overlay",
|
||||
media_type="image/png",
|
||||
)
|
||||
vegetation_descriptor = _artifact(
|
||||
ddrnet_root / "overlay-frames" / f"frame-{slot + 1:06d}.png",
|
||||
temporary,
|
||||
f"{relative_root}/vegetation.png",
|
||||
artifacts,
|
||||
role="mixed-route-ddrnet-overlay",
|
||||
media_type="image/png",
|
||||
)
|
||||
tgs_descriptor = _artifact(
|
||||
tgs_images[slot],
|
||||
temporary,
|
||||
f"{relative_root}/tgs.png",
|
||||
artifacts,
|
||||
role="mixed-route-tgs-costmap",
|
||||
media_type="image/png",
|
||||
)
|
||||
anchor = tgs_anchors[slot]
|
||||
cases.append(
|
||||
{
|
||||
"case_id": case_id,
|
||||
"phase": PHASES[slot],
|
||||
"source_sequence": int(row["source_sequence"]),
|
||||
"session_seconds": float(row["session_seconds"]),
|
||||
"assets": {
|
||||
"source": _image_proof(source_descriptor),
|
||||
"city": _image_proof(city_descriptor),
|
||||
"vegetation": _image_proof(vegetation_descriptor),
|
||||
"tgs": _image_proof(tgs_descriptor),
|
||||
},
|
||||
"tgs": {
|
||||
"ground_cells": int(anchor["ground_cell_count"]),
|
||||
"occupied_cells": int(anchor["nonground_cell_count"]),
|
||||
"rejected_cells": int(anchor["rejected_cell_count"]),
|
||||
"unobserved_cells": int(anchor["unobserved_cell_count"]),
|
||||
},
|
||||
}
|
||||
)
|
||||
shutil.rmtree(temporary / ".tgs-render")
|
||||
|
||||
proofs = {}
|
||||
for key, path in (
|
||||
("base", base_root / "result.json"),
|
||||
("eomt", eomt_root / "run-report.partial.json"),
|
||||
("ddrnet", ddrnet_root / "result.json"),
|
||||
("tgs", tgs_root / "result.json"),
|
||||
):
|
||||
descriptor = _artifact(
|
||||
path,
|
||||
temporary,
|
||||
f"proofs/{key}.json",
|
||||
artifacts,
|
||||
role="mixed-route-proof",
|
||||
media_type="application/json",
|
||||
)
|
||||
proofs[key] = _image_proof(descriptor)
|
||||
_artifact(
|
||||
tgs_root / str(tgs["evidence"]["path"]),
|
||||
temporary,
|
||||
"proofs/tgs-evidence.npz",
|
||||
artifacts,
|
||||
role="mixed-route-tgs-evidence",
|
||||
media_type="application/x-npz",
|
||||
)
|
||||
|
||||
route_review = {
|
||||
"source_id": "RAVNOVES004TREE",
|
||||
"session_id": "20260828T130511Z_viewer_live",
|
||||
"pack_id": pack["pack_id"],
|
||||
"frame_count": FRAME_COUNT,
|
||||
"ground_truth": False,
|
||||
"selection_policy": "same-scene-camera-lidar-aligned-review-islands/v1",
|
||||
"models": {
|
||||
"city": {
|
||||
"name": "EoMT Cityscapes",
|
||||
"frames": FRAME_COUNT,
|
||||
"inference_fps": eomt["metrics"]["inference_frames_per_second"],
|
||||
"end_to_end_p95_ms": eomt["metrics"]["latency_ms"]["end_to_end_ms"]["p95"],
|
||||
},
|
||||
"vegetation": {
|
||||
"name": ddrnet["candidate"]["loaded_model_name"],
|
||||
"result_id": ddrnet["result_id"],
|
||||
"frames": FRAME_COUNT,
|
||||
"latency_p95_ms": ddrnet["timing"]["latency_ms_p95"],
|
||||
},
|
||||
"tgs": {
|
||||
"name": "TRAVEL/TGS causal rolling 1 s",
|
||||
"frames": FRAME_COUNT,
|
||||
"latency_p95_ms": tgs["timing"]["wall_seconds_p95"] * 1000.0,
|
||||
"cell_size_m": tgs["costmap"]["cell_size_m"],
|
||||
"radius_m": tgs["costmap"]["radius_m"],
|
||||
},
|
||||
},
|
||||
"cases": cases,
|
||||
"proofs": proofs,
|
||||
"limitations": [
|
||||
"Ten aligned review islands are not a complete route timeline.",
|
||||
"RAVNOVES004TREE has no manual truth.",
|
||||
"DDRNet vegetation subtypes remain visually noisy and are not planner authority.",
|
||||
"TGS does not prove ditch or negative-obstacle detection.",
|
||||
"People and vehicles require an independent fail-safe detector and STOP path.",
|
||||
],
|
||||
}
|
||||
authority = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_accepted": False,
|
||||
"camera_semantics_can_clear_rigid_geometry": False,
|
||||
}
|
||||
identity = {
|
||||
"lab_id": "lab-v1-vegetation-mission-policy",
|
||||
"base_result_id": base["result_id"],
|
||||
"selected_candidate": base_identity["selected_candidate"],
|
||||
"candidate_metrics": base_identity["candidate_metrics"],
|
||||
"source": {
|
||||
"shadow_session": "RAVNOVES004TREE",
|
||||
"shadow_camera": "sensor.camera.right",
|
||||
"shadow_frame_count": FRAME_COUNT,
|
||||
"video_shadow_frame_count": 0,
|
||||
},
|
||||
"route_review": route_review,
|
||||
"authority": authority,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest()
|
||||
result_id = f"{RESULT_PREFIX}{identity_sha256}"
|
||||
manifest = {
|
||||
"schema_version": LAB_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": datetime.now(UTC).isoformat(),
|
||||
"ground_truth": False,
|
||||
"status": "visual-shadow-ready-policy-not-authorized",
|
||||
"identity": identity,
|
||||
"source": identity["source"],
|
||||
"route_video": None,
|
||||
"route_review": route_review,
|
||||
"method": {
|
||||
"completeness": "bounded-review-islands",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "ravnoves004tree-eomt-ddrnet-causal-tgs-review/v1",
|
||||
},
|
||||
"metrics": {"candidates": base["metrics"]["candidates"]},
|
||||
"decision": {
|
||||
"selected_candidate": base_identity["selected_candidate"],
|
||||
"visual_shadow_ready": True,
|
||||
"full_video_shadow_ready": False,
|
||||
"mission_policy_ready_for_configuration": True,
|
||||
"multilayer_policy_review_ready": True,
|
||||
"navigation_accepted": False,
|
||||
"production_accepted": False,
|
||||
},
|
||||
"limitations": route_review["limitations"],
|
||||
"authority": authority,
|
||||
"catalogs": {"goose": [], "ravnoves": []},
|
||||
"artifacts": artifacts,
|
||||
}
|
||||
(temporary / "result.json").write_bytes(canonical_json(manifest) + b"\n")
|
||||
destination = output_root / result_id
|
||||
if destination.exists():
|
||||
raise VegetationShadowLabError("immutable mixed-route LAB result already exists")
|
||||
os.replace(temporary, destination)
|
||||
return destination
|
||||
except Exception:
|
||||
shutil.rmtree(temporary, ignore_errors=True)
|
||||
raise
|
||||
|
||||
|
||||
def seal_mixed_route_full_video_review(
|
||||
*,
|
||||
base_lab_root: Path,
|
||||
job_root: Path,
|
||||
recorded_media_preparation_path: Path,
|
||||
eomt_root: Path,
|
||||
eomt_profile_path: Path,
|
||||
ddrnet_root: Path,
|
||||
output_root: Path,
|
||||
) -> Path:
|
||||
"""Publish the complete 004 city/nature pass in the existing M4.7 LAB."""
|
||||
|
||||
base_root = base_lab_root.resolve(strict=True)
|
||||
base = _read_json(base_root / "result.json", "base vegetation LAB")
|
||||
base_identity = base.get("identity")
|
||||
if (
|
||||
base.get("schema_version") != LAB_SCHEMA
|
||||
or not isinstance(base_identity, dict)
|
||||
or hashlib.sha256(canonical_json(base_identity)).hexdigest()
|
||||
!= base.get("identity_sha256")
|
||||
or base.get("result_id") != base_root.name
|
||||
or not base_root.name.startswith(RESULT_PREFIX)
|
||||
or base.get("authority", {}).get("commands_enabled") is not False
|
||||
):
|
||||
raise VegetationShadowLabError("base vegetation LAB proof changed")
|
||||
|
||||
job = validate_camera_compute_job(job_root)
|
||||
if (
|
||||
job.job_id != FULL_ROUTE_JOB_ID
|
||||
or job.input_sha256 != FULL_ROUTE_INPUT_SHA256
|
||||
or job.session_id != "20260828T130511Z_viewer_live"
|
||||
or job.source_id != "sensor.camera.right"
|
||||
or job.segment_count != FULL_ROUTE_FRAME_COUNT
|
||||
):
|
||||
raise VegetationShadowLabError("full-route camera job changed")
|
||||
|
||||
eomt = _read_json(eomt_root / "result.json", "full-route EoMT result")
|
||||
eomt_report = _read_json(eomt_root / "run-report.json", "full-route EoMT report")
|
||||
decode_repair = _read_json(
|
||||
eomt_root / "decode-repair.json",
|
||||
"full-route video decode repair",
|
||||
)
|
||||
eomt_input = eomt_report.get("input")
|
||||
eomt_metrics = eomt_report.get("metrics")
|
||||
if (
|
||||
eomt.get("schema_version") != "missioncore.recorded-perception-result/v2"
|
||||
or eomt.get("ground_truth") is not False
|
||||
or eomt.get("frames_processed") != FULL_ROUTE_FRAME_COUNT
|
||||
or not isinstance(eomt_input, dict)
|
||||
or eomt_input.get("job_id") != job.job_id
|
||||
or eomt_input.get("input_sha256") != job.input_sha256
|
||||
or eomt_input.get("frames_admitted") != FULL_ROUTE_FRAME_COUNT
|
||||
or not isinstance(eomt_metrics, dict)
|
||||
or eomt_metrics.get("frames_processed") != FULL_ROUTE_FRAME_COUNT
|
||||
):
|
||||
raise VegetationShadowLabError("full-route EoMT contract changed")
|
||||
if (
|
||||
decode_repair.get("schema_version")
|
||||
!= "missioncore.recorded-video-decode-repair/v1"
|
||||
or decode_repair.get("decoder") != "ffmpeg-h264_cuvid-output-corrupt"
|
||||
or decode_repair.get("packets_requested") != FULL_ROUTE_FRAME_COUNT
|
||||
or decode_repair.get("frames_decoded") != FULL_ROUTE_FRAME_COUNT - 1
|
||||
or decode_repair.get("repaired_frame_count") != 1
|
||||
or decode_repair.get("repairs")
|
||||
!= [
|
||||
{
|
||||
"sequence": 6092,
|
||||
"packet_pts": 55656450,
|
||||
"method": "duplicate-previous-decoded-frame",
|
||||
}
|
||||
]
|
||||
):
|
||||
raise VegetationShadowLabError("full-route video decode repair changed")
|
||||
eomt_artifacts = {
|
||||
item.get("kind"): item
|
||||
for item in eomt.get("artifacts", [])
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
eomt_archive_proof = eomt_artifacts.get("panoptic-mask-archive")
|
||||
if not isinstance(eomt_archive_proof, dict):
|
||||
raise VegetationShadowLabError("full-route EoMT mask proof is missing")
|
||||
eomt_archive = eomt_root / str(eomt_archive_proof.get("path"))
|
||||
if (
|
||||
not eomt_archive.is_file()
|
||||
or eomt_archive.stat().st_size != eomt_archive_proof.get("byte_length")
|
||||
or sha256_path(eomt_archive) != eomt_archive_proof.get("sha256")
|
||||
):
|
||||
raise VegetationShadowLabError("full-route EoMT mask proof changed")
|
||||
|
||||
ddrnet = _read_json(ddrnet_root / "result.json", "full-route DDRNet result")
|
||||
ddrnet_decode_repair = _read_json(
|
||||
ddrnet_root / "decode-repair.json",
|
||||
"full-route DDRNet video decode repair",
|
||||
)
|
||||
ddrnet_source = ddrnet.get("source")
|
||||
ddrnet_video = ddrnet.get("video_semantics")
|
||||
if (
|
||||
ddrnet.get("schema_version") != "missioncore.lab-v1-goose-vegetation-run/v1"
|
||||
or ddrnet.get("mode") != "ravnoves-video"
|
||||
or ddrnet.get("candidate", {}).get("candidate_key") != "ddrnet"
|
||||
or not isinstance(ddrnet_source, dict)
|
||||
or ddrnet_source.get("source_id")
|
||||
!= f"{FULL_ROUTE_SOURCE_ID}/right-{FULL_ROUTE_STREAM_SHA256}"
|
||||
or ddrnet_source.get("input_count") != FULL_ROUTE_FRAME_COUNT
|
||||
or ddrnet_source.get("ground_truth_available") is not False
|
||||
or not isinstance(ddrnet_video, dict)
|
||||
or ddrnet_video.get("base_m4_result_id") is not None
|
||||
or ddrnet.get("authority", {}).get("navigation_accepted") is not False
|
||||
or ddrnet.get("authority", {}).get("actuation_accepted") is not False
|
||||
):
|
||||
raise VegetationShadowLabError("full-route DDRNet contract changed")
|
||||
if ddrnet_decode_repair != decode_repair:
|
||||
raise VegetationShadowLabError("full-route model decoders disagree")
|
||||
ddrnet_archive_proof = ddrnet_video.get("mask_archive")
|
||||
ddrnet_taxonomy = ddrnet_video.get("taxonomy")
|
||||
if (
|
||||
not isinstance(ddrnet_archive_proof, dict)
|
||||
or ddrnet_archive_proof.get("frame_count") != FULL_ROUTE_FRAME_COUNT
|
||||
or not isinstance(ddrnet_taxonomy, dict)
|
||||
):
|
||||
raise VegetationShadowLabError("full-route DDRNet mask proof changed")
|
||||
ddrnet_archive = ddrnet_root / str(ddrnet_archive_proof.get("path"))
|
||||
if (
|
||||
not ddrnet_archive.is_file()
|
||||
or ddrnet_archive.stat().st_size != ddrnet_archive_proof.get("byte_length")
|
||||
or sha256_path(ddrnet_archive) != ddrnet_archive_proof.get("sha256")
|
||||
):
|
||||
raise VegetationShadowLabError("full-route DDRNet archive changed")
|
||||
_validate_zip_masks(ddrnet_archive, FULL_ROUTE_FRAME_COUNT)
|
||||
|
||||
media_document = _read_json(
|
||||
recorded_media_preparation_path.resolve(strict=True),
|
||||
"recorded media preparation",
|
||||
)
|
||||
media = media_document.get("manifest")
|
||||
if (
|
||||
media_document.get("schema_version") != "missioncore.recorded-media-preparation/v3"
|
||||
or media_document.get("session_id") != job.session_id
|
||||
or media_document.get("artifact_id") != "recorded-video-6a3945242828a038"
|
||||
or media_document.get("checksum_sha256")
|
||||
!= "557e61f2839140dc9f97b5aea855c576b0616573080dff5d2852ab1df0558665"
|
||||
or not isinstance(media, dict)
|
||||
or media.get("source_id") != "recorded.camera.6a3945242828a038"
|
||||
or media.get("generation_sha256")
|
||||
!= "b073ea1e7babf1c77a664e1a5b95e3702d0e05b0e34c1e85a7c67a6f8b392ded"
|
||||
or media.get("byte_length") != 551674491
|
||||
or media.get("timeline_start_seconds") != job.timeline_start_seconds
|
||||
or media.get("timeline_end_seconds") != job.timeline_end_seconds
|
||||
or media.get("synchronization") != "host-arrival-best-effort"
|
||||
):
|
||||
raise VegetationShadowLabError("recorded media preparation changed")
|
||||
frame_times_ns = _full_route_frame_times(media, FULL_ROUTE_FRAME_COUNT)
|
||||
eomt_profile = _read_json(eomt_profile_path.resolve(strict=True), "EoMT profile")
|
||||
eomt_taxonomy = _eomt_taxonomy(eomt_profile)
|
||||
|
||||
output_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
temporary = Path(tempfile.mkdtemp(prefix=".mixed-route-full-video-", dir=output_root))
|
||||
artifacts: list[dict[str, object]] = []
|
||||
try:
|
||||
eomt_destination = temporary / "video" / "eomt-semantic-masks.zip"
|
||||
_repack_eomt_masks(eomt_archive, eomt_destination, FULL_ROUTE_FRAME_COUNT)
|
||||
_validate_zip_masks(eomt_destination, FULL_ROUTE_FRAME_COUNT)
|
||||
eomt_descriptor = _mask_archive_descriptor(
|
||||
eomt_destination,
|
||||
"video/eomt-semantic-masks.zip",
|
||||
artifacts,
|
||||
role="full-route-eomt-semantic-mask-archive",
|
||||
)
|
||||
ddrnet_descriptor = _artifact(
|
||||
ddrnet_archive,
|
||||
temporary,
|
||||
"video/ddrnet-semantic-masks.zip",
|
||||
artifacts,
|
||||
role="full-route-ddrnet-semantic-mask-archive",
|
||||
media_type="application/zip",
|
||||
)
|
||||
_validate_zip_masks(
|
||||
temporary / "video" / "ddrnet-semantic-masks.zip",
|
||||
FULL_ROUTE_FRAME_COUNT,
|
||||
)
|
||||
timeline_destination = temporary / "video" / "frame-source-times-ns.bin"
|
||||
timeline_destination.write_bytes(
|
||||
struct.pack(f"<{FULL_ROUTE_FRAME_COUNT}Q", *frame_times_ns)
|
||||
)
|
||||
timeline_descriptor = {
|
||||
"role": "full-route-frame-timeline",
|
||||
"path": "video/frame-source-times-ns.bin",
|
||||
"byte_length": timeline_destination.stat().st_size,
|
||||
"sha256": sha256_path(timeline_destination),
|
||||
"media_type": "application/octet-stream",
|
||||
}
|
||||
artifacts.append(timeline_descriptor)
|
||||
proof_descriptors: dict[str, dict[str, object]] = {}
|
||||
for key, path in (
|
||||
("base", base_root / "result.json"),
|
||||
("job", job.manifest_path),
|
||||
("media", recorded_media_preparation_path.resolve(strict=True)),
|
||||
("eomt", eomt_root / "result.json"),
|
||||
("eomt_report", eomt_root / "run-report.json"),
|
||||
("decode_repair", eomt_root / "decode-repair.json"),
|
||||
("ddrnet", ddrnet_root / "result.json"),
|
||||
("ddrnet_decode_repair", ddrnet_root / "decode-repair.json"),
|
||||
):
|
||||
descriptor = _artifact(
|
||||
path,
|
||||
temporary,
|
||||
f"proofs/{key}.json",
|
||||
artifacts,
|
||||
role="full-route-proof",
|
||||
media_type="application/json",
|
||||
)
|
||||
proof_descriptors[key] = _image_proof(descriptor)
|
||||
|
||||
full_route = {
|
||||
"source_id": FULL_ROUTE_SOURCE_ID,
|
||||
"session_id": job.session_id,
|
||||
"source_job_id": job.job_id,
|
||||
"source_job_input_sha256": job.input_sha256,
|
||||
"source_stream_sha256": FULL_ROUTE_STREAM_SHA256,
|
||||
"recorded_media_source_id": media["source_id"],
|
||||
"recorded_media_generation_sha256": media["generation_sha256"],
|
||||
"frame_count": FULL_ROUTE_FRAME_COUNT,
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"timeline_start_seconds": job.timeline_start_seconds,
|
||||
"timeline_end_seconds": job.timeline_end_seconds,
|
||||
"timeline": {
|
||||
"path": timeline_descriptor["path"],
|
||||
"sha256": timeline_descriptor["sha256"],
|
||||
"byte_length": timeline_descriptor["byte_length"],
|
||||
"encoding": "uint64-le-nanoseconds",
|
||||
"frame_count": FULL_ROUTE_FRAME_COUNT,
|
||||
},
|
||||
"ground_truth": False,
|
||||
"decode_repair": {
|
||||
"repaired_frame_count": 1,
|
||||
"sequence": 6092,
|
||||
"method": "duplicate-previous-decoded-frame",
|
||||
"proofs": {
|
||||
"eomt": proof_descriptors["decode_repair"],
|
||||
"ddrnet": proof_descriptors["ddrnet_decode_repair"],
|
||||
},
|
||||
},
|
||||
"layers": {
|
||||
"city": {
|
||||
"name": "EoMT Cityscapes",
|
||||
"result_id": eomt["result_id"],
|
||||
"frame_count": FULL_ROUTE_FRAME_COUNT,
|
||||
"taxonomy": eomt_taxonomy,
|
||||
"mask_archive": {
|
||||
"path": eomt_descriptor["path"],
|
||||
"sha256": eomt_descriptor["sha256"],
|
||||
"byte_length": eomt_descriptor["byte_length"],
|
||||
},
|
||||
"inference_fps": eomt_metrics["inference_frames_per_second"],
|
||||
"latency_p95_ms": eomt_metrics["latency_ms"]["end_to_end_ms"]["p95"],
|
||||
"peak_reserved_vram_bytes": int(
|
||||
float(eomt_metrics["cuda_peak_memory_reserved_mib"]) * 1024 * 1024
|
||||
),
|
||||
},
|
||||
"vegetation": {
|
||||
"name": ddrnet["candidate"]["loaded_model_name"],
|
||||
"result_id": ddrnet["result_id"],
|
||||
"frame_count": FULL_ROUTE_FRAME_COUNT,
|
||||
"taxonomy": ddrnet_taxonomy,
|
||||
"mask_archive": {
|
||||
"path": ddrnet_descriptor["path"],
|
||||
"sha256": ddrnet_descriptor["sha256"],
|
||||
"byte_length": ddrnet_descriptor["byte_length"],
|
||||
},
|
||||
"inference_fps": ddrnet["timing"]["throughput_fps_from_mean_inference"],
|
||||
"latency_p95_ms": ddrnet["timing"]["latency_ms_p95"],
|
||||
"peak_reserved_vram_bytes": ddrnet["resource"]["peak_reserved_vram_bytes"],
|
||||
},
|
||||
},
|
||||
"proofs": proof_descriptors,
|
||||
"limitations": [
|
||||
"RAVNOVES004TREE has no manual route truth.",
|
||||
"One corrupt H.264 packet at sequence 6092 was represented by the previous decoded frame; the repair is sealed as evidence.",
|
||||
"EoMT and DDRNet were executed sequentially, not as a concurrent realtime stack.",
|
||||
"DDRNet vegetation subtypes remain prediction-only and are not planner authority.",
|
||||
"This full-video pass does not add full-route TGS, ditch or negative-obstacle proof.",
|
||||
"People and vehicles still require an independent fail-safe detector and STOP path.",
|
||||
],
|
||||
}
|
||||
authority = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_accepted": False,
|
||||
"camera_semantics_can_clear_rigid_geometry": False,
|
||||
}
|
||||
identity = {
|
||||
"lab_id": "lab-v1-vegetation-mission-policy",
|
||||
"base_result_id": base["result_id"],
|
||||
"selected_candidate": base_identity["selected_candidate"],
|
||||
"candidate_metrics": base_identity["candidate_metrics"],
|
||||
"source": {
|
||||
"shadow_session": FULL_ROUTE_SOURCE_ID,
|
||||
"shadow_camera": job.source_id,
|
||||
"shadow_frame_count": FULL_ROUTE_FRAME_COUNT,
|
||||
"video_shadow_frame_count": FULL_ROUTE_FRAME_COUNT,
|
||||
},
|
||||
"route_full_review": full_route,
|
||||
"authority": authority,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest()
|
||||
result_id = f"{RESULT_PREFIX}{identity_sha256}"
|
||||
manifest = {
|
||||
"schema_version": LAB_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": datetime.now(UTC).isoformat(),
|
||||
"ground_truth": False,
|
||||
"status": "visual-shadow-ready-policy-not-authorized",
|
||||
"identity": identity,
|
||||
"source": identity["source"],
|
||||
"route_video": None,
|
||||
"route_review": None,
|
||||
"route_full_review": full_route,
|
||||
"method": {
|
||||
"completeness": "complete",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
},
|
||||
"metrics": {"candidates": base["metrics"]["candidates"]},
|
||||
"decision": {
|
||||
"selected_candidate": base_identity["selected_candidate"],
|
||||
"visual_shadow_ready": True,
|
||||
"full_video_shadow_ready": True,
|
||||
"mission_policy_ready_for_configuration": True,
|
||||
"multilayer_policy_review_ready": True,
|
||||
"navigation_accepted": False,
|
||||
"production_accepted": False,
|
||||
},
|
||||
"limitations": full_route["limitations"],
|
||||
"authority": authority,
|
||||
"catalogs": {"goose": [], "ravnoves": []},
|
||||
"artifacts": artifacts,
|
||||
}
|
||||
(temporary / "result.json").write_bytes(canonical_json(manifest) + b"\n")
|
||||
destination = output_root / result_id
|
||||
if destination.exists():
|
||||
raise VegetationShadowLabError("immutable full-route LAB result already exists")
|
||||
os.replace(temporary, destination)
|
||||
return destination
|
||||
except Exception:
|
||||
shutil.rmtree(temporary, ignore_errors=True)
|
||||
raise
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--base-lab-root", type=Path, required=True)
|
||||
parser.add_argument("--review-pack-root", type=Path, required=True)
|
||||
parser.add_argument("--eomt-root", type=Path, required=True)
|
||||
parser.add_argument("--ddrnet-root", type=Path, required=True)
|
||||
parser.add_argument("--tgs-root", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
print(
|
||||
seal_mixed_route_vegetation_review(
|
||||
base_lab_root=args.base_lab_root,
|
||||
review_pack_root=args.review_pack_root,
|
||||
eomt_root=args.eomt_root,
|
||||
ddrnet_root=args.ddrnet_root,
|
||||
tgs_root=args.tgs_root,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Seal a benchmark-only vegetation result into its archival LAB namespace."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any, Final
|
||||
|
||||
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
|
||||
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
|
||||
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA
|
||||
|
||||
_SOURCE_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||
work_id="lab-v1-vegetation-shadow",
|
||||
runtime_relative_root=PurePosixPath("lab-v1-vegetation/results"),
|
||||
result_id_prefix="lab-v1-vegetation-shadow",
|
||||
document_name="result.json",
|
||||
result_schema_version=LAB_SCHEMA,
|
||||
)
|
||||
_ARCHIVE_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||
work_id="lab-v1-vegetation-benchmark",
|
||||
runtime_relative_root=PurePosixPath("lab-v1-vegetation-benchmark/results"),
|
||||
result_id_prefix="lab-v1-vegetation-benchmark",
|
||||
document_name="result.json",
|
||||
result_schema_version=LAB_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
class VegetationBenchmarkArchiveError(ValueError):
|
||||
"""The source result is not a valid benchmark-only immutable result."""
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise VegetationBenchmarkArchiveError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def seal_vegetation_benchmark_archive(
|
||||
*,
|
||||
source_result_root: Path,
|
||||
output_root: Path,
|
||||
) -> Path:
|
||||
source = source_result_root.resolve(strict=True)
|
||||
verify_laboratory_evidence_result(_SOURCE_DEFINITION, source)
|
||||
manifest = _object(
|
||||
json.loads((source / "result.json").read_text("utf-8")),
|
||||
"source result",
|
||||
)
|
||||
if manifest.get("route_video") is not None:
|
||||
raise VegetationBenchmarkArchiveError("benchmark archive source contains route video")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list):
|
||||
raise VegetationBenchmarkArchiveError("source artifacts are invalid")
|
||||
|
||||
identity = copy.deepcopy(_object(manifest.get("identity"), "source identity"))
|
||||
identity.update(
|
||||
{
|
||||
"lab_id": "lab-v1-vegetation-benchmark-archive",
|
||||
"archived_from_result_id": source.name,
|
||||
}
|
||||
)
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"lab-v1-vegetation-benchmark-{identity_sha256}"
|
||||
|
||||
output_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
destination = output_root / result_id
|
||||
if destination.exists():
|
||||
verify_laboratory_evidence_result(_ARCHIVE_DEFINITION, destination)
|
||||
return destination
|
||||
|
||||
temporary = Path(tempfile.mkdtemp(prefix=".vegetation-benchmark-", dir=output_root))
|
||||
try:
|
||||
for raw in artifacts:
|
||||
descriptor = _object(raw, "artifact descriptor")
|
||||
relative_text = descriptor.get("path")
|
||||
if not isinstance(relative_text, str):
|
||||
raise VegetationBenchmarkArchiveError("artifact path is invalid")
|
||||
relative = PurePosixPath(relative_text)
|
||||
if relative.is_absolute() or any(part in {"", ".", ".."} for part in relative.parts):
|
||||
raise VegetationBenchmarkArchiveError("artifact path is unsafe")
|
||||
source_path = source.joinpath(*relative.parts)
|
||||
destination_path = temporary.joinpath(*relative.parts)
|
||||
destination_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
shutil.copyfile(source_path, destination_path)
|
||||
|
||||
archived = copy.deepcopy(manifest)
|
||||
archived.update(
|
||||
{
|
||||
"result_id": result_id,
|
||||
"identity": identity,
|
||||
"identity_sha256": identity_sha256,
|
||||
"archived_from_result_id": source.name,
|
||||
}
|
||||
)
|
||||
(temporary / "result.json").write_bytes(_canonical_json(archived) + b"\n")
|
||||
temporary.rename(destination)
|
||||
verify_laboratory_evidence_result(_ARCHIVE_DEFINITION, destination)
|
||||
return destination
|
||||
except Exception:
|
||||
shutil.rmtree(temporary, ignore_errors=True)
|
||||
raise
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source-result-root", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
print(
|
||||
seal_vegetation_benchmark_archive(
|
||||
source_result_root=args.source_result_root,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"VegetationBenchmarkArchiveError",
|
||||
"seal_vegetation_benchmark_archive",
|
||||
]
|
||||
@@ -112,6 +112,7 @@ def seal_vegetation_policy_review(
|
||||
mission_policy_path: Path,
|
||||
provider_label_map_path: Path,
|
||||
m49_tgs_full_shadow_root: Path,
|
||||
valid_fov_mask_path: Path,
|
||||
output_root: Path,
|
||||
created_at_utc: str | None = None,
|
||||
) -> Path:
|
||||
@@ -142,6 +143,7 @@ def seal_vegetation_policy_review(
|
||||
raise VegetationPolicyReviewError("fine mask archive identity changed")
|
||||
raw_archive_path = base_root / "video" / "ddrnet-semantic-masks.zip"
|
||||
fine_taxonomy = _object(base_route.get("taxonomy"), "fine taxonomy")
|
||||
valid_fov_source = valid_fov_mask_path.resolve(strict=True)
|
||||
|
||||
output_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
temporary = Path(tempfile.mkdtemp(prefix=".lab-v1-policy-", dir=output_root))
|
||||
@@ -152,11 +154,23 @@ def seal_vegetation_policy_review(
|
||||
artifacts=base.get("artifacts"),
|
||||
)
|
||||
policy_archive = temporary / "video" / "coarse-material-policy-masks.zip"
|
||||
valid_fov_destination = temporary / "video" / "valid-fov-mask.png"
|
||||
valid_fov_destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
shutil.copyfile(valid_fov_source, valid_fov_destination)
|
||||
valid_fov_proof = {
|
||||
"role": "route-camera-valid-fov-mask",
|
||||
"path": "video/valid-fov-mask.png",
|
||||
"byte_length": valid_fov_destination.stat().st_size,
|
||||
"sha256": sha256_path(valid_fov_destination),
|
||||
"media_type": "image/png",
|
||||
}
|
||||
artifacts.append(valid_fov_proof)
|
||||
policy_counts = build_policy_mask_archive(
|
||||
source_archive=raw_archive_path,
|
||||
destination_archive=policy_archive,
|
||||
fine_taxonomy=fine_taxonomy,
|
||||
provider_label_map=provider_map,
|
||||
valid_fov_mask=valid_fov_destination,
|
||||
)
|
||||
policy_archive_proof = {
|
||||
"role": "route-coarse-material-mask-archive",
|
||||
@@ -180,6 +194,11 @@ def seal_vegetation_policy_review(
|
||||
"taxonomy": policy_taxonomy(),
|
||||
"aggregate_prediction_pixels": policy_counts,
|
||||
"linked_tgs_result_id": tgs.result_id,
|
||||
"valid_fov": {
|
||||
"mask_path": valid_fov_proof["path"],
|
||||
"mask_sha256": valid_fov_proof["sha256"],
|
||||
"outside_valid_fov_class_id": 9,
|
||||
},
|
||||
"policy": {
|
||||
"profile_id": mission_policy["profile_id"],
|
||||
"profile_sha256": sha256_path(mission_policy_path),
|
||||
@@ -196,6 +215,7 @@ def seal_vegetation_policy_review(
|
||||
"spatial_safety_veto_layer": "M4.9 full TGS gravity-local costmap",
|
||||
"temporal_consensus_owner": "TGS causal rolling 1 s and metric obstacle tracks",
|
||||
"camera_semantic_temporal_filter": "none",
|
||||
"camera_valid_fov_filter": "sealed exact KB4 valid-FOV mask",
|
||||
"reason": "No admitted TGS-to-camera pixel projection exists.",
|
||||
},
|
||||
}
|
||||
@@ -238,7 +258,7 @@ def seal_vegetation_policy_review(
|
||||
"Vegetation semantics never clears YOLOX, LiDAR, metric obstacle "
|
||||
"or TGS vetoes."
|
||||
),
|
||||
"Undefined pixels outside the 600x600 center crop remain fail-closed.",
|
||||
"Pixels outside the exact KB4 valid FOV are transparent UNOBSERVED evidence.",
|
||||
(
|
||||
"TGS remains in gravity-local space; no uncalibrated pixel "
|
||||
"projection is fabricated."
|
||||
@@ -269,6 +289,7 @@ def main() -> None:
|
||||
parser.add_argument("--mission-policy-path", type=Path, required=True)
|
||||
parser.add_argument("--provider-label-map-path", type=Path, required=True)
|
||||
parser.add_argument("--m49-tgs-full-shadow-root", type=Path, required=True)
|
||||
parser.add_argument("--valid-fov-mask-path", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
print(seal_vegetation_policy_review(**vars(args)))
|
||||
|
||||
@@ -90,6 +90,14 @@ POLICY_CLASSES: Final = (
|
||||
"material_class": "vegetation_unknown",
|
||||
"evidence_state": "VEGETATION_UNKNOWN",
|
||||
},
|
||||
{
|
||||
"class_id": 9,
|
||||
"label": "OUTSIDE VALID FOV · NO SENSOR EVIDENCE",
|
||||
"color_rgb": [0, 0, 0],
|
||||
"disposition": "undefined",
|
||||
"material_class": None,
|
||||
"evidence_state": "UNOBSERVED",
|
||||
},
|
||||
)
|
||||
|
||||
_MATERIAL_TO_CLASS: Final = {
|
||||
@@ -155,10 +163,18 @@ def build_policy_mask_archive(
|
||||
destination_archive: Path,
|
||||
fine_taxonomy: dict[str, object],
|
||||
provider_label_map: dict[str, Any],
|
||||
valid_fov_mask: Path,
|
||||
) -> list[int]:
|
||||
"""Map every fine mask to coarse evidence; safety vetoes remain separate layers."""
|
||||
|
||||
lut = fine_to_policy_lut(fine_taxonomy, provider_label_map)
|
||||
try:
|
||||
with Image.open(valid_fov_mask) as image:
|
||||
valid_fov = np.asarray(image.convert("L"), dtype=np.uint8) > 0
|
||||
except OSError as exc:
|
||||
raise VegetationPolicyVideoError("valid-FOV mask is unreadable") from exc
|
||||
if valid_fov.shape != (HEIGHT, WIDTH) or not np.any(valid_fov) or np.all(valid_fov):
|
||||
raise VegetationPolicyVideoError("valid-FOV mask geometry is invalid")
|
||||
counts = np.zeros(len(POLICY_CLASSES), dtype=np.int64)
|
||||
destination_archive.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
try:
|
||||
@@ -175,6 +191,7 @@ def build_policy_mask_archive(
|
||||
f"fine mask {member} has shape {fine.shape}, expected {(HEIGHT, WIDTH)}"
|
||||
)
|
||||
coarse = lut[fine]
|
||||
coarse[~valid_fov] = 9
|
||||
counts += np.bincount(
|
||||
coarse.reshape(-1),
|
||||
minlength=len(POLICY_CLASSES),
|
||||
|
||||
@@ -327,6 +327,7 @@ def seal_vegetation_shadow_lab(
|
||||
mission_policy_path: Path | None = None,
|
||||
provider_label_map_path: Path | None = None,
|
||||
m49_tgs_full_shadow_root: Path | None = None,
|
||||
valid_fov_mask_path: Path | None = None,
|
||||
) -> Path:
|
||||
roots = {
|
||||
("ddrnet", "goose"): ddrnet_goose_root.resolve(),
|
||||
@@ -349,6 +350,7 @@ def seal_vegetation_shadow_lab(
|
||||
mission_policy_path,
|
||||
provider_label_map_path,
|
||||
m49_tgs_full_shadow_root,
|
||||
valid_fov_mask_path,
|
||||
)
|
||||
if any(value is not None for value in policy_inputs) and not all(
|
||||
value is not None for value in policy_inputs
|
||||
@@ -385,6 +387,7 @@ def seal_vegetation_shadow_lab(
|
||||
mission_policy_path is not None
|
||||
and provider_label_map_path is not None
|
||||
and m49_tgs_full_shadow_root is not None
|
||||
and valid_fov_mask_path is not None
|
||||
and route_video is not None
|
||||
):
|
||||
repository_root = mission_policy_path.resolve().parents[2]
|
||||
@@ -543,13 +546,25 @@ def seal_vegetation_shadow_lab(
|
||||
and linked_tgs_result_id is not None
|
||||
and mission_policy_path is not None
|
||||
and provider_label_map_path is not None
|
||||
and valid_fov_mask_path is not None
|
||||
):
|
||||
policy_archive = temporary / "video" / "coarse-material-policy-masks.zip"
|
||||
valid_fov_destination = temporary / "video" / "valid-fov-mask.png"
|
||||
shutil.copyfile(valid_fov_mask_path.resolve(strict=True), valid_fov_destination)
|
||||
valid_fov_descriptor = {
|
||||
"role": "route-camera-valid-fov-mask",
|
||||
"path": "video/valid-fov-mask.png",
|
||||
"byte_length": valid_fov_destination.stat().st_size,
|
||||
"sha256": sha256_path(valid_fov_destination),
|
||||
"media_type": "image/png",
|
||||
}
|
||||
artifacts.append(valid_fov_descriptor)
|
||||
policy_counts = build_policy_mask_archive(
|
||||
source_archive=route_video_archive,
|
||||
destination_archive=policy_archive,
|
||||
fine_taxonomy=_object(route_video["taxonomy"], "fine video taxonomy"),
|
||||
provider_label_map=provider_label_map,
|
||||
valid_fov_mask=valid_fov_destination,
|
||||
)
|
||||
policy_descriptor = {
|
||||
"role": "route-coarse-material-mask-archive",
|
||||
@@ -571,6 +586,11 @@ def seal_vegetation_shadow_lab(
|
||||
"taxonomy": policy_taxonomy(),
|
||||
"aggregate_prediction_pixels": policy_counts,
|
||||
"linked_tgs_result_id": linked_tgs_result_id,
|
||||
"valid_fov": {
|
||||
"mask_path": valid_fov_descriptor["path"],
|
||||
"mask_sha256": valid_fov_descriptor["sha256"],
|
||||
"outside_valid_fov_class_id": 9,
|
||||
},
|
||||
"policy": {
|
||||
"profile_id": mission_policy["profile_id"],
|
||||
"profile_sha256": sha256_path(mission_policy_path),
|
||||
@@ -591,6 +611,7 @@ def seal_vegetation_shadow_lab(
|
||||
"TGS causal rolling 1 s and metric obstacle tracks"
|
||||
),
|
||||
"camera_semantic_temporal_filter": "none",
|
||||
"camera_valid_fov_filter": "sealed exact KB4 valid-FOV mask",
|
||||
"reason": "No admitted TGS-to-camera pixel projection exists.",
|
||||
},
|
||||
}
|
||||
@@ -680,7 +701,11 @@ def seal_vegetation_shadow_lab(
|
||||
)
|
||||
),
|
||||
"Vegetation semantics never clears rigid LiDAR/TGS occupancy.",
|
||||
"Undefined pixels outside the 600x600 center crop remain fail-closed.",
|
||||
(
|
||||
"Pixels outside the exact KB4 valid FOV are transparent UNOBSERVED evidence."
|
||||
if mission_policy is not None
|
||||
else "Undefined pixels outside the 600x600 center crop remain fail-closed."
|
||||
),
|
||||
*(
|
||||
[
|
||||
(
|
||||
@@ -723,6 +748,7 @@ def _parse_args() -> argparse.Namespace:
|
||||
parser.add_argument("--mission-policy-path", type=Path)
|
||||
parser.add_argument("--provider-label-map-path", type=Path)
|
||||
parser.add_argument("--m49-tgs-full-shadow-root", type=Path)
|
||||
parser.add_argument("--valid-fov-mask-path", type=Path)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -739,6 +765,7 @@ def main() -> None:
|
||||
mission_policy_path=args.mission_policy_path,
|
||||
provider_label_map_path=args.provider_label_map_path,
|
||||
m49_tgs_full_shadow_root=args.m49_tgs_full_shadow_root,
|
||||
valid_fov_mask_path=args.valid_fov_mask_path,
|
||||
)
|
||||
print(destination)
|
||||
|
||||
|
||||
@@ -767,6 +767,24 @@ def _discover_bundle_members(root: Path, entrypoint: str, source_format: str) ->
|
||||
return _logical_path(logical, "descriptor member")
|
||||
|
||||
members = {entrypoint}
|
||||
source_meshes: list[str] = []
|
||||
for candidate in root.rglob("*"):
|
||||
if candidate.is_symlink():
|
||||
raise GaussianPipelineIntegrityError(
|
||||
"Gaussian source bundle contains a symlink"
|
||||
)
|
||||
if not candidate.is_file():
|
||||
continue
|
||||
logical_path = candidate.relative_to(root).as_posix()
|
||||
if is_xgrids_source_mesh_path(logical_path):
|
||||
source_meshes.append(logical_path)
|
||||
source_meshes.sort(key=lambda value: value.encode("utf-8"))
|
||||
if len(source_meshes) > 1:
|
||||
raise GaussianPipelineIntegrityError(
|
||||
"Gaussian source bundle must contain at most one PLY mesh inside Mesh_Files"
|
||||
)
|
||||
if source_meshes:
|
||||
members.add(source_meshes[0])
|
||||
if source_format == "lcc":
|
||||
members.update({related("index.bin"), related("data.bin")})
|
||||
file_type = document.get("fileType")
|
||||
@@ -810,6 +828,11 @@ def _discover_bundle_members(root: Path, entrypoint: str, source_format: str) ->
|
||||
return members
|
||||
|
||||
|
||||
def is_xgrids_source_mesh_path(logical_path: str) -> bool:
|
||||
parts = logical_path.lower().replace("\\", "/").split("/")
|
||||
return "mesh_files" in parts and parts[-1].endswith(".ply")
|
||||
|
||||
|
||||
def _logical_path(value: str, label: str) -> str:
|
||||
if (
|
||||
not value
|
||||
|
||||
@@ -26,6 +26,7 @@ from k1link.simulation.gaussian_pipeline_gateway import (
|
||||
GaussianPipelineUnavailableError,
|
||||
configured_gaussian_pipeline_gateway,
|
||||
discover_gaussian_source_bundle,
|
||||
is_xgrids_source_mesh_path,
|
||||
)
|
||||
|
||||
PROJECT_SCHEMA: Final = "missioncore.simulation-project/v1"
|
||||
@@ -610,6 +611,22 @@ class SimulationProjectService:
|
||||
entrypoint=entrypoint,
|
||||
source_format=source_format,
|
||||
)
|
||||
source_mesh_available = any(
|
||||
is_xgrids_source_mesh_path(member.logical_path)
|
||||
for member in source.members
|
||||
)
|
||||
collision_profile = (
|
||||
{
|
||||
"scene_type": project["scene_type"],
|
||||
"seed_position": [0.0, 0.0, 0.0],
|
||||
"capsule_height": 0.4,
|
||||
"capsule_radius": 0.4,
|
||||
"voxel_size": 0.05,
|
||||
"mesh_shape": "source",
|
||||
}
|
||||
if source_mesh_available
|
||||
else None
|
||||
)
|
||||
request = {
|
||||
"schema_version": BUILD_REQUEST_SCHEMA,
|
||||
"idempotency_key": f"missioncore-{project_id}",
|
||||
@@ -617,10 +634,10 @@ class SimulationProjectService:
|
||||
"outputs": {
|
||||
"preview_sog": True,
|
||||
"streamed_sog": True,
|
||||
"collision": False,
|
||||
"collision": source_mesh_available,
|
||||
},
|
||||
"preview_lod": "coarsest",
|
||||
"collision_profile": None,
|
||||
"collision_profile": collision_profile,
|
||||
}
|
||||
submitted = provider.submit_build(request)
|
||||
job_id = submitted.get("job_id")
|
||||
|
||||
+15
-1
@@ -138,7 +138,6 @@ from k1link.web.m49_physical_safety_playback_api import (
|
||||
)
|
||||
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
|
||||
from k1link.web.m49_tgs_full_shadow_api import build_m49_tgs_full_shadow_router
|
||||
from k1link.web.vegetation_shadow_lab_api import build_vegetation_shadow_lab_router
|
||||
from k1link.web.map_api import (
|
||||
MapGatewayConfiguration,
|
||||
MapGatewayProxy,
|
||||
@@ -165,6 +164,10 @@ from k1link.web.session_api import build_session_router
|
||||
from k1link.web.simulation_projects_api import build_simulation_projects_router
|
||||
from k1link.web.simulation_world_provider_api import build_simulation_world_provider_router
|
||||
from k1link.web.system_telemetry_api import build_system_telemetry_router
|
||||
from k1link.web.vegetation_shadow_lab_api import (
|
||||
build_vegetation_benchmark_lab_router,
|
||||
build_vegetation_shadow_lab_router,
|
||||
)
|
||||
from k1link.web.viewer_diagnostics_api import build_viewer_diagnostics_router
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
|
||||
@@ -1031,6 +1034,17 @@ app.include_router(
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_vegetation_benchmark_lab_router(
|
||||
root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "lab-v1-vegetation-benchmark"
|
||||
/ "results"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_m49_physical_safety_playback_router(
|
||||
root_provider=lambda: (
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import zipfile
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
@@ -20,10 +19,9 @@ from k1link.laboratory.evidence_report import (
|
||||
LaboratoryEvidenceReportError,
|
||||
verify_laboratory_evidence_result,
|
||||
)
|
||||
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA, RESULT_PREFIX
|
||||
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA
|
||||
|
||||
RootProvider = Callable[[], Path | None]
|
||||
RESULT_ID: Final = re.compile(rf"^{re.escape(RESULT_PREFIX)}[a-f0-9]{{64}}$")
|
||||
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
|
||||
_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||
work_id="lab-v1-vegetation-shadow",
|
||||
@@ -32,25 +30,55 @@ _DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||
document_name="result.json",
|
||||
result_schema_version=LAB_SCHEMA,
|
||||
)
|
||||
_BENCHMARK_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||
work_id="lab-v1-vegetation-benchmark",
|
||||
runtime_relative_root=PurePosixPath("lab-v1-vegetation-benchmark/results"),
|
||||
result_id_prefix="lab-v1-vegetation-benchmark",
|
||||
document_name="result.json",
|
||||
result_schema_version=LAB_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
def build_vegetation_shadow_lab_router(
|
||||
*, root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(
|
||||
return _build_vegetation_lab_router(
|
||||
prefix="/api/v1/laboratory/vegetation-shadow",
|
||||
definition=_DEFINITION,
|
||||
root_provider=root_provider,
|
||||
)
|
||||
|
||||
|
||||
def build_vegetation_benchmark_lab_router(
|
||||
*, root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
return _build_vegetation_lab_router(
|
||||
prefix="/api/v1/laboratory/vegetation-benchmark",
|
||||
definition=_BENCHMARK_DEFINITION,
|
||||
root_provider=root_provider,
|
||||
)
|
||||
|
||||
|
||||
def _build_vegetation_lab_router(
|
||||
*,
|
||||
prefix: str,
|
||||
definition: LaboratoryEvidenceDefinition,
|
||||
root_provider: RootProvider,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(
|
||||
prefix=prefix,
|
||||
tags=["laboratory"],
|
||||
)
|
||||
|
||||
@router.get("/{result_id}")
|
||||
def get_result(result_id: str) -> dict[str, object]:
|
||||
candidate = _resolve_candidate(root_provider, result_id)
|
||||
return {**copy.deepcopy(_read_verified(candidate)), "access": "read-only"}
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
return {**copy.deepcopy(_read_verified(candidate, definition)), "access": "read-only"}
|
||||
|
||||
@router.get("/{result_id}/assets/{asset_path:path}")
|
||||
def get_asset(result_id: str, asset_path: str) -> FileResponse:
|
||||
candidate = _resolve_candidate(root_provider, result_id)
|
||||
manifest = _read_verified(candidate)
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
manifest = _read_verified(candidate, definition)
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list):
|
||||
raise HTTPException(status_code=404, detail="Vegetation LAB asset not found")
|
||||
@@ -90,8 +118,8 @@ def build_vegetation_shadow_lab_router(
|
||||
|
||||
@router.get("/{result_id}/masks/{sequence}")
|
||||
def get_video_mask(result_id: str, sequence: int) -> Response:
|
||||
candidate = _resolve_candidate(root_provider, result_id)
|
||||
manifest = _read_verified(candidate)
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
manifest = _read_verified(candidate, definition)
|
||||
route_video = manifest.get("route_video")
|
||||
if (
|
||||
not isinstance(route_video, dict)
|
||||
@@ -151,9 +179,125 @@ def build_vegetation_shadow_lab_router(
|
||||
},
|
||||
)
|
||||
|
||||
@router.get("/{result_id}/route-masks/{layer}/{sequence}")
|
||||
def get_full_route_mask(result_id: str, layer: str, sequence: int) -> Response:
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
manifest = _read_verified(candidate, definition)
|
||||
route = manifest.get("route_full_review")
|
||||
layers = route.get("layers") if isinstance(route, dict) else None
|
||||
frame_count = route.get("frame_count") if isinstance(route, dict) else None
|
||||
selected = layers.get(layer) if isinstance(layers, dict) else None
|
||||
archive = selected.get("mask_archive") if isinstance(selected, dict) else None
|
||||
archive_relative = archive.get("path") if isinstance(archive, dict) else None
|
||||
if (
|
||||
layer not in {"city", "vegetation"}
|
||||
or not isinstance(frame_count, int)
|
||||
or not 0 <= sequence < frame_count
|
||||
or not isinstance(archive_relative, str)
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="Full-route semantic mask not found")
|
||||
relative = PurePosixPath(archive_relative)
|
||||
artifacts = manifest.get("artifacts")
|
||||
if (
|
||||
relative.is_absolute()
|
||||
or str(relative) != archive_relative
|
||||
or any(part in {"", ".", ".."} for part in relative.parts)
|
||||
or relative.suffix != ".zip"
|
||||
or not isinstance(artifacts, list)
|
||||
or not any(
|
||||
isinstance(item, dict)
|
||||
and item.get("path") == archive_relative
|
||||
and item.get("media_type") == "application/zip"
|
||||
for item in artifacts
|
||||
)
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="Full-route semantic mask not found")
|
||||
return _zip_mask_response(candidate.joinpath(*relative.parts), sequence)
|
||||
|
||||
@router.get("/{result_id}/route-timeline")
|
||||
def get_full_route_timeline(result_id: str) -> FileResponse:
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
manifest = _read_verified(candidate, definition)
|
||||
route = manifest.get("route_full_review")
|
||||
timeline = route.get("timeline") if isinstance(route, dict) else None
|
||||
relative_text = timeline.get("path") if isinstance(timeline, dict) else None
|
||||
frame_count = timeline.get("frame_count") if isinstance(timeline, dict) else None
|
||||
byte_length = timeline.get("byte_length") if isinstance(timeline, dict) else None
|
||||
sha256 = timeline.get("sha256") if isinstance(timeline, dict) else None
|
||||
if (
|
||||
not isinstance(relative_text, str)
|
||||
or frame_count != route.get("frame_count")
|
||||
or byte_length != frame_count * 8
|
||||
or not isinstance(sha256, str)
|
||||
or len(sha256) != 64
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="Full-route timeline not found")
|
||||
relative = PurePosixPath(relative_text)
|
||||
artifacts = manifest.get("artifacts")
|
||||
if (
|
||||
relative.is_absolute()
|
||||
or str(relative) != relative_text
|
||||
or any(part in {"", ".", ".."} for part in relative.parts)
|
||||
or not isinstance(artifacts, list)
|
||||
or not any(
|
||||
isinstance(item, dict)
|
||||
and item.get("path") == relative_text
|
||||
and item.get("byte_length") == byte_length
|
||||
and item.get("sha256") == sha256
|
||||
and item.get("media_type") == "application/octet-stream"
|
||||
for item in artifacts
|
||||
)
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="Full-route timeline not found")
|
||||
path = candidate.joinpath(*relative.parts)
|
||||
if not path.is_file() or path.is_symlink() or path.stat().st_size != byte_length:
|
||||
raise HTTPException(status_code=404, detail="Full-route timeline not found")
|
||||
return FileResponse(
|
||||
path,
|
||||
media_type="application/octet-stream",
|
||||
headers={
|
||||
"Cache-Control": "private, max-age=31536000, immutable",
|
||||
"ETag": f'"{sha256}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _zip_mask_response(archive_path: Path, sequence: int) -> Response:
|
||||
member = f"masks/frame-{sequence + 1:06d}.png"
|
||||
try:
|
||||
before = archive_path.stat()
|
||||
with zipfile.ZipFile(archive_path) as frozen:
|
||||
info = frozen.getinfo(member)
|
||||
if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024:
|
||||
raise ValueError("Semantic mask member is invalid")
|
||||
payload = frozen.read(info)
|
||||
after = archive_path.stat()
|
||||
if (
|
||||
before.st_size != after.st_size
|
||||
or before.st_mtime_ns != after.st_mtime_ns
|
||||
or len(payload) != info.file_size
|
||||
):
|
||||
raise ValueError("Semantic mask archive changed during read")
|
||||
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Semantic mask failed verification",
|
||||
) from None
|
||||
digest = hashlib.sha256(payload).hexdigest()
|
||||
return Response(
|
||||
content=payload,
|
||||
media_type="image/png",
|
||||
headers={
|
||||
"Cache-Control": "private, max-age=31536000, immutable",
|
||||
"ETag": f'"{digest}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
candidate = provider()
|
||||
if candidate is None:
|
||||
@@ -168,9 +312,13 @@ def _configured_root(provider: RootProvider) -> Path | None:
|
||||
return root if root.is_dir() else None
|
||||
|
||||
|
||||
def _resolve_candidate(provider: RootProvider, result_id: str) -> Path:
|
||||
def _resolve_candidate(
|
||||
provider: RootProvider,
|
||||
definition: LaboratoryEvidenceDefinition,
|
||||
result_id: str,
|
||||
) -> Path:
|
||||
root = _configured_root(provider)
|
||||
if root is None or RESULT_ID.fullmatch(result_id) is None:
|
||||
if root is None or definition.result_id_pattern.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="Vegetation LAB result not found")
|
||||
candidate = root / result_id
|
||||
if candidate.is_symlink():
|
||||
@@ -184,7 +332,10 @@ def _resolve_candidate(provider: RootProvider, result_id: str) -> Path:
|
||||
return resolved
|
||||
|
||||
|
||||
def _read_verified(candidate: Path) -> dict[str, Any]:
|
||||
def _read_verified(
|
||||
candidate: Path,
|
||||
definition: LaboratoryEvidenceDefinition,
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
rows: list[tuple[str, int, int, int, int]] = []
|
||||
for path in sorted(candidate.rglob("*"), key=lambda item: item.as_posix()):
|
||||
@@ -202,19 +353,39 @@ def _read_verified(candidate: Path) -> dict[str, Any]:
|
||||
status_code=503,
|
||||
detail="Vegetation LAB evidence failed verification",
|
||||
) from None
|
||||
return _read_verified_cached(str(candidate), signature)
|
||||
return _read_verified_cached(
|
||||
str(candidate),
|
||||
signature,
|
||||
definition.work_id,
|
||||
str(definition.runtime_relative_root),
|
||||
definition.result_id_prefix,
|
||||
definition.document_name,
|
||||
definition.result_schema_version,
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=16)
|
||||
def _read_verified_cached(
|
||||
candidate_text: str,
|
||||
signature: tuple[tuple[str, int, int, int, int], ...],
|
||||
work_id: str,
|
||||
runtime_relative_root: str,
|
||||
result_id_prefix: str,
|
||||
document_name: str,
|
||||
result_schema_version: str,
|
||||
) -> dict[str, Any]:
|
||||
del signature
|
||||
candidate = Path(candidate_text)
|
||||
definition = LaboratoryEvidenceDefinition(
|
||||
work_id=work_id,
|
||||
runtime_relative_root=PurePosixPath(runtime_relative_root),
|
||||
result_id_prefix=result_id_prefix,
|
||||
document_name=document_name,
|
||||
result_schema_version=result_schema_version,
|
||||
)
|
||||
try:
|
||||
verify_laboratory_evidence_result(_DEFINITION, candidate)
|
||||
path = candidate / "result.json"
|
||||
verify_laboratory_evidence_result(definition, candidate)
|
||||
path = candidate / definition.document_name
|
||||
if path.stat().st_size > _MAX_DOCUMENT_BYTES:
|
||||
raise LaboratoryEvidenceReportError("Vegetation LAB document is too large")
|
||||
payload = json.loads(path.read_text("utf-8"))
|
||||
@@ -228,4 +399,7 @@ def _read_verified_cached(
|
||||
return payload
|
||||
|
||||
|
||||
__all__ = ["build_vegetation_shadow_lab_router"]
|
||||
__all__ = [
|
||||
"build_vegetation_benchmark_lab_router",
|
||||
"build_vegetation_shadow_lab_router",
|
||||
]
|
||||
|
||||
@@ -104,3 +104,20 @@ def test_e4_class_fractions_use_only_valid_fov_pixels() -> None:
|
||||
|
||||
assert {item["id"]: item["pixels"] for item in classes} == {1: 1, 4: 2, 7: 1}
|
||||
assert sum(float(item["fraction_of_valid_fov"]) for item in classes) == 1.0
|
||||
|
||||
|
||||
def test_e4_orchestrator_seals_a_single_decoder_gap_without_frame_shift() -> None:
|
||||
path = (
|
||||
Path(__file__).parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "Invoke-E4FullSessionSegmentation.ps1"
|
||||
)
|
||||
source = path.read_text(encoding="utf-8")
|
||||
assert "-c:v h264_cuvid" in source
|
||||
assert "-frame_pts 1" in source
|
||||
assert '$decodedPath = Join-Path $decodedFramesRoot ("frame-{0}.png" -f $pts)' in source
|
||||
assert "$repairs.Count -ge 1" in source
|
||||
assert 'method = "duplicate-previous-decoded-frame"' in source
|
||||
assert 'schema_version = "missioncore.recorded-video-decode-repair/v1"' in source
|
||||
|
||||
@@ -117,6 +117,28 @@ def test_gateway_uploads_lcc_bundle_with_tus_and_reads_provider_contract(tmp_pat
|
||||
assert descriptor.total_byte_length == sum(member.byte_length for member in descriptor.members)
|
||||
|
||||
|
||||
def test_folder_discovery_retains_one_nested_xgrids_source_mesh(tmp_path: Path) -> None:
|
||||
root = tmp_path / "export"
|
||||
scene = root / "LCC_Results"
|
||||
mesh = root / "Mesh_Files"
|
||||
scene.mkdir(parents=True)
|
||||
mesh.mkdir()
|
||||
(scene / "scan.lcc").write_text(
|
||||
json.dumps({"fileType": "Portable"}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(scene / "index.bin").write_bytes(b"index")
|
||||
(scene / "data.bin").write_bytes(b"data")
|
||||
(mesh / "scan.ply").write_bytes(b"ply")
|
||||
|
||||
members = _discover_bundle_members(root, "LCC_Results/scan.lcc", "lcc")
|
||||
|
||||
assert "Mesh_Files/scan.ply" in members
|
||||
(mesh / "duplicate.ply").write_bytes(b"ply")
|
||||
with pytest.raises(GaussianPipelineIntegrityError, match="at most one"):
|
||||
_discover_bundle_members(root, "LCC_Results/scan.lcc", "lcc")
|
||||
|
||||
|
||||
def test_gateway_uploads_and_normalizes_archive_with_tus(tmp_path: Path) -> None:
|
||||
archive_bytes = b"portable-archive"
|
||||
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
||||
|
||||
@@ -25,6 +25,12 @@ POWERSHELL_PATH = (
|
||||
/ "worker"
|
||||
/ "Invoke-LabV1VegetationGooseBenchmark.ps1"
|
||||
)
|
||||
RAV004_SOURCE_PATH = (
|
||||
REPOSITORY_ROOT
|
||||
/ "config"
|
||||
/ "perception"
|
||||
/ "lab-v1-ravnoves004tree-full-video-source-v1.json"
|
||||
)
|
||||
|
||||
|
||||
def test_benchmark_contract_is_bounded_and_fail_closed() -> None:
|
||||
@@ -93,3 +99,21 @@ def test_worker_wrapper_is_isolated_from_canonical_triton() -> None:
|
||||
assert '"--cap-drop", "ALL"' in source
|
||||
assert '"--security-opt", "no-new-privileges"' in source
|
||||
assert "if ($canonicalAfter -ne $canonicalBefore)" in source
|
||||
|
||||
|
||||
def test_rav004_full_video_profile_and_decoder_gap_are_explicit() -> None:
|
||||
profile = json.loads(RAV004_SOURCE_PATH.read_text(encoding="utf-8"))
|
||||
source_profile = profile["source"]
|
||||
assert profile["schema_version"] == "missioncore.lab-v1-ravnoves-source/v1"
|
||||
assert source_profile["source_job_id"] == (
|
||||
"recorded-camera-eb2783c5480d56bda07c8af0"
|
||||
)
|
||||
assert source_profile["expected_frame_count"] == 6830
|
||||
assert source_profile["base_m4_result_id"] is None
|
||||
source = POWERSHELL_PATH.read_text(encoding="utf-8")
|
||||
assert "-c:v h264_cuvid" in source
|
||||
assert 'schema_version = "missioncore.recorded-video-decode-repair/v1"' in source
|
||||
assert 'method = "duplicate-previous-decoded-frame"' in source
|
||||
assert "$repairs.Count -ge 1" in source
|
||||
assert '& docker @arguments 2>&1 | ForEach-Object { Write-Output $_ }' in source
|
||||
assert 'if ($dockerExitCode -ne 0)' in source
|
||||
|
||||
@@ -127,9 +127,10 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
|
||||
repository_root / "config" / "laboratories"
|
||||
)
|
||||
|
||||
assert len(registry.definitions) == 43
|
||||
assert len(registry.definitions) == 44
|
||||
assert {item.work_id for item in registry.definitions} >= {
|
||||
"lab-v1-vegetation-shadow",
|
||||
"lab-v1-vegetation-benchmark",
|
||||
"e31-source-binding",
|
||||
"e46j-raw-fisheye-realtime",
|
||||
"e47-semantic-slam-shadow",
|
||||
|
||||
@@ -80,7 +80,7 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
|
||||
root / "config" / "laboratory-value-review.json"
|
||||
)
|
||||
|
||||
assert len(registry.entries) == 41
|
||||
assert len(registry.entries) == 42
|
||||
assert {entry.catalog_id for entry in registry.entries} >= {
|
||||
"e28-local-surface",
|
||||
"e46d-temporal-failure-audit",
|
||||
@@ -94,4 +94,5 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
|
||||
"m49-tgs-fail-closed-evidence",
|
||||
"m49-tgs-full-shadow",
|
||||
"lab-v1-vegetation-shadow",
|
||||
"lab-v1-vegetation-benchmark",
|
||||
}
|
||||
|
||||
@@ -156,11 +156,12 @@ def test_store_preserves_provider_job_and_stage_timing(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
class _ReadyProvider:
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, *, source_mesh: bool = False) -> None:
|
||||
self.deleted: list[str] = []
|
||||
self.upload_calls = 0
|
||||
self.submit_calls = 0
|
||||
self.submitted_document: dict[str, object] | None = None
|
||||
self.source_mesh = source_mesh
|
||||
|
||||
def capabilities(self) -> dict[str, object]:
|
||||
return {"outputs": ["preview.sog", "streamed-sog"]}
|
||||
@@ -173,17 +174,26 @@ class _ReadyProvider:
|
||||
source_format: str,
|
||||
) -> GaussianSourceBundleUpload:
|
||||
self.upload_calls += 1
|
||||
members = (
|
||||
members = [
|
||||
GaussianSourceMemberUpload("upload-1", entrypoint, "a" * 64, 23),
|
||||
GaussianSourceMemberUpload("upload-2", "export/data.bin", "b" * 64, 4),
|
||||
GaussianSourceMemberUpload("upload-3", "export/index.bin", "c" * 64, 5),
|
||||
)
|
||||
]
|
||||
if self.source_mesh:
|
||||
members.append(
|
||||
GaussianSourceMemberUpload(
|
||||
"upload-4",
|
||||
"export/Mesh_Files/scene.ply",
|
||||
"e" * 64,
|
||||
3,
|
||||
)
|
||||
)
|
||||
return GaussianSourceBundleUpload(
|
||||
format=source_format,
|
||||
entrypoint=entrypoint,
|
||||
bundle_sha256="d" * 64,
|
||||
total_byte_length=32,
|
||||
members=members,
|
||||
total_byte_length=sum(member.byte_length for member in members),
|
||||
members=tuple(members),
|
||||
)
|
||||
|
||||
def submit_build(self, document: dict[str, object]) -> dict[str, object]:
|
||||
@@ -208,6 +218,39 @@ class _ReadyProvider:
|
||||
}
|
||||
|
||||
def get_result(self, _job_id: str) -> dict[str, object]:
|
||||
artifacts = [
|
||||
{
|
||||
"role": "preview",
|
||||
"logical_path": "preview.sog",
|
||||
"media_type": "application/octet-stream",
|
||||
"sha256": "1" * 64,
|
||||
"byte_length": 7,
|
||||
},
|
||||
{
|
||||
"role": "stream-manifest",
|
||||
"logical_path": "streamed/lod-meta.json",
|
||||
"media_type": "application/json",
|
||||
"sha256": "2" * 64,
|
||||
"byte_length": 2,
|
||||
},
|
||||
]
|
||||
if self.source_mesh:
|
||||
artifacts.extend([
|
||||
{
|
||||
"role": "collision-mesh",
|
||||
"logical_path": "collision/scene.collision.glb",
|
||||
"media_type": "model/gltf-binary",
|
||||
"sha256": "3" * 64,
|
||||
"byte_length": 3,
|
||||
},
|
||||
{
|
||||
"role": "collision-repair-report",
|
||||
"logical_path": "collision/scene.repair.json",
|
||||
"media_type": "application/json",
|
||||
"sha256": "4" * 64,
|
||||
"byte_length": 2,
|
||||
},
|
||||
])
|
||||
return {
|
||||
"schema_version": "gaussian-pipeline.build-result/v1",
|
||||
"job_id": "gsp-20260826000000-deadbeef",
|
||||
@@ -215,22 +258,7 @@ class _ReadyProvider:
|
||||
"source_revision": "e" * 40,
|
||||
"image_digest": f"sha256:{'f' * 64}",
|
||||
},
|
||||
"artifacts": [
|
||||
{
|
||||
"role": "preview",
|
||||
"logical_path": "preview.sog",
|
||||
"media_type": "application/octet-stream",
|
||||
"sha256": "1" * 64,
|
||||
"byte_length": 7,
|
||||
},
|
||||
{
|
||||
"role": "stream-manifest",
|
||||
"logical_path": "streamed/lod-meta.json",
|
||||
"media_type": "application/json",
|
||||
"sha256": "2" * 64,
|
||||
"byte_length": 2,
|
||||
},
|
||||
],
|
||||
"artifacts": artifacts,
|
||||
}
|
||||
|
||||
def download_artifact(
|
||||
@@ -240,7 +268,13 @@ class _ReadyProvider:
|
||||
destination: Path,
|
||||
) -> Path:
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_bytes(b"preview" if descriptor["role"] == "preview" else b"{}")
|
||||
payload = {
|
||||
"preview": b"preview",
|
||||
"stream-manifest": b"{}",
|
||||
"collision-mesh": b"glb",
|
||||
"collision-repair-report": b"{}",
|
||||
}[str(descriptor["role"])]
|
||||
destination.write_bytes(payload)
|
||||
return destination
|
||||
|
||||
def delete_job(self, job_id: str) -> None:
|
||||
@@ -318,6 +352,59 @@ def test_service_builds_visual_world_without_automatic_collision(tmp_path: Path)
|
||||
assert provider.deleted == ["gsp-20260826000000-deadbeef"]
|
||||
|
||||
|
||||
def test_service_automatically_builds_repaired_source_mesh_collision(tmp_path: Path) -> None:
|
||||
store = SimulationProjectStore(tmp_path)
|
||||
files = [
|
||||
*_folder_files(),
|
||||
{"logical_path": "export/Mesh_Files/scene.ply", "byte_length": 3},
|
||||
]
|
||||
project = store.create(
|
||||
name="Source mesh scene",
|
||||
scene_type="outdoor",
|
||||
source_kind="folder",
|
||||
files=files,
|
||||
)
|
||||
payloads = {
|
||||
"export/scene.lcc": b'{"fileType":"Portable"}',
|
||||
"export/index.bin": b"index",
|
||||
"export/data.bin": b"data",
|
||||
"export/Mesh_Files/scene.ply": b"ply",
|
||||
}
|
||||
for source_file in project["source"]["files"]:
|
||||
store.append_upload(
|
||||
project["project_id"],
|
||||
source_file["file_id"],
|
||||
offset=0,
|
||||
payload=payloads[source_file["logical_path"]],
|
||||
)
|
||||
store.begin_build(project["project_id"])
|
||||
provider = _ReadyProvider(source_mesh=True)
|
||||
service = SimulationProjectService(store, provider_factory=lambda: provider) # type: ignore[arg-type]
|
||||
|
||||
service.process(project["project_id"])
|
||||
|
||||
ready = store.get(project["project_id"])
|
||||
assert ready["status"] == "ready"
|
||||
assert ready["world_manifest"]["collision"]["available"] is True
|
||||
assert ready["world_manifest"]["collision"]["mesh_url"].endswith(
|
||||
"/collision/scene.collision.glb"
|
||||
)
|
||||
assert provider.submitted_document is not None
|
||||
assert provider.submitted_document["outputs"] == {
|
||||
"preview_sog": True,
|
||||
"streamed_sog": True,
|
||||
"collision": True,
|
||||
}
|
||||
assert provider.submitted_document["collision_profile"] == {
|
||||
"scene_type": "outdoor",
|
||||
"seed_position": [0.0, 0.0, 0.0],
|
||||
"capsule_height": 0.4,
|
||||
"capsule_radius": 0.4,
|
||||
"voxel_size": 0.05,
|
||||
"mesh_shape": "source",
|
||||
}
|
||||
|
||||
|
||||
def test_service_queue_processes_projects_strictly_one_at_a_time(tmp_path: Path) -> None:
|
||||
store = SimulationProjectStore(tmp_path)
|
||||
projects: list[dict[str, Any]] = []
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import shutil
|
||||
import struct
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from PIL import Image
|
||||
|
||||
import k1link.laboratory.vegetation_policy_review as policy_review_module
|
||||
import k1link.laboratory.vegetation_policy_video as policy_video_module
|
||||
import k1link.laboratory.vegetation_shadow_lab as vegetation_lab_module
|
||||
from k1link.laboratory import LaboratoryEvidenceRegistry
|
||||
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
|
||||
@@ -21,6 +26,45 @@ from k1link.web.vegetation_shadow_lab_api import build_vegetation_shadow_lab_rou
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_coarse_policy_masks_mark_every_outside_fov_pixel_undefined(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(policy_video_module, "FRAME_COUNT", 1)
|
||||
monkeypatch.setattr(
|
||||
policy_video_module,
|
||||
"fine_to_policy_lut",
|
||||
lambda _taxonomy, _provider_map: np.full(256, 4, dtype=np.uint8),
|
||||
)
|
||||
source = tmp_path / "fine.zip"
|
||||
fine_buffer = io.BytesIO()
|
||||
Image.new("L", (800, 600), color=1).save(fine_buffer, format="PNG")
|
||||
with zipfile.ZipFile(source, "w") as archive:
|
||||
archive.writestr("masks/frame-000001.png", fine_buffer.getvalue())
|
||||
|
||||
valid_fov = np.zeros((600, 800), dtype=np.uint8)
|
||||
valid_fov[:, :400] = 255
|
||||
valid_fov_path = tmp_path / "valid-fov.png"
|
||||
Image.fromarray(valid_fov, mode="L").save(valid_fov_path)
|
||||
destination = tmp_path / "coarse.zip"
|
||||
counts = policy_video_module.build_policy_mask_archive(
|
||||
source_archive=source,
|
||||
destination_archive=destination,
|
||||
fine_taxonomy={},
|
||||
provider_label_map={},
|
||||
valid_fov_mask=valid_fov_path,
|
||||
)
|
||||
with (
|
||||
zipfile.ZipFile(destination) as archive,
|
||||
Image.open(io.BytesIO(archive.read("masks/frame-000001.png"))) as image,
|
||||
):
|
||||
coarse = np.asarray(image.convert("L"))
|
||||
assert np.all(coarse[:, :400] == 4)
|
||||
assert np.all(coarse[:, 400:] == 9)
|
||||
assert counts[4] == 600 * 400
|
||||
assert counts[9] == 600 * 400
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
@@ -227,6 +271,90 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(
|
||||
assert mask.content == b"\x89PNG\r\n\x1a\n"
|
||||
assert mask.headers["cache-control"].endswith("immutable")
|
||||
|
||||
full_archive_payloads = (b"\x89PNG\r\n\x1a\ncity", b"\x89PNG\r\n\x1a\nvegetation")
|
||||
full_timeline_payload = struct.pack("<2Q", 1_000_000_000, 1_100_000_000)
|
||||
full_identity = dict(manifest["identity"])
|
||||
full_route = {
|
||||
"frame_count": 2,
|
||||
"timeline": {
|
||||
"path": "video/frame-source-times-ns.bin",
|
||||
"sha256": hashlib.sha256(full_timeline_payload).hexdigest(),
|
||||
"byte_length": len(full_timeline_payload),
|
||||
"encoding": "uint64-le-nanoseconds",
|
||||
"frame_count": 2,
|
||||
},
|
||||
"layers": {
|
||||
layer: {"mask_archive": {"path": "video/full-route-masks.zip"}}
|
||||
for layer in ("city", "vegetation")
|
||||
},
|
||||
}
|
||||
full_identity["route_full_review"] = full_route
|
||||
full_identity_sha = hashlib.sha256(
|
||||
json.dumps(
|
||||
full_identity,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
).hexdigest()
|
||||
full_result_id = f"lab-v1-vegetation-shadow-{full_identity_sha}"
|
||||
full_root = result_root.parent / full_result_id
|
||||
shutil.copytree(result_root, full_root)
|
||||
full_archive = full_root / "video" / "full-route-masks.zip"
|
||||
full_archive.parent.mkdir(exist_ok=True)
|
||||
with zipfile.ZipFile(full_archive, "x", compression=zipfile.ZIP_STORED) as frozen:
|
||||
for sequence, payload in enumerate(full_archive_payloads, start=1):
|
||||
frozen.writestr(f"masks/frame-{sequence:06d}.png", payload)
|
||||
full_timeline = full_root / "video" / "frame-source-times-ns.bin"
|
||||
full_timeline.write_bytes(full_timeline_payload)
|
||||
full_manifest = dict(manifest)
|
||||
full_manifest["result_id"] = full_result_id
|
||||
full_manifest["identity"] = full_identity
|
||||
full_manifest["identity_sha256"] = full_identity_sha
|
||||
full_manifest["route_full_review"] = full_route
|
||||
full_manifest["artifacts"] = [
|
||||
*manifest["artifacts"],
|
||||
{
|
||||
"role": "full-route-mask-fixture",
|
||||
"path": "video/full-route-masks.zip",
|
||||
"byte_length": full_archive.stat().st_size,
|
||||
"sha256": _sha256(full_archive),
|
||||
"media_type": "application/zip",
|
||||
},
|
||||
{
|
||||
"role": "full-route-frame-timeline",
|
||||
"path": "video/frame-source-times-ns.bin",
|
||||
"byte_length": full_timeline.stat().st_size,
|
||||
"sha256": _sha256(full_timeline),
|
||||
"media_type": "application/octet-stream",
|
||||
},
|
||||
]
|
||||
(full_root / "result.json").write_text(
|
||||
json.dumps(full_manifest, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
for layer, sequence, expected in (
|
||||
("city", 0, full_archive_payloads[0]),
|
||||
("vegetation", 1, full_archive_payloads[1]),
|
||||
):
|
||||
response = client.get(
|
||||
f"/api/v1/laboratory/vegetation-shadow/{full_result_id}"
|
||||
f"/route-masks/{layer}/{sequence}"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.content == expected
|
||||
assert response.headers["cache-control"].endswith("immutable")
|
||||
assert client.get(
|
||||
f"/api/v1/laboratory/vegetation-shadow/{full_result_id}/route-masks/city/2"
|
||||
).status_code == 404
|
||||
timeline = client.get(
|
||||
f"/api/v1/laboratory/vegetation-shadow/{full_result_id}/route-timeline"
|
||||
)
|
||||
assert timeline.status_code == 200
|
||||
assert timeline.content == full_timeline_payload
|
||||
assert timeline.headers["cache-control"].endswith("immutable")
|
||||
|
||||
(result_root / asset_path).write_bytes(b"tampered")
|
||||
assert (
|
||||
client.get(f"/api/v1/laboratory/vegetation-shadow/{result_root.name}").status_code
|
||||
@@ -290,9 +418,12 @@ def test_policy_review_reuses_sealed_video_and_links_yolox_tgs(
|
||||
|
||||
def fake_policy_archive(**kwargs) -> list[int]:
|
||||
shutil.copyfile(kwargs["source_archive"], kwargs["destination_archive"])
|
||||
return [4489 * 800 * 600, *([0] * 8)]
|
||||
assert kwargs["valid_fov_mask"].is_file()
|
||||
return [4489 * 800 * 600, *([0] * 9)]
|
||||
|
||||
monkeypatch.setattr(policy_review_module, "build_policy_mask_archive", fake_policy_archive)
|
||||
valid_fov_mask = tmp_path / "valid-fov-mask.png"
|
||||
Image.new("L", (800, 600), color=255).save(valid_fov_mask)
|
||||
result_root = seal_vegetation_policy_review(
|
||||
base_lab_root=base_root,
|
||||
mission_policy_path=REPOSITORY_ROOT
|
||||
@@ -300,6 +431,7 @@ def test_policy_review_reuses_sealed_video_and_links_yolox_tgs(
|
||||
provider_label_map_path=REPOSITORY_ROOT
|
||||
/ "config/perception/lab-v1-vegetation-provider-label-map-v1.json",
|
||||
m49_tgs_full_shadow_root=tmp_path / "sealed-tgs",
|
||||
valid_fov_mask_path=valid_fov_mask,
|
||||
output_root=tmp_path / "results",
|
||||
created_at_utc="2026-08-28T08:00:00+00:00",
|
||||
)
|
||||
@@ -312,8 +444,9 @@ def test_policy_review_reuses_sealed_video_and_links_yolox_tgs(
|
||||
assert route["taxonomy"]["schema_version"] == (
|
||||
"missioncore.lab-v1-terrain-policy-taxonomy/v1"
|
||||
)
|
||||
assert len(route["taxonomy"]["classes"]) == 9
|
||||
assert len(manifest["artifacts"]) == 79
|
||||
assert len(route["taxonomy"]["classes"]) == 10
|
||||
assert route["valid_fov"]["outside_valid_fov_class_id"] == 9
|
||||
assert len(manifest["artifacts"]) == 80
|
||||
assert manifest["authority"]["commands_enabled"] is False
|
||||
assert manifest["decision"]["multilayer_policy_review_ready"] is True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user