Author SHA1 Message Date
DCCONSTRUCTIONS e9ffb829c9 docs(sim): зафиксировать контур AI-улучшения гауссов 2026-08-30 11:57:49 +03:00
DCCONSTRUCTIONS 757d368c86 fix(lab): seal RAV004 spatial replay transport 2026-08-30 10:42:41 +03:00
DCCONSTRUCTIONS f1cbe0061a refactor(lab): restore canonical RAV004 replay 2026-08-30 01:22:44 +03:00
DCCONSTRUCTIONS 74da6437e9 refactor(lab): canonicalize recorded spatial replay 2026-08-29 23:38:17 +03:00
DCCONSTRUCTIONS bd2892140f fix(lab): enforce canonical replay runtime 2026-08-29 22:42:30 +03:00
DCCONSTRUCTIONS 525ab74168 fix(lab): restore canonical RAV004 replay 2026-08-29 21:08:49 +03:00
DCCONSTRUCTIONS c30d77572e fix(lab): restore canonical vegetation evidence layers 2026-08-29 20:33:52 +03:00
DCCONSTRUCTIONS 5179e93f4a fix(lab): restore canonical spatial replay 2026-08-29 19:52:06 +03:00
DCCONSTRUCTIONS c06b709fd7 refactor(viewer): profile rerun loading by playback stage 2026-08-29 17:46:07 +03:00
DCCONSTRUCTIONS 856b61be99 fix(simulation): standardize streamed rendering and UGV proxies 2026-08-29 17:39:40 +03:00
DCCONSTRUCTIONS 76694cc869 fix(simulation): reserve disk space before artifact import 2026-08-29 16:18:56 +03:00
DCCONSTRUCTIONS e6df8cb264 fix(simulation): retry temporary provider proxy failures 2026-08-29 16:01:28 +03:00
DCCONSTRUCTIONS 4b487e367f fix(simulation): retry builds across worker tunnel outages 2026-08-29 14:37:27 +03:00
DCCONSTRUCTIONS d7d9485724 fix(lab): overlay full route semantic masks 2026-08-29 14:19:32 +03:00
DCCONSTRUCTIONS 0a0cde7b0e fix(lab): keep full route result indexable 2026-08-29 13:54:40 +03:00
DCCONSTRUCTIONS 9dec37a8a9 feat(lab): seal full RAVNOVES004TREE semantic review 2026-08-29 13:37:57 +03:00
DCCONSTRUCTIONS 24b65926fe fix(simulation): hold ugv on sloped collision meshes 2026-08-29 12:41:28 +03:00
DCCONSTRUCTIONS 57dacc5a04 fix(simulation): model parked UGV tyre grip 2026-08-29 12:03:33 +03:00
DCCONSTRUCTIONS d9ec8c9cef fix(simulation): tune physical UGV braking 2026-08-29 11:26:01 +03:00
57 changed files with 5873 additions and 1332 deletions
@@ -29,7 +29,9 @@ export type RecordedMediaPresentationState = "loading" | "ready" | "waiting" | "
export const RECORDED_MEDIA_DURATION_TOLERANCE_SECONDS = 1;
const RECORDED_MEDIA_SOURCE_OPEN_TIMEOUT_MS = 10_000;
const RECORDED_MEDIA_TARGET_TIMEOUT_MS = 10_000;
// A valid local fragment becomes decoder-ready well below one second. Keeping
// a damaged GOP on screen for ten seconds only delays the keyframe recovery.
const RECORDED_MEDIA_TARGET_TIMEOUT_MS = 2_500;
const RECORDED_MEDIA_FRAGMENT_TIMEOUT_MS = 15_000;
const RECORDED_MEDIA_REQUIRED_AHEAD_SEGMENTS = 12;
const RECORDED_MEDIA_SEGMENTS_AHEAD = 36;
@@ -72,6 +74,39 @@ export function recordedMediaDecodeStartSequence(
return selected;
}
export function nextRecordedMediaRandomAccessSequence(
randomAccessSequences: readonly number[],
failedSequence: number,
): number | null {
if (!Number.isInteger(failedSequence) || failedSequence < 1) return null;
for (const sequence of randomAccessSequences) {
if (!Number.isInteger(sequence) || sequence < 1) return null;
if (sequence > failedSequence) return sequence;
}
return null;
}
export function recordedMediaRecoveryTargetSequence(
requestedSequence: number | null,
failedSequence: number | null,
recoverySequence: number | null,
): number | null {
if (requestedSequence === null) return null;
if (
!Number.isInteger(requestedSequence)
|| requestedSequence < 1
|| failedSequence === null
|| recoverySequence === null
|| !Number.isInteger(failedSequence)
|| !Number.isInteger(recoverySequence)
|| failedSequence < 1
|| recoverySequence <= failedSequence
) return requestedSequence;
return requestedSequence >= failedSequence && requestedSequence < recoverySequence
? recoverySequence
: requestedSequence;
}
export function recordedMediaSegmentAppendOrder(
appended: ReadonlySet<number>,
decodeStartSequence: number,
@@ -90,6 +125,32 @@ export function recordedMediaSegmentAppendOrder(
return missing;
}
/** Resolve a source-clock timestamp to the first fMP4 fragment covering it. */
export function recordedMediaSegmentSequenceAtTime(
segmentEndTimesSeconds: readonly number[],
epochStartSeconds: number,
currentSeconds: number,
): number | null {
if (
!segmentEndTimesSeconds.length ||
!Number.isFinite(epochStartSeconds) ||
!Number.isFinite(currentSeconds)
) return null;
const localSeconds = Math.max(0, currentSeconds - epochStartSeconds);
let left = 0;
let right = segmentEndTimesSeconds.length - 1;
while (left < right) {
const middle = Math.floor((left + right) / 2);
const endSeconds = segmentEndTimesSeconds[middle];
if (!Number.isFinite(endSeconds) || endSeconds <= 0) return null;
if (endSeconds + 0.001 >= localSeconds) right = middle;
else left = middle + 1;
}
const finalEndSeconds = segmentEndTimesSeconds[left];
if (!Number.isFinite(finalEndSeconds) || finalEndSeconds + 0.001 < localSeconds) return null;
return left + 1;
}
export function recordedMediaCanRollTarget(
previousSequence: number,
nextSequence: number,
@@ -121,6 +182,27 @@ function recordedMediaTimeRangesContain(
return false;
}
export function recordedMediaTimestampStallRecoveryTarget(
currentSeconds: number,
bufferedRanges: readonly (readonly [number, number])[],
skipSeconds = 0.18,
): number | null {
if (!Number.isFinite(currentSeconds) || !Number.isFinite(skipSeconds) || skipSeconds <= 0) {
return null;
}
for (const [startSeconds, endSeconds] of bufferedRanges) {
if (
!Number.isFinite(startSeconds)
|| !Number.isFinite(endSeconds)
|| currentSeconds < startSeconds - 0.05
|| currentSeconds > endSeconds
) continue;
const targetSeconds = Math.min(currentSeconds + skipSeconds, endSeconds - 0.05);
return targetSeconds >= currentSeconds + 0.04 ? targetSeconds : null;
}
return null;
}
export function recordedMediaPresentationState(
state: "loading" | "ready" | "error",
readyGeneration: string | null,
@@ -151,6 +233,24 @@ export function selectRecordedMediaEpoch(
return selected && currentSeconds <= selected.timelineEndSeconds ? selected : null;
}
/**
* Keep admission bounded even when the shared Rerun clock is currently before,
* between, or after camera epochs. Presentation still reports `waiting`; this
* selector only chooses the nearest epoch whose first/last fragment can prove
* that the camera transport is usable without downloading the whole MP4.
*/
export function selectRecordedMediaPreparationEpoch(
epochs: readonly ObservationRecordedMediaEpoch[],
currentSeconds: number,
): ObservationRecordedMediaEpoch | null {
if (!epochs.length || !Number.isFinite(currentSeconds)) return null;
const active = selectRecordedMediaEpoch(epochs, currentSeconds);
if (active) return active;
return epochs.find((epoch) => epoch.timelineStartSeconds > currentSeconds)
?? epochs.at(-1)
?? null;
}
export function recordedMediaSeekableCoverage(
durationSeconds: number,
seekableEndSeconds: number,
@@ -231,29 +331,14 @@ export async function fetchRecordedMediaArchive(
return { manifest, byteLength: totalBytes };
}
function videoHasSeekableArchive(
function waitForRecordedVideoInitialFrame(
video: HTMLVideoElement,
declaredDurationSeconds: number,
): boolean {
if (video.readyState < 1 || video.seekable.length < 1) return false;
return recordedMediaSeekableCoverage(
video.duration,
video.seekable.end(video.seekable.length - 1),
declaredDurationSeconds,
RECORDED_MEDIA_DURATION_TOLERANCE_SECONDS,
video.seekable.start(0),
);
}
function waitForSeekableArchive(
video: HTMLVideoElement,
declaredDurationSeconds: number,
signal: AbortSignal,
): Promise<void> {
if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
if (videoHasSeekableArchive(video, declaredDurationSeconds)) return Promise.resolve();
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) return Promise.resolve();
return new Promise((resolve, reject) => {
const events = ["loadedmetadata", "durationchange", "progress", "canplay"] as const;
const events = ["loadeddata", "canplay", "progress"] as const;
let stallTimer: ReturnType<typeof globalThis.setTimeout> | undefined;
const armStallTimer = () => {
if (stallTimer !== undefined) globalThis.clearTimeout(stallTimer);
@@ -270,7 +355,7 @@ function waitForSeekableArchive(
};
const onProgress = () => {
armStallTimer();
if (!videoHasSeekableArchive(video, declaredDurationSeconds)) return;
if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return;
cleanup();
resolve();
};
@@ -313,11 +398,7 @@ async function mountRecordedEpochStream(
};
video.load();
try {
await waitForSeekableArchive(
video,
descriptor.timelineEndSeconds - descriptor.timelineStartSeconds,
signal,
);
await waitForRecordedVideoInitialFrame(video, signal);
return cleanup;
} catch (error) {
cleanup();
@@ -336,6 +417,11 @@ interface RecordedSegmentTarget {
resetAttempts: number;
}
interface RecordedSegmentRecovery {
readonly failedSequence: number;
readonly recoverySequence: number;
}
interface RecordedSegmentStreamRuntime {
readonly generation: string;
readonly mediaSource: MediaSource;
@@ -716,6 +802,9 @@ export function RecordedFmp4Player({
onAdmissionChange,
onPlaybackChange,
onPlayingRejected,
playbackAuthority = "media",
playbackTransport = "segmented",
recoverTimestampStalls = false,
}: {
source: ObservationSourceDescriptor;
playback?: RecordedObservationPlayback | null;
@@ -728,6 +817,9 @@ export function RecordedFmp4Player({
onAdmissionChange?: (state: RecordedCameraAdmissionState) => void;
onPlaybackChange?: (playback: RecordedObservationPlayback) => void;
onPlayingRejected?: () => void;
playbackAuthority?: "media" | "host";
playbackTransport?: "segmented" | "epoch-stream";
recoverTimestampStalls?: boolean;
}) {
const videoRef = useRef<HTMLVideoElement>(null);
const onAdmissionChangeRef = useRef(onAdmissionChange);
@@ -768,12 +860,16 @@ export function RecordedFmp4Player({
);
const [archive, setArchive] = useState<RecordedMediaArchive | null>(null);
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [readyGeneration, setReadyGeneration] = useState<string | null>(null);
const [bufferRevision, setBufferRevision] = useState(0);
const [segmentRecoveryGeneration, setSegmentRecoveryGeneration] = useState(0);
const [segmentRecovery, setSegmentRecovery] = useState<RecordedSegmentRecovery | null>(null);
const segmentedRuntimeRef = useRef<RecordedSegmentStreamRuntime | null>(null);
const [segmentedRuntimeGeneration, setSegmentedRuntimeGeneration] = useState<string | null>(null);
const targetRevisionRef = useRef(0);
const targetReadyAbortRef = useRef<AbortController | null>(null);
const lastSegmentRecoveryRef = useRef<string | null>(null);
const playAttemptRevisionRef = useRef(0);
const currentSeconds = playback?.currentSeconds ?? contract?.timelineStartSeconds ?? 0;
const playbackPlayingRef = useRef(Boolean(playback?.playing));
@@ -781,25 +877,63 @@ export function RecordedFmp4Player({
const playbackRate = playback?.rate && Number.isFinite(playback.rate)
? Math.min(4, Math.max(0.25, playback.rate))
: 1;
const epoch = useMemo(
const playbackRateRef = useRef(playbackRate);
playbackRateRef.current = playbackRate;
const presentationEpoch = useMemo(
() => selectRecordedMediaEpoch(archive?.manifest.epochs ?? [], currentSeconds),
[archive?.manifest.epochs, currentSeconds],
);
const epoch = useMemo(
() => selectRecordedMediaPreparationEpoch(
archive?.manifest.epochs ?? [],
currentSeconds,
),
[archive?.manifest.epochs, currentSeconds],
);
const segmentClockSeconds = epoch
? Math.min(
Math.max(currentSeconds, epoch.timelineStartSeconds),
epoch.timelineEndSeconds,
)
: currentSeconds;
const effectiveSegmentCount = segmentCount ?? epoch?.segmentCount ?? null;
const requestedSegmentSequence = segmentSequence ?? (epoch
? recordedMediaSegmentSequenceAtTime(
epoch.segmentEndTimesSeconds,
epoch.timelineStartSeconds,
segmentClockSeconds,
)
: null);
const effectiveSegmentSequence = recordedMediaRecoveryTargetSequence(
requestedSegmentSequence,
segmentRecovery?.failedSequence ?? null,
segmentRecovery?.recoverySequence ?? null,
);
const holdingForSegmentRecovery = Boolean(
segmentRecovery
&& requestedSegmentSequence !== null
&& effectiveSegmentSequence !== requestedSegmentSequence,
);
const segmented = Boolean(
segmentCount !== null
&& Number.isInteger(segmentCount)
&& segmentCount >= 1
playbackTransport === "segmented"
&& requestedSegmentSequence !== null
&& Number.isInteger(requestedSegmentSequence)
&& requestedSegmentSequence >= 1
&&
effectiveSegmentCount !== null
&& Number.isInteger(effectiveSegmentCount)
&& effectiveSegmentCount >= 1
&& typeof MediaSource !== "undefined"
&& epoch
&& epoch.segmentCount === segmentCount
&& epoch.segmentCount === effectiveSegmentCount
&& epoch.randomAccessSequences.length > 0
&& epoch.segmentEndTimesSeconds.length === segmentCount
&& epoch.segmentEndTimesSeconds.length === effectiveSegmentCount
&& MediaSource.isTypeSupported(epoch.mediaType),
);
const directPlaybackSeconds = segmented ? null : currentSeconds;
const waitingForEpoch = Boolean(archive && !epoch);
const selectedGeneration = contract && epoch
? `${contract.manifestGenerationSha256}:${epoch.ordinal}:${epoch.timelineStartSeconds}:${epoch.timelineEndSeconds}`
const waitingForEpoch = Boolean(archive && !presentationEpoch);
const selectedGeneration = contract && presentationEpoch
? `${contract.manifestGenerationSha256}:${presentationEpoch.ordinal}:${presentationEpoch.timelineStartSeconds}:${presentationEpoch.timelineEndSeconds}`
: null;
const visualState = recordedMediaPresentationState(
state,
@@ -813,6 +947,7 @@ export function RecordedFmp4Player({
if (!contract) {
setArchive(null);
setReadyGeneration(null);
setErrorMessage("Некорректный descriptor записанной камеры.");
setState("error");
reportAdmission({
phase: "error",
@@ -825,6 +960,9 @@ export function RecordedFmp4Player({
const abort = new AbortController();
setArchive(null);
setReadyGeneration(null);
setSegmentRecovery(null);
lastSegmentRecoveryRef.current = null;
setErrorMessage(null);
setState("loading");
reportAdmission({
phase: "loading",
@@ -842,6 +980,7 @@ export function RecordedFmp4Player({
}
setArchive(null);
setReadyGeneration(null);
setErrorMessage("Архив записанной камеры не прошёл проверку.");
setState("error");
reportAdmission({
phase: "error",
@@ -853,43 +992,14 @@ export function RecordedFmp4Player({
}, [admissionKey, contract, prepare]);
useEffect(() => {
if (!archive || !contract || !prepare || segmented) return;
const abort = new AbortController();
let disposed = false;
void (async () => {
for (const candidate of archive.manifest.epochs) {
const probe = document.createElement("video");
probe.muted = true;
probe.playsInline = true;
const cleanup = await mountRecordedEpochStream(probe, candidate, abort.signal);
cleanup();
if (disposed || abort.signal.aborted) return;
}
if (disposed || abort.signal.aborted) return;
reportAdmission({
phase: "ready",
byteLength: archive.byteLength,
message: null,
});
})().catch((error: unknown) => {
if (
disposed ||
abort.signal.aborted ||
(error instanceof DOMException && error.name === "AbortError")
) return;
setReadyGeneration(null);
setState("error");
reportAdmission({
phase: "error",
byteLength: archive.byteLength,
message: "Не все codec epoch записанной камеры декодируются и доступны для seek.",
});
});
return () => {
disposed = true;
abort.abort();
};
}, [admissionKey, archive, contract, prepare, segmented]);
if (!segmentRecovery || requestedSegmentSequence === null) return;
if (
requestedSegmentSequence >= segmentRecovery.failedSequence
&& requestedSegmentSequence < segmentRecovery.recoverySequence
) return;
lastSegmentRecoveryRef.current = null;
setSegmentRecovery(null);
}, [requestedSegmentSequence, segmentRecovery]);
useEffect(() => {
const video = videoRef.current;
@@ -910,6 +1020,7 @@ export function RecordedFmp4Player({
setReadyGeneration(null);
setSegmentedRuntimeGeneration(null);
setErrorMessage(null);
setState("loading");
video.pause();
const sourceOpened = waitForMediaSourceOpen(mediaSource, abort.signal);
@@ -957,6 +1068,7 @@ export function RecordedFmp4Player({
return;
}
setReadyGeneration(null);
setErrorMessage("Покадровый буфер записанной камеры не открылся.");
setState("error");
reportAdmission({
phase: "error",
@@ -982,7 +1094,14 @@ export function RecordedFmp4Player({
}
URL.revokeObjectURL(objectUrl);
};
}, [archive, contract, epoch, segmentCount, segmented]);
}, [
archive,
contract,
effectiveSegmentCount,
epoch,
segmented,
segmentRecoveryGeneration,
]);
useEffect(() => {
const runtime = segmentedRuntimeRef.current;
@@ -993,18 +1112,19 @@ export function RecordedFmp4Player({
|| !archive
|| !segmented
|| segmentedRuntimeGeneration !== runtime.generation
|| segmentSequence === null
|| !Number.isInteger(segmentSequence)
|| segmentSequence < 1
|| segmentSequence > runtime.segmentCount
|| effectiveSegmentSequence === null
|| !Number.isInteger(effectiveSegmentSequence)
|| effectiveSegmentSequence < 1
|| effectiveSegmentSequence > runtime.segmentCount
) return;
const archiveByteLength = archive.byteLength;
const decodeStart = recordedMediaDecodeStartSequence(
runtime.randomAccessSequences,
segmentSequence,
effectiveSegmentSequence,
);
if (decodeStart === null) {
setReadyGeneration(null);
setErrorMessage("Для кадра записанной камеры нет random-access фрагмента.");
setState("error");
reportAdmission({
phase: "error",
@@ -1015,10 +1135,11 @@ export function RecordedFmp4Player({
}
const targetSeconds = recordedSegmentStartSeconds(
runtime.segmentEndTimesSeconds,
segmentSequence,
effectiveSegmentSequence,
);
if (targetSeconds === null) {
setReadyGeneration(null);
setErrorMessage("Для кадра записанной камеры нет точной media timestamp.");
setState("error");
reportAdmission({
phase: "error",
@@ -1030,15 +1151,15 @@ export function RecordedFmp4Player({
const previousTarget = runtime.target;
const readyEnd = Math.min(
runtime.segmentCount,
segmentSequence + RECORDED_MEDIA_REQUIRED_AHEAD_SEGMENTS,
effectiveSegmentSequence + RECORDED_MEDIA_REQUIRED_AHEAD_SEGMENTS,
);
const desiredEnd = Math.min(
runtime.segmentCount,
segmentSequence + RECORDED_MEDIA_SEGMENTS_AHEAD,
effectiveSegmentSequence + RECORDED_MEDIA_SEGMENTS_AHEAD,
);
const candidateTarget: RecordedSegmentTarget = {
revision: previousTarget?.revision ?? 0,
sequence: segmentSequence,
sequence: effectiveSegmentSequence,
decodeStart,
readyEnd,
desiredEnd,
@@ -1046,6 +1167,24 @@ export function RecordedFmp4Player({
forceReset: false,
resetAttempts: 0,
};
const recoverFromSegmentFailure = (failedSequence: number): boolean => {
const recoverySequence = nextRecordedMediaRandomAccessSequence(
runtime.randomAccessSequences,
failedSequence,
);
if (recoverySequence === null) return false;
const recoveryToken = `${runtime.generation}:${failedSequence}:${recoverySequence}`;
if (lastSegmentRecoveryRef.current === recoveryToken) return true;
lastSegmentRecoveryRef.current = recoveryToken;
setReadyGeneration(null);
setSegmentRecovery({ failedSequence, recoverySequence });
setErrorMessage(
`Восстанавливаем камеру с ключевого кадра ${recoverySequence}.`,
);
setState("loading");
setSegmentRecoveryGeneration((generation) => generation + 1);
return true;
};
const rollingTarget = Boolean(previousTarget && recordedMediaCanRollTarget(
previousTarget.sequence,
candidateTarget.sequence,
@@ -1059,21 +1198,62 @@ export function RecordedFmp4Player({
|| runtime.abort.signal.aborted
|| (error instanceof DOMException && error.name === "AbortError")
) return;
const failedSequence = runtime.target?.sequence ?? effectiveSegmentSequence;
if (failedSequence !== null && recoverFromSegmentFailure(failedSequence)) return;
const detail = error instanceof Error && error.message
? `: ${error.message}`
: ".";
const message = `Покадровый фрагмент записанной камеры недоступен${detail}`;
setReadyGeneration(null);
setErrorMessage(message);
setState("error");
reportAdmission({
phase: "error",
byteLength: archiveByteLength,
message: "Покадровый фрагмент записанной камеры недоступен.",
message,
});
};
const resumePlaybackIfRequested = async (revision: number) => {
if (
runtime.disposed
|| segmentedRuntimeRef.current !== runtime
|| runtime.target?.revision !== revision
|| !playbackPlayingRef.current
) return;
video.playbackRate = playbackRateRef.current;
try {
await video.play();
} catch {
if (
runtime.disposed
|| segmentedRuntimeRef.current !== runtime
|| runtime.target?.revision !== revision
) return;
// Canonical recorded LABs run one host-owned clock for camera and
// spatial evidence. A transient MSE play() rejection (commonly a
// pause/reset race while the next fragment is admitted) must stay a
// decoder concern: the rolling target will retry and catch up.
if (playbackAuthority === "host") return;
onPlayingRejectedRef.current?.();
setReadyGeneration(null);
setErrorMessage("Запуск записанной камеры отклонён браузером.");
setState("error");
reportAdmission({
phase: "error",
byteLength: archiveByteLength,
message: "Запуск записанной камеры отклонён браузером.",
});
}
};
if (rollingTarget && previousTarget) {
runtime.target = {
...candidateTarget,
revision: previousTarget.revision,
};
runtime.onTargetBuffered = null;
void pumpRecordedSegmentWindow(runtime).catch(reportPumpError);
void pumpRecordedSegmentWindow(runtime)
.then(() => resumePlaybackIfRequested(previousTarget.revision))
.catch(reportPumpError);
return;
}
@@ -1128,18 +1308,22 @@ export function RecordedFmp4Player({
setBufferRevision((revision) => revision + 1);
runtime.hasPresentedFrame = true;
setReadyGeneration(runtime.generation);
setErrorMessage(null);
setState("ready");
reportAdmission({
phase: "ready",
byteLength: archiveByteLength,
message: null,
});
await resumePlaybackIfRequested(bufferedTarget.revision);
} catch (error) {
if (
targetReadyAbort.signal.aborted
|| (error instanceof DOMException && error.name === "AbortError")
) return;
if (recoverFromSegmentFailure(bufferedTarget.sequence)) return;
setReadyGeneration(null);
setErrorMessage("Кадр записанной камеры не стал decoder-ready.");
setState("error");
reportAdmission({
phase: "error",
@@ -1160,7 +1344,13 @@ export function RecordedFmp4Player({
if (targetReadyAbortRef.current === targetReadyAbort) targetReadyAbortRef.current = null;
if (runtime.onTargetBuffered === markBuffered) runtime.onTargetBuffered = null;
};
}, [archive?.byteLength, segmentSequence, segmented, segmentedRuntimeGeneration]);
}, [
archive?.byteLength,
effectiveSegmentSequence,
playbackAuthority,
segmented,
segmentedRuntimeGeneration,
]);
useEffect(() => {
const video = videoRef.current;
@@ -1170,6 +1360,7 @@ export function RecordedFmp4Player({
? `${contract.manifestGenerationSha256}:${epochDescriptor.ordinal}:${epochDescriptor.timelineStartSeconds}:${epochDescriptor.timelineEndSeconds}`
: null;
setReadyGeneration(null);
setErrorMessage(null);
setState("loading");
const abort = new AbortController();
let disposed = false;
@@ -1185,7 +1376,13 @@ export function RecordedFmp4Player({
}
setBufferRevision((revision) => revision + 1);
setReadyGeneration(generation);
setErrorMessage(null);
setState("ready");
reportAdmission({
phase: "ready",
byteLength: archive?.byteLength ?? null,
message: null,
});
} catch (error) {
if (
disposed ||
@@ -1195,11 +1392,12 @@ export function RecordedFmp4Player({
return;
}
setReadyGeneration(null);
setErrorMessage("Записанная камера не открыла первый декодируемый кадр.");
setState("error");
reportAdmission({
phase: "error",
byteLength: archive?.byteLength ?? null,
message: "Записанная камера не стала seekable.",
message: "Записанная камера не открыла первый декодируемый кадр.",
});
}
};
@@ -1234,6 +1432,7 @@ export function RecordedFmp4Player({
video.currentTime = target;
} catch {
setReadyGeneration(null);
setErrorMessage("Seek записанной камеры завершился ошибкой.");
setState("error");
reportAdmission({
phase: "error",
@@ -1244,11 +1443,13 @@ export function RecordedFmp4Player({
}
}
video.playbackRate = playbackRate;
if (playback?.playing) {
if (playback?.playing && !holdingForSegmentRecovery) {
void video.play().catch(() => {
if (playAttemptRevisionRef.current !== playAttemptRevision) return;
if (playbackAuthority === "host") return;
onPlayingRejectedRef.current?.();
setReadyGeneration(null);
setErrorMessage("Запуск записанной камеры отклонён браузером.");
setState("error");
reportAdmission({
phase: "error",
@@ -1264,12 +1465,95 @@ export function RecordedFmp4Player({
bufferRevision,
directPlaybackSeconds,
epoch,
holdingForSegmentRecovery,
playback?.playing,
playbackAuthority,
playbackRate,
segmented,
visualState,
]);
useEffect(() => {
const video = videoRef.current;
if (!video || !recoverTimestampStalls || !playback?.playing || visualState !== "ready") {
return;
}
let lastSeconds = video.currentTime;
let lastProgressAtMs = performance.now();
const interval = window.setInterval(() => {
if (
!playbackPlayingRef.current
|| video.paused
|| video.ended
|| video.seeking
|| video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA
) {
lastSeconds = video.currentTime;
lastProgressAtMs = performance.now();
return;
}
const nowMs = performance.now();
if (video.currentTime >= lastSeconds + 0.02) {
lastSeconds = video.currentTime;
lastProgressAtMs = nowMs;
return;
}
if (nowMs - lastProgressAtMs < 1_250) return;
const bufferedRanges = Array.from(
{ length: video.buffered.length },
(_, index) => [video.buffered.start(index), video.buffered.end(index)] as const,
);
const targetSeconds = recordedMediaTimestampStallRecoveryTarget(
video.currentTime,
bufferedRanges,
);
lastProgressAtMs = nowMs;
if (targetSeconds === null) return;
// Field recordings can contain non-monotonic or corrupt H.264 timestamps.
// If decoded time is frozen despite proven buffered media ahead, skip only
// the broken timestamp interval and return authority to the media clock.
video.currentTime = targetSeconds;
lastSeconds = targetSeconds;
}, 250);
return () => window.clearInterval(interval);
}, [playback?.playing, recoverTimestampStalls, visualState]);
useEffect(() => {
const video = videoRef.current;
if (!video || !segmented || !playback?.playing || visualState !== "ready") return;
let cancelled = false;
const resumeIfDecoderReady = () => {
if (cancelled || !playbackPlayingRef.current || !video.paused) return;
const runtime = segmentedRuntimeRef.current;
const target = runtime?.target;
if (
!runtime
|| !target
|| runtime.disposed
|| video.seeking
|| video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA
|| Math.abs(video.currentTime - target.targetSeconds) > 0.25
|| !recordedMediaTimeRangesContain(video.buffered, target.targetSeconds)
) return;
playAttemptRevisionRef.current += 1;
const playAttemptRevision = playAttemptRevisionRef.current;
video.playbackRate = playbackRateRef.current;
void video.play().catch(() => {
if (cancelled || playAttemptRevisionRef.current !== playAttemptRevision) return;
if (playbackAuthority === "host") return;
onPlayingRejectedRef.current?.();
});
};
const queueResume = () => window.queueMicrotask(resumeIfDecoderReady);
const events = ["pause", "canplay", "seeked"] as const;
for (const event of events) video.addEventListener(event, queueResume);
resumeIfDecoderReady();
return () => {
cancelled = true;
for (const event of events) video.removeEventListener(event, queueResume);
};
}, [bufferRevision, playback?.playing, playbackAuthority, segmented, visualState]);
useEffect(() => {
const video = videoRef.current;
if ((!interactive && onPlaybackChange === undefined) || !video || !epoch || visualState !== "ready") return;
@@ -1327,7 +1611,9 @@ export function RecordedFmp4Player({
{visualState === "waiting"
? "Камера на этой позиции ещё не записывалась"
: visualState === "error"
? "Записанное видео недоступно"
? errorMessage ?? "Записанное видео недоступно"
: errorMessage
? errorMessage
: archive
? "Проверяем seek и codec записанного видео…"
: "Читаем manifest записанного видео…"}
@@ -11,10 +11,18 @@ import {
LIVE_RECEIVER_OPEN_CHECK_INTERVAL_MS,
requestLiveReceiverRecovery,
} from "../core/observation/liveReceiverWatchdog";
import {
claimExclusiveLiveViewer,
createReentrantViewerDisposer,
isLiveRerunPresentationReady,
liveRerunReceiverBindingIdentity,
liveTimelineNeedsSynchronization,
} from "../core/observation/liveRerunLifecycle";
import {
createLiveViewerDiagnosticLifecycle,
createLiveViewerInstanceId,
createLiveViewerLineage,
reloadRecordedViewerAfterStaleModuleFailure,
subscribeToLiveViewerBuildFence,
type LiveViewerFailureStage,
} from "../core/observation/liveViewerDiagnostics";
@@ -22,10 +30,63 @@ import {
fetchPerceptionPreparationStatus,
perceptionPreparationMessage,
} from "../core/observation/perceptionPreparation";
import type { RecordedAdmissionPhase } from "../core/observation/recordedSessionAdmission";
import {
RECORDED_BASE_POINT_COLOR_KEY,
canPublishRecordedPlaybackController,
createRecordedAutoplayGate,
createRecordedOpenWatchdog,
} from "../core/observation/recordedRerunLifecycle";
import type {
RecordedPerceptionLayers,
RecordedRerunView,
RecordedRrdArtifactDescriptor,
RerunPlaybackController,
RerunPlaybackState,
RerunViewerProfile,
RerunViewportStatus,
} from "../core/observation/viewerProfile";
export type {
RecordedPerceptionLayers,
RecordedRerunView,
RecordedRrdArtifactDescriptor,
RerunPlaybackController,
RerunPlaybackState,
RerunViewerProfile,
RerunViewportStatus,
} from "../core/observation/viewerProfile";
export {
attemptRecordedAutoplay,
canPublishRecordedPlaybackController,
createRecordedAutoplayGate,
createRecordedOpenWatchdog,
isRecordedPlaybackFullyBuffered,
isRecordedPlaybackPresentationReady,
isRecordedPlaybackReady,
isUsableRecordedPlaybackRange,
recordedOpenWatchdogTimeoutMs,
recordedPlaybackBufferState,
recordedPlaybackRangeWhenReady,
recordedPointColorKey,
rerunPresentationStatus,
type RecordedPlaybackBufferState,
} from "../core/observation/recordedRerunLifecycle";
export {
claimExclusiveLiveViewer,
createReentrantViewerDisposer,
isLiveRerunPresentationReady,
liveRerunReceiverBindingIdentity,
liveTimelineNeedsSynchronization,
} from "../core/observation/liveRerunLifecycle";
import {
isRecordedPlaybackReady,
recordedPlaybackBufferState,
recordedPlaybackRangeWhenReady,
recordedPointColorKey,
rerunPresentationStatus,
} from "../core/observation/recordedRerunLifecycle";
export type RerunViewportStatus = "idle" | "loading" | "ready" | "error";
export type RecordedRerunView = "spatial" | "perception" | "perception3d" | "metrics";
export type RecordedPerceptionLoadPhase =
| "idle"
| "loading"
@@ -46,55 +107,14 @@ export type RecordedPointColorLoadState = Pick<
"phase" | "receivedBytes" | "totalBytes" | "progress" | "message"
>;
export interface RecordedPerceptionLayers {
enabled: boolean;
detections2d: boolean;
segmentation: boolean;
cuboids3d: boolean;
}
export interface RerunSelection {
entityPath: string;
viewName?: string;
position?: [number, number, number];
}
export interface RerunPlaybackState {
recordingId: string;
timeline: string;
rangeNs: { min: number; max: number } | null;
currentNs: number;
playing: boolean;
/** Latest session-time value currently available to the browser receiver. */
bufferedEndNs: number | null;
/** Declared first session-time value, when the archive descriptor provides it. */
expectedStartNs: number | null;
/** Declared final session-time value, when the archive descriptor provides it. */
expectedEndNs: number | null;
/** Download progress in the closed interval 0..1, or null without a valid expectation. */
bufferProgress: number | null;
/** True only when the buffered range has reached the declared archive end. */
fullyBuffered: boolean;
}
export interface RerunPlaybackController {
seek: (timeNs: number) => void;
setPlaying: (playing: boolean) => void;
jumpToEnd: () => void;
}
export interface RerunViewportProps {
sourceUrl: string;
recordedArtifact?: RecordedRrdArtifactDescriptor | null;
followLive?: boolean;
liveActivitySequence?: number | null;
liveStreamId?: string | null;
liveRecoveryAuthorityIdentity?: string | null;
autoplayWhenReady?: boolean;
presentationGate?: RecordedAdmissionPhase;
expectedTimelineStartSeconds?: number;
expectedTimelineEndSeconds?: number;
initialPlaybackStartSeconds?: number;
profile: RerunViewerProfile;
onStatusChange?: (status: RerunViewportStatus, message?: string) => void;
onSelectionChange?: (selection: RerunSelection | null) => void;
onPlaybackChange?: (state: RerunPlaybackState | null) => void;
@@ -110,23 +130,10 @@ export interface RerunViewportProps {
| "palette"
| "customColor"
>;
recordedView?: RecordedRerunView;
recordedViewResetGeneration?: 0 | 1;
recordedFollowTrajectory?: boolean;
recordedPerceptionLayers?: RecordedPerceptionLayers;
recordedPerceptionRetryGeneration?: number;
lockPerceptionCameraInteraction?: boolean;
onPerceptionLoadChange?: (state: RecordedPerceptionLoadState) => void;
onPointColorLoadChange?: (state: RecordedPointColorLoadState) => void;
}
export interface RecordedRrdArtifactDescriptor {
sourceUrl: string;
viewerSourceUrl: string;
byteLength: number;
sha256: string;
}
interface RerunBlueprintChannel {
endpointUrl: string;
channel: {
@@ -147,375 +154,6 @@ const RECORDED_PERCEPTION_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][
const RECORDED_POINT_COLORS_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/point-colors\.rrd$/;
const MAX_BLUEPRINT_BYTES = 1_048_576;
const MAX_PERCEPTION_BYTES = 512 * 1024 * 1024;
const BUFFER_END_TOLERANCE_NS = 1_000_000;
const RECORDED_OPEN_MIN_TIMEOUT_MS = 120_000;
const RECORDED_OPEN_MAX_TIMEOUT_MS = 1_800_000;
const RECORDED_OPEN_GRACE_MS = 30_000;
const RECORDED_OPEN_MIN_BYTES_PER_SECOND = 2 * 1024 * 1024;
const RECORDED_BASE_POINT_COLOR_KEY = "intensity|turbo|-";
export function recordedPointColorKey(
settings: Pick<SceneSettings, "colorMode" | "palette" | "customColor">,
): string {
const custom = settings.palette === "custom" || settings.colorMode === "class"
? settings.customColor.toLowerCase()
: "-";
return `${settings.colorMode}|${settings.palette}|${custom}`;
}
export function recordedOpenWatchdogTimeoutMs(byteLength: number): number {
if (!Number.isSafeInteger(byteLength) || byteLength < 4) {
throw new Error("Unsafe recorded RRD byte length");
}
const transferBudgetMs = Math.ceil(
(byteLength / RECORDED_OPEN_MIN_BYTES_PER_SECOND) * 1_000,
);
return Math.min(
RECORDED_OPEN_MAX_TIMEOUT_MS,
Math.max(RECORDED_OPEN_MIN_TIMEOUT_MS, transferBudgetMs + RECORDED_OPEN_GRACE_MS),
);
}
export function createRecordedOpenWatchdog<T>({
byteLength,
schedule,
cancel,
onTimeout,
}: {
byteLength: number;
schedule: (callback: () => void, timeoutMs: number) => T;
cancel: (handle: T) => void;
onTimeout: () => void;
}): { arm: () => void; clear: () => void; pending: () => boolean } {
const timeoutMs = recordedOpenWatchdogTimeoutMs(byteLength);
let handle: T | null = null;
return {
arm() {
if (handle !== null) return;
handle = schedule(() => {
handle = null;
onTimeout();
}, timeoutMs);
},
clear() {
if (handle === null) return;
cancel(handle);
handle = null;
},
pending: () => handle !== null,
};
}
export function createReentrantViewerDisposer(
cleanupOnce: () => void,
releaseNativeViewer: () => void,
): () => void {
let cleanupComplete = false;
return () => {
try {
if (!cleanupComplete) {
cleanupComplete = true;
cleanupOnce();
}
} finally {
// `viewer.start()` can resolve after an earlier pre-ready stop. Reapply
// native release on every disposal boundary so that a stale viewer can
// never reopen after React and diagnostics have already unmounted it.
releaseNativeViewer();
}
};
}
interface ActiveLiveViewerOwner {
release: () => void;
}
let activeLiveViewerOwner: ActiveLiveViewerOwner | null = null;
/**
* Own exactly one native live receiver per application document.
*
* React route/StrictMode transitions can overlap two mounted workspaces for a
* render turn. Rerun keeps each native gRPC receiver alive independently, so
* the overlap used to consume the bounded live replay slots and leave the
* operator's visible canvas black. Claiming the next owner synchronously
* retires the previous native receiver before the next one starts.
*/
export function claimExclusiveLiveViewer(release: () => void): () => void {
const owner = { release };
const previous = activeLiveViewerOwner;
activeLiveViewerOwner = owner;
previous?.release();
return () => {
if (activeLiveViewerOwner === owner) activeLiveViewerOwner = null;
};
}
export interface RecordedPlaybackBufferState {
bufferedEndNs: number | null;
expectedStartNs: number | null;
expectedEndNs: number | null;
bufferProgress: number | null;
fullyBuffered: boolean;
}
/**
* A Rerun time range is usable as soon as it contains one finite timestamp.
* A first frame commonly has min === max; waiting for positive duration would
* needlessly keep that visible frame behind the loading screen.
*/
export function isUsableRecordedPlaybackRange(
rangeNs: { min: number; max: number } | null,
): rangeNs is { min: number; max: number } {
return Boolean(
rangeNs &&
Number.isFinite(rangeNs.min) &&
Number.isFinite(rangeNs.max) &&
rangeNs.max >= rangeNs.min,
);
}
/**
* A live receiver is presentable only after the exact browser store exposes
* real timeline data that the backend has also confirmed publishing.
* `WebViewer.start()` and a non-null active recording id are transport setup,
* not evidence that the spatial scene can render.
*/
export function isLiveRerunPresentationReady(
viewerStarted: boolean,
rangeNs: { min: number; max: number } | null,
backendActivitySequence: number | null,
): boolean {
return viewerStarted &&
Number.isSafeInteger(backendActivitySequence) &&
(backendActivitySequence ?? 0) > 0 &&
isUsableRecordedPlaybackRange(rangeNs);
}
/**
* `recording_open` can arrive before Rerun has registered the live timeline.
* Selecting it at that point is a silent no-op, so keep retrying only until
* the exact live timeline is both available and active.
*/
export function liveTimelineNeedsSynchronization(
followLive: boolean,
activeTimeline: string | null | undefined,
rangeNs: { min: number; max: number } | null,
): boolean {
return followLive &&
isUsableRecordedPlaybackRange(rangeNs) &&
activeTimeline !== "stream_time";
}
/**
* Key the native receiver to its data-plane binding. Recovery authority is a
* retry fence projected from changing supervisor snapshots; it must not tear
* down a healthy WebViewer while this acquisition and URL remain unchanged.
*/
export function liveRerunReceiverBindingIdentity(
sourceUrl: string,
liveStreamId: string | null,
followLive: boolean,
): string {
return JSON.stringify([
followLive ? "live" : "recorded",
sourceUrl.trim(),
followLive ? liveStreamId?.trim() ?? "" : "",
]);
}
/** Describe progressive archive availability without gating first rendering. */
export function recordedPlaybackBufferState(
rangeNs: { min: number; max: number } | null,
expectedTimelineEndSeconds?: number,
expectedTimelineStartSeconds?: number,
): RecordedPlaybackBufferState {
const usableRange = isUsableRecordedPlaybackRange(rangeNs) ? rangeNs : null;
const bufferedEndNs = usableRange?.max ?? null;
if (expectedTimelineEndSeconds === undefined) {
return {
bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null,
bufferProgress: null,
fullyBuffered: usableRange !== null,
};
}
if (!Number.isFinite(expectedTimelineEndSeconds) || expectedTimelineEndSeconds < 0) {
return {
bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null,
bufferProgress: null,
fullyBuffered: false,
};
}
const expectedEndNs = expectedTimelineEndSeconds * 1_000_000_000;
const expectedStartNs = expectedTimelineStartSeconds === undefined
? 0
: expectedTimelineStartSeconds * 1_000_000_000;
if (
!Number.isFinite(expectedEndNs) ||
!Number.isFinite(expectedStartNs) ||
expectedStartNs < 0 ||
expectedEndNs < expectedStartNs
) {
return {
bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null,
bufferProgress: null,
fullyBuffered: false,
};
}
// Rerun split preserves whole boundary chunks so that all generated splits
// still sum exactly to the source recording. A verified bounded artifact can
// therefore begin slightly before its declared operator window. Admission
// requires coverage of the declared window, not byte-chunk boundary equality.
const fullyBuffered = usableRange !== null &&
usableRange.min <= expectedStartNs + BUFFER_END_TOLERANCE_NS &&
usableRange.max >= expectedEndNs - BUFFER_END_TOLERANCE_NS;
const expectedDurationNs = expectedEndNs - expectedStartNs;
const bufferProgress = bufferedEndNs === null
? 0
: expectedDurationNs <= 0
? (fullyBuffered ? 1 : 0)
: Math.min(1, Math.max(0, (bufferedEndNs - expectedStartNs) / expectedDurationNs));
return {
bufferedEndNs,
expectedStartNs,
expectedEndNs,
bufferProgress,
fullyBuffered,
};
}
/** A recorded viewport is publishable only after its declared range is complete. */
export function isRecordedPlaybackReady(
viewerStarted: boolean,
artifactVerified: boolean,
buffer: RecordedPlaybackBufferState,
): boolean {
return viewerStarted && artifactVerified && buffer.fullyBuffered;
}
/** Keep the host scrubber and its controls disconnected from a partial archive. */
export function recordedPlaybackRangeWhenReady(
rangeNs: { min: number; max: number } | null,
buffer: RecordedPlaybackBufferState,
artifactVerified: boolean,
): { min: number; max: number } | null {
if (
!artifactVerified ||
!buffer.fullyBuffered ||
!isUsableRecordedPlaybackRange(rangeNs)
) return null;
return {
min: buffer.expectedStartNs === null
? rangeNs.min
: Math.max(rangeNs.min, buffer.expectedStartNs),
max: buffer.expectedEndNs === null
? rangeNs.max
: Math.min(rangeNs.max, buffer.expectedEndNs),
};
}
/** Do not mount host timeline controls while a recorded generation is partial. */
export function isRecordedPlaybackPresentationReady(
status: RerunViewportStatus,
playback: RerunPlaybackState | null,
): boolean {
return status === "ready" &&
playback?.fullyBuffered === true &&
isUsableRecordedPlaybackRange(playback.rangeNs);
}
export function rerunPresentationStatus(
status: RerunViewportStatus,
gate: RecordedAdmissionPhase,
recorded: boolean,
): RerunViewportStatus {
if (!recorded) return status;
if (status === "error" || gate === "error") return "error";
if (gate !== "ready") return "loading";
return status;
}
export function isRecordedPlaybackFullyBuffered(
rangeNs: { min: number; max: number } | null,
expectedTimelineEndSeconds?: number,
): boolean {
return recordedPlaybackBufferState(rangeNs, expectedTimelineEndSeconds).fullyBuffered;
}
export function attemptRecordedAutoplay(
seekToStart: () => void,
startPlaying: () => void,
): boolean {
try {
seekToStart();
startPlaying();
return true;
} catch {
return false;
}
}
/**
* Admit autoplay once, after the viewer and the complete recorded range are
* ready. The attempt is consumed even if a vendor call throws: subsequent
* polling must never seek the operator back to the beginning a second time.
*/
export function createRecordedAutoplayGate(): {
attempt: (
viewerStarted: boolean,
fullyBuffered: boolean,
presentationReady: boolean,
rangeNs: { min: number; max: number } | null,
seekToStart: (startNs: number) => void,
startPlaying: () => void,
preferredStartNs?: number,
) => boolean;
attempted: () => boolean;
} {
let consumed = false;
return {
attempt(
viewerStarted,
fullyBuffered,
presentationReady,
rangeNs,
seekToStart,
startPlaying,
preferredStartNs,
) {
if (
consumed ||
!viewerStarted ||
!fullyBuffered ||
!presentationReady ||
!isUsableRecordedPlaybackRange(rangeNs)
) return false;
consumed = true;
const startNs = Number.isFinite(preferredStartNs)
? Math.min(Math.max(preferredStartNs as number, rangeNs.min), rangeNs.max)
: rangeNs.min;
return attemptRecordedAutoplay(
() => seekToStart(startNs),
startPlaying,
);
},
attempted: () => consumed,
};
}
export function canPublishRecordedPlaybackController(
readyToRender: boolean,
presentationGate: RecordedAdmissionPhase,
): boolean {
return readyToRender && presentationGate === "ready";
}
export function createLatestAnimationFrameEmitter<T>({
emit,
@@ -917,36 +555,43 @@ export async function fetchRecordedPerceptionRrd(
}
export function RerunViewport({
sourceUrl,
recordedArtifact = null,
followLive = false,
liveActivitySequence = null,
liveStreamId = null,
liveRecoveryAuthorityIdentity = null,
autoplayWhenReady = false,
presentationGate = "ready",
expectedTimelineStartSeconds,
expectedTimelineEndSeconds,
initialPlaybackStartSeconds,
profile,
onStatusChange,
onSelectionChange,
onPlaybackChange,
onPlaybackControllerChange,
sceneSettings,
recordedView = "spatial",
recordedViewResetGeneration = 0,
recordedFollowTrajectory = false,
recordedPerceptionLayers = {
onPerceptionLoadChange,
onPointColorLoadChange,
}: RerunViewportProps) {
const recordedProfile = profile.kind === "recorded-session" ? profile : null;
const liveProfile = profile.kind === "live-acquisition" ? profile : null;
const sourceUrl = profile.sourceUrl;
const recordedArtifact = recordedProfile?.artifact ?? null;
const followLive = liveProfile !== null;
const liveActivitySequence = liveProfile?.liveActivitySequence ?? null;
const liveStreamId = liveProfile?.liveStreamId ?? null;
const liveRecoveryAuthorityIdentity =
liveProfile?.liveRecoveryAuthorityIdentity ?? null;
const autoplayWhenReady = recordedProfile?.autoplayWhenReady ?? false;
const presentationGate = recordedProfile?.presentationGate ?? "ready";
const expectedTimelineStartSeconds =
recordedProfile?.expectedTimelineStartSeconds;
const expectedTimelineEndSeconds = recordedProfile?.expectedTimelineEndSeconds;
const initialPlaybackStartSeconds = recordedProfile?.initialPlaybackStartSeconds;
const recordedView = recordedProfile?.view ?? "spatial";
const recordedViewResetGeneration = recordedProfile?.viewResetGeneration ?? 0;
const recordedFollowTrajectory = recordedProfile?.followTrajectory ?? false;
const recordedPerceptionLayers = recordedProfile?.perceptionLayers ?? {
enabled: false,
detections2d: false,
segmentation: false,
cuboids3d: false,
},
recordedPerceptionRetryGeneration = 0,
lockPerceptionCameraInteraction = false,
onPerceptionLoadChange,
onPointColorLoadChange,
}: RerunViewportProps) {
};
const recordedPerceptionRetryGeneration =
recordedProfile?.perceptionRetryGeneration ?? 0;
const lockPerceptionCameraInteraction =
recordedProfile?.lockPerceptionCameraInteraction ?? false;
const hostRef = useRef<HTMLDivElement>(null);
const [status, setStatus] = useState<RerunViewportStatus>(sourceUrl ? "loading" : "idle");
const [recordingBufferProgress, setRecordingBufferProgress] = useState<number | null>(null);
@@ -1588,7 +1233,7 @@ export function RerunViewport({
liveRecoveryRef.current = initialLiveReceiverRecoveryState();
}
if (!followLive) clearRecordedAdmissionWatchdog();
if (!followLive) setRecordingBufferProgress(1);
if (!followLive) setRecordingBufferProgress(recordedBuffer.bufferProgress);
recordedSceneAdmitted = true;
if (!followLive) onPlaybackChange?.(playbackState);
setStatus("ready");
@@ -1597,7 +1242,10 @@ export function RerunViewport({
if (
!followLive &&
!playbackControllerPublished &&
canPublishRecordedPlaybackController(readyToRender, presentationGateRef.current)
canPublishRecordedPlaybackController(
recordedBuffer.fullyBuffered,
presentationGateRef.current,
)
) {
playbackControllerPublished = true;
onPlaybackControllerChange?.(playbackController);
@@ -1740,9 +1388,16 @@ export function RerunViewport({
}
}
})
.catch(() => {
.catch(async () => {
if (disposed) return;
disposeViewer?.();
if (
isRecordedSource &&
await reloadRecordedViewerAfterStaleModuleFailure({
loadedUiBuildId: diagnosticLifecycle.lineage.uiBuildId,
signal: diagnosticLifecycle.signal,
})
) return;
if (requestLiveRecovery("module-load")) return;
reportError("Не удалось загрузить модуль визуализатора.");
});
@@ -1786,7 +1441,18 @@ export function RerunViewport({
}, [recordedPointColorsUrl]);
useEffect(() => {
if (!recordedPerceptionUrl) return;
if (!recordedPerceptionUrl || !recordedPerceptionLayers.enabled) {
if (!recordedPerceptionLayers.enabled) {
onPerceptionLoadChange?.({
phase: "idle",
receivedBytes: 0,
totalBytes: null,
progress: null,
message: "",
});
}
return;
}
const active = perceptionChannelRef.current;
const identity = recordedIdentityRef.current;
if (
@@ -1931,6 +1597,7 @@ export function RerunViewport({
}, [
onPerceptionLoadChange,
perceptionChannelRevision,
recordedPerceptionLayers.enabled,
recordedPerceptionRetryGeneration,
recordedPerceptionUrl,
]);
@@ -0,0 +1,245 @@
import { useCallback, useEffect, useState, type ReactNode } from "react";
import { SegmentedControl, SplitPane, type SplitPaneOrientation } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "./LaboratoryEvidenceViewer";
export const CANONICAL_RECORDED_LAB_REPLAY_CONTRACT =
"missioncore.canonical-recorded-lab-replay/v1";
export interface CanonicalRecordedLabMode<T extends string> {
value: T;
label: string;
}
/**
* The interaction contract from the accepted recorded-LAB instrument.
*
* Keeping pane selection, collapse semantics, responsive split orientation,
* expansion and splitter state here prevents individual experiments from
* quietly growing their own replay behaviour. Experiments provide evidence
* layers; they do not reimplement the laboratory shell.
*/
export function useCanonicalRecordedLabReplayState<
TMedia extends string,
TSpatial extends string,
>({
initialMediaMode,
initialSpatialMode,
}: {
initialMediaMode: TMedia;
initialSpatialMode: TSpatial | null;
}) {
const [mediaMode, setMediaMode] = useState<TMedia | null>(initialMediaMode);
const [spatialMode, setSpatialMode] = useState<TSpatial | null>(initialSpatialMode);
const [splitPrimarySize, setSplitPrimarySize] = useState(50);
const [splitOrientation, setSplitOrientation] = useState<SplitPaneOrientation>(() => (
typeof window !== "undefined" && window.matchMedia("(max-width: 900px)").matches
? "horizontal"
: "vertical"
));
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const query = window.matchMedia("(max-width: 900px)");
const update = () => setSplitOrientation(query.matches ? "horizontal" : "vertical");
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
const onMediaModeChange = useCallback((next: TMedia | "none") => {
if (next === "none") return;
setMediaMode((current) => current === next ? null : next);
}, []);
const onSpatialModeChange = useCallback((next: TSpatial | "none") => {
if (next === "none") return;
setSpatialMode((current) => current === next ? null : next);
}, []);
return {
mediaMode,
spatialMode,
splitView: mediaMode !== null && spatialMode !== null,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange,
onSpatialModeChange,
onSplitPrimarySizeChange: setSplitPrimarySize,
onExpandedChange: setExpanded,
};
}
export function CanonicalRecordedLabReplay<
TMedia extends string,
TSpatial extends string,
>({
label,
mediaMode,
mediaModes,
spatialMode,
spatialModes,
expanded,
splitPrimarySize,
splitOrientation,
mediaAriaLabel,
spatialAriaLabel,
mediaLayerControls,
spatialLayerControls,
spatialLeadingControl,
mediaMultiLayer = false,
mediaContent,
spatialContent,
emptyMessage,
deckOverlays,
actions,
overlay,
transport,
trailingActions,
onMediaModeChange,
onSpatialModeChange,
onExpandedChange,
onSplitPrimarySizeChange,
}: {
label: string;
mediaMode: TMedia;
mediaModes: readonly CanonicalRecordedLabMode<TMedia>[];
spatialMode: TSpatial;
spatialModes: readonly CanonicalRecordedLabMode<TSpatial>[];
expanded: boolean;
splitPrimarySize: number;
splitOrientation: SplitPaneOrientation;
mediaAriaLabel: string;
spatialAriaLabel: string;
mediaLayerControls?: ReactNode;
spatialLayerControls?: ReactNode;
spatialLeadingControl?: ReactNode;
mediaMultiLayer?: boolean;
mediaContent: ReactNode;
spatialContent: ReactNode;
emptyMessage: string;
deckOverlays?: ReactNode;
actions?: ReactNode;
overlay?: ReactNode;
transport?: ReactNode;
trailingActions?: ReactNode;
onMediaModeChange: (mode: TMedia) => void;
onSpatialModeChange: (mode: TSpatial) => void;
onExpandedChange: (expanded: boolean) => void;
onSplitPrimarySizeChange: (size: number) => void;
}) {
const splitView = mediaMode !== "none" && spatialMode !== "none";
const mediaModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="media">
<SegmentedControl
value={mediaMode}
items={[...mediaModes]}
label="Видео и камера"
onChange={onMediaModeChange}
/>
</div>
);
const spatialModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="spatial">
<SegmentedControl
value={spatialMode}
items={[...spatialModes]}
label="3D и план"
onChange={onSpatialModeChange}
/>
</div>
);
const mediaPane = (
<section
className="m4-replay-threat-visual__pane"
data-pane="media"
aria-label={mediaAriaLabel}
hidden={mediaMode === "none"}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="media"
data-multi-semantic={mediaMultiLayer ? "true" : undefined}
>
{mediaLayerControls}
{mediaModeControls}
</div>
) : null}
{mediaContent}
</section>
);
const spatialPane = spatialMode !== "none" ? (
<section
className="m4-replay-threat-visual__pane"
data-pane="spatial"
aria-label={spatialAriaLabel}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="spatial"
>
{spatialLeadingControl}
<div className="m4-replay-threat-visual__spatial-toolbar-end">
{spatialLayerControls}
{spatialModeControls}
</div>
</div>
) : null}
{spatialContent}
</section>
) : null;
return (
<div
className="l3-visual-audit m4-replay-threat-visual"
data-contract={CANONICAL_RECORDED_LAB_REPLAY_CONTRACT}
>
<LaboratoryEvidenceViewer
label={label}
className="m4-replay-threat-evidence-viewer"
mode={mediaMode}
modes={[...mediaModes]}
secondaryMode={{
value: spatialMode,
modes: [...spatialModes],
label: "3D и план",
onChange: onSpatialModeChange,
}}
expanded={expanded}
onModeChange={onMediaModeChange}
onExpandedChange={onExpandedChange}
modeControlsVisible={!splitView}
actions={actions}
overlay={overlay}
transport={transport}
trailingActions={trailingActions}
>
<div
className="m4-replay-threat-visual__deck"
data-split={splitView ? "true" : undefined}
data-empty={mediaMode === "none" && spatialMode === "none" ? "true" : undefined}
>
<SplitPane
primary={mediaPane}
secondary={spatialPane ?? <div />}
primarySize={splitView ? splitPrimarySize : mediaMode !== "none" ? 100 : 0}
onPrimarySizeChange={onSplitPrimarySizeChange}
orientation={splitOrientation}
minPrimarySize={splitView ? 24 : 0}
minSecondarySize={splitView ? 24 : 0}
resizable={splitView}
separatorLabel="Изменить размер VIDEO/CAMERA и 3D/PLAN"
/>
{mediaMode === "none" && spatialMode === "none" ? (
<div className="l3-visual-audit__state" role="status">
{emptyMessage}
</div>
) : null}
{deckOverlays}
</div>
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -59,6 +59,16 @@ export function laboratoryRecordedClipEndExclusiveNs(
return last.sourceTimeNs + typicalDelta;
}
export function laboratoryRecordedClipClockGate(
pendingSequence: number | null,
observedSequence: number,
): { accept: boolean; pendingSequence: number | null } {
if (pendingSequence !== null && pendingSequence !== observedSequence) {
return { accept: false, pendingSequence };
}
return { accept: true, pendingSequence: null };
}
export function LaboratoryRecordedClipPlayer({
source,
segmentCount,
@@ -94,7 +104,8 @@ export function LaboratoryRecordedClipPlayer({
}) {
const [companionSpatialSize, setCompanionSpatialSize] = useState(69);
const lastEmittedSequenceRef = useRef(sequence);
lastEmittedSequenceRef.current = sequence;
const lastObservedSequenceRef = useRef<number | null>(sequence);
const pendingSequenceRef = useRef<number | null>(null);
const frame = useMemo(
() => frames.find((candidate) => candidate.sequence === sequence) ?? frames[0] ?? null,
[frames, sequence],
@@ -113,23 +124,42 @@ export function LaboratoryRecordedClipPlayer({
if (!continuousPlayback && playing) onPlayingChange(false);
}, [continuousPlayback, onPlayingChange, playing]);
useEffect(() => {
if (lastObservedSequenceRef.current !== sequence) {
pendingSequenceRef.current = sequence;
}
lastEmittedSequenceRef.current = sequence;
}, [sequence]);
const emitSequence = useCallback((nextSequence: number) => {
if (lastEmittedSequenceRef.current === nextSequence) return;
lastEmittedSequenceRef.current = nextSequence;
onSequenceChange(nextSequence);
}, [onSequenceChange]);
const requestSequence = useCallback((nextSequence: number) => {
pendingSequenceRef.current = nextSequence;
emitSequence(nextSequence);
}, [emitSequence]);
const handlePlaybackChange = useCallback((next: RecordedObservationPlayback) => {
const sourceTimeNs = Math.round(next.currentSeconds * 1_000_000_000);
const first = frames[0];
if (!first || endExclusiveNs === null) return;
if (sourceTimeNs >= endExclusiveNs) {
emitSequence(first.sequence);
requestSequence(first.sequence);
return;
}
const nearest = nearestLaboratoryRecordedClipFrame(frames, sourceTimeNs);
if (nearest) emitSequence(nearest.sequence);
}, [emitSequence, endExclusiveNs, frames]);
if (!nearest) return;
lastObservedSequenceRef.current = nearest.sequence;
const gate = laboratoryRecordedClipClockGate(
pendingSequenceRef.current,
nearest.sequence,
);
pendingSequenceRef.current = gate.pendingSequence;
if (gate.accept) emitSequence(nearest.sequence);
}, [emitSequence, endExclusiveNs, frames, requestSequence]);
const timelineStart = frames[0]?.sourceTimeNs ?? 0;
const timelineEnd = frames.at(-1)?.sourceTimeNs ?? timelineStart + 1;
@@ -142,7 +172,7 @@ export function LaboratoryRecordedClipPlayer({
className="laboratory-recorded-clip-player__spatial"
aria-hidden={cameraPresentation === "primary"}
>
{cameraPresentation !== "primary" ? alternativeScene : null}
{alternativeScene}
</div>
);
const cameraPane = (
@@ -202,7 +232,7 @@ export function LaboratoryRecordedClipPlayer({
onPlayingChange={continuousPlayback ? onPlayingChange : undefined}
onSeek={(timeNs) => {
const nearest = nearestLaboratoryRecordedClipFrame(frames, timeNs);
if (nearest) emitSequence(nearest.sequence);
if (nearest) requestSequence(nearest.sequence);
}}
showJumpToEnd={false}
/>
@@ -1,8 +1,11 @@
import { useEffect, useState } from "react";
import {
RecordedFmp4Player,
type RecordedObservationPlayback,
} from "../RecordedFmp4Player";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
import type { RecordedCameraAdmissionState } from "../../core/observation/recordedSessionAdmission";
import {
RecordedEvidenceBoxOverlay,
type RecordedEvidenceBox,
@@ -25,6 +28,7 @@ export function RecordedEvidenceVideoScene({
imageWidth,
imageHeight,
boxes,
overlaySeconds,
semanticOverlay,
pointCloudOverlay,
ariaLabel,
@@ -33,12 +37,17 @@ export function RecordedEvidenceVideoScene({
segmentCount,
onPlaybackChange,
onPlayingRejected,
onAdmissionChange,
playbackAuthority = "media",
playbackTransport = "segmented",
recoverTimestampStalls = false,
}: {
source: ObservationSourceDescriptor;
playback: RecordedObservationPlayback;
imageWidth: number;
imageHeight: number;
boxes: readonly RecordedEvidenceBox[];
overlaySeconds?: number;
semanticOverlay?: RecordedEvidenceSemanticOverlay;
pointCloudOverlay?: RecordedEvidencePointCloudOverlayData;
ariaLabel: string;
@@ -47,7 +56,41 @@ export function RecordedEvidenceVideoScene({
segmentCount?: number;
onPlaybackChange?: (playback: RecordedObservationPlayback) => void;
onPlayingRejected?: () => void;
onAdmissionChange?: (state: RecordedCameraAdmissionState) => void;
playbackAuthority?: "media" | "host";
playbackTransport?: "segmented" | "epoch-stream";
recoverTimestampStalls?: boolean;
}) {
const generation = source.delivery?.kind === "recorded-fmp4-manifest"
? source.delivery.manifestGenerationSha256
: "invalid";
const [admissionPhase, setAdmissionPhase] = useState<RecordedCameraAdmissionState["phase"]>(
"loading",
);
const [presentedSeconds, setPresentedSeconds] = useState<number | null>(null);
useEffect(() => {
setAdmissionPhase("loading");
setPresentedSeconds(null);
}, [generation, source.id]);
const handleAdmissionChange = (next: RecordedCameraAdmissionState) => {
setAdmissionPhase(next.phase);
onAdmissionChange?.(next);
};
const sourceReady = admissionPhase === "ready";
const overlaysPresented = sourceReady
&& presentedSeconds !== null
&& Math.abs(presentedSeconds - playback.currentSeconds) <= 0.25
&& (overlaySeconds === undefined || Math.abs(presentedSeconds - overlaySeconds) <= 0.25);
const handlePlaybackChange = (next: RecordedObservationPlayback) => {
setPresentedSeconds(next.currentSeconds);
// During a paused operator seek the existing media element can emit its old
// timestamp while the requested MSE window is being rebuilt. That stale
// callback must not undo the host target before the decoder reaches it.
if (!playback.playing && Math.abs(next.currentSeconds - playback.currentSeconds) > 0.35) {
return;
}
onPlaybackChange?.(next);
};
return (
<div className="recorded-evidence-video-scene">
<RecordedFmp4Player
@@ -57,29 +100,35 @@ export function RecordedEvidenceVideoScene({
prepare
segmentSequence={segmentSequence}
segmentCount={segmentCount}
onPlaybackChange={onPlaybackChange}
onPlaybackChange={handlePlaybackChange}
onPlayingRejected={onPlayingRejected}
onAdmissionChange={handleAdmissionChange}
playbackAuthority={playbackAuthority}
playbackTransport={playbackTransport}
recoverTimestampStalls={recoverTimestampStalls}
/>
{semanticOverlay ? (
{overlaysPresented && semanticOverlay ? (
<RecordedEvidenceSemanticMaskOverlay
{...semanticOverlay}
imageWidth={imageWidth}
imageHeight={imageHeight}
/>
) : null}
{pointCloudOverlay ? (
{overlaysPresented && pointCloudOverlay ? (
<RecordedEvidencePointCloudOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
overlay={pointCloudOverlay}
/>
) : null}
<RecordedEvidenceBoxOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
boxes={boxes}
ariaLabel={ariaLabel}
/>
{overlaysPresented ? (
<RecordedEvidenceBoxOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
boxes={boxes}
ariaLabel={ariaLabel}
/>
) : null}
</div>
);
}
@@ -0,0 +1,116 @@
import { useEffect, useRef, useState } from "react";
import {
fetchCanonicalRecordedLabSpatialFrame,
type CanonicalRecordedLabSpatialFrame,
} from "../../core/laboratory/canonicalRecordedLabSpatial";
const FRAME_CACHE_LIMIT = 12;
/**
* Shared latest-request-wins scheduler for recorded LAB spatial evidence.
*
* A feature supplies only the sealed session identity and host-clock time.
* Cache ownership, identity fencing and stale-response suppression remain in
* the canonical instrument instead of being reimplemented per experiment.
*/
export function useCanonicalRecordedLabSpatialFrame({
sessionId,
generationSha256,
targetTimeNs,
}: {
sessionId: string;
generationSha256: string | 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 identityRef = useRef("");
const cacheRef = useRef(new Map<number, CanonicalRecordedLabSpatialFrame>());
const pumpRef = useRef<() => void>(() => undefined);
const identity = `${sessionId}:${generationSha256 ?? "unavailable"}`;
identityRef.current = identity;
pumpRef.current = () => {
if (runningRef.current || desiredRef.current === null || !generationSha256) return;
runningRef.current = true;
const requestIdentity = identity;
let settledTimeNs: number | null = null;
void (async () => {
while (
mountedRef.current
&& identityRef.current === requestIdentity
&& desiredRef.current !== null
) {
const requestedTimeNs = desiredRef.current;
const cached = cacheRef.current.get(requestedTimeNs);
try {
const next = cached ?? await fetchCanonicalRecordedLabSpatialFrame(
sessionId,
generationSha256,
requestedTimeNs,
);
if (!mountedRef.current || identityRef.current !== requestIdentity) break;
if (!cached) {
cacheRef.current.set(requestedTimeNs, next);
while (cacheRef.current.size > FRAME_CACHE_LIMIT) {
const oldest = cacheRef.current.keys().next().value as number | undefined;
if (oldest === undefined) break;
cacheRef.current.delete(oldest);
}
}
setFrame(next);
setError(null);
} catch (caught: unknown) {
if (!mountedRef.current || identityRef.current !== requestIdentity) break;
setError(caught instanceof Error
? caught.message
: "Spatial-слои записанной LAB недоступны.");
}
settledTimeNs = requestedTimeNs;
if (desiredRef.current === requestedTimeNs) break;
}
})().finally(() => {
runningRef.current = false;
if (
mountedRef.current
&& desiredRef.current !== null
&& (identityRef.current !== requestIdentity || desiredRef.current !== settledTimeNs)
) {
pumpRef.current();
}
});
};
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
desiredRef.current = null;
};
}, []);
useEffect(() => {
cacheRef.current.clear();
desiredRef.current = null;
setFrame(null);
setError(null);
}, [identity]);
useEffect(() => {
if (!generationSha256) return;
desiredRef.current = targetTimeNs;
const cached = cacheRef.current.get(targetTimeNs);
if (cached) {
setFrame(cached);
setError(null);
return;
}
pumpRef.current();
}, [generationSha256, identity, targetTimeNs]);
return { frame, error, loading: Boolean(generationSha256) && !frame && !error };
}
@@ -130,8 +130,12 @@ export function useRecordedEvidencePlayback(
const synchronize = useCallback((next: RecordedObservationPlayback) => {
if (!validRange(range) || !Number.isFinite(next.currentSeconds)) return;
// Animation-clock mode is retained only for non-media diagnostics. A
// recorded LAB with video uses the external media clock so spatial and
// overlays never advance past the frame the decoder actually presented.
if (clock === "animation") return;
setPlayback((current) => synchronizeRecordedEvidencePlayback(current, next, range));
}, [range]);
}, [clock, range]);
return useMemo(() => ({
playback,
@@ -118,6 +118,9 @@ export class PlayCanvasRuntime implements SimulationRuntime {
private renderIntervalMs = 1000 / 60;
private renderAccumulatorMs = 0;
private disposed = false;
private requestGsplatFrame = (): void => {
this.requestRender();
};
private updateFrame = (deltaSeconds: number): void => {
if (!this.app || !this.camera || this.disposed) return;
this.ugvController?.update(deltaSeconds);
@@ -158,6 +161,7 @@ export class PlayCanvasRuntime implements SimulationRuntime {
this.app = app;
app.autoRender = false;
app.on("update", this.updateFrame);
app.systems.gsplat?.on("frame:request", this.requestGsplatFrame);
app.setCanvasFillMode(FILLMODE_NONE, 1, 1);
app.setCanvasResolution(RESOLUTION_AUTO);
app.scene.ambientLight = new Color(0.35, 0.37, 0.42);
@@ -379,6 +383,7 @@ export class PlayCanvasRuntime implements SimulationRuntime {
if (this.app) {
this.app.autoRender = false;
this.app.renderNextFrame = false;
this.app.systems.gsplat?.off("frame:request", this.requestGsplatFrame);
this.app.off("update", this.updateFrame);
this.app.destroy();
}
@@ -24,7 +24,14 @@ const PHYSICS_PROXY_ERROR = 0.0003;
const PHYSICS_PROXY_MAX_TRIANGLES = 240_000;
const SERVICE_BRAKE_DECELERATION_MPS2 = 1.8;
const COAST_DECELERATION_MPS2 = 0.18;
const BRAKE_ATTITUDE_DAMPING = 6;
const PARKING_BRAKE_HOLD_DECELERATION_MPS2 = 6;
const PARKING_BRAKE_ENGAGE_SPEED_MPS = 0.08;
const TYRE_FRICTION_SLIP = 8.5;
const TYRE_STATIC_FRICTION_COEFFICIENT = 0.95;
const TYRE_KINETIC_FRICTION_COEFFICIENT = 0.78;
const TYRE_CONTACT_VELOCITY_RESPONSE_PER_SECOND = 10;
const GRAVITY_METERS_PER_SECOND_SQUARED = 9.81;
const MIN_TYRE_NORMAL_FORCE_NEWTONS = 1;
const DEFAULT_ORBIT_PITCH = 0.48;
const CAMERA_RETURN_DELAY_SECONDS = 1.2;
const CAMERA_RETURN_DURATION_SECONDS = 2;
@@ -79,12 +86,20 @@ interface NativeTransform extends NativeObject {
getRotation(): NativeQuaternion;
}
interface NativeRaycastInfo extends NativeObject {
get_m_contactNormalWS(): NativeVector3;
get_m_contactPointWS(): NativeVector3;
get_m_wheelAxleWS(): NativeVector3;
}
interface NativeWheelInfo extends NativeObject {
set_m_suspensionStiffness(value: number): void;
set_m_wheelsDampingRelaxation(value: number): void;
set_m_wheelsDampingCompression(value: number): void;
set_m_frictionSlip(value: number): void;
set_m_rollInfluence(value: number): void;
get_m_wheelsSuspensionForce(): number;
get_m_raycastInfo(): NativeRaycastInfo;
}
interface NativeRaycastVehicle extends NativeObject {
@@ -102,6 +117,7 @@ interface NativeRaycastVehicle extends NativeObject {
setBrake(force: number, wheel: number): void;
setSteeringValue(value: number, wheel: number): void;
getNumWheels(): number;
getWheelInfo(wheel: number): NativeWheelInfo;
updateWheelTransform(wheel: number, interpolated: boolean): void;
getWheelTransformWS(wheel: number): NativeTransform;
getForwardVector(): NativeVector3;
@@ -126,6 +142,11 @@ interface NativeDynamicsWorld extends NativeObject {
removeAction(action: NativeObject): void;
}
interface NativeRigidBody extends NativeObject {
applyImpulse(impulse: NativeVector3, relativePosition: NativeVector3): void;
setActivationState(state: number): void;
}
interface PhysicsSystemAccess {
systems: {
rigidbody: {
@@ -137,7 +158,7 @@ interface PhysicsSystemAccess {
interface NativeRigidBodyAccess {
rigidbody?: {
body: NativeObject | null;
body: NativeRigidBody | null;
linearVelocity: Vec3;
angularVelocity: Vec3;
teleport(position: Vec3, rotation?: Vec3 | Quat): void;
@@ -192,6 +213,13 @@ export class SimulationUgvController {
private readonly smoothedCamera = new Vec3();
private readonly limitedLinearVelocity = new Vec3();
private readonly limitedAngularVelocity = new Vec3();
private readonly tyreContactNormal = new Vec3();
private readonly tyreLateralDirection = new Vec3();
private readonly tyreLongitudinalDirection = new Vec3();
private readonly tyreContactPoint = new Vec3();
private readonly tyreRelativePosition = new Vec3();
private readonly tyreAngularContactVelocity = new Vec3();
private readonly tyreContactVelocity = new Vec3();
private readonly spawnPosition = UGV_SPAWN_POSITION.clone();
private orbitPointerId: number | null = null;
private orbitPointerX = 0;
@@ -204,8 +232,9 @@ export class SimulationUgvController {
private active = false;
private vehicleEntity: Entity | null = null;
private vehicle: NativeRaycastVehicle | null = null;
private vehicleTuning: NativeObject | null = null;
private vehicleRaycaster: NativeObject | null = null;
private tyreImpulseNative: NativeVector3 | null = null;
private tyreRelativePositionNative: NativeVector3 | null = null;
private dynamicsWorld: NativeDynamicsWorld | null = null;
private chassisMaterial: StandardMaterial | null = null;
private wheelMaterial: StandardMaterial | null = null;
@@ -308,9 +337,27 @@ export class SimulationUgvController {
const braking = this.pressed.has("Space");
const rigidbody = (this.vehicleEntity as Entity & NativeRigidBodyAccess).rigidbody;
const speedMetersPerSecond = this.vehicle.getCurrentSpeedKmHour() / 3.6;
const nativeForward = this.vehicle.getForwardVector();
const nativeForwardLength = Math.hypot(
nativeForward.x(),
nativeForward.y(),
nativeForward.z(),
);
const longitudinalSpeedMetersPerSecond = rigidbody && nativeForwardLength > 0.001
? Math.abs(
(
rigidbody.linearVelocity.x * nativeForward.x()
+ rigidbody.linearVelocity.y * nativeForward.y()
+ rigidbody.linearVelocity.z * nativeForward.z()
) / nativeForwardLength,
)
: Math.abs(speedMetersPerSecond);
const maxSpeed = this.settings.maxSpeedMetersPerSecond;
const maxTurnRate = this.settings.maxTurnRateDegrees * Math.PI / 180;
const pureTurn = !braking && forwardInput === 0 && turnInput !== 0;
const holding = !braking && forwardInput === 0 && turnInput === 0;
const parkingBrakeEngaged = holding
&& longitudinalSpeedMetersPerSecond <= PARKING_BRAKE_ENGAGE_SPEED_MPS;
const desiredSpeed = forwardInput * maxSpeed;
const speedError = desiredSpeed - speedMetersPerSecond;
const speedResponseRange = Math.max(0.35, maxSpeed * 0.2);
@@ -325,13 +372,27 @@ export class SimulationUgvController {
const leftCommand = clamp(forwardCommand - turnCommand, -1, 1);
const rightCommand = clamp(forwardCommand + turnCommand, -1, 1);
const engineForce = pureTurn ? pivotForcePerWheel : driveForcePerWheel;
const brakeDeceleration = braking
? SERVICE_BRAKE_DECELERATION_MPS2
: holding
? parkingBrakeEngaged
? PARKING_BRAKE_HOLD_DECELERATION_MPS2
: COAST_DECELERATION_MPS2
: 0;
const wheelBrakeForce = this.settings.massKg * brakeDeceleration
/ Math.max(1, this.wheelDefinitions.length);
for (let index = 0; index < this.wheelDefinitions.length; index += 1) {
const definition = this.wheelDefinitions[index];
const command = definition.left ? leftCommand : rightCommand;
// Parking contact is solved below with one 2D Coulomb limit; disable the
// raycast vehicle's parallel friction impulse so grip is not counted twice.
this.vehicle.getWheelInfo(index).set_m_frictionSlip(
parkingBrakeEngaged ? 0 : TYRE_FRICTION_SLIP,
);
this.vehicle.setSteeringValue(0, index);
this.vehicle.applyEngineForce(command * engineForce, index);
this.vehicle.setBrake(0, index);
this.vehicle.setBrake(wheelBrakeForce, index);
this.vehicle.updateWheelTransform(index, true);
const transform = this.vehicle.getWheelTransformWS(index);
const position = transform.getOrigin();
@@ -340,22 +401,21 @@ export class SimulationUgvController {
definition.anchor.setRotation(rotation.x(), rotation.y(), rotation.z(), rotation.w());
}
const body = rigidbody?.body ?? null;
if (rigidbody && body) {
this.applyParkingTyreContact(
deltaSeconds,
rigidbody,
body,
parkingBrakeEngaged,
);
}
if (rigidbody) {
const linearVelocity = rigidbody.linearVelocity;
let nextLinearX = linearVelocity.x;
let nextLinearZ = linearVelocity.z;
let horizontalSpeed = Math.hypot(nextLinearX, nextLinearZ);
const coasting = !braking && forwardInput === 0 && turnInput === 0;
if ((braking || coasting) && horizontalSpeed > 0.001) {
const deceleration = braking
? SERVICE_BRAKE_DECELERATION_MPS2
: COAST_DECELERATION_MPS2;
const nextSpeed = Math.max(0, horizontalSpeed - deceleration * Math.max(0, deltaSeconds));
const scale = nextSpeed / horizontalSpeed;
nextLinearX *= scale;
nextLinearZ *= scale;
horizontalSpeed = nextSpeed;
}
if (pureTurn && horizontalSpeed > 0.001) {
const pivotDamping = Math.exp(-Math.max(0, deltaSeconds) * 8);
nextLinearX *= pivotDamping;
@@ -372,9 +432,6 @@ export class SimulationUgvController {
rigidbody.linearVelocity = this.limitedLinearVelocity;
}
const angularVelocity = rigidbody.angularVelocity;
const attitudeDamping = braking
? Math.exp(-Math.max(0, deltaSeconds) * BRAKE_ATTITUDE_DAMPING)
: 1;
let nextAngularY = angularVelocity.y;
if (pureTurn) {
const desiredYawRate = -turnInput * maxTurnRate;
@@ -387,24 +444,137 @@ export class SimulationUgvController {
} else if (Math.abs(angularVelocity.y) > maxTurnRate) {
nextAngularY = Math.sign(angularVelocity.y) * maxTurnRate;
}
if (attitudeDamping !== 1 || nextAngularY !== angularVelocity.y) {
if (nextAngularY !== angularVelocity.y) {
this.limitedAngularVelocity.set(
angularVelocity.x * attitudeDamping,
angularVelocity.x,
nextAngularY,
angularVelocity.z * attitudeDamping,
angularVelocity.z,
);
rigidbody.angularVelocity = this.limitedAngularVelocity;
}
}
const body = (this.vehicleEntity as Entity & NativeRigidBodyAccess).rigidbody?.body as {
setActivationState?: (state: number) => void;
} | null;
body?.setActivationState?.(DISABLE_DEACTIVATION);
body?.setActivationState(DISABLE_DEACTIVATION);
if (this.vehicleEntity.getPosition().y < -8) this.reset();
this.updateCamera(deltaSeconds);
}
private applyParkingTyreContact(
deltaSeconds: number,
rigidbody: NonNullable<NativeRigidBodyAccess["rigidbody"]>,
body: NativeRigidBody,
parkingBrakeEngaged: boolean,
): void {
if (!this.vehicle || !this.vehicleEntity || !this.tyreImpulseNative
|| !this.tyreRelativePositionNative) return;
if (!parkingBrakeEngaged) return;
const timeStep = Math.min(Math.max(0, deltaSeconds), 1 / 30);
if (timeStep === 0) return;
const wheelEffectiveMass = this.settings.massKg
/ Math.max(1, this.wheelDefinitions.length);
const chassisPosition = this.vehicleEntity.getPosition();
for (let index = 0; index < this.wheelDefinitions.length; index += 1) {
const wheel = this.vehicle.getWheelInfo(index);
const raycast = wheel.get_m_raycastInfo();
const normalForce = wheel.get_m_wheelsSuspensionForce();
if (!Number.isFinite(normalForce) || normalForce < MIN_TYRE_NORMAL_FORCE_NEWTONS) {
continue;
}
const nativeNormal = raycast.get_m_contactNormalWS();
this.tyreContactNormal.set(nativeNormal.x(), nativeNormal.y(), nativeNormal.z());
if (this.tyreContactNormal.lengthSq() < 0.001) continue;
this.tyreContactNormal.normalize();
const nativeAxle = raycast.get_m_wheelAxleWS();
this.tyreLateralDirection.set(nativeAxle.x(), nativeAxle.y(), nativeAxle.z());
this.tyreLateralDirection.addScaled(
this.tyreContactNormal,
-this.tyreLateralDirection.dot(this.tyreContactNormal),
);
if (this.tyreLateralDirection.lengthSq() < 0.001) continue;
this.tyreLateralDirection.normalize();
this.tyreLongitudinalDirection.cross(
this.tyreContactNormal,
this.tyreLateralDirection,
).normalize();
const nativeContactPoint = raycast.get_m_contactPointWS();
this.tyreContactPoint.set(
nativeContactPoint.x(),
nativeContactPoint.y(),
nativeContactPoint.z(),
);
this.tyreRelativePosition.sub2(this.tyreContactPoint, chassisPosition);
this.tyreAngularContactVelocity.cross(
rigidbody.angularVelocity,
this.tyreRelativePosition,
);
this.tyreContactVelocity.add2(
rigidbody.linearVelocity,
this.tyreAngularContactVelocity,
);
const lateralSlipSpeed = this.tyreContactVelocity.dot(this.tyreLateralDirection);
const longitudinalSlipSpeed = this.tyreContactVelocity.dot(
this.tyreLongitudinalDirection,
);
// Static tyre friction is a contact constraint: it balances the component
// of gravity along the surface and damps slip at the contact patch. The
// force still passes through a Coulomb circle and is applied at the wheel,
// so the chassis remains a fully dynamic rigid body.
const lateralGravityAcceleration = -GRAVITY_METERS_PER_SECOND_SQUARED
* this.tyreLateralDirection.y;
const longitudinalGravityAcceleration = -GRAVITY_METERS_PER_SECOND_SQUARED
* this.tyreLongitudinalDirection.y;
const trialLateralForce = -wheelEffectiveMass * (
lateralGravityAcceleration
+ TYRE_CONTACT_VELOCITY_RESPONSE_PER_SECOND * lateralSlipSpeed
);
const trialLongitudinalForce = -wheelEffectiveMass * (
longitudinalGravityAcceleration
+ TYRE_CONTACT_VELOCITY_RESPONSE_PER_SECOND * longitudinalSlipSpeed
);
const staticFrictionLimit = TYRE_STATIC_FRICTION_COEFFICIENT * normalForce;
let lateralForce = trialLateralForce;
let longitudinalForce = trialLongitudinalForce;
if (Math.hypot(trialLateralForce, trialLongitudinalForce) > staticFrictionLimit) {
const slipSpeed = Math.hypot(lateralSlipSpeed, longitudinalSlipSpeed);
const kineticFrictionLimit = TYRE_KINETIC_FRICTION_COEFFICIENT * normalForce;
if (slipSpeed > 0.0001) {
lateralForce = -(lateralSlipSpeed / slipSpeed) * kineticFrictionLimit;
longitudinalForce = -(longitudinalSlipSpeed / slipSpeed) * kineticFrictionLimit;
} else {
const forceScale = staticFrictionLimit
/ Math.hypot(trialLateralForce, trialLongitudinalForce);
lateralForce = trialLateralForce * forceScale;
longitudinalForce = trialLongitudinalForce * forceScale;
}
}
const lateralImpulse = lateralForce * timeStep;
const longitudinalImpulse = longitudinalForce * timeStep;
this.tyreImpulseNative.setValue(
this.tyreLateralDirection.x * lateralImpulse
+ this.tyreLongitudinalDirection.x * longitudinalImpulse,
this.tyreLateralDirection.y * lateralImpulse
+ this.tyreLongitudinalDirection.y * longitudinalImpulse,
this.tyreLateralDirection.z * lateralImpulse
+ this.tyreLongitudinalDirection.z * longitudinalImpulse,
);
this.tyreRelativePositionNative.setValue(
this.tyreRelativePosition.x,
this.tyreRelativePosition.y,
this.tyreRelativePosition.z,
);
body.applyImpulse(this.tyreImpulseNative, this.tyreRelativePositionNative);
}
}
private async createStaticCollisionBodies(collisionWorld: Entity): Promise<void> {
const models = collisionWorld.findComponents("model") as ModelComponent[];
if (models.length === 0) throw new Error("В слое коллизий нет геометрии для физики UGV.");
@@ -428,7 +598,12 @@ export class SimulationUgvController {
this.app,
meshInstance.mesh,
new Mat4().mul2(rootInverse, meshInstance.node.getWorldTransform()),
targetRatio,
Math.max(
1,
Math.floor(
Math.floor((meshInstance.mesh.primitive[0]?.count ?? 0) / 3) * targetRatio,
),
),
);
if (!physicsMesh) continue;
const entity = new Entity(`UGV physics proxy ${index + 1}`);
@@ -464,8 +639,9 @@ export class SimulationUgvController {
type: "dynamic",
mass: this.settings.massKg,
friction: 0.85,
linearDamping: 0.08,
angularDamping: 0.45,
rollingFriction: 0.12,
linearDamping: 0.12,
angularDamping: 0.6,
});
this.chassisMaterial = createMaterial(readThemeAccent(), new Color(0.03, 0.04, 0.05));
@@ -525,7 +701,10 @@ export class SimulationUgvController {
applyMaterial(wheelMesh, this.wheelMaterial);
anchor.addChild(wheelMesh);
vehicle.addChild(anchor);
this.wheelDefinitions.push({ ...definition, anchor });
this.wheelDefinitions.push({
...definition,
anchor,
});
}
vehicle.setLocalPosition(this.spawnPosition);
@@ -559,16 +738,17 @@ export class SimulationUgvController {
wheel.set_m_suspensionStiffness(24);
wheel.set_m_wheelsDampingRelaxation(3.2);
wheel.set_m_wheelsDampingCompression(4.8);
wheel.set_m_frictionSlip(5.5);
wheel.set_m_frictionSlip(TYRE_FRICTION_SLIP);
wheel.set_m_rollInfluence(0.08);
}
this.ammo.destroy(axle);
this.ammo.destroy(direction);
this.ammo.destroy(connection);
this.tyreImpulseNative = new this.ammo.btVector3(0, 0, 0);
this.tyreRelativePositionNative = new this.ammo.btVector3(0, 0, 0);
dynamicsWorld.addAction(nativeVehicle);
this.vehicleEntity = vehicle;
this.vehicleTuning = tuning;
this.vehicleRaycaster = raycaster;
this.vehicle = nativeVehicle;
this.dynamicsWorld = dynamicsWorld;
@@ -684,10 +864,12 @@ export class SimulationUgvController {
}
if (this.vehicle) runCleanup("destroy vehicle", () => this.ammo.destroy(this.vehicle as NativeObject));
if (this.vehicleRaycaster) runCleanup("destroy vehicle raycaster", () => this.ammo.destroy(this.vehicleRaycaster as NativeObject));
if (this.vehicleTuning) runCleanup("destroy vehicle tuning", () => this.ammo.destroy(this.vehicleTuning as NativeObject));
if (this.tyreImpulseNative) runCleanup("destroy tyre impulse vector", () => this.ammo.destroy(this.tyreImpulseNative as NativeObject));
if (this.tyreRelativePositionNative) runCleanup("destroy tyre relative-position vector", () => this.ammo.destroy(this.tyreRelativePositionNative as NativeObject));
this.vehicle = null;
this.vehicleRaycaster = null;
this.vehicleTuning = null;
this.tyreImpulseNative = null;
this.tyreRelativePositionNative = null;
this.dynamicsWorld = null;
if (this.vehicleEntity) runCleanup("destroy vehicle entity", () => this.vehicleEntity?.destroy());
@@ -824,7 +1006,7 @@ function createPhysicsProxyMesh(
app: Application,
source: Mesh,
localTransform: Mat4,
targetRatio: number,
targetTriangleCount: number,
): Mesh | null {
const primitive = source.primitive[0];
const sourceVertexCount = source.vertexBuffer?.numVertices ?? 0;
@@ -858,8 +1040,11 @@ function createPhysicsProxyMesh(
if (triangleIndexCount < 3) return null;
if (triangleIndexCount !== indices.length) indices = indices.slice(0, triangleIndexCount);
if (targetRatio < 1) {
const targetIndexCount = Math.max(3, Math.floor(indices.length * targetRatio / 3) * 3);
const targetIndexCount = Math.max(
3,
Math.min(indices.length, Math.floor(targetTriangleCount) * 3),
);
if (indices.length > targetIndexCount) {
const [simplifiedIndices] = MeshoptSimplifier.simplify(
indices,
transformed,
@@ -868,6 +1053,26 @@ function createPhysicsProxyMesh(
PHYSICS_PROXY_ERROR,
);
indices = new Uint32Array(simplifiedIndices);
if (indices.length > targetIndexCount) {
// Topologically noisy scanner meshes can make the quality simplifier stop
// above its requested target. Ammo must never receive that unbounded result:
// the spatial fallback preserves the surface envelope while enforcing the
// same deterministic physics budget for every imported location.
const [boundedIndices] = MeshoptSimplifier.simplifySloppy(
indices,
transformed,
3,
null,
targetIndexCount,
1,
);
indices = new Uint32Array(boundedIndices);
}
if (indices.length > targetIndexCount) {
throw new Error(
`Physics proxy превысил лимит: ${Math.floor(indices.length / 3)} > ${Math.floor(targetIndexCount / 3)} треугольников.`,
);
}
}
const compactedIndices = new Uint32Array(indices);
@@ -29,7 +29,6 @@ export interface E31LaboratoryResult {
limitations: readonly string[];
access: "read-only";
}
export interface E32LaboratoryResult {
resultId: string;
createdAtUtc: string | null;
@@ -53,7 +52,6 @@ export interface E32LaboratoryResult {
};
access: "read-only";
}
export interface E33LaboratoryResult {
resultId: string;
createdAtUtc: string | null;
@@ -0,0 +1,99 @@
export const CANONICAL_RECORDED_LAB_TGS_HISTORY_SECONDS = 1;
export const CANONICAL_RECORDED_LAB_SPATIAL_PROFILE = "source-paced-ground-v3";
export interface CanonicalRecordedLabPackedCellEvidence {
centersBodyXyM: Float32Array;
zBoundsM: Float32Array;
stateCodes: Uint8Array;
}
export interface CanonicalRecordedLabBodyGroundFrame {
originMapXyzM: readonly [number, number, number];
sensorOriginMapXyzM: readonly [number, number, number];
basisMapFromBody: readonly [
readonly [number, number, number],
readonly [number, number, number],
readonly [number, number, number],
];
}
export interface CanonicalRecordedLabTgsCostmap {
centersXyM: readonly (readonly [number, number])[];
stateCodes: readonly number[];
zBoundsM: readonly (readonly [number | null, number | null])[];
}
export function canonicalRecordedLabTgsIsCurrent(
currentTimeNs: number,
anchorTimeNs: number,
historySeconds = CANONICAL_RECORDED_LAB_TGS_HISTORY_SECONDS,
): boolean {
if (
!Number.isSafeInteger(currentTimeNs)
|| !Number.isSafeInteger(anchorTimeNs)
|| !Number.isFinite(historySeconds)
|| historySeconds <= 0
) return false;
const ageNs = currentTimeNs - anchorTimeNs;
return ageNs >= 0 && ageNs <= Math.round(historySeconds * 1_000_000_000);
}
export function canonicalMapGravityLocalPointToBodyGround(
point: readonly [number, number, number],
anchor: CanonicalRecordedLabBodyGroundFrame,
current: CanonicalRecordedLabBodyGroundFrame,
): readonly [number, number, number] {
// TGS is translation-only map-gravity-local: its axes are map axes and its
// origin is the LiDAR at the source frame. It is not an anchor body frame.
const map: readonly [number, number, number] = [
anchor.sensorOriginMapXyzM[0] + point[0],
anchor.sensorOriginMapXyzM[1] + point[1],
anchor.sensorOriginMapXyzM[2] + point[2],
];
const delta: readonly [number, number, number] = [
map[0] - current.originMapXyzM[0],
map[1] - current.originMapXyzM[1],
map[2] - current.originMapXyzM[2],
];
return [
current.basisMapFromBody[0][0] * delta[0]
+ current.basisMapFromBody[1][0] * delta[1]
+ current.basisMapFromBody[2][0] * delta[2],
current.basisMapFromBody[0][1] * delta[0]
+ current.basisMapFromBody[1][1] * delta[1]
+ current.basisMapFromBody[2][1] * delta[2],
current.basisMapFromBody[0][2] * delta[0]
+ current.basisMapFromBody[1][2] * delta[1]
+ current.basisMapFromBody[2][2] * delta[2],
];
}
export function canonicalRecordedLabPackedTgsCells(
costmap: CanonicalRecordedLabTgsCostmap,
anchor: CanonicalRecordedLabBodyGroundFrame,
current: CanonicalRecordedLabBodyGroundFrame,
): CanonicalRecordedLabPackedCellEvidence {
if (
costmap.centersXyM.length !== costmap.stateCodes.length
|| costmap.centersXyM.length !== costmap.zBoundsM.length
) throw new Error("Canonical recorded LAB TGS accounting changed");
const centers: number[] = [];
const zBounds: number[] = [];
costmap.centersXyM.forEach(([x, y], index) => {
const bounds = costmap.zBoundsM[index] ?? [null, null];
const center = canonicalMapGravityLocalPointToBodyGround([x, y, 0], anchor, current);
centers.push(center[0], center[1]);
if (bounds[0] === null || bounds[1] === null) {
zBounds.push(Number.NaN, Number.NaN);
return;
}
const bottom = canonicalMapGravityLocalPointToBodyGround([x, y, bounds[0]], anchor, current);
const top = canonicalMapGravityLocalPointToBodyGround([x, y, bounds[1]], anchor, current);
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(costmap.stateCodes),
};
}
@@ -0,0 +1,259 @@
import type { LaboratoryFetch } from "./advancedResults";
import { CANONICAL_RECORDED_LAB_SPATIAL_PROFILE } from "./canonicalRecordedLab";
const SAFE_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const SHA256 = /^[a-f0-9]{64}$/;
export class CanonicalRecordedLabSpatialContractError extends Error {}
export interface CanonicalRecordedLabSpatialFrame {
targetTimeNs: number;
sourceTimeNs: number;
poseTimeNs: number;
trajectoryTimeNs: number;
sourcePointCount: number;
coordinateFrame: "body-ground";
sensorHeight: {
meters: number;
source: "local-source-cloud-ground-quantile-median" | "session-source-cloud-fallback";
sampleCount: number;
madM: number;
authority: "visual-derived";
};
spatialProfile: {
profileId: typeof CANONICAL_RECORDED_LAB_SPATIAL_PROFILE;
localSlamHistorySeconds: number;
localSlamRadiusM: number;
localSlamVoxelSizeM: number;
localSlamPointLimit: number;
};
bodyFrame: {
originMapXyzM: readonly [number, number, number];
sensorOriginMapXyzM: readonly [number, number, number];
basisMapFromBody: readonly [
readonly [number, number, number],
readonly [number, number, number],
readonly [number, number, number],
];
};
sourcePointsBodyXyzM: readonly (readonly [number, number, number])[];
localSlamSourceFrameCount: number;
localSlamSourcePointCount: number;
localSlamBodyXyzM: readonly (readonly [number, number, number])[];
}
function objectValue(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new CanonicalRecordedLabSpatialContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function arrayValue(value: unknown, label: string): unknown[] {
if (!Array.isArray(value)) {
throw new CanonicalRecordedLabSpatialContractError(`${label}: ожидался массив.`);
}
return value;
}
function exact(value: unknown, expected: unknown, label: string): void {
if (value !== expected) {
throw new CanonicalRecordedLabSpatialContractError(`${label}: контракт изменён.`);
}
}
function numberValue(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new CanonicalRecordedLabSpatialContractError(`${label}: ожидалось число.`);
}
return value;
}
function integerValue(value: unknown, label: string): number {
const parsed = numberValue(value, label);
if (!Number.isSafeInteger(parsed) || parsed < 0) {
throw new CanonicalRecordedLabSpatialContractError(`${label}: ожидалось целое значение.`);
}
return parsed;
}
function pointList(
value: unknown,
label: string,
): readonly (readonly [number, number, number])[] {
return arrayValue(value, label).map((entry, index) => {
const point = arrayValue(entry, `${label}[${index}]`).map(
(channel, channelIndex) => numberValue(channel, `${label}[${index}][${channelIndex}]`),
);
if (point.length !== 3) {
throw new CanonicalRecordedLabSpatialContractError(`${label}[${index}]: размер изменён.`);
}
return [point[0]!, point[1]!, point[2]!] as const;
});
}
export async function fetchCanonicalRecordedLabSpatialFrame(
sessionId: string,
generationSha256: string,
targetTimeNs: number,
{
fetcher = fetch,
signal,
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<CanonicalRecordedLabSpatialFrame> {
if (
!SAFE_SESSION_ID.test(sessionId)
|| !SHA256.test(generationSha256)
|| !Number.isSafeInteger(targetTimeNs)
|| targetTimeNs < 0
) {
throw new CanonicalRecordedLabSpatialContractError(
"Canonical LAB spatial identity недопустима.",
);
}
const query = new URLSearchParams({
generation: generationSha256,
time_ns: String(targetTimeNs),
profile: CANONICAL_RECORDED_LAB_SPATIAL_PROFILE,
});
const response = await fetcher(
`/api/v1/observation-sessions/${encodeURIComponent(sessionId)}`
+ `/canonical-lab/spatial-frame?${query.toString()}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new CanonicalRecordedLabSpatialContractError(
`Canonical LAB spatial frame недоступен: HTTP ${response.status}.`,
);
}
const payload = objectValue(await response.json(), "canonical_lab.spatial_frame");
exact(
payload.schema_version,
"missioncore.canonical-recorded-lab-spatial-frame/v3",
"canonical_lab.spatial_frame.schema_version",
);
exact(payload.coordinate_frame, "body-ground", "canonical_lab.spatial_frame.coordinate_frame");
exact(payload.target_time_ns, targetTimeNs, "canonical_lab.spatial_frame.target_time_ns");
const sourcePoints = pointList(
payload.source_points_body_xyz_m,
"canonical_lab.spatial_frame.source_points",
);
const localSlam = pointList(
payload.local_slam_body_xyz_m,
"canonical_lab.spatial_frame.local_slam",
);
const sourcePointCount = integerValue(
payload.source_point_count,
"canonical_lab.spatial_frame.source_point_count",
);
const localSlamPointCount = integerValue(
payload.local_slam_point_count,
"canonical_lab.spatial_frame.local_slam_point_count",
);
if (
sourcePointCount !== sourcePoints.length
|| sourcePointCount > 100_000
|| localSlamPointCount !== localSlam.length
|| localSlam.length > 27_000
) {
throw new CanonicalRecordedLabSpatialContractError(
"Canonical LAB spatial accounting изменён.",
);
}
const bodyFrame = objectValue(payload.body_frame, "canonical_lab.spatial_frame.body_frame");
const origin = pointList(
[bodyFrame.origin_map_xyz_m],
"canonical_lab.spatial_frame.body_frame.origin",
)[0]!;
const sensorOrigin = pointList(
[bodyFrame.sensor_origin_map_xyz_m],
"canonical_lab.spatial_frame.body_frame.sensor_origin",
)[0]!;
const basisRows = pointList(
bodyFrame.basis_map_from_body,
"canonical_lab.spatial_frame.body_frame.basis",
);
if (basisRows.length !== 3) {
throw new CanonicalRecordedLabSpatialContractError(
"Canonical LAB spatial basis изменён.",
);
}
const sensorHeight = objectValue(payload.sensor_height, "canonical_lab.spatial_frame.sensor_height");
if (
sensorHeight.source !== "local-source-cloud-ground-quantile-median"
&& sensorHeight.source !== "session-source-cloud-fallback"
) {
throw new CanonicalRecordedLabSpatialContractError(
"canonical_lab.spatial_frame.sensor_height.source: контракт изменён.",
);
}
exact(
sensorHeight.authority,
"visual-derived",
"canonical_lab.spatial_frame.sensor_height.authority",
);
const spatialProfile = objectValue(
payload.spatial_profile,
"canonical_lab.spatial_frame.spatial_profile",
);
exact(
spatialProfile.profile_id,
CANONICAL_RECORDED_LAB_SPATIAL_PROFILE,
"canonical_lab.spatial_frame.spatial_profile.profile_id",
);
return {
targetTimeNs,
sourceTimeNs: integerValue(payload.source_time_ns, "canonical_lab.spatial_frame.source_time_ns"),
poseTimeNs: integerValue(payload.pose_time_ns, "canonical_lab.spatial_frame.pose_time_ns"),
trajectoryTimeNs: integerValue(
payload.trajectory_time_ns,
"canonical_lab.spatial_frame.trajectory_time_ns",
),
sourcePointCount,
coordinateFrame: "body-ground",
sensorHeight: {
meters: numberValue(sensorHeight.meters, "canonical_lab.spatial_frame.sensor_height.meters"),
source: sensorHeight.source,
sampleCount: integerValue(
sensorHeight.sample_count,
"canonical_lab.spatial_frame.sensor_height.sample_count",
),
madM: numberValue(sensorHeight.mad_m, "canonical_lab.spatial_frame.sensor_height.mad_m"),
authority: "visual-derived",
},
spatialProfile: {
profileId: CANONICAL_RECORDED_LAB_SPATIAL_PROFILE,
localSlamHistorySeconds: numberValue(
spatialProfile.local_slam_history_seconds,
"canonical_lab.spatial_frame.spatial_profile.history",
),
localSlamRadiusM: numberValue(
spatialProfile.local_slam_radius_m,
"canonical_lab.spatial_frame.spatial_profile.radius",
),
localSlamVoxelSizeM: numberValue(
spatialProfile.local_slam_voxel_size_m,
"canonical_lab.spatial_frame.spatial_profile.voxel",
),
localSlamPointLimit: integerValue(
spatialProfile.local_slam_point_limit,
"canonical_lab.spatial_frame.spatial_profile.limit",
),
},
bodyFrame: {
originMapXyzM: origin,
sensorOriginMapXyzM: sensorOrigin,
basisMapFromBody: [basisRows[0]!, basisRows[1]!, basisRows[2]!],
},
sourcePointsBodyXyzM: sourcePoints,
localSlamSourceFrameCount: integerValue(
payload.local_slam_source_frame_count,
"canonical_lab.spatial_frame.local_slam_source_frames",
),
localSlamSourcePointCount: integerValue(
payload.local_slam_source_point_count,
"canonical_lab.spatial_frame.local_slam_source_points",
),
localSlamBodyXyzM: localSlam,
};
}
@@ -154,6 +154,9 @@ export interface M4ThreatTimelineFrame {
pointCloudSourceCount: number;
pointCloudSampleCount: number;
pointCloudLayer: "current-increment";
localSlamBodyXyzM?: readonly M4Point3[];
localSlamSourceFrameCount?: number;
localSlamSourcePointCount?: number;
cameraProjectedPointsXyd: readonly (readonly [number, number, number])[];
cameraProjectedSourceCount: number;
cameraProjectedPointCount: number;
@@ -168,10 +171,11 @@ export interface M4ThreatTimelineFrame {
export interface M4ThreatTimeline {
resultId: string;
recordedSourceSessionId: "20260720T065719Z_viewer_live";
recordedSourceSessionId: string;
recordedSourceId: string;
imageWidth: 800;
imageHeight: 600;
frameCount: 4489;
frameCount: number;
frameTimesNs: readonly number[];
timelineStartSeconds: number;
timelineEndSeconds: number;
@@ -234,7 +238,7 @@ export interface M4ThreatPlaybackProgress {
export interface M4ThreatPlaybackPointPack {
resultId: string;
frameCount: 4489;
frameCount: number;
pointCount: number;
pointOffsets: Uint32Array;
pointsMapXyzM: Float32Array;
@@ -257,7 +261,7 @@ export interface M4ThreatPlaybackChunkDescriptor {
export interface M4ThreatPlaybackManifest {
resultId: string;
frameCount: 4489;
frameCount: number;
pointCount: number;
pointOffsets: Uint32Array;
chunkFrameCount: 24;
@@ -337,7 +341,7 @@ const motion = (value: unknown): M4ThreatMotion => {
};
const resultId = (value: unknown): string => {
const parsed = text(value, "M4.6 result id");
if (!/^m4-threat-replay-[a-f0-9]{64}$/.test(parsed)) {
if (!/^[a-z0-9][a-z0-9-]{0,127}-[a-f0-9]{64}$/.test(parsed)) {
throw new M4ThreatContractError("M4.6 result id: нарушена идентичность.");
}
return parsed;
@@ -704,12 +708,8 @@ export async function fetchM4ThreatTimeline(
exact(payload.result_id, result, "M4.6 timeline result");
exact(payload.authority, "replay-simulated", "M4.6 timeline authority");
const recorded = object(payload.recorded_source, "M4.6 recorded source");
exact(
recorded.session_id,
"20260720T065719Z_viewer_live",
"M4.6 recorded session",
);
exact(recorded.source_id, "RAVNOVES00", "M4.6 recorded source id");
const recordedSessionId = text(recorded.session_id, "M4.6 recorded session");
const recordedSourceId = text(recorded.source_id, "M4.6 recorded source id");
exact(
recorded.representation_id,
"registered-map-increment-v1",
@@ -720,7 +720,10 @@ export async function fetchM4ThreatTimeline(
"host-arrival-best-effort",
"M4.6 recorded synchronization",
);
const frameCount = exact(payload.frame_count, 4489, "M4.6 timeline frame count");
const frameCount = integer(payload.frame_count, "M4.6 timeline frame count");
if (frameCount < 1) {
throw new M4ThreatContractError("M4.6 timeline frame count: пустой timeline.");
}
const frameTimesNs = array(payload.frame_times_ns, "M4.6 timeline index").map(
(value) => integer(value, "M4.6 timeline time"),
);
@@ -738,7 +741,8 @@ export async function fetchM4ThreatTimeline(
);
return {
resultId: result,
recordedSourceSessionId: "20260720T065719Z_viewer_live",
recordedSourceSessionId: recordedSessionId,
recordedSourceId,
imageWidth: exact(payload.image_width, 800, "M4.6 image width"),
imageHeight: exact(payload.image_height, 600, "M4.6 image height"),
frameCount,
@@ -849,12 +853,14 @@ export async function fetchM4ThreatTimelineChunk(
endpointRoot = M4_THREAT_TIMELINE_ENDPOINT_ROOT,
cameraObstacleProjectionDelivery = null,
playbackPointPack,
includePoints = true,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
endpointRoot?: string;
cameraObstacleProjectionDelivery?: M4ThreatTimeline["cameraObstacleProjectionDelivery"];
playbackPointPack?: M4ThreatPlaybackPointPack;
includePoints?: boolean;
} = {},
): Promise<M4ThreatTimelineChunk> {
const params = new URLSearchParams({
@@ -864,7 +870,7 @@ export async function fetchM4ThreatTimelineChunk(
if (cameraObstacleProjectionDelivery !== null) {
params.set("obstacle_projection", cameraObstacleProjectionDelivery);
}
if (playbackPointPack) params.set("include_points", "false");
if (playbackPointPack || !includePoints) params.set("include_points", "false");
const response = await fetcher(
`${endpointRoot}/${result}/timeline/chunk?${params}`,
{ headers: { Accept: "application/json" }, signal },
@@ -995,7 +1001,10 @@ export async function fetchM4ThreatPlaybackManifest(
exact(manifest.result_id, result, "M4.6 playback result");
exact(manifest.coordinate_frame, "map", "M4.6 playback coordinate frame");
exact(manifest.access, "read-only-sealed-binary-playback", "M4.6 playback access");
const frameCount = exact(integer(manifest.frame_count, "M4.6 playback frames"), 4489, "M4.6 playback frames");
const frameCount = integer(manifest.frame_count, "M4.6 playback frames");
if (frameCount < 1) {
throw new M4ThreatContractError("M4.6 playback frames: пустой playback недопустим.");
}
const pointCount = integer(manifest.point_count, "M4.6 playback points");
const offsetsRaw = array(manifest.point_offsets, "M4.6 playback offsets");
if (offsetsRaw.length !== frameCount + 1) {
@@ -1332,6 +1341,17 @@ function parseTimelineFrame(
"current-increment",
"M4.6 timeline point layer",
),
localSlamBodyXyzM: item.local_slam_body_xyz_m === undefined
? []
: array(item.local_slam_body_xyz_m, "M4.6 local SLAM points").map(
(point) => vector(point, 3, "M4.6 local SLAM point") as [number, number, number],
),
localSlamSourceFrameCount: item.local_slam_source_frame_count === undefined
? undefined
: integer(item.local_slam_source_frame_count, "M4.6 local SLAM source frames"),
localSlamSourcePointCount: item.local_slam_source_point_count === undefined
? undefined
: integer(item.local_slam_source_point_count, "M4.6 local SLAM source points"),
cameraProjectedPointsXyd: item.camera_projected_points_xyd === undefined
? []
: array(item.camera_projected_points_xyd, "M4.6 camera points").map(
@@ -0,0 +1,57 @@
import {
LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE,
type LaboratoryRecordedEvidenceViewerProfile,
} from "../observation/viewerProfile";
export type LaboratoryRecordedMediaMode = "video" | "camera" | null;
export type LaboratoryRecordedSpatialMode = "3d" | "plan" | null;
export type LaboratoryClassifiedSpatialMode = "none" | "overlay" | "replace-source";
export interface LaboratoryRecordedEvidenceVisibility {
mediaMode: LaboratoryRecordedMediaMode;
spatialMode: LaboratoryRecordedSpatialMode;
showMediaSemantic: boolean;
showSpatialSemantic: boolean;
showMediaPoints: boolean;
classifiedSpatialMode: LaboratoryClassifiedSpatialMode;
}
export interface LaboratoryRecordedEvidenceDemand {
sourceTimelineMetadata: true;
recordedVideo: boolean;
exactCameraFrame: boolean;
sourceSpatialPoints: boolean;
cameraPointOverlay: boolean;
selectedSemanticMask: boolean;
selectedSemanticPoints: boolean;
classifiedSpatial: boolean;
}
/**
* Translate the already-visible M4 evidence composition into explicit data
* demand. The profile never starts a hidden point, semantic or camera channel
* merely because the selected LAB happens to publish that artifact.
*/
export function laboratoryRecordedEvidenceDemand(
visibility: LaboratoryRecordedEvidenceVisibility,
profile: LaboratoryRecordedEvidenceViewerProfile =
LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE,
): LaboratoryRecordedEvidenceDemand {
if (profile.loadPolicy !== "visible-evidence-only") {
throw new Error("Unsupported LAB recorded evidence load policy");
}
const mediaVisible = visibility.mediaMode !== null;
const spatialVisible = visibility.spatialMode !== null;
return {
sourceTimelineMetadata: true,
recordedVideo: visibility.mediaMode === "video",
exactCameraFrame: visibility.mediaMode === "camera",
sourceSpatialPoints:
spatialVisible && visibility.classifiedSpatialMode !== "replace-source",
cameraPointOverlay: mediaVisible && visibility.showMediaPoints,
selectedSemanticMask: mediaVisible && visibility.showMediaSemantic,
selectedSemanticPoints: spatialVisible && visibility.showSpatialSemantic,
classifiedSpatial:
spatialVisible && visibility.classifiedSpatialMode !== "none",
};
}
@@ -106,6 +106,18 @@ export interface VegetationMixedRouteReview {
cases: readonly VegetationMixedRouteCase[];
}
export interface VegetationRouteTgsAnchor {
sourceSequence: number;
slot: number;
currentPointsXyzM: readonly (readonly [number, number, number])[];
costmap: {
cellSizeM: 0.45;
centersXyM: readonly (readonly [number, number])[];
stateCodes: readonly number[];
zBoundsM: readonly (readonly [number | null, number | null])[];
};
}
export interface VegetationFullRouteLayer {
name: string;
resultId: string;
@@ -119,6 +131,7 @@ export interface VegetationFullRouteLayer {
export interface VegetationFullRouteReview {
sourceId: "RAVNOVES004TREE";
sessionId: "20260828T130511Z_viewer_live";
linkedRouteReviewResultId: string;
sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0";
sourceJobInputSha256: string;
sourceStreamSha256: string;
@@ -692,6 +705,15 @@ function fullRouteReviewValue(value: unknown): VegetationFullRouteReview | null
"recorded-camera-eb2783c5480d56bda07c8af0",
"vegetation.route_full_review.source_job_id",
);
const linkedRouteReviewResultId = textValue(
row.linked_route_review_result_id,
"vegetation.route_full_review.linked_route_review_result_id",
);
if (!RESULT_ID.test(linkedRouteReviewResultId)) {
throw new VegetationShadowContractError(
"vegetation.route_full_review: linked route review identity invalid.",
);
}
exact(row.frame_count, 6830, "vegetation.route_full_review.frame_count");
exact(row.width, 800, "vegetation.route_full_review.width");
exact(row.height, 600, "vegetation.route_full_review.height");
@@ -810,6 +832,7 @@ function fullRouteReviewValue(value: unknown): VegetationFullRouteReview | null
return {
sourceId: "RAVNOVES004TREE",
sessionId: "20260828T130511Z_viewer_live",
linkedRouteReviewResultId,
sourceJobId: "recorded-camera-eb2783c5480d56bda07c8af0",
sourceJobInputSha256,
sourceStreamSha256,
@@ -931,6 +954,78 @@ export function vegetationFullRouteMaskUrl(
return `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/route-masks/${layer}/${sequence}`;
}
export async function fetchVegetationRouteTgsAnchor(
resultId: string,
sourceSequence: number,
{
fetcher = fetch,
signal,
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<VegetationRouteTgsAnchor> {
if (!RESULT_ID.test(resultId) || !Number.isInteger(sourceSequence) || sourceSequence < 1) {
throw new VegetationShadowContractError("Vegetation TGS anchor identity недопустима.");
}
const response = await fetcher(
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}`
+ `/route-tgs-anchor/${sourceSequence}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new VegetationShadowContractError(`Vegetation TGS anchor недоступен: HTTP ${response.status}.`);
}
const payload = objectValue(await response.json(), "vegetation.route_tgs_anchor");
exact(
payload.schema_version,
"missioncore.lab-v1-route-tgs-anchor/v1",
"vegetation.route_tgs_anchor.schema_version",
);
exact(payload.source_sequence, sourceSequence, "vegetation.route_tgs_anchor.source_sequence");
const pointValue = (value: unknown, label: string): readonly number[] => {
const point = arrayValue(value, label).map((item, index) => numberValue(item, `${label}[${index}]`));
if (point.length !== 2 && point.length !== 3) {
throw new VegetationShadowContractError(`${label}: размер изменён.`);
}
return point;
};
const points = arrayValue(payload.current_points_xyz_m, "vegetation.route_tgs_anchor.points")
.map((value, index) => pointValue(value, `vegetation.route_tgs_anchor.points[${index}]`));
const costmap = objectValue(payload.costmap, "vegetation.route_tgs_anchor.costmap");
exact(costmap.cell_size_m, 0.45, "vegetation.route_tgs_anchor.costmap.cell_size_m");
const centers = arrayValue(costmap.centers_xy_m, "vegetation.route_tgs_anchor.costmap.centers")
.map((value, index) => pointValue(value, `vegetation.route_tgs_anchor.costmap.centers[${index}]`));
const stateCodes = arrayValue(costmap.state_codes, "vegetation.route_tgs_anchor.costmap.states")
.map((value, index) => integerValue(value, `vegetation.route_tgs_anchor.costmap.states[${index}]`));
const zBounds = arrayValue(costmap.z_bounds_m, "vegetation.route_tgs_anchor.costmap.z_bounds")
.map((value, index) => {
const row = arrayValue(value, `vegetation.route_tgs_anchor.costmap.z_bounds[${index}]`);
if (row.length !== 2 || row.some((item) => item !== null && (typeof item !== "number" || !Number.isFinite(item)))) {
throw new VegetationShadowContractError("vegetation.route_tgs_anchor.costmap.z_bounds: контракт изменён.");
}
return row as readonly [number | null, number | null];
});
if (
centers.length !== 2244
|| stateCodes.length !== 2244
|| zBounds.length !== 2244
|| stateCodes.some((value) => value > 3)
|| points.some((point) => point.length !== 3)
|| centers.some((point) => point.length !== 2)
) {
throw new VegetationShadowContractError("Vegetation TGS anchor shape изменён.");
}
return {
sourceSequence,
slot: integerValue(payload.slot, "vegetation.route_tgs_anchor.slot"),
currentPointsXyzM: points.map((point) => [point[0]!, point[1]!, point[2]!] as const),
costmap: {
cellSizeM: 0.45,
centersXyM: centers.map((point) => [point[0]!, point[1]!] as const),
stateCodes,
zBoundsM: zBounds,
},
};
}
export async function fetchVegetationShadowResult(
resultId: string,
{
@@ -0,0 +1,70 @@
import { isUsableRecordedPlaybackRange } from "./recordedRerunLifecycle";
export function createReentrantViewerDisposer(
cleanupOnce: () => void,
releaseNativeViewer: () => void,
): () => void {
let cleanupComplete = false;
return () => {
try {
if (!cleanupComplete) {
cleanupComplete = true;
cleanupOnce();
}
} finally {
// A deferred native start can resolve after a pre-ready stop. Reapply
// release at every boundary so it cannot reopen after React unmount.
releaseNativeViewer();
}
};
}
interface ActiveLiveViewerOwner {
release: () => void;
}
let activeLiveViewerOwner: ActiveLiveViewerOwner | null = null;
/** Own exactly one native live receiver per application document. */
export function claimExclusiveLiveViewer(release: () => void): () => void {
const owner = { release };
const previous = activeLiveViewerOwner;
activeLiveViewerOwner = owner;
previous?.release();
return () => {
if (activeLiveViewerOwner === owner) activeLiveViewerOwner = null;
};
}
export function isLiveRerunPresentationReady(
viewerStarted: boolean,
rangeNs: { min: number; max: number } | null,
backendActivitySequence: number | null,
): boolean {
return viewerStarted
&& Number.isSafeInteger(backendActivitySequence)
&& (backendActivitySequence ?? 0) > 0
&& isUsableRecordedPlaybackRange(rangeNs);
}
export function liveTimelineNeedsSynchronization(
followLive: boolean,
activeTimeline: string | null | undefined,
rangeNs: { min: number; max: number } | null,
): boolean {
return followLive
&& isUsableRecordedPlaybackRange(rangeNs)
&& activeTimeline !== "stream_time";
}
export function liveRerunReceiverBindingIdentity(
sourceUrl: string,
liveStreamId: string | null,
followLive: boolean,
): string {
return JSON.stringify([
followLive ? "live" : "recorded",
sourceUrl.trim(),
followLive ? liveStreamId?.trim() ?? "" : "",
]);
}
@@ -327,6 +327,53 @@ export function verifyLiveViewerClientBuild(
});
}
/**
* Recover a recorded viewer whose lazy module disappeared during a frontend
* deployment. Live acquisition deliberately never reloads automatically, but
* a saved recording has no device-side authority to preserve and can safely
* move to the current immutable application build.
*/
export async function reloadRecordedViewerAfterStaleModuleFailure({
loadedUiBuildId,
signal,
fetcher = globalThis.fetch,
reload = () => window.location.reload(),
}: {
loadedUiBuildId: string;
signal?: AbortSignal;
fetcher?: typeof globalThis.fetch;
reload?: () => void;
}): Promise<boolean> {
if (
signal?.aborted ||
loadedUiBuildId === DEVELOPMENT_UI_BUILD_ID ||
!HASHED_UI_BUILD_ID.test(loadedUiBuildId)
) return false;
try {
const response = await fetcher("/api/v1/viewer/client-contract", {
method: "GET",
headers: {
Accept: "application/json",
"X-MissionCore-UI-Build": loadedUiBuildId,
},
cache: "no-store",
signal,
});
if (signal?.aborted || !response.ok) return false;
const expectedUiBuildId = response.headers.get(UI_BUILD_HEADER);
if (
!expectedUiBuildId ||
expectedUiBuildId === loadedUiBuildId ||
!HASHED_UI_BUILD_ID.test(expectedUiBuildId)
) return false;
browserUiBuildCoordinator().report({ loadedUiBuildId, expectedUiBuildId });
reload();
return true;
} catch {
return false;
}
}
function startBuildMonitor(): void {
if (buildMonitorAbort || typeof window === "undefined") return;
const lineage = createLiveViewerLineage(createLiveViewerInstanceId(), 1);
@@ -0,0 +1,260 @@
import type { SceneSettings } from "../../sceneSettings";
import type { RecordedAdmissionPhase } from "./recordedSessionAdmission";
import type { RerunPlaybackState, RerunViewportStatus } from "./viewerProfile";
const BUFFER_END_TOLERANCE_NS = 1_000_000;
const RECORDED_OPEN_MIN_TIMEOUT_MS = 120_000;
const RECORDED_OPEN_MAX_TIMEOUT_MS = 1_800_000;
const RECORDED_OPEN_GRACE_MS = 30_000;
const RECORDED_OPEN_MIN_BYTES_PER_SECOND = 2 * 1024 * 1024;
export const RECORDED_BASE_POINT_COLOR_KEY = "intensity|turbo|-";
export interface RecordedPlaybackBufferState {
bufferedEndNs: number | null;
expectedStartNs: number | null;
expectedEndNs: number | null;
bufferProgress: number | null;
fullyBuffered: boolean;
}
export function recordedPointColorKey(
settings: Pick<SceneSettings, "colorMode" | "palette" | "customColor">,
): string {
const custom = settings.palette === "custom" || settings.colorMode === "class"
? settings.customColor.toLowerCase()
: "-";
return `${settings.colorMode}|${settings.palette}|${custom}`;
}
export function recordedOpenWatchdogTimeoutMs(byteLength: number): number {
if (!Number.isSafeInteger(byteLength) || byteLength < 4) {
throw new Error("Unsafe recorded RRD byte length");
}
const transferBudgetMs = Math.ceil(
(byteLength / RECORDED_OPEN_MIN_BYTES_PER_SECOND) * 1_000,
);
return Math.min(
RECORDED_OPEN_MAX_TIMEOUT_MS,
Math.max(RECORDED_OPEN_MIN_TIMEOUT_MS, transferBudgetMs + RECORDED_OPEN_GRACE_MS),
);
}
export function createRecordedOpenWatchdog<T>({
byteLength,
schedule,
cancel,
onTimeout,
}: {
byteLength: number;
schedule: (callback: () => void, timeoutMs: number) => T;
cancel: (handle: T) => void;
onTimeout: () => void;
}): { arm: () => void; clear: () => void; pending: () => boolean } {
const timeoutMs = recordedOpenWatchdogTimeoutMs(byteLength);
let handle: T | null = null;
return {
arm() {
if (handle !== null) return;
handle = schedule(() => {
handle = null;
onTimeout();
}, timeoutMs);
},
clear() {
if (handle === null) return;
cancel(handle);
handle = null;
},
pending: () => handle !== null,
};
}
export function isUsableRecordedPlaybackRange(
rangeNs: { min: number; max: number } | null,
): rangeNs is { min: number; max: number } {
return Boolean(
rangeNs
&& Number.isFinite(rangeNs.min)
&& Number.isFinite(rangeNs.max)
&& rangeNs.max >= rangeNs.min,
);
}
export function recordedPlaybackBufferState(
rangeNs: { min: number; max: number } | null,
expectedTimelineEndSeconds?: number,
expectedTimelineStartSeconds?: number,
): RecordedPlaybackBufferState {
const usableRange = isUsableRecordedPlaybackRange(rangeNs) ? rangeNs : null;
const bufferedEndNs = usableRange?.max ?? null;
if (expectedTimelineEndSeconds === undefined) {
return {
bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null,
bufferProgress: null,
fullyBuffered: usableRange !== null,
};
}
if (!Number.isFinite(expectedTimelineEndSeconds) || expectedTimelineEndSeconds < 0) {
return {
bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null,
bufferProgress: null,
fullyBuffered: false,
};
}
const expectedEndNs = expectedTimelineEndSeconds * 1_000_000_000;
const expectedStartNs = expectedTimelineStartSeconds === undefined
? 0
: expectedTimelineStartSeconds * 1_000_000_000;
if (
!Number.isFinite(expectedEndNs)
|| !Number.isFinite(expectedStartNs)
|| expectedStartNs < 0
|| expectedEndNs < expectedStartNs
) {
return {
bufferedEndNs,
expectedStartNs: null,
expectedEndNs: null,
bufferProgress: null,
fullyBuffered: false,
};
}
const fullyBuffered = usableRange !== null
&& usableRange.min <= expectedStartNs + BUFFER_END_TOLERANCE_NS
&& usableRange.max >= expectedEndNs - BUFFER_END_TOLERANCE_NS;
const expectedDurationNs = expectedEndNs - expectedStartNs;
const bufferProgress = bufferedEndNs === null
? 0
: expectedDurationNs <= 0
? (fullyBuffered ? 1 : 0)
: Math.min(1, Math.max(0, (bufferedEndNs - expectedStartNs) / expectedDurationNs));
return {
bufferedEndNs,
expectedStartNs,
expectedEndNs,
bufferProgress,
fullyBuffered,
};
}
export function isRecordedPlaybackReady(
viewerStarted: boolean,
artifactVerified: boolean,
buffer: RecordedPlaybackBufferState,
): boolean {
return viewerStarted && artifactVerified && buffer.bufferedEndNs !== null;
}
export function recordedPlaybackRangeWhenReady(
rangeNs: { min: number; max: number } | null,
buffer: RecordedPlaybackBufferState,
artifactVerified: boolean,
): { min: number; max: number } | null {
if (
!artifactVerified
|| !buffer.fullyBuffered
|| !isUsableRecordedPlaybackRange(rangeNs)
) return null;
return {
min: buffer.expectedStartNs === null
? rangeNs.min
: Math.max(rangeNs.min, buffer.expectedStartNs),
max: buffer.expectedEndNs === null
? rangeNs.max
: Math.min(rangeNs.max, buffer.expectedEndNs),
};
}
export function isRecordedPlaybackPresentationReady(
status: RerunViewportStatus,
playback: RerunPlaybackState | null,
): boolean {
return status === "ready"
&& playback?.fullyBuffered === true
&& isUsableRecordedPlaybackRange(playback.rangeNs);
}
export function rerunPresentationStatus(
status: RerunViewportStatus,
_gate: RecordedAdmissionPhase,
recorded: boolean,
): RerunViewportStatus {
if (!recorded) return status;
return status === "error" ? "error" : status;
}
export function isRecordedPlaybackFullyBuffered(
rangeNs: { min: number; max: number } | null,
expectedTimelineEndSeconds?: number,
): boolean {
return recordedPlaybackBufferState(rangeNs, expectedTimelineEndSeconds).fullyBuffered;
}
export function attemptRecordedAutoplay(
seekToStart: () => void,
startPlaying: () => void,
): boolean {
try {
seekToStart();
startPlaying();
return true;
} catch {
return false;
}
}
export function createRecordedAutoplayGate(): {
attempt: (
viewerStarted: boolean,
fullyBuffered: boolean,
presentationReady: boolean,
rangeNs: { min: number; max: number } | null,
seekToStart: (startNs: number) => void,
startPlaying: () => void,
preferredStartNs?: number,
) => boolean;
attempted: () => boolean;
} {
let consumed = false;
return {
attempt(
viewerStarted,
fullyBuffered,
presentationReady,
rangeNs,
seekToStart,
startPlaying,
preferredStartNs,
) {
if (
consumed
|| !viewerStarted
|| !fullyBuffered
|| !presentationReady
|| !isUsableRecordedPlaybackRange(rangeNs)
) return false;
consumed = true;
const startNs = Number.isFinite(preferredStartNs)
? Math.min(Math.max(preferredStartNs as number, rangeNs.min), rangeNs.max)
: rangeNs.min;
return attemptRecordedAutoplay(
() => seekToStart(startNs),
startPlaying,
);
},
attempted: () => consumed,
};
}
export function canPublishRecordedPlaybackController(
readyToRender: boolean,
presentationGate: RecordedAdmissionPhase,
): boolean {
return readyToRender && presentationGate === "ready";
}
@@ -0,0 +1,114 @@
import type { RecordedAdmissionPhase } from "./recordedSessionAdmission";
export type RerunViewportStatus = "idle" | "loading" | "ready" | "error";
export type RecordedRerunView = "spatial" | "perception" | "perception3d" | "metrics";
export interface RerunPlaybackState {
recordingId: string;
timeline: string;
rangeNs: { min: number; max: number } | null;
currentNs: number;
playing: boolean;
bufferedEndNs: number | null;
expectedStartNs: number | null;
expectedEndNs: number | null;
bufferProgress: number | null;
fullyBuffered: boolean;
}
export interface RerunPlaybackController {
seek: (timeNs: number) => void;
setPlaying: (playing: boolean) => void;
jumpToEnd: () => void;
}
export interface RecordedPerceptionLayers {
enabled: boolean;
detections2d: boolean;
segmentation: boolean;
cuboids3d: boolean;
}
export interface RecordedRrdArtifactDescriptor {
sourceUrl: string;
viewerSourceUrl: string;
byteLength: number;
sha256: string;
}
/**
* The native live receiver owns one acquisition lineage and never exposes
* recorded transport or autoplay policy.
*/
export interface LiveAcquisitionRerunProfile {
kind: "live-acquisition";
clock: "stream_time";
sourceUrl: string;
liveActivitySequence: number | null;
liveStreamId: string | null;
liveRecoveryAuthorityIdentity: string | null;
}
/**
* A sealed session is progressively presentable, while its shared playback
* controls remain fenced by the aggregate session admission gate.
*/
export interface RecordedSessionRerunProfile {
kind: "recorded-session";
clock: "session_time";
sourceUrl: string;
artifact: RecordedRrdArtifactDescriptor | null;
autoplayWhenReady: boolean;
presentationGate: RecordedAdmissionPhase;
expectedTimelineStartSeconds?: number;
expectedTimelineEndSeconds?: number;
initialPlaybackStartSeconds?: number;
view: RecordedRerunView;
viewResetGeneration: 0 | 1;
followTrajectory: boolean;
perceptionLayers: RecordedPerceptionLayers;
perceptionRetryGeneration: number;
lockPerceptionCameraInteraction: boolean;
}
/**
* LAB recorded evidence is not a native Rerun mode. It owns a source-sequence
* clock and composes the existing sealed fMP4 and retained spatial primitives.
*/
export interface LaboratoryRecordedEvidenceViewerProfile {
kind: "lab-recorded-evidence";
clock: "source-sequence";
cameraTransport: "generation-bound-fmp4";
spatialTransport: "bounded-sealed-artifacts";
loadPolicy: "visible-evidence-only";
workerRequired: false;
}
export type RerunViewerProfile =
| LiveAcquisitionRerunProfile
| RecordedSessionRerunProfile;
export type ObservationViewerProfile =
| RerunViewerProfile
| LaboratoryRecordedEvidenceViewerProfile;
export const LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE = Object.freeze({
kind: "lab-recorded-evidence",
clock: "source-sequence",
cameraTransport: "generation-bound-fmp4",
spatialTransport: "bounded-sealed-artifacts",
loadPolicy: "visible-evidence-only",
workerRequired: false,
} satisfies LaboratoryRecordedEvidenceViewerProfile);
export function liveAcquisitionRerunProfile(
input: Omit<LiveAcquisitionRerunProfile, "kind" | "clock">,
): LiveAcquisitionRerunProfile {
return { kind: "live-acquisition", clock: "stream_time", ...input };
}
export function recordedSessionRerunProfile(
input: Omit<RecordedSessionRerunProfile, "kind" | "clock">,
): RecordedSessionRerunProfile {
return { kind: "recorded-session", clock: "session_time", ...input };
}
@@ -12,6 +12,7 @@ import type {
RecordedCameraAdmissionState,
} from "../core/observation/recordedSessionAdmission";
import { liveRerunRecoveryAuthorityIdentity } from "../core/observation/liveReceiverWatchdog";
import { liveAcquisitionRerunProfile, recordedSessionRerunProfile } from "../core/observation/viewerProfile";
import type { ObservationSourceDescriptor } from "../core/runtime/contracts";
import {
RerunViewport,
@@ -46,7 +47,6 @@ function statusTone(status: CapabilityStatus): "success" | "accent" | "warning"
if (status === "contract") return "warning";
return "neutral";
}
function FeatureInventory({ definition }: { definition: WorkspaceDefinition }) {
return (
<div className="feature-inventory">
@@ -76,7 +76,6 @@ function FeatureInventory({ definition }: { definition: WorkspaceDefinition }) {
</div>
);
}
function WorkspaceLead({ definition, note }: { definition: WorkspaceDefinition; note?: string }) {
return (
<section className="workspace-lead workspace-lead--compact">
@@ -89,7 +88,6 @@ function WorkspaceLead({ definition, note }: { definition: WorkspaceDefinition;
</section>
);
}
function EmptySpatialStage({ settings }: { settings: SceneSettings }) {
return (
<div className="empty-spatial-stage" data-grid={settings.showGrid ? "true" : undefined}>
@@ -181,7 +179,7 @@ function SpatialWorkspace({
);
const recordedPerceptionSupported =
recordedSource && perceptionLoad.phase !== "unavailable";
const recordedPerceptionReady = recordedSource && perceptionLoad.phase === "ready";
const recordedPerceptionLoading = recordedSource && perceptionLoad.phase === "loading";
const recordedPerceptionEnabled =
showDetections2d || showSegmentation || showCuboids3d;
// The native recorded camera remains the authoritative original. Only 2D
@@ -261,7 +259,7 @@ function SpatialWorkspace({
const shouldPrepareRecordedSource = useCallback((sourceId: string) => {
if (!recordedSessionAdmission) return false;
return recordedSessionAdmission.activeCameraSourceIds.has(sourceId) ||
recordedSessionAdmission.cameras[sourceId]?.phase === "ready";
["ready", "error"].includes(recordedSessionAdmission.cameras[sourceId]?.phase ?? "loading");
}, [recordedSessionAdmission]);
const onSelectionChange = useCallback((next: RerunSelection | null) => setSelection(next), []);
const onPlaybackChange = useCallback(
@@ -381,6 +379,35 @@ function SpatialWorkspace({
: presentedViewerStatus === "error"
? "danger"
: "neutral";
const rerunViewerProfile = recordedSource
? recordedSessionRerunProfile({
sourceUrl,
artifact: recordedReplay,
autoplayWhenReady: true,
presentationGate: recordedSessionGate,
expectedTimelineStartSeconds: state?.observationTimeline?.range?.startSeconds,
expectedTimelineEndSeconds: state?.observationTimeline?.range?.endSeconds,
initialPlaybackStartSeconds: initialRecordedPlaybackStartSeconds,
view: "spatial",
viewResetGeneration: recordedViewResetGeneration,
followTrajectory: followRecordedTrajectory,
perceptionLayers: {
enabled: recordedPerceptionSupported && recordedPerceptionEnabled,
detections2d: showDetections2d,
segmentation: showSegmentation,
cuboids3d: showCuboids3d,
},
perceptionRetryGeneration,
lockPerceptionCameraInteraction: unifiedPerception,
})
: liveAcquisitionRerunProfile({
sourceUrl,
liveActivitySequence: livePresentationActivitySequence,
liveStreamId: state?.spatialSource?.id ?? null,
liveRecoveryAuthorityIdentity: streamActive
? liveRerunRecoveryAuthorityIdentity(pointCloudSource, state?.spatialSource)
: null,
});
return (
<div
@@ -405,7 +432,7 @@ function SpatialWorkspace({
variant={detections2dActive ? "primary" : "secondary"}
icon={<Icon name="target" />}
aria-pressed={detections2dActive}
disabled={recordedSource && !recordedPerceptionReady}
disabled={recordedPerceptionLoading}
onClick={() => recordedSource
? setShowDetections2d((current) => !current)
: onLivePerceptionLayersChange({
@@ -420,7 +447,7 @@ function SpatialWorkspace({
variant={segmentationActive ? "primary" : "secondary"}
icon={<Icon name="image" />}
aria-pressed={segmentationActive}
disabled={recordedSource && !recordedPerceptionReady}
disabled={recordedPerceptionLoading}
onClick={() => recordedSource
? setShowSegmentation((current) => !current)
: onLivePerceptionLayersChange({
@@ -435,7 +462,7 @@ function SpatialWorkspace({
variant={cuboids3dActive ? "primary" : "secondary"}
icon={<Icon name="apps" />}
aria-pressed={cuboids3dActive}
disabled={recordedSource && !recordedPerceptionReady}
disabled={recordedPerceptionLoading}
onClick={() => recordedSource
? setShowCuboids3d((current) => !current)
: onLivePerceptionLayersChange({
@@ -489,35 +516,8 @@ function SpatialWorkspace({
>
{sourceUrl.trim() && pointCloudVisible && !intentionalSourceEnd ? (
<RerunViewport
sourceUrl={sourceUrl}
recordedArtifact={recordedSource ? recordedReplay : null}
followLive={liveRerunSource}
liveActivitySequence={livePresentationActivitySequence}
liveStreamId={state?.spatialSource?.id}
liveRecoveryAuthorityIdentity={!recordedSource && streamActive ? liveRerunRecoveryAuthorityIdentity(pointCloudSource, state?.spatialSource) : null}
autoplayWhenReady={recordedSource}
presentationGate={recordedSessionGate}
expectedTimelineStartSeconds={recordedSource
? state?.observationTimeline?.range?.startSeconds
: undefined}
expectedTimelineEndSeconds={recordedSource
? state?.observationTimeline?.range?.endSeconds
: undefined}
initialPlaybackStartSeconds={initialRecordedPlaybackStartSeconds}
profile={rerunViewerProfile}
sceneSettings={sceneSettings}
recordedViewResetGeneration={recordedViewResetGeneration}
recordedFollowTrajectory={followRecordedTrajectory}
recordedPerceptionLayers={{
enabled:
recordedPerceptionSupported &&
recordedPerceptionReady &&
recordedPerceptionEnabled,
detections2d: showDetections2d,
segmentation: showSegmentation,
cuboids3d: showCuboids3d,
}}
recordedPerceptionRetryGeneration={perceptionRetryGeneration}
lockPerceptionCameraInteraction={unifiedPerception}
onPerceptionLoadChange={onPerceptionLoadChange}
onPointColorLoadChange={onPointColorLoadChange}
onStatusChange={onStatusChange}
@@ -905,7 +905,7 @@ function CamerasWorkspace({
if (!recordedReplay) return true;
if (!recordedSessionAdmission) return false;
return recordedSessionAdmission.activeCameraSourceIds.has(sourceId) ||
recordedSessionAdmission.cameras[sourceId]?.phase === "ready";
["ready", "error"].includes(recordedSessionAdmission.cameras[sourceId]?.phase ?? "loading");
}, [recordedReplay, recordedSessionAdmission]);
return (
<div className="standard-workspace cameras-workspace" data-focused={focusedSource ? "true" : undefined}>
@@ -215,12 +215,24 @@ export function M49TgsFullShadowEvidence({
controlLabel: semanticOverride.controlLabel ?? "ПРИРОДА · DDRNet",
}] : []),
], [semantic, semanticOverride]);
const spatialSemantic = useMemo<M4ReplayThreatSemanticLayer | undefined>(() => (
semantic ? {
id: "spatial-urban",
controlLabel: "SEMANTICS",
resultId: semantic.resultId,
spatialResultId: semantic.resultId,
taxonomy: semantic.taxonomy,
label: "EoMT Cityscapes semantic · point-aligned E47",
maskAriaLabel: "EoMT urban semantic prediction",
} : undefined
), [semantic]);
return (
<>
<M4ReplayThreatVisual
resultId={result.source.linkedVisualResultId}
semanticLayers={semanticLayers}
spatialSemantic={spatialSemantic}
initialSemanticLayerId={semanticOverride ? "vegetation" : "urban"}
showReviewAnchorBoxes={false}
reviewLabel="4 489 source-paced TGS frames"
@@ -1,15 +1,25 @@
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
type ReactNode,
} from "react";
import {
Button,
Icon,
IconButton,
Select,
SegmentedControl,
SplitPane,
type SplitPaneOrientation,
} from "@nodedc/ui-react";
import { ObservationTimeline } from "../../components/ObservationTimeline";
import {
CanonicalRecordedLabReplay,
useCanonicalRecordedLabReplayState,
} from "../../components/laboratory/CanonicalRecordedLabReplay";
import {
LaboratoryMetricEvidenceScene,
type LaboratoryMetricCellEvidence,
@@ -35,6 +45,7 @@ import {
type E47SemanticClass,
type E47SemanticTimelineFrame,
} from "../../core/laboratory/e47SemanticSlam";
import { laboratoryRecordedEvidenceDemand } from "../../core/laboratory/recordedEvidenceProfile";
import type {
M4ThreatCameraProposal,
M4ThreatTimelineFrame,
@@ -52,8 +63,6 @@ import { useE47SemanticTimelineFrame } from "./useE47SemanticTimeline";
import { buildM4StaticObstacleBoxes } from "./m4StaticObstacleBoxes";
type M4ThreatMediaMode = "video" | "camera";
type M4ThreatMediaSelection = M4ThreatMediaMode | "none";
type M4ThreatSpatialSelection = LaboratoryMetricSceneMode | "none";
function toneForProposal(proposal: M4ThreatCameraProposal): RecordedEvidenceBox["tone"] {
if (proposal.threatDecision === "threat") return "danger";
@@ -144,6 +153,7 @@ export interface M4ReplayClassifiedSpatialLayer {
label: string;
pointLayerLabel: string;
cellLayerLabel: string;
cellLayerAvailable?: boolean;
expectedAtSequence: boolean;
frame: M4ReplayClassifiedSpatialFrame | null;
loading: boolean;
@@ -158,6 +168,7 @@ export function M4ReplayThreatVisual({
resultId,
semantic,
semanticLayers,
spatialSemantic,
initialSemanticLayerId,
reviewAnchors = EMPTY_REVIEW_ANCHORS,
showReviewAnchorBoxes = true,
@@ -168,11 +179,15 @@ export function M4ReplayThreatVisual({
classifiedSpatialLayer,
showReferenceMediaLayers = true,
showSpatialOverlaySummary = true,
playbackTransport = "epoch-stream",
spatialPlaybackTransport = "auto",
recoverTimestampStalls = false,
onActiveSequenceChange,
}: {
resultId: string;
semantic?: M4ReplayThreatSemanticLayer;
semanticLayers?: readonly M4ReplayThreatSemanticLayer[];
spatialSemantic?: M4ReplayThreatSemanticLayer;
initialSemanticLayerId?: string;
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
showReviewAnchorBoxes?: boolean;
@@ -183,12 +198,26 @@ export function M4ReplayThreatVisual({
classifiedSpatialLayer?: M4ReplayClassifiedSpatialLayer;
showReferenceMediaLayers?: boolean;
showSpatialOverlaySummary?: boolean;
playbackTransport?: "segmented" | "epoch-stream";
spatialPlaybackTransport?: "auto" | "sealed-binary" | "json";
recoverTimestampStalls?: boolean;
onActiveSequenceChange?: (sequence: number | null) => void;
}) {
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(
const {
mediaMode,
spatialMode,
splitView,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange: handleMediaModeChange,
onSpatialModeChange: handleSpatialModeChange,
onSplitPrimarySizeChange: setSplitPrimarySize,
onExpandedChange: setExpanded,
} = useCanonicalRecordedLabReplayState<M4ThreatMediaMode, LaboratoryMetricSceneMode>({
initialMediaMode: "video",
initialSpatialMode,
);
});
const [showCurrentIncrement, setShowCurrentIncrement] = useState(true);
const [showLocalSurface, setShowLocalSurface] = useState(true);
const [showRollingMap, setShowRollingMap] = useState(true);
@@ -197,13 +226,6 @@ export function M4ReplayThreatVisual({
const [showSpatialSemantic, setShowSpatialSemantic] = useState(true);
const [showMediaPoints, setShowMediaPoints] = useState(false);
const [showStaticObstacles, setShowStaticObstacles] = useState(true);
const [splitPrimarySize, setSplitPrimarySize] = useState(50);
const [splitOrientation, setSplitOrientation] = useState<SplitPaneOrientation>(() => (
typeof window !== "undefined" && window.matchMedia("(max-width: 900px)").matches
? "horizontal"
: "vertical"
));
const [expanded, setExpanded] = useState(false);
const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0);
const availableSemanticLayers = useMemo<readonly M4ReplayThreatSemanticLayer[]>(
() => semanticLayers?.length ? semanticLayers : semantic ? [semantic] : [],
@@ -234,6 +256,28 @@ export function M4ReplayThreatVisual({
const activeSemantic = availableSemanticLayers.find(
(layer, index) => (layer.id ?? `${layer.resultId}:${index}`) === selectedSemanticLayerId,
) ?? availableSemanticLayers[0];
const activeSpatialSemantic = spatialSemantic ?? activeSemantic;
const evidenceDemand = useMemo(() => laboratoryRecordedEvidenceDemand({
mediaMode,
spatialMode,
showMediaSemantic: Boolean(activeSemantic) && showMediaSemantic,
showSpatialSemantic: Boolean(activeSpatialSemantic) && showSpatialSemantic,
showMediaPoints,
classifiedSpatialMode: !classifiedSpatialLayer || classifiedSpatialLayer.cellLayerAvailable === false
? "none"
: classifiedSpatialLayer.replacePointCloud
? "replace-source"
: "overlay",
}), [
activeSemantic,
activeSpatialSemantic,
classifiedSpatialLayer,
mediaMode,
showMediaPoints,
showMediaSemantic,
showSpatialSemantic,
spatialMode,
]);
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
const metadata = useM4ThreatTimelineMetadata(resultId, timelineEndpointRoot);
const playbackRange = useMemo(() => metadata.timeline ? ({
@@ -241,7 +285,7 @@ export function M4ReplayThreatVisual({
endSeconds: metadata.timeline.timelineEndSeconds,
}) : null, [metadata.timeline]);
const playbackController = useRecordedEvidencePlayback(playbackRange, {
clock: mediaMode === "video" ? "external" : "animation",
clock: "external",
});
const seekPlayback = playbackController.seek;
const setPlaybackPlaying = playbackController.setPlaying;
@@ -249,7 +293,9 @@ export function M4ReplayThreatVisual({
resultId,
timeline: metadata.timeline,
currentSeconds: playbackController.playback.currentSeconds,
includeSpatialPoints: evidenceDemand.sourceSpatialPoints,
endpointRoot: timelineEndpointRoot,
spatialPlaybackTransport,
});
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
const [videoLoading, setVideoLoading] = useState(false);
@@ -260,16 +306,12 @@ export function M4ReplayThreatVisual({
setVideoError(null);
}, [resultId]);
useEffect(() => {
const query = window.matchMedia("(max-width: 900px)");
const update = () => setSplitOrientation(query.matches ? "horizontal" : "vertical");
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
useEffect(() => {
const timeline = metadata.timeline;
if (!evidenceDemand.recordedVideo) {
setVideoLoading(false);
return;
}
if (!timeline || videoSource) return;
const controller = new AbortController();
setVideoLoading(true);
@@ -304,7 +346,7 @@ export function M4ReplayThreatVisual({
if (!controller.signal.aborted) setVideoLoading(false);
});
return () => controller.abort();
}, [metadata.timeline, videoSource]);
}, [evidenceDemand.recordedVideo, metadata.timeline, videoSource]);
const lastFrameRef = useRef<M4ThreatTimelineFrame | null>(null);
useEffect(() => {
@@ -319,41 +361,53 @@ export function M4ReplayThreatVisual({
resultId: string;
frame: M4ThreatTimelineFrame;
} | null>(null);
if (frame?.spatialAvailable) {
lastSpatialFrameRef.current = { resultId, frame };
useEffect(() => {
lastSpatialFrameRef.current = null;
}, [evidenceDemand.sourceSpatialPoints, resultId]);
const latestAvailableSpatialFrame = [...timelineFrame.availableFrames]
.reverse()
.find((candidate) => (
candidate.spatialAvailable
&& (timelineFrame.activeSequence === null
|| candidate.sequence <= timelineFrame.activeSequence)
)) ?? null;
const currentSpatialFrame = frame?.spatialAvailable ? frame : latestAvailableSpatialFrame;
if (currentSpatialFrame) {
lastSpatialFrameRef.current = { resultId, frame: currentSpatialFrame };
}
const spatialFrame = frame?.spatialAvailable
? frame
const spatialFrame = currentSpatialFrame
? currentSpatialFrame
: lastSpatialFrameRef.current?.resultId === resultId
? lastSpatialFrameRef.current.frame
: null;
const cameraPointOverlay = useM4ThreatCameraPointOverlay({
enabled: showReferenceMediaLayers && showMediaPoints,
enabled: showReferenceMediaLayers && evidenceDemand.cameraPointOverlay,
resultId,
sequence: frame?.sequence ?? null,
endpointRoot: timelineEndpointRoot,
});
const semanticSpatialResultId = activeSemantic
? activeSemantic.spatialResultId === undefined
? activeSemantic.resultId
: activeSemantic.spatialResultId
const semanticSpatialResultId = activeSpatialSemantic
? activeSpatialSemantic.spatialResultId === undefined
? activeSpatialSemantic.resultId
: activeSpatialSemantic.spatialResultId
: null;
const spatialSemanticTaxonomy = useMemo<readonly E47SemanticClass[]>(
() => semanticSpatialResultId && activeSemantic
? activeSemantic.taxonomy.map((item) => ({
() => semanticSpatialResultId && activeSpatialSemantic
? activeSpatialSemantic.taxonomy.map((item) => ({
classId: item.classId,
label: item.label,
disposition: item.disposition === "ambiguous" ? "ambiguous" : "labeled",
colorRgb: item.colorRgb,
}))
: [],
[activeSemantic, semanticSpatialResultId],
[activeSpatialSemantic, semanticSpatialResultId],
);
const semanticTimeline = useE47SemanticTimelineFrame({
resultId: semanticSpatialResultId,
activeSequence: frame?.sequence ?? timelineFrame.activeSequence,
frameCount: metadata.timeline?.frameCount ?? 0,
taxonomy: spatialSemanticTaxonomy,
enabled: evidenceDemand.selectedSemanticPoints,
});
const displayingBufferedFrame = Boolean(
frame
@@ -443,6 +497,27 @@ export function M4ReplayThreatVisual({
})) ?? [],
[activeSemantic?.taxonomy],
);
const spatialSemanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
() => activeSpatialSemantic?.taxonomy.map((item) => ({
id: item.classId,
label: `semantic: ${item.label}`,
})) ?? [],
[activeSpatialSemantic?.taxonomy],
);
const spatialSemanticPalette = useMemo<readonly RecordedEvidenceSemanticPaletteEntry[]>(
() => activeSpatialSemantic?.taxonomy.map((item) => ({
classId: item.classId,
color: item.disposition === "undefined"
? { kind: "transparent" as const }
: item.disposition === "ambiguous"
? { kind: "token" as const, token: "--nodedc-warning-rgb" as const }
: { kind: "diagnostic" as const, rgb: item.colorRgb },
opacity: item.disposition === "undefined"
? 0
: item.disposition === "ambiguous" ? 0.52 : 0.92,
})) ?? [],
[activeSpatialSemantic?.taxonomy],
);
const semanticFrame = semanticTimeline.activeFrame?.sequence === frame?.sequence
? semanticTimeline.activeFrame
: null;
@@ -459,7 +534,7 @@ export function M4ReplayThreatVisual({
&& lastSpatialSemanticFrameRef.current.frame.sequence === spatialFrame?.sequence
? lastSpatialSemanticFrameRef.current.frame
: null;
const semanticIntegrityError = activeSemantic && spatialFrame && spatialSemanticFrame && (
const semanticIntegrityError = activeSpatialSemantic && spatialFrame && spatialSemanticFrame && (
spatialSemanticFrame.sourcePointCount !== spatialFrame.pointCloudSourceCount
|| spatialFrame.pointCloudSampleCount !== spatialFrame.pointCloudSourceCount
|| spatialFrame.pointCloudBodyXyzM.length !== spatialFrame.pointCloudSourceCount
@@ -468,7 +543,7 @@ export function M4ReplayThreatVisual({
: null;
const alignedSemanticPointIds = useMemo<readonly (number | null)[] | undefined>(() => {
if (
!activeSemantic
!activeSpatialSemantic
|| !showSpatialSemantic
|| !spatialFrame
|| !spatialSemanticFrame
@@ -478,18 +553,24 @@ export function M4ReplayThreatVisual({
const status = spatialSemanticFrame.statusCodes[index];
return status === 2 || status === 3 ? classId : null;
});
}, [activeSemantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
}, [activeSpatialSemantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence
? spatialFrame
: null;
const classifiedSpatialFrame = classifiedSpatialLayer?.frame?.sourceSequence === timelineFrame.activeSequence
const hasClassifiedSpatialOutput = Boolean(
classifiedSpatialLayer && classifiedSpatialLayer.cellLayerAvailable !== false,
);
const classifiedSpatialFrame = hasClassifiedSpatialOutput
&& classifiedSpatialLayer?.frame?.sourceSequence === timelineFrame.activeSequence
? classifiedSpatialLayer?.frame ?? null
: null;
const lastClassifiedSpatialFrameRef = useRef<{
resultId: string;
frame: M4ReplayClassifiedSpatialFrame;
} | null>(null);
const incomingClassifiedSpatialFrame = classifiedSpatialLayer?.frame ?? null;
const incomingClassifiedSpatialFrame = hasClassifiedSpatialOutput
? classifiedSpatialLayer?.frame ?? null
: null;
if (incomingClassifiedSpatialFrame && incomingClassifiedSpatialFrame.sampleAvailable !== false) {
lastClassifiedSpatialFrameRef.current = { resultId, frame: incomingClassifiedSpatialFrame };
}
@@ -518,7 +599,9 @@ export function M4ReplayThreatVisual({
? spatialFrame
: null)
: null;
const replaceClassifiedPointCloud = classifiedSpatialLayer?.replacePointCloud ?? true;
const replaceClassifiedPointCloud = hasClassifiedSpatialOutput
? classifiedSpatialLayer?.replacePointCloud ?? true
: false;
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
const mapGravityLocalSensorToBodyGround = useCallback((
point: readonly [number, number, number],
@@ -636,7 +719,12 @@ export function M4ReplayThreatVisual({
.map((item) => item.assessment.closestApproachM)
.filter((value): value is number => value !== null)
.sort((left, right) => left - right)[0] ?? null;
const localSurface = useMemo(() => buildM4LocalSurface(
const localSurface = useMemo(() => spatialFrame?.localSlamBodyXyzM?.length ? ({
pointsBodyXyzM: spatialFrame.localSlamBodyXyzM,
sourceFrameCount: spatialFrame.localSlamSourceFrameCount ?? 0,
sourcePointCount: spatialFrame.localSlamSourcePointCount ?? 0,
voxelCount: spatialFrame.localSlamBodyXyzM.length,
}) : buildM4LocalSurface(
timelineFrame.availableFrames,
spatialFrame,
metadata.timeline?.localSurfaceVisualization ?? {
@@ -647,7 +735,7 @@ export function M4ReplayThreatVisual({
},
), [metadata.timeline, spatialFrame, timelineFrame.availableFrames]);
const semanticOverlay: RecordedEvidenceSemanticOverlay | undefined =
activeSemantic && showMediaSemantic && frame
activeSemantic && evidenceDemand.selectedSemanticMask && frame
? {
src: activeSemantic.maskUrl?.(frame.sequence)
?? e47SemanticMaskUrl(activeSemantic.resultId, frame.sequence),
@@ -684,50 +772,15 @@ export function M4ReplayThreatVisual({
}
: undefined;
const handleMediaModeChange = (next: M4ThreatMediaSelection) => {
if (next === "none") return;
setMediaMode((current) => current === next ? null : next);
};
const handleSpatialModeChange = (next: M4ThreatSpatialSelection) => {
if (next === "none") return;
setSpatialMode((current) => current === next ? null : next);
};
useEffect(() => {
if (playbackController.playback.playing || !frame) return;
if (
playbackController.playback.playing
|| !evidenceDemand.exactCameraFrame
|| !frame
) return;
const image = new Image();
image.src = frame.cameraUrl;
}, [frame?.cameraUrl, playbackController.playback.playing]);
const splitView = mediaMode !== null && spatialMode !== null;
const mediaModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="media">
<SegmentedControl
value={mediaMode ?? "none"}
items={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
]}
label="Видео и камера"
onChange={handleMediaModeChange}
/>
</div>
);
const spatialModeControls = (
<div className="m4-replay-threat-visual__pane-mode-controls" data-pane-mode="spatial">
<SegmentedControl
value={spatialMode ?? "none"}
items={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
]}
label="3D и план"
onChange={handleSpatialModeChange}
/>
</div>
);
}, [evidenceDemand.exactCameraFrame, frame?.cameraUrl, playbackController.playback.playing]);
const mediaLayerControls = activeSemantic
|| (showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery)
@@ -819,16 +872,24 @@ export function M4ReplayThreatVisual({
shape="pill"
variant={showRollingMap ? "primary" : "secondary"}
aria-pressed={showRollingMap}
disabled={classifiedSpatialLayer.cellLayerAvailable === false}
title={classifiedSpatialLayer.cellLayerAvailable === false
? `${classifiedSpatialLayer.cellLayerLabel} недоступен: для этой записи нет запечатанного полного результата`
: undefined}
onClick={() => setShowRollingMap((visible) => !visible)}
>
{classifiedSpatialLayer.cellLayerLabel}
</Button>
{semanticSpatialResultId ? (
{activeSpatialSemantic ? (
<Button
size="compact"
shape="pill"
variant={showSpatialSemantic ? "primary" : "secondary"}
aria-pressed={showSpatialSemantic}
disabled={!semanticSpatialResultId}
title={semanticSpatialResultId
? "Point-aligned semantic evidence"
: "Point-aligned 3D semantics отсутствует в запечатанном результате"}
onClick={() => setShowSpatialSemantic((visible) => !visible)}
>
SEMANTICS
@@ -881,12 +942,16 @@ export function M4ReplayThreatVisual({
LOW-STEP
</Button>
) : null}
{semanticSpatialResultId ? (
{activeSpatialSemantic ? (
<Button
size="compact"
shape="pill"
variant={showSpatialSemantic ? "primary" : "secondary"}
aria-pressed={showSpatialSemantic}
disabled={!semanticSpatialResultId}
title={semanticSpatialResultId
? "Point-aligned semantic evidence"
: "Point-aligned 3D semantics отсутствует в запечатанном результате"}
onClick={() => setShowSpatialSemantic((visible) => !visible)}
>
SEMANTICS
@@ -990,14 +1055,14 @@ export function M4ReplayThreatVisual({
<>
<div>
<span>Spatial evidence</span>
<strong>{classifiedSpatialLayer
<strong>{hasClassifiedSpatialOutput
? classifiedSpatialFrame
? replaceClassifiedPointCloud
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedCellCount.toLocaleString("ru-RU")} cells`
: `${(activeSpatialFrame?.pointCloudSourceCount ?? classifiedSpatialFrame.sourcePointCount ?? 0).toLocaleString("ru-RU")} source points · ${classifiedCellCount.toLocaleString("ru-RU")} TGS cells`
: "TGS spatial buffer"
: `${currentIncrementObstacles.length} current · ${rollingMapObstacles.length} rolling${metadata.timeline.occupancyProvenanceDelivery ? ` · ${lowStepObstacles.length} low-step` : ""}`}</strong>
<small>{classifiedSpatialLayer
<small>{hasClassifiedSpatialOutput
? classifiedSpatialFrame
? classifiedSpatialFrame.sampleAvailable === false
? displayedClassifiedFrameHeld && displayedClassifiedSpatialFrame
@@ -1006,9 +1071,9 @@ export function M4ReplayThreatVisual({
: activeSpatialFrame
? "map-gravity-local · all eligible points accounted · causal rolling 1 s"
: "TGS рассчитан · linked source cloud недоступен для этого кадра"
: classifiedSpatialLayer.error
?? classifiedSpatialLayer.loadingLabel
?? `Открываем ${classifiedSpatialLayer.label}`
: classifiedSpatialLayer?.error
?? classifiedSpatialLayer?.loadingLabel
?? `Открываем ${classifiedSpatialLayer?.label ?? "spatial evidence"}`
: (
<>
{spatialFrame
@@ -1033,13 +1098,13 @@ export function M4ReplayThreatVisual({
)}</small>
</div>
<div>
<span>{classifiedSpatialLayer ? "TGS fail-closed" : "Virtual corridor"}</span>
<strong>{classifiedSpatialLayer
<span>{hasClassifiedSpatialOutput ? "TGS fail-closed" : "Virtual corridor"}</span>
<strong>{hasClassifiedSpatialOutput
? classifiedSpatialFrame
? `${classifiedCellCounts.occupied} occupied · ${classifiedCellCounts.rejected} rejected · ${classifiedCellCounts.unobserved} unobserved`
: classifiedSpatialLayer.loading || displayingBufferedFrame ? "loading" : "unavailable"
: classifiedSpatialLayer?.loading || displayingBufferedFrame ? "loading" : "unavailable"
: `${spatialFrame?.decisionCounts.threat ?? 0} threat · nearest ${nearest === null ? "—" : `${nearest.toFixed(2)} м`}`}</strong>
<small>{classifiedSpatialLayer
<small>{hasClassifiedSpatialOutput
? classifiedSpatialFrame
? `${classifiedCellCounts.ground} ground-support · visual review only · navigation authority OFF`
: "visual review only · navigation authority OFF"
@@ -1051,7 +1116,12 @@ export function M4ReplayThreatVisual({
) : undefined;
const timeline = metadata.timeline;
let content;
let content: ReactNode = null;
let canonicalContent: {
mediaContent: ReactNode;
spatialContent: ReactNode;
deckOverlays: ReactNode;
} | null = null;
if (metadata.error) {
content = <SpatialState message={metadata.error} />;
} else if (!timeline) {
@@ -1062,23 +1132,8 @@ export function M4ReplayThreatVisual({
</div>
);
} else {
const mediaPane = (
<section
className="m4-replay-threat-visual__pane"
data-pane="media"
aria-label={mediaMode === "camera" ? "Камера" : "Видео"}
hidden={!mediaMode}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="media"
data-multi-semantic={availableSemanticLayers.length > 1 ? "true" : undefined}
>
{mediaLayerControls}
{mediaModeControls}
</div>
) : null}
const mediaContent = (
<>
<div
className="m4-replay-threat-visual__media-layer"
data-media="video"
@@ -1091,6 +1146,7 @@ export function M4ReplayThreatVisual({
imageWidth={timeline.imageWidth}
imageHeight={timeline.imageHeight}
boxes={activeBoxes}
overlaySeconds={frame?.sessionSeconds}
semanticOverlay={mediaMode === "video" ? semanticOverlay : undefined}
pointCloudOverlay={mediaMode === "video" ? pointCloudOverlay : undefined}
ariaLabel={`${evidenceLabel} recorded-realtime frame ${frame?.sequence ?? 0}: ${activeBoxes.length} proposals`}
@@ -1102,7 +1158,9 @@ export function M4ReplayThreatVisual({
}
segmentCount={timeline.frameCount}
onPlaybackChange={playbackController.synchronize}
onPlayingRejected={() => playbackController.setPlaying(false)}
playbackAuthority="media"
playbackTransport={playbackTransport}
recoverTimestampStalls={recoverTimestampStalls}
/>
) : videoError ? (
<SpatialState message={videoError} />
@@ -1124,34 +1182,20 @@ export function M4ReplayThreatVisual({
ariaLabel={`${evidenceLabel} exact camera frame ${frame.sequence}: ${activeBoxes.length} proposals`}
/>
) : null}
</section>
</>
);
const spatialPane = spatialMode ? (
<section
className="m4-replay-threat-visual__pane"
data-pane="spatial"
aria-label={spatialMode === "3d" ? "Трёхмерная сцена" : "Вид сверху"}
>
{splitView ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="spatial"
>
{resetSpatialView}
<div className="m4-replay-threat-visual__spatial-toolbar-end">
{spatialLayerControls}
{spatialModeControls}
</div>
</div>
) : null}
const spatialContent = spatialMode ? (
<>
<LaboratoryMetricEvidenceScene
ref={metricSceneRef}
pointCloudBodyXyzM={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? classifiedPointsBody
: classifiedContextSpatialFrame?.pointCloudBodyXyzM ?? []}
: classifiedContextSpatialFrame?.pointCloudBodyXyzM
?? activeSpatialFrame?.pointCloudBodyXyzM
?? []}
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
obstacles={classifiedSpatialLayer ? [] : sceneObstacles}
obstacles={hasClassifiedSpatialOutput ? [] : sceneObstacles}
rig={timeline.rig}
corridor={timeline.corridor}
occupiedVoxelSizeM={displayedClassifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
@@ -1160,22 +1204,22 @@ export function M4ReplayThreatVisual({
showCurrentIncrement={showCurrentIncrement}
showLocalSurface={showLocalSurface}
showRollingMap={showRollingMap}
showLowStep={classifiedSpatialLayer ? false : showLowStep}
showLowStep={hasClassifiedSpatialOutput ? false : showLowStep}
pointSemanticClassIds={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.pointClassIds
: alignedSemanticPointIds}
semanticClasses={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.classes
: semanticClasses}
: spatialSemanticClasses}
semanticPalette={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.palette
: semanticPalette}
: spatialSemanticPalette}
classifiedCells={classifiedCellsBody}
classifiedPackedCells={classifiedPackedCellsBody}
classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM}
showClassifiedCells={showRollingMap}
/>
{classifiedSpatialLayer && !displayedClassifiedSpatialFrame ? (
{hasClassifiedSpatialOutput && classifiedSpatialLayer && !displayedClassifiedSpatialFrame ? (
<div className="l3-visual-audit__state" role={classifiedSpatialLayer.error ? "alert" : "status"}>
{classifiedSpatialLayer.loading || displayingBufferedFrame
? <span className="busy-indicator" aria-hidden="true" />
@@ -1206,31 +1250,11 @@ export function M4ReplayThreatVisual({
: `На кадре ${frame.sequence + 1} нет body frame; ждём первый квалифицированный spatial evidence.`}
</div>
) : null}
</section>
</>
) : null;
content = (
<div
className="m4-replay-threat-visual__deck"
data-split={splitView ? "true" : undefined}
data-empty={!mediaMode && !spatialMode ? "true" : undefined}
>
<SplitPane
primary={mediaPane}
secondary={spatialPane ?? <div />}
primarySize={splitView ? splitPrimarySize : mediaMode ? 100 : 0}
onPrimarySizeChange={setSplitPrimarySize}
orientation={splitOrientation}
minPrimarySize={splitView ? 24 : 0}
minSecondarySize={splitView ? 24 : 0}
resizable={splitView}
separatorLabel="Изменить размер VIDEO/CAMERA и 3D/PLAN"
/>
{!mediaMode && !spatialMode ? (
<div className="l3-visual-audit__state" role="status">
Выберите VIDEO/CAMERA или 3D/PLAN. Общий таймлайн останется на месте.
</div>
) : null}
const deckOverlays = (
<>
{timelineFrame.loading || displayingBufferedFrame ? (
<div className="m4-replay-threat-visual__buffering" role="status">
<span className="busy-indicator" aria-hidden="true" />
@@ -1261,8 +1285,9 @@ export function M4ReplayThreatVisual({
<span>{semanticIntegrityError}</span>
</div>
) : null}
</div>
</>
);
canonicalContent = { mediaContent, spatialContent, deckOverlays };
}
const transport = timeline ? (
@@ -1287,38 +1312,59 @@ export function M4ReplayThreatVisual({
/>
) : undefined;
if (!timeline || !canonicalContent) {
return (
<div className="l3-visual-audit m4-replay-threat-visual">
<LaboratoryEvidenceViewer
label={`${evidenceLabel} recorded-realtime replay`}
className="m4-replay-threat-evidence-viewer"
mode="video"
modes={[{ value: "video", label: "VIDEO" }]}
expanded={expanded}
onModeChange={() => undefined}
onExpandedChange={setExpanded}
>
{content}
</LaboratoryEvidenceViewer>
</div>
);
}
return (
<div className="l3-visual-audit m4-replay-threat-visual">
<LaboratoryEvidenceViewer
label={activeSemantic
? activeSemantic.label ?? "Semantic diagnostic replay"
: `${evidenceLabel} recorded-realtime replay`}
className="m4-replay-threat-evidence-viewer"
mode={mediaMode ?? "none"}
modes={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
]}
secondaryMode={{
value: spatialMode ?? "none",
modes: [
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
],
label: "3D и план",
onChange: handleSpatialModeChange,
}}
expanded={expanded}
onModeChange={handleMediaModeChange}
onExpandedChange={setExpanded}
modeControlsVisible={!splitView}
actions={actions}
overlay={overlay}
transport={transport}
trailingActions={trailingActions}
>
{content}
</LaboratoryEvidenceViewer>
</div>
<CanonicalRecordedLabReplay
label={activeSemantic
? activeSemantic.label ?? "Semantic diagnostic replay"
: `${evidenceLabel} recorded-realtime replay`}
mediaMode={mediaMode ?? "none"}
mediaModes={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
]}
spatialMode={spatialMode ?? "none"}
spatialModes={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "PLAN" },
]}
expanded={expanded}
splitPrimarySize={splitPrimarySize}
splitOrientation={splitOrientation}
mediaAriaLabel={mediaMode === "camera" ? "Камера" : "Видео"}
spatialAriaLabel={spatialMode === "3d" ? "Трёхмерная сцена" : "Вид сверху"}
mediaLayerControls={mediaLayerControls}
spatialLayerControls={spatialLayerControls}
spatialLeadingControl={resetSpatialView}
mediaMultiLayer={availableSemanticLayers.length > 1}
mediaContent={canonicalContent.mediaContent}
spatialContent={canonicalContent.spatialContent}
emptyMessage="Выберите VIDEO/CAMERA или 3D/PLAN. Общий таймлайн останется на месте."
deckOverlays={canonicalContent.deckOverlays}
actions={actions}
overlay={overlay}
transport={transport}
trailingActions={trailingActions}
onMediaModeChange={handleMediaModeChange}
onSpatialModeChange={handleSpatialModeChange}
onExpandedChange={setExpanded}
onSplitPrimarySizeChange={setSplitPrimarySize}
/>
);
}
@@ -1,69 +1,34 @@
import { useEffect, useMemo, useState } from "react";
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import { LaboratoryRecordedClipPlayer } from "../../components/laboratory/LaboratoryRecordedClipPlayer";
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import {
RecordedEvidenceSemanticMaskOverlay,
type RecordedEvidenceSemanticClass,
type RecordedEvidenceSemanticPaletteEntry,
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
import {
vegetationFullRouteMaskUrl,
vegetationVideoMaskUrl,
type VegetationFullRouteLayer,
type VegetationFullRouteReview,
type VegetationMixedRouteReview,
type VegetationShadowResult,
} from "../../core/laboratory/vegetationShadow";
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
import {
fetchM49TgsFullShadowResult,
type M49TgsFullShadowResult,
} from "../../core/laboratory/m49TgsFullShadow";
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
import {
M4ReplayThreatVisual,
type M4ReplayClassifiedSpatialLayer,
type M4ReplayThreatSemanticLayer,
} from "./M4ReplayThreatVisual";
const VEGETATION_TIMELINE_ENDPOINT = "/api/v1/laboratory/vegetation-shadow";
function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
}
const MIXED_ROUTE_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "city", label: "ГОРОД · EoMT" },
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
{ value: "tgs", label: "TGS" },
] as const;
const FULL_ROUTE_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "city", label: "ГОРОД · EoMT" },
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
] as const;
function semanticPresentation(layer: VegetationFullRouteLayer): {
classes: readonly RecordedEvidenceSemanticClass[];
palette: readonly RecordedEvidenceSemanticPaletteEntry[];
} {
return {
classes: layer.taxonomy.map((item) => ({ id: item.classId, label: item.label })),
palette: layer.taxonomy.map((item) => ({
classId: item.classId,
color: item.classId === 0
? { kind: "transparent" as const }
: { kind: "diagnostic" as const, rgb: item.colorRgb },
})),
};
}
function FullRouteReviewEvidence({
resultId,
review,
@@ -71,116 +36,55 @@ function FullRouteReviewEvidence({
resultId: string;
review: VegetationFullRouteReview;
}) {
const [sequence, setSequence] = useState(1);
const [playing, setPlaying] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1);
const [mode, setMode] = useState<typeof FULL_ROUTE_MODES[number]["value"]>("vegetation");
const [expanded, setExpanded] = useState(false);
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
const [videoError, setVideoError] = useState<string | null>(null);
const frames = useMemo(
() => review.frameSourceTimesNs.map((sourceTimeNs, index) => ({
sequence: index + 1,
sourceTimeNs,
})),
[review.frameSourceTimesNs],
);
const layer = mode === "source" ? null : review[mode];
const semantic = useMemo(() => layer ? semanticPresentation(layer) : null, [layer]);
const maskSequence = sequence - 1;
const prefetchSrcs = useMemo(() => layer
? Array.from({ length: 8 }, (_, offset) => maskSequence + offset + 1)
.filter((candidate) => candidate < review.frameCount)
.map((candidate) => vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", candidate))
: [], [layer, maskSequence, mode, resultId, review.frameCount]);
useEffect(() => {
const controller = new AbortController();
setVideoSource(null);
setVideoError(null);
void resolveObservationSessionReplay(review.sessionId, { signal: controller.signal })
.then((launch) => {
const source = recordedObservationSources(launch).find((candidate) => (
candidate.id === review.recordedMediaSourceId
&& candidate.modality === "video"
&& candidate.semanticChannelId === "camera.video.recorded"
&& candidate.delivery?.kind === "recorded-fmp4-manifest"
&& candidate.delivery.manifestGenerationSha256 === review.recordedMediaGenerationSha256
&& candidate.delivery.timelineStartSeconds === review.timelineStartSeconds
&& candidate.delivery.timelineEndSeconds >= review.timelineEndSeconds
));
if (!source) {
throw new Error("RIGHT-видео не совпало с sealed RAVNOVES004TREE timeline.");
}
if (!controller.signal.aborted) setVideoSource(source);
})
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
setVideoError(caught instanceof Error ? caught.message : "Записанное видео недоступно.");
}
});
return () => controller.abort();
}, [
review.recordedMediaGenerationSha256,
review.recordedMediaSourceId,
review.sessionId,
review.timelineEndSeconds,
review.timelineStartSeconds,
]);
const semanticLayers = useMemo<readonly M4ReplayThreatSemanticLayer[]>(() => ([
{
id: "city",
controlLabel: "ГОРОД · EoMT",
resultId,
spatialResultId: null,
taxonomy: review.city.taxonomy,
maskUrl: (sequence) => vegetationFullRouteMaskUrl(resultId, "city", sequence),
label: review.city.name,
maskAriaLabel: "EoMT city semantic prediction",
},
{
id: "vegetation",
controlLabel: "ПРИРОДА · DDRNet",
resultId,
spatialResultId: null,
taxonomy: review.vegetation.taxonomy,
maskUrl: (sequence) => vegetationFullRouteMaskUrl(resultId, "vegetation", sequence),
label: review.vegetation.name,
maskAriaLabel: "DDRNet nature semantic prediction",
},
]), [resultId, review.city, review.vegetation]);
const sealedSpatialGap = useMemo<M4ReplayClassifiedSpatialLayer>(() => ({
label: "RAVNOVES004TREE",
pointLayerLabel: "SOURCE POINTS",
cellLayerLabel: "TGS COSTMAP",
cellLayerAvailable: false,
expectedAtSequence: false,
frame: null,
loading: false,
error: null,
replacePointCloud: false,
}), []);
return (
<LaboratoryEvidenceViewer
label="RAVNOVES004TREE full recorded review"
className="m48-atlas-visual"
mode={mode}
modes={FULL_ROUTE_MODES}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
chromeLayout="stacked"
>
{videoSource ? (
<LaboratoryRecordedClipPlayer
source={videoSource}
segmentCount={review.frameCount}
frames={frames}
sequence={sequence}
playing={playing}
playbackRate={playbackRate}
cameraPresentation="primary"
continuousPlayback
sourceCount={1}
onSequenceChange={setSequence}
onPlayingChange={setPlaying}
onPlaybackRateChange={setPlaybackRate}
cameraOverlay={(
<>
<div className="m48-clip-player__pane-label" data-pane="camera">
{mode === "source" ? "SOURCE" : `${mode === "city" ? "EoMT CITY" : "DDRNet NATURE"} · КАДР ${sequence}/${review.frameCount}`}
</div>
{layer && semantic ? (
<div className="m48-clip-player__overlay">
<RecordedEvidenceSemanticMaskOverlay
src={vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", maskSequence)}
prefetchSrcs={prefetchSrcs}
imageWidth={review.width}
imageHeight={review.height}
classes={semantic.classes}
palette={semantic.palette}
opacity={0.76}
ariaLabel={`${layer.name} semantic prediction`}
/>
</div>
) : null}
</>
)}
/>
) : (
<div className="m4-replay-threat-visual__pane-status" role={videoError ? "alert" : "status"}>
{videoError ?? "Открываем автономный recorded source…"}
</div>
)}
</LaboratoryEvidenceViewer>
<M4ReplayThreatVisual
resultId={resultId}
timelineEndpointRoot={VEGETATION_TIMELINE_ENDPOINT}
semanticLayers={semanticLayers}
initialSemanticLayerId="vegetation"
initialSpatialMode="3d"
classifiedSpatialLayer={sealedSpatialGap}
evidenceLabel="RAVNOVES004TREE"
playbackTransport="segmented"
spatialPlaybackTransport="sealed-binary"
recoverTimestampStalls
showReferenceMediaLayers
showSpatialOverlaySummary
/>
);
}
@@ -198,26 +102,30 @@ function FullRouteReviewResult({
summary={(
<LaboratorySummary
title="LAB V1 · RAVNOVES004TREE · полный маршрут"
description="Существующий M4.7-шаблон воспроизводит всю запись и переключает два независимых sealed semantic-слоя: городской EoMT и природный DDRNet. Worker для открытия результата не нужен."
description="Принятый recorded-LAB инструмент воспроизводит RAV004 без отдельного viewer: одна media-clock timeline, RIGHT camera, source points, bounded Local SLAM и переключаемые EoMT/DDRNet."
status="FULL RECORDED REVIEW · truth отсутствует · commands OFF"
statusTone="warning"
facts={[
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount}/${review.frameCount} frames` },
{ 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` },
]}
brief={{
question: "Как оба semantic-кандидата ведут себя на полном переходе от сельской среды к городской?",
approach: "Все 6830 позиции одной recorded timeline последовательно прогнаны на Worker 006 и сохранены двумя независимыми архивами масок. В M4.7 переключается только видимый слой.",
principalResult: "Полная временная шкала доступна локально в SOURCE / EoMT CITY / DDRNet NATURE без обращения к Worker.",
limitation: "Ручной truth отсутствует. Один повреждённый H.264-пакет на позиции 6092 представлен предыдущим декодированным кадром и явно зафиксирован в proof. Полный TGS и кюветы этим прогоном не проверялись.",
question: "Что реально видно на полном RAV004-прогоне с высокой травой, оврагами и переходом к городу?",
approach: "RAV004 поставляет только data/provider configuration в тот же M4 recorded viewer. Видеодекодер владеет clock; новые source increments проецируются в gravity-stable forward/left/up frame, Local SLAM ограничен пятью секундами.",
principalResult: "RAV004 больше не имеет отдельной логики окон, таймера, seek, cache или 3D controls. Модели и подписи меняются конфигурацией, архитектура переключения остаётся общей.",
limitation: "Full-route TGS, независимый person/vehicle detector, ручной truth и point-aligned 3D semantics пока не запечатаны. Semantic-derived рамки диагностические и не являются STOP-authority.",
}}
method={{
completeness: "complete",
executionClass: "ai-inference",
pipelineId: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
pipelineId: "canonical-recorded-lab-rav004tree/v3",
components: [
{ kind: "algorithm", name: "Canonical recorded replay", version: "media-clock / one viewer", role: "shared camera + spatial transport", identitySha256: null },
{ kind: "algorithm", name: "Recorded source points + bounded Local SLAM", version: "source-paced-ground-v3", role: "gravity-stable spatial 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 },
],
@@ -226,9 +134,9 @@ function FullRouteReviewResult({
)}
evidence={(
<LaboratoryEvidence
eyebrow="M4.7 TEMPLATE · RAVNOVES004TREE FULL VIDEO"
title="SOURCE / EoMT CITY / DDRNet NATURE · 6830/6830 · TRUTH отсутствует"
kind="diagnostic-model"
eyebrow="CANONICAL RECORDED LAB · RAVNOVES004TREE"
title="CAMERA + SOURCE POINTS + LOCAL SLAM + TGS COSTMAP + SEMANTICS · 6830/6830"
kind="recorded-replay"
resizable
>
<FullRouteReviewEvidence resultId={resultId} review={review} />
@@ -236,134 +144,19 @@ function FullRouteReviewResult({
)}
result={(
<LaboratoryResultSummary
title="Полный двухслойный visual review собран; управление не авторизовано"
status="Recorded evidence ready · navigation/actuation OFF"
title="RAV004 переведён на общий replay-каркас; safety evidence ещё не полно"
status="Recorded evidence · navigation/actuation OFF"
statusTone="warning"
metrics={[
{ label: "Route masks", value: "6830/6830 × 2", hint: "sealed local archives · Worker не требуется" },
{ label: "EoMT p95", value: `${decimal(review.city.latencyP95Ms, 2)} ms`, hint: "последовательный изолированный прогон" },
{ label: "DDRNet p95", value: `${decimal(review.vegetation.latencyP95Ms, 2)} ms`, hint: "последовательный изолированный прогон" },
{ label: "Decode repair", value: "1/6830", hint: "sequence 6092 · previous frame · sealed proof" },
{ 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 не принята" },
]}
conclusion={{
proved: "Городской EoMT и природный DDRNet воспроизводимо обработали полную запись и доступны в одном существующем M4.7 viewer.",
notProved: "Не доказаны truth accuracy, одновременный realtime-load, полный TGS, отрицательные препятствия и безопасное управление ровером.",
decision: "Использовать результат только как визуальную диагностику. Navigation/actuation оставить OFF; следующий gate — оценка временной стабильности и независимый person/vehicle STOP.",
}}
/>
)}
/>
);
}
function MixedRouteReviewEvidence({ review }: { review: VegetationMixedRouteReview }) {
const [index, setIndex] = useState(0);
const [mode, setMode] = useState<typeof MIXED_ROUTE_MODES[number]["value"]>("vegetation");
const [expanded, setExpanded] = useState(false);
const item = review.cases[index]!;
return (
<LaboratoryEvidenceViewer
label="RAVNOVES004TREE mixed route review"
className="m48-atlas-visual"
mode={mode}
modes={MIXED_ROUTE_MODES}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
chromeLayout="stacked"
actions={(
<>
<IconButton label="Предыдущая сцена" onClick={() => setIndex((index - 1 + review.cases.length) % review.cases.length)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующая сцена" onClick={() => setIndex((index + 1) % review.cases.length)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</>
)}
overlay={(
<div className="m48-atlas-visual__case">
<StatusBadge tone={item.phase === "urban" ? "accent" : item.phase === "transition" ? "warning" : "neutral"}>
{item.phase.toUpperCase()} · {index + 1}/{review.cases.length}
</StatusBadge>
<strong>sequence {item.sourceSequence} · +{decimal(item.sessionSeconds, 2)} s</strong>
<small>
TGS: {item.tgs.groundCells} ground · {item.tgs.occupiedCells} occupied · {item.tgs.unobservedCells} unobserved
</small>
</div>
)}
>
<div className="recorded-evidence-image-scene">
<img src={item.assets[mode]} alt="" draggable={false} />
</div>
</LaboratoryEvidenceViewer>
);
}
function MixedRouteReviewResult({
rigLabel,
review,
}: {
rigLabel: string;
review: VegetationMixedRouteReview;
}) {
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB V1 · RAVNOVES004TREE · село → город"
description="Существующий LAB-шаблон показывает 10 синхронных camera/LiDAR сцен одной записи. EoMT и DDRNet остаются независимыми слоями; TGS показывает отдельную геометрию и не может быть очищен семантической маской."
status="BOUNDED RECORDED REVIEW · truth отсутствует · commands OFF"
statusTone="warning"
facts={[
{ label: "Источник", value: `${review.sourceId} · ${review.frameCount} camera/LiDAR islands` },
{ label: "Переход", value: "5 rural · 1 transition · 4 urban" },
{ label: "Слои", value: "SOURCE · EoMT CITY · DDRNet VEGETATION · causal TGS" },
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
]}
brief={{
question: "Сохраняются ли городская семантика, растительность и геометрия при переходе из сельской среды в город?",
approach: "Выбраны десять соседних с исходными сцен camera-кадров, каждый синхронизирован с LiDAR в пределах 100 мс. Все три вычислительных слоя прогнаны на Worker 006 и запечатаны локально.",
principalResult: "Все 10 сцен обработаны EoMT, DDRNet и causal TGS. Слои можно переключать без наложения цветов и без зависимости LAB от воркера.",
limitation: "Это bounded islands без ручной truth. DDRNet шумит по подтипам растительности; TGS не доказывает обнаружение кювета или отрицательного препятствия.",
}}
method={{
completeness: "complete",
executionClass: "ai-inference",
pipelineId: "ravnoves004tree-eomt-ddrnet-causal-tgs-review/v1",
components: [
{ kind: "model", name: review.models.city.name, version: "sealed Worker run", role: "urban semantic review", identitySha256: null },
{ kind: "model", name: review.models.vegetation.name, version: "GOOSE DDRNet-39", role: "vegetation semantic review", identitySha256: null },
{ kind: "algorithm", name: review.models.tgs.name, version: "TRAVEL compatibility runner", role: "independent local geometry", identitySha256: null },
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="M4.7 TEMPLATE · RAVNOVES004TREE"
title="SOURCE / ГОРОД / ПРИРОДА / TGS · 10/10 · TRUTH отсутствует"
kind="diagnostic-model"
resizable
>
<MixedRouteReviewEvidence review={review} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Переход село → город воспроизведён; safety gate не закрыт"
status="Review ready · navigation/actuation OFF"
statusTone="warning"
metrics={[
{ label: "Aligned scenes", value: "10/10", hint: "camera + LiDAR + pose · автономный archive" },
{ label: "EoMT end-to-end p95", value: `${decimal(review.models.city.endToEndP95Ms, 2)} ms`, hint: `${decimal(review.models.city.inferenceFps, 2)} fps в изолированном прогоне` },
{ label: "DDRNet inference p95", value: `${decimal(review.models.vegetation.latencyP95Ms, 2)} ms`, hint: "candidate review · не совместный realtime stack" },
{ label: "TGS p95", value: `${decimal(review.models.tgs.latencyP95Ms, 2)} ms`, hint: `${review.models.tgs.cellSizeM} m cells · ${review.models.tgs.radiusM} m radius` },
]}
conclusion={{
proved: "Оба semantic слоя и causal TGS воспроизводимо работают на сельской, переходной и городской части новой записи.",
notProved: "Не доказаны accuracy без truth, временная стабильность по всему видео, детект кюветов и безопасное совместное realtime-управление ровером.",
decision: "Оставить navigation/actuation OFF. Следующий короткий gate — непрерывный realtime-load двух моделей плюс независимый person/vehicle STOP; кюветы проверять отдельной записью.",
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.",
}}
/>
)}
@@ -373,6 +166,7 @@ function MixedRouteReviewResult({
function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) {
const route = result.routeVideo!;
const linkedTgsResultId = route.linkedTgsResultId;
const [tgs, setTgs] = useState<M49TgsFullShadowResult | null>(null);
const [tgsError, setTgsError] = useState<string | null>(null);
@@ -380,8 +174,8 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
const controller = new AbortController();
setTgs(null);
setTgsError(null);
if (!route.linkedTgsResultId) return () => controller.abort();
void fetchM49TgsFullShadowResult(route.linkedTgsResultId, {
if (!linkedTgsResultId) return () => controller.abort();
void fetchM49TgsFullShadowResult(linkedTgsResultId, {
signal: controller.signal,
}).then((next) => {
if (controller.signal.aborted) return;
@@ -395,7 +189,11 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
}
});
return () => controller.abort();
}, [route.baseM4ResultId, route.linkedTgsResultId]);
}, [linkedTgsResultId, route.baseM4ResultId]);
if (!linkedTgsResultId) {
throw new Error("Vegetation LAB result has no linked canonical M4.9 TGS evidence.");
}
const semantic = {
id: "vegetation",
@@ -408,16 +206,14 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
maskAriaLabel: "DDRNet vegetation material prediction",
} as const;
if (route.linkedTgsResultId && tgs) {
if (tgsError) {
return (
<M49TgsFullShadowEvidence
result={tgs}
semanticOverride={semantic}
evidenceLabel="LAB V1 · EoMT + DDRNet + YOLOX + TGS"
/>
<div className="m4-replay-threat-visual__pane-status" role="alert">
Канонический M4.9 TGS слой недоступен: {tgsError}
</div>
);
}
if (route.linkedTgsResultId && !tgsError) {
if (!tgs) {
return (
<div className="m4-replay-threat-visual__pane-status" role="status">
Открываем sealed EoMT, TGS и coarse vegetation timeline
@@ -425,20 +221,11 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
);
}
return (
<>
<M4ReplayThreatVisual
resultId={route.baseM4ResultId}
evidenceLabel="LAB V1 · DDRNet"
showReferenceMediaLayers
showSpatialOverlaySummary={false}
semantic={semantic}
/>
{tgsError ? (
<div className="m4-replay-threat-visual__pane-status" role="alert">
TGS слой недоступен: {tgsError}
</div>
) : null}
</>
<M49TgsFullShadowEvidence
result={tgs}
semanticOverride={semantic}
evidenceLabel="LAB V1 · EoMT + DDRNet + YOLOX + TGS"
/>
);
}
@@ -458,8 +245,10 @@ export function VegetationShadowResultView({
/>
);
}
if (result.routeReview) {
return <MixedRouteReviewResult rigLabel={rigLabel} review={result.routeReview} />;
if (!result.routeVideo?.linkedTgsResultId) {
throw new Error(
"Vegetation LAB result has no canonical M4 source timeline and linked M4.9 TGS evidence.",
);
}
const route = result.routeVideo;
const selected = result.candidates.find(
@@ -472,9 +261,7 @@ export function VegetationShadowResultView({
<LaboratorySummary
title="LAB V1 · карта ровера · город + растительность"
description="Один recorded-контур RAVNOVES00 синхронно показывает городской EoMT, природный DDRNet, frozen YOLOX detections и causal TGS. Семантические маски переключаются, чтобы их цвета не скрывали друг друга; геометрическое veto остаётся независимым."
status={route
? "MULTILAYER RECORDED REVIEW · commands OFF · route truth отсутствует"
: "ROUTE EVIDENCE MISSING · commands OFF"}
status="MULTILAYER RECORDED REVIEW · commands OFF · route truth отсутствует"
statusTone="warning"
facts={[
{ label: "Источник", value: "RAVNOVES00 · sensor.camera.right · 4489 recorded frames" },
@@ -485,61 +272,31 @@ export function VegetationShadowResultView({
]}
brief={{
question: "Можно ли одновременно видеть городской и природный semantic stack, не теряя независимую геометрическую защиту?",
approach: "EoMT и DDRNet сохранены как два независимых sealed слоя на одной M4 timeline. В штатном M4.7 viewer пользователь переключает только отображаемую маску; YOLOX и TGS остаются активными слоями evidence.",
principalResult: route
? "Оба semantic archive доступны в одном viewer. Это не пиксельный fusion и не единая новая модель: городской и природный ответы остаются раздельными."
: "Route archive для этой immutable identity отсутствует.",
approach: "EoMT и DDRNet сохранены как два независимых sealed слоя на одной M4 timeline. В штатном M4.9 viewer пользователь переключает только отображаемую маску; YOLOX и TGS остаются активными слоями evidence.",
principalResult: "Оба semantic archive доступны в одном viewer. Это не пиксельный fusion и не единая новая модель: городской и природный ответы остаются раздельными.",
limitation: "RAVNOVES00 не имеет ручной truth. DDRNet заметно прыгает между HIGH GRASS, WOODY и UNKNOWN; поэтому subtype нельзя подавать напрямую в planner. Отсутствие класса никогда не означает свободный путь.",
}}
method={{
completeness: route ? "complete" : "legacy-partial",
completeness: "complete",
executionClass: "ai-inference",
pipelineId: "ravnoves-eomt-ddrnet-yolox-causal-tgs-recorded-review/v1",
components: [
{
kind: "model",
name: "EoMT Cityscapes semantic",
version: "sealed E47 archive",
role: "urban semantic review",
identitySha256: null,
},
{
kind: "model",
name: selected.loadedModelName,
version: selected.candidate,
role: "vegetation material candidate",
identitySha256: selected.checkpointSha256,
},
{
kind: "algorithm",
name: "Frozen YOLOX + causal TGS",
version: "linked M4/M4.9 archives",
role: "independent object and geometry veto",
identitySha256: null,
},
{ kind: "model", name: "EoMT Cityscapes semantic", version: "sealed E47 archive", role: "urban semantic review", identitySha256: null },
{ kind: "model", name: selected.loadedModelName, version: selected.candidate, role: "vegetation material candidate", identitySha256: selected.checkpointSha256 },
{ kind: "algorithm", name: "Frozen YOLOX + causal TGS", version: "linked M4/M4.9 archives", role: "independent object and geometry veto", identitySha256: null },
],
}}
/>
)}
evidence={route ? (
evidence={(
<LaboratoryEvidence
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
eyebrow="M4.9 · RAVNOVES00 FULL VIDEO"
title="EoMT CITY / DDRNet VEGETATION + YOLOX + CAUSAL TGS · 4489/4489 · TRUTH отсутствует"
kind="diagnostic-model"
resizable
>
<VegetationRouteEvidence result={result} />
</LaboratoryEvidence>
) : (
<LaboratoryEvidence
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
title="ROUTE ARCHIVE отсутствует"
kind="diagnostic-model"
>
<div className="m4-replay-threat-visual__pane-status" role="alert">
Для этой immutable identity нет полного route video evidence.
</div>
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
@@ -547,26 +304,10 @@ export function VegetationShadowResultView({
status="Semantics advisory · YOLOX/TGS veto cannot be cleared"
statusTone="warning"
metrics={[
{
label: "Route masks",
value: route ? `${route.frameCount}/${route.frameCount}` : "0/4489",
hint: "sealed local playback · Worker для открытия не нужен",
},
{
label: "Semantic sources",
value: route ? "2 independent layers" : "0",
hint: "EoMT CITY / DDRNet VEGETATION · display switches, evidence does not fuse",
},
{
label: "Vegetation worker p95",
value: `${decimal(selected.shadowLatencyP95Ms, 2)} ms`,
hint: "изолированный DDRNet inference; не совместный realtime stack",
},
{
label: "Vegetation peak VRAM",
value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} GiB`,
hint: "DDRNet candidate на Worker 006",
},
{ label: "Route masks", value: `${route.frameCount}/${route.frameCount}`, hint: "sealed local playback · Worker для открытия не нужен" },
{ label: "Semantic sources", value: "2 independent layers", hint: "EoMT CITY / DDRNet VEGETATION · display switches, evidence does not fuse" },
{ label: "Vegetation worker p95", value: `${decimal(selected.shadowLatencyP95Ms, 2)} ms`, hint: "изолированный DDRNet inference; не совместный realtime stack" },
{ label: "Vegetation peak VRAM", value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} GiB`, hint: "DDRNet candidate на Worker 006" },
]}
conclusion={{
proved: "На одной recorded timeline доступны городской EoMT, природный DDRNet, YOLOX detections и causal TGS; LAB автономна от Worker.",
@@ -10,6 +10,7 @@ interface M48EvidenceModeControlProps {
mode: M48BlindEvidenceMode;
cameraVisible: boolean;
spatialAvailable: boolean;
planAvailable?: boolean;
onModeChange: (mode: M48BlindEvidenceMode) => void;
onCameraVisibleChange: (visible: boolean) => void;
}
@@ -34,6 +35,7 @@ export function M48EvidenceModeControls({
mode,
cameraVisible,
spatialAvailable,
planAvailable = spatialAvailable,
onModeChange,
onCameraVisibleChange,
}: M48EvidenceModeControlProps) {
@@ -68,9 +70,9 @@ export function M48EvidenceModeControls({
<IconButton
label={spatialMode === "plan" ? "Скрыть план" : "Показать план"}
aria-pressed={spatialMode === "plan"}
disabled={!spatialAvailable || (!cameraVisible && spatialMode === "plan")}
disabled={!planAvailable || (!cameraVisible && spatialMode === "plan")}
onClick={() => {
if (!spatialAvailable) return;
if (!planAvailable) return;
onModeChange(nextM48SpatialMode(mode, cameraVisible, "plan"));
}}
>
@@ -11,11 +11,30 @@ const CHUNK_SIZE = 24;
const RETAINED_CHUNK_COUNT = 8;
const PREFETCH_CHUNKS_AHEAD = 2;
function chunkWindowStarts(activeStart: number, frameCount: number): readonly number[] {
return Array.from(
{ length: PREFETCH_CHUNKS_AHEAD + 2 },
(_, index) => activeStart + (index - 1) * CHUNK_SIZE,
).filter((start) => start >= 0 && start < frameCount);
export function e47SemanticChunkWindowStarts(
activeStart: number,
frameCount: number,
): readonly number[] {
return [
activeStart,
...Array.from(
{ length: PREFETCH_CHUNKS_AHEAD },
(_, index) => activeStart + (index + 1) * CHUNK_SIZE,
),
activeStart - CHUNK_SIZE,
].filter((start) => start >= 0 && start < frameCount);
}
export function cancelE47SemanticRequestsOutsideWindow<T extends { abort(): void }>(
inFlight: Map<number, T>,
desiredStarts: readonly number[],
): void {
const desired = new Set(desiredStarts);
for (const [start, controller] of inFlight) {
if (desired.has(start)) continue;
controller.abort();
inFlight.delete(start);
}
}
function errorMessage(error: unknown): string {
@@ -29,11 +48,13 @@ export function useE47SemanticTimelineFrame({
activeSequence,
frameCount,
taxonomy,
enabled = true,
}: {
resultId: string | null;
activeSequence: number | null;
frameCount: number;
taxonomy: readonly E47SemanticClass[];
enabled?: boolean;
}) {
const [chunks, setChunks] = useState<ReadonlyMap<number, E47SemanticTimelineChunk>>(
() => new Map(),
@@ -55,16 +76,21 @@ export function useE47SemanticTimelineFrame({
for (const controller of inFlight.current.values()) controller.abort();
inFlight.current.clear();
};
}, [resultId]);
}, [enabled, resultId]);
const activeStart = activeSequence === null
const activeStart = !enabled || activeSequence === null
? null
: Math.floor(activeSequence / CHUNK_SIZE) * CHUNK_SIZE;
activeStartRef.current = activeStart;
useEffect(() => {
if (!resultId || activeStart === null || frameCount < 1) return;
for (const start of chunkWindowStarts(activeStart, frameCount)) {
if (!enabled || !resultId || activeStart === null || frameCount < 1) {
cancelE47SemanticRequestsOutsideWindow(inFlight.current, []);
return;
}
const starts = e47SemanticChunkWindowStarts(activeStart, frameCount);
cancelE47SemanticRequestsOutsideWindow(inFlight.current, starts);
for (const start of starts) {
if (chunksRef.current.has(start) || inFlight.current.has(start)) continue;
const controller = new AbortController();
inFlight.current.set(start, controller);
@@ -95,8 +121,11 @@ export function useE47SemanticTimelineFrame({
.finally(() => {
if (inFlight.current.get(start) === controller) inFlight.current.delete(start);
});
// Semantic point arrays are large JSON payloads. Admit the active chunk
// first, then advance the bounded prefetch window one request per render.
break;
}
}, [activeStart, frameCount, resultId, taxonomy]);
}, [activeStart, chunks, enabled, frameCount, resultId, taxonomy]);
const activeFrame: E47SemanticTimelineFrame | null = useMemo(() => {
if (activeSequence === null || activeStart === null) return null;
@@ -107,7 +136,7 @@ export function useE47SemanticTimelineFrame({
return {
activeFrame,
loading: Boolean(resultId) && activeSequence !== null && !activeFrame && !error,
loading: enabled && Boolean(resultId) && activeSequence !== null && !activeFrame && !error,
error,
};
}
@@ -18,6 +18,7 @@ import {
const REQUESTED_CHUNK_FRAMES = 24;
const RETAINED_CHUNK_COUNT = 4;
const RETAINED_CHUNKS_BEHIND = 1;
const PREFETCH_CHUNKS_AHEAD = 1;
const RETAINED_CAMERA_POINT_OVERLAYS = 12;
@@ -31,10 +32,17 @@ export function m4ThreatChunkWindowStarts(
frameCount: number,
): readonly number[] {
if (chunkSize < 1 || frameCount < 1) return [];
return Array.from(
{ length: PREFETCH_CHUNKS_AHEAD + 1 },
(_, index) => activeChunkStart + index * chunkSize,
).filter((start) => start >= 0 && start < frameCount);
return [
activeChunkStart,
...Array.from(
{ length: PREFETCH_CHUNKS_AHEAD },
(_, index) => activeChunkStart + (index + 1) * chunkSize,
),
...Array.from(
{ length: RETAINED_CHUNKS_BEHIND },
(_, index) => activeChunkStart - (index + 1) * chunkSize,
),
].filter((start) => start >= 0 && start < frameCount);
}
export function cancelM4ThreatChunkRequestsOutsideWindow<T extends { abort(): void }>(
@@ -76,12 +84,16 @@ export function useM4ThreatTimelineFrame({
resultId,
timeline,
currentSeconds,
includeSpatialPoints = true,
endpointRoot,
spatialPlaybackTransport = "auto",
}: {
resultId: string;
timeline: M4ThreatTimeline | null;
currentSeconds: number;
includeSpatialPoints?: boolean;
endpointRoot?: string;
spatialPlaybackTransport?: "auto" | "sealed-binary" | "json";
}) {
const [chunks, setChunks] = useState<ReadonlyMap<number, M4ThreatTimelineChunk>>(
() => new Map(),
@@ -94,8 +106,10 @@ export function useM4ThreatTimelineFrame({
totalBytes: 0,
});
const [playbackError, setPlaybackError] = useState<string | null>(null);
const binaryPlayback = endpointRoot === undefined
|| endpointRoot === M4_THREAT_TIMELINE_ENDPOINT_ROOT;
const binaryPlayback = spatialPlaybackTransport === "sealed-binary"
|| (spatialPlaybackTransport === "auto" && (
endpointRoot === undefined || endpointRoot === M4_THREAT_TIMELINE_ENDPOINT_ROOT
));
const inFlight = useRef(new Map<number, AbortController>());
const chunksRef = useRef(chunks);
const activeChunkStartRef = useRef<number | null>(null);
@@ -107,7 +121,7 @@ export function useM4ThreatTimelineFrame({
setPlaybackError(null);
setPlaybackProgress({ phase: "manifest", loadedBytes: 0, totalBytes: 0 });
if (!timeline) return () => controller.abort();
if (!binaryPlayback) {
if (!binaryPlayback || !includeSpatialPoints) {
setPlaybackProgress({ phase: "ready", loadedBytes: 0, totalBytes: 0 });
return () => controller.abort();
}
@@ -127,7 +141,7 @@ export function useM4ThreatTimelineFrame({
}
});
return () => controller.abort();
}, [binaryPlayback, endpointRoot, resultId, timeline]);
}, [binaryPlayback, endpointRoot, includeSpatialPoints, resultId, timeline]);
useEffect(() => {
for (const controller of inFlight.current.values()) controller.abort();
@@ -140,7 +154,7 @@ export function useM4ThreatTimelineFrame({
for (const controller of inFlight.current.values()) controller.abort();
inFlight.current.clear();
};
}, [resultId, timeline]);
}, [includeSpatialPoints, resultId, timeline]);
const activeSequence = useMemo(
() => timeline
@@ -158,7 +172,11 @@ export function useM4ThreatTimelineFrame({
activeChunkStartRef.current = activeChunkStart;
useEffect(() => {
if (!timeline || activeChunkStart === null || (binaryPlayback && !playbackManifest)) return;
if (
!timeline
|| activeChunkStart === null
|| (binaryPlayback && includeSpatialPoints && !playbackManifest)
) return;
const starts = m4ThreatChunkWindowStarts(
activeChunkStart,
chunkSize,
@@ -170,7 +188,7 @@ export function useM4ThreatTimelineFrame({
const controller = new AbortController();
inFlight.current.set(start, controller);
void (async () => {
const playbackPointPack = binaryPlayback && playbackManifest
const playbackPointPack = binaryPlayback && includeSpatialPoints && playbackManifest
? await fetchM4ThreatPlaybackPointChunk(
playbackManifest,
Math.floor(start / playbackManifest.chunkFrameCount),
@@ -189,6 +207,7 @@ export function useM4ThreatTimelineFrame({
endpointRoot,
cameraObstacleProjectionDelivery: timeline.cameraObstacleProjectionDelivery,
playbackPointPack,
includePoints: includeSpatialPoints,
});
})()
.then((chunk) => {
@@ -220,7 +239,7 @@ export function useM4ThreatTimelineFrame({
// loaded first, then the next chunk is prefetched on the following render.
break;
}
}, [activeChunkStart, binaryPlayback, chunkSize, chunks, endpointRoot, playbackManifest, resultId, timeline]);
}, [activeChunkStart, binaryPlayback, chunkSize, chunks, endpointRoot, includeSpatialPoints, playbackManifest, resultId, timeline]);
const activeFrame: M4ThreatTimelineFrame | null = useMemo(() => {
if (activeSequence === null || activeChunkStart === null) return null;
@@ -0,0 +1,78 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let canonicalMapGravityLocalPointToBodyGround;
let canonicalRecordedLabPackedTgsCells;
let canonicalRecordedLabTgsIsCurrent;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
canonicalMapGravityLocalPointToBodyGround,
canonicalRecordedLabPackedTgsCells,
canonicalRecordedLabTgsIsCurrent,
} = await server.ssrLoadModule("/src/core/laboratory/canonicalRecordedLab.ts"));
});
after(async () => {
await server?.close();
});
const identity = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
];
test("canonical TGS validity never retains a sparse anchor beyond its sealed history", () => {
assert.equal(canonicalRecordedLabTgsIsCurrent(2_000_000_000, 1_000_000_000), true);
assert.equal(canonicalRecordedLabTgsIsCurrent(2_000_000_001, 1_000_000_000), false);
assert.equal(canonicalRecordedLabTgsIsCurrent(999_999_999, 1_000_000_000), false);
});
test("map-gravity-local TGS uses sensor translation and current ground body exactly once", () => {
const anchor = {
originMapXyzM: [10, 20, 0.68],
sensorOriginMapXyzM: [10, 20, 1],
basisMapFromBody: identity,
};
const current = {
originMapXyzM: [8, 20, 0],
sensorOriginMapXyzM: [8, 20, 0.32],
basisMapFromBody: identity,
};
assert.deepEqual(
canonicalMapGravityLocalPointToBodyGround([1, 2, -1], anchor, current),
[3, 2, 0],
);
const packed = canonicalRecordedLabPackedTgsCells({
centersXyM: [[1, 2]],
stateCodes: [2],
zBoundsM: [[-1, 0]],
}, anchor, current);
assert.deepEqual([...packed.centersBodyXyM], [3, 2]);
assert.deepEqual([...packed.zBoundsM], [0, 1]);
assert.deepEqual([...packed.stateCodes], [2]);
});
test("recorded LAB spatial loading is shared, profile-bound and experiment-neutral", async () => {
const [contract, scheduler, vegetation] = await Promise.all([
readFile(new URL("../src/core/laboratory/canonicalRecordedLabSpatial.ts", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/useCanonicalRecordedLabSpatialFrame.ts", import.meta.url), "utf8"),
readFile(new URL("../src/core/laboratory/vegetationShadow.ts", import.meta.url), "utf8"),
]);
assert.match(contract, /CANONICAL_RECORDED_LAB_SPATIAL_PROFILE/);
assert.match(contract, /profile: CANONICAL_RECORDED_LAB_SPATIAL_PROFILE/);
assert.match(scheduler, /Shared latest-request-wins scheduler/);
assert.match(scheduler, /identityRef\.current !== requestIdentity/);
assert.doesNotMatch(scheduler, /RAVNOVES|vegetation|DDRNet/);
assert.doesNotMatch(vegetation, /fetchCanonicalRecordedLabSpatialFrame|CanonicalRecordedLabSpatialFrame/);
});
@@ -0,0 +1,98 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let laboratoryRecordedEvidenceDemand;
let e47SemanticChunkWindowStarts;
let cancelE47SemanticRequestsOutsideWindow;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({ laboratoryRecordedEvidenceDemand } = await server.ssrLoadModule(
"/src/core/laboratory/recordedEvidenceProfile.ts",
));
({
e47SemanticChunkWindowStarts,
cancelE47SemanticRequestsOutsideWindow,
} = await server.ssrLoadModule(
"/src/workspaces/laboratory/useE47SemanticTimeline.ts",
));
});
after(async () => {
await server?.close();
});
test("camera-only LAB presentation does not acquire hidden spatial or semantic payloads", () => {
assert.deepEqual(laboratoryRecordedEvidenceDemand({
mediaMode: "video",
spatialMode: null,
showMediaSemantic: false,
showSpatialSemantic: true,
showMediaPoints: false,
classifiedSpatialMode: "overlay",
}), {
sourceTimelineMetadata: true,
recordedVideo: true,
exactCameraFrame: false,
sourceSpatialPoints: false,
cameraPointOverlay: false,
selectedSemanticMask: false,
selectedSemanticPoints: false,
classifiedSpatial: false,
});
});
test("visible M4 layers acquire only their selected camera and spatial evidence", () => {
assert.deepEqual(laboratoryRecordedEvidenceDemand({
mediaMode: "camera",
spatialMode: "plan",
showMediaSemantic: true,
showSpatialSemantic: true,
showMediaPoints: true,
classifiedSpatialMode: "overlay",
}), {
sourceTimelineMetadata: true,
recordedVideo: false,
exactCameraFrame: true,
sourceSpatialPoints: true,
cameraPointOverlay: true,
selectedSemanticMask: true,
selectedSemanticPoints: true,
classifiedSpatial: true,
});
});
test("a classified replacement does not also load the hidden source point track", () => {
const demand = laboratoryRecordedEvidenceDemand({
mediaMode: null,
spatialMode: "3d",
showMediaSemantic: false,
showSpatialSemantic: false,
showMediaPoints: false,
classifiedSpatialMode: "replace-source",
});
assert.equal(demand.sourceSpatialPoints, false);
assert.equal(demand.classifiedSpatial, true);
});
test("semantic point chunks are ordered active-first and stale requests are cancelled", () => {
assert.deepEqual(e47SemanticChunkWindowStarts(48, 4_489), [48, 72, 96, 24]);
assert.deepEqual(e47SemanticChunkWindowStarts(0, 4_489), [0, 24, 48]);
const cancelled = [];
const requests = new Map([
[0, { abort: () => cancelled.push(0) }],
[24, { abort: () => cancelled.push(24) }],
[240, { abort: () => cancelled.push(240) }],
]);
cancelE47SemanticRequestsOutsideWindow(requests, [240, 264]);
assert.deepEqual(cancelled, [0, 24]);
assert.deepEqual([...requests.keys()], [240]);
});
@@ -7,6 +7,7 @@ let createLiveViewerDiagnosticLifecycle;
let createAbortFencedBuildVerifier;
let createUiBuildStaleCoordinator;
let liveViewerDiagnosticBody;
let reloadRecordedViewerAfterStaleModuleFailure;
let server;
let uiBuildIdFromModuleScripts;
let verifyLiveViewerClientBuild;
@@ -22,6 +23,7 @@ before(async () => {
createLiveViewerDiagnosticLifecycle,
createUiBuildStaleCoordinator,
liveViewerDiagnosticBody,
reloadRecordedViewerAfterStaleModuleFailure,
uiBuildIdFromModuleScripts,
verifyLiveViewerClientBuild,
} = await server.ssrLoadModule("/src/core/observation/liveViewerDiagnostics.ts"));
@@ -204,6 +206,36 @@ test("build drift is reported once with the exact loaded build", async () => {
);
});
test("a recorded viewer reloads only when its failed lazy module belongs to a stale build", async () => {
const reloads = [];
const loadedUiBuildId = "/assets/index-abcdefgh.js";
const currentUiBuildId = "/assets/index-ijklmnop.js";
const fetcher = async (_url, options) => {
assert.equal(options.headers["X-MissionCore-UI-Build"], loadedUiBuildId);
return new Response(JSON.stringify({}), {
status: 200,
headers: { "X-MissionCore-UI-Build": currentUiBuildId },
});
};
assert.equal(await reloadRecordedViewerAfterStaleModuleFailure({
loadedUiBuildId,
fetcher,
reload: () => reloads.push("reload"),
}), true);
assert.deepEqual(reloads, ["reload"]);
assert.equal(await reloadRecordedViewerAfterStaleModuleFailure({
loadedUiBuildId: currentUiBuildId,
fetcher: async () => new Response(JSON.stringify({}), {
status: 200,
headers: { "X-MissionCore-UI-Build": currentUiBuildId },
}),
reload: () => reloads.push("unexpected"),
}), false);
assert.deepEqual(reloads, ["reload"]);
});
test("last unsubscribe fences an already queued build verification callback", () => {
const controller = new AbortController();
const observedSignals = [];
@@ -18,6 +18,7 @@ let nextM48ObjectId;
let laboratoryMetricLegendEntries;
let nearestLaboratoryRecordedClipFrame;
let laboratoryRecordedClipEndExclusiveNs;
let laboratoryRecordedClipClockGate;
let m48SpatialPlaybackWindow;
let trimM48SpatialPlaybackCache;
let nextM48CameraVisibility;
@@ -45,6 +46,7 @@ before(async () => {
({
nearestLaboratoryRecordedClipFrame,
laboratoryRecordedClipEndExclusiveNs,
laboratoryRecordedClipClockGate,
} = await server.ssrLoadModule(
"/src/components/laboratory/LaboratoryRecordedClipPlayer.tsx",
));
@@ -342,6 +344,21 @@ test("shared recorded clip clock selects exact frames and one stable loop bounda
assert.equal(laboratoryRecordedClipEndExclusiveNs(frames), 1_300_000_000);
});
test("shared recorded clip rejects stale media callbacks until an explicit seek lands", () => {
assert.deepEqual(laboratoryRecordedClipClockGate(1, 123), {
accept: false,
pendingSequence: 1,
});
assert.deepEqual(laboratoryRecordedClipClockGate(1, 1), {
accept: true,
pendingSequence: null,
});
assert.deepEqual(laboratoryRecordedClipClockGate(null, 124), {
accept: true,
pendingSequence: null,
});
});
test("M4.8 camera and spatial visibility are independent without an empty viewer", () => {
assert.equal(nextM48CameraVisibility("camera", true), true);
assert.equal(nextM48CameraVisibility("3d", true), false);
@@ -379,14 +379,16 @@ test("M4.6 hydrates a lightweight timeline frame from one retained binary point
});
test("M4.6 source cloud opens from one verified bounded chunk instead of the 105 MiB track", async () => {
const pointOffsets = [0, ...Array(4489).fill(2)];
const playbackResultId = `lab-v1-vegetation-shadow-${"b".repeat(64)}`;
const frameCount = 48;
const pointOffsets = [0, ...Array(frameCount).fill(2)];
const points = new Float32Array([11, 20, 30.25, 12, 19.5, 30]);
const pointBytes = points.buffer;
const pointSha256 = createHash("sha256").update(Buffer.from(pointBytes)).digest("hex");
const endpointRoot = "/api/v1/laboratory/m4-threat/results";
const chunks = Array.from({ length: 188 }, (_, index) => {
const chunks = Array.from({ length: Math.ceil(frameCount / 24) }, (_, index) => {
const start = index * 24;
const count = Math.min(24, 4489 - start);
const count = Math.min(24, frameCount - start);
const pointStart = pointOffsets[start];
const pointStop = pointOffsets[start + count];
const pointCount = pointStop - pointStart;
@@ -396,7 +398,7 @@ test("M4.6 source cloud opens from one verified bounded chunk instead of the 105
count,
point_start: pointStart,
point_count: pointCount,
url: `${endpointRoot}/${resultId}/timeline/playback/chunks/${index}`,
url: `${endpointRoot}/${playbackResultId}/timeline/playback/chunks/${index}`,
media_type: "application/octet-stream",
dtype: "<f4",
shape: [pointCount, 3],
@@ -406,8 +408,8 @@ test("M4.6 source cloud opens from one verified bounded chunk instead of the 105
});
const manifestPayload = {
schema_version: "missioncore.recorded-spatial-playback/v1",
result_id: resultId,
frame_count: 4489,
result_id: playbackResultId,
frame_count: frameCount,
point_count: 2,
point_offsets: pointOffsets,
chunk_frame_count: 24,
@@ -416,7 +418,7 @@ test("M4.6 source cloud opens from one verified bounded chunk instead of the 105
chunks,
track: {
id: "points-map-f32",
url: `${endpointRoot}/${resultId}/timeline/playback/tracks/points-map-f32`,
url: `${endpointRoot}/${playbackResultId}/timeline/playback/tracks/points-map-f32`,
media_type: "application/octet-stream",
dtype: "<f4",
shape: [2, 3],
@@ -434,12 +436,12 @@ test("M4.6 source cloud opens from one verified bounded chunk instead of the 105
if (url.endsWith("/timeline/playback/chunks/0")) return new Response(pointBytes.slice(0));
return new Response(null, { status: 404 });
};
const manifest = await fetchM4ThreatPlaybackManifest(resultId, { fetcher });
const manifest = await fetchM4ThreatPlaybackManifest(playbackResultId, { fetcher });
const chunk = await fetchM4ThreatPlaybackPointChunk(manifest, 0, { fetcher });
assert.deepEqual(requested, [
`${endpointRoot}/${resultId}/timeline/playback`,
`${endpointRoot}/${resultId}/timeline/playback/chunks/0`,
`${endpointRoot}/${playbackResultId}/timeline/playback`,
`${endpointRoot}/${playbackResultId}/timeline/playback/chunks/0`,
]);
assert.equal(chunk.pointCount, 2);
assert.equal(chunk.pointStart, 0);
@@ -769,7 +771,7 @@ test("M4.6 local SLAM surface reprojects registered increments into the active b
});
test("M4.6 spatial buffering keeps the active and one future chunk", () => {
assert.deepEqual(m4ThreatChunkWindowStarts(48, 24, 4489), [48, 72]);
assert.deepEqual(m4ThreatChunkWindowStarts(48, 24, 4489), [48, 72, 24]);
assert.deepEqual(m4ThreatChunkWindowStarts(0, 24, 4489), [0, 24]);
});
@@ -786,8 +788,8 @@ test("M4.6 spatial buffering drops stale in-flight windows across rapid jumps",
if (!inFlight.has(start)) inFlight.set(start, controller(start));
}
}
assert.deepEqual([...inFlight.keys()], [4488]);
assert.deepEqual(aborted, [0, 24, 1488, 1512]);
assert.deepEqual([...inFlight.keys()], [4488, 4464]);
assert.deepEqual(aborted, [0, 24, 1488, 1512, 1464]);
});
test("recorded evidence clock advances by selected rate and stops at the sealed end", () => {
@@ -847,8 +849,9 @@ test("recorded VIDEO clock cannot reverse an explicit operator pause", () => {
});
test("M4.6 viewer keeps media and spatial panes on one playback clock", async () => {
const [visual, visualCss, imageScene, videoScene, pointOverlay, metricScene] = await Promise.all([
const [visual, canonical, visualCss, imageScene, videoScene, pointOverlay, metricScene] = await Promise.all([
readFile(new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/CanonicalRecordedLabReplay.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/styles/m4-replay-threat.css", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/RecordedEvidenceImageScene.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/RecordedEvidenceVideoScene.tsx", import.meta.url), "utf8"),
@@ -858,10 +861,11 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(visual, /<RecordedEvidenceVideoScene/);
assert.match(visual, /<RecordedEvidenceImageScene/);
assert.match(visual, /<LaboratoryMetricEvidenceScene/);
assert.match(visual, /m4-replay-threat-visual__deck/);
assert.match(visual, /<CanonicalRecordedLabReplay/);
assert.match(canonical, /m4-replay-threat-visual__deck/);
assert.match(visual, /lastFrameRef/);
assert.match(visual, /lastSpatialFrameRef/);
assert.match(visual, /const spatialFrame = frame\?\.spatialAvailable/);
assert.match(visual, /const spatialFrame = currentSpatialFrame/);
assert.match(visual, /<ObservationTimeline/);
assert.match(visual, /useM4ThreatTimelineFrame/);
assert.match(visual, /resolveObservationSessionReplay\(timeline\.recordedSourceSessionId/);
@@ -876,19 +880,33 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(visual, /pointCloudOverlay=/);
assert.match(visual, /mediaMode/);
assert.match(visual, /spatialMode/);
assert.match(visual, /current === next \? null : next/);
assert.match(visual, /data-split=\{splitView \? "true" : undefined\}/);
assert.match(visual, /<SplitPane/);
assert.match(visual, /primarySize=\{splitView \? splitPrimarySize : mediaMode \? 100 : 0\}/);
assert.match(visual, /resizable=\{splitView\}/);
assert.match(visual, /separatorLabel="Изменить размер VIDEO\/CAMERA и 3D\/PLAN"/);
assert.match(visual, /secondaryMode=\{\{/);
assert.match(canonical, /current === next \? null : next/);
assert.match(canonical, /data-split=\{splitView \? "true" : undefined\}/);
assert.match(canonical, /<SplitPane/);
assert.match(canonical, /primary=\{mediaPane\}/);
assert.match(canonical, /secondary=\{spatialPane/);
assert.match(canonical, /primarySize=\{splitView \? splitPrimarySize : mediaMode !== "none" \? 100 : 0\}/);
assert.match(canonical, /resizable=\{splitView\}/);
assert.match(canonical, /separatorLabel="Изменить размер VIDEO\/CAMERA и 3D\/PLAN"/);
assert.match(canonical, /secondaryMode=\{\{/);
assert.match(visual, /playback=\{playbackController\.playback\}/);
assert.match(visual, /clock: mediaMode === "video" \? "external" : "animation"/);
assert.match(visual, /clock: "external"/);
assert.match(visual, /useCanonicalRecordedLabReplayState/);
assert.match(canonical, /current === next \? null : next/);
assert.match(visual, /timelineFrame\.activeSequence \+ 1/);
assert.match(visual, /segmentCount=\{timeline\.frameCount\}/);
assert.match(visual, /onPlaybackChange=\{playbackController\.synchronize\}/);
assert.match(visual, /onPlayingRejected=\{\(\) => playbackController\.setPlaying\(false\)\}/);
assert.match(
await readFile(new URL("../src/components/laboratory/useRecordedEvidencePlayback.ts", import.meta.url), "utf8"),
/if \(clock === "animation"\) return;/,
);
assert.match(visual, /playbackAuthority="media"/);
assert.match(visual, /playbackTransport = "epoch-stream"/);
assert.match(visual, /playbackTransport=\{playbackTransport\}/);
assert.match(
await readFile(new URL("../src/components/RecordedFmp4Player.tsx", import.meta.url), "utf8"),
/if \(playbackAuthority === "host"\) return;/,
);
assert.match(visual, /currentSeconds: playbackController\.playback\.currentSeconds/);
assert.match(visualCss, /m4-replay-threat-visual__deck > \.nodedc-split-pane/);
assert.match(visualCss, /m4-replay-threat-visual__pane-toolbar\[data-pane-toolbar="media"\]/);
@@ -912,6 +930,7 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(visualCss, /laboratory-metric-evidence-scene__legend/);
assert.match(visualCss, /bottom: auto/);
assert.match(videoScene, /<RecordedFmp4Player/);
assert.match(videoScene, /!playback\.playing && Math\.abs\(next\.currentSeconds - playback\.currentSeconds\) > 0\.35/);
assert.match(imageScene, /<RecordedEvidenceBoxOverlay/);
assert.match(imageScene, /<RecordedEvidencePointCloudOverlay/);
assert.match(videoScene, /<RecordedEvidencePointCloudOverlay/);
@@ -920,19 +939,20 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(metricScene, /OrbitControls/);
assert.match(visual, /LOCAL SLAM/);
assert.match(visual, /showLocalSurface/);
assert.match(visual, /const latestAvailableSpatialFrame = \[\.\.\.timelineFrame\.availableFrames\][\s\S]*candidate\.spatialAvailable[\s\S]*candidate\.sequence <= timelineFrame\.activeSequence/);
assert.match(
visual,
/pointCloudBodyXyzM=\{displayedClassifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: classifiedContextSpatialFrame\?\.pointCloudBodyXyzM \?\? \[\]\}/,
/pointCloudBodyXyzM=\{displayedClassifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: classifiedContextSpatialFrame\?\.pointCloudBodyXyzM[\s\S]*\?\? activeSpatialFrame\?\.pointCloudBodyXyzM[\s\S]*\?\? \[\]\}/,
);
assert.match(
visual,
/const classifiedSpatialFrame = classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence[\s\S]*lastClassifiedSpatialFrameRef[\s\S]*const displayedClassifiedSpatialFrame = classifiedSpatialFrame\s*&&\s*classifiedSpatialFrame\.sampleAvailable !== false/,
/const classifiedSpatialFrame = hasClassifiedSpatialOutput[\s\S]*classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence[\s\S]*lastClassifiedSpatialFrameRef[\s\S]*const displayedClassifiedSpatialFrame = classifiedSpatialFrame\s*&&\s*classifiedSpatialFrame\.sampleAvailable !== false/,
);
assert.doesNotMatch(visual, /classifiedSpatialFrame\?\.sampleAvailable !== false/);
assert.match(visual, /timelineFrame\.availableFrames\.find/);
assert.match(visual, /localSurfaceBodyXyzM=\{localSurface\.pointsBodyXyzM\}/);
assert.match(visual, /showLocalSurface=\{showLocalSurface\}/);
assert.match(visual, /\{semantic \? \([\s\S]*>\s*SEMANTICS\s*<\/Button>/);
assert.match(visual, /\{activeSemantic \? \([\s\S]*>\s*SEMANTICS\s*<\/Button>/);
assert.match(visual, /current safety — все \{classifiedCellCount\.toLocaleString\("ru-RU"\)\} TGS-ячейки UNOBSERVED/);
assert.match(
visual,
@@ -951,12 +971,18 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
});
test("M4.6 keeps the recorded VIDEO player mounted across media mode toggles", async () => {
const visual = await readFile(
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
"utf8",
);
assert.match(visual, /const mediaPane = \(/);
assert.match(visual, /hidden=\{!mediaMode\}/);
const [visual, canonical] = await Promise.all([
readFile(
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/components/laboratory/CanonicalRecordedLabReplay.tsx", import.meta.url),
"utf8",
),
]);
assert.match(canonical, /const mediaPane = \(/);
assert.match(canonical, /hidden=\{mediaMode === "none"\}/);
assert.match(visual, /data-media="video"/);
assert.match(visual, /hidden=\{mediaMode !== "video"\}/);
assert.match(visual, /\{videoSource \? \(/);
@@ -11,7 +11,12 @@ let recordedMediaSeekableCoverage;
let recordedMediaFragmentUrl;
let recordedMediaDecodeStartSequence;
let recordedMediaSegmentAppendOrder;
let recordedMediaSegmentSequenceAtTime;
let recordedMediaCanRollTarget;
let recordedMediaTimestampStallRecoveryTarget;
let nextRecordedMediaRandomAccessSequence;
let recordedMediaRecoveryTargetSequence;
let selectRecordedMediaPreparationEpoch;
before(async () => {
server = await createServer({
@@ -26,7 +31,12 @@ before(async () => {
recordedMediaFragmentUrl,
recordedMediaDecodeStartSequence,
recordedMediaSegmentAppendOrder,
recordedMediaSegmentSequenceAtTime,
recordedMediaCanRollTarget,
recordedMediaTimestampStallRecoveryTarget,
nextRecordedMediaRandomAccessSequence,
recordedMediaRecoveryTargetSequence,
selectRecordedMediaPreparationEpoch,
} = await server.ssrLoadModule("/src/components/RecordedFmp4Player.tsx"));
});
@@ -161,7 +171,7 @@ test("decoded duration and seekable range cover the complete declared epoch", ()
assert.equal(recordedMediaSeekableCoverage(20, 20, 20, 1, 1.01), false);
});
test("recorded player keeps full-archive range fallback and uses bounded generation-bound fragments", async () => {
test("production replay derives bounded fragments and retains native range fallback", async () => {
const source = await readFile(
new URL("../src/components/RecordedFmp4Player.tsx", import.meta.url),
"utf8",
@@ -176,6 +186,13 @@ test("recorded player keeps full-archive range fallback and uses bounded generat
assert.match(source, /hasPresentedFrame/);
assert.match(source, /retainForwardFrame/);
assert.match(source, /candidateTarget\.sequence >= previousTarget\.sequence/);
assert.match(source, /effectiveSegmentCount = segmentCount \?\? epoch\?\.segmentCount \?\? null/);
assert.match(source, /requestedSegmentSequence = segmentSequence \?\?/);
assert.match(source, /waitForRecordedVideoInitialFrame/);
assert.match(source, /setSegmentRecoveryGeneration/);
assert.match(source, /resumePlaybackIfRequested/);
assert.match(source, /!playbackPlayingRef\.current/);
assert.match(source, /await video\.play\(\)/);
assert.equal(recordedMediaDecodeStartSequence([1, 1491, 1501], 1500), 1491);
assert.deepEqual(
@@ -209,6 +226,44 @@ test("recorded player keeps full-archive range fallback and uses bounded generat
);
});
test("production replay derives its bounded fragment directly from the source clock", () => {
const ends = [0.101, 0.185, 0.286, 0.401];
const epochStart = 39.215263458;
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart), 1);
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 0.101), 1);
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 0.103), 2);
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 0.4), 4);
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 1), null);
});
test("a corrupt fragment advances recovery to the next random-access frame", () => {
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 1), 11);
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 20), 21);
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 49), null);
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 0), null);
});
test("paused seek displays the recovery keyframe until the source clock leaves the corrupt GOP", () => {
assert.equal(recordedMediaRecoveryTargetSequence(120, 120, 149), 149);
assert.equal(recordedMediaRecoveryTargetSequence(130, 120, 149), 149);
assert.equal(recordedMediaRecoveryTargetSequence(119, 120, 149), 119);
assert.equal(recordedMediaRecoveryTargetSequence(149, 120, 149), 149);
assert.equal(recordedMediaRecoveryTargetSequence(151, 120, 149), 151);
assert.equal(recordedMediaRecoveryTargetSequence(null, 120, 149), null);
assert.equal(recordedMediaRecoveryTargetSequence(120, null, null), 120);
});
test("camera admission selects one bounded epoch while the shared clock is outside video", () => {
const epochs = [
{ ordinal: 1, timelineStartSeconds: 39, timelineEndSeconds: 100 },
{ ordinal: 2, timelineStartSeconds: 120, timelineEndSeconds: 180 },
];
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 0).ordinal, 1);
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 70).ordinal, 1);
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 110).ordinal, 2);
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 200).ordinal, 2);
});
test("recorded player preserves forward rolling playback but seeks backward clip loops", () => {
assert.equal(recordedMediaCanRollTarget(20, 21, true, true), true);
assert.equal(recordedMediaCanRollTarget(20, 20, true, true), true);
@@ -217,6 +272,12 @@ test("recorded player preserves forward rolling playback but seeks backward clip
assert.equal(recordedMediaCanRollTarget(20, 21, true, false), false);
});
test("recorded player skips only a proven buffered corrupt timestamp interval", () => {
assert.equal(recordedMediaTimestampStallRecoveryTarget(11.422, [[0, 16.287]]), 11.602);
assert.equal(recordedMediaTimestampStallRecoveryTarget(16.25, [[0, 16.287]]), null);
assert.equal(recordedMediaTimestampStallRecoveryTarget(20, [[0, 16.287]]), null);
});
test("loading and error overlays fully conceal recorded camera pixels", async () => {
const css = await readFile(
new URL("../src/styles/observation.css", import.meta.url),
@@ -34,7 +34,7 @@ function camera(phase, byteLength = 1_024) {
return { phase, byteLength, message: null };
}
test("recorded session admits RRD and every declared camera as one atomic generation", () => {
test("recorded session keeps camera and timeline atomic without hiding a ready spatial frame", () => {
const ids = ["camera.left", "camera.right"];
const oneCamera = {
"camera.left": camera("ready"),
@@ -42,7 +42,7 @@ test("recorded session admits RRD and every declared camera as one atomic genera
};
const partialGate = admission.recordedSessionAdmissionPhase("ready", ids, oneCamera);
assert.equal(partialGate, "loading");
assert.equal(rerunPresentationStatus("ready", partialGate, true), "loading");
assert.equal(rerunPresentationStatus("ready", partialGate, true), "ready");
assert.equal(
recordedMediaPresentationState("ready", "generation", "generation", false, partialGate),
"loading",
@@ -71,7 +71,7 @@ test("any RRD or camera failure closes the complete recorded session", () => {
...cameras,
"camera.right": camera("ready"),
}), "error");
assert.equal(rerunPresentationStatus("ready", "error", true), "error");
assert.equal(rerunPresentationStatus("ready", "error", true), "ready");
assert.equal(
recordedMediaPresentationState("ready", "generation", "generation", false, "error"),
"error",
@@ -257,6 +257,14 @@ test("recorded RRD bytes are never split across LogChannel.send_rrd calls", asyn
source,
/if \(readyToRender && !readyPublished\)[\s\S]*clearRecordedAdmissionWatchdog\(\);/,
);
assert.match(
source,
/reloadRecordedViewerAfterStaleModuleFailure\(\{[\s\S]*loadedUiBuildId: diagnosticLifecycle\.lineage\.uiBuildId/,
);
assert.match(
source,
/!recordedPerceptionUrl \|\| !recordedPerceptionLayers\.enabled/,
);
});
test("one live document owns one native Rerun receiver", async () => {
@@ -287,8 +295,12 @@ test("raw replay exercises the same streaming receiver lifecycle as a live scan"
source,
/const livePresentationActivitySequence = metrics\?\.publishedFrameCount \?\?[\s\S]*liveRerunSource && !streamActive \? 1 : null/,
);
assert.match(source, /followLive=\{liveRerunSource\}/);
assert.match(source, /liveActivitySequence=\{livePresentationActivitySequence\}/);
assert.match(
source,
/liveAcquisitionRerunProfile\(\{[\s\S]*liveActivitySequence: livePresentationActivitySequence/,
);
assert.match(source, /<RerunViewport[\s\S]*profile=\{rerunViewerProfile\}/);
assert.doesNotMatch(source, /followLive=\{liveRerunSource\}/);
assert.match(
source,
/sourceUrl\.trim\(\) && pointCloudVisible && !intentionalSourceEnd/,
@@ -11,6 +11,7 @@ let isRecordedPlaybackPresentationReady;
let isUsableRecordedPlaybackRange;
let recordedPlaybackBufferState;
let recordedPlaybackRangeWhenReady;
let rerunPresentationStatus;
before(async () => {
server = await createServer({
@@ -26,6 +27,7 @@ before(async () => {
isUsableRecordedPlaybackRange,
recordedPlaybackBufferState,
recordedPlaybackRangeWhenReady,
rerunPresentationStatus,
} = await server.ssrLoadModule("/src/components/RerunViewport.tsx"));
});
@@ -33,7 +35,7 @@ after(async () => {
await server?.close();
});
test("a first frame reports buffer telemetry but is not ready for presentation", () => {
test("a verified first frame is presentable while full-range controls stay closed", () => {
assert.equal(isUsableRecordedPlaybackRange(null), false);
assert.equal(isUsableRecordedPlaybackRange({ min: Number.NaN, max: 0 }), false);
assert.equal(isUsableRecordedPlaybackRange({ min: 2, max: 1 }), false);
@@ -47,8 +49,11 @@ test("a first frame reports buffer telemetry but is not ready for presentation",
bufferProgress: 0,
fullyBuffered: false,
});
assert.equal(isRecordedPlaybackReady(true, true, firstFrame), false);
assert.equal(isRecordedPlaybackReady(true, true, firstFrame), true);
assert.equal(recordedPlaybackRangeWhenReady({ min: 0, max: 0 }, firstFrame, true), null);
assert.equal(rerunPresentationStatus("ready", "loading", true), "ready");
assert.equal(rerunPresentationStatus("loading", "ready", true), "loading");
assert.equal(rerunPresentationStatus("ready", "error", true), "ready");
});
test("host timeline remains unmounted until the verified recording is fully ready", () => {
@@ -107,7 +112,7 @@ test("buffer progress grows independently and preserves the full-buffer toleranc
);
assert.equal(missingDeclaredStart.bufferProgress, 1);
assert.equal(missingDeclaredStart.fullyBuffered, false);
assert.equal(isRecordedPlaybackReady(true, true, missingDeclaredStart), false);
assert.equal(isRecordedPlaybackReady(true, true, missingDeclaredStart), true);
});
test("a verified split boundary spill covers and clamps the declared LAB window", () => {
@@ -75,20 +75,29 @@ test("semantic point alignment follows the last qualified spatial increment", as
});
test("M4 keeps independent semantic controls in media and spatial panes", async () => {
const source = await readFile(
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
"utf8",
);
const [source, canonical] = await Promise.all([
readFile(
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/components/laboratory/CanonicalRecordedLabReplay.tsx", import.meta.url),
"utf8",
),
]);
assert.match(source, /showMediaSemantic/);
assert.match(source, /showSpatialSemantic/);
assert.match(source, /semantic && showMediaSemantic && frame/);
assert.match(source, /activeSpatialSemantic = spatialSemantic \?\? activeSemantic/);
assert.match(source, /spatialSemanticClasses/);
assert.match(source, /spatialSemanticPalette/);
assert.match(source, /activeSemantic && evidenceDemand\.selectedSemanticMask && frame/);
assert.match(source, /Array\.from\(\{ length: 12 \}, \(_, index\) => index \+ 1\)/);
assert.match(source, /\|\| !showSpatialSemantic/);
assert.match(source, /aria-label="Слои камеры и видео"/);
assert.match(source, /aria-label="Слои 3D и плана"/);
assert.match(source, /data-pane-mode="media"/);
assert.match(source, /data-pane-mode="spatial"/);
assert.match(source, /modeControlsVisible=\{!splitView\}/);
assert.match(canonical, /data-pane-mode="media"/);
assert.match(canonical, /data-pane-mode="spatial"/);
assert.match(canonical, /modeControlsVisible=\{!splitView\}/);
assert.match(source, /semanticOverlay=\{mediaMode === "video" \? semanticOverlay : undefined\}/);
});
@@ -100,6 +100,8 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity
assert.match(runtime, /maximum: \{[^}]*lodRangeMin: 0, lodRangeMax: 5, pixelRatio: 2, splatBudget: 4_000_000, targetFps: 60/);
assert.match(runtime, /app\.scene\.gsplat\.splatBudget = profile\.splatBudget/);
assert.match(runtime, /app\.autoRender = false/);
assert.match(runtime, /app\.systems\.gsplat\?\.on\("frame:request", this\.requestGsplatFrame\)/);
assert.match(runtime, /app\.systems\.gsplat\?\.off\("frame:request", this\.requestGsplatFrame\)/);
assert.match(runtime, /app\.on\("update", this\.updateFrame\)/);
assert.match(runtime, /if \(!cameraChanged && this\.controlMode === "free"\) return/);
assert.doesNotMatch(runtime, /app\.on\("frameupdate"/);
@@ -162,6 +164,8 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity
assert.match(ugv, /new this\.ammo\.btRaycastVehicle/);
assert.match(ugv, /type: "mesh"/);
assert.match(ugv, /MeshoptSimplifier\.simplify/);
assert.match(ugv, /MeshoptSimplifier\.simplifySloppy/);
assert.match(ugv, /Physics proxy превысил лимит/);
assert.match(ugv, /PHYSICS_PROXY_MAX_TRIANGLES = 240_000/);
assert.match(ugv, /createPhysicsProxyMesh/);
assert.match(ugv, /findSafeSpawnPosition/);
@@ -179,8 +183,33 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity
assert.match(ugv, /desiredSpeed = forwardInput \* maxSpeed/);
assert.match(ugv, /maximumAcceleration = clamp\(1\.4 \+ maxSpeed \* 0\.35, 1\.8, 4\.2\)/);
assert.match(ugv, /SERVICE_BRAKE_DECELERATION_MPS2 = 1\.8/);
assert.match(ugv, /horizontalSpeed - deceleration \* Math\.max\(0, deltaSeconds\)/);
assert.match(ugv, /this\.vehicle\.setBrake\(0, index\)/);
assert.match(ugv, /PARKING_BRAKE_HOLD_DECELERATION_MPS2 = 6/);
assert.match(ugv, /PARKING_BRAKE_ENGAGE_SPEED_MPS = 0\.08/);
assert.match(ugv, /TYRE_FRICTION_SLIP = 8\.5/);
assert.match(ugv, /TYRE_STATIC_FRICTION_COEFFICIENT = 0\.95/);
assert.match(ugv, /TYRE_KINETIC_FRICTION_COEFFICIENT = 0\.78/);
assert.match(ugv, /TYRE_CONTACT_VELOCITY_RESPONSE_PER_SECOND = 10/);
assert.match(ugv, /GRAVITY_METERS_PER_SECOND_SQUARED = 9\.81/);
assert.match(ugv, /holding = !braking && forwardInput === 0 && turnInput === 0/);
assert.match(ugv, /longitudinalSpeedMetersPerSecond/);
assert.match(ugv, /parkingBrakeEngaged = holding/);
assert.match(ugv, /this\.settings\.massKg \* brakeDeceleration/);
assert.match(ugv, /this\.vehicle\.setBrake\(wheelBrakeForce, index\)/);
assert.match(ugv, /set_m_frictionSlip\(\s*parkingBrakeEngaged \? 0 : TYRE_FRICTION_SLIP/);
assert.match(ugv, /applyParkingTyreContact/);
assert.match(ugv, /wheel\.get_m_wheelsSuspensionForce\(\)/);
assert.doesNotMatch(ugv, /get_m_isInContact/);
assert.match(ugv, /normalForce < MIN_TYRE_NORMAL_FORCE_NEWTONS/);
assert.match(ugv, /lateralGravityAcceleration/);
assert.match(ugv, /longitudinalGravityAcceleration/);
assert.match(ugv, /TYRE_CONTACT_VELOCITY_RESPONSE_PER_SECOND \* lateralSlipSpeed/);
assert.match(ugv, /TYRE_CONTACT_VELOCITY_RESPONSE_PER_SECOND \* longitudinalSlipSpeed/);
assert.match(ugv, /Math\.hypot\(trialLateralForce, trialLongitudinalForce\)/);
assert.match(ugv, /body\.applyImpulse\(this\.tyreImpulseNative, this\.tyreRelativePositionNative\)/);
assert.doesNotMatch(ugv, /horizontalSpeed - deceleration \* Math\.max\(0, deltaSeconds\)/);
assert.match(ugv, /rollingFriction: 0\.12/);
assert.match(ugv, /angularDamping: 0\.6/);
assert.match(ugv, /wheel\.set_m_frictionSlip\(TYRE_FRICTION_SLIP\)/);
assert.doesNotMatch(ugv, /massKg \* 3/);
assert.match(ugv, /pureTurn = !braking && forwardInput === 0 && turnInput !== 0/);
assert.match(ugv, /desiredYawRate = -turnInput \* maxTurnRate/);
@@ -6,7 +6,9 @@ import { createServer } from "vite";
let server;
let fetchVegetationBenchmarkResult;
let fetchCanonicalRecordedLabSpatialFrame;
let fetchVegetationShadowResult;
let fetchVegetationRouteTgsAnchor;
let vegetationFullRouteMaskUrl;
before(async () => {
@@ -18,10 +20,14 @@ before(async () => {
({
fetchVegetationBenchmarkResult,
fetchVegetationShadowResult,
fetchVegetationRouteTgsAnchor,
vegetationFullRouteMaskUrl,
} = await server.ssrLoadModule(
"/src/core/laboratory/vegetationShadow.ts",
));
({ fetchCanonicalRecordedLabSpatialFrame } = await server.ssrLoadModule(
"/src/core/laboratory/canonicalRecordedLabSpatial.ts",
));
});
after(async () => {
@@ -185,6 +191,7 @@ function fullRouteReview() {
return {
source_id: "RAVNOVES004TREE",
session_id: "20260828T130511Z_viewer_live",
linked_route_review_result_id: `lab-v1-vegetation-shadow-${"9".repeat(64)}`,
source_job_id: "recorded-camera-eb2783c5480d56bda07c8af0",
source_job_input_sha256: "eb2783c5480d56bda07c8af008dff5344d19dc550ef70fe2075d6f098f7cc715",
source_stream_sha256: "e5eb017e2cc0f546736eda5235ca157b501913093cb64af5e548e335417e1bac",
@@ -364,8 +371,88 @@ test("vegetation GOOSE benchmark opens through its separate archival endpoint",
assert.equal(result.validationCases.length, 12);
});
test("vegetation route TGS anchor keeps exact sealed metric shapes", async () => {
let requestedUrl = "";
const anchor = await fetchVegetationRouteTgsAnchor(resultId, 409, {
fetcher: async (url) => {
requestedUrl = String(url);
return new Response(JSON.stringify({
schema_version: "missioncore.lab-v1-route-tgs-anchor/v1",
source_sequence: 409,
slot: 1,
current_points_xyz_m: [[1, 2, 3], [4, 5, 6]],
costmap: {
cell_size_m: 0.45,
centers_xy_m: Array.from({ length: 2244 }, (_, index) => [index, -index]),
state_codes: Array.from({ length: 2244 }, (_, index) => index % 4),
z_bounds_m: Array.from({ length: 2244 }, () => [null, null]),
},
}), { status: 200, headers: { "Content-Type": "application/json" } });
},
});
assert.equal(
requestedUrl,
`/api/v1/laboratory/vegetation-shadow/${resultId}/route-tgs-anchor/409`,
);
assert.equal(anchor.sourceSequence, 409);
assert.equal(anchor.currentPointsXyzM.length, 2);
assert.equal(anchor.costmap.centersXyM.length, 2244);
assert.deepEqual(new Set(anchor.costmap.stateCodes), new Set([0, 1, 2, 3]));
});
test("canonical recorded LAB spatial frame keeps source, SLAM and body identity on one clock", async () => {
const generation = "e".repeat(64);
let requestedUrl = "";
const frame = await fetchCanonicalRecordedLabSpatialFrame("session-004", generation, 82_770_000_000, {
fetcher: async (url) => {
requestedUrl = String(url);
return new Response(JSON.stringify({
schema_version: "missioncore.canonical-recorded-lab-spatial-frame/v3",
target_time_ns: 82_770_000_000,
source_time_ns: 82_769_535_708,
pose_time_ns: 82_769_535_708,
trajectory_time_ns: 82_700_000_000,
coordinate_frame: "body-ground",
sensor_height: {
meters: 0.32,
source: "local-source-cloud-ground-quantile-median",
sample_count: 20,
mad_m: 0.03,
authority: "visual-derived",
},
spatial_profile: {
profile_id: "source-paced-ground-v3",
local_slam_history_seconds: 5,
local_slam_radius_m: 30,
local_slam_voxel_size_m: 0.12,
local_slam_point_limit: 27000,
},
source_point_count: 2,
source_points_body_xyz_m: [[1, 2, 3], [4, 5, 6]],
local_slam_source_frame_count: 2,
local_slam_source_point_count: 4,
local_slam_point_count: 2,
local_slam_body_xyz_m: [[0, 0, 0], [1, 0, 0]],
body_frame: {
origin_map_xyz_m: [33, 4, 1],
sensor_origin_map_xyz_m: [33, 4, 1.32],
basis_map_from_body: [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
},
}), { status: 200, headers: { "Content-Type": "application/json" } });
},
});
assert.equal(
requestedUrl,
`/api/v1/observation-sessions/session-004/canonical-lab/spatial-frame?generation=${generation}&time_ns=82770000000&profile=source-paced-ground-v3`,
);
assert.equal(frame.sourcePointCount, 2);
assert.equal(frame.localSlamBodyXyzM.length, 2);
assert.equal(frame.sensorHeight.meters, 0.32);
assert.deepEqual(frame.bodyFrame.originMapXyzM, [33, 4, 1]);
});
test("vegetation realtime LAB and archival benchmark use separate admitted instruments", async () => {
const [resultSource, benchmarkSource] = await Promise.all([
const [resultSource, benchmarkSource, m49Source, canonicalSource] = await Promise.all([
readFile(
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
"utf8",
@@ -374,17 +461,40 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr
new URL("../src/workspaces/laboratory/VegetationBenchmarkResult.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/components/laboratory/CanonicalRecordedLabReplay.tsx", import.meta.url),
"utf8",
),
]);
assert.doesNotMatch(resultSource, /M48MaskComparisonVisual/);
assert.match(resultSource, /M4ReplayThreatVisual/);
assert.match(resultSource, /M49TgsFullShadowEvidence/);
assert.match(resultSource, /semanticOverride/);
assert.match(resultSource, /EoMT CITY \/ DDRNet VEGETATION/);
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 4);
assert.match(resultSource, /RAVNOVES004TREE mixed route review/);
assert.match(resultSource, /RAVNOVES004TREE full recorded review/);
assert.match(resultSource, /LaboratoryRecordedClipPlayer/);
assert.match(resultSource, /className="m48-clip-player__overlay"/);
assert.match(m49Source, /spatialSemantic=\{spatialSemantic\}/);
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, /<M4ReplayThreatVisual/);
assert.match(resultSource, /timelineEndpointRoot=\{VEGETATION_TIMELINE_ENDPOINT\}/);
assert.match(resultSource, /playbackTransport="segmented"/);
assert.match(resultSource, /recoverTimestampStalls/);
assert.doesNotMatch(resultSource, /RerunViewport/);
assert.doesNotMatch(resultSource, /cacheRef|pumpRef|desiredRef/);
assert.doesNotMatch(resultSource, /LaboratoryRecordedClipPlayer|M48EvidenceModeRail/);
assert.doesNotMatch(resultSource, /assets\.tgs|<img/);
assert.match(resultSource, /SOURCE POINTS/);
assert.match(resultSource, /TGS COSTMAP/);
assert.match(resultSource, /point-aligned 3D semantics пока не запечатаны/);
assert.match(resultSource, /cellLayerAvailable: false/);
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(resultSource, /linked canonical M4\.9 TGS evidence/);
assert.match(resultSource, /linkedTgsResultId/);
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
assert.doesNotMatch(benchmarkSource, /M49TgsFullShadowEvidence/);
@@ -0,0 +1,88 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE;
let liveAcquisitionRerunProfile;
let recordedSessionRerunProfile;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE,
liveAcquisitionRerunProfile,
recordedSessionRerunProfile,
} = await server.ssrLoadModule("/src/core/observation/viewerProfile.ts"));
});
after(async () => {
await server?.close();
});
test("live acquisition profile cannot acquire recorded playback policy", () => {
assert.deepEqual(liveAcquisitionRerunProfile({
sourceUrl: "rerun+http://127.0.0.1:9877/proxy",
liveActivitySequence: 12,
liveStreamId: "acquisition-1",
liveRecoveryAuthorityIdentity: "authority-1",
}), {
kind: "live-acquisition",
clock: "stream_time",
sourceUrl: "rerun+http://127.0.0.1:9877/proxy",
liveActivitySequence: 12,
liveStreamId: "acquisition-1",
liveRecoveryAuthorityIdentity: "authority-1",
});
});
test("recorded session profile owns progressive admission and on-demand layers", () => {
const artifact = {
sourceUrl: "/api/v1/observation-sessions/session-1/recording.rrd",
viewerSourceUrl: "/api/v1/observation-sessions/session-1/recording.rrd?generation=abc",
byteLength: 42,
sha256: "a".repeat(64),
};
const profile = recordedSessionRerunProfile({
sourceUrl: artifact.sourceUrl,
artifact,
autoplayWhenReady: true,
presentationGate: "loading",
expectedTimelineStartSeconds: 0,
expectedTimelineEndSeconds: 20,
initialPlaybackStartSeconds: 1,
view: "spatial",
viewResetGeneration: 0,
followTrajectory: false,
perceptionLayers: {
enabled: false,
detections2d: false,
segmentation: false,
cuboids3d: false,
},
perceptionRetryGeneration: 0,
lockPerceptionCameraInteraction: false,
});
assert.equal(profile.kind, "recorded-session");
assert.equal(profile.clock, "session_time");
assert.equal(profile.artifact, artifact);
assert.equal(profile.perceptionLayers.enabled, false);
});
test("LAB recorded evidence remains outside native Rerun lifecycle", () => {
assert.deepEqual(LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE, {
kind: "lab-recorded-evidence",
clock: "source-sequence",
cameraTransport: "generation-bound-fmp4",
spatialTransport: "bounded-sealed-artifacts",
loadPolicy: "visible-evidence-only",
workerRequired: false,
});
assert.equal(Object.isFrozen(LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE), true);
});
+20
View File
@@ -122,3 +122,23 @@ Primary implementation references:
- a ready project mounts direct PlayCanvas Engine and loads Streamed SOG with preview fallback;
- visual, collision and combined remain distinct runtime modes, and collision is loaded lazily;
- viewer quality, layer-axis correction and camera inversion survive navigation and reload.
## AI-assisted visual quality extension
AI-assisted visual repair is a candidate lifecycle of one ready Simulation World project. It does
not replace or modify the accepted ingest/optimization path. The original LCC/LCC2 bundle remains
the source of record, while an explicitly promoted enhanced generation may replace only the active
visual SOG references.
The product entry point is `Улучшить качество` in the existing project edit window. It opens a
canonical bounded Window for XGRIDS Creator Data admission, region-of-interest selection, actual
provider state and baseline/candidate review. It is not placed in the live scene controls because
source admission and a long-running candidate build are project operations. It does not receive a
separate workspace because the candidate has no identity outside its parent project.
The control is not shipped as a placeholder. It becomes available only with a ready project and an
enhancement provider publishing the accepted capability. The provider consumes full-quality PLY
derived from the immutable LCC/LCC2 source plus aligned Creator Data images/COLMAP cameras; SOG is
delivery output only. Generated visual regions remain forbidden as collision, navigation or
qualification evidence. The detailed boundary and first experiment are defined by
[ADR 0044](adr/0044-ai-assisted-gaussian-visual-repair.md).
@@ -0,0 +1,136 @@
# ADR 0044: AI-assisted Gaussian visual repair remains a candidate pipeline
## Status
Proposed for a Worker 006 ROI experiment on 2026-08-29. The existing Gaussian optimization and
collision paths remain accepted and unchanged. No repair model is admitted for production use by
this decision.
## Context
The current Simulation Worlds vertical accepts one XGRIDS LCC/LCC2 source, builds preview and
streamed SOG artifacts through DC Gaussian Pipeline, and publishes source-mesh collision
independently. The two current ready projects prove that path. AutoCap repairs only conservative
holes in the source collision mesh; it does not repair the visual Gaussian representation.
The retained MAROSEYKA archive contains an LCC Quality scene with 86,471,152 splats and a
44,787-sample 10 Hz device trajectory. It does not contain the captured camera images, camera
intrinsics or camera extrinsics. The pose records do not identify RGB frames and cannot be treated
as calibrated camera poses.
AI repair methods that permanently improve a 3DGS model need rendered novel views, real reference
images and known cameras. Plausible generation alone cannot recover the factual appearance or
geometry of a surface that was never observed.
XGRIDS LCC Studio Creator Data provides the missing supported interchange boundary:
- `perspective/images/` contains undistorted perspective images;
- `perspective/masks/` contains invalid-region masks;
- `perspective/sparse/` contains COLMAP camera intrinsics and extrinsics aligned with the optimized
LiDAR point cloud;
- `poses.csv` and `high_frequency_poses.csv` retain device trajectories when they are useful for
selecting a repair corridor.
## Decision
1. The immutable original LCC/LCC2 bundle is the visual source of record. SOG is a compressed web
delivery artifact and is never the input to AI repair.
2. A repair candidate starts from full-quality `LCC/LCC2 -> standard 3DGS PLY` conversion. It may
operate only on bounded spatial tiles or route-derived regions of interest; loading or training
the complete 86M-splat scene as one model is not an accepted Worker 006 profile.
3. A repair request additionally admits XGRIDS Creator Data. The minimum input is undistorted
images plus a complete COLMAP sparse model. Masks are strongly preferred. Device poses alone do
not satisfy camera admission.
4. The first experiment uses FreeFix at an exact source revision with the SDXL refinement path. It
was selected because it is fine-tuning-free at the diffusion-model level, uses per-pixel
confidence to preserve reliable regions, reports outdoor/Waymo evaluation, and publishes MIT
code. An adapter must import standard XGRIDS/PlayCanvas PLY attributes into the gsplat checkpoint
layout and export a standard PLY after refinement.
5. Difix3D+ is the mandatory comparison baseline for the same ROI. It directly targets artifacts in
under-constrained views and supports progressive distillation into gsplat, but its combined
NVIDIA/Stability licensing needs a separate commercial-use review.
6. The experiment runs in an adjacent `ndc-` prefixed Docker composition on Worker 006. It shares
neither Python/CUDA environments nor writable model directories with DC Gaussian Pipeline. The
existing pipeline is called only after a candidate PLY is complete and immutable.
7. Every result is a candidate generation. Mission Core keeps the active visual generation until
an operator compares the baseline and candidate and explicitly promotes the candidate. Failure
or cancellation cannot modify the active scene.
8. Generated visual content never becomes collision, navigation, traversability or ground-truth
evidence. Existing source-mesh collision and AutoCap remain independent. Each candidate retains
an uncertainty/hallucination mask and exact model/config provenance.
## Candidate contract
A quality-enhancement request must bind all inputs by digest:
- project ID and original source-bundle SHA-256;
- LCC/LCC2 entrypoint and full-quality PLY conversion revision;
- Creator Data bundle SHA-256;
- COLMAP cameras, images and points model plus their coordinate-alignment report;
- selected route interval and/or world-space ROI;
- algorithm, source revision, model IDs/digests, prompt policy and numerical parameters.
The adjacent provider returns:
- a standard enhanced PLY for the selected tile or composed candidate;
- preview and streamed SOG artifacts created by the unchanged optimization pipeline;
- baseline/candidate camera-path renders and objective image metrics where held-out real views
exist;
- changed-region, confidence and generated-content masks;
- wall time, peak VRAM, source revision, image digest and terminal job state.
The provider states are transport-neutral and bounded: `queued`, `validating_source`,
`aligning_cameras`, `extracting_roi`, `importing_splats`, `refining`, `building_delivery`,
`evaluating`, `ready`, `failed`, and `cancelled`.
## Product placement
The action belongs in the existing project edit window as `Улучшить качество`, because it acts on
one durable world and does not create a new workspace. The action opens a canonical bounded Window
that owns Creator Data admission, ROI choice, actual provider state and baseline/candidate review.
It is shown only for a ready outdoor/interior project and becomes an executable action only when
the enhancement provider publishes the exact accepted capability.
Alternatives considered:
1. Add controls to the live PlayCanvas scene. Rejected because source admission and a long-running
candidate lifecycle are project operations, not per-frame runtime controls.
2. Add a new top-level workspace. Rejected because repair has no independent identity outside one
Simulation World project.
3. Add an always-enabled button before provider/source admission exists. Rejected because it would
be placeholder product UI and would misrepresent the current system state.
## Experiment acceptance
The first ROI experiment is accepted only when:
- the adjacent composition starts and stops without changing the current Gaussian Pipeline
containers or native SplatTransform spool;
- one Creator Data bundle passes COLMAP/image/alignment validation;
- one bounded road-and-facade ROI fits the RTX 4090 24 GB profile without host OOM or impact on the
active optimization queue;
- FreeFix and Difix3D+ run on identical admitted cameras and ROI;
- candidate output round-trips through standard PLY and the existing SOG build;
- held-out views and an operator review show improvement without unacceptable changes in reliable
regions;
- active SOG and collision artifacts remain byte-identical until explicit promotion.
## Consequences
- The first useful input request is XGRIDS Creator Data, not a raw device track and not SOG.
- Existing ready RAR archives cannot start factual image-conditioned repair by themselves.
- Large scenes require ROI/tile scheduling, overlap blending and candidate composition.
- Visually plausible fill may be valuable for rendering while remaining inadmissible as physical
truth.
- Worker deployment is intentionally blocked until a Creator Data sample and restored Worker 006
provider connectivity are available.
## Primary references
- [XGRIDS Creator Data](https://docs.xgrids.com/en-us/06-lixel-cybercolor/01-lcc-studio/v2.3.0/06-model-reconstruction.html#creator-data-and-nvidia-ncore-data)
- [PlayCanvas SplatTransform](https://github.com/playcanvas/splat-transform)
- [FreeFix](https://github.com/hyzhou404/FreeFix)
- [Difix3D+](https://github.com/nv-tlabs/Difix3D)
- [GSFix3D](https://github.com/GSFix3D/GSFix3D)
- [ArtifactWorld](https://github.com/fyting/ArtifactWorld)
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

@@ -0,0 +1,199 @@
# RAVNOVES004TREE canonical LAB replay audit
Date: 2026-08-30
Scope: Mission Core recorded LAB replay, RAVNOVES004TREE, OPS perception state
Excluded: Gaussian/simulation workers and their artifacts
## Outcome
RAVNOVES004TREE no longer owns a custom LAB viewer. It supplies recording and
model configuration to the same `M4ReplayThreatVisual` and
`CanonicalRecordedLabReplay` implementation used by the accepted recorded LAB.
No new window or status type was added. The stable interaction contract remains:
- media: `SEMANTICS`, model selector, `VIDEO` / `CAMERA`;
- spatial: `SOURCE POINTS`, `LOCAL SLAM`, `TGS COSTMAP`, `SEMANTICS`, `3D` / `PLAN`;
- one timeline, one resizable split and one media-owned playback clock.
Models, result IDs, endpoints, labels and replay transport are configuration.
Window structure, switching, seek, buffering and spatial scene code are shared.
## Why the previous LAB failed
### RAV004 did not use the accepted replay data plane
The working M4/Hologravity LAB uses sealed binary numeric tracks, retained scene
state and bounded JSON metadata. RAV004 reused the visual component but silently
left its custom timeline endpoint on the JSON fallback. Each eight-frame spatial
window therefore transferred approximately 1.2-2.9 MB and took 0.87-1.37 s to
produce while representing only about 0.84 s of playback. The next request
aborted and replaced the previous one before spatial state could catch up.
The user screenshot captured the failure exactly: camera frame 366 was active
while the last delivered spatial evidence was frame 230, a 136-frame gap. The
backend also reopened and indexed the 6830-entry semantic ZIP for every requested
mask and proposal frame. This explains why the same canonical viewer was smooth
for Hologravity but stalled for RAV004: the window and interaction code were
shared, but the data-plane contract was not.
RAV004 now publishes the same `missioncore.recorded-spatial-playback/v1`
contract as the accepted LAB: an immutable Float32 map-point track, camera-frame
offsets and 24-frame binary chunks. JSON chunks contain bounded frame metadata
only; retained Local SLAM is reconstructed from exact source increments in the
shared client. The semantic ZIP handle and member index are cached per immutable
artifact instead of being reparsed per frame.
### Video and spatial state had different clocks
The removed RAV004 viewer advanced an animation/host clock even when the browser
decoder stopped. The point cloud therefore continued while the camera frame and
timeline could remain frozen. The shared viewer now uses the decoded media time
as the external clock, and image masks/boxes are rendered only when their time is
within 250 ms of the actually presented video time.
The RAV004 MP4 itself is not clean. An independent `ffmpeg` decode around the
reproducible stop at 11.422 s reported non-monotonic DTS values and corrupt H.264
macroblocks. The RAV004 profile therefore uses the shared segmented MSE transport
and an opt-in timestamp recovery rule. Recovery is allowed only when all of these
conditions are true:
- playback is requested and the media element is not paused, ended or seeking;
- decoded media time has not advanced by 20 ms for at least 1.25 s;
- the browser reports decoded media buffered ahead of the frozen timestamp.
Only then is the broken timestamp interval skipped by 180 ms. The media clock
immediately remains authoritative; the host does not free-run. A stale callback
from the old MSE window is also prevented from undoing an operator seek.
### LiDAR orientation inherited the wrong axes
The RRD declares `/world` as RFU (`Right`, `Forward`, `Up`) and logs
`/world/points` in map space. The earlier adapter treated raw LiDAR quaternion
columns as rover forward/left/up and inherited sensor roll/pitch. That is why the
grid, rover and facade could visibly disagree.
The v3 adapter now uses:
- map `+Z` as gravity/up;
- the smoothed pose-trajectory tangent projected onto the ground as forward;
- `left = up × forward`;
- projected sensor `+Y` only as a fallback when the tangent is unavailable.
This is a deterministic coordinate contract, not a visual angle correction.
### Sensor height was treated as a constant
RAV004 does not have a stable 0.4 m mounting height throughout the recording.
The adapter now estimates the local ground plane from a causal one-second
near-field point window and uses the sealed session estimate only as fallback.
Observed local heights include approximately 0.17 m, 1.24 m, 1.05 m and 0.22 m
at different route positions; a single hand-entered value is therefore invalid.
### Sparse LiDAR frames were held incorrectly
Camera is approximately 9.51 Hz while source points arrive at approximately
2 Hz. A camera frame without a new LiDAR increment used to retain whichever
spatial frame happened to finish loading last; under fast playback this could be
dozens of seconds old. The buffer now loads the active chunk first, the preceding
chunk second and the next chunk as prefetch. The scene selects the latest proven
source increment whose sequence is not later than the active camera frame.
At the final UI check, playback restarted at frame 1, then ran continuously past
frame 462. At frame 188 the DDRNet mask was frame 188, proposals were frame 187
and spatial state was delivered without buffering; the one-frame proposal delay
is the recorded causal overlay, not stale UI state. Before the transport fix the
user's run had already fallen 136 frames behind by camera frame 366.
## Capability ledger
| Layer | RAV004 full route | UI behavior | Authority |
|---|---:|---|---|
| Recorded RIGHT camera | 6830/6830 | `VIDEO` / `CAMERA`, segmented playback | recorded evidence |
| DDRNet semantic mask | 6830/6830 | selectable, opaque enough for review | diagnostic prediction |
| EoMT semantic mask | 6830/6830 | selectable | diagnostic prediction |
| Diagnostic object boxes | derived from connected EoMT mask components | media-time gated | not an independent detector |
| Source points | 1444 increments | `SOURCE POINTS` | recorded geometry |
| Bounded Local SLAM | causal 5 s / 27k-point limit | `LOCAL SLAM` | visual-derived |
| Full-route TGS | **absent** | canonical `TGS COSTMAP` control is visible but disabled | unavailable, fail closed |
| Point-aligned 3D semantics | **absent** | canonical `SEMANTICS` control is visible but disabled | unavailable |
| Independent person/vehicle detector | **absent** | no STOP claim | unavailable |
Ten old TGS review anchors exist, but they are not a continuous route artifact.
They are not repeated or held as if they were full TGS. The accepted RAVNOVES00
full-TGS result is also not reused because it has a different source identity and
4489-frame timeline.
## Performance evidence
Measured on the canonical local service and current immutable artifacts:
- replay launch POST: 3.55 s on first opening;
- first binary playback-manifest build after a service restart: 32.33 s while
the process-local RRD point track is materialized; warm manifest: 0.18-0.20 s;
- full retained point track: 3,893,445 Float32 map points, 46,721,340 bytes,
divided into 285 immutable 24-frame chunks;
- representative active binary chunks: 155-224 KB at 9-10 ms;
- representative 24-frame metadata chunks: 24-32 KB at 0.78-1.0 s, covering
about 2.4 s of playback;
- cached semantic-mask reads: 5.6-7.8 ms instead of approximately 80 ms;
- UI replay: reset to frame 1 and ran continuously beyond frame 462 with camera,
semantic overlay and retained spatial state advancing together;
- operator reset seek: successful, one mounted media worker;
- browser console after the acceptance run: no warnings or errors.
The first RRD index is still process-local rather than a persistent disk cache.
That is an explicit remaining performance gap; warm playback is the admitted
profile, cold restart latency is not yet accepted.
## Nature perception: current OPS stopping point
OPS card `MISSIONCOR-65` defines the intended independent layers as EoMT,
DDRNet, frozen YOLOX and TGS. The current immutable RAV004 artifact proves full
EoMT and DDRNet inference only. It does not prove full TGS, negative-obstacle
handling, an independent person/vehicle STOP layer or combined real-time load.
Isolated full-route measurements:
- DDRNet-39: p95 27.44 ms, 52.67 inference FPS, validation mean IoU 29.715%,
vegetation mean IoU 0.3701;
- EoMT: p95 361.62 ms, approximately 3.01 inference FPS;
- prior accepted RAVNOVES00 TGS: p95 1.694 ms CPU-only, but this is algorithm
performance on another source, not RAV004 proof.
The DDRNet isolated throughput is sufficient for a 10 FPS budget. DDRNet is not
accepted for driving policy because temporal stability and nature quality are
not sufficient: the OPS temporal sample recorded adjacent-frame IoU near 0.195
for high grass and 0.400 for woody vegetation. EoMT does not meet 10 FPS in its
current form. The next evidentiary milestone is therefore not another UI model
toggle; it is synchronized truth for grass/tree/ditch/drop-off, full TGS and
negative-obstacle evidence, frozen independent detector output and a combined
load test at at least 10 FPS.
Worker 006 was audited read-only. Triton and the Gaussian containers were left
untouched. The separate Mission Core perception worker is currently in a restart
loop (404 during model inference startup); this audit did not stop, recreate or
deploy it.
## Acceptance performed
- 12 focused backend spatial/API tests passed;
- 16 focused frontend replay transport/manifest tests passed;
- TypeScript project typecheck passed;
- production Vite build passed (only existing large-chunk warnings);
- `git diff --check` passed;
- live browser run verified reset seek, the shared controls, disabled unsealed
TGS/3D semantics, continuous playback through the former failing interval,
causal spatial hold and a clean console.
Visual QA: `docs/handoff/2026-08-30_RAV004_CANONICAL_LAB_QA.jpg`.
## External coordinate and media references
- Rerun ViewCoordinates: <https://rerun.io/docs/reference/types/datatypes/view_coordinates>
- Rerun transform relation: <https://rerun.io/docs/reference/types/components/transform_relation>
- Rerun transforms: <https://rerun.io/docs/concepts/logging-and-ingestion/transforms>
- Rerun Transform3D: <https://rerun.io/docs/reference/types/archetypes/transform3d>
- WHATWG media element model: <https://html.spec.whatwg.org/multipage/media.html>
- W3C Media Source Extensions: <https://www.w3.org/TR/media-source-2/>
@@ -742,6 +742,7 @@ def seal_mixed_route_full_video_review(
full_route = {
"source_id": FULL_ROUTE_SOURCE_ID,
"session_id": job.session_id,
"linked_route_review_result_id": base["result_id"],
"source_job_id": job.job_id,
"source_job_input_sha256": job.input_sha256,
"source_stream_sha256": FULL_ROUTE_STREAM_SHA256,
@@ -0,0 +1,658 @@
"""Canonical recorded-LAB spatial adapter for sealed Rerun recordings.
The LAB viewer must not run an independent Rerun transport beside the camera
transport. This adapter reads the immutable recording once, indexes the
recorded source cloud and sensor pose, estimates the session sensor height from
the initial stationary cloud, and returns both the current increment and a
bounded accumulated local-SLAM cloud in a ground-rebased body frame. Camera,
spatial layers and the common timeline can therefore be driven by one media
clock without a per-LAB coordinate adapter.
"""
from __future__ import annotations
from bisect import bisect_right
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from threading import Lock
from typing import Any, Final
import numpy as np
import rerun_bindings as rr_bindings
CANONICAL_LAB_SPATIAL_PROFILE: Final = "source-paced-ground-v3"
_POINT_ENTITY: Final = "/world/points"
_POSE_ENTITY: Final = "/world/sensor_pose"
_TRAJECTORY_ENTITY: Final = "/world/trajectory"
_POINT_COMPONENT: Final = "Points3D:positions"
_POSE_TRANSLATION_COMPONENT: Final = "Transform3D:translation"
_POSE_QUATERNION_COMPONENT: Final = "Transform3D:quaternion"
_TRAJECTORY_COMPONENT: Final = "LineStrips3D:strips"
_INDEX_LOCK: Final = Lock()
_HEIGHT_CALIBRATION_SECONDS: Final = 60.0
_HEIGHT_CALIBRATION_MAX_FRAMES: Final = 120
_HEIGHT_NEAR_MIN_RADIUS_M: Final = 1.0
_HEIGHT_NEAR_MAX_RADIUS_M: Final = 6.0
_HEIGHT_LOWER_QUANTILE: Final = 0.025
_LOCAL_HEIGHT_QUANTILE: Final = 0.10
_LOCAL_HEIGHT_HALF_WINDOW_SECONDS: Final = 1.0
_LOCAL_SLAM_HISTORY_SECONDS: Final = 5.0
_LOCAL_SLAM_RADIUS_M: Final = 30.0
_LOCAL_SLAM_VERTICAL_LIMIT_M: Final = 6.0
_LOCAL_SLAM_VOXEL_SIZE_M: Final = 0.12
_LOCAL_SLAM_POINT_LIMIT: Final = 27_000
_FORWARD_HALF_WINDOW_SECONDS: Final = 1.0
_FORWARD_MINIMUM_DISPLACEMENT_M: Final = 0.15
@dataclass(frozen=True)
class _TimedPoints:
times_ns: tuple[int, ...]
values: tuple[np.ndarray, ...]
@dataclass(frozen=True)
class _TimedPoses:
times_ns: tuple[int, ...]
translations: tuple[np.ndarray, ...]
quaternions_xyzw: tuple[np.ndarray, ...]
@dataclass(frozen=True)
class _CanonicalSpatialIndex:
points: _TimedPoints
poses: _TimedPoses
trajectories: _TimedPoints
sensor_height_m: float
sensor_height_sample_count: int
sensor_height_mad_m: float
def _session_times(batch: Any) -> Any | None:
if "session_time" not in batch.schema.names:
return None
return batch.column("session_time")
def _point_rows(
chunks: list[Any],
entity: str,
component: str,
*,
nested: bool = False,
) -> _TimedPoints:
rows: list[tuple[int, np.ndarray]] = []
for chunk in chunks:
if chunk.entity_path != entity:
continue
batch = chunk.to_record_batch()
times = _session_times(batch)
if times is None or component not in batch.schema.names:
continue
column = batch.column(component)
for row_index in range(batch.num_rows):
timestamp = int(times[row_index].value)
payload = column[row_index].as_py()
if nested:
payload = payload[0] if payload else []
values = np.asarray(payload, dtype=np.float32)
if values.ndim != 2 or values.shape[1] != 3 or not np.isfinite(values).all():
continue
values.setflags(write=False)
rows.append((timestamp, values))
rows.sort(key=lambda item: item[0])
return _TimedPoints(
times_ns=tuple(timestamp for timestamp, _ in rows),
values=tuple(values for _, values in rows),
)
def _pose_rows(chunks: list[Any]) -> _TimedPoses:
rows: list[tuple[int, np.ndarray, np.ndarray]] = []
for chunk in chunks:
if chunk.entity_path != _POSE_ENTITY:
continue
batch = chunk.to_record_batch()
times = _session_times(batch)
if (
times is None
or _POSE_TRANSLATION_COMPONENT not in batch.schema.names
or _POSE_QUATERNION_COMPONENT not in batch.schema.names
):
continue
translations = batch.column(_POSE_TRANSLATION_COMPONENT)
quaternions = batch.column(_POSE_QUATERNION_COMPONENT)
for row_index in range(batch.num_rows):
translation_values = translations[row_index].as_py()
quaternion_values = quaternions[row_index].as_py()
if len(translation_values) != 1 or len(quaternion_values) != 1:
continue
translation = np.asarray(translation_values[0], dtype=np.float64)
quaternion = np.asarray(quaternion_values[0], dtype=np.float64)
if (
translation.shape != (3,)
or quaternion.shape != (4,)
or not np.isfinite(translation).all()
or not np.isfinite(quaternion).all()
):
continue
norm = float(np.linalg.norm(quaternion))
if norm <= 1e-9:
continue
translation.setflags(write=False)
normalized = quaternion / norm
normalized.setflags(write=False)
rows.append((int(times[row_index].value), translation, normalized))
rows.sort(key=lambda item: item[0])
return _TimedPoses(
times_ns=tuple(timestamp for timestamp, _, _ in rows),
translations=tuple(translation for _, translation, _ in rows),
quaternions_xyzw=tuple(quaternion for _, _, quaternion in rows),
)
@lru_cache(maxsize=4)
def _load_index_cached(
path_text: str,
byte_length: int,
modified_ns: int,
generation_sha256: str,
) -> _CanonicalSpatialIndex:
path = Path(path_text)
stat = path.stat()
if stat.st_size != byte_length or stat.st_mtime_ns != modified_ns:
raise ValueError("Recorded LAB source changed during spatial indexing")
if len(generation_sha256) != 64:
raise ValueError("Recorded LAB generation is invalid")
# Decode only the three canonical entities in one pass. Building a lazy
# store first decodes the complete RRD (including unrelated payloads), and
# then scanning that store once per layer made first-open take more than a
# minute on RAVNOVES004TREE.
chunks = (
rr_bindings.RrdReaderInternal(str(path))
.stream()
.filter(content=[_POINT_ENTITY, _POSE_ENTITY, _TRAJECTORY_ENTITY])
.to_chunks()
)
points = _point_rows(chunks, _POINT_ENTITY, _POINT_COMPONENT)
poses = _pose_rows(chunks)
trajectories = _point_rows(
chunks,
_TRAJECTORY_ENTITY,
_TRAJECTORY_COMPONENT,
nested=True,
)
if not points.times_ns or not poses.times_ns or not trajectories.times_ns:
raise ValueError("Recorded LAB source has no canonical spatial layers")
sensor_height_m, sensor_height_sample_count, sensor_height_mad_m = (
_estimate_sensor_height(points, poses)
)
return _CanonicalSpatialIndex(
points=points,
poses=poses,
trajectories=trajectories,
sensor_height_m=sensor_height_m,
sensor_height_sample_count=sensor_height_sample_count,
sensor_height_mad_m=sensor_height_mad_m,
)
def _load_index(
path_text: str,
byte_length: int,
modified_ns: int,
generation_sha256: str,
) -> _CanonicalSpatialIndex:
# functools.lru_cache is coherent but intentionally releases its lock
# during a miss. Serialize cold RRD indexing so simultaneous camera/TGS
# admission cannot parse the same 80 MiB recording twice.
with _INDEX_LOCK:
return _load_index_cached(
path_text,
byte_length,
modified_ns,
generation_sha256,
)
def _latest_index(times_ns: tuple[int, ...], target_ns: int) -> int:
return max(0, min(len(times_ns) - 1, bisect_right(times_ns, target_ns) - 1))
def _rotation_map_from_body(quaternion_xyzw: np.ndarray) -> np.ndarray:
x, y, z, w = (float(value) for value in quaternion_xyzw)
return np.asarray(
[
[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
[2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
[2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
],
dtype=np.float64,
)
def _map_points_to_body(
points_map: np.ndarray,
translation_map: np.ndarray,
quaternion_xyzw: np.ndarray,
) -> np.ndarray:
rotation = _rotation_map_from_body(quaternion_xyzw)
# Row vectors: inverse(map_from_body) == right-multiply by map_from_body.
body = (points_map.astype(np.float64) - translation_map) @ rotation
return body.astype(np.float32)
def _gravity_stable_basis_map_from_body(
poses: _TimedPoses,
target_time_ns: int,
) -> tuple[np.ndarray, str]:
"""Return a right-handed forward/left/up base frame in the RFU map.
Rerun declares this recording map as RFU, while the metric LAB scene
consumes points as forward/left/up. The LiDAR quaternion columns are sensor
right/forward/up and also contain rover or handheld roll/pitch, so they are
not a body basis. Route displacement owns yaw when available; the sensor's
local +Y (Rerun Forward) projected onto map gravity is the stationary
fallback. Map +Z always owns up.
"""
center = _latest_index(poses.times_ns, target_time_ns)
half_window_ns = round(_FORWARD_HALF_WINDOW_SECONDS * 1_000_000_000)
first = _latest_index(poses.times_ns, max(0, target_time_ns - half_window_ns))
last = min(
len(poses.times_ns) - 1,
max(0, bisect_right(poses.times_ns, target_time_ns + half_window_ns) - 1),
)
route = poses.translations[last] - poses.translations[first]
route_xy = np.asarray([route[0], route[1], 0.0], dtype=np.float64)
route_norm = float(np.linalg.norm(route_xy))
sensor_rotation = _rotation_map_from_body(poses.quaternions_xyzw[center])
sensor_forward = np.asarray(
[sensor_rotation[0, 1], sensor_rotation[1, 1], 0.0],
dtype=np.float64,
)
sensor_forward_norm = float(np.linalg.norm(sensor_forward))
if sensor_forward_norm <= 1e-9:
raise ValueError("Recorded LAB sensor forward axis is invalid")
sensor_forward /= sensor_forward_norm
if route_norm >= _FORWARD_MINIMUM_DISPLACEMENT_M:
forward = route_xy / route_norm
if float(np.dot(forward, sensor_forward)) < 0.0:
forward = -forward
forward_source = "smoothed-pose-trajectory-tangent"
else:
forward = sensor_forward
forward_source = "rerun-rfu-sensor-forward-fallback"
up = np.asarray([0.0, 0.0, 1.0], dtype=np.float64)
left = np.cross(up, forward)
left_norm = float(np.linalg.norm(left))
if left_norm <= 1e-9:
raise ValueError("Recorded LAB body left axis is invalid")
left /= left_norm
forward = np.cross(left, up)
forward /= float(np.linalg.norm(forward))
basis = np.column_stack((forward, left, up))
if (
not np.allclose(basis.T @ basis, np.eye(3), atol=1e-7)
or np.linalg.det(basis) < 0.999999
):
raise ValueError("Recorded LAB gravity-stable body basis is invalid")
return basis, forward_source
def _estimate_sensor_height(points: _TimedPoints, poses: _TimedPoses) -> tuple[float, int, float]:
"""Estimate one session mount height from the initial qualified cloud.
The K1 recording has no explicit physical mount-height entity. The initial
stationary minute is therefore the only admissible automatic calibration
source. A low near-field quantile is measured per source increment and the
session median rejects vegetation/ravine outliers. The result stays
diagnostic and is never promoted to navigation authority by this adapter.
"""
first_time_ns = points.times_ns[0]
calibration_end_ns = first_time_ns + round(_HEIGHT_CALIBRATION_SECONDS * 1_000_000_000)
candidates = [
index
for index, timestamp in enumerate(points.times_ns)
if timestamp <= calibration_end_ns
][:_HEIGHT_CALIBRATION_MAX_FRAMES]
estimates: list[float] = []
for point_index in candidates:
pose_index = _latest_index(poses.times_ns, points.times_ns[point_index])
delta = points.values[point_index].astype(np.float64) - poses.translations[pose_index]
radius = np.linalg.norm(delta[:, :2], axis=1)
eligible = delta[
(radius >= _HEIGHT_NEAR_MIN_RADIUS_M)
& (radius <= _HEIGHT_NEAR_MAX_RADIUS_M)
& (delta[:, 2] >= -2.0)
& (delta[:, 2] <= 0.5)
]
if eligible.shape[0] < 100:
continue
estimate = -float(np.quantile(eligible[:, 2], _HEIGHT_LOWER_QUANTILE))
if 0.08 <= estimate <= 2.5:
estimates.append(estimate)
if len(estimates) < 8:
raise ValueError("Recorded LAB sensor height cannot be estimated from source cloud")
values = np.asarray(estimates, dtype=np.float64)
height = float(np.median(values))
mad = float(np.median(np.abs(values - height)))
return height, len(estimates), mad
def _estimate_local_sensor_height(
points: _TimedPoints,
poses: _TimedPoses,
target_time_ns: int,
fallback_height_m: float,
) -> tuple[float, int, float, str]:
"""Estimate the current gravity-axis height without a fixed camera mount.
RAVNOVES004TREE changes sensor height during the route. A session-wide
constant therefore moves the scene vertically whenever the operator raises
or lowers K1. Use a short source-time window and a conservative near-field
ground quantile; fall back to the sealed session calibration only when the
current cloud has insufficient support.
"""
half_window_ns = round(_LOCAL_HEIGHT_HALF_WINDOW_SECONDS * 1_000_000_000)
first = bisect_right(points.times_ns, max(0, target_time_ns - half_window_ns) - 1)
last = bisect_right(points.times_ns, target_time_ns + half_window_ns)
estimates: list[float] = []
for point_index in range(first, last):
pose_index = _latest_index(poses.times_ns, points.times_ns[point_index])
delta = points.values[point_index].astype(np.float64) - poses.translations[pose_index]
radius = np.linalg.norm(delta[:, :2], axis=1)
eligible = delta[
(radius >= _HEIGHT_NEAR_MIN_RADIUS_M)
& (radius <= _HEIGHT_NEAR_MAX_RADIUS_M)
& (delta[:, 2] >= -2.5)
& (delta[:, 2] <= 0.5)
]
if eligible.shape[0] < 100:
continue
estimate = -float(np.quantile(eligible[:, 2], _LOCAL_HEIGHT_QUANTILE))
if 0.03 <= estimate <= 2.5:
estimates.append(estimate)
if not estimates:
return fallback_height_m, 0, 0.0, "session-source-cloud-fallback"
values = np.asarray(estimates, dtype=np.float64)
height = float(np.median(values))
mad = float(np.median(np.abs(values - height)))
return height, len(estimates), mad, "local-source-cloud-ground-quantile-median"
def _ground_origin_map(
sensor_origin_map: np.ndarray,
sensor_height_m: float,
) -> np.ndarray:
# The calibrated height belongs to the map gravity axis. Sensor roll/pitch
# must never tilt the ground origin or the accumulated world cloud.
return sensor_origin_map - np.asarray([0.0, 0.0, sensor_height_m])
def _map_points_to_ground_body(
points_map: np.ndarray,
ground_origin_map: np.ndarray,
basis_map_from_body: np.ndarray,
) -> np.ndarray:
body = (points_map.astype(np.float64) - ground_origin_map) @ basis_map_from_body
return body.astype(np.float32)
def _bounded_local_slam(
points: _TimedPoints,
target_time_ns: int,
ground_origin_map: np.ndarray,
basis_map_from_body: np.ndarray,
) -> tuple[np.ndarray, int, int]:
start_ns = target_time_ns - round(_LOCAL_SLAM_HISTORY_SECONDS * 1_000_000_000)
first = bisect_right(points.times_ns, start_ns - 1)
last = bisect_right(points.times_ns, target_time_ns)
selected = points.values[first:last]
if not selected:
return np.empty((0, 3), dtype=np.float32), 0, 0
source_count = sum(int(value.shape[0]) for value in selected)
local = _map_points_to_ground_body(
np.concatenate(selected, axis=0),
ground_origin_map,
basis_map_from_body,
)
mask = (
(np.linalg.norm(local[:, :2], axis=1) <= _LOCAL_SLAM_RADIUS_M)
& (np.abs(local[:, 2]) <= _LOCAL_SLAM_VERTICAL_LIMIT_M)
)
local = local[mask]
if local.shape[0] == 0:
return local, len(selected), source_count
voxel = np.floor(local / _LOCAL_SLAM_VOXEL_SIZE_M).astype(np.int32)
_, retained = np.unique(voxel, axis=0, return_index=True)
local = local[np.sort(retained)]
if local.shape[0] > _LOCAL_SLAM_POINT_LIMIT:
stride = int(np.ceil(local.shape[0] / _LOCAL_SLAM_POINT_LIMIT))
local = local[::stride][:_LOCAL_SLAM_POINT_LIMIT]
return np.ascontiguousarray(local, dtype=np.float32), len(selected), source_count
def _canonical_lab_spatial_frame_from_index(
index: _CanonicalSpatialIndex,
target_time_ns: int,
*,
include_local_slam: bool = True,
) -> dict[str, object]:
point_index = _latest_index(index.points.times_ns, target_time_ns)
pose_index = _latest_index(index.poses.times_ns, index.points.times_ns[point_index])
trajectory_index = _latest_index(index.trajectories.times_ns, target_time_ns)
translation = index.poses.translations[pose_index]
sensor_height_m, sensor_height_sample_count, sensor_height_mad_m, height_source = (
_estimate_local_sensor_height(
index.points,
index.poses,
index.points.times_ns[point_index],
index.sensor_height_m,
)
)
basis_map_from_body, forward_source = _gravity_stable_basis_map_from_body(
index.poses,
index.points.times_ns[point_index],
)
ground_origin = _ground_origin_map(
translation,
sensor_height_m,
)
points_body = _map_points_to_ground_body(
index.points.values[point_index],
ground_origin,
basis_map_from_body,
)
if include_local_slam:
local_slam, local_slam_source_frames, local_slam_source_points = _bounded_local_slam(
index.points,
index.points.times_ns[point_index],
ground_origin,
basis_map_from_body,
)
else:
local_slam = np.empty((0, 3), dtype=np.float32)
local_slam_source_frames = 0
local_slam_source_points = 0
return {
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v3",
"target_time_ns": target_time_ns,
"source_time_ns": index.points.times_ns[point_index],
"pose_time_ns": index.poses.times_ns[pose_index],
"trajectory_time_ns": index.trajectories.times_ns[trajectory_index],
"coordinate_frame": "body-ground",
"sensor_height": {
"meters": sensor_height_m,
"source": height_source,
"sample_count": sensor_height_sample_count,
"mad_m": sensor_height_mad_m,
"session_fallback_meters": index.sensor_height_m,
"authority": "visual-derived",
},
"spatial_profile": {
"profile_id": CANONICAL_LAB_SPATIAL_PROFILE,
"local_slam_history_seconds": _LOCAL_SLAM_HISTORY_SECONDS,
"local_slam_radius_m": _LOCAL_SLAM_RADIUS_M,
"local_slam_voxel_size_m": _LOCAL_SLAM_VOXEL_SIZE_M,
"local_slam_point_limit": _LOCAL_SLAM_POINT_LIMIT,
},
"body_frame": {
"origin_map_xyz_m": ground_origin.tolist(),
"sensor_origin_map_xyz_m": translation.tolist(),
"basis_map_from_body": basis_map_from_body.tolist(),
"up_source": "rerun-rfu-map-gravity-axis",
"forward_source": forward_source,
},
"source_point_count": int(points_body.shape[0]),
"source_points_body_xyz_m": points_body.tolist(),
"local_slam_source_frame_count": local_slam_source_frames,
"local_slam_source_point_count": local_slam_source_points,
"local_slam_point_count": int(local_slam.shape[0]),
"local_slam_body_xyz_m": local_slam.tolist(),
}
def canonical_lab_spatial_frame(
recording_path: Path,
generation_sha256: str,
target_time_ns: int,
) -> dict[str, object]:
"""Return the current source cloud and bounded Local SLAM on one media time."""
if target_time_ns < 0:
raise ValueError("Recorded LAB target time is invalid")
stat = recording_path.stat()
index = _load_index(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
)
return _canonical_lab_spatial_frame_from_index(index, target_time_ns)
def canonical_lab_spatial_timeline_samples(
recording_path: Path,
generation_sha256: str,
frame_times_ns: tuple[int, ...],
start_sequence: int,
frame_count: int,
*,
include_local_slam: bool = True,
) -> tuple[dict[str, object] | None, ...]:
"""Project only new source increments onto a denser camera timeline.
Camera is roughly 10 Hz in RAVNOVES004TREE while the sealed source cloud is
roughly 2 Hz. Returning the same JSON point array for every camera frame
multiplies transfer and parse cost and makes the viewer chase itself. A row
is populated only when its nearest causal source increment changes; the
canonical viewer retains that spatial frame until the next increment.
"""
if (
start_sequence < 0
or frame_count < 1
or start_sequence >= len(frame_times_ns)
or any(
current <= previous
for previous, current in zip(frame_times_ns, frame_times_ns[1:], strict=False)
)
):
raise ValueError("Recorded LAB timeline sample request is invalid")
stat = recording_path.stat()
index = _load_index(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
)
stop = min(len(frame_times_ns), start_sequence + frame_count)
samples: list[dict[str, object] | None] = []
for sequence in range(start_sequence, stop):
target_time_ns = frame_times_ns[sequence]
point_index = _latest_index(index.points.times_ns, target_time_ns)
previous_point_index = (
-1
if sequence == 0
else _latest_index(index.points.times_ns, frame_times_ns[sequence - 1])
)
samples.append(
_canonical_lab_spatial_frame_from_index(
index,
target_time_ns,
include_local_slam=include_local_slam,
)
if point_index != previous_point_index
else None
)
return tuple(samples)
@lru_cache(maxsize=2)
def _canonical_lab_spatial_playback_points_cached(
recording_path_text: str,
recording_size: int,
recording_mtime_ns: int,
generation_sha256: str,
frame_times_ns: tuple[int, ...],
) -> tuple[np.ndarray, tuple[int, ...]]:
del recording_size, recording_mtime_ns
recording_path = Path(recording_path_text)
stat = recording_path.stat()
index = _load_index(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
)
increments: list[np.ndarray] = []
offsets = [0]
point_count = 0
previous_point_index = -1
for target_time_ns in frame_times_ns:
point_index = _latest_index(index.points.times_ns, target_time_ns)
if point_index != previous_point_index:
increment = np.ascontiguousarray(index.points.values[point_index], dtype="<f4")
increments.append(increment)
point_count += int(increment.shape[0])
offsets.append(point_count)
previous_point_index = point_index
points = (
np.ascontiguousarray(np.concatenate(increments, axis=0), dtype="<f4")
if increments
else np.empty((0, 3), dtype="<f4")
)
points.setflags(write=False)
return points, tuple(offsets)
def canonical_lab_spatial_playback_points(
recording_path: Path,
generation_sha256: str,
frame_times_ns: tuple[int, ...],
) -> tuple[np.ndarray, tuple[int, ...]]:
"""Return one retained map-coordinate point track for a camera timeline."""
if (
not frame_times_ns
or any(
current <= previous
for previous, current in zip(frame_times_ns, frame_times_ns[1:], strict=False)
)
):
raise ValueError("Recorded LAB playback timeline is invalid")
stat = recording_path.stat()
return _canonical_lab_spatial_playback_points_cached(
str(recording_path),
stat.st_size,
stat.st_mtime_ns,
generation_sha256,
frame_times_ns,
)
@@ -36,6 +36,7 @@ IMAGE_DIGEST_PATTERN: Final = re.compile(r"^sha256:[a-f0-9]{64}$")
DEFAULT_CHUNK_BYTES: Final = 8 * 1024 * 1024
MAX_JSON_RESPONSE_BYTES: Final = 32 * 1024 * 1024
MAX_RETRIES: Final = 3
RETRYABLE_PROVIDER_STATUS_CODES: Final = {502, 503, 504}
DEFAULT_INGEST_TIMEOUT_SECONDS: Final = 30 * 60.0
@@ -899,7 +900,10 @@ def _validate_runtime_provenance(document: Mapping[str, object]) -> None:
def _unavailable(message: str, error: httpx.HTTPError) -> GaussianPipelineGatewayError:
if isinstance(error, httpx.TransportError):
if isinstance(error, httpx.TransportError) or (
isinstance(error, httpx.HTTPStatusError)
and error.response.status_code in RETRYABLE_PROVIDER_STATUS_CODES
):
return GaussianPipelineUnavailableError(message)
return GaussianPipelineGatewayError(message)
@@ -922,4 +926,6 @@ def _provider_rejection(response: httpx.Response) -> GaussianPipelineGatewayErro
message = f"Gaussian provider rejected request (HTTP {response.status_code})"
if detail is not None:
message = f"{message}: {detail}"
if response.status_code in RETRYABLE_PROVIDER_STATUS_CODES:
return GaussianPipelineUnavailableError(message)
return GaussianPipelineGatewayError(message)
+100 -3
View File
@@ -14,7 +14,7 @@ from collections import deque
from collections.abc import Callable
from contextlib import suppress
from pathlib import Path
from typing import Any, Final, TypeVar
from typing import Any, Final
from urllib.parse import quote
from uuid import uuid4
@@ -55,7 +55,7 @@ PROVIDER_JOB_STATES: Final = {
}
PROVIDER_POLL_TIMEOUT_SECONDS: Final = 2 * 60 * 60 + 5 * 60
PROVIDER_UNAVAILABLE_RETRY_LIMIT: Final = 6
_T = TypeVar("_T")
IMPORT_DISK_RESERVE_BYTES: Final = 512 * 1024 * 1024
class SimulationProjectError(RuntimeError):
@@ -701,6 +701,7 @@ class SimulationProjectService:
self._raise_if_cancelled(project_id)
artifacts = _artifact_descriptors(result.get("artifacts"))
artifacts_root = self.store.artifacts_root(project_id)
self._ensure_import_capacity(project_id, artifacts, artifacts_root)
for descriptor in artifacts:
self._raise_if_cancelled(project_id)
_retry_provider_unavailable(
@@ -722,6 +723,8 @@ class SimulationProjectService:
)
except _SimulationProcessingCancelled:
pass
except GaussianPipelineUnavailableError:
self._requeue_provider_unavailable(project_id)
except (GaussianPipelineGatewayError, SimulationProjectError, OSError) as exc:
with suppress(SimulationProjectError):
self.store.fail(project_id, str(exc))
@@ -731,6 +734,70 @@ class SimulationProjectService:
if provider is not None:
provider.close()
def _requeue_provider_unavailable(self, project_id: str) -> None:
"""Keep a retained source pending while its worker transport is unavailable."""
try:
self.store.update_processing(project_id, status="queued")
except SimulationProjectError:
return
with self._condition:
cancel = self._cancel_events.get(project_id)
if cancel is not None and cancel.is_set():
return
if self._active_project_id == project_id and project_id not in self._queued_ids:
self._queue.append(project_id)
self._queued_ids.add(project_id)
self._condition.notify_all()
def _ensure_import_capacity(
self,
project_id: str,
artifacts: list[dict[str, Any]],
artifacts_root: Path,
) -> None:
required_bytes = _remaining_import_bytes(artifacts_root, artifacts)
available_bytes = shutil.disk_usage(artifacts_root).free
if available_bytes >= required_bytes:
return
project = self.store.get(project_id)
source = project.get("source")
provider = project.get("provider")
if (
isinstance(source, dict)
and source.get("kind") == "archive"
and isinstance(provider, dict)
and isinstance(provider.get("job_id"), str)
):
source_files = source.get("files")
if isinstance(source_files, list) and len(source_files) == 1:
source_file = source_files[0]
if isinstance(source_file, dict):
logical_path = source_file.get("logical_path")
byte_length = source_file.get("byte_length")
if isinstance(logical_path, str) and isinstance(byte_length, int):
source_path = _confined_path(
self.store.source_root(project_id),
logical_path,
)
try:
source_stat = source_path.stat()
except OSError:
source_stat = None
if (
source_stat is not None
and source_path.is_file()
and not source_path.is_symlink()
and source_stat.st_size == byte_length
):
source_path.unlink()
available_bytes = shutil.disk_usage(artifacts_root).free
if available_bytes < required_bytes:
raise SimulationProjectError(
"Недостаточно места для импорта Gaussian-мира: "
f"нужно {_human_bytes(required_bytes)}, "
f"доступно {_human_bytes(available_bytes)}."
)
def delete(self, project_id: str) -> None:
project = self.store.get(project_id)
with self._condition:
@@ -794,7 +861,7 @@ class SimulationProjectService:
raise _SimulationProcessingCancelled(project_id)
def _retry_provider_unavailable(operation: Callable[[], _T]) -> _T:
def _retry_provider_unavailable[T](operation: Callable[[], T]) -> T:
delay_seconds = 1.0
for attempt in range(PROVIDER_UNAVAILABLE_RETRY_LIMIT):
try:
@@ -841,6 +908,36 @@ def _artifact_descriptors(value: object) -> list[dict[str, Any]]:
return descriptors
def _remaining_import_bytes(
artifacts_root: Path,
artifacts: list[dict[str, Any]],
) -> int:
missing_bytes = 0
replacement_scratch_bytes = 0
for descriptor in artifacts:
logical_path = str(descriptor["logical_path"])
expected_bytes = int(descriptor["byte_length"])
target = _confined_path(artifacts_root, logical_path)
try:
current = target.stat()
except OSError:
current = None
if (
current is not None
and target.is_file()
and not target.is_symlink()
and current.st_size == expected_bytes
):
replacement_scratch_bytes = max(replacement_scratch_bytes, expected_bytes)
else:
missing_bytes += expected_bytes
return missing_bytes + replacement_scratch_bytes + IMPORT_DISK_RESERVE_BYTES
def _human_bytes(value: int) -> str:
return f"{value / (1024**3):.2f} ГиБ"
def _world_manifest(project_id: str, artifacts: list[dict[str, Any]]) -> dict[str, Any]:
def url_for(role: str) -> str | None:
descriptor = next((item for item in artifacts if item["role"] == role), None)
+43
View File
@@ -75,6 +75,9 @@ _E37_RESULT_ID = re.compile(r"^e37-ravnoves-acceptance-[a-f0-9]{64}$")
_E38_RESULT_ID = re.compile(r"^e38-perception-baseline-[a-f0-9]{64}$")
_E39_RESULT_ID = re.compile(r"^e39-perception-refinement-[a-f0-9]{64}$")
_E40_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
_M4_RESULT_ID = re.compile(r"^m4-threat-replay-[a-f0-9]{64}$")
_M49_TGS_RESULT_ID = re.compile(r"^m49-tgs-full-shadow-[a-f0-9]{64}$")
_VEGETATION_RESULT_ID = re.compile(r"^lab-v1-vegetation-shadow-[a-f0-9]{64}$")
RootProvider = Callable[[], Path | None]
@@ -245,6 +248,7 @@ def _advanced_index_item(
raise ValueError("advanced LAB authority is invalid")
if document.get("ground_truth") not in (None, False):
raise ValueError("advanced LAB ground-truth claim is invalid")
_validate_product_publication_shape(document, work_id=work_id)
created_at_utc = document.get("created_at_utc")
if not isinstance(created_at_utc, str) or not created_at_utc.strip():
raise ValueError("advanced LAB creation time is invalid")
@@ -256,6 +260,45 @@ def _advanced_index_item(
}
def _validate_product_publication_shape(
document: dict[str, Any],
*,
work_id: str,
) -> None:
if work_id != "lab-v1-vegetation-shadow":
return
full_route = document.get("route_full_review")
if isinstance(full_route, dict):
if (
full_route.get("source_id") != "RAVNOVES004TREE"
or full_route.get("session_id") != "20260828T130511Z_viewer_live"
or full_route.get("frame_count") != 6830
or _VEGETATION_RESULT_ID.fullmatch(
str(full_route.get("linked_route_review_result_id", ""))
)
is None
or document.get("route_video") is not None
or document.get("route_review") is not None
):
raise ValueError("vegetation LAB has no canonical RAVNOVES004TREE publication shape")
return
route = document.get("route_video")
fusion = route.get("fusion") if isinstance(route, dict) else None
if (
not isinstance(route, dict)
or route.get("view_kind") != "coarse-material-policy-review"
or _M4_RESULT_ID.fullmatch(str(route.get("base_m4_result_id", ""))) is None
or _M49_TGS_RESULT_ID.fullmatch(str(route.get("linked_tgs_result_id", "")))
is None
or not isinstance(fusion, dict)
or fusion.get("mode") != "synchronised-multilayer-review"
or fusion.get("pixel_raster_fusion") is not False
or document.get("route_review") is not None
or document.get("route_full_review") is not None
):
raise ValueError("vegetation LAB has no canonical M4/M4.9 publication shape")
def _advanced_index(
specs: tuple[_AdvancedIndexSpec, ...],
) -> dict[str, object]:
+15
View File
@@ -360,6 +360,15 @@ def _m48_recorded_camera_playback_source(
return session_recorded_camera_frame_service.playback_source(session_id)
def _canonical_lab_recording_source(session_id: str) -> tuple[Path, str] | None:
"""Resolve one already-published immutable RRD without starting new work."""
snapshot = session_recording_preparation_manager.status(session_id)
if snapshot is None or snapshot.state != "ready" or snapshot.recording is None:
return None
return snapshot.recording.path, snapshot.recording.sha256
def refresh_observation_catalog() -> tuple[str, ...]:
"""Discover completed or recoverable local evidence without copying payloads."""
@@ -1032,6 +1041,12 @@ app.include_router(
/ "lab-v1-vegetation"
/ "results"
),
canonical_recording_provider=_canonical_lab_recording_source,
camera_frame_provider=(
session_recorded_camera_frame_service.extract
if session_recorded_camera_frame_service is not None
else None
),
)
)
app.include_router(
+78
View File
@@ -37,6 +37,10 @@ from k1link.sessions import (
SessionStore,
validate_recorded_media_timeline,
)
from k1link.sessions.canonical_lab_spatial import (
CANONICAL_LAB_SPATIAL_PROFILE,
canonical_lab_spatial_frame,
)
from k1link.sessions.plugin_contract import RecordedPointColorRenderer
from k1link.viewer.recorded import (
APPLICATION_ID as RECORDED_APPLICATION_ID,
@@ -824,6 +828,80 @@ def build_session_router(
**response_kwargs,
)
@router.get(
"/api/v1/observation-sessions/{session_id}/canonical-lab/spatial-frame"
)
async def get_observation_session_canonical_lab_spatial_frame(
session_id: str,
generation: Annotated[str, Query(min_length=64, max_length=64)],
time_ns: Annotated[int, Query(ge=0, le=MAX_SAFE_INTEGER)],
profile: Literal["source-paced-ground-v3"],
) -> JSONResponse:
"""Serve one body-frame sample for the canonical recorded-LAB clock.
The camera media clock owns playback. Spatial evidence is sampled from
the same immutable recording instead of starting a second Rerun clock.
"""
if SAFE_SHA256.fullmatch(generation) is None:
raise HTTPException(
status_code=412,
detail="Поколение spatial-записи не совпадает.",
)
if recording_preparation_manager is None:
raise HTTPException(
status_code=503,
detail="Сервис canonical LAB spatial playback не настроен.",
)
snapshot = recording_preparation_manager.status(session_id)
if snapshot is None or snapshot.state != "ready" or snapshot.recording is None:
raise HTTPException(
status_code=409,
detail="Запись canonical LAB ещё не подготовлена.",
)
_require_matching_recording_generation(snapshot.recording.sha256, generation)
pinned = recording_preparation_manager.pin_ready(
session_id,
preparation_id=snapshot.preparation_id,
)
if pinned is None:
raise HTTPException(
status_code=412,
detail="Подготовленная spatial-запись была заменена.",
)
pinned_snapshot, release_recording = pinned
try:
recording = pinned_snapshot.recording
if recording is None:
raise HTTPException(
status_code=500,
detail="Подготовленная spatial-запись недоступна.",
)
payload = await run_in_threadpool(
canonical_lab_spatial_frame,
recording.path,
generation,
time_ns,
)
except (OSError, ValueError) as exc:
raise HTTPException(
status_code=503,
detail="Canonical LAB spatial frame не прошёл проверку.",
) from exc
finally:
release_recording()
return JSONResponse(
payload,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": (
f'"{generation}:{CANONICAL_LAB_SPATIAL_PROFILE}:'
f'{payload["source_time_ns"]}"'
),
"X-Content-Type-Options": "nosniff",
},
)
@router.post("/api/v1/observation-sessions/{session_id}/blueprint.rrd")
async def get_observation_session_blueprint(
session_id: str,
+726 -28
View File
@@ -4,15 +4,20 @@ from __future__ import annotations
import copy
import hashlib
import io
import json
import math
import statistics
import zipfile
from collections.abc import Callable
from functools import lru_cache
from pathlib import Path, PurePosixPath
from typing import Any, Final
import numpy as np
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse, Response
from fastapi.responses import FileResponse, JSONResponse, Response
from PIL import Image
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
from k1link.laboratory.evidence_report import (
@@ -20,9 +25,17 @@ from k1link.laboratory.evidence_report import (
verify_laboratory_evidence_result,
)
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA
from k1link.sessions import RecordedCameraFrame, SessionIntegrityError
from k1link.sessions.canonical_lab_spatial import (
canonical_lab_spatial_playback_points,
canonical_lab_spatial_timeline_samples,
)
RootProvider = Callable[[], Path | None]
CanonicalRecordingProvider = Callable[[str], tuple[Path, str] | None]
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
_CANONICAL_ROUTE_CHUNK_FRAMES: Final = 24
_DEFINITION: Final = LaboratoryEvidenceDefinition(
work_id="lab-v1-vegetation-shadow",
runtime_relative_root=PurePosixPath("lab-v1-vegetation/results"),
@@ -40,12 +53,17 @@ _BENCHMARK_DEFINITION: Final = LaboratoryEvidenceDefinition(
def build_vegetation_shadow_lab_router(
*, root_provider: RootProvider = lambda: None,
*,
root_provider: RootProvider = lambda: None,
canonical_recording_provider: CanonicalRecordingProvider | None = None,
camera_frame_provider: CameraFrameProvider | None = None,
) -> APIRouter:
return _build_vegetation_lab_router(
prefix="/api/v1/laboratory/vegetation-shadow",
definition=_DEFINITION,
root_provider=root_provider,
canonical_recording_provider=canonical_recording_provider,
camera_frame_provider=camera_frame_provider,
)
@@ -64,6 +82,8 @@ def _build_vegetation_lab_router(
prefix: str,
definition: LaboratoryEvidenceDefinition,
root_provider: RootProvider,
canonical_recording_provider: CanonicalRecordingProvider | None = None,
camera_frame_provider: CameraFrameProvider | None = None,
) -> APIRouter:
router = APIRouter(
prefix=prefix,
@@ -150,19 +170,7 @@ def _build_vegetation_lab_router(
archive_path = candidate.joinpath(*relative.parts)
member = f"masks/frame-{sequence + 1:06d}.png"
try:
before = archive_path.stat()
with zipfile.ZipFile(archive_path) as frozen:
info = frozen.getinfo(member)
if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024:
raise ValueError("Vegetation video mask member is invalid")
payload = frozen.read(info)
after = archive_path.stat()
if (
before.st_size != after.st_size
or before.st_mtime_ns != after.st_mtime_ns
or len(payload) != info.file_size
):
raise ValueError("Vegetation video mask archive changed during read")
payload = _read_cached_mask_member(archive_path, member)
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
raise HTTPException(
status_code=503,
@@ -262,25 +270,684 @@ def _build_vegetation_lab_router(
},
)
@router.get("/{result_id}/timeline")
def get_canonical_route_timeline(result_id: str) -> dict[str, object]:
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
intervals = [
(current - previous) / 1_000_000_000
for previous, current in zip(frame_times_ns, frame_times_ns[1:], strict=False)
]
nominal_interval = statistics.median(intervals)
if not math.isfinite(nominal_interval) or nominal_interval <= 0:
raise HTTPException(status_code=503, detail="Full-route timeline cadence is invalid")
return {
"schema_version": "missioncore.recorded-spatial-evidence-timeline/v1",
"result_id": result_id,
"recorded_source": {
"session_id": route["session_id"],
"source_id": route["source_id"],
"representation_id": "registered-map-increment-v1",
"synchronization": "host-arrival-best-effort",
},
"frame_count": len(frame_times_ns),
"frame_times_ns": list(frame_times_ns),
"timeline_start_seconds": frame_times_ns[0] / 1_000_000_000,
"timeline_end_seconds": frame_times_ns[-1] / 1_000_000_000,
"nominal_frame_interval_seconds": nominal_interval,
"nominal_rate_hz": 1.0 / nominal_interval,
"max_chunk_frames": _CANONICAL_ROUTE_CHUNK_FRAMES,
"point_sample_limit": 100_000,
"maximum_source_points_per_frame": 100_000,
"point_delivery": "exact-current-increment",
"world_state_frame_count": len(frame_times_ns),
"superseded_frame_count": 0,
"local_surface_visualization": {
"derivation": "bounded-registered-increment-accumulation",
"window_seconds": 5.0,
"voxel_size_m": 0.12,
"radius_m": 30.0,
"point_limit": 27_000,
"authority": "visual-derived",
},
"image_width": route["width"],
"image_height": route["height"],
"rig": {"length_m": 1.0, "width_m": 0.8, "nominal_sensor_height_m": 0.4},
"corridor": {
"forward_length_m": 8.0,
"rear_margin_m": 0.5,
"occupied_voxel_size_m": 0.45,
"half_width_m": 0.6,
"prediction_horizon_seconds": 8.0,
},
"ground_truth": False,
"authority": "replay-simulated",
"access": "read-only-bounded-recorded-replay",
}
@router.get("/{result_id}/timeline/chunk")
def get_canonical_route_timeline_chunk(
result_id: str,
start: int = 0,
count: int = _CANONICAL_ROUTE_CHUNK_FRAMES,
include_points: bool = True,
) -> dict[str, object]:
if start < 0 or not 1 <= count <= _CANONICAL_ROUTE_CHUNK_FRAMES:
raise HTTPException(status_code=422, detail="Full-route timeline chunk is invalid")
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
if start >= len(frame_times_ns):
raise HTTPException(status_code=404, detail="Full-route timeline chunk not found")
if canonical_recording_provider is None:
raise HTTPException(
status_code=503,
detail="Canonical spatial recording is unavailable",
)
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
try:
samples = canonical_lab_spatial_timeline_samples(
recording_path,
generation_sha256,
frame_times_ns,
start,
count,
include_local_slam=False,
)
except (OSError, ValueError):
raise HTTPException(status_code=503, detail="Canonical spatial chunk failed") from None
stop = start + len(samples)
frames = [
_canonical_timeline_frame(
result_id=result_id,
endpoint_prefix=prefix,
candidate=candidate,
route=route,
sequence=sequence,
source_time_ns=frame_times_ns[sequence],
spatial=sample,
include_points=include_points,
)
for sequence, sample in zip(range(start, stop), samples, strict=True)
]
return {
"schema_version": "missioncore.recorded-spatial-evidence-chunk/v1",
"result_id": result_id,
"start_sequence": start,
"frame_count": len(frames),
"next_sequence": stop if stop < len(frame_times_ns) else None,
"frames": frames,
"ground_truth": False,
"authority": "replay-simulated",
"access": "read-only-bounded-recorded-replay",
}
@router.get("/{result_id}/timeline/playback")
def get_canonical_route_timeline_playback(result_id: str) -> dict[str, object]:
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
points, offsets = _canonical_route_playback(
canonical_recording_provider,
route,
frame_times_ns,
)
points_view = memoryview(points).cast("B")
return {
"schema_version": "missioncore.recorded-spatial-playback/v1",
"result_id": result_id,
"frame_count": len(frame_times_ns),
"point_count": int(points.shape[0]),
"point_offsets": list(offsets),
"chunk_frame_count": _CANONICAL_ROUTE_CHUNK_FRAMES,
"resident_chunk_count_max": 4,
"forward_prefetch_chunk_count": 1,
"chunks": _canonical_route_playback_chunk_catalog(
prefix,
result_id,
points_view,
offsets,
),
"track": {
"id": "points-map-f32",
"url": f"{prefix}/{result_id}/timeline/playback/tracks/points-map-f32",
"media_type": "application/octet-stream",
"dtype": "<f4",
"shape": [int(points.shape[0]), 3],
"bytes": int(points.nbytes),
"sha256": hashlib.sha256(points_view).hexdigest(),
},
"coordinate_frame": "map",
"ground_truth": False,
"authority": "replay-simulated",
"access": "read-only-sealed-binary-playback",
}
@router.get("/{result_id}/timeline/playback/chunks/{chunk_index}")
def get_canonical_route_timeline_playback_chunk(
result_id: str,
chunk_index: int,
) -> Response:
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
points, offsets = _canonical_route_playback(
canonical_recording_provider,
route,
frame_times_ns,
)
points_view = memoryview(points).cast("B")
descriptor = _canonical_route_playback_chunk_descriptor(
prefix,
result_id,
points_view,
offsets,
chunk_index,
)
if descriptor is None:
raise HTTPException(status_code=404, detail="Full-route playback chunk not found")
point_start = int(descriptor["point_start"])
byte_length = int(descriptor["bytes"])
byte_start = point_start * 3 * 4
payload = bytes(points_view[byte_start : byte_start + byte_length])
return Response(
content=payload,
media_type="application/octet-stream",
headers=_immutable_binary_headers(byte_length, str(descriptor["sha256"])),
)
@router.get("/{result_id}/timeline/playback/tracks/points-map-f32")
def get_canonical_route_timeline_playback_track(result_id: str) -> Response:
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
points, _ = _canonical_route_playback(
canonical_recording_provider,
route,
frame_times_ns,
)
payload = memoryview(points).cast("B")
digest = hashlib.sha256(payload).hexdigest()
return Response(
content=bytes(payload),
media_type="application/octet-stream",
headers=_immutable_binary_headers(payload.nbytes, digest),
)
@router.get("/{result_id}/timeline/frames/{sequence}/camera")
def get_canonical_route_camera(result_id: str, sequence: int) -> Response:
if camera_frame_provider is None:
raise HTTPException(status_code=503, detail="Recorded camera decoder is unavailable")
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
route, frame_times_ns = _full_route_context(candidate, manifest)
if not 0 <= sequence < len(frame_times_ns):
raise HTTPException(status_code=404, detail="Full-route camera frame not found")
try:
camera = camera_frame_provider(str(route["session_id"]), sequence)
except (OSError, SessionIntegrityError, ValueError):
raise HTTPException(
status_code=503,
detail="Full-route camera frame unavailable",
) from None
if camera.width != route["width"] or camera.height != route["height"]:
raise HTTPException(status_code=503, detail="Full-route camera dimensions changed")
return Response(
content=camera.payload,
media_type=camera.media_type,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{camera.sha256}"',
"X-Content-Type-Options": "nosniff",
},
)
@router.get("/{result_id}/route-tgs-anchor/{source_sequence}")
def get_route_tgs_anchor(result_id: str, source_sequence: int) -> JSONResponse:
candidate = _resolve_candidate(root_provider, definition, result_id)
manifest = _read_verified(candidate, definition)
review = manifest.get("route_review")
cases = review.get("cases") if isinstance(review, dict) else None
if (
not isinstance(cases, list)
or review.get("source_id") != "RAVNOVES004TREE"
or review.get("session_id") != "20260828T130511Z_viewer_live"
or not any(
isinstance(item, dict) and item.get("source_sequence") == source_sequence
for item in cases
)
):
raise HTTPException(status_code=404, detail="Route TGS anchor not found")
artifacts = manifest.get("artifacts")
descriptor = next(
(
item
for item in artifacts if isinstance(item, dict)
and item.get("role") == "mixed-route-tgs-evidence"
and item.get("path") == "proofs/tgs-evidence.npz"
and item.get("media_type") == "application/x-npz"
),
None,
) if isinstance(artifacts, list) else None
if descriptor is None:
raise HTTPException(status_code=404, detail="Route TGS anchor not found")
try:
payload = _route_tgs_anchor_payload(
candidate / "proofs" / "tgs-evidence.npz",
source_sequence,
)
except (KeyError, OSError, ValueError):
raise HTTPException(
status_code=503,
detail="Route TGS anchor failed verification",
) from None
return JSONResponse(
payload,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{descriptor.get("sha256", "")}"',
"X-Content-Type-Options": "nosniff",
},
)
return router
def _full_route_context(
candidate: Path,
manifest: dict[str, Any],
) -> tuple[dict[str, Any], tuple[int, ...]]:
route = manifest.get("route_full_review")
timeline = route.get("timeline") if isinstance(route, dict) else None
relative_text = timeline.get("path") if isinstance(timeline, dict) else None
if (
not isinstance(route, dict)
or route.get("source_id") != "RAVNOVES004TREE"
or route.get("session_id") != "20260828T130511Z_viewer_live"
or route.get("frame_count") != 6830
or route.get("width") != 800
or route.get("height") != 600
or not isinstance(relative_text, str)
):
raise HTTPException(status_code=404, detail="Full-route canonical timeline not found")
path = candidate.joinpath(*PurePosixPath(relative_text).parts)
try:
payload = path.read_bytes()
if (
len(payload) != timeline.get("byte_length")
or hashlib.sha256(payload).hexdigest() != timeline.get("sha256")
):
raise ValueError("timeline digest changed")
values = np.frombuffer(payload, dtype="<u8")
frame_times_ns = tuple(int(value) for value in values)
except (OSError, ValueError):
raise HTTPException(
status_code=503,
detail="Full-route timeline verification failed",
) from None
if (
len(frame_times_ns) != route["frame_count"]
or any(
current <= previous
for previous, current in zip(frame_times_ns, frame_times_ns[1:], strict=False)
)
):
raise HTTPException(status_code=503, detail="Full-route timeline order changed")
return route, frame_times_ns
def _canonical_route_playback(
provider: CanonicalRecordingProvider | None,
route: dict[str, Any],
frame_times_ns: tuple[int, ...],
) -> tuple[np.ndarray, tuple[int, ...]]:
if provider is None:
raise HTTPException(status_code=503, detail="Canonical spatial recording is unavailable")
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
try:
return canonical_lab_spatial_playback_points(
recording_path,
generation_sha256,
frame_times_ns,
)
except (OSError, ValueError):
raise HTTPException(status_code=503, detail="Canonical spatial playback failed") from None
def _canonical_route_playback_chunk_descriptor(
endpoint_prefix: str,
result_id: str,
points_view: memoryview,
offsets: tuple[int, ...],
chunk_index: int,
) -> dict[str, object] | None:
frame_count = len(offsets) - 1
start = chunk_index * _CANONICAL_ROUTE_CHUNK_FRAMES
if chunk_index < 0 or start >= frame_count:
return None
count = min(_CANONICAL_ROUTE_CHUNK_FRAMES, frame_count - start)
point_start = offsets[start]
point_stop = offsets[start + count]
byte_start = point_start * 3 * 4
byte_stop = point_stop * 3 * 4
payload = points_view[byte_start:byte_stop]
return {
"index": chunk_index,
"start": start,
"count": count,
"point_start": point_start,
"point_count": point_stop - point_start,
"url": f"{endpoint_prefix}/{result_id}/timeline/playback/chunks/{chunk_index}",
"media_type": "application/octet-stream",
"dtype": "<f4",
"shape": [point_stop - point_start, 3],
"bytes": payload.nbytes,
"sha256": hashlib.sha256(payload).hexdigest(),
}
def _canonical_route_playback_chunk_catalog(
endpoint_prefix: str,
result_id: str,
points_view: memoryview,
offsets: tuple[int, ...],
) -> list[dict[str, object]]:
frame_count = len(offsets) - 1
chunk_count = (
frame_count + _CANONICAL_ROUTE_CHUNK_FRAMES - 1
) // _CANONICAL_ROUTE_CHUNK_FRAMES
return [
descriptor
for chunk_index in range(chunk_count)
if (
descriptor := _canonical_route_playback_chunk_descriptor(
endpoint_prefix,
result_id,
points_view,
offsets,
chunk_index,
)
)
is not None
]
def _immutable_binary_headers(byte_length: int, sha256: str) -> dict[str, str]:
return {
"Cache-Control": "private, max-age=31536000, immutable",
"Content-Encoding": "identity",
"Content-Length": str(byte_length),
"ETag": f'"{sha256}"',
"X-Content-Type-Options": "nosniff",
"X-Uncompressed-Content-Length": str(byte_length),
}
def _canonical_timeline_frame(
*,
result_id: str,
endpoint_prefix: str,
candidate: Path,
route: dict[str, Any],
sequence: int,
source_time_ns: int,
spatial: dict[str, object] | None,
include_points: bool,
) -> dict[str, object]:
points = [] if spatial is None or not include_points else spatial["source_points_body_xyz_m"]
point_count = 0 if spatial is None else int(spatial["source_point_count"])
body_frame = None if spatial is None else spatial["body_frame"]
return {
"schema_version": "missioncore.recorded-spatial-evidence-frame/v1",
"sequence": sequence,
"frame_id": f"frame-{sequence:06d}",
"source_time_ns": source_time_ns,
"session_seconds": source_time_ns / 1_000_000_000,
"source_available": spatial is not None,
"spatial_available": spatial is not None,
"world_state_available": True,
"terminal_outcome": "delivered",
"body_frame": body_frame,
"point_cloud_body_xyz_m": points,
"point_cloud_source_count": point_count,
"point_cloud_sample_count": point_count if not include_points else len(points),
"point_cloud_layer": "current-increment",
"rolling_map_component_count": 0,
"metric_obstacles": [],
"camera_proposals": _semantic_component_proposals(candidate, route, sequence),
"decision_counts": {"threat": 0, "not-threat": 0, "unknown": 0},
"camera_url": (
f"{endpoint_prefix}/{result_id}/timeline/frames/{sequence}/camera"
),
"ground_truth": False,
"authority": "replay-simulated",
}
def _semantic_component_proposals(
candidate: Path,
route: dict[str, Any],
sequence: int,
) -> list[dict[str, object]]:
layers = route.get("layers")
city = layers.get("city") if isinstance(layers, dict) else None
archive = city.get("mask_archive") if isinstance(city, dict) else None
relative = archive.get("path") if isinstance(archive, dict) else None
if not isinstance(relative, str):
return []
archive_path = candidate.joinpath(*PurePosixPath(relative).parts)
try:
stat = archive_path.stat()
except OSError:
return []
return [
dict(proposal)
for proposal in _semantic_component_proposals_cached(
str(archive_path),
stat.st_size,
stat.st_mtime_ns,
sequence,
)
]
@lru_cache(maxsize=256)
def _semantic_component_proposals_cached(
archive_path_text: str,
archive_size: int,
archive_mtime_ns: int,
sequence: int,
) -> tuple[dict[str, object], ...]:
member = f"masks/frame-{sequence + 1:06d}.png"
try:
frozen = _cached_zip_archive(
archive_path_text,
archive_size,
archive_mtime_ns,
)
payload = frozen.read(member)
with Image.open(io.BytesIO(payload)) as image:
mask = np.asarray(image.convert("L"), dtype=np.uint8)
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
return ()
labels = {
1: "semantic person",
2: "semantic bicycle",
3: "semantic motorcycle",
4: "semantic car",
5: "semantic heavy vehicle",
13: "semantic static obstacle",
14: "semantic animal",
}
proposals: list[dict[str, object]] = []
for class_id, label in labels.items():
minimum_pixels = 80 if class_id == 13 else 24
for component_index, (left, top, right, bottom, pixel_count) in enumerate(
_mask_component_boxes(mask, class_id, minimum_pixels=minimum_pixels)[:12]
):
proposals.append({
"proposal_id": f"semantic-{class_id}-{sequence}-{component_index}",
"bbox_xyxy": [left, top, right, bottom],
"objectness": round(min(0.99, 0.5 + pixel_count / 20_000), 4),
"semantic_hint": label,
"occupied_support": False,
"range_m": None,
"threat_decision": None,
"threat_reason_codes": ["semantic-mask-derived-not-fail-safe-detector"],
})
proposals.sort(
key=lambda proposal: (
-float(proposal["objectness"]),
str(proposal["proposal_id"]),
)
)
return tuple(proposals[:32])
def _mask_component_boxes(
mask: np.ndarray,
class_id: int,
*,
minimum_pixels: int,
) -> list[tuple[int, int, int, int, int]]:
"""Return 8-connected run-length components without an OpenCV dependency."""
if mask.ndim != 2 or minimum_pixels < 1:
return []
parents: list[int] = []
runs: list[tuple[int, int, int, int]] = []
def root(index: int) -> int:
while parents[index] != index:
parents[index] = parents[parents[index]]
index = parents[index]
return index
def union(left: int, right: int) -> None:
left_root = root(left)
right_root = root(right)
if left_root != right_root:
parents[right_root] = left_root
previous: list[int] = []
for row_index, row in enumerate(mask):
matches = np.flatnonzero(row == class_id)
if matches.size == 0:
previous = []
continue
split_at = np.flatnonzero(np.diff(matches) > 1) + 1
groups = np.split(matches, split_at)
current: list[int] = []
previous_cursor = 0
for group in groups:
start = int(group[0])
stop = int(group[-1]) + 1
run_index = len(runs)
runs.append((row_index, start, stop, stop - start))
parents.append(run_index)
current.append(run_index)
while (
previous_cursor < len(previous)
and runs[previous[previous_cursor]][2] < start
):
previous_cursor += 1
candidate_cursor = previous_cursor
while candidate_cursor < len(previous):
previous_index = previous[candidate_cursor]
_, previous_start, previous_stop, _ = runs[previous_index]
if previous_start > stop:
break
union(run_index, previous_index)
candidate_cursor += 1
previous = current
components: dict[int, list[int]] = {}
for run_index, (row, start, stop, count) in enumerate(runs):
component = components.setdefault(root(run_index), [start, row, stop, row + 1, 0])
component[0] = min(component[0], start)
component[1] = min(component[1], row)
component[2] = max(component[2], stop)
component[3] = max(component[3], row + 1)
component[4] += count
result = [
(left, top, right, bottom, count)
for left, top, right, bottom, count in components.values()
if count >= minimum_pixels and right - left >= 2 and bottom - top >= 3
]
result.sort(key=lambda box: (-box[4], box[1], box[0]))
return result
def _route_tgs_anchor_payload(path: Path, source_sequence: int) -> dict[str, object]:
before = path.stat()
with np.load(path, allow_pickle=False) as archive:
source_indices = archive["source_frame_indices"]
offsets = archive["current_increment_point_offsets"]
points = archive["current_increment_points_xyz_m"]
centers = archive["costmap_cell_centers_xy_m"]
states = archive["causal_rolling_1s_costmap_states"]
z_bounds = archive["causal_rolling_1s_costmap_z_bounds_m"]
if (
source_indices.shape != (10,)
or offsets.shape != (11,)
or points.ndim != 2
or points.shape[1] != 3
or centers.shape != (2244, 2)
or states.shape != (10, 2244)
or z_bounds.shape != (10, 2244, 2)
):
raise ValueError("Route TGS evidence shape changed")
matches = np.flatnonzero(source_indices == source_sequence - 1)
if matches.shape != (1,):
raise ValueError("Route TGS source sequence changed")
slot = int(matches[0])
start = int(offsets[slot])
end = int(offsets[slot + 1])
if not 0 <= start <= end <= points.shape[0]:
raise ValueError("Route TGS point offsets changed")
selected_points = np.ascontiguousarray(points[start:end], dtype=np.float32)
selected_states = np.ascontiguousarray(states[slot], dtype=np.uint8)
selected_z_bounds = np.ascontiguousarray(z_bounds[slot], dtype=np.float32)
if (
not np.isfinite(selected_points).all()
or not np.isin(selected_states, [0, 1, 2, 3]).all()
):
raise ValueError("Route TGS payload changed")
result = {
"schema_version": "missioncore.lab-v1-route-tgs-anchor/v1",
"source_sequence": source_sequence,
"slot": slot,
"current_points_xyz_m": selected_points.astype(float).tolist(),
"costmap": {
"cell_size_m": 0.45,
"centers_xy_m": centers.astype(float).tolist(),
"state_codes": selected_states.astype(int).tolist(),
"z_bounds_m": [
[
float(row[0]) if np.isfinite(row[0]) else None,
float(row[1]) if np.isfinite(row[1]) else None,
]
for row in selected_z_bounds
],
},
}
after = path.stat()
if before.st_size != after.st_size or before.st_mtime_ns != after.st_mtime_ns:
raise ValueError("Route TGS evidence changed during read")
return result
def _zip_mask_response(archive_path: Path, sequence: int) -> Response:
member = f"masks/frame-{sequence + 1:06d}.png"
try:
before = archive_path.stat()
with zipfile.ZipFile(archive_path) as frozen:
info = frozen.getinfo(member)
if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024:
raise ValueError("Semantic mask member is invalid")
payload = frozen.read(info)
after = archive_path.stat()
if (
before.st_size != after.st_size
or before.st_mtime_ns != after.st_mtime_ns
or len(payload) != info.file_size
):
raise ValueError("Semantic mask archive changed during read")
payload = _read_cached_mask_member(archive_path, member)
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
raise HTTPException(
status_code=503,
@@ -298,6 +965,37 @@ def _zip_mask_response(archive_path: Path, sequence: int) -> Response:
)
@lru_cache(maxsize=8)
def _cached_zip_archive(
archive_path_text: str,
archive_size: int,
archive_mtime_ns: int,
) -> zipfile.ZipFile:
del archive_size, archive_mtime_ns
return zipfile.ZipFile(archive_path_text)
def _read_cached_mask_member(archive_path: Path, member: str) -> bytes:
before = archive_path.stat()
frozen = _cached_zip_archive(
str(archive_path),
before.st_size,
before.st_mtime_ns,
)
info = frozen.getinfo(member)
if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024:
raise ValueError("Semantic mask member is invalid")
payload = frozen.read(info)
after = archive_path.stat()
if (
before.st_size != after.st_size
or before.st_mtime_ns != after.st_mtime_ns
or len(payload) != info.file_size
):
raise ValueError("Semantic mask archive changed during read")
return payload
def _configured_root(provider: RootProvider) -> Path | None:
candidate = provider()
if candidate is None:
+79
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import os
from pathlib import Path, PurePosixPath
from types import SimpleNamespace
@@ -151,6 +152,84 @@ def test_advanced_index_projects_one_most_mature_lifecycle_phase(tmp_path: Path)
]
def test_advanced_index_prefers_canonical_rav004_full_review(tmp_path: Path) -> None:
root = tmp_path / "vegetation"
def publish(digest: str, *, publication: str) -> Path:
result_id = f"lab-v1-vegetation-shadow-{digest}"
candidate = root / result_id
candidate.mkdir(parents=True)
route_video = {
"view_kind": "coarse-material-policy-review",
"base_m4_result_id": f"m4-threat-replay-{'1' * 64}",
"linked_tgs_result_id": f"m49-tgs-full-shadow-{'2' * 64}",
"fusion": {
"mode": "synchronised-multilayer-review",
"pixel_raster_fusion": False,
},
} if publication == "rav00" else None
route_full_review = {
"source_id": "RAVNOVES004TREE",
"session_id": "20260828T130511Z_viewer_live",
"frame_count": 6830,
"linked_route_review_result_id": (
f"lab-v1-vegetation-shadow-{'4' * 64}"
if publication == "rav004"
else None
),
} if publication != "rav00" else None
(candidate / "manifest.json").write_text(
json.dumps(
{
"schema_version": "missioncore.lab-v1-vegetation-shadow/v1",
"result_id": result_id,
"identity_sha256": digest,
"identity": {
"authority": {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
}
},
"created_at_utc": "2026-08-29T10:00:00Z",
"ground_truth": False,
"route_video": route_video,
"route_review": None,
"route_full_review": route_full_review,
}
),
encoding="utf-8",
)
return candidate
rav00 = publish("a" * 64, publication="rav00")
incomplete = publish("b" * 64, publication="incomplete")
rav004 = publish("c" * 64, publication="rav004")
os.utime(rav00, ns=(10_000_000_000, 10_000_000_000))
os.utime(incomplete, ns=(20_000_000_000, 20_000_000_000))
os.utime(rav004, ns=(30_000_000_000, 30_000_000_000))
registry = _evidence_registry(
root,
work_id="lab-v1-vegetation-shadow",
result_id_prefix="lab-v1-vegetation-shadow",
schema_version="missioncore.lab-v1-vegetation-shadow/v1",
)
router = build_advanced_laboratory_router(
evidence_registry=registry,
evidence_runtime_root_provider=lambda: root.parent,
)
index = _endpoint(router, "/api/v1/laboratory/advanced-index")()
assert index["items"] == [ # type: ignore[index]
{
"work_id": "lab-v1-vegetation-shadow",
"result_id": rav004.name,
"created_at_utc": "2026-08-29T10:00:00Z",
"access": "read-only",
}
]
def test_advanced_index_includes_valid_l31_identity(
tmp_path: Path,
monkeypatch: MonkeyPatch,
+172
View File
@@ -0,0 +1,172 @@
from __future__ import annotations
import numpy as np
import pytest
import k1link.sessions.canonical_lab_spatial as spatial_module
from k1link.sessions.canonical_lab_spatial import (
_bounded_local_slam,
_CanonicalSpatialIndex,
_estimate_local_sensor_height,
_estimate_sensor_height,
_gravity_stable_basis_map_from_body,
_ground_origin_map,
_TimedPoints,
_TimedPoses,
canonical_lab_spatial_playback_points,
)
def _calibration_cloud(height_m: float, seed: int) -> np.ndarray:
rng = np.random.default_rng(seed)
xy = rng.uniform(-5.5, 5.5, size=(500, 2)).astype(np.float32)
radius = np.linalg.norm(xy, axis=1)
xy = xy[(radius >= 1.0) & (radius <= 5.5)][:360]
ground = np.column_stack((
xy,
rng.normal(-height_m, 0.006, size=xy.shape[0]),
)).astype(np.float32)
vegetation = np.column_stack((
rng.uniform(-5, 5, size=(300, 2)),
rng.uniform(0.0, 1.2, size=300),
)).astype(np.float32)
return np.concatenate((ground, vegetation), axis=0)
def test_session_sensor_height_is_derived_from_initial_source_cloud() -> None:
times = tuple(index * 500_000_000 for index in range(12))
points = _TimedPoints(
times_ns=times,
values=tuple(_calibration_cloud(0.32, index) for index in range(12)),
)
poses = _TimedPoses(
times_ns=times,
translations=tuple(np.zeros(3) for _ in times),
quaternions_xyzw=tuple(np.asarray([0.0, 0.0, 0.0, 1.0]) for _ in times),
)
height, sample_count, mad = _estimate_sensor_height(points, poses)
assert height == pytest.approx(0.32, abs=0.02)
assert sample_count == 12
assert mad < 0.02
def test_local_slam_accumulates_source_increments_in_ground_body_frame() -> None:
points = _TimedPoints(
times_ns=(0, 1_000_000_000, 2_000_000_000),
values=(
np.asarray([[1.0, 0.0, -0.32]], dtype=np.float32),
np.asarray([[2.0, 0.0, -0.32]], dtype=np.float32),
np.asarray([[3.0, 0.0, -0.32]], dtype=np.float32),
),
)
basis = np.eye(3)
ground_origin = _ground_origin_map(np.asarray([0.0, 0.0, 0.0]), 0.32)
local, frame_count, source_count = _bounded_local_slam(
points,
2_000_000_000,
ground_origin,
basis,
)
assert frame_count == 3
assert source_count == 3
assert local[:, 2].tolist() == pytest.approx([0.0, 0.0, 0.0], abs=1e-6)
def test_gravity_stable_body_frame_converts_rfu_to_forward_left_up() -> None:
times = (0, 1_000_000_000, 2_000_000_000)
poses = _TimedPoses(
times_ns=times,
translations=(
np.asarray([0.0, 0.0, 0.4]),
np.asarray([0.0, 1.0, 0.5]),
np.asarray([0.0, 2.0, 0.3]),
),
quaternions_xyzw=tuple(
np.asarray([0.25, 0.0, 0.0, np.sqrt(1.0 - 0.25**2)]) for _ in times
),
)
basis, source = _gravity_stable_basis_map_from_body(poses, 1_000_000_000)
assert source == "smoothed-pose-trajectory-tangent"
assert basis[:, 0].tolist() == pytest.approx([0.0, 1.0, 0.0], abs=1e-7)
assert basis[:, 1].tolist() == pytest.approx([-1.0, 0.0, 0.0], abs=1e-7)
assert basis[:, 2].tolist() == pytest.approx([0.0, 0.0, 1.0], abs=1e-7)
assert np.linalg.det(basis) == pytest.approx(1.0, abs=1e-7)
def test_ground_origin_is_projected_only_along_map_gravity() -> None:
origin = _ground_origin_map(np.asarray([4.0, -2.0, 1.25]), 0.32)
assert origin.tolist() == pytest.approx([4.0, -2.0, 0.93], abs=1e-9)
def test_sensor_height_tracks_current_source_window_instead_of_fixed_mount() -> None:
times = tuple(index * 500_000_000 for index in range(8))
points = _TimedPoints(
times_ns=times,
values=tuple(
_calibration_cloud(0.18 if index < 4 else 1.05, index)
for index in range(8)
),
)
poses = _TimedPoses(
times_ns=times,
translations=tuple(np.zeros(3) for _ in times),
quaternions_xyzw=tuple(np.asarray([0.0, 0.0, 0.0, 1.0]) for _ in times),
)
low, low_samples, _, low_source = _estimate_local_sensor_height(
points, poses, 500_000_000, 0.5,
)
high, high_samples, _, high_source = _estimate_local_sensor_height(
points, poses, 3_000_000_000, 0.5,
)
assert low == pytest.approx(0.18, abs=0.03)
assert high == pytest.approx(1.05, abs=0.03)
assert low_samples >= 3 and high_samples >= 3
assert low_source == high_source == "local-source-cloud-ground-quantile-median"
def test_playback_track_binds_sparse_map_increments_to_dense_camera_timeline(
tmp_path,
monkeypatch,
) -> None:
recording = tmp_path / "recording.rrd"
recording.write_bytes(b"sealed")
points = _TimedPoints(
times_ns=(10, 20),
values=(
np.asarray([[1.0, 2.0, 3.0]], dtype=np.float32),
np.asarray([[4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float32),
),
)
empty_poses = _TimedPoses(times_ns=(), translations=(), quaternions_xyzw=())
index = _CanonicalSpatialIndex(
points=points,
poses=empty_poses,
trajectories=_TimedPoints(times_ns=(), values=()),
sensor_height_m=0.4,
sensor_height_sample_count=0,
sensor_height_mad_m=0.0,
)
monkeypatch.setattr(spatial_module, "_load_index", lambda *_args: index)
track, offsets = canonical_lab_spatial_playback_points(
recording,
"a" * 64,
(10, 15, 20, 25),
)
assert offsets == (0, 1, 1, 3, 3)
assert track.tolist() == [
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
]
assert track.dtype == np.dtype("<f4")
assert not track.flags.writeable
+25
View File
@@ -14,6 +14,7 @@ from k1link.simulation.gaussian_pipeline_gateway import (
GaussianPipelineGateway,
GaussianPipelineGatewayError,
GaussianPipelineIntegrityError,
GaussianPipelineUnavailableError,
_discover_bundle_members,
)
@@ -256,6 +257,30 @@ def test_gateway_surfaces_bounded_provider_rejection_detail(tmp_path: Path) -> N
))
@pytest.mark.parametrize("status_code", [502, 503, 504])
def test_gateway_classifies_temporary_provider_proxy_failures_as_unavailable(
tmp_path: Path,
status_code: int,
) -> None:
with (
GaussianPipelineGateway(
"http://gaussian.test",
_token_file(tmp_path),
transport=httpx.MockTransport(
lambda _request: httpx.Response(
status_code,
json={"error": "provider_unavailable"},
)
),
) as gateway,
pytest.raises(
GaussianPipelineUnavailableError,
match=rf"HTTP {status_code}.*provider_unavailable",
),
):
gateway.capabilities()
def test_gateway_rejects_incomplete_lcc_bundle(tmp_path: Path) -> None:
bundle = tmp_path / "bundle"
bundle.mkdir()
+97
View File
@@ -17,6 +17,7 @@ from fastapi.responses import FileResponse
from fastapi.routing import APIRoute
import k1link.sessions.media as recorded_media_module
import k1link.web.session_api as session_api_module
from k1link.compute import RecordedPerceptionOverlayArtifact, RecordedPerceptionVideo
from k1link.device_plugins.xgrids_k1 import xgrids_k1_archive_source
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
@@ -458,6 +459,102 @@ def test_completed_recording_get_does_not_hold_delete_for_launch_lease(
manager.close()
def test_canonical_lab_spatial_frame_uses_ready_immutable_recording(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
repository = tmp_path / "repo"
sessions = repository / "sessions"
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
store = SessionStore(repository, data_dir=tmp_path / "data")
store.reconcile_archive(xgrids_k1_archive_source(sessions))
payload = b"sealed-spatial-recording"
def export_recording(source: Path, destination: Path) -> dict[str, object]:
destination.write_bytes(payload)
return {
"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
"rrd_sha256": hashlib.sha256(payload).hexdigest(),
"rrd_bytes": len(payload),
"timeline": "session_time",
"timeline_start_ns": 0,
"timeline_end_ns": 1_000_000_000,
}
materializer = SessionRecordingMaterializer(store.data_dir, exporter=export_recording)
command = store.prepare_replay(session.name)
recording = materializer.materialize(command)
manager = SessionRecordingPreparationManager(materializer)
resolved = manager.resolve_cached(command)
assert resolved is not None and resolved.recording is not None
generation = hashlib.sha256(payload).hexdigest()
expected = {
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v3",
"target_time_ns": 500_000_000,
"source_time_ns": 499_000_000,
"pose_time_ns": 499_000_000,
"trajectory_time_ns": 490_000_000,
"coordinate_frame": "body-ground",
"sensor_height": {
"meters": 0.32,
"source": "local-source-cloud-ground-quantile-median",
"sample_count": 20,
"mad_m": 0.03,
"authority": "visual-derived",
},
"spatial_profile": {
"profile_id": "source-paced-ground-v3",
"local_slam_history_seconds": 5.0,
"local_slam_radius_m": 30.0,
"local_slam_voxel_size_m": 0.12,
"local_slam_point_limit": 27000,
},
"body_frame": {
"origin_map_xyz_m": [0.0, 0.0, 0.0],
"sensor_origin_map_xyz_m": [0.0, 0.0, 0.32],
"basis_map_from_body": [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
},
"source_point_count": 1,
"source_points_body_xyz_m": [[1.0, 2.0, 3.0]],
"local_slam_source_frame_count": 1,
"local_slam_source_point_count": 1,
"local_slam_point_count": 1,
"local_slam_body_xyz_m": [[0.0, 0.0, 0.0]],
}
def spatial_frame(path: Path, sha256: str, time_ns: int) -> dict[str, object]:
assert path == recording.path
assert sha256 == generation
assert time_ns == 500_000_000
return expected
monkeypatch.setattr(session_api_module, "canonical_lab_spatial_frame", spatial_frame)
router = build_session_router(
store,
recording_materializer=materializer,
recording_preparation_manager=manager,
)
spatial_route = endpoint(
router,
"/api/v1/observation-sessions/{session_id}/canonical-lab/spatial-frame",
"GET",
)
try:
response = asyncio.run(spatial_route(
session_id=session.name,
generation=generation,
time_ns=500_000_000,
profile="source-paced-ground-v3",
))
assert json.loads(response.body) == expected
assert response.headers["etag"] == (
f'"{generation}:source-paced-ground-v3:499000000"'
)
assert response.headers["cache-control"].endswith("immutable")
finally:
manager.close()
def test_session_router_returns_seekable_recording_and_serves_byte_ranges(
tmp_path: Path,
) -> None:
+107
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import json
from pathlib import Path
from threading import Event
from time import monotonic, sleep
from typing import Any
import pytest
@@ -444,6 +445,55 @@ def test_service_queue_processes_projects_strictly_one_at_a_time(tmp_path: Path)
assert order == [projects[0]["project_id"], projects[1]["project_id"]]
def test_service_keeps_retained_source_queued_across_temporary_worker_outage(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
store = SimulationProjectStore(tmp_path)
project = store.create(
name="Reconnect without browser reupload",
scene_type="outdoor",
source_kind="folder",
files=_folder_files(),
)
_upload_all(store, project)
store.begin_build(project["project_id"])
class _ReconnectProvider(_ReadyProvider):
def __init__(self) -> None:
super().__init__()
self.capability_calls = 0
def capabilities(self) -> dict[str, object]:
self.capability_calls += 1
if self.capability_calls == 1:
raise GaussianPipelineUnavailableError("temporary tunnel failure")
return super().capabilities()
provider = _ReconnectProvider()
monkeypatch.setattr(
"k1link.simulation.projects.PROVIDER_UNAVAILABLE_RETRY_LIMIT",
1,
)
service = SimulationProjectService(
store,
provider_factory=lambda: provider,
) # type: ignore[arg-type]
service.enqueue(project["project_id"])
deadline = monotonic() + 1.0
while store.get(project["project_id"])["status"] != "ready" and monotonic() < deadline:
sleep(0.01)
recovered = store.get(project["project_id"])
assert recovered["status"] == "ready"
assert recovered["error"] is None
assert recovered["source"]["uploaded_byte_length"] == recovered["source"]["total_byte_length"]
assert provider.capability_calls == 2
assert provider.upload_calls == 1
assert provider.submit_calls == 1
def test_service_deletes_a_queued_project_before_worker_submission(tmp_path: Path) -> None:
store = SimulationProjectStore(tmp_path)
project = store.create(
@@ -561,6 +611,63 @@ def test_failed_project_retries_from_retained_source_and_releases_old_job(tmp_pa
assert queued["source"]["uploaded_byte_length"] == queued["source"]["total_byte_length"]
def test_import_evicts_only_worker_backed_archive_staging_when_disk_is_low(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
store = SimulationProjectStore(tmp_path)
project = store.create(
name="Worker-backed archive",
scene_type="outdoor",
source_kind="archive",
files=[{"logical_path": "scene.rar", "byte_length": 6}],
)
source_file = project["source"]["files"][0]
store.append_upload(
project["project_id"],
source_file["file_id"],
offset=0,
payload=b"source",
)
store.begin_build(project["project_id"])
store.update_processing(
project["project_id"],
status="processing",
provider_job_id="gsp-20260826000000-deadbeef",
provider_state="ready",
)
service = SimulationProjectService(store, provider_factory=lambda: None)
source_path = store.source_root(project["project_id"]) / "scene.rar"
class _DiskUsage:
def __init__(self, free: int) -> None:
self.free = free
monkeypatch.setattr(
"k1link.simulation.projects.shutil.disk_usage",
lambda _path: _DiskUsage(0 if source_path.exists() else 10 * 1024**3),
)
artifacts = [
{
"role": "preview",
"logical_path": "preview.sog",
"media_type": "application/octet-stream",
"sha256": "a" * 64,
"byte_length": 1024,
}
]
service._ensure_import_capacity(
project["project_id"],
artifacts,
store.artifacts_root(project["project_id"]),
)
assert not source_path.exists()
retained_metadata = store.get(project["project_id"])["source"]
assert retained_metadata["uploaded_byte_length"] == retained_metadata["total_byte_length"]
def test_failed_local_project_reattaches_to_live_provider_job_without_rebuild(
tmp_path: Path,
) -> None:
+71 -1
View File
@@ -21,11 +21,81 @@ from k1link.laboratory import LaboratoryEvidenceRegistry
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
from k1link.laboratory.vegetation_policy_review import seal_vegetation_policy_review
from k1link.laboratory.vegetation_shadow_lab import seal_vegetation_shadow_lab
from k1link.web.vegetation_shadow_lab_api import build_vegetation_shadow_lab_router
from k1link.web.vegetation_shadow_lab_api import (
_canonical_route_playback_chunk_descriptor,
_mask_component_boxes,
_route_tgs_anchor_payload,
build_vegetation_shadow_lab_router,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
def test_semantic_component_boxes_keep_distinct_objects_separate() -> None:
mask = np.zeros((20, 30), dtype=np.uint8)
mask[2:10, 3:8] = 4
mask[4:12, 18:24] = 4
mask[15:17, 3:5] = 4
assert _mask_component_boxes(mask, 4, minimum_pixels=20) == [
(18, 4, 24, 12, 48),
(3, 2, 8, 10, 40),
]
def test_route_playback_chunk_descriptor_seals_only_requested_binary_window() -> None:
points = np.arange(18, dtype="<f4").reshape(6, 3)
descriptor = _canonical_route_playback_chunk_descriptor(
"/api/v1/laboratory/vegetation-shadow",
f"lab-v1-vegetation-shadow-{'a' * 64}",
memoryview(points).cast("B"),
(0, 1, 1, 3, 6),
0,
)
assert descriptor is not None
assert descriptor["start"] == 0
assert descriptor["count"] == 4
assert descriptor["point_count"] == 6
assert descriptor["bytes"] == points.nbytes
assert descriptor["shape"] == [6, 3]
assert len(str(descriptor["sha256"])) == 64
def test_route_tgs_anchor_payload_preserves_metric_evidence(tmp_path: Path) -> None:
path = tmp_path / "tgs-evidence.npz"
point_counts = np.arange(1, 11, dtype=np.int64)
offsets = np.concatenate(([0], np.cumsum(point_counts)))
points = np.arange(int(offsets[-1]) * 3, dtype=np.float32).reshape(-1, 3)
centers = np.arange(2244 * 2, dtype=np.float32).reshape(2244, 2) * 0.45
states = np.tile(np.arange(2244, dtype=np.uint16) % 4, (10, 1)).astype(np.uint8)
z_bounds = np.zeros((10, 2244, 2), dtype=np.float32)
z_bounds[..., 0] = np.nan
z_bounds[..., 1] = 1.25
np.savez(
path,
source_frame_indices=np.array(
[20, 408, 789, 1189, 1609, 1992, 2380, 3190, 4810, 6381],
dtype=np.int64,
),
current_increment_point_offsets=offsets,
current_increment_points_xyz_m=points,
costmap_cell_centers_xy_m=centers,
causal_rolling_1s_costmap_states=states,
causal_rolling_1s_costmap_z_bounds_m=z_bounds,
)
payload = _route_tgs_anchor_payload(path, 409)
assert payload["schema_version"] == "missioncore.lab-v1-route-tgs-anchor/v1"
assert payload["source_sequence"] == 409
assert payload["slot"] == 1
assert len(payload["current_points_xyz_m"]) == 2
assert len(payload["costmap"]["centers_xy_m"]) == 2244
assert set(payload["costmap"]["state_codes"]) == {0, 1, 2, 3}
assert payload["costmap"]["z_bounds_m"][0] == [None, 1.25]
def test_coarse_policy_masks_mark_every_outside_fov_pixel_undefined(
tmp_path: Path,
monkeypatch,