fix(lab): preserve replay view and restore SLAM semantics
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import type { LaboratoryFetch } from "./advancedResults";
|
import type { LaboratoryFetch } from "./advancedResults";
|
||||||
|
|
||||||
const RESULT_ID = /^m49-tgs-full-shadow-[a-f0-9]{64}$/;
|
const RESULT_ID = /^m49-tgs-full-shadow-[a-f0-9]{64}$/;
|
||||||
|
const SEMANTIC_RESULT_ID = /^e47-semantic-slam-[a-f0-9]{64}$/;
|
||||||
|
|
||||||
export type M49TgsFullShadowStateCode = 0 | 1 | 2 | 3;
|
export type M49TgsFullShadowStateCode = 0 | 1 | 2 | 3;
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ export interface M49TgsFullShadowResult {
|
|||||||
source: {
|
source: {
|
||||||
sourcePackSha256: string;
|
sourcePackSha256: string;
|
||||||
linkedVisualResultId: string;
|
linkedVisualResultId: string;
|
||||||
|
linkedSemanticResultId: string;
|
||||||
};
|
};
|
||||||
configuration: {
|
configuration: {
|
||||||
configSha256: string;
|
configSha256: string;
|
||||||
@@ -158,12 +160,20 @@ export async function fetchM49TgsFullShadowResult(
|
|||||||
const performance = objectValue(payload.performance, "M49 full performance");
|
const performance = objectValue(payload.performance, "M49 full performance");
|
||||||
const acceptance = objectValue(payload.acceptance, "M49 full acceptance");
|
const acceptance = objectValue(payload.acceptance, "M49 full acceptance");
|
||||||
const decision = objectValue(payload.decision, "M49 full decision");
|
const decision = objectValue(payload.decision, "M49 full decision");
|
||||||
|
const linkedSemanticResultId = text(
|
||||||
|
source.linked_semantic_result_id,
|
||||||
|
"M49 full semantic result",
|
||||||
|
);
|
||||||
|
if (!SEMANTIC_RESULT_ID.test(linkedSemanticResultId)) {
|
||||||
|
throw new M49TgsFullShadowContractError("M49 full semantic result: нарушена идентичность.");
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
resultId: text(payload.result_id, "M49 full result"),
|
resultId: text(payload.result_id, "M49 full result"),
|
||||||
createdAtUtc: text(payload.created_at_utc, "M49 full created"),
|
createdAtUtc: text(payload.created_at_utc, "M49 full created"),
|
||||||
source: {
|
source: {
|
||||||
sourcePackSha256: text(source.source_pack_sha256, "M49 full source pack"),
|
sourcePackSha256: text(source.source_pack_sha256, "M49 full source pack"),
|
||||||
linkedVisualResultId: text(source.linked_visual_result_id, "M49 full visual result"),
|
linkedVisualResultId: text(source.linked_visual_result_id, "M49 full visual result"),
|
||||||
|
linkedSemanticResultId,
|
||||||
},
|
},
|
||||||
configuration: {
|
configuration: {
|
||||||
configSha256: text(configuration.config_sha256, "M49 full config"),
|
configSha256: text(configuration.config_sha256, "M49 full config"),
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import type {
|
|||||||
RecordedEvidenceSemanticClass,
|
RecordedEvidenceSemanticClass,
|
||||||
RecordedEvidenceSemanticPaletteEntry,
|
RecordedEvidenceSemanticPaletteEntry,
|
||||||
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
||||||
|
import {
|
||||||
|
fetchE47SemanticSlamResult,
|
||||||
|
type E47SemanticSlamResult,
|
||||||
|
} from "../../core/laboratory/e47SemanticSlam";
|
||||||
import {
|
import {
|
||||||
fetchM49TgsFullShadowSpatialChunk,
|
fetchM49TgsFullShadowSpatialChunk,
|
||||||
type M49TgsFullShadowResult,
|
type M49TgsFullShadowResult,
|
||||||
@@ -46,6 +50,8 @@ function message(error: unknown): string {
|
|||||||
|
|
||||||
export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowResult }) {
|
export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowResult }) {
|
||||||
const [activeSequence, setActiveSequence] = useState<number | null>(null);
|
const [activeSequence, setActiveSequence] = useState<number | null>(null);
|
||||||
|
const [semantic, setSemantic] = useState<E47SemanticSlamResult | null>(null);
|
||||||
|
const [semanticError, setSemanticError] = useState<string | null>(null);
|
||||||
const [chunks, setChunks] = useState<ReadonlyMap<number, M49TgsFullShadowSpatialChunk>>(
|
const [chunks, setChunks] = useState<ReadonlyMap<number, M49TgsFullShadowSpatialChunk>>(
|
||||||
() => new Map(),
|
() => new Map(),
|
||||||
);
|
);
|
||||||
@@ -57,6 +63,27 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
|
|||||||
const activeChunkStartRef = useRef(activeChunkStart);
|
const activeChunkStartRef = useRef(activeChunkStart);
|
||||||
activeChunkStartRef.current = activeChunkStart;
|
activeChunkStartRef.current = activeChunkStart;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
setSemantic(null);
|
||||||
|
setSemanticError(null);
|
||||||
|
void fetchE47SemanticSlamResult({
|
||||||
|
resultId: result.source.linkedSemanticResultId,
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
.then((next) => {
|
||||||
|
if (controller.signal.aborted) return;
|
||||||
|
if (!next || next.baseM4ResultId !== result.source.linkedVisualResultId) {
|
||||||
|
throw new Error("Semantic archive не совпал с исходным M4 timeline.");
|
||||||
|
}
|
||||||
|
setSemantic(next);
|
||||||
|
})
|
||||||
|
.catch((caught: unknown) => {
|
||||||
|
if (!controller.signal.aborted) setSemanticError(message(caught));
|
||||||
|
});
|
||||||
|
return () => controller.abort();
|
||||||
|
}, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
for (const controller of inFlightRef.current.values()) controller.abort();
|
for (const controller of inFlightRef.current.values()) controller.abort();
|
||||||
inFlightRef.current.clear();
|
inFlightRef.current.clear();
|
||||||
@@ -142,23 +169,34 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<M4ReplayThreatVisual
|
<>
|
||||||
resultId={result.source.linkedVisualResultId}
|
<M4ReplayThreatVisual
|
||||||
showReviewAnchorBoxes={false}
|
resultId={result.source.linkedVisualResultId}
|
||||||
reviewLabel="4 489 source-paced TGS frames"
|
semantic={semantic ? {
|
||||||
evidenceLabel="M49 · full TGS shadow"
|
resultId: semantic.resultId,
|
||||||
initialSpatialMode="3d"
|
taxonomy: semantic.taxonomy,
|
||||||
onActiveSequenceChange={handleSequenceChange}
|
} : undefined}
|
||||||
classifiedSpatialLayer={{
|
showReviewAnchorBoxes={false}
|
||||||
label: "TGS full shadow · causal rolling 1 s",
|
reviewLabel="4 489 source-paced TGS frames"
|
||||||
pointLayerLabel: "SOURCE POINTS",
|
evidenceLabel="M49 · full TGS shadow"
|
||||||
cellLayerLabel: "TGS COSTMAP",
|
initialSpatialMode="3d"
|
||||||
expectedAtSequence: true,
|
onActiveSequenceChange={handleSequenceChange}
|
||||||
frame: classifiedFrame,
|
classifiedSpatialLayer={{
|
||||||
loading,
|
label: "TGS full shadow · causal rolling 1 s",
|
||||||
error,
|
pointLayerLabel: "SOURCE POINTS",
|
||||||
replacePointCloud: false,
|
cellLayerLabel: "TGS COSTMAP",
|
||||||
}}
|
expectedAtSequence: true,
|
||||||
/>
|
frame: classifiedFrame,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
replacePointCloud: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{semanticError ? (
|
||||||
|
<div className="m4-replay-threat-visual__pane-status" role="alert">
|
||||||
|
Semantic overlay недоступен: {semanticError}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -409,6 +409,17 @@ export function M4ReplayThreatVisual({
|
|||||||
const classifiedSpatialFrame = classifiedSpatialLayer?.frame?.sourceSequence === timelineFrame.activeSequence
|
const classifiedSpatialFrame = classifiedSpatialLayer?.frame?.sourceSequence === timelineFrame.activeSequence
|
||||||
? classifiedSpatialLayer?.frame ?? null
|
? classifiedSpatialLayer?.frame ?? null
|
||||||
: null;
|
: null;
|
||||||
|
const lastClassifiedSpatialFrameRef = useRef<{
|
||||||
|
resultId: string;
|
||||||
|
frame: M4ReplayClassifiedSpatialFrame;
|
||||||
|
} | null>(null);
|
||||||
|
if (classifiedSpatialLayer?.frame) {
|
||||||
|
lastClassifiedSpatialFrameRef.current = { resultId, frame: classifiedSpatialLayer.frame };
|
||||||
|
}
|
||||||
|
const displayedClassifiedSpatialFrame = classifiedSpatialFrame
|
||||||
|
?? (lastClassifiedSpatialFrameRef.current?.resultId === resultId
|
||||||
|
? lastClassifiedSpatialFrameRef.current.frame
|
||||||
|
: null);
|
||||||
const replaceClassifiedPointCloud = classifiedSpatialLayer?.replacePointCloud ?? true;
|
const replaceClassifiedPointCloud = classifiedSpatialLayer?.replacePointCloud ?? true;
|
||||||
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
|
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
|
||||||
const mapGravityLocalSensorToBodyGround = useCallback((
|
const mapGravityLocalSensorToBodyGround = useCallback((
|
||||||
@@ -425,13 +436,13 @@ export function M4ReplayThreatVisual({
|
|||||||
return [rotated[0], rotated[1], rotated[2] + nominalSensorHeightM];
|
return [rotated[0], rotated[1], rotated[2] + nominalSensorHeightM];
|
||||||
}, [activeSpatialFrame?.bodyFrame?.basisMapFromBody, nominalSensorHeightM]);
|
}, [activeSpatialFrame?.bodyFrame?.basisMapFromBody, nominalSensorHeightM]);
|
||||||
const classifiedPointsBody = useMemo(
|
const classifiedPointsBody = useMemo(
|
||||||
() => classifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
|
() => displayedClassifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
|
||||||
mapGravityLocalSensorToBodyGround,
|
mapGravityLocalSensorToBodyGround,
|
||||||
) ?? [],
|
) ?? [],
|
||||||
[classifiedSpatialFrame, mapGravityLocalSensorToBodyGround],
|
[displayedClassifiedSpatialFrame, mapGravityLocalSensorToBodyGround],
|
||||||
);
|
);
|
||||||
const classifiedCellsBody = useMemo<readonly LaboratoryMetricCellEvidence[]>(
|
const classifiedCellsBody = useMemo<readonly LaboratoryMetricCellEvidence[]>(
|
||||||
() => classifiedSpatialFrame?.cellsMapGravityLocal.map((cell) => {
|
() => displayedClassifiedSpatialFrame?.cellsMapGravityLocal.map((cell) => {
|
||||||
const body = mapGravityLocalSensorToBodyGround([
|
const body = mapGravityLocalSensorToBodyGround([
|
||||||
cell.centerXyM[0],
|
cell.centerXyM[0],
|
||||||
cell.centerXyM[1],
|
cell.centerXyM[1],
|
||||||
@@ -451,7 +462,7 @@ export function M4ReplayThreatVisual({
|
|||||||
state: cell.state,
|
state: cell.state,
|
||||||
};
|
};
|
||||||
}) ?? [],
|
}) ?? [],
|
||||||
[classifiedSpatialFrame, mapGravityLocalSensorToBodyGround, nominalSensorHeightM],
|
[displayedClassifiedSpatialFrame, mapGravityLocalSensorToBodyGround, nominalSensorHeightM],
|
||||||
);
|
);
|
||||||
const classifiedCellCounts = useMemo(() => ({
|
const classifiedCellCounts = useMemo(() => ({
|
||||||
ground: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
|
ground: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
|
||||||
@@ -641,6 +652,16 @@ export function M4ReplayThreatVisual({
|
|||||||
>
|
>
|
||||||
{classifiedSpatialLayer.pointLayerLabel}
|
{classifiedSpatialLayer.pointLayerLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="compact"
|
||||||
|
shape="pill"
|
||||||
|
variant={showLocalSurface ? "primary" : "secondary"}
|
||||||
|
aria-pressed={showLocalSurface}
|
||||||
|
title="Bounded local SLAM surface · visual-derived"
|
||||||
|
onClick={() => setShowLocalSurface((visible) => !visible)}
|
||||||
|
>
|
||||||
|
LOCAL SLAM
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="compact"
|
size="compact"
|
||||||
shape="pill"
|
shape="pill"
|
||||||
@@ -650,6 +671,17 @@ export function M4ReplayThreatVisual({
|
|||||||
>
|
>
|
||||||
{classifiedSpatialLayer.cellLayerLabel}
|
{classifiedSpatialLayer.cellLayerLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
{semantic ? (
|
||||||
|
<Button
|
||||||
|
size="compact"
|
||||||
|
shape="pill"
|
||||||
|
variant={showSpatialSemantic ? "primary" : "secondary"}
|
||||||
|
aria-pressed={showSpatialSemantic}
|
||||||
|
onClick={() => setShowSpatialSemantic((visible) => !visible)}
|
||||||
|
>
|
||||||
|
SEMANTICS
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@@ -952,38 +984,38 @@ export function M4ReplayThreatVisual({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{(!classifiedSpatialLayer ? spatialFrame : classifiedSpatialFrame) ? (
|
{(!classifiedSpatialLayer ? spatialFrame : displayedClassifiedSpatialFrame) ? (
|
||||||
<LaboratoryMetricEvidenceScene
|
<LaboratoryMetricEvidenceScene
|
||||||
ref={metricSceneRef}
|
ref={metricSceneRef}
|
||||||
pointCloudBodyXyzM={classifiedSpatialFrame && replaceClassifiedPointCloud
|
pointCloudBodyXyzM={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
? classifiedPointsBody
|
? classifiedPointsBody
|
||||||
: activeSpatialFrame?.pointCloudBodyXyzM ?? []}
|
: activeSpatialFrame?.pointCloudBodyXyzM ?? []}
|
||||||
localSurfaceBodyXyzM={classifiedSpatialFrame ? [] : localSurface.pointsBodyXyzM}
|
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
|
||||||
obstacles={classifiedSpatialFrame ? [] : sceneObstacles}
|
obstacles={displayedClassifiedSpatialFrame ? [] : sceneObstacles}
|
||||||
rig={timeline.rig}
|
rig={timeline.rig}
|
||||||
corridor={timeline.corridor}
|
corridor={timeline.corridor}
|
||||||
occupiedVoxelSizeM={classifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
|
occupiedVoxelSizeM={displayedClassifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
|
||||||
mode={spatialMode}
|
mode={spatialMode}
|
||||||
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
|
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
|
||||||
showCurrentIncrement={showCurrentIncrement}
|
showCurrentIncrement={showCurrentIncrement}
|
||||||
showLocalSurface={classifiedSpatialFrame ? false : showLocalSurface}
|
showLocalSurface={showLocalSurface}
|
||||||
showRollingMap={showRollingMap}
|
showRollingMap={showRollingMap}
|
||||||
showLowStep={classifiedSpatialFrame ? false : showLowStep}
|
showLowStep={displayedClassifiedSpatialFrame ? false : showLowStep}
|
||||||
pointSemanticClassIds={classifiedSpatialFrame && replaceClassifiedPointCloud
|
pointSemanticClassIds={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
? classifiedSpatialFrame.pointClassIds
|
? displayedClassifiedSpatialFrame.pointClassIds
|
||||||
: alignedSemanticPointIds}
|
: alignedSemanticPointIds}
|
||||||
semanticClasses={classifiedSpatialFrame && replaceClassifiedPointCloud
|
semanticClasses={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
? classifiedSpatialFrame.classes
|
? displayedClassifiedSpatialFrame.classes
|
||||||
: semanticClasses}
|
: semanticClasses}
|
||||||
semanticPalette={classifiedSpatialFrame && replaceClassifiedPointCloud
|
semanticPalette={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
? classifiedSpatialFrame.palette
|
? displayedClassifiedSpatialFrame.palette
|
||||||
: semanticPalette}
|
: semanticPalette}
|
||||||
classifiedCells={classifiedCellsBody}
|
classifiedCells={classifiedCellsBody}
|
||||||
classifiedCellSizeM={classifiedSpatialFrame?.cellSizeM}
|
classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM}
|
||||||
showClassifiedCells={showRollingMap}
|
showClassifiedCells={showRollingMap}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{classifiedSpatialLayer && !classifiedSpatialFrame ? (
|
{classifiedSpatialLayer && !displayedClassifiedSpatialFrame ? (
|
||||||
<div className="l3-visual-audit__state" role={classifiedSpatialLayer.error ? "alert" : "status"}>
|
<div className="l3-visual-audit__state" role={classifiedSpatialLayer.error ? "alert" : "status"}>
|
||||||
{classifiedSpatialLayer.loading || displayingBufferedFrame
|
{classifiedSpatialLayer.loading || displayingBufferedFrame
|
||||||
? <span className="busy-indicator" aria-hidden="true" />
|
? <span className="busy-indicator" aria-hidden="true" />
|
||||||
|
|||||||
@@ -74,12 +74,22 @@ test("M4.9T5 chunk keeps every missing-LiDAR cell explicitly UNOBSERVED", async
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("M4.9T5 viewer prefetches 24-frame immutable chunks", async () => {
|
test("M4.9T5 viewer prefetches 24-frame immutable chunks", async () => {
|
||||||
const source = await readFile(
|
const [source, contract] = await Promise.all([
|
||||||
new URL("../src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx", import.meta.url),
|
readFile(
|
||||||
"utf8",
|
new URL("../src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx", import.meta.url),
|
||||||
);
|
"utf8",
|
||||||
|
),
|
||||||
|
readFile(
|
||||||
|
new URL("../src/core/laboratory/m49TgsFullShadow.ts", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
]);
|
||||||
assert.match(source, /const CHUNK_FRAMES = 24/);
|
assert.match(source, /const CHUNK_FRAMES = 24/);
|
||||||
assert.match(source, /activeChunkStart \+ CHUNK_FRAMES/);
|
assert.match(source, /activeChunkStart \+ CHUNK_FRAMES/);
|
||||||
assert.match(source, /fetchM49TgsFullShadowSpatialChunk/);
|
assert.match(source, /fetchM49TgsFullShadowSpatialChunk/);
|
||||||
assert.match(source, /sampleAvailable: spatial\.sampleAvailable/);
|
assert.match(source, /sampleAvailable: spatial\.sampleAvailable/);
|
||||||
|
assert.match(source, /fetchE47SemanticSlamResult/);
|
||||||
|
assert.match(source, /next\.baseM4ResultId !== result\.source\.linkedVisualResultId/);
|
||||||
|
assert.match(source, /semantic=\{semantic \? \{/);
|
||||||
|
assert.match(contract, /linked_semantic_result_id/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -801,12 +801,15 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
|
|||||||
assert.match(visual, /showLocalSurface/);
|
assert.match(visual, /showLocalSurface/);
|
||||||
assert.match(
|
assert.match(
|
||||||
visual,
|
visual,
|
||||||
/pointCloudBodyXyzM=\{classifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: activeSpatialFrame\?\.pointCloudBodyXyzM \?\? \[\]\}/,
|
/pointCloudBodyXyzM=\{displayedClassifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: activeSpatialFrame\?\.pointCloudBodyXyzM \?\? \[\]\}/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
visual,
|
visual,
|
||||||
/const activeSpatialFrame = spatialFrame\?\.sequence === timelineFrame\.activeSequence[\s\S]*const classifiedSpatialFrame = classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence/,
|
/const classifiedSpatialFrame = classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence[\s\S]*const displayedClassifiedSpatialFrame = classifiedSpatialFrame[\s\S]*lastClassifiedSpatialFrameRef/,
|
||||||
);
|
);
|
||||||
|
assert.match(visual, /localSurfaceBodyXyzM=\{localSurface\.pointsBodyXyzM\}/);
|
||||||
|
assert.match(visual, /showLocalSurface=\{showLocalSurface\}/);
|
||||||
|
assert.match(visual, /\{semantic \? \([\s\S]*>\s*SEMANTICS\s*<\/Button>/);
|
||||||
assert.match(visual, /все 2 244 TGS-ячейки явно UNOBSERVED/);
|
assert.match(visual, /все 2 244 TGS-ячейки явно UNOBSERVED/);
|
||||||
assert.match(
|
assert.match(
|
||||||
visual,
|
visual,
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ correct for vegetation, terrain, gaps or the future vehicle envelope.
|
|||||||
|
|
||||||
| Item | Identity |
|
| Item | Identity |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| LAB result | `m49-tgs-full-shadow-0faeaaf3aba8dccae974eab51ff9cccf264a13ec285abb1920c1e5a09a7e87bc` |
|
| LAB result | `m49-tgs-full-shadow-ef98de7db7596d48e8c8c0549ce68e6704ee03e87c8c4bcf1e3e748b7ccb032e` |
|
||||||
| Worker run | `Worker 006 / ravnoves00-full-001` |
|
| Worker run | `Worker 006 / ravnoves00-full-001` |
|
||||||
| Mission Core revision used by Worker | `40c850b167dda366d8aa45d828520168affaf9fd` |
|
| Mission Core revision used by Worker | `40c850b167dda366d8aa45d828520168affaf9fd` |
|
||||||
| Deterministic Worker artifact | `5e0ea16c7a5cc760463836718b0cd8b0006ffc4b202e5f706a20a86ef2f912ab` |
|
| Deterministic Worker artifact | `5e0ea16c7a5cc760463836718b0cd8b0006ffc4b202e5f706a20a86ef2f912ab` |
|
||||||
| Source pack SHA-256 | `0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944` |
|
| Source pack SHA-256 | `0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944` |
|
||||||
| TGS config SHA-256 | `c2e07010aaee78259d36c057962d6bfb885349251ff7356d867e5813e632881c` |
|
| TGS config SHA-256 | `c2e07010aaee78259d36c057962d6bfb885349251ff7356d867e5813e632881c` |
|
||||||
| Linked visual result | `m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324` |
|
| Linked visual result | `m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324` |
|
||||||
|
| Linked semantic result | `e47-semantic-slam-f8c53ba12e9719856195890c7dcd2ba6c114434674159f948064368012e6b3bc` |
|
||||||
|
|
||||||
The profile is a `0.45 m`, `12 m` radius, `1 s` causal rolling
|
The profile is a `0.45 m`, `12 m` radius, `1 s` causal rolling
|
||||||
`map-gravity-local` costmap. The four states remain separate:
|
`map-gravity-local` costmap. The four states remain separate:
|
||||||
@@ -62,7 +63,9 @@ source points. This preserves chronology without inventing free space.
|
|||||||
|
|
||||||
The immutable result is published on the canonical Mission Core service at
|
The immutable result is published on the canonical Mission Core service at
|
||||||
port `8000`. The LAB reuses the recorded camera timeline and exposes independent
|
port `8000`. The LAB reuses the recorded camera timeline and exposes independent
|
||||||
`SOURCE POINTS`, `TGS COSTMAP`, `3D` and `PLAN` controls.
|
`SOURCE POINTS`, `LOCAL SLAM`, `TGS COSTMAP`, `SEMANTICS`, `3D` and `PLAN`
|
||||||
|
controls. The semantic mask is the exact E47 full-route archive bound to the
|
||||||
|
same M4 camera result; it remains diagnostic and does not change TGS states.
|
||||||
|
|
||||||
The first implementation fetched one large JSON frame at a time. Browser QA
|
The first implementation fetched one large JSON frame at a time. Browser QA
|
||||||
showed that this was only intermittently exact at `1×`. The published viewer
|
showed that this was only intermittently exact at `1×`. The published viewer
|
||||||
@@ -73,6 +76,14 @@ showed the loading placeholder. A real missing-LiDAR frame was separately
|
|||||||
accepted with `0 source points`, `2,244 unobserved`, `0 occupied` and no retained
|
accepted with `0 source points`, `2,244 unobserved`, `0 occupied` and no retained
|
||||||
previous cloud.
|
previous cloud.
|
||||||
|
|
||||||
|
A follow-up playback acceptance fixed a UI lifecycle defect that remounted the
|
||||||
|
Three.js scene while the next TGS frame crossed the React buffer boundary. The
|
||||||
|
scene now retains the last sealed classified frame until the exact next frame
|
||||||
|
arrives, so orbit controls and the operator-selected view survive continuous
|
||||||
|
playback. Twelve consecutive `250 ms` playback samples kept one mounted 3D
|
||||||
|
canvas, a visible bounded `LOCAL SLAM` surface and the synchronized E47 video
|
||||||
|
mask. Pausing is no longer required to rotate, pan or zoom the 3D view.
|
||||||
|
|
||||||
## Sealed evidence
|
## Sealed evidence
|
||||||
|
|
||||||
| File | Bytes | SHA-256 |
|
| File | Bytes | SHA-256 |
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ def seal_m49_tgs_full_shadow(
|
|||||||
destination_root: Path,
|
destination_root: Path,
|
||||||
profile_path: Path,
|
profile_path: Path,
|
||||||
linked_visual_result_id: str,
|
linked_visual_result_id: str,
|
||||||
|
linked_semantic_result_id: str,
|
||||||
created_at_utc: str | None = None,
|
created_at_utc: str | None = None,
|
||||||
) -> M49TgsFullShadowResult:
|
) -> M49TgsFullShadowResult:
|
||||||
source = source_root.expanduser().resolve(strict=True)
|
source = source_root.expanduser().resolve(strict=True)
|
||||||
@@ -115,6 +116,11 @@ def seal_m49_tgs_full_shadow(
|
|||||||
or len(linked_visual_result_id) != len("m4-threat-replay-") + 64
|
or len(linked_visual_result_id) != len("m4-threat-replay-") + 64
|
||||||
):
|
):
|
||||||
raise M49TgsFullShadowError("linked visual result is invalid")
|
raise M49TgsFullShadowError("linked visual result is invalid")
|
||||||
|
if (
|
||||||
|
not linked_semantic_result_id.startswith("e47-semantic-slam-")
|
||||||
|
or len(linked_semantic_result_id) != len("e47-semantic-slam-") + 64
|
||||||
|
):
|
||||||
|
raise M49TgsFullShadowError("linked semantic result is invalid")
|
||||||
for name in EVIDENCE_FILES:
|
for name in EVIDENCE_FILES:
|
||||||
path = source / name
|
path = source / name
|
||||||
proof = worker.get("files", {}).get(name, {})
|
proof = worker.get("files", {}).get(name, {})
|
||||||
@@ -131,6 +137,7 @@ def seal_m49_tgs_full_shadow(
|
|||||||
"input_manifest_sha256": worker["input_manifest_sha256"],
|
"input_manifest_sha256": worker["input_manifest_sha256"],
|
||||||
"config_sha256": worker["config_sha256"],
|
"config_sha256": worker["config_sha256"],
|
||||||
"linked_visual_result_id": linked_visual_result_id,
|
"linked_visual_result_id": linked_visual_result_id,
|
||||||
|
"linked_semantic_result_id": linked_semantic_result_id,
|
||||||
"files": {name: worker["files"][name]["sha256"] for name in EVIDENCE_FILES},
|
"files": {name: worker["files"][name]["sha256"] for name in EVIDENCE_FILES},
|
||||||
"authority": {
|
"authority": {
|
||||||
"commands_enabled": False,
|
"commands_enabled": False,
|
||||||
@@ -154,6 +161,7 @@ def seal_m49_tgs_full_shadow(
|
|||||||
"source_session_id": "20260720T065719Z_viewer_live",
|
"source_session_id": "20260720T065719Z_viewer_live",
|
||||||
"source_pack_sha256": worker["source_pack_sha256"],
|
"source_pack_sha256": worker["source_pack_sha256"],
|
||||||
"linked_visual_result_id": linked_visual_result_id,
|
"linked_visual_result_id": linked_visual_result_id,
|
||||||
|
"linked_semantic_result_id": linked_semantic_result_id,
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"profile_id": profile["profile_id"],
|
"profile_id": profile["profile_id"],
|
||||||
@@ -211,6 +219,7 @@ def seal_m49_tgs_full_shadow(
|
|||||||
"visual_review": {
|
"visual_review": {
|
||||||
"instrument": "m4-canonical-reference-graph",
|
"instrument": "m4-canonical-reference-graph",
|
||||||
"linked_visual_result_id": linked_visual_result_id,
|
"linked_visual_result_id": linked_visual_result_id,
|
||||||
|
"linked_semantic_result_id": linked_semantic_result_id,
|
||||||
"frame_count": 4489,
|
"frame_count": 4489,
|
||||||
"state_codes": profile["state_codes"],
|
"state_codes": profile["state_codes"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
@@ -7,6 +8,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from k1link.laboratory.m49_tgs_full_shadow import seal_m49_tgs_full_shadow
|
||||||
from k1link.web import m49_tgs_full_shadow_api as full_shadow_api
|
from k1link.web import m49_tgs_full_shadow_api as full_shadow_api
|
||||||
|
|
||||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||||
@@ -59,6 +61,76 @@ def test_full_shadow_worker_artifact_is_deterministic_and_cpu_only(tmp_path: Pat
|
|||||||
assert "gpu_requested = $false" in runner_text
|
assert "gpu_requested = $false" in runner_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_shadow_seal_binds_visual_and_semantic_timelines(tmp_path: Path) -> None:
|
||||||
|
source = tmp_path / "worker"
|
||||||
|
source.mkdir()
|
||||||
|
files: dict[str, dict[str, object]] = {}
|
||||||
|
for name in (
|
||||||
|
"costmap-cell-centers-xy-m.npy",
|
||||||
|
"costmap-cell-indices-xy.npy",
|
||||||
|
"costmap-states.npy",
|
||||||
|
"costmap-z-bounds-m.npy",
|
||||||
|
"frames.ndjson",
|
||||||
|
):
|
||||||
|
payload = f"sealed:{name}\n".encode()
|
||||||
|
(source / name).write_bytes(payload)
|
||||||
|
files[name] = {
|
||||||
|
"bytes": len(payload),
|
||||||
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||||
|
}
|
||||||
|
worker = {
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-result/v1",
|
||||||
|
"status": "passed",
|
||||||
|
"source_pack_sha256": "a" * 64,
|
||||||
|
"input_manifest_sha256": "b" * 64,
|
||||||
|
"config_sha256": "c" * 64,
|
||||||
|
"timeline": {
|
||||||
|
"frame_count": 4489,
|
||||||
|
"available_lidar_frame_count": 3928,
|
||||||
|
"missing_lidar_frame_count": 561,
|
||||||
|
},
|
||||||
|
"point_accounting": {"unaccounted": 0},
|
||||||
|
"costmap": {"cell_size_m": 0.45, "radius_m": 12.0},
|
||||||
|
"performance": {},
|
||||||
|
"acceptance": {},
|
||||||
|
"files": files,
|
||||||
|
}
|
||||||
|
(source / "result.json").write_text(json.dumps(worker), encoding="utf-8")
|
||||||
|
(source / "worker-summary.json").write_text(json.dumps({
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-worker-summary/v1",
|
||||||
|
"gpu_requested": False,
|
||||||
|
"aos_used": False,
|
||||||
|
"all_timeline_frames_accounted": True,
|
||||||
|
"all_eligible_points_accounted": True,
|
||||||
|
"canonical_triton_health": "healthy",
|
||||||
|
"canonical_triton_id": "triton",
|
||||||
|
"wall_seconds": 1.0,
|
||||||
|
}), encoding="utf-8")
|
||||||
|
profile = tmp_path / "profile.json"
|
||||||
|
profile.write_text(json.dumps({
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-profile/v1",
|
||||||
|
"profile_id": "test",
|
||||||
|
"profile": {"history_seconds": 1.0},
|
||||||
|
"costmap": {"state_priority": ["NONGROUND_OCCUPIED"]},
|
||||||
|
"state_codes": {"UNOBSERVED": 0},
|
||||||
|
}), encoding="utf-8")
|
||||||
|
visual = "m4-threat-replay-" + "d" * 64
|
||||||
|
semantic = "e47-semantic-slam-" + "e" * 64
|
||||||
|
|
||||||
|
sealed = seal_m49_tgs_full_shadow(
|
||||||
|
source_root=source,
|
||||||
|
destination_root=tmp_path / "results",
|
||||||
|
profile_path=profile,
|
||||||
|
linked_visual_result_id=visual,
|
||||||
|
linked_semantic_result_id=semantic,
|
||||||
|
created_at_utc="2026-08-26T20:27:19Z",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert sealed.report["source"]["linked_visual_result_id"] == visual
|
||||||
|
assert sealed.report["source"]["linked_semantic_result_id"] == semantic
|
||||||
|
assert sealed.manifest["identity"]["linked_semantic_result_id"] == semantic
|
||||||
|
|
||||||
|
|
||||||
def test_full_shadow_chunk_contract_keeps_missing_lidar_unobserved(
|
def test_full_shadow_chunk_contract_keeps_missing_lidar_unobserved(
|
||||||
monkeypatch,
|
monkeypatch,
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
|
|||||||
Reference in New Issue
Block a user