fix(lab): keep full route result indexable
This commit is contained in:
@@ -129,6 +129,10 @@ export interface VegetationFullRouteReview {
|
||||
height: 600;
|
||||
timelineStartSeconds: number;
|
||||
timelineEndSeconds: number;
|
||||
timelineArtifact: {
|
||||
sha256: string;
|
||||
byteLength: number;
|
||||
};
|
||||
frameSourceTimesNs: readonly number[];
|
||||
decodeRepair: {
|
||||
repairedFrameCount: 1;
|
||||
@@ -742,22 +746,30 @@ function fullRouteReviewValue(value: unknown): VegetationFullRouteReview | null
|
||||
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 timeline = objectValue(row.timeline, "vegetation.route_full_review.timeline");
|
||||
exact(
|
||||
timeline.path,
|
||||
"video/frame-source-times-ns.bin",
|
||||
"vegetation.route_full_review.timeline.path",
|
||||
);
|
||||
exact(
|
||||
timeline.encoding,
|
||||
"uint64-le-nanoseconds",
|
||||
"vegetation.route_full_review.timeline.encoding",
|
||||
);
|
||||
exact(timeline.frame_count, 6830, "vegetation.route_full_review.timeline.frame_count");
|
||||
const timelineSha256 = textValue(
|
||||
timeline.sha256,
|
||||
"vegetation.route_full_review.timeline.sha256",
|
||||
);
|
||||
if (!SHA256.test(timelineSha256)) {
|
||||
throw new VegetationShadowContractError("vegetation.route_full_review: timeline digest invalid.");
|
||||
}
|
||||
const timelineByteLength = integerValue(
|
||||
timeline.byte_length,
|
||||
"vegetation.route_full_review.timeline.byte_length",
|
||||
);
|
||||
exact(timelineByteLength, 6830 * 8, "vegetation.route_full_review.timeline.byte_length");
|
||||
const decodeRepair = objectValue(
|
||||
row.decode_repair,
|
||||
"vegetation.route_full_review.decode_repair",
|
||||
@@ -808,7 +820,8 @@ function fullRouteReviewValue(value: unknown): VegetationFullRouteReview | null
|
||||
height: 600,
|
||||
timelineStartSeconds,
|
||||
timelineEndSeconds,
|
||||
frameSourceTimesNs,
|
||||
timelineArtifact: { sha256: timelineSha256, byteLength: timelineByteLength },
|
||||
frameSourceTimesNs: [],
|
||||
decodeRepair: {
|
||||
repairedFrameCount: 1,
|
||||
sequence: 6092,
|
||||
@@ -935,11 +948,49 @@ export async function fetchVegetationShadowResult(
|
||||
if (!response.ok) {
|
||||
throw new VegetationShadowContractError(`Vegetation LAB недоступна: HTTP ${response.status}.`);
|
||||
}
|
||||
return parseResult(
|
||||
const result = parseResult(
|
||||
await response.json(),
|
||||
resultId,
|
||||
"/api/v1/laboratory/vegetation-shadow",
|
||||
);
|
||||
if (!result.routeFullReview) return result;
|
||||
const timelineResponse = await fetcher(
|
||||
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/route-timeline`,
|
||||
{ method: "GET", headers: { Accept: "application/octet-stream" }, signal },
|
||||
);
|
||||
if (!timelineResponse.ok) {
|
||||
throw new VegetationShadowContractError(
|
||||
`Vegetation LAB timeline недоступна: HTTP ${timelineResponse.status}.`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
timelineResponse.headers.get("etag")
|
||||
!== `"${result.routeFullReview.timelineArtifact.sha256}"`
|
||||
) {
|
||||
throw new VegetationShadowContractError("Vegetation LAB timeline digest изменён.");
|
||||
}
|
||||
const timelinePayload = await timelineResponse.arrayBuffer();
|
||||
if (timelinePayload.byteLength !== result.routeFullReview.timelineArtifact.byteLength) {
|
||||
throw new VegetationShadowContractError("Vegetation LAB timeline size изменён.");
|
||||
}
|
||||
const timelineView = new DataView(timelinePayload);
|
||||
const frameSourceTimesNs = Array.from({ length: result.routeFullReview.frameCount }, (_, index) => {
|
||||
const value = Number(timelineView.getBigUint64(index * 8, true));
|
||||
if (!Number.isSafeInteger(value)) {
|
||||
throw new VegetationShadowContractError("Vegetation LAB timeline содержит unsafe time.");
|
||||
}
|
||||
return value;
|
||||
});
|
||||
if (
|
||||
frameSourceTimesNs[0] !== Math.round(result.routeFullReview.timelineStartSeconds * 1_000_000_000)
|
||||
|| frameSourceTimesNs.some((time, index) => index > 0 && time <= frameSourceTimesNs[index - 1]!)
|
||||
) {
|
||||
throw new VegetationShadowContractError("Vegetation LAB timeline нарушена.");
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
routeFullReview: { ...result.routeFullReview, frameSourceTimesNs },
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchVegetationBenchmarkResult(
|
||||
|
||||
@@ -195,7 +195,13 @@ function fullRouteReview() {
|
||||
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),
|
||||
timeline: {
|
||||
path: "video/frame-source-times-ns.bin",
|
||||
sha256: "5".repeat(64),
|
||||
byte_length: 6830 * 8,
|
||||
encoding: "uint64-le-nanoseconds",
|
||||
frame_count: 6830,
|
||||
},
|
||||
ground_truth: false,
|
||||
decode_repair: {
|
||||
repaired_frame_count: 1,
|
||||
@@ -301,17 +307,35 @@ test("vegetation LAB parses the full 004 pass inside the existing result contrac
|
||||
catalogs: { goose: [], ravnoves: [] },
|
||||
route_full_review: fullRouteReview(),
|
||||
};
|
||||
const timeline = new ArrayBuffer(6830 * 8);
|
||||
const timelineView = new DataView(timeline);
|
||||
for (let index = 0; index < 6830; index += 1) {
|
||||
timelineView.setBigUint64(
|
||||
index * 8,
|
||||
BigInt(39_215_263_458 + index * 100_000_000),
|
||||
true,
|
||||
);
|
||||
}
|
||||
const result = await fetchVegetationShadowResult(resultId, {
|
||||
fetcher: async () => new Response(JSON.stringify(payload), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
fetcher: async (url) => String(url).endsWith("/route-timeline")
|
||||
? new Response(timeline, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
ETag: `"${"5".repeat(64)}"`,
|
||||
},
|
||||
})
|
||||
: new Response(JSON.stringify(payload), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
});
|
||||
assert.equal(result.routeVideo, null);
|
||||
assert.equal(result.routeFullReview.frameCount, 6830);
|
||||
assert.equal(result.routeFullReview.city.taxonomy.length, 16);
|
||||
assert.equal(result.routeFullReview.vegetation.taxonomy.length, 64);
|
||||
assert.equal(result.routeFullReview.decodeRepair.sequence, 6092);
|
||||
assert.equal(result.routeFullReview.frameSourceTimesNs.length, 6830);
|
||||
assert.equal(
|
||||
vegetationFullRouteMaskUrl(resultId, "vegetation", 6829),
|
||||
`/api/v1/laboratory/vegetation-shadow/${resultId}/route-masks/vegetation/6829`,
|
||||
|
||||
Reference in New Issue
Block a user