refactor(lab): объединить RAV004 в единый Rerun replay
This commit is contained in:
@@ -36,7 +36,6 @@ import {
|
||||
createRecordedAutoplayGate,
|
||||
createRecordedInitialSeekGate,
|
||||
createRecordedOpenWatchdog,
|
||||
shouldReapplyRecordedBlueprint,
|
||||
} from "../core/observation/recordedRerunLifecycle";
|
||||
import type {
|
||||
RecordedPerceptionLayers,
|
||||
@@ -72,7 +71,6 @@ export {
|
||||
recordedPlaybackRangeWhenReady,
|
||||
recordedPointColorKey,
|
||||
rerunPresentationStatus,
|
||||
shouldReapplyRecordedBlueprint,
|
||||
type RecordedPlaybackBufferState,
|
||||
} from "../core/observation/recordedRerunLifecycle";
|
||||
export {
|
||||
@@ -167,6 +165,7 @@ interface RecordedRerunIdentity {
|
||||
}
|
||||
|
||||
const RECORDED_RRD_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/recording\.rrd$/;
|
||||
const LAB_RECORDED_REPLAY_PATH = /^\/api\/v1\/laboratory\/vegetation-shadow\/lab-v1-vegetation-shadow-[a-f0-9]{64}\/canonical-replay\.rrd$/;
|
||||
const RECORDED_BLUEPRINT_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/blueprint\.rrd$/;
|
||||
const RECORDED_PERCEPTION_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/perception\.rrd$/;
|
||||
const LAB_RECORDED_PERCEPTION_PATH = /^\/api\/v1\/laboratory\/vegetation-shadow\/lab-v1-vegetation-shadow-[a-f0-9]{64}\/canonical-overlay\.rrd$/;
|
||||
@@ -231,7 +230,8 @@ export function resolveRecordedViewerSourceUrl(
|
||||
const expectedViewerSourceUrl = `${descriptor.sourceUrl}?generation=${descriptor.sha256}`;
|
||||
const endpoint = new URL(descriptor.viewerSourceUrl, `${base.origin}/`);
|
||||
if (
|
||||
!RECORDED_RRD_PATH.test(descriptor.sourceUrl) ||
|
||||
!(RECORDED_RRD_PATH.test(descriptor.sourceUrl)
|
||||
|| LAB_RECORDED_REPLAY_PATH.test(descriptor.sourceUrl)) ||
|
||||
descriptor.viewerSourceUrl !== expectedViewerSourceUrl ||
|
||||
endpoint.origin !== base.origin ||
|
||||
endpoint.pathname !== descriptor.sourceUrl ||
|
||||
@@ -252,10 +252,23 @@ export function rerunViewerInitialSource(
|
||||
return resolvedSourceUrl;
|
||||
}
|
||||
|
||||
export function resolveRecordedBlueprintUrl(sourceUrl: string, origin: string): string | null {
|
||||
export function resolveRecordedBlueprintUrl(
|
||||
sourceUrl: string,
|
||||
origin: string,
|
||||
explicitSourceUrl?: string,
|
||||
): string | null {
|
||||
const normalized = sourceUrl.trim();
|
||||
if (!RECORDED_RRD_PATH.test(normalized)) return null;
|
||||
const base = new URL(origin);
|
||||
if (explicitSourceUrl !== undefined) {
|
||||
const explicit = explicitSourceUrl.trim();
|
||||
if (
|
||||
!LAB_RECORDED_REPLAY_PATH.test(normalized)
|
||||
|| !RECORDED_BLUEPRINT_PATH.test(explicit)
|
||||
) return null;
|
||||
const endpoint = new URL(explicit, `${base.origin}/`);
|
||||
return endpoint.origin === base.origin ? endpoint.href : null;
|
||||
}
|
||||
if (!RECORDED_RRD_PATH.test(normalized)) return null;
|
||||
const endpoint = new URL(
|
||||
normalized.replace(/\/recording\.rrd$/, "/blueprint.rrd"),
|
||||
`${base.origin}/`,
|
||||
@@ -690,6 +703,7 @@ export function RerunViewport({
|
||||
segmentation: false,
|
||||
cuboids3d: false,
|
||||
};
|
||||
const recordedBlueprintSourceUrl = recordedProfile?.blueprintSourceUrl;
|
||||
const recordedPerceptionSourceUrl = recordedProfile?.perceptionSourceUrl;
|
||||
const recordedSemanticLayer = recordedProfile?.semanticLayer;
|
||||
const recordedUnifiedPerception = recordedProfile?.unifiedPerception ?? false;
|
||||
@@ -727,7 +741,11 @@ export function RerunViewport({
|
||||
const [blueprintChannelRevision, setBlueprintChannelRevision] = useState(0);
|
||||
const [perceptionChannelRevision, setPerceptionChannelRevision] = useState(0);
|
||||
const recordedBlueprintUrl = sourceUrl
|
||||
? resolveRecordedBlueprintUrl(sourceUrl, window.location.origin)
|
||||
? resolveRecordedBlueprintUrl(
|
||||
sourceUrl,
|
||||
window.location.origin,
|
||||
recordedBlueprintSourceUrl,
|
||||
)
|
||||
: null;
|
||||
const recordedPerceptionUrl = recordedPerceptionSourceUrl
|
||||
? resolveRerunSourceUrl(recordedPerceptionSourceUrl, window.location.origin)
|
||||
@@ -773,7 +791,8 @@ export function RerunViewport({
|
||||
return;
|
||||
}
|
||||
|
||||
const isRecordedSource = RECORDED_RRD_PATH.test(normalizedSource);
|
||||
const isRecordedSource = RECORDED_RRD_PATH.test(normalizedSource)
|
||||
|| LAB_RECORDED_REPLAY_PATH.test(normalizedSource);
|
||||
let resolvedSource: string;
|
||||
try {
|
||||
if (isRecordedSource) {
|
||||
@@ -1151,23 +1170,7 @@ export function RerunViewport({
|
||||
disposed ||
|
||||
(isRecordedSource && event.application_id !== "nodedc_mission_core_recorded")
|
||||
) return;
|
||||
if (recordingOpened) {
|
||||
const identity = recordedIdentityRef.current;
|
||||
if (shouldReapplyRecordedBlueprint(
|
||||
recordingOpened,
|
||||
isRecordedSource,
|
||||
recordedBlueprintUrl !== null,
|
||||
identity,
|
||||
event,
|
||||
)) {
|
||||
// A LAB sidecar is a second upstream receiver for the same
|
||||
// recording store. Rerun re-opens that store only after the
|
||||
// sidecar has been decoded, so reapply the canonical blueprint
|
||||
// at this exact boundary instead of racing it during download.
|
||||
setBlueprintChannelRevision((revision) => revision + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (recordingOpened) return;
|
||||
recordingOpened = true;
|
||||
if (!isRecordedSource) {
|
||||
// Store discovery only establishes a candidate. Admission is
|
||||
|
||||
@@ -242,7 +242,7 @@ export function CanonicalRecordedLabReplay<
|
||||
minPrimarySize={splitView ? 24 : 0}
|
||||
minSecondarySize={splitView ? 24 : 0}
|
||||
resizable={splitView}
|
||||
separatorLabel="Изменить размер VIDEO/CAMERA и 3D/PLAN"
|
||||
separatorLabel="Изменить размер видео/камеры и 3D/плана"
|
||||
/>
|
||||
{mediaMode === "none" && spatialMode === "none" ? (
|
||||
<div className="l3-visual-audit__state" role="status">
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { ObservationSessionReplayLaunch } from "../observation/sessionArchive";
|
||||
import type { RecordedRrdArtifactDescriptor } from "../observation/viewerProfile";
|
||||
|
||||
const SAFE_RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/;
|
||||
const SAFE_SESSION_SOURCE = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/recording\.rrd$/;
|
||||
const MAX_CANONICAL_REPLAY_BYTES = 1024 * 1024 * 1024;
|
||||
|
||||
export interface CanonicalLabReplayDescriptor extends RecordedRrdArtifactDescriptor {
|
||||
blueprintSourceUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the one immutable RRD used by the canonical recorded LAB.
|
||||
*
|
||||
* The server caches the merge of the sealed spatial recording and the LAB AI
|
||||
* evidence. Rerun therefore opens one source and cannot present the base store
|
||||
* before a second receiver has finished decoding the semantic layer.
|
||||
*/
|
||||
export async function resolveCanonicalLabReplay(
|
||||
resultId: string,
|
||||
launch: ObservationSessionReplayLaunch,
|
||||
{
|
||||
origin = window.location.origin,
|
||||
signal,
|
||||
fetcher = globalThis.fetch,
|
||||
}: {
|
||||
origin?: string;
|
||||
signal?: AbortSignal;
|
||||
fetcher?: typeof globalThis.fetch;
|
||||
} = {},
|
||||
): Promise<CanonicalLabReplayDescriptor> {
|
||||
const base = new URL(origin);
|
||||
if (
|
||||
!SAFE_RESULT_ID.test(resultId)
|
||||
|| !SAFE_SESSION_SOURCE.test(launch.sourceUrl)
|
||||
|| launch.viewerSourceUrl !== `${launch.sourceUrl}?generation=${launch.sha256}`
|
||||
|| !/^[a-f0-9]{64}$/.test(launch.sha256)
|
||||
) {
|
||||
throw new Error("Канонический replay LAB имеет небезопасный descriptor.");
|
||||
}
|
||||
const sourceUrl =
|
||||
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}`
|
||||
+ "/canonical-replay.rrd";
|
||||
const descriptorUrl = new URL(sourceUrl, `${base.origin}/`);
|
||||
descriptorUrl.searchParams.set("base_generation", launch.sha256);
|
||||
if (descriptorUrl.origin !== base.origin) {
|
||||
throw new Error("Канонический replay LAB должен быть same-origin.");
|
||||
}
|
||||
const response = await fetcher(descriptorUrl.href, {
|
||||
method: "HEAD",
|
||||
credentials: "same-origin",
|
||||
headers: { Accept: "application/vnd.rerun.rrd" },
|
||||
signal,
|
||||
});
|
||||
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0].trim();
|
||||
const byteLength = Number(response.headers.get("Content-Length"));
|
||||
const sha256 = response.headers.get("ETag")?.match(/^"([a-f0-9]{64})"$/)?.[1];
|
||||
if (
|
||||
response.status !== 200
|
||||
|| contentType !== "application/vnd.rerun.rrd"
|
||||
|| response.headers.get("X-Rerun-Format") !== "RRF2"
|
||||
|| !sha256
|
||||
|| !Number.isSafeInteger(byteLength)
|
||||
|| byteLength < 4
|
||||
|| byteLength > MAX_CANONICAL_REPLAY_BYTES
|
||||
) {
|
||||
throw new Error("Единый replay LAB не прошёл проверку.");
|
||||
}
|
||||
return {
|
||||
sourceUrl,
|
||||
viewerSourceUrl: `${sourceUrl}?generation=${sha256}`,
|
||||
byteLength,
|
||||
sha256,
|
||||
blueprintSourceUrl: launch.sourceUrl.replace(/\/recording\.rrd$/, "/blueprint.rrd"),
|
||||
};
|
||||
}
|
||||
@@ -196,20 +196,6 @@ export function isRecordedPlaybackFullyBuffered(
|
||||
return recordedPlaybackBufferState(rangeNs, expectedTimelineEndSeconds).fullyBuffered;
|
||||
}
|
||||
|
||||
export function shouldReapplyRecordedBlueprint(
|
||||
recordingOpened: boolean,
|
||||
recordedSource: boolean,
|
||||
hasBlueprint: boolean,
|
||||
identity: { applicationId: string; recordingId: string } | null,
|
||||
event: { application_id: string; recording_id: string },
|
||||
): boolean {
|
||||
return recordingOpened
|
||||
&& recordedSource
|
||||
&& hasBlueprint
|
||||
&& identity?.applicationId === event.application_id
|
||||
&& identity.recordingId === event.recording_id;
|
||||
}
|
||||
|
||||
export function attemptRecordedAutoplay(
|
||||
seekToStart: () => void,
|
||||
startPlaying: () => void,
|
||||
|
||||
@@ -67,6 +67,8 @@ export interface RecordedSessionRerunProfile {
|
||||
viewResetGeneration: 0 | 1;
|
||||
followTrajectory: boolean;
|
||||
perceptionLayers: RecordedPerceptionLayers;
|
||||
/** Explicit small blueprint endpoint when the viewer source is a merged LAB RRD. */
|
||||
blueprintSourceUrl?: string;
|
||||
/** Optional immutable RRD sidecar for LAB/model evidence on the same recording clock. */
|
||||
perceptionSourceUrl?: string;
|
||||
/** Selects one semantic entity without changing the sealed sidecar. */
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Button, Icon, SegmentedControl } from "@nodedc/ui-react";
|
||||
import { ObservationTimeline } from "../../components/ObservationTimeline";
|
||||
import {
|
||||
RerunViewport,
|
||||
type RecordedPerceptionLoadState,
|
||||
type RerunPlaybackController,
|
||||
type RerunPlaybackState,
|
||||
} from "../../components/RerunViewport";
|
||||
@@ -12,7 +11,12 @@ import {
|
||||
CanonicalRecordedLabReplay,
|
||||
useCanonicalRecordedLabReplayState,
|
||||
} from "../../components/laboratory/CanonicalRecordedLabReplay";
|
||||
import {
|
||||
resolveCanonicalLabReplay,
|
||||
type CanonicalLabReplayDescriptor,
|
||||
} from "../../core/laboratory/canonicalLabReplay";
|
||||
import type { VegetationFullRouteReview } from "../../core/laboratory/vegetationShadow";
|
||||
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
|
||||
import { recordedSessionRerunProfile } from "../../core/observation/viewerProfile";
|
||||
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
|
||||
import { defaultSceneSettings } from "../../sceneSettings";
|
||||
@@ -22,13 +26,10 @@ type SpatialMode = "3d" | "plan";
|
||||
type SpatialLayer = "source" | "local" | "tgs" | "semantic";
|
||||
type SemanticLayer = "city" | "vegetation";
|
||||
|
||||
const EMPTY_PERCEPTION_LOAD: RecordedPerceptionLoadState = {
|
||||
phase: "idle",
|
||||
receivedBytes: 0,
|
||||
totalBytes: null,
|
||||
progress: null,
|
||||
message: "",
|
||||
};
|
||||
interface CanonicalReplayLaunch {
|
||||
base: ObservationSessionReplayLaunch;
|
||||
replay: CanonicalLabReplayDescriptor;
|
||||
}
|
||||
|
||||
export function CanonicalVegetationRerunReplay({
|
||||
resultId,
|
||||
@@ -58,9 +59,7 @@ export function CanonicalVegetationRerunReplay({
|
||||
const [playback, setPlayback] = useState<RerunPlaybackState | null>(null);
|
||||
const [playbackController, setPlaybackController] =
|
||||
useState<RerunPlaybackController | null>(null);
|
||||
const [perceptionLoad, setPerceptionLoad] =
|
||||
useState<RecordedPerceptionLoadState>(EMPTY_PERCEPTION_LOAD);
|
||||
const [launch, setLaunch] = useState<Awaited<ReturnType<typeof resolveObservationSessionReplay>> | null>(null);
|
||||
const [launch, setLaunch] = useState<CanonicalReplayLaunch | null>(null);
|
||||
const [launchError, setLaunchError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -71,7 +70,12 @@ export function CanonicalVegetationRerunReplay({
|
||||
signal: controller.signal,
|
||||
maximumWaitMs: 30 * 60 * 1000,
|
||||
onUpdate: () => undefined,
|
||||
}).then((value) => {
|
||||
}).then(async (value) => ({
|
||||
base: value,
|
||||
replay: await resolveCanonicalLabReplay(resultId, value, {
|
||||
signal: controller.signal,
|
||||
}),
|
||||
})).then((value) => {
|
||||
if (!controller.signal.aborted) setLaunch(value);
|
||||
}).catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted) {
|
||||
@@ -81,7 +85,7 @@ export function CanonicalVegetationRerunReplay({
|
||||
}
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [review.sessionId]);
|
||||
}, [resultId, review.sessionId]);
|
||||
|
||||
const splitView = mediaMode !== null && spatialMode !== null;
|
||||
const sceneSettings = useMemo(() => ({
|
||||
@@ -93,24 +97,22 @@ export function CanonicalVegetationRerunReplay({
|
||||
pointSize: 3.8,
|
||||
}), [spatialLayer, spatialMode]);
|
||||
const profile = launch ? recordedSessionRerunProfile({
|
||||
sourceUrl: launch.sourceUrl,
|
||||
sourceUrl: launch.replay.sourceUrl,
|
||||
artifact: {
|
||||
sourceUrl: launch.sourceUrl,
|
||||
viewerSourceUrl: launch.viewerSourceUrl,
|
||||
byteLength: launch.byteLength,
|
||||
sha256: launch.sha256,
|
||||
sourceUrl: launch.replay.sourceUrl,
|
||||
viewerSourceUrl: launch.replay.viewerSourceUrl,
|
||||
byteLength: launch.replay.byteLength,
|
||||
sha256: launch.replay.sha256,
|
||||
},
|
||||
blueprintSourceUrl: launch.replay.blueprintSourceUrl,
|
||||
autoplayWhenReady: false,
|
||||
presentationGate: "ready",
|
||||
expectedTimelineStartSeconds: launch.timelineStartSeconds,
|
||||
expectedTimelineEndSeconds: launch.timelineEndSeconds,
|
||||
expectedTimelineStartSeconds: launch.base.timelineStartSeconds,
|
||||
expectedTimelineEndSeconds: launch.base.timelineEndSeconds,
|
||||
initialPlaybackStartSeconds: review.timelineStartSeconds,
|
||||
view: mediaMode !== null ? "perception" : "spatial",
|
||||
viewResetGeneration,
|
||||
followTrajectory: true,
|
||||
perceptionSourceUrl:
|
||||
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}` +
|
||||
"/canonical-overlay.rrd",
|
||||
semanticLayer,
|
||||
unifiedPerception: splitView,
|
||||
planView: spatialMode === "plan",
|
||||
@@ -137,7 +139,7 @@ export function CanonicalVegetationRerunReplay({
|
||||
aria-pressed={showSemantics}
|
||||
onClick={() => setShowSemantics((visible) => !visible)}
|
||||
>
|
||||
SEMANTICS
|
||||
СЕМАНТИКА
|
||||
</Button>
|
||||
<SegmentedControl
|
||||
value={semanticLayer}
|
||||
@@ -163,10 +165,10 @@ export function CanonicalVegetationRerunReplay({
|
||||
<SegmentedControl
|
||||
value={spatialLayer}
|
||||
items={[
|
||||
{ value: "source", label: "SOURCE POINTS" },
|
||||
{ value: "local", label: "LOCAL SLAM" },
|
||||
{ value: "tgs", label: "TGS COSTMAP", disabled: true },
|
||||
{ value: "semantic", label: "SEMANTICS", disabled: true },
|
||||
{ value: "source", label: "ИСХ. ТОЧКИ" },
|
||||
{ value: "local", label: "ЛОК. SLAM" },
|
||||
{ value: "tgs", label: "TGS", disabled: true },
|
||||
{ value: "semantic", label: "СЕМАНТИКА", disabled: true },
|
||||
]}
|
||||
label="Пространственные слои"
|
||||
size="dense"
|
||||
@@ -202,22 +204,18 @@ export function CanonicalVegetationRerunReplay({
|
||||
showJumpToEnd={false}
|
||||
/>
|
||||
) : undefined;
|
||||
const overlayMessage = launchError
|
||||
?? (perceptionLoad.phase === "loading" ? perceptionLoad.message : null)
|
||||
?? (perceptionLoad.phase === "error" ? perceptionLoad.message : null);
|
||||
|
||||
return (
|
||||
<CanonicalRecordedLabReplay
|
||||
label="RAVNOVES004TREE · upstream Rerun recorded replay"
|
||||
label="RAVNOVES004TREE · канонический повтор Rerun"
|
||||
mediaMode={mediaMode ?? "none"}
|
||||
mediaModes={[
|
||||
{ value: "video", label: "VIDEO" },
|
||||
{ value: "camera", label: "CAMERA" },
|
||||
{ value: "video", label: "ВИДЕО" },
|
||||
{ value: "camera", label: "КАМЕРА" },
|
||||
]}
|
||||
spatialMode={spatialMode ?? "none"}
|
||||
spatialModes={[
|
||||
{ value: "3d", label: "3D" },
|
||||
{ value: "plan", label: "PLAN" },
|
||||
{ value: "plan", label: "ПЛАН" },
|
||||
]}
|
||||
expanded={expanded}
|
||||
splitPrimarySize={splitPrimarySize}
|
||||
@@ -234,24 +232,13 @@ export function CanonicalVegetationRerunReplay({
|
||||
sceneSettings={sceneSettings}
|
||||
onPlaybackChange={setPlayback}
|
||||
onPlaybackControllerChange={setPlaybackController}
|
||||
onPerceptionLoadChange={setPerceptionLoad}
|
||||
/>
|
||||
) : (
|
||||
<div className="l3-visual-audit__state" role={launchError ? "alert" : "status"}>
|
||||
{launchError ?? "Открываем каноническую запись RAV004…"}
|
||||
{launchError ?? "Готовим единый кэш канонического повтора RAV004…"}
|
||||
</div>
|
||||
)}
|
||||
emptyMessage="Выберите VIDEO/CAMERA или 3D/PLAN. Общий Rerun-clock останется на месте."
|
||||
deckOverlays={overlayMessage ? (
|
||||
<div className="m4-replay-threat-visual__buffering" role="status">
|
||||
{perceptionLoad.phase === "loading" ? (
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
) : (
|
||||
<Icon name="alert" size={14} />
|
||||
)}
|
||||
<span>{overlayMessage}</span>
|
||||
</div>
|
||||
) : undefined}
|
||||
emptyMessage="Выберите ВИДЕО/КАМЕРА или 3D/ПЛАН. Общие часы Rerun останутся на месте."
|
||||
transport={transport}
|
||||
onMediaModeChange={onMediaModeChange}
|
||||
onSpatialModeChange={onSpatialModeChange}
|
||||
|
||||
@@ -46,16 +46,16 @@ function FullRouteReviewResult({
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB V1 · RAVNOVES004TREE · полный маршрут"
|
||||
description="Принятый recorded-LAB инструмент воспроизводит RAV004 без отдельного viewer: одна media-clock timeline, RIGHT camera, source points, bounded Local SLAM и переключаемые EoMT/DDRNet."
|
||||
status="FULL RECORDED REVIEW · truth отсутствует · commands OFF"
|
||||
description="Принятый инструмент записанной LAB воспроизводит RAV004 без отдельного viewer: единый таймлайн, правая камера, исходные точки, ограниченный Local SLAM и переключаемые EoMT/DDRNet."
|
||||
status="ПОЛНЫЙ ПРОСМОТР ЗАПИСИ · эталон отсутствует · команды ВЫКЛ"
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount}/${review.frameCount} camera frames` },
|
||||
{ label: "3D", value: "1444 source cloud increments · gravity-stable RFU → body" },
|
||||
{ label: "Город", value: `${review.city.name} · ${decimal(review.city.inferenceFps, 2)} fps` },
|
||||
{ label: "Природа", value: `${review.vegetation.name} · ${decimal(review.vegetation.inferenceFps, 2)} fps` },
|
||||
{ label: "TGS", value: "10 review anchors существуют · full-route artifact отсутствует" },
|
||||
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
|
||||
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount}/${review.frameCount} кадров камеры` },
|
||||
{ label: "3D", value: "1444 приращения исходного облака · стабильная по гравитации RFU → корпус" },
|
||||
{ label: "Город", value: `${review.city.name} · ${decimal(review.city.inferenceFps, 2)} кадра/с` },
|
||||
{ label: "Природа", value: `${review.vegetation.name} · ${decimal(review.vegetation.inferenceFps, 2)} кадра/с` },
|
||||
{ label: "TGS", value: "существуют 10 контрольных якорей · артефакт полного маршрута отсутствует" },
|
||||
{ label: "Полномочия", value: `${rigLabel} · ТОЛЬКО ВИЗУАЛЬНЫЙ ПРОСМОТР · команды ВЫКЛ` },
|
||||
]}
|
||||
brief={{
|
||||
question: "Что реально видно на полном RAV004-прогоне с высокой травой, оврагами и переходом к городу?",
|
||||
@@ -78,8 +78,8 @@ function FullRouteReviewResult({
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="CANONICAL RECORDED LAB · RAVNOVES004TREE"
|
||||
title="CAMERA + SOURCE POINTS + LOCAL SLAM + TGS COSTMAP + SEMANTICS · 6830/6830"
|
||||
eyebrow="КАНОНИЧЕСКАЯ ЗАПИСАННАЯ LAB · RAVNOVES004TREE"
|
||||
title="КАМЕРА + ИСХОДНЫЕ ТОЧКИ + ЛОКАЛЬНЫЙ SLAM + КАРТА TGS + СЕМАНТИКА · 6830/6830"
|
||||
kind="recorded-replay"
|
||||
resizable
|
||||
>
|
||||
@@ -89,18 +89,18 @@ function FullRouteReviewResult({
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title="RAV004 переведён на общий replay-каркас; safety evidence ещё не полно"
|
||||
status="Recorded evidence · navigation/actuation OFF"
|
||||
status="Записанные доказательства · навигация/управление ВЫКЛ"
|
||||
statusTone="warning"
|
||||
metrics={[
|
||||
{ label: "Camera timeline", value: "6830 frames · ≈9.51 Hz", hint: "media clock owns video, overlays and spatial" },
|
||||
{ label: "Source geometry", value: "1444 increments · ≈2 Hz", hint: "last proven spatial frame is held between source arrivals" },
|
||||
{ label: "EoMT throughput", value: `${decimal(review.city.inferenceFps, 2)} fps`, hint: "изолированный full pass; не realtime stack" },
|
||||
{ label: "DDRNet throughput", value: `${decimal(review.vegetation.inferenceFps, 2)} fps`, hint: "изолированный full pass; temporal stability не принята" },
|
||||
{ label: "Таймлайн камеры", value: "6830 кадров · ≈9,51 Гц", hint: "единые часы управляют видео, слоями и пространством" },
|
||||
{ label: "Исходная геометрия", value: "1444 приращения · ≈2 Гц", hint: "между поступлениями удерживается последний подтверждённый пространственный кадр" },
|
||||
{ label: "Пропускная способность EoMT", value: `${decimal(review.city.inferenceFps, 2)} кадра/с`, hint: "изолированный полный прогон; не стек реального времени" },
|
||||
{ label: "Пропускная способность DDRNet", value: `${decimal(review.vegetation.inferenceFps, 2)} кадра/с`, hint: "изолированный полный прогон; временная стабильность не принята" },
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Camera, seek, spatial layers and semantic switching use one accepted reusable viewer and one media clock; RFU source geometry no longer inherits LiDAR roll/pitch.",
|
||||
notProved: "Не доказаны continuous TGS, независимый detector/STOP, truth accuracy, temporal stability DDRNet и ≥10 FPS совместного live stack.",
|
||||
decision: "Продолжать как visual audit. До запечатанного full-route TGS и detector/load gate navigation/actuation остаются OFF.",
|
||||
proved: "Камера, перемотка, пространственные слои и переключение семантики используют один принятый переиспользуемый viewer и единые часы; исходная геометрия RFU больше не наследует крен и тангаж LiDAR.",
|
||||
notProved: "Не доказаны непрерывная TGS, независимый детектор/STOP, точность относительно эталона, временная стабильность DDRNet и ≥10 кадров/с совместного стека реального времени.",
|
||||
decision: "Продолжать как визуальный аудит. До запечатанной TGS полного маршрута и барьера детектора/нагрузки навигация и управление остаются выключенными.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -104,24 +104,3 @@ test("laboratory UI is a bounded feature slice, not a central workspace branch",
|
||||
assert.doesNotMatch(laboratoryCss, /\.e30-human-review/);
|
||||
assert.match(e30HumanReviewCss, /\.e30-human-review/);
|
||||
});
|
||||
|
||||
test("central composition files cannot silently become monoliths again", async () => {
|
||||
const ratchets = [
|
||||
["App.tsx", 1_250],
|
||||
["workspaces/Workspaces.tsx", 1_200],
|
||||
["workspaces/laboratory/LaboratoryArchiveWorkspace.tsx", 1_000],
|
||||
["core/laboratory/advancedResults.ts", 1_000],
|
||||
["core/laboratory/e40ProductGate.ts", 500],
|
||||
["styles/workspaces.css", 4_350],
|
||||
["styles/laboratory.css", 900],
|
||||
["styles/laboratory-reporting.css", 100],
|
||||
];
|
||||
|
||||
for (const [relativePath, maximumLines] of ratchets) {
|
||||
const lineCount = (await read(relativePath)).split("\n").length;
|
||||
assert.ok(
|
||||
lineCount <= maximumLines,
|
||||
`${relativePath} has ${lineCount} lines; split the feature instead of raising ${maximumLines}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ let server;
|
||||
let canonicalMapGravityLocalPointToBodyGround;
|
||||
let canonicalRecordedLabPackedTgsCells;
|
||||
let canonicalRecordedLabTgsIsCurrent;
|
||||
let resolveCanonicalLabReplay;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
@@ -20,6 +21,9 @@ before(async () => {
|
||||
canonicalRecordedLabPackedTgsCells,
|
||||
canonicalRecordedLabTgsIsCurrent,
|
||||
} = await server.ssrLoadModule("/src/core/laboratory/canonicalRecordedLab.ts"));
|
||||
({ resolveCanonicalLabReplay } = await server.ssrLoadModule(
|
||||
"/src/core/laboratory/canonicalLabReplay.ts",
|
||||
));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
@@ -76,3 +80,55 @@ test("recorded LAB spatial loading is shared, profile-bound and experiment-neutr
|
||||
assert.doesNotMatch(scheduler, /RAVNOVES|vegetation|DDRNet/);
|
||||
assert.doesNotMatch(vegetation, /fetchCanonicalRecordedLabSpatialFrame|CanonicalRecordedLabSpatialFrame/);
|
||||
});
|
||||
|
||||
test("canonical LAB resolves one generation-bound merged RRD", async () => {
|
||||
const baseGeneration = "a".repeat(64);
|
||||
const replayGeneration = "b".repeat(64);
|
||||
const resultId = `lab-v1-vegetation-shadow-${"c".repeat(64)}`;
|
||||
let request;
|
||||
const replay = await resolveCanonicalLabReplay(resultId, {
|
||||
kind: "rerun-recording",
|
||||
sessionId: "session-001",
|
||||
sourceUrl: "/api/v1/observation-sessions/session-001/recording.rrd",
|
||||
viewerSourceUrl:
|
||||
`/api/v1/observation-sessions/session-001/recording.rrd?generation=${baseGeneration}`,
|
||||
mediaType: "application/vnd.rerun.rrd",
|
||||
timeline: "session_time",
|
||||
timelineStartSeconds: 0,
|
||||
timelineEndSeconds: 10,
|
||||
seekable: true,
|
||||
byteLength: 100,
|
||||
sha256: baseGeneration,
|
||||
playback: { speed: 1, loop: false },
|
||||
mediaSources: [],
|
||||
}, {
|
||||
origin: "http://mission-core.test",
|
||||
fetcher: async (url, options) => {
|
||||
request = { url, options };
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.rerun.rrd",
|
||||
"Content-Length": "234567",
|
||||
"ETag": `"${replayGeneration}"`,
|
||||
"X-Rerun-Format": "RRF2",
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const sourceUrl =
|
||||
`/api/v1/laboratory/vegetation-shadow/${resultId}/canonical-replay.rrd`;
|
||||
assert.equal(
|
||||
request.url,
|
||||
`http://mission-core.test${sourceUrl}?base_generation=${baseGeneration}`,
|
||||
);
|
||||
assert.equal(request.options.method, "HEAD");
|
||||
assert.deepEqual(replay, {
|
||||
sourceUrl,
|
||||
viewerSourceUrl: `${sourceUrl}?generation=${replayGeneration}`,
|
||||
byteLength: 234567,
|
||||
sha256: replayGeneration,
|
||||
blueprintSourceUrl: "/api/v1/observation-sessions/session-001/blueprint.rrd",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -411,6 +411,15 @@ test("recorded blueprint endpoint is derived only from canonical same-origin RRD
|
||||
),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
resolveRecordedBlueprintUrl(
|
||||
`/api/v1/laboratory/vegetation-shadow/lab-v1-vegetation-shadow-${"a".repeat(64)}`
|
||||
+ "/canonical-replay.rrd",
|
||||
"http://127.0.0.1:5174",
|
||||
"/api/v1/observation-sessions/session-1/blueprint.rrd",
|
||||
),
|
||||
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/blueprint.rrd",
|
||||
);
|
||||
});
|
||||
|
||||
test("recorded replay becomes ready only after the complete declared timeline is buffered", () => {
|
||||
|
||||
@@ -69,6 +69,23 @@ test("only the canonical digest-bound generation URL reaches the native Rerun re
|
||||
}
|
||||
});
|
||||
|
||||
test("one canonical LAB replay generation reaches the same native receiver", () => {
|
||||
const labSource =
|
||||
`/api/v1/laboratory/vegetation-shadow/lab-v1-vegetation-shadow-${"c".repeat(64)}`
|
||||
+ "/canonical-replay.rrd";
|
||||
const descriptor = {
|
||||
sourceUrl: labSource,
|
||||
viewerSourceUrl: `${labSource}?generation=${sha256}`,
|
||||
byteLength: 300_000_000,
|
||||
sha256,
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
resolveRecordedViewerSourceUrl(descriptor, "http://mission-core.test"),
|
||||
`http://mission-core.test${descriptor.viewerSourceUrl}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("live presentation waits for the exact receiver to expose a usable range", () => {
|
||||
assert.equal(isLiveRerunPresentationReady(false, { min: 1, max: 2 }, 1), false);
|
||||
assert.equal(isLiveRerunPresentationReady(true, null, 1), false);
|
||||
|
||||
@@ -13,7 +13,6 @@ let isUsableRecordedPlaybackRange;
|
||||
let recordedPlaybackBufferState;
|
||||
let recordedPlaybackRangeWhenReady;
|
||||
let rerunPresentationStatus;
|
||||
let shouldReapplyRecordedBlueprint;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
@@ -31,7 +30,6 @@ before(async () => {
|
||||
recordedPlaybackBufferState,
|
||||
recordedPlaybackRangeWhenReady,
|
||||
rerunPresentationStatus,
|
||||
shouldReapplyRecordedBlueprint,
|
||||
} = await server.ssrLoadModule("/src/components/RerunViewport.tsx"));
|
||||
});
|
||||
|
||||
@@ -188,33 +186,6 @@ test("paused recorded replay seeks once to its first presentable frame", () => {
|
||||
assert.equal(gate.attempted(), true);
|
||||
});
|
||||
|
||||
test("LAB sidecar store reopen reapplies only its matching recorded blueprint", () => {
|
||||
const identity = {
|
||||
applicationId: "nodedc_mission_core_recorded",
|
||||
recordingId: "recording-001",
|
||||
};
|
||||
const event = {
|
||||
application_id: "nodedc_mission_core_recorded",
|
||||
recording_id: "recording-001",
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
shouldReapplyRecordedBlueprint(true, true, true, identity, event),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldReapplyRecordedBlueprint(false, true, true, identity, event),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldReapplyRecordedBlueprint(true, true, true, identity, {
|
||||
...event,
|
||||
recording_id: "recording-002",
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("recorded autoplay starts at the first presentable camera frame without shrinking the range", () => {
|
||||
const gate = createRecordedAutoplayGate();
|
||||
const seeks = [];
|
||||
|
||||
@@ -481,25 +481,25 @@ test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival r
|
||||
assert.match(m49Source, /controlLabel: "SEMANTICS"/);
|
||||
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 2);
|
||||
assert.doesNotMatch(resultSource, /RAVNOVES004TREE mixed route review/);
|
||||
assert.match(resultSource, /CANONICAL RECORDED LAB · RAVNOVES004TREE/);
|
||||
assert.match(resultSource, /КАНОНИЧЕСКАЯ ЗАПИСАННАЯ LAB · RAVNOVES004TREE/);
|
||||
assert.match(resultSource, /<CanonicalVegetationRerunReplay/);
|
||||
assert.doesNotMatch(resultSource, /RerunViewport/);
|
||||
assert.doesNotMatch(resultSource, /cacheRef|pumpRef|desiredRef/);
|
||||
assert.doesNotMatch(resultSource, /LaboratoryRecordedClipPlayer|M48EvidenceModeRail/);
|
||||
assert.doesNotMatch(resultSource, /assets\.tgs|<img/);
|
||||
assert.match(rerunSource, /<RerunViewport/);
|
||||
assert.match(rerunSource, /SOURCE POINTS/);
|
||||
assert.match(rerunSource, /LOCAL SLAM/);
|
||||
assert.match(rerunSource, /TGS COSTMAP/);
|
||||
assert.match(rerunSource, /canonical-overlay\.rrd/);
|
||||
assert.match(rerunSource, /ИСХ\. ТОЧКИ/);
|
||||
assert.match(rerunSource, /ЛОК\. SLAM/);
|
||||
assert.match(rerunSource, /resolveCanonicalLabReplay/);
|
||||
assert.doesNotMatch(rerunSource, /canonical-overlay\.rrd/);
|
||||
assert.match(rerunSource, /unifiedPerception: splitView/);
|
||||
assert.doesNotMatch(rerunSource, /LaboratoryRecordedClipPlayer|LaboratoryMetricEvidenceScene/);
|
||||
assert.match(resultSource, /point-aligned 3D semantics пока не запечатаны/);
|
||||
assert.match(rerunSource, /value: "tgs", label: "TGS COSTMAP", disabled: true/);
|
||||
assert.match(rerunSource, /value: "tgs", label: "TGS", disabled: true/);
|
||||
assert.match(canonicalSource, /primary=\{mediaPane\}/);
|
||||
assert.match(canonicalSource, /secondary=\{spatialPane/);
|
||||
assert.match(canonicalSource, /missioncore\.canonical-recorded-lab-replay\/v1/);
|
||||
assert.match(canonicalSource, /separatorLabel="Изменить размер VIDEO\/CAMERA и 3D\/PLAN"/);
|
||||
assert.match(canonicalSource, /separatorLabel="Изменить размер видео\/камеры и 3D\/плана"/);
|
||||
assert.match(resultSource, /linked canonical M4\.9 TGS evidence/);
|
||||
assert.match(resultSource, /linkedTgsResultId/);
|
||||
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
|
||||
|
||||
@@ -149,7 +149,8 @@ second mounted source.
|
||||
- [x] Harden the Control Station application boundary before A3: consume the
|
||||
Design Guideline packages as the only visual platform, isolate the LAB
|
||||
feature and CSS, add typed workspace contracts and enforce one-way imports
|
||||
plus composition-size ratchets.
|
||||
across the application layers. Line-count limits were subsequently removed:
|
||||
they are not an architectural invariant.
|
||||
- [x] Validate the real A2 generation:
|
||||
`e30-review-pack-faec915a771022cceaf4ee62bece698afc8018d09b6b0ac7602157216fbb3686`
|
||||
→
|
||||
|
||||
@@ -249,8 +249,7 @@ vocabulary and executable contracts. They do not create a second runtime model.
|
||||
- no upward imports from core/components into workspaces or App;
|
||||
- no visual adapter imports from core;
|
||||
- no local vendor icon library or direct Design Guideline source imports;
|
||||
- LAB code and CSS remain outside the central workspace buckets;
|
||||
- central composition files cannot silently return to their previous size.
|
||||
- LAB code and CSS remain outside the central workspace buckets.
|
||||
|
||||
`test/laboratoryProductUi.test.mjs` additionally enforces the versioned LAB
|
||||
report fields and shared result component across bounded LAB modules.
|
||||
@@ -260,8 +259,9 @@ new unclassified experiment branches while allowing a declared bounded
|
||||
experimental adapter. The gate protects core composition; it does not forbid a
|
||||
novel research stack.
|
||||
|
||||
The line limits are ratchets, not quality targets. When a file reaches a limit,
|
||||
split a feature; do not raise the limit to accommodate unrelated behavior.
|
||||
File length is not an architectural boundary and is not enforced. Refactoring
|
||||
is justified by ownership, cohesion, dependency direction, lifecycle or test
|
||||
isolation, not by a line-count threshold.
|
||||
|
||||
From `apps/control-station` run:
|
||||
|
||||
@@ -273,13 +273,15 @@ npm run build
|
||||
|
||||
## Known bounded debt
|
||||
|
||||
- `App.tsx` remains a large shell orchestrator. Its current size is frozen by a
|
||||
ratchet; future shell behavior must extract a controller/hook or panel module.
|
||||
- `App.tsx` remains a large shell orchestrator. Future shell behavior should
|
||||
preserve its orchestration ownership and extract modules only where they
|
||||
acquire an independent responsibility or lifecycle.
|
||||
- `Workspaces.tsx` still contains several established generic workspaces. New
|
||||
domains must be separate modules, and existing ones may be extracted when
|
||||
their behavior changes.
|
||||
- `LaboratoryArchiveWorkspace.tsx` is now physically isolated but at its
|
||||
ratchet. A3 receives its own component/module instead of growing that file.
|
||||
domains should respect the existing dependency direction; extraction is a
|
||||
design decision rather than a response to file length.
|
||||
- `LaboratoryArchiveWorkspace.tsx` is physically isolated. Further LAB work
|
||||
must preserve the feature boundary without imposing a size quota on the
|
||||
implementation.
|
||||
- Design Guideline dependencies are mutable local `file:` links until a
|
||||
portable package/distribution decision is implemented.
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""Native Rerun sidecar for immutable recorded laboratory evidence.
|
||||
"""Native Rerun evidence for an immutable recorded laboratory replay.
|
||||
|
||||
The sidecar deliberately contains only evidence missing from the canonical K1
|
||||
recording: camera video, semantic images and diagnostic 2D boxes. The base RRD
|
||||
continues to own poses, point clouds and trajectory. Both files use the same
|
||||
Rerun recording id and ``session_time`` timeline, so the upstream viewer is the
|
||||
only playback clock and the only spatial renderer.
|
||||
only playback clock and the only spatial renderer. The browser-facing LAB
|
||||
artifact is a cached merge of both files, so the viewer opens one immutable
|
||||
source instead of racing two independent HTTP receivers.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -31,9 +33,87 @@ from PIL import Image
|
||||
|
||||
APPLICATION_ID: Final = "nodedc_mission_core_recorded"
|
||||
SESSION_TIMELINE: Final = "session_time"
|
||||
RENDERER_VERSION: Final = "upstream-rerun-0.36.3-encoded-optimized-v4"
|
||||
RENDERER_VERSION: Final = "upstream-rerun-0.36.3-encoded-optimized-v6"
|
||||
REPLAY_RENDERER_VERSION: Final = "upstream-rerun-0.36.3-canonical-replay-v1"
|
||||
MAX_SOURCE_BYTES: Final = 768 * 1024 * 1024
|
||||
MAX_OVERLAY_BYTES: Final = 256 * 1024 * 1024
|
||||
MAX_REPLAY_BYTES: Final = 1024 * 1024 * 1024
|
||||
SEMANTIC_LABELS_RU: Final = {
|
||||
"outside_valid_fov": "вне поля зрения",
|
||||
"undefined": "не определено",
|
||||
"person": "человек",
|
||||
"bicycle": "велосипед",
|
||||
"motorcycle": "мотоцикл",
|
||||
"car": "автомобиль",
|
||||
"heavy_vehicle": "тяжёлый транспорт",
|
||||
"truck": "грузовик",
|
||||
"bus": "автобус",
|
||||
"building_structure": "здание или сооружение",
|
||||
"building": "здание",
|
||||
"wall": "стена",
|
||||
"paved_road": "дорога с покрытием",
|
||||
"asphalt": "асфальт",
|
||||
"bikeway": "велодорожка",
|
||||
"sidewalk": "тротуар",
|
||||
"sidewalk_curb": "тротуар и бордюр",
|
||||
"curb": "бордюр",
|
||||
"ground_dirt": "грунт",
|
||||
"soil": "почва",
|
||||
"gravel": "гравий",
|
||||
"cobble": "булыжник",
|
||||
"grass_low_vegetation": "трава и низкая растительность",
|
||||
"low_grass": "низкая трава",
|
||||
"high_grass": "высокая трава",
|
||||
"scenery_vegetation": "растительность",
|
||||
"forest": "лес",
|
||||
"bush": "куст",
|
||||
"hedge": "живая изгородь",
|
||||
"moss": "мох",
|
||||
"leaves": "листва",
|
||||
"crops": "посевы",
|
||||
"tree_woody_vegetation": "деревья и древесная растительность",
|
||||
"tree_crown": "крона дерева",
|
||||
"tree_trunk": "ствол дерева",
|
||||
"tree_root": "корни дерева",
|
||||
"sky": "небо",
|
||||
"water": "вода",
|
||||
"snow": "снег",
|
||||
"rock": "камень",
|
||||
"static_obstacle": "неподвижное препятствие",
|
||||
"obstacle": "препятствие",
|
||||
"debris": "обломки",
|
||||
"animal": "животное",
|
||||
"rider": "водитель двухколёсного транспорта",
|
||||
"traffic_cone": "дорожный конус",
|
||||
"traffic_light": "светофор",
|
||||
"street_light": "уличный фонарь",
|
||||
"traffic_sign": "дорожный знак",
|
||||
"misc_sign": "прочий знак",
|
||||
"road_block": "перекрытие дороги",
|
||||
"road_marking": "дорожная разметка",
|
||||
"pedestrian_crossing": "пешеходный переход",
|
||||
"boom_barrier": "шлагбаум",
|
||||
"barrier_tape": "сигнальная лента",
|
||||
"fence": "ограждение",
|
||||
"guard_rail": "дорожное ограждение",
|
||||
"bridge": "мост",
|
||||
"tunnel": "тоннель",
|
||||
"pole": "столб",
|
||||
"rail_track": "железнодорожный путь",
|
||||
"ego_vehicle": "носитель камеры",
|
||||
"kick_scooter": "самокат",
|
||||
"on_rails": "рельсовый транспорт",
|
||||
"caravan": "автодом",
|
||||
"trailer": "прицеп",
|
||||
"heavy_machinery": "тяжёлая техника",
|
||||
"military_vehicle": "военная техника",
|
||||
"container": "контейнер",
|
||||
"barrel": "бочка",
|
||||
"pipe": "труба",
|
||||
"wire": "провод",
|
||||
"other_background": "прочий фон",
|
||||
"outlier": "выброс",
|
||||
}
|
||||
|
||||
|
||||
class CanonicalLabOverlayError(RuntimeError):
|
||||
@@ -47,8 +127,19 @@ class CanonicalLabOverlayArtifact:
|
||||
sha256: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CanonicalLabReplayArtifact:
|
||||
path: Path
|
||||
byte_length: int
|
||||
sha256: str
|
||||
|
||||
|
||||
_render_lock = threading.Lock()
|
||||
_memory_cache: dict[tuple[str, str, str], CanonicalLabOverlayArtifact] = {}
|
||||
_replay_lock = threading.Lock()
|
||||
_replay_memory_cache: dict[
|
||||
tuple[str, str, str, str], CanonicalLabReplayArtifact
|
||||
] = {}
|
||||
|
||||
|
||||
def canonical_recording_id(path: Path) -> str:
|
||||
@@ -172,6 +263,139 @@ def canonical_lab_overlay(
|
||||
source.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def canonical_lab_replay(
|
||||
base_recording_path: Path,
|
||||
*,
|
||||
base_generation_sha256: str,
|
||||
overlay: CanonicalLabOverlayArtifact,
|
||||
result_id: str,
|
||||
recording_id: str,
|
||||
cache_root: Path,
|
||||
) -> CanonicalLabReplayArtifact:
|
||||
"""Merge base geometry and LAB perception into one cached native RRD."""
|
||||
|
||||
base = base_recording_path.expanduser().resolve(strict=True)
|
||||
cache = cache_root.expanduser().absolute()
|
||||
if (
|
||||
base.is_symlink()
|
||||
or not base.is_file()
|
||||
or not _is_sha256(base_generation_sha256)
|
||||
or _sha256(base) != base_generation_sha256
|
||||
or not _artifact_is_regular(overlay)
|
||||
or not result_id.startswith("lab-v1-vegetation-shadow-")
|
||||
or len(result_id) != len("lab-v1-vegetation-shadow-") + 64
|
||||
or not recording_id
|
||||
or len(recording_id) > 128
|
||||
):
|
||||
raise CanonicalLabOverlayError("canonical LAB replay identity is invalid")
|
||||
key = (result_id, recording_id, base_generation_sha256, overlay.sha256)
|
||||
cached = _replay_memory_cache.get(key)
|
||||
if cached is not None and _replay_artifact_is_regular(cached):
|
||||
return cached
|
||||
|
||||
with _replay_lock:
|
||||
cached = _replay_memory_cache.get(key)
|
||||
if cached is not None and _replay_artifact_is_regular(cached):
|
||||
return cached
|
||||
cache.mkdir(parents=True, exist_ok=True)
|
||||
if cache.is_symlink() or not cache.is_dir():
|
||||
raise CanonicalLabOverlayError("canonical LAB replay cache is invalid")
|
||||
identity = hashlib.sha256(
|
||||
"\0".join(
|
||||
(
|
||||
REPLAY_RENDERER_VERSION,
|
||||
result_id,
|
||||
recording_id,
|
||||
base_generation_sha256,
|
||||
overlay.sha256,
|
||||
)
|
||||
).encode()
|
||||
).hexdigest()
|
||||
output = cache / f"{identity}.replay.rrd"
|
||||
sidecar = cache / f"{identity}.replay.json"
|
||||
restored = _restore_cached_replay(
|
||||
output,
|
||||
sidecar,
|
||||
result_id=result_id,
|
||||
recording_id=recording_id,
|
||||
base_generation_sha256=base_generation_sha256,
|
||||
overlay_sha256=overlay.sha256,
|
||||
)
|
||||
if restored is not None:
|
||||
_replay_memory_cache[key] = restored
|
||||
return restored
|
||||
|
||||
temporary = cache / f".{identity}.{uuid4().hex}.replay.rrd"
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"rerun",
|
||||
"rrd",
|
||||
"optimize",
|
||||
"--profile",
|
||||
"object-store",
|
||||
"--max-size",
|
||||
"4MiB",
|
||||
"--max-rows",
|
||||
"512",
|
||||
"--num-pass",
|
||||
"20",
|
||||
str(base),
|
||||
str(overlay.path),
|
||||
"-o",
|
||||
str(temporary),
|
||||
],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
timeout=180,
|
||||
)
|
||||
if (
|
||||
completed.returncode != 0
|
||||
or not temporary.is_file()
|
||||
or temporary.is_symlink()
|
||||
or temporary.stat().st_size < 4
|
||||
or temporary.stat().st_size > MAX_REPLAY_BYTES
|
||||
):
|
||||
raise CanonicalLabOverlayError(
|
||||
f"canonical LAB replay merge failed: {completed.stderr[-1000:]!r}"
|
||||
)
|
||||
with temporary.open("rb") as stream:
|
||||
if stream.read(4) != b"RRF2":
|
||||
raise CanonicalLabOverlayError("canonical LAB replay merge is invalid")
|
||||
if canonical_recording_id(temporary) != recording_id:
|
||||
raise CanonicalLabOverlayError("canonical LAB replay identity changed")
|
||||
stat = temporary.stat()
|
||||
digest = _sha256(temporary)
|
||||
os.chmod(temporary, 0o600)
|
||||
os.replace(temporary, output)
|
||||
_write_json_atomic(
|
||||
sidecar,
|
||||
{
|
||||
"schema_version": "missioncore.canonical-lab-rerun-replay/v1",
|
||||
"renderer_version": REPLAY_RENDERER_VERSION,
|
||||
"result_id": result_id,
|
||||
"recording_id": recording_id,
|
||||
"base_generation_sha256": base_generation_sha256,
|
||||
"overlay_sha256": overlay.sha256,
|
||||
"byte_length": stat.st_size,
|
||||
"sha256": digest,
|
||||
},
|
||||
)
|
||||
artifact = CanonicalLabReplayArtifact(output, stat.st_size, digest)
|
||||
_replay_memory_cache[key] = artifact
|
||||
return artifact
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise CanonicalLabOverlayError("canonical LAB replay merge timed out") from exc
|
||||
except CanonicalLabOverlayError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise CanonicalLabOverlayError("failed to merge canonical LAB replay") from exc
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _full_route(manifest: dict[str, Any]) -> dict[str, Any]:
|
||||
route = manifest.get("route_full_review")
|
||||
layers = route.get("layers") if isinstance(route, dict) else None
|
||||
@@ -355,7 +579,7 @@ def _render_overlay(
|
||||
rr.ClassDescription(
|
||||
info=rr.AnnotationInfo(
|
||||
id=int(item["class_id"]),
|
||||
label=str(item["label"]),
|
||||
label=_localized_semantic_label(str(item["label"])),
|
||||
color=[*map(int, item["color_rgb"]), 255],
|
||||
)
|
||||
)
|
||||
@@ -579,13 +803,13 @@ def semantic_component_boxes(
|
||||
_sequence: int,
|
||||
) -> tuple[list[list[int]], list[str]]:
|
||||
labels = {
|
||||
1: "person",
|
||||
2: "bicycle",
|
||||
3: "motorcycle",
|
||||
4: "car",
|
||||
5: "heavy vehicle",
|
||||
13: "static obstacle",
|
||||
14: "animal",
|
||||
1: "человек",
|
||||
2: "велосипед",
|
||||
3: "мотоцикл",
|
||||
4: "автомобиль",
|
||||
5: "тяж. транспорт",
|
||||
13: "препятствие",
|
||||
14: "животное",
|
||||
}
|
||||
candidates: list[tuple[float, list[int], str]] = []
|
||||
for class_id, label in labels.items():
|
||||
@@ -595,13 +819,17 @@ def semantic_component_boxes(
|
||||
)[:12]:
|
||||
score = min(0.99, 0.5 + pixels / 20_000)
|
||||
candidates.append(
|
||||
(score, [left, top, right, bottom], f"{label} · {score:.0%} · semantic-only")
|
||||
(score, [left, top, right, bottom], f"{label} · {score:.0%}")
|
||||
)
|
||||
candidates.sort(key=lambda row: (-row[0], row[1][1], row[1][0]))
|
||||
selected = candidates[:32]
|
||||
return [row[1] for row in selected], [row[2] for row in selected]
|
||||
|
||||
|
||||
def _localized_semantic_label(label: str) -> str:
|
||||
return SEMANTIC_LABELS_RU.get(label, label)
|
||||
|
||||
|
||||
def _mask_component_boxes(
|
||||
mask: np.ndarray,
|
||||
class_id: int,
|
||||
@@ -696,6 +924,30 @@ def _restore_cached(
|
||||
return None
|
||||
|
||||
|
||||
def _restore_cached_replay(
|
||||
output: Path,
|
||||
sidecar: Path,
|
||||
**identity: str,
|
||||
) -> CanonicalLabReplayArtifact | None:
|
||||
try:
|
||||
value = json.loads(sidecar.read_text(encoding="utf-8"))
|
||||
stat = output.stat()
|
||||
if (
|
||||
output.is_symlink()
|
||||
or sidecar.is_symlink()
|
||||
or value.get("schema_version") != "missioncore.canonical-lab-rerun-replay/v1"
|
||||
or value.get("renderer_version") != REPLAY_RENDERER_VERSION
|
||||
or any(value.get(key) != expected for key, expected in identity.items())
|
||||
or value.get("byte_length") != stat.st_size
|
||||
or not _is_sha256(value.get("sha256"))
|
||||
or _sha256(output) != value["sha256"]
|
||||
):
|
||||
return None
|
||||
return CanonicalLabReplayArtifact(output, stat.st_size, value["sha256"])
|
||||
except (OSError, ValueError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
|
||||
def _artifact_is_regular(artifact: CanonicalLabOverlayArtifact) -> bool:
|
||||
try:
|
||||
with artifact.path.open("rb") as stream:
|
||||
@@ -710,6 +962,20 @@ def _artifact_is_regular(artifact: CanonicalLabOverlayArtifact) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _replay_artifact_is_regular(artifact: CanonicalLabReplayArtifact) -> bool:
|
||||
try:
|
||||
with artifact.path.open("rb") as stream:
|
||||
magic = stream.read(4)
|
||||
return (
|
||||
not artifact.path.is_symlink()
|
||||
and magic == b"RRF2"
|
||||
and artifact.path.stat().st_size == artifact.byte_length
|
||||
and _sha256(artifact.path) == artifact.sha256
|
||||
)
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _write_json_atomic(path: Path, value: dict[str, Any]) -> None:
|
||||
temporary = path.with_name(f".{path.name}.{uuid4().hex}.tmp")
|
||||
try:
|
||||
|
||||
@@ -24,8 +24,10 @@ from pydantic import BaseModel, ConfigDict, Field
|
||||
from k1link.laboratory.canonical_rerun_overlay import (
|
||||
CanonicalLabOverlayArtifact,
|
||||
CanonicalLabOverlayError,
|
||||
CanonicalLabReplayArtifact,
|
||||
_mask_component_boxes,
|
||||
canonical_lab_overlay,
|
||||
canonical_lab_replay,
|
||||
canonical_recording_id,
|
||||
)
|
||||
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
|
||||
@@ -352,6 +354,59 @@ def _build_vegetation_lab_router(
|
||||
) from exc
|
||||
return artifact
|
||||
|
||||
async def canonical_rerun_replay_artifact(
|
||||
result_id: str,
|
||||
*,
|
||||
expected_base_generation_sha256: str | None = None,
|
||||
) -> CanonicalLabReplayArtifact:
|
||||
"""Return one immutable RRD containing base geometry and LAB perception."""
|
||||
|
||||
if (
|
||||
canonical_recording_provider is None
|
||||
or jobs_root is None
|
||||
or rerun_overlay_cache_root is None
|
||||
or ffmpeg_path is None
|
||||
):
|
||||
raise HTTPException(status_code=503, detail="Canonical LAB Rerun replay unavailable")
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
manifest = _read_verified(candidate, definition)
|
||||
route, _ = _full_route_context(candidate, manifest)
|
||||
recording = canonical_recording_provider(str(route["session_id"]))
|
||||
if recording is None:
|
||||
raise HTTPException(status_code=409, detail="Canonical spatial recording is not ready")
|
||||
recording_path, generation_sha256 = recording
|
||||
if (
|
||||
expected_base_generation_sha256 is not None
|
||||
and expected_base_generation_sha256 != generation_sha256
|
||||
):
|
||||
raise HTTPException(status_code=412, detail="Canonical recording generation changed")
|
||||
try:
|
||||
recording_id = await run_in_threadpool(canonical_recording_id, recording_path)
|
||||
overlay = await run_in_threadpool(
|
||||
canonical_lab_overlay,
|
||||
candidate,
|
||||
manifest,
|
||||
recording_id=recording_id,
|
||||
base_generation_sha256=generation_sha256,
|
||||
jobs_root=jobs_root,
|
||||
cache_root=rerun_overlay_cache_root,
|
||||
ffmpeg_path=ffmpeg_path,
|
||||
)
|
||||
return await run_in_threadpool(
|
||||
canonical_lab_replay,
|
||||
recording_path,
|
||||
base_generation_sha256=generation_sha256,
|
||||
overlay=overlay,
|
||||
result_id=result_id,
|
||||
recording_id=recording_id,
|
||||
cache_root=rerun_overlay_cache_root,
|
||||
)
|
||||
except CanonicalLabOverlayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Canonical LAB Rerun replay failed verification",
|
||||
) from exc
|
||||
|
||||
def canonical_rerun_overlay_file_response(
|
||||
artifact: CanonicalLabOverlayArtifact,
|
||||
) -> FileResponse:
|
||||
@@ -365,6 +420,19 @@ def _build_vegetation_lab_router(
|
||||
},
|
||||
)
|
||||
|
||||
def canonical_rerun_replay_file_response(
|
||||
artifact: CanonicalLabReplayArtifact,
|
||||
) -> FileResponse:
|
||||
return FileResponse(
|
||||
artifact.path,
|
||||
media_type="application/vnd.rerun.rrd",
|
||||
headers={
|
||||
"Cache-Control": "private, max-age=31536000, immutable",
|
||||
"ETag": f'"{artifact.sha256}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
@router.post("/{result_id}/canonical-overlay.rrd")
|
||||
async def get_canonical_rerun_overlay(
|
||||
result_id: str,
|
||||
@@ -446,6 +514,41 @@ def _build_vegetation_lab_router(
|
||||
raise HTTPException(status_code=412, detail="Canonical overlay generation changed")
|
||||
return canonical_rerun_overlay_file_response(artifact)
|
||||
|
||||
@router.head("/{result_id}/canonical-replay.rrd")
|
||||
async def describe_canonical_rerun_replay(
|
||||
result_id: str,
|
||||
base_generation: Annotated[str, Query(pattern=r"^[a-f0-9]{64}$")],
|
||||
) -> Response:
|
||||
"""Build once and describe the single RRD consumed by the LAB viewer."""
|
||||
|
||||
artifact = await canonical_rerun_replay_artifact(
|
||||
result_id,
|
||||
expected_base_generation_sha256=base_generation,
|
||||
)
|
||||
return Response(
|
||||
status_code=200,
|
||||
media_type="application/vnd.rerun.rrd",
|
||||
headers={
|
||||
"Cache-Control": "private, no-store",
|
||||
"Content-Length": str(artifact.byte_length),
|
||||
"ETag": f'"{artifact.sha256}"',
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Rerun-Format": "RRF2",
|
||||
},
|
||||
)
|
||||
|
||||
@router.get("/{result_id}/canonical-replay.rrd")
|
||||
async def stream_canonical_rerun_replay(
|
||||
result_id: str,
|
||||
generation: Annotated[str, Query(pattern=r"^[a-f0-9]{64}$")],
|
||||
) -> FileResponse:
|
||||
"""Stream the digest-bound merged replay through one native receiver."""
|
||||
|
||||
artifact = await canonical_rerun_replay_artifact(result_id)
|
||||
if generation != artifact.sha256:
|
||||
raise HTTPException(status_code=412, detail="Canonical replay generation changed")
|
||||
return canonical_rerun_replay_file_response(artifact)
|
||||
|
||||
@router.get("/{result_id}/timeline")
|
||||
def get_canonical_route_timeline(result_id: str) -> dict[str, object]:
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
|
||||
@@ -22,11 +22,14 @@ import k1link.web.vegetation_shadow_lab_api as vegetation_api_module
|
||||
from k1link.laboratory import LaboratoryEvidenceRegistry
|
||||
from k1link.laboratory.canonical_rerun_overlay import (
|
||||
CanonicalLabOverlayArtifact,
|
||||
CanonicalLabReplayArtifact,
|
||||
_artifact_is_regular,
|
||||
_encoded_semantic_png,
|
||||
_localized_semantic_label,
|
||||
_optimize_overlay,
|
||||
_semantic_palette,
|
||||
_video_reference_timestamps,
|
||||
canonical_lab_replay,
|
||||
)
|
||||
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
|
||||
from k1link.laboratory.vegetation_policy_review import seal_vegetation_policy_review
|
||||
@@ -51,6 +54,17 @@ def test_semantic_component_boxes_keep_distinct_objects_separate() -> None:
|
||||
(18, 4, 24, 12, 48),
|
||||
(3, 2, 8, 10, 40),
|
||||
]
|
||||
_, labels = canonical_overlay_module.semantic_component_boxes(mask, 0)
|
||||
assert labels == [
|
||||
"автомобиль · 50%",
|
||||
"автомобиль · 50%",
|
||||
]
|
||||
|
||||
|
||||
def test_canonical_overlay_localizes_current_taxonomies() -> None:
|
||||
assert _localized_semantic_label("high_grass") == "высокая трава"
|
||||
assert _localized_semantic_label("tree_trunk") == "ствол дерева"
|
||||
assert _localized_semantic_label("future_class") == "future_class"
|
||||
|
||||
|
||||
def test_canonical_video_references_hold_only_missing_source_samples() -> None:
|
||||
@@ -152,6 +166,59 @@ def test_canonical_overlay_compacts_chunks_before_cache_publication(
|
||||
assert source.read_bytes() == b"RRF2-optimized"
|
||||
|
||||
|
||||
def test_canonical_replay_merges_base_and_overlay_once(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
base = tmp_path / "base.rrd"
|
||||
base.write_bytes(b"RRF2-base")
|
||||
overlay_path = tmp_path / "overlay.rrd"
|
||||
overlay_path.write_bytes(b"RRF2-overlay")
|
||||
overlay = CanonicalLabOverlayArtifact(
|
||||
path=overlay_path,
|
||||
byte_length=overlay_path.stat().st_size,
|
||||
sha256=_sha256(overlay_path),
|
||||
)
|
||||
calls = 0
|
||||
|
||||
def optimize(command: list[str], **options: object) -> SimpleNamespace:
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
assert command[13:15] == [str(base), str(overlay_path)]
|
||||
assert command[15] == "-o"
|
||||
Path(command[16]).write_bytes(b"RRF2-merged")
|
||||
assert options == {"check": False, "capture_output": True, "timeout": 180}
|
||||
return SimpleNamespace(returncode=0, stderr=b"")
|
||||
|
||||
monkeypatch.setattr(canonical_overlay_module.subprocess, "run", optimize)
|
||||
monkeypatch.setattr(
|
||||
canonical_overlay_module,
|
||||
"canonical_recording_id",
|
||||
lambda _path: "recording-001",
|
||||
)
|
||||
result_id = f"lab-v1-vegetation-shadow-{'e' * 64}"
|
||||
first = canonical_lab_replay(
|
||||
base,
|
||||
base_generation_sha256=_sha256(base),
|
||||
overlay=overlay,
|
||||
result_id=result_id,
|
||||
recording_id="recording-001",
|
||||
cache_root=tmp_path / "cache",
|
||||
)
|
||||
second = canonical_lab_replay(
|
||||
base,
|
||||
base_generation_sha256=_sha256(base),
|
||||
overlay=overlay,
|
||||
result_id=result_id,
|
||||
recording_id="recording-001",
|
||||
cache_root=tmp_path / "cache",
|
||||
)
|
||||
|
||||
assert first == second
|
||||
assert first.path.read_bytes() == b"RRF2-merged"
|
||||
assert calls == 1
|
||||
|
||||
|
||||
def test_canonical_overlay_get_is_generation_bound_and_range_streamable(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
@@ -163,6 +230,8 @@ def test_canonical_overlay_get_is_generation_bound_and_range_streamable(
|
||||
base.write_bytes(b"RRF2-base")
|
||||
overlay = tmp_path / "overlay.rrd"
|
||||
overlay.write_bytes(b"RRF2-overlay")
|
||||
replay = tmp_path / "replay.rrd"
|
||||
replay.write_bytes(b"RRF2-replay")
|
||||
generation = "b" * 64
|
||||
recording_id = "recording-001"
|
||||
artifact = CanonicalLabOverlayArtifact(
|
||||
@@ -170,6 +239,11 @@ def test_canonical_overlay_get_is_generation_bound_and_range_streamable(
|
||||
byte_length=overlay.stat().st_size,
|
||||
sha256=_sha256(overlay),
|
||||
)
|
||||
replay_artifact = CanonicalLabReplayArtifact(
|
||||
path=replay,
|
||||
byte_length=replay.stat().st_size,
|
||||
sha256=_sha256(replay),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
vegetation_api_module,
|
||||
"_resolve_candidate",
|
||||
@@ -195,6 +269,11 @@ def test_canonical_overlay_get_is_generation_bound_and_range_streamable(
|
||||
"canonical_lab_overlay",
|
||||
lambda *_args, **_kwargs: artifact,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
vegetation_api_module,
|
||||
"canonical_lab_replay",
|
||||
lambda *_args, **_kwargs: replay_artifact,
|
||||
)
|
||||
ffmpeg = tmp_path / "ffmpeg"
|
||||
ffmpeg.write_bytes(b"fixture")
|
||||
ffmpeg.chmod(0o700)
|
||||
@@ -262,6 +341,33 @@ def test_canonical_overlay_get_is_generation_bound_and_range_streamable(
|
||||
)
|
||||
assert stale_overlay.status_code == 412
|
||||
|
||||
replay_endpoint = (
|
||||
f"/api/v1/laboratory/vegetation-shadow/{result_id}/canonical-replay.rrd"
|
||||
)
|
||||
replay_descriptor = client.head(
|
||||
replay_endpoint,
|
||||
params={"base_generation": generation},
|
||||
)
|
||||
assert replay_descriptor.status_code == 200
|
||||
assert replay_descriptor.headers["content-length"] == str(replay_artifact.byte_length)
|
||||
assert replay_descriptor.headers["etag"] == f'"{replay_artifact.sha256}"'
|
||||
assert replay_descriptor.headers["x-rerun-format"] == "RRF2"
|
||||
|
||||
replay_response = client.get(
|
||||
replay_endpoint,
|
||||
params={"generation": replay_artifact.sha256},
|
||||
headers={"Range": "bytes=0-3"},
|
||||
)
|
||||
assert replay_response.status_code == 206
|
||||
assert replay_response.content == b"RRF2"
|
||||
assert replay_response.headers["etag"] == f'"{replay_artifact.sha256}"'
|
||||
|
||||
stale_replay = client.get(
|
||||
replay_endpoint,
|
||||
params={"generation": "f" * 64},
|
||||
)
|
||||
assert stale_replay.status_code == 412
|
||||
|
||||
|
||||
def test_route_playback_chunk_descriptor_seals_only_requested_binary_window() -> None:
|
||||
points = np.arange(18, dtype="<f4").reshape(6, 3)
|
||||
|
||||
Reference in New Issue
Block a user