feat(perception): split vegetation evidence layers

This commit is contained in:
DCCONSTRUCTIONS
2026-08-28 15:37:10 +03:00
parent a2c3385062
commit e10b96b546
26 changed files with 868 additions and 233 deletions
@@ -48,9 +48,13 @@ import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality"; import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
import { fetchM49TgsFailClosedResult } from "./m49TgsFailClosed"; import { fetchM49TgsFailClosedResult } from "./m49TgsFailClosed";
import { fetchM49TgsFullShadowResult } from "./m49TgsFullShadow"; import { fetchM49TgsFullShadowResult } from "./m49TgsFullShadow";
import { fetchVegetationShadowResult } from "./vegetationShadow"; import {
fetchVegetationBenchmarkResult,
fetchVegetationShadowResult,
} from "./vegetationShadow";
export type AdvancedLaboratoryWorkId = export type AdvancedLaboratoryWorkId =
| "lab-v1-vegetation-benchmark"
| "lab-v1-vegetation-shadow" | "lab-v1-vegetation-shadow"
| "m48-object-centric-quality" | "m48-object-centric-quality"
| "m48-small-static-passage-regression" | "m48-small-static-passage-regression"
@@ -102,6 +106,7 @@ export interface AdvancedLaboratoryIndexItem {
} }
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
"lab-v1-vegetation-benchmark",
"lab-v1-vegetation-shadow", "lab-v1-vegetation-shadow",
"m48-object-centric-quality", "m48-object-centric-quality",
"m48-small-static-passage-regression", "m48-small-static-passage-regression",
@@ -148,6 +153,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
]; ];
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = { const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
"lab-v1-vegetation-benchmark": "lab-v1-vegetation-benchmark",
"lab-v1-vegetation-shadow": "lab-v1-vegetation-shadow", "lab-v1-vegetation-shadow": "lab-v1-vegetation-shadow",
"m48-object-centric-quality": "m48-object-quality-(?:pack|result)", "m48-object-centric-quality": "m48-object-quality-(?:pack|result)",
"m48-small-static-passage-regression": "m48-small-static-passage-regression", "m48-small-static-passage-regression": "m48-small-static-passage-regression",
@@ -201,6 +207,7 @@ export function isAdvancedLaboratoryWorkId(
export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults { export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
return { return {
vegetationBenchmark: null,
vegetationShadow: null, vegetationShadow: null,
m47Graph: null, m47Graph: null,
m48: null, m48: null,
@@ -335,7 +342,8 @@ export function advancedLaboratoryResultAvailable(
workId: AdvancedLaboratoryWorkId, workId: AdvancedLaboratoryWorkId,
results: AdvancedLaboratoryResults, results: AdvancedLaboratoryResults,
): boolean { ): 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-object-centric-quality" ? results.m48 !== null
: workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== null : workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== null
: workId === "m48-static-occupancy-qualification" ? results.m48StaticOccupancy !== null : workId === "m48-static-occupancy-qualification" ? results.m48StaticOccupancy !== null
@@ -393,7 +401,10 @@ export async function fetchAdvancedLaboratoryResult(
} = {}, } = {},
): Promise<AdvancedLaboratoryResults> { ): Promise<AdvancedLaboratoryResults> {
const results = emptyAdvancedLaboratoryResults(); 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 не выбрана."); if (!resultId) throw new AdvancedLaboratoryContractError("Vegetation LAB identity не выбрана.");
results.vegetationShadow = await fetchVegetationShadowResult(resultId, { fetcher, signal }); results.vegetationShadow = await fetchVegetationShadowResult(resultId, { fetcher, signal });
} else if (workId === "m48-object-centric-quality") { } else if (workId === "m48-object-centric-quality") {
@@ -45,6 +45,7 @@ import type { M49TgsFullShadowResult } from "./m49TgsFullShadow";
import type { VegetationShadowResult } from "./vegetationShadow"; import type { VegetationShadowResult } from "./vegetationShadow";
export interface AdvancedLaboratoryResults { export interface AdvancedLaboratoryResults {
vegetationBenchmark: VegetationShadowResult | null;
vegetationShadow: VegetationShadowResult | null; vegetationShadow: VegetationShadowResult | null;
m47Graph: M47ReferenceGraphLabResult | null; m47Graph: M47ReferenceGraphLabResult | null;
m48: M48AdvancedResult | null; m48: M48AdvancedResult | null;
@@ -967,6 +967,7 @@ export async function fetchAdvancedLaboratoryResults({
const e39 = settledCatalogValue(settled[7]); const e39 = settledCatalogValue(settled[7]);
const e40 = settledCatalogValue(settled[8]); const e40 = settledCatalogValue(settled[8]);
return { return {
vegetationBenchmark: null,
vegetationShadow: null, vegetationShadow: null,
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null, m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
m48r3StaticOccupancy: null, m48r3StaticOccupancy: null,
@@ -1,6 +1,7 @@
import type { LaboratoryFetch } from "./advancedResults"; import type { LaboratoryFetch } from "./advancedResults";
const RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/; 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 SHA256 = /^[a-f0-9]{64}$/;
const CANDIDATES = ["ddrnet", "ppliteseg"] as const; const CANDIDATES = ["ddrnet", "ppliteseg"] as const;
const ROUTE_MODES = ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"] as const; const ROUTE_MODES = ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"] as const;
@@ -75,6 +76,7 @@ export interface VegetationRouteVideo {
aggregatePredictionPixels: readonly number[]; aggregatePredictionPixels: readonly number[];
policyPresets: Readonly<Record<string, Readonly<Record<string, string>>>> | null; policyPresets: Readonly<Record<string, Readonly<Record<string, string>>>> | null;
fusionMode: "synchronised-multilayer-review" | null; fusionMode: "synchronised-multilayer-review" | null;
validFovMaskSha256: string | null;
} }
export interface VegetationShadowResult { export interface VegetationShadowResult {
@@ -192,6 +194,7 @@ function visualCaseValue(
value: unknown, value: unknown,
resultId: string, resultId: string,
expectedKind: "goose" | "ravnoves", expectedKind: "goose" | "ravnoves",
endpointRoot: string,
): VegetationVisualCase { ): VegetationVisualCase {
const row = objectValue(value, `vegetation.${expectedKind}.case`); const row = objectValue(value, `vegetation.${expectedKind}.case`);
exact(row.source_kind, expectedKind, "vegetation.case.source_kind"); exact(row.source_kind, expectedKind, "vegetation.case.source_kind");
@@ -210,7 +213,7 @@ function visualCaseValue(
if (!SHA256.test(sha256) || !path.startsWith(`visual/${expectedKind}/${caseId}/`)) { if (!SHA256.test(sha256) || !path.startsWith(`visual/${expectedKind}/${caseId}/`)) {
throw new VegetationShadowContractError(`vegetation.case.assets.${key}: proof invalid.`); 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("/") .split("/")
.map(encodeURIComponent) .map(encodeURIComponent)
.join("/")}`; .join("/")}`;
@@ -342,10 +345,16 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null {
evidenceState, evidenceState,
}; };
}); });
const expectedClassCount = viewKind === "coarse-material-policy-review" ? 9 : 64; const expectedClassCount = viewKind === "coarse-material-policy-review" ? 10 : 64;
if (classes.length !== expectedClassCount) { if (classes.length !== expectedClassCount) {
throw new VegetationShadowContractError("vegetation.route_video: taxonomy size changed."); 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( const aggregatePredictionPixels = arrayValue(
row.aggregate_prediction_pixels, row.aggregate_prediction_pixels,
"vegetation.route_video.aggregate_prediction_pixels", "vegetation.route_video.aggregate_prediction_pixels",
@@ -364,7 +373,18 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null {
integerValue(maskArchive.byte_length, "vegetation.route_video.mask_archive.byte_length"); integerValue(maskArchive.byte_length, "vegetation.route_video.mask_archive.byte_length");
let policyPresets: VegetationRouteVideo["policyPresets"] = null; let policyPresets: VegetationRouteVideo["policyPresets"] = null;
let fusionMode: VegetationRouteVideo["fusionMode"] = null; let fusionMode: VegetationRouteVideo["fusionMode"] = null;
let validFovMaskSha256: string | null = null;
if (viewKind === "coarse-material-policy-review") { 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 policy = objectValue(row.policy, "vegetation.route_video.policy");
const presets = objectValue(policy.presets, "vegetation.route_video.policy.presets"); const presets = objectValue(policy.presets, "vegetation.route_video.policy.presets");
policyPresets = Object.fromEntries(Object.entries(presets).map(([presetId, rawRules]) => { policyPresets = Object.fromEntries(Object.entries(presets).map(([presetId, rawRules]) => {
@@ -399,10 +419,15 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null {
aggregatePredictionPixels, aggregatePredictionPixels,
policyPresets, policyPresets,
fusionMode, fusionMode,
validFovMaskSha256,
}; };
} }
function parseResult(value: unknown, resultId: string): VegetationShadowResult { function parseResult(
value: unknown,
resultId: string,
endpointRoot: string,
): VegetationShadowResult {
const payload = objectValue(value, "Vegetation LAB"); const payload = objectValue(value, "Vegetation LAB");
exact(payload.schema_version, "missioncore.lab-v1-vegetation-shadow/v1", "vegetation.schema"); exact(payload.schema_version, "missioncore.lab-v1-vegetation-shadow/v1", "vegetation.schema");
exact(payload.result_id, resultId, "vegetation.result_id"); exact(payload.result_id, resultId, "vegetation.result_id");
@@ -438,9 +463,9 @@ function parseResult(value: unknown, resultId: string): VegetationShadowResult {
"vegetation.authority.camera_semantics_can_clear_rigid_geometry", "vegetation.authority.camera_semantics_can_clear_rigid_geometry",
); );
const routeCases = arrayValue(catalogs.ravnoves, "vegetation.catalogs.ravnoves") 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") const validationCases = arrayValue(catalogs.goose, "vegetation.catalogs.goose")
.map((item) => visualCaseValue(item, resultId, "goose")); .map((item) => visualCaseValue(item, resultId, "goose", endpointRoot));
if (routeCases.length !== 0 || validationCases.length !== 12) { if (routeCases.length !== 0 || validationCases.length !== 12) {
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer."); throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer.");
} }
@@ -490,5 +515,36 @@ export async function fetchVegetationShadowResult(
if (!response.ok) { if (!response.ok) {
throw new VegetationShadowContractError(`Vegetation LAB недоступна: HTTP ${response.status}.`); throw new VegetationShadowContractError(`Vegetation LAB недоступна: HTTP ${response.status}.`);
} }
return parseResult(await response.json(), resultId); return parseResult(
await response.json(),
resultId,
"/api/v1/laboratory/vegetation-shadow",
);
}
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; 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-evidence-viewer[data-mode-controls="content"]:has(
.m4-replay-threat-visual__review-controls .m4-replay-threat-visual__review-controls
) .m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"] { ) .m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"] {
@@ -51,6 +51,7 @@ import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
import { M49TgsFailClosedResultView } from "./M49TgsFailClosedResult"; import { M49TgsFailClosedResultView } from "./M49TgsFailClosedResult";
import { M49TgsFullShadowResultView } from "./M49TgsFullShadowResult"; import { M49TgsFullShadowResultView } from "./M49TgsFullShadowResult";
import { VegetationShadowResultView } from "./VegetationShadowResult"; import { VegetationShadowResultView } from "./VegetationShadowResult";
import { VegetationBenchmarkResultView } from "./VegetationBenchmarkResult";
export { isAdvancedLaboratoryWorkId }; export { isAdvancedLaboratoryWorkId };
export type { AdvancedLaboratoryWorkId }; export type { AdvancedLaboratoryWorkId };
@@ -93,6 +94,9 @@ export function AdvancedLaboratoryResult({
failedSessionId: string | null; failedSessionId: string | null;
replayError: 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) { if (workId === "lab-v1-vegetation-shadow" && results.vegetationShadow) {
return <VegetationShadowResultView rigLabel={rigLabel} result={results.vegetationShadow} />; return <VegetationShadowResultView rigLabel={rigLabel} result={results.vegetationShadow} />;
} }
@@ -71,7 +71,6 @@ export function M49TgsFullShadowEvidence({
const controller = new AbortController(); const controller = new AbortController();
setSemantic(null); setSemantic(null);
setSemanticError(null); setSemanticError(null);
if (semanticOverride) return () => controller.abort();
void fetchE47SemanticSlamResult({ void fetchE47SemanticSlamResult({
resultId: result.source.linkedSemanticResultId, resultId: result.source.linkedSemanticResultId,
signal: controller.signal, signal: controller.signal,
@@ -87,7 +86,7 @@ export function M49TgsFullShadowEvidence({
if (!controller.signal.aborted) setSemanticError(message(caught)); if (!controller.signal.aborted) setSemanticError(message(caught));
}); });
return () => controller.abort(); return () => controller.abort();
}, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId, semanticOverride]); }, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId]);
useEffect(() => { useEffect(() => {
const controller = new AbortController(); const controller = new AbortController();
@@ -201,15 +200,28 @@ export function M49TgsFullShadowEvidence({
const handleSequenceChange = useCallback((sequence: number | null) => { const handleSequenceChange = useCallback((sequence: number | null) => {
setActiveSequence(sequence); 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 ( return (
<> <>
<M4ReplayThreatVisual <M4ReplayThreatVisual
resultId={result.source.linkedVisualResultId} resultId={result.source.linkedVisualResultId}
semantic={semanticOverride ?? (semantic ? { semanticLayers={semanticLayers}
resultId: semantic.resultId, initialSemanticLayerId={semanticOverride ? "vegetation" : "urban"}
taxonomy: semantic.taxonomy,
} : undefined)}
showReviewAnchorBoxes={false} showReviewAnchorBoxes={false}
reviewLabel="4 489 source-paced TGS frames" reviewLabel="4 489 source-paced TGS frames"
evidenceLabel={evidenceLabel} evidenceLabel={evidenceLabel}
@@ -227,7 +239,7 @@ export function M49TgsFullShadowEvidence({
replacePointCloud: false, replacePointCloud: false,
}} }}
/> />
{!semanticOverride && semanticError ? ( {semanticError ? (
<div className="m4-replay-threat-visual__pane-status" role="alert"> <div className="m4-replay-threat-visual__pane-status" role="alert">
Semantic overlay недоступен: {semanticError} Semantic overlay недоступен: {semanticError}
</div> </div>
@@ -96,6 +96,8 @@ function SpatialState({ message: text }: { message: string }) {
} }
export interface M4ReplayThreatSemanticLayer { export interface M4ReplayThreatSemanticLayer {
id?: string;
controlLabel?: string;
resultId: string; resultId: string;
spatialResultId?: string | null; spatialResultId?: string | null;
maskUrl?: (sequence: number) => string; maskUrl?: (sequence: number) => string;
@@ -155,6 +157,8 @@ const EMPTY_REVIEW_ANCHORS: readonly M4ReplayThreatReviewAnchor[] = [];
export function M4ReplayThreatVisual({ export function M4ReplayThreatVisual({
resultId, resultId,
semantic, semantic,
semanticLayers,
initialSemanticLayerId,
reviewAnchors = EMPTY_REVIEW_ANCHORS, reviewAnchors = EMPTY_REVIEW_ANCHORS,
showReviewAnchorBoxes = true, showReviewAnchorBoxes = true,
reviewLabel = "Контрольные примеры M4.8R1", reviewLabel = "Контрольные примеры M4.8R1",
@@ -168,6 +172,8 @@ export function M4ReplayThreatVisual({
}: { }: {
resultId: string; resultId: string;
semantic?: M4ReplayThreatSemanticLayer; semantic?: M4ReplayThreatSemanticLayer;
semanticLayers?: readonly M4ReplayThreatSemanticLayer[];
initialSemanticLayerId?: string;
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[]; reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
showReviewAnchorBoxes?: boolean; showReviewAnchorBoxes?: boolean;
reviewLabel?: string; reviewLabel?: string;
@@ -199,6 +205,35 @@ export function M4ReplayThreatVisual({
)); ));
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0); 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 metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
const metadata = useM4ThreatTimelineMetadata(resultId, timelineEndpointRoot); const metadata = useM4ThreatTimelineMetadata(resultId, timelineEndpointRoot);
const playbackRange = useMemo(() => metadata.timeline ? ({ const playbackRange = useMemo(() => metadata.timeline ? ({
@@ -298,19 +333,21 @@ export function M4ReplayThreatVisual({
sequence: frame?.sequence ?? null, sequence: frame?.sequence ?? null,
endpointRoot: timelineEndpointRoot, endpointRoot: timelineEndpointRoot,
}); });
const semanticSpatialResultId = semantic const semanticSpatialResultId = activeSemantic
? semantic.spatialResultId === undefined ? semantic.resultId : semantic.spatialResultId ? activeSemantic.spatialResultId === undefined
? activeSemantic.resultId
: activeSemantic.spatialResultId
: null; : null;
const spatialSemanticTaxonomy = useMemo<readonly E47SemanticClass[]>( const spatialSemanticTaxonomy = useMemo<readonly E47SemanticClass[]>(
() => semanticSpatialResultId && semantic () => semanticSpatialResultId && activeSemantic
? semantic.taxonomy.map((item) => ({ ? activeSemantic.taxonomy.map((item) => ({
classId: item.classId, classId: item.classId,
label: item.label, label: item.label,
disposition: item.disposition === "ambiguous" ? "ambiguous" : "labeled", disposition: item.disposition === "ambiguous" ? "ambiguous" : "labeled",
colorRgb: item.colorRgb, colorRgb: item.colorRgb,
})) }))
: [], : [],
[semantic, semanticSpatialResultId], [activeSemantic, semanticSpatialResultId],
); );
const semanticTimeline = useE47SemanticTimelineFrame({ const semanticTimeline = useE47SemanticTimelineFrame({
resultId: semanticSpatialResultId, resultId: semanticSpatialResultId,
@@ -386,14 +423,14 @@ export function M4ReplayThreatVisual({
[frame, reviewAnchorBoxes, showReferenceMediaLayers, staticObstacleBoxes], [frame, reviewAnchorBoxes, showReferenceMediaLayers, staticObstacleBoxes],
); );
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>( const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
() => semantic?.taxonomy.map((item) => ({ () => activeSemantic?.taxonomy.map((item) => ({
id: item.classId, id: item.classId,
label: `semantic: ${item.label}`, label: `semantic: ${item.label}`,
})) ?? [], })) ?? [],
[semantic?.taxonomy], [activeSemantic?.taxonomy],
); );
const semanticPalette = useMemo<readonly RecordedEvidenceSemanticPaletteEntry[]>( const semanticPalette = useMemo<readonly RecordedEvidenceSemanticPaletteEntry[]>(
() => semantic?.taxonomy.map((item) => ({ () => activeSemantic?.taxonomy.map((item) => ({
classId: item.classId, classId: item.classId,
color: item.disposition === "undefined" color: item.disposition === "undefined"
? { kind: "transparent" as const } ? { kind: "transparent" as const }
@@ -404,7 +441,7 @@ export function M4ReplayThreatVisual({
? 0 ? 0
: item.disposition === "ambiguous" ? 0.52 : 0.92, : item.disposition === "ambiguous" ? 0.52 : 0.92,
})) ?? [], })) ?? [],
[semantic?.taxonomy], [activeSemantic?.taxonomy],
); );
const semanticFrame = semanticTimeline.activeFrame?.sequence === frame?.sequence const semanticFrame = semanticTimeline.activeFrame?.sequence === frame?.sequence
? semanticTimeline.activeFrame ? semanticTimeline.activeFrame
@@ -422,7 +459,7 @@ export function M4ReplayThreatVisual({
&& lastSpatialSemanticFrameRef.current.frame.sequence === spatialFrame?.sequence && lastSpatialSemanticFrameRef.current.frame.sequence === spatialFrame?.sequence
? lastSpatialSemanticFrameRef.current.frame ? lastSpatialSemanticFrameRef.current.frame
: null; : null;
const semanticIntegrityError = semantic && spatialFrame && spatialSemanticFrame && ( const semanticIntegrityError = activeSemantic && spatialFrame && spatialSemanticFrame && (
spatialSemanticFrame.sourcePointCount !== spatialFrame.pointCloudSourceCount spatialSemanticFrame.sourcePointCount !== spatialFrame.pointCloudSourceCount
|| spatialFrame.pointCloudSampleCount !== spatialFrame.pointCloudSourceCount || spatialFrame.pointCloudSampleCount !== spatialFrame.pointCloudSourceCount
|| spatialFrame.pointCloudBodyXyzM.length !== spatialFrame.pointCloudSourceCount || spatialFrame.pointCloudBodyXyzM.length !== spatialFrame.pointCloudSourceCount
@@ -431,7 +468,7 @@ export function M4ReplayThreatVisual({
: null; : null;
const alignedSemanticPointIds = useMemo<readonly (number | null)[] | undefined>(() => { const alignedSemanticPointIds = useMemo<readonly (number | null)[] | undefined>(() => {
if ( if (
!semantic !activeSemantic
|| !showSpatialSemantic || !showSpatialSemantic
|| !spatialFrame || !spatialFrame
|| !spatialSemanticFrame || !spatialSemanticFrame
@@ -441,7 +478,7 @@ export function M4ReplayThreatVisual({
const status = spatialSemanticFrame.statusCodes[index]; const status = spatialSemanticFrame.statusCodes[index];
return status === 2 || status === 3 ? classId : null; return status === 2 || status === 3 ? classId : null;
}); });
}, [semantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]); }, [activeSemantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence
? spatialFrame ? spatialFrame
: null; : null;
@@ -610,19 +647,19 @@ export function M4ReplayThreatVisual({
}, },
), [metadata.timeline, spatialFrame, timelineFrame.availableFrames]); ), [metadata.timeline, spatialFrame, timelineFrame.availableFrames]);
const semanticOverlay: RecordedEvidenceSemanticOverlay | undefined = const semanticOverlay: RecordedEvidenceSemanticOverlay | undefined =
semantic && showMediaSemantic && frame activeSemantic && showMediaSemantic && frame
? { ? {
src: semantic.maskUrl?.(frame.sequence) src: activeSemantic.maskUrl?.(frame.sequence)
?? e47SemanticMaskUrl(semantic.resultId, frame.sequence), ?? e47SemanticMaskUrl(activeSemantic.resultId, frame.sequence),
prefetchSrcs: Array.from({ length: 12 }, (_, index) => index + 1) prefetchSrcs: Array.from({ length: 12 }, (_, index) => index + 1)
.map((offset) => frame.sequence + offset) .map((offset) => frame.sequence + offset)
.filter((sequence) => sequence < (metadata.timeline?.frameCount ?? 0)) .filter((sequence) => sequence < (metadata.timeline?.frameCount ?? 0))
.map((sequence) => semantic.maskUrl?.(sequence) .map((sequence) => activeSemantic.maskUrl?.(sequence)
?? e47SemanticMaskUrl(semantic.resultId, sequence)), ?? e47SemanticMaskUrl(activeSemantic.resultId, sequence)),
classes: semanticClasses, classes: semanticClasses,
palette: semanticPalette, palette: semanticPalette,
opacity: 0.9, opacity: 0.9,
ariaLabel: `${semantic.maskAriaLabel ?? "Semantic prediction"} frame ${frame.sequence + 1}`, ariaLabel: `${activeSemantic.maskAriaLabel ?? "Semantic prediction"} frame ${frame.sequence + 1}`,
} }
: undefined; : undefined;
const accumulatedCameraPoints = cameraPointOverlay.overlay?.sequence === frame?.sequence const accumulatedCameraPoints = cameraPointOverlay.overlay?.sequence === frame?.sequence
@@ -692,7 +729,7 @@ export function M4ReplayThreatVisual({
</div> </div>
); );
const mediaLayerControls = semantic const mediaLayerControls = activeSemantic
|| (showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery) || (showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery)
|| (showReferenceMediaLayers && metadata.timeline?.cameraObstacleProjectionDelivery) ? ( || (showReferenceMediaLayers && metadata.timeline?.cameraObstacleProjectionDelivery) ? (
<div <div
@@ -700,7 +737,7 @@ export function M4ReplayThreatVisual({
role="group" role="group"
aria-label="Слои камеры и видео" aria-label="Слои камеры и видео"
> >
{semantic ? ( {activeSemantic ? (
<Button <Button
size="compact" size="compact"
shape="pill" shape="pill"
@@ -711,6 +748,20 @@ export function M4ReplayThreatVisual({
SEMANTICS SEMANTICS
</Button> </Button>
) : null} ) : 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 ? ( {showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery ? (
<Button <Button
size="compact" size="compact"
@@ -1022,6 +1073,7 @@ export function M4ReplayThreatVisual({
<div <div
className="m4-replay-threat-visual__pane-toolbar" className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="media" data-pane-toolbar="media"
data-multi-semantic={availableSemanticLayers.length > 1 ? "true" : undefined}
> >
{mediaLayerControls} {mediaLayerControls}
{mediaModeControls} {mediaModeControls}
@@ -1238,8 +1290,8 @@ export function M4ReplayThreatVisual({
return ( return (
<div className="l3-visual-audit m4-replay-threat-visual"> <div className="l3-visual-audit m4-replay-threat-visual">
<LaboratoryEvidenceViewer <LaboratoryEvidenceViewer
label={semantic label={activeSemantic
? semantic.label ?? "Semantic diagnostic replay" ? activeSemantic.label ?? "Semantic diagnostic replay"
: `${evidenceLabel} recorded-realtime replay`} : `${evidenceLabel} recorded-realtime replay`}
className="m4-replay-threat-evidence-viewer" className="m4-replay-threat-evidence-viewer"
mode={mediaMode ?? "none"} 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.",
}}
/>
)}
/>
);
}
@@ -14,10 +14,6 @@ import {
fetchM49TgsFullShadowResult, fetchM49TgsFullShadowResult,
type M49TgsFullShadowResult, type M49TgsFullShadowResult,
} from "../../core/laboratory/m49TgsFullShadow"; } from "../../core/laboratory/m49TgsFullShadow";
import {
M48MaskComparisonVisual,
type M48MaskComparisonCase,
} from "./M48FailureAtlasVisual";
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual"; import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence"; import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
@@ -25,37 +21,6 @@ function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits }); 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,
},
};
});
}
function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) { function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) {
const route = result.routeVideo!; const route = result.routeVideo!;
const [tgs, setTgs] = useState<M49TgsFullShadowResult | null>(null); const [tgs, setTgs] = useState<M49TgsFullShadowResult | null>(null);
@@ -83,16 +48,14 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
}, [route.baseM4ResultId, route.linkedTgsResultId]); }, [route.baseM4ResultId, route.linkedTgsResultId]);
const semantic = { const semantic = {
id: "vegetation",
controlLabel: "ПРИРОДА · DDRNet",
resultId: route.workerResultId, resultId: route.workerResultId,
spatialResultId: null, spatialResultId: null,
taxonomy: route.taxonomy, taxonomy: route.taxonomy,
maskUrl: (sequence: number) => vegetationVideoMaskUrl(result.resultId, sequence), maskUrl: (sequence: number) => vegetationVideoMaskUrl(result.resultId, sequence),
label: route.viewKind === "coarse-material-policy-review" label: "DDRNet coarse vegetation material · recorded video",
? "Coarse material evidence · recorded video" maskAriaLabel: "DDRNet vegetation material prediction",
: "DDRNet vegetation prediction · recorded video",
maskAriaLabel: route.viewKind === "coarse-material-policy-review"
? "Coarse material policy evidence"
: "DDRNet vegetation prediction",
} as const; } as const;
if (route.linkedTgsResultId && tgs) { if (route.linkedTgsResultId && tgs) {
@@ -100,19 +63,23 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
<M49TgsFullShadowEvidence <M49TgsFullShadowEvidence
result={tgs} result={tgs}
semanticOverride={semantic} semanticOverride={semantic}
evidenceLabel="LAB V1 · MATERIAL + YOLOX + TGS" evidenceLabel="LAB V1 · EoMT + DDRNet + YOLOX + TGS"
/> />
); );
} }
if (route.linkedTgsResultId && !tgsError) { 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 ( return (
<> <>
<M4ReplayThreatVisual <M4ReplayThreatVisual
resultId={route.baseM4ResultId} resultId={route.baseM4ResultId}
evidenceLabel="LAB V1 · DDRNet" evidenceLabel="LAB V1 · DDRNet"
showReferenceMediaLayers={route.viewKind === "coarse-material-policy-review"} showReferenceMediaLayers
showSpatialOverlaySummary={false} showSpatialOverlaySummary={false}
semantic={semantic} semantic={semantic}
/> />
@@ -132,146 +99,117 @@ export function VegetationShadowResultView({
rigLabel: string; rigLabel: string;
result: VegetationShadowResult; result: VegetationShadowResult;
}) { }) {
const route = result.routeVideo;
const selected = result.candidates.find( const selected = result.candidates.find(
(candidate) => candidate.candidate === result.selectedCandidate, (candidate) => candidate.candidate === result.selectedCandidate,
)!; )!;
const alternative = result.candidates.find(
(candidate) => candidate.candidate !== result.selectedCandidate,
)!;
return ( return (
<LaboratoryWorkTemplate <LaboratoryWorkTemplate
summary={( summary={(
<LaboratorySummary <LaboratorySummary
title="LAB V1 · готовые модели растительности" title="LAB V1 · карта ровера · город + растительность"
description={result.routeVideo description="Один recorded-контур RAVNOVES00 синхронно показывает городской EoMT, природный DDRNet, frozen YOLOX detections и causal TGS. Семантические маски переключаются, чтобы их цвета не скрывали друг друга; геометрическое veto остаётся независимым."
? result.routeVideo.viewKind === "coarse-material-policy-review" status={route
? "M4.8 сохраняет truth-backed сравнение моделей, а штатный M4.7 синхронно показывает coarse material evidence, frozen YOLOX vetoes и causal TGS на всей записи RAVNOVES00. Все слои запечатаны локально и открываются без Worker 006." ? "MULTILAYER RECORDED REVIEW · commands OFF · route truth отсутствует"
: "M4.8 сохраняет truth-backed сравнение моделей, а штатный M4.7 viewer показывает фактический DDRNet prediction на всей записи RAVNOVES00. Все 4489 масок запечатаны локально и открываются без Worker 006." : "ROUTE EVIDENCE MISSING · commands OFF"}
: "Штатный 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 не принят"}
statusTone="warning" statusTone="warning"
facts={[ facts={[
{ label: "Источник", value: "GOOSE validation · 962 размеченных кадра · 12 vegetation hard cases" }, { label: "Источник", value: "RAVNOVES00 · sensor.camera.right · 4489 recorded frames" },
{ label: "Сравнение", value: "DDRNet-39 vs PPLiteSeg · official fine-64 weights" }, { label: "Город", value: "EoMT Cityscapes · sealed E47 semantic archive" },
{ label: "Кейсы", value: "трава · куст · ствол · крона · изгородь · лес · посевы" }, { label: "Растительность", value: "DDRNet-39 fine-64 → coarse mission-neutral materials" },
...(result.routeVideo ? [{ { label: "Safety", value: "YOLOX object boxes + causal TGS · semantic masks не снимают veto" },
label: "Видео", { label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
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` },
]} ]}
brief={{ brief={{
question: "Какие готовые веса лучше различают проезжаемую траву, кусты и стволы на размеченных off-road кадрах?", question: "Можно ли одновременно видеть городской и природный semantic stack, не теряя независимую геометрическую защиту?",
approach: "Обе модели последовательно прогнаны в одном изолированном CUDA-runtime на 962 кадрах. 12 визуальных кейсов выбраны детерминированно по truth-поддержке восьми растительных классов; один M4.8 viewer показывает source, truth, prediction и material-error для выбранной модели.", approach: "EoMT и DDRNet сохранены как два независимых sealed слоя на одной M4 timeline. В штатном M4.7 viewer пользователь переключает только отображаемую маску; YOLOX и TGS остаются активными слоями evidence.",
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." : "Ошибки по каждому типу проверяются в одном штатном инструменте."}`, principalResult: route
limitation: "GOOSE — внешний размеченный домен; RAVNOVES00 — наш fisheye, но без ручной truth-разметки. Материалы — prediction, а не доказательство проходимости. TGS не проецируется в пиксели без отдельной принятой калибровки.", ? "Оба semantic archive доступны в одном viewer. Это не пиксельный fusion и не единая новая модель: городской и природный ответы остаются раздельными."
: "Route archive для этой immutable identity отсутствует.",
limitation: "RAVNOVES00 не имеет ручной truth. DDRNet заметно прыгает между HIGH GRASS, WOODY и UNKNOWN; поэтому subtype нельзя подавать напрямую в planner. Отсутствие класса никогда не означает свободный путь.",
}} }}
method={{ method={{
completeness: "complete", completeness: route ? "complete" : "legacy-partial",
executionClass: "ai-inference", executionClass: "ai-inference",
pipelineId: "goose-fine64-ready-weights-to-ravnoves-policy-shadow/v1", pipelineId: "ravnoves-eomt-ddrnet-yolox-causal-tgs-recorded-review/v1",
components: result.candidates.map((candidate) => ({ components: [
kind: "model" as const, {
name: candidate.loadedModelName, kind: "model",
version: candidate.candidate, name: "EoMT Cityscapes semantic",
role: candidate.candidate === result.selectedCandidate ? "selected policy provider" : "comparison candidate", version: "sealed E47 archive",
identitySha256: candidate.checkpointSha256, 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={( evidence={route ? (
<> <LaboratoryEvidence
<LaboratoryEvidence eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
eyebrow="M4.8 · GOOSE VEGETATION HARD CASES" title="EoMT CITY / DDRNet VEGETATION + YOLOX + CAUSAL TGS · 4489/4489 · TRUTH отсутствует"
title="ERROR: красный — пропуск · жёлтый — лишнее · фиолетовый — перепутан тип · зелёный — совпадение" kind="diagnostic-model"
kind="diagnostic-model" resizable
resizable >
> <VegetationRouteEvidence result={result} />
<M48MaskComparisonVisual </LaboratoryEvidence>
cases={comparisonCases(result)} ) : (
initialCandidate={result.selectedCandidate} <LaboratoryEvidence
/> eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
</LaboratoryEvidence> title="ROUTE ARCHIVE отсутствует"
{result.routeVideo ? ( kind="diagnostic-model"
<LaboratoryEvidence >
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO" <div className="m4-replay-threat-visual__pane-status" role="alert">
title={result.routeVideo.viewKind === "coarse-material-policy-review" Для этой immutable identity нет полного route video evidence.
? "COARSE MATERIAL + YOLOX VETO + CAUSAL TGS · 4489/4489 · TRUTH отсутствует" </div>
: "DDRNet PREDICTION · 4489/4489 кадров · TRUTH для этой записи отсутствует"} </LaboratoryEvidence>
kind="diagnostic-model"
resizable
>
<VegetationRouteEvidence result={result} />
</LaboratoryEvidence>
) : null}
</>
)} )}
result={( result={(
<LaboratoryResultSummary <LaboratoryResultSummary
title={result.routeVideo?.viewKind === "coarse-material-policy-review" title="Многослойный visual review собран; управление не авторизовано"
? "Слои собраны для визуального policy review; управление не авторизовано" status="Semantics advisory · YOLOX/TGS veto cannot be cleared"
: "DDRNet — стартовые веса; перенос на ровер ещё не доказан"}
status={result.routeVideo?.viewKind === "coarse-material-policy-review"
? "Materials are advisory · YOLOX/TGS veto cannot be cleared"
: `${selected.loadedModelName} выбран только как vegetation candidate`}
statusTone="warning" statusTone="warning"
metrics={[ metrics={[
{ {
label: "GOOSE mIoU", label: "Route masks",
value: `${decimal(selected.meanIouPercent, 2)}% / ${decimal(alternative.meanIouPercent, 2)}%`, value: route ? `${route.frameCount}/${route.frameCount}` : "0/4489",
hint: `${selected.candidate} / ${alternative.candidate} · полный validation split`, hint: "sealed local playback · Worker для открытия не нужен",
}, },
{ {
label: "Vegetation IoU", label: "Semantic sources",
value: `${decimal(selected.vegetationMeanIouPercent, 2)}% / ${decimal(alternative.vegetationMeanIouPercent, 2)}%`, value: route ? "2 independent layers" : "0",
hint: "агрегация классов grass/vegetation/bush/tree и родственных fine-64 labels", hint: "EoMT CITY / DDRNet VEGETATION · display switches, evidence does not fuse",
}, },
{ {
label: "Worker shadow p95", label: "Vegetation worker p95",
value: `${decimal(selected.shadowLatencyP95Ms, 2)} / ${decimal(alternative.shadowLatencyP95Ms, 2)} ms`, value: `${decimal(selected.shadowLatencyP95Ms, 2)} ms`,
hint: "чистый inference · одна тяжёлая модель за раз", hint: "изолированный DDRNet inference; не совместный realtime stack",
}, },
{ {
label: "Cold prewarm", label: "Vegetation peak VRAM",
value: `${decimal(selected.shadowPrewarmLatencyMs, 1)} / ${decimal(alternative.shadowPrewarmLatencyMs, 1)} ms`, value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} GiB`,
hint: "один явный inference до допуска кадров; исключён из steady-state p95", 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={{ conclusion={{
proved: "Обе официальные fine-64 модели воспроизводимо запускаются на Worker 006; DDRNet лучше по aggregate vegetation IoU. Truth-backed hard cases прямо показывают траву, кусты и стволы, а не случайные автомобили и здания.", proved: "На одной recorded timeline доступны городской EoMT, природный DDRNet, YOLOX detections и causal TGS; LAB автономна от Worker.",
notProved: "Не доказаны accuracy на нашем fisheye-домене, папоротник как отдельный материал, collision safety и physical-live поведение ровера. Видео позволяет увидеть temporal stability, но без truth не превращает её в метрику качества.", notProved: "Не доказаны совместный live-runtime EoMT+DDRNet, truth accuracy на fisheye, стабильные vegetation subtypes и безопасное управление ровером.",
decision: result.routeVideo?.viewKind === "coarse-material-policy-review" decision: "Использовать маски только для диагностики. Следующий qualification gate — motion-aware temporal vegetation fusion и отдельный совместный realtime load test; до него planner/actuation остаются OFF.",
? "На одном 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 геометрию не ослаблять.",
}} }}
/> />
)} )}
@@ -10,6 +10,7 @@ export type LaboratoryProfileId =
| "rig-camera-local-surface-v1" | "rig-camera-local-surface-v1"
| "rig-track-geometry-temporal-v1" | "rig-track-geometry-temporal-v1"
| "rig-ravnoves-perception-gate-v1" | "rig-ravnoves-perception-gate-v1"
| "rig-goose-vegetation-benchmark-v1"
| "rig-pointpillars-transfer-v1" | "rig-pointpillars-transfer-v1"
| "rig-right-yolox-lidar-range-v1" | "rig-right-yolox-lidar-range-v1"
| "rig-nvidia-ready-stack-v1" | "rig-nvidia-ready-stack-v1"
@@ -63,12 +64,19 @@ interface KnownWorkDefinition {
const rig = (rigLabel: string): string => rigLabel.trim() || "Сенсорный риг"; const rig = (rigLabel: string): string => rigLabel.trim() || "Сенсорный риг";
const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`>, KnownWorkDefinition>> = { 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": { "lab-v1-vegetation-shadow": {
profileId: "rig-ravnoves-perception-gate-v1", 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", experimentId: "lab-v1-vegetation-mission-policy",
experimentName: "DDRNet vs PPLiteSeg · truth-backed vegetation hard cases", experimentName: "RAVNOVES00 · city + vegetation + TGS review",
variantName: "LAB V1 · готовые vegetation weights · GOOSE truth", variantName: "LAB V1 · EoMT + DDRNet + YOLOX + TGS · commands OFF",
}, },
"m48-object-centric-quality": { "m48-object-centric-quality": {
profileId: "rig-dual-evidence-virtual-corridor-v1", profileId: "rig-dual-evidence-virtual-corridor-v1",
@@ -18,6 +18,7 @@ function mergeResults(
next: AdvancedLaboratoryResults, next: AdvancedLaboratoryResults,
): AdvancedLaboratoryResults { ): AdvancedLaboratoryResults {
return { return {
vegetationBenchmark: next.vegetationBenchmark ?? current.vegetationBenchmark,
vegetationShadow: next.vegetationShadow ?? current.vegetationShadow, vegetationShadow: next.vegetationShadow ?? current.vegetationShadow,
m47Graph: next.m47Graph ?? current.m47Graph, m47Graph: next.m47Graph ?? current.m47Graph,
m48: next.m48 ?? current.m48, m48: next.m48 ?? current.m48,
@@ -122,6 +123,7 @@ export function useAdvancedLaboratoryCatalog({
const indexedResultId = index.find((item) => item.workId === selectedWorkId)?.resultId; const indexedResultId = index.find((item) => item.workId === selectedWorkId)?.resultId;
if ( if (
[ [
"lab-v1-vegetation-benchmark",
"lab-v1-vegetation-shadow", "lab-v1-vegetation-shadow",
"m47-reference-graph-shadow", "m47-reference-graph-shadow",
"m48-object-centric-quality", "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.doesNotMatch(source, /centersXyM\.map\(/);
assert.match(source, /fetchE47SemanticSlamResult/); assert.match(source, /fetchE47SemanticSlamResult/);
assert.match(source, /next\.baseM4ResultId !== result\.source\.linkedVisualResultId/); 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.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.doesNotMatch(visual, /classifiedSpatialLayer \|\| !showReferenceMediaLayers \? \[\]/);
assert.match( assert.match(
visual, visual,
@@ -5,6 +5,7 @@ import { after, before, test } from "node:test";
import { createServer } from "vite"; import { createServer } from "vite";
let server; let server;
let fetchVegetationBenchmarkResult;
let fetchVegetationShadowResult; let fetchVegetationShadowResult;
before(async () => { before(async () => {
@@ -13,7 +14,7 @@ before(async () => {
logLevel: "silent", logLevel: "silent",
server: { middlewareMode: true }, server: { middlewareMode: true },
}); });
({ fetchVegetationShadowResult } = await server.ssrLoadModule( ({ fetchVegetationBenchmarkResult, fetchVegetationShadowResult } = await server.ssrLoadModule(
"/src/core/laboratory/vegetationShadow.ts", "/src/core/laboratory/vegetationShadow.ts",
)); ));
}); });
@@ -23,6 +24,7 @@ after(async () => {
}); });
const resultId = `lab-v1-vegetation-shadow-${"a".repeat(64)}`; const resultId = `lab-v1-vegetation-shadow-${"a".repeat(64)}`;
const benchmarkResultId = `lab-v1-vegetation-benchmark-${"d".repeat(64)}`;
function candidate(candidateKey, vegetationIou) { function candidate(candidateKey, vegetationIou) {
return { return {
@@ -111,21 +113,26 @@ function coarseRouteVideo() {
linked_tgs_result_id: `m49-tgs-full-shadow-${"2".repeat(64)}`, linked_tgs_result_id: `m49-tgs-full-shadow-${"2".repeat(64)}`,
taxonomy: { taxonomy: {
schema_version: "missioncore.lab-v1-terrain-policy-taxonomy/v1", schema_version: "missioncore.lab-v1-terrain-policy-taxonomy/v1",
classes: Array.from({ length: 9 }, (_, classId) => ({ classes: Array.from({ length: 10 }, (_, classId) => ({
class_id: classId, class_id: classId,
label: `policy-${classId}`, label: `policy-${classId}`,
color_rgb: [classId, classId, classId], color_rgb: [classId, classId, classId],
disposition: classId === 0 ? "ambiguous" : "prediction", disposition: classId === 0 ? "ambiguous" : classId === 9 ? "undefined" : "prediction",
material_class: classId === 0 ? null : "grass", material_class: classId === 0 || classId === 9 ? null : "grass",
evidence_state: classId === 0 ? "UNOBSERVED" : "SUPPORTED_GROUND", 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: { mask_archive: {
path: "video/coarse-material-policy-masks.zip", path: "video/coarse-material-policy-masks.zip",
sha256: "8".repeat(64), sha256: "8".repeat(64),
byte_length: 2048, byte_length: 2048,
}, },
valid_fov: {
mask_path: "video/valid-fov-mask.png",
mask_sha256: "7".repeat(64),
outside_valid_fov_class_id: 9,
},
policy: { policy: {
presets: { presets: {
urban: { grass: "NO_GO" }, urban: { grass: "NO_GO" },
@@ -220,23 +227,55 @@ test("vegetation LAB parses coarse material policy and sealed TGS binding", asyn
}); });
assert.equal(result.routeVideo.viewKind, "coarse-material-policy-review"); assert.equal(result.routeVideo.viewKind, "coarse-material-policy-review");
assert.match(result.routeVideo.linkedTgsResultId, /^m49-tgs-full-shadow-/); 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.taxonomy[0].evidenceState, "UNOBSERVED");
assert.equal(result.routeVideo.policyPresets.urban.grass, "NO_GO"); assert.equal(result.routeVideo.policyPresets.urban.grass, "NO_GO");
assert.equal(result.routeVideo.fusionMode, "synchronised-multilayer-review"); assert.equal(result.routeVideo.fusionMode, "synchronised-multilayer-review");
}); });
test("vegetation LAB reuses the admitted M4.8 and M4.7 instruments", async () => { test("vegetation GOOSE benchmark opens through its separate archival endpoint", async () => {
const resultSource = await readFile( let requestedUrl = "";
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url), const result = await fetchVegetationBenchmarkResult(benchmarkResultId, {
"utf8", 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.match(resultSource, /M48MaskComparisonVisual/); 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, /M4ReplayThreatVisual/);
assert.match(resultSource, /M49TgsFullShadowEvidence/); assert.match(resultSource, /M49TgsFullShadowEvidence/);
assert.match(resultSource, /semanticOverride/); assert.match(resultSource, /semanticOverride/);
assert.match(resultSource, /EoMT CITY \/ DDRNet VEGETATION/);
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 2); assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 2);
assert.match(resultSource, /linkedTgsResultId/); 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/); assert.doesNotMatch(resultSource, /VegetationRouteVisual|urban\/rural\/off-road presets/);
await assert.rejects( await assert.rejects(
access(new URL("../src/workspaces/laboratory/VegetationShadowVisual.tsx", import.meta.url)), 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"
}
}
+1
View File
@@ -212,6 +212,7 @@
} }
], ],
"legacy_work_ids": [ "legacy_work_ids": [
"lab-v1-vegetation-benchmark",
"m48r3-static-occupancy-shadow", "m48r3-static-occupancy-shadow",
"m47-reference-graph-shadow", "m47-reference-graph-shadow",
"e31-source-binding", "e31-source-binding",
+9 -2
View File
@@ -282,10 +282,17 @@
"lifecycle": "current", "lifecycle": "current",
"visual_evidence": "available" "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", "catalog_id": "lab-v1-vegetation-shadow",
"evidence_id": "lab-v1-vegetation-shadow-ad4d9fbbb21ff8a270b77f559b4e78dcdaf0455afd61afb5033009623984e554", "evidence_id": "lab-v1-vegetation-shadow-394a5bca860e49a619a90c0f267259fd17e550a0abc4b51f821b1f482040c00e",
"signal": "failed", "signal": "progress",
"lifecycle": "current", "lifecycle": "current",
"visual_evidence": "available" "visual_evidence": "available"
} }
@@ -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, mission_policy_path: Path,
provider_label_map_path: Path, provider_label_map_path: Path,
m49_tgs_full_shadow_root: Path, m49_tgs_full_shadow_root: Path,
valid_fov_mask_path: Path,
output_root: Path, output_root: Path,
created_at_utc: str | None = None, created_at_utc: str | None = None,
) -> Path: ) -> Path:
@@ -142,6 +143,7 @@ def seal_vegetation_policy_review(
raise VegetationPolicyReviewError("fine mask archive identity changed") raise VegetationPolicyReviewError("fine mask archive identity changed")
raw_archive_path = base_root / "video" / "ddrnet-semantic-masks.zip" raw_archive_path = base_root / "video" / "ddrnet-semantic-masks.zip"
fine_taxonomy = _object(base_route.get("taxonomy"), "fine taxonomy") 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) output_root.mkdir(mode=0o700, parents=True, exist_ok=True)
temporary = Path(tempfile.mkdtemp(prefix=".lab-v1-policy-", dir=output_root)) temporary = Path(tempfile.mkdtemp(prefix=".lab-v1-policy-", dir=output_root))
@@ -152,11 +154,23 @@ def seal_vegetation_policy_review(
artifacts=base.get("artifacts"), artifacts=base.get("artifacts"),
) )
policy_archive = temporary / "video" / "coarse-material-policy-masks.zip" 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( policy_counts = build_policy_mask_archive(
source_archive=raw_archive_path, source_archive=raw_archive_path,
destination_archive=policy_archive, destination_archive=policy_archive,
fine_taxonomy=fine_taxonomy, fine_taxonomy=fine_taxonomy,
provider_label_map=provider_map, provider_label_map=provider_map,
valid_fov_mask=valid_fov_destination,
) )
policy_archive_proof = { policy_archive_proof = {
"role": "route-coarse-material-mask-archive", "role": "route-coarse-material-mask-archive",
@@ -180,6 +194,11 @@ def seal_vegetation_policy_review(
"taxonomy": policy_taxonomy(), "taxonomy": policy_taxonomy(),
"aggregate_prediction_pixels": policy_counts, "aggregate_prediction_pixels": policy_counts,
"linked_tgs_result_id": tgs.result_id, "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": { "policy": {
"profile_id": mission_policy["profile_id"], "profile_id": mission_policy["profile_id"],
"profile_sha256": sha256_path(mission_policy_path), "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", "spatial_safety_veto_layer": "M4.9 full TGS gravity-local costmap",
"temporal_consensus_owner": "TGS causal rolling 1 s and metric obstacle tracks", "temporal_consensus_owner": "TGS causal rolling 1 s and metric obstacle tracks",
"camera_semantic_temporal_filter": "none", "camera_semantic_temporal_filter": "none",
"camera_valid_fov_filter": "sealed exact KB4 valid-FOV mask",
"reason": "No admitted TGS-to-camera pixel projection exists.", "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 " "Vegetation semantics never clears YOLOX, LiDAR, metric obstacle "
"or TGS vetoes." "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 " "TGS remains in gravity-local space; no uncalibrated pixel "
"projection is fabricated." "projection is fabricated."
@@ -269,6 +289,7 @@ def main() -> None:
parser.add_argument("--mission-policy-path", type=Path, required=True) parser.add_argument("--mission-policy-path", type=Path, required=True)
parser.add_argument("--provider-label-map-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("--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) parser.add_argument("--output-root", type=Path, required=True)
args = parser.parse_args() args = parser.parse_args()
print(seal_vegetation_policy_review(**vars(args))) print(seal_vegetation_policy_review(**vars(args)))
@@ -90,6 +90,14 @@ POLICY_CLASSES: Final = (
"material_class": "vegetation_unknown", "material_class": "vegetation_unknown",
"evidence_state": "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 = { _MATERIAL_TO_CLASS: Final = {
@@ -155,10 +163,18 @@ def build_policy_mask_archive(
destination_archive: Path, destination_archive: Path,
fine_taxonomy: dict[str, object], fine_taxonomy: dict[str, object],
provider_label_map: dict[str, Any], provider_label_map: dict[str, Any],
valid_fov_mask: Path,
) -> list[int]: ) -> list[int]:
"""Map every fine mask to coarse evidence; safety vetoes remain separate layers.""" """Map every fine mask to coarse evidence; safety vetoes remain separate layers."""
lut = fine_to_policy_lut(fine_taxonomy, provider_label_map) 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) counts = np.zeros(len(POLICY_CLASSES), dtype=np.int64)
destination_archive.parent.mkdir(mode=0o700, parents=True, exist_ok=True) destination_archive.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
try: try:
@@ -175,6 +191,7 @@ def build_policy_mask_archive(
f"fine mask {member} has shape {fine.shape}, expected {(HEIGHT, WIDTH)}" f"fine mask {member} has shape {fine.shape}, expected {(HEIGHT, WIDTH)}"
) )
coarse = lut[fine] coarse = lut[fine]
coarse[~valid_fov] = 9
counts += np.bincount( counts += np.bincount(
coarse.reshape(-1), coarse.reshape(-1),
minlength=len(POLICY_CLASSES), minlength=len(POLICY_CLASSES),
+28 -1
View File
@@ -327,6 +327,7 @@ def seal_vegetation_shadow_lab(
mission_policy_path: Path | None = None, mission_policy_path: Path | None = None,
provider_label_map_path: Path | None = None, provider_label_map_path: Path | None = None,
m49_tgs_full_shadow_root: Path | None = None, m49_tgs_full_shadow_root: Path | None = None,
valid_fov_mask_path: Path | None = None,
) -> Path: ) -> Path:
roots = { roots = {
("ddrnet", "goose"): ddrnet_goose_root.resolve(), ("ddrnet", "goose"): ddrnet_goose_root.resolve(),
@@ -349,6 +350,7 @@ def seal_vegetation_shadow_lab(
mission_policy_path, mission_policy_path,
provider_label_map_path, provider_label_map_path,
m49_tgs_full_shadow_root, m49_tgs_full_shadow_root,
valid_fov_mask_path,
) )
if any(value is not None for value in policy_inputs) and not all( if any(value is not None for value in policy_inputs) and not all(
value is not None for value in policy_inputs value is not None for value in policy_inputs
@@ -385,6 +387,7 @@ def seal_vegetation_shadow_lab(
mission_policy_path is not None mission_policy_path is not None
and provider_label_map_path is not None and provider_label_map_path is not None
and m49_tgs_full_shadow_root 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 and route_video is not None
): ):
repository_root = mission_policy_path.resolve().parents[2] 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 linked_tgs_result_id is not None
and mission_policy_path is not None and mission_policy_path is not None
and provider_label_map_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" 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( policy_counts = build_policy_mask_archive(
source_archive=route_video_archive, source_archive=route_video_archive,
destination_archive=policy_archive, destination_archive=policy_archive,
fine_taxonomy=_object(route_video["taxonomy"], "fine video taxonomy"), fine_taxonomy=_object(route_video["taxonomy"], "fine video taxonomy"),
provider_label_map=provider_label_map, provider_label_map=provider_label_map,
valid_fov_mask=valid_fov_destination,
) )
policy_descriptor = { policy_descriptor = {
"role": "route-coarse-material-mask-archive", "role": "route-coarse-material-mask-archive",
@@ -571,6 +586,11 @@ def seal_vegetation_shadow_lab(
"taxonomy": policy_taxonomy(), "taxonomy": policy_taxonomy(),
"aggregate_prediction_pixels": policy_counts, "aggregate_prediction_pixels": policy_counts,
"linked_tgs_result_id": linked_tgs_result_id, "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": { "policy": {
"profile_id": mission_policy["profile_id"], "profile_id": mission_policy["profile_id"],
"profile_sha256": sha256_path(mission_policy_path), "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" "TGS causal rolling 1 s and metric obstacle tracks"
), ),
"camera_semantic_temporal_filter": "none", "camera_semantic_temporal_filter": "none",
"camera_valid_fov_filter": "sealed exact KB4 valid-FOV mask",
"reason": "No admitted TGS-to-camera pixel projection exists.", "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.", "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("--mission-policy-path", type=Path)
parser.add_argument("--provider-label-map-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("--m49-tgs-full-shadow-root", type=Path)
parser.add_argument("--valid-fov-mask-path", type=Path)
return parser.parse_args() return parser.parse_args()
@@ -739,6 +765,7 @@ def main() -> None:
mission_policy_path=args.mission_policy_path, mission_policy_path=args.mission_policy_path,
provider_label_map_path=args.provider_label_map_path, provider_label_map_path=args.provider_label_map_path,
m49_tgs_full_shadow_root=args.m49_tgs_full_shadow_root, m49_tgs_full_shadow_root=args.m49_tgs_full_shadow_root,
valid_fov_mask_path=args.valid_fov_mask_path,
) )
print(destination) print(destination)
+15 -1
View File
@@ -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_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.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 ( from k1link.web.map_api import (
MapGatewayConfiguration, MapGatewayConfiguration,
MapGatewayProxy, 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_projects_api import build_simulation_projects_router
from k1link.web.simulation_world_provider_api import build_simulation_world_provider_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.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 from k1link.web.viewer_diagnostics_api import build_viewer_diagnostics_router
REPOSITORY_ROOT = Path(__file__).resolve().parents[3] 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( app.include_router(
build_m49_physical_safety_playback_router( build_m49_physical_safety_playback_router(
root_provider=lambda: ( root_provider=lambda: (
+75 -17
View File
@@ -5,7 +5,6 @@ from __future__ import annotations
import copy import copy
import hashlib import hashlib
import json import json
import re
import zipfile import zipfile
from collections.abc import Callable from collections.abc import Callable
from functools import lru_cache from functools import lru_cache
@@ -20,10 +19,9 @@ from k1link.laboratory.evidence_report import (
LaboratoryEvidenceReportError, LaboratoryEvidenceReportError,
verify_laboratory_evidence_result, 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] RootProvider = Callable[[], Path | None]
RESULT_ID: Final = re.compile(rf"^{re.escape(RESULT_PREFIX)}[a-f0-9]{{64}}$")
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024 _MAX_DOCUMENT_BYTES: Final = 1024 * 1024
_DEFINITION: Final = LaboratoryEvidenceDefinition( _DEFINITION: Final = LaboratoryEvidenceDefinition(
work_id="lab-v1-vegetation-shadow", work_id="lab-v1-vegetation-shadow",
@@ -32,25 +30,55 @@ _DEFINITION: Final = LaboratoryEvidenceDefinition(
document_name="result.json", document_name="result.json",
result_schema_version=LAB_SCHEMA, 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( def build_vegetation_shadow_lab_router(
*, root_provider: RootProvider = lambda: None, *, root_provider: RootProvider = lambda: None,
) -> APIRouter: ) -> APIRouter:
router = APIRouter( return _build_vegetation_lab_router(
prefix="/api/v1/laboratory/vegetation-shadow", 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"], tags=["laboratory"],
) )
@router.get("/{result_id}") @router.get("/{result_id}")
def get_result(result_id: str) -> dict[str, object]: def get_result(result_id: str) -> dict[str, object]:
candidate = _resolve_candidate(root_provider, result_id) candidate = _resolve_candidate(root_provider, definition, result_id)
return {**copy.deepcopy(_read_verified(candidate)), "access": "read-only"} return {**copy.deepcopy(_read_verified(candidate, definition)), "access": "read-only"}
@router.get("/{result_id}/assets/{asset_path:path}") @router.get("/{result_id}/assets/{asset_path:path}")
def get_asset(result_id: str, asset_path: str) -> FileResponse: def get_asset(result_id: str, asset_path: str) -> FileResponse:
candidate = _resolve_candidate(root_provider, result_id) candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate) manifest = _read_verified(candidate, definition)
artifacts = manifest.get("artifacts") artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list): if not isinstance(artifacts, list):
raise HTTPException(status_code=404, detail="Vegetation LAB asset not found") 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}") @router.get("/{result_id}/masks/{sequence}")
def get_video_mask(result_id: str, sequence: int) -> Response: def get_video_mask(result_id: str, sequence: int) -> Response:
candidate = _resolve_candidate(root_provider, result_id) candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate) manifest = _read_verified(candidate, definition)
route_video = manifest.get("route_video") route_video = manifest.get("route_video")
if ( if (
not isinstance(route_video, dict) not isinstance(route_video, dict)
@@ -168,9 +196,13 @@ def _configured_root(provider: RootProvider) -> Path | None:
return root if root.is_dir() else 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) 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") raise HTTPException(status_code=404, detail="Vegetation LAB result not found")
candidate = root / result_id candidate = root / result_id
if candidate.is_symlink(): if candidate.is_symlink():
@@ -184,7 +216,10 @@ def _resolve_candidate(provider: RootProvider, result_id: str) -> Path:
return resolved return resolved
def _read_verified(candidate: Path) -> dict[str, Any]: def _read_verified(
candidate: Path,
definition: LaboratoryEvidenceDefinition,
) -> dict[str, Any]:
try: try:
rows: list[tuple[str, int, int, int, int]] = [] rows: list[tuple[str, int, int, int, int]] = []
for path in sorted(candidate.rglob("*"), key=lambda item: item.as_posix()): for path in sorted(candidate.rglob("*"), key=lambda item: item.as_posix()):
@@ -202,19 +237,39 @@ def _read_verified(candidate: Path) -> dict[str, Any]:
status_code=503, status_code=503,
detail="Vegetation LAB evidence failed verification", detail="Vegetation LAB evidence failed verification",
) from None ) 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) @lru_cache(maxsize=16)
def _read_verified_cached( def _read_verified_cached(
candidate_text: str, candidate_text: str,
signature: tuple[tuple[str, int, int, int, int], ...], 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]: ) -> dict[str, Any]:
del signature del signature
candidate = Path(candidate_text) 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: try:
verify_laboratory_evidence_result(_DEFINITION, candidate) verify_laboratory_evidence_result(definition, candidate)
path = candidate / "result.json" path = candidate / definition.document_name
if path.stat().st_size > _MAX_DOCUMENT_BYTES: if path.stat().st_size > _MAX_DOCUMENT_BYTES:
raise LaboratoryEvidenceReportError("Vegetation LAB document is too large") raise LaboratoryEvidenceReportError("Vegetation LAB document is too large")
payload = json.loads(path.read_text("utf-8")) payload = json.loads(path.read_text("utf-8"))
@@ -228,4 +283,7 @@ def _read_verified_cached(
return payload return payload
__all__ = ["build_vegetation_shadow_lab_router"] __all__ = [
"build_vegetation_benchmark_lab_router",
"build_vegetation_shadow_lab_router",
]
+2 -1
View File
@@ -127,9 +127,10 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
repository_root / "config" / "laboratories" repository_root / "config" / "laboratories"
) )
assert len(registry.definitions) == 43 assert len(registry.definitions) == 44
assert {item.work_id for item in registry.definitions} >= { assert {item.work_id for item in registry.definitions} >= {
"lab-v1-vegetation-shadow", "lab-v1-vegetation-shadow",
"lab-v1-vegetation-benchmark",
"e31-source-binding", "e31-source-binding",
"e46j-raw-fisheye-realtime", "e46j-raw-fisheye-realtime",
"e47-semantic-slam-shadow", "e47-semantic-slam-shadow",
@@ -80,7 +80,7 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
root / "config" / "laboratory-value-review.json" 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} >= { assert {entry.catalog_id for entry in registry.entries} >= {
"e28-local-surface", "e28-local-surface",
"e46d-temporal-failure-audit", "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-fail-closed-evidence",
"m49-tgs-full-shadow", "m49-tgs-full-shadow",
"lab-v1-vegetation-shadow", "lab-v1-vegetation-shadow",
"lab-v1-vegetation-benchmark",
} }
+51 -3
View File
@@ -1,16 +1,20 @@
from __future__ import annotations from __future__ import annotations
import hashlib import hashlib
import io
import json import json
import shutil import shutil
import zipfile import zipfile
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
import numpy as np
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from PIL import Image
import k1link.laboratory.vegetation_policy_review as policy_review_module 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 import k1link.laboratory.vegetation_shadow_lab as vegetation_lab_module
from k1link.laboratory import LaboratoryEvidenceRegistry from k1link.laboratory import LaboratoryEvidenceRegistry
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
@@ -21,6 +25,45 @@ from k1link.web.vegetation_shadow_lab_api import build_vegetation_shadow_lab_rou
REPOSITORY_ROOT = Path(__file__).resolve().parents[1] 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: def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest() return hashlib.sha256(path.read_bytes()).hexdigest()
@@ -290,9 +333,12 @@ def test_policy_review_reuses_sealed_video_and_links_yolox_tgs(
def fake_policy_archive(**kwargs) -> list[int]: def fake_policy_archive(**kwargs) -> list[int]:
shutil.copyfile(kwargs["source_archive"], kwargs["destination_archive"]) 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) 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( result_root = seal_vegetation_policy_review(
base_lab_root=base_root, base_lab_root=base_root,
mission_policy_path=REPOSITORY_ROOT mission_policy_path=REPOSITORY_ROOT
@@ -300,6 +346,7 @@ def test_policy_review_reuses_sealed_video_and_links_yolox_tgs(
provider_label_map_path=REPOSITORY_ROOT provider_label_map_path=REPOSITORY_ROOT
/ "config/perception/lab-v1-vegetation-provider-label-map-v1.json", / "config/perception/lab-v1-vegetation-provider-label-map-v1.json",
m49_tgs_full_shadow_root=tmp_path / "sealed-tgs", m49_tgs_full_shadow_root=tmp_path / "sealed-tgs",
valid_fov_mask_path=valid_fov_mask,
output_root=tmp_path / "results", output_root=tmp_path / "results",
created_at_utc="2026-08-28T08:00:00+00:00", created_at_utc="2026-08-28T08:00:00+00:00",
) )
@@ -312,8 +359,9 @@ def test_policy_review_reuses_sealed_video_and_links_yolox_tgs(
assert route["taxonomy"]["schema_version"] == ( assert route["taxonomy"]["schema_version"] == (
"missioncore.lab-v1-terrain-policy-taxonomy/v1" "missioncore.lab-v1-terrain-policy-taxonomy/v1"
) )
assert len(route["taxonomy"]["classes"]) == 9 assert len(route["taxonomy"]["classes"]) == 10
assert len(manifest["artifacts"]) == 79 assert route["valid_fov"]["outside_valid_fov_class_id"] == 9
assert len(manifest["artifacts"]) == 80
assert manifest["authority"]["commands_enabled"] is False assert manifest["authority"]["commands_enabled"] is False
assert manifest["decision"]["multilayer_policy_review_ready"] is True assert manifest["decision"]["multilayer_policy_review_ready"] is True