feat(lab): seal full RAVNOVES004TREE semantic review
This commit is contained in:
@@ -106,6 +106,39 @@ export interface VegetationMixedRouteReview {
|
||||
cases: readonly VegetationMixedRouteCase[];
|
||||
}
|
||||
|
||||
export interface VegetationFullRouteLayer {
|
||||
name: string;
|
||||
resultId: string;
|
||||
frameCount: 6830;
|
||||
taxonomy: readonly VegetationVideoSemanticClass[];
|
||||
inferenceFps: number;
|
||||
latencyP95Ms: number;
|
||||
peakReservedVramBytes: number;
|
||||
}
|
||||
|
||||
export interface VegetationFullRouteReview {
|
||||
sourceId: "RAVNOVES004TREE";
|
||||
sessionId: "20260828T130511Z_viewer_live";
|
||||
sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0";
|
||||
sourceJobInputSha256: string;
|
||||
sourceStreamSha256: string;
|
||||
recordedMediaSourceId: "recorded.camera.6a3945242828a038";
|
||||
recordedMediaGenerationSha256: string;
|
||||
frameCount: 6830;
|
||||
width: 800;
|
||||
height: 600;
|
||||
timelineStartSeconds: number;
|
||||
timelineEndSeconds: number;
|
||||
frameSourceTimesNs: readonly number[];
|
||||
decodeRepair: {
|
||||
repairedFrameCount: 1;
|
||||
sequence: 6092;
|
||||
method: "duplicate-previous-decoded-frame";
|
||||
};
|
||||
city: VegetationFullRouteLayer;
|
||||
vegetation: VegetationFullRouteLayer;
|
||||
}
|
||||
|
||||
export interface VegetationShadowResult {
|
||||
resultId: string;
|
||||
createdAtUtc: string;
|
||||
@@ -116,6 +149,7 @@ export interface VegetationShadowResult {
|
||||
validationCases: readonly VegetationVisualCase[];
|
||||
routeVideo: VegetationRouteVideo | null;
|
||||
routeReview: VegetationMixedRouteReview | null;
|
||||
routeFullReview: VegetationFullRouteReview | null;
|
||||
limitations: readonly string[];
|
||||
visualShadowReady: true;
|
||||
missionPolicyReadyForConfiguration: true;
|
||||
@@ -545,6 +579,246 @@ function mixedRouteReviewValue(
|
||||
};
|
||||
}
|
||||
|
||||
function fullRouteTaxonomyValue(
|
||||
value: unknown,
|
||||
label: string,
|
||||
schema: string,
|
||||
classCount: number,
|
||||
): readonly VegetationVideoSemanticClass[] {
|
||||
const taxonomy = objectValue(value, `${label}.taxonomy`);
|
||||
exact(taxonomy.schema_version, schema, `${label}.taxonomy.schema`);
|
||||
const classes = arrayValue(taxonomy.classes, `${label}.taxonomy.classes`).map(
|
||||
(raw, expectedId): VegetationVideoSemanticClass => {
|
||||
const item = objectValue(raw, `${label}.taxonomy[${expectedId}]`);
|
||||
const classId = integerValue(item.class_id, `${label}.class_id[${expectedId}]`);
|
||||
if (classId !== expectedId) {
|
||||
throw new VegetationShadowContractError(`${label}: taxonomy order changed.`);
|
||||
}
|
||||
const color = arrayValue(item.color_rgb, `${label}.color[${expectedId}]`)
|
||||
.map((channel, index) => integerValue(channel, `${label}.color[${expectedId}][${index}]`));
|
||||
if (color.length !== 3 || color.some((channel) => channel > 255)) {
|
||||
throw new VegetationShadowContractError(`${label}: taxonomy color invalid.`);
|
||||
}
|
||||
const disposition = item.disposition;
|
||||
if (
|
||||
disposition !== "labeled"
|
||||
&& disposition !== "ambiguous"
|
||||
&& disposition !== "prediction"
|
||||
&& disposition !== "undefined"
|
||||
) {
|
||||
throw new VegetationShadowContractError(`${label}: taxonomy disposition changed.`);
|
||||
}
|
||||
return {
|
||||
classId,
|
||||
label: textValue(item.label, `${label}.label[${expectedId}]`),
|
||||
colorRgb: color as unknown as readonly [number, number, number],
|
||||
disposition,
|
||||
materialClass: item.material_class === null || item.material_class === undefined
|
||||
? null
|
||||
: textValue(item.material_class, `${label}.material[${expectedId}]`),
|
||||
evidenceState: item.evidence_state === null || item.evidence_state === undefined
|
||||
? null
|
||||
: textValue(item.evidence_state, `${label}.evidence[${expectedId}]`),
|
||||
};
|
||||
},
|
||||
);
|
||||
if (classes.length !== classCount) {
|
||||
throw new VegetationShadowContractError(`${label}: taxonomy size changed.`);
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
function fullRouteLayerValue(
|
||||
value: unknown,
|
||||
layer: "city" | "vegetation",
|
||||
): VegetationFullRouteLayer {
|
||||
const label = `vegetation.route_full_review.layers.${layer}`;
|
||||
const row = objectValue(value, label);
|
||||
const resultId = textValue(row.result_id, `${label}.result_id`);
|
||||
const identity = layer === "city"
|
||||
? /^result-[a-f0-9]{64}$/
|
||||
: /^lab-v1-ravnoves-video-ddrnet-[a-f0-9]{64}$/;
|
||||
if (!identity.test(resultId)) {
|
||||
throw new VegetationShadowContractError(`${label}: identity invalid.`);
|
||||
}
|
||||
exact(row.frame_count, 6830, `${label}.frame_count`);
|
||||
const archive = objectValue(row.mask_archive, `${label}.mask_archive`);
|
||||
exact(
|
||||
archive.path,
|
||||
layer === "city" ? "video/eomt-semantic-masks.zip" : "video/ddrnet-semantic-masks.zip",
|
||||
`${label}.mask_archive.path`,
|
||||
);
|
||||
const digest = textValue(archive.sha256, `${label}.mask_archive.sha256`);
|
||||
if (!SHA256.test(digest)) {
|
||||
throw new VegetationShadowContractError(`${label}: archive digest invalid.`);
|
||||
}
|
||||
integerValue(archive.byte_length, `${label}.mask_archive.byte_length`);
|
||||
return {
|
||||
name: textValue(row.name, `${label}.name`),
|
||||
resultId,
|
||||
frameCount: 6830,
|
||||
taxonomy: fullRouteTaxonomyValue(
|
||||
row.taxonomy,
|
||||
label,
|
||||
layer === "city"
|
||||
? "missioncore.recorded-eomt-taxonomy/v1"
|
||||
: "missioncore.lab-v1-vegetation-taxonomy/v1",
|
||||
layer === "city" ? 16 : 64,
|
||||
),
|
||||
inferenceFps: numberValue(row.inference_fps, `${label}.inference_fps`),
|
||||
latencyP95Ms: numberValue(row.latency_p95_ms, `${label}.latency_p95_ms`),
|
||||
peakReservedVramBytes: integerValue(
|
||||
row.peak_reserved_vram_bytes,
|
||||
`${label}.peak_reserved_vram_bytes`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function fullRouteReviewValue(value: unknown): VegetationFullRouteReview | null {
|
||||
if (value === null || value === undefined) return null;
|
||||
const row = objectValue(value, "vegetation.route_full_review");
|
||||
exact(row.source_id, "RAVNOVES004TREE", "vegetation.route_full_review.source_id");
|
||||
exact(
|
||||
row.session_id,
|
||||
"20260828T130511Z_viewer_live",
|
||||
"vegetation.route_full_review.session_id",
|
||||
);
|
||||
exact(
|
||||
row.source_job_id,
|
||||
"recorded-camera-eb2783c5480d56bda07c8af0",
|
||||
"vegetation.route_full_review.source_job_id",
|
||||
);
|
||||
exact(row.frame_count, 6830, "vegetation.route_full_review.frame_count");
|
||||
exact(row.width, 800, "vegetation.route_full_review.width");
|
||||
exact(row.height, 600, "vegetation.route_full_review.height");
|
||||
exact(row.ground_truth, false, "vegetation.route_full_review.ground_truth");
|
||||
const sourceJobInputSha256 = textValue(
|
||||
row.source_job_input_sha256,
|
||||
"vegetation.route_full_review.source_job_input_sha256",
|
||||
);
|
||||
const sourceStreamSha256 = textValue(
|
||||
row.source_stream_sha256,
|
||||
"vegetation.route_full_review.source_stream_sha256",
|
||||
);
|
||||
exact(
|
||||
sourceJobInputSha256,
|
||||
"eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715",
|
||||
"vegetation.route_full_review.source_job_input_sha256",
|
||||
);
|
||||
exact(
|
||||
sourceStreamSha256,
|
||||
"e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac",
|
||||
"vegetation.route_full_review.source_stream_sha256",
|
||||
);
|
||||
exact(
|
||||
row.recorded_media_source_id,
|
||||
"recorded.camera.6a3945242828a038",
|
||||
"vegetation.route_full_review.recorded_media_source_id",
|
||||
);
|
||||
const recordedMediaGenerationSha256 = textValue(
|
||||
row.recorded_media_generation_sha256,
|
||||
"vegetation.route_full_review.recorded_media_generation_sha256",
|
||||
);
|
||||
exact(
|
||||
recordedMediaGenerationSha256,
|
||||
"b073ea1e7babf1c77a664e1a5b95e3702d0e05b0e34c1e85a7c67a6f8b392ded",
|
||||
"vegetation.route_full_review.recorded_media_generation_sha256",
|
||||
);
|
||||
if (
|
||||
!SHA256.test(sourceJobInputSha256)
|
||||
|| !SHA256.test(sourceStreamSha256)
|
||||
|| !SHA256.test(recordedMediaGenerationSha256)
|
||||
) {
|
||||
throw new VegetationShadowContractError("vegetation.route_full_review: source digest invalid.");
|
||||
}
|
||||
const timelineStartSeconds = numberValue(
|
||||
row.timeline_start_seconds,
|
||||
"vegetation.route_full_review.timeline_start_seconds",
|
||||
);
|
||||
const timelineEndSeconds = numberValue(
|
||||
row.timeline_end_seconds,
|
||||
"vegetation.route_full_review.timeline_end_seconds",
|
||||
);
|
||||
if (timelineEndSeconds <= timelineStartSeconds) {
|
||||
throw new VegetationShadowContractError("vegetation.route_full_review: timeline invalid.");
|
||||
}
|
||||
const frameSourceTimesNs = arrayValue(
|
||||
row.frame_source_times_ns,
|
||||
"vegetation.route_full_review.frame_source_times_ns",
|
||||
).map((raw, index) => {
|
||||
const time = integerValue(raw, `vegetation.route_full_review.frame_source_times_ns[${index}]`);
|
||||
if (!Number.isSafeInteger(time)) {
|
||||
throw new VegetationShadowContractError("vegetation.route_full_review: unsafe frame time.");
|
||||
}
|
||||
return time;
|
||||
});
|
||||
if (
|
||||
frameSourceTimesNs.length !== 6830
|
||||
|| frameSourceTimesNs.some((time, index) => index > 0 && time <= frameSourceTimesNs[index - 1]!)
|
||||
) {
|
||||
throw new VegetationShadowContractError("vegetation.route_full_review: frame timeline changed.");
|
||||
}
|
||||
const decodeRepair = objectValue(
|
||||
row.decode_repair,
|
||||
"vegetation.route_full_review.decode_repair",
|
||||
);
|
||||
exact(decodeRepair.repaired_frame_count, 1, "vegetation.route_full_review.decode_repair.count");
|
||||
exact(decodeRepair.sequence, 6092, "vegetation.route_full_review.decode_repair.sequence");
|
||||
exact(
|
||||
decodeRepair.method,
|
||||
"duplicate-previous-decoded-frame",
|
||||
"vegetation.route_full_review.decode_repair.method",
|
||||
);
|
||||
const repairProofs = objectValue(
|
||||
decodeRepair.proofs,
|
||||
"vegetation.route_full_review.decode_repair.proofs",
|
||||
);
|
||||
for (const [key, expectedPath] of Object.entries({
|
||||
eomt: "proofs/decode_repair.json",
|
||||
ddrnet: "proofs/ddrnet_decode_repair.json",
|
||||
})) {
|
||||
const proof = objectValue(
|
||||
repairProofs[key],
|
||||
`vegetation.route_full_review.decode_repair.proofs.${key}`,
|
||||
);
|
||||
exact(
|
||||
proof.path,
|
||||
expectedPath,
|
||||
`vegetation.route_full_review.decode_repair.proofs.${key}.path`,
|
||||
);
|
||||
const digest = textValue(
|
||||
proof.sha256,
|
||||
`vegetation.route_full_review.decode_repair.proofs.${key}.sha256`,
|
||||
);
|
||||
if (!SHA256.test(digest)) {
|
||||
throw new VegetationShadowContractError("vegetation.route_full_review: repair proof invalid.");
|
||||
}
|
||||
}
|
||||
const layers = objectValue(row.layers, "vegetation.route_full_review.layers");
|
||||
return {
|
||||
sourceId: "RAVNOVES004TREE",
|
||||
sessionId: "20260828T130511Z_viewer_live",
|
||||
sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0",
|
||||
sourceJobInputSha256,
|
||||
sourceStreamSha256,
|
||||
recordedMediaSourceId: "recorded.camera.6a3945242828a038",
|
||||
recordedMediaGenerationSha256,
|
||||
frameCount: 6830,
|
||||
width: 800,
|
||||
height: 600,
|
||||
timelineStartSeconds,
|
||||
timelineEndSeconds,
|
||||
frameSourceTimesNs,
|
||||
decodeRepair: {
|
||||
repairedFrameCount: 1,
|
||||
sequence: 6092,
|
||||
method: "duplicate-previous-decoded-frame",
|
||||
},
|
||||
city: fullRouteLayerValue(layers.city, "city"),
|
||||
vegetation: fullRouteLayerValue(layers.vegetation, "vegetation"),
|
||||
};
|
||||
}
|
||||
|
||||
function parseResult(
|
||||
value: unknown,
|
||||
resultId: string,
|
||||
@@ -589,9 +863,10 @@ function parseResult(
|
||||
const validationCases = arrayValue(catalogs.goose, "vegetation.catalogs.goose")
|
||||
.map((item) => visualCaseValue(item, resultId, "goose", endpointRoot));
|
||||
const routeReview = mixedRouteReviewValue(payload.route_review, resultId, endpointRoot);
|
||||
const routeFullReview = fullRouteReviewValue(payload.route_full_review);
|
||||
if (
|
||||
routeCases.length !== 0
|
||||
|| (routeReview ? validationCases.length !== 0 : validationCases.length !== 12)
|
||||
|| (routeReview || routeFullReview ? validationCases.length !== 0 : validationCases.length !== 12)
|
||||
) {
|
||||
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer.");
|
||||
}
|
||||
@@ -605,6 +880,7 @@ function parseResult(
|
||||
validationCases,
|
||||
routeVideo: routeVideoValue(payload.route_video),
|
||||
routeReview,
|
||||
routeFullReview,
|
||||
limitations: arrayValue(payload.limitations, "vegetation.limitations")
|
||||
.map((item, index) => textValue(item, `vegetation.limitations[${index}]`)),
|
||||
visualShadowReady: true,
|
||||
@@ -625,6 +901,23 @@ export function vegetationVideoMaskUrl(resultId: string, sequence: number): stri
|
||||
return `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/masks/${sequence}`;
|
||||
}
|
||||
|
||||
export function vegetationFullRouteMaskUrl(
|
||||
resultId: string,
|
||||
layer: "city" | "vegetation",
|
||||
sequence: number,
|
||||
): string {
|
||||
if (
|
||||
!RESULT_ID.test(resultId)
|
||||
|| (layer !== "city" && layer !== "vegetation")
|
||||
|| !Number.isInteger(sequence)
|
||||
|| sequence < 0
|
||||
|| sequence >= 6830
|
||||
) {
|
||||
throw new VegetationShadowContractError("Vegetation full-route mask identity недопустима.");
|
||||
}
|
||||
return `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/route-masks/${layer}/${sequence}`;
|
||||
}
|
||||
|
||||
export async function fetchVegetationShadowResult(
|
||||
resultId: string,
|
||||
{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||
import { LaboratoryRecordedClipPlayer } from "../../components/laboratory/LaboratoryRecordedClipPlayer";
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
@@ -9,10 +10,21 @@ import {
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import {
|
||||
RecordedEvidenceSemanticMaskOverlay,
|
||||
type RecordedEvidenceSemanticClass,
|
||||
type RecordedEvidenceSemanticPaletteEntry,
|
||||
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
||||
import {
|
||||
vegetationFullRouteMaskUrl,
|
||||
vegetationVideoMaskUrl,
|
||||
type VegetationFullRouteLayer,
|
||||
type VegetationFullRouteReview,
|
||||
type VegetationMixedRouteReview,
|
||||
type VegetationShadowResult,
|
||||
} from "../../core/laboratory/vegetationShadow";
|
||||
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
|
||||
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
|
||||
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
|
||||
import {
|
||||
fetchM49TgsFullShadowResult,
|
||||
type M49TgsFullShadowResult,
|
||||
@@ -31,6 +43,217 @@ const MIXED_ROUTE_MODES = [
|
||||
{ value: "tgs", label: "TGS" },
|
||||
] as const;
|
||||
|
||||
const FULL_ROUTE_MODES = [
|
||||
{ value: "source", label: "SOURCE" },
|
||||
{ value: "city", label: "ГОРОД · EoMT" },
|
||||
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
|
||||
] as const;
|
||||
|
||||
function semanticPresentation(layer: VegetationFullRouteLayer): {
|
||||
classes: readonly RecordedEvidenceSemanticClass[];
|
||||
palette: readonly RecordedEvidenceSemanticPaletteEntry[];
|
||||
} {
|
||||
return {
|
||||
classes: layer.taxonomy.map((item) => ({ id: item.classId, label: item.label })),
|
||||
palette: layer.taxonomy.map((item) => ({
|
||||
classId: item.classId,
|
||||
color: item.classId === 0
|
||||
? { kind: "transparent" as const }
|
||||
: { kind: "diagnostic" as const, rgb: item.colorRgb },
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function FullRouteReviewEvidence({
|
||||
resultId,
|
||||
review,
|
||||
}: {
|
||||
resultId: string;
|
||||
review: VegetationFullRouteReview;
|
||||
}) {
|
||||
const [sequence, setSequence] = useState(1);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [playbackRate, setPlaybackRate] = useState(1);
|
||||
const [mode, setMode] = useState<typeof FULL_ROUTE_MODES[number]["value"]>("vegetation");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
|
||||
const [videoError, setVideoError] = useState<string | null>(null);
|
||||
const frames = useMemo(
|
||||
() => review.frameSourceTimesNs.map((sourceTimeNs, index) => ({
|
||||
sequence: index + 1,
|
||||
sourceTimeNs,
|
||||
})),
|
||||
[review.frameSourceTimesNs],
|
||||
);
|
||||
const layer = mode === "source" ? null : review[mode];
|
||||
const semantic = useMemo(() => layer ? semanticPresentation(layer) : null, [layer]);
|
||||
const maskSequence = sequence - 1;
|
||||
const prefetchSrcs = useMemo(() => layer
|
||||
? Array.from({ length: 8 }, (_, offset) => maskSequence + offset + 1)
|
||||
.filter((candidate) => candidate < review.frameCount)
|
||||
.map((candidate) => vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", candidate))
|
||||
: [], [layer, maskSequence, mode, resultId, review.frameCount]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setVideoSource(null);
|
||||
setVideoError(null);
|
||||
void resolveObservationSessionReplay(review.sessionId, { signal: controller.signal })
|
||||
.then((launch) => {
|
||||
const source = recordedObservationSources(launch).find((candidate) => (
|
||||
candidate.id === review.recordedMediaSourceId
|
||||
&& candidate.modality === "video"
|
||||
&& candidate.semanticChannelId === "camera.video.recorded"
|
||||
&& candidate.delivery?.kind === "recorded-fmp4-manifest"
|
||||
&& candidate.delivery.manifestGenerationSha256 === review.recordedMediaGenerationSha256
|
||||
&& candidate.delivery.timelineStartSeconds === review.timelineStartSeconds
|
||||
&& candidate.delivery.timelineEndSeconds >= review.timelineEndSeconds
|
||||
));
|
||||
if (!source) {
|
||||
throw new Error("RIGHT-видео не совпало с sealed RAVNOVES004TREE timeline.");
|
||||
}
|
||||
if (!controller.signal.aborted) setVideoSource(source);
|
||||
})
|
||||
.catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setVideoError(caught instanceof Error ? caught.message : "Записанное видео недоступно.");
|
||||
}
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [
|
||||
review.recordedMediaGenerationSha256,
|
||||
review.recordedMediaSourceId,
|
||||
review.sessionId,
|
||||
review.timelineEndSeconds,
|
||||
review.timelineStartSeconds,
|
||||
]);
|
||||
|
||||
return (
|
||||
<LaboratoryEvidenceViewer
|
||||
label="RAVNOVES004TREE full recorded review"
|
||||
className="m48-atlas-visual"
|
||||
mode={mode}
|
||||
modes={FULL_ROUTE_MODES}
|
||||
expanded={expanded}
|
||||
onModeChange={setMode}
|
||||
onExpandedChange={setExpanded}
|
||||
chromeLayout="stacked"
|
||||
>
|
||||
{videoSource ? (
|
||||
<LaboratoryRecordedClipPlayer
|
||||
source={videoSource}
|
||||
segmentCount={review.frameCount}
|
||||
frames={frames}
|
||||
sequence={sequence}
|
||||
playing={playing}
|
||||
playbackRate={playbackRate}
|
||||
cameraPresentation="primary"
|
||||
continuousPlayback
|
||||
sourceCount={1}
|
||||
onSequenceChange={setSequence}
|
||||
onPlayingChange={setPlaying}
|
||||
onPlaybackRateChange={setPlaybackRate}
|
||||
cameraOverlay={(
|
||||
<>
|
||||
<div className="m48-clip-player__pane-label" data-pane="camera">
|
||||
{mode === "source" ? "SOURCE" : `${mode === "city" ? "EoMT CITY" : "DDRNet NATURE"} · КАДР ${sequence}/${review.frameCount}`}
|
||||
</div>
|
||||
{layer && semantic ? (
|
||||
<RecordedEvidenceSemanticMaskOverlay
|
||||
src={vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", maskSequence)}
|
||||
prefetchSrcs={prefetchSrcs}
|
||||
imageWidth={review.width}
|
||||
imageHeight={review.height}
|
||||
classes={semantic.classes}
|
||||
palette={semantic.palette}
|
||||
opacity={0.76}
|
||||
ariaLabel={`${layer.name} semantic prediction`}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="m4-replay-threat-visual__pane-status" role={videoError ? "alert" : "status"}>
|
||||
{videoError ?? "Открываем автономный recorded source…"}
|
||||
</div>
|
||||
)}
|
||||
</LaboratoryEvidenceViewer>
|
||||
);
|
||||
}
|
||||
|
||||
function FullRouteReviewResult({
|
||||
rigLabel,
|
||||
resultId,
|
||||
review,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
resultId: string;
|
||||
review: VegetationFullRouteReview;
|
||||
}) {
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB V1 · RAVNOVES004TREE · полный маршрут"
|
||||
description="Существующий M4.7-шаблон воспроизводит всю запись и переключает два независимых sealed semantic-слоя: городской EoMT и природный DDRNet. Worker для открытия результата не нужен."
|
||||
status="FULL RECORDED REVIEW · truth отсутствует · commands OFF"
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount}/${review.frameCount} frames` },
|
||||
{ label: "Город", value: `${review.city.name} · ${decimal(review.city.inferenceFps, 2)} fps` },
|
||||
{ label: "Природа", value: `${review.vegetation.name} · ${decimal(review.vegetation.inferenceFps, 2)} fps` },
|
||||
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
|
||||
]}
|
||||
brief={{
|
||||
question: "Как оба semantic-кандидата ведут себя на полном переходе от сельской среды к городской?",
|
||||
approach: "Все 6830 позиции одной recorded timeline последовательно прогнаны на Worker 006 и сохранены двумя независимыми архивами масок. В M4.7 переключается только видимый слой.",
|
||||
principalResult: "Полная временная шкала доступна локально в SOURCE / EoMT CITY / DDRNet NATURE без обращения к Worker.",
|
||||
limitation: "Ручной truth отсутствует. Один повреждённый H.264-пакет на позиции 6092 представлен предыдущим декодированным кадром и явно зафиксирован в proof. Полный TGS и кюветы этим прогоном не проверялись.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "ai-inference",
|
||||
pipelineId: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
components: [
|
||||
{ kind: "model", name: review.city.name, version: "sealed Worker 006 run", role: "urban semantic review", identitySha256: null },
|
||||
{ kind: "model", name: review.vegetation.name, version: "GOOSE DDRNet-39", role: "vegetation semantic review", identitySha256: null },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="M4.7 TEMPLATE · RAVNOVES004TREE FULL VIDEO"
|
||||
title="SOURCE / EoMT CITY / DDRNet NATURE · 6830/6830 · TRUTH отсутствует"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<FullRouteReviewEvidence resultId={resultId} review={review} />
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title="Полный двухслойный visual review собран; управление не авторизовано"
|
||||
status="Recorded evidence ready · navigation/actuation OFF"
|
||||
statusTone="warning"
|
||||
metrics={[
|
||||
{ label: "Route masks", value: "6830/6830 × 2", hint: "sealed local archives · Worker не требуется" },
|
||||
{ label: "EoMT p95", value: `${decimal(review.city.latencyP95Ms, 2)} ms`, hint: "последовательный изолированный прогон" },
|
||||
{ label: "DDRNet p95", value: `${decimal(review.vegetation.latencyP95Ms, 2)} ms`, hint: "последовательный изолированный прогон" },
|
||||
{ label: "Decode repair", value: "1/6830", hint: "sequence 6092 · previous frame · sealed proof" },
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Городской EoMT и природный DDRNet воспроизводимо обработали полную запись и доступны в одном существующем M4.7 viewer.",
|
||||
notProved: "Не доказаны truth accuracy, одновременный realtime-load, полный TGS, отрицательные препятствия и безопасное управление ровером.",
|
||||
decision: "Использовать результат только как визуальную диагностику. Navigation/actuation оставить OFF; следующий gate — оценка временной стабильности и независимый person/vehicle STOP.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MixedRouteReviewEvidence({ review }: { review: VegetationMixedRouteReview }) {
|
||||
const [index, setIndex] = useState(0);
|
||||
const [mode, setMode] = useState<typeof MIXED_ROUTE_MODES[number]["value"]>("vegetation");
|
||||
@@ -224,6 +447,15 @@ export function VegetationShadowResultView({
|
||||
rigLabel: string;
|
||||
result: VegetationShadowResult;
|
||||
}) {
|
||||
if (result.routeFullReview) {
|
||||
return (
|
||||
<FullRouteReviewResult
|
||||
rigLabel={rigLabel}
|
||||
resultId={result.resultId}
|
||||
review={result.routeFullReview}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (result.routeReview) {
|
||||
return <MixedRouteReviewResult rigLabel={rigLabel} review={result.routeReview} />;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createServer } from "vite";
|
||||
let server;
|
||||
let fetchVegetationBenchmarkResult;
|
||||
let fetchVegetationShadowResult;
|
||||
let vegetationFullRouteMaskUrl;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
@@ -14,7 +15,11 @@ before(async () => {
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({ fetchVegetationBenchmarkResult, fetchVegetationShadowResult } = await server.ssrLoadModule(
|
||||
({
|
||||
fetchVegetationBenchmarkResult,
|
||||
fetchVegetationShadowResult,
|
||||
vegetationFullRouteMaskUrl,
|
||||
} = await server.ssrLoadModule(
|
||||
"/src/core/laboratory/vegetationShadow.ts",
|
||||
));
|
||||
});
|
||||
@@ -148,6 +153,63 @@ function coarseRouteVideo() {
|
||||
};
|
||||
}
|
||||
|
||||
function fullRouteReview() {
|
||||
const layer = (kind) => ({
|
||||
name: kind === "city" ? "EoMT Cityscapes" : "ddrnet_39",
|
||||
result_id: kind === "city"
|
||||
? `result-${"2".repeat(64)}`
|
||||
: `lab-v1-ravnoves-video-ddrnet-${"3".repeat(64)}`,
|
||||
frame_count: 6830,
|
||||
taxonomy: {
|
||||
schema_version: kind === "city"
|
||||
? "missioncore.recorded-eomt-taxonomy/v1"
|
||||
: "missioncore.lab-v1-vegetation-taxonomy/v1",
|
||||
classes: Array.from({ length: kind === "city" ? 16 : 64 }, (_, classId) => ({
|
||||
class_id: classId,
|
||||
label: classId === 0 ? "undefined" : `${kind}-${classId}`,
|
||||
color_rgb: [classId, classId, classId],
|
||||
disposition: classId === 0 ? "undefined" : "prediction",
|
||||
})),
|
||||
},
|
||||
mask_archive: {
|
||||
path: kind === "city"
|
||||
? "video/eomt-semantic-masks.zip"
|
||||
: "video/ddrnet-semantic-masks.zip",
|
||||
sha256: "4".repeat(64),
|
||||
byte_length: 4096,
|
||||
},
|
||||
inference_fps: 9.5,
|
||||
latency_p95_ms: 101.2,
|
||||
peak_reserved_vram_bytes: 3_000_000_000,
|
||||
});
|
||||
return {
|
||||
source_id: "RAVNOVES004TREE",
|
||||
session_id: "20260828T130511Z_viewer_live",
|
||||
source_job_id: "recorded-camera-eb2783c5480d56bda07c8af0",
|
||||
source_job_input_sha256: "eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715",
|
||||
source_stream_sha256: "e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac",
|
||||
recorded_media_source_id: "recorded.camera.6a3945242828a038",
|
||||
recorded_media_generation_sha256: "b073ea1e7babf1c77a664e1a5b95e3702d0e05b0e34c1e85a7c67a6f8b392ded",
|
||||
frame_count: 6830,
|
||||
width: 800,
|
||||
height: 600,
|
||||
timeline_start_seconds: 39.215263458,
|
||||
timeline_end_seconds: 757.260263458,
|
||||
frame_source_times_ns: Array.from({ length: 6830 }, (_, index) => 39_215_263_458 + index * 100_000_000),
|
||||
ground_truth: false,
|
||||
decode_repair: {
|
||||
repaired_frame_count: 1,
|
||||
sequence: 6092,
|
||||
method: "duplicate-previous-decoded-frame",
|
||||
proofs: {
|
||||
eomt: { path: "proofs/decode_repair.json", sha256: "7".repeat(64) },
|
||||
ddrnet: { path: "proofs/ddrnet_decode_repair.json", sha256: "8".repeat(64) },
|
||||
},
|
||||
},
|
||||
layers: { city: layer("city"), vegetation: layer("vegetation") },
|
||||
};
|
||||
}
|
||||
|
||||
function labPayload(route = routeVideo()) {
|
||||
return {
|
||||
schema_version: "missioncore.lab-v1-vegetation-shadow/v1",
|
||||
@@ -233,6 +295,29 @@ test("vegetation LAB parses coarse material policy and sealed TGS binding", asyn
|
||||
assert.equal(result.routeVideo.fusionMode, "synchronised-multilayer-review");
|
||||
});
|
||||
|
||||
test("vegetation LAB parses the full 004 pass inside the existing result contract", async () => {
|
||||
const payload = {
|
||||
...labPayload(null),
|
||||
catalogs: { goose: [], ravnoves: [] },
|
||||
route_full_review: fullRouteReview(),
|
||||
};
|
||||
const result = await fetchVegetationShadowResult(resultId, {
|
||||
fetcher: async () => new Response(JSON.stringify(payload), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
});
|
||||
assert.equal(result.routeVideo, null);
|
||||
assert.equal(result.routeFullReview.frameCount, 6830);
|
||||
assert.equal(result.routeFullReview.city.taxonomy.length, 16);
|
||||
assert.equal(result.routeFullReview.vegetation.taxonomy.length, 64);
|
||||
assert.equal(result.routeFullReview.decodeRepair.sequence, 6092);
|
||||
assert.equal(
|
||||
vegetationFullRouteMaskUrl(resultId, "vegetation", 6829),
|
||||
`/api/v1/laboratory/vegetation-shadow/${resultId}/route-masks/vegetation/6829`,
|
||||
);
|
||||
});
|
||||
|
||||
test("vegetation GOOSE benchmark opens through its separate archival endpoint", async () => {
|
||||
let requestedUrl = "";
|
||||
const result = await fetchVegetationBenchmarkResult(benchmarkResultId, {
|
||||
@@ -271,8 +356,10 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr
|
||||
assert.match(resultSource, /M49TgsFullShadowEvidence/);
|
||||
assert.match(resultSource, /semanticOverride/);
|
||||
assert.match(resultSource, /EoMT CITY \/ DDRNet VEGETATION/);
|
||||
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 3);
|
||||
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 4);
|
||||
assert.match(resultSource, /RAVNOVES004TREE mixed route review/);
|
||||
assert.match(resultSource, /RAVNOVES004TREE full recorded review/);
|
||||
assert.match(resultSource, /LaboratoryRecordedClipPlayer/);
|
||||
assert.match(resultSource, /linkedTgsResultId/);
|
||||
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
|
||||
assert.doesNotMatch(benchmarkSource, /M49TgsFullShadowEvidence/);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"schema_version": "missioncore.lab-v1-ravnoves-source/v1",
|
||||
"profile_id": "ravnoves004tree-full-video-source/v1",
|
||||
"source": {
|
||||
"source_id": "RAVNOVES004TREE/right-e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac",
|
||||
"source_sha256": "e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac",
|
||||
"source_job_id": "recorded-camera-eb2783c5480d56bda07c8af0",
|
||||
"source_job_input_sha256": "eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715",
|
||||
"session_id": "20260828T130511Z_viewer_live",
|
||||
"base_m4_result_id": null,
|
||||
"expected_width": 800,
|
||||
"expected_height": 600,
|
||||
"expected_frame_count": 6830,
|
||||
"timeline_start_seconds": 39.215263458,
|
||||
"timeline_end_seconds": 757.260263458,
|
||||
"frame_indices": [],
|
||||
"crop_contract": "center-600-square-to-512; outside-crop-is-undefined"
|
||||
}
|
||||
}
|
||||
@@ -187,12 +187,15 @@ Write-Output "PHASE=e4-preflight-complete"
|
||||
$runToken = [Guid]::NewGuid().ToString("N")
|
||||
$workRoot = Join-Path $tmpRoot ("{0}-e4-{1}" -f $job.job_id, $runToken)
|
||||
$framesRoot = Join-Path $workRoot "frames"
|
||||
$decodedFramesRoot = Join-Path $workRoot "decoded-by-pts"
|
||||
$streamPath = Join-Path $workRoot "camera.mp4"
|
||||
$ptsPath = Join-Path $workRoot "pts.json"
|
||||
$packetsPath = Join-Path $workRoot "packets.csv"
|
||||
$decodeRepairPath = Join-Path $workRoot "decode-repair.json"
|
||||
$timelinePath = Join-Path $workRoot "timeline.jsonl"
|
||||
$publishRoot = Join-Path $derivedRoot (".{0}-e4-{1}.publish" -f $job.job_id, $runToken)
|
||||
$stagingRoot = Join-Path $publishRoot "output"
|
||||
$null = New-Item -ItemType Directory -Path $framesRoot
|
||||
$null = New-Item -ItemType Directory -Path $decodedFramesRoot
|
||||
$null = New-Item -ItemType Directory -Path $publishRoot
|
||||
$totalWatch = [Diagnostics.Stopwatch]::StartNew()
|
||||
$completed = $false
|
||||
@@ -230,29 +233,69 @@ try {
|
||||
|
||||
$extractWatch = [Diagnostics.Stopwatch]::StartNew()
|
||||
Write-Output "PHASE=e4-frame-extraction-start"
|
||||
& ffmpeg -hide_banner -loglevel fatal -i $streamPath -map 0:v:0 -fps_mode passthrough -frames:v $activeFrameCount (Join-Path $framesRoot "frame-%06d.png")
|
||||
& ffprobe -v error -select_streams v:0 -show_packets -show_entries packet=pts,flags -of csv=p=0 -o $packetsPath $streamPath
|
||||
Assert-LastExitCode "LAB E4 packet timestamp probe"
|
||||
$packetRows = @(Get-Content -LiteralPath $packetsPath | Select-Object -First $activeFrameCount)
|
||||
if ($packetRows.Count -ne $activeFrameCount) {
|
||||
throw "LAB E4 packet count differs from the requested camera epoch"
|
||||
}
|
||||
|
||||
& ffmpeg -hide_banner -loglevel error `
|
||||
-hwaccel cuda -hwaccel_output_format cuda -c:v h264_cuvid `
|
||||
-err_detect ignore_err -flags +output_corrupt -copyts `
|
||||
-i $streamPath -map 0:v:0 -vf "hwdownload,format=nv12" `
|
||||
-fps_mode passthrough -enc_time_base demux -frames:v $activeFrameCount `
|
||||
-frame_pts 1 (Join-Path $decodedFramesRoot "frame-%d.png")
|
||||
Assert-LastExitCode "LAB E4 camera extraction"
|
||||
& ffprobe -v error -select_streams v:0 -show_entries frame=best_effort_timestamp_time -of json $streamPath | Set-Content -LiteralPath $ptsPath -Encoding utf8
|
||||
Assert-LastExitCode "LAB E4 camera timestamp probe"
|
||||
|
||||
$decodedCount = @(Get-ChildItem -LiteralPath $decodedFramesRoot -File -Filter "frame-*.png").Count
|
||||
$repairs = @()
|
||||
$packetPts = @()
|
||||
for ($index = 0; $index -lt $activeFrameCount; $index++) {
|
||||
$columns = ([string]$packetRows[$index]).Split(",")
|
||||
if ($columns.Count -lt 2) {
|
||||
throw "LAB E4 packet timestamp row is malformed"
|
||||
}
|
||||
$pts = [int64]::Parse($columns[0].Trim(), [Globalization.CultureInfo]::InvariantCulture)
|
||||
$packetPts += $pts
|
||||
$decodedPath = Join-Path $decodedFramesRoot ("frame-{0}.png" -f $pts)
|
||||
$canonicalPath = Join-Path $framesRoot ("frame-{0:D6}.png" -f ($index + 1))
|
||||
if (Test-Path -LiteralPath $decodedPath -PathType Leaf) {
|
||||
Move-Item -LiteralPath $decodedPath -Destination $canonicalPath
|
||||
continue
|
||||
}
|
||||
if ($index -eq 0 -or $repairs.Count -ge 1) {
|
||||
throw "LAB E4 source contains more than one recoverable decoder gap"
|
||||
}
|
||||
$previousPath = Join-Path $framesRoot ("frame-{0:D6}.png" -f $index)
|
||||
Copy-Item -LiteralPath $previousPath -Destination $canonicalPath
|
||||
$repairs += [ordered]@{
|
||||
sequence = $index + 1
|
||||
packet_pts = $pts
|
||||
method = "duplicate-previous-decoded-frame"
|
||||
}
|
||||
}
|
||||
|
||||
$decodedFrames = @(Get-ChildItem -LiteralPath $framesRoot -File -Filter "frame-*.png")
|
||||
$ptsDocument = Get-Content -LiteralPath $ptsPath -Raw | ConvertFrom-Json
|
||||
$pts = @($ptsDocument.frames)
|
||||
if ($decodedFrames.Count -ne $activeFrameCount -or $pts.Count -lt $activeFrameCount) {
|
||||
if ($decodedFrames.Count -ne $activeFrameCount) {
|
||||
throw "Decoded LAB E4 frame count differs from the requested camera epoch"
|
||||
}
|
||||
$firstEpochSeconds = [double]::Parse(
|
||||
([string]$pts[0].best_effort_timestamp_time).Trim(),
|
||||
[Globalization.CultureInfo]::InvariantCulture
|
||||
)
|
||||
$decodeRepair = [ordered]@{
|
||||
schema_version = "missioncore.recorded-video-decode-repair/v1"
|
||||
decoder = "ffmpeg-h264_cuvid-output-corrupt"
|
||||
packets_requested = $activeFrameCount
|
||||
frames_decoded = $decodedCount
|
||||
repaired_frame_count = $repairs.Count
|
||||
repairs = $repairs
|
||||
}
|
||||
$decodeRepair | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $decodeRepairPath -Encoding utf8
|
||||
|
||||
$firstPacketPts = [int64]$packetPts[0]
|
||||
$previousEpochSeconds = -1.0
|
||||
$timelineWriter = [IO.StreamWriter]::new($timelinePath, $false, [Text.UTF8Encoding]::new($false))
|
||||
try {
|
||||
for ($index = 0; $index -lt $activeFrameCount; $index++) {
|
||||
$epochSeconds = [double]::Parse(
|
||||
([string]$pts[$index].best_effort_timestamp_time).Trim(),
|
||||
[Globalization.CultureInfo]::InvariantCulture
|
||||
) - $firstEpochSeconds
|
||||
$epochSeconds = ([int64]$packetPts[$index] - $firstPacketPts) / 90000.0
|
||||
if ($epochSeconds -le $previousEpochSeconds -or $epochSeconds -gt ($timelineDuration + 0.001)) {
|
||||
throw "Decoded LAB E4 timestamps are not strictly monotonic inside the camera timeline"
|
||||
}
|
||||
@@ -313,6 +356,7 @@ try {
|
||||
Write-Output ("PHASE=e4-inference-start FRAMES={0}" -f $activeFrameCount)
|
||||
& docker @runArgs
|
||||
Assert-LastExitCode "LAB E4 semantic inference"
|
||||
Copy-Item -LiteralPath $decodeRepairPath -Destination (Join-Path $stagingRoot "decode-repair.json")
|
||||
$freeBytesPostInference = Assert-FreeSpace "post-inference"
|
||||
Write-Output "PHASE=e4-inference-complete"
|
||||
|
||||
|
||||
@@ -12,7 +12,18 @@ param(
|
||||
|
||||
[string]$OutputRoot = "D:\NDC_MISSIONCORE\runtime\experiments\lab-v1-vegetation",
|
||||
|
||||
[string]$RavnovesVideo = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8.mp4"
|
||||
[string]$RavnovesVideo = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8.mp4",
|
||||
|
||||
[string]$RavnovesSourceId = "RAVNOVES00/right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
||||
|
||||
[string]$RavnovesSha256 = "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
||||
|
||||
[ValidateRange(1, 1000000)]
|
||||
[int]$RavnovesExpectedFrameCount = 4489,
|
||||
|
||||
[string]$RavnovesBaseM4ResultId = "m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324",
|
||||
|
||||
[string]$RavnovesSourceProfile = ""
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -38,6 +49,31 @@ $configRoot = Join-Path $ToolRoot "config"
|
||||
$benchmarkConfig = Join-Path $configRoot "lab-v1-goose-vegetation-benchmark-v1.json"
|
||||
$policyConfig = Join-Path $configRoot "lab-v1-vegetation-mission-policy-v1.json"
|
||||
$providerMapConfig = Join-Path $configRoot "lab-v1-vegetation-provider-label-map-v1.json"
|
||||
$ravnovesProfileDocument = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($RavnovesSourceProfile)) {
|
||||
$resolvedProfile = (Resolve-Path -LiteralPath $RavnovesSourceProfile).Path
|
||||
if (-not $resolvedProfile.StartsWith($ToolRoot, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "RAVNOVES source profile must stay under ToolRoot"
|
||||
}
|
||||
$ravnovesProfileDocument = Get-Content -LiteralPath $resolvedProfile -Raw | ConvertFrom-Json
|
||||
$source = $ravnovesProfileDocument.source
|
||||
if (
|
||||
$ravnovesProfileDocument.schema_version -ne "missioncore.lab-v1-ravnoves-source/v1" -or
|
||||
$null -eq $source -or
|
||||
[string]::IsNullOrWhiteSpace([string]$source.source_id) -or
|
||||
[string]$source.source_sha256 -notmatch "^[a-f0-9]{64}$" -or
|
||||
[int]$source.expected_width -ne 800 -or
|
||||
[int]$source.expected_height -ne 600 -or
|
||||
[int]$source.expected_frame_count -lt 1 -or
|
||||
[string]$source.crop_contract -ne "center-600-square-to-512; outside-crop-is-undefined"
|
||||
) {
|
||||
throw "RAVNOVES source profile is incompatible"
|
||||
}
|
||||
$RavnovesSourceId = [string]$source.source_id
|
||||
$RavnovesSha256 = [string]$source.source_sha256
|
||||
$RavnovesExpectedFrameCount = [int]$source.expected_frame_count
|
||||
$RavnovesBaseM4ResultId = [string]$source.base_m4_result_id
|
||||
}
|
||||
$datasetRoot = Join-Path $AssetRoot "goose-2d\validation"
|
||||
$checkpointRelative = if ($candidateKey -eq "ddrnet") {
|
||||
"models\goose\ddrnet_class_512.pth"
|
||||
@@ -51,7 +87,6 @@ $expectedCheckpointSha256 = if ($candidateKey -eq "ddrnet") {
|
||||
"6dd412c0c99115e359896c4cab43a8e6bce9e09b843e7fa885fe597b0a6121cd"
|
||||
}
|
||||
$expectedCheckpointBytes = if ($candidateKey -eq "ddrnet") { 259419077 } else { 98208249 }
|
||||
$ravnovesSha256 = "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8"
|
||||
$frameIndices = @(0, 253, 512, 768, 1024, 1536, 2048, 2560, 3072, 3584, 4096, 4488)
|
||||
$dockerConfig = "D:\NDC_MISSIONCORE\datasets\state\lab-v1-vegetation\docker-config"
|
||||
|
||||
@@ -110,6 +145,18 @@ function Invoke-IsolatedRun {
|
||||
[string]$FramesRoot = ""
|
||||
)
|
||||
$containerName = "ndc-lab-v1-goose-$candidateKey-$([Guid]::NewGuid().ToString('N').Substring(0, 10))"
|
||||
$activeConfigRoot = $configRoot
|
||||
if ($RunMode -eq "ravnoves-video" -and $null -ne $ravnovesProfileDocument) {
|
||||
$activeConfigRoot = Join-Path $RunRoot "effective-config"
|
||||
New-Item -ItemType Directory -Path $activeConfigRoot | Out-Null
|
||||
Copy-Item -LiteralPath $policyConfig -Destination $activeConfigRoot
|
||||
Copy-Item -LiteralPath $providerMapConfig -Destination $activeConfigRoot
|
||||
$benchmark = Get-Content -LiteralPath $benchmarkConfig -Raw | ConvertFrom-Json
|
||||
$benchmark.ravnoves = $ravnovesProfileDocument.source
|
||||
$benchmark | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath (
|
||||
Join-Path $activeConfigRoot "lab-v1-goose-vegetation-benchmark-v1.json"
|
||||
) -Encoding utf8
|
||||
}
|
||||
$visualCount = if ($RunMode -eq "ravnoves-video") { 0 } else { 12 }
|
||||
$arguments = @(
|
||||
"run", "--rm", "--name", $containerName,
|
||||
@@ -125,7 +172,7 @@ function Invoke-IsolatedRun {
|
||||
"--env", "HOME=/tmp",
|
||||
"--mount", "type=bind,src=$datasetRoot,dst=/data/goose,readonly",
|
||||
"--mount", "type=bind,src=$checkpoint,dst=/models/candidate.pth,readonly",
|
||||
"--mount", "type=bind,src=$configRoot,dst=/config,readonly",
|
||||
"--mount", "type=bind,src=$activeConfigRoot,dst=/config,readonly",
|
||||
"--mount", "type=bind,src=$RunRoot,dst=/output",
|
||||
$image,
|
||||
"--mode", $RunMode,
|
||||
@@ -147,15 +194,27 @@ function Invoke-IsolatedRun {
|
||||
$tail = @($arguments[$mountIndex..($arguments.Count - 1)])
|
||||
$arguments = $head + @("--mount", "type=bind,src=$FramesRoot,dst=/input,readonly") + $tail
|
||||
}
|
||||
& docker @arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "LAB V1 container failed with exit code $LASTEXITCODE"
|
||||
$dockerExitCode = -1
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
try {
|
||||
# Windows PowerShell exposes native stderr as ErrorRecord objects. Model
|
||||
# libraries legitimately emit warnings there, so merge the stream and
|
||||
# fail only on the native process exit code.
|
||||
$ErrorActionPreference = "Continue"
|
||||
& docker @arguments 2>&1 | ForEach-Object { Write-Output $_ }
|
||||
$dockerExitCode = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($dockerExitCode -ne 0) {
|
||||
throw "LAB V1 container failed with exit code $dockerExitCode"
|
||||
}
|
||||
}
|
||||
|
||||
function Export-RavnovesFrames {
|
||||
param([string]$Destination)
|
||||
Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $ravnovesSha256
|
||||
Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $RavnovesSha256
|
||||
New-Item -ItemType Directory -Path $Destination | Out-Null
|
||||
$expression = ($frameIndices | ForEach-Object { "eq(n\,$_ )" }) -join "+"
|
||||
$temporaryPattern = Join-Path $Destination "selected-%03d.png"
|
||||
@@ -175,14 +234,68 @@ function Export-RavnovesFrames {
|
||||
|
||||
function Export-RavnovesVideoFrames {
|
||||
param([string]$Destination)
|
||||
Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $ravnovesSha256
|
||||
Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $RavnovesSha256
|
||||
New-Item -ItemType Directory -Path $Destination | Out-Null
|
||||
& ffmpeg -hide_banner -loglevel error -i $RavnovesVideo -map 0:v:0 -fps_mode passthrough (Join-Path $Destination "frame-%06d.png")
|
||||
$decodedRoot = "{0}-decoded-by-pts" -f $Destination
|
||||
$packetsPath = "{0}-packets.csv" -f $Destination
|
||||
New-Item -ItemType Directory -Path $decodedRoot | Out-Null
|
||||
& ffprobe -v error -select_streams v:0 -show_packets -show_entries packet=pts,flags -of csv=p=0 -o $packetsPath $RavnovesVideo
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "RAVNOVES full-video packet probe failed"
|
||||
}
|
||||
$packetRows = @(Get-Content -LiteralPath $packetsPath | Select-Object -First $RavnovesExpectedFrameCount)
|
||||
if ($packetRows.Count -ne $RavnovesExpectedFrameCount) {
|
||||
throw "RAVNOVES full-video packet sequence changed"
|
||||
}
|
||||
& ffmpeg -hide_banner -loglevel error `
|
||||
-hwaccel cuda -hwaccel_output_format cuda -c:v h264_cuvid `
|
||||
-err_detect ignore_err -flags +output_corrupt -copyts `
|
||||
-i $RavnovesVideo -map 0:v:0 -vf "hwdownload,format=nv12" `
|
||||
-fps_mode passthrough -enc_time_base demux -frames:v $RavnovesExpectedFrameCount `
|
||||
-frame_pts 1 (Join-Path $decodedRoot "frame-%d.png")
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "RAVNOVES full-video frame extraction failed"
|
||||
}
|
||||
$decodedCount = @(Get-ChildItem -LiteralPath $decodedRoot -File -Filter "frame-*.png").Count
|
||||
$repairs = @()
|
||||
for ($index = 0; $index -lt $RavnovesExpectedFrameCount; $index++) {
|
||||
$columns = ([string]$packetRows[$index]).Split(",")
|
||||
if ($columns.Count -lt 2) {
|
||||
throw "RAVNOVES full-video packet row is malformed"
|
||||
}
|
||||
$pts = [int64]::Parse($columns[0].Trim(), [Globalization.CultureInfo]::InvariantCulture)
|
||||
$decodedPath = Join-Path $decodedRoot ("frame-{0}.png" -f $pts)
|
||||
$canonicalPath = Join-Path $Destination ("frame-{0:D6}.png" -f ($index + 1))
|
||||
if (Test-Path -LiteralPath $decodedPath -PathType Leaf) {
|
||||
Move-Item -LiteralPath $decodedPath -Destination $canonicalPath
|
||||
continue
|
||||
}
|
||||
if ($index -eq 0 -or $repairs.Count -ge 1) {
|
||||
throw "RAVNOVES source contains more than one recoverable decoder gap"
|
||||
}
|
||||
$previousPath = Join-Path $Destination ("frame-{0:D6}.png" -f $index)
|
||||
Copy-Item -LiteralPath $previousPath -Destination $canonicalPath
|
||||
$repairs += [ordered]@{
|
||||
sequence = $index + 1
|
||||
packet_pts = $pts
|
||||
method = "duplicate-previous-decoded-frame"
|
||||
}
|
||||
}
|
||||
Remove-Item -LiteralPath $decodedRoot -Recurse -Force
|
||||
Remove-Item -LiteralPath $packetsPath -Force
|
||||
[ordered]@{
|
||||
schema_version = "missioncore.recorded-video-decode-repair/v1"
|
||||
decoder = "ffmpeg-h264_cuvid-output-corrupt"
|
||||
packets_requested = $RavnovesExpectedFrameCount
|
||||
frames_decoded = $decodedCount
|
||||
repaired_frame_count = $repairs.Count
|
||||
repairs = $repairs
|
||||
} | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (
|
||||
Join-Path (Split-Path $Destination -Parent) "decode-repair.json"
|
||||
) -Encoding utf8
|
||||
$frames = @(Get-ChildItem -LiteralPath $Destination -File -Filter "frame-*.png" | Sort-Object Name)
|
||||
if ($frames.Count -ne 4489 -or $frames[0].Name -ne "frame-000001.png" -or $frames[-1].Name -ne "frame-004489.png") {
|
||||
$lastFrameName = "frame-{0:D6}.png" -f $RavnovesExpectedFrameCount
|
||||
if ($frames.Count -ne $RavnovesExpectedFrameCount -or $frames[0].Name -ne "frame-000001.png" -or $frames[-1].Name -ne $lastFrameName) {
|
||||
throw "RAVNOVES full-video frame sequence changed"
|
||||
}
|
||||
}
|
||||
@@ -244,6 +357,9 @@ try {
|
||||
$framesRoot = Join-Path $runRoot "input-frames"
|
||||
Export-RavnovesVideoFrames -Destination $framesRoot
|
||||
Invoke-IsolatedRun -RunMode "ravnoves-video" -RunRoot $runRoot -Limit 0 -FramesRoot $framesRoot
|
||||
Copy-Item -LiteralPath (Join-Path $runRoot "decode-repair.json") -Destination (
|
||||
Join-Path $runRoot "result\decode-repair.json"
|
||||
)
|
||||
Remove-Item -LiteralPath $framesRoot -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seal the complete RAVNOVES004TREE semantic pass into existing LAB V1."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.laboratory.mixed_route_vegetation_review import (
|
||||
seal_mixed_route_full_video_review,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--base-lab-root", type=Path, required=True)
|
||||
parser.add_argument("--job-root", type=Path, required=True)
|
||||
parser.add_argument("--recorded-media-preparation", type=Path, required=True)
|
||||
parser.add_argument("--eomt-root", type=Path, required=True)
|
||||
parser.add_argument("--eomt-profile", type=Path, required=True)
|
||||
parser.add_argument("--ddrnet-root", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
print(
|
||||
seal_mixed_route_full_video_review(
|
||||
base_lab_root=args.base_lab_root,
|
||||
job_root=args.job_root,
|
||||
recorded_media_preparation_path=args.recorded_media_preparation,
|
||||
eomt_root=args.eomt_root,
|
||||
eomt_profile_path=args.eomt_profile,
|
||||
ddrnet_root=args.ddrnet_root,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -7,7 +7,9 @@ import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tarfile
|
||||
import tempfile
|
||||
import zipfile
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -15,6 +17,8 @@ from typing import Any
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from k1link.compute.jobs import validate_camera_compute_job
|
||||
|
||||
from k1link.laboratory.vegetation_shadow_lab import (
|
||||
LAB_SCHEMA,
|
||||
RESULT_PREFIX,
|
||||
@@ -45,11 +49,20 @@ TGS_COLORS = {
|
||||
2: (235, 112, 122),
|
||||
3: (150, 154, 163),
|
||||
}
|
||||
FULL_ROUTE_SOURCE_ID = "RAVNOVES004TREE"
|
||||
FULL_ROUTE_FRAME_COUNT = 6830
|
||||
FULL_ROUTE_JOB_ID = "recorded-camera-eb2783c5480d56bda07c8af0"
|
||||
FULL_ROUTE_INPUT_SHA256 = (
|
||||
"eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715"
|
||||
)
|
||||
FULL_ROUTE_STREAM_SHA256 = (
|
||||
"e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac"
|
||||
)
|
||||
|
||||
|
||||
def _read_json(path: Path, label: str) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise VegetationShadowLabError(f"{label} is invalid") from exc
|
||||
if not isinstance(value, dict):
|
||||
@@ -86,6 +99,126 @@ def _image_proof(descriptor: dict[str, object]) -> dict[str, object]:
|
||||
return {"path": descriptor["path"], "sha256": descriptor["sha256"]}
|
||||
|
||||
|
||||
def _mask_archive_descriptor(
|
||||
path: Path,
|
||||
relative: str,
|
||||
artifacts: list[dict[str, object]],
|
||||
*,
|
||||
role: str,
|
||||
) -> dict[str, object]:
|
||||
descriptor = {
|
||||
"role": role,
|
||||
"path": relative,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": sha256_path(path),
|
||||
"media_type": "application/zip",
|
||||
}
|
||||
artifacts.append(descriptor)
|
||||
return descriptor
|
||||
|
||||
|
||||
def _repack_eomt_masks(source: Path, destination: Path, frame_count: int) -> None:
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
expected = [f"semantic-masks/frame-{sequence + 1:06d}.png" for sequence in range(frame_count)]
|
||||
try:
|
||||
with (
|
||||
tarfile.open(source, mode="r:gz") as archive,
|
||||
zipfile.ZipFile(
|
||||
destination,
|
||||
mode="x",
|
||||
compression=zipfile.ZIP_STORED,
|
||||
allowZip64=True,
|
||||
) as output,
|
||||
):
|
||||
members = [member for member in archive.getmembers() if member.isfile()]
|
||||
if [member.name.removeprefix("./") for member in members] != expected:
|
||||
raise VegetationShadowLabError("full-route EoMT mask sequence changed")
|
||||
for member, expected_name in zip(members, expected, strict=True):
|
||||
if member.size < 8 or member.size > 1024 * 1024:
|
||||
raise VegetationShadowLabError("full-route EoMT mask size changed")
|
||||
stream = archive.extractfile(member)
|
||||
if stream is None:
|
||||
raise VegetationShadowLabError("full-route EoMT mask is unavailable")
|
||||
output.writestr(
|
||||
f"masks/{Path(expected_name).name}",
|
||||
stream.read(),
|
||||
)
|
||||
except (OSError, tarfile.TarError, zipfile.BadZipFile) as exc:
|
||||
destination.unlink(missing_ok=True)
|
||||
raise VegetationShadowLabError("full-route EoMT archive is invalid") from exc
|
||||
|
||||
|
||||
def _validate_zip_masks(path: Path, frame_count: int) -> None:
|
||||
expected = [f"masks/frame-{sequence + 1:06d}.png" for sequence in range(frame_count)]
|
||||
try:
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
members = archive.infolist()
|
||||
if (
|
||||
[member.filename for member in members] != expected
|
||||
or any(
|
||||
member.is_dir() or member.file_size < 8 or member.file_size > 1024 * 1024
|
||||
for member in members
|
||||
)
|
||||
):
|
||||
raise VegetationShadowLabError("full-route semantic mask sequence changed")
|
||||
except (OSError, zipfile.BadZipFile) as exc:
|
||||
raise VegetationShadowLabError("full-route semantic archive is invalid") from exc
|
||||
|
||||
|
||||
def _full_route_frame_times(media: dict[str, Any], frame_count: int) -> list[int]:
|
||||
epochs = media.get("epochs")
|
||||
start = media.get("timeline_start_seconds")
|
||||
end = media.get("timeline_end_seconds")
|
||||
if (
|
||||
not isinstance(epochs, list)
|
||||
or len(epochs) != 1
|
||||
or not isinstance(start, (int, float))
|
||||
or not isinstance(end, (int, float))
|
||||
):
|
||||
raise VegetationShadowLabError("recorded media timeline changed")
|
||||
epoch = epochs[0]
|
||||
segments = epoch.get("segments") if isinstance(epoch, dict) else None
|
||||
if not isinstance(segments, list) or len(segments) != frame_count:
|
||||
raise VegetationShadowLabError("recorded media segment count changed")
|
||||
starts = [float(start)]
|
||||
previous_end = 0.0
|
||||
for sequence, raw in enumerate(segments, start=1):
|
||||
if (
|
||||
not isinstance(raw, dict)
|
||||
or raw.get("sequence") != sequence
|
||||
or not isinstance(raw.get("end_time_seconds"), (int, float))
|
||||
or float(raw["end_time_seconds"]) <= previous_end
|
||||
):
|
||||
raise VegetationShadowLabError("recorded media segment timeline changed")
|
||||
if sequence < frame_count:
|
||||
starts.append(float(start) + float(raw["end_time_seconds"]))
|
||||
previous_end = float(raw["end_time_seconds"])
|
||||
if abs((float(start) + previous_end) - float(end)) > 0.001:
|
||||
raise VegetationShadowLabError("recorded media duration changed")
|
||||
return [round(value * 1_000_000_000) for value in starts]
|
||||
|
||||
|
||||
def _eomt_taxonomy(profile: dict[str, Any]) -> dict[str, object]:
|
||||
taxonomy = profile.get("target_taxonomy")
|
||||
if not isinstance(taxonomy, dict) or set(taxonomy) != {str(index) for index in range(16)}:
|
||||
raise VegetationShadowLabError("EoMT target taxonomy changed")
|
||||
classes = []
|
||||
for class_id in range(16):
|
||||
digest = hashlib.sha256(f"mission-core-segment-{class_id}".encode()).digest()
|
||||
classes.append(
|
||||
{
|
||||
"class_id": class_id,
|
||||
"label": taxonomy[str(class_id)],
|
||||
"color_rgb": [64 + digest[index] % 176 for index in range(3)],
|
||||
"disposition": "undefined" if class_id == 0 else "prediction",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"schema_version": "missioncore.recorded-eomt-taxonomy/v1",
|
||||
"classes": classes,
|
||||
}
|
||||
|
||||
|
||||
def _render_tgs_costmaps(tgs_root: Path, destination: Path) -> list[Path]:
|
||||
result = _read_json(tgs_root / "result.json", "mixed-route TGS result")
|
||||
evidence = result.get("evidence")
|
||||
@@ -392,6 +525,338 @@ def seal_mixed_route_vegetation_review(
|
||||
raise
|
||||
|
||||
|
||||
def seal_mixed_route_full_video_review(
|
||||
*,
|
||||
base_lab_root: Path,
|
||||
job_root: Path,
|
||||
recorded_media_preparation_path: Path,
|
||||
eomt_root: Path,
|
||||
eomt_profile_path: Path,
|
||||
ddrnet_root: Path,
|
||||
output_root: Path,
|
||||
) -> Path:
|
||||
"""Publish the complete 004 city/nature pass in the existing M4.7 LAB."""
|
||||
|
||||
base_root = base_lab_root.resolve(strict=True)
|
||||
base = _read_json(base_root / "result.json", "base vegetation LAB")
|
||||
base_identity = base.get("identity")
|
||||
if (
|
||||
base.get("schema_version") != LAB_SCHEMA
|
||||
or not isinstance(base_identity, dict)
|
||||
or hashlib.sha256(canonical_json(base_identity)).hexdigest()
|
||||
!= base.get("identity_sha256")
|
||||
or base.get("result_id") != base_root.name
|
||||
or not base_root.name.startswith(RESULT_PREFIX)
|
||||
or base.get("authority", {}).get("commands_enabled") is not False
|
||||
):
|
||||
raise VegetationShadowLabError("base vegetation LAB proof changed")
|
||||
|
||||
job = validate_camera_compute_job(job_root)
|
||||
if (
|
||||
job.job_id != FULL_ROUTE_JOB_ID
|
||||
or job.input_sha256 != FULL_ROUTE_INPUT_SHA256
|
||||
or job.session_id != "20260828T130511Z_viewer_live"
|
||||
or job.source_id != "sensor.camera.right"
|
||||
or job.segment_count != FULL_ROUTE_FRAME_COUNT
|
||||
):
|
||||
raise VegetationShadowLabError("full-route camera job changed")
|
||||
|
||||
eomt = _read_json(eomt_root / "result.json", "full-route EoMT result")
|
||||
eomt_report = _read_json(eomt_root / "run-report.json", "full-route EoMT report")
|
||||
decode_repair = _read_json(
|
||||
eomt_root / "decode-repair.json",
|
||||
"full-route video decode repair",
|
||||
)
|
||||
eomt_input = eomt_report.get("input")
|
||||
eomt_metrics = eomt_report.get("metrics")
|
||||
if (
|
||||
eomt.get("schema_version") != "missioncore.recorded-perception-result/v2"
|
||||
or eomt.get("ground_truth") is not False
|
||||
or eomt.get("frames_processed") != FULL_ROUTE_FRAME_COUNT
|
||||
or not isinstance(eomt_input, dict)
|
||||
or eomt_input.get("job_id") != job.job_id
|
||||
or eomt_input.get("input_sha256") != job.input_sha256
|
||||
or eomt_input.get("frames_admitted") != FULL_ROUTE_FRAME_COUNT
|
||||
or not isinstance(eomt_metrics, dict)
|
||||
or eomt_metrics.get("frames_processed") != FULL_ROUTE_FRAME_COUNT
|
||||
):
|
||||
raise VegetationShadowLabError("full-route EoMT contract changed")
|
||||
if (
|
||||
decode_repair.get("schema_version")
|
||||
!= "missioncore.recorded-video-decode-repair/v1"
|
||||
or decode_repair.get("decoder") != "ffmpeg-h264_cuvid-output-corrupt"
|
||||
or decode_repair.get("packets_requested") != FULL_ROUTE_FRAME_COUNT
|
||||
or decode_repair.get("frames_decoded") != FULL_ROUTE_FRAME_COUNT - 1
|
||||
or decode_repair.get("repaired_frame_count") != 1
|
||||
or decode_repair.get("repairs")
|
||||
!= [
|
||||
{
|
||||
"sequence": 6092,
|
||||
"packet_pts": 55656450,
|
||||
"method": "duplicate-previous-decoded-frame",
|
||||
}
|
||||
]
|
||||
):
|
||||
raise VegetationShadowLabError("full-route video decode repair changed")
|
||||
eomt_artifacts = {
|
||||
item.get("kind"): item
|
||||
for item in eomt.get("artifacts", [])
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
eomt_archive_proof = eomt_artifacts.get("panoptic-mask-archive")
|
||||
if not isinstance(eomt_archive_proof, dict):
|
||||
raise VegetationShadowLabError("full-route EoMT mask proof is missing")
|
||||
eomt_archive = eomt_root / str(eomt_archive_proof.get("path"))
|
||||
if (
|
||||
not eomt_archive.is_file()
|
||||
or eomt_archive.stat().st_size != eomt_archive_proof.get("byte_length")
|
||||
or sha256_path(eomt_archive) != eomt_archive_proof.get("sha256")
|
||||
):
|
||||
raise VegetationShadowLabError("full-route EoMT mask proof changed")
|
||||
|
||||
ddrnet = _read_json(ddrnet_root / "result.json", "full-route DDRNet result")
|
||||
ddrnet_decode_repair = _read_json(
|
||||
ddrnet_root / "decode-repair.json",
|
||||
"full-route DDRNet video decode repair",
|
||||
)
|
||||
ddrnet_source = ddrnet.get("source")
|
||||
ddrnet_video = ddrnet.get("video_semantics")
|
||||
if (
|
||||
ddrnet.get("schema_version") != "missioncore.lab-v1-goose-vegetation-run/v1"
|
||||
or ddrnet.get("mode") != "ravnoves-video"
|
||||
or ddrnet.get("candidate", {}).get("candidate_key") != "ddrnet"
|
||||
or not isinstance(ddrnet_source, dict)
|
||||
or ddrnet_source.get("source_id")
|
||||
!= f"{FULL_ROUTE_SOURCE_ID}/right-{FULL_ROUTE_STREAM_SHA256}"
|
||||
or ddrnet_source.get("input_count") != FULL_ROUTE_FRAME_COUNT
|
||||
or ddrnet_source.get("ground_truth_available") is not False
|
||||
or not isinstance(ddrnet_video, dict)
|
||||
or ddrnet_video.get("base_m4_result_id") is not None
|
||||
or ddrnet.get("authority", {}).get("navigation_accepted") is not False
|
||||
or ddrnet.get("authority", {}).get("actuation_accepted") is not False
|
||||
):
|
||||
raise VegetationShadowLabError("full-route DDRNet contract changed")
|
||||
if ddrnet_decode_repair != decode_repair:
|
||||
raise VegetationShadowLabError("full-route model decoders disagree")
|
||||
ddrnet_archive_proof = ddrnet_video.get("mask_archive")
|
||||
ddrnet_taxonomy = ddrnet_video.get("taxonomy")
|
||||
if (
|
||||
not isinstance(ddrnet_archive_proof, dict)
|
||||
or ddrnet_archive_proof.get("frame_count") != FULL_ROUTE_FRAME_COUNT
|
||||
or not isinstance(ddrnet_taxonomy, dict)
|
||||
):
|
||||
raise VegetationShadowLabError("full-route DDRNet mask proof changed")
|
||||
ddrnet_archive = ddrnet_root / str(ddrnet_archive_proof.get("path"))
|
||||
if (
|
||||
not ddrnet_archive.is_file()
|
||||
or ddrnet_archive.stat().st_size != ddrnet_archive_proof.get("byte_length")
|
||||
or sha256_path(ddrnet_archive) != ddrnet_archive_proof.get("sha256")
|
||||
):
|
||||
raise VegetationShadowLabError("full-route DDRNet archive changed")
|
||||
_validate_zip_masks(ddrnet_archive, FULL_ROUTE_FRAME_COUNT)
|
||||
|
||||
media_document = _read_json(
|
||||
recorded_media_preparation_path.resolve(strict=True),
|
||||
"recorded media preparation",
|
||||
)
|
||||
media = media_document.get("manifest")
|
||||
if (
|
||||
media_document.get("schema_version") != "missioncore.recorded-media-preparation/v3"
|
||||
or media_document.get("session_id") != job.session_id
|
||||
or media_document.get("artifact_id") != "recorded-video-6a3945242828a038"
|
||||
or media_document.get("checksum_sha256")
|
||||
!= "557e61f2839140dc9f97b5aea855c576b0616573080dff5d2852ab1df0558665"
|
||||
or not isinstance(media, dict)
|
||||
or media.get("source_id") != "recorded.camera.6a3945242828a038"
|
||||
or media.get("generation_sha256")
|
||||
!= "b073ea1e7babf1c77a664e1a5b95e3702d0e05b0e34c1e85a7c67a6f8b392ded"
|
||||
or media.get("byte_length") != 551674491
|
||||
or media.get("timeline_start_seconds") != job.timeline_start_seconds
|
||||
or media.get("timeline_end_seconds") != job.timeline_end_seconds
|
||||
or media.get("synchronization") != "host-arrival-best-effort"
|
||||
):
|
||||
raise VegetationShadowLabError("recorded media preparation changed")
|
||||
frame_times_ns = _full_route_frame_times(media, FULL_ROUTE_FRAME_COUNT)
|
||||
eomt_profile = _read_json(eomt_profile_path.resolve(strict=True), "EoMT profile")
|
||||
eomt_taxonomy = _eomt_taxonomy(eomt_profile)
|
||||
|
||||
output_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
temporary = Path(tempfile.mkdtemp(prefix=".mixed-route-full-video-", dir=output_root))
|
||||
artifacts: list[dict[str, object]] = []
|
||||
try:
|
||||
eomt_destination = temporary / "video" / "eomt-semantic-masks.zip"
|
||||
_repack_eomt_masks(eomt_archive, eomt_destination, FULL_ROUTE_FRAME_COUNT)
|
||||
_validate_zip_masks(eomt_destination, FULL_ROUTE_FRAME_COUNT)
|
||||
eomt_descriptor = _mask_archive_descriptor(
|
||||
eomt_destination,
|
||||
"video/eomt-semantic-masks.zip",
|
||||
artifacts,
|
||||
role="full-route-eomt-semantic-mask-archive",
|
||||
)
|
||||
ddrnet_descriptor = _artifact(
|
||||
ddrnet_archive,
|
||||
temporary,
|
||||
"video/ddrnet-semantic-masks.zip",
|
||||
artifacts,
|
||||
role="full-route-ddrnet-semantic-mask-archive",
|
||||
media_type="application/zip",
|
||||
)
|
||||
_validate_zip_masks(
|
||||
temporary / "video" / "ddrnet-semantic-masks.zip",
|
||||
FULL_ROUTE_FRAME_COUNT,
|
||||
)
|
||||
proof_descriptors: dict[str, dict[str, object]] = {}
|
||||
for key, path in (
|
||||
("base", base_root / "result.json"),
|
||||
("job", job.manifest_path),
|
||||
("media", recorded_media_preparation_path.resolve(strict=True)),
|
||||
("eomt", eomt_root / "result.json"),
|
||||
("eomt_report", eomt_root / "run-report.json"),
|
||||
("decode_repair", eomt_root / "decode-repair.json"),
|
||||
("ddrnet", ddrnet_root / "result.json"),
|
||||
("ddrnet_decode_repair", ddrnet_root / "decode-repair.json"),
|
||||
):
|
||||
descriptor = _artifact(
|
||||
path,
|
||||
temporary,
|
||||
f"proofs/{key}.json",
|
||||
artifacts,
|
||||
role="full-route-proof",
|
||||
media_type="application/json",
|
||||
)
|
||||
proof_descriptors[key] = _image_proof(descriptor)
|
||||
|
||||
full_route = {
|
||||
"source_id": FULL_ROUTE_SOURCE_ID,
|
||||
"session_id": job.session_id,
|
||||
"source_job_id": job.job_id,
|
||||
"source_job_input_sha256": job.input_sha256,
|
||||
"source_stream_sha256": FULL_ROUTE_STREAM_SHA256,
|
||||
"recorded_media_source_id": media["source_id"],
|
||||
"recorded_media_generation_sha256": media["generation_sha256"],
|
||||
"frame_count": FULL_ROUTE_FRAME_COUNT,
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"timeline_start_seconds": job.timeline_start_seconds,
|
||||
"timeline_end_seconds": job.timeline_end_seconds,
|
||||
"frame_source_times_ns": frame_times_ns,
|
||||
"ground_truth": False,
|
||||
"decode_repair": {
|
||||
"repaired_frame_count": 1,
|
||||
"sequence": 6092,
|
||||
"method": "duplicate-previous-decoded-frame",
|
||||
"proofs": {
|
||||
"eomt": proof_descriptors["decode_repair"],
|
||||
"ddrnet": proof_descriptors["ddrnet_decode_repair"],
|
||||
},
|
||||
},
|
||||
"layers": {
|
||||
"city": {
|
||||
"name": "EoMT Cityscapes",
|
||||
"result_id": eomt["result_id"],
|
||||
"frame_count": FULL_ROUTE_FRAME_COUNT,
|
||||
"taxonomy": eomt_taxonomy,
|
||||
"mask_archive": {
|
||||
"path": eomt_descriptor["path"],
|
||||
"sha256": eomt_descriptor["sha256"],
|
||||
"byte_length": eomt_descriptor["byte_length"],
|
||||
},
|
||||
"inference_fps": eomt_metrics["inference_frames_per_second"],
|
||||
"latency_p95_ms": eomt_metrics["latency_ms"]["end_to_end_ms"]["p95"],
|
||||
"peak_reserved_vram_bytes": int(
|
||||
float(eomt_metrics["cuda_peak_memory_reserved_mib"]) * 1024 * 1024
|
||||
),
|
||||
},
|
||||
"vegetation": {
|
||||
"name": ddrnet["candidate"]["loaded_model_name"],
|
||||
"result_id": ddrnet["result_id"],
|
||||
"frame_count": FULL_ROUTE_FRAME_COUNT,
|
||||
"taxonomy": ddrnet_taxonomy,
|
||||
"mask_archive": {
|
||||
"path": ddrnet_descriptor["path"],
|
||||
"sha256": ddrnet_descriptor["sha256"],
|
||||
"byte_length": ddrnet_descriptor["byte_length"],
|
||||
},
|
||||
"inference_fps": ddrnet["timing"]["throughput_fps_from_mean_inference"],
|
||||
"latency_p95_ms": ddrnet["timing"]["latency_ms_p95"],
|
||||
"peak_reserved_vram_bytes": ddrnet["resource"]["peak_reserved_vram_bytes"],
|
||||
},
|
||||
},
|
||||
"proofs": proof_descriptors,
|
||||
"limitations": [
|
||||
"RAVNOVES004TREE has no manual route truth.",
|
||||
"One corrupt H.264 packet at sequence 6092 was represented by the previous decoded frame; the repair is sealed as evidence.",
|
||||
"EoMT and DDRNet were executed sequentially, not as a concurrent realtime stack.",
|
||||
"DDRNet vegetation subtypes remain prediction-only and are not planner authority.",
|
||||
"This full-video pass does not add full-route TGS, ditch or negative-obstacle proof.",
|
||||
"People and vehicles still require an independent fail-safe detector and STOP path.",
|
||||
],
|
||||
}
|
||||
authority = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"actuation_accepted": False,
|
||||
"camera_semantics_can_clear_rigid_geometry": False,
|
||||
}
|
||||
identity = {
|
||||
"lab_id": "lab-v1-vegetation-mission-policy",
|
||||
"base_result_id": base["result_id"],
|
||||
"selected_candidate": base_identity["selected_candidate"],
|
||||
"candidate_metrics": base_identity["candidate_metrics"],
|
||||
"source": {
|
||||
"shadow_session": FULL_ROUTE_SOURCE_ID,
|
||||
"shadow_camera": job.source_id,
|
||||
"shadow_frame_count": FULL_ROUTE_FRAME_COUNT,
|
||||
"video_shadow_frame_count": FULL_ROUTE_FRAME_COUNT,
|
||||
},
|
||||
"route_full_review": full_route,
|
||||
"authority": authority,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest()
|
||||
result_id = f"{RESULT_PREFIX}{identity_sha256}"
|
||||
manifest = {
|
||||
"schema_version": LAB_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"created_at_utc": datetime.now(UTC).isoformat(),
|
||||
"ground_truth": False,
|
||||
"status": "visual-shadow-ready-policy-not-authorized",
|
||||
"identity": identity,
|
||||
"source": identity["source"],
|
||||
"route_video": None,
|
||||
"route_review": None,
|
||||
"route_full_review": full_route,
|
||||
"method": {
|
||||
"completeness": "complete",
|
||||
"execution_class": "ai-inference",
|
||||
"pipeline_id": "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
},
|
||||
"metrics": {"candidates": base["metrics"]["candidates"]},
|
||||
"decision": {
|
||||
"selected_candidate": base_identity["selected_candidate"],
|
||||
"visual_shadow_ready": True,
|
||||
"full_video_shadow_ready": True,
|
||||
"mission_policy_ready_for_configuration": True,
|
||||
"multilayer_policy_review_ready": True,
|
||||
"navigation_accepted": False,
|
||||
"production_accepted": False,
|
||||
},
|
||||
"limitations": full_route["limitations"],
|
||||
"authority": authority,
|
||||
"catalogs": {"goose": [], "ravnoves": []},
|
||||
"artifacts": artifacts,
|
||||
}
|
||||
(temporary / "result.json").write_bytes(canonical_json(manifest) + b"\n")
|
||||
destination = output_root / result_id
|
||||
if destination.exists():
|
||||
raise VegetationShadowLabError("immutable full-route LAB result already exists")
|
||||
os.replace(temporary, destination)
|
||||
return destination
|
||||
except Exception:
|
||||
shutil.rmtree(temporary, ignore_errors=True)
|
||||
raise
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--base-lab-root", type=Path, required=True)
|
||||
|
||||
@@ -179,9 +179,77 @@ def _build_vegetation_lab_router(
|
||||
},
|
||||
)
|
||||
|
||||
@router.get("/{result_id}/route-masks/{layer}/{sequence}")
|
||||
def get_full_route_mask(result_id: str, layer: str, sequence: int) -> Response:
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
manifest = _read_verified(candidate, definition)
|
||||
route = manifest.get("route_full_review")
|
||||
layers = route.get("layers") if isinstance(route, dict) else None
|
||||
frame_count = route.get("frame_count") if isinstance(route, dict) else None
|
||||
selected = layers.get(layer) if isinstance(layers, dict) else None
|
||||
archive = selected.get("mask_archive") if isinstance(selected, dict) else None
|
||||
archive_relative = archive.get("path") if isinstance(archive, dict) else None
|
||||
if (
|
||||
layer not in {"city", "vegetation"}
|
||||
or not isinstance(frame_count, int)
|
||||
or not 0 <= sequence < frame_count
|
||||
or not isinstance(archive_relative, str)
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="Full-route semantic mask not found")
|
||||
relative = PurePosixPath(archive_relative)
|
||||
artifacts = manifest.get("artifacts")
|
||||
if (
|
||||
relative.is_absolute()
|
||||
or str(relative) != archive_relative
|
||||
or any(part in {"", ".", ".."} for part in relative.parts)
|
||||
or relative.suffix != ".zip"
|
||||
or not isinstance(artifacts, list)
|
||||
or not any(
|
||||
isinstance(item, dict)
|
||||
and item.get("path") == archive_relative
|
||||
and item.get("media_type") == "application/zip"
|
||||
for item in artifacts
|
||||
)
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="Full-route semantic mask not found")
|
||||
return _zip_mask_response(candidate.joinpath(*relative.parts), sequence)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
def _zip_mask_response(archive_path: Path, sequence: int) -> Response:
|
||||
member = f"masks/frame-{sequence + 1:06d}.png"
|
||||
try:
|
||||
before = archive_path.stat()
|
||||
with zipfile.ZipFile(archive_path) as frozen:
|
||||
info = frozen.getinfo(member)
|
||||
if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024:
|
||||
raise ValueError("Semantic mask member is invalid")
|
||||
payload = frozen.read(info)
|
||||
after = archive_path.stat()
|
||||
if (
|
||||
before.st_size != after.st_size
|
||||
or before.st_mtime_ns != after.st_mtime_ns
|
||||
or len(payload) != info.file_size
|
||||
):
|
||||
raise ValueError("Semantic mask archive changed during read")
|
||||
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Semantic mask failed verification",
|
||||
) from None
|
||||
digest = hashlib.sha256(payload).hexdigest()
|
||||
return Response(
|
||||
content=payload,
|
||||
media_type="image/png",
|
||||
headers={
|
||||
"Cache-Control": "private, max-age=31536000, immutable",
|
||||
"ETag": f'"{digest}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
candidate = provider()
|
||||
if candidate is None:
|
||||
|
||||
@@ -104,3 +104,20 @@ def test_e4_class_fractions_use_only_valid_fov_pixels() -> None:
|
||||
|
||||
assert {item["id"]: item["pixels"] for item in classes} == {1: 1, 4: 2, 7: 1}
|
||||
assert sum(float(item["fraction_of_valid_fov"]) for item in classes) == 1.0
|
||||
|
||||
|
||||
def test_e4_orchestrator_seals_a_single_decoder_gap_without_frame_shift() -> None:
|
||||
path = (
|
||||
Path(__file__).parents[1]
|
||||
/ "experiments"
|
||||
/ "perception"
|
||||
/ "worker"
|
||||
/ "Invoke-E4FullSessionSegmentation.ps1"
|
||||
)
|
||||
source = path.read_text(encoding="utf-8")
|
||||
assert "-c:v h264_cuvid" in source
|
||||
assert "-frame_pts 1" in source
|
||||
assert '$decodedPath = Join-Path $decodedFramesRoot ("frame-{0}.png" -f $pts)' in source
|
||||
assert "$repairs.Count -ge 1" in source
|
||||
assert 'method = "duplicate-previous-decoded-frame"' in source
|
||||
assert 'schema_version = "missioncore.recorded-video-decode-repair/v1"' in source
|
||||
|
||||
@@ -25,6 +25,12 @@ POWERSHELL_PATH = (
|
||||
/ "worker"
|
||||
/ "Invoke-LabV1VegetationGooseBenchmark.ps1"
|
||||
)
|
||||
RAV004_SOURCE_PATH = (
|
||||
REPOSITORY_ROOT
|
||||
/ "config"
|
||||
/ "perception"
|
||||
/ "lab-v1-ravnoves004tree-full-video-source-v1.json"
|
||||
)
|
||||
|
||||
|
||||
def test_benchmark_contract_is_bounded_and_fail_closed() -> None:
|
||||
@@ -93,3 +99,21 @@ def test_worker_wrapper_is_isolated_from_canonical_triton() -> None:
|
||||
assert '"--cap-drop", "ALL"' in source
|
||||
assert '"--security-opt", "no-new-privileges"' in source
|
||||
assert "if ($canonicalAfter -ne $canonicalBefore)" in source
|
||||
|
||||
|
||||
def test_rav004_full_video_profile_and_decoder_gap_are_explicit() -> None:
|
||||
profile = json.loads(RAV004_SOURCE_PATH.read_text(encoding="utf-8"))
|
||||
source_profile = profile["source"]
|
||||
assert profile["schema_version"] == "missioncore.lab-v1-ravnoves-source/v1"
|
||||
assert source_profile["source_job_id"] == (
|
||||
"recorded-camera-eb2783c5480d56bda07c8af0"
|
||||
)
|
||||
assert source_profile["expected_frame_count"] == 6830
|
||||
assert source_profile["base_m4_result_id"] is None
|
||||
source = POWERSHELL_PATH.read_text(encoding="utf-8")
|
||||
assert "-c:v h264_cuvid" in source
|
||||
assert 'schema_version = "missioncore.recorded-video-decode-repair/v1"' in source
|
||||
assert 'method = "duplicate-previous-decoded-frame"' in source
|
||||
assert "$repairs.Count -ge 1" in source
|
||||
assert '& docker @arguments 2>&1 | ForEach-Object { Write-Output $_ }' in source
|
||||
assert 'if ($dockerExitCode -ne 0)' in source
|
||||
|
||||
@@ -270,6 +270,67 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(
|
||||
assert mask.content == b"\x89PNG\r\n\x1a\n"
|
||||
assert mask.headers["cache-control"].endswith("immutable")
|
||||
|
||||
full_archive_payloads = (b"\x89PNG\r\n\x1a\ncity", b"\x89PNG\r\n\x1a\nvegetation")
|
||||
full_identity = dict(manifest["identity"])
|
||||
full_route = {
|
||||
"frame_count": 2,
|
||||
"layers": {
|
||||
layer: {"mask_archive": {"path": "video/full-route-masks.zip"}}
|
||||
for layer in ("city", "vegetation")
|
||||
},
|
||||
}
|
||||
full_identity["route_full_review"] = full_route
|
||||
full_identity_sha = hashlib.sha256(
|
||||
json.dumps(
|
||||
full_identity,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
).hexdigest()
|
||||
full_result_id = f"lab-v1-vegetation-shadow-{full_identity_sha}"
|
||||
full_root = result_root.parent / full_result_id
|
||||
shutil.copytree(result_root, full_root)
|
||||
full_archive = full_root / "video" / "full-route-masks.zip"
|
||||
full_archive.parent.mkdir(exist_ok=True)
|
||||
with zipfile.ZipFile(full_archive, "x", compression=zipfile.ZIP_STORED) as frozen:
|
||||
for sequence, payload in enumerate(full_archive_payloads, start=1):
|
||||
frozen.writestr(f"masks/frame-{sequence:06d}.png", payload)
|
||||
full_manifest = dict(manifest)
|
||||
full_manifest["result_id"] = full_result_id
|
||||
full_manifest["identity"] = full_identity
|
||||
full_manifest["identity_sha256"] = full_identity_sha
|
||||
full_manifest["route_full_review"] = full_route
|
||||
full_manifest["artifacts"] = [
|
||||
*manifest["artifacts"],
|
||||
{
|
||||
"role": "full-route-mask-fixture",
|
||||
"path": "video/full-route-masks.zip",
|
||||
"byte_length": full_archive.stat().st_size,
|
||||
"sha256": _sha256(full_archive),
|
||||
"media_type": "application/zip",
|
||||
},
|
||||
]
|
||||
(full_root / "result.json").write_text(
|
||||
json.dumps(full_manifest, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
for layer, sequence, expected in (
|
||||
("city", 0, full_archive_payloads[0]),
|
||||
("vegetation", 1, full_archive_payloads[1]),
|
||||
):
|
||||
response = client.get(
|
||||
f"/api/v1/laboratory/vegetation-shadow/{full_result_id}"
|
||||
f"/route-masks/{layer}/{sequence}"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.content == expected
|
||||
assert response.headers["cache-control"].endswith("immutable")
|
||||
assert client.get(
|
||||
f"/api/v1/laboratory/vegetation-shadow/{full_result_id}/route-masks/city/2"
|
||||
).status_code == 404
|
||||
|
||||
(result_root / asset_path).write_bytes(b"tampered")
|
||||
assert (
|
||||
client.get(f"/api/v1/laboratory/vegetation-shadow/{result_root.name}").status_code
|
||||
|
||||
Reference in New Issue
Block a user