refactor(lab): canonicalize recorded spatial replay
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
type LaboratoryMetricPackedCellEvidence,
|
||||
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
|
||||
import { RecordedEvidenceVideoScene } from "../../components/laboratory/RecordedEvidenceVideoScene";
|
||||
import { useCanonicalRecordedLabSpatialFrame } from "../../components/laboratory/useCanonicalRecordedLabSpatialFrame";
|
||||
import { useRecordedEvidencePlayback } from "../../components/laboratory/useRecordedEvidencePlayback";
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
@@ -35,12 +36,14 @@ import {
|
||||
type RecordedEvidenceSemanticPaletteEntry,
|
||||
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
||||
import {
|
||||
fetchCanonicalRecordedLabSpatialFrame,
|
||||
canonicalRecordedLabPackedTgsCells,
|
||||
canonicalRecordedLabTgsIsCurrent,
|
||||
} from "../../core/laboratory/canonicalRecordedLab";
|
||||
import {
|
||||
fetchVegetationShadowResult,
|
||||
fetchVegetationRouteTgsAnchor,
|
||||
vegetationFullRouteMaskUrl,
|
||||
vegetationVideoMaskUrl,
|
||||
type CanonicalRecordedLabSpatialFrame,
|
||||
type VegetationFullRouteLayer,
|
||||
type VegetationFullRouteReview,
|
||||
type VegetationMixedRouteCase,
|
||||
@@ -105,9 +108,7 @@ function causalTgsCase(
|
||||
&& (!latest || candidate.sourceSequence > latest.sourceSequence)
|
||||
? candidate
|
||||
: latest
|
||||
), null) ?? cases.reduce((first, candidate) => (
|
||||
candidate.sourceSequence < first.sourceSequence ? candidate : first
|
||||
));
|
||||
), null);
|
||||
}
|
||||
|
||||
function nearestFullRouteFrameIndex(
|
||||
@@ -130,92 +131,6 @@ function nearestFullRouteFrameIndex(
|
||||
: low;
|
||||
}
|
||||
|
||||
function useCanonicalRavSpatialFrame(
|
||||
review: VegetationFullRouteReview,
|
||||
replayLaunch: ObservationSessionReplayLaunch | null,
|
||||
targetTimeNs: number,
|
||||
) {
|
||||
const [frame, setFrame] = useState<CanonicalRecordedLabSpatialFrame | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const desiredRef = useRef<number | null>(null);
|
||||
const runningRef = useRef(false);
|
||||
const mountedRef = useRef(true);
|
||||
const cacheRef = useRef(new Map<number, CanonicalRecordedLabSpatialFrame>());
|
||||
const pumpRef = useRef<() => void>(() => undefined);
|
||||
|
||||
pumpRef.current = () => {
|
||||
if (runningRef.current || desiredRef.current === null || !replayLaunch) return;
|
||||
runningRef.current = true;
|
||||
let settledTimeNs: number | null = null;
|
||||
void (async () => {
|
||||
while (mountedRef.current && desiredRef.current !== null) {
|
||||
const requestedTimeNs = desiredRef.current;
|
||||
const cached = cacheRef.current.get(requestedTimeNs);
|
||||
try {
|
||||
const next = cached ?? await fetchCanonicalRecordedLabSpatialFrame(
|
||||
review.sessionId,
|
||||
replayLaunch.sha256,
|
||||
requestedTimeNs,
|
||||
);
|
||||
if (!cached) {
|
||||
cacheRef.current.set(requestedTimeNs, next);
|
||||
while (cacheRef.current.size > 12) {
|
||||
const oldest = cacheRef.current.keys().next().value as number | undefined;
|
||||
if (oldest === undefined) break;
|
||||
cacheRef.current.delete(oldest);
|
||||
}
|
||||
}
|
||||
if (!mountedRef.current) break;
|
||||
setFrame(next);
|
||||
setError(null);
|
||||
} catch (caught: unknown) {
|
||||
if (!mountedRef.current) break;
|
||||
setError(caught instanceof Error ? caught.message : "Spatial-слои RAV004 недоступны.");
|
||||
}
|
||||
settledTimeNs = requestedTimeNs;
|
||||
if (desiredRef.current === requestedTimeNs) break;
|
||||
}
|
||||
})().finally(() => {
|
||||
runningRef.current = false;
|
||||
if (
|
||||
mountedRef.current
|
||||
&& desiredRef.current !== null
|
||||
&& desiredRef.current !== settledTimeNs
|
||||
) {
|
||||
pumpRef.current();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
desiredRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
cacheRef.current.clear();
|
||||
setFrame(null);
|
||||
setError(null);
|
||||
}, [replayLaunch?.sha256, review.sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!replayLaunch) return;
|
||||
desiredRef.current = targetTimeNs;
|
||||
const cached = cacheRef.current.get(targetTimeNs);
|
||||
if (cached) {
|
||||
setFrame(cached);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
pumpRef.current();
|
||||
}, [replayLaunch, targetTimeNs]);
|
||||
|
||||
return { frame, error, loading: Boolean(replayLaunch) && !frame && !error };
|
||||
}
|
||||
|
||||
function FullRouteReviewEvidence({
|
||||
resultId,
|
||||
review,
|
||||
@@ -266,7 +181,11 @@ function FullRouteReviewEvidence({
|
||||
const spatialRequestTimeNs = review.frameSourceTimesNs[spatialRequestIndex]
|
||||
?? review.frameSourceTimesNs[sequenceIndex]
|
||||
?? Math.round(playbackController.playback.currentSeconds * 1_000_000_000);
|
||||
const spatialEvidence = useCanonicalRavSpatialFrame(review, replayLaunch, spatialRequestTimeNs);
|
||||
const spatialEvidence = useCanonicalRecordedLabSpatialFrame({
|
||||
sessionId: review.sessionId,
|
||||
generationSha256: replayLaunch?.sha256 ?? null,
|
||||
targetTimeNs: spatialRequestTimeNs,
|
||||
});
|
||||
const layer = review[semanticLayer];
|
||||
const semantic = useMemo(() => semanticPresentation(layer), [layer]);
|
||||
const prefetchSrcs = useMemo(() => showCameraSemantic
|
||||
@@ -343,11 +262,17 @@ function FullRouteReviewEvidence({
|
||||
? review.frameSourceTimesNs[selectedTgsCase.sourceSequence - 1]
|
||||
?? Math.round(selectedTgsCase.sessionSeconds * 1_000_000_000)
|
||||
: spatialRequestTimeNs;
|
||||
const tgsReferenceEvidence = useCanonicalRavSpatialFrame(
|
||||
review,
|
||||
replayLaunch,
|
||||
selectedTgsTimeNs,
|
||||
const currentFrameTimeNs = review.frameSourceTimesNs[sequenceIndex]
|
||||
?? Math.round(playbackController.playback.currentSeconds * 1_000_000_000);
|
||||
const tgsWithinEvidenceWindow = Boolean(
|
||||
selectedTgsCase
|
||||
&& canonicalRecordedLabTgsIsCurrent(currentFrameTimeNs, selectedTgsTimeNs),
|
||||
);
|
||||
const tgsReferenceEvidence = useCanonicalRecordedLabSpatialFrame({
|
||||
sessionId: review.sessionId,
|
||||
generationSha256: replayLaunch?.sha256 ?? null,
|
||||
targetTimeNs: selectedTgsTimeNs,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!showTgs || !selectedTgsCase) {
|
||||
@@ -376,53 +301,24 @@ function FullRouteReviewEvidence({
|
||||
}, [review.linkedRouteReviewResultId, selectedTgsCase?.sourceSequence, showTgs]);
|
||||
|
||||
const packedTgsCells = useMemo<LaboratoryMetricPackedCellEvidence | undefined>(() => {
|
||||
if (!tgsAnchor) return undefined;
|
||||
if (!tgsAnchor || !tgsWithinEvidenceWindow) return undefined;
|
||||
const currentBody = spatialEvidence.frame?.bodyFrame;
|
||||
const anchorBody = tgsReferenceEvidence.frame?.bodyFrame;
|
||||
const transformPoint = (point: readonly [number, number, number]) => {
|
||||
if (!currentBody || !anchorBody) return point;
|
||||
const map = [0, 1, 2].map((row) => (
|
||||
anchorBody.originMapXyzM[row]!
|
||||
+ anchorBody.basisMapFromBody[row]!.reduce(
|
||||
(sum, coefficient, column) => sum + coefficient * point[column]!,
|
||||
0,
|
||||
)
|
||||
));
|
||||
const delta = map.map((value, index) => value - currentBody.originMapXyzM[index]!);
|
||||
return [0, 1, 2].map((column) => (
|
||||
currentBody.basisMapFromBody.reduce(
|
||||
(sum, row, rowIndex) => sum + row[column]! * delta[rowIndex]!,
|
||||
0,
|
||||
)
|
||||
)) as [number, number, number];
|
||||
};
|
||||
const centers: number[] = [];
|
||||
const zBounds: number[] = [];
|
||||
tgsAnchor.costmap.centersXyM.forEach(([x, y], index) => {
|
||||
const bounds = tgsAnchor.costmap.zBoundsM[index] ?? [null, null];
|
||||
const center = transformPoint([x, y, 0]);
|
||||
centers.push(center[0], center[1]);
|
||||
if (bounds[0] === null || bounds[1] === null) {
|
||||
zBounds.push(Number.NaN, Number.NaN);
|
||||
} else {
|
||||
const bottom = transformPoint([x, y, bounds[0]]);
|
||||
const top = transformPoint([x, y, bounds[1]]);
|
||||
zBounds.push(Math.min(bottom[2], top[2]), Math.max(bottom[2], top[2]));
|
||||
}
|
||||
});
|
||||
return {
|
||||
centersBodyXyM: Float32Array.from(centers),
|
||||
zBoundsM: Float32Array.from(zBounds),
|
||||
stateCodes: Uint8Array.from(tgsAnchor.costmap.stateCodes),
|
||||
};
|
||||
}, [spatialEvidence.frame?.bodyFrame, tgsAnchor, tgsReferenceEvidence.frame?.bodyFrame]);
|
||||
if (!currentBody || !anchorBody) return undefined;
|
||||
return canonicalRecordedLabPackedTgsCells(tgsAnchor.costmap, anchorBody, currentBody);
|
||||
}, [
|
||||
spatialEvidence.frame?.bodyFrame,
|
||||
tgsAnchor,
|
||||
tgsReferenceEvidence.frame?.bodyFrame,
|
||||
tgsWithinEvidenceWindow,
|
||||
]);
|
||||
|
||||
const semanticOverlay = showCameraSemantic ? {
|
||||
src: vegetationFullRouteMaskUrl(resultId, semanticLayer, sequenceIndex),
|
||||
prefetchSrcs,
|
||||
classes: semantic.classes,
|
||||
palette: semantic.palette,
|
||||
opacity: 0.76,
|
||||
opacity: 0.46,
|
||||
ariaLabel: `${layer.name} semantic prediction frame ${sequence}`,
|
||||
} : undefined;
|
||||
|
||||
@@ -464,7 +360,11 @@ function FullRouteReviewEvidence({
|
||||
? spatialEvidence.frame.localSlamBodyXyzM
|
||||
: []}
|
||||
obstacles={[]}
|
||||
rig={{ lengthM: 1, widthM: 0.8, nominalSensorHeightM: 0.4 }}
|
||||
rig={{
|
||||
lengthM: 1,
|
||||
widthM: 0.8,
|
||||
nominalSensorHeightM: spatialEvidence.frame.sensorHeight.meters,
|
||||
}}
|
||||
corridor={{ forwardLengthM: 12, rearMarginM: 1, halfWidthM: 0.4 }}
|
||||
occupiedVoxelSizeM={tgsAnchor?.costmap.cellSizeM ?? 0.45}
|
||||
mode={spatialMode}
|
||||
@@ -487,7 +387,9 @@ function FullRouteReviewEvidence({
|
||||
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||
{tgsAnchorError ?? linkedReviewError ?? (tgsAnchorLoading
|
||||
? `Открываем sealed TGS anchor ${selectedTgsCase.sourceSequence}; source/SLAM и общий clock продолжаются.`
|
||||
: `TGS anchor ${selectedTgsCase.sourceSequence} из 10; source/SLAM и общий clock продолжаются.`)}
|
||||
: tgsWithinEvidenceWindow
|
||||
? `TGS anchor ${selectedTgsCase.sourceSequence} из 10; source/SLAM и общий clock продолжаются.`
|
||||
: `TGS anchor ${selectedTgsCase.sourceSequence} старше доказанного окна 1 с; слой скрыт, playback продолжается.`)}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
@@ -597,12 +499,12 @@ function FullRouteReviewEvidence({
|
||||
</div>
|
||||
<div>
|
||||
<span>Spatial evidence</span>
|
||||
<strong>{showTgs && selectedTgsCase
|
||||
<strong>{showTgs && selectedTgsCase && tgsWithinEvidenceWindow
|
||||
? `TGS anchor ${selectedTgsCase.sourceSequence} · ${selectedTgsCase.tgs.occupiedCells} occupied`
|
||||
: "source RRD · points + SLAM"}</strong>
|
||||
: "source RRD · points + bounded Local SLAM"}</strong>
|
||||
<small>{showTgs
|
||||
? "latest causal of 10 sealed anchors · continuous playback retained"
|
||||
: "causal 1 s view · grayscale intensity · recorded source identity"}</small>
|
||||
? "TGS visible only inside sealed 1 s evidence window · playback retained"
|
||||
: "5 s bounded Local SLAM · ground-rebased recorded source"}</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -673,7 +575,7 @@ function FullRouteReviewResult({
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB V1 · RAVNOVES004TREE · полный маршрут"
|
||||
description="Общий recorded-LAB шаблон воспроизводит запись с травой и оврагами: RIGHT camera, исходное облако, SLAM trajectory, два независимых semantic-слоя и десять реально просчитанных TGS-якорей."
|
||||
description="Общий recorded-LAB шаблон воспроизводит запись с травой и оврагами: RIGHT camera, исходное облако, ограниченный Local SLAM, два независимых semantic-слоя и десять реально просчитанных TGS-якорей."
|
||||
status="FULL RECORDED REVIEW · truth отсутствует · commands OFF"
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
@@ -686,7 +588,7 @@ function FullRouteReviewResult({
|
||||
]}
|
||||
brief={{
|
||||
question: "Что реально видно на полном RAV004-прогоне с высокой травой, оврагами и переходом к городу?",
|
||||
approach: "Одна recorded timeline открывается общим LAB viewer. Camera и RRD синхронизированы; EoMT/DDRNet переключаются на камере, source points и SLAM trajectory — в 3D, TGS — только на десяти запечатанных якорях.",
|
||||
approach: "Одна recorded timeline открывается общим LAB viewer. Camera и RRD синхронизированы; EoMT/DDRNet переключаются на камере, source points и 5-секундный Local SLAM — в 3D, TGS — только в доказанном окне десяти запечатанных якорей.",
|
||||
principalResult: "RAV004 больше не подменяется RAV00: доступна полная исходная запись и её реальные пространственные слои.",
|
||||
limitation: "Ручной truth, continuous TGS и point-aligned 3D semantics отсутствуют. Один повреждённый H.264-пакет на позиции 6092 заменён предыдущим декодированным кадром и отражён в proof.",
|
||||
}}
|
||||
@@ -695,7 +597,7 @@ function FullRouteReviewResult({
|
||||
executionClass: "ai-inference",
|
||||
pipelineId: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
components: [
|
||||
{ kind: "algorithm", name: "Recorded source points + SLAM trajectory", version: "sealed RRD", role: "spatial source evidence", identitySha256: null },
|
||||
{ kind: "algorithm", name: "Recorded source points + bounded Local SLAM", version: "source-paced-ground-v2", role: "spatial source evidence", identitySha256: null },
|
||||
{ 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 },
|
||||
{ kind: "algorithm", name: "Causal TGS", version: "10 linked route anchors", role: "bounded geometric evidence", identitySha256: null },
|
||||
@@ -725,7 +627,7 @@ function FullRouteReviewResult({
|
||||
{ label: "Spatial evidence", value: "RRD + 10 TGS anchors", hint: "continuous TGS и 3D semantics отсутствуют" },
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Полный RAV004 открывается в каноническом recorded viewer с camera, source points, SLAM trajectory, EoMT, DDRNet и связанными TGS-якорями.",
|
||||
proved: "Полный RAV004 открывается в каноническом recorded viewer с camera, source points, bounded Local SLAM, EoMT, DDRNet и связанными TGS-якорями.",
|
||||
notProved: "Не доказаны truth accuracy, временная стабильность DDRNet, continuous negative-obstacle detection и безопасное управление ровером.",
|
||||
decision: "Использовать как visual audit. Следующий gate — truth-набор овраг/трава/дерево/яма и motion-aware temporal evaluation при целевых ≥10 FPS; navigation/actuation оставить OFF.",
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user