feat(perception): integrate calibrated operator pipeline

Add calibrated K1 projection, recorded and near-live perception qualification, unified Rerun operator layers, bounded replay admission, audited viewer controls, worker experiments, and lab evidence.
This commit is contained in:
DCCONSTRUCTIONS 2026-07-23 00:21:49 +03:00
parent ada2a55ee6
commit b53d6d5a45
221 changed files with 55923 additions and 1357 deletions

2
.gitattributes vendored
View File

@ -9,4 +9,4 @@
*.xbin binary
*.las binary
*.lcc binary
apps/control-station/vendor/rerun-web-viewer-0.34.1/re_viewer_bg.nodedc.wasm filter=lfs diff=lfs merge=lfs -text

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ __pycache__/
.coverage
htmlcov/
dist/
apps/control-station/dist-shadow/
build/
*.egg-info/

View File

@ -158,13 +158,16 @@ local reception; plugin-commanded v0.5.0 acquisition uses the separately gated
canonical K1 START/STOP dialogue.
Plugin v0.6.0 makes the local connection direction explicit. Bridge remains the
default; Direct Connect sends the same reviewed BLE frame with credentials for
an already-running controller hotspot; Quick Connect sends no BLE write and
instead associates this Mac with the K1 AP once. Each mode has a distinct
topology attestation and no automatic retry. The Bridge path is physically
accepted. Direct Connect and the Mission Core Quick Connect host adapter are
implemented and offline-verified but still require their own owner-operated
physical acceptance cycles; see [ADR 0013](docs/adr/0013-k1-local-connection-matrix.md).
default and accepted product path; Direct Connect sends the reviewed station
provisioning frame for an already-running controller hotspot. Quick Connect
sends one separately reviewed AP-enable frame and can associate a prepared Mac
through CoreWLAN. Its device activation and prepared-host association were
physically accepted, but credential bootstrap is not portable: a clean host
cannot acquire the firmware-defined AP material from the reviewed BLE protocol.
Quick Connect therefore remains laboratory functionality, with no automatic
firmware download, iPhone extraction or hard-coded fallback. Each mode has a
distinct topology attestation and no automatic retry; see
[ADR 0013](docs/adr/0013-k1-local-connection-matrix.md).
The exact recovered `ModelingRequest` start/stop encoder, response correlator
and device-status state machine are installed in the operator-gated interactive

View File

@ -1,10 +1,15 @@
import { readFileSync, writeFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { copyFileSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const packageRoot = resolve(root, "node_modules/@rerun-io/web-viewer");
const manifest = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
const vendorRoot = resolve(root, "vendor/rerun-web-viewer-0.34.1");
const sha256File = (path) =>
createHash("sha256").update(readFileSync(path)).digest("hex");
if (manifest.version !== "0.34.1") {
throw new Error(
@ -12,6 +17,51 @@ if (manifest.version !== "0.34.1") {
);
}
const vendorRuntime = [
{
label: "wasm runtime",
packagePath: resolve(packageRoot, "re_viewer_bg.wasm"),
vendorPath: resolve(vendorRoot, "re_viewer_bg.nodedc.wasm"),
publishedSha256: "3fe7aab8ea6bb0fd03c3ef694932943411ee174029e398c539c390beb0824d35",
previousNodedcSha256: "1c25e8cecd7641e6f8044d00a6a1cf9d08a615b6f8737026c7d93623b3e06ee7",
nodedcSha256: "38d19bac06b7c3b8e549489469cf7c4a24f319ec953849e4656b5827a6c105bb",
},
{
label: "wasm JavaScript glue",
packagePath: resolve(packageRoot, "re_viewer.js"),
vendorPath: resolve(vendorRoot, "re_viewer.nodedc.js"),
publishedSha256: "7c4ba900820137a6ba23e0f7f57d56e3b007db0de3825d5c8bf0d7684b3cc6a6",
nodedcSha256: "0f7b76c9f24cbd8437021b5d37499894aeadc586183e422ebc82ef556d7b8339",
},
];
for (const runtime of vendorRuntime) {
const vendorSha256 = sha256File(runtime.vendorPath);
if (vendorSha256 !== runtime.nodedcSha256) {
throw new Error(
`Refusing corrupt NODE.DC Rerun ${runtime.label}: ${vendorSha256} != ${runtime.nodedcSha256}`,
);
}
const installedSha256 = sha256File(runtime.packagePath);
if (
![
runtime.publishedSha256,
runtime.previousNodedcSha256,
runtime.nodedcSha256,
].includes(installedSha256)
) {
throw new Error(
`Refusing to replace unexpected Rerun ${runtime.label}: ${installedSha256}`,
);
}
copyFileSync(runtime.vendorPath, runtime.packagePath);
if (sha256File(runtime.packagePath) !== runtime.nodedcSha256) {
throw new Error(`NODE.DC Rerun ${runtime.label} verification failed after copy`);
}
}
const patches = [
{
label: "compiled watchdog",
@ -57,5 +107,5 @@ for (const path of [resolve(packageRoot, "index.js"), resolve(packageRoot, "inde
}
console.log(
"Patched @rerun-io/web-viewer 0.34.1 watchdog teardown and singleton global keyup listener.",
"Patched @rerun-io/web-viewer 0.34.1 native zoom-to-cursor, watchdog teardown, and singleton global keyup listener.",
);

View File

@ -0,0 +1,128 @@
import { createHash } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
const [inputArgument, outputArgument] = process.argv.slice(2);
if (!inputArgument || !outputArgument) {
throw new Error("Usage: node transform-rerun-web-viewer-glue.mjs INPUT OUTPUT");
}
const inputPath = resolve(inputArgument);
const outputPath = resolve(outputArgument);
const expectedGeneratedSha256 =
"cc196a93c5be972c801d46be4dc9934f7f042eb62941f0aa0678f1c8416c6874";
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
let code = readFileSync(inputPath, "utf8");
if (sha256(code) !== expectedGeneratedSha256) {
throw new Error(`Refusing to transform unexpected wasm-bindgen output: ${inputPath}`);
}
// This is the same no-modules-base transformation used by Rerun 0.34.1's
// rerun_js/web-viewer/build-wasm.mjs. Each factory call gets isolated closure state.
const wrapperStart = "let wasm_bindgen = (function(exports) {";
const wrapperEnd = `return Object.assign(__wbg_init, { initSync }, exports);
})({ __proto__: null });`;
if (!code.includes(wrapperStart) || !code.includes(wrapperEnd)) {
throw new Error("Rerun wasm-bindgen wrapper markers no longer match");
}
code = code.replace(wrapperStart, "").replace(wrapperEnd, "");
code = `
export default function() {
const exports = { __proto__: null };
${code}
function deinit() {
__wbg_init.__wbindgen_wasm_module = null;
wasmModule = null;
wasm = null;
cachedDataViewMemory0 = null;
cachedFloat32ArrayMemory0 = null;
cachedInt16ArrayMemory0 = null;
cachedInt32ArrayMemory0 = null;
cachedInt8ArrayMemory0 = null;
cachedUint16ArrayMemory0 = null;
cachedUint32ArrayMemory0 = null;
cachedUint8ArrayMemory0 = null;
}
return Object.assign(__wbg_init, { initSync, deinit }, exports);
}
`;
const closureDtorsOriginal = `const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(state => wasm.__wbindgen_destroy_closure(state.a, state.b));`;
const closureDtorsPatch = `const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(state => {
if (wasm) wasm.__wbindgen_destroy_closure(state.a, state.b);
});`;
if (!code.includes(closureDtorsOriginal)) {
throw new Error("Rerun CLOSURE_DTORS block no longer matches");
}
code = code.replace(closureDtorsOriginal, closureDtorsPatch);
const makeMutClosureOriginal = `function makeMutClosure(arg0, arg1, f) {
const state = { a: arg0, b: arg1, cnt: 1 };
const real = (...args) => {
// First up with a closure we increment the internal reference
// count. This ensures that the Rust closure environment won't
// be deallocated while we're invoking it.
state.cnt++;
const a = state.a;
state.a = 0;
try {
return f(a, state.b, ...args);
} finally {
state.a = a;
real._wbg_cb_unref();
}
};
real._wbg_cb_unref = () => {
if (--state.cnt === 0) {
wasm.__wbindgen_destroy_closure(state.a, state.b);
state.a = 0;
CLOSURE_DTORS.unregister(state);
}
};
CLOSURE_DTORS.register(real, state, state);
return real;
}`;
const makeMutClosurePatch = `function makeMutClosure(arg0, arg1, f) {
const state = { a: arg0, b: arg1, cnt: 1 };
const real = (...args) => {
state.cnt++;
const a = state.a;
state.a = 0;
try {
if (!wasm) return;
return f(a, state.b, ...args);
} finally {
state.a = a;
real._wbg_cb_unref();
}
};
real._wbg_cb_unref = () => {
if (--state.cnt === 0) {
if (wasm) wasm.__wbindgen_destroy_closure(state.a, state.b);
state.a = 0;
CLOSURE_DTORS.unregister(state);
}
};
CLOSURE_DTORS.register(real, state, state);
return real;
}`;
if (!code.includes(makeMutClosureOriginal)) {
throw new Error("Rerun makeMutClosure block no longer matches");
}
code = code.replace(makeMutClosureOriginal, makeMutClosurePatch);
writeFileSync(outputPath, code);
console.log(`Transformed Rerun wasm glue: ${sha256(code)} ${outputPath}`);

View File

@ -88,7 +88,22 @@ const paletteOptions: Array<{ value: PointPalette; label: string; description: s
{ value: "custom", label: "Свой цвет", description: "Один назначенный цвет" },
];
function toViewerSettings(settings: SceneSettings): ViewerSettings {
interface LivePerceptionLayers {
detections2d: boolean;
segmentation: boolean;
cuboids3d: boolean;
}
const defaultLivePerceptionLayers: LivePerceptionLayers = {
detections2d: false,
segmentation: false,
cuboids3d: false,
};
function toViewerSettings(
settings: SceneSettings,
perception: LivePerceptionLayers = defaultLivePerceptionLayers,
): ViewerSettings {
return {
point_size: settings.pointSize,
color_mode: settings.colorMode,
@ -98,6 +113,9 @@ function toViewerSettings(settings: SceneSettings): ViewerSettings {
show_points: settings.showPoints,
show_trajectory: settings.showTrajectory,
show_grid: settings.showGrid,
show_detections_2d: perception.detections2d,
show_segmentation: perception.segmentation,
show_cuboids_3d: perception.cuboids3d,
};
}
@ -149,6 +167,9 @@ export default function App() {
const [layoutSaveNotice, setLayoutSaveNotice] = useState<string | null>(null);
const [sceneSettings, setSceneSettings] = useState<SceneSettings>(defaultSceneSettings);
const [displayDraft, setDisplayDraft] = useState<SceneSettings>(defaultSceneSettings);
const [livePerceptionLayers, setLivePerceptionLayers] = useState<LivePerceptionLayers>(
defaultLivePerceptionLayers,
);
const sourceSwitchBlocked = isSpatialSourceSwitchBlocked(runtime.state);
const sourceSwitchBlockedReason = sourceSwitchBlocked
? SPATIAL_SOURCE_SWITCH_BLOCKED_REASON
@ -161,6 +182,8 @@ export default function App() {
const confirmedSceneSettingsRef = useRef<SceneSettings>(defaultSceneSettings);
const viewerSettingsCommitTimerRef = useRef<number | null>(null);
const runtimeUpdateViewerSettingsRef = useRef(runtime.updateViewerSettings);
const livePerceptionLayersRef = useRef<LivePerceptionLayers>(defaultLivePerceptionLayers);
const livePerceptionRevisionRef = useRef(0);
const replayActiveRef = useRef(false);
const sceneSettingsCommitterActiveRef = useRef(true);
const sceneSettingsCommitterRef = useRef<LatestAsyncCommitter<SceneSettings> | null>(null);
@ -197,7 +220,9 @@ export default function App() {
sceneSettingsCommitterRef.current = createLatestAsyncCommitter<SceneSettings>({
commit: (settings) => replayActiveRef.current
? Promise.resolve(true)
: runtimeUpdateViewerSettingsRef.current(toViewerSettings(settings)),
: runtimeUpdateViewerSettingsRef.current(
toViewerSettings(settings, livePerceptionLayersRef.current),
),
onSettled: ({ value, applied, superseded }) => {
if (!sceneSettingsCommitterActiveRef.current) return;
if (applied) confirmedSceneSettingsRef.current = value;
@ -275,6 +300,13 @@ export default function App() {
sceneSettingsCommitterRef.current?.isBusy()
) return;
const merged = mergeViewerSettings(sceneSettingsRef.current, remote);
const remoteLayers = {
detections2d: remote.show_detections_2d ?? false,
segmentation: remote.show_segmentation ?? false,
cuboids3d: remote.show_cuboids_3d ?? false,
};
livePerceptionLayersRef.current = remoteLayers;
setLivePerceptionLayers(remoteLayers);
sceneSettingsRef.current = merged;
confirmedSceneSettingsRef.current = merged;
setSceneSettings(merged);
@ -334,7 +366,9 @@ export default function App() {
return;
}
appliedProfileKeyRef.current = applicationKey;
void runtime.updateViewerSettings(toViewerSettings(profile.sceneSettings)).then((applied) => {
void runtime.updateViewerSettings(
toViewerSettings(profile.sceneSettings, livePerceptionLayersRef.current),
).then((applied) => {
if (!applied && appliedProfileKeyRef.current === applicationKey) {
appliedProfileKeyRef.current = null;
}
@ -441,6 +475,21 @@ export default function App() {
activateSceneWindow("layers");
};
const changeLivePerceptionLayers = useCallback((next: LivePerceptionLayers) => {
const previous = livePerceptionLayersRef.current;
const revision = ++livePerceptionRevisionRef.current;
livePerceptionLayersRef.current = next;
setLivePerceptionLayers(next);
if (replayActiveRef.current) return;
void runtimeUpdateViewerSettingsRef.current(
toViewerSettings(sceneSettingsRef.current, next),
).then((applied) => {
if (applied || livePerceptionRevisionRef.current !== revision) return;
livePerceptionLayersRef.current = previous;
setLivePerceptionLayers(previous);
});
}, []);
const beginRecordedReplaySwitch = useCallback(async () => {
if (sourceSwitchBlockedRef.current) {
throw new Error(SPATIAL_SOURCE_SWITCH_BLOCKED_REASON);
@ -706,6 +755,8 @@ export default function App() {
onAccumulationChange={(accumulationSeconds) =>
stageDisplayPatch({ accumulationSeconds })}
onAccumulationCommit={flushDisplaySettings}
livePerceptionLayers={livePerceptionLayers}
onLivePerceptionLayersChange={changeLivePerceptionLayers}
observationLayout={observationLayout}
spatialControls={selection?.SpatialControlsView
? {

View File

@ -72,6 +72,7 @@ export function FloatingObservationWindow({
rect,
maximized,
active,
hidden = false,
onRectChange,
onMaximizedChange,
onActivate,
@ -89,6 +90,7 @@ export function FloatingObservationWindow({
rect?: ObservationWindowRect;
maximized: boolean;
active: boolean;
hidden?: boolean;
onRectChange: (rect: ObservationWindowRect) => void;
onMaximizedChange: (maximized: boolean) => void;
onActivate: () => void;
@ -153,7 +155,7 @@ export function FloatingObservationWindow({
resizable={source.capabilities.resizable}
active={active}
zIndex={maximized ? 15 : active ? 9 : 7}
className="floating-observation-window"
className={`floating-observation-window${hidden ? " floating-observation-window--hidden" : ""}`}
onPointerDownCapture={(event) => {
if (!shouldCaptureWorkspacePointer(event.button, event.target as HTMLElement)) return;
try {

View File

@ -52,18 +52,17 @@ const preparationLabel: Record<ObservationPreparationPhase, string> = {
requesting: "Запрашиваем подготовку",
queued: "В очереди",
validating: "Проверяем запись",
exporting: "Готовим облако точек",
exporting: "Готовим операторскую сцену",
finalizing: "Завершаем подготовку",
failed: "Подготовка не выполнена",
cancelled: "Подготовка отменена",
};
function progressCopy(
phase: ObservationPreparationPhase,
progress: number | null,
): string {
const label = preparationLabel[phase];
return progress === null ? label : `${label} · ${Math.round(progress * 100)}%`;
function progressCopy(phase: ObservationPreparationPhase): string {
// Backend progress values are phase markers and heartbeat revisions, not a
// measured fraction of bytes or frames. Presenting 0.5 as "50%" made a
// healthy long export look stalled, especially after a browser reload.
return preparationLabel[phase];
}
function formatStartedAt(value: string): string {
@ -126,7 +125,7 @@ export function ObservationSessionSelect({
onReplaySettled,
});
const triggerCopy = sessions.replayProgress
? progressCopy(sessions.replayProgress.phase, sessions.replayProgress.progress)
? progressCopy(sessions.replayProgress.phase)
: sessions.state === "loading"
? "Загружаем сессии…"
: "Сохранённые сессии";

View File

@ -1,5 +1,3 @@
import { sha256 } from "@noble/hashes/sha2.js";
import { bytesToHex } from "@noble/hashes/utils.js";
import { useEffect, useMemo, useRef, useState } from "react";
import {
@ -11,7 +9,6 @@ import {
type ObservationSessionFetch,
} from "../core/observation/sessionArchive";
import {
MAX_RECORDED_MEDIA_SOURCE_BYTES,
type RecordedAdmissionPhase,
type RecordedCameraAdmissionState,
} from "../core/observation/recordedSessionAdmission";
@ -22,32 +19,11 @@ export interface RecordedObservationPlayback {
playing: boolean;
}
export interface RecordedMediaLoadProgress {
loadedBytes: number;
totalBytes: number;
loadedParts: number;
totalParts: number;
}
interface VerifiedRecordedMediaEpoch {
descriptor: ObservationRecordedMediaEpoch;
readonly init: ArrayBuffer;
readonly segments: readonly ArrayBuffer[];
}
export interface VerifiedRecordedMediaArchive {
export interface RecordedMediaArchive {
manifest: ObservationRecordedMediaManifest;
epochs: readonly VerifiedRecordedMediaEpoch[];
byteLength: number;
}
interface RecordedMediaBinaryDescriptor {
url: string;
byteLength: number;
sha256: string;
accept: string;
}
export type RecordedMediaPresentationState = "loading" | "ready" | "waiting" | "error";
export const RECORDED_MEDIA_DURATION_TOLERANCE_SECONDS = 1;
@ -134,205 +110,33 @@ function sourceContract(source: ObservationSourceDescriptor): ObservationRecorde
};
}
function expectedPayloadEtag(digest: string): string {
return `"sha256:${digest}"`;
}
export async function fetchVerifiedRecordedMediaBytes(
descriptor: RecordedMediaBinaryDescriptor,
{
signal,
fetcher = globalThis.fetch,
}: { signal?: AbortSignal; fetcher?: ObservationSessionFetch } = {},
): Promise<ArrayBuffer> {
let response: Response;
try {
response = await fetcher(descriptor.url, {
method: "GET",
headers: {
Accept: descriptor.accept,
"If-Match": expectedPayloadEtag(descriptor.sha256),
},
signal,
});
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") throw error;
throw new ObservationSessionContractError(
"Не удалось загрузить канонический фрагмент записанного видео.",
);
}
if (!response.ok || response.status !== 200) {
throw new ObservationSessionContractError(
`Фрагмент записанного видео вернул HTTP ${response.status}.`,
);
}
if (response.headers.get("ETag") !== expectedPayloadEtag(descriptor.sha256)) {
throw new ObservationSessionContractError(
"Фрагмент записанного видео не соответствует immutable manifest.",
);
}
const contentLength = response.headers.get("Content-Length");
if (
contentLength === null ||
!/^[1-9][0-9]*$/.test(contentLength) ||
Number(contentLength) !== descriptor.byteLength
) {
throw new ObservationSessionContractError(
"Длина фрагмента записанного видео не соответствует immutable manifest.",
);
}
const payload = await response.arrayBuffer();
if (payload.byteLength !== descriptor.byteLength) {
throw new ObservationSessionContractError(
"Фрагмент записанного видео был усечён во время передачи.",
);
}
const digest = bytesToHex(sha256(new Uint8Array(payload)));
if (digest !== descriptor.sha256) {
throw new ObservationSessionContractError(
"SHA-256 фрагмента записанного видео не совпадает с immutable manifest.",
);
}
return payload;
}
function manifestIdentity(manifest: ObservationRecordedMediaManifest): string {
return JSON.stringify(manifest);
}
export async function fetchVerifiedRecordedMediaArchive(
export async function fetchRecordedMediaArchive(
source: ObservationRecordedMediaSource,
{
signal,
fetcher = globalThis.fetch,
onProgress,
}: {
signal?: AbortSignal;
fetcher?: ObservationSessionFetch;
onProgress?: (progress: RecordedMediaLoadProgress) => void;
} = {},
): Promise<VerifiedRecordedMediaArchive> {
): Promise<RecordedMediaArchive> {
const manifest = await fetchObservationRecordedMediaManifest(source, {
signal,
fetcher,
expectedGenerationSha256: source.manifestGenerationSha256,
});
const totalBytes = manifest.epochs.reduce(
(archiveTotal, epoch) => archiveTotal + epoch.initByteLength + epoch.segments.reduce(
(epochTotal, segment) => epochTotal + segment.byteLength,
0,
),
0,
);
const totalParts = manifest.epochs.reduce(
(total, epoch) => total + 1 + epoch.segments.length,
0,
);
const totalBytes = manifest.epochs.reduce((total, epoch) => total + epoch.byteLength, 0);
if (
!Number.isSafeInteger(totalBytes) ||
totalBytes < 1 ||
totalBytes > MAX_RECORDED_MEDIA_SOURCE_BYTES ||
totalBytes !== manifest.byteLength ||
totalBytes !== source.byteLength
) {
throw new ObservationSessionContractError(
"Размер записанного медиаканала выходит за безопасный лимит браузера.",
"Размеры потоков записанного медиаканала не совпадают с manifest.",
);
}
let loadedBytes = 0;
let loadedParts = 0;
const publishProgress = () => onProgress?.({
loadedBytes,
totalBytes,
loadedParts,
totalParts,
});
publishProgress();
const epochs: VerifiedRecordedMediaEpoch[] = [];
for (const epoch of manifest.epochs) {
const init = await fetchVerifiedRecordedMediaBytes({
url: epoch.initUrl,
byteLength: epoch.initByteLength,
sha256: epoch.initSha256,
accept: "video/mp4",
}, { signal, fetcher });
loadedBytes += init.byteLength;
loadedParts += 1;
publishProgress();
const segments: ArrayBuffer[] = [];
for (const segment of epoch.segments) {
const payload = await fetchVerifiedRecordedMediaBytes({
url: segment.url,
byteLength: segment.byteLength,
sha256: segment.sha256,
accept: "video/iso.segment",
}, { signal, fetcher });
segments.push(payload);
loadedBytes += payload.byteLength;
loadedParts += 1;
publishProgress();
}
epochs.push({ descriptor: epoch, init, segments });
}
// Bind the complete byte set to the same manifest generation at both ends
// of the transfer. A replacement during a long camera download fails closed.
const confirmed = await fetchObservationRecordedMediaManifest(source, {
signal,
fetcher,
expectedGenerationSha256: manifest.generationSha256,
});
if (manifestIdentity(confirmed) !== manifestIdentity(manifest)) {
throw new ObservationSessionContractError(
"Manifest записанного видео изменился во время полной загрузки.",
);
}
if (loadedBytes !== source.byteLength) {
throw new ObservationSessionContractError(
"Полная загрузка камеры не совпала с launch byte_length.",
);
}
return { manifest, epochs, byteLength: loadedBytes };
}
export function appendRecordedMediaBuffer(
sourceBuffer: SourceBuffer,
payload: ArrayBuffer,
signal: AbortSignal,
): Promise<void> {
return new Promise((resolve, reject) => {
const onUpdateEnd = () => {
cleanup();
resolve();
};
const onError = () => {
cleanup();
reject(new Error("SourceBuffer rejected archived fMP4 data"));
};
const onAbort = () => {
cleanup();
reject(new DOMException("Aborted", "AbortError"));
};
const cleanup = () => {
sourceBuffer.removeEventListener("updateend", onUpdateEnd);
sourceBuffer.removeEventListener("error", onError);
signal.removeEventListener("abort", onAbort);
};
if (signal.aborted) {
onAbort();
return;
}
sourceBuffer.addEventListener("updateend", onUpdateEnd, { once: true });
sourceBuffer.addEventListener("error", onError, { once: true });
signal.addEventListener("abort", onAbort, { once: true });
try {
sourceBuffer.appendBuffer(payload);
} catch (error) {
cleanup();
reject(error);
}
});
return { manifest, byteLength: totalBytes };
}
function videoHasSeekableArchive(
@ -358,17 +162,22 @@ function waitForSeekableArchive(
if (videoHasSeekableArchive(video, declaredDurationSeconds)) return Promise.resolve();
return new Promise((resolve, reject) => {
const events = ["loadedmetadata", "durationchange", "progress", "canplay"] as const;
const timeout = globalThis.setTimeout(() => {
cleanup();
reject(new Error("Archived camera did not become seekable"));
}, 20_000);
let stallTimer: ReturnType<typeof globalThis.setTimeout> | undefined;
const armStallTimer = () => {
if (stallTimer !== undefined) globalThis.clearTimeout(stallTimer);
stallTimer = globalThis.setTimeout(() => {
cleanup();
reject(new Error("Archived camera stream stalled"));
}, 45_000);
};
const cleanup = () => {
globalThis.clearTimeout(timeout);
if (stallTimer !== undefined) globalThis.clearTimeout(stallTimer);
for (const event of events) video.removeEventListener(event, onProgress);
video.removeEventListener("error", onError);
signal.removeEventListener("abort", onAbort);
};
const onProgress = () => {
armStallTimer();
if (!videoHasSeekableArchive(video, declaredDurationSeconds)) return;
cleanup();
resolve();
@ -384,43 +193,30 @@ function waitForSeekableArchive(
for (const event of events) video.addEventListener(event, onProgress);
video.addEventListener("error", onError, { once: true });
signal.addEventListener("abort", onAbort, { once: true });
armStallTimer();
});
}
async function mountVerifiedRecordedEpoch(
async function mountRecordedEpochStream(
video: HTMLVideoElement,
epoch: VerifiedRecordedMediaEpoch,
descriptor: ObservationRecordedMediaEpoch,
signal: AbortSignal,
): Promise<() => void> {
const descriptor = epoch.descriptor;
if (!descriptor.mediaType.startsWith("video/mp4;") || !video.canPlayType(descriptor.mediaType)) {
throw new Error("Archived camera codec is not supported");
}
if (signal.aborted) throw new DOMException("Aborted", "AbortError");
// A sealed fragmented-MP4 generation is already complete and immutable.
// Present it as one Blob so Chromium can index and seek the whole archive
// without retaining the same 166+ MiB epoch in a quota-limited MSE
// SourceBuffer. Every constituent byte was fetched and SHA-256 verified
// before this boundary, and Blob preserves their canonical order.
const payload = new Blob([epoch.init, ...epoch.segments], {
type: descriptor.mediaType,
});
const expectedByteLength = epoch.init.byteLength + epoch.segments.reduce(
(total, segment) => total + segment.byteLength,
0,
);
if (payload.size !== expectedByteLength) {
throw new Error("Archived camera Blob is incomplete");
}
const objectUrl = URL.createObjectURL(payload);
// The generation token binds the native media request to the exact manifest.
// The browser range-streams the virtual init+fragment file from the server;
// no complete camera archive is copied into JavaScript memory.
const cleanup = () => {
video.pause();
video.removeAttribute("src");
video.load();
URL.revokeObjectURL(objectUrl);
};
video.src = objectUrl;
video.preload = "auto";
video.src = descriptor.streamUrl;
video.load();
try {
await waitForSeekableArchive(
@ -483,22 +279,15 @@ export function RecordedFmp4Player({
recordedDelivery?.timelineEndSeconds,
],
);
const [archive, setArchive] = useState<VerifiedRecordedMediaArchive | null>(null);
const [archive, setArchive] = useState<RecordedMediaArchive | null>(null);
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
const [readyGeneration, setReadyGeneration] = useState<string | null>(null);
const [progress, setProgress] = useState<RecordedMediaLoadProgress | null>(null);
const [bufferRevision, setBufferRevision] = useState(0);
const currentSeconds = playback?.currentSeconds ?? contract?.timelineStartSeconds ?? 0;
const epoch = useMemo(
() => selectRecordedMediaEpoch(archive?.manifest.epochs ?? [], currentSeconds),
[archive?.manifest.epochs, currentSeconds],
);
const verifiedEpoch = useMemo(
() => epoch
? archive?.epochs.find(({ descriptor }) => descriptor.ordinal === epoch.ordinal) ?? null
: null,
[archive?.epochs, epoch],
);
const waitingForEpoch = Boolean(archive && !epoch);
const selectedGeneration = contract && epoch
? `${contract.manifestGenerationSha256}:${epoch.ordinal}:${epoch.timelineStartSeconds}:${epoch.timelineEndSeconds}`
@ -514,7 +303,6 @@ export function RecordedFmp4Player({
useEffect(() => {
if (!contract) {
setArchive(null);
setProgress(null);
setReadyGeneration(null);
setState("error");
reportAdmission({
@ -527,7 +315,6 @@ export function RecordedFmp4Player({
if (!prepare) return;
const abort = new AbortController();
setArchive(null);
setProgress(null);
setReadyGeneration(null);
setState("loading");
reportAdmission({
@ -535,12 +322,7 @@ export function RecordedFmp4Player({
byteLength: contract.byteLength,
message: null,
});
void fetchVerifiedRecordedMediaArchive(contract, {
signal: abort.signal,
onProgress: (next) => {
if (!abort.signal.aborted) setProgress(next);
},
})
void fetchRecordedMediaArchive(contract, { signal: abort.signal })
.then((loaded) => {
if (abort.signal.aborted) return;
setArchive(loaded);
@ -566,11 +348,11 @@ export function RecordedFmp4Player({
const abort = new AbortController();
let disposed = false;
void (async () => {
for (const candidate of archive.epochs) {
for (const candidate of archive.manifest.epochs) {
const probe = document.createElement("video");
probe.muted = true;
probe.playsInline = true;
const cleanup = await mountVerifiedRecordedEpoch(probe, candidate, abort.signal);
const cleanup = await mountRecordedEpochStream(probe, candidate, abort.signal);
cleanup();
if (disposed || abort.signal.aborted) return;
}
@ -602,8 +384,8 @@ export function RecordedFmp4Player({
useEffect(() => {
const video = videoRef.current;
if (!video || !verifiedEpoch) return;
const epochDescriptor = verifiedEpoch.descriptor;
if (!video || !epoch) return;
const epochDescriptor = epoch;
const generation = contract
? `${contract.manifestGenerationSha256}:${epochDescriptor.ordinal}:${epochDescriptor.timelineStartSeconds}:${epochDescriptor.timelineEndSeconds}`
: null;
@ -615,7 +397,7 @@ export function RecordedFmp4Player({
const loadEpoch = async () => {
try {
cleanup = await mountVerifiedRecordedEpoch(video, verifiedEpoch, abort.signal);
cleanup = await mountRecordedEpochStream(video, epochDescriptor, abort.signal);
if (disposed || abort.signal.aborted) {
cleanup();
cleanup = null;
@ -648,7 +430,7 @@ export function RecordedFmp4Player({
abort.abort();
cleanup?.();
};
}, [archive?.byteLength, contract, verifiedEpoch]);
}, [archive?.byteLength, contract, epoch]);
useEffect(() => {
const video = videoRef.current;
@ -679,10 +461,6 @@ export function RecordedFmp4Player({
}
}, [archive?.byteLength, bufferRevision, currentSeconds, epoch, playback?.playing, visualState]);
const progressPercent = progress && progress.totalBytes > 0
? Math.min(100, Math.floor((progress.loadedBytes / progress.totalBytes) * 100))
: 0;
return (
<div
className="recorded-media-player"
@ -707,8 +485,8 @@ export function RecordedFmp4Player({
: visualState === "error"
? "Записанное видео недоступно"
: archive
? "Проверяем полную готовность записанного видео…"
: `Загружаем и проверяем записанное видео · ${progressPercent}%`}
? "Проверяем seek и codec записанного видео…"
: "Читаем manifest записанного видео…"}
</div>
) : null}
</div>

View File

@ -4,7 +4,14 @@ import type { SceneSettings } from "../sceneSettings";
import type { RecordedAdmissionPhase } from "../core/observation/recordedSessionAdmission";
export type RerunViewportStatus = "idle" | "loading" | "ready" | "error";
export type RecordedRerunView = "spatial" | "perception" | "metrics";
export type RecordedRerunView = "spatial" | "perception" | "perception3d" | "metrics";
export interface RecordedPerceptionLayers {
enabled: boolean;
detections2d: boolean;
segmentation: boolean;
cuboids3d: boolean;
}
export interface RerunSelection {
entityPath: string;
@ -57,6 +64,8 @@ export interface RerunViewportProps {
| "customColor"
>;
recordedView?: RecordedRerunView;
recordedViewResetGeneration?: 0 | 1;
recordedPerceptionLayers?: RecordedPerceptionLayers;
onPerceptionAvailabilityChange?: (available: boolean) => void;
}
@ -438,11 +447,20 @@ export async function fetchRecordedBlueprintRrd(
origin,
signal,
activeView = "spatial",
viewResetGeneration = 0,
perceptionLayers = {
enabled: false,
detections2d: false,
segmentation: false,
cuboids3d: false,
},
fetcher = globalThis.fetch,
}: {
origin: string;
signal?: AbortSignal;
activeView?: RecordedRerunView;
viewResetGeneration?: 0 | 1;
perceptionLayers?: RecordedPerceptionLayers;
fetcher?: typeof globalThis.fetch;
},
): Promise<Uint8Array> {
@ -461,7 +479,14 @@ export async function fetchRecordedBlueprintRrd(
settings.pointSize > 32 ||
!["turbo", "viridis", "plasma", "grayscale", "custom"].includes(settings.palette) ||
!/^#[0-9A-Fa-f]{6}$/.test(settings.customColor) ||
!["spatial", "perception", "metrics"].includes(activeView) ||
!["spatial", "perception", "perception3d", "metrics"].includes(activeView) ||
![0, 1].includes(viewResetGeneration) ||
[
perceptionLayers.enabled,
perceptionLayers.detections2d,
perceptionLayers.segmentation,
perceptionLayers.cuboids3d,
].some((value) => typeof value !== "boolean") ||
identity.applicationId !== "nodedc_mission_core_recorded" ||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(identity.recordingId)
) {
@ -485,6 +510,11 @@ export async function fetchRecordedBlueprintRrd(
palette: settings.palette,
custom_color: settings.customColor,
active_view: activeView,
view_reset_generation: viewResetGeneration,
unified_perception: perceptionLayers.enabled,
show_detections_2d: perceptionLayers.detections2d,
show_segmentation: perceptionLayers.segmentation,
show_cuboids_3d: perceptionLayers.cuboids3d,
}),
signal,
});
@ -588,6 +618,13 @@ export function RerunViewport({
onPlaybackControllerChange,
sceneSettings,
recordedView = "spatial",
recordedViewResetGeneration = 0,
recordedPerceptionLayers = {
enabled: false,
detections2d: false,
segmentation: false,
cuboids3d: false,
},
onPerceptionAvailabilityChange,
}: RerunViewportProps) {
const hostRef = useRef<HTMLDivElement>(null);
@ -596,6 +633,7 @@ export function RerunViewport({
const [retryNonce, setRetryNonce] = useState(0);
const blueprintChannelRef = useRef<RerunBlueprintChannel | null>(null);
const perceptionChannelRef = useRef<RerunBlueprintChannel | null>(null);
const loadedPerceptionChannelRef = useRef<RerunBlueprintChannel | null>(null);
const recordedIdentityRef = useRef<RecordedRerunIdentity | null>(null);
const presentationGateRef = useRef(presentationGate);
presentationGateRef.current = presentationGate;
@ -1123,11 +1161,11 @@ export function RerunViewport({
]);
useEffect(() => {
onPerceptionAvailabilityChange?.(false);
}, [onPerceptionAvailabilityChange, recordedPerceptionUrl]);
loadedPerceptionChannelRef.current = null;
}, [recordedPerceptionUrl]);
useEffect(() => {
if (!recordedPerceptionUrl) return;
if (!recordedPerceptionUrl || !recordedPerceptionLayers.enabled) return;
const active = perceptionChannelRef.current;
const identity = recordedIdentityRef.current;
if (
@ -1136,6 +1174,10 @@ export function RerunViewport({
active.endpointUrl !== recordedPerceptionUrl ||
!active.channel.ready
) return;
if (loadedPerceptionChannelRef.current === active) {
onPerceptionAvailabilityChange?.(true);
return;
}
const abort = new AbortController();
void fetchRecordedPerceptionRrd(recordedPerceptionUrl, identity, {
origin: window.location.origin,
@ -1154,6 +1196,7 @@ export function RerunViewport({
return;
}
active.channel.send_rrd(payload);
loadedPerceptionChannelRef.current = active;
onPerceptionAvailabilityChange?.(true);
}).catch(() => {
onPerceptionAvailabilityChange?.(false);
@ -1163,6 +1206,7 @@ export function RerunViewport({
}, [
onPerceptionAvailabilityChange,
perceptionChannelRevision,
recordedPerceptionLayers.enabled,
recordedPerceptionUrl,
]);
@ -1181,6 +1225,8 @@ export function RerunViewport({
origin: window.location.origin,
signal: abort.signal,
activeView: recordedView,
viewResetGeneration: recordedViewResetGeneration,
perceptionLayers: recordedPerceptionLayers,
}).then((payload) => {
if (
abort.signal.aborted ||
@ -1200,6 +1246,11 @@ export function RerunViewport({
blueprintChannelRevision,
recordedBlueprintUrl,
recordedView,
recordedViewResetGeneration,
recordedPerceptionLayers.enabled,
recordedPerceptionLayers.detections2d,
recordedPerceptionLayers.segmentation,
recordedPerceptionLayers.cuboids3d,
sceneSettings?.accumulationSeconds,
sceneSettings?.customColor,
sceneSettings?.palette,

View File

@ -38,47 +38,57 @@ export function recordedObservationSources(
spatialRegistration: "native",
},
};
const media = launch.mediaSources.map((source, index): ObservationSourceDescriptor => ({
id: source.id,
sourceId: source.id,
semanticChannelId: "camera.video.recorded",
label: source.label,
description: "Сохранённый видеоканал на общей временной шкале сессии",
modality: "video",
role: "auxiliary",
availability: "available",
transport: "recording",
endpointLabel: "Сохранённая сессия",
previewUrl: null,
delivery: {
id: `${launch.sessionId}:${source.id}`,
kind: "recorded-fmp4-manifest",
url: source.manifestUrl,
mediaType: source.mediaType,
manifestGenerationSha256: source.manifestGenerationSha256,
byteLength: source.byteLength,
timelineStartSeconds: source.timelineStartSeconds,
timelineEndSeconds: source.timelineEndSeconds,
},
activation: null,
provider: {
pluginId: "missioncore.session-archive",
pluginVersion: "1",
modelId: "recorded-media",
compatibilityProfileId: null,
},
binding: {},
capabilities: {
overlay: true,
fullscreen: true,
resizable: true,
defaultVisible: index < 2,
timelineMode: "recorded",
seekable: true,
sessionRecording: true,
clockId: "session_time",
spatialRegistration: "unresolved",
},
}));
const media = launch.mediaSources.map((source, index): ObservationSourceDescriptor => {
const perception = source.id.startsWith("recorded.perception.");
return {
id: source.id,
sourceId: source.id,
semanticChannelId: perception
? "camera.perception.panoptic.recorded"
: "camera.video.recorded",
label: source.label,
description: perception
? "Покадровая instance + semantic сегментация на общей временной шкале"
: "Сохранённый видеоканал на общей временной шкале сессии",
modality: "video",
role: "auxiliary",
availability: "available",
transport: "recording",
endpointLabel: "Сохранённая сессия",
previewUrl: null,
delivery: {
id: `${launch.sessionId}:${source.id}`,
kind: "recorded-fmp4-manifest",
url: source.manifestUrl,
mediaType: source.mediaType,
manifestGenerationSha256: source.manifestGenerationSha256,
byteLength: source.byteLength,
timelineStartSeconds: source.timelineStartSeconds,
timelineEndSeconds: source.timelineEndSeconds,
},
activation: null,
provider: {
pluginId: "missioncore.session-archive",
pluginVersion: "1",
modelId: perception ? "recorded-panoptic-perception" : "recorded-media",
compatibilityProfileId: null,
},
binding: {},
capabilities: {
overlay: true,
fullscreen: true,
resizable: true,
// AI presentation belongs to the unified Rerun composition. Keep the
// pre-rendered perception video as an explicit fallback instead of
// opening it as a second, independently controlled stream.
defaultVisible: !perception && index < 2,
timelineMode: "recorded",
seekable: true,
sessionRecording: true,
clockId: "session_time",
spatialRegistration: perception ? "calibrated" : "unresolved",
},
};
});
return [spatial, ...media];
}

View File

@ -1,12 +1,8 @@
// A device-agnostic frontend safety policy. Sixteen channels covers multi-rig
// vehicles while the independent byte/concurrency limits keep admission
// bounded. One real accepted archive is ~163 MiB for a single camera, so the
// old 128 MiB laboratory ceiling rejected a valid sealed generation before the
// first manifest request. OPFS-backed sealed generations remain the scaling
// path beyond this in-memory policy.
// Camera count and preparation concurrency remain device-agnostic scheduling
// policy. Recorded duration and aggregate bytes are deliberately not admission
// criteria: sealed media is presented through a generation-bound HTTP stream,
// so a one-, three- or ten-hour recording never has to fit in browser memory.
export const MAX_RECORDED_CAMERA_SOURCES = 16;
export const MAX_RECORDED_MEDIA_SOURCE_BYTES = 256 * 1024 * 1024;
export const MAX_RECORDED_SESSION_CAMERA_BYTES = 512 * 1024 * 1024;
export const MAX_CONCURRENT_RECORDED_CAMERA_PREPARATIONS = 1;
export type RecordedAdmissionPhase = "loading" | "ready" | "error";
@ -32,18 +28,12 @@ export function recordedCameraDescriptorPreflight(
): RecordedAdmissionPhase {
if (sources.length > MAX_RECORDED_CAMERA_SOURCES) return "error";
if (new Set(sources.map(({ id }) => id)).size !== sources.length) return "error";
let totalBytes = 0;
for (const source of sources) {
if (
!source.id ||
!Number.isSafeInteger(source.byteLength) ||
source.byteLength < 1 ||
source.byteLength > MAX_RECORDED_MEDIA_SOURCE_BYTES
source.byteLength < 1
) return "error";
totalBytes += source.byteLength;
if (!Number.isSafeInteger(totalBytes) || totalBytes > MAX_RECORDED_SESSION_CAMERA_BYTES) {
return "error";
}
}
return "ready";
}
@ -67,7 +57,6 @@ export function recordedSessionAdmissionPhase(
if (sourceIds.length > MAX_RECORDED_CAMERA_SOURCES || spatialPhase === "error") {
return "error";
}
let totalBytes = 0;
for (const sourceId of sourceIds) {
const camera = cameras[sourceId];
if (!camera || camera.phase === "error") return "error";
@ -75,13 +64,10 @@ export function recordedSessionAdmissionPhase(
if (camera.byteLength !== null) {
if (
!Number.isSafeInteger(camera.byteLength) ||
camera.byteLength < 1 ||
camera.byteLength > MAX_RECORDED_MEDIA_SOURCE_BYTES
camera.byteLength < 1
) return "error";
totalBytes += camera.byteLength;
}
}
if (totalBytes > MAX_RECORDED_SESSION_CAMERA_BYTES) return "error";
if (spatialPhase !== "ready") return "loading";
return sourceIds.every((sourceId) => cameras[sourceId]?.phase === "ready")
? "ready"

View File

@ -1,7 +1,5 @@
import {
MAX_RECORDED_CAMERA_SOURCES,
MAX_RECORDED_MEDIA_SOURCE_BYTES,
MAX_RECORDED_SESSION_CAMERA_BYTES,
} from "./recordedSessionAdmission";
export type ObservationSessionStatus =
@ -99,19 +97,8 @@ export interface ObservationRecordedMediaEpoch {
timelineStartSeconds: number;
timelineEndSeconds: number;
mediaType: string;
initUrl: string;
initByteLength: number;
initSha256: string;
segmentCount: number;
segmentUrlPrefix: string;
segments: readonly ObservationRecordedMediaSegment[];
}
export interface ObservationRecordedMediaSegment {
sequence: number;
url: string;
byteLength: number;
sha256: string;
streamUrl: string;
}
export interface ObservationRecordedMediaManifest {
@ -215,18 +202,8 @@ const RECORDED_MEDIA_EPOCH_KEYS = new Set([
"timeline_start_seconds",
"timeline_end_seconds",
"media_type",
"init_url",
"init_byte_length",
"init_sha256",
"segment_count",
"segment_url_prefix",
"segments",
]);
const RECORDED_MEDIA_SEGMENT_KEYS = new Set([
"sequence",
"url",
"byte_length",
"sha256",
"stream_url",
]);
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const SAFE_MODALITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
@ -234,10 +211,9 @@ const ISO_WITH_TIMEZONE = /^\d{4}-\d{2}-\d{2}T.+(?:Z|[+-]\d{2}:\d{2})$/;
const SHA256 = /^[a-f0-9]{64}$/;
const SAFE_RECORDING_URL = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9._%:-]+\/recording\.rrd$/;
const SAFE_PREPARATION_URL = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9._%:-]+\/recording-preparation$/;
const SAFE_MEDIA_MANIFEST_URL = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9._%:-]+\/media\/[A-Za-z0-9._%-]+\/manifest$/;
const SAFE_MEDIA_MANIFEST_URL = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9._%:-]+\/(?:media\/[A-Za-z0-9._%-]+|perception-media\/result-[a-f0-9]{64})\/manifest$/;
const SAFE_MEDIA_STREAM_URL = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9._%:-]+\/(?:media\/[A-Za-z0-9._%-]+\/epochs\/[1-9][0-9]*|perception-media\/result-[a-f0-9]{64})\/recording\.mp4\?generation=[a-f0-9]{64}$/;
const SAFE_MP4_MEDIA_TYPE = /^video\/mp4(?:; codecs="[A-Za-z0-9.,_-]+")?$/;
const MAX_RECORDED_INIT_BYTES = 8 * 1024 * 1024;
const MAX_RECORDED_SEGMENT_BYTES = 64 * 1024 * 1024;
export class ObservationSessionContractError extends Error {
constructor(message: string) {
@ -570,15 +546,6 @@ export function decodeObservationSessionReplay(
"Descriptor записи содержит повторяющиеся медиаканалы.",
);
}
const mediaByteLength = mediaSources.reduce((total, source) => total + source.byteLength, 0);
if (
!Number.isSafeInteger(mediaByteLength) ||
mediaByteLength > MAX_RECORDED_SESSION_CAMERA_BYTES
) {
throw new ObservationSessionContractError(
"Совокупный объём записанных медиаканалов превышает безопасный лимит браузера.",
);
}
return {
kind: "rerun-recording",
sessionId,
@ -709,7 +676,10 @@ function decodeRecordedMediaSource(
}
assertExactKeys(value, RECORDED_MEDIA_SOURCE_KEYS, `Медиаканал ${index}`);
const id = requireString(value.id, `media_sources[${index}].id`, 128);
if (!SAFE_ID.test(id) || !id.startsWith("recorded.camera.")) {
if (
!SAFE_ID.test(id) ||
(!id.startsWith("recorded.camera.") && !id.startsWith("recorded.perception."))
) {
throw new ObservationSessionContractError("Медиаканал содержит небезопасный opaque id.");
}
if (value.modality !== "video" || value.media_type !== "video/mp4") {
@ -728,10 +698,10 @@ function decodeRecordedMediaSource(
"Медиаканал не содержит immutable generation SHA-256.",
);
}
const sessionPrefix = `/api/v1/observation-sessions/${encodeURIComponent(sessionId)}/media/`;
const sessionBase = `/api/v1/observation-sessions/${encodeURIComponent(sessionId)}/`;
if (
!SAFE_MEDIA_MANIFEST_URL.test(manifestUrl) ||
!manifestUrl.startsWith(sessionPrefix) ||
!manifestUrl.startsWith(sessionBase) ||
manifestUrl.includes("..")
) {
throw new ObservationSessionContractError(
@ -765,7 +735,7 @@ function decodeRecordedMediaSource(
byteLength: requireFiniteNumber(
value.byte_length,
`media_sources[${index}].byte_length`,
{ minimum: 1, maximum: MAX_RECORDED_MEDIA_SOURCE_BYTES, integer: true },
{ minimum: 1, maximum: Number.MAX_SAFE_INTEGER, integer: true },
),
mediaType: "video/mp4",
timelineStartSeconds,
@ -784,22 +754,21 @@ export function decodeObservationRecordedMediaManifest(
}
assertExactKeys(payload, RECORDED_MEDIA_MANIFEST_KEYS, "Manifest записанного видео");
if (
payload.schema_version !== "missioncore.observation-recorded-media/v2" ||
payload.schema_version !== "missioncore.observation-recorded-media/v3" ||
payload.source_id !== source.id ||
typeof payload.generation_sha256 !== "string" ||
!SHA256.test(payload.generation_sha256) ||
payload.generation_sha256 !== source.manifestGenerationSha256 ||
payload.synchronization !== "host-arrival-best-effort" ||
!Array.isArray(payload.epochs) ||
payload.epochs.length < 1 ||
payload.epochs.length > 1_000
payload.epochs.length < 1
) {
throw new ObservationSessionContractError("Manifest записанного видео несовместим.");
}
const manifestBase = source.manifestUrl.slice(0, -"/manifest".length);
const manifestByteLength = requireFiniteNumber(payload.byte_length, "manifest.byte_length", {
minimum: 1,
maximum: MAX_RECORDED_MEDIA_SOURCE_BYTES,
maximum: Number.MAX_SAFE_INTEGER,
integer: true,
});
if (manifestByteLength !== source.byteLength) {
@ -833,7 +802,7 @@ export function decodeObservationRecordedMediaManifest(
assertExactKeys(entry, RECORDED_MEDIA_EPOCH_KEYS, `Codec epoch ${index}`);
const ordinal = requireFiniteNumber(entry.ordinal, `epochs[${index}].ordinal`, {
minimum: 1,
maximum: 1_000,
maximum: Number.MAX_SAFE_INTEGER,
integer: true,
});
if (ordinal !== index + 1) {
@ -860,87 +829,28 @@ export function decodeObservationRecordedMediaManifest(
if (!SAFE_MP4_MEDIA_TYPE.test(mediaType)) {
throw new ObservationSessionContractError("Codec epoch содержит небезопасный media type.");
}
const initUrl = requireString(entry.init_url, `epochs[${index}].init_url`, 512);
const segmentUrlPrefix = requireString(
entry.segment_url_prefix,
`epochs[${index}].segment_url_prefix`,
512,
const byteLength = requireFiniteNumber(
entry.byte_length,
`epochs[${index}].byte_length`,
{ minimum: 1, maximum: Number.MAX_SAFE_INTEGER, integer: true },
);
const expectedEpochBase = `${manifestBase}/epochs/${ordinal}`;
const streamUrl = requireString(entry.stream_url, `epochs[${index}].stream_url`, 768);
const expectedEpochBase = source.id.startsWith("recorded.perception.")
? manifestBase
: `${manifestBase}/epochs/${ordinal}`;
const expectedStreamUrl =
`${expectedEpochBase}/recording.mp4?generation=${payload.generation_sha256}`;
if (
initUrl !== `${expectedEpochBase}/init.mp4` ||
segmentUrlPrefix !== `${expectedEpochBase}/segments/` ||
initUrl.includes("..") ||
segmentUrlPrefix.includes("..")
!SAFE_MEDIA_STREAM_URL.test(streamUrl) ||
streamUrl !== expectedStreamUrl ||
streamUrl.includes("..")
) {
throw new ObservationSessionContractError("Codec epoch содержит небезопасный API URL.");
}
const initByteLength = requireFiniteNumber(
entry.init_byte_length,
`epochs[${index}].init_byte_length`,
{ minimum: 1, maximum: MAX_RECORDED_INIT_BYTES, integer: true },
);
if (typeof entry.init_sha256 !== "string" || !SHA256.test(entry.init_sha256)) {
throw new ObservationSessionContractError("Codec epoch не содержит SHA-256 init-сегмента.");
}
const segmentCount = requireFiniteNumber(
entry.segment_count,
`epochs[${index}].segment_count`,
{ minimum: 1, maximum: 500_000, integer: true },
);
if (!Array.isArray(entry.segments) || entry.segments.length !== segmentCount) {
declaredBytes += byteLength;
if (!Number.isSafeInteger(declaredBytes)) {
throw new ObservationSessionContractError(
"Codec epoch содержит неполный список канонических сегментов.",
);
}
const segments = entry.segments.map((segment, segmentIndex): ObservationRecordedMediaSegment => {
if (!isRecord(segment)) {
throw new ObservationSessionContractError(
`Сегмент epochs[${index}].segments[${segmentIndex}] должен быть объектом.`,
);
}
assertExactKeys(
segment,
RECORDED_MEDIA_SEGMENT_KEYS,
`Сегмент epochs[${index}].segments[${segmentIndex}]`,
);
const sequence = requireFiniteNumber(
segment.sequence,
`epochs[${index}].segments[${segmentIndex}].sequence`,
{ minimum: 1, maximum: 500_000, integer: true },
);
if (sequence !== segmentIndex + 1) {
throw new ObservationSessionContractError(
"Codec epoch содержит непоследовательный сегмент.",
);
}
const url = requireString(
segment.url,
`epochs[${index}].segments[${segmentIndex}].url`,
512,
);
if (url !== `${segmentUrlPrefix}${sequence}.m4s` || url.includes("..")) {
throw new ObservationSessionContractError(
"Codec epoch содержит небезопасный URL сегмента.",
);
}
const byteLength = requireFiniteNumber(
segment.byte_length,
`epochs[${index}].segments[${segmentIndex}].byte_length`,
{ minimum: 1, maximum: MAX_RECORDED_SEGMENT_BYTES, integer: true },
);
if (typeof segment.sha256 !== "string" || !SHA256.test(segment.sha256)) {
throw new ObservationSessionContractError(
"Codec epoch содержит сегмент без SHA-256.",
);
}
declaredBytes += byteLength;
return { sequence, url, byteLength, sha256: segment.sha256 };
});
declaredBytes += initByteLength;
if (declaredBytes > MAX_RECORDED_MEDIA_SOURCE_BYTES) {
throw new ObservationSessionContractError(
"Записанный медиаканал превышает безопасный лимит браузера.",
"Суммарный размер codec epoch выходит за точный числовой диапазон клиента.",
);
}
return {
@ -948,17 +858,13 @@ export function decodeObservationRecordedMediaManifest(
timelineStartSeconds,
timelineEndSeconds,
mediaType,
initUrl,
initByteLength,
initSha256: entry.init_sha256,
segmentCount,
segmentUrlPrefix,
segments,
byteLength,
streamUrl,
};
});
if (declaredBytes !== manifestByteLength) {
throw new ObservationSessionContractError(
"Сумма init- и media-сегментов не совпадает с размером immutable manifest.",
"Сумма codec epoch не совпадает с размером immutable manifest.",
);
}
if (

View File

@ -285,11 +285,16 @@ export async function resolveObservationSessionReplay(
sessionId: string,
options: ObservationPreparationPollingOptions,
): Promise<ObservationSessionReplayLaunch> {
const response = await withRequestTimeout(
options.signal,
Math.max(100, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS),
(signal) => replayObservationSession(sessionId, { signal, fetcher: options.fetcher }),
);
// The initial replay request may restore and integrity-check large, already
// published artifacts before it can return either a launch descriptor or a
// background-preparation handle. Its duration therefore scales with the
// recording package and must not be confused with a stalled status poll.
// Keep it cancellable by the owning UI attempt, but do not impose the short
// per-poll timeout used once the server has returned a preparation handle.
const response = await replayObservationSession(sessionId, {
signal: options.signal,
fetcher: options.fetcher,
});
if (response.kind === "ready") return response.launch;
if (!pendingPreparation(response.preparation)) {
options.onUpdate?.(response.preparation);

View File

@ -22,6 +22,9 @@ export interface ViewerSettings {
show_points: boolean;
show_trajectory: boolean;
show_grid: boolean;
show_detections_2d: boolean;
show_segmentation: boolean;
show_cuboids_3d: boolean;
}
export interface StreamMetrics {
@ -29,6 +32,10 @@ export interface StreamMetrics {
frameRateHz?: number | null;
pointCount?: number | null;
droppedPreviewFrames?: number | null;
aiLatencyMs?: number | null;
aiFrameRateHz?: number | null;
aiDroppedFrames?: number | null;
aiStaleMs?: number | null;
elapsedSeconds?: number | null;
routeDistanceMeters?: number | null;
speedMetersPerSecond?: number | null;

View File

@ -32,6 +32,25 @@
backdrop-filter: blur(18px);
}
.scene-navigation-hint {
position: absolute;
z-index: 11;
right: 0.85rem;
bottom: 4.9rem;
display: flex;
align-items: center;
gap: 0.75rem;
border: 1px solid rgb(255 255 255 / 0.07);
border-radius: 999px;
background: rgb(9 10 13 / 0.7);
color: var(--nodedc-text-muted);
padding: 0.42rem 0.65rem;
font-size: 0.52rem;
font-weight: 680;
pointer-events: none;
backdrop-filter: blur(14px);
}
.scene-source-picker__trigger {
position: relative;
}
@ -350,6 +369,11 @@ i[data-availability="error"] {
box-shadow: 0 1.5rem 4rem rgb(0 0 0 / 0.34);
}
.floating-observation-window--hidden {
visibility: hidden;
pointer-events: none;
}
.observation-timeline {
display: block;
min-width: 0;

View File

@ -25,6 +25,20 @@
gap: 0.45rem;
}
.spatial-toolbar__view-switch {
display: inline-flex;
align-items: center;
gap: 0.25rem;
border: 1px solid rgb(255 255 255 / 0.07);
border-radius: 999px;
background: rgb(255 255 255 / 0.025);
padding: 0.2rem;
}
.spatial-toolbar__view-switch .nodedc-button {
border-radius: 999px;
}
.spatial-viewport-shell {
position: relative;
min-width: 0;

View File

@ -45,7 +45,6 @@ import {
type RerunPlaybackState,
type RerunSelection,
type RerunViewportStatus,
type RecordedRerunView,
} from "../components/RerunViewport";
import {
capabilityStatusLabel,
@ -124,6 +123,16 @@ export interface WorkspaceRendererProps {
accumulationSeconds: number;
onAccumulationChange: (value: number) => void;
onAccumulationCommit: () => void;
livePerceptionLayers: {
detections2d: boolean;
segmentation: boolean;
cuboids3d: boolean;
};
onLivePerceptionLayersChange: (next: {
detections2d: boolean;
segmentation: boolean;
cuboids3d: boolean;
}) => void;
observationLayout: ObservationLayoutController;
navigation: WorkspaceNavigation;
spatialControls: {
@ -286,6 +295,8 @@ function SpatialWorkspace({
accumulationSeconds,
onAccumulationChange,
onAccumulationCommit,
livePerceptionLayers,
onLivePerceptionLayersChange,
observationLayout,
navigation,
spatialControls,
@ -295,8 +306,13 @@ function SpatialWorkspace({
const [selection, setSelection] = useState<RerunSelection | null>(null);
const [playbackState, setPlaybackState] = useState<RerunPlaybackState | null>(null);
const [playbackController, setPlaybackController] = useState<RerunPlaybackController | null>(null);
const [recordedRerunView, setRecordedRerunView] = useState<RecordedRerunView>("spatial");
const [perceptionAvailable, setPerceptionAvailable] = useState(false);
const [recordedViewResetGeneration, setRecordedViewResetGeneration] = useState<0 | 1>(0);
const [perceptionAvailability, setPerceptionAvailability] = useState<
"unknown" | "available" | "unavailable"
>("unknown");
const [showDetections2d, setShowDetections2d] = useState(false);
const [showSegmentation, setShowSegmentation] = useState(false);
const [showCuboids3d, setShowCuboids3d] = useState(false);
const recordedSource = state?.sourceMode === "replay" || /\.rrd(?:$|[?#])/i.test(sourceUrl);
const recordedSessionGate: RecordedAdmissionPhase = recordedSource
? recordedSessionAdmission?.phase ?? "loading"
@ -309,6 +325,8 @@ function SpatialWorkspace({
const latency = pipelineLatency(metrics);
const frameRate = finiteMetric(metrics?.frameRateHz);
const points = finiteMetric(metrics?.pointCount);
const aiLatency = finiteMetric(metrics?.aiLatencyMs);
const aiFrameRate = finiteMetric(metrics?.aiFrameRateHz);
const observationSources = state?.observationSources ?? [];
const pointCloudSource = observationSources.find((source) => source.modality === "point-cloud");
const pointCloudVisible = pointCloudSource
@ -317,13 +335,29 @@ function SpatialWorkspace({
const mediaSources = observationSources.filter(
(source) => source.capabilities.overlay && source.modality !== "point-cloud",
);
const recordedPerceptionSupported = recordedSource && perceptionAvailability !== "unavailable";
const recordedPerceptionEnabled = showDetections2d || showSegmentation || showCuboids3d;
const unifiedPerception = recordedPerceptionSupported && recordedPerceptionEnabled;
const livePerceptionAvailable = !recordedSource && streamActive;
const detections2dActive = recordedSource
? showDetections2d
: livePerceptionLayers.detections2d;
const segmentationActive = recordedSource
? showSegmentation
: livePerceptionLayers.segmentation;
const cuboids3dActive = recordedSource
? showCuboids3d
: livePerceptionLayers.cuboids3d;
const visibleMediaSources = mediaSources.filter((source) =>
observationLayout.visibleSourceIds.has(source.id),
observationLayout.visibleSourceIds.has(source.id) &&
!source.id.startsWith("recorded.perception."),
);
const presentedMediaSourceCount = unifiedPerception ? 0 : visibleMediaSources.length;
const pointCloudFocused = Boolean(
pointCloudSource && observationLayout.focusedSourceId === pointCloudSource.id,
);
const floatingSourceMaximized = Boolean(observationLayout.maximizedFloatingSourceId);
const floatingSourceMaximized = !unifiedPerception &&
Boolean(observationLayout.maximizedFloatingSourceId);
const timeline = state?.observationTimeline;
const viewportRef = useRef<HTMLDivElement>(null);
const intentionalSourceEnd = !recordedSource && [
@ -373,13 +407,20 @@ function SpatialWorkspace({
[],
);
const onPerceptionAvailabilityChange = useCallback((available: boolean) => {
setPerceptionAvailable(available);
if (!available) setRecordedRerunView("spatial");
setPerceptionAvailability(available ? "available" : "unavailable");
if (!available) {
setShowDetections2d(false);
setShowSegmentation(false);
setShowCuboids3d(false);
}
}, []);
useEffect(() => {
setPerceptionAvailable(false);
setRecordedRerunView("spatial");
setPerceptionAvailability("unknown");
setShowDetections2d(false);
setShowSegmentation(false);
setShowCuboids3d(false);
setRecordedViewResetGeneration(0);
}, [sourceUrl]);
useEffect(() => {
@ -389,8 +430,10 @@ function SpatialWorkspace({
setSelection(null);
setPlaybackState(null);
setPlaybackController(null);
setPerceptionAvailable(false);
setRecordedRerunView("spatial");
setPerceptionAvailability("unknown");
setShowDetections2d(false);
setShowSegmentation(false);
setShowCuboids3d(false);
}, [pointCloudVisible, sourceUrl]);
useEffect(() => {
@ -428,20 +471,74 @@ function SpatialWorkspace({
className="spatial-workspace"
data-focused={pointCloudFocused || floatingSourceMaximized ? "true" : undefined}
>
<div className="spatial-toolbar">
<div className="spatial-toolbar" data-viewer-controls="v1">
<div className="spatial-toolbar__mode">
<span className="section-eyebrow">СЦЕНА 3D · RERUN</span>
</div>
<div className="spatial-toolbar__actions">
{recordedSource && perceptionAvailable ? (
{recordedPerceptionSupported || livePerceptionAvailable ? (
<div className="spatial-toolbar__view-switch" role="group" aria-label="Слои распознавания сцены">
<Button
size="compact"
variant="primary"
icon={<Icon name="video" />}
aria-pressed="true"
disabled
>
Оригинал
</Button>
<Button
size="compact"
variant={detections2dActive ? "primary" : "secondary"}
icon={<Icon name="target" />}
aria-pressed={detections2dActive}
onClick={() => recordedSource
? setShowDetections2d((current) => !current)
: onLivePerceptionLayersChange({
...livePerceptionLayers,
detections2d: !livePerceptionLayers.detections2d,
})}
>
Объекты 2D
</Button>
<Button
size="compact"
variant={segmentationActive ? "primary" : "secondary"}
icon={<Icon name="image" />}
aria-pressed={segmentationActive}
onClick={() => recordedSource
? setShowSegmentation((current) => !current)
: onLivePerceptionLayersChange({
...livePerceptionLayers,
segmentation: !livePerceptionLayers.segmentation,
})}
>
Сегментация
</Button>
<Button
size="compact"
variant={cuboids3dActive ? "primary" : "secondary"}
icon={<Icon name="apps" />}
aria-pressed={cuboids3dActive}
onClick={() => recordedSource
? setShowCuboids3d((current) => !current)
: onLivePerceptionLayersChange({
...livePerceptionLayers,
cuboids3d: !livePerceptionLayers.cuboids3d,
})}
>
Кубы 3D
</Button>
</div>
) : null}
{recordedSource && presentedViewerStatus === "ready" ? (
<Button
size="compact"
variant={recordedRerunView === "perception" ? "primary" : "secondary"}
icon={<Icon name={recordedRerunView === "perception" ? "globe" : "image"} />}
onClick={() => setRecordedRerunView((current) =>
current === "perception" ? "spatial" : "perception")}
variant="secondary"
icon={<Icon name="refresh" />}
onClick={() => setRecordedViewResetGeneration((current) => current === 0 ? 1 : 0)}
>
{recordedRerunView === "perception" ? "Облако точек" : "Распознавание"}
Сброс вида
</Button>
) : null}
<Button size="compact" variant="secondary" icon={<Icon name="network" />} onClick={navigation.openSource}>
@ -476,7 +573,13 @@ function SpatialWorkspace({
? state?.observationTimeline?.range?.endSeconds
: undefined}
sceneSettings={sceneSettings}
recordedView={recordedRerunView}
recordedViewResetGeneration={recordedViewResetGeneration}
recordedPerceptionLayers={{
enabled: unifiedPerception,
detections2d: showDetections2d,
segmentation: showSegmentation,
cuboids3d: showCuboids3d,
}}
onPerceptionAvailabilityChange={onPerceptionAvailabilityChange}
onStatusChange={onStatusChange}
onSelectionChange={onSelectionChange}
@ -511,7 +614,9 @@ function SpatialWorkspace({
) : !floatingSourceMaximized ? (
<div className="scene-source-controls">
<ObservationSourcePicker
sources={observationSources}
sources={unifiedPerception
? observationSources.filter((source) => source.modality === "point-cloud")
: observationSources}
visibleSourceIds={observationLayout.visibleSourceIds}
pendingSourceIds={observationLayout.pendingSourceIds}
onToggle={observationLayout.toggleSource}
@ -555,6 +660,16 @@ function SpatialWorkspace({
<span>До публикации</span>
<strong>{formatNumber(latency)}<small> мс</small></strong>
</div>
{streamActive ? (
<div>
<span>AI</span>
<strong>
{aiLatency === null ? "—" : formatNumber(aiLatency)}
<small>{aiLatency === null ? "" : " мс"}</small>
{aiFrameRate === null ? null : <small> · {formatNumber(aiFrameRate)} Гц</small>}
</strong>
</div>
) : null}
</div>
{!pointCloudFocused && !floatingSourceMaximized && state?.sourceMode && state.sourceMode !== "idle" && !sourceUrl.trim() ? (
@ -575,10 +690,17 @@ function SpatialWorkspace({
</div>
) : null}
{presentedViewerStatus === "ready" && !floatingSourceMaximized ? (
<div className="scene-navigation-hint" aria-label="Навигация по 3D-сцене">
<span>Колесо · зум к курсору</span>
<span>WASD · свободный проход</span>
</div>
) : null}
{!pointCloudFocused && !floatingSourceMaximized && recordedPlaybackReady ? (
<ObservationTimeline
active={presentedViewerStatus === "ready"}
sourceCount={Math.max(1, 1 + visibleMediaSources.length)}
sourceCount={Math.max(1, (unifiedPerception ? 2 : 1) + presentedMediaSourceCount)}
mode={recordedSource && playbackState?.rangeNs
? "recorded"
: timeline?.mode}
@ -599,7 +721,7 @@ function SpatialWorkspace({
/>
) : null}
{!pointCloudFocused ? visibleMediaSources.map((source, index) => (
{visibleMediaSources.map((source, index) => (
<FloatingObservationWindow
key={source.id}
source={source}
@ -609,6 +731,7 @@ function SpatialWorkspace({
rect={observationLayout.windowRects[source.id]}
maximized={observationLayout.maximizedFloatingSourceId === source.id}
active={observationLayout.activeFloatingSourceId === source.id}
hidden={pointCloudFocused || unifiedPerception}
onRectChange={(rect) => observationLayout.setWindowRect(source.id, rect)}
onMaximizedChange={(maximized) =>
observationLayout.setFloatingMaximized(source.id, maximized)}
@ -627,12 +750,12 @@ function SpatialWorkspace({
void observationLayout.hideSource(source.id);
}}
/>
)) : null}
))}
{recordedSource ? (
<div className="recorded-session-preloaders" aria-hidden="true">
{mediaSources.filter((source) => (
source.delivery?.kind === "recorded-fmp4-manifest" &&
(pointCloudFocused || !observationLayout.visibleSourceIds.has(source.id)) &&
!observationLayout.visibleSourceIds.has(source.id) &&
shouldPrepareRecordedSource(source.id)
)).map((source) => (
<ObservationMedia
@ -657,7 +780,9 @@ function SpatialWorkspace({
<span><i data-state="ready" />Траектория</span>
<span><i data-state="ready" />Преобразования</span>
<span><i data-state="contract" />Камеры в 3D</span>
<span><i data-state={perceptionAvailable ? "ready" : "contract"} />Объекты / рамки</span>
<span><i data-state={detections2dActive ? "ready" : "contract"} />Объекты 2D</span>
<span><i data-state={segmentationActive ? "ready" : "contract"} />Сегментация</span>
<span><i data-state={cuboids3dActive ? "ready" : "contract"} />Кубы 3D</span>
<span><i data-state="contract" />Компоновка</span>
</div>
</div>

View File

@ -484,6 +484,31 @@ test("202 preparation polls through explicit phases and reveals launch only when
assert.deepEqual(calls.map((entry) => entry[2]), [null, preparationEtag, preparationEtag]);
});
test("initial replay restore is not mistaken for a stalled preparation poll", async () => {
const sessionId = "session-20260716T205632Z";
let requestSignal;
const launch = await resolveObservationSessionReplay(sessionId, {
signal: new AbortController().signal,
requestTimeoutMs: 100,
fetcher: async (_input, init) => {
requestSignal = init.signal;
await new Promise((resolve, reject) => {
const timer = setTimeout(resolve, 150);
init.signal.addEventListener("abort", () => {
clearTimeout(timer);
reject(new DOMException("cancelled", "AbortError"));
}, { once: true });
});
return new Response(JSON.stringify(replay({ session_id: sessionId })), {
status: 200,
headers: { "Content-Type": "application/json" },
});
},
});
assert.equal(requestSignal.aborted, false);
assert.equal(launch.sessionId, sessionId);
});
test("status-ready response must match the preparation ETag before launch is accepted", async () => {
const decoded = decodeObservationSessionPreparation(
preparation({ state: "finalizing", progress: 0.95 }),
@ -704,6 +729,29 @@ test("switching saved sessions aborts only local polling and never cancels share
assert.doesNotMatch(selectorSource, /cancelReplay|>\s*Отменить\s*</);
});
test("recording preparation never presents phase heartbeats as fake percentages", async () => {
const selectorSource = await readFile(
new URL("../src/components/ObservationSessionSelect.tsx", import.meta.url),
"utf8",
);
assert.match(selectorSource, /exporting:\s*["']Готовим операторскую сцену["']/);
assert.doesNotMatch(selectorSource, /Math\.round\(progress\s*\*\s*100\)/);
});
test("unified recorded AI view keeps the raw camera mounted but does not cover overlays", async () => {
const workspaceSource = await readFile(
new URL("../src/workspaces/Workspaces.tsx", import.meta.url),
"utf8",
);
assert.match(workspaceSource, /hidden=\{pointCloudFocused \|\| unifiedPerception\}/);
assert.match(
workspaceSource,
/presentedMediaSourceCount = unifiedPerception \? 0 : visibleMediaSources\.length/,
);
});
test("replay descriptor rejects path leaks, mismatched sessions and non-seekable data", () => {
assert.throws(
() => decodeObservationSessionReplay(replay({ source_url: "file:///private/session.rrd" })),
@ -776,7 +824,7 @@ test("replay decodes opaque recorded cameras and their same-origin fMP4 manifest
);
const manifest = decodeObservationRecordedMediaManifest({
schema_version: "missioncore.observation-recorded-media/v2",
schema_version: "missioncore.observation-recorded-media/v3",
source_id: source.id,
generation_sha256: "c".repeat(64),
byte_length: 3_266,
@ -788,21 +836,15 @@ test("replay decodes opaque recorded cameras and their same-origin fMP4 manifest
timeline_start_seconds: 0.25,
timeline_end_seconds: 20,
media_type: 'video/mp4; codecs="avc1.640028"',
init_url: source.manifest_url.replace("/manifest", "/epochs/1/init.mp4"),
init_byte_length: 128,
init_sha256: "a".repeat(64),
segment_count: 12,
segment_url_prefix: source.manifest_url.replace("/manifest", "/epochs/1/segments/"),
segments: Array.from({ length: 12 }, (_, index) => ({
sequence: index + 1,
url: source.manifest_url.replace("/manifest", `/epochs/1/segments/${index + 1}.m4s`),
byte_length: 256 + index,
sha256: "b".repeat(64),
})),
byte_length: 3_266,
stream_url: source.manifest_url.replace(
"/manifest",
`/epochs/1/recording.mp4?generation=${"c".repeat(64)}`,
),
}],
}, decoded.mediaSources[0]);
assert.equal(manifest.epochs[0].segmentCount, 12);
assert.equal(manifest.epochs[0].segments.length, 12);
assert.equal(manifest.epochs[0].byteLength, 3_266);
assert.match(manifest.epochs[0].streamUrl, /recording\.mp4\?generation=/);
assert.equal(manifest.epochs[0].timelineStartSeconds, 0.25);
assert.throws(
() => decodeObservationRecordedMediaManifest({
@ -821,7 +863,7 @@ test("replay decodes opaque recorded cameras and their same-origin fMP4 manifest
);
assert.throws(
() => decodeObservationRecordedMediaManifest({
schema_version: "missioncore.observation-recorded-media/v2",
schema_version: "missioncore.observation-recorded-media/v3",
source_id: source.id,
generation_sha256: "c".repeat(64),
byte_length: 3_267,
@ -834,6 +876,53 @@ test("replay decodes opaque recorded cameras and their same-origin fMP4 manifest
);
});
test("replay decodes a synchronized full-epoch perception video", () => {
const sessionId = "20260720T065719Z_viewer_live";
const resultId = `result-${"d".repeat(64)}`;
const generation = "e".repeat(64);
const source = {
id: "recorded.perception.right",
label: "Сегментация · камера right",
modality: "video",
manifest_url: `/api/v1/observation-sessions/${sessionId}/perception-media/${resultId}/manifest`,
manifest_generation_sha256: generation,
byte_length: 12_345_678,
media_type: "video/mp4",
timeline_start_seconds: 35.421857292,
timeline_end_seconds: 484.144857292,
seekable: true,
synchronization: "host-arrival-best-effort",
};
const decoded = decodeObservationSessionReplay(replay({
session_id: sessionId,
timeline_end_seconds: 500,
media_sources: [source],
}));
assert.equal(decoded.mediaSources[0].id, "recorded.perception.right");
assert.equal(decoded.mediaSources[0].manifestUrl, source.manifest_url);
const manifest = decodeObservationRecordedMediaManifest({
schema_version: "missioncore.observation-recorded-media/v3",
source_id: source.id,
generation_sha256: generation,
byte_length: source.byte_length,
timeline_start_seconds: source.timeline_start_seconds,
timeline_end_seconds: source.timeline_end_seconds,
synchronization: "host-arrival-best-effort",
epochs: [{
ordinal: 1,
timeline_start_seconds: source.timeline_start_seconds,
timeline_end_seconds: source.timeline_end_seconds,
media_type: 'video/mp4; codecs="avc1.640028"',
byte_length: source.byte_length,
stream_url: `/api/v1/observation-sessions/${sessionId}/perception-media/${resultId}/recording.mp4?generation=${generation}`,
}],
}, decoded.mediaSources[0]);
assert.equal(manifest.epochs.length, 1);
assert.equal(manifest.epochs[0].timelineStartSeconds, source.timeline_start_seconds);
assert.equal(manifest.epochs[0].timelineEndSeconds, source.timeline_end_seconds);
});
test("recorded camera contracts reject foreign origins, path escapes and unknown fields", () => {
const sessionId = "session-20260716T205632Z";
const base = {
@ -888,7 +977,7 @@ test("recorded camera contracts reject foreign origins, path escapes and unknown
}));
assert.throws(
() => decodeObservationRecordedMediaManifest({
schema_version: "missioncore.observation-recorded-media/v2",
schema_version: "missioncore.observation-recorded-media/v3",
source_id: base.id,
generation_sha256: "c".repeat(64),
byte_length: 384,
@ -900,17 +989,8 @@ test("recorded camera contracts reject foreign origins, path escapes and unknown
timeline_start_seconds: 0,
timeline_end_seconds: 20,
media_type: 'video/mp4; codecs="avc1.640028"',
init_url: "file:///private/init.mp4",
init_byte_length: 128,
init_sha256: "a".repeat(64),
segment_count: 1,
segment_url_prefix: "file:///private/segments/",
segments: [{
sequence: 1,
url: "file:///private/segments/1.m4s",
byte_length: 256,
sha256: "b".repeat(64),
}],
byte_length: 384,
stream_url: "file:///private/recording.mp4",
}],
}, decoded.mediaSources[0]),
/небезопасный API URL/,

View File

@ -269,7 +269,14 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
{
origin: "http://127.0.0.1:5174",
activeView: "perception",
activeView: "perception3d",
viewResetGeneration: 1,
perceptionLayers: {
enabled: true,
detections2d: true,
segmentation: false,
cuboids3d: true,
},
fetcher: async (input, init) => {
calls.push({ input: String(input), init, body: JSON.parse(String(init.body)) });
return new Response(payload, {
@ -292,7 +299,12 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
point_size: 4.5,
palette: "custom",
custom_color: "#35d7c1",
active_view: "perception",
active_view: "perception3d",
view_reset_generation: 1,
unified_perception: true,
show_detections_2d: true,
show_segmentation: false,
show_cuboids_3d: true,
});
await assert.rejects(
@ -665,7 +677,7 @@ test("camera delivery rejects literal and encoded endpoint or credential leaks",
"/api/preview?upstream=rtsp://camera.local/live",
"/api/preview?upstream=rtsp%3A%2F%2Fcamera.local%2Flive",
"/api/preview?upstream=rtsp%253A%252F%252Fcamera.local%252Flive",
"/api/preview?endpoint=192.168.68.52:8554",
"/api/preview?endpoint=192.0.2.52:8554",
"/api/preview?endpoint=192%2E168%2E68%2E52",
"/api/preview?password=not-for-the-browser",
"/api/preview?%70%61%73%73%77%6f%72%64=not-for-the-browser",

View File

@ -1,15 +1,13 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let fetchVerifiedRecordedMediaArchive;
let fetchRecordedMediaArchive;
let recordedMediaPresentationState;
let recordedMediaSeekableCoverage;
let appendRecordedMediaBuffer;
before(async () => {
server = await createServer({
@ -18,10 +16,9 @@ before(async () => {
server: { middlewareMode: true },
});
({
fetchVerifiedRecordedMediaArchive,
fetchRecordedMediaArchive,
recordedMediaPresentationState,
recordedMediaSeekableCoverage,
appendRecordedMediaBuffer,
} = await server.ssrLoadModule("/src/components/RecordedFmp4Player.tsx"));
});
@ -29,41 +26,28 @@ after(async () => {
await server?.close();
});
function digest(payload) {
return createHash("sha256").update(payload).digest("hex");
}
function fixture() {
function fixture({ byteLength = 36_000_000_000 } = {}) {
const manifestUrl = "/api/v1/observation-sessions/session-1/media/camera-1/manifest";
const init = Buffer.from("canonical-init");
const first = Buffer.from("canonical-first-fragment");
const second = Buffer.from("canonical-second-fragment");
const generation = "a".repeat(64);
const segmentPrefix = manifestUrl.replace("/manifest", "/epochs/1/segments/");
const streamUrl = manifestUrl.replace(
"/manifest",
`/epochs/1/recording.mp4?generation=${generation}`,
);
const manifest = {
schema_version: "missioncore.observation-recorded-media/v2",
schema_version: "missioncore.observation-recorded-media/v3",
source_id: "recorded.camera.camera-1",
generation_sha256: generation,
byte_length: init.byteLength + first.byteLength + second.byteLength,
byte_length: byteLength,
timeline_start_seconds: 0,
timeline_end_seconds: 20,
timeline_end_seconds: 36_000,
synchronization: "host-arrival-best-effort",
epochs: [{
ordinal: 1,
timeline_start_seconds: 0,
timeline_end_seconds: 20,
timeline_end_seconds: 36_000,
media_type: 'video/mp4; codecs="avc1.640028"',
init_url: manifestUrl.replace("/manifest", "/epochs/1/init.mp4"),
init_byte_length: init.byteLength,
init_sha256: digest(init),
segment_count: 2,
segment_url_prefix: segmentPrefix,
segments: [first, second].map((payload, index) => ({
sequence: index + 1,
url: `${segmentPrefix}${index + 1}.m4s`,
byte_length: payload.byteLength,
sha256: digest(payload),
})),
byte_length: byteLength,
stream_url: streamUrl,
}],
};
const source = {
@ -72,14 +56,14 @@ function fixture() {
modality: "video",
manifestUrl,
manifestGenerationSha256: generation,
byteLength: manifest.byte_length,
byteLength,
mediaType: "video/mp4",
timelineStartSeconds: 0,
timelineEndSeconds: 20,
timelineEndSeconds: 36_000,
seekable: true,
synchronization: "host-arrival-best-effort",
};
return { source, manifest, generation, init, first, second };
return { source, manifest, generation, streamUrl };
}
function jsonResponse(payload, generation) {
@ -92,145 +76,54 @@ function jsonResponse(payload, generation) {
});
}
function mediaResponse(payload, sha = digest(payload), length = payload.byteLength) {
return new Response(payload, {
status: 200,
headers: {
"Content-Length": String(length),
ETag: `"sha256:${sha}"`,
test("multi-hour camera admission fetches only its compact generation-bound manifest", async () => {
const { source, manifest, generation, streamUrl } = fixture();
const requested = [];
const archive = await fetchRecordedMediaArchive(source, {
fetcher: async (input, request = {}) => {
requested.push(String(input));
assert.equal(new Headers(request.headers).get("If-Match"), `"sha256:${generation}"`);
return jsonResponse(manifest, generation);
},
});
}
function deferred() {
let resolve;
let reject;
const promise = new Promise((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
async function flushAsync() {
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise((resolve) => setTimeout(resolve, 0));
}
test("recorded camera archive stays pending until every canonical byte is verified", async () => {
const { source, manifest, generation, init, first, second } = fixture();
const finalSegment = deferred();
const requested = [];
let manifestRequests = 0;
const fetcher = async (input, request = {}) => {
const url = String(input);
requested.push(url);
const headers = new Headers(request.headers);
if (url === source.manifestUrl) {
manifestRequests += 1;
assert.equal(headers.get("If-Match"), `"sha256:${generation}"`);
return jsonResponse(manifest, generation);
}
const epoch = manifest.epochs[0];
if (url === epoch.init_url) {
assert.equal(headers.get("If-Match"), `"sha256:${epoch.init_sha256}"`);
return mediaResponse(init);
}
if (url === epoch.segments[0].url) return mediaResponse(first);
if (url === epoch.segments[1].url) return finalSegment.promise;
throw new Error(`Unexpected URL ${url}`);
};
let settled = false;
const archivePromise = fetchVerifiedRecordedMediaArchive(source, { fetcher })
.then((archive) => {
settled = true;
return archive;
});
await flushAsync();
assert.equal(settled, false, "init and a partial segment prefix must not publish the camera");
assert.deepEqual(requested.slice(0, 4), [
source.manifestUrl,
manifest.epochs[0].init_url,
manifest.epochs[0].segments[0].url,
manifest.epochs[0].segments[1].url,
]);
finalSegment.resolve(mediaResponse(second));
const archive = await archivePromise;
assert.equal(manifestRequests, 2, "the immutable generation is revalidated after transfer");
assert.equal(archive.byteLength, init.byteLength + first.byteLength + second.byteLength);
assert.equal(archive.epochs[0].segments.length, 2);
assert.deepEqual(requested, [source.manifestUrl]);
assert.equal(archive.byteLength, 36_000_000_000);
assert.equal(archive.manifest.epochs[0].streamUrl, streamUrl);
});
test("first camera manifest request is launch-generation bound and rejects replacement", async () => {
test("first camera manifest request rejects a replaced generation", async () => {
const { source, manifest, generation } = fixture();
const replacementGeneration = "d".repeat(64);
let requests = 0;
const fetcher = async (input, request = {}) => {
requests += 1;
assert.equal(String(input), source.manifestUrl);
assert.equal(
new Headers(request.headers).get("If-Match"),
`"sha256:${generation}"`,
);
return jsonResponse(
{ ...manifest, generation_sha256: replacementGeneration },
replacementGeneration,
);
};
await assert.rejects(
fetchVerifiedRecordedMediaArchive(source, { fetcher }),
fetchRecordedMediaArchive(source, {
fetcher: async (_input, request = {}) => {
assert.equal(new Headers(request.headers).get("If-Match"), `"sha256:${generation}"`);
return jsonResponse(
{ ...manifest, generation_sha256: replacementGeneration },
replacementGeneration,
);
},
}),
/несовместим|заменён/,
);
assert.equal(requests, 1);
});
test("recorded camera archive fails closed on truncation despite plausible headers", async () => {
const { source, manifest, generation, init, first, second } = fixture();
const fetcher = async (input) => {
const url = String(input);
if (url === source.manifestUrl) return jsonResponse(manifest, generation);
const epoch = manifest.epochs[0];
if (url === epoch.init_url) return mediaResponse(init);
if (url === epoch.segments[0].url) return mediaResponse(first);
if (url === epoch.segments[1].url) {
return mediaResponse(second.subarray(0, second.byteLength - 1), digest(second), second.byteLength);
}
throw new Error(`Unexpected URL ${url}`);
test("camera manifest fails closed when epoch bytes do not match launch bytes", async () => {
const { source, manifest, generation } = fixture();
const mismatched = {
...manifest,
epochs: [{ ...manifest.epochs[0], byte_length: manifest.byte_length - 1 }],
};
await assert.rejects(
fetchVerifiedRecordedMediaArchive(source, { fetcher }),
/усечён/,
fetchRecordedMediaArchive(source, {
fetcher: async () => jsonResponse(mismatched, generation),
}),
/не совпадает/,
);
});
test("recorded camera archive fails closed when bytes are replaced under an old ETag", async () => {
const { source, manifest, generation, init, first, second } = fixture();
const replacement = Buffer.from(second.map((value) => value ^ 0xff));
assert.equal(replacement.byteLength, second.byteLength);
const fetcher = async (input) => {
const url = String(input);
if (url === source.manifestUrl) return jsonResponse(manifest, generation);
const epoch = manifest.epochs[0];
if (url === epoch.init_url) return mediaResponse(init);
if (url === epoch.segments[0].url) return mediaResponse(first);
if (url === epoch.segments[1].url) {
return mediaResponse(replacement, digest(second), second.byteLength);
}
throw new Error(`Unexpected URL ${url}`);
};
await assert.rejects(
fetchVerifiedRecordedMediaArchive(source, { fetcher }),
/SHA-256/,
);
});
test("camera presentation gate opens only for the completely appended selected epoch", () => {
test("camera presentation gate opens only for the seekable selected epoch", () => {
assert.equal(recordedMediaPresentationState("loading", null, "g1", false), "loading");
assert.equal(recordedMediaPresentationState("ready", null, "g1", false), "loading");
assert.equal(recordedMediaPresentationState("ready", "g2", "g1", false), "loading");
@ -251,40 +144,13 @@ test("decoded duration and seekable range cover the complete declared epoch", ()
assert.equal(recordedMediaSeekableCoverage(20, 20, 20, 1, 1.01), false);
});
test("aborting SourceBuffer append removes every temporary listener", async () => {
const listeners = new Map();
const sourceBuffer = {
addEventListener(type, listener) {
const bucket = listeners.get(type) ?? new Set();
bucket.add(listener);
listeners.set(type, bucket);
},
removeEventListener(type, listener) {
listeners.get(type)?.delete(listener);
},
appendBuffer() {},
};
const abort = new AbortController();
const pending = appendRecordedMediaBuffer(
sourceBuffer,
new ArrayBuffer(8),
abort.signal,
);
assert.equal(listeners.get("updateend")?.size, 1);
assert.equal(listeners.get("error")?.size, 1);
abort.abort();
await assert.rejects(pending, (error) => error?.name === "AbortError");
assert.equal(listeners.get("updateend")?.size, 0);
assert.equal(listeners.get("error")?.size, 0);
});
test("verified camera archives remain immutable for safe player remount", async () => {
test("recorded player range-streams and never builds a whole-video RAM Blob", async () => {
const source = await readFile(
new URL("../src/components/RecordedFmp4Player.tsx", import.meta.url),
"utf8",
);
assert.doesNotMatch(source, /\.init\s*=\s*new ArrayBuffer/);
assert.doesNotMatch(source, /\.segments\.length\s*=\s*0/);
assert.match(source, /video\.src\s*=\s*descriptor\.streamUrl/);
assert.doesNotMatch(source, /new Blob\(|response\.arrayBuffer\(|SourceBuffer/);
});
test("loading and error overlays fully conceal recorded camera pixels", async () => {
@ -301,3 +167,20 @@ test("loading and error overlays fully conceal recorded camera pixels", async ()
/\.recorded-media-player__notice\s*\{[^}]*inset:\s*0;[^}]*background:\s*#070809/s,
);
});
test("point-cloud fullscreen keeps the admitted recorded camera worker mounted", async () => {
const source = await readFile(
new URL("../src/workspaces/Workspaces.tsx", import.meta.url),
"utf8",
);
assert.match(source, /\{visibleMediaSources\.map\(\(source, index\) => \(/);
assert.match(source, /hidden=\{pointCloudFocused \|\| unifiedPerception\}/);
assert.doesNotMatch(
source,
/\{!pointCloudFocused \? visibleMediaSources\.map/,
);
assert.doesNotMatch(
source,
/\(pointCloudFocused \|\| !observationLayout\.visibleSourceIds\.has\(source\.id\)\)/,
);
});

View File

@ -78,34 +78,27 @@ test("any RRD or camera failure closes the complete recorded session", () => {
);
});
test("camera admission enforces independent 16/256/512 MiB limits before fetching", () => {
test("camera admission keeps source scheduling bounded but has no duration or byte ceiling", () => {
const {
MAX_RECORDED_CAMERA_SOURCES,
MAX_RECORDED_MEDIA_SOURCE_BYTES,
MAX_RECORDED_SESSION_CAMERA_BYTES,
recordedCameraDescriptorPreflight,
} = admission;
assert.equal(MAX_RECORDED_CAMERA_SOURCES, 16);
assert.equal(MAX_RECORDED_MEDIA_SOURCE_BYTES, 256 * 1024 * 1024);
assert.equal(MAX_RECORDED_SESSION_CAMERA_BYTES, 512 * 1024 * 1024);
assert.equal(recordedCameraDescriptorPreflight(
Array.from({ length: 2 }, (_, index) => ({
id: `camera.${index}`,
byteLength: MAX_RECORDED_MEDIA_SOURCE_BYTES,
byteLength: 36_000_000_000,
})),
), "ready");
assert.equal(recordedCameraDescriptorPreflight(
Array.from({ length: 17 }, (_, index) => ({ id: `camera.${index}`, byteLength: 1 })),
), "error");
assert.equal(recordedCameraDescriptorPreflight([
{ id: "camera.large", byteLength: MAX_RECORDED_MEDIA_SOURCE_BYTES + 1 },
{ id: "camera.ten-hours", byteLength: 36_000_000_000 },
]), "ready");
assert.equal(recordedCameraDescriptorPreflight([
{ id: "camera.invalid", byteLength: Number.MAX_SAFE_INTEGER + 1 },
]), "error");
assert.equal(recordedCameraDescriptorPreflight(
Array.from({ length: 5 }, (_, index) => ({
id: `camera.${index}`,
byteLength: 110 * 1024 * 1024,
})),
), "error");
assert.equal(recordedCameraDescriptorPreflight([
{ id: "camera.duplicate", byteLength: 1 },
{ id: "camera.duplicate", byteLength: 1 },

View File

@ -0,0 +1,67 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import test from "node:test";
import makeRerunRuntime from "../vendor/rerun-web-viewer-0.34.1/re_viewer.nodedc.js";
const root = resolve(import.meta.dirname, "..");
const packageRoot = resolve(root, "node_modules/@rerun-io/web-viewer");
const vendorRoot = resolve(root, "vendor/rerun-web-viewer-0.34.1");
const sha256 = (path) =>
createHash("sha256").update(readFileSync(path)).digest("hex");
test("NODE.DC Rerun runtime is the audited 0.34.1 zoom-to-cursor build", () => {
const manifest = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
assert.equal(manifest.version, "0.34.1");
const expectedWasm = "38d19bac06b7c3b8e549489469cf7c4a24f319ec953849e4656b5827a6c105bb";
const expectedGlue = "0f7b76c9f24cbd8437021b5d37499894aeadc586183e422ebc82ef556d7b8339";
assert.equal(sha256(resolve(vendorRoot, "re_viewer_bg.nodedc.wasm")), expectedWasm);
assert.equal(sha256(resolve(vendorRoot, "re_viewer.nodedc.js")), expectedGlue);
assert.equal(sha256(resolve(packageRoot, "re_viewer_bg.wasm")), expectedWasm);
assert.equal(sha256(resolve(packageRoot, "re_viewer.js")), expectedGlue);
});
test("custom JavaScript glue references only exports present in its paired WASM", () => {
const wasmPath = resolve(vendorRoot, "re_viewer_bg.nodedc.wasm");
const gluePath = resolve(vendorRoot, "re_viewer.nodedc.js");
const module = new WebAssembly.Module(readFileSync(wasmPath));
const exports = new Set(WebAssembly.Module.exports(module).map(({ name }) => name));
const imports = WebAssembly.Module.imports(module);
const glue = readFileSync(gluePath, "utf8");
const referencedExports = new Set(
[...glue.matchAll(/\bwasm\.([A-Za-z_$][\w$]*)/g)].map((match) => match[1]),
);
const missingExports = [...referencedExports].filter((name) => !exports.has(name));
assert.equal(imports.length, 927);
assert.equal(exports.size, 79);
assert.deepEqual(missingExports, []);
assert.match(glue, /export default function\(\)/);
assert.match(glue, /if \(!wasm\) return;/);
});
test("custom Rerun WASM initializes and grows its externref table", () => {
const runtime = makeRerunRuntime();
runtime.initSync({
module: readFileSync(resolve(vendorRoot, "re_viewer_bg.nodedc.wasm")),
});
assert.equal(typeof runtime.WebHandle, "function");
runtime.deinit();
});
test("source patch carries cursor pivot, minimum-radius handoff, and geometry tests", () => {
const patch = readFileSync(resolve(vendorRoot, "NODEDC_ZOOM_TO_CURSOR.patch"), "utf8");
assert.match(patch, /fn pointer_ray_direction/);
assert.match(patch, /fn zoom_orbit_towards_pointer/);
assert.match(patch, /near_limit_hands_excess_zoom_to_cursor_directed_dolly/);
assert.match(patch, /crossing_near_limit_preserves_unconsumed_scene_scaled_zoom/);
assert.match(patch, /remaining_zoom_factor\.ln\(\) \* self\.speed/);
assert.match(patch, /off_center_pointer_stays_on_the_same_view_ray/);
});

View File

@ -0,0 +1,68 @@
# NODE.DC Rerun web viewer 0.34.1
This directory contains the audited Mission Core camera-controller override for
`@rerun-io/web-viewer` 0.34.1. It changes only the native orbital zoom behavior:
- the pointer ray selects an anchor on the current focus plane;
- eye position and look target scale around that anchor, so the point under the
cursor remains under the cursor;
- after Rerun's `0.02 m` near-plane safety radius is reached, excess zoom becomes
a cursor-directed dolly scaled by the scene's navigation speed instead of
silently ignoring the wheel or moving by imperceptible millimeters;
- first-person movement, orbit rotation, panning, WASD and Rerun's zoom-out cap
are unchanged.
## Source identity
- Upstream: `rerun-io/rerun`
- Tag: `0.34.1`
- Commit: `4efb18f17f6f0e41985cda99a2bdcd012febc8d5`
- Patched file: `crates/viewer/re_view_spatial/src/eye.rs`
- Patch: `NODEDC_ZOOM_TO_CURSOR.patch`
- Rust: `1.92.0`
- Binaryen / `wasm-opt`: `117` (the version pinned by Rerun's `pixi.lock`)
- Build image: `rust:1.92-bookworm`
- Build image digest:
`sha256:e90e846de4124376164ddfbaab4b0774c7bdeef5e738866295e5a90a34a307a2`
- Build date: `2026-07-22` (`Europe/Moscow`)
## Reproduction
Apply the patch to the exact upstream commit, then run Rerun's own builder:
```sh
git apply NODEDC_ZOOM_TO_CURSOR.patch
cargo test -p re_view_spatial --lib eye::tests:: -- --nocapture
cargo run -p re_dev_tools -- build-web-viewer \
--release -g \
--target no-modules-base \
--no-default-features \
--features map_view \
-o rerun_js/web-viewer
```
The container also needs Binaryen `117` for Rerun's final `wasm-opt -O2` step.
Binaryen `108` from Debian 12 must not be used: it produced a module whose
`externref` table could not grow during initialization. The
generated `re_viewer.js` is transformed with
`scripts/transform-rerun-web-viewer-glue.mjs`, which mirrors Rerun 0.34.1's
`rerun_js/web-viewer/build-wasm.mjs` no-modules wrapper and teardown guards.
## Verified artifacts
| Artifact | SHA-256 |
| --- | --- |
| `re_viewer_bg.nodedc.wasm` | `38d19bac06b7c3b8e549489469cf7c4a24f319ec953849e4656b5827a6c105bb` |
| raw generated `re_viewer.js` | `cc196a93c5be972c801d46be4dc9934f7f042eb62941f0aa0678f1c8416c6874` |
| `re_viewer.nodedc.js` | `0f7b76c9f24cbd8437021b5d37499894aeadc586183e422ebc82ef556d7b8339` |
The focused Rust suite completed with `5 passed, 0 failed`, including the
orbit-to-dolly boundary case. A Node `initSync`
smoke test completed successfully, including the `externref` table-growth step.
The Mission Core Node suite also checks both artifact hashes and verifies that
every `wasm.*` reference in the JavaScript glue exists in the paired WASM
exports.
`scripts/patch-rerun-web-viewer.mjs` installs the pair after npm extracts the
official package. It accepts only the known published or NODE.DC hashes and
fails closed on any other package contents.

View File

@ -0,0 +1,230 @@
diff --git a/crates/viewer/re_view_spatial/src/eye.rs b/crates/viewer/re_view_spatial/src/eye.rs
index e59b311..3f33259 100644
--- a/crates/viewer/re_view_spatial/src/eye.rs
+++ b/crates/viewer/re_view_spatial/src/eye.rs
@@ -512,8 +512,85 @@ impl EyeController {
}
}
+ /// Returns the world-space ray under the pointer.
+ ///
+ /// Keeping this calculation local to the eye controller lets orbital zoom use the pointer
+ /// synchronously. GPU picking arrives a frame later and would make the zoom pivot visibly lag.
+ fn pointer_ray_direction(&self, rect: Rect, pointer: egui::Pos2) -> Option<Vec3> {
+ if !rect.contains(pointer) || rect.width() <= 0.0 || rect.height() <= 0.0 {
+ return None;
+ }
+
+ let fov_y = self.fov_y.unwrap_or(Eye::DEFAULT_FOV_Y);
+ let aspect_ratio = rect.width() / rect.height();
+ let focal_scale = (fov_y * 0.5).tan();
+ let x = (2.0 * (pointer.x - rect.left()) / rect.width() - 1.0)
+ * focal_scale
+ * aspect_ratio;
+ let y = (1.0 - 2.0 * (pointer.y - rect.top()) / rect.height()) * focal_scale;
+
+ (self.rotation() * vec3(x, y, -1.0)).try_normalize()
+ }
+
+ /// Zoom an orbital eye around the point under the pointer on the current focus plane.
+ ///
+ /// The position and look target are scaled around the same anchor. This preserves the
+ /// projected pointer position while retaining the existing orbit direction and controls.
+ /// Once the near-plane safety radius is reached, excess zoom becomes a cursor-directed dolly
+ /// measured in the scene's navigation speed instead of near-plane millimeters.
+ fn zoom_orbit_towards_pointer(
+ &mut self,
+ zoom_factor: f32,
+ max_radius: f32,
+ rect: Rect,
+ pointer: Option<egui::Pos2>,
+ ) {
+ let radius = self.radius();
+ if !radius.is_finite() || radius <= f32::MIN_POSITIVE {
+ return;
+ }
+
+ let requested_radius = radius / zoom_factor;
+ let new_radius = requested_radius.clamp(Self::MIN_ORBIT_DISTANCE, max_radius);
+ let scale = new_radius / radius;
+
+ let ray_direction = pointer.and_then(|pointer| self.pointer_ray_direction(rect, pointer));
+ if let Some(ray_direction) = ray_direction {
+ let forward = self.fwd();
+ let denominator = ray_direction.dot(forward);
+ if denominator > 1.0e-4 {
+ let anchor = self.pos + ray_direction * (radius / denominator);
+ self.pos = anchor + (self.pos - anchor) * scale;
+ self.look_target = anchor + (self.look_target - anchor) * scale;
+
+ if requested_radius < Self::MIN_ORBIT_DISTANCE {
+ // Shrinking the remaining 2 cm orbit radius consumes only part of this input.
+ // Hand the logarithmic remainder to the same scene-scaled speed used by WASD
+ // and first-person scroll. Basing this on the near-plane remainder itself made
+ // each wheel event move by millimeters and felt indistinguishable from a hard
+ // zoom limit on building- and map-scale recordings.
+ let orbit_zoom_factor = (radius / Self::MIN_ORBIT_DISTANCE).max(1.0);
+ let remaining_zoom_factor = zoom_factor / orbit_zoom_factor;
+ if remaining_zoom_factor > 1.0 && remaining_zoom_factor.is_finite() {
+ let dolly = remaining_zoom_factor.ln() * self.speed as f32;
+ self.pos += ray_direction * dolly;
+ self.look_target += ray_direction * dolly;
+ }
+ }
+ self.did_interact = true;
+ return;
+ }
+ }
+
+ // Pointer data can be absent for synthetic zoom events. Preserve Rerun's centered zoom
+ // behavior in that case instead of dropping the input.
+ self.pos = self.look_target - self.fwd() * new_radius;
+ self.did_interact = true;
+ }
+
/// Handle zoom/scroll input.
- fn handle_zoom(&mut self, egui_ctx: &egui::Context, scene_bounding_box: &macaw::BoundingBox) {
+ fn handle_zoom(&mut self, response: &egui::Response, scene_bounding_box: &macaw::BoundingBox) {
+ let egui_ctx = &response.ctx;
let zoom_factor = egui_ctx.input(|input| {
// egui's default horizontal_scroll_modifier is shift, which is also our speed-up modifier.
// This means that a user who wants to speed up scroll-to-zoom will generate a horizontal scroll delta.
@@ -528,22 +605,12 @@ impl EyeController {
match self.kind {
Eye3DKind::Orbital => {
- let radius = self.pos.distance(self.look_target);
-
// Cap zoom-out against the scene bounding box. If we're already past the cap
// (e.g. right after loading) use the current radius instead — no snap-back.
+ let radius = self.radius();
let max_radius = max_orbital_radius(scene_bounding_box).max(radius);
- let new_radius = (radius / zoom_factor).clamp(Self::MIN_ORBIT_DISTANCE, max_radius);
-
- // The user may be scrolling to move the camera closer, but are not realizing
- // the radius is now tiny.
- // TODO(emilk): inform the users somehow that scrolling won't help, and that they should use WSAD instead.
- // It might be tempting to start moving the camera here on scroll, but that would is bad for other reasons.
-
- if f32::MIN_POSITIVE < new_radius {
- self.pos = self.look_target - self.fwd() * new_radius;
- self.did_interact = true;
- }
+ let pointer = response.ctx.pointer_latest_pos();
+ self.zoom_orbit_towards_pointer(zoom_factor, max_radius, response.rect, pointer);
}
Eye3DKind::FirstPerson => {
// Move along the forward axis when zooming in first person mode.
@@ -687,7 +754,7 @@ impl EyeController {
self.handle_drag(response, drag_threshold);
if response.hovered() {
- self.handle_zoom(&response.ctx, scene_bounding_box);
+ self.handle_zoom(response, scene_bounding_box);
}
if response.has_focus() {
@@ -1249,3 +1316,103 @@ impl EyeState {
Ok(eye)
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn orbital_controller(pos: Vec3, look_target: Vec3) -> EyeController {
+ EyeController {
+ pos,
+ look_target,
+ kind: Eye3DKind::Orbital,
+ speed: 1.0,
+ eye_up: Vec3::Z,
+ fov_y: Some(Eye::DEFAULT_FOV_Y),
+ did_interact: false,
+ }
+ }
+
+ fn test_rect() -> Rect {
+ Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0))
+ }
+
+ fn assert_vec3_close(actual: Vec3, expected: Vec3) {
+ assert!(
+ actual.abs_diff_eq(expected, 1.0e-5),
+ "actual={actual:?}, expected={expected:?}"
+ );
+ }
+
+ #[test]
+ fn centered_pointer_keeps_the_orbit_target() {
+ let rect = test_rect();
+ let mut controller = orbital_controller(vec3(0.0, -10.0, 0.0), Vec3::ZERO);
+
+ controller.zoom_orbit_towards_pointer(2.0, 100.0, rect, Some(rect.center()));
+
+ assert_vec3_close(controller.pos, vec3(0.0, -5.0, 0.0));
+ assert_vec3_close(controller.look_target, Vec3::ZERO);
+ assert!(controller.did_interact);
+ }
+
+ #[test]
+ fn off_center_pointer_stays_on_the_same_view_ray() {
+ let rect = test_rect();
+ let pointer = egui::pos2(650.0, 240.0);
+ let mut controller = orbital_controller(vec3(0.0, -10.0, 0.0), Vec3::ZERO);
+ let original_ray = controller
+ .pointer_ray_direction(rect, pointer)
+ .expect("test pointer must produce a ray");
+ let forward = controller.fwd();
+ let anchor = controller.pos
+ + original_ray * (controller.radius() / original_ray.dot(forward));
+
+ controller.zoom_orbit_towards_pointer(2.0, 100.0, rect, Some(pointer));
+
+ assert!((controller.radius() - 5.0).abs() < 1.0e-5);
+ let ray_after = (anchor - controller.pos).normalize();
+ assert!(ray_after.dot(original_ray) > 0.99999);
+ }
+
+ #[test]
+ fn near_limit_hands_excess_zoom_to_cursor_directed_dolly() {
+ let rect = test_rect();
+ let radius = EyeController::MIN_ORBIT_DISTANCE;
+ let mut controller = orbital_controller(vec3(0.0, -radius, 0.0), Vec3::ZERO);
+ let old_pos = controller.pos;
+ let old_target = controller.look_target;
+
+ controller.zoom_orbit_towards_pointer(2.0, 100.0, rect, Some(rect.center()));
+
+ assert!((controller.radius() - radius).abs() < 1.0e-6);
+ assert_vec3_close(controller.pos, old_pos + Vec3::Y * 2.0_f32.ln());
+ assert_vec3_close(
+ controller.look_target,
+ old_target + Vec3::Y * 2.0_f32.ln(),
+ );
+ assert_vec3_close(controller.look_target - controller.pos, Vec3::Y * radius);
+ }
+
+ #[test]
+ fn crossing_near_limit_preserves_unconsumed_scene_scaled_zoom() {
+ let rect = test_rect();
+ let mut controller = orbital_controller(vec3(0.0, -0.03, 0.0), Vec3::ZERO);
+
+ controller.zoom_orbit_towards_pointer(3.0, 100.0, rect, Some(rect.center()));
+
+ assert!((controller.radius() - EyeController::MIN_ORBIT_DISTANCE).abs() < 1.0e-6);
+ assert_vec3_close(controller.pos, vec3(0.0, 2.0_f32.ln() - 0.02, 0.0));
+ assert_vec3_close(controller.look_target, vec3(0.0, 2.0_f32.ln(), 0.0));
+ }
+
+ #[test]
+ fn zoom_out_respects_scene_radius_cap() {
+ let rect = test_rect();
+ let mut controller = orbital_controller(vec3(0.0, -10.0, 0.0), Vec3::ZERO);
+
+ controller.zoom_orbit_towards_pointer(0.1, 25.0, rect, Some(rect.center()));
+
+ assert!((controller.radius() - 25.0).abs() < 1.0e-5);
+ }
+}

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -53,6 +53,13 @@ export default defineConfig(({ mode }) => {
host: "127.0.0.1",
port: 4173,
strictPort: true,
proxy: {
"/api": {
target: apiTarget,
changeOrigin: false,
ws: true,
},
},
},
};
});

View File

@ -3,7 +3,7 @@
This plan supersedes the app-dependent experiment order in the reference Bible.
Each gate produces evidence and an explicit GO, PAUSE or BLOCKED result.
## Current checkpoint — 2026-07-19
## Current checkpoint — 2026-07-20
| Stage | Result |
| --- | --- |
@ -11,7 +11,7 @@ Each gate produces evidence and an explicit GO, PAUSE or BLOCKED result.
| Gate 1 physical operation | GO — autonomous double-click start/stop verified |
| Stage 1 BLE discovery | GO — repeatable advertisement and GATT profile |
| Stage 2 BLE provisioning | GO — reviewed 99-byte profile, LAN association confirmed |
| Stage 2 local connection matrix | GO (implementation) — Bridge remains physically accepted; Direct Connect reuses the reviewed one-shot BLE frame; Quick Connect uses one CoreWLAN host association and no GATT write. Direct Connect and Mission Core Quick Connect still require separate physical acceptance |
| Stage 2 local connection matrix | GO for Bridge/direct-LAN; LAB-ONLY for Quick Connect — firmware review proved the K1 3.0.2 AP credential is firmware-constant and the SSID follows the `XGR-` identity/MAC rule. One prepared Mac completed AP-enable, AP-ready, CoreWLAN association and the normal control lifecycle. The successful host had been seeded from the reviewed firmware archive; a copied build on a clean host cannot acquire that material automatically. Automatic firmware download, iPhone extraction and hard-coding were rejected. Bridge remains the product path; Direct Connect remains physically pending |
| Stage 3 application session | GO — MQTT 3.1.1 on confirmed K1 TCP 1883 |
| Stage 4 artifacts/flows | GO — bounded capture, hashes and negative control |
| Stage 5 point cloud | GO — raw-LZ4 protobuf, 1,140 live frames decoded |

View File

@ -16,6 +16,35 @@ profile is not a generic XGRIDS protocol claim and must not be used for fuzzing.
- The application sends operator-supplied router credentials; it does not derive
them from the K1 and it does not require the password of the K1's own AP.
The 99-byte `7f01` profile is used when K1 joins an external network (Bridge or
controller hotspot). It is not the Quick Connect bootstrap. A live Mission Core
negative run on 2026-07-19 proved that host association alone cannot work while
the reviewed K1 remains in station mode: `7f02` still reported the existing LAN.
Re-review of the owner-supplied LixelGO QuickLink branch then recovered the
missing first step. After a real BLE connection, LixelGO sends one fixed
100-byte frame to the same `7f01` characteristic: bytes `0..98` are zero and
byte `99` is `0x01`. Its callback is the AP-launch result; only then does the
application passes that device record's `WiFiAP_SSID` and `WiFiAP_Password` to
the operating-system connector. Later analysis of the official K1 `3.0.2`
firmware recovered the upstream source: `lixel_nman` invokes the bundled
NetworkManager AP script, which assigns a firmware-constant WPA2 credential.
The persisted app fields are per-device records, but the reviewed firmware
material is not per-device. The reviewed app requests write-without-response.
Mission Core follows the live characteristic properties on macOS, where this K1
advertises write-with-response, and never retries either transport or command
automatically.
The browser/API carries no AP password. An optional laboratory importer authenticates the
exact official `3.0.2` archive, resolves the reviewed material without printing
it and installs one firmware-scoped credential source in the OS secure store.
Mission Core then binds a device-scoped host profile ID to the selected
BLE-advertised SSID. Before any device write, the macOS helper materializes that
profile entirely inside Keychain from the firmware-scoped source. The actual
secret never reaches the browser, API, argv, logs, manifests or evidence. The
bounded Python importer holds it only in a short-lived mutable buffer, sends it
through helper stdin and zeroizes the buffer.
Full UUIDs:
- service: `00007f00-0000-1000-8000-00805f9b34fb`;
@ -38,6 +67,59 @@ There is no checksum, nonce, token, certificate, signature, or separate commit
command in this production call path. The implementation must never print,
persist, or accept the password as a command-line argument.
## Quick Connect AP-enable frame
The separately reviewed Quick Connect device action writes exactly 100 bytes:
| Offset | Length | Meaning |
| --- | ---: | --- |
| 0 | 99 | zero |
| 99 | 1 | enable device AP (`0x01`) |
It contains no SSID, password, device identifier or user input. The reviewed
client maps response byte 51 of `7f02` to its AP-ready flag. Mission Core
therefore requires all three observations before host association: mode
`WIFI_AP`, address `192.168.56.1`, and non-zero byte 51. A failed or ambiguous
attempt is not automatically repeated.
The first controlled AP attempt was physically accepted: one write-with-response
completed, `7f02` changed to `WIFI_AP / 192.168.56.1`, and a later exact
CoreWLAN scan found the SSID matching the selected BLE device. The Mac remained
on its previous LAN only because the former host adapter looked up an invalid
global profile. That profile inference and its APK importer were withdrawn.
A later physical attempt established another boundary: `7f02` can continue to
report `WIFI_AP / 192.168.56.1` with byte 51 zero after the exact SSID is no
longer beaconing. Mode and address alone are therefore not an idempotency or
readiness signal. Like the reviewed LixelGO action, each new explicit Mission
Core Quick Connect intent emits one AP-enable frame even when the baseline mode
already says `WIFI_AP`, then polls for the byte-51 ready flag for at most 15
seconds. It never retries the device write automatically.
Static review of the original client also established a lifecycle requirement:
LixelGO keeps the same BLE manager connected after AP-ready and invokes native
Wi-Fi association from that live session. Mission Core now retains the same
`BleakClient` while CoreWLAN performs bounded exact-SSID discovery and one
association. The discovery window may contain multiple read-only scans; it does
not repeat the BLE write or Wi-Fi association.
The next physical attempt exposed a CoreBluetooth lifecycle boundary before any
write: K1 appeared in the explicit six-second BLE scan, but a second lookup by
its macOS UUID failed moments later. The UUID is a transport-local selector, not
a durable rediscovery contract. The runtime now retains the live `BLEDevice`
handle from the operator's scan and connects that exact selected handle in the
following network action. A fallback lookup remains only for non-UI callers
that did not perform discovery first.
The 2026-07-20 prepared-host acceptance installed the exact firmware provider,
found one expected K1 candidate, emitted one AP-enable write, observed AP-ready
and completed one CoreWLAN association without an iPhone or manual credential.
Mission Core admitted `192.168.56.1`; the operator disconnected afterward only
to restore the external chat route. Full redacted evidence and artifact hashes
are recorded in `docs/lab/004_K1_FW302_AP_CREDENTIAL_PROVIDER_20260720.redacted.md`.
This proves the mechanism on that Mac, not automatic credential acquisition by
a copied application on a clean host.
The Android application unequivocally requests a write without response and
negotiates MTU 120, making the 99-byte frame one ATT command. A live read-only
CoreBluetooth check reports MTU 256 and a maximum write-without-response size of
@ -53,11 +135,13 @@ mode. It never fragments or retries the payload automatically.
## Expected transition and evidence of acceptance
A completed GATT write only proves transport completion. It does not prove that
the K1 joined Wi-Fi. The application polls `7f02`; the observed response frame
contains a fixed-width mode slot, an address slot, and a status byte at offset
50. The current AP baseline reports mode `WIFI_AP` and address `192.168.56.1`.
the K1 joined Wi-Fi or began beaconing. The application polls `7f02`; the
observed response frame contains a fixed-width mode slot, an address slot, a
status byte at offset 50 and the AP-ready flag at offset 51. The stale AP
baseline reports `WIFI_AP / 192.168.56.1 / byte51=0`; the physically observed
ready transition reports the same mode/address with `byte51=1`.
For the controlled experiment, acceptance required at least one of:
For Bridge/Direct Connect, acceptance requires at least one of:
1. `7f02` reports a non-AP IPv4 address;
2. the same address appears as a new router/ARP client after the write;
@ -66,10 +150,35 @@ For the controlled experiment, acceptance required at least one of:
Do not infer success from a write callback alone.
The Bridge/Direct Connect address is a DHCP lease, not configuration and not
device identity. Mission Core re-reads `7f02` without writing before every new
LAN control session, implicit-host acquisition and factory-calibration read.
If the value changes, it rotates `device_session_id`; it never retargets an
active acquisition. A correlated MQTT `DeviceInfo` response supplies the live
model/firmware/serial identity barrier.
The 2026-07-20 reboot/power-cycle check observed the startup race directly:
one read returned the earlier `.54` lease while that exact address had no ARP or
application endpoint; a later read returned `.52`, where exact probes found
MQTT 1883 and RTSP 8554. No subnet scan, route change, network-profile change or
VPN action was used. This is why no owner-LAN IPv4 value is a product constant
and why a BLE lease observation alone is not reported as live DeviceInfo.
For Quick Connect, host association is not admitted until the canonical
byte-51 ready flag is observed. CoreWLAN then searches only for the exact
device-profile SSID for at most 15 seconds and performs at most one association.
## Safety, recovery and stop conditions
- Perform one write per explicitly named attempt, using credentials for the LAN
already used by the Mac. Never retry automatically.
- Perform one write per explicitly named attempt. Never retry automatically.
- For Bridge/Direct Connect, use credentials for the operator-selected network.
- For Quick Connect, the exact `3.0.2` provider must already exist in the OS
secure store. Missing or mismatched firmware material fails before the AP
write. Never extrapolate this provider to another firmware or model.
- The macOS adapter materializes a device-scoped Keychain item from the exact
firmware source, then performs one association. Standard Wi-Fi Keychain and
native prompt paths remain compatibility fallbacks, not the reviewed
zero-touch path. It never asks the browser for a password.
- Do not alter Deco settings, scan the subnet, or guess any credential.
- If the status does not change, do not retry automatically.
- If the supplied credentials are wrong, reconnect over BLE and overwrite them

View File

@ -15,6 +15,9 @@ fully usable with only their available modalities.
1. Enter the required project name and start a normal acquisition from **Парк →
Локальное устройство**. The NFKC-normalized/trimmed value is local display
metadata, not a filesystem path, and becomes the saved-session catalog name.
A normal acquisition has no duration deadline and runs until the operator
explicitly stops it. Compatibility clients may still request a positive
finite duration, without an application-level maximum.
2. Open **Наблюдение → Пространственная сцена**. Live point cloud, trajectory
and the selected camera remain live-only while acquisition is running.
3. Stop acquisition normally, or allow the local service to recover an
@ -165,7 +168,8 @@ a separately attested storage root:
export MISSIONCORE_DATA_DIR=/absolute/private/path/mission-core
# Full K1 point-plus-camera evidence must currently remain below the checkout.
export MISSIONCORE_EVIDENCE_DIR=/absolute/path/to/NODEDC_MISSION_CORE/.runtime/mission-core/evidence/sessions
export MISSIONCORE_RRD_CACHE_MAX_BYTES=8589934592
# Optional operator retention quota; unset means no application byte quota.
# export MISSIONCORE_RRD_CACHE_MAX_BYTES=8589934592
export MISSIONCORE_RRD_FREE_SPACE_RESERVE_BYTES=2147483648
uv run k1link serve
```
@ -226,13 +230,15 @@ keeps the current device scan generation and does not accumulate an earlier
distance after the device resets scan time and route distance. These values are
not reconstructed from pose integration or a browser timer.
Expensive cache misses run through the bounded preparation worker and one global
cross-process export gate to cap peak RAM, CPU and temporary-disk use. Crash
leftovers from candidates, exporter temporary files and staged replay prefixes
are scavenged under that lock before quota accounting. Ready cache hits and
active response leases do not wait behind that gate. The derived cache has an
8 GiB default quota, preserves a 2 GiB default filesystem reserve and evicts
least-recently-used RRDs only; it never deletes native evidence.
Expensive cache misses run through the single-worker preparation queue and one
global cross-process export gate to cap concurrent RAM, CPU and temporary-disk
use. Crash leftovers from candidates, exporter temporary files and staged replay
prefixes are scavenged under that lock before capacity accounting. Ready cache
hits and active response leases do not wait behind that gate. The derived cache
has no application byte quota by default, so a single multi-hour RRD is not
rejected at 8 GiB. It still preserves a 2 GiB default filesystem reserve. An
operator may set `MISSIONCORE_RRD_CACHE_MAX_BYTES` to enable LRU eviction of
derived RRDs only; native evidence is never deleted.
## Capture-clock envelope
@ -313,7 +319,9 @@ time are not proven to use a shared device clock. Do not infer frame-accurate
calibration from the playback timeline.
Finalized media is prepared once by the same process-owned background job that
materializes the RRD. The gateway records host time only after a complete
materializes the RRD. JSONL indexes are read incrementally and no total index,
segment-count or archive-byte ceiling is used. The gateway records host time
only after a complete
`moof+mdat` fragment has arrived, so that timestamp is an availability/end
anchor, never a fragment-start timestamp. Preparation reads and SHA-verifies
every fragment, parses bounded ISO-BMFF timing tables (`mdhd`, `trex`, `tfhd`,
@ -332,7 +340,7 @@ coverage can be navigated. A missing, ambiguous, oversized or otherwise
unparseable timing table fails preparation; the archive remains evidence but is
never advertised as seekable media.
The prepared path-free v2 descriptor and full source stat identity (native raw
The durable path-free v2 descriptor and full source stat identity (native raw
and timing metadata plus every camera summary, index, init and segment file) are
written under the private derived cache with a schema, generation and checksum.
Publication uses a private temporary file, file and directory `fsync`, and atomic
@ -379,8 +387,9 @@ camera acceptance run retained point/pose evidence only.
provisional `transport` generation, but discovery will not use either to
advertise camera coverage. A mismatched origin/envelope/summary fails closed.
- A native capture with a missing final summary is accepted only when the raw
and metadata prefix is bounded, aligned and structurally valid. It is cataloged
as `interrupted`, never silently promoted to `ready`.
and metadata prefix is aligned and structurally valid. Recovery streams the
metadata JSONL one row at a time with no total-byte or message-count ceiling;
it is cataloged as `interrupted`, never silently promoted to `ready`.
- A non-newline metadata crash tail can be ignored. Newline-terminated or
mid-file corruption fails closed.
- Camera recovery retains a contiguous valid segment prefix, quarantines
@ -426,6 +435,7 @@ POST /api/v1/observation-sessions/{id}/blueprint.rrd
GET /api/v1/observation-sessions/{id}/media/{artifact}/manifest
GET /api/v1/observation-sessions/{id}/media/{artifact}/epochs/{n}/init.mp4
GET /api/v1/observation-sessions/{id}/media/{artifact}/epochs/{n}/segments/{m}.m4s
GET /api/v1/observation-sessions/{id}/media/{artifact}/epochs/{n}/recording.mp4?generation=<sha256>
GET /api/v1/workspace-layouts/observation.spatial
PUT /api/v1/workspace-layouts/observation.spatial
@ -461,40 +471,40 @@ revision in `If-Match`; stale writers receive HTTP 412 rather than overwriting
another saved profile.
Recorded media routes expose only opaque catalog identifiers and ordinal codec
epochs. The required `missioncore.observation-recorded-media/v2` manifest carries
a strong `generation_sha256`, exact JS-safe aggregate `byte_length`, and finite
epochs. The public compact
`missioncore.observation-recorded-media/v3` manifest carries a strong
`generation_sha256`, exact JS-safe aggregate `byte_length`, and finite
`timeline_start_seconds` / `timeline_end_seconds` for every epoch. Epoch ends
participate in the generation digest. Every epoch also declares its init byte
length/digest and a complete contiguous segment list with sequence, URL, byte
length and digest. The launch source repeats the same aggregate `byte_length`
and uses exactly `max(epoch.timeline_end_seconds)` as its end; the spatial RRD
end must never pad camera coverage. The browser cross-checks launch, manifest and
component lengths. The current browser laboratory policy accepts at most 16
camera sources, 128 MiB per source and 512 MiB across the session. This launch
preflight finishes before the first camera manifest/init/segment GET; admitted
cameras are then downloaded and decode-probed one at a time. Verified raw
buffers remain immutable so route changes and player remounts cannot append
emptied data. This bounded in-memory strategy is deliberate for the laboratory
milestone; an OPFS-backed sealed-generation cache is the next scaling step for
larger rigs and must preserve the same launch/manifest/hash admission contract.
participate in the generation digest. Each epoch declares one generation-bound
`stream_url`, media type and aggregate byte length; thousands of internal
segment rows never enter browser memory. The launch source repeats the same
aggregate `byte_length` and uses exactly `max(epoch.timeline_end_seconds)` as
its end; the spatial RRD end must never pad camera coverage. The browser
cross-checks launch and manifest identity, but applies no duration, per-source
byte or aggregate-session byte admission ceiling.
The manifest response ETag is the exact generation, and the manifest GET itself
requires that generation in `If-Match`. Init and media GETs likewise require the
descriptor's exact SHA `If-Match`, then open through a confined directory file
descriptor with no symlink following, verify the declared SHA-256 and serve
exact-length bytes with immutable private `no-transform` caching and byte-range
support. Physical source ids, RTSP addresses and storage paths never cross the
API boundary.
The `<video>` element reads the immutable virtual fMP4 through native HTTP Range
requests. The server maps each requested interval onto init/segment files,
opens them through confined descriptors with no symlink following and verifies
the digest of each touched component. It never assembles the full video in
backend or JavaScript memory. Responses carry exact `Content-Length` /
`Content-Range`, a generation-and-epoch ETag, `Accept-Ranges: bytes` and private
immutable `no-transform` caching. The manifest ETag is the exact generation and
its GET requires the matching `If-Match`; the stream URL binds that same
generation in its query. The older init/segment routes remain internal
compatibility surfaces. Physical source ids, RTSP addresses and storage paths
never cross the API boundary.
## Current limit
## Current synchronization boundary
The spatial RRD, trajectory, archived device-metric series and archived fMP4
cameras use the same operator scrubber now. Camera epochs are aligned to
zero-based `session_time` from the shared host-arrival monotonic clock and
rendered through MSE. The metric tab carries device-reported route distance,
rendered through the browser's native fMP4/Range pipeline. The metric tab carries
device-reported route distance,
speed and scan time at their `ModelingReport` receive times. The client selects
a camera epoch only inside its declared inclusive interval and verifies that the
decoded MSE seekable duration covers that interval. This remains best-effort
decoded native-media seekable duration covers that interval. This remains best-effort
correlation: codec PTS, K1 sensor exposure time and LiDAR firing time are not
proven to share a device clock. A codec epoch whose init segment does not expose
a browser-supported codec or whose timing cannot be proven remains retained

View File

@ -61,6 +61,306 @@ The result manifest binds the detection artifact's length and SHA-256. A repeat
with the same identity validates the existing directory and returns it without
calling Triton.
## Full-epoch panoptic result v2
`missioncore.recorded-perception-result/v2` is the complete recorded-camera
profile. It is separate from the YOLOX detector proof and processes every
admitted frame without sampling. Its immutable identity binds the complete
compute job, factory-calibration generation and camera slot, exact model
revisions/weight files, thresholds, alpha values, runner SHA-256 and publication
encoder.
The current research configuration produces:
- TorchVision Mask R-CNN ResNet50-FPN v2 instance masks and COCO labels;
- Microsoft BEiT ADE20K-150 semantic masks at the original 800x600 frame size;
- a seekable H.264 MP4 with the two overlays combined for operator playback;
- lossless instance and semantic mask PNGs for later calibrated fusion;
- ordered per-frame JSONL and one-second GPU telemetry;
- a machine-readable run report with input/config/model identities, decode,
inference, encode and end-to-end timing, latency percentiles, throughput,
CUDA peak allocation/reservation, process peak RSS, system load and GPU
utilization/VRAM/temperature/power samples.
The worker runs with `--network none` and cached model weights. A preflight
revalidates the complete transferred payload, CUDA execution and both cached
model generations before decoding a long epoch. The run then repeats payload
validation inside the inference container, requires exactly the declared frame
count and a strictly increasing session-time row for every frame, and publishes
only by atomic rename after every output digest is sealed.
There is no recorded-duration, frame-count or aggregate-video-byte admission
ceiling in this profile. Resource use therefore scales with the real input and
is reported, not hidden behind an arbitrary eight-minute laboratory limit.
## Native panoptic playback
Full raster masks are not copied into the RRD. That would turn a long video into
a multi-gigabyte browser-memory object. Instead Mission Core validates the v2
result and exposes its MP4 through the same generation-bound, seekable HTTP
Range contract as a recorded camera. Replay advertises an additional opaque
source such as `recorded.perception.right`; the Control Station opens it in a
native video window on the shared `session_time` timeline. A one-, three- or
ten-hour video remains disk/range streamed and does not have to fit in RAM.
The lossless masks remain private derived evidence. A host-side calibrated
fusion step samples them at K1 KB4 LiDAR projections and publishes compact
semantic `Points3D`, support-gated `Boxes3D` and diagnostic distances as a
separate replaceable generation. Missing or rejected v2/fusion results never
replace or invalidate the base raw point-cloud recording.
## RAVNOVES00 qualification · 2026-07-20
The first full recorded run admitted all 4,489 frames from
`sensor.camera.right` without sampling, failures or skips. The sealed input was
363,235,615 bytes over `35.421857292484.144857292` session seconds. Its
immutable result is
`result-f4cebdea8a82698a5b8a65d2c3fbdb0428b88b9dc49fe45f8cb37d740ed83d02`.
Measured RTX 4090 worker results:
- inference: 2,674.722 s and 1.678 frames/s;
- end to end: 2,816.349 s and 1.594 frames/s;
- instance latency: 89.826 ms p50, 121.344 ms p95, 915.325 ms max;
- semantic latency: 249.678 ms p50, 284.826 ms p95, 406.594 ms max;
- GPU utilization: 67% p50, 82% p95, 90% max over 2,675 one-second samples;
- process CUDA peak: 2,230.8 MiB allocated and 2,872 MiB reserved;
- total GPU memory observed, including the worker's shared resident services:
13,373 MiB p50 and 13,388 MiB max;
- GPU power/temperature: 182.46 W p50, 190.10 W p95 and 49 C p50, 54 C max;
- process peak RSS: 2,375.9 MiB;
- publication: 31.229 s decode, 3.397 s NVENC, 81,109,627-byte H.264 MP4,
60,326,719-byte lossless mask archive.
The factory-calibrated full fusion generation
`fusion-0b1be23128ebd3d230562cffd96491169e99f0e839e56b812661c974e4fdc00b`
matched LiDAR and pose within the admitted 250 ms host-arrival window for 4,323
frames and declared 166 frames `depth-unavailable`. It produced 6,124,145
semantic points and 13,496 support-gated diagnostic boxes in 77.708 s. The
compact fusion payload is 43 MiB.
Native browser QA opened `RAVNOVES00`, played the raw and panoptic 800x600
videos together at ready-state 4, and measured approximately 12 ms between
their media clocks. The Rerun scene showed the synchronized semantic points and
distance-labeled diagnostic boxes. The current baseline is deliberately not an
accuracy or safety acceptance: generic perspective-trained models produce
large fisheye false positives in 916 frames, and timing/distance have not been
ground-truthed. The next A/B should compare an admitted undistort/ROI transform
before inference rather than silently hiding these observations.
## E1 valid-FOV preprocessing qualification · 2026-07-20
The first post-baseline A/B uses two immutable inputs derived from the same
RAVNOVES00 job and factory calibration:
- valid-FOV generation
`valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2`;
- qualification-slice generation
`qualification-slice-2394070b4f3e38f1b8c483e878fcc11fd3c29f751f3d4cd7e3a2553304c5c142`.
The mask is not estimated from each image. It is bound to the exact calibration
SHA, `sensor.camera.right`, `camera_1`, the admitted 800x600 linear-resize
profile and the KB4 principal point `(396.319, 301.496)`. A four-pixel inner
margin produces a 293.504-pixel radius, 270,606 valid pixels (56.37625%) and an
exclusive crop rectangle `[103, 8, 690, 595]`. Repeated preparation reuses the
same content-addressed PNG and manifest.
The slice selects 256 exact frame indices uniformly across all 4,489 frames,
including both endpoints. The same loaded FP32 Mask R-CNN and BEiT generations
were interleaved per frame across three variants: unmodified baseline, fixed
valid-FOV fill, and valid-FOV crop remapped into the original pixel coordinates.
The sealed result is
`qualification-result-98a2fee3d22979f3e18847719667cc76bdeacf25904c0fd1da4c5202253b3940`.
The run completed in 318.969 s and emitted 12 preview frames per variant. GPU
telemetry recorded 319 one-second samples: utilization was 74% p50 and 82% p95,
power was 199.34 W p50 and 206.96 W p95, and temperature was 50 C p50 and 55 C
max. The qualification process peaked at 1,849.9 MiB CUDA allocated, 2,256 MiB
reserved and 2,298.2 MiB RSS.
Measured mean model paths, excluding the frame decode shared by all variants:
| Variant | Mask R-CNN path | BEiT path | Combined |
|---|---:|---:|---:|
| baseline | 106.292 ms | 278.064 ms | 384.356 ms |
| valid-FOV fill | 107.027 ms | 280.762 ms | 387.789 ms |
| valid-FOV crop | 104.055 ms | 282.840 ms | 386.895 ms |
The crop reduced Mask R-CNN forward time by 6.38%, but BEiT still receives its
fixed 640x640 tensor and became 1.18% slower. With mask/crop preprocessing
included, neither variant improved the combined path; fill was 0.89% slower and
crop was 0.66% slower than baseline. A binary mask improves admission quality,
but multiplying an unchanged tensor by it does not remove dense neural FLOPs.
The quality proxies are useful but are not ground truth. Baseline produced 58
instance masks and 58 boxes larger than half the admitted comparison area; both
masked variants produced zero. The fraction of raw predicted instance-mask
pixels outside the canonical FOV fell from 41.263% to 0.077% for fill and 0.911%
for crop before the final output clamp. Inside the valid circle, mean BEiT
disagreement with baseline was 8.713% for fill and 11.547% for crop. This is a
measure of change, not accuracy.
E1 therefore accepts the immutable valid-FOV artifact and the 256-frame gate.
Fixed fill is the conservative next accuracy baseline because it preserves the
800x600 geometry, removes the exterior lens region and changes the semantic
result less than crop. Crop remains an experimental model-specific option, not
a general speed optimization. The next run needs human labels/ground truth and
must compare native KB4 input, calibrated virtual views and fisheye-trained
models before promoting any preprocessing profile to a full-epoch result.
## E2 evaluation pack and annotation gate · 2026-07-20
LAB E2 starts from the same immutable RAVNOVES00 compute job, E1 qualification
slice and factory-calibrated valid-FOV generation. All eight contact sheets,
covering the 256 uniformly distributed E1 candidates, were reviewed before
selection. The sealed evaluation generation is
`evaluation-pack-7a983bba75d46c7c260252cb2d461e1384dcb92cda9e164397e841e6ebb37789`.
The pack contains 64 exact 800x600 images: 48 reviewed full-epoch anchors from
the E1 slice and four four-frame consecutive clips for temporal measurements.
The clips cover a person with a stroller, a close moving car, vehicle
occlusion/relative motion and a near building/terrace scene with a partially
visible carried laptop. The anchors retain the recording's road, sidewalk,
ground, grass, woody vegetation, buildings, sky, people, cars, trucks, lens
boundary and hard-negative diversity.
Every image is stored both as the raw decoded RGB frame and as the accepted E1
fixed-valid-FOV-fill input. The identity binds the job/input SHA, source, codec
epoch, selected segment SHA, decoded session timestamp, E1 qualification,
valid-FOV generation, calibration SHA, camera slot, FFmpeg 7.1.1 generation and
the raw/fill RGB pixel hashes. It also binds the reviewed selection-document
SHA and both producer-code hashes. The pack has 130 hashed payload artifacts
plus its manifest and occupies approximately 67 MiB locally.
One earlier local preparation generation, `evaluation-pack-b6d9215a…`, was not
promoted because its identity omitted the producer-code and selection-document
hashes. It remains a superseded diagnostic artifact and is not an accepted E2
input.
The immutable pack is deliberately `unannotated`. Its annotation contract
defines 15 robotics-oriented thing/stuff classes, label 0 for the excluded lens
exterior, label 255 for genuinely unresolved pixels, two-pass human review and
required semantic, instance, safety-proxy and temporal metrics. The empty
annotation template must be copied to a review workspace; it must never be
edited inside the sealed pack. Model-generated prelabels may accelerate review
but are not accepted as ground truth without a human pass.
No AP, mIoU or model-ranking claim is attached to E2 yet. The next gate is to
complete and seal the reviewed annotations. Only then may candidate models be
ranked on this pack; a full 4,489-frame run remains prohibited until one
configuration passes both the accuracy and throughput gates.
The first model-assisted draft is sealed separately as
`evaluation-prelabels-4ba26bbf6eb8a49631f5caf984267e0445958540aeda2b5b0d82ca6440835cf1`.
It reuses the exact E0 Mask R-CNN and BEiT weights and maps their COCO/ADE
classes into the E2 taxonomy. It is explicitly marked
`unreviewed-model-draft`; it never mutates the evaluation pack or annotation
template.
The isolated RTX 4090 run processed 64/64 frames in 31.551 s. Mean forward time
was 57.539 ms for Mask R-CNN, 215.164 ms for BEiT and 272.703 ms combined. The
process peaked at 1,842.8 MiB CUDA allocated, 2,768 MiB reserved and 2,239.9 MiB
RSS. Across 32 one-second samples, GPU utilization was 51% p50 / 71% p95, power
167.73 W p50 / 186.89 W p95 and temperature 42 C p50 / 47 C max. Shared Triton,
Frigate and Ollama services remained running and healthy.
The draft emitted 775 mapped instances: 640 car, 58 static obstacle, 45 person,
25 heavy vehicle, five bicycle and one each motorcycle/animal. Sixteen previews
were reviewed. They confirm that the fixed-FOV exterior stays clean and that
the draft is useful for annotation assistance, but also expose the expected E0
domain errors: duplicated/distant car boxes, unstable small instances, coarse
fisheye boundaries and excessive static-obstacle proposals on planters. These
counts are workload indicators for review, not precision or recall.
The first prelabel attempt stopped before model loading because the container
mountpoint `/evaluation-pack` was incorrectly required to equal the
content-addressed generation basename. The path-name check was removed while
all manifest, artifact and identity hashes remained mandatory. No failed result
was published; the second attempt completed and 147 payload artifacts were
reverified locally with zero digest/length mismatches.
The review handoff is sealed separately as
`annotation-workspace-9a950d1c37d56dc12cc285b13c5addd7795285879cbcb1fbb2d5811c3c69821a`.
It contains a deterministic 64-image upload, a 775-instance COCO RLE draft, a
dense CVAT Segmentation Mask archive, an exact frame/timestamp map, the fixed
valid-FOV mask, the 15-class label specification and an unreviewed two-pass
checklist. The three ZIP archives passed both the workspace validator and
independent ZIP integrity checks. The 11 payload artifacts occupy 31,861,798
bytes. This workspace remains `ground_truth=false`; the two synchronized CVAT
tasks must be reviewed and their accepted exports sealed as a separate
generation before any AP or mIoU claim is allowed.
The real prelabel generation also exposed one identity-serialization defect in
its producer: `target_categories` used integer dictionary keys while hashing,
but JSON reloads them as strings and changes their sorted order. The stored
artifact files and all 147 recorded payload hashes are unchanged. The workspace
validator admits only this exact reversible legacy representation and binds the
serialized `result.json` SHA separately. The worker producer now emits string
keys before hashing, so subsequent prelabel identities are stable across a JSON
round trip. The existing result was neither rewritten nor renamed.
Prepare or reproduce the review inputs with:
```console
.venv/bin/python experiments/perception/prepare_e2_evaluation_pack.py candidates \
--job-root .runtime/compute-jobs/<job-id> \
--qualification-root .runtime/compute-experiments/e1/qualification-slices/<generation> \
--output-root .runtime/compute-experiments/e2/<candidate-review>
.venv/bin/python experiments/perception/prepare_e2_evaluation_pack.py seal \
--job-root .runtime/compute-jobs/<job-id> \
--qualification-root .runtime/compute-experiments/e1/qualification-slices/<generation> \
--valid-fov-root .runtime/compute-experiments/e1/valid-fov/<generation> \
--selection .runtime/compute-experiments/e2/selection-e2.json \
--output-root .runtime/compute-experiments/e2/evaluation-packs
.venv/bin/python experiments/perception/prepare_e2_annotation_workspace.py prepare \
--evaluation-pack .runtime/compute-experiments/e2/evaluation-packs/<generation> \
--prelabels .runtime/compute-experiments/e2/prelabels/<generation> \
--valid-fov-root .runtime/compute-experiments/e1/valid-fov/<generation> \
--output-root .runtime/compute-experiments/e2/annotation-workspaces
```
## E4 full-session semantic playback · 2026-07-21
LAB E4 promotes the plain EoMT valid-FOV control from LAB E3 into the first
complete saved-session semantic playback. It consumed all 4,489 frames of
RAVNOVES00 (`20260720T065719Z_viewer_live`) from
`sensor.camera.right`, using factory calibration slot `camera_1`, FP16
autocast, batch size one and no sampling. CLAHE, five-view rectification and the
instance branch were deliberately disabled.
The immutable published result is
`result-793785170472c519486ccd666be102fb04d169d92383acda3fcc29eecf045d30`.
It contains an 800x600 H.264 semantic-overlay video, 4,489 semantic masks,
4,489 timestamp rows, one-second GPU telemetry and a run report. FFprobe and the
recorded-perception validator independently confirmed the exact frame count,
448.723-second timeline, artifact hashes and input/job/calibration binding.
The inference loop ran for 1,447.565 seconds at 3.101 FPS. Full extraction,
inference, publication, hashing and validation took 1,668.259 seconds at 2.691
FPS. GPU utilization was 73.05% mean / 89% p95, E4 process CUDA allocation
peaked at 2,099.8 MiB, process RSS at 2,009.3 MiB, power at 253.23 W and
temperature at 58 C. The exact configuration is about 3.72 times slower than
the source recording rate and is therefore an offline baseline, not a live
configuration.
All task-controlled worker paths remained under `D:\NDC_MISSIONCORE`. The
orchestrator enforced a 360 GiB free-space floor and a 17.195 GiB conservative
working-set reserve. Final free space after exact task-temporary cleanup was
379.395 GiB; C: was not used or mounted by the task.
Mission Core exposes the result in **Сохранённые сессии → RAVNOVES00 →
Источники данных сцены → Сегментация · камера right**. Browser acceptance
confirmed the exact result source, 800x600 dimensions, full duration, no media
error and advancing playback time. The detailed configuration, timing tables,
artifact hashes, disk checkpoints, limitations and next gates are recorded in
`experiments/perception/LAB_E4_REPORT_2026-07-21.md` and Ops card MISSIONCOR-18.
E4 remains `ground_truth=false` and semantic-only. It makes no claim about live
latency, instances, tracking, 3D cuboids, LiDAR association, distance accuracy,
point-cloud labels or safety fitness.
## Recorded Rerun projection
Mission Core discovers only results whose validated job names the opened
@ -105,3 +405,84 @@ accepted for obstacle avoidance, free-space estimation or safety decisions.
tests.
4. Introduce tracking, segmentation/free-space, calibration and point-cloud
models as separate versioned pipelines.
## E5 recorded instance tracking qualification · 2026-07-21
LAB E5 establishes the first measured temporal object-identity baseline on a
preselected 601-frame, 60.069-second RAVNOVES00 interval. It uses the official
Apache-2.0 YOLOX-S ONNX release through the existing Triton service, the
immutable `camera_1`-bound valid-FOV mask, and a ByteTrack-style two-stage IoU
tracker. The accepted profile, runner, model and configuration are pinned by
SHA-256; output remains `ground_truth=false` and qualification-only.
The immutable result is
`e5-tracking-88aace13ef9963f8dc07f85228e530f9d28c2b49aca9192409f7975512b058f6`.
It contains an 800x600 H.264 ID-overlay video, exactly 601 timestamped detection
and track rows, one-second GPU telemetry, a contact sheet and the complete run
report. The result validator rehashed all artifacts and verified the exact job,
input, session, source, clip and timeline binding. FFprobe independently
confirmed 601 declared/read frames and 60.068948 seconds.
The frame loop ran for 88.093 seconds at 6.822 FPS; the complete worker run took
166.336 seconds at 3.613 FPS. Mean per-frame time was 12.359 ms in Triton,
0.247 ms in tracking, 30.132 ms decoding and 87.363 ms writing overlays. The
detector and tracker are therefore not the main live-rate bottleneck; artifact
I/O must be decoupled before a live path is admitted. GPU utilization was
51.65% mean / 56% max, power 144.78 W mean / 148.40 W max, temperature 43 C max
and process RSS 126.199 MiB.
The run admitted 4,049 detections and emitted 3,320 observations across 167
confirmed IDs. Useful clear-view persistence is proven, including vehicle
tracks lasting 100212 frames. The result also exposes real limitations: an ID
fragments during the close woman/stroller occlusion, the stroller can be called
`motorcycle`, and parked vehicles can flicker between `car` and `truck`. There
is no identity ground truth, so IDF1, HOTA, MOTA and true ID-switch counts are
not claimed.
All task-controlled worker paths remained under `D:\NDC_MISSIONCORE`; the
orchestrator enforced the 360 GiB floor and a 4.402 GiB working-set reserve.
Free space after exact temporary cleanup was 380.595 GiB. The YOLOX model was
loaded only for the run and restored to its prior unloaded state. Detailed
provenance, pilot history, thresholds, timing tables, artifacts, hashes,
quality findings and the LAB E6 gate are in
`experiments/perception/LAB_E5_REPORT_2026-07-21.md` and Ops card
MISSIONCOR-19.
The next bounded gate is calibrated 2D-track/LiDAR association on the same
601-frame interval: robust point support, metric range, coarse 3D cuboids and a
qualification-only Rerun recording. Full-session and live promotion remain
blocked on measured identity quality and a bounded queue/drop design.
## E6 factory-calibrated tracked LiDAR fusion · 2026-07-21
LAB E6 closes the recorded 2D-track/LiDAR association gate on the exact E5
601-frame interval. It reuses the immutable E4 semantic masks, E5 track IDs,
raw K1 point/pose capture and the XGRIDS `camera_1` factory KB4 calibration.
Fused observations must satisfy 100 ms camera/point and pose/point gates.
Nearest-depth buffering, semantic support, depth splitting, 3D connected
components, robust range history and plausible-size gates prevent unsupported
image rectangles from becoming fabricated 3D boxes.
The immutable result is
`e6-fusion-b4e4226674a66f6196c033785eb307c255a7bdd8493ef9809dfb1d6e5bd68eaa`.
It processed 601/601 frames, fused 526, emitted 918 point-supported oriented
cuboids across 56 track IDs and failed closed for depth on 75 frames outside
the strict timing gate. LiDAR/camera absolute delta was 29.389 ms mean,
70.794 ms p95 and 96.107 ms maximum. Accepted range spans 1.23937.749 m with a
10.189 m median.
The single-process local geometry/artifact path took 14.774 seconds, peaked at
240.469 MiB RSS and is faster than the source rate. This is not an end-to-end
live result because E4 and E5 were precomputed. The external worker and both
worker disks were untouched. The output includes an 800x600 H.264 overlay,
timestamped JSONL/NPZ data, a contact sheet and a standalone 60 MiB Rerun
recording with camera, map-frame cloud, support points and translucent
`Boxes3D`. Visual QA confirmed that these are real oriented Rerun primitives;
they remain visible-surface envelopes, not ground-truthed complete object
volumes.
Detailed provenance, profile freeze, pilot failures, thresholds, timing,
rejection counts, artifact hashes, limitations and the LAB E7 gate are in
`experiments/perception/LAB_E6_REPORT_2026-07-21.md`. Ops synchronization is
pending restoration of the direct `nodedc-ops-agent` tasker tools; the legacy
Ops API was not used.

View File

@ -0,0 +1,453 @@
# K1 calibrated perception roadmap
Status date: 2026-07-20.
## Outcome
Mission Core will use the K1 factory camera/LiDAR calibration instead of
building a new calibration workflow for the assembled rig. The first product
vertical is recorded, deterministic and fail-closed:
```text
canonical camera epoch + canonical LiDAR epoch
+ device-bound factory calibration snapshot
+ versioned segmentation/detection result
-> pixel masks
-> labeled LiDAR points and object distances
-> evidence-backed 3D boxes
-> optional derived Rerun layer
```
Only after this vertical passes on `RAVNOVES00` does the same contract move to
bounded live processing. Rerun remains the viewer and recording projection;
inference, calibration validation and fusion remain Mission Core concerns.
## Facts already established
### Camera identity is already canonical
The K1 plugin does not need to infer left versus right from image content. It
owns the exact RTSP selection:
| Mission Core source | K1 RTSP endpoint |
|---|---|
| `sensor.camera.left` | `/live/chn_left_main` |
| `sensor.camera.right` | `/live/chn_right_main` |
The selected source ID is retained by the camera gateway, the durable archive
summary and `missioncore.compute-job/v1`. A source switch creates another codec
epoch rather than silently changing the meaning of an existing epoch.
The accepted session `20260720T065719Z_viewer_live`, displayed as
`RAVNOVES00`, contains one recorded video source:
```text
source_id: sensor.camera.right
codec_epoch: 1
decoded video: H.264, 800x600
```
Consequently camera selection for perception must be a lookup from the job's
canonical `source_id`, never a UI default and never a computer-vision guess.
### Factory calibration exists and is used by K1
Read-only firmware analysis of K1 firmware `3.0.2` established:
- factory camera parameters are read from
`/mnt/system/factory-data/config/camera.yaml`;
- camera/LiDAR extrinsics are read from
`/mnt/system/factory-data/config/extrinsic_camera_lidar.yaml`;
- the internal frontend consumes camera intrinsics, distortion and
`T_rgb_lidar`;
- `libxcolor` consumes the calibration and produces the colorized point cloud;
- K1 enables PTP, synchronized left/right main cameras and real-time xcolor;
- the main camera calibration model is KB4 fisheye at `4000x3000`;
- the exposed file protocol has a distinct read action, `CacFileRead = 5`, and
a distinct write action, `CacFileWrite = 6`.
The OTA image contains model defaults. The exact values for one physical K1
live in that unit's `factory-data`; Mission Core must snapshot those exact
files before it claims calibrated fusion.
### Geometric calibration is now normalized; timing remains separate
The physical A4/FW 3.0.2 unit was read on 2026-07-20. Firmware declarations,
the xcolor loader and the unit's exact native resolutions jointly establish:
| Mission Core source | firmware camera | factory slot | native | admitted |
|---|---|---|---:|---:|
| `sensor.camera.left` | `camera_left_main` | `camera_0` | 4000x3000 | 800x600 |
| `sensor.camera.right` | `camera_right_main` | `camera_1` | 4000x3000 | 800x600 |
`camera_2` and `camera_3` are the 1280x800 left/right secondary pair and are
not silently substituted for a main RTSP source. The main H.264 substream is a
firmware-configured 800x600 linear resize with scale `(0.2, 0.2)` and no crop or
warp stage in the admitted profile. KB4 distortion is unchanged; `fx`, `fy`,
`cx` and `cy` are scaled on their respective axes.
The normalized matrix notation is `T_destination_from_source`, row-major
homogeneous 4x4, with translation in metres. The file-level `transform` is
`T_camera_0_from_lidar`. A serialized `camera_N.camera_pose` is
`T_camera_0_from_camera_N`; the same inverse-and-compose operation used by
xcolor produces:
```text
T_camera_N_from_lidar = inverse(T_camera_0_from_camera_N)
* T_camera_0_from_lidar
```
The remaining uncertainty is temporal: exact camera exposure time versus the
host-arrival timestamps currently retained by the external archive. This is a
separate temporal-calibration gate and is not hidden inside accepted geometry.
## Calibration product boundary
### What the UI should expose
The normal device panel should show a small calibration status card:
- `Ready`, `Missing`, `Mismatch` or `Unverified`;
- device model and firmware profile;
- captured-at time and immutable snapshot digest;
- canonical left/right source to calibration-slot mapping;
- native calibrated resolution and admitted stream resolution;
- geometry verification status;
- timing status separately from geometry status.
It may expose one explicit **Read snapshot from connected K1** operation. That
operation is read-only at the product level and must show the exact two admitted
files and the resulting digest.
### What the UI should not expose
Mission Core must not accept or upload a firmware archive merely to obtain
calibration. A general firmware uploader would add OTA, integrity, rollback and
device-bricking responsibilities without helping the perception path.
An advanced offline import is useful only for replay, recovery or a device that
cannot currently be reached. It must accept a small Mission Core calibration
bundle containing only the two admitted YAML documents plus identity metadata.
It must not accept a firmware TAR, arbitrary filesystem path or device write.
### Read-only device snapshot rules
The existing plugin action `calibration.device-snapshot.read` becomes the only
live entry point. Its implementation must:
1. require an already verified, owner-controlled K1 session;
2. publish only the reviewed `CacFileRead` request;
3. use an exact path allowlist for the two factory YAML files;
4. reject redirects, relative paths, extra response paths and write commands;
5. bound each response before YAML parsing;
6. parse with a safe loader and an exact schema/finite-number validation;
7. record original byte length and SHA-256 without rewriting the source bytes;
8. bind the snapshot to device identity, model, firmware and compatibility
profile;
9. publish the complete snapshot atomically in private storage;
10. leave `CacFileWrite` unavailable.
The snapshot contract must name transform direction and coordinate convention
explicitly. A matrix called only `extrinsic` or `T` is not admissible at the
Mission Core boundary.
### Implementation status — 2026-07-20
The first read-only boundary is now implemented:
- `protocol/calibration_file.py` encodes only command `5` and has no API for
command `6`;
- its request path is a closed two-value type at runtime: `camera.yaml` or
`extrinsic_camera_lidar.yaml` at their exact factory-data locations;
- response identity, session, authority, command, path, result code, UTF-8,
duplicates and byte bounds fail closed;
- `protocol/calibration_mqtt.py` performs one `DeviceInfo` read followed by the
two sequential file reads on one clean MQTT connection, with no reconnect or
automatic retry;
- `calibration.device-snapshot.read` now invokes that physical read instead of
returning the generic plugin state;
- before that read, Bridge/Direct Connect refresh the selected K1's DHCP lease
from the read-only BLE status characteristic; the IP is session state, while
the correlated MQTT `DeviceInfo` is the identity barrier;
- each successful read seals a new private `0700` directory with `0600` source
files, a manifest and SHA-256 identities; no earlier snapshot is replaced;
- `calibration_schema.py` rejects duplicate keys, anchors, aliases, explicit
tags, unknown fields, non-finite values, non-K1 dimensions and invalid rigid
transforms before a snapshot can become available;
- the manifest contains explicit left/right bindings, the 800x600 image
transform, KB4 parameters and `T_camera_N_from_lidar` matrices;
- the physical normalized snapshot captured at `2026-07-20T10:54:32.613Z`
has content identity
`05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`;
- the complete Python suite passes with the boundary installed.
The P0 geometric-input subgate is accepted. The recorded Rerun diagnostic is
also generated and opens in the native viewer. P0 as a whole still requires
measured reprojection agreement across the image and a temporal-offset budget;
parsing a valid matrix or visually plausible overlay is not a substitute for
those measurements.
The first reproducible recorded diagnostic was generated on 2026-07-20 by the
new `k1link analyze calibrated-overlay` path. Experiment
`20260720T112515Z_k1_calibrated_overlay_2655c5c7c38e` is bound to the immutable
RAVNOVES00 MQTT, camera and calibration generations. It produced four
depth-coloured PNG overlays plus a 4,016,830-byte Rerun diagnostic with SHA-256
`83bc5c1134563987c3be5734d6c069d07ad8a9e5b6806682169c89f4bd5354d9`.
The experiment established one previously implicit frame operation: current
`lio_pcl` coordinates are in the K1 map frame, so projection uses
`inverse(T_map_from_lidar)` before `T_camera_1_from_lidar` and KB4. The four
selected camera frames projected 1,613/2,617, 983/1,813, 1,543/2,744 and
1,730/1,880 source points respectively. Visual inspection shows coherent
facade, vehicle, vegetation and ground structure across the fisheye image.
This is diagnostic evidence, not a measured pixel-error acceptance: explicit
static landmark correspondences and the temporal-offset sweep remain open.
## Recorded segmentation and calibrated fusion probe
A four-frame P1/P2/P3 probe now exists on the same immutable RAVNOVES00 camera
anchors. The RTX worker received the raw camera pixels, not the LiDAR-coloured
overlay, and the transferred PNG SHA-256 values were verified on both hosts.
The admitted research profiles are:
- TorchVision Mask R-CNN ResNet50-FPN v2 / COCO_V1 for instance masks, with
weight SHA-256
`73cbd0190fcbe3ba339921fbce2c3a0b6bb9126c9a133c85e43a2a8e060a109e`;
- Microsoft BEiT base / ADE20K-150 for semantic masks, revision
`a8b6f5ef4acb2ea55d882989deaa02d39401e2b2`, with weight SHA-256
`e0747360d190bd7c0f53d2fe3b2ed560c304d3eefef94574af9c7e93aaf8e7a9`.
BEiT was published only after an exact checkpoint load with no missing,
unexpected or mismatched keys. An earlier Intel DPT/ADE probe was rejected:
its checkpoint left twelve fusion-layer parameters newly initialized. The
strict repeat failed before publication, so that output is diagnostic only and
cannot enter fusion.
The accepted segmentation generation is
`1ef80a6df60830d8ac37f09db679696325adb65ef61094b666165f561b992a08`.
On the three warm frames, sequential Mask R-CNN inference took 100124 ms and
BEiT took 244312 ms. The combined 365412 ms is only about 2.42.7 FPS before
decode, transfer and fusion; this is not a live real-time acceptance.
Mission Core sampled both masks at factory-calibrated LiDAR projections and
published fusion experiment
`20260720T121000Z_k1_segmentation_fusion_d09020d0cb13`, generation
`3d150012cf88ad477c602aa072d0b6c2675ca472204b61d9377eb8b83c128842`.
Distance is Euclidean range from the current LiDAR origin. A diagnostic box is
published only after the instance mask contains at least four points in one
contiguous depth cluster; its current bounds are map-frame axis-aligned p05p95
bounds with a 12 m maximum span gate.
| camera session | projected points | diagnostic Boxes3D |
|---:|---:|---:|
| 95.471 s | 1,613 | 2 |
| 215.408 s | 983 | 5 |
| 335.424 s | 1,543 | 3 |
| 455.450 s | 1,730 | 4 |
The span gate rejected a large false `car` mask even though it contained 854
clustered points. Sparse objects remain 2D observations with an explicit
insufficient-support status. The 11,582,442-byte Rerun RRD has SHA-256
`1c190e6dc64b8c1e187c053cd950dabc4167d1d7fe3c8ec5e2499c0d5d3fa471`.
Native headless viewer QA confirmed synchronized raw, semantic and fusion image
views, semantic `Points3D`, and translucent `Boxes3D` with distance/support
labels. Distances and boxes are not ground-truthed, tracked or safety accepted.
## Automatic camera/calibration binding
The binding is performed when a compute job is prepared:
```text
camera archive summary.source_id
-> compatibility-profile source mapping
-> exact camera slot in device calibration snapshot
-> image-transform profile for the admitted codec epoch
-> calibration generation SHA-256 in the compute job
```
Preparation fails closed when any link is absent or ambiguous. The worker must
not choose a default camera. The result repeats the calibration generation and
camera source; Mission Core revalidates both before accepting a derived layer.
The left/right mapping is now device-snapshot data backed by the exact FW 3.0.2
profile. The compute-job binding is still pending; no worker may infer a slot
from list order or use a default camera.
## Minimal recorded vertical
### Gate P0 — calibration snapshot and camera mapping
Deliverables:
- bounded protobuf codec for `CalibFileRequest` and `CalibFileResponse`;
- whitelisted read-only snapshot action and tests;
- immutable private snapshot with source digests;
- verified mapping for both canonical camera source IDs;
- validated 4000x3000-to-800x600 image transform;
- a Rerun diagnostic layer showing projected LiDAR samples over the image.
Acceptance requires visible and measured agreement on several static regions
across the image, not one hand-picked object at the center.
### Gate P1 — full-timeline 2D perception
Run the complete `RAVNOVES00` camera epoch, not one still image. Preserve every
decoded frame's admitted session timestamp and produce two independent
versioned outputs:
- instance masks for countable objects such as people and vehicles;
- semantic classes for background/stuff such as ground, building and
vegetation.
Together these form the requested panoptic view. The existing YOLOX-S result is
a detector smoke test and remains useful as a baseline, but it cannot satisfy
this gate. Model choice remains replaceable behind the result contract and is
accepted only after measured accuracy, latency, VRAM and license review.
Operator playback uses a generation-bound H.264 panoptic MP4 beside the native
Rerun canvas. Embedding every 800x600 `SegmentationImage` row in RRD would make
multi-hour raster playback depend on browser memory. Lossless masks remain
content-addressed derived evidence for fusion, while the MP4 is read through
native HTTP Range and follows the same `session_time` controller as the source
camera and point cloud. Small diagnostic RRDs may still use
`SegmentationImage`, `AnnotationContext` and `Boxes2D`; that is not the scalable
full-epoch transport.
### Gate P2 — 2D-to-3D fusion and distance
For each admitted camera frame:
1. select a temporally compatible LiDAR frame/window;
2. transform LiDAR points into the selected camera frame;
3. project with the K1 KB4 model and the validated stream transform;
4. reject points behind the camera and resolve occlusion with a depth buffer;
5. sample the panoptic mask at each visible projected point;
6. retain labeled points in LiDAR coordinates;
7. compute per-instance nearest robust distance, centroid and distance spread;
8. cluster instance points and fit an oriented 3D box only when support is
sufficient.
A 2D rectangle must never be blindly extruded into a fake 3D box. An object
with insufficient or stale LiDAR support remains a 2D observation with an
explicit `depth unavailable` reason.
Distance v1 should be displayed directly on each object label. Click-to-query
is a later interaction enhancement, not a dependency of correct distance.
### Gate P3 — Rerun 3D presentation
The derived layer logs:
- semantic/instance masks on the camera image;
- labeled points as `Points3D` using the same class ontology;
- supported objects as oriented `Boxes3D`;
- object ID, class, confidence, distance and support count;
- calibration/result generation metadata and degraded-state reasons.
Rerun's native camera projection is pinhole-only. K1 uses KB4 fisheye, so
Mission Core must perform the calibrated KB4 projection itself or log a proven
undistorted image. It must not present an uncorrected pinhole frustum as exact
K1 geometry.
### Gate P4 — recorded qualification
Qualify the entire 89 minute session with:
- no frame or segment ceiling;
- deterministic repeat identity;
- end-to-end throughput and wall time;
- CPU, RAM, GPU, VRAM and disk telemetry;
- calibration reprojection statistics;
- temporal-offset sweep and moving-object error report;
- explicit failure/degraded reasons;
- immutable raw inputs and separately replaceable derived results.
## Bounded live vertical
Only after P0P4 pass does live mode reuse the same model and fusion contracts.
The live path has two different policies:
- archival inputs are never dropped by the viewer or inference queue;
- derived preview is latest-wins with a small bounded queue and explicit
dropped/stale counters.
The first live target is stable 510 Hz derived perception, not an unsupported
claim of matching every camera or LiDAR message. It must expose:
- capture, queue, decode, inference, fusion and render latency;
- queue depth and dropped derived frames;
- calibration generation in use;
- worker disconnect/recovery state;
- `healthy`, `degraded`, `stale` and `unavailable` states;
- raw recording continuity when the worker is slow or absent.
The external RTX worker remains an executor. It receives bounded observations
and calibration references; it gains no K1 command authority.
## Deferred intentionally
The following are not prerequisites for the first correct vertical:
- uploading full firmware through Mission Core;
- calibrating the already factory-calibrated assembled K1 rig again;
- native LiDAR-only neural segmentation;
- multi-camera stitching;
- multi-object tracking across long occlusions;
- HD maps, ROS 2, vehicle control or safety decisions;
- a universal model marketplace or scheduler.
Native point-cloud segmentation and tracking may be added later as independent
pipelines and compared against the camera-fused labels. They must not block the
first evidence-backed 3D boxes and distances.
## Immediate implementation order
1. Keep the accepted Windows reboot path healthy and reproducible.
2. ~~Implement and offline-test the bounded calibration file protobuf codec.~~
3. ~~Implement `calibration.device-snapshot.read` with the two-path allowlist.~~
4. ~~Capture the physical K1 snapshot and prove left/right slot mapping.~~
5. ~~Generate a reproducible calibrated LiDAR-over-video diagnostic on
`RAVNOVES00`.~~ Record measured static-landmark reprojection error and the
temporal-offset budget for the accepted 800x600 transform.
6. Extend compute job/result contracts with camera source and calibration
generation binding.
7. ~~Prove instance and semantic masks on four physical frames.~~ Produce the
complete recorded epoch with immutable result identity and exact-repeat
reuse.
8. ~~Prove mask-to-point fusion, diagnostic distances and support-gated 3D
boxes on four frames.~~ Add ground truth, tracking and accepted oriented-box
fitting.
9. ~~Open the four-frame fused result in native Rerun.~~ Qualify the full
session with resource and timing telemetry.
10. ~~Qualify the bounded latest-wins scheduler and machine world-state contract
with source-paced replay.~~ Move the same contract onto the real worker
inference path, then connect the live K1.
## LAB E7 replay-real-time checkpoint · 2026-07-21
LAB E7 accepted the first source-paced downstream perception runtime on the
same 601-frame E6 interval. The result
`e7-live-replay-e339aaed75fff05ed893ae48770bd127cc0c764f3124fc4b6be94b5fd9f8389c`
published 601/601 machine world states at 10.021845 Hz with zero drops, a
capacity-two queue and 5.927 ms p95 scheduled-tick-to-publication latency. The
526 fused frames were healthy; all 75 E6 sync-gated frames were explicitly
degraded, with no stale or unavailable states.
The world state carries object identity, class, map and K1-LiDAR position,
visible-surface 3D size, range, support, guarded diagnostic velocity and a
72-sector diagnostic clearance observation. Vehicle-body coordinates remain
unavailable until a real sensor-to-vehicle transform is supplied. Rerun is a
subscriber showing point cloud, `Boxes3D`, latency, queue depth and health; it
is not the perception authority.
An intentional 250 ms consumer-delay control processed 33/80 frames, discarded
47 obsolete derived frames, never exceeded queue depth two and marked 32
published states stale. Raw evidence remained outside the derived queue. This
proves bounded overload behavior, not live inference. E4, E5 and E6 inputs were
precomputed and the AI worker was untouched.
Full provenance, pilot corrections, profile hashes, latency tables, velocity
guards, clearance limits, artifacts and the LAB E8 gate are recorded in
`experiments/perception/LAB_E7_REPORT_2026-07-21.md`.

View File

@ -1,6 +1,6 @@
# ADR 0013: explicit K1 local connection matrix
- Status: accepted for implementation; physical acceptance is mode-specific
- Status: amended 2026-07-20; Bridge is the product path, Quick Connect retained as a prepared-host laboratory path
- Date: 2026-07-19
- Extends: ADR 0004, ADR 0005 and ADR 0012
@ -28,7 +28,7 @@ firmware `3.0.2` compatibility profile:
| --- | --- | --- | --- | --- |
| Bridge | `direct-lan` | one reviewed BLE provisioning write | none | physically accepted in Mission Core |
| Direct Connect | `controller-hotspot` | the same reviewed BLE provisioning write | operator prepares the hotspot and route | implementation complete; physical run pending |
| Quick Connect | `device-ap` | none | one CoreWLAN scan and association | LixelGO data plane observed; Mission Core host run pending |
| Quick Connect | `device-ap` | one fixed reviewed 100-byte AP-enable write | preinstalled exact-firmware credential provider, bounded exact-SSID discovery and one association | physically accepted on one prepared Mac; not portable bootstrap |
Bridge remains the default. Every API request carries both `connection_mode`
and its exact topology attestation; mismatched pairs are rejected before any
@ -36,36 +36,121 @@ network action. Acquisition and camera admission must match the active mode.
The fixed K1 AP address is admitted only after a successful Quick Connect host
association.
Bridge and Direct Connect do not have a fixed product IP. The address reported
by BLE characteristic `7f02` is a DHCP lease observation scoped to the current
connection, never device identity and never a durable configuration value.
Before a new application-control session, an implicit-host acquisition, or a
factory-calibration read, Mission Core performs one read-only `7f02` read for
the selected CoreBluetooth device and replaces the previous target. An address
change creates a new `device_session_id` and invalidates calibration captured
for the previous session. An active acquisition is never retargeted in place.
The later correlated MQTT `DeviceInfo` response supplies model, firmware,
serial and vendor identity; IP equality alone cannot identify a K1.
Product decision on 2026-07-20: Bridge/direct-LAN is the continuing route.
Quick Connect remains visible and executable on an already prepared host, but
is not a deployment dependency or portability claim.
Direct Connect does not introduce a new vendor payload. The operator first
starts a hotspot on the controlling device and enters that hotspot's SSID and
password. Mission Core performs the same single, physically reviewed BLE write
used by Bridge and accepts only the non-AP private IPv4 returned by K1 status.
Quick Connect does not write GATT. A short-lived macOS Swift/CoreWLAN helper
receives the operator-entered AP SSID and password as bounded JSON on stdin.
The credential never appears in argv, environment, stdout, stderr, manifests or
browser persistence. The helper performs at most one scan and one association;
Mission Core does not retry. The implementation intentionally does not infer or
read the K1 AP password from an undocumented BLE structure. Until that read has
its own captured characteristic, bounded decoder and review, the operator must
enter the AP credentials shown for the owner-controlled scanner.
Quick Connect does not accept a credential from the browser/API. The first
implementation incorrectly jumped from BLE discovery directly to host Wi-Fi
association. A physical negative run showed that the selected K1 was still on
the existing LAN and the expected AP was not visible. Re-review of the exact
LixelGO branch recovered the missing state transition: after BLE connection it
writes one 100-byte `7f01` frame, zero except for `0x01` at offset 99, and treats
the callback as the AP-launch result before invoking the OS Wi-Fi connector.
No connection-verification button emits a probe. It validates only the admitted
private address; the later canonical MQTT session supplies the real data-plane
connection attempt. A failed or ambiguous network action remains terminal until
the operator checks physical state and explicitly starts a new operation.
Mission Core now performs that bounded AP-enable write at most once. The first
physical run reached `WIFI_AP` at the reviewed AP address, and a targeted
CoreWLAN scan found the SSID matching the selected BLE advertised name. It also
disproved the earlier assumption that LixelGO carried one global Quick Connect
profile: retained client analysis exposes `WiFiAP_SSID` and `WiFiAP_Password` as
fields of each device record.
A subsequent run found that `7f02` may retain `WIFI_AP / 192.168.56.1` after the
SSID stops beaconing. Static review then recovered the missing discriminator:
LixelGO maps response byte 51 to its AP-ready flag and waits up to 15 seconds for
that flag. Mission Core therefore mirrors LixelGO: every new operator Quick
Connect action emits exactly one reviewed enable frame, including from a
`WIFI_AP` baseline, and admits host discovery only after byte 51 becomes
non-zero. This is not an automatic retry.
The same review proved that LixelGO does not close its BLE manager between the
AP-ready callback and native Wi-Fi connect. A failed Mission Core run had done
exactly that: byte 51 changed from zero to one, the Python BLE context exited,
and the following cold Swift/CoreWLAN process missed the beacon. The corrected
implementation holds the selected `BleakClient` open through bounded native
SSID discovery and the single association call.
BLE discovery and the selected device action form one host session. A physical
run proved that immediately rediscovering the same K1 by its CoreBluetooth UUID
can fail even though the preceding scan exposed it. Mission Core retains the
non-serializable `BLEDevice` handle process-locally and uses that exact handle
for the next selected network action; it never exposes the handle through API
state or treats the macOS UUID as durable device identity.
The corrected host boundary derives a non-secret, device-scoped profile ID from
the selected SSID. The reviewed client contains per-device `WiFiAP_SSID` and
`WiFiAP_Password` fields, but the 2026-07-20 review of the exact official K1
`3.0.2` firmware recovered their upstream source: the scanner's bundled
NetworkManager AP script assigns one firmware-constant WPA2 material, while
`lixel_nman` constructs `XGR-` plus six device-identity characters with a MAC
fallback. It is neither an iPhone credential nor a per-device secret.
The laboratory implementation can install a firmware-scoped credential source from the
authenticated official archive. The offline importer validates the exact
SHA-256, streams the bounded application-partition range, requires one valid AP
declaration and writes the value to the OS secure store through helper stdin.
On macOS, the Keychain helper materializes the selected device profile from
that opaque source before any BLE write. A missing provider fails closed. The
browser, API, argv, logs, manifests and evidence never receive the secret; the
importer's short-lived mutable buffer is zeroized after the Keychain handoff.
The host-network boundary, rather than the XGRIDS frontend, owns platform
association. Browsers expose no Wi-Fi join API, and Apple's iOS
`NEHotspotConfiguration` consent flow is unavailable on macOS. The current
implementation therefore uses a short-lived Swift/CoreWLAN + macOS Keychain
helper; Windows Credential Manager and Linux Secret Service adapters remain
separate platform work. The helper performs repeated read-only exact-SSID scans
inside one 15-second discovery window and at most one association. It never
repeats the BLE command, guesses a password or treats `7f01` as a credential-read
command. The credential-bearing 99-byte station-provisioning frame and fixed
100-byte AP-enable frame are separate reviewed payloads.
No reviewed BLE characteristic or response supplies the AP credential. That
does not prove a universal negative for every vendor build, but it means the
current plugin has no evidence-backed device-side credential acquisition path.
The owner also observed no explicit device/account pairing in the normal
LixelGo onboarding flow; this is consistent with a firmware-defined AP secret,
but does not establish account-wide authorization for arbitrary scanners.
Connection verification refreshes the session-scoped lease with the same
read-only BLE status operation. It does not write a characteristic, re-provision
Wi-Fi, scan the subnet, change a host route, or touch VPN configuration. The
later canonical MQTT session supplies the real data-plane connection and live
`DeviceInfo` identity check. A BLE lease observation is therefore not by itself
a claim that MQTT/RTSP is reachable.
## Consequences
- The UI now represents all three intended directions instead of disabled
placeholders.
- Quick Connect can temporarily remove the Mac from its previous Wi-Fi network.
macOS may require Wi-Fi/location permission for the process running Mission
Core.
- Quick Connect can temporarily remove the controlling host from its previous
Wi-Fi network. The operating system may require Wi-Fi/location permission.
- A new host needs a separate exact-firmware provider installation. The current
application does not obtain it from BLE and does not bootstrap it by itself.
- Automatic firmware download, iPhone extraction and hard-coded credential
delivery are explicitly rejected product routes. Windows/Linux adapters are
therefore not scheduled for this Quick Connect path.
- Direct Connect requires an already-running hotspot and a controller route;
Mission Core does not create or manage that hotspot.
- The application-control, START/STOP and raw-first acquisition protocol is
unchanged after a target address is admitted.
- Direct Connect and Mission Core Quick Connect remain explicitly pending one
owner-operated physical acceptance cycle each. Offline tests cannot promote
those claims.
- Direct Connect remains explicitly pending one owner-operated physical
acceptance cycle. Quick Connect AP activation and host association are
physically accepted only on the prepared K1/FW 3.0.2 Mac stand. Bridge is the
only accepted portable product path in this matrix.

View File

@ -0,0 +1,627 @@
# Mission Core handoff: AI worker, external perception and K1 Quick Connect
Date of factual refresh: 2026-07-19 21:15 MSK.
> 2026-07-20 superseding update: official firmware analysis proved that the
> exact K1 `3.0.2` AP credential is firmware-constant and that the SSID follows
> an identity/MAC rule. A prepared Mac completed AP-ready, CoreWLAN association
> and the normal control lifecycle. This did **not** solve clean-host bootstrap:
> the provider had been separately imported from the firmware archive. The
> owner closed this product branch, rejected automatic firmware download,
> iPhone extraction and hard-coding, retained the working UI/Keychain state and
> selected Bridge/direct-LAN as the continuing route. Canonical details:
> `docs/lab/004_K1_FW302_AP_CREDENTIAL_PROVIDER_20260720.redacted.md`.
This document is a transition context for a new engineering chat. It separates
committed/accepted work from current uncommitted K1 work and from future plans.
It contains operational addresses, ports, repository paths and public
fingerprints, but no password, private key, token, device secret, raw capture or
camera image.
## Executive state
Mission Core currently has two independently meaningful physical results:
1. The Mac/K1 control and archive path is accepted for one owner-controlled
XGRIDS LixelKity K1, firmware 3.0.2, Bridge/direct-LAN, handheld, GNSS without
RTK. Mission Core can run the reviewed MQTT bootstrap, START acquisition,
display live points, send explicit STOP, observe final READY/unbound and open
the durable Rerun/camera archive.
2. A replaceable Windows RTX 4090 worker is accepted for one recorded,
camera-only perception job. The Mac publishes a bounded immutable
`missioncore.compute-job/v1`; Windows verifies it, runs pinned YOLOX-S through
pinned Triton, publishes `missioncore.compute-result/v1`; Mission Core
revalidates and projects the optional result into the matching saved Rerun
recording.
This is not yet a live autonomous-vehicle stack. The worker has no K1 authority,
Triton is not exposed to the LAN, the current transfer is SSH/SCP laboratory
bootstrap, Tailscale is not installed, and live fan-out/fusion/tracking/free
space/control are not accepted.
The current K1 Quick Connect implementation is a separate uncommitted worktree.
It proved BLE AP activation, exact AP-ready observation and prepared-host native
macOS association/control. It remains laboratory-only because a clean host has
no autonomous evidence-backed credential source. The implementation and current
machine state are retained, but Bridge/direct-LAN is the product route.
## Canonical topology and authority
```text
XGRIDS K1
| BLE / MQTT / RTSP
v
Mission Core Edge on Mac
- sole K1 command authority
- sole acquisition lifecycle owner
- authoritative raw .k1mqtt + camera fMP4 archive
- durable session catalog and Rerun projection
|
| bounded immutable missioncore.compute-job/v1
| current lab transport: SSH/SCP
v
Mission Core AI Server Worker on Windows / RTX 4090
- verifies the complete job and every digest
- reconstructs and decodes only the admitted camera epoch
- calls local pinned Triton / pinned model
- atomically publishes missioncore.compute-result/v1
|
v
Mission Core Edge validates result and creates optional derived Rerun layer
```
The Windows machine is an executor, not a second Mission Core. It does not know
K1 credentials, IP, MQTT topics or RTSP endpoints and cannot discover,
provision, START or STOP the scanner. Derived inference never replaces or
rewrites the Mac source archive.
## Why the original architecture proposal was narrowed
The supplied architecture review was accepted as a boundary, not as a mandate
to install every named technology immediately. The following principles were
kept:
- one vendor owner for K1 control and source streams;
- immutable raw-first evidence and separately versioned derived data;
- replaceable external compute;
- local mission/safety authority rather than a remote GPU dependency;
- explicit separation of command/state, real-time flow and archive transport;
- declarative, reproducible deployment built mostly from mature components.
The following were deliberately deferred because they would precede the first
vertical evidence:
- custom universal Mission Core Node Runtime;
- custom cross-platform installer, node console, OTA, model package format and
generic scheduler;
- ROS 2 before a real onboard computer, robot state and actuator/autopilot
contract exist;
- BehaviorTree.CPP before a local mission executor is needed;
- Zenoh until direct/routed transport can be compared on identical data;
- MCAP as a replacement for the accepted K1 raw source;
- dora-rs as a control/safety foundation;
- LM Studio for the camera/LiDAR detector vertical.
Triton was selected for the first executor because it already provides pinned
model repositories, HTTP/gRPC, health, metrics and replaceable inference
backends. Mission Core continues to own its northern job/result contracts.
## Windows host identity and hardware
Live read-only inventory at the refresh time:
- host: `<worker-host>`;
- interactive/SSH account: `<worker-host>\<worker-user>`;
- OS: Windows 11 Pro x64, version 10.0.26200, build 26200;
- last boot: 2026-07-19 15:38:18 MSK;
- PowerShell: 5.1.26100.7705;
- CPU: Intel Core i9-13900KF, 24 cores / 32 logical processors;
- RAM: 95.85 GiB;
- GPU: NVIDIA GeForce RTX 4090, 24,564 MiB VRAM, compute capability 8.9;
- NVIDIA driver: Studio 610.47 WHQL;
- live GPU snapshot: about 10,229 MiB used, 13,910 MiB free, 41 C and 68.68 W;
- disk `C:`: NTFS, 1,861.99 GiB total, 1,700.86 GiB free;
- disk `D:`: NTFS label `DC`, 1,863.00 GiB total, 411.44 GiB free;
- Git: 2.53.0.windows.1;
- Codex CLI: 0.142.1;
- FFmpeg: 8.0.1 essentials build;
- native host Python: not installed; Windows Store aliases exist but do not
resolve to a Python runtime;
- Tailscale: not installed.
The driver was upgraded from 591.86 to 610.47 after the first Triton startup
showed CUDA Minor Version Compatibility mode. The official NVIDIA installer was
978,481,008 bytes, SHA-256
`59AC4A1659664AAD0A6FC525E5DF99B3FA76887BDE663F9E36E0E7EBB5DBA937`,
had a valid NVIDIA Corporation Authenticode signature and returned exit code 0.
After reboot, the compatibility warning disappeared and the full compute smoke
passed.
## WSL and Docker substrate
- WSL: 2.7.8.0;
- default distribution: Ubuntu 24.04, WSL version 2;
- kernel: 6.18.33.1-microsoft-standard-WSL2;
- Docker Desktop distribution: WSL2 and currently running;
- Docker Desktop: 4.78.0;
- Docker Engine/client: 29.5.3;
- Docker Compose: 5.1.4;
- Docker Linux VM: 32 CPUs and approximately 50.43 GB memory;
- storage driver: overlayfs;
- NVIDIA container runtime is registered; default runtime remains `runc` and
the Compose service explicitly requests all GPUs.
Docker Desktop's engine-owned image/layer VHDX remains on `C:`. Mission Core
application state is on `D:`. Moving Docker's VHDX was intentionally not made a
pilot prerequisite.
## Isolated Windows directory layout
Root: `D:\NDC_MISSIONCORE`.
```text
D:\NDC_MISSIONCORE\
README.md
workspace\
mission-core-compute\ versioned deployment and worker repository
runtime\
cache\ disposable downloads/build cache
derived\ decoded intermediates and immutable results
docker-cli\ isolated Docker CLI state for SSH automation
jobs\ bounded transferred compute jobs
logs\ service/task transcripts
models\ Triton model repository and weights
tmp\ disposable temporary data
secrets\ machine-local credentials, never committed
```
The root itself is not a Git repository. Only
`D:\NDC_MISSIONCORE\workspace\mission-core-compute` is versioned. Runtime
weights, jobs, results, caches, logs and secrets are outside Git.
## Windows worker repository
Repository:
`D:\NDC_MISSIONCORE\workspace\mission-core-compute`.
Current state:
- branch: `main`;
- HEAD: `a3c36c3 feat(perception): prove recorded Triton vertical`;
- previous commits:
- `3c62815 guard native CUDA runtime compatibility`;
- `d1043d0 qualify CUDA 13.3 driver path`;
- `8edd003 bootstrap Windows GPU compute node`;
- working tree: clean;
- Git remote: none configured.
Versioned content includes `compose.yaml`, `.env.example`, model config,
baseline/decision/acceptance docs, bounded PowerShell start/stop/test/handoff
scripts and `worker/run_yolox_recorded.py`.
No custom Windows service, host Python environment, LM Studio integration,
universal model loader or second Mission Core instance was installed.
## Active Triton deployment
Container: `mission-core-triton`, Compose project `mission-core-compute`.
Pinned image:
`nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794`
The local image reports Triton 2.70.0 and a roughly 22.7 GB image size.
Effective deployment properties:
- `gpus: all`;
- shared memory: 2 GiB;
- model control: explicit;
- automatic model config completion: disabled;
- strict readiness: enabled;
- server does not exit for one model error;
- HTTP 8000, gRPC 8001 and metrics 8002 enabled;
- every host binding is loopback-only: `127.0.0.1:8000-8002`;
- `D:\NDC_MISSIONCORE\runtime\models` is mounted read-only at `/models`;
- `no-new-privileges:true`;
- container restart policy: `no`;
- health check: HTTP `/v2/health/ready` every 5 seconds;
- current state: running and healthy;
- live, ready and metrics checks: HTTP 200;
- GPU metrics present;
- CUDA compatibility warning absent;
- deprecated model-config warning absent.
The committed `.env` contains only the loopback bind and pinned Triton image.
No credential is required for the current loopback-only service.
## Startup, shutdown and task bridge
The normal operator scripts are:
```powershell
Set-Location D:\NDC_MISSIONCORE\workspace\mission-core-compute
.\scripts\Start-MissionCoreCompute.ps1
.\scripts\Test-MissionCoreCompute.ps1
.\scripts\Stop-MissionCoreCompute.ps1
```
`Start-MissionCoreCompute.ps1` uses the node-local Docker CLI directory,
targets Docker Desktop's Linux engine pipe, creates loopback-only `.env` from
the example when absent, starts Docker Desktop when needed, performs Compose
pull/up and waits for Triton readiness.
Docker Desktop's Windows credential helper rejects the OpenSSH network-logon
token even for public image pulls. The reviewed workaround is not a second
deployment path: `Invoke-StartAsInteractiveUser.ps1` creates/starts the
on-demand `MissionCore-StartCompute` task in the logged-on user's limited
interactive token. It executes the same committed start script and writes
`D:\NDC_MISSIONCORE\runtime\logs\start-compute-latest.log`.
Scheduled task state:
- `MissionCore-DockerDesktop`: enabled logon trigger for the interactive user,
limited privilege, currently Running;
- `MissionCore-StartCompute`: no trigger, on-demand only, last result 0,
currently Ready;
- Triton Compose restart policy is `no`.
Therefore Docker Desktop starts at user logon, but Triton is not a fully
autonomous reboot-surviving service. It is started explicitly/on demand.
## Model repository
Accepted model: official YOLOX-S ONNX release 0.1.1rc0, Apache-2.0.
- model path: `runtime\models\yolox_s\1\model.onnx`;
- byte length: 35,858,002;
- SHA-256:
`C5C2D13E59AE883E6AF3B45DAEA64AF4833A4951C92D116EC270D9DDBE998063`;
- Triton platform: ONNX Runtime GPU;
- model version: 1 only;
- input: FP32 `[1,3,640,640]`, name `images`;
- output: FP32 `[1,8400,85]`, name `output`;
- one GPU instance;
- preprocessing: bilinear resize, top-left letterbox, BGR, pad 114;
- postprocessing: official YOLOX grid/stride decode, score 0.25, NMS 0.45,
COCO-80.
The model is useful only as a contract/timing smoke. It is not accepted for
navigation, obstacle avoidance, free-space or safety decisions.
## Accepted recorded job and result
Source Mission Core session:
`20260718T201659Z_viewer_live` / display name `TEST007`.
Accepted input:
- source: `sensor.camera.left`, physical codec epoch 1;
- job: `recorded-camera-fae25b92ecf645c2c9765dd4`;
- schema: `missioncore.compute-job/v1`;
- full input SHA-256:
`fae25b92ecf645c2c9765dd4b4963f23cf8c101f22fd5a2e14a104f0b447140d`;
- 59 digest-bound files, 4,567,569 package bytes;
- reconstructed H.264 stream: 4,549,458 bytes;
- stream SHA-256:
`bceca577f762fdb79c4e8901e5c2a2330fd07d24dc091b1b703c5af4010d8a56`;
- decode: H.264 High, 800x600, 56 frames, 5.497 seconds;
- synchronization: `host-arrival-best-effort`;
- frame timestamps are made strictly increasing from best-effort decode time;
this is not calibrated camera/LiDAR sensor time.
Accepted result:
- result:
`result-5484a72e81192b19f4e1da2a2dcfd10c876c277ff3e96c6d60cbc9d917b9f604`;
- schema: `missioncore.compute-result/v1`;
- detection schema: `missioncore.object-detections/v1`;
- 56/56 frames processed;
- 54 frames with detections;
- 56 generic COCO `person` detections;
- detection artifact: 23,802 bytes;
- detection SHA-256:
`71b291616e87bbdd2b7a295b1e00dac2ff3e60c627e63b72a27ba802c32a3ae0`;
- client-observed inference latency: mean 15.611 ms, p50 15.001 ms,
p95 23.728 ms, max 31.099 ms.
Triton currently reports 168 successful inference requests, 0 failed. Three
56-frame executions account for that count. The final exact repeated command
reused the already published content-addressed result and did not increase the
counter, proving idempotent reuse.
The first runner attempt completed inference but failed its post-inference EOF
guard. No partial result was published. The runner was corrected before the
accepted immutable result was produced.
## Rerun result projection on the Mac
Mission Core revalidates exact session/job/result binding, all digests,
timestamps, dimensions and frame count before projection. It builds one complete
RRF2 derived overlay, approximately 62.55 MB, cached privately by
result/recording identity.
The browser accepts the overlay on an isolated Rerun channel. Missing or invalid
perception returns no optional layer and never replaces the base point-cloud
recording. In TEST007 the `Распознавание` switch showed the real recorded left
camera frame with `person · 34%` Boxes2D; `Облако точек` returned to the base 3D
view.
Committed Mac work:
- `648d5bc feat(compute): add bounded camera job contract`;
- `31fc4f6 docs(perception): record external worker acceptance`;
- `2b53168 feat(perception): project recorded results into Rerun`;
- `ada2a55 docs(perception): record Rerun projection acceptance`.
Mac `main` is currently at `ada2a55`; `origin/main` remains at `75d2e5d`, so the
four perception commits are local and not pushed.
## Other active workloads on the RTX host
The GPU workstation is shared. These containers are not part of Mission Core:
- `sentinel-frigate`, image `ghcr.io/blakeblackshear/frigate:0.17.2-tensorrt`,
healthy, publishes host ports 5000, 8554, 8555 TCP/UDP and 8971;
- `sentinel-ollama`, image `ollama/ollama:latest`, publishes host port 11434 and
has no configured container health check.
Their current resource use contributes to the roughly 10 GiB observed GPU
allocation. Mission Core currently has no resource reservation, scheduler or
admission policy against these workloads. Long-epoch and live performance tests
must record this co-tenancy or isolate it deliberately.
## SSH access from the Mac
Windows OpenSSH was installed through official winget package
`Microsoft.OpenSSH.Preview` 10.0.0.0 because Windows Feature-on-Demand failed:
- `Add-WindowsCapability`: Access denied;
- DISM with `/LimitAccess`: `0x800f0912`, source files not found;
- `wuauserv` startup/ACL changes were denied by host policy.
Current Windows SSH facts:
- service: `sshd`, Running, Automatic;
- binary: `C:\Program Files\OpenSSH\sshd.exe`, version 10.0.0.0;
- listen: `0.0.0.0:22` and `[::]:22`;
- ED25519 host fingerprint:
`SHA256:LgvblT9h+5KJFtkDq6JWGUyvgpiFyeL6cztVVMY0ZQM`;
- public-key authentication: enabled;
- password authentication: also enabled;
- strict modes: enabled;
- max authentication attempts: 6.
The Mac connection uses a key and strict pinning, but the server itself is not
yet key-only. Disabling server password authentication is an explicit future
hardening action, not a completed fact.
Mac SSH alias in `~/.ssh/config`:
```sshconfig
Host mission-gpu
HostName <worker-host>.local
User <worker-user>
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
HostKeyAlias mission-gpu
UserKnownHostsFile ~/.ssh/known_hosts_mission_core
StrictHostKeyChecking yes
ServerAliveInterval 15
ServerAliveCountMax 3
```
Connection and safe read-only smoke:
```bash
ssh -o BatchMode=yes mission-gpu whoami
ssh -o BatchMode=yes mission-gpu \
'powershell.exe -NoProfile -NonInteractive -Command "$env:COMPUTERNAME"'
```
The dedicated known-hosts entry matches the Windows ED25519 fingerprint above.
No private key or password is stored in this repository or Ops.
## Windows network and firewall
Current physical Wi-Fi:
- SSID/profile: `<owner-lan-ssid>`;
- interface: `Беспроводная сеть`;
- category: Private;
- IPv4: `<worker-lan-ip>/<owner-lan-prefix>`;
- IPv4 connectivity: Internet.
VPN profile:
- name: `Сеть 5`;
- interface: `hidemy.name VPN OpenVPN Adapter`;
- category: Public;
- IPv4: `<vpn-assigned-ip>/<vpn-prefix>`.
Windows Firewall is enabled for Private and Public profiles. The active custom
SSH rule is:
- name: `MissionCore-SSH-From-Mac`;
- display name: `MISSION CORE SSH from Mac`;
- enabled, inbound allow, Private only;
- interface: `Беспроводная сеть` only;
- protocol/local port: TCP/22;
- remote address: `LocalSubnet`;
- local address: Any.
The broad MSI-created `OpenSSH SSH Server Preview (sshd)` inbound rule is
disabled. The current custom rule is broader than the original single Mac IP
because it admits the local subnet, but it remains confined to the Private Wi-Fi
interface and does not apply to the Public VPN profile.
Triton ports 8000-8002 listen only on 127.0.0.1. No Windows firewall rule exposes
them to the LAN. Consequently the Mac cannot yet call Triton directly; the
accepted recorded path transfers bounded artifacts through SSH/SCP and executes
the runner on Windows.
The current worker IPv4 `<worker-lan-ip>` previously appeared as a K1 target in
historical TEST007 evidence. Before concurrent K1/worker LAN exposure, reserve
non-conflicting addresses or prove topology separation. Do not expose Triton on
this address until that conflict and the source-restricted firewall contract are
reviewed.
## Current K1 Quick Connect state
The K1 plugin exposes three explicit local connection directions:
- Bridge/direct-LAN: K1 joins an existing shared network; physically accepted;
- Direct Connect/controller-hotspot: K1 joins the controller's network;
implemented/offline-verified, not physically accepted;
- Quick Connect/device-AP: K1 becomes the AP and a prepared Mac joins it;
physically accepted on the current host, retained as laboratory-only.
The reviewed LixelGO flow is:
1. keep one BLE connection open;
2. write one reviewed 100-byte AP-enable frame (99 zero bytes plus final 1);
3. poll `7f02` until the live status reports `WIFI_AP`, baseline address
`192.168.56.1` and the AP-ready byte changes from 0 to nonzero;
4. while BLE remains alive, use the native OS Wi-Fi API to scan exact SSID and
associate;
5. continue to K1 network/control only after association.
The earlier evidence session
`.runtime/mission-core/evidence/sessions/20260719T173132Z_viewer_k1_ap_association`
proved AP activation, AP-ready and native SSID discovery but stopped at the
secure prompt. A later prepared-host session
`.runtime/mission-core/evidence/sessions/20260719T220850Z_viewer_k1_ap_association`
completed one CoreWLAN association and admitted `192.168.56.1`; the normal UI
then completed control/acquisition. The operator disconnected afterward only to
restore the external chat route.
Final credential findings:
- LixelGO persists `WiFiAP_SSID` and `WiFiAP_Password` in a per-device profile;
- the Unity flow later passes those profile values to the native join path;
- no password derivation from DeviceInfo, serial, BLE UUID or AP SSID was found;
- official K1 `3.0.2` firmware contains one firmware-constant AP credential and
constructs the SSID from device identity with a MAC fallback;
- the reviewed BLE exchange enables the AP and reports readiness but exposes no
observed credential response;
- LixelGo login and the scanner AP are separate layers. The owner performed no
explicit hardware-to-account pairing, which is consistent with the local
firmware-defined credential, but does not prove universal account access;
- the successful Mission Core run depended on a separate firmware import into
this Mac's Keychain. A copied build on a clean Mac would fail before BLE.
The owner closed this branch. Do not download firmware during connection, seek
the value from iPhone, hard-code it, or present machine-local Keychain state as
portable automation. Keep the existing UI, implementation, Keychain/provider
state and evidence intact. Continue through Bridge/direct-LAN.
## Current Mac repository and runtime state
Repository:
`<mission-core-repository>`.
- branch: `main`;
- HEAD: `ada2a55`;
- `origin/main`: `75d2e5d`;
- local Mission Core HTTP service: `127.0.0.1:8000`;
- current health: service OK, one of one plugin runtimes ready;
- current local server PID at 2026-07-20 closure refresh: 97887;
- worker/perception commits are clean and local;
- K1 Quick Connect changes are uncommitted and make the worktree dirty.
The dirty set spans K1 docs/profile/frontend, native CoreWLAN helper,
BLE AP activation, host-network adapter and tests. It must not be reset or
cleaned because it contains the current physical Quick Connect implementation
and evidence-aligned fixes.
Latest validation of the current dirty tree:
- complete backend suite: passed;
- Ruff: clean;
- mypy: clean across 75 source files;
- frontend: 140 unit tests passed;
- TypeScript typecheck: clean;
- production Vite build: clean;
- Swift helper parse: clean;
- `git diff --check`: clean.
## What is accepted, partial and not started
Accepted:
- physical K1 Bridge control lifecycle START/live/STOP/READY;
- durable session catalog, point cloud and camera playback;
- bounded content-addressed recorded camera job;
- Windows digest verification and deterministic 56-frame decode;
- pinned Triton/YOLOX-S inference on RTX 4090;
- immutable content-addressed result and exact-repeat reuse;
- validated optional Rerun camera/Boxes2D projection;
- strict SSH host-key pinning and local-lab access path;
- reproducible Windows worker repository and operator scripts.
Partial/laboratory-only:
- Quick Connect is physically accepted on one prepared Mac but has no clean-host
credential bootstrap and is closed as a product route;
- Windows is a reproducible lab worker but Triton is loopback-only and manually
started;
- SSH is source-restricted by firewall and the Mac uses a key, but sshd still
permits passwords;
- the worker result is recorded-only and camera-only;
- GPU resource use is shared with unrelated Frigate/Ollama workloads.
Not accepted/not implemented:
- Direct Connect physical test;
- second K1 or firmware portability matrix;
- RTK, UGV, UAV, calibrated sensor time or camera/LiDAR extrinsics;
- long TEST007 epoch throughput/resource qualification;
- bounded live inference, queue/drop policy and recovery testing;
- direct authenticated LAN worker transport;
- Tailscale/routed worker transport and deny-by-default grants;
- point-cloud inference, camera/LiDAR fusion, tracking, segmentation or free
space;
- actuator/autopilot integration, mission executor or safety authority;
- production process supervision, operation journal, retention, encryption,
replication and resource scheduler;
- custom installer/node console/model marketplace, intentionally deferred.
## Recommended next gates in exact order
1. Continue K1 operation through the accepted Bridge/direct-LAN path. Preserve
the prepared Quick Connect implementation and evidence without extending it.
2. Commit the retained K1 mechanism/evidence/docs in logical commits when the
owner chooses; do not claim clean-host portability.
3. Qualify the 206-second TEST007 camera epoch on the same immutable model
profile, recording transfer, decode, inference, GPU memory, queue depth and
co-tenant effects.
4. Define a direct authenticated worker endpoint that preserves the current
`compute-job/result` schemas; reserve a non-conflicting worker address and add
a source-restricted firewall rule before any LAN port exposure.
5. Add bounded live fan-out from the sole Mac RTSP owner with explicit sampling,
latest-wins/drop counters, deadlines, stale state and failure isolation.
6. Kill/restart Triton, power off the worker and break transport while proving
K1 control/raw archive continue and the UI reports degraded perception.
7. Repeat the same endpoint contract through Tailscale; do not expose public
Triton ports.
8. Only after this evaluate tracking, segmentation/free-space and a LiDAR CPU
processor. Keep every result versioned and non-safety until calibrated and
separately accepted.
9. Evaluate ROS 2/BehaviorTree only when an actual onboard computer,
robot/autopilot and actuator/safety contract exist. Evaluate Zenoh only
against the measured direct transport.
## Canonical local sources for the next chat
- `docs/10_EXTERNAL_PERCEPTION_WORKER.md` — job/result and Rerun contract;
- `docs/adr/0014-bounded-external-perception-worker.md` — accepted architecture;
- `docs/lab/005_FIRST_RECORDED_PERCEPTION_20260719.redacted.md` — physical
recorded evidence;
- `src/k1link/compute/jobs.py` — compute job creation/validation;
- `src/k1link/compute/results.py` — result validation/Rerun projection inputs;
- `docs/adr/0013-k1-local-connection-matrix.md` — K1 connection directions;
- `src/k1link/device_plugins/xgrids_k1/ble/ap_activation.py` — reviewed AP
activation session;
- `src/k1link/host_network/wifi.py` — OS Wi-Fi profile boundary;
- `plugins/xgrids-k1/macos/associate_wifi.swift` — CoreWLAN and Keychain helper;
- this handoff — exact current machine/runtime/SSH/partial-acceptance snapshot.

View File

@ -0,0 +1,123 @@
# Lab 004 — K1 FW 3.0.2 Quick Connect mechanism and closure
- Date: 2026-07-20
- Device scope: one owner-controlled LixelKity K1
- Firmware scope: exact `3.0.2`
- Host: owner-controlled Apple-silicon Mac
- Safety: offline firmware analysis followed by one explicit BLE/AP/host-association cycle
- Secret policy: no AP credential value or digest appears in this report, Git, argv,
logs, manifests or fixtures
## Official artifact
The full K1 `3.0.2` archive was downloaded from the XGRIDS international
[K1 support page](https://www.xgrids.com/intl/support/download?page=K1). The
local private artifact was `1,252,252,754` bytes with SHA-256
`e5830feae54d586cdeda2824495d08598920dc9cf4541059d01f0efeb858a750`.
It remains under the ignored `sessions/firmware-analysis/` tree with mode
`0600`.
The archive contains a Rockchip `RKFW`/`RKAF` image. The reviewed partitions
were extracted read-only as sparse ext4 images:
- `rootfs.img`: 4,294,967,296 bytes, SHA-256
`b6e565fa1dd37d539a34b6fe92457bba6e793ab8db0ca216e1fe73a6602b975e`;
- `apps.img`: 230,801,408 bytes, SHA-256
`c4acb8d60b2e84d56487e5a110b41806fb2f4aa2f4b3cf6f1bc4d8b704b7e59a`.
No firmware was uploaded to the device and no filesystem was mounted
read-write.
## Recovered mechanism
The application partition contains
`/system/scripts/wifi_ap/wifi_ap.sh`. The script deletes prior Wi-Fi profiles,
creates NetworkManager connection `WIFI_AP`, configures WPA2/RSN/CCMP, assigns
`192.168.56.1/24`, installs one firmware-constant PSK and raises the AP.
The stripped AArch64 `lixel_nman` service invokes that script for AP mode. Its
reviewed control flow constructs the SSID as `XGR-` plus six device-identity
characters; if the expected ten-character identity is unavailable it falls
back to the final three MAC bytes formatted as six hexadecimal characters. The
owner-controlled scanner's observed BLE name and AP SSID match this rule.
Therefore, for exact K1 firmware `3.0.2`:
- the AP credential is not supplied by the AP-enable BLE frame;
- it is not recovered from an iPhone or synchronized Wi-Fi history;
- it is not per-device material;
- it is an exact-firmware credential profile defined by the scanner itself.
This does not authorize extrapolation to another K1 firmware or XGRIDS model.
The owner workflow is consistent with this separation. LixelGo required an
application account, but the operator performed no explicit scanner-to-account
pairing, ownership confirmation or per-device credential enrollment. The
available evidence therefore supports treating login as application access and
the AP credential as local firmware behavior. It does not prove that one
account is authorized for every XGRIDS device, and no such broader claim is
made.
## Provider implementation
Mission Core now identifies the source as
`xgrids.lixelkity-k1.quick-connect.fw-3.0.2.official-firmware.v1`.
The offline importer:
1. requires the exact reviewed archive SHA-256;
2. streams the nested Rockchip image without extracting the 4.57-GB container;
3. scans only the bounded apps-partition range;
4. requires exactly one syntactically valid NetworkManager PSK declaration;
5. writes the short-lived value to the host credential store through stdin;
6. zeroizes the mutable Python buffer and emits only redacted provider metadata.
On macOS the firmware-scoped material lives in Keychain. Before any device
write, the CoreWLAN helper materializes the selected SSID's opaque device
profile entirely inside Keychain. The browser/API receives neither the source
material nor a password. A missing source fails before AP activation.
This is a working laboratory provider, not a portable product bootstrap. A new
host needs the provider material installed before Quick Connect. Windows and
Linux would additionally need their own secure-store and Wi-Fi adapters.
## Physical acceptance
After the firmware-scoped provider was installed, one six-second BLE scan found
exactly one expected K1 candidate. Mission Core then performed:
1. one reviewed 100-byte AP-enable write;
2. one bounded wait for the canonical byte-51 AP-ready transition;
3. one exact-SSID CoreWLAN association using the Keychain-only profile.
The operation succeeded and admitted the reviewed K1 AP address
`192.168.56.1`. No credential was entered or displayed. The operator then
manually disconnected the Mac from the scanner AP to preserve the external
chat connection; that later host action does not invalidate the completed
association acceptance.
## Result and closure decision
The device-side mechanism and the prepared-host transport are physically
accepted: Mission Core can enable the K1 AP, observe canonical readiness,
associate one configured Mac and continue into the normal local control path.
The result does **not** establish a self-contained, transferable plugin. The
successful Mac had first received the firmware-scoped material through a
separate administrative import; copying the current application to a clean Mac
would fail closed with `credential-source-unavailable` before the BLE write.
On 2026-07-20 the owner rejected all remaining bootstrap variants for the
product path:
- downloading the 1.25-GB firmware archive during first connection;
- extracting or enrolling the value manually from an iPhone;
- embedding or deriving an unversioned password in application source;
- presenting the machine-local Keychain setup as cross-host automation.
Quick Connect remains in the interface and its current Mac Keychain/provider
state is retained. No evidence, credential material or private firmware files
were deleted. Development of credential acquisition is closed without claiming
that the password can be read from BLE: the reviewed BLE exchange contains AP
activation and status, but no observed credential response. The preferred and
accepted product topology is now Bridge/direct-LAN, where Mission Core sends
operator-selected LAN credentials to K1 through the already reviewed 99-byte
provisioning frame.

View File

@ -0,0 +1,111 @@
# External perception stack review — 2026-07-20
## Objective
Select mature, reusable components for Mission Core's K1 camera/LiDAR pipeline
instead of growing the LAB implementation into a bespoke stack by trial and
error. The target outputs are reviewed 2D/dense semantic labels, classified 3D
boxes in the point cloud, distance to objects, and a path to recorded and live
execution.
## Decision
Keep Rerun as the synchronized viewer and experiment evidence surface. Do not
treat it as the perception engine. Use CVAT as the human-review and
ground-truth control plane. Reuse NVIDIA's production inference patterns for
LiDAR and multimodal execution, adapting only K1 data loading, timestamps, and
factory calibration. Use open model frameworks as benchmark sources, not as a
second production runtime.
The shortest credible route is:
1. Review LAB E2 drafts in CVAT and freeze a versioned ground-truth subset.
2. Benchmark corrected/masked K1 images on that same subset.
3. Replace heuristic 3D cube construction with a LiDAR-native PointPillars
baseline.
4. Serve the selected 2D and 3D models through the existing Triton worker and
log outputs to Rerun.
5. Gate camera/LiDAR BEVFusion on proven synchronization, calibration, and
baseline metrics.
## Evidence from mature implementations
| Reference | Reusable practice | Mission Core use | Do not copy blindly |
| --- | --- | --- | --- |
| [Rerun](https://rerun.io/docs/overview/what-is-rerun) and its [examples](https://rerun.io/examples) | Time-aligned logging of images, point clouds, masks, 2D/3D boxes, trajectories, and model outputs | Viewer, debugging, playback, run evidence | It does not infer segmentation, depth, or boxes |
| [OctoSense](https://github.com/anthonytec2/OctoSense) | Camera rectification before inference; RGB/LiDAR timestamp alignment; semantic labeling; dynamic-object masks; calibrated LiDAR projection; dataset packaging | Copy the pipeline shape and QA gates | It is primarily a dataset-generation recipe, not a drop-in real-time Mission Core runtime |
| [DeepStream LiDAR 3D inference](https://docs.nvidia.com/metropolis/deepstream/7.1/text/DS_3D_Lidar_Inference.html) | PointPillars preprocessing, inference, postprocessing, and 3D box visualization in a production pipeline | First classified 3D-box baseline | Its sample data loader and calibration assumptions must be replaced with K1 adapters |
| [DeepStream multimodal fusion](https://docs.nvidia.com/metropolis/deepstream/8.0/text/DS_3D_MultiModal_Lidar_Sensor_Fusion.html) | Camera/LiDAR preprocessing and BEVFusion deployment | Later accuracy/robustness stage after LiDAR-only baseline | Too expensive as the first 3D fix; requires trustworthy synchronization and extrinsics |
| [MMDetection3D](https://github.com/open-mmlab/mmdetection3d) and [OpenPCDet](https://github.com/open-mmlab/OpenPCDet) | Reproducible 3D model/config baselines and pretrained checkpoints | Offline comparison and checkpoint selection | Do not introduce a parallel permanent serving stack beside Triton |
| [TAO auto-label](https://docs.nvidia.com/tao/tao-toolkit/text/data_services/auto-label.html) | Assisted labeling and model-driven annotation | Candidate accelerator after the first reviewed GT set exists | Auto-label output is still draft data, not accuracy evidence |
| [FiftyOne annotation integration](https://docs.voxel51.com/integrations/annotation.html) | Dataset curation, slice analysis, and CVAT round trips | Add when error analysis across many sessions becomes the bottleneck | Not needed to complete the first GT gate |
| [Isaac ROS nvblox](https://github.com/NVIDIA-ISAAC-ROS/isaac_ros_nvblox) | GPU mapping, reconstruction, and cost-map production | Later navigation/mapping consumer | It does not replace object detection or classified 3D boxes |
| [Isaac Sim camera sensors](https://docs.isaacsim.omniverse.nvidia.com/latest/sensors/isaacsim_sensors_camera.html) | Fisheye/OpenCV camera models and calibration-aware simulation | Synthetic regression cases after real-data metrics exist | Simulation is not a substitute for validating K1 factory calibration on recorded data |
## Selected stack boundaries
| Layer | Selected role | Current state | Gate before expansion |
| --- | --- | --- | --- |
| K1 ingestion | Preserve native timestamps, camera identity, point cloud, and vendor calibration | Recording works | Validate camera identity and projection on representative frames |
| Image preprocessing | Stable optical-circle mask plus calibration-derived correction profile | LAB E2 mask/draft path exists | Freeze profile ID and compare corrected versus raw on identical frames |
| 2D/dense perception | Generate prelabels, then evaluate against reviewed CVAT exports | LAB E2 drafts imported | Human two-pass review and class-level metrics |
| 3D detection | LiDAR-native PointPillars baseline | Next model integration | K1 point format adapter, coordinate convention test, reviewed 3D subset |
| Multimodal fusion | BEVFusion/DeepStream candidate | Deferred | Baselines, sync tolerance, extrinsic validation, GPU budget |
| Serving | Existing Triton worker | Running | Pin model/config artifacts and record per-stage latency/resource metrics |
| Visualization | Rerun | Running | Log reviewed labels and model outputs separately |
| Annotation/GT | CVAT `v2.70.0` | Deployed on D; LAB E2 tasks imported | Two-pass review; export with immutable manifest and hashes |
## Configuration practices to adopt
- Rectify or calibration-map the fisheye image before model inference when the
model expects a pinhole-like domain. Keep the raw frame alongside it.
- Apply a cached optical-circle mask/profile by camera identity; do not
recompute the mask for every frame.
- Synchronize camera frames to LiDAR timestamps explicitly and record the
chosen tolerance and dropped/unmatched frames.
- Keep 2D detections, dense semantics, LiDAR-native 3D detections, and fused
results as separate evidence streams. Fusion must not erase provenance.
- Project 3D boxes into the camera for validation, but do not construct final
3D boxes by simply extruding every 2D detection. That heuristic produces the
overlapping-cube failure observed in LAB E1.
- Record model identity, checkpoint hash, preprocessing profile, precision,
batch size, GPU memory, stage latency, wall time, and output hashes for every
material experiment.
- Treat every model-produced label as `ground_truth=false` until reviewed and
exported through the ground-truth gate.
## Next experiment gates
### LAB E2 review gate
- Review both imported tasks across the same 64 frames.
- First pass: correct labels, masks, and missed/false objects.
- Second pass: independent consistency review.
- Export reviewed instance and semantic annotations with manifest hashes.
### LAB E3 image baseline
- Compare raw-circle-mask, rectified, and correction-profile inputs on the
frozen reviewed subset.
- Select by class metrics and temporal stability, not by visual preference.
- Measure preprocessing, inference, postprocessing, and end-to-end time
separately.
### LAB E4 3D baseline
- Integrate a PointPillars-compatible K1 point loader.
- Lock coordinate axes, units, timestamps, and calibration version in the run
manifest.
- Evaluate classified 3D boxes on a small reviewed subset before any BEVFusion
work.
## Rejected shortcuts
- Replacing Rerun with OctoSense: wrong boundary; OctoSense supplies useful
pipeline patterns while Rerun remains the viewer.
- Moving the full stack to Omniverse: useful for simulation and synthetic data,
not the shortest path to recorded K1 accuracy.
- Treating current semantic/instance drafts as GT: invalidates all subsequent
model comparisons.
- Starting with BEVFusion: adds synchronization, calibration, conversion, and
runtime risks before the simpler LiDAR-native baseline is measured.

View File

@ -0,0 +1,223 @@
# LAB E10-N1 — Semantic-worker loss negative control
Date: 2026-07-22
State: completed; negative control accepted
Ground truth: no
Navigation/safety acceptance: no
Publication scope: laboratory negative control only
## Purpose and answer
E10-N1 tests a failure property required by the unmanned-system runtime:
> If the lower-rate semantic worker stops, does the 10 Hz detector/world-state
> path continue, does semantic freshness become stale explicitly, and does 3D
> fusion refuse to use an expired mask?
The answer is **yes for the accepted 60-second recorded source-paced control**:
- semantics was intentionally stopped after exactly 20 completed results;
- detector/tracker processed 601/601 frames at 10.010952 FPS with zero drops;
- world-state age p95 remained 90.268570 ms;
- semantic binding transitioned to 498 `stale` detector states;
- 3D fusion stopped after 96 fresh-mask frames and never resumed;
- freshness violations: zero;
- every negative-control acceptance check passed.
This proves fail-closed behavior for a controlled semantic-worker stop. It does
not yet prove recovery/restart, direct K1 transport or a process/container crash.
## Input and configuration identity
The camera, calibration, LiDAR pack, models and 60-second source interval are
identical to accepted LAB E10:
- session: `RAVNOVES00` / `20260720T065719Z_viewer_live`;
- source: `sensor.camera.right`, 800x600;
- source frames: 1,0001,600 inclusive;
- session time: 135.365857292195.334857292 seconds;
- source span: 59.969 seconds;
- calibration slot: `camera_1`;
- calibration SHA-256:
`05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`;
- LiDAR pack:
`e10-lidar-pack-5da0396d32a27f9d1ca537cc2e8a371d386078d6f0dc71737b78620992af9625`;
- detector: YOLOX-S/Triton every frame;
- semantics: EoMT/FP16 scheduled every fifth frame;
- semantic TTL: 750 ms;
- detector queue capacity: 2;
- semantic queue capacity: 1.
The only intentional change is:
```text
mode = semantic-loss-negative-control
stop_after_completed_results = 20
```
Profile SHA-256:
`eb2b114048336c273e77f3046d1eeac5187d27ec01e830de89ff201fb326c8c8`.
## Failure injection and expected behavior
The semantic worker exits cleanly immediately after publishing its twentieth
real mask. The source producer and detector consumer remain unchanged. The last
semantic result may be reused only until its 750 ms TTL expires.
After TTL expiration:
- semantic status must become `stale`;
- world-state delivery health becomes degraded where LiDAR exists;
- LiDAR association and cuboid generation must not execute with that mask;
- raw source scheduling and detector/tracker must continue to the final frame;
- the bounded semantic queue may discard scheduled work because its consumer is
intentionally absent, but this must not block the detector queue.
The result uses publication scope
`recorded-integrated-semantic-loss-negative-control-only`. Mission Core's E10
overlay store explicitly excludes that scope, so the negative control cannot
replace the accepted visual E10 result in `RAVNOVES00`.
## Result
Result ID:
`e10-integrated-perception-cf3b763731515120dc2714b6c3d7030496ed75057ce41ca88a1df62c21c4149c`
### Detector and world-state continuity
| Measurement | Result |
|---|---:|
| Detector scheduled / processed | 601 / 601 |
| Detector drops | 0 |
| Detector effective rate | 10.010952 FPS |
| Detector queue maximum / capacity | 1 / 2 |
| Replay wall / source span | 60.034251 / 59.969 s |
| World-state age mean | 63.686918 ms |
| World-state age p95 | 90.268570 ms |
| World-state age max | 140.979884 ms |
The main path remained source-paced after semantic loss. Its p95 stayed well
inside the frozen 175 ms laboratory budget.
### Semantic loss and freshness
| Measurement | Result |
|---|---:|
| Scheduled semantic frames | 121 |
| Completed before injected stop | 20 |
| Queue-overwritten after stop | 100 |
| Final bounded queue depth | 1 |
| Unavailable detector states | 5 |
| Fresh detector states | 98 |
| Stale detector states | 498 |
| Fresh coverage | 16.3062% |
Queue accounting is exact: 20 consumed + 100 overwritten + 1 final bounded
entry = 121 scheduled frames. These 100 semantic discards are the expected
effect of intentionally removing the consumer; detector drops remained zero.
### Fail-closed fusion
| Fusion state | Frames |
|---|---:|
| `fused` | 96 |
| `semantic-stale` | 428 |
| `semantic-unavailable` | 2 |
| `depth-unavailable-sync-gate` | 75 |
- accepted cuboid observations before loss became stale: 279;
- fused frames after the mask became stale: zero;
- fusion freshness violations: zero.
The 428 `semantic-stale` fusion states are fewer than the 498 stale detector
states because the depth synchronization gate takes precedence on 70 of those
frames. Neither path performs fusion with an expired semantic mask.
## Acceptance contract
| Check | Threshold | Result |
|---|---:|---:|
| Detector accounting | exact | pass |
| Detector rate | >= 9.5 FPS | 10.010952 |
| Detector drop fraction | <= 0.5% | 0% |
| World-state age p95 | <= 175 ms | 90.268570 ms |
| Semantic stop | exactly 20 results | 20 |
| Stale detector states | >= 450 | 498 |
| Semantic queue accounting | exact | pass |
| Fusion with non-fresh mask | zero | zero |
| Worker/producer errors | zero | zero |
All checks passed. `navigation_or_safety_accepted` remains false.
## GPU, memory and disk
Across 61 telemetry samples:
- GPU utilization: 58.72% mean, 90% p95, 93% max;
- board memory used: 13,151.64 MiB mean, 13,161 MiB max;
- power: 160.05 W mean, 211.02 W p95, 219.41 W max;
- temperature: 41.28 C mean, 42 C max;
- CUDA peak allocated: 2,107.925 MiB;
- CUDA peak reserved: 2,840.0 MiB;
- process peak RSS: 2,023.730 MiB.
The worker orchestrator used only `D:\NDC_MISSIONCORE`, enforced a 360 GiB
floor, and completed with approximately 376.88 GiB free after cleanup. Windows
C was not used. YOLOX was returned to its previous load state.
## Producer and artifact identity
- runner SHA-256:
`894e679c91d398b0084fa7894346c827e6a54b01e1841cf505a21be76a6c7188`;
- fusion runtime SHA-256:
`042136fffbc6e06ef62c6d223d5272ce48e0136651f430ab1d59729b32c4bd90`;
- orchestrator SHA-256:
`ac0ee42495839773a2de60090ebcb49eee979dc7ee3b8587662a3c59e19a4d03`;
- profile SHA-256:
`eb2b114048336c273e77f3046d1eeac5187d27ec01e830de89ff201fb326c8c8`;
- independent validator/renderer SHA-256:
`deb9e9f581b0868c9cf3ecae73b17b3253152c969d46089f11ad364efa6f6804`.
| Artifact | SHA-256 |
|---|---|
| `semantic-frames.jsonl` | `ad187a84335cefed40b2d2db7261c16ff053057d49554dc9097e1258a63fa81e` |
| `fusion-frames.jsonl` | `dbd8f203c41ed3f9ba0325b9a53227c7d25c66a34733caf0f2d687a18c26b5a6` |
| `world-state.jsonl` | `2a4e1e0e1bade8f07b12e880602c24fb4b4f8814b683ab01b407174592bd4230` |
| `transient-perception.npz` | `b653d8be9bc2474804268e9aa0a8c7dfdb1b2cacc779b515ef6b112aa9231e89` |
| `gpu-telemetry.jsonl` | `07708357da7a5c464db2015f897137deb730762c68e247ed5eeb40cee19ac108` |
| `run-report.json` | `06a8170ccdf561c51616050887a0ddd489894e36d22d35f0dd02ebbc700cf9e1` |
| `result.json` | `e23821353a3afdd3789b9fee57d58915ca56c002828453662ec30824a878d3f5` |
Independent validation accepted the result and confirmed its negative-control
publication scope. A store-selection regression proved that the UI continues to
select the accepted qualification result
`e10-integrated-perception-c6e0263...`, not E10-N1. The targeted regression
suite now passes 48 tests.
## Accepted scope
- controlled semantic-worker stop after real inference results;
- detector/tracker continuity at source pace;
- explicit unavailable/fresh/stale state transition;
- fail-closed LiDAR fusion after TTL expiration;
- bounded semantic queue behavior without detector backpressure;
- negative-control isolation from the user-facing Rerun result;
- D-only storage and disk-floor enforcement.
## Not accepted
- automatic semantic-worker restart or state recovery;
- hard process/container/GPU failure;
- direct K1 live transport;
- long-duration operation;
- target onboard hardware;
- navigation or safety use.
## Next gate
The next gate is the full `RAVNOVES00` stability run with the normal accepted E10
configuration. It must cover the complete approximately nine-minute camera
track, build a matching full-session LiDAR replay pack, preserve bounded queues
and memory, and keep the D free-space floor. After that, the recorded source
adapter can be replaced with direct K1 transport under the same contracts.

View File

@ -0,0 +1,443 @@
# LAB E10 — Integrated source-paced perception and world state
Date: 2026-07-22 (Europe/Moscow)
Run timestamp: 2026-07-21T22:30:03.785Z
State: completed; 60-second qualification accepted
Ground truth: no
Navigation/safety acceptance: no
## Purpose and answer
LAB E10 is the first Mission Core experiment that executes the current
perception branches as one source-paced pipeline rather than composing separate
offline outputs:
```text
recorded K1 camera at original timestamps
-> valid fisheye FOV
-> YOLOX/Triton detections at ~10 Hz
-> ByteTrack-style temporal IDs
+ concurrent EoMT semantics at ~2 Hz
+ factory KB4 camera/LiDAR projection
+ strict recorded LiDAR synchronization gate
-> point-supported distance and oriented 3D cuboids
-> clearance sectors and transient world state
-> Mission Core/Rerun recorded-session overlay
```
The answer is **yes for the accepted recorded 60-second interval on the RTX
4090 worker**:
- detector/tracker: 601/601 frames, 0 drops, 9.996120 FPS;
- semantics: 121/121 scheduled frames, 0 drops, 2.012530 FPS;
- fresh semantic coverage: 597/601 states, 99.3344%;
- LiDAR-fused states: 525;
- accepted point-supported cuboids: 760 observations;
- world-state age p95: 76.514918 ms;
- measured replay wall time: 60.123327 seconds for 59.969 source seconds;
- every frozen acceptance check passed.
This closes the integration gap left by E9. Semantic masks used by LiDAR
association were the actual newest in-memory EoMT results from the concurrent
worker. E6 fusion and E7-style world-state calculation were executed inside the
same critical replay loop. Per-frame masks were not synchronously encoded as
PNG during the measurement.
This result does **not** prove direct live K1 transport, long-duration stability,
forest-domain accuracy, target onboard compute performance or safety fitness.
## Immutable input identity
- Compute job: `recorded-camera-602ac89026ed12978619801d`
- Input SHA-256:
`602ac89026ed12978619801d4edea0cae24b5cc3afabd9f7af2858de6505a20e`
- Session title: `RAVNOVES00`
- Session ID: `20260720T065719Z_viewer_live`
- Camera source: `sensor.camera.right`
- Resolution: 800x600
- Factory calibration slot: `camera_1`
- Calibration content SHA-256:
`05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`
- Source frame selection: 1,0001,600 inclusive, 601 frames
- Session timeline: 135.365857292195.334857292 seconds
- Timeline SHA-256:
`06e3514228b488137b92c303cbca171154ede00368783e32107ed2c39c41baa6`
- Source span: 59.969 seconds
Valid-FOV identity:
- generation:
`valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2`;
- mask SHA-256:
`a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63`;
- valid pixels: 270,606 of 480,000.
The FOV mask is generated once and content-addressed. Pixels outside the usable
fisheye circle are excluded before model processing; no repeated per-frame mask
discovery occurs.
## LiDAR replay pack
The 60-second LiDAR input is a private immutable replay pack derived from the
accepted E6 synchronization evidence:
- pack ID:
`e10-lidar-pack-5da0396d32a27f9d1ca537cc2e8a371d386078d6f0dc71737b78620992af9625`;
- 601 camera timeline rows;
- 526 LiDAR rows passing the accepted strict synchronization gate;
- 1,182,292 total points;
- source coordinate system: K1 map frame;
- target camera: `sensor.camera.right` / factory `camera_1`;
- projection model: KB4, 800x600;
- temporal contract: accepted E6 nearest-host-arrival best effort;
- `lidar-pack.npz`: 9,629,449 bytes;
- pack SHA-256:
`72aa73340b20fcfaa21b330ef5b93b975c14a70e2cd16a57752d9952ff05ad9a`;
- manifest SHA-256:
`189a56660b69b05d39308b8dfa6c5ad8ed8812e870a218188181787cc6ea00ba`.
The pack is a replay optimization only. It does not change camera/LiDAR
calibration, points, timestamps or the E6 synchronization decision.
## Models and worker runtime
Detector/tracker branch:
- official Megvii YOLOX-S ONNX release `0.1.1rc0`;
- COCO-80 classes;
- model SHA-256:
`c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063`;
- ONNX Runtime GPU through the existing Triton container;
- ByteTrack-style two-stage IoU tracking inherited from E5/E8/E9.
Semantic branch:
- `tue-mps/cityscapes_semantic_eomt_large_1024`;
- revision: `8d6b6d1a3f7b50d441afd7d247c2ed10db186e8f`;
- weights: 1,276,175,488 bytes;
- weights SHA-256:
`c265da9a74f58f5c3f4826d23ca4ca78beac0b106cca5842beca61580de5b782`;
- batch size one, FP16 autocast;
- Cityscapes output mapped to the existing Mission Core 015 taxonomy.
Worker runtime:
- GPU: NVIDIA GeForce RTX 4090;
- container: `nvcr.io/nvidia/tritonserver:26.06-py3`;
- Python 3.12.3;
- PyTorch 2.13.0+cu130;
- Transformers 4.57.6;
- NumPy 1.26.4;
- SciPy 1.16.3.
## Frozen scheduling and fusion configuration
Detector path:
- every source frame, nominal 10 Hz;
- bounded latest-wins queue capacity: 2;
- observed maximum queue depth: 1;
- overflow drops: 0.
Semantic path:
- every fifth source frame, nominal 2 Hz;
- bounded latest-wins queue capacity: 1;
- TTL: 750 ms;
- overflow drops: 0;
- states before the first completed mask are explicitly `unavailable`.
LiDAR association:
- nearest-depth KB4 projection buffer;
- horizontal box inset 5%; top inset 4%; bottom inset 2%;
- semantic support required for person, bicycle, motorcycle and vehicle groups;
- class-dependent spatial clustering radius: 0.9 m for person/two-wheelers,
1.5 m for vehicles;
- minimum support points: 3 for person/two-wheelers, 5 for vehicles;
- depth-cluster split: max(0.5 m, 6% of depth);
- same-group NMS IoU: 0.55;
- distance history: 5 frames;
- distance innovation gate: max(1.5 m, 25%);
- oriented cuboid dimensions use robust p05p95 surface extents and explicit
class-dependent plausibility limits.
World state:
- velocity history: 1.0 second;
- clearance: 72 azimuth sectors;
- considered range: 0.530 m;
- obstacle height above estimated ground: 0.23.0 m;
- forward clearance half-angle: 15 degrees.
## Frozen acceptance contract
| Check | Threshold | Result |
|---|---:|---:|
| Detector effective rate | >= 9.5 FPS | 9.996120 FPS |
| Detector drop fraction | <= 0.5% | 0% |
| Semantic effective rate | >= 1.8 FPS | 2.012530 FPS |
| Semantic drop fraction | <= 5% | 0% |
| Semantic completion age p95 | <= 400 ms | 179.682194 ms |
| Fresh semantic coverage | >= 90% | 99.3344% |
| World-state age p95 | <= 175 ms | 76.514918 ms |
| LiDAR-fused frames | >= 450 | 525 |
| Accepted cuboid observations | >= 100 | 760 |
| Failures | zero | zero |
All checks were frozen before the qualification output and all passed. The
result remains explicitly `navigation_or_safety_accepted=false`.
## Timing result
### Critical detector/fusion/world-state path
| Stage | Mean | p50 | p95 | Max |
|---|---:|---:|---:|---:|
| World-state age from source deadline | 56.074 ms | 52.017 ms | 76.515 ms | 118.755 ms |
| Camera decode | 28.826 ms | 28.332 ms | 33.181 ms | 51.831 ms |
| Detector inference path | 23.442 ms | 20.298 ms | 36.127 ms | 48.794 ms |
| Detector queue wait | 0.087 ms | 0.066 ms | 0.155 ms | 5.694 ms |
| LiDAR projection | 0.411 ms | 0.358 ms | 0.723 ms | 4.258 ms |
| Semantic/LiDAR association | 1.649 ms | 1.391 ms | 3.858 ms | 12.513 ms |
| Clearance | 0.187 ms | 0.175 ms | 0.394 ms | 0.667 ms |
| World-state serialization/calculation | 0.088 ms | 0.076 ms | 0.226 ms | 0.586 ms |
The critical path stayed below its 175 ms laboratory ceiling without frame
drops. Decode plus detection dominates the 10 Hz path; LiDAR projection,
association, cuboid generation, clearance and world-state computation consume
only a few milliseconds at p95 on this input.
### Semantic branch
| Stage | Mean | p50 | p95 | Max |
|---|---:|---:|---:|---:|
| Completion age from source deadline | 155.067 ms | 150.439 ms | 179.682 ms | 384.761 ms |
| Processing after dequeue | 124.748 ms | 122.595 ms | 138.430 ms | 353.011 ms |
| EoMT forward | 98.849 ms | 96.101 ms | 112.343 ms | 309.115 ms |
| Processor | 9.647 ms | 9.347 ms | 11.958 ms | 31.297 ms |
| Host to device | 5.680 ms | 6.063 ms | 8.184 ms | 11.627 ms |
| Model postprocess | 7.451 ms | 6.703 ms | 11.166 ms | 12.413 ms |
| Valid-FOV fill | 1.927 ms | 1.882 ms | 2.249 ms | 3.075 ms |
| Queue wait | 0.145 ms | 0.155 ms | 0.194 ms | 0.308 ms |
The semantic branch is deliberately asynchronous. A semantic outlier therefore
does not block detector admission or raw recording. The newest completed mask
is bound only to current/future detector states and only while its TTL remains
valid.
## Fusion and world-state output
Fusion states across 601 detector rows:
- `fused`: 525;
- `depth-unavailable-sync-gate`: 75;
- `semantic-unavailable`: 1.
The LiDAR pack exposes 526 strict-sync frames. One of those occurred before the
first semantic result completed, explaining 525 actual fused states without
inventing a mask.
Cuboid association counts:
- accepted point-supported cuboid observations: 760;
- rejected, fewer than five clustered points: 996;
- rejected, no semantic LiDAR support: 574;
- rejected, distance innovation: 172;
- rejected, fewer than three clustered points: 45;
- rejected, implausible cuboid: 14.
The 760 value is a count of accepted per-frame cuboid observations, not 760
unique physical objects. Cuboids bound the visible LiDAR-supported surface. They
must not be interpreted as complete object volume or ground truth.
## GPU, memory and disk
Across 61 one-second telemetry samples:
- GPU utilization: 45.57% mean, 61% p95, 65% max;
- board memory used: 13,119.30 MiB mean, 13,128 MiB max;
- board memory utilization: 19.51% mean, 26% max;
- power: 154.70 W mean, 166.75 W p95, 178.45 W max;
- temperature: 43.87 C mean, 46 C max;
- EoMT process CUDA peak allocated: 2,107.925 MiB;
- EoMT process CUDA peak reserved: 2,840.0 MiB;
- process peak RSS: 2,022.535 MiB.
This shows usable compute headroom on the laboratory RTX 4090 for this frozen
configuration. It says nothing yet about the final onboard GPU/accelerator.
That hardware must receive its own experiment and budgets.
Task-controlled worker storage remained under `D:\NDC_MISSIONCORE`:
| Disk point | Free bytes | Approx. free GiB |
|---|---:|---:|
| Before measured replay | 404,946,915,328 | 377.136 |
| After measured replay | 404,933,578,752 | 377.124 |
| Enforced floor | 386,547,056,640 | 360.000 |
Temporary decoded frames were cleaned from the exact run-owned D path. The
YOLOX model state was restored after the run. Triton, Frigate and Ollama were
left running. Windows C was not used or modified.
## Result artifacts and integrity
Published result:
`e10-integrated-perception-c6e0263b9e18be7e4f447597065b5ff93b8511463e8f453800f98a61f25c37df`
| Artifact | Bytes | SHA-256 |
|---|---:|---|
| `semantic-frames.jsonl` | 55,025 | `16cb53c7df8491b6620d0483f5004953d0aa54e83198bb101ae357c06e34be89` |
| `fusion-frames.jsonl` | 1,418,427 | `1b28baf9dfc6c8a793f60233f5ffd03a3e40f63a4dfac7a97bff2813f5bd967d` |
| `world-state.jsonl` | 1,524,066 | `9bdab9c1749fca98797ab428bd94f3e1b5a80b7977f013f323283ce9559040ee` |
| `transient-perception.npz` | 1,106,484 | `7e0d0761584f4700e55caf31965d53de1c13b4f8a17c7cfbd2279af4c5f4cabd` |
| `gpu-telemetry.jsonl` | 13,704 | `e036cdd35addc96152c3c534e75e249ac25b18b588cecabb30a8729d36d80f3c` |
| `run-report.json` | 13,563 | `a053d629ea1b48e4a039d7277e0753108926e6efa0000971bc89008eac66ec26` |
| `result.json` | immutable manifest | `48ac8865ff629b00e56ca90cb4a3a68b161f06275465a86ae7973808f8472c2b` |
Accepted producer identities:
- runner SHA-256:
`3f667e291f18bcccd58d0ced391e23e0cca05ba3cdeb0da50b0f2fb814c4ff70`;
- fusion runtime SHA-256:
`4d4d9d7fcf8f3b86ac84228902df1948aafc2f48643c53f77250e77bf9854d5d`;
- profile SHA-256:
`42e2a49945e65daf86e7891794e30ef2c5182b71d073c76ae764c03ea2501589`;
- accepted run orchestrator SHA-256:
`8bc939e9a5e94707cd30bf7d948fed893087c03d3345e31c20c853b237ecde76`.
After acceptance, only the orchestrator's human-readable free-space line was
changed from pipeline output to host output so future variable assignment does
not capture the label. Current orchestrator SHA-256 is
`ac0ee42495839773a2de60090ebcb49eee979dc7ee3b8587662a3c59e19a4d03`.
This post-run correction does not alter or relabel the accepted E10 result.
The local E10 Python sources were subsequently normalized by the repository's
Ruff formatter and passed the same 47-test regression suite. The accepted
worker staging directory remains the immutable source of the producer hashes
above. Current local source hashes for the next run are:
- fusion runtime:
`042136fffbc6e06ef62c6d223d5272ce48e0136651f430ab1d59729b32c4bd90`;
- LiDAR-pack builder:
`e75875342f11a0573eb5eef106f10ff9545debf6c012e717c1df281d479b746b`;
- worker runner:
`9e434a82d370d1ecf079d76cec5c7a4cf955ca631651862eacb8572b94e33c8b`;
- Mission Core validator/renderer:
`260a03c623bcab15c6dafd63caa36514e9e35a40e0e6bd4d6551eb3db832c058`.
Durable local evidence root:
```text
.runtime/compute-experiments/e10/worker-results/
```
Worker result root:
```text
D:\NDC_MISSIONCORE\runtime\derived\
e10-integrated-perception-c6e0263b9e18be7e4f447597065b5ff93b8511463e8f453800f98a61f25c37df
```
## Independent validation and regression checks
The independent E10 validator confirmed:
- content-addressed result identity and artifact hashes;
- exact job/session/source/calibration/model/profile/runner binding;
- detector and semantic queue accounting;
- ordered source timestamps and semantic stride;
- semantic binding never comes from a future source frame;
- unavailable states contain no invented perception payload;
- LiDAR pack identity, size and hash;
- every frozen acceptance check.
The standalone E10 fusion runtime was also run against the accepted E6
precomputed E4/E5 inputs. It reproduced E6 exactly: 526 fused frames and 918
accepted cuboid observations. This is the control that the E10 integration did
not silently change the established E6 geometry algorithm.
Targeted automated tests passed: 47 tests across E10, compute-result discovery,
session API, E9, and E6. Direct Rerun rendering produced a valid 58,342,680-byte
`RRF2` stream. The Mission Core perception endpoint returned HTTP 200 with a
58,342,936-byte `RRF2` response.
## Mission Core / Rerun publication
Mission Core now discovers the newest accepted E10 result for the active
recording before falling back to E6 or legacy 2D results. The generated Rerun
stream contains:
- camera frames;
- semantic masks;
- 2D tracked detections;
- raw map-frame point cloud;
- LiDAR support points;
- oriented point-supported 3D cuboids;
- range and world-state evidence.
Visual acceptance was completed in the local Control Station:
1. `Наблюдение` -> `Пространственная сцена`;
2. saved session `RAVNOVES00`;
3. three active sources loaded;
4. original camera and `Сегментация · камера right` played together on the
common session timeline;
5. real point cloud and trajectory remained visible behind the camera panels;
6. playback time advanced normally, proving the result is not a single image.
The E10 perception overlay covers only its qualified 60-second source interval.
The underlying saved session remains the full 8:55.718 recording.
## Accepted scope
- actual concurrent YOLOX/Triton and EoMT/PyTorch execution;
- actual current in-memory semantic mask consumed by LiDAR association;
- source-paced detector/tracker near 10 Hz and semantics near 2 Hz;
- bounded latest-wins queues with zero measured drops;
- factory KB4 projection using XGRIDS calibration;
- point-supported distance and oriented 3D cuboids;
- clearance and transient world-state publication in the critical loop;
- 60-second latency, GPU, memory and disk evidence;
- playback in Mission Core/Rerun for the recorded session;
- D-only worker storage and restored service/model state.
## Not accepted
- direct K1 MQTT/camera transport into the perception scheduler;
- the full 8:55.718 session or multi-hour continuous operation;
- hardware-clock camera/LiDAR synchronization proof;
- failure behavior after semantic-worker loss;
- forest/off-road model quality or class coverage;
- final onboard compute rate, thermals or power;
- complete-object 3D geometry;
- navigation or safety use.
## Next gates
The next work is aimed at the unmanned-system operating contour, not cosmetic
offline quality:
1. **Semantic-loss negative control.** Kill or stall the semantic branch during
source-paced replay and prove detector/raw recording continue, freshness
becomes stale/unavailable, and no false fresh mask is published.
2. **Full RAVNOVES00 stability run.** Build a full-session immutable LiDAR pack,
run the same frozen pipeline for approximately nine minutes, verify bounded
memory/queues, zero recording interference and D free-space floor.
3. **Direct K1 transport.** Replace the recorded camera/LiDAR source adapter
with the existing live K1 ingestion while preserving the accepted scheduler,
freshness and world-state contracts.
4. **Target onboard worker qualification.** Repeat the same gates on the actual
deployable compute hardware; optimize models only against explicit rate,
latency, thermal and power budgets.
5. **Domain quality branch.** In parallel with runtime work, create forest and
off-road evaluation data for classes, traversability and geometry. This must
not block proving the live transport and scheduling contour.
Item 1 was subsequently completed and accepted as
`LAB_E10_N1_REPORT_2026-07-22.md`: detector continuity remained 10.010952 FPS
with zero drops, 498 states became explicitly stale, and fusion recorded zero
freshness violations after the controlled semantic stop. The immediate gate is
therefore the full-session run. No model upgrade or UI polish is justified
before long-duration boundedness is measured.

View File

@ -0,0 +1,369 @@
# LAB E11 — Full-session integrated perception stability qualification
Date: 2026-07-22
Accepted result creation time: 2026-07-22T06:27:51.941Z
Laboratory status: **accepted**
Navigation or safety status: **not accepted**
## Objective
LAB E11 tested whether the integrated Mission Core perception contour proven on
the 60-second LAB E10 interval remains bounded and source-paced across the
complete recorded right-camera epoch from `RAVNOVES00`.
The qualification retained the intended onboard architecture:
```text
recorded camera at original source timestamps
-> bounded latest-wins detector queue
-> YOLOX-S + ByteTrack-compatible tracking at camera rate
-> bounded sampled semantic queue
-> EoMT Cityscapes semantics at 1/5 camera rate
-> factory camera_1 KB4 camera/LiDAR projection
-> visible LiDAR-supported 3D cuboids and distance
-> clearance sectors and timestamped world state
```
This was not an offline maximum-quality render. The replay speed was exactly
`1.0`, queues were bounded, and acceptance required the worker to keep up with
the recorded source without detector loss.
## Result summary
Accepted immutable result:
`e10-integrated-perception-7613b09021455ddec941144224142f7879e3c6ecac5b2370ea8a7d4cb01918ba`
Worker result root:
`D:\NDC_MISSIONCORE\runtime\derived\e10-integrated-perception-7613b09021455ddec941144224142f7879e3c6ecac5b2370ea8a7d4cb01918ba`
Local verified mirror:
`.runtime/compute-experiments/e10/worker-results/e10-integrated-perception-7613b09021455ddec941144224142f7879e3c6ecac5b2370ea8a7d4cb01918ba`
| Measure | Result | Frozen gate | State |
|---|---:|---:|---|
| Source frames | 4,489/4,489 | exactly 4,489 | pass |
| Detector drops | 0 | 0 | pass |
| Detector effective rate | 10.004402 FPS | >=9.5 FPS | pass |
| Semantic results | 898/898 scheduled | accounting exact | pass |
| Semantic drops | 0 | <=5% | pass |
| Semantic effective rate | 2.001326 FPS | >=1.8 FPS | pass |
| Fresh semantic coverage | 99.754956% | >=90% | pass |
| Semantic completion age p95 | 267.211864 ms | <=400 ms | pass |
| World-state age p95 | 102.714712 ms | <=175 ms | pass |
| LiDAR-fused frames | 3,917 | >=3,500 | pass |
| Accepted cuboid observations | 6,157 | >=500 | pass |
| Inference/producer failures | 0 | 0 | pass |
| Fusion with non-fresh semantics | 0 | 0 | pass |
The complete 448.623-second camera interval was replayed in 448.702 seconds.
This is source-paced operation, not an accelerated batch claim.
## Frozen inputs
### Recorded source
- UI session alias: `RAVNOVES00`;
- session ID: `20260720T065719Z_viewer_live`;
- compute job: `recorded-camera-602ac89026ed12978619801d`;
- job input SHA-256:
`602ac89026ed12978619801d4edea0cae24b5cc3afabd9f7af2858de6505a20e`;
- source: `sensor.camera.right`;
- codec epoch: `1`;
- resolution: 800x600;
- source frame indices: 04,488 inclusive;
- camera timeline: 35.421857292484.044857292 seconds;
- timeline SHA-256:
`f91dad0a3cc998f4250be794b48f437943b3c60edccbe7e5ceed59030854a807`;
- job payload: 363,235,615 bytes;
- raw MQTT evidence: 133,572,722 bytes.
### Factory calibration
- slot: `camera_1`;
- content identity SHA-256:
`05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`;
- projection: factory KB4 intrinsics plus camera/LiDAR extrinsic;
- LiDAR coordinate source: K1 map frame.
No calibration was fitted or modified during LAB E11.
### Full LiDAR replay pack
Immutable pack:
`e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b`
- frames: 4,489;
- frames with accepted LiDAR and pose: 3,928;
- strict-sync coverage: 87.502785%;
- point observations: 9,207,270;
- compressed payload: 72,996,000 bytes;
- payload SHA-256:
`0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944`;
- builder SHA-256:
`bc81a1aa8759a750480745a52af34f4c700261e8bfbf5d615e8c6e5fa96bb69c`;
- accepted E6 temporal-profile SHA-256:
`b8d3a6f043fde54ffd32ea05bfe580e2be7aace1a23cdbf7e266eb3325d147b2`;
- accepted E6 result:
`e6-fusion-b4e4226674a66f6196c033785eb307c255a7bdd8493ef9809dfb1d6e5bd68eaa`.
The pack uses the unchanged E6 temporal policy:
- clock source: recorded host monotonic arrival;
- binding: nearest host-arrival best effort;
- maximum camera/LiDAR delta: 100 ms;
- maximum point/pose delta: 100 ms.
The pack is a replay optimization and immutable input. It does not improve or
reinterpret synchronization accuracy.
## Frozen software configuration
### Integrated scheduling profile
Profile: `e10_full_session_profile.json`
SHA-256: `73dd8472b59c9ad48b781600c89eadb0aab12cb6ef59c4572ce5a8e17dcd4472`
- mode: `full-session-qualification`;
- replay speed: 1.0;
- detector queue: latest-wins, capacity 2;
- semantic queue: latest-wins, capacity 1;
- semantic scheduling: every fifth camera frame;
- semantic TTL: 750 ms;
- detector loss gate: exactly zero;
- required source selection: frames 04,488 and at least 448 seconds.
### Producer identities
| Component | SHA-256 |
|---|---|
| Integrated runner | `5548cc607f245cd1c01c85d6b821fdf5b91e512ee9b2f8ad6862175c93b7628c` |
| Fusion runtime | `844a3a3ecc56313be2f96a6b861abd8f55100ec7427a72379e52a55fbd4c5749` |
| PowerShell orchestrator | `ac0ee42495839773a2de60090ebcb49eee979dc7ee3b8587662a3c59e19a4d03` |
| Detector profile | `819bdfb0da521d187ec9ce3d3039a0ff75daa0ab473fb3d8e5fa92db9c1ddca1` |
| Semantic profile | `ea583966bc3409f5cf563cbf4fad05e366907e67187082eb692aff53d9f5d875` |
Worker runner generation:
`D:\NDC_MISSIONCORE\runtime\derived\e10-runner-5548cc607f24-ac0ee4249583`
### Models
Detector:
- YOLOX-S, COCO-80;
- ONNX SHA-256:
`c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063`;
- Triton config SHA-256:
`5795c737a7935a655961b069e8404d336d891f9762fb6dffb93956a076479604`.
Semantic model:
- `tue-mps/cityscapes_semantic_eomt_large_1024`;
- revision `8d6b6d1a3f7b50d441afd7d247c2ed10db186e8f`;
- `model.safetensors` SHA-256:
`c265da9a74f58f5c3f4826d23ca4ca78beac0b106cca5842beca61580de5b782`;
- precision: FP16 autocast;
- batch size: 1.
Valid-FOV mask:
- generation:
`valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2`;
- mask SHA-256:
`a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63`;
- admitted pixels: 270,606/480,000.
## Worker and resource envelope
- GPU: NVIDIA GeForce RTX 4090;
- container: `nvcr.io/nvidia/tritonserver:26.06-py3`;
- Python: 3.12.3;
- PyTorch: 2.13.0+cu130;
- Transformers: 4.57.6;
- NumPy: 1.26.4;
- SciPy: 1.16.3.
GPU telemetry, 449 one-second samples:
| Measure | Mean | p95 | Max |
|---|---:|---:|---:|
| GPU utilization | 67.70% | 92% | 96% |
| Board GPU memory used | 13,151.48 MiB | 13,161 MiB | 13,161 MiB |
| GPU-memory controller utilization | 16.40% | 33% | 34% |
| Power | 207.34 W | 215.51 W | 229.74 W |
| Temperature | 48.46 C | 53 C | 54 C |
Process and CUDA measurements:
- process peak RSS: 2,944.93 MiB;
- CUDA peak allocated by the runner: 2,107.93 MiB;
- CUDA peak reserved by the runner: 2,840 MiB.
The board-memory figure includes the existing Triton/model context and is not
equivalent to runner-only allocation.
## Latency detail
| Stage | Mean | p95 | Max |
|---|---:|---:|---:|
| Frame decode | 34.198 ms | 47.488 ms | 84.767 ms |
| Detector | 32.099 ms | 47.759 ms | 70.328 ms |
| Detector queue wait | 0.230 ms | 0.202 ms | 33.917 ms |
| LiDAR projection | 0.491 ms | 0.940 ms | 7.396 ms |
| Object association | 2.176 ms | 5.795 ms | 20.093 ms |
| Clearance | 0.236 ms | 0.577 ms | 4.740 ms |
| World-state construction | 0.117 ms | 0.333 ms | 4.562 ms |
| End-to-end world-state age | 71.634 ms | 102.715 ms | 189.403 ms |
Semantic inference:
| Stage | Mean | p95 | Max |
|---|---:|---:|---:|
| Processing | 191.693 ms | 209.808 ms | 448.629 ms |
| Forward | 156.680 ms | 170.785 ms | 414.754 ms |
| Completion age | 228.427 ms | 267.212 ms | 477.220 ms |
| Semantic queue wait | 0.186 ms | 0.279 ms | 4.548 ms |
The acceptance contract uses p95 rather than the isolated maximum. Five startup
frames had no semantic state and six later frames observed a stale state; no 3D
fusion was asserted on any non-fresh semantic state.
## Fusion detail
- LiDAR available: 3,928 frames;
- fused with fresh semantics: 3,917 frames;
- fused share of LiDAR-available frames: 99.719959%;
- depth unavailable under the frozen sync gate: 561 frames;
- semantic unavailable: 5 frames;
- semantic stale: 6 frames;
- accepted cuboid observations: 6,157;
- fusion freshness violations: 0.
Rejections are expected diagnostic outcomes:
| Reason | Count |
|---|---:|
| Insufficient 5-point support | 8,268 |
| No semantic LiDAR support | 4,892 |
| Distance innovation gate | 1,045 |
| Insufficient 3-point support | 142 |
| Implausible cuboid extent | 24 |
These counts describe per-frame object observations, not unique physical
objects.
## Disk safety and storage behavior
The worker orchestrator was confined to `D:\NDC_MISSIONCORE`. Drive C was not
read or written by this task.
- configured D free-space floor: 360 GiB;
- preflight free space: 376.506 GiB;
- calculated additional reserve requirement: 11.834 GiB;
- after full stream reconstruction: 376.127 GiB;
- after 4,489 PNG extraction: 373.578 GiB;
- after publication, before temporary cleanup: 373.347 GiB;
- after cleanup and model-state restoration: 376.223 GiB.
Temporary frames and the reconstructed camera stream were deleted by the
orchestrator. No raw recording, prior result, model, cache, or accepted runner
generation was deleted.
Known orchestrator timings:
- frame preparation: 31.425 seconds;
- source-paced replay: 448.702 seconds;
- total orchestrator wall time, including model validation/loading and cleanup:
600.623 seconds.
## Published artifacts
| Artifact | Bytes | SHA-256 |
|---|---:|---|
| `semantic-frames.jsonl` | 404,380 | `49b1a859924b26ab5f550ca3c6d6ec907c32c0ef01870c270c33ef3eb103db53` |
| `fusion-frames.jsonl` | 11,322,702 | `67ee78bc1385e5733ff33106fb7d257825ce297f69356af763c2598a531e8f3c` |
| `world-state.jsonl` | 11,703,966 | `d4772484dd0740ba009a947114d4c6bde7b5fc2ffbe55ddf7a341cf9aa11e5a5` |
| `transient-perception.npz` | 8,538,476 | `e53f0aea9b94704cd95c898b4b6f9c21da4fdad864373889213a9c1d66080ccf` |
| `gpu-telemetry.jsonl` | 100,714 | `2fd0d8f9497f5cff06a3035b9f251c780a0f4fcccf14d3a89f49a18584f43e92` |
| `run-report.json` | 14,076 | `3f0577b0dc8b55b38b8528c8f081be71af856327485f088f0ab0305e5a731ab1` |
Publication scope is
`recorded-integrated-realtime-qualification-only`. The accepted result is
eligible to become the newest recorded integrated overlay for this session.
## Verification
The copied result was independently validated on the Mission Core host using
`validate_integrated_perception_result`. Validation rechecked:
- compute-job identity;
- result and configuration identities;
- full contiguous source selection;
- LiDAR-pack identity and calibration binding;
- artifact sizes and SHA-256 values;
- 4,489 fusion rows and 4,489 world-state rows;
- semantic timeline ordering;
- NPZ array shapes and offsets;
- report/result acceptance consistency.
The focused automated suite completed with 24 passing tests, including the E10,
E8, E9, live-perception and qualification validators.
## Controlled preflight rejection
An initial invocation passed the base E5 detector profile where the integrated
runner requires the E8 real-time tracking wrapper. The schema guard rejected it
during preflight with `LAB E8 profile schema changed`.
That invocation did not extract frames, start replay, publish a result, or alter
model state. The accepted invocation changed only the detector-profile argument
to the already frozen E8 profile. This rejected preflight is retained here so
the laboratory history is complete.
## What LAB E11 proves
LAB E11 proves that the current RTX 4090 worker and bounded scheduling contour
can replay the complete recorded `RAVNOVES00` right-camera epoch at source rate
while producing:
- 10 Hz 2D detection/tracking;
- approximately 2 Hz semantic state;
- factory-calibrated LiDAR-supported distance and visible-surface cuboids;
- timestamped world state and clearance;
- bounded queues with no detector or semantic loss in this run.
It also shows that the one-minute LAB E10 result was not a short-interval
artifact: source-paced behavior remained stable for the full 448.623 seconds.
## What LAB E11 does not prove
- direct live K1 camera/MQTT ingestion into this scheduler;
- hardware-clock camera/LiDAR synchronization;
- forest-domain accuracy or robustness;
- safety or navigation fitness;
- complete amodal 3D object dimensions;
- multi-camera fusion;
- performance on weaker onboard compute hardware.
YOLOX COCO and EoMT Cityscapes are generic models. Cuboids describe the visible
LiDAR-supported surface selected by the current association policy, not a
ground-truth full object volume.
## Next gate
The next priority is not another offline quality-only pass. It is a transport
integration gate:
1. feed the same bounded scheduler from direct K1 camera and LiDAR/MQTT sources;
2. keep the latest-wins queues and freshness rules unchanged;
3. expose live world-state age, drop counters and semantic freshness;
4. qualify a controlled near-real-time live interval on this same worker;
5. only then compare lower-power onboard targets or more domain-specific models.
The full-session recorded overlay may be rendered on demand for visual QA, but
its generation is presentation work and is not required to accept the compute
stability result.

View File

@ -0,0 +1,237 @@
# LAB E12 — Bounded shadow transport to the external perception worker
Date: 2026-07-22
State: near-live transport qualification accepted; physical K1 live run pending
Ground truth: no
Navigation/safety acceptance: no
Publication scope: diagnostic shadow transport only
## Purpose and answer
LAB E12 asks whether Mission Core can carry the already proven camera,
LiDAR and pose inputs to the external RTX worker at source rate without giving
that worker a second connection to K1, without allowing it to back-pressure the
raw recording, and without granting it any command authority.
The answer is **yes for a 15-second source-paced RAVNOVES00 near-live run**:
- Mission Core admitted and the Windows worker verified 446/446 ordered events;
- the stream contained 151 camera frames, one camera init, 142 LiDAR frames,
150 poses and two lifecycle records;
- ingress sequence gaps, payload-integrity failures, queue overflow and oversize
rejection were all zero in the nominal run;
- the worker completed in 15.073 seconds for a 15-second source interval;
- the worker connected only to an authenticated Mission Core endpoint through
an SSH reverse tunnel and had no K1 address or K1 transport;
- both sides declared `commands_enabled=false` and
`navigation_or_safety_accepted=false`.
The physical K1 run was not attempted because the unit was not present in the
BLE scan and the only unclassified LAN neighbour rejected both MQTT and RTSP.
No subnet scan or guessed device address was used. The exact same ingress is
already attached to the physical acquisition path and will receive live events
when K1 is next awake.
## Architecture under test
```text
K1 camera RTSP ──> Mission Core FFmpeg ──> durable fMP4 segment commit
└─> camera latest-wins queue (2)
K1 MQTT ──> durable raw K1MQTT commit ──> reviewed live callback
├─> LiDAR latest-wins queue (2)
└─> pose bounded queue (16)
separate modality queues ──> one exclusive localhost WebSocket consumer
──> bearer authentication ──> SSH reverse tunnel
──> read-only Docker probe on RTX worker D:
```
The camera observer is invoked only after `CameraArchiveWriter.append()` has
atomically published and fsynced the fragment, its index entry and its crash
checkpoint. The MQTT observer is invoked only by `capture_mqtt`'s
`on_message_recorded` callback after the raw frame has been committed. Derived
perception therefore remains expendable: it cannot become the source of record.
The ingress is process-local and bounded by modality:
| Queue | Capacity | Maximum payload |
|---|---:|---:|
| control | 4 | 16 KiB |
| camera init | 1 | 1 MiB |
| camera frame | 2 | 1 MiB |
| LiDAR | 2 | 2 MiB |
| pose | 16 | 2 MiB |
Camera bursts cannot evict pose or LiDAR observations. A full queue drops its
oldest derived item and retains the latest work; it never waits on the worker.
Only one authenticated worker consumer may hold the stream lease.
## Nominal qualification input
- canonical session: `20260720T065719Z_viewer_live` / `RAVNOVES00`;
- camera source: `sensor.camera.right`, codec epoch 1, 800x600 H.264 fMP4;
- source interval: the first 15 seconds of the canonical camera epoch;
- MQTT input: the matching canonical raw K1MQTT arrival interval;
- source pacing: 1.0x recorded host-arrival time;
- camera index SHA-256:
`e029815a60ad9fbfedb6169142c7449df2b119a51d1ce001f08806e04eb0be14`;
- raw MQTT SHA-256:
`70da0edad5cbf0e89b6e2355c0aac294f8c36a67249cd33b8918353fd88c83af`.
This replay is a transport qualification, not a synthetic message generator.
Every camera segment was rechecked against its durable length and SHA-256 index
record. K1MQTT framing and aligned timing metadata were read through the
existing reviewed replay parser.
## Nominal result
### Event accounting
| Modality | Selected | Edge admitted | Worker verified | Payload bytes |
|---|---:|---:|---:|---:|
| camera init | 1 | 1 | 1 | 763 |
| camera frame | 151 | 151 | 151 | 11,583,362 |
| LiDAR | 142 | 142 | 142 | 3,674,244 |
| pose | 150 | 150 | 150 | 25,346 |
| control | 2 | 2 | 2 | 48 |
| **Total** | **446** | **446** | **446** | **15,283,763** |
- worker first/last ingress sequence: 1/446;
- sequence gaps: 0;
- per-event payload digest mismatches: 0;
- session-end observed: yes;
- worker wall time: 15.072754 s;
- edge publication wall time including orderly disconnect: 15.698036 s.
### Queue behaviour
| Queue | Maximum depth / capacity | Consumed | Dropped | Oversize rejected | Final depth |
|---|---:|---:|---:|---:|---:|
| control | 1 / 4 | 2 | 0 | 0 | 0 |
| camera init | 1 / 1 | 1 | 0 | 0 | 0 |
| camera frame | 2 / 2 | 151 | 0 | 0 | 0 |
| LiDAR | 2 / 2 | 142 | 0 | 0 | 0 |
| pose | 2 / 16 | 150 | 0 | 0 | 0 |
The camera and LiDAR queues reached their configured maximum capacity but did
not overflow: the tunnel/worker consumer drained them within source rate.
## Worker-disconnect negative control
A separate four-second source-paced run intentionally removed the consumer
after approximately one second. The source still admitted its complete selected
interval:
- camera frames: 41/41 admitted;
- LiDAR: 38/38 admitted;
- pose: 40/40 admitted;
- camera init: 1/1 admitted;
- source run completed in 4.147395 seconds;
- the worker saw 23 ordered events and no session-end, as intended.
After disconnect, the bounded derived queues behaved as designed:
| Queue | Consumed | Overflow drops | Retained final depth / capacity |
|---|---:|---:|---:|
| camera frame | 10 | 29 | 2 / 2 |
| LiDAR | 6 | 30 | 2 / 2 |
| pose | 6 | 18 | 16 / 16 |
| control | 1 | 0 | 1 / 4 |
Exact accounting holds for every modality: consumed + dropped + retained equals
published. The consumer failure did not stop, delay or throw into the source
publisher. This is the required fail-open property for raw capture and the
required fail-closed property for derived perception availability.
## Transport and security boundary
- Mission Core remains bound to `127.0.0.1`.
- A private 0600 bearer token is stored under
`.runtime/live-perception/shadow-worker.token`; it is not returned in plugin
state or passed on a remote command line.
- The Mac opens an SSH reverse tunnel using the pinned `mission-gpu` host key.
- The worker receives the token on stdin and connects through
`host.docker.internal` to that tunnel.
- The server permits one consumer lease and immediately releases it on client
disconnect.
- Every binary event contains an explicit schema, bounded header, exact payload
length and SHA-256 digest.
- The worker container was read-only, capability-dropped, `no-new-privileges`,
PID-limited and given only read-only runner plus D-backed report mounts.
- No GPU access was required for this transport-only probe.
- The worker never received a K1 address and never opened MQTT or RTSP.
## Storage and runtime
All task-controlled Windows files remained under `D:\NDC_MISSIONCORE`:
- runner:
`D:\NDC_MISSIONCORE\workspace\mission-core-compute\run_e12_shadow_transport_probe.py`;
- report root:
`D:\NDC_MISSIONCORE\runtime\derived\e12-shadow-transport`.
Windows drive C was not used by this task. D had approximately 375.75 GiB free
before the nominal run and 376.06 GiB at the final read; the report is only a
small JSON document and raw payloads were not retained on the worker.
## Artifact and producer identity
- nominal edge report:
`.runtime/compute-experiments/e12/20260722T065957Z-e12-source.json`,
SHA-256 `36018374bc82b720d6ee50dace9ca1621431a5f90f49bfaf824054ecf5e6dd15`;
- nominal D-worker report:
`D:\NDC_MISSIONCORE\runtime\derived\e12-shadow-transport\20260722T065955Z-e12-shadow-transport.json`,
SHA-256 `4983b6bcb489892170d7dbc8014dc04e4576202a8590394658259dd8d1b42de6`;
- negative edge report:
`.runtime/compute-experiments/e12/20260722T070212Z-e12-source.json`,
SHA-256 `2cc4f45959b4ce58fa2a58e004a6572a26a483b33e458f46a471012bf5abda74`;
- negative worker report:
`.runtime/compute-experiments/e12/local-negative/20260722T070209Z-e12-shadow-transport.json`,
SHA-256 `311e34f3e514f77caf0a36355902b15a2fc22cd861ecbf00a91d00f08a695a63`;
- ingress implementation SHA-256:
`73e9359d926f7004ebe9519e0de4a05e4230922b714058ded9d27eff55ca06c0`;
- authenticated router SHA-256:
`4c409299d99304a0330256113f188c0bca97aadab6f742a9a626c059a91c0ce6`;
- camera raw-first hook SHA-256:
`086adf61d8143587025b85f46a59c9af746eee5656213cc1aa1c80974592d0a9`;
- physical acquisition integration SHA-256:
`a9aaf129f083cfb219767e13765b8575fc62df8e0ed05e6d50cff00c45ef8186`;
- near-live source runner SHA-256:
`9492804d8ecb629cb4a43b7b2d3220484c7b545b2e53dbad09eb8069f680d17d`;
- D-worker transport probe SHA-256 on both hosts:
`0ad67a1eb6935dd111c490128645b0d4345dc86d171dd7c4045ba990287da44d`.
## Accepted scope
- raw-first post-commit camera/MQTT fan-out;
- separate bounded modality queues;
- exclusive authenticated worker lease;
- source-rate camera/LiDAR/pose transport through the real Windows/Docker path;
- per-event integrity and ordered lifecycle;
- worker-disconnect isolation from the source;
- D-only worker persistence;
- zero K1 command authority.
## Not accepted
- a physical live K1 run in this experiment;
- live camera decode on the worker;
- live detector, semantic model or LiDAR fusion execution over this transport;
- cross-host latency based on unsynchronised wall clocks;
- long-duration soak, reconnect/recovery or worker-process restart;
- forest/off-road domain accuracy;
- vehicle-body extrinsics, navigation or safety use.
## Next gate
1. Attach the worker's persistent camera decoder to the camera-init/frame branch.
2. Feed decoded frames to the already accepted capacity-2 10 Hz detector queue.
3. Decode LiDAR/pose into their verified K1 schemas and carry the latest matched
depth epoch into the existing E10 fusion runtime.
4. Run YOLOX/Triton at 10 Hz and EoMT semantics at 2 Hz without writing per-frame
images; publish only transient diagnostic world state and telemetry.
5. Repeat the disconnect negative control with inference loaded.
6. When K1 is awake, run the same profile first for 15 seconds and then for 60
seconds against the physical acquisition path before any longer soak.

View File

@ -0,0 +1,238 @@
# LAB E13 — class-aware amodal 3D cuboids
Дата прогона: 2026-07-22
Статус: accepted for recorded near-live qualification; not accepted for navigation or safety
Основной результат: `e10-integrated-perception-eaf7427637326e74dbc5fe66aaffcab83d0a6c18c0c45b93fdc2ecff062b94ee`
## 1. Цель
LAB E13 заменяет диагностические p05p95 оболочки LAB E10/E11 на полные 3D cuboids, пригодные для дальнейшей разработки «глаз» беспилотника. Старый fitter описывал только видимую LiDAR-поверхность: например, у автомобиля получалась длинная, но очень тонкая пластина. E13 должен:
- сохранить реальное LiDAR-свидетельство отдельно от достроенной геометрии;
- получить полный class-aware объём car/truck/bus/person/bicycle/motorcycle;
- привязать нижнюю грань к локальной оценке земли;
- стабилизировать центр, размеры и yaw по `track_id`;
- не публиковать старую оболочку как полноценный 3D object box, если достройка не прошла проверки;
- остаться в ранее принятом near-live бюджете: detector около 10 FPS, semantic около 2 FPS, p95 возраста world state не более 175 ms.
E13 не утверждает, что невидимая часть объекта измерена. Полный cuboid явно маркируется как `class-prior-completed-from-visible-lidar-support`, а исходная измеренная оболочка сохраняется в `observed_cuboid_*`.
## 2. Входные данные
- Сессия: `20260720T065719Z_viewer_live` (`RAVNOVES00`).
- Камера: `sensor.camera.right`, XGRIDS calibration slot `camera_1`.
- Калибровка SHA-256: `05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`.
- Camera compute job: `recorded-camera-602ac89026ed12978619801d`.
- LiDAR pack: `e10-lidar-pack-5da0396d32a27f9d1ca537cc2e8a371d386078d6f0dc71737b78620992af9625`.
- Кадры: 10001600 включительно, 601 кадр.
- Временной интервал общей шкалы: 135.365857292195.334857292 s.
- Source span: 59.969 s.
- Timeline SHA-256: `06e3514228b488137b92c303cbca171154ede00368783e32107ed2c39c41baa6`.
- Valid-FOV mask: 270,606 из 480,000 пикселей.
- Valid-FOV mask SHA-256: `a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63`.
## 3. Вычислительный контур
- Worker: `<worker-host>`, выполнение только на диске `D:`.
- GPU: NVIDIA GeForce RTX 4090.
- Контейнер: `nvcr.io/nvidia/tritonserver:26.06-py3`.
- Python 3.12.3; NumPy 1.26.4; SciPy 1.16.3; PyTorch 2.13.0+cu130; Transformers 4.57.6.
- Detector: YOLOX-S, COCO-80, Triton, 640×640.
- Detector model SHA-256: `c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063`.
- Semantic model: `tue-mps/cityscapes_semantic_eomt_large_1024`, FP16 autocast.
- Semantic model SHA-256: `c265da9a74f58f5c3f4826d23ca4ca78beac0b106cca5842beca61580de5b782`.
- Source pace: 1.0×.
- Detector queue: latest-wins capacity 2.
- Semantic queue: latest-wins capacity 1; sampling every fifth camera frame.
- Profile SHA-256: `1890d5039d93abea29bd75cf5609264d64b2c0b0fa5b032233fa5e45872776cd`.
- Runner SHA-256: `bf04efa64c3614688742fb28d2da7c428ff70b4ca61a513318cd2440e79ff776`.
- Fusion runtime SHA-256: `9724a4b5af719ea902f02a8a1efdb720e64b5af68b5759367bdc756bdd31b5c9`.
- PowerShell orchestrator SHA-256: `ac0ee42495839773a2de60090ebcb49eee979dc7ee3b8587662a3c59e19a4d03`.
Перед финальным прогоном на `D:` было 375.612 GiB свободно при жёстком floor 360 GiB. После трёх сохранённых A/B-прогонов и публикации текущий объём свободного места составлял 375.317 GiB. Диск `C:` не использовался и не проверялся.
## 4. Алгоритм E13
### 4.1 Измеренная часть
Сохраняется прежний доказанный контур:
1. calibrated KB4 projection LiDAR → camera;
2. nearest-depth-per-rounded-pixel occlusion gate;
3. пересечение 2D track, semantic class mask и LiDAR projection;
4. depth-contiguous cluster;
5. spatial connected cluster;
6. robust oriented p05p95 envelope как `observed_cuboid`.
### 4.2 Достройка полного объёма
- Для каждого detector label задан nominal/min/max размер и support padding.
- Видимая грань достраивается от сенсора, то есть невидимый объём продолжается за измеренной поверхностью, а не симметрично в обе стороны.
- Для вытянутых классов fitter различает длинную боковую поверхность и короткую фронтальную/заднюю грань. При короткой грани yaw поворачивается на 90° по нормали поверхности.
- При слабой анизотропии ориентация берётся из track history; при отсутствии истории используется sensor-bearing fallback.
- Локальная земля оценивается по нижнему процентилю полного облака в радиусе 2.5 m вокруг объекта. Нереалистичная оценка заменяется ограниченным fallback относительно нижней границы support points.
- Размеры ограничиваются физическими class-bounds. Выход за bound означает rejection, а не расширение коробки до заведомо ложного размера.
- Центр, размер и yaw фильтруются по `track_id`.
- Cuboid должен содержать не менее 75% текущих support points.
- Если temporal smoothing уводит box от текущих точек, фильтр сбрасывается на текущий несглаженный candidate.
- `failure_policy=reject`: если полный cuboid нельзя построить честно, 2D detection и observed support остаются в журнале, но 3D box не публикуется.
### 4.3 Классовые nominal sizes
| Detector label | Nominal L×W×H, m | Допустимый диапазон, m |
|---|---:|---:|
| person | 0.55×0.55×1.72 | 0.351.20 × 0.351.20 × 1.302.30 |
| bicycle | 1.80×0.65×1.50 | 1.202.50 × 0.401.20 × 1.002.20 |
| motorcycle | 2.10×0.80×1.45 | 1.403.00 × 0.501.40 × 1.002.20 |
| car | 4.50×1.85×1.55 | 3.205.80 × 1.452.40 × 1.202.30 |
| truck | 7.00×2.50×3.00 | 4.8012.50 × 1.803.20 × 1.804.20 |
| bus | 10.50×2.55×3.20 | 7.0013.50 × 2.103.20 × 2.504.20 |
## 5. Итерации A/B
### E13.1 — completion с визуальным fallback
- Result: `e10-integrated-perception-54e660dc90333f5d38b9ad7054e05e32a808e268080220db20d2d2a1141cc2c3`.
- 494 опубликованных 3D boxes: 382 class-prior-completed и 112 fallback visible envelopes.
- World-state p95 age: 97.876 ms.
- Решение отклонено как финальное: интерфейс смешивал полные cuboids и старые короткие оболочки.
### E13.2 — fail-closed и yaw axis hysteresis
- Result: `e10-integrated-perception-b35c3e94e8c8614ad4cf51a5b9a41ebe5a7be856aa1b3cc837b2d9bd6f05e8fe`.
- Только 334 class-prior-completed boxes опубликованы.
- 81 rejection по support coverage и 79 rejection по class-size bounds.
- 90°/innovation resets снижены с 76 до 16.
- World-state p95 age: 100.072 ms.
- Найден дефект: при coverage reset смешивались новый центр/yaw и ранее сглаженный размер.
### E13.3 — полный current-candidate reset
- Result: `e10-integrated-perception-eaf7427637326e74dbc5fe66aaffcab83d0a6c18c0c45b93fdc2ecff062b94ee`.
- 375 class-prior-completed boxes опубликованы.
- Coverage rejections снижены с 81 до 39.
- Class-bound rejections: 81; они сохранены как корректный fail-closed outcome.
- Innovation resets: 15.
- Финальная итерация принята.
## 6. Финальные результаты
### 6.1 Throughput и latency
| Метрика | LAB E10 baseline, тот же clip | LAB E13.3 | Gate |
|---|---:|---:|---:|
| Detector effective FPS | 9.996 | 9.986 | ≥9.5 |
| Detector dropped frames | 0 | 0 | 0 |
| Semantic effective FPS | 2.013 | 2.011 | ≥1.8 |
| Fresh semantic coverage | 99.33% | 99.00% | ≥90% |
| Fused frames | 525 | 523 | ≥450 |
| Published 3D boxes | 760 visible envelopes | 375 completed cuboids | ≥100 |
| Association p50 | 1.391 ms | 1.751 ms | diagnostic |
| Association p95 | 3.858 ms | 5.184 ms | diagnostic |
| World-state age p50 | 52.017 ms | 68.547 ms | diagnostic |
| World-state age p95 | 76.515 ms | 101.829 ms | ≤175 ms |
| Replay wall time | 60.123 s | 60.184 s | about 1.0× |
Добавленная геометрия увеличила p95 association примерно на 1.33 ms. Разница полного world-state age между отдельными запусками также содержит вариативность decode, detector и semantic runtimes; её нельзя целиком приписывать cuboid fitter.
### 6.2 Ресурсы
- Process peak RSS: 2022.719 MiB.
- CUDA peak allocated: 2107.925 MiB.
- CUDA peak reserved: 2840 MiB.
- GPU telemetry mean/max utilization: 63.77% / 79%.
- GPU telemetry mean/max memory used: 13,381 / 13,391 MiB.
- GPU power mean/max: 215.40 / 228.72 W.
- GPU temperature mean/max: 46 / 49 °C.
### 6.3 Геометрия и качество фильтра
- Accepted cuboids: 375.
- По классам: car 267; truck 100; person 6; bicycle 2; motorcycle/bus 0 в accepted set.
- Mean support coverage: 85.82%.
- Mean completion fraction: 92.67%; p50 95.06%; p95 99.46%.
- Средний accepted car box: 4.536×1.891×1.569 m.
- Средний accepted truck box: 7.080×2.500×3.001 m.
- Средний accepted person box: 0.572×0.618×1.720 m.
- Temporal statuses: confirmed 185; tentative 115; reset-support-coverage 60; reset-innovation 15.
- Orientation sources: support-PCA 196; support-face-normal 129; track-history axis disambiguation 48; track-history 1; sensor-bearing fallback 1.
На 256 соседних accepted observations одного track с разрывом не более 0.25 s:
| Temporal metric | Measured visible envelope | Completed E13 cuboid |
|---|---:|---:|
| Median size step | 0.548 m | 0.000 m |
| p95 size step | 2.167 m | 0.152 m |
| Median yaw step | 4.67° | 1.31° |
| p95 yaw step | 58.61° | 22.10° |
Это не является сравнением с ground truth, но подтверждает, что размер и yaw перестали повторять шум каждой отдельной видимой поверхности.
### 6.4 Fail-closed accounting
- `rejected-fewer-than-8-clustered-points`: 1288 vehicle observations.
- `rejected-fewer-than-4-clustered-points`: 66 non-vehicle observations.
- `rejected-no-semantic-lidar-support`: 590.
- `rejected-distance-innovation`: 98.
- `rejected-amodal-completion-required-size-exceeds-class-bound`: 81.
- `rejected-amodal-completion-support-coverage-below-threshold`: 39.
- `rejected-implausible-cuboid`: 12.
Высокое число rejected candidates ожидаемо: E13 сознательно уменьшает визуальный мусор и публикует только полный cuboid, прошедший semantic, clustering, distance, class-size и support-coverage gates.
## 7. Визуальная проверка и публикация
- Результат прошёл `validate_integrated_perception_result` локально.
- Rerun perception overlay: 58,326,068 bytes; API build time 9.778 s; HTTP 200.
- E13 опубликован как newest accepted overlay для `RAVNOVES00`.
- Проверено в Mission Core на общей шкале около 02:27: cloud, trajectory, recorded camera, semantic overlay и полные полупрозрачные 3D cuboids отображаются совместно.
- Принятый вид оставлен открытым в локальном интерфейсе.
## 8. Артефакты
Worker root:
`D:\NDC_MISSIONCORE\runtime\derived\e10-integrated-perception-eaf7427637326e74dbc5fe66aaffcab83d0a6c18c0c45b93fdc2ecff062b94ee`
Local laboratory copy:
`.runtime/compute-experiments/e13/worker-results/e10-integrated-perception-eaf7427637326e74dbc5fe66aaffcab83d0a6c18c0c45b93fdc2ecff062b94ee`
Published overlay source:
`.runtime/compute-experiments/e10/worker-results/e10-integrated-perception-eaf7427637326e74dbc5fe66aaffcab83d0a6c18c0c45b93fdc2ecff062b94ee`
| Artifact | Bytes | SHA-256 |
|---|---:|---|
| `result.json` | — | `512f5fcb72d3c10f4d3d6beb081b28dbb65addcd166e7d4a4bdc3f55a63be4c6` |
| `semantic-frames.jsonl` | 55,023 | `43e72dc9f3ec9fc74f4e1c69dc1ba2fd900ac445aa1744f8d5ea2cc2165cd52b` |
| `fusion-frames.jsonl` | 2,096,208 | `ed8256b41c8a56b8d1dd561e6c47479932a913c80e64cf73c4dc761722637acf` |
| `world-state.jsonl` | 1,336,724 | `a418d1b90fddd810b3ec3fa20c6e5bfa14ecaf2f348500f4a9b56a761a2063d0` |
| `transient-perception.npz` | 1,065,071 | `a68e5bd06eee4f2372eacaf0d94326515b94848189e6527d063002398d0c0f56` |
| `gpu-telemetry.jsonl` | 13,704 | `a80cced041a87446cfb9e074a0867ab5f980108f826b347ca5175664d2b9c907` |
| `run-report.json` | 17,870 | `b6e67b710e6a2f9d3132a0293ac1fae54ca688b6a1ebbc42174893388c0ab88d` |
## 9. Ограничения
1. Это recorded source-paced qualification, а не физический live K1 → worker прогон.
2. Host-arrival camera/LiDAR binding не доказывает hardware-clock synchronization.
3. COCO YOLOX и Cityscapes EoMT не являются forest-domain или safety-validated моделями.
4. Средняя доля достроенного объёма 92.67% показывает, что текущие «нормальные» cuboids в основном опираются на class priors. Это инженерный amodal estimate, не измеренная форма объекта.
5. 3D ground truth отсутствует. Нельзя утверждать 3D IoU, heading accuracy, center error или recall относительно истины.
6. Truck detections наследуют ограниченность COCO label и могут включать крупные автомобили, которые в конкретном кадре классифицированы неточно.
7. Current local-ground percentile не заменяет полноценную road/terrain plane model для уклонов, бордюров и лесной поверхности.
8. E13 не имеет navigation/control authority и не принят для safety.
## 10. Решение и следующий gate
E13.3 принят как новый визуальный и world-state baseline вместо visible-surface boxes. Он доказывает, что class-aware full cuboids и temporal stabilization укладываются в near-live бюджет на существующем worker.
Следующий обязательный этап — LAB E14:
1. добавить 3D ground-truth/prelabel contour хотя бы для репрезентативного набора кадров;
2. провести внешний benchmark learned LiDAR/BEV 3D detectors (CenterPoint/PointPillars и camera-LiDAR fusion вариант) на том же K1 point pattern;
3. сравнить learned box с E13 prior-completed baseline по latency, 3D IoU/center/yaw и стабильности;
4. подключить принятый fitter к bounded E12 shadow ingress и проверить disconnect/overflow вместе с inference;
5. после этого выполнить физический live K1 gate без control/navigation authority.
До появления 3D labels E13 следует использовать как честно маркированный near-live amodal baseline, а не как финальное решение 3D detection.

View File

@ -0,0 +1,276 @@
# LAB E14 — full-session recorded near-live qualification
Дата прогона: 2026-07-22
Статус вычислительного прогона: accepted for recorded near-live qualification
Статус physical live K1: not tested
Navigation/safety acceptance: false
Основной результат: `e10-integrated-perception-9c10dc36640b409393c161f6cbd59b000581b39851ef7a8931898cf8f46d7e8b`
## 1. Цель
LAB E14 переносит принятый в E13 class-aware amodal 3D cuboid contour с минутной выборки на всю доступную запись `RAVNOVES00`. Эксперимент отвечает на практический вопрос: может ли текущий RTX 4090 worker непрерывно обрабатывать полный recorded stream в темпе источника, одновременно выполняя:
- декодирование fisheye-видео;
- YOLOX-S 2D detection на каждом camera frame;
- EoMT semantic segmentation на каждом пятом frame;
- calibrated KB4 cameraLiDAR projection;
- semantic/LiDAR association;
- class-prior amodal 3D cuboid completion и temporal stabilization;
- clearance и world-state publication;
- GPU/runtime telemetry;
- fail-closed accounting без скрытых потерь очередей.
E14 является capacity qualification на записанном источнике. Он не заменяет физический live K1 gate, не подтверждает hardware-clock synchronization и не выдаёт контуру право управлять беспилотником.
## 2. Входные данные
- Сессия: `20260720T065719Z_viewer_live`, операторское имя `RAVNOVES00`.
- Камера: `sensor.camera.right`.
- XGRIDS calibration slot: `camera_1`.
- Калибровка SHA-256: `05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`.
- Camera compute job: `recorded-camera-602ac89026ed12978619801d`.
- Camera input SHA-256: `602ac89026ed12978619801d4edea0cae24b5cc3afabd9f7af2858de6505a20e`.
- LiDAR pack: `e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b`.
- Кадры: 04488 включительно, 4489 camera frames.
- Timeline: 35.421857292484.044857292 s.
- Source span: 448.623 s.
- Timeline SHA-256: `f91dad0a3cc998f4250be794b48f437943b3c60edccbe7e5ceed59030854a807`.
- Разрешение: 800×600.
- Valid-FOV mask: 270,606 из 480,000 пикселей, 56.38% кадра.
- Valid-FOV mask SHA-256: `a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63`.
Valid-FOV mask вычислена заранее из стабильной геометрии объектива и переиспользуется. За пределами полезного круга detector/semantic contour не получает содержательного изображения; маска не пересчитывается для каждого кадра.
## 3. Воспроизводимая конфигурация
### 3.1 Worker и модели
- Worker host: `<worker-host>`.
- GPU: NVIDIA GeForce RTX 4090.
- Worker storage: только `D:\NDC_MISSIONCORE`.
- Диск `C:` не использовался и не проверялся.
- Container image: `nvcr.io/nvidia/tritonserver:26.06-py3`.
- Python 3.12.3; NumPy 1.26.4; SciPy 1.16.3; PyTorch 2.13.0+cu130; Transformers 4.57.6.
- Detector: YOLOX-S, COCO-80, Triton, 640×640.
- Detector model SHA-256: `c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063`.
- Semantic: `tue-mps/cityscapes_semantic_eomt_large_1024`, FP16 autocast.
- Semantic model revision: `8d6b6d1a3f7b50d441afd7d247c2ed10db186e8f`.
- Semantic model SHA-256: `c265da9a74f58f5c3f4826d23ca4ca78beac0b106cca5842beca61580de5b782`.
### 3.2 Scheduler и профили
- Source pace: 1.0×.
- Detector: каждый frame, latest-wins queue capacity 2.
- Semantic: каждый пятый frame, latest-wins queue capacity 1.
- Semantic TTL: 750 ms.
- Profile: `experiments/perception/worker/e14_full_session_amodal_profile.json`.
- Profile SHA-256: `a010e32070459b123423f3d4f57aa1d839f73ed05f3df8580d12693406a1b218`.
- Detector profile SHA-256: `819bdfb0da521d187ec9ce3d3039a0ff75daa0ab473fb3d8e5fa92db9c1ddca1`.
- Semantic profile SHA-256: `ea583966bc3409f5cf563cbf4fad05e366907e67187082eb692aff53d9f5d875`.
- Runner SHA-256: `bf04efa64c3614688742fb28d2da7c428ff70b4ca61a513318cd2440e79ff776`.
- Fusion runtime SHA-256: `9724a4b5af719ea902f02a8a1efdb720e64b5af68b5759367bdc756bdd31b5c9`.
- PowerShell orchestrator SHA-256: `ac0ee42495839773a2de60090ebcb49eee979dc7ee3b8587662a3c59e19a4d03`.
- Runner generation: `D:\NDC_MISSIONCORE\runtime\derived\e14-runner-bf04efa64c36-ac0ee4249583-a010e3207045`.
### 3.3 Acceptance gates
| Gate | Порог |
|---|---:|
| Detector effective FPS | ≥9.5 |
| Detector drop fraction | ≤0.5% |
| Semantic effective FPS | ≥1.8 |
| Semantic drop fraction | ≤5% |
| Semantic completion age p95 | ≤400 ms |
| Fresh semantic coverage | ≥90% |
| World-state age p95 | ≤175 ms |
| Fused frames | ≥3500 |
| Accepted completed cuboids | ≥1500 |
| Detector/semantic failures | 0 |
## 4. Выполнение и время
- Подготовка worker input/runtime: 31.249 s.
- Source-paced replay внутри runner: 448.699 s.
- Source duration: 448.623 s.
- Replay/source ratio: 1.00017×, то есть вычислительный цикл удержал темп записи.
- Полный PowerShell orchestration wall time: 597.752 s.
Разница между replay и orchestration включает подготовку, запуск/проверку контейнерного контура, сбор и валидацию артефактов, восстановление model state и финальную уборку. Она не является inference latency кадра.
Контур выполнялся параллельно: detector и semantic workers имели отдельные bounded queues; fusion/world-state потреблял detector result на camera cadence и последнюю допустимую semantic result. Медленная semantic модель не блокировала detector loop и не накапливала безразмерную очередь.
## 5. Итоговые near-live метрики
| Метрика | Результат | Gate | Статус |
|---|---:|---:|---|
| Detector frames | 4489/4489 | полное accounting | pass |
| Detector effective FPS | 10.0045 | ≥9.5 | pass |
| Detector drops | 0 | ≤0.5% | pass |
| Detector queue max depth | 1 из 2 | bounded | pass |
| Semantic frames | 898 | every fifth frame | pass |
| Semantic effective FPS | 2.0013 | ≥1.8 | pass |
| Semantic drops | 0 | ≤5% | pass |
| Fresh semantic coverage | 99.7104% | ≥90% | pass |
| Semantic completion age p95 | 261.259 ms | ≤400 ms | pass |
| Fused frames | 3915 | ≥3500 | pass |
| Accepted 3D cuboids | 3656 | ≥1500 | pass |
| World-state age p50 / p95 | 67.439 / 102.883 ms | p95 ≤175 ms | pass |
| Failures | 0 | 0 | pass |
Fusion state accounting:
- `fused`: 3915;
- `depth-unavailable-sync-gate`: 561;
- `semantic-stale`: 8;
- `semantic-unavailable`: 5;
- semantic freshness violations: 0.
## 6. Latency decomposition
| Stage | Mean, ms | p50, ms | p95, ms | Max, ms |
|---|---:|---:|---:|---:|
| Decode | 33.438 | 31.333 | 48.551 | 77.638 |
| Detector | 31.502 | 28.524 | 47.158 | 65.501 |
| Detector queue wait | 0.202 | 0.074 | 0.200 | 23.505 |
| Projection | 0.476 | 0.397 | 0.884 | 12.028 |
| Association + cuboid | 2.575 | 1.964 | 6.834 | 26.413 |
| Clearance | 0.222 | 0.187 | 0.517 | 5.949 |
| World-state serialization | 0.075 | 0.046 | 0.233 | 0.964 |
| End-to-end world-state age | 70.425 | 67.439 | 102.883 | 182.733 |
Semantic processing:
| Stage | Mean, ms | p95, ms | Max, ms |
|---|---:|---:|---:|
| Host → device | 6.869 | 10.309 | 18.386 |
| Forward | 154.711 | 167.953 | 407.725 |
| Model postprocess | 9.825 | 14.110 | 20.840 |
| Full semantic processing | 188.479 | 206.391 | 434.399 |
| Completion age | 224.099 | 261.259 | 467.008 |
## 7. 3D geometry
Accepted class-prior-completed cuboids: 3656.
| Class | Count | Mean L×W×H, m |
|---|---:|---:|
| car | 3118 | 4.514×1.881×1.575 |
| truck | 493 | 7.016×2.514×3.125 |
| person | 31 | 0.622×0.588×1.720 |
| bus | 10 | 10.500×2.631×3.200 |
| bicycle | 2 | 1.800×1.016×1.500 |
| motorcycle | 2 | 2.100×0.874×1.450 |
- Mean support coverage: 86.19%.
- Mean inferred completion fraction: 91.56%.
- Temporal statuses: confirmed 2070; tentative 920; support-coverage reset 574; innovation reset 92.
- Orientation: support-PCA 1890; support-face-normal 1034; track-history axis disambiguation 702; track-history 23; sensor-bearing fallback 7.
Высокая completion fraction означает, что значительная часть полного объёма достроена по class priors из видимой LiDAR-поверхности. Это диагностическая геометрия с измеренным support audit, а не 3D ground truth.
Fail-closed rejections:
- fewer than 8 clustered vehicle points: 10,563;
- no semantic/LiDAR support: 4,884;
- distance innovation: 505;
- required size exceeds class bound: 395;
- support coverage below threshold: 306;
- fewer than 4 clustered non-vehicle points: 184;
- implausible cuboid: 20.
Rejection сохраняет 2D/audit evidence, но не публикует ложный полноценный 3D box.
## 8. Ресурсы и диски
- Process peak RSS: 2964.414 MiB.
- CUDA peak allocated: 2107.925 MiB.
- CUDA peak reserved: 2840 MiB.
- GPU utilization mean/p50/p95/max: 74.59% / 75% / 91% / 98%.
- GPU memory used mean/p50/p95/max: 13,274.69 / 13,273 / 13,282 / 13,284 MiB.
- GPU memory utilization mean/p95/max: 22.67% / 32% / 36%.
- GPU power mean/p95/max: 200.44 / 208.43 / 225.21 W.
- GPU temperature mean/p95/max: 49.09 / 54 / 55 °C.
- Telemetry samples: 449 at 1 s interval.
На `D:` до staging/preparation было приблизительно 389.65 GiB свободно. Runner стартовал с 415,364,739,072 bytes и закончил с 415,598,956,544 bytes при hard floor 386,547,056,640 bytes (360 GiB). После orchestration и уборки оставалось около 387.05 GiB. Временные camera frames удалены оркестратором; model state восстановлен. Диск `C:` не затрагивался.
На локальном Mac опубликованный Rerun overlay занимает 443,421,386 bytes. На момент отчёта локально оставалось около 32 GiB; лишние копии видео не создавались.
## 9. Публикация и интерфейсная проверка
- Результат дважды прошёл строгую локальную `validate_integrated_perception_result`: до и после immutable publication в canonical provider root.
- Full-session perception RRD: 443,421,386 bytes.
- RRD SHA-256: `1dc29900ddeb09897af70c7334755823b02aeaa95f203d979be4bd75fae5080b`.
- Mission Core API вернул HTTP 200 и сохранил immutable result-scoped cache.
- Сервис не перезапускался и оставался healthy во время построения и браузерной проверки.
- В Mission Core подтверждена полная шкала `00:00 / 08:55.718`.
- В режиме `Распознавание` исходная камера и `Сегментация · камера right` синхронно менялись при воспроизведении; это не статичный кадр.
Во время проверки обнаружен presentation defect режима `Кубы 3D`: blueprint имел origin `/world/perception`, поэтому исключал native cloud `/world/points` и показывал cuboids в отдельной пустой сцене. Исправление выполнено в source:
- cuboid view перенесён на origin `/world`;
- native cloud и `/world/perception` объединены в одном latest-at view;
- 12-second accumulation к cuboid view не применяется, поэтому коробки разных кадров не наслаиваются;
- дублирующий серый `/world/perception/lidar` скрыт;
- добавлены regression assertions для initial и dynamic blueprints.
Backend не перезапускался из-за операторского требования никогда не оставлять Mission Core недоступным. Поэтому вычислительный 3D result принят, source fix протестирован, но визуальная runtime-проверка исправленного blueprint остаётся отдельным коротким gate после следующего штатного перезапуска сервиса.
## 10. Артефакты
Worker result root:
`D:\NDC_MISSIONCORE\runtime\derived\e10-integrated-perception-9c10dc36640b409393c161f6cbd59b000581b39851ef7a8931898cf8f46d7e8b`
Local laboratory copy:
`.runtime/compute-experiments/e14/worker-results/e10-integrated-perception-9c10dc36640b409393c161f6cbd59b000581b39851ef7a8931898cf8f46d7e8b`
Published provider root:
`.runtime/compute-experiments/e10/worker-results/e10-integrated-perception-9c10dc36640b409393c161f6cbd59b000581b39851ef7a8931898cf8f46d7e8b`
| Artifact | Bytes | SHA-256 |
|---|---:|---|
| `result.json` | 13,799 | `ff129ee1e51e19b514ffb790a145cf17292bd49e9256de2a77a2ca2557e50e68` |
| `run-report.json` | 18,144 | `a85a714215358715120efb057991087f8b214fe6ef86ce2f68124f19287f2a4d` |
| `semantic-frames.jsonl` | 404,398 | `0156bd7450cf6e89fcc3e66831377e87089c6f322a0db382d7135246241cf6b3` |
| `fusion-frames.jsonl` | 17,008,643 | `04c6589c0d5258b47937114c80a1562b6a427456b22e8aedb8cfa86741fe183b` |
| `world-state.jsonl` | 10,699,803 | `5b030270c4ae9029690666cbde7e6191df4129f393a8530dc561b76a68b61f89` |
| `transient-perception.npz` | 8,291,832 | `36677d1b2f4260f821dcf29929dc0dea90b28a4e74ff5c777d320dda7213f659` |
| `gpu-telemetry.jsonl` | 100,917 | `d4fb026b46d25b97b7d35397d598fec23647276af6339c98898cbfea97345601` |
| full-session `perception.rrd` | 443,421,386 | `1dc29900ddeb09897af70c7334755823b02aeaa95f203d979be4bd75fae5080b` |
## 11. Что доказано и что не доказано
Доказано:
1. На RTX 4090 текущий YOLOX + EoMT + calibrated LiDAR fusion + E13 cuboid/world-state contour удерживает полный 448.623-second recorded stream в 1.0×.
2. Detector и semantic queues bounded; за 4489 frames нет ни overflow drops, ни model failures.
3. p95 world-state age 102.883 ms оставляет запас до лабораторного 175 ms gate.
4. Full-session 2D/semantic playback опубликован и работает на общей временной шкале Mission Core.
5. Все 3656 опубликованных 3D boxes прошли class bounds и support gates; rejected candidates не маскируются.
Не доказано:
1. Физический K1 live ingress → decode → inference → fusion на реальном сетевом потоке.
2. Hardware-clock camera/LiDAR synchronization.
3. Устойчивость к реальному disconnect/reconnect при работающем inference.
4. Forest-domain качество COCO/Cityscapes моделей.
5. 3D IoU, center/yaw error и recall относительно ground truth.
6. Navigation/safety authority.
## 12. Следующий gate
Следующий этап не должен улучшать recorded-видео в вакууме. Он переносит доказанный E14 compute contour на уже принятый E12 ingress:
1. подключить persistent fMP4 camera decoder к bounded shadow transport;
2. выполнять detector на 10 Hz и semantic на 2 Hz через latest-wins queues;
3. декодировать LiDAR/pose и публиковать тот же calibrated fusion/world-state contract;
4. вывести live telemetry: source age, decode/inference/fusion age, queue depth, drops, semantic freshness, reconnect generation;
5. выполнить физический K1 shadow gate 15 s;
6. при нулевых gaps/failures выполнить 60 s gate;
7. только после этого увеличивать длительность и проверять disconnect/reconnect;
8. control/navigation output оставить физически отключённым.
Отдельная короткая presentation-проверка после следующего штатного перезапуска Mission Core должна подтвердить, что исправленный `Кубы 3D` показывает latest-at cuboids непосредственно поверх native point cloud.

View File

@ -0,0 +1,351 @@
# LAB E15 — source-paced live shadow inference qualification
Дата: 22 июля 2026 года
Финальный accepted run: 20:2320:26 MSK / `2026-07-22T17:26:37Z` source completion
Статус: **accepted для recorded-source-paced shadow diagnostic**
Safety/navigation authority: **не выдана**
## 1. Что проверялось
LAB E15 впервые соединяет принятые ранее части Mission Core в один потоковый контур, близкий к будущему onboard perception loop:
1. bounded Mission Core shadow ingress;
2. инкрементальный decode сохранённого fMP4 без сборки видео целиком и без PNG на диске;
3. YOLOX-S object detection на каждом camera frame;
4. EoMT semantic segmentation на каждом пятом camera frame;
5. nearest-time camera → LiDAR → pose binding;
6. KB4 calibrated cameraLiDAR projection;
7. semantic/LiDAR association;
8. class-prior amodal 3D cuboids;
9. clearance и timestamped world-state;
10. bounded queues, telemetry и fail-closed acceptance.
Это не offline-просчёт «как можно качественнее». Источник отдавал 15 секунд реальной записи в темпе 1.0×, а worker обязан был удерживать camera cadence, bounded memory и ограничение на возраст результата.
## 2. Граница доказательства
Доказано:
- один RTX 4090 worker принимает camera/LiDAR/pose через live shadow wire contract;
- 151 fMP4 fragment декодируется инкрементально без потерь и без временных frame-файлов;
- detector работает практически на camera cadence 10 Hz;
- semantic contour работает примерно на 2 Hz и не блокирует detector;
- LiDAR/pose fusion и 3D cuboid/world-state укладываются в заданный p95 age budget;
- transport, model queues и media buffer остаются bounded;
- disconnect/replay transport не вмешивается в raw recorder;
- весь persistent worker contour и результаты размещены только на `D:`.
Не доказано:
- работа от физически подключённого K1 в этом конкретном E15 run;
- hardware-clock synchronization камеры и LiDAR;
- forest-domain accuracy, recall или safety quality;
- пригодность COCO/Cityscapes labels для автономной навигации;
- vehicle-body transform;
- управление беспилотником или safety authority;
- публикация текущего live world-state в операторский Rerun UI.
Итоговый scope результата: `live-shadow-diagnostic-only`.
## 3. Входные данные и provenance
### 3.1 Источник
- Session: `20260720T065719Z_viewer_live` (`RAVNOVES00`).
- Camera source: `sensor.camera.right`.
- Camera calibration slot: `camera_1`.
- Camera resolution: 800×600.
- Source interval: 14.989973 s.
- Replay pace: 1.0× recorded host arrival time.
- Camera frames: 151.
- LiDAR messages: 142.
- Pose messages: 150.
- Control messages: 2.
- Total wire events: 446.
- Camera index SHA-256: `e029815a60ad9fbfedb6169142c7449df2b119a51d1ce001f08806e04eb0be14`.
- MQTT raw SHA-256: `70da0edad5cbf0e89b6e2355c0aac294f8c36a67249cd33b8918353fd88c83af`.
- Accepted source report: `.runtime/compute-experiments/e12/20260722T172637Z-e12-source.json`.
- Accepted source report SHA-256: `8d36a9715b19fcd820e585629e0774d97ea21dfd5b763fd48e9672e098814e48`.
### 3.2 Калибровка
- Calibration SHA-256: `05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`.
- Projection model: KB4.
- Projection pack: `e15-live-projection-713fa7344805b7a592d12e949a6db38116e5370f768bde1cf9144fab5f93f0ae`.
- Projection arrays SHA-256: `d37bff4299b3fd6232926ba6e04d3cbd2a5aba0e66bf5ed02e0873340597566c`.
- Pack classification: `calibration-only-no-recorded-sensor-frames`.
- В projection pack нет записанных camera/LiDAR frames: только intrinsics, KB4 distortion и `T_camera_from_lidar`.
### 3.3 Valid-FOV
- Generation: `valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2`.
- Mask SHA-256: `a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63`.
- Valid pixels: 270,606 из 480,000.
- Маска вычислена заранее и переиспользуется; область за пределами полезного круга не подаётся моделям как содержательное изображение.
## 4. Software и model configuration
### 4.1 Runtime
- Host: `<worker-host>`.
- GPU: NVIDIA GeForce RTX 4090.
- Container image: `nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794`.
- Python: 3.12.3.
- NumPy: 1.26.4.
- SciPy: 1.16.3.
- PyTorch: 2.13.0+cu130.
- Transformers: 4.57.6.
- PyAV: 18.0.0.
- lz4: 4.4.5.
- Media runtime payload SHA-256: `67487da59bba27676e5a5b4e43566e956fb350f1730589d93d55f22bcddc4c6b`.
### 4.2 Models
Detector:
- YOLOX-S, COCO-80, Triton inference;
- input 1×3×640×640;
- model SHA-256: `c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063`;
- config SHA-256: `5795c737a7935a655961b069e8404d336d891f9762fb6dffb93956a076479604`.
Semantic:
- `tue-mps/cityscapes_semantic_eomt_large_1024`;
- revision `8d6b6d1a3f7b50d441afd7d247c2ed10db186e8f`;
- FP16 autocast;
- model SHA-256: `c265da9a74f58f5c3f4826d23ca4ca78beac0b106cca5842beca61580de5b782`.
### 4.3 Immutable code/config identities
- Accepted live profile SHA-256: `8b7f267ee947932bdd52d1ef4b241adc8c9b4c4d5a8a3cd6c1e7f6e066983699`.
- E14 profile SHA-256: `a010e32070459b123423f3d4f57aa1d839f73ed05f3df8580d12693406a1b218`.
- Detector profile SHA-256: `819bdfb0da521d187ec9ce3d3039a0ff75daa0ab473fb3d8e5fa92db9c1ddca1`.
- Semantic profile SHA-256: `ea583966bc3409f5cf563cbf4fad05e366907e67187082eb692aff53d9f5d875`.
- Runner SHA-256: `8e487108404d912739b99d0043b8cbdd605ae148d8ad1d32ab30819ecfbb7dbc`.
- fMP4 runtime SHA-256: `6785763350f6e2c00796f3580ea6707e0c44339f18fa8eb56687178c6f0403fc`.
- PowerShell orchestrator SHA-256: `129bb6874730191cece10667b580fe5b8aa4e0878d442779679977079aa6069e`.
- Minimal worker package: `e15-worker-package-dd67306b6116dd6fa091557371eb80b27afb48dae4568fb0173f253a1fb4613a`.
- Minimal worker package identity SHA-256: `dd67306b6116dd6fa091557371eb80b27afb48dae4568fb0173f253a1fb4613a`.
- Package содержит 11 runtime modules и не тянет MQTT/BLE/web/device lifecycle в GPU-контейнер.
## 5. Scheduler и bounded-state contract
- Detector queue: latest-wins, capacity 2.
- Semantic queue: latest-wins, capacity 1.
- Semantic sample: каждый пятый camera frame.
- Semantic TTL: 750 ms.
- Sensor buffers: 32 LiDAR + 32 pose records максимум.
- Sensor retention: 3 s.
- Camera metadata queue: 16 максимум.
- Incremental media buffer: 8 MiB максимум.
- Camera → LiDAR maximum absolute delta: 100 ms.
- LiDAR → pose maximum absolute delta: 100 ms.
- Accepted sensor wait budget: 90 ms.
- Команды: disabled.
- Navigation/safety acceptance: false.
Камера не блокируется semantic model. Decoder публикует frame в две bounded очереди. Detector идёт на camera cadence; semantic worker независимо обновляет latest semantic mask. Detector result связывается с latest fresh semantic result и ближайшей допустимой LiDAR/pose парой.
## 6. Экспериментальные попытки
### 6.1 Attempt A — 20 ms sensor wait, rejected
Result ID: `e15-shadow-inference-396152240d878551ffe4913e03aee95be0b0ec572b69d76313edd73a7ee0cf7d`.
Все transport, decoder, FPS, semantic, latency, queue и authority checks прошли. Единственный failed check:
- fusion coverage 127/151 = 84.106%; требование ≥85%.
Причина:
- 19 camera frames получили `lidar-camera-delta-exceeded`;
- для девяти из этих frames следующий LiDAR находился всего в 4988 ms после camera timestamp;
- wait budget 20 ms завершал binding раньше, чем этот LiDAR был декодирован и опубликован;
- расширять temporal gate более 100 ms не потребовалось.
### 6.2 Infrastructure interruption — не считается lab attempt
Один повтор был остановлен idle SSH timeout во время model preflight. Consumer к source не подключился, события не публиковались, immutable result не создавался. После проверки оставались только штатные `mission-core-triton`, `sentinel-frigate`, `sentinel-ollama`. Повтор выполнен с временными SSH `ServerAlive` options без изменения сетевой/VPN конфигурации.
### 6.3 Attempt B — 90 ms sensor wait, accepted
Result ID: `e15-shadow-inference-f3f06e7c6b338add445f7ee4e4a81f831cd763a899ac2fbe29ba3476c4bdf0c8`.
Изменён только `sensor_wait_ms`: 20 → 90. Temporal gate остался 100 ms, models и все acceptance thresholds не менялись.
Результат:
- fused frames: 136/151 = 90.066%;
- `lidar-camera-delta-exceeded`: 19 → 10;
- accepted cuboid states: 329 → 355;
- p95 world-state age: 79.28 → 149.43 ms;
- лимит p95 world-state age: 200 ms;
- итог: accepted.
Это осознанный обмен дополнительного bounded ожидания на более полное depth coverage без допуска LiDAR старше 100 ms.
## 7. Acceptance matrix финального run
| Gate | Требование | Результат | State |
|---|---:|---:|---|
| Camera frames | ≥140 | 151 | pass |
| Camera decode accounting | 1:1 | 151/151 | pass |
| Detector effective FPS | ≥9.5 | 9.881 | pass |
| Detector drops | ≤1% | 0/151 | pass |
| Semantic effective FPS | ≥1.8 | 2.029 | pass |
| Semantic drops | ≤5% | 0/31 | pass |
| Semantic completion age p95 | ≤400 ms | 162.162 ms | pass |
| Fresh semantic coverage | ≥90% | 96.689% | pass |
| Fused fraction | ≥85% | 90.066% | pass |
| Decode age p95 | ≤80 ms | 15.150 ms | pass |
| World-state age p95 | ≤200 ms | 149.427 ms | pass |
| Ingress sequence gaps | 0 | 0 | pass |
| Camera sequence gaps | 0 | 0 | pass |
| Failures | 0 | 0 | pass |
| Session-end | required | seen | pass |
| Commands/navigation authority | false | false | pass |
Все 18 acceptance checks прошли.
## 8. Производительность финального run
### 8.1 End-to-end
- Source span: 14.989973 s.
- Measured run wall: 15.281886 s.
- Detector: 151 frames, 9.881 FPS.
- Semantic: 31 frames, 2.029 FPS.
- Fused: 136 frames.
- Source-paced factor: около 1.019× wall/source, то есть контур удержал темп источника.
- Full orchestrator wall including isolated preflight, second model load, warm-up, run and publication: 113.669 s.
Большой orchestrator wall — cold-start overhead лабораторного запуска, а не latency активного perception loop. Для onboard-контура модели должны жить в постоянном worker process и не загружаться дважды перед каждой сессией.
### 8.2 Latency, ms
| Stage | mean | p50 | p95 | max |
|---|---:|---:|---:|---:|
| fMP4 decode age | 8.003 | 6.428 | 15.150 | 41.297 |
| Detector queue wait | 8.577 | 0.086 | 47.431 | 165.393 |
| Detector | 25.607 | 23.899 | 36.631 | 43.492 |
| Sensor wait | 10.075 | 0.033 | 90.136 | 110.995 |
| KB4 projection | 0.538 | 0.372 | 0.838 | 12.403 |
| Association/cuboid | 5.297 | 4.100 | 14.197 | 55.061 |
| Clearance | 0.204 | 0.177 | 0.413 | 0.877 |
| World-state projection | 0.154 | 0.131 | 0.358 | 0.748 |
| End-to-end world-state age | 58.154 | 43.815 | 149.427 | 282.994 |
World-state health:
- healthy: 136 frames;
- degraded: 13 frames;
- stale: 2 frames;
- unavailable: 0 frames.
Два max-latency spikes выше 200 ms не нарушили заданный p95 gate, но подтверждают, что результат пока не имеет safety authority.
### 8.3 Semantic timing, ms
| Stage | mean | p50 | p95 | max |
|---|---:|---:|---:|---:|
| Total processing | 135.524 | 126.185 | 147.835 | 396.316 |
| Forward | 107.197 | 99.099 | 109.701 | 369.566 |
| Completion age | 146.979 | 137.378 | 162.162 | 437.821 |
Semantic fresh status был доступен для 146/151 detector frames. Первые пять frames корректно помечены `semantic-unavailable`, а не замаскированы фиктивным результатом.
### 8.4 Sensor decode
- LiDAR decode mean/p95/max: 12.373 / 12.913 / 97.968 ms.
- Pose decode mean/p95/max: 0.077 / 0.103 / 0.186 ms.
- Synchronizer maximum stored depth: 32 point frames + 32 pose frames.
- Media buffer maximum actual depth: 178,687 bytes из 8,388,608 bytes.
- Camera metadata maximum depth: 2 из 16.
## 9. Functional output
- 1,188 detector/fusion object records рассмотрено за 151 frames.
- 355 accepted amodal cuboid states опубликовано.
- Frames с хотя бы одним accepted 3D object: 130/151.
- Maximum simultaneous accepted objects: 7.
- Accepted class states: 350 vehicle, 5 person.
- Cuboids являются temporal object states, а не 355 уникальными физическими объектами.
- Ground-truth quality в E15 не измерялась.
Основные rejection classes:
- fewer than 8 clustered points: 572;
- no semantic/LiDAR support: 173;
- amodal support coverage below threshold: 72;
- amodal required size exceeds class bound: 7;
- fewer than 4 clustered points: 6;
- distance innovation: 3.
Эти отказы fail-closed: сомнительная геометрия не превращается в принятый 3D cuboid.
## 10. Hardware и storage
Accepted run:
- process peak RSS: 2,023.5 MiB;
- process CUDA peak allocated: 2,107.9 MiB;
- process CUDA peak reserved: 2,840 MiB;
- total GPU memory used telemetry: около 13,298 MiB, включая соседний Triton contour;
- GPU utilization mean/p95/max: 27.4% / 45% / 58%;
- GPU memory-utilization mean/p95/max: 11.8% / 20% / 25%;
- GPU power mean/p95/max: 140.5 / 144.65 / 145.41 W;
- GPU temperature mean/p95/max: 44.6 / 47 / 48 °C.
Storage:
- hard free-space floor: 360 GiB;
- observed final free space: около 389.34 GiB;
- accepted result payload: около 1.56 MiB;
- no per-frame PNG/JPEG output;
- no recorded video copy in result;
- disk `C:` не проверялся и не использовался;
- persistent inputs, runtime и results находятся на `D:`.
## 11. Accepted artifacts
Local audit mirror:
`.runtime/compute-experiments/e15/results/e15-shadow-inference-f3f06e7c6b338add445f7ee4e4a81f831cd763a899ac2fbe29ba3476c4bdf0c8`
| Artifact | Bytes | SHA-256 |
|---|---:|---|
| `fusion-frames.jsonl` | 1,016,064 | `ae510afe881d7d6c5bc94520c5f87c385fdeb984bd30bd0cff70bd103f718963` |
| `world-state.jsonl` | 568,384 | `579266c99ae1e65b0240e8c5c224beba4073d05c531c5a725f096e9bbf9a3c05` |
| `semantic-frames.jsonl` | 13,955 | `21a8a659cc19e2acc9b971b93b746a4e26f8bb51829d1e1d13c4f42c1f79f1a5` |
| `gpu-telemetry.jsonl` | 3,591 | `51024d0774dd7e648ca4ae0cba69cca4de70baf8e47c425fa78c6805c2824712` |
| `run-report.json` | 13,230 | `f04fce80d9246d3c19f7368d6174787eb2fd73cdc7c51fa8d18555a0bfa0bf65` |
| `result.json` | 6,384 | `5b933109eedba02483ad35c65221d344da19f7e2584bd700d9be7c6b2ecef995` |
Rejected Attempt A также сохранён неизменяемо:
`.runtime/compute-experiments/e15/results/e15-shadow-inference-396152240d878551ffe4913e03aee95be0b0ec572b69d76313edd73a7ee0cf7d`
## 12. Вывод
На текущем RTX 4090 доказан не просто offline pipeline, а bounded source-paced perception loop на 15-секундном recorded replay:
- camera 10 Hz;
- detector около 10 Hz;
- semantic около 2 Hz;
- calibrated LiDAR/pose fusion;
- diagnostic 3D cuboids и world-state;
- p95 результата 149.4 ms;
- нулевые transport/model queue drops;
- 90.1% fused coverage;
- никакой command authority.
Следующий правильный gate — не дальнейшее улучшение recorded-video качества, а тот же профиль на физическом K1 shadow stream. Параллельно нужно убрать лабораторный cold start, превратив model-loaded contour в постоянный worker service. Только после повторяемого physical live gate имеет смысл подключать live world-state/3D cuboids к Rerun UI и отдельно начинать forest-domain evaluation и safety policy.
## 13. Следующие действия
1. Поднять persistent model-loaded E15 worker на `D:` без двойной загрузки моделей на каждую сессию.
2. Подключить физический K1 через уже существующий shadow ingress без command path.
3. Повторить ≥15 s physical shadow gate с теми же thresholds и отдельным LAB E16 report.
4. Добавить reconnect/worker-loss negative control: raw recorder обязан продолжить запись.
5. Передавать accepted `world-state` и cuboids в live Rerun entities с source/result timestamps и health.
6. После live transport qualification собирать forest-domain ground truth и считать 2D segmentation/object, distance и 3D cuboid quality отдельно от throughput.

View File

@ -0,0 +1,148 @@
# LAB E16 — Persistent AI worker and unified live perception publication
Date: 2026-07-22
Acceptance time: 2026-07-22 21:23 MSK / 2026-07-22T18:23:49Z
## Purpose
LAB E16 validates the infrastructure between the previously accepted E15 perception
pipeline and one unified Mission Core Rerun scene. This is an infrastructure
acceptance, not a physical K1 perception acceptance. No device, acquisition or camera
session was active during this lab.
The target operator contract is one original-video timeline with independently
switchable derived layers:
- tracked 2D detections with confidence and metric distance when fusion provides it;
- semantic segmentation on the original camera image;
- accepted, calibrated 3D cuboids in the LiDAR/world scene.
## Safety and authority
- The production Mission Core process at `127.0.0.1:8000` was not stopped or restarted.
- UI and backend acceptance used an isolated shadow process at `127.0.0.1:8001`.
- The AI worker ran only on the Windows worker's `D:` drive.
- VPN, Hidemyname, Wi-Fi and host network configuration were not modified.
- Worker authority remained `commands_enabled=false` and
`navigation_or_safety_accepted=false`.
- The result direction is diagnostic-only and contains no command endpoint.
## Immutable worker inputs
- Runner SHA-256:
`f2f2889e07c609e3ba53b4504c3f2417f151fbb9920828f4680ea375a5d3010e`.
- Persistent-service orchestrator SHA-256:
`ed2dc35de423d0d9f8ac0501db283a10081e78b6535ebeb8d521a58ca6988b11`.
- Persistent-run orchestrator SHA-256:
`1c6af15736396ee0124d4aabe309b7bc24bb1425d88e24295c5675c686fc27bc`.
- Worker package:
`e15-worker-package-34fb22ce506aab6e3b6a0fd8058c868e24076a8cfc739b4299b2474f2cb92b99`.
- Runner root on the worker:
`D:\NDC_MISSIONCORE\runtime\derived\e16-runner-f2f2889e07c6-ed2dc35de423-1c6af1573639-34fb22ce506a`.
- Worker package root:
`D:\NDC_MISSIONCORE\runtime\inputs\e16\worker-packages\e15-worker-package-34fb22ce506aab6e3b6a0fd8058c868e24076a8cfc739b4299b2474f2cb92b99`.
- Container: `mission-core-perception-worker`.
- Pinned base image: `nvcr.io/nvidia/tritonserver:26.06-py3` with the E15 digest.
## Persistent model-loaded worker result
- GPU: NVIDIA GeForce RTX 4090.
- Model load and warm-up: 38.296654 s.
- Health: `ready`.
- Models loaded: true.
- Completed physical runs: 0.
- Failed physical runs: 0.
- D: free after startup: 418,065,694,720 bytes, approximately 389.354 GiB.
- Guarded D: floor: 360 GiB plus a 512 MiB startup reserve.
The service binds its control HTTP endpoint only to `127.0.0.1` inside the worker
container's shared Triton network namespace. It has no exposed LAN control port. A run
request is single-flight, authenticated by the existing shadow token, limited to one
direct child of the guarded D:-backed publication root and reuses already loaded models.
## Result transport contract
The authenticated K1 shadow WebSocket remains the sensor ingress and now has one
bounded worker-to-Mission-Core diagnostic result direction.
- Result schema: `missioncore.live-perception-result-wire/v1`.
- Result queue: latest-wins, capacity 2.
- Maximum result payload: 2 MiB.
- Maximum JSON header: 256 KiB.
- Maximum original JPEG: 1 MiB.
- Segmentation: fixed 800x600 uint8 mask compressed with zlib.
- Maximum objects per frame: 128.
- Payload SHA-256, lengths, geometry, timestamps and authority are validated before
the frame is admitted to the visualization runtime.
- Partial cuboids, non-finite geometry, invalid JPEG/mask payloads and any authority
claim are rejected.
## Unified Rerun publication
Accepted live results use the same timeline and stable entities:
- `/perception/camera/image` — original worker-decoded camera frame;
- `/perception/camera/detections` — 2D boxes, track id, class, confidence and distance;
- `/perception/camera/segmentation` — semantic mask;
- `/world/perception/boxes3d` — calibrated accepted cuboids.
The live blueprint switches between the normal 3D-only scene and a single horizontal
original-video plus 3D-world composition. The three AI controls are independent; they
are not mutually exclusive views. When dynamic perception is active, Rerun uses
latest-at presentation so historical cuboids do not stack into a cloud of boxes.
## Validation
- Complete Python test suite: passed.
- Focused live result, WebSocket return, visualization runtime and Rerun bridge tests:
passed.
- Ruff on the changed Python surface: passed.
- Frontend TypeScript build: passed.
- Frontend unit suite: 144/144 passed.
- Production frontend build: passed.
- Windows PowerShell parser accepted both service and run orchestrators.
- In-container encode/decode probe returned a 600x800 segmentation mask successfully.
- Shadow backend health at `127.0.0.1:8001`: healthy.
- Production backend health at `127.0.0.1:8000`: healthy and uninterrupted.
- Browser QA opened RAVNOVES00, reached Rerun ready state, enabled 2D,
segmentation and 3D cuboids simultaneously, and reported zero browser errors.
## Disk handling
The earlier 418 MiB temporary recorded-perception RRD used for payload inspection was
removed after validation. It was a regenerable temporary duplicate, not source evidence.
No source recording or user data was deleted.
## Accepted conclusions
1. The models can remain loaded between runs; the previous double-load startup cost is
removed from subsequent run requests.
2. Mission Core has a bounded, authenticated, non-authoritative return path for live AI
presentation.
3. Original video, 2D detections, segmentation and 3D cuboids now share one Rerun
timeline and one operator composition.
4. Layer visibility is independently controlled and no longer modeled as separate
perception streams.
## Explicitly not accepted yet
- Physical K1 shadow inference: no device session was active.
- End-to-end detector, segmentation and fusion FPS through the new persistent service:
requires the next physical 20-second K1 gate.
- The new backend code is intentionally not active on production port 8000 yet because
that process was not restarted. Port 8001 is the accepted shadow build.
- 3D semantic point coloring is not yet returned by the live result frame; E16 covers
camera segmentation and 3D cuboids.
- The recorded full-session perception RRD remains a large monolith and is not the
target real-time transport.
- None of the outputs are ground truth or safety/navigation accepted.
## Next gate
1. Connect K1 and establish a normal Mission Core device/acquisition/camera session.
2. Run one 20-second persistent physical shadow request without command authority.
3. Record detector FPS, semantic FPS, fusion fraction, result age, result payload rate,
GPU telemetry, queue drops and D: free space.
4. Verify the same four entities visually in the live Rerun scene.
5. Add bounded semantic LiDAR point coloring, then repeat as LAB E17.

View File

@ -0,0 +1,297 @@
# LAB E3 — K1 rectified segmentation baseline
Date: 2026-07-20
Completed at: 2026-07-20T18:57:23.215Z
State: completed, model draft, not ground truth
Ops card: MISSIONCOR-17
## Objective
Compare an adult Cityscapes semantic model on the same immutable XGRIDS K1
evaluation pack under three input profiles:
1. valid-FOV fisheye image;
2. valid-FOV fisheye image with CLAHE;
3. factory-calibrated KB4 five-view perspective rectification with CLAHE and
fusion back into the original fisheye coordinate system.
The experiment answers whether calibration-derived rectification is technically
valid, how much it costs, and whether it is a credible next baseline. It does not
measure accuracy because no reviewed ground truth exists yet.
## Immutable inputs and provenance
- Evaluation pack:
`evaluation-pack-7a983bba75d46c7c260252cb2d461e1384dcb92cda9e164397e841e6ebb37789`
- Session: `20260720T065719Z_viewer_live`
- Frames: 64, image IDs 164, resolution 800×600
- Source: `sensor.camera.right`
- Factory calibration slot: `camera_1`
- Factory calibration content SHA-256:
`05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`
- Valid-FOV generation:
`valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2`
- Valid pixels per frame: 270,606
- LAB E2 comparison draft:
`evaluation-prelabels-4ba26bbf6eb8a49631f5caf984267e0445958540aeda2b5b0d82ca6440835cf1`
The LAB E2 draft has a legacy provenance inconsistency. Its declared identity
SHA-256 is `4ba26b…`, while a fresh canonical hash of the stored identity document
is `33292c85eb8281d68c1b444baaf0d7931c7aa84938ac1d329b89e93f3b525099`.
LAB E3 did not alter LAB E2. It independently verified the evaluation-pack
binding, the declared result/directory relationship, and the size and SHA-256 of
every one of the 64 consumed semantic artifacts. Both hashes and
`e2_prelabel_identity_consistent=false` are preserved in the LAB E3 identity.
## Calibration and preprocessing configuration
Camera-1 intrinsics (`fx, fy, cx, cy`):
```text
194.59817287616025
194.57531427932872
396.31861150187996
301.49644357408005
```
KannalaBrandt KB4 coefficients:
```text
-0.023164451386679667
-0.0014974198594105452
-0.001039213149441563
-0.000035237331915978814
```
Rectification profile:
- projection: five perspective gnomonic views;
- views: front, left, right, up, down;
- yaw/pitch: `0/0`, `-90/0`, `90/0`, `0/90`, `0/-90` degrees;
- tile size: 768×768;
- horizontal and vertical FOV: 100 degrees;
- RGB interpolation: OpenCV linear remap;
- fused-label sampling: nearest pixel;
- overlap winner: maximum optical-axis cosine;
- contrast: CLAHE on LAB luminance, clip limit 2.0, grid 8×8.
Measured geometry coverage:
- valid pixels covered: 270,606 of 270,606;
- coverage: 100%;
- views covering one valid pixel: minimum 1, maximum 3, mean 1.190099259;
- uncovered valid pixels: 0.
## Model and software identity
- Model: `tue-mps/cityscapes_semantic_eomt_large_1024`
- Architecture: `EomtForUniversalSegmentation`
- Revision: `8d6b6d1a3f7b50d441afd7d247c2ed10db186e8f`
- Precision: FP16 autocast
- Batch size: 1
- Model weights: 1,276,175,488 bytes, SHA-256
`c265da9a74f58f5c3f4826d23ca4ca78beac0b106cca5842beca61580de5b782`
- Config SHA-256:
`7f4aa94fa4e43c0dbd79a5420edb511120aef62bd82bfbcbcece79948286a650`
- Preprocessor SHA-256:
`97e2fbf7f0bdba2cfc90251c5133bae9c27ddc9c4410509f40670be2332854e7`
- OpenCV distribution: `opencv-python-headless==4.13.0.92`
- OpenCV runtime module: `4.13.0`
- Transformers: `4.57.6`
- PyTorch: `2.13.0+cu130`
- CUDA runtime: `13.0`
- Container: existing `nvcr.io/nvidia/tritonserver:26.06-py3`; no image pull
- Profile SHA-256:
`ea583966bc3409f5cf563cbf4fad05e366907e67187082eb692aff53d9f5d875`
- Dependency identity SHA-256:
`4eb1f8d33236806e74f9e5bb96b7dce2ac37623dc39b2184be2aa8d7d00e983e`
- Final runner SHA-256:
`01881862d4eaa218955f776a948124bf19c34be2b5ec282115daeacb15c53ae6`
The Cityscapes labels were mapped into the existing Mission Core 015
perception taxonomy. Outputs outside the admitted K1 valid FOV were forced to
category 0.
## Execution topology
The comparison was deliberately sequential so each profile has isolated timing:
```text
frame
-> EoMT on fisheye mask
-> CLAHE -> EoMT on fisheye mask
-> KB4 remap of five views -> CLAHE -> five sequential EoMT calls
-> nearest-label fusion into the raw fisheye coordinates
-> masks, preview, metrics, and telemetry
```
There were seven model evaluations per frame and 448 evaluations for the full
pack. No software GPU-utilization or power cap was applied. The experiment did
not batch or parallelize the five rectified views; that is a future optimization,
not part of the present baseline.
## Full-run results
Immutable result:
`e3-segmentation-01bd497c44c2b940add145ec784d3418010327bce0baddd4420b0925317e8a16`
| Variant | Mean end-to-end/frame | Mean forward | Approx. standalone throughput | Mean change vs E2 draft |
|---|---:|---:|---:|---:|
| EoMT fisheye mask | 121.02 ms | 98.64 ms | 8.26 FPS | 16.44% pixels |
| EoMT fisheye + CLAHE | 127.41 ms | 99.10 ms | 7.85 FPS | 16.51% pixels |
| EoMT KB4 cubemap5 + CLAHE | 341.97 ms | 267.39 ms across 5 views | 2.92 FPS | 18.13% pixels |
Additional timing facts:
- full comparison wall time: 46.170785 seconds;
- average full comparison time: 721.42 ms/frame including all three variants,
preview/mask encoding, metrics, and telemetry;
- five-view rectification preprocessing: 22.60 ms/frame mean;
- five-view label fusion: 3.03 ms/frame mean;
- one rectified tile forward: 53.48 ms mean, 65.85 ms p95;
- raw-vs-cubemap pixel disagreement: 8.24% mean, 5.80% median, 24.66% p95,
36.76% maximum.
The largest raw/cubemap difference occurs at image 59 / frame 4224. The
near-structure temporal clip (images 6063, frames 44334436) contributes four
of the next five largest differences. This is the correct area to prioritize in
human review: extreme distortion and nearby planar structures are precisely
where projection choice changes the model most.
## GPU, CPU, memory, and disk
Worker GPU: NVIDIA GeForce RTX 4090, compute capability 8.9.
- process CUDA peak allocated: 2,099.8 MiB;
- process CUDA peak reserved: 2,840.0 MiB;
- total board memory used, including pre-existing services: 13,406 MiB max,
13,396.9 MiB mean;
- GPU utilization: 72.34% mean, 91% max;
- GPU memory-controller utilization: 27.38% mean, 41% max;
- GPU power: 288.88 W mean, 334.09 W max;
- GPU temperature: 55.94°C mean, 61°C max;
- process peak RSS: 2,097.14 MiB;
- GPU telemetry samples: 47 at one-second intervals.
All task-controlled persistent artifacts are on `D:\NDC_MISSIONCORE`:
| Artifact group | Bytes | MiB |
|---|---:|---:|
| Pinned E3 model cache | 1,276,294,838 | 1,217.17 |
| Pinned OpenCV environment | 211,776,082 | 201.97 |
| Four-frame pilot | 2,040,795 | 1.95 |
| Full 64-frame result | 7,924,484 | 7.56 |
| Total | 1,498,036,199 | 1,428.64 |
Final D: free space was 414,903,472,128 bytes (386.409 GiB), 26.409 GiB above
the enforced 360 GiB floor. No task-controlled C: path was created or mounted.
The report does not claim to measure incidental internal writes by Windows or an
already configured Docker Desktop engine.
## Visual findings
1. The factory KB4 geometry is usable now. The five views are upright, have the
expected content, and fuse back without missing valid pixels.
2. EoMT produces visibly cleaner road, grass, building, person, and vehicle
regions than the LAB E2 BEiT draft on the inspected samples.
3. CLAHE changes the global result very little while adding about 6.4 ms/frame.
It is not justified as the default from this unreviewed proxy alone.
4. Cubemap5 often improves separation of people, cars, and rectified planar
surfaces, especially where raw fisheye distortion is severe.
5. Hard winner-take-all fusion creates local seam/checker artifacts, most visible
near the lower rim, the sun/sky, and close structures. Cubemap5 should not be
promoted to the default without seam-aware fusion and reviewed accuracy.
6. Cityscapes has no explicit dirt, animal, or general other-background output in
this mapping. That limits this model as a complete forest/off-road taxonomy.
## Integrity and acceptance status
- 211 of 211 listed output artifacts were rehashed successfully after transfer.
- 192 of 192 semantic masks were checked.
- Every semantic value was within categories 015.
- Every pixel outside the valid-FOV mask was category 0.
- Four-frame pilot and full-run outputs are immutable and separately identified.
- Outputs remain `ground_truth=false`.
- No mIoU, AP, distance accuracy, 3D geometry, tracking, or safety claim is made.
## Engineering gates encountered
The following preflight failures happened before a result was accepted and are
preserved as part of the laboratory history:
- Git revision and SHA-256 were initially treated as the same hash shape;
- the OpenCV package version `4.13.0.92` differs from runtime `cv2` version
`4.13.0`;
- Hugging Face snapshot files are symlinks into a content-addressed blob store;
- the initially recorded weight SHA was replaced by the independently measured
blob SHA-256 `c265…`;
- EoMT processor output includes a non-tensor `task_inputs` field;
- LAB E2 contains the identity inconsistency documented above;
- a PowerShell full-run expected-frame lookup initially read the wrong JSON
level. That technical run was stopped, its exact transient container was
removed, its staging was cleaned, and no result was published.
These were fail-closed contract checks, not model failures.
## Decision and next gate
The current evidence supports this decision:
- keep plain EoMT fisheye-mask as the speed/quality control baseline;
- keep cubemap5 as the calibration-derived challenger;
- do not make CLAHE the default yet;
- do not select a production winner from proxy disagreement or visual inspection;
- review the high-disagreement and temporal frames in CVAT, export reviewed masks,
and calculate class-wise IoU, boundary IoU, temporal stability, and object-class
recall before changing the default pipeline;
- if cubemap5 survives reviewed accuracy, replace hard view selection with
overlap-aware/logit fusion and then test batched five-view inference.
3D cuboids, LiDAR association, distance accuracy, and point-cloud segmentation
remain outside LAB E3 and should use the reviewed 2D baseline as an input to the
next dedicated experiments rather than being mixed into this result.
## CVAT review handoff
The control and challenger were converted into deterministic CVAT Segmentation
Mask 1.1 packages and imported into the existing D-hosted CVAT v2.70.0 instance.
The original 64-frame image archive is reused and was not duplicated.
- Workspace:
`e3-cvat-review-ca599521e345446ca8e9af5a9013062099278e7317f83ff89740c6b092ddc52f`
- Workspace manifest SHA-256:
`5e1b318d31431c2081c169235ec877693591ef2b05bbebab61c72061bc1621d9`
- CVAT import report SHA-256:
`fe0933efc3a5bb1bcb7b995bb1241b308e7a860113cd7a97351aedfbd43c9b64`
- [Task 3 — EoMT fisheye control](http://localhost:18080/tasks/3): 64 frames,
5,284 imported shapes.
- [Task 4 — EoMT KB4 cubemap5 challenger](http://localhost:18080/tasks/4): 64
frames, 5,762 imported shapes.
- Both task pages returned HTTP 200 through the existing SSH tunnel.
- Both tasks remain model drafts and `ground_truth=false`.
- Review priority by measured raw/cubemap disagreement: image IDs
`59, 63, 62, 61, 60, 64, 50, 30, 17, 29`.
- Review bundle size on D: 1,390,051 bytes.
- Post-import D: free space: 414,762,061,824 bytes (386.277 GiB), 26.277 GiB
above the enforced floor.
The required human sequence is still two passes: correction/completeness first,
then independent consistency review. Importing the drafts does not complete that
gate and does not convert either task into ground truth.
## Durable paths
Worker full result:
```text
D:\NDC_MISSIONCORE\runtime\derived\e3-segmentation\e3-segmentation-01bd497c44c2b940add145ec784d3418010327bce0baddd4420b0925317e8a16
```
Local evidence copy:
```text
.runtime/compute-experiments/e3/full/e3-segmentation-01bd497c44c2b940add145ec784d3418010327bce0baddd4420b0925317e8a16
```
The `result.json`, `run-report.json`, 16 comparison previews, rectification
atlas, 192 semantic masks, and GPU telemetry are contained in that result.

View File

@ -0,0 +1,360 @@
# LAB E4 — Full-session EoMT segmentation playback
Date: 2026-07-21
Completed at: 2026-07-21T07:21:16.791Z
State: completed and published, model output, not ground truth
Ops card: MISSIONCOR-18
## Objective
Turn the plain EoMT valid-FOV configuration selected in LAB E3 into the first
complete, watchable semantic-segmentation result for a saved Mission Core
session. The acceptance target was deliberately narrow and observable:
1. process every recorded camera frame, without sampling;
2. preserve the exact saved-session timeline;
3. publish an H.264 semantic-overlay video and per-frame semantic masks through
the existing recorded-perception contract;
4. make that video selectable and playable in the RAVNOVES00 saved-session UI;
5. measure full inference, publication, GPU, memory, power, temperature and disk
behavior on the connected AI worker;
6. keep all task-controlled worker data on `D:\NDC_MISSIONCORE` and enforce a
360 GiB free-space floor.
LAB E4 is semantic-only. It does not claim instance detection, tracking, 3D
cuboids, LiDAR association, metric distance, point-cloud segmentation or
safety-grade perception.
## Immutable input and provenance
- UI session alias: `RAVNOVES00`
- Session ID: `20260720T065719Z_viewer_live`
- Compute job: `recorded-camera-602ac89026ed12978619801d`
- Source: `sensor.camera.right`
- Factory calibration slot: `camera_1`
- Input SHA-256:
`602ac89026ed12978619801d4edea0cae24b5cc3afabd9f7af2858de6505a20e`
- Factory calibration content SHA-256:
`05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`
- Codec epoch: 1
- Resolution: 800x600
- Segments/frames admitted: 4,489/4,489
- Timeline: 35.421857292484.144857292 session seconds
- Media duration: 448.723 seconds
- Approximate source rate: 10.0037 FPS
- Frame timeline SHA-256:
`16098d7606e5462f1d6ccbc1b6e99158f68e2b2ca85ea11794799ef709b61c61`
The valid image circle is the immutable LAB E1 artifact
`valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2`.
Its mask SHA-256 is
`a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63`.
It admits 270,606 of 480,000 pixels. The mask is loaded once and reused; it is
not recomputed from every frame.
## Accepted configuration
LAB E4 deliberately promotes the LAB E3 speed/quality control, not the more
expensive five-view challenger:
- pipeline: `recorded-semantic-eomt-fisheye-mask/v1`;
- model: `tue-mps/cityscapes_semantic_eomt_large_1024`;
- architecture: `EomtForUniversalSegmentation`;
- pinned revision:
`8d6b6d1a3f7b50d441afd7d247c2ed10db186e8f`;
- model weights: 1,276,175,488 bytes, SHA-256
`c265da9a74f58f5c3f4826d23ca4ca78beac0b106cca5842beca61580de5b782`;
- config SHA-256:
`7f4aa94fa4e43c0dbd79a5420edb511120aef62bd82bfbcbcece79948286a650`;
- preprocessor SHA-256:
`97e2fbf7f0bdba2cfc90251c5133bae9c27ddc9c4410509f40670be2332854e7`;
- precision: FP16 autocast;
- batch size: 1;
- frame policy: all frames, no sampling;
- semantic-overlay alpha: 0.48;
- CLAHE: disabled;
- KB4 cubemap rectification: disabled;
- instance branch: disabled;
- container: existing `nvcr.io/nvidia/tritonserver:26.06-py3`;
- PyTorch: `2.13.0+cu130`;
- CUDA runtime: `13.0`;
- Transformers: `4.57.6`;
- NumPy: `1.26.4`;
- Pillow: `12.3.0`.
Producer identities:
- runner SHA-256:
`651e8e06c3912dffb036b7fd08f2c0623f7563d8306cc7aee05db562798518f4`;
- orchestrator SHA-256:
`c1c16fc351e7c8c4806fe2fd17a56e659495d843db5ff3e9ac3699a8d9722640`;
- profile SHA-256:
`ea583966bc3409f5cf563cbf4fad05e366907e67187082eb692aff53d9f5d875`;
- dependency identity SHA-256:
`4eb1f8d33236806e74f9e5bb96b7dce2ac37623dc39b2184be2aa8d7d00e983e`.
The Cityscapes outputs are mapped into the existing Mission Core 015
robotics taxonomy. Pixels outside the admitted FOV are category 0. Cityscapes
does not provide the required forest-specific `ground_dirt`, `animal` and
general `other_background` classes, so those mapped totals are zero. This is a
known taxonomy limitation, not evidence that those contents are absent.
## Execution topology
The work was sequential at frame level:
```text
recorded fMP4 epoch
-> exact RGB frame reconstruction
-> fixed calibration-bound valid-FOV fill
-> EoMT semantic forward pass on RTX 4090
-> resize/map semantic labels into Mission Core taxonomy
-> force excluded FOV pixels to category 0
-> write indexed semantic PNG and 48%-alpha overlay PNG
-> append timestamped frame metadata
-> encode overlays as H.264 MP4
-> archive semantic masks
-> hash, validate and publish result
```
The input stream and model loop were not split across parallel jobs. Batch size
was one. GPU work is internally asynchronous where PyTorch/CUDA permits, but
the next frame was not admitted until the current frame's semantic result and
artifacts had been produced. The final video was encoded on NVENC after the
overlay sequence was complete.
No software power, utilization or memory cap was applied. The main deliberate
limits were batch size one, one model, a 360 GiB D: free-space floor and a
conservative preflight working-set reserve.
## Pilot gate
An eight-frame non-publishable pilot ran first under
`pilot-8-4038b699764048579f9fb7c6886a0a10`.
- frames: 8/8;
- inference wall: 2.381609 seconds;
- throughput including frame artifacts: 3.359074 FPS;
- visual check: valid-FOV alignment, road, vegetation, vehicles, person and
building overlays were usable;
- publication: prohibited by design.
The pilot exposed a report-only PowerShell parsing defect in a free-space
measurement: status text and the numeric value reached the same output stream.
The full-run orchestrator separated them before execution. The pilot data was
retained as evidence; no pilot result entered the saved-session result store.
## Full-run result
Published result:
`result-793785170472c519486ccd666be102fb04d169d92383acda3fcc29eecf045d30`.
| Measurement | Result |
|---|---:|
| Frames expected / processed | 4,489 / 4,489 |
| Frames failed / skipped | 0 / 0 |
| Inference wall | 1,447.565362 s (24:07.565) |
| Inference throughput | 3.101069 FPS |
| Extract/reconstruction phase | 31.837595 s |
| H.264 encoding | 3.374570 s |
| Mask archive | 1.332036 s |
| Full worker wall | 1,668.259063 s (27:48.259) |
| End-to-end throughput | 2.690829 FPS |
The source plays at approximately 10.0037 FPS. This offline E4 path is therefore
about 3.72 times slower than source real time. It is a complete playback
baseline, not yet a live-stream configuration.
The earlier LAB E0 full path achieved 1.678305 inference FPS and 1.593908
end-to-end FPS while executing both Mask R-CNN and BEiT. E4 is approximately
1.85 times faster on the measured inference loop and 1.69 times faster overall,
but the comparison is not like-for-like: E4 is semantic-only and intentionally
disables the instance branch.
### Per-frame latency
| Stage | Mean | p50 | p95 | Max |
|---|---:|---:|---:|---:|
| End to end | 322.458 ms | 320.654 ms | 348.467 ms | 441.481 ms |
| Model forward | 141.142 ms | 140.483 ms | 153.122 ms | 185.848 ms |
| Artifact write | 99.477 ms | 98.231 ms | 113.678 ms | 155.797 ms |
| Image decode | 30.677 ms | 28.915 ms | 42.952 ms | 82.429 ms |
| Processor | 10.656 ms | 9.597 ms | 15.529 ms | 85.791 ms |
| Host to device | 7.737 ms | 7.515 ms | 10.726 ms | 21.986 ms |
| Model postprocess | 10.214 ms | 10.421 ms | 14.078 ms | 25.200 ms |
| Overlay | 18.712 ms | 17.595 ms | 25.332 ms | 42.439 ms |
| Valid-FOV fill | 1.952 ms | 1.834 ms | 2.582 ms | 7.701 ms |
The measured stage means account for the frame loop. Approximately 184.15
seconds of the full worker wall remain outside extract, inference, encode and
archive. This includes full input/model hashing, model load, container/preflight
work, exact stream setup, result finalization, output hashing and validation.
## GPU, process and neighboring services
Worker GPU: NVIDIA GeForce RTX 4090, compute capability 8.9.
- one-second GPU samples: 1,448;
- GPU utilization: 73.05% mean, 73% p50, 89% p95, 95% max;
- board memory used, including pre-existing services: 13,397.65 MiB mean,
13,409 MiB max;
- GPU memory-controller utilization: 21.41% mean, 32% p95, 35% max;
- power: 234.24 W mean, 232.91 W p50, 247.15 W p95, 253.23 W max;
- temperature: 52.94 C mean, 53 C p50, 57 C p95, 58 C max;
- process CUDA peak allocated: 2,099.8 MiB;
- process CUDA peak reserved: 2,840.0 MiB;
- process peak RSS: 2,009.332 MiB.
The board-memory figure is not the E4 process allocation. It is the total
device usage observed by `nvidia-smi`, including already running services. The
per-process CUDA peaks above are the relevant E4 allocation measurements.
`mission-core-triton`, `sentinel-frigate` and `sentinel-ollama` remained up.
## Disk policy and measured usage
The orchestrator admitted only explicit paths rooted at
`D:\NDC_MISSIONCORE`. It performed a conservative space preflight, sampled free
space every 100 frames and failed closed if the floor would be crossed.
| Disk point | Free bytes | Free GiB |
|---|---:|---:|
| Before run | 410,711,453,696 | 382.505 |
| After input extraction | 407,553,695,744 | 379.564 |
| After inference | 404,899,336,192 | 377.092 |
| After final artifacts | 404,796,903,424 | 376.996 |
| After task temporary cleanup | 407,372,025,856 | 379.395 |
| Enforced floor | 386,547,056,640 | 360.000 |
The conservative working-set reserve was 18,462,883,615 bytes (17.195 GiB).
The floor was never crossed. Final headroom above the floor was approximately
19.395 GiB. No task-controlled C: path was used, created or mounted. This does
not claim to observe incidental internal writes by Windows or the pre-existing
Docker Desktop engine.
Only exact E4 temporary extraction, mask and overlay work directories were
removed after successful publication. Immutable result artifacts, reports,
telemetry, the pilot and runner identities remain recoverable on D:.
## Published artifacts and integrity
| Artifact | Bytes | SHA-256 |
|---|---:|---|
| `perception.mp4` | 75,353,780 | `6e58cf72d9571ebbb003a814132b44907b7c0e31d18b5449cce330afcad29829` |
| `masks.tar.gz` | 39,812,627 | `ef291034e51c5c719dcc1c0902bae1e3daedf919d7b55a21fcb0a31af298071f` |
| `frames.jsonl` | 3,927,616 | `4780339f5ef94e1f58922dfe2df67ecdf983c4b4e030761f4a990f65b511d5cf` |
| `gpu-telemetry.jsonl` | 325,407 | `c6a4db1a8ea01d1ca66aded4bcd990ed91fc015930113869b20f024632a2aacf` |
| `run-report.json` | 7,894 | `8ee04bf2ed8e563641dc1dacd320aee6fc8cd4077a43fab564033a9c29cbaa07` |
The published local result occupies approximately 123 MiB. Independent checks
confirmed:
- all five artifact size/digest pairs match `result.json`;
- `frames.jsonl` contains exactly 4,489 ordered timestamp rows;
- the mask archive contains exactly 4,489 indexed masks;
- FFprobe reports H.264, 800x600, 448.723 seconds and exactly 4,489 frames;
- the complete recorded-perception v2 result validates against its job, input,
session, source, timestamp bounds and `camera_1` calibration binding;
- the result store selects this result as the latest admitted generation for
`recorded.perception.right`.
Durable local result root:
```text
.runtime/compute-results/recorded-camera-602ac89026ed12978619801d/
result-793785170472c519486ccd666be102fb04d169d92383acda3fcc29eecf045d30/
```
Worker evidence root:
```text
D:\NDC_MISSIONCORE\runtime\derived\
result-793785170472c519486ccd666be102fb04d169d92383acda3fcc29eecf045d30
```
## Mission Core publication and UI proof
The saved-session replay API returned schema
`missioncore.observation-session-replay/v2` with source
`recorded.perception.right`, label `Сегментация · камера right`, the exact result
ID and the MP4 generation SHA above. The media manifest validated as
`missioncore.observation-recorded-media/v3`. A byte-range request returned HTTP
206 with `Content-Range: bytes 0-1023/75353780`.
The final acceptance was performed in the real Mission Core browser UI:
1. open **Сохранённые сессии**;
2. select **RAVNOVES00**;
3. open the scene source picker;
4. select **Сегментация · камера right**;
5. press **Воспроизвести**.
The visible video element loaded the exact new result, reported 800x600,
duration 448.723 seconds, `readyState=4` and no media error. Its current time
advanced from 128.522 to 130.282 seconds during a timed browser check, proving
that the result is not merely discoverable but actually plays.
## Visual findings
Start, middle and end frames were inspected from the final encoded video.
1. The fixed valid-FOV circle remains aligned across the complete recording;
excluded lens pixels do not generate semantic clutter.
2. Road, sidewalk, sky, buildings and vegetation form spatially coherent
regions at the inspected start/middle/end points.
3. Cars and people are visibly segmented when present, but this is semantic
class color, not a persistent object identity.
4. The output is substantially more usable than the original E0 visual result
for playback, while still showing expected Cityscapes/domain artifacts on
close structures, thin boundaries and forest/off-road content.
5. No visual inspection can substitute for the pending human-reviewed E2
accuracy set. The result remains `ground_truth=false`.
Final contact sheet:
`.runtime/compute-experiments/e4/full-preview/start-mid-end.png`, SHA-256
`a16f78732346db4b476b7d66dd140857d92eb1f11add5826c49e4dddf0ac1d5f`.
## Acceptance status and limitations
Accepted:
- complete no-sampling processing of the chosen saved session;
- zero failed/skipped frames;
- immutable semantic masks and frame timestamps;
- exact H.264 playback on the original session timeline;
- source discovery, manifest and HTTP range transport;
- real UI selection and playback;
- measured worker resource and disk behavior;
- D-only task-controlled worker storage with enforced floor.
Not accepted or not attempted:
- live real-time throughput;
- instance masks, object IDs or tracking;
- 2D detector boxes from an E4 model branch;
- 3D cuboids or point-cloud semantic labels;
- LiDAR-camera association and distance-to-object accuracy;
- forest/off-road model fitness;
- mIoU, boundary IoU, AP or safety metrics;
- production or safety qualification.
## Decision and next gate
E4 closes the first full-session semantic playback milestone. Plain EoMT with
the immutable valid-FOV mask is now the operational reference for recorded
video: reproducible, watchable and measured.
The next work should not be another blind full-epoch run. It should proceed in
two bounded tracks:
1. finish human review of the E2 evaluation pack and measure semantic accuracy,
especially people, vehicles, road/free-space boundaries and near distorted
structures; use those metrics to decide whether to keep the raw fisheye
baseline, improve the model/taxonomy or revive seam-aware rectification;
2. add an instance/detection and tracking branch, then project accepted 2D
observations through the factory camera/LiDAR calibration into the point
cloud for distance and 3D-cuboid experiments.
Live processing should be admitted only after the chosen branches are profiled
with bounded queues and an explicit drop/sampling policy. At the present 3.10
inference FPS, this exact configuration cannot consume the recorded ~10 FPS
camera stream without backlog.

View File

@ -0,0 +1,405 @@
# LAB E5 — Instance detection and temporal tracking qualification
Date: 2026-07-21
Completed at: 2026-07-21T08:45:46.307Z
State: completed qualification baseline, model output, not ground truth
Ops card: MISSIONCOR-19
## Objective
Establish the first measured temporal object-identity baseline on recorded K1
camera data before attempting LiDAR association and 3D cuboids. The acceptance
target was deliberately bounded:
1. run an upstream, reproducibly pinned detector through the existing Triton
service on the AI worker;
2. reuse the immutable calibration-bound valid-FOV mask instead of processing
the black fisheye border;
3. attach persistent IDs with a ByteTrack-style two-stage association loop;
4. produce a watchable 2D tracking video and exact timestamped frame metadata;
5. measure detector, tracking, artifact, GPU, memory and disk behavior;
6. retain all task-controlled worker data under `D:\NDC_MISSIONCORE`, enforce
the 360 GiB free-space floor and restore the prior Triton model state;
7. record visible failure modes honestly before using these identities for 3D.
LAB E5 is a qualification clip, not a full-session or saved-session production
source. It does not claim tracking ground truth, ID-switch accuracy, instance
masks, 3D cuboids, LiDAR distance, point-cloud labels, real-time operation or
safety fitness.
## Immutable input and clip selection
- UI session alias: `RAVNOVES00`
- Session ID: `20260720T065719Z_viewer_live`
- Compute job: `recorded-camera-602ac89026ed12978619801d`
- Source: `sensor.camera.right`
- Factory calibration slot: `camera_1`
- Input SHA-256:
`602ac89026ed12978619801d4edea0cae24b5cc3afabd9f7af2858de6505a20e`
- Factory calibration content SHA-256:
`05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`
- Codec epoch: 1
- Resolution: 800x600
- Full source: 4,489 frames, 35.421857292484.144857292 session seconds
- Qualification selection: source frames 1,0001,600 inclusive
- Frames admitted: 601/601
- Qualification timeline: 135.365857292195.334857292 session seconds
- Qualification media duration: 60.068948 seconds
- Qualification timeline SHA-256:
`06e3514228b488137b92c303cbca171154ede00368783e32107ed2c39c41baa6`
Frames 1,0001,600 were selected before the full run. The contiguous interval
contains stationary and changing views, multiple parked vehicles, distant
pedestrians, and a woman with a child/stroller passing close to the camera.
That close pass deliberately stresses scale change, fisheye distortion,
occlusion and identity continuity. It was not selected from the final output to
make the result look favorable.
The valid image circle is the immutable LAB E1 artifact
`valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2`.
Its mask SHA-256 is
`a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63`.
It admits 270,606 of 480,000 pixels. The mask is loaded once and reused; it is
not recomputed per frame.
## Accepted software configuration
Detector:
- official Megvii YOLOX-S ONNX release `0.1.1rc0`;
- license: Apache-2.0;
- classes: COCO-80, filtered to person, bicycle, car, motorcycle, bus and
truck;
- model SHA-256:
`c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063`;
- Triton config SHA-256:
`5795c737a7935a655961b069e8404d336d891f9762fb6dffb93956a076479604`;
- input: FP32 `[1,3,640,640]`, BGR, bilinear top-left letterbox, fill 114;
- output: FP32 `[1,8400,85]`, official grid/stride decode;
- container: `nvcr.io/nvidia/tritonserver:26.06-py3`;
- Triton backend: ONNX Runtime GPU.
Detection and FOV filtering:
- detector minimum score: 0.10;
- high/new-track threshold: 0.25;
- same-class NMS IoU: 0.45;
- same-class containment suppression: 0.80;
- minimum box area: 64 pixels;
- maximum box area: 50% of image;
- minimum valid-FOV box fraction: 0.50;
- box center must lie inside the valid FOV.
Tracking:
- algorithm: `bytetrack-style-two-stage-iou/v1`;
- assignment: SciPy linear-sum assignment;
- primary high-score match IoU: 0.20;
- secondary low-score match IoU: 0.10;
- lost-track buffer: 15 frames;
- confirmation: 2 hits;
- association is class-consistent;
- no appearance ReID and no camera-motion compensation.
Producer identities:
- pipeline: `recorded-yolox-bytetrack-style-qualification/v1`;
- runner SHA-256:
`adc7c924cb8d01278ccf63c9eeb3c9f810c09d2d1d7fe169e136102ee32c247a`;
- orchestrator SHA-256:
`8fff9c87e6213883ebde257a4226765d96b16cd4b8a2a5bb492aef728e3e2242`;
- profile SHA-256:
`1de8a83ef0fe630ca317b70bbe3a8ec497ae7cb5267fabda2ea234ee2cf2e562`;
- Python 3.12.3, NumPy 2.5.1 and SciPy 1.16.3 inside the bounded run
container.
The profile was frozen after the second visual pilot. No thresholds were tuned
against the 601-frame result.
## Execution topology
The pipeline was sequential at frame level:
```text
recorded fMP4 epoch
-> exact RGB frame reconstruction
-> fixed calibration-bound valid-FOV fill
-> YOLOX-S Triton request
-> YOLOX decode and class/FOV filtering
-> same-class NMS plus nested-box suppression
-> high-score association
-> low-score association to unmatched tracks
-> confirm/age/retire tracks
-> write ID boxes and trails
-> append exact timestamped detections/tracks
-> encode overlays as H.264
-> hash, validate and publish qualification result
```
Batch size was one. Frames were not parallelized. Triton/CUDA may execute
internal work asynchronously, but each source frame completed decode,
inference, association and overlay writing before the next frame was admitted.
NVENC encoded the completed overlay sequence after inference.
The model was absent from Triton's ready set before the run. The orchestrator
loaded it only for LAB E5 and restored the original unloaded state in `finally`.
The pre-existing `mission-core-triton`, `sentinel-frigate` and
`sentinel-ollama` services remained running.
## Pilot history and configuration freeze
The first launch failed closed during preflight because the base Triton image
did not contain Pillow. It performed no inference, loaded no model and
published no result. The correction mounted the already existing D-only
perception environment read-only at `/opt/env`; no new environment was placed
on C:.
The first successful 32-frame pilot used frames 1,2301,261 and published the
immutable diagnostic result
`e5-tracking-005090ae34dc2c09d75f0dcfe68b4f9eed9e011d480e0c4b0d650be9369ed841`.
It exposed two implementation effects:
1. nested same-class body/torso boxes survived conventional IoU NMS;
2. the first SciPy assignment import contaminated the first tracking latency
with a 422 ms one-time cost.
The accepted runner added 80% containment suppression and warmed the assignment
solver before timed frames. A second 32-frame pilot then published
`e5-tracking-8035c0013e36a7cfb68aa3652b05ab79fe36baf136462d5fbf91b348ee85ec08`.
It processed 32/32 frames in 4.843450 seconds at 6.606861 FPS, reduced
detections from 235 to 227, retained zero same-class pairs at IoU >= 0.8 and
reduced tracking p95 to 0.401 ms. Visual inspection confirmed that the remaining
nearby person boxes represented the woman and child, not nested duplicates.
Both successful pilots remain immutable evidence. Only the second profile was
admitted to the main qualification run.
## Qualification result
Published result:
`e5-tracking-88aace13ef9963f8dc07f85228e530f9d28c2b49aca9192409f7975512b058f6`.
| Measurement | Result |
|---|---:|
| Frames expected / processed | 601 / 601 |
| Frames failed / skipped | 0 / 0 |
| Frames with confirmed tracks | 600 |
| Inference/tracking/artifact loop | 88.092835 s |
| Loop throughput | 6.822348 FPS |
| Stream extraction | 10.243353 s |
| H.264 encoding | 0.686277 s |
| Full worker wall | 166.335664 s |
| End-to-end throughput | 3.613176 FPS |
| Process peak RSS | 126.199 MiB |
The source rate is approximately 10.004 FPS. The measured frame loop is about
1.47 times slower than source real time, and the complete worker run is about
2.77 times slower. This exact artifact-heavy path is therefore offline. It is
not admitted as a live pipeline.
### Per-frame latency
| Stage | Mean | p50 | p95 | Max |
|---|---:|---:|---:|---:|
| End to end | 146.279 ms | 144.622 ms | 163.340 ms | 215.759 ms |
| Overlay write | 87.363 ms | 86.227 ms | 99.219 ms | 139.534 ms |
| Image decode | 30.132 ms | 28.455 ms | 40.723 ms | 63.155 ms |
| Triton request | 12.359 ms | 12.148 ms | 17.948 ms | 31.662 ms |
| Detector postprocess | 7.307 ms | 7.016 ms | 10.734 ms | 21.003 ms |
| Preprocess | 6.202 ms | 5.803 ms | 7.991 ms | 18.645 ms |
| Tracking | 0.247 ms | 0.234 ms | 0.390 ms | 0.894 ms |
The tracker is not the speed problem. The Triton request itself is also below
the approximately 100 ms/frame source budget on average. Synchronous PNG
overlay writing and image decoding dominate this qualification loop. A live
design must decouple visualization/artifact production from the inference
critical path and define bounded queues and a drop policy; it must not simply
run this script against an unbounded stream.
## Detection and identity observations
| Metric | Result |
|---|---:|
| Detections admitted | 4,049 |
| Confirmed track observations | 3,320 |
| Confirmed track IDs | 167 |
| Tracker tracks created / retired | 177 / 166 |
| Confirmed length mean / p50 / p95 / max | 19.88 / 6 / 94 / 212 frames |
| Same-class duplicate pairs at IoU >= 0.8 | 0 |
| Rejected oversized boxes | 7 |
Admitted detections by detector label:
| Label | Detections | Confirmed observations | Confirmed IDs |
|---|---:|---:|---:|
| car | 2,972 | 2,556 | 123 |
| truck | 646 | 483 | 15 |
| person | 364 | 249 | 24 |
| bicycle | 45 | 29 | 3 |
| motorcycle | 16 | 2 | 1 |
| bus | 6 | 1 | 1 |
Track persistence without ground-truth interpretation:
- 150/167 IDs had at least 2 confirmed observations;
- 97/167 had at least 5 observations;
- 63/167 had at least 10 observations;
- 30/167 had at least 30 observations;
- 15/167 had at least 60 observations;
- 7/167 had at least 100 observations.
These counts prove that the temporal state machine is operating; they do not
measure accuracy. The high number of short vehicle IDs is a visible baseline
limitation, driven by changing viewpoint, intermittent low-confidence
detections and class changes between `car` and `truck` while association is
class-consistent.
### Visual findings
1. Boxes remain inside the calibrated valid image circle; the black lens border
no longer produces visual clutter or tracking candidates.
2. Parked vehicles and normally moving people retain IDs over useful clear-view
intervals. Several vehicle tracks persist for 100212 frames.
3. The close woman/child sequence demonstrates both success and failure. IDs
54 and 60 persist through the unobstructed approach. When the woman/stroller
fills the lower-left fisheye region and becomes heavily occluded, ID 60 is
lost and a new person ID 76 begins. This is a real identity fragmentation,
not hidden by the report.
4. YOLOX-S sometimes classifies the stroller as `motorcycle`, and the same
parked vehicle can alternate between `car` and `truck`. COCO has no stroller
class, and the generic road detector is not calibrated for the K1 fisheye.
5. Conventional NMS plus containment suppression removed the visible nested
body/torso duplicates from the first pilot without suppressing the distinct
woman and child.
6. There is no human-reviewed identity ground truth. IDF1, HOTA, MOTA and true
ID-switch count therefore remain unknown and must not be inferred from the
track-length statistics.
## GPU and neighboring services
Worker GPU: NVIDIA GeForce RTX 4090.
- one-second GPU samples: 88;
- GPU utilization: 51.65% mean, 52% p50, 55% p95, 56% max;
- board memory used, including pre-existing services: 10,239.0 MiB mean,
10,249 MiB max;
- memory-controller utilization: 6.11% mean, 7% max;
- power: 144.78 W mean, 146.71 W p95, 148.40 W max;
- temperature: 42.60 C mean, 43 C max;
- process peak RSS: 126.199 MiB.
The board-memory value is total device use observed by `nvidia-smi`, not an E5
process allocation. This run did not expose a trustworthy per-process CUDA
peak, so none is claimed.
## Disk policy and measured usage
The orchestrator admitted only explicit worker paths rooted at
`D:\NDC_MISSIONCORE`. It reserved a conservative 4,726,173,047-byte working set,
checked D: at each major phase and every 100 frames, and failed closed if the
360 GiB floor would be crossed.
| Disk point | Free bytes | Free GiB |
|---|---:|---:|
| Before run | 409,176,440,832 | 381.075 |
| After frame extraction | 408,682,668,032 | 380.615 |
| After inference | 408,303,550,464 | 380.262 |
| After final artifacts | 408,300,429,312 | 380.259 |
| After exact temporary cleanup | 408,660,688,896 | 380.595 |
| Enforced floor | 386,547,056,640 | 360.000 |
The floor was never approached closer than 20.259 GiB. Only exact E5 temporary
stream, frame and overlay roots were removed. The immutable pilots, accepted
result, telemetry, runner and profiles remain on D:. No task-controlled C: path
was created, mounted or used. This does not claim visibility into incidental
internal writes by Windows or the pre-existing Docker Desktop engine.
## Artifacts and independent validation
| Artifact | Bytes | SHA-256 |
|---|---:|---|
| `tracking.mp4` | 12,270,533 | `d911efc46c5c3df38ca961700f82c407c44222ba465af752d2a7de75911740ba` |
| `frames.jsonl` | 1,116,700 | `e5f19e5dd8bf76040cb7859ffada3f1bb68a0f5cd93fac243947276899cce014` |
| `gpu-telemetry.jsonl` | 19,694 | `84cbaa4d5a7d503129f2fa329ab2b231cff54a6972cd31ba95fe91f48f01adad` |
| `contact-sheet.png` | 926,575 | `d9a23cc76e1a9a4aba1156609f85b781bc2502cd80f3b17ac83ca4228f2121a8` |
| `run-report.json` | 9,183 | `96d1cb059b35a0aa3fcc7c3458a0f47f57a4bd52a64c34f3c7607302e49206d9` |
Independent local validation confirmed:
- the result directory name equals its canonical identity SHA-256;
- all five artifact size/digest pairs match `result.json`;
- the result is bound to the exact job, input, session, source, clip bounds and
timestamp basis;
- `frames.jsonl` contains exactly 601 ordered source indices and strictly
increasing session timestamps;
- every reported detection and track box is finite and inside 800x600;
- no frame failed or was skipped;
- FFprobe reports H.264, yuv420p, 800x600, 60.068948 seconds, and exactly
601 declared/read frames;
- the custom result validator accepts the immutable result;
- the E5 source and test files pass Ruff, and the focused E5 test suite covers
profile pinning, FOV math, YOLOX decode, containment suppression, two-stage
low-score identity retention, class isolation and nonzero source timelines.
Durable local result root:
```text
.runtime/compute-experiments/e5/results/
e5-tracking-88aace13ef9963f8dc07f85228e530f9d28c2b49aca9192409f7975512b058f6/
```
Worker evidence root:
```text
D:\NDC_MISSIONCORE\runtime\derived\
e5-tracking-88aace13ef9963f8dc07f85228e530f9d28c2b49aca9192409f7975512b058f6
```
## Acceptance status
Accepted:
- reproducible official YOLOX-S inference through the existing Triton worker;
- fixed calibration-bound valid-FOV preprocessing;
- ByteTrack-style high/low-score temporal association;
- 601/601 exact recorded frames with no failures or skips;
- watchable 2D ID overlay, trails and complete frame metadata;
- honest timing, GPU, process and disk telemetry;
- content-addressed immutable qualification artifacts;
- D-only task-controlled worker storage and restoration of Triton model state.
Not accepted:
- production-quality object classes or identity continuity;
- IDF1, HOTA, MOTA or measured ID-switch rate;
- handling of heavy close occlusion, re-entry or long disappearance;
- appearance ReID or camera-motion compensation;
- instance masks, 3D cuboids, LiDAR association or metric distance;
- a live unbounded stream or safety use;
- promotion of the partial clip into the saved-session production source list.
## Decision and next gate
LAB E5 closes the first temporal identity baseline. The result is useful enough
to become input to a bounded 3D fusion experiment, but not strong enough to be
called production tracking.
The next gate should be LAB E6 on the same frozen 601-frame interval:
1. use factory `camera_1` calibration to project synchronized LiDAR points into
each accepted 2D observation;
2. reject points outside the valid FOV and use robust depth/cluster gates rather
than a single nearest pixel;
3. compute per-object range and coarse point-supported 3D cuboids with explicit
support counts and confidence;
4. smooth distance over the existing track ID where continuity is available;
5. treat COCO `car`/`truck` as one vehicle association group in the fusion
layer so detector class flicker does not automatically break geometry;
6. publish a bounded Rerun qualification recording only after visual and
numeric checks pass.
In parallel with, but not as a prerequisite for, this first 3D proof, the 2D
branch should later compare camera-motion compensation and appearance ReID on a
small human-reviewed identity set. Full-session and live runs should wait until
the artifact-writing path is removed from the inference critical section and a
bounded queue/drop policy has been tested.

View File

@ -0,0 +1,292 @@
# LAB E6 — Factory-calibrated tracked LiDAR fusion
Date: 2026-07-21
Completed at: 2026-07-21T09:18:15.532Z
State: completed qualification baseline, diagnostic geometry, not ground truth
Ops card: pending direct `nodedc-ops-agent` availability
## Objective
Connect the already qualified E5 temporal identities to metric observations from
the K1 LiDAR without inventing depth from image boxes. The bounded acceptance
target was:
1. reuse the exact 601-frame E5 interval and E4 semantic masks;
2. bind the right camera to factory calibration slot `camera_1`;
3. project raw K1 points with the factory KB4 intrinsics and camera/LiDAR
extrinsics;
4. reject camera/point/pose combinations outside a strict 100 ms host-arrival
gate;
5. associate only semantically compatible, nearest-depth, spatially connected
LiDAR support with each tracked image object;
6. estimate a robust metric range and a point-supported oriented 3D envelope;
7. publish a watchable overlay and a standalone Rerun recording containing the
camera view, map-frame point cloud, support points and translucent `Boxes3D`;
8. measure the geometry path and preserve every pilot and accepted artifact.
LAB E6 proves that the existing XGRIDS factory calibration can drive useful
recorded camera/LiDAR fusion. It does not claim amodal vehicle dimensions,
object-complete 3D boxes, ground truth, live-rate admission or safety fitness.
## Immutable inputs
- UI session alias: `RAVNOVES00`
- Session ID: `20260720T065719Z_viewer_live`
- Camera job: `recorded-camera-602ac89026ed12978619801d`
- Camera source: `sensor.camera.right`
- Factory calibration slot: `camera_1`
- Camera input SHA-256:
`602ac89026ed12978619801d4edea0cae24b5cc3afabd9f7af2858de6505a20e`
- Factory calibration content SHA-256:
`05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`
- Raw MQTT capture SHA-256:
`70da0edad5cbf0e89b6e2355c0aac294f8c36a67249cd33b8918353fd88c83af`
- E4 semantic result:
`result-793785170472c519486ccd666be102fb04d169d92383acda3fcc29eecf045d30`
- E5 tracking result:
`e5-tracking-88aace13ef9963f8dc07f85228e530f9d28c2b49aca9192409f7975512b058f6`
- Source frames: 1,0001,600 inclusive
- Frames admitted: 601/601
- Session interval: 135.365857292195.334857292 seconds
- Media duration: 60.068948 seconds
- Resolution: 800x600
The 127 MiB MQTT capture contains the raw point and pose observations used by
the run. The calibration snapshot files are preserved separately from Git:
| File | SHA-256 |
|---|---|
| `camera.yaml` | `d123ebd2207e18a800cf5d07d055bf38231d28390bf3ba8c4e718e3cb3a7775e` |
| `extrinsic_camera_lidar.yaml` | `d09b2eda51e1deb7e3a31ff8441cba6006e8da60afc3e33de44a857437dfcfb8` |
| `manifest.json` | `75e1d582f661dabafb48fc6e8c7aabe0f217e0d95ed934b7d9dac18bdfc1216c` |
## Accepted configuration
Producer identities:
- pipeline: `factory-kb4-tracked-box-semantic-lidar/v1`;
- profile schema: `missioncore.e6-tracking-lidar-profile/v1`;
- profile SHA-256:
`b8d3a6f043fde54ffd32ea05bfe580e2be7aace1a23cdbf7e266eb3325d147b2`;
- runner SHA-256:
`9765be5e0338b9978db7a58a739313b228bc7ceee1352a7a76af0ff2a528a953`.
Temporal binding:
- nearest host-arrival observation, best effort;
- maximum LiDAR/camera delta: 100 ms;
- maximum pose/point delta: 100 ms;
- frames outside the gate fail closed for depth; image tracking remains visible.
Projection and occlusion:
- factory KB4 projection for `camera_1`;
- source points transformed from the K1 map frame through the recorded pose;
- only camera-front, in-frame points are retained;
- one nearest-depth point is retained per rounded image pixel;
- no fabricated depth and no 2D-box extrusion.
Association:
- image boxes are inset 5% horizontally, 4% at the top and 2% at the bottom;
- `car`, `truck` and `bus` share the `vehicle` association group;
- same-frame vehicle overlap suppression uses IoU 0.55;
- E4 semantic IDs gate LiDAR support: person 1, bicycle 2, motorcycle 3,
vehicle 4/5;
- depth splitting uses `max(0.5 m, 6% of range)`;
- the selected depth component is split again by 3D connected components;
- spatial radii are 0.9 m for person/bicycle/motorcycle and 1.5 m for vehicle;
- minimum support is 3 points for person/bicycle/motorcycle and 5 for vehicle;
- distance uses cluster median, with p10 retained for diagnostics;
- a five-frame track history smooths distance;
- an observation is rejected when its distance innovation exceeds
`max(1.5 m, 25% of track history)`.
Cuboid construction:
- yaw comes from PCA over supported map-frame XY points;
- dimensions are the oriented p05p95 support extent;
- every axis has a 0.15 m diagnostic minimum for visibility;
- maximum accepted extents are 1.5x1.5x2.8 m for person,
3.5x2.0x2.5 m for bicycle/motorcycle and 6.5x3.5x4.0 m for vehicle;
- rejected or unsupported detections never produce a 3D cuboid.
## Execution topology and resource boundary
The E6 geometry stage ran as a single local CPU process on the Mission Core
host, an Apple M3 Pro with 18 GiB RAM. It used Python 3.12.13, NumPy 2.5.1,
Pillow 12.3.0, Rerun 0.34.1 and FFmpeg 7.1.1. The external AI worker and its
Triton model state were not changed. No path on worker disk C: or D: was read,
written or cleaned by this stage.
```text
exact E5 frame + track IDs
+ exact E4 semantic mask
+ nearest gated K1 point/pose sample
-> factory KB4 camera projection
-> per-pixel nearest-depth buffer
-> box inset + semantic support gate
-> 1D depth split + 3D connected component
-> range innovation gate
-> robust range + PCA-yaw p05/p95 envelope
-> overlay / NPZ / JSONL / Rerun recording
-> content hash, qualification validation and publish
```
Frames were processed sequentially. Artifact writing was performed inside the
same measured process. This is a recorded qualification path, not the live
queue/worker architecture.
## Pilot history and profile freeze
Three immutable 32-frame pilots were retained:
1. `e6-fusion-a72468...` exposed 78 m person envelopes when a depth-only
cluster admitted background structure. It was rejected.
2. `e6-fusion-693246...` added 3D connectivity and plausible size gates. Box
dimensions became plausible, but range could still jump between foreground
and background support on the same track. It was rejected.
3. `e6-fusion-70fa670ed9005957266ae7a46ac2bfd2d526f1ea170cd052f4494a2397fff76f`
added the distance-innovation gate and was accepted. It fused 32/32 frames,
produced 47 cuboids across five track IDs, rejected two range jumps, ran in
3.681 seconds and peaked at 159.828 MiB RSS.
The accepted configuration was frozen after the third pilot. The 601-frame
result was not used to tune its thresholds.
## Qualification result
Published result:
`e6-fusion-b4e4226674a66f6196c033785eb307c255a7bdd8493ef9809dfb1d6e5bd68eaa`.
| Measurement | Result |
|---|---:|
| Frames expected / processed | 601 / 601 |
| Frames fused | 526 |
| Frames outside strict sync gate | 75 |
| Frames with at least one accepted cuboid | 438 |
| Accepted cuboids | 918 |
| Unique track IDs with depth | 56 |
| Source cloud points consumed | 1,182,292 |
| Accepted support points | 14,025 |
| Geometry/artifact wall time | 14.774254 s |
| Effective throughput | 40.678 FPS |
| User / system CPU | 13.571966 / 0.772126 s |
| Process peak RSS | 240.469 MiB |
This recorded geometry/artifact stage is about 4.07 times faster than the
approximately 10.004 FPS camera source. That does not establish a live
end-to-end rate: E4 segmentation and E5 detection/tracking are precomputed
inputs and were not included in the 14.774-second measurement.
### Synchronization
| Delta | Mean | p50 | p95 | Max |
|---|---:|---:|---:|---:|
| LiDAR to camera | 29.389 ms | 23.905 ms | 70.794 ms | 96.107 ms |
| Pose to point cloud | 7.940 ms | 6.370 ms | 19.113 ms | 46.139 ms |
All fused frames satisfy the configured 100 ms gates. The 75 nonconforming
camera frames retain their 2D presentation but carry no depth or 3D claim.
Host-arrival time is still only a best-effort clock; hardware timestamps have
not been ground-truthed.
### Accepted geometry distribution
| Group | Cuboids | Unique track IDs |
|---|---:|---:|
| Vehicle | 809 | 43 |
| Person | 96 | 12 |
| Bicycle | 13 | 1 |
| Total | 918 | 56 |
- accepted range: 1.23937.749 m, p50 10.189 m, p95 24.126 m;
- support per cuboid: minimum 3, p50 10, p95 45, maximum 89 points;
- p95 oriented extent: 5.383x1.197x2.222 m;
- maximum admitted extent: 6.490x2.948x2.783 m.
The extent is an envelope of the visible supported surface. It is intentionally
not an amodal estimate of the object's complete physical volume. A parked car
seen from one side can therefore receive a thin cuboid that does not wrap the
whole vehicle silhouette.
### Fail-closed decisions
| Status | Observations |
|---|---:|
| Accepted point-supported cuboid | 918 |
| Fewer than 5 vehicle support points | 1,023 |
| No semantically compatible LiDAR support | 365 |
| Rejected distance innovation | 209 |
| Fewer than 3 non-vehicle support points | 28 |
| Implausible cuboid extent | 24 |
These rejection counts are part of the result, not discarded errors. In
particular, the high low-support count prevents distant image detections from
being turned into false metric objects.
## Rerun and visual QA
The Rerun recording logs seven entities across the `session_time` timeline:
- encoded camera fusion overlay;
- raw map-frame LiDAR points;
- selected object support points;
- translucent oriented `Boxes3D` with track/class/range labels;
- a visible qualification contract stating the timing and safety limits.
The web viewer was loaded from the local gRPC proxy and inspected at multiple
timestamps. Camera overlay, point cloud, support and `Boxes3D` layers load
together. Selecting a cuboid exposes its real center, half-sizes, quaternion,
fill mode and label in Rerun. Browser logs contained no load/render errors;
only Rerun web-backend deprecation/default warnings were present.
## Artifacts
| Artifact | Bytes | SHA-256 |
|---|---:|---|
| `fusion.npz` | 9,745,489 | `3e0e63797c56505eb9755d253f262459d46ec841e80cc5b785a6b495ed2007f9` |
| `fusion-frames.jsonl` | 1,607,124 | `ea1621bb337c6c268f91f6e2c7cef997d78672da4025975328c9e2583a0718e0` |
| `box-labels.json` | 30,328 | `0c423024a086344fcdbac8665635a45e88fef81270be73d9e1af3e6bc4eb860c` |
| `fusion-overlay.mp4` | 24,156,459 | `a863d1910eb973cef1f4cca28432cb16b26030149e197ff7f9ad3d168b69954c` |
| `fusion.rrd` | 63,092,299 | `77b0b91171176dd7ea6d2ef726cc946ab1fc24367750101d3f10617360cb4d63` |
| `contact-sheet.png` | 3,066,509 | `adcad4db4fd3089175ed947b312129fc86f04abc065314175eceb62368adac49` |
| `run-report.json` | 5,405 | `ca01cb5fa0325d80504199b623f2972141f45b680b4b16ad6a5271f9dd1deb2c` |
FFprobe independently confirmed H.264/yuv420p, 800x600, 601 declared/read
frames and 60.068948 seconds. The custom E6 validator rehashed every artifact
and verified the exact session, job, camera, calibration, clip, upstream result
and configuration identities. The complete E6 workspace including all pilots
uses 124 MiB on the Mission Core host.
## What is proven and what remains open
Proven:
- the right-camera/`camera_1` factory calibration is operational for recorded
K1 camera/LiDAR projection;
- E5 track identities can receive robust metric LiDAR ranges;
- unsupported 2D boxes do not become fake 3D geometry;
- Rerun can display the camera, map cloud, support and true oriented 3D
primitives on one timeline;
- the geometry stage itself is comfortably faster than source rate on this
host.
Not proven:
- object-complete vehicle/person volume or metric box accuracy;
- calibration error in pixels or centimetres against a measured target;
- hardware-clock synchronization accuracy;
- tracking accuracy, instance-mask accuracy or semantic ground truth;
- live end-to-end throughput, bounded queues, drop policy or recovery;
- safety or navigation fitness.
## Next gate — LAB E7
Use this exact E6 result as the control and improve 3D geometry without hiding
rejections. The bounded E7 experiment should add ground removal, object-aware
support completion and a reviewed upstream 3D detection baseline; compare box
coverage, range stability, support/rejection rates and runtime on the same
601-frame interval. Only after that comparison should the winning geometry
path be connected to a bounded live queue. Multi-camera fusion and product UI
work remain outside this gate.

View File

@ -0,0 +1,348 @@
# LAB E7 — Bounded replay-real-time perception loop
Date: 2026-07-21
Completed at: 2026-07-21T10:12:16.828Z
State: accepted replay scheduler and world-state qualification
Navigation/safety state: not accepted
Ops card: pending direct `nodedc-ops-agent` availability
## Objective
Stop treating recorded perception as a video-enhancement exercise and establish
the first measured runtime contract for the K1 as the eyes of an autonomous
platform. LAB E7 replays the immutable E6 interval at its original timing and
requires the derived perception path to:
1. publish a machine-readable world state, not only a rendered overlay;
2. keep derived work in a small bounded queue;
3. absorb short source bursts without discarding processable frames;
4. discard old derived frames under real overload instead of building latency;
5. expose result age, queue depth, drops and explicit health states;
6. preserve object identity, metric range and 3D geometry in both map and
LiDAR-relative coordinates;
7. reject untrustworthy velocity estimates instead of publishing extreme
motion caused by cuboid jitter;
8. emit a diagnostic polar clearance observation from the LiDAR;
9. prove the scheduler at the approximately 10 Hz source rate before connecting
the real inference worker or live K1 stream.
This experiment qualifies the downstream scheduler, world-state contract and
diagnostic publication path. E4 segmentation, E5 detection/tracking and E6
fusion are immutable precomputed inputs. LAB E7 therefore does not claim live
inference, live sensor transport, collision avoidance or navigation fitness.
## Immutable input binding
- Session: `20260720T065719Z_viewer_live`
- Source: `sensor.camera.right`
- Camera slot: `camera_1`
- Camera job: `recorded-camera-602ac89026ed12978619801d`
- Input SHA-256:
`602ac89026ed12978619801d4edea0cae24b5cc3afabd9f7af2858de6505a20e`
- Factory calibration SHA-256:
`05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`
- E6 result:
`e6-fusion-b4e4226674a66f6196c033785eb307c255a7bdd8493ef9809dfb1d6e5bd68eaa`
- E7 selection: E6 frames 0600 inclusive
- Source frames: 1,0001,600 inclusive
- Frames attempted: 601
- Replay timing span: 59.969 seconds
- Replay speed: 1.0x
The adapter spent 9.490253 seconds before the timed replay revalidating inputs,
reading the raw point/pose capture and reconstructing the exact E6 pose binding.
That preparation is measured separately and is not counted as live latency. A
real source will receive frames incrementally and must not preload a multi-hour
session into memory.
## Accepted configuration
Producer identities:
- pipeline: `bounded-latest-wins-e6-world-state-replay/v1`;
- profile schema: `missioncore.e7-replay-realtime-profile/v1`;
- profile SHA-256:
`7222c65ddef20798a242d83edc53973cdfd6f5dec8fb9ffb6fcb5dbcef57f1c5`;
- runner SHA-256:
`ce36b919fe5860bbb81f04b29c58a8a333915b1ba9bd7f233f256ce77163b15d`;
- runtime contract SHA-256:
`4c65140550e47c08c528b10d51922d5f73b7d19a1c34ef07896c052f492309f5`;
- result validator SHA-256:
`719b966ff263f196a97b4d72e946b00fd10705a0158b7df6ecf452c6c9bc4e5b`;
- E6 replay adapter SHA-256:
`9765be5e0338b9978db7a58a739313b228bc7ceee1352a7a76af0ff2a528a953`.
Scheduler:
- one source-paced producer and one derived-state consumer;
- replay speed 1.0x using the original per-frame session timestamps;
- queue capacity: two derived frames;
- overflow policy: evict the oldest queued derived frame;
- normal consumer delay injection: zero;
- stale threshold: 300 ms;
- unavailable threshold: 1,000 ms;
- raw evidence is outside this queue and is never dropped by it.
Acceptance thresholds:
- delivery rate at least 9.5 Hz;
- p95 result age at most 300 ms;
- drop fraction at most 1%;
- maximum queue depth two;
- process RSS at most 512 MiB.
## World-state contract
Each published state uses
`missioncore.live-perception-world-state/v1` and contains:
- exact E6/source frame identity and session timestamp;
- `healthy`, `degraded`, `stale` or `unavailable` delivery health;
- the reason for every non-healthy state;
- accepted obstacle track ID, class, detector label and confidence;
- map-frame and K1-LiDAR-frame 3D positions;
- map-frame orientation and visible-surface cuboid dimensions;
- robust LiDAR range and support-point count;
- qualified diagnostic map-frame velocity or an explicit rejection reason;
- 72-sector LiDAR-relative obstacle-clearance observation.
The vehicle-body frame is explicitly
`unavailable-no-rig-to-vehicle-transform`. K1 LiDAR coordinates must not be
silently presented as the final autopilot body frame. A measured rigid
sensor-to-vehicle transform is required before behavior integration.
### Velocity policy
Naive frame-to-frame cuboid differencing produced visibly false speeds because
the supported surface moves within an object as viewpoint and point density
change. The accepted implementation uses a one-second bounded history,
pairwise slopes with at least 200 ms baselines, median velocity, a 400 ms
minimum history, a 20 m/s diagnostic bound and a 0.75 m median position-residual
gate.
The field is published only as `diagnostic-robust-history`. Observations with
insufficient history, excessive residual or an implausible speed retain the
object and range but publish no velocity.
### Clearance policy
The first clearance observation is deliberately simple and diagnostic:
- 72 horizontal sectors over 360 degrees;
- 0.530 m range;
- local ground estimate at the fifth LiDAR Z percentile;
- obstacle slice 0.23.0 m above that estimate;
- `front_m` is the nearest retained point within ±15 degrees;
- unobserved sectors remain `null`, never free by assumption.
This is obstacle clearance evidence, not a traversable free-space polygon. It
does not yet model vehicle footprint, slope, negative obstacles or a certified
ground plane.
## Pilot history and architecture correction
### Pilot 1 — rejected queue consumption policy
Result:
`e7-live-replay-a578764161fc007c8fc8f8e8b5ffc339e2e861668f13c1fee7dc017ac3c4b972`.
It processed 61/64 frames at 9.820 Hz with 10.787 ms p95 latency, but discarded
three frames during short timestamp bursts even though the consumer was fast.
The first implementation always selected the newest queued frame. This was too
aggressive for a capacity-two jitter buffer and was rejected.
The correction preserves FIFO order while capacity remains available and
evicts the oldest frame only when a new publication would overflow the queue.
This retains short processable bursts while remaining latest-wins under actual
backpressure.
### Pilot 2 — scheduler accepted, velocity rejected by review
Result:
`e7-live-replay-810e2628c756d3e3694603c4da5429a8a333e684208cd137c6a076a7e9ae8b73`.
It processed 64/64 frames with zero drops at 10.303 Hz and 5.943 ms p95
latency. Scheduler acceptance passed, but inspection of naive frame-difference
velocity found a 1,028.5 m/s maximum caused by a cuboid-center jump. The run is
retained as immutable evidence and rejected as a world-motion baseline.
### Overload negative control — accepted
Result:
`e7-live-replay-1c5ce5718d8be7c4dfcbe540c12fe96339ca04a598e8f5f2fb85e84bc0748796`.
An intentional 250 ms consumer delay was applied to 80 source-paced frames:
| Measurement | Result |
|---|---:|
| Frames attempted / processed / dropped | 80 / 33 / 47 |
| Drop fraction | 58.75% |
| Maximum / final queue depth | 2 / 0 |
| Healthy / stale states | 1 / 32 |
| End-to-end p95 | 468.525 ms |
| Peak RSS | 373.656 MiB |
The negative control proves the intended failure mode: derived output quality
degrades and old derived frames are discarded, but queue depth remains bounded
and the runtime reports staleness. It does not block or delete raw evidence.
### Pilot 3 — accepted profile freeze
Result:
`e7-live-replay-2fb63b4177900348fc876a1dc580a19015753742c3665c4a044e1f251b44a430`.
It processed 64/64 frames with zero drops at 10.303 Hz and 5.865 ms p95
latency. Robust-history velocity replaced naive differencing. In this interval
72 velocities were admitted with 0.439 m/s median, 2.046 m/s p95 and 4.811 m/s
maximum; eight noisy histories were rejected by the position-residual gate.
The configuration was then frozen for the full run.
### Preliminary full run — metrics valid, provenance incomplete
Result:
`e7-live-replay-24222ede524e2e7824e4c7e6e9d17e87aa75bf3b8768f9e88c4b97eb410b633b`.
It processed 601/601 frames with zero drops and passed every runtime threshold.
Final review found that its identity pinned the E7 runner but did not separately
pin the imported runtime contract, validator and E6 adapter. The immutable run
is retained as evidence, but it is not the reference result. Those dependencies
were added to the identity and the complete 1.0x run was repeated.
## Full qualification result
Published result:
`e7-live-replay-e339aaed75fff05ed893ae48770bd127cc0c764f3124fc4b6be94b5fd9f8389c`.
| Measurement | Result |
|---|---:|
| Frames attempted / processed / dropped | 601 / 601 / 0 |
| Delivery rate | 10.021845 Hz |
| Replay timing span | 59.969000 s |
| Actual replay wall | 59.981432 s |
| Queue capacity / maximum / final depth | 2 / 2 / 0 |
| Healthy / degraded / stale / unavailable | 526 / 75 / 0 / 0 |
| Process peak RSS | 398.047 MiB |
The 75 degraded states exactly match E6 frames that failed the strict
camera/LiDAR timing gate. They remain timestamped states with no fabricated
depth. Every fused E6 frame is healthy in this downstream replay. No frame was
lost by the accepted runtime.
### Runtime latency
| Stage | Mean | p95 | Max |
|---|---:|---:|---:|
| Scheduled tick to world-state publication | 4.880 ms | 5.927 ms | 8.895 ms |
| Producer scheduling lag | 4.124 ms | 5.019 ms | 5.644 ms |
| Queue wait | 0.071 ms | 0.144 ms | 1.154 ms |
| World projection and clearance | 0.372 ms | 0.538 ms | 1.647 ms |
| Rerun publication | 0.257 ms | 0.355 ms | 1.662 ms |
The p95 result is 50 times below the 300 ms initial laboratory threshold. This
headroom belongs only to the downstream replay/world-state stage; live decode,
network transport, E4/E5 inference and fresh E6 geometry must still be added to
the latency budget.
### Object state
| Group | Observations | Unique track IDs |
|---|---:|---:|
| Vehicle | 809 | 43 |
| Person | 96 | 12 |
| Bicycle | 13 | 1 |
| Total | 918 | 56 |
- range p50 / p95 / maximum: 10.189 / 24.126 / 37.749 m;
- 572 object observations received a diagnostic robust velocity;
- 263 retained the object but reported insufficient velocity history;
- 83 retained the object but rejected velocity due to position residual;
- admitted speed p50 / p95 / maximum: 0.600 / 2.640 / 6.431 m/s.
These velocities are behavior-facing schema candidates, not validated motion
ground truth. A 3D motion filter and annotated moving/static evaluation set are
still required.
### Clearance observation
- valid front clearance on 526 fused frames;
- front minimum / p50 / p95 / maximum:
0.801 / 5.356 / 11.092 / 25.954 m;
- observed-sector fraction p50 / p95: 81.94% / 87.50%;
- all 75 frames without admitted LiDAR remain clearance-unavailable.
## Rerun visual QA
The standalone Rerun recording contains:
- the qualification contract;
- map-frame LiDAR point cloud and oriented obstacle `Boxes3D`;
- end-to-end latency chart;
- queue-depth chart;
- health-state chart;
- the exact 59.969-second replay timeline.
The web viewer loaded all 601 ticks and exposed real cuboid centers, half-sizes,
quaternions, labels and ranges. Latency remains approximately 08.9 ms, queue
depth remains 01 at consumption with a measured maximum of two, and health
transitions correspond to depth-unavailable E6 frames. No browser load/render
errors were observed; only Rerun web-backend warnings were present.
## Artifacts
| Artifact | Bytes | SHA-256 |
|---|---:|---|
| `world-state.jsonl` | 1,730,546 | `6078d19c6414c2bf2f7ca9d95c5164b46f388496690d5fee67c662724fb7f956` |
| `telemetry.jsonl` | 317,486 | `c25a01dc2d7a067672b8ae667e08450f2010ead3e3ccbaea7fde47ec3638119c` |
| `world-state.rrd` | 16,234,276 | `540c7d91a6c4cf8c19276994f6bbafcb267686ddb1fdbae46d7fd2aba7d2c4b9` |
| `run-report.json` | 4,163 | `04fd00324c57f6fa06cd6360256c14f70ab5c3fe51d5e662e427780bd228345d` |
The E7 validator rehashes every artifact, revalidates the complete E6 upstream
chain, checks frame/drop/queue/health accounting, validates ordered world-state
and telemetry rows, and verifies the RRF2 recording. All pilots, the preliminary
full result and the reference result occupy 44 MiB on the Mission Core host.
The AI worker and worker disks C: and D: were not accessed or modified.
## What is proven
- the accepted E6 observations can be delivered as machine world state at the
source rate with a capacity-two derived queue and zero loss;
- source timestamp bursts are absorbed without unnecessary drops;
- real overload fails boundedly and explicitly rather than growing latency;
- result freshness and sensor/fusion degradation are separate states;
- accepted obstacles have map and K1-LiDAR positions, range, geometry and
guarded velocity fields;
- a first diagnostic LiDAR clearance signal is available;
- Rerun remains a subscriber/diagnostic surface, not the perception authority.
## What remains open
- actual live camera/LiDAR ingestion through this scheduler;
- network and inference latency on the RTX worker;
- 10 Hz detector/tracker execution rather than precomputed E5 observations;
- asynchronous lower-rate semantic inference rather than precomputed E4 masks;
- current-frame E6 fusion rather than precomputed geometry;
- calibrated K1-to-vehicle-body transform;
- ground-truthed velocity, clearance, object and free-space accuracy;
- bounded worker disconnect/recovery and authenticated non-SSH transport;
- navigation or safety acceptance.
## Next gate — LAB E8
Move the same contract onto the real executor path without changing K1 command
ownership:
1. replay encoded camera frames to the RTX worker at 10 Hz;
2. run YOLOX detection/tracking without synchronous video-artifact writes;
3. run semantic segmentation asynchronously at a lower measured rate and
expire old masks explicitly;
4. feed current detections, latest valid semantics and current LiDAR/pose into
the E6 fusion adapter;
5. publish the same E7 world-state schema through the capacity-two queue;
6. measure transport, decode, inference, fusion and publication latency
separately;
7. repeat the 250 ms overload and worker-disconnect negative controls;
8. connect the live K1 only after source-paced worker replay passes.
The initial E8 gate remains 510 Hz derived perception with p95 world-state age
below 300 ms and uninterrupted raw recording. A visually better model is
accepted only if it improves obstacle evidence inside that operational budget.

View File

@ -0,0 +1,369 @@
# LAB E8 — Source-paced RTX detector/tracker qualification
Date: 2026-07-21
State: completed; qualification and overload-control results accepted
Ground truth: no
Navigation/safety acceptance: no
## Purpose
LAB E8 answers one narrow but essential autonomous-perception question:
> Can the existing RTX worker consume the K1 right-camera stream at its recorded
> approximately 10 Hz rate, produce YOLOX detections and temporal tracks before
> the next source deadlines, and remain bounded if processing becomes slower than
> the source?
The lab deliberately removes synchronous overlay rendering and per-frame output
images from the inference critical path. It does not improve playback video in
isolation. It qualifies the detector/tracker branch that will feed the
near-real-time fused world state.
The answer is **yes for this recorded 60-second qualification interval**. The
worker processed 601/601 source frames with zero drops at 10.009945 effective
FPS. Result-age p95 was 93.158479 ms. A separate forced-overload run proved that
the queue remains bounded and discards superseded derived frames instead of
building an unbounded latency tail.
## Input identity
- Compute job: `recorded-camera-602ac89026ed12978619801d`
- Session: `RAVNOVES00`
- Source: `sensor.camera.right`
- Calibration slot: `camera_1`
- Calibration content SHA-256:
`05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`
- Codec epoch: 1
- Resolution: 800x600
- Source selection: frames 1,0001,600 inclusive
- Frames scheduled: 601
- Session timeline: 135.365857292195.334857292 seconds
- Source span: 59.969 seconds
- Approximate source rate: 10.004 FPS
The interval is the same predeclared E5/E6/E7 qualification slice. It contains
parked and moving vehicles, people at several scales, a close woman/child/stroller
pass, fisheye distortion, occlusion and viewpoint change.
The valid-FOV mask is the immutable E1 result
`valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2`.
Its mask SHA-256 is
`a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63`.
It admits 270,606 of 480,000 pixels and is loaded once per run.
## Worker and neighboring services
Read-only preflight immediately before implementation confirmed:
- host alias: `mission-gpu`;
- Windows hostname: `<worker-host>`;
- GPU: NVIDIA GeForce RTX 4090, 24,564 MiB;
- driver: 610.47;
- worker free space at preflight: 407,939,772,416 bytes;
- `mission-core-triton`: healthy;
- `sentinel-frigate`: healthy;
- `sentinel-ollama`: running.
The qualification ran inside the existing
`nvcr.io/nvidia/tritonserver:26.06-py3` image and used the existing D-backed
Python environment. No task-controlled C: path was created, mounted or used.
All run inputs, temporary files, model data and immutable evidence remained
under `D:\NDC_MISSIONCORE`.
After all E8 runs and exact temporary cleanup:
- D: free bytes: 407,465,738,240 (approximately 379.37 GiB);
- enforced floor: 386,547,056,640 bytes (360 GiB);
- YOLOX returned to its pre-run unloaded state;
- Triton, Frigate and Ollama remained running.
## Accepted software configuration
Detector and tracker configuration are inherited without threshold changes from
accepted LAB E5:
- detector: official Megvii YOLOX-S ONNX release `0.1.1rc0`;
- model SHA-256:
`c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063`;
- Triton config SHA-256:
`5795c737a7935a655961b069e8404d336d891f9762fb6dffb93956a076479604`;
- Triton backend: ONNX Runtime GPU;
- input: FP32 `[1,3,640,640]`, BGR, bilinear top-left letterbox, fill 114;
- COCO classes admitted: person, bicycle, car, motorcycle, bus and truck;
- detector minimum score: 0.10;
- NMS IoU: 0.45;
- nested-box containment suppression: 0.80;
- ByteTrack-style, class-consistent, two-stage IoU association;
- high/new-track threshold: 0.25;
- primary/secondary match IoU: 0.20/0.10;
- lost-track buffer: 15 processed frames;
- minimum confirmation: two hits;
- no appearance ReID and no camera-motion compensation.
Real-time contract:
- source pacing: exact recorded session timestamps at 1.0x;
- queue: bounded latest-wins;
- capacity: two decoded frames;
- overflow action: discard the oldest unprocessed derived frame;
- stale threshold: 150 ms from scheduled source time;
- unavailable threshold: 500 ms;
- synchronous overlays and video encoding: disabled;
- output in the critical path: compact frame and telemetry JSONL only.
Accepted producer identities:
- runner SHA-256:
`3eebf55479151788467af804c551b11660a551364c29b128a78e3ca7de36b2b2`;
- orchestrator SHA-256:
`a98990024a719b2ac1f0b75c0aea93d9ec18341be653f44594afff391b700f76`;
- qualification profile SHA-256:
`819bdfb0da521d187ec9ce3d3039a0ff75daa0ab473fb3d8e5fa92db9c1ddca1`;
- overload profile SHA-256:
`ac2ca648b46857953baa1bd6f07254bcc680c70566ed32d48b7431bf428bf757`;
- independent result validator SHA-256:
`32eb8f1d56b80ceac1a63b850767f3f7ccb1aea14b530412199f5f48f1c7be20`.
## Execution topology
```text
recorded fMP4 camera epoch
-> temporary exact PNG reconstruction on D:
-> producer waits for each recorded source timestamp
-> decode RGB + fixed valid-FOV fill
-> bounded latest-wins queue (capacity 2)
-> YOLOX-S Triton request
-> YOLOX decode, FOV/class/NMS filtering
-> ByteTrack-style temporal association
-> compact timestamped result + freshness telemetry
```
The producer and inference consumer run independently. The producer never waits
for an unbounded queue. If the queue is full, the oldest unprocessed derived
frame is replaced by the new frame. Raw source preservation is outside this
drop policy; only recomputable perception preview work may be dropped.
The measured `result_age_ms` begins at the scheduled source time and ends after
detection, association and result construction. It therefore includes source
release lag, PNG decode, queue wait, preprocessing, Triton, postprocessing and
tracking. It does not include the one-time job validation, model load, fMP4
reconstruction or PNG extraction that occurs before the paced interval.
## Pilot gate
Pilot result:
`e8-realtime-tracking-ba7f73c8f4e0bde1634cce89d7fda5c94c43d0b55af8080a1eeec8af636474ba`.
- selection: source frames 1,2301,293;
- source span: 6.296 seconds;
- processed: 64/64;
- dropped: 0;
- effective rate: 10.052679 FPS;
- queue maximum depth: 1/2;
- result-age p95: 78.527889 ms;
- health: 64 healthy;
- accepted: yes.
The pilot proved the model, queue, timing basis, disk guard and publication
contract before the full interval was admitted.
## Full qualification result
Published result:
`e8-realtime-tracking-2802f407cb9d2c24576a4ba32d3e35f4d2b2092bfa99b37c438a8ac8120e1b16`.
| Measurement | Result |
|---|---:|
| Frames scheduled / processed | 601 / 601 |
| Frames dropped / failed | 0 / 0 |
| Source span | 59.969 s |
| Paced replay wall | 60.040290 s |
| Effective output rate | 10.009945 FPS |
| Queue capacity / maximum depth | 2 / 1 |
| Healthy results | 601 |
| Stale / unavailable results | 0 / 0 |
| Detections | 4,049 |
| Unique confirmed track IDs | 167 |
All qualification checks passed:
- minimum 9.5 effective FPS;
- maximum 0.5% drop fraction;
- result-age p95 at or below 100 ms;
- zero failures;
- exact producer/consumer/drop accounting;
- queue depth never above its declared capacity.
### Critical-path latency
| Stage | Mean | p50 | p95 | Max |
|---|---:|---:|---:|---:|
| Result age from scheduled source time | 66.719 ms | 62.533 ms | 93.158 ms | 145.973 ms |
| Image decode | 32.693 ms | 30.396 ms | 48.431 ms | 70.110 ms |
| Source release lag | 1.865 ms | 0.088 ms | 21.608 ms | 46.695 ms |
| Queue wait | 0.090 ms | 0.064 ms | 0.121 ms | 8.993 ms |
| Processing after dequeue | 32.070 ms | 30.770 ms | 43.526 ms | 83.312 ms |
| Preprocess | 7.045 ms | 6.926 ms | 9.574 ms | 20.348 ms |
| Triton request | 12.223 ms | 11.684 ms | 18.589 ms | 36.336 ms |
| Detector postprocess | 8.813 ms | 8.729 ms | 15.167 ms | 28.547 ms |
| Tracking | 0.305 ms | 0.253 ms | 0.638 ms | 2.210 ms |
The maximum result age crossed the 100 ms nominal frame period on isolated
frames, but p95 remained below 100 ms and no queue accumulation or drop
occurred. The current performance margin is usable for this 10 Hz source but is
not large enough to add the E4 semantic model synchronously to every frame.
### GPU telemetry
The 61 one-second samples during the paced interval measured total board state,
including neighboring services:
- GPU utilization: 52.08% mean, 57% p95, 63% max;
- board memory used: 10,240.57 MiB mean, 10,250 MiB max;
- power: 144.57 W mean, 147.49 W p95, 150.23 W max;
- temperature: 42.95 C mean, 44 C max.
These are board totals, not per-process CUDA allocation claims.
## Forced-overload negative control
Published result:
`e8-realtime-tracking-93ab963d9e4f25a93b60148a9a512e5ac7e1df3cdacec60ab105435b344fcf61`.
The same 64-frame pilot interval was replayed at 1.0x with an intentional
140 ms consumer delay after every processed frame. This makes the consumer
slower than the approximately 100 ms source period.
| Measurement | Result |
|---|---:|
| Frames published by source | 64 |
| Frames processed | 40 |
| Superseded frames dropped | 24 (37.5%) |
| Effective output rate | 5.895414 FPS |
| Queue capacity / maximum depth | 2 / 2 |
| Result-age p95 | 442.345170 ms |
| Health | 40 stale |
This is the intended failure behavior: derived perception loses temporal
resolution and declares staleness, but memory/latency does not grow without a
bound. The result is accepted only as an overload-control proof, not as an
operational performance result.
## Comparison with LAB E5
LAB E5 processed the same 601 frames sequentially while writing a PNG overlay
for every frame. Its inference/tracking/artifact loop achieved 6.822348 FPS and
had 146.279 ms mean end-to-end time. The synchronous overlay write alone cost
87.363 ms mean.
LAB E8 removes that visualization work from the inference critical path:
- 10.009945 FPS instead of 6.822348 FPS;
- 32.070 ms mean dequeue-to-result processing;
- 12.223 ms mean Triton request;
- 0.305 ms mean tracking;
- no source frame drops in the 60-second qualification.
This proves that LAB E5's offline rate was not the detector/tracker ceiling.
The remaining largest per-frame cost in E8 is temporary PNG decode. Direct
camera/video ingest can reduce it later, but it is not required to meet the
present 10 Hz gate.
## Disk policy and measured usage
The full qualification reserved a conservative 3.273 GiB working set and
failed closed if the 360 GiB D: floor would be crossed.
| Disk point | Free bytes | Free GiB |
|---|---:|---:|
| Before qualification | 407,538,298,880 | 379.550 |
| After temporary frame extraction | 406,955,905,024 | 379.007 |
| After paced replay | 406,868,586,496 | 378.926 |
| After immutable publication, before temp cleanup | 406,868,582,400 | 378.926 |
| After all E8 runs and exact temp cleanup | 407,465,738,240 | 379.370 |
| Enforced floor | 386,547,056,640 | 360.000 |
No unrelated data was removed. The immutable pilot, qualification, overload
control and staged runner bundles remain on D: as evidence.
## Artifact integrity
The full result contains:
| Artifact | Bytes | SHA-256 |
|---|---:|---|
| `frames.jsonl` | 1,156,936 | `df5fe55f52df887c8d04e1ed3937f0faf5b08d379b6f41e45638016f1bd2a479` |
| `telemetry.jsonl` | 145,235 | `8d904465fd8a352d59a62bb7ecdeebdc0c5ea07b1700c95136e14a197c617182` |
| `gpu-telemetry.jsonl` | 13,652 | `6d87dcf9944831ad5bd9bb1c88381ac940ff30d175475068891893de1e7dcdb4` |
| `run-report.json` | 9,478 | `6fb136fa54ebf7d655b559da3218aacab1aa5838308d5ed83211081857f8e2f5` |
Independent host validation confirmed for pilot, full qualification and
overload control:
- result directory equals the canonical identity SHA-256;
- every declared artifact size and SHA-256 matches;
- result identity binds job, input, session, source, selection, calibration,
model, profile, runner and orchestrator;
- all frame and telemetry rows are strictly ordered and mutually bound;
- full qualification contains all 601 source indices;
- overload-control gaps exactly account for the 24 latest-wins drops;
- all detection and track boxes are finite and confined to 800x600;
- queue producer, consumer and drop totals are exact;
- all three immutable results pass the dedicated E8 validator.
Durable local evidence root:
```text
.runtime/compute-experiments/e8/worker-results/
```
Worker evidence roots:
```text
D:\NDC_MISSIONCORE\runtime\derived\
e8-realtime-tracking-ba7f73c8f4e0bde1634cce89d7fda5c94c43d0b55af8080a1eeec8af636474ba
e8-realtime-tracking-2802f407cb9d2c24576a4ba32d3e35f4d2b2092bfa99b37c438a8ac8120e1b16
e8-realtime-tracking-93ab963d9e4f25a93b60148a9a512e5ac7e1df3cdacec60ab105435b344fcf61
```
## What is accepted
- actual YOLOX-S inference on the RTX worker at the recorded 10 Hz source rate;
- temporal tracking in the same source-paced critical path;
- fixed calibration-bound valid-FOV preprocessing;
- 601/601 processed frames, zero drops and p95 below 100 ms;
- explicit health/freshness state;
- bounded latest-wins overload behavior;
- D-only task-controlled storage and exact temporary cleanup;
- restoration of the pre-run Triton model state;
- immutable, content-addressed evidence and independent validation.
## What is not accepted
- direct live K1 network/camera transport;
- real-time semantic segmentation;
- live LiDAR association, 3D cuboids or metric range in this worker run;
- appearance ReID or camera-motion compensation;
- human-reviewed accuracy, IDF1/HOTA/MOTA or safety metrics;
- navigation or actuation use;
- production transport between the worker and Mission Core;
- evidence that this exact margin holds for arbitrarily long sessions.
## Next gate
The next experiment must preserve this accepted detector/tracker path and add
semantics as a separate lower-rate asynchronous branch:
1. keep detector/tracker at the 10 Hz source cadence;
2. schedule semantic inference at a lower rate with a capacity-one latest-wins
queue;
3. attach each semantic result to its exact source timestamp;
4. reuse a semantic result only within an explicit TTL and expose its age;
5. never block detector/tracker or raw recording on semantic completion;
6. measure GPU contention against the accepted E8 baseline;
7. merge fresh tracking, semantics and existing E6 LiDAR geometry into the E7
world-state contract;
8. qualify overload and semantic-worker loss before any live K1 trial.
Ops publication remains pending because this Codex session did not expose the
required direct `nodedc-ops-agent` tools. No legacy Ops path was used.

View File

@ -0,0 +1,327 @@
# LAB E9 — Concurrent multirate detector/tracker and semantics
Date: 2026-07-21
State: completed; full qualification accepted
Ground truth: no
Navigation/safety acceptance: no
## Purpose and answer
LAB E9 tests the next autonomous-perception requirement after E8:
> Can the RTX worker preserve the approximately 10 Hz detector/tracker path
> while running the existing EoMT semantic model concurrently at a lower rate,
> with independent bounded queues and explicit semantic freshness?
The answer is **yes for the accepted recorded 60-second interval**:
- detector/tracker: 601/601 frames, 0 drops, 9.984702 FPS;
- detector result-age p95: 92.583758 ms;
- semantics: 121/121 scheduled frames, 0 drops, 2.010231 FPS;
- semantic completion-age p95: 250.404840 ms;
- fresh semantic binding: 597/601 detector states (99.3344%);
- detector queue maximum: 1/2;
- semantic queue maximum: 1/1.
This is a concurrent worker measurement, not an offline composition of
precomputed E4 and E5 artifacts. Both YOLOX/Triton and EoMT/PyTorch executed on
the RTX 4090 during the source-paced interval.
## Input identity
- Compute job: `recorded-camera-602ac89026ed12978619801d`
- Session: `RAVNOVES00`
- Source: `sensor.camera.right`
- Calibration slot: `camera_1`
- Calibration content SHA-256:
`05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`
- Resolution: 800x600
- Full qualification selection: source frames 1,0001,600 inclusive
- Detector frames scheduled: 601
- Semantic stride: every fifth source frame
- Semantic frames scheduled: 121
- Session timeline: 135.365857292195.334857292 seconds
- Source span: 59.969 seconds
The immutable valid-FOV mask is
`valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2`.
Mask SHA-256:
`a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63`.
## Models and runtime
Detector/tracker branch:
- official Megvii YOLOX-S ONNX release `0.1.1rc0`;
- model SHA-256:
`c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063`;
- ONNX Runtime GPU through the existing Triton container;
- accepted E8 detector profile SHA-256:
`819bdfb0da521d187ec9ce3d3039a0ff75daa0ab473fb3d8e5fa92db9c1ddca1`;
- ByteTrack-style two-stage IoU tracking, unchanged from E5/E8.
Semantic branch:
- `tue-mps/cityscapes_semantic_eomt_large_1024`;
- revision: `8d6b6d1a3f7b50d441afd7d247c2ed10db186e8f`;
- weights: 1,276,175,488 bytes;
- model SHA-256:
`c265da9a74f58f5c3f4826d23ca4ca78beac0b106cca5842beca61580de5b782`;
- precision: FP16 autocast, batch size one;
- accepted E4 semantic profile SHA-256:
`ea583966bc3409f5cf563cbf4fad05e366907e67187082eb692aff53d9f5d875`;
- Cityscapes output mapped into the existing Mission Core 015 taxonomy.
Worker runtime:
- NVIDIA GeForce RTX 4090;
- Python 3.12.3;
- PyTorch 2.13.0+cu130;
- Transformers 4.57.6;
- NumPy 1.26.4;
- SciPy 1.16.3;
- container: `nvcr.io/nvidia/tritonserver:26.06-py3`.
Accepted producer identities:
- runner SHA-256:
`89e3e6de6ed3d6c70687927d6b55335399ac5af83f60e41b09b8b1936def390f`;
- orchestrator SHA-256:
`ea4351d8761fa03c53d434b35ee64d56c12baec6a18d5cdf2ae7c0b7b461c655`;
- accepted multirate profile SHA-256:
`4c6a68e5be69b2140cd9efbcfdbc76a97d382a1de3822eb10ffcf47f94a2108e`;
- independent validator SHA-256:
`ba796085856c91918dcb5d3e3b48ccf4989354da40ccdbbe6d936319e0cfedf2`.
## Scheduling contract
```text
recorded source at exact session timestamps
├─ detector queue: capacity 2, every frame (~10 Hz)
│ -> valid-FOV -> YOLOX/Triton -> tracking
└─ semantic queue: capacity 1, every fifth frame (~2 Hz)
-> valid-FOV -> EoMT/PyTorch -> semantic mask identity/classes
detector result
+ newest completed semantic result whose source timestamp is not future
-> semantic status: fresh / stale / unavailable
```
Both queues use latest-wins overflow behavior. A slow semantic inference cannot
block detector admission, raw recording or source timing. Semantic TTL is
750 ms. Before the first semantic result completes, merged detector states are
explicitly `unavailable`; no mask is invented. Afterward, a mask is `fresh`
only while its source age is within TTL.
The paced interval begins only after both models are loaded and warmed. The
orchestrator intentionally performs full model/hash preflight and then launches
an isolated run container, so the laboratory wall time includes two EoMT loads.
A persistent production worker would load once. Startup is reported separately
from frame latency and is not hidden inside the 60-second replay metrics.
## Pilot history and profile freeze
### Pilot 1 — rejected strict budget
Result:
`e9-multirate-perception-4dba9d71ee14e8be6d66f4ae0a18d31c0be7d442039d400a3d8fd5b4396a3af3`.
The first 64-frame pilot required detector result-age p95 at or below 100 ms.
It produced:
- detector: 64/64, 0 drops, 10.041497 FPS;
- detector result-age p95: 121.054151 ms, max 126.765087 ms;
- semantics: 13/13, 0 drops, 2.039679 FPS;
- semantic completion-age p95: 244.136232 ms;
- fresh coverage: 92.1875%;
- queue maxima: detector 1/2, semantic 1/1.
Only the detector p95 check failed. GPU contention was measured directly:
Triton request p95 rose from E8's 18.589 ms to 32.569 ms while the concurrent
EoMT forward pass occupied the GPU. No queue accumulation, drop, stale detector
state or >150 ms result occurred.
The rejected result remains immutable evidence. It was not retroactively
accepted.
### Pilot 2 — accepted multirate budget
The second profile defines a multirate detector p95 ceiling of 150 ms, matching
the already established E8 stale boundary. This is not a safety claim; it is an
explicit near-real-time laboratory budget for the combined workload.
Result:
`e9-multirate-perception-51b279ffa70c9ce98e9bd41498821aa527022effe3bcf5fa0b0fac9c5f31cfae`.
- detector: 64/64, 0 drops, 10.074834 FPS;
- detector result-age p95: 104.053520 ms;
- semantics: 13/13, 0 drops, 2.046451 FPS;
- semantic completion-age p95: 244.872902 ms;
- fresh coverage: 93.75%;
- accepted.
The v2 profile was frozen before the full run. No threshold was changed after
seeing full-run output.
## Full qualification
Published result:
`e9-multirate-perception-161e0601d2a344e0a48c2498b68cefcac340e0afb77e4a45fecfa05bb44e4f2a`.
| Measurement | Detector/tracker | Semantics |
|---|---:|---:|
| Scheduled | 601 | 121 |
| Processed | 601 | 121 |
| Dropped | 0 | 0 |
| Effective rate | 9.984702 FPS | 2.010231 FPS |
| Queue capacity / max | 2 / 1 | 1 / 1 |
| Result/completion age p95 | 92.583758 ms | 250.404840 ms |
| Result/completion age max | 139.250452 ms | 443.920248 ms |
Merged semantic binding:
- fresh: 597 detector states;
- unavailable during startup: 4 detector states;
- stale: 0;
- fresh coverage: 99.3344%.
Detector output remained consistent with E5/E8:
- detections: 4,049;
- unique confirmed track IDs: 167;
- confirmed track observations: 3,320;
- same-class duplicate pairs at IoU >= 0.8: 0.
### Detector critical-path latency
| Stage | Mean | p50 | p95 | Max |
|---|---:|---:|---:|---:|
| Result age from source deadline | 64.529 ms | 61.538 ms | 92.584 ms | 139.250 ms |
| Decode | 31.736 ms | 29.785 ms | 44.604 ms | 62.679 ms |
| Queue wait | 0.175 ms | 0.073 ms | 0.183 ms | 16.178 ms |
| Processing after dequeue | 30.839 ms | 28.704 ms | 46.099 ms | 76.031 ms |
| Triton request | 16.898 ms | 14.462 ms | 30.324 ms | 39.523 ms |
| Tracking | 0.379 ms | 0.286 ms | 0.909 ms | 4.888 ms |
### Semantic latency
| Stage | Mean | p50 | p95 | Max |
|---|---:|---:|---:|---:|
| Completion age from source deadline | 216.777 ms | 212.143 ms | 250.405 ms | 443.920 ms |
| Processing after dequeue | 183.485 ms | 180.182 ms | 199.038 ms | 413.802 ms |
| EoMT forward | 150.739 ms | 149.165 ms | 162.677 ms | 376.747 ms |
| Processor | 12.344 ms | 11.261 ms | 20.817 ms | 47.351 ms |
| Host to device | 7.952 ms | 7.730 ms | 11.735 ms | 15.397 ms |
| Model postprocess | 9.324 ms | 9.145 ms | 13.969 ms | 20.305 ms |
| Queue wait | 0.192 ms | 0.170 ms | 0.277 ms | 1.786 ms |
## GPU and memory
Across 61 one-second samples during the measured interval:
- GPU utilization: 80.21% mean, 93% p95, 94% max;
- board memory used: 13,558.15 MiB mean, 13,568 MiB max;
- power: 200.66 W mean, 210.16 W p95, 225.70 W max;
- temperature: 47.11 C mean, 52 C max;
- EoMT process CUDA peak allocated: 2,107.925 MiB;
- EoMT process CUDA peak reserved: 2,840.0 MiB;
- process peak RSS: 2,007.344 MiB.
Board memory includes Triton and neighboring services. The high 93% p95 GPU
utilization is important: the accepted configuration has limited compute
headroom. Increasing semantic frequency or adding another large model cannot be
assumed safe without a new experiment.
## Disk and service safety
All task-controlled paths were confined to `D:\NDC_MISSIONCORE`. The full run
reserved 4.273 GiB and enforced the 360 GiB free-space floor.
| Disk point | Free bytes | Free GiB |
|---|---:|---:|
| Before full run | 406,969,856,000 | 379.020 |
| After temporary frame extraction | 406,493,011,968 | 378.576 |
| After replay/publication, before temp cleanup | 406,374,842,368 | 378.466 |
| After all E9 cleanup/evidence transfer | 406,836,731,904 | 378.896 |
| Enforced floor | 386,547,056,640 | 360.000 |
The temporary stream and PNG frame set were removed by exact run-owned path.
No unrelated data was removed. YOLOX was restored to its pre-run unloaded
state. Triton and Frigate remained healthy; Ollama remained running.
## Artifacts and integrity
| Artifact | Bytes | SHA-256 |
|---|---:|---|
| `detector-frames.jsonl` | 1,115,551 | `c17a82bc71abe5deb2542f9df257cbe52fa2ae88b1c4ae1986587e207b2243e0` |
| `semantic-frames.jsonl` | 87,225 | `6da2beca271922ebd9491ccef35e833c6d607387bcde816045a298cedb9a9f45` |
| `merged-frames.jsonl` | 689,114 | `c9a6c60df8c646067f988e17fe2a7b1b943200e88c5caaf4dca400c24cd5d5b3` |
| `gpu-telemetry.jsonl` | 13,710 | `7390a32aa924cbe87a5b0beab261de9429712db4794a25d77dfa6b171b33b6da` |
| `run-report.json` | 11,605 | `4be352a59665d1fe71630894a8a9324d5f27dde934650f65cef4305ac610a4e6` |
The semantic mask itself is held in memory in this scheduling gate. Each
semantic row records the exact source frame, completion age, taxonomy counts
and SHA-256 of the 800x600 target mask. Mask images are not synchronously written
and therefore cannot distort the measured critical path.
Independent validation confirmed for the rejected pilot, accepted pilot and
accepted full result:
- content-addressed identity and all artifact hashes;
- exact job/session/source/calibration/model/profile/runner binding;
- detector and semantic queue accounting;
- ordered detector and semantic source timestamps;
- semantic frames occur only on the declared stride;
- merged semantics never come from a future source frame;
- unavailable bindings contain no invented mask data;
- all three results pass the dedicated E9 validator.
Durable local evidence root:
```text
.runtime/compute-experiments/e9/worker-results/
```
Worker full-result root:
```text
D:\NDC_MISSIONCORE\runtime\derived\
e9-multirate-perception-161e0601d2a344e0a48c2498b68cefcac340e0afb77e4a45fecfa05bb44e4f2a
```
## Accepted scope
- actual concurrent YOLOX/Triton and EoMT/PyTorch execution on the RTX worker;
- detector/tracker near 10 Hz with no drops;
- semantics near 2 Hz with no drops;
- independent bounded latest-wins queues;
- exact semantic source binding, TTL and explicit unavailable state;
- 60-second pacing, latency, GPU, memory and disk evidence;
- D-only task-controlled storage and restored model/service state.
## Not accepted
- live K1 transport or arbitrarily long continuous operation;
- semantic mask delivery into Mission Core/Rerun;
- live LiDAR fusion, metric distance or 3D cuboids in this concurrent run;
- domain accuracy for forest/off-road navigation;
- optimized semantic model, TensorRT semantic deployment or extra GPU headroom;
- navigation or safety use.
## Next gate
The next step is integration rather than another isolated AI benchmark:
1. carry the latest in-memory semantic mask and exact freshness into the E7
world-state publisher;
2. run existing E6 LiDAR association/cuboids against the live 10 Hz tracking
branch;
3. show transient 2D segmentation plus 3D objects/range in Rerun without writing
per-frame PNGs;
4. inject semantic-worker loss and verify tracking/LiDAR continue with explicit
degraded state;
5. only then replace recorded pacing with direct K1/worker transport.
Ops publication remains pending because this Codex session exposes no direct
`nodedc-ops-agent` tasker tools. No legacy Ops writer was used.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,72 @@
{
"association": {
"box_inset": {
"bottom_fraction": 0.02,
"horizontal_fraction": 0.05,
"top_fraction": 0.04
},
"depth_cluster_gap_fraction": 0.06,
"depth_cluster_minimum_gap_m": 0.5,
"group_nms_iou_threshold": 0.55,
"maximum_cuboid_span_m": 12.0,
"maximum_distance_innovation_fraction": 0.25,
"maximum_distance_innovation_m": 1.5,
"maximum_oriented_extent_m": {
"bicycle": [3.5, 2.0, 2.5],
"motorcycle": [3.5, 2.0, 2.5],
"person": [1.5, 1.5, 2.8],
"vehicle": [6.5, 3.5, 4.0]
},
"minimum_cuboid_extent_m": 0.15,
"minimum_support_points": {
"bicycle": 3,
"motorcycle": 3,
"person": 3,
"vehicle": 5
},
"semantic_ids": {
"bicycle": [2],
"motorcycle": [3],
"person": [1],
"vehicle": [4, 5]
},
"spatial_cluster_radius_m": {
"bicycle": 0.9,
"motorcycle": 0.9,
"person": 0.9,
"vehicle": 1.5
},
"vehicle_labels": ["car", "truck", "bus"]
},
"inputs": {
"semantic_result_id": "result-793785170472c519486ccd666be102fb04d169d92383acda3fcc29eecf045d30",
"tracking_result_id": "e5-tracking-8035c0013e36a7cfb68aa3652b05ab79fe36baf136462d5fbf91b348ee85ec08"
},
"presentation": {
"contact_sheet_columns": 3,
"contact_sheet_frames": 6,
"distance_history_frames": 5,
"jpeg_quality": 85,
"overlay_crf": 20,
"point_radius_ui": 1.5,
"support_radius_ui": 3.0
},
"projection": {
"occlusion_policy": "nearest-depth-per-rounded-pixel",
"pixel_rounding": "nearest",
"source_coordinates": "k1-map-frame",
"visible_point_policy": "camera-front-in-frame"
},
"schema_version": "missioncore.e6-tracking-lidar-profile/v1",
"source": {
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9",
"calibration_slot": "camera_1",
"resolution": [800, 600],
"source_id": "sensor.camera.right"
},
"temporal": {
"binding": "nearest-host-arrival-best-effort",
"maximum_lidar_camera_delta_ms": 100.0,
"maximum_pose_point_delta_ms": 100.0
}
}

View File

@ -0,0 +1,72 @@
{
"association": {
"box_inset": {
"bottom_fraction": 0.02,
"horizontal_fraction": 0.05,
"top_fraction": 0.04
},
"depth_cluster_gap_fraction": 0.06,
"depth_cluster_minimum_gap_m": 0.5,
"group_nms_iou_threshold": 0.55,
"maximum_cuboid_span_m": 12.0,
"maximum_distance_innovation_fraction": 0.25,
"maximum_distance_innovation_m": 1.5,
"maximum_oriented_extent_m": {
"bicycle": [3.5, 2.0, 2.5],
"motorcycle": [3.5, 2.0, 2.5],
"person": [1.5, 1.5, 2.8],
"vehicle": [6.5, 3.5, 4.0]
},
"minimum_cuboid_extent_m": 0.15,
"minimum_support_points": {
"bicycle": 3,
"motorcycle": 3,
"person": 3,
"vehicle": 5
},
"semantic_ids": {
"bicycle": [2],
"motorcycle": [3],
"person": [1],
"vehicle": [4, 5]
},
"spatial_cluster_radius_m": {
"bicycle": 0.9,
"motorcycle": 0.9,
"person": 0.9,
"vehicle": 1.5
},
"vehicle_labels": ["car", "truck", "bus"]
},
"inputs": {
"semantic_result_id": "result-793785170472c519486ccd666be102fb04d169d92383acda3fcc29eecf045d30",
"tracking_result_id": "e5-tracking-88aace13ef9963f8dc07f85228e530f9d28c2b49aca9192409f7975512b058f6"
},
"presentation": {
"contact_sheet_columns": 3,
"contact_sheet_frames": 6,
"distance_history_frames": 5,
"jpeg_quality": 85,
"overlay_crf": 20,
"point_radius_ui": 1.5,
"support_radius_ui": 3.0
},
"projection": {
"occlusion_policy": "nearest-depth-per-rounded-pixel",
"pixel_rounding": "nearest",
"source_coordinates": "k1-map-frame",
"visible_point_policy": "camera-front-in-frame"
},
"schema_version": "missioncore.e6-tracking-lidar-profile/v1",
"source": {
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9",
"calibration_slot": "camera_1",
"resolution": [800, 600],
"source_id": "sensor.camera.right"
},
"temporal": {
"binding": "nearest-host-arrival-best-effort",
"maximum_lidar_camera_delta_ms": 100.0,
"maximum_pose_point_delta_ms": 100.0
}
}

View File

@ -0,0 +1,38 @@
{
"schema_version": "missioncore.e7-replay-realtime-profile/v1",
"mode": "overload-negative-control",
"selection": {
"start_frame_index": 120,
"frame_count": 80
},
"replay": {
"speed": 1.0,
"queue_capacity": 2,
"consumer_delay_ms": 250.0
},
"health": {
"stale_after_ms": 300.0,
"unavailable_after_ms": 1000.0
},
"world_state": {
"velocity_history_limit_s": 1.0,
"clearance": {
"sector_count": 72,
"minimum_range_m": 0.5,
"maximum_range_m": 30.0,
"ground_percentile": 5.0,
"minimum_height_above_ground_m": 0.2,
"maximum_height_above_ground_m": 3.0,
"front_half_angle_degrees": 15.0
}
},
"presentation": {
"log_point_cloud": false,
"point_radius_ui": 1.25
},
"acceptance": {
"minimum_drop_fraction": 0.25,
"maximum_queue_depth": 2,
"maximum_process_rss_mib": 512.0
}
}

View File

@ -0,0 +1,40 @@
{
"schema_version": "missioncore.e7-replay-realtime-profile/v1",
"mode": "pilot",
"selection": {
"start_frame_index": 120,
"frame_count": 64
},
"replay": {
"speed": 1.0,
"queue_capacity": 2,
"consumer_delay_ms": 0.0
},
"health": {
"stale_after_ms": 300.0,
"unavailable_after_ms": 1000.0
},
"world_state": {
"velocity_history_limit_s": 1.0,
"clearance": {
"sector_count": 72,
"minimum_range_m": 0.5,
"maximum_range_m": 30.0,
"ground_percentile": 5.0,
"minimum_height_above_ground_m": 0.2,
"maximum_height_above_ground_m": 3.0,
"front_half_angle_degrees": 15.0
}
},
"presentation": {
"log_point_cloud": true,
"point_radius_ui": 1.25
},
"acceptance": {
"minimum_delivery_rate_hz": 9.0,
"maximum_end_to_end_p95_ms": 300.0,
"maximum_drop_fraction": 0.02,
"maximum_queue_depth": 2,
"maximum_process_rss_mib": 512.0
}
}

View File

@ -0,0 +1,40 @@
{
"schema_version": "missioncore.e7-replay-realtime-profile/v1",
"mode": "qualification",
"selection": {
"start_frame_index": 0,
"frame_count": 601
},
"replay": {
"speed": 1.0,
"queue_capacity": 2,
"consumer_delay_ms": 0.0
},
"health": {
"stale_after_ms": 300.0,
"unavailable_after_ms": 1000.0
},
"world_state": {
"velocity_history_limit_s": 1.0,
"clearance": {
"sector_count": 72,
"minimum_range_m": 0.5,
"maximum_range_m": 30.0,
"ground_percentile": 5.0,
"minimum_height_above_ground_m": 0.2,
"maximum_height_above_ground_m": 3.0,
"front_half_angle_degrees": 15.0
}
},
"presentation": {
"log_point_cloud": true,
"point_radius_ui": 1.25
},
"acceptance": {
"minimum_delivery_rate_hz": 9.5,
"maximum_end_to_end_p95_ms": 300.0,
"maximum_drop_fraction": 0.01,
"maximum_queue_depth": 2,
"maximum_process_rss_mib": 512.0
}
}

View File

@ -0,0 +1,543 @@
#!/usr/bin/env python3
"""Fuse accepted recorded image masks into calibrated K1 LiDAR observations."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from uuid import uuid4
import numpy as np
import numpy.typing as npt
import rerun as rr
from PIL import Image, ImageDraw
from rerun.components import FillMode
from k1link.device_plugins.xgrids_k1.analyze.calibrated_overlay import (
_extract_camera_frames,
_load_calibration_snapshot,
_select_camera_anchors,
_select_lidar_samples,
)
from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
Kb4ProjectionProfile,
map_points_to_lidar,
project_map_points_kb4,
)
from k1link.device_plugins.xgrids_k1.mqtt.capture import read_capture_clock_origin
from k1link.sessions import inspect_recorded_media_epoch
SCHEMA = "missioncore.calibrated-segmentation-fusion-experiment/v1"
ACCEPTED_INSTANCE_MODEL = "torchvision/maskrcnn_resnet50_fpn_v2"
ACCEPTED_SEMANTIC_MODEL = "microsoft/beit-base-finetuned-ade-640-640"
MIN_BOX_POINTS = 4
MAX_BOX_SPAN_M = 12.0
FloatArray = npt.NDArray[np.float64]
RgbImage = npt.NDArray[np.uint8]
@dataclass(frozen=True, slots=True)
class ObjectFusion:
instance_id: int
label: str
score: float
candidate_points: int
clustered_points: int
distance_p10_m: float | None
distance_median_m: float | None
box_center_map: tuple[float, float, float] | None
box_half_size_map: tuple[float, float, float] | None
box_status: str
source_indices: npt.NDArray[np.int64]
@dataclass(frozen=True, slots=True)
class FusedFrame:
stem: str
session_time_seconds: float
image_rgb: RgbImage
semantic_overlay_rgb: RgbImage
fusion_overlay_rgb: RgbImage
points_map: FloatArray
projected_source_indices: npt.NDArray[np.int64]
projected_pixels: FloatArray
semantic_colors: RgbImage
objects: tuple[ObjectFusion, ...]
def _arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--session", type=Path, required=True)
parser.add_argument("--calibration", type=Path, required=True)
parser.add_argument("--calibrated-manifest", type=Path, required=True)
parser.add_argument("--segmentation", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
parser.add_argument("--ffmpeg", type=Path, required=True)
return parser.parse_args()
def _read_json(path: Path) -> dict[str, Any]:
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise RuntimeError(f"{path.name} is not a JSON object")
return value
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _palette(index: int) -> tuple[int, int, int]:
digest = hashlib.sha256(f"mission-core-segment-{index}".encode()).digest()
return (64 + digest[0] % 176, 64 + digest[1] % 176, 64 + digest[2] % 176)
def _depth_cluster(indices: npt.NDArray[np.int64], depths: FloatArray) -> npt.NDArray[np.int64]:
if indices.size < 2:
return indices
order = np.argsort(depths[indices])
ordered = indices[order]
ordered_depths = depths[ordered]
groups: list[npt.NDArray[np.int64]] = []
start = 0
for offset, gap in enumerate(np.diff(ordered_depths), start=1):
threshold = max(0.6, 0.08 * float(ordered_depths[offset - 1]))
if float(gap) > threshold:
groups.append(ordered[start:offset])
start = offset
groups.append(ordered[start:])
return min(
groups,
key=lambda group: (-int(group.size), float(np.median(depths[group]))),
)
def _object_fusions(
*,
instance_map: npt.NDArray[np.uint16],
instances: list[dict[str, Any]],
pixel_x: npt.NDArray[np.int64],
pixel_y: npt.NDArray[np.int64],
depths: FloatArray,
projected_source_indices: npt.NDArray[np.int64],
points_map: FloatArray,
points_lidar: FloatArray,
) -> tuple[ObjectFusion, ...]:
sampled_instances = instance_map[pixel_y, pixel_x]
fused: list[ObjectFusion] = []
for item in instances:
instance_id = int(item["instance_id"])
candidates = np.flatnonzero(sampled_instances == instance_id).astype(np.int64)
clustered = _depth_cluster(candidates, depths)
source_indices = projected_source_indices[clustered]
ranges = np.linalg.norm(points_lidar[source_indices], axis=1)
distance_p10 = None if ranges.size == 0 else float(np.percentile(ranges, 10.0))
distance_median = None if ranges.size == 0 else float(np.median(ranges))
center: tuple[float, float, float] | None = None
half_size: tuple[float, float, float] | None = None
if source_indices.size < MIN_BOX_POINTS:
status = f"rejected-fewer-than-{MIN_BOX_POINTS}-clustered-points"
else:
selected = points_map[source_indices]
lower = np.percentile(selected, 5.0, axis=0)
upper = np.percentile(selected, 95.0, axis=0)
spans = upper - lower
if not np.isfinite(spans).all() or np.any(spans > MAX_BOX_SPAN_M):
status = "rejected-implausible-span"
else:
sizes = np.maximum(spans, 0.15)
center = tuple(float(value) for value in ((lower + upper) * 0.5))
half_size = tuple(float(value) for value in (sizes * 0.5))
status = "accepted-diagnostic-axis-aligned"
fused.append(
ObjectFusion(
instance_id=instance_id,
label=str(item["label"]),
score=float(item["score"]),
candidate_points=int(candidates.size),
clustered_points=int(clustered.size),
distance_p10_m=distance_p10,
distance_median_m=distance_median,
box_center_map=center,
box_half_size_map=half_size,
box_status=status,
source_indices=source_indices,
)
)
return tuple(fused)
def _fusion_overlay(
semantic_overlay: RgbImage,
projected_pixels: FloatArray,
semantic_colors: RgbImage,
objects: tuple[ObjectFusion, ...],
instances: list[dict[str, Any]],
) -> RgbImage:
canvas = Image.fromarray(semantic_overlay).convert("RGBA")
draw = ImageDraw.Draw(canvas, "RGBA")
for (u, v), color in zip(projected_pixels, semantic_colors, strict=True):
red, green, blue = (int(value) for value in color)
draw.ellipse((u - 1.5, v - 1.5, u + 1.5, v + 1.5), fill=(red, green, blue, 210))
by_id = {item.instance_id: item for item in objects}
for item in instances:
fused = by_id[int(item["instance_id"])]
x1, y1, x2, y2 = (float(value) for value in item["box_xyxy"])
color = _palette(500 + fused.instance_id)
accepted = fused.box_center_map is not None
alpha = 255 if accepted else 135
draw.rectangle((x1, y1, x2, y2), outline=(*color, alpha), width=2)
distance = (
"no LiDAR"
if fused.distance_median_m is None
else f"{fused.distance_median_m:.1f}m/{fused.clustered_points}pts"
)
draw.text(
(x1 + 2, max(0.0, y1 - 12)),
f"{fused.label} {distance}",
fill=(255, 255, 255, alpha),
stroke_width=2,
stroke_fill=(0, 0, 0, alpha),
)
return np.asarray(canvas.convert("RGB"), dtype=np.uint8)
def _object_document(item: ObjectFusion) -> dict[str, Any]:
return {
"instance_id": item.instance_id,
"label": item.label,
"score": round(item.score, 9),
"candidate_projected_points": item.candidate_points,
"clustered_points": item.clustered_points,
"distance_p10_m": item.distance_p10_m,
"distance_median_m": item.distance_median_m,
"box_status": item.box_status,
"box_center_map": item.box_center_map,
"box_half_size_map": item.box_half_size_map,
}
def _write_rerun(path: Path, experiment_id: str, frames: tuple[FusedFrame, ...]) -> None:
recording = rr.RecordingStream("nodedc_k1_calibrated_segmentation", recording_id=experiment_id)
recording.set_sinks(rr.FileSink(path, write_footer=True))
try:
recording.log(
"contract",
rr.TextDocument(
"Recorded-only diagnostic: exact image masks sampled at factory-calibrated "
"K1 LiDAR projections; 3D boxes require clustered LiDAR support."
),
static=True,
)
for frame in frames:
recording.set_time(
"session_time",
duration=np.timedelta64(round(frame.session_time_seconds * 1_000_000_000), "ns"),
)
recording.log("camera/raw", rr.Image(frame.image_rgb))
recording.log("camera/semantic", rr.Image(frame.semantic_overlay_rgb))
recording.log("camera/fusion", rr.Image(frame.fusion_overlay_rgb))
recording.log(
"camera/projected_lidar_semantic",
rr.Points2D(
frame.projected_pixels.astype(np.float32),
colors=frame.semantic_colors,
radii=rr.Radius.ui_points(2.0),
),
)
recording.log(
"world/semantic_points",
rr.Points3D(
frame.points_map[frame.projected_source_indices].astype(np.float32),
colors=frame.semantic_colors,
radii=rr.Radius.ui_points(2.0),
),
)
boxed = [item for item in frame.objects if item.box_center_map is not None]
if boxed:
recording.log(
"world/boxes3d",
rr.Boxes3D(
centers=[item.box_center_map for item in boxed],
half_sizes=[item.box_half_size_map for item in boxed],
colors=[(*_palette(500 + item.instance_id), 96) for item in boxed],
fill_mode=FillMode.Solid,
labels=[
f"{item.label} · {item.distance_median_m:.1f} m · "
f"{item.clustered_points} pts"
for item in boxed
],
show_labels=True,
),
)
finally:
recording.flush(timeout_sec=30.0)
recording.disconnect()
def main() -> int:
args = _arguments()
session = args.session.resolve(strict=True)
calibration_root = args.calibration.resolve(strict=True)
calibrated_manifest = _read_json(args.calibrated_manifest.resolve(strict=True))
segmentation_root = args.segmentation.resolve(strict=True)
segmentation_manifest = _read_json(segmentation_root / "manifest.redacted.json")
ffmpeg = args.ffmpeg.resolve(strict=True)
models = segmentation_manifest.get("models")
acceptance = segmentation_manifest.get("acceptance")
if not isinstance(models, dict) or not isinstance(acceptance, dict):
raise RuntimeError("segmentation manifest is incomplete")
instance_model = models.get("instance")
semantic_model = models.get("semantic")
if (
not isinstance(instance_model, dict)
or instance_model.get("id") != ACCEPTED_INSTANCE_MODEL
or not isinstance(semantic_model, dict)
or semantic_model.get("id") != ACCEPTED_SEMANTIC_MODEL
or semantic_model.get("checkpoint_load") != "exact-no-missing-unexpected-or-mismatched-keys"
or acceptance.get("recorded_segmentation_artifact") != "generated"
):
raise RuntimeError("segmentation result is not an admitted strict-load experiment")
calibration, calibration_identity = _load_calibration_snapshot(calibration_root)
calibrated_input = calibrated_manifest.get("input")
if (
not isinstance(calibrated_input, dict)
or calibrated_input.get("session_id") != session.name
or calibrated_input.get("calibration_content_identity_sha256") != calibration_identity
):
raise RuntimeError("calibrated experiment does not bind this session/calibration")
source_id = str(calibrated_input["source_id"])
offsets = tuple(float(value) for value in calibrated_input["video_offsets_seconds"])
profile = Kb4ProjectionProfile.from_factory_calibration(calibration, source_id)
capture_root = session / "captures" / "mqtt_live"
origin = read_capture_clock_origin(capture_root / "mqtt.timeline.origin.json")
epoch_root = session / "media" / source_id / "epoch-1"
epoch = inspect_recorded_media_epoch(
epoch_root,
expected_source_name=source_id,
origin_epoch_ns=origin.started_at_epoch_ns,
origin_monotonic_ns=origin.started_monotonic_ns,
)
anchors = _select_camera_anchors(
epoch_root / "index.jsonl",
expected_count=len(epoch.segments),
offsets_seconds=offsets,
timeline_start_seconds=epoch.timeline_start_seconds,
origin_monotonic_ns=origin.started_monotonic_ns,
)
images = _extract_camera_frames(
ffmpeg,
init_path=epoch.init_path,
segment_paths=tuple(item.path for item in epoch.segments),
frame_sequences=tuple(item.sequence for item in anchors),
width=profile.width,
height=profile.height,
)
lidar_samples = _select_lidar_samples(
capture_root / "mqtt.raw.k1mqtt",
anchors,
origin_monotonic_ns=origin.started_monotonic_ns,
temporal_offset_seconds=float(calibrated_input["temporal_offset_seconds"]),
)
segmentation_inputs = {
str(item["name"]): str(item["sha256"])
for item in segmentation_manifest["input"]
if isinstance(item, dict)
}
segmentation_frames = segmentation_manifest["frames"]
fused_frames: list[FusedFrame] = []
for image, anchor, lidar_sample in zip(images, anchors, lidar_samples, strict=True):
stem = (
f"camera-{anchor.sequence:06d}-"
f"session-{round(anchor.session_time_seconds * 1000):09d}ms"
)
input_name = f"{stem}.png"
input_artifact = args.calibrated_manifest.parent / input_name
if segmentation_inputs.get(input_name) != _sha256(input_artifact):
raise RuntimeError(f"segmentation input identity changed for {input_name}")
if _sha256(input_artifact) != hashlib.sha256(image.tobytes()).hexdigest():
decoded = np.asarray(Image.open(input_artifact).convert("RGB"), dtype=np.uint8)
if not np.array_equal(decoded, image):
raise RuntimeError(f"decoded camera pixels changed for {input_name}")
semantic_map = np.asarray(Image.open(segmentation_root / f"{stem}.semantic.png"))
instance_map = np.asarray(Image.open(segmentation_root / f"{stem}.instances.png"))
semantic_overlay = np.asarray(
Image.open(segmentation_root / f"{stem}.semantic-overlay.png").convert("RGB")
)
if semantic_map.shape != image.shape[:2] or instance_map.shape != image.shape[:2]:
raise RuntimeError("segmentation maps do not match the camera epoch")
points_map = np.asarray(
[
point.scaled_xyz(lidar_sample.point_frame.header.scaler)
for point in lidar_sample.point_frame.points
],
dtype=np.float64,
)
points_lidar = map_points_to_lidar(
points_map,
position_map_xyz=lidar_sample.pose_frame.position_xyz,
orientation_map_from_lidar_xyzw=lidar_sample.pose_frame.orientation_xyzw,
)
projection = project_map_points_kb4(
points_map,
position_map_xyz=lidar_sample.pose_frame.position_xyz,
orientation_map_from_lidar_xyzw=lidar_sample.pose_frame.orientation_xyzw,
profile=profile,
)
pixel_x = np.clip(
np.rint(projection.pixels_xy[:, 0]).astype(np.int64), 0, profile.width - 1
)
pixel_y = np.clip(
np.rint(projection.pixels_xy[:, 1]).astype(np.int64), 0, profile.height - 1
)
semantic_ids = semantic_map[pixel_y, pixel_x].astype(np.int64)
semantic_colors = np.asarray(
[_palette(int(value)) for value in semantic_ids], dtype=np.uint8
)
frame_document = segmentation_frames[stem]
instances = frame_document["instance"]["instances"]
objects = _object_fusions(
instance_map=instance_map.astype(np.uint16),
instances=instances,
pixel_x=pixel_x,
pixel_y=pixel_y,
depths=projection.depths_m,
projected_source_indices=projection.source_indices,
points_map=points_map,
points_lidar=points_lidar,
)
fusion_overlay = _fusion_overlay(
semantic_overlay,
projection.pixels_xy,
semantic_colors,
objects,
instances,
)
fused_frames.append(
FusedFrame(
stem=stem,
session_time_seconds=anchor.session_time_seconds,
image_rgb=image,
semantic_overlay_rgb=semantic_overlay,
fusion_overlay_rgb=fusion_overlay,
points_map=points_map,
projected_source_indices=projection.source_indices,
projected_pixels=projection.pixels_xy,
semantic_colors=semantic_colors,
objects=objects,
)
)
created_at = datetime.now(UTC)
experiment_id = (
f"{created_at.strftime('%Y%m%dT%H%M%SZ')}_k1_segmentation_fusion_{uuid4().hex[:12]}"
)
private_root = args.output_root.resolve() / "private" / "perception-experiments"
private_root.mkdir(mode=0o700, parents=True, exist_ok=True)
output = private_root / experiment_id
staging = private_root / f".{experiment_id}.incomplete"
staging.mkdir(mode=0o700, exist_ok=False)
try:
for frame in fused_frames:
Image.fromarray(frame.fusion_overlay_rgb).save(staging / f"{frame.stem}.fusion.png")
mosaic_rows = [
np.concatenate(
(frame.image_rgb, frame.semantic_overlay_rgb, frame.fusion_overlay_rgb),
axis=1,
)
for frame in fused_frames
]
Image.fromarray(np.concatenate(mosaic_rows, axis=0)).save(staging / "fusion-mosaic.png")
_write_rerun(staging / "fusion.rrd", experiment_id, tuple(fused_frames))
identity = {
"calibrated_generation_sha256": calibrated_manifest["generation_sha256"],
"segmentation_generation_sha256": segmentation_manifest["generation_sha256"],
"calibration_content_identity_sha256": calibration_identity,
"session_id": session.name,
"source_id": source_id,
}
outputs = sorted(staging.iterdir())
manifest = {
"schema_version": SCHEMA,
"experiment_id": experiment_id,
"created_at_utc": created_at.isoformat(timespec="milliseconds").replace("+00:00", "Z"),
"generation_sha256": hashlib.sha256(
json.dumps(identity, sort_keys=True, separators=(",", ":")).encode()
).hexdigest(),
"input": identity,
"fusion": {
"pixel_sampling": "nearest-calibrated-projection",
"occlusion_policy": "largest-depth-contiguous-cluster-nearest-tiebreak",
"distance": "LiDAR-origin Euclidean p10 and median",
"box": {
"frame": "map",
"orientation": "axis-aligned-diagnostic",
"minimum_clustered_points": MIN_BOX_POINTS,
"robust_bounds": "p05-p95",
"maximum_span_m": MAX_BOX_SPAN_M,
},
},
"frames": [
{
"stem": frame.stem,
"session_time_seconds": frame.session_time_seconds,
"projected_points": int(frame.projected_source_indices.size),
"semantic_class_ids": sorted(
{
int(value)
for value in np.asarray(
Image.open(segmentation_root / f"{frame.stem}.semantic.png")
).reshape(-1)
}
),
"objects": [_object_document(item) for item in frame.objects],
"accepted_boxes": sum(
item.box_center_map is not None for item in frame.objects
),
}
for frame in fused_frames
],
"acceptance": {
"recorded_mask_to_lidar_fusion": "generated",
"distance": "diagnostic-not-ground-truthed",
"boxes3d": "diagnostic-not-tracked-or-ground-truthed",
"live": "not-tested",
"safety": "not-accepted",
},
"outputs": [
{"name": path.name, "bytes": path.stat().st_size, "sha256": _sha256(path)}
for path in outputs
],
}
(staging / "manifest.redacted.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
os.rename(staging, output)
except BaseException:
shutil.rmtree(staging, ignore_errors=True)
raise
print(json.dumps({"experiment_id": experiment_id, "output": str(output)}))
return 0
if __name__ == "__main__":
raise SystemExit(main())

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,498 @@
#!/usr/bin/env python3
"""Fuse one admitted full camera-perception epoch into compact 3D observations."""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import os
import shutil
import tarfile
import time
from collections import deque
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import numpy as np
import numpy.typing as npt
from fuse_calibrated_segmentation import _object_fusions, _palette
from PIL import Image
from k1link.compute import validate_recorded_perception_epoch_result
from k1link.device_plugins.xgrids_k1.analyze.calibrated_overlay import (
MAX_SYNC_DELTA_SECONDS,
_load_calibration_snapshot,
)
from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
Kb4ProjectionProfile,
map_points_to_lidar,
project_map_points_kb4,
)
from k1link.device_plugins.xgrids_k1.mqtt.capture import read_capture_clock_origin
from k1link.device_plugins.xgrids_k1.protocol.streams import (
LioPointCloudFrame,
LioPoseFrame,
decode_lio_pcl,
decode_lio_pose,
)
from k1link.device_plugins.xgrids_k1.viewer.replay import iter_replay_messages
SCHEMA = "missioncore.recorded-calibrated-fusion/v1"
IDENTITY_SCHEMA = "missioncore.recorded-calibrated-fusion-identity/v1"
MAX_MASK_MEMBER_BYTES = 4 * 1024 * 1024
MAX_INDEX_LINE_BYTES = 64 * 1024
FloatArray = npt.NDArray[np.float64]
@dataclass(frozen=True, slots=True)
class CameraAnchor:
sequence: int
host_session_seconds: float
video_session_seconds: float
@dataclass(frozen=True, slots=True)
class LidarSample:
point_session_seconds: float
pose_session_seconds: float
point_frame: LioPointCloudFrame
pose_frame: LioPoseFrame
def _arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--job", type=Path, required=True)
parser.add_argument("--result", type=Path, required=True)
parser.add_argument("--session", type=Path, required=True)
parser.add_argument("--calibration", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
return parser.parse_args()
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode()
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def _read_frame_metadata(path: Path, expected_count: int) -> list[dict[str, Any]]:
frames: list[dict[str, Any]] = []
with path.open("r", encoding="utf-8") as stream:
for expected_index, line in enumerate(stream):
value = json.loads(line)
if not isinstance(value, dict) or value.get("frame_index") != expected_index:
raise RuntimeError("perception frame metadata changed")
frames.append(value)
if len(frames) != expected_count:
raise RuntimeError("perception frame metadata is incomplete")
return frames
def _extract_masks(archive: Path, destination: Path, frame_count: int) -> None:
expected = {
f"{kind}/frame-{sequence:06d}.png"
for kind in ("instance-masks", "semantic-masks")
for sequence in range(1, frame_count + 1)
}
with tarfile.open(archive, mode="r:gz") as source:
members = [member for member in source.getmembers() if member.isfile()]
found = {member.name.replace("\\", "/").removeprefix("./") for member in members}
if found != expected or len(members) != len(expected):
raise RuntimeError("panoptic mask archive contents changed")
for member in members:
name = member.name.replace("\\", "/").removeprefix("./")
if (
member.issym()
or member.islnk()
or member.size < 1
or member.size > MAX_MASK_MEMBER_BYTES
):
raise RuntimeError("panoptic mask archive member is unsafe")
target = destination.joinpath(*name.split("/"))
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
payload = source.extractfile(member)
if payload is None:
raise RuntimeError("panoptic mask archive member is unavailable")
with target.open("xb") as sink:
shutil.copyfileobj(payload, sink, length=1024 * 1024)
if target.stat().st_size != member.size:
raise RuntimeError("panoptic mask archive member was truncated")
def _camera_anchors(
index_path: Path,
frame_metadata: list[dict[str, Any]],
*,
origin_monotonic_ns: int,
) -> list[CameraAnchor]:
anchors: list[CameraAnchor] = []
with index_path.open("rb") as stream:
for sequence, frame in enumerate(frame_metadata, start=1):
line = stream.readline(MAX_INDEX_LINE_BYTES + 1)
if not line or len(line) > MAX_INDEX_LINE_BYTES or not line.endswith(b"\n"):
raise RuntimeError("camera index is incomplete")
value = json.loads(line)
monotonic_ns = value.get("host_monotonic_ns") if isinstance(value, dict) else None
video_time = frame.get("session_seconds")
if (
not isinstance(value, dict)
or value.get("sequence") != sequence
or not isinstance(monotonic_ns, int)
or monotonic_ns < origin_monotonic_ns
or not isinstance(video_time, (int, float))
or not math.isfinite(float(video_time))
):
raise RuntimeError("camera anchor is invalid")
anchors.append(
CameraAnchor(
sequence=sequence,
host_session_seconds=(monotonic_ns - origin_monotonic_ns) / 1e9,
video_session_seconds=float(video_time),
)
)
if stream.read(1):
raise RuntimeError("camera index has undeclared rows")
return anchors
def _lidar_samples(
raw_path: Path,
anchors: list[CameraAnchor],
*,
origin_monotonic_ns: int,
) -> Iterator[LidarSample | None]:
messages = iter(iter_replay_messages(raw_path))
pending = next(messages, None)
points: deque[tuple[float, LioPointCloudFrame]] = deque()
poses: deque[tuple[float, LioPoseFrame]] = deque()
def message_time(message: Any) -> float:
monotonic_ns = message.received_monotonic_ns
if not isinstance(monotonic_ns, int) or monotonic_ns < origin_monotonic_ns:
raise RuntimeError("MQTT replay message has no compatible monotonic time")
return (monotonic_ns - origin_monotonic_ns) / 1e9
for anchor in anchors:
target = anchor.host_session_seconds
while pending is not None and message_time(pending) <= target + 0.5:
timestamp = message_time(pending)
if timestamp >= target - 0.5:
if pending.topic.endswith("/lio_pcl"):
points.append((timestamp, decode_lio_pcl(pending.payload)))
elif pending.topic.endswith("/lio_pose"):
poses.append((timestamp, decode_lio_pose(pending.payload)))
pending = next(messages, None)
while points and points[0][0] < target - 0.5:
points.popleft()
while poses and poses[0][0] < target - 0.5:
poses.popleft()
point_candidates = [
item for item in points if abs(item[0] - target) <= MAX_SYNC_DELTA_SECONDS
]
if not point_candidates:
yield None
continue
point_time, point_frame = min(point_candidates, key=lambda item: abs(item[0] - target))
pose_candidates = [
item for item in poses if abs(item[0] - point_time) <= MAX_SYNC_DELTA_SECONDS
]
if not pose_candidates:
yield None
continue
pose_time, pose_frame = min(pose_candidates, key=lambda item: abs(item[0] - point_time))
yield LidarSample(point_time, pose_time, point_frame, pose_frame)
def _empty_points() -> npt.NDArray[np.float32]:
return np.empty((0, 3), dtype=np.float32)
def _empty_colors(channels: int) -> npt.NDArray[np.uint8]:
return np.empty((0, channels), dtype=np.uint8)
def main() -> int:
args = _arguments()
started = time.perf_counter()
result = validate_recorded_perception_epoch_result(args.job, args.result)
session = args.session.resolve(strict=True)
if session.name != result.job.session_id:
raise RuntimeError("fusion session does not match the perception result")
calibration, calibration_sha256 = _load_calibration_snapshot(
args.calibration.resolve(strict=True)
)
if calibration_sha256 != result.calibration_sha256:
raise RuntimeError("fusion calibration generation does not match perception")
profile = Kb4ProjectionProfile.from_factory_calibration(calibration, result.job.source_id)
frame_metadata = _read_frame_metadata(
result.artifact("panoptic-frame-metadata").path,
result.job.segment_count,
)
capture_root = session / "captures" / "mqtt_live"
origin = read_capture_clock_origin(capture_root / "mqtt.timeline.origin.json")
epoch_root = (
result.job.job_root
/ "input"
/ "camera"
/ result.job.source_id
/ f"epoch-{result.job.codec_epoch}"
)
anchors = _camera_anchors(
epoch_root / "index.jsonl",
frame_metadata,
origin_monotonic_ns=origin.started_monotonic_ns,
)
identity = {
"schema_version": IDENTITY_SCHEMA,
"job_id": result.job.job_id,
"input_sha256": result.job.input_sha256,
"perception_result_id": result.result_id,
"calibration_sha256": calibration_sha256,
"camera_slot": result.calibration_slot,
"configuration": {
"pipeline": "factory-kb4-mask-to-lidar/v1",
"temporal_binding": "nearest-host-arrival-best-effort",
"maximum_sync_delta_seconds": MAX_SYNC_DELTA_SECONDS,
"semantic_sampling": "nearest-projected-pixel",
"box_policy": "depth-clustered-map-axis-aligned-p05-p95-diagnostic",
},
}
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
fusion_id = f"fusion-{identity_sha256}"
parent = args.output_root.resolve() / result.job.job_id
parent.mkdir(mode=0o700, parents=True, exist_ok=True)
output = parent / fusion_id
if output.exists():
print(json.dumps({"fusion_id": fusion_id, "output": str(output), "reused": True}))
return 0
staging = parent / f".{fusion_id}.{os.getpid()}.incomplete"
staging.mkdir(mode=0o700, exist_ok=False)
masks_root = staging / "masks"
masks_root.mkdir(mode=0o700)
try:
_extract_masks(
result.artifact("panoptic-mask-archive").path,
masks_root,
result.job.segment_count,
)
frame_times_ns: list[int] = []
point_offsets = [0]
point_arrays: list[npt.NDArray[np.float32]] = []
point_color_arrays: list[npt.NDArray[np.uint8]] = []
box_offsets = [0]
box_centers: list[tuple[float, float, float]] = []
box_half_sizes: list[tuple[float, float, float]] = []
box_colors: list[tuple[int, int, int, int]] = []
box_labels: list[str] = []
frame_reports: list[dict[str, Any]] = []
compatible_frames = 0
accepted_boxes = 0
for index, (anchor, sample) in enumerate(
zip(
anchors,
_lidar_samples(
capture_root / "mqtt.raw.k1mqtt",
anchors,
origin_monotonic_ns=origin.started_monotonic_ns,
),
strict=True,
)
):
frame_times_ns.append(round(anchor.video_session_seconds * 1e9))
if sample is None:
point_arrays.append(_empty_points())
point_color_arrays.append(_empty_colors(3))
point_offsets.append(point_offsets[-1])
box_offsets.append(box_offsets[-1])
frame_reports.append(
{"frame_index": index, "state": "depth-unavailable", "points": 0, "boxes": 0}
)
continue
compatible_frames += 1
with Image.open(
masks_root / "semantic-masks" / f"frame-{index + 1:06d}.png"
) as semantic_image:
semantic_map = np.array(semantic_image, dtype=np.uint8, copy=True)
with Image.open(
masks_root / "instance-masks" / f"frame-{index + 1:06d}.png"
) as instance_image:
instance_map = np.array(instance_image, dtype=np.uint16, copy=True)
if semantic_map.shape != (profile.height, profile.width) or instance_map.shape != (
profile.height,
profile.width,
):
raise RuntimeError("panoptic mask dimensions changed")
points_map = np.asarray(
[
point.scaled_xyz(sample.point_frame.header.scaler)
for point in sample.point_frame.points
],
dtype=np.float64,
)
points_lidar = map_points_to_lidar(
points_map,
position_map_xyz=sample.pose_frame.position_xyz,
orientation_map_from_lidar_xyzw=sample.pose_frame.orientation_xyzw,
)
projection = project_map_points_kb4(
points_map,
position_map_xyz=sample.pose_frame.position_xyz,
orientation_map_from_lidar_xyzw=sample.pose_frame.orientation_xyzw,
profile=profile,
)
pixel_x = np.clip(
np.rint(projection.pixels_xy[:, 0]).astype(np.int64),
0,
profile.width - 1,
)
pixel_y = np.clip(
np.rint(projection.pixels_xy[:, 1]).astype(np.int64),
0,
profile.height - 1,
)
semantic_ids = semantic_map[pixel_y, pixel_x].astype(np.int64)
colors = np.asarray([_palette(int(value)) for value in semantic_ids], dtype=np.uint8)
projected = points_map[projection.source_indices].astype(np.float32)
point_arrays.append(projected)
point_color_arrays.append(colors)
point_offsets.append(point_offsets[-1] + projected.shape[0])
instances = frame_metadata[index]["instances"]
objects = _object_fusions(
instance_map=instance_map,
instances=instances,
pixel_x=pixel_x,
pixel_y=pixel_y,
depths=projection.depths_m,
projected_source_indices=projection.source_indices,
points_map=points_map,
points_lidar=points_lidar,
)
frame_box_count = 0
for item in objects:
if item.box_center_map is None or item.box_half_size_map is None:
continue
frame_box_count += 1
accepted_boxes += 1
box_centers.append(item.box_center_map)
box_half_sizes.append(item.box_half_size_map)
box_colors.append((*_palette(500 + item.instance_id), 96))
box_labels.append(
f"{item.label} · {item.distance_median_m:.1f} m · {item.clustered_points} pts"
)
box_offsets.append(box_offsets[-1] + frame_box_count)
frame_reports.append(
{
"frame_index": index,
"state": "fused",
"points": int(projected.shape[0]),
"boxes": frame_box_count,
"lidar_camera_delta_ms": round(
(sample.point_session_seconds - anchor.host_session_seconds) * 1000,
6,
),
"pose_point_delta_ms": round(
(sample.pose_session_seconds - sample.point_session_seconds) * 1000,
6,
),
}
)
if (index + 1) % 250 == 0 or index + 1 == result.job.segment_count:
print(
json.dumps(
{"phase": "fusion", "frames": index + 1, "total": result.job.segment_count}
),
flush=True,
)
arrays_path = staging / "fusion.npz"
np.savez_compressed(
arrays_path,
frame_times_ns=np.asarray(frame_times_ns, dtype=np.int64),
point_offsets=np.asarray(point_offsets, dtype=np.int64),
points=(np.concatenate(point_arrays) if point_arrays else _empty_points()),
point_colors=(
np.concatenate(point_color_arrays) if point_color_arrays else _empty_colors(3)
),
box_offsets=np.asarray(box_offsets, dtype=np.int64),
box_centers=np.asarray(box_centers, dtype=np.float32).reshape((-1, 3)),
box_half_sizes=np.asarray(box_half_sizes, dtype=np.float32).reshape((-1, 3)),
box_colors=np.asarray(box_colors, dtype=np.uint8).reshape((-1, 4)),
)
labels_path = staging / "box-labels.json"
labels_path.write_text(
json.dumps(box_labels, ensure_ascii=False, separators=(",", ":")),
encoding="utf-8",
)
frames_path = staging / "fusion-frames.jsonl"
frames_path.write_text(
"".join(json.dumps(item, sort_keys=True) + "\n" for item in frame_reports),
encoding="utf-8",
)
shutil.rmtree(masks_root)
created_at = datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
artifacts = [
{
"name": path.name,
"byte_length": path.stat().st_size,
"sha256": _sha256(path),
}
for path in (arrays_path, labels_path, frames_path)
]
manifest = {
"schema_version": SCHEMA,
"fusion_id": fusion_id,
"identity_sha256": identity_sha256,
"identity": identity,
"created_at_utc": created_at,
"session_id": result.job.session_id,
"source_id": result.job.source_id,
"frame_count": result.job.segment_count,
"timeline_start_seconds": result.job.timeline_start_seconds,
"timeline_end_seconds": result.job.timeline_end_seconds,
"metrics": {
"frames_fused": compatible_frames,
"frames_depth_unavailable": result.job.segment_count - compatible_frames,
"semantic_points": point_offsets[-1],
"accepted_diagnostic_boxes": accepted_boxes,
"wall_seconds": round(time.perf_counter() - started, 6),
},
"acceptance": {
"geometry": "factory-calibrated-kb4",
"timing": "host-arrival-best-effort-not-ground-truthed",
"distance": "diagnostic-not-ground-truthed",
"boxes3d": "support-gated-diagnostic-not-tracked",
"safety": "not-accepted",
},
"artifacts": artifacts,
}
(staging / "manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
os.replace(staging, output)
except BaseException:
shutil.rmtree(staging, ignore_errors=True)
raise
print(json.dumps({"fusion_id": fusion_id, "output": str(output), "reused": False}))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,307 @@
#!/usr/bin/env python3
"""Build a full-camera-epoch LAB E10 LiDAR/pose replay pack from raw evidence."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import numpy as np
from e10_fusion_runtime import PACK_SCHEMA, LidarReplayPack, canonical_json, sha256
from fuse_e6_tracking_lidar import _camera_anchors, _lidar_samples, _read_profile
from k1link.compute import validate_tracked_fusion_qualification_result
from k1link.device_plugins.xgrids_k1.analyze.calibrated_overlay import (
_load_calibration_snapshot,
)
from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
Kb4ProjectionProfile,
)
from k1link.device_plugins.xgrids_k1.mqtt.capture import read_capture_clock_origin
def arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--job", type=Path, required=True)
parser.add_argument("--tracking", type=Path, required=True)
parser.add_argument("--semantic", type=Path, required=True)
parser.add_argument("--e6-result", type=Path, required=True)
parser.add_argument("--e6-profile", type=Path, required=True)
parser.add_argument("--session", type=Path, required=True)
parser.add_argument("--calibration", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
return parser.parse_args()
def _read_e6_identity(result_root: Path) -> dict[str, Any]:
document = json.loads(
(result_root.resolve(strict=True) / "result.json").read_text(encoding="utf-8")
)
identity = document.get("identity") if isinstance(document, dict) else None
if not isinstance(identity, dict):
raise RuntimeError("accepted LAB E6 identity is unavailable")
return identity
def _read_full_timeline(path: Path, expected_count: int) -> list[dict[str, Any]]:
frames: list[dict[str, Any]] = []
previous_seconds = -1.0
with path.resolve(strict=True).open(encoding="utf-8") as stream:
for frame_index, line in enumerate(stream):
value = json.loads(line)
session_seconds = value.get("session_seconds") if isinstance(value, dict) else None
if (
not isinstance(value, dict)
or value.get("frame_index") != frame_index
or value.get("sequence") != frame_index + 1
or not isinstance(session_seconds, (int, float))
or isinstance(session_seconds, bool)
or not float(session_seconds) > previous_seconds
):
raise RuntimeError("full camera timeline is invalid")
frames.append(
{
"frame_index": frame_index,
"source_frame_index": frame_index,
"session_seconds": float(session_seconds),
"tracks": [],
}
)
previous_seconds = float(session_seconds)
if len(frames) != expected_count:
raise RuntimeError("full camera timeline count changed")
return frames
def main() -> int:
args = arguments()
upstream = validate_tracked_fusion_qualification_result(
args.job,
args.tracking,
args.semantic,
args.e6_result,
)
session = args.session.resolve(strict=True)
if session.name != upstream.job.session_id:
raise RuntimeError("LAB E10 full session differs from accepted E6")
e6_profile, e6_profile_sha256 = _read_profile(args.e6_profile)
e6_identity = _read_e6_identity(upstream.result_root)
if (
e6_identity.get("profile_sha256") != e6_profile_sha256
or e6_identity.get("configuration") != e6_profile
):
raise RuntimeError("LAB E10 full pack temporal policy differs from accepted E6")
calibration, calibration_sha256 = _load_calibration_snapshot(
args.calibration.resolve(strict=True)
)
if calibration_sha256 != upstream.semantic.calibration_sha256:
raise RuntimeError("LAB E10 full pack calibration differs from accepted E6")
projection = Kb4ProjectionProfile.from_factory_calibration(
calibration,
upstream.job.source_id,
)
timeline_frames = _read_full_timeline(
upstream.semantic.artifact("panoptic-frame-metadata").path,
upstream.job.segment_count,
)
capture_root = session / "captures" / "mqtt_live"
origin = read_capture_clock_origin(capture_root / "mqtt.timeline.origin.json")
epoch_root = (
upstream.job.job_root
/ "input"
/ "camera"
/ upstream.job.source_id
/ f"epoch-{upstream.job.codec_epoch}"
)
anchors = _camera_anchors(
epoch_root / "index.jsonl",
timeline_frames,
origin_monotonic_ns=origin.started_monotonic_ns,
)
maximum_lidar_delta_s = float(e6_profile["temporal"]["maximum_lidar_camera_delta_ms"]) / 1000.0
maximum_pose_delta_s = float(e6_profile["temporal"]["maximum_pose_point_delta_ms"]) / 1000.0
samples = _lidar_samples(
capture_root / "mqtt.raw.k1mqtt",
anchors,
origin_monotonic_ns=origin.started_monotonic_ns,
maximum_lidar_camera_delta_s=maximum_lidar_delta_s,
maximum_pose_point_delta_s=maximum_pose_delta_s,
)
count = len(anchors)
available = np.zeros((count,), dtype=np.bool_)
offsets = [0]
clouds: list[np.ndarray] = []
positions = np.full((count, 3), np.nan, dtype=np.float64)
quaternions = np.full((count, 4), np.nan, dtype=np.float64)
lidar_delta = np.full((count,), np.nan, dtype=np.float64)
pose_delta = np.full((count,), np.nan, dtype=np.float64)
for index, (anchor, sample) in enumerate(zip(anchors, samples, strict=True)):
if sample is None:
offsets.append(offsets[-1])
else:
cloud = np.asarray(
[
point.scaled_xyz(sample.point_frame.header.scaler)
for point in sample.point_frame.points
],
dtype=np.float32,
).reshape((-1, 3))
available[index] = True
clouds.append(cloud)
offsets.append(offsets[-1] + cloud.shape[0])
positions[index] = sample.pose_frame.position_xyz
quaternions[index] = sample.pose_frame.orientation_xyzw
lidar_delta[index] = (
sample.point_session_seconds - anchor.host_session_seconds
) * 1000.0
pose_delta[index] = (
sample.pose_session_seconds - sample.point_session_seconds
) * 1000.0
if (index + 1) % 500 == 0 or index + 1 == count:
print(
json.dumps(
{
"phase": "full-lidar-pack",
"frames": index + 1,
"total": count,
"lidar_frames": int(available[: index + 1].sum()),
"points": offsets[-1],
},
sort_keys=True,
),
flush=True,
)
source_indices = np.arange(count, dtype=np.int64)
session_seconds = np.asarray(
[anchor.video_session_seconds for anchor in anchors], dtype=np.float64
)
temporal = {
"binding": e6_profile["temporal"]["binding"],
"maximum_lidar_camera_delta_ms": float(
e6_profile["temporal"]["maximum_lidar_camera_delta_ms"]
),
"maximum_pose_point_delta_ms": float(e6_profile["temporal"]["maximum_pose_point_delta_ms"]),
"clock_source": "recorded-host-monotonic-arrival",
}
identity = {
"schema_version": PACK_SCHEMA,
"job_id": upstream.job.job_id,
"input_sha256": upstream.job.input_sha256,
"session_id": upstream.job.session_id,
"source_id": upstream.job.source_id,
"camera_slot": upstream.semantic.calibration_slot,
"calibration_sha256": calibration_sha256,
"e6_result_id": upstream.result_id,
"e6_profile_sha256": e6_profile_sha256,
"semantic_timeline_result_id": upstream.semantic.result_id,
"frame_count": count,
"source_start_frame_index": 0,
"source_end_frame_index": count - 1,
"timeline_start_seconds": float(session_seconds[0]),
"timeline_end_seconds": float(session_seconds[-1]),
"available_lidar_frames": int(available.sum()),
"point_count": int(offsets[-1]),
"temporal_binding": "accepted-e6-nearest-host-arrival-best-effort",
"temporal_policy": temporal,
"projection": {
"model": "kb4",
"width": projection.width,
"height": projection.height,
"source_coordinates": "k1-map",
"target_camera": upstream.job.source_id,
},
"producer_sha256": sha256(Path(__file__).resolve(strict=True)),
}
identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest()
pack_id = f"e10-lidar-pack-{identity_sha256}"
parent = args.output_root.resolve()
parent.mkdir(mode=0o700, parents=True, exist_ok=True)
output = parent / pack_id
if output.exists():
print(json.dumps({"pack_id": pack_id, "output": str(output), "reused": True}))
return 0
staging = parent / f".{pack_id}.{os.getpid()}.incomplete"
staging.mkdir(mode=0o700, exist_ok=False)
try:
arrays_path = staging / "lidar-pack.npz"
np.savez_compressed(
arrays_path,
frame_indices=np.arange(count, dtype=np.int64),
source_frame_indices=source_indices,
session_seconds=session_seconds,
sample_available=available,
cloud_offsets=np.asarray(offsets, dtype=np.int64),
cloud_points_map=(
np.concatenate(clouds) if clouds else np.empty((0, 3), dtype=np.float32)
),
pose_positions_map=positions,
pose_quaternions_map_from_lidar=quaternions,
lidar_camera_delta_ms=lidar_delta,
pose_point_delta_ms=pose_delta,
intrinsic_fx_fy_cx_cy=np.asarray(projection.intrinsic_fx_fy_cx_cy, dtype=np.float64),
distortion_kb4=np.asarray(projection.distortion_kb4, dtype=np.float64),
t_camera_from_lidar=np.asarray(projection.t_camera_from_lidar, dtype=np.float64),
)
manifest = {
"schema_version": PACK_SCHEMA,
"pack_id": pack_id,
"identity_sha256": identity_sha256,
"identity": identity,
"created_at_utc": datetime.now(UTC)
.isoformat(timespec="milliseconds")
.replace("+00:00", "Z"),
"classification": "private-recorded-sensor-replay-input",
"ground_truth": False,
"artifact": {
"path": arrays_path.name,
"media_type": "application/x-npz",
"byte_length": arrays_path.stat().st_size,
"sha256": sha256(arrays_path),
},
}
(staging / "manifest.json").write_bytes(
json.dumps(
manifest,
ensure_ascii=False,
sort_keys=True,
indent=2,
allow_nan=False,
).encode()
+ b"\n"
)
os.replace(staging, output)
try:
validation = LidarReplayPack(output, expected_job_id=upstream.job.job_id)
validation.close()
except BaseException:
shutil.rmtree(output, ignore_errors=True)
raise
except BaseException:
shutil.rmtree(staging, ignore_errors=True)
raise
print(
json.dumps(
{
"pack_id": pack_id,
"output": str(output),
"frames": count,
"lidar_frames": int(available.sum()),
"points": int(offsets[-1]),
"byte_length": (output / "lidar-pack.npz").stat().st_size,
},
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,211 @@
#!/usr/bin/env python3
"""Build a content-addressed LAB E10 LiDAR/pose replay pack from accepted E6."""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import os
import shutil
import sys
from datetime import UTC, datetime
from pathlib import Path
from types import ModuleType
import numpy as np
from e10_fusion_runtime import PACK_SCHEMA, canonical_json, sha256
from k1link.compute import validate_tracked_fusion_qualification_result
from k1link.device_plugins.xgrids_k1.analyze.calibrated_overlay import (
_load_calibration_snapshot,
)
from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
Kb4ProjectionProfile,
)
def arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--job", type=Path, required=True)
parser.add_argument("--tracking", type=Path, required=True)
parser.add_argument("--semantic", type=Path, required=True)
parser.add_argument("--e6-result", type=Path, required=True)
parser.add_argument("--session", type=Path, required=True)
parser.add_argument("--calibration", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
return parser.parse_args()
def load_e7_runner() -> ModuleType:
path = Path(__file__).with_name("run_e7_replay_realtime.py").resolve(strict=True)
spec = importlib.util.spec_from_file_location("missioncore_e10_e7_adapter", path)
if spec is None or spec.loader is None:
raise RuntimeError("LAB E7 adapter cannot be loaded")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def main() -> int:
args = arguments()
upstream = validate_tracked_fusion_qualification_result(
args.job,
args.tracking,
args.semantic,
args.e6_result,
)
session = args.session.resolve(strict=True)
if session.name != upstream.job.session_id:
raise RuntimeError("LAB E10 session differs from the accepted E6 input")
calibration, calibration_sha256 = _load_calibration_snapshot(
args.calibration.resolve(strict=True)
)
if calibration_sha256 != upstream.semantic.calibration_sha256:
raise RuntimeError("LAB E10 calibration differs from the accepted E6 input")
projection = Kb4ProjectionProfile.from_factory_calibration(
calibration,
upstream.job.source_id,
)
adapter = load_e7_runner()
frames = adapter._prepare_frames(
e6=upstream,
session=session,
selection_start=0,
selection_count=upstream.frame_count,
)
if len(frames) != upstream.frame_count:
raise RuntimeError("LAB E10 E6 replay frame count changed")
frame_indices = np.arange(len(frames), dtype=np.int64)
source_indices = np.asarray(
[int(frame.frame["source_frame_index"]) for frame in frames], dtype=np.int64
)
session_seconds = np.asarray(
[float(frame.frame["session_seconds"]) for frame in frames], dtype=np.float64
)
available = np.asarray([frame.position_map_xyz is not None for frame in frames], dtype=np.bool_)
offsets = [0]
clouds = []
positions = np.full((len(frames), 3), np.nan, dtype=np.float64)
quaternions = np.full((len(frames), 4), np.nan, dtype=np.float64)
lidar_delta = np.full((len(frames),), np.nan, dtype=np.float64)
pose_delta = np.full((len(frames),), np.nan, dtype=np.float64)
for index, frame in enumerate(frames):
cloud = np.asarray(frame.cloud_map, dtype=np.float32)
clouds.append(cloud)
offsets.append(offsets[-1] + cloud.shape[0])
if frame.position_map_xyz is not None and frame.orientation_map_from_lidar_xyzw is not None:
positions[index] = frame.position_map_xyz
quaternions[index] = frame.orientation_map_from_lidar_xyzw
lidar_delta[index] = float(frame.frame["lidar_camera_delta_ms"])
pose_delta[index] = float(frame.frame["pose_point_delta_ms"])
identity = {
"schema_version": PACK_SCHEMA,
"job_id": upstream.job.job_id,
"input_sha256": upstream.job.input_sha256,
"session_id": upstream.job.session_id,
"source_id": upstream.job.source_id,
"camera_slot": upstream.semantic.calibration_slot,
"calibration_sha256": calibration_sha256,
"e6_result_id": upstream.result_id,
"frame_count": len(frames),
"source_start_frame_index": int(source_indices[0]),
"source_end_frame_index": int(source_indices[-1]),
"timeline_start_seconds": float(session_seconds[0]),
"timeline_end_seconds": float(session_seconds[-1]),
"available_lidar_frames": int(available.sum()),
"point_count": int(offsets[-1]),
"temporal_binding": "accepted-e6-nearest-host-arrival-best-effort",
"projection": {
"model": "kb4",
"width": projection.width,
"height": projection.height,
"source_coordinates": "k1-map",
"target_camera": upstream.job.source_id,
},
"producer_sha256": sha256(Path(__file__).resolve(strict=True)),
}
identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest()
pack_id = f"e10-lidar-pack-{identity_sha256}"
parent = args.output_root.resolve()
parent.mkdir(mode=0o700, parents=True, exist_ok=True)
output = parent / pack_id
if output.exists():
print(json.dumps({"pack_id": pack_id, "output": str(output), "reused": True}))
return 0
staging = parent / f".{pack_id}.{os.getpid()}.incomplete"
staging.mkdir(mode=0o700, exist_ok=False)
try:
arrays_path = staging / "lidar-pack.npz"
np.savez_compressed(
arrays_path,
frame_indices=frame_indices,
source_frame_indices=source_indices,
session_seconds=session_seconds,
sample_available=available,
cloud_offsets=np.asarray(offsets, dtype=np.int64),
cloud_points_map=(
np.concatenate(clouds) if clouds else np.empty((0, 3), dtype=np.float32)
),
pose_positions_map=positions,
pose_quaternions_map_from_lidar=quaternions,
lidar_camera_delta_ms=lidar_delta,
pose_point_delta_ms=pose_delta,
intrinsic_fx_fy_cx_cy=np.asarray(projection.intrinsic_fx_fy_cx_cy, dtype=np.float64),
distortion_kb4=np.asarray(projection.distortion_kb4, dtype=np.float64),
t_camera_from_lidar=np.asarray(projection.t_camera_from_lidar, dtype=np.float64),
)
manifest = {
"schema_version": PACK_SCHEMA,
"pack_id": pack_id,
"identity_sha256": identity_sha256,
"identity": identity,
"created_at_utc": datetime.now(UTC)
.isoformat(timespec="milliseconds")
.replace("+00:00", "Z"),
"classification": "private-recorded-sensor-replay-input",
"ground_truth": False,
"artifact": {
"path": arrays_path.name,
"media_type": "application/x-npz",
"byte_length": arrays_path.stat().st_size,
"sha256": sha256(arrays_path),
},
}
manifest_path = staging / "manifest.json"
manifest_path.write_bytes(
json.dumps(
manifest,
ensure_ascii=False,
sort_keys=True,
indent=2,
allow_nan=False,
).encode()
+ b"\n"
)
os.replace(staging, output)
except BaseException:
shutil.rmtree(staging, ignore_errors=True)
raise
print(
json.dumps(
{
"pack_id": pack_id,
"output": str(output),
"frames": len(frames),
"lidar_frames": int(available.sum()),
"points": int(offsets[-1]),
"byte_length": (output / "lidar-pack.npz").stat().st_size,
},
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""Extract a minimal content-addressed live projection pack from accepted E14 input."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import numpy as np
SCHEMA = "missioncore.e15-live-projection-pack/v1"
PACK_ARRAYS = {
"intrinsic_fx_fy_cx_cy",
"distortion_kb4",
"t_camera_from_lidar",
}
def _canonical(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode()
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while block := stream.read(1024 * 1024):
digest.update(block)
return digest.hexdigest()
def _read_object(path: Path) -> dict[str, Any]:
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise RuntimeError("projection source manifest is invalid")
return value
def build_projection_pack(
source_root: Path,
output_root: Path,
*,
expected_calibration_sha256: str,
) -> Path:
source = source_root.expanduser().resolve(strict=True)
manifest_path = source / "manifest.json"
arrays_path = source / "lidar-pack.npz"
manifest = _read_object(manifest_path)
identity = manifest.get("identity")
if (
not isinstance(identity, dict)
or identity.get("calibration_sha256") != expected_calibration_sha256
or identity.get("source_id") != "sensor.camera.right"
or identity.get("camera_slot") != "camera_1"
or manifest.get("artifact", {}).get("sha256") != _sha256(arrays_path)
):
raise RuntimeError("accepted LiDAR pack calibration binding changed")
with np.load(arrays_path, allow_pickle=False) as source_arrays:
if not PACK_ARRAYS.issubset(source_arrays.files):
raise RuntimeError("accepted LiDAR pack projection arrays changed")
intrinsic = np.asarray(source_arrays["intrinsic_fx_fy_cx_cy"], dtype=np.float64)
distortion = np.asarray(source_arrays["distortion_kb4"], dtype=np.float64)
transform = np.asarray(source_arrays["t_camera_from_lidar"], dtype=np.float64)
if (
intrinsic.shape != (4,)
or distortion.shape != (4,)
or transform.shape != (4, 4)
or not np.isfinite(intrinsic).all()
or not np.isfinite(distortion).all()
or not np.isfinite(transform).all()
):
raise RuntimeError("live projection arrays are invalid")
pack_identity = {
"schema_version": SCHEMA,
"source_id": "sensor.camera.right",
"calibration_slot": "camera_1",
"calibration_sha256": expected_calibration_sha256,
"resolution": [800, 600],
"projection_model": "kb4",
"coordinate_contract": "k1-map-to-camera_1-via-inverse-map-pose",
"source_lidar_pack_id": manifest.get("pack_id"),
"source_lidar_pack_identity_sha256": manifest.get("identity_sha256"),
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
}
identity_sha256 = hashlib.sha256(_canonical(pack_identity)).hexdigest()
pack_id = f"e15-live-projection-{identity_sha256}"
parent = output_root.expanduser().resolve()
parent.mkdir(mode=0o700, parents=True, exist_ok=True)
destination = parent / pack_id
if destination.exists():
validate_projection_pack(destination, expected_calibration_sha256)
return destination
staging = parent / f".{pack_id}.{os.getpid()}.incomplete"
staging.mkdir(mode=0o700, exist_ok=False)
try:
output_arrays = staging / "projection.npz"
np.savez(
output_arrays,
intrinsic_fx_fy_cx_cy=intrinsic,
distortion_kb4=distortion,
t_camera_from_lidar=transform,
)
document = {
"schema_version": SCHEMA,
"pack_id": pack_id,
"identity_sha256": identity_sha256,
"identity": pack_identity,
"created_at_utc": datetime.now(UTC)
.isoformat(timespec="milliseconds")
.replace("+00:00", "Z"),
"classification": "calibration-only-no-recorded-sensor-frames",
"artifact": {
"path": output_arrays.name,
"byte_length": output_arrays.stat().st_size,
"sha256": _sha256(output_arrays),
},
}
(staging / "manifest.json").write_bytes(
json.dumps(document, indent=2, sort_keys=True).encode() + b"\n"
)
os.replace(staging, destination)
except BaseException:
shutil.rmtree(staging, ignore_errors=True)
raise
validate_projection_pack(destination, expected_calibration_sha256)
return destination
def validate_projection_pack(root: Path, expected_calibration_sha256: str) -> dict[str, Any]:
resolved = root.expanduser().resolve(strict=True)
manifest = _read_object(resolved / "manifest.json")
identity = manifest.get("identity")
identity_sha256 = manifest.get("identity_sha256")
artifact = manifest.get("artifact")
arrays_path = resolved / "projection.npz"
if (
manifest.get("schema_version") != SCHEMA
or not isinstance(identity, dict)
or identity.get("schema_version") != SCHEMA
or identity.get("calibration_sha256") != expected_calibration_sha256
or not isinstance(identity_sha256, str)
or hashlib.sha256(_canonical(identity)).hexdigest() != identity_sha256
or resolved.name != f"e15-live-projection-{identity_sha256}"
or manifest.get("pack_id") != resolved.name
or not isinstance(artifact, dict)
or artifact.get("path") != arrays_path.name
or artifact.get("byte_length") != arrays_path.stat().st_size
or artifact.get("sha256") != _sha256(arrays_path)
):
raise RuntimeError("live projection pack identity is invalid")
with np.load(arrays_path, allow_pickle=False) as arrays:
if set(arrays.files) != PACK_ARRAYS:
raise RuntimeError("live projection pack arrays changed")
if (
arrays["intrinsic_fx_fy_cx_cy"].shape != (4,)
or arrays["distortion_kb4"].shape != (4,)
or arrays["t_camera_from_lidar"].shape != (4, 4)
):
raise RuntimeError("live projection pack shapes changed")
return manifest
def _arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--source-root", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
parser.add_argument("--calibration-sha256", required=True)
return parser.parse_args()
def main() -> int:
args = _arguments()
output = build_projection_pack(
args.source_root,
args.output_root,
expected_calibration_sha256=args.calibration_sha256,
)
print(json.dumps({"projection_pack": str(output)}, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""Build the minimal hash-addressed K1 runtime imported by the E15 worker."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
import uuid
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
SCHEMA = "missioncore.e15-worker-package/v1"
COPIED_FILES = (
"k1link/__init__.py",
"k1link/compute/live_perception.py",
"k1link/data_plane/__init__.py",
"k1link/data_plane/views.py",
"k1link/device_plugins/__init__.py",
"k1link/device_plugins/xgrids_k1/protocol/__init__.py",
"k1link/device_plugins/xgrids_k1/protocol/normalizer.py",
"k1link/device_plugins/xgrids_k1/protocol/protobuf_wire.py",
"k1link/device_plugins/xgrids_k1/protocol/streams.py",
)
GENERATED_FILES = {
"k1link/compute/__init__.py": (
'"""Minimal E15 worker projection; import live_perception explicitly."""\n'
),
"k1link/device_plugins/xgrids_k1/__init__.py": (
'"""Minimal E15 K1 decoder projection; no device-runtime side effects."""\n'
),
}
def _canonical(value: object) -> bytes:
return json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode()
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def _identity(source_root: Path) -> dict[str, Any]:
source_files = []
for relative in COPIED_FILES:
source = source_root / relative
if not source.is_file() or source.is_symlink():
raise RuntimeError(f"E15 worker source is not a regular file: {relative}")
source_files.append({"path": relative, "sha256": _sha256(source)})
return {
"schema_version": SCHEMA,
"classification": "minimal-live-worker-import-projection",
"source_files": source_files,
"generated_initializers": {
path: hashlib.sha256(content.encode()).hexdigest()
for path, content in sorted(GENERATED_FILES.items())
},
"imports": [
"k1link.compute.live_perception.LiveSensorSynchronizer",
"k1link.data_plane.DecodedPointCloudView",
"k1link.data_plane.DecodedPoseView",
"k1link.device_plugins.xgrids_k1.protocol.normalizer.normalize_k1_message",
],
}
def validate_worker_package(root: Path, *, allow_staging: bool = False) -> dict[str, Any]:
resolved = root.resolve(strict=True)
manifest_path = resolved / "manifest.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
identity = manifest.get("identity")
identity_sha256 = manifest.get("identity_sha256")
artifacts = manifest.get("artifacts")
expected_paths = {*COPIED_FILES, *GENERATED_FILES, "manifest.json"}
actual_paths = {
path.relative_to(resolved).as_posix()
for path in resolved.rglob("*")
if path.is_file()
}
if (
manifest.get("schema_version") != SCHEMA
or not isinstance(identity, dict)
or identity.get("schema_version") != SCHEMA
or not isinstance(identity_sha256, str)
or hashlib.sha256(_canonical(identity)).hexdigest() != identity_sha256
or manifest.get("package_id") != f"e15-worker-package-{identity_sha256}"
or (
resolved.name != manifest.get("package_id")
and not (
allow_staging
and resolved.name.startswith(f".{manifest.get('package_id')}.")
and resolved.name.endswith(".tmp")
)
)
or not isinstance(artifacts, list)
or actual_paths != expected_paths
):
raise RuntimeError("E15 worker package identity is invalid")
artifact_paths = set()
for artifact in artifacts:
if not isinstance(artifact, dict):
raise RuntimeError("E15 worker package artifact is invalid")
relative = str(artifact.get("path", ""))
path = resolved / relative
if (
relative not in expected_paths - {"manifest.json"}
or relative in artifact_paths
or not path.is_file()
or path.is_symlink()
or artifact.get("byte_length") != path.stat().st_size
or artifact.get("sha256") != _sha256(path)
):
raise RuntimeError("E15 worker package artifact changed")
artifact_paths.add(relative)
if artifact_paths != expected_paths - {"manifest.json"}:
raise RuntimeError("E15 worker package artifact set changed")
return manifest
def build_worker_package(source_root: Path, output_root: Path) -> Path:
source = source_root.resolve(strict=True)
output = output_root.resolve()
output.mkdir(mode=0o700, parents=True, exist_ok=True)
identity = _identity(source)
identity_sha256 = hashlib.sha256(_canonical(identity)).hexdigest()
package_id = f"e15-worker-package-{identity_sha256}"
destination = output / package_id
if destination.exists():
validate_worker_package(destination)
return destination
staging = output / f".{package_id}.{uuid.uuid4().hex}.tmp"
staging.mkdir(mode=0o700)
try:
for relative in COPIED_FILES:
target = staging / relative
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source / relative, target)
for relative, content in GENERATED_FILES.items():
target = staging / relative
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
artifacts = []
for relative in sorted((*COPIED_FILES, *GENERATED_FILES)):
path = staging / relative
artifacts.append(
{
"path": relative,
"byte_length": path.stat().st_size,
"sha256": _sha256(path),
}
)
manifest = {
"schema_version": SCHEMA,
"package_id": package_id,
"identity_sha256": identity_sha256,
"identity": identity,
"created_at_utc": datetime.now(UTC)
.isoformat(timespec="milliseconds")
.replace("+00:00", "Z"),
"artifacts": artifacts,
}
with (staging / "manifest.json").open("x", encoding="utf-8", newline="\n") as stream:
json.dump(manifest, stream, indent=2, sort_keys=True)
stream.write("\n")
stream.flush()
os.fsync(stream.fileno())
validate_worker_package(staging, allow_staging=True)
staging.replace(destination)
finally:
if staging.exists():
shutil.rmtree(staging)
validate_worker_package(destination)
return destination
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--source-root", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
args = parser.parse_args()
destination = build_worker_package(args.source_root, args.output_root)
print(destination)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Prepare immutable E1 preprocessing and frame-selection inputs."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from k1link.compute.qualification import (
DEFAULT_QUALIFICATION_FRAME_COUNT,
prepare_recorded_qualification_slice,
)
from k1link.device_plugins.xgrids_k1.analyze.valid_fov import (
DEFAULT_EDGE_MARGIN_PIXELS,
prepare_k1_valid_fov_mask,
)
def _arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--job-root", type=Path, required=True)
parser.add_argument("--calibration-root", type=Path, required=True)
parser.add_argument("--source-id", required=True)
parser.add_argument("--output-root", type=Path, required=True)
parser.add_argument(
"--sample-count",
type=int,
default=DEFAULT_QUALIFICATION_FRAME_COUNT,
)
parser.add_argument(
"--edge-margin-pixels",
type=float,
default=DEFAULT_EDGE_MARGIN_PIXELS,
)
return parser.parse_args()
def main() -> int:
args = _arguments()
mask = prepare_k1_valid_fov_mask(
calibration_snapshot_root=args.calibration_root,
source_id=args.source_id,
output_root=args.output_root / "valid-fov",
edge_margin_pixels=args.edge_margin_pixels,
)
qualification = prepare_recorded_qualification_slice(
job_root=args.job_root,
output_root=args.output_root / "qualification-slices",
sample_count=args.sample_count,
)
print(
json.dumps(
{
"schema_version": "missioncore.e1-qualification-preparation/v1",
"valid_fov": {
"generation_id": mask.generation_id,
"manifest": str(mask.manifest_path),
"mask": str(mask.mask_path),
"calibration_sha256": mask.calibration_sha256,
"source_id": mask.source_id,
"calibration_slot": mask.calibration_slot,
"center_xy": list(mask.center_xy),
"radius_pixels": mask.radius_pixels,
"crop_xyxy_exclusive": list(mask.crop_xyxy),
"valid_pixel_count": mask.valid_pixel_count,
"valid_fraction": mask.valid_fraction,
},
"qualification_slice": {
"generation_id": qualification.generation_id,
"manifest": str(qualification.manifest_path),
"job_id": qualification.job_id,
"input_sha256": qualification.input_sha256,
"policy": qualification.policy,
"source_frame_count": qualification.source_frame_count,
"selected_frame_count": len(qualification.frames),
"first_frame_index": qualification.frames[0].frame_index,
"last_frame_index": qualification.frames[-1].frame_index,
},
},
ensure_ascii=False,
indent=2,
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Build or validate the immutable LAB E2 CVAT annotation handoff."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
import k1link.compute.annotation_workspace as workspace_module
from k1link.compute import prepare_annotation_workspace, validate_annotation_workspace
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser()
commands = parser.add_subparsers(dest="command", required=True)
prepare = commands.add_parser("prepare")
prepare.add_argument("--evaluation-pack", type=Path, required=True)
prepare.add_argument("--prelabels", type=Path, required=True)
prepare.add_argument("--valid-fov-root", type=Path, required=True)
prepare.add_argument("--output-root", type=Path, required=True)
validate = commands.add_parser("validate")
validate.add_argument("--workspace", type=Path, required=True)
validate.add_argument("--evaluation-pack", type=Path, required=True)
validate.add_argument("--prelabels", type=Path, required=True)
validate.add_argument("--valid-fov-root", type=Path, required=True)
return parser.parse_args()
def main() -> int:
args = _arguments()
if args.command == "prepare":
result = prepare_annotation_workspace(
evaluation_pack_root=args.evaluation_pack,
prelabels_root=args.prelabels,
valid_fov_root=args.valid_fov_root,
output_root=args.output_root,
producer_files=(
(
"annotation_workspace.py",
_sha256(Path(str(workspace_module.__file__)).resolve(strict=True)),
),
(
"prepare_e2_annotation_workspace.py",
_sha256(Path(__file__).resolve(strict=True)),
),
),
)
else:
result = validate_annotation_workspace(
args.workspace,
evaluation_pack_root=args.evaluation_pack,
prelabels_root=args.prelabels,
valid_fov_root=args.valid_fov_root,
)
print(
json.dumps(
{
"generation_id": result.generation_id,
"root": str(result.root),
"frame_count": result.frame_count,
"draft_instance_count": result.draft_instance_count,
"ground_truth": False,
},
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,364 @@
#!/usr/bin/env python3
"""Review and seal the LAB E2 recorded-perception evaluation pack."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Any
from PIL import Image, ImageDraw, ImageFont
import k1link.compute.evaluation_pack as evaluation_pack_module
from k1link.artifacts import utc_now_iso, write_json_atomic
from k1link.compute import (
EvaluationFrameRequest,
prepare_recorded_evaluation_pack,
validate_camera_compute_job,
validate_recorded_qualification_slice,
)
CANDIDATE_SCHEMA = "missioncore.e2-evaluation-candidates/v1"
SELECTION_SCHEMA = "missioncore.e2-evaluation-selection/v1"
def _arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser()
commands = parser.add_subparsers(dest="command", required=True)
candidates = commands.add_parser("candidates")
candidates.add_argument("--job-root", type=Path, required=True)
candidates.add_argument("--qualification-root", type=Path, required=True)
candidates.add_argument("--output-root", type=Path, required=True)
seal = commands.add_parser("seal")
seal.add_argument("--job-root", type=Path, required=True)
seal.add_argument("--qualification-root", type=Path, required=True)
seal.add_argument("--valid-fov-root", type=Path, required=True)
seal.add_argument("--selection", type=Path, required=True)
seal.add_argument("--output-root", type=Path, required=True)
return parser.parse_args()
def _run(command: list[str]) -> subprocess.CompletedProcess[str]:
return subprocess.run(command, check=True, text=True, capture_output=True)
def _ffmpeg_version() -> str:
completed = _run(["ffmpeg", "-version"])
return completed.stdout.splitlines()[0].strip()
def _reconstruct_stream(job_root: Path, destination: Path) -> None:
job = validate_camera_compute_job(job_root)
epoch = (
job.job_root
/ "input"
/ "camera"
/ job.source_id
/ f"epoch-{job.codec_epoch}"
)
with destination.open("xb") as output:
with (epoch / "init.mp4").open("rb") as source:
shutil.copyfileobj(source, output, 1024 * 1024)
for sequence in range(1, job.segment_count + 1):
with (epoch / "segments" / f"{sequence}.m4s").open("rb") as source:
shutil.copyfileobj(source, output, 1024 * 1024)
output.flush()
os.fsync(output.fileno())
def _timeline(stream_path: Path, start_seconds: float, expected_count: int) -> list[float]:
completed = _run(
[
"ffprobe",
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"frame=best_effort_timestamp_time",
"-of",
"json",
str(stream_path),
]
)
document = json.loads(completed.stdout)
frames = document.get("frames")
if not isinstance(frames, list) or len(frames) != expected_count:
raise RuntimeError("decoded camera timestamp count differs from the compute job")
epoch_values = [float(frame["best_effort_timestamp_time"]) for frame in frames]
first = epoch_values[0]
values = [start_seconds + value - first for value in epoch_values]
if any(right <= left for left, right in zip(values, values[1:], strict=False)):
raise RuntimeError("decoded camera timestamps are not strictly monotonic")
return values
def _extract_frames(stream_path: Path, indices: list[int], output_root: Path) -> None:
if not indices or indices != sorted(set(indices)):
raise RuntimeError("frame extraction indices must be sorted and unique")
output_root.mkdir(mode=0o700, parents=True, exist_ok=False)
expression = "+".join(f"eq(n\\,{index})" for index in indices)
pattern = output_root / "selected-%06d.png"
_run(
[
"ffmpeg",
"-hide_banner",
"-loglevel",
"error",
"-i",
str(stream_path),
"-map",
"0:v:0",
"-vf",
f"select={expression}",
"-fps_mode",
"passthrough",
str(pattern),
]
)
extracted = sorted(output_root.glob("selected-*.png"))
if len(extracted) != len(indices):
raise RuntimeError("ffmpeg did not extract the requested frame set")
for source, frame_index in zip(extracted, indices, strict=True):
destination = output_root / f"frame-{frame_index:06d}.png"
source.replace(destination)
os.chmod(destination, 0o600)
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def _contact_sheets(
frames_root: Path,
indices: list[int],
timestamps: list[float],
output_root: Path,
) -> list[dict[str, Any]]:
output_root.mkdir(mode=0o700, parents=True, exist_ok=False)
columns = 8
rows = 4
thumb_size = (200, 150)
label_height = 25
font = ImageFont.load_default(size=15)
records: list[dict[str, Any]] = []
page_size = columns * rows
for page, offset in enumerate(range(0, len(indices), page_size), start=1):
page_indices = indices[offset : offset + page_size]
canvas = Image.new(
"RGB",
(columns * thumb_size[0], rows * (thumb_size[1] + label_height)),
(18, 18, 18),
)
draw = ImageDraw.Draw(canvas)
for position, frame_index in enumerate(page_indices):
row, column = divmod(position, columns)
x = column * thumb_size[0]
y = row * (thumb_size[1] + label_height)
with Image.open(frames_root / f"frame-{frame_index:06d}.png") as opened:
image = opened.convert("RGB")
image.thumbnail(thumb_size, Image.Resampling.LANCZOS)
canvas.paste(image, (x, y))
draw.text(
(x + 4, y + thumb_size[1] + 3),
f"f={frame_index} t={timestamps[frame_index]:.3f}s",
fill=(240, 240, 240),
font=font,
)
path = output_root / f"contact-sheet-{page:02d}.png"
canvas.save(path, format="PNG", optimize=False)
os.chmod(path, 0o600)
records.append(
{
"path": path.name,
"first_frame_index": page_indices[0],
"last_frame_index": page_indices[-1],
"frame_count": len(page_indices),
"byte_length": path.stat().st_size,
"sha256": _sha256(path),
}
)
return records
def _candidates(args: argparse.Namespace) -> int:
job = validate_camera_compute_job(args.job_root)
qualification = validate_recorded_qualification_slice(
args.qualification_root,
job_root=job.job_root,
)
output = args.output_root.expanduser()
output.mkdir(mode=0o700, parents=True, exist_ok=False)
published = False
try:
with tempfile.TemporaryDirectory(prefix="missioncore-e2-") as temporary:
stream_path = Path(temporary) / "camera.mp4"
_reconstruct_stream(job.job_root, stream_path)
timestamps = _timeline(stream_path, job.timeline_start_seconds, job.segment_count)
indices = [frame.frame_index for frame in qualification.frames]
frames_root = output / "frames"
_extract_frames(stream_path, indices, frames_root)
sheets = _contact_sheets(frames_root, indices, timestamps, output / "contact-sheets")
manifest = {
"schema_version": CANDIDATE_SCHEMA,
"created_at_utc": utc_now_iso(),
"state": "review-candidates",
"job_id": job.job_id,
"input_sha256": job.input_sha256,
"qualification_generation_id": qualification.generation_id,
"ffmpeg": _ffmpeg_version(),
"frames": [
{
"qualification_order": order,
"frame_index": frame.frame_index,
"sequence": frame.sequence,
"segment_sha256": frame.segment_sha256,
"session_seconds": timestamps[frame.frame_index],
"path": f"frames/frame-{frame.frame_index:06d}.png",
"byte_length": (
frames_root / f"frame-{frame.frame_index:06d}.png"
).stat().st_size,
"sha256": _sha256(frames_root / f"frame-{frame.frame_index:06d}.png"),
}
for order, frame in enumerate(qualification.frames, start=1)
],
"contact_sheets": sheets,
"selection_guidance": {
"anchor_target": 48,
"temporal_clip_target": {"clips": 4, "frames_per_clip": 4},
"total_target": 64,
"required_coverage": [
"person",
"car_or_heavy_vehicle",
"building_structure",
"paved_or_unpaved_ground",
"grass_and_woody_vegetation",
"lens_boundary_hard_negative",
"motion_or_occlusion",
],
},
}
write_json_atomic(output / "manifest.json", manifest)
os.chmod(output / "manifest.json", 0o600)
published = True
finally:
if not published and output.exists():
shutil.rmtree(output)
print(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True))
return 0
def _load_selection(path: Path) -> tuple[EvaluationFrameRequest, ...]:
document = json.loads(path.expanduser().read_text(encoding="utf-8"))
rows = document.get("frames") if isinstance(document, dict) else None
if document.get("schema_version") != SELECTION_SCHEMA or not isinstance(rows, list):
raise RuntimeError("E2 selection document is incompatible")
selection: list[EvaluationFrameRequest] = []
for row in rows:
if not isinstance(row, dict):
raise RuntimeError("E2 selection frame is invalid")
selection.append(
EvaluationFrameRequest(
frame_index=int(row["frame_index"]),
role=str(row["role"]),
group_id=str(row["group_id"]),
)
)
return tuple(selection)
def _write_timeline(path: Path, timestamps: list[float]) -> None:
with path.open("x", encoding="utf-8", newline="\n") as stream:
for frame_index, session_seconds in enumerate(timestamps):
stream.write(
json.dumps(
{"frame_index": frame_index, "session_seconds": session_seconds},
separators=(",", ":"),
)
+ "\n"
)
stream.flush()
os.fsync(stream.fileno())
def _seal(args: argparse.Namespace) -> int:
job = validate_camera_compute_job(args.job_root)
selection = _load_selection(args.selection)
indices = [frame.frame_index for frame in selection]
with tempfile.TemporaryDirectory(prefix="missioncore-e2-") as temporary:
temporary_root = Path(temporary)
stream_path = temporary_root / "camera.mp4"
frames_root = temporary_root / "frames"
timeline_path = temporary_root / "timeline.jsonl"
_reconstruct_stream(job.job_root, stream_path)
timestamps = _timeline(stream_path, job.timeline_start_seconds, job.segment_count)
_extract_frames(stream_path, indices, frames_root)
_write_timeline(timeline_path, timestamps)
pack = prepare_recorded_evaluation_pack(
job_root=job.job_root,
qualification_root=args.qualification_root,
valid_fov_root=args.valid_fov_root,
decoded_frames_root=frames_root,
timeline_path=timeline_path,
output_root=args.output_root,
selection=selection,
decoder_version=_ffmpeg_version(),
selection_document_sha256=_sha256(args.selection.expanduser().resolve(strict=True)),
producer_files=(
(
"evaluation_pack.py",
_sha256(Path(str(evaluation_pack_module.__file__)).resolve(strict=True)),
),
(
"prepare_e2_evaluation_pack.py",
_sha256(Path(__file__).resolve(strict=True)),
),
),
)
print(
json.dumps(
{
"schema_version": "missioncore.e2-evaluation-preparation/v1",
"generation_id": pack.generation_id,
"root": str(pack.root),
"job_id": pack.job_id,
"input_sha256": pack.input_sha256,
"qualification_generation_id": pack.qualification_generation_id,
"valid_fov_generation_id": pack.valid_fov_generation_id,
"calibration_sha256": pack.calibration_sha256,
"frame_count": len(pack.frames),
"anchor_count": sum(frame.role == "anchor" for frame in pack.frames),
"temporal_frame_count": sum(frame.role == "temporal" for frame in pack.frames),
"annotation_state": "unannotated",
},
ensure_ascii=False,
indent=2,
sort_keys=True,
)
)
return 0
def main() -> int:
args = _arguments()
if args.command == "candidates":
return _candidates(args)
if args.command == "seal":
return _seal(args)
raise RuntimeError("unknown E2 command")
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""Build an immutable CVAT review handoff for the LAB E3 control and challenger."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
import zipfile
from pathlib import Path, PurePosixPath
from typing import Any
import numpy as np
from PIL import Image
RESULT_SCHEMA = "missioncore.k1-e3-rectified-segmentation-result/v1"
PACK_SCHEMA = "missioncore.perception-evaluation-pack/v1"
LABEL_SCHEMA = "missioncore.perception-cvat-labels/v1"
MANIFEST_SCHEMA = "missioncore.lab-e3-cvat-review-workspace/v1"
IDENTITY_SCHEMA = "missioncore.lab-e3-cvat-review-identity/v1"
CONTROL = "eomt-fisheye-mask"
CHALLENGER = "eomt-kb4-cubemap5-clahe"
EXPECTED_FRAMES = 64
PRIORITY_IMAGE_IDS = (59, 63, 62, 61, 60, 64, 50, 30, 17, 29)
ZIP_TIMESTAMP = (1980, 1, 1, 0, 0, 0)
def _arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--evaluation-pack", type=Path, required=True)
parser.add_argument("--e3-result", type=Path, required=True)
parser.add_argument("--labels", type=Path, required=True)
parser.add_argument("--images-zip", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
return parser.parse_args()
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode()
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _read_object(path: Path) -> dict[str, Any]:
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise RuntimeError(f"JSON root is not an object: {path}")
return value
def _valid_sha256(value: object) -> bool:
return (
isinstance(value, str)
and len(value) == 64
and all(character in "0123456789abcdef" for character in value)
)
def _safe_artifact(root: Path, encoded: object) -> Path:
if not isinstance(encoded, str):
raise RuntimeError("artifact path is not a string")
relative = PurePosixPath(encoded)
if relative.is_absolute() or ".." in relative.parts or not relative.parts:
raise RuntimeError("artifact path is unsafe")
path = root.joinpath(*relative.parts).resolve(strict=True)
if not path.is_file() or not path.is_relative_to(root):
raise RuntimeError("artifact path escaped its root")
return path
def _artifact(path: Path, root: Path) -> dict[str, Any]:
return {
"path": path.relative_to(root).as_posix(),
"bytes": path.stat().st_size,
"sha256": _sha256(path),
}
def _validate_pack(root: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]:
manifest = _read_object(root / "manifest.json")
identity = manifest.get("identity")
identity_sha256 = manifest.get("identity_sha256")
if (
manifest.get("schema_version") != PACK_SCHEMA
or not isinstance(identity, dict)
or not _valid_sha256(identity_sha256)
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
or manifest.get("generation_id") != f"evaluation-pack-{identity_sha256}"
or identity.get("source_id") != "sensor.camera.right"
or identity.get("calibration_slot") != "camera_1"
):
raise RuntimeError("evaluation pack is incompatible")
frames = identity.get("frames")
if (
not isinstance(frames, list)
or len(frames) != EXPECTED_FRAMES
or [frame.get("image_id") for frame in frames] != list(range(1, 65))
):
raise RuntimeError("evaluation pack frame contract changed")
return manifest, frames
def _validate_result(root: Path, pack: dict[str, Any]) -> dict[str, Any]:
result = _read_object(root / "result.json")
identity = result.get("identity")
identity_sha256 = result.get("identity_sha256")
if (
result.get("schema_version") != RESULT_SCHEMA
or result.get("ground_truth") is not False
or not isinstance(identity, dict)
or not _valid_sha256(identity_sha256)
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
or result.get("result_id") != f"e3-segmentation-{identity_sha256}"
or root.name != result.get("result_id")
or identity.get("evaluation_pack_id") != pack["generation_id"]
or identity.get("evaluation_identity_sha256") != pack["identity_sha256"]
or identity.get("frame_count") != EXPECTED_FRAMES
or CONTROL not in identity.get("variants", ())
or CHALLENGER not in identity.get("variants", ())
):
raise RuntimeError("LAB E3 result is incompatible")
artifacts = result.get("artifacts")
if not isinstance(artifacts, list):
raise RuntimeError("LAB E3 artifact list is invalid")
descriptors = {
item.get("path"): item
for item in artifacts
if isinstance(item, dict) and isinstance(item.get("path"), str)
}
for variant in (CONTROL, CHALLENGER):
for image_id in range(1, 65):
encoded = f"semantic-masks/{variant}/image-{image_id:03d}.png"
descriptor = descriptors.get(encoded)
path = _safe_artifact(root, encoded)
if (
not isinstance(descriptor, dict)
or path.stat().st_size != descriptor.get("bytes")
or not _valid_sha256(descriptor.get("sha256"))
or _sha256(path) != descriptor["sha256"]
):
raise RuntimeError(f"LAB E3 semantic artifact changed: {encoded}")
return result
def _semantic_labels(document: dict[str, Any]) -> list[dict[str, Any]]:
if document.get("schema_version") != LABEL_SCHEMA:
raise RuntimeError("CVAT label document is incompatible")
raw = document.get("semantic_task")
if not isinstance(raw, list):
raise RuntimeError("CVAT semantic labels are absent")
labels = [
{"id": 0, "name": "background", "color": "#000000"},
*[
{"id": int(item["id"]), "name": str(item["name"]), "color": str(item["color"])}
for item in raw
],
]
if [label["id"] for label in labels] != list(range(16)) + [255]:
raise RuntimeError("CVAT semantic taxonomy changed")
return labels
def _rgb(value: str) -> tuple[int, int, int]:
if len(value) != 7 or not value.startswith("#"):
raise RuntimeError("CVAT label color is invalid")
return tuple(int(value[index : index + 2], 16) for index in (1, 3, 5))
def _zip_info(name: str) -> zipfile.ZipInfo:
info = zipfile.ZipInfo(name, date_time=ZIP_TIMESTAMP)
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
return info
def _write_segmentation_zip(
destination: Path,
*,
result_root: Path,
variant: str,
frames: list[dict[str, Any]],
labels: list[dict[str, Any]],
) -> None:
palette = np.zeros((256, 3), dtype=np.uint8)
for label in labels:
palette[label["id"]] = _rgb(label["color"])
labelmap = "".join(
f"{label['name']}:{','.join(str(value) for value in _rgb(label['color']))}::\n"
for label in labels
).encode()
stems = [
f"image-{frame['image_id']:03d}-frame-{frame['frame_index']:06d}"
for frame in frames
]
with zipfile.ZipFile(destination, mode="x", compression=zipfile.ZIP_DEFLATED) as archive:
archive.writestr(_zip_info("labelmap.txt"), labelmap)
archive.writestr(
_zip_info("ImageSets/Segmentation/default.txt"),
("\n".join(stems) + "\n").encode(),
)
for image_id, stem in enumerate(stems, start=1):
source = result_root / "semantic-masks" / variant / f"image-{image_id:03d}.png"
with Image.open(source) as opened:
indexed = np.asarray(opened.convert("L"), dtype=np.uint8)
if indexed.shape != (600, 800) or np.any(~np.isin(indexed, list(range(16)))):
raise RuntimeError(f"semantic mask taxonomy changed: {source}")
rgb = palette[indexed]
from io import BytesIO
stream = BytesIO()
Image.fromarray(rgb, mode="RGB").save(stream, format="PNG", optimize=True)
archive.writestr(
_zip_info(f"SegmentationClass/{stem}.png"),
stream.getvalue(),
)
def _validate_existing(root: Path, identity_sha256: str) -> dict[str, Any]:
manifest = _read_object(root / "manifest.json")
if (
manifest.get("schema_version") != MANIFEST_SCHEMA
or manifest.get("workspace_id") != f"e3-cvat-review-{identity_sha256}"
or manifest.get("identity_sha256") != identity_sha256
or manifest.get("ground_truth") is not False
):
raise RuntimeError("existing LAB E3 CVAT workspace is incompatible")
for descriptor in manifest.get("artifacts", ()):
path = _safe_artifact(root, descriptor.get("path"))
if path.stat().st_size != descriptor.get("bytes") or _sha256(path) != descriptor.get(
"sha256"
):
raise RuntimeError("existing LAB E3 CVAT artifact changed")
return manifest
def main() -> int:
args = _arguments()
pack_root = args.evaluation_pack.resolve(strict=True)
result_root = args.e3_result.resolve(strict=True)
labels_path = args.labels.resolve(strict=True)
images_zip = args.images_zip.resolve(strict=True)
output_root = args.output_root.resolve()
pack, frames = _validate_pack(pack_root)
result = _validate_result(result_root, pack)
labels_document = _read_object(labels_path)
labels = _semantic_labels(labels_document)
with zipfile.ZipFile(images_zip) as archive:
if len(archive.namelist()) != EXPECTED_FRAMES:
raise RuntimeError("CVAT image archive frame count changed")
identity = {
"schema_version": IDENTITY_SCHEMA,
"evaluation_pack_id": pack["generation_id"],
"evaluation_identity_sha256": pack["identity_sha256"],
"e3_result_id": result["result_id"],
"e3_identity_sha256": result["identity_sha256"],
"control_variant": CONTROL,
"challenger_variant": CHALLENGER,
"frame_count": EXPECTED_FRAMES,
"labels_sha256": _sha256(labels_path),
"images_zip_sha256": _sha256(images_zip),
"priority_image_ids": list(PRIORITY_IMAGE_IDS),
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
}
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
workspace_id = f"e3-cvat-review-{identity_sha256}"
final_root = output_root / workspace_id
if final_root.exists():
manifest = _validate_existing(final_root.resolve(strict=True), identity_sha256)
print(json.dumps({"state": "reused", **manifest}, sort_keys=True))
return 0
output_root.mkdir(parents=True, exist_ok=True)
staging = output_root / f".{workspace_id}.{os.getpid()}.publish"
staging.mkdir(mode=0o700)
try:
labels_output = staging / "labels.json"
labels_output.write_text(json.dumps(labels_document, indent=2) + "\n", encoding="utf-8")
for variant, filename in (
(CONTROL, "control-fisheye-mask.zip"),
(CHALLENGER, "challenger-kb4-cubemap5.zip"),
):
_write_segmentation_zip(
staging / filename,
result_root=result_root,
variant=variant,
frames=frames,
labels=labels,
)
artifacts = [
_artifact(path, staging)
for path in sorted(staging.iterdir())
if path.is_file()
]
manifest = {
"schema_version": MANIFEST_SCHEMA,
"workspace_id": workspace_id,
"identity_sha256": identity_sha256,
"identity": identity,
"ground_truth": False,
"artifacts": artifacts,
"next_gate": "two-pass CVAT review and reviewed export",
}
(staging / "manifest.json").write_text(
json.dumps(manifest, indent=2) + "\n",
encoding="utf-8",
)
staging.replace(final_root)
finally:
if staging.exists():
shutil.rmtree(staging)
print(
json.dumps(
{"state": "created", "workspace_id": workspace_id, "root": str(final_root)},
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,285 @@
#!/usr/bin/env python3
"""Source-paced RAVNOVES00 qualification of the live shadow transport."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import threading
import time
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Literal
import uvicorn
from fastapi import FastAPI
from k1link.compute.live_perception import LiveIngressModality, LivePerceptionIngress
from k1link.device_plugins.xgrids_k1.live_perception_shadow import (
build_live_perception_shadow_router,
ensure_live_shadow_token,
)
from k1link.device_plugins.xgrids_k1.viewer.replay import iter_replay_messages
REPORT_SCHEMA = "missioncore.e12-shadow-transport-source-report/v1"
PLUGIN_ID = "nodedc.device.xgrids-lixelkity-k1"
@dataclass(frozen=True, slots=True)
class ReplayEvent:
modality: LiveIngressModality
source_id: str
source_sequence: int
epoch_ns: int
monotonic_ns: int
payload: bytes
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def _camera_events(epoch_root: Path, duration_seconds: float) -> list[ReplayEvent]:
index_path = epoch_root / "index.jsonl"
if not index_path.is_file():
raise RuntimeError("camera index is missing")
events: list[ReplayEvent] = []
start_epoch_ns: int | None = None
end_epoch_ns: int | None = None
with index_path.open("r", encoding="utf-8") as stream:
for line in stream:
record = json.loads(line)
epoch_ns = int(record["host_epoch_ns"])
if start_epoch_ns is None:
start_epoch_ns = epoch_ns
end_epoch_ns = epoch_ns + int(duration_seconds * 1_000_000_000)
assert end_epoch_ns is not None
if epoch_ns > end_epoch_ns:
break
payload_path = epoch_root / str(record["path"])
payload = payload_path.read_bytes()
if len(payload) != int(record["length"]):
raise RuntimeError("camera replay segment length differs from its index")
if hashlib.sha256(payload).hexdigest() != record["sha256"]:
raise RuntimeError("camera replay segment digest differs from its index")
events.append(
ReplayEvent(
modality="camera-frame",
source_id="sensor.camera.right",
source_sequence=int(record["sequence"]),
epoch_ns=epoch_ns,
monotonic_ns=int(record["host_monotonic_ns"]),
payload=payload,
)
)
if not events or start_epoch_ns is None:
raise RuntimeError("camera replay interval is empty")
init_payload = (epoch_root / "init.mp4").read_bytes()
events.insert(
0,
ReplayEvent(
modality="camera-init",
source_id="sensor.camera.right",
source_sequence=0,
epoch_ns=start_epoch_ns - 1,
monotonic_ns=events[0].monotonic_ns - 1,
payload=init_payload,
),
)
return events
def _mqtt_events(
capture_path: Path,
*,
start_epoch_ns: int,
duration_seconds: float,
) -> list[ReplayEvent]:
end_epoch_ns = start_epoch_ns + int(duration_seconds * 1_000_000_000)
events: list[ReplayEvent] = []
for message in iter_replay_messages(capture_path):
if message.received_at_epoch_ns < start_epoch_ns:
continue
if message.received_at_epoch_ns > end_epoch_ns:
break
if message.topic.endswith("/lio_pcl") or message.topic == "RealtimePointcloud":
modality: Literal["lidar", "pose"] = "lidar"
elif message.topic.endswith("/lio_pose") or message.topic == "RealtimePath":
modality = "pose"
else:
continue
events.append(
ReplayEvent(
modality=modality,
source_id=message.topic,
source_sequence=message.sequence,
epoch_ns=message.received_at_epoch_ns,
monotonic_ns=message.received_monotonic_ns or 0,
payload=message.payload,
)
)
return events
def _wait_for(predicate: object, *, timeout_seconds: float, label: str) -> None:
if not callable(predicate):
raise TypeError("wait predicate must be callable")
deadline = time.monotonic() + timeout_seconds
while time.monotonic() < deadline:
if predicate():
return
time.sleep(0.05)
raise RuntimeError(f"timed out waiting for {label}")
def run(args: argparse.Namespace) -> dict[str, object]:
repository_root = Path(args.repository_root).expanduser().resolve()
session_root = Path(args.session_root).expanduser().resolve()
epoch_root = session_root / "media" / "sensor.camera.right" / "epoch-1"
capture_path = session_root / "captures" / "mqtt_live" / "mqtt.raw.k1mqtt"
camera = _camera_events(epoch_root, args.duration_seconds)
mqtt = _mqtt_events(
capture_path,
start_epoch_ns=camera[1].epoch_ns,
duration_seconds=args.duration_seconds,
)
events = sorted((*camera, *mqtt), key=lambda event: (event.epoch_ns, event.modality))
if not mqtt:
raise RuntimeError("selected replay interval contains no LiDAR or pose events")
ingress = LivePerceptionIngress()
_, token = ensure_live_shadow_token(repository_root)
app = FastAPI()
app.include_router(
build_live_perception_shadow_router(
ingress,
PLUGIN_ID,
bearer_token=token,
)
)
server = uvicorn.Server(
uvicorn.Config(
app,
host="127.0.0.1",
port=args.port,
log_level="warning",
access_log=False,
)
)
server_thread = threading.Thread(target=server.run, name="e12-shadow-source", daemon=True)
server_thread.start()
_wait_for(lambda: server.started, timeout_seconds=10.0, label="E12 source server")
session_id = f"e12-ravnoves00-{int(time.time())}"
ingress.begin_session(session_id)
_wait_for(
lambda: ingress.snapshot()["consumer_connected"],
timeout_seconds=args.consumer_timeout_seconds,
label="exclusive shadow worker",
)
started_monotonic = time.monotonic()
source_start_ns = events[0].epoch_ns
published: dict[str, int] = {}
try:
for event in events:
target = started_monotonic + (event.epoch_ns - source_start_ns) / 1_000_000_000
remaining = target - time.monotonic()
if remaining > 0:
time.sleep(remaining)
accepted = ingress.publish(
modality=event.modality,
source_id=event.source_id,
source_sequence=event.source_sequence,
captured_at_epoch_ns=event.epoch_ns,
received_monotonic_ns=event.monotonic_ns,
payload=event.payload,
)
if accepted:
published[event.modality] = published.get(event.modality, 0) + 1
ingress.end_session(session_id)
_wait_for(
lambda: not ingress.snapshot()["consumer_connected"],
timeout_seconds=10.0,
label="shadow worker report completion",
)
finally:
ingress.end_session(session_id)
ingress.close()
server.should_exit = True
server_thread.join(timeout=10.0)
completed_monotonic = time.monotonic()
source_snapshot = ingress.snapshot()
report: dict[str, object] = {
"schema_version": REPORT_SCHEMA,
"state": "completed",
"session_id": session_id,
"source": {
"kind": "recorded-source-paced-near-live",
"session": session_root.name,
"camera_source": "sensor.camera.right",
"camera_epoch": 1,
"duration_seconds": args.duration_seconds,
"mqtt_raw_sha256": _sha256(capture_path),
"camera_index_sha256": _sha256(epoch_root / "index.jsonl"),
},
"events_selected": {
"camera-init": sum(event.modality == "camera-init" for event in events),
"camera-frame": sum(event.modality == "camera-frame" for event in events),
"lidar": sum(event.modality == "lidar" for event in events),
"pose": sum(event.modality == "pose" for event in events),
},
"events_admitted": published,
"wall_seconds": completed_monotonic - started_monotonic,
"ingress": source_snapshot,
"authority": {
"mode": "shadow-diagnostic-only",
"commands_enabled": False,
"navigation_or_safety_accepted": False,
},
}
output_root = repository_root / ".runtime" / "compute-experiments" / "e12"
output_root.mkdir(parents=True, exist_ok=True)
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
temporary = output_root / f".{stamp}-e12-source.json.tmp"
destination = output_root / f"{stamp}-e12-source.json"
encoded = json.dumps(report, indent=2, sort_keys=True).encode("utf-8") + b"\n"
with temporary.open("xb") as stream:
stream.write(encoded)
stream.flush()
os.fsync(stream.fileno())
temporary.replace(destination)
report["report_path"] = str(destination)
return report
def _arguments() -> argparse.Namespace:
root = Path(__file__).resolve().parents[2]
parser = argparse.ArgumentParser()
parser.add_argument("--repository-root", default=str(root))
parser.add_argument(
"--session-root",
default=str(
root
/ ".runtime"
/ "mission-core"
/ "evidence"
/ "sessions"
/ "20260720T065719Z_viewer_live"
),
)
parser.add_argument("--port", type=int, default=8012)
parser.add_argument("--duration-seconds", type=float, default=15.0)
parser.add_argument("--consumer-timeout-seconds", type=float, default=30.0)
return parser.parse_args()
if __name__ == "__main__":
print(json.dumps(run(_arguments()), indent=2, sort_keys=True))

View File

@ -0,0 +1,809 @@
#!/usr/bin/env python3
"""Qualify a bounded replay-as-live world-state loop on immutable LAB E6 data."""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import math
import os
import re
import resource
import shutil
import sys
import threading
import time
from collections import Counter
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from types import ModuleType
from typing import Any
import numpy as np
import numpy.typing as npt
import rerun as rr
from rerun.components import FillMode
import k1link.compute.live_perception as live_perception_module
import k1link.compute.live_replay_qualification as live_validator_module
from k1link.compute import (
TELEMETRY_SCHEMA,
LatestWinsQueue,
WorldStateProjector,
classify_health,
validate_live_replay_qualification_result,
validate_tracked_fusion_qualification_result,
)
from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
map_points_to_lidar,
)
from k1link.device_plugins.xgrids_k1.mqtt.capture import read_capture_clock_origin
PROFILE_SCHEMA = "missioncore.e7-replay-realtime-profile/v1"
RESULT_SCHEMA = "missioncore.e7-live-replay-result/v1"
IDENTITY_SCHEMA = "missioncore.e7-live-replay-identity/v1"
REPORT_SCHEMA = "missioncore.e7-live-replay-run-report/v1"
PIPELINE_ID = "bounded-latest-wins-e6-world-state-replay/v1"
_EXECUTION_ID = re.compile(r"^[a-z0-9][a-z0-9._-]{2,95}$")
@dataclass(frozen=True, slots=True)
class ReplayFrame:
ordinal: int
frame: dict[str, Any]
cloud_map: npt.NDArray[np.float32]
position_map_xyz: tuple[float, float, float] | None
orientation_map_from_lidar_xyzw: tuple[float, float, float, float] | None
@dataclass(frozen=True, slots=True)
class QueueEnvelope:
replay_frame: ReplayFrame
scheduled_monotonic: float
enqueued_monotonic: float
producer_lag_ms: float
def _arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--job", type=Path, required=True)
parser.add_argument("--tracking", type=Path, required=True)
parser.add_argument("--semantic", type=Path, required=True)
parser.add_argument("--e6-result", type=Path, required=True)
parser.add_argument("--session", type=Path, required=True)
parser.add_argument("--profile", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
parser.add_argument("--execution-id", required=True)
return parser.parse_args()
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode()
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def _read_profile(path: Path) -> tuple[dict[str, Any], str]:
resolved = path.resolve(strict=True)
profile = json.loads(resolved.read_text(encoding="utf-8"))
if not isinstance(profile, dict) or profile.get("schema_version") != PROFILE_SCHEMA:
raise RuntimeError("LAB E7 profile schema changed")
selection = profile.get("selection")
replay = profile.get("replay")
health = profile.get("health")
world = profile.get("world_state")
presentation = profile.get("presentation")
acceptance = profile.get("acceptance")
if not all(
isinstance(value, dict)
for value in (selection, replay, health, world, presentation, acceptance)
):
raise RuntimeError("LAB E7 profile sections are invalid")
mode = profile.get("mode")
if mode not in {"pilot", "qualification", "overload-negative-control"}:
raise RuntimeError("LAB E7 mode is invalid")
start = selection.get("start_frame_index")
count = selection.get("frame_count")
capacity = replay.get("queue_capacity")
speed = replay.get("speed")
delay = replay.get("consumer_delay_ms")
stale = health.get("stale_after_ms")
unavailable = health.get("unavailable_after_ms")
if (
not isinstance(start, int)
or start < 0
or not isinstance(count, int)
or count < 2
or not isinstance(capacity, int)
or not 1 <= capacity <= 8
or not isinstance(speed, int | float)
or not 0.1 <= float(speed) <= 10.0
or not isinstance(delay, int | float)
or not 0 <= float(delay) <= 5000
or not isinstance(stale, int | float)
or not isinstance(unavailable, int | float)
or not 0 < float(stale) < float(unavailable)
):
raise RuntimeError("LAB E7 scheduling contract is invalid")
clearance = world.get("clearance")
if not isinstance(clearance, dict):
raise RuntimeError("LAB E7 clearance contract is invalid")
sectors = clearance.get("sector_count")
if not isinstance(sectors, int) or not 12 <= sectors <= 360:
raise RuntimeError("LAB E7 clearance sector count is invalid")
return profile, _sha256(resolved)
def _load_e6_runner() -> ModuleType:
path = Path(__file__).with_name("fuse_e6_tracking_lidar.py").resolve(strict=True)
spec = importlib.util.spec_from_file_location("missioncore_e6_replay_adapter", path)
if spec is None or spec.loader is None:
raise RuntimeError("LAB E6 replay adapter cannot be loaded")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
with path.open(encoding="utf-8") as stream:
for line in stream:
value = json.loads(line)
if not isinstance(value, dict):
raise RuntimeError("LAB E7 input JSONL row is invalid")
rows.append(value)
return rows
def _artifact_path(artifacts: Sequence[Mapping[str, Any]], kind: str, root: Path) -> Path:
matches = [item for item in artifacts if item.get("kind") == kind]
if len(matches) != 1 or not isinstance(matches[0].get("path"), str):
raise RuntimeError(f"LAB E7 upstream artifact is missing: {kind}")
return root / str(matches[0]["path"])
def _prepare_frames(
*,
e6: Any,
session: Path,
selection_start: int,
selection_count: int,
) -> list[ReplayFrame]:
if selection_start + selection_count > e6.frame_count:
raise RuntimeError("LAB E7 selection exceeds the E6 frame range")
runner = _load_e6_runner()
frames_path = _artifact_path(
e6.artifacts,
"tracked-lidar-frame-metadata",
e6.result_root,
)
frame_rows = _read_jsonl(frames_path)
arrays_path = _artifact_path(e6.artifacts, "tracked-lidar-arrays", e6.result_root)
tracking_frames = runner._read_tracking_frames(
e6.tracking.artifact("tracking-frame-metadata").path,
e6.tracking.frame_count,
e6.tracking.source_start_frame_index,
)
origin = read_capture_clock_origin(
session / "captures" / "mqtt_live" / "mqtt.timeline.origin.json"
)
epoch_root = (
e6.job.job_root
/ "input"
/ "camera"
/ e6.job.source_id
/ f"epoch-{e6.job.codec_epoch}"
)
anchors = runner._camera_anchors(
epoch_root / "index.jsonl",
tracking_frames,
origin_monotonic_ns=origin.started_monotonic_ns,
)
temporal = json.loads(
(e6.result_root / "run-report.json").read_text(encoding="utf-8")
)["identity"]["configuration"]["temporal"]
samples = list(
runner._lidar_samples(
session / "captures" / "mqtt_live" / "mqtt.raw.k1mqtt",
anchors,
origin_monotonic_ns=origin.started_monotonic_ns,
maximum_lidar_camera_delta_s=float(
temporal["maximum_lidar_camera_delta_ms"]
)
/ 1000.0,
maximum_pose_point_delta_s=float(temporal["maximum_pose_point_delta_ms"])
/ 1000.0,
)
)
prepared: list[ReplayFrame] = []
with np.load(arrays_path, allow_pickle=False) as arrays:
offsets = arrays["cloud_offsets"]
clouds = arrays["cloud_points"]
for ordinal in range(selection_start, selection_start + selection_count):
frame = frame_rows[ordinal]
sample = samples[ordinal]
if (frame["state"] == "fused") != (sample is not None):
raise RuntimeError("LAB E7 pose binding changed from the accepted E6 result")
start = int(offsets[ordinal])
end = int(offsets[ordinal + 1])
prepared.append(
ReplayFrame(
ordinal=ordinal,
frame=frame,
cloud_map=np.asarray(clouds[start:end], dtype=np.float32).copy(),
position_map_xyz=(
None
if sample is None
else tuple(float(value) for value in sample.pose_frame.position_xyz)
),
orientation_map_from_lidar_xyzw=(
None
if sample is None
else tuple(
float(value)
for value in sample.pose_frame.orientation_xyzw
)
),
)
)
return prepared
def _transform_to_lidar(
points_map: npt.NDArray[np.float32] | npt.NDArray[np.float64],
frame: ReplayFrame,
) -> npt.NDArray[np.float64]:
if frame.position_map_xyz is None or frame.orientation_map_from_lidar_xyzw is None:
return np.empty((0, 3), dtype=np.float64)
return map_points_to_lidar(
np.asarray(points_map, dtype=np.float64),
position_map_xyz=frame.position_map_xyz,
orientation_map_from_lidar_xyzw=frame.orientation_map_from_lidar_xyzw,
)
def _clearance(
points_lidar: npt.NDArray[np.float64],
profile: Mapping[str, Any],
) -> dict[str, Any]:
sector_count = int(profile["sector_count"])
empty = {
"schema": "diagnostic-polar-obstacle-clearance/v1",
"state": "unavailable",
"frame": "k1-lidar",
"front_m": None,
"observed_sector_fraction": 0.0,
"sector_ranges_m": [None] * sector_count,
}
if points_lidar.size == 0:
return empty
finite = np.all(np.isfinite(points_lidar), axis=1)
points = points_lidar[finite]
if points.size == 0:
return empty
ground_z = float(np.percentile(points[:, 2], float(profile["ground_percentile"])))
ranges = np.linalg.norm(points[:, :2], axis=1)
keep = (
(ranges >= float(profile["minimum_range_m"]))
& (ranges <= float(profile["maximum_range_m"]))
& (points[:, 2] >= ground_z + float(profile["minimum_height_above_ground_m"]))
& (points[:, 2] <= ground_z + float(profile["maximum_height_above_ground_m"]))
)
points = points[keep]
ranges = ranges[keep]
if points.size == 0:
return {**empty, "ground_z_estimate_m": ground_z}
angles = np.arctan2(points[:, 1], points[:, 0])
indices = np.floor((angles + math.pi) / (2.0 * math.pi) * sector_count).astype(int)
indices = np.clip(indices, 0, sector_count - 1)
sector_ranges = np.full(sector_count, np.inf, dtype=np.float64)
np.minimum.at(sector_ranges, indices, ranges)
observed = np.isfinite(sector_ranges)
front_half = math.radians(float(profile["front_half_angle_degrees"]))
front = ranges[np.abs(angles) <= front_half]
return {
"schema": "diagnostic-polar-obstacle-clearance/v1",
"state": "observed" if np.any(observed) else "unavailable",
"frame": "k1-lidar",
"ground_z_estimate_m": ground_z,
"front_m": None if front.size == 0 else float(np.min(front)),
"observed_sector_fraction": float(np.mean(observed)),
"sector_ranges_m": [
None if not math.isfinite(value) else float(value) for value in sector_ranges
],
"safety_accepted": False,
}
def _percentile(values: Sequence[float], quantile: float) -> float:
return float(np.percentile(np.asarray(values, dtype=np.float64), quantile))
def _peak_rss_mib() -> float:
value = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
return float(value / (1024 * 1024) if sys.platform == "darwin" else value / 1024)
def _queue_document(queue: LatestWinsQueue[Any]) -> dict[str, Any]:
snapshot = queue.snapshot()
return {
"capacity": snapshot.capacity,
"depth": snapshot.depth,
"maximum_depth": snapshot.maximum_depth,
"published": snapshot.published,
"consumed": snapshot.consumed,
"dropped_overflow": snapshot.dropped_overflow,
"dropped_superseded": snapshot.dropped_superseded,
"closed": snapshot.closed,
}
def _artifact(path: Path, kind: str, media_type: str, schema: str | None = None) -> dict[str, Any]:
value: dict[str, Any] = {
"kind": kind,
"path": path.name,
"media_type": media_type,
"byte_length": path.stat().st_size,
"sha256": _sha256(path),
}
if schema is not None:
value["schema_version"] = schema
return value
def _accepted(profile: Mapping[str, Any], metrics: Mapping[str, Any]) -> tuple[bool, list[str]]:
acceptance = profile["acceptance"]
reasons: list[str] = []
if metrics["queue"]["maximum_depth"] > int(acceptance["maximum_queue_depth"]):
reasons.append("queue-depth")
if metrics["process_peak_rss_mib"] > float(acceptance["maximum_process_rss_mib"]):
reasons.append("process-rss")
if profile["mode"] == "overload-negative-control":
if metrics["drop_fraction"] < float(acceptance["minimum_drop_fraction"]):
reasons.append("negative-control-did-not-overload")
else:
if metrics["delivery_rate_hz"] < float(acceptance["minimum_delivery_rate_hz"]):
reasons.append("delivery-rate")
if metrics["end_to_end_latency_ms"]["p95"] > float(
acceptance["maximum_end_to_end_p95_ms"]
):
reasons.append("latency-p95")
if metrics["drop_fraction"] > float(acceptance["maximum_drop_fraction"]):
reasons.append("drop-fraction")
return not reasons, reasons
def main() -> int:
args = _arguments()
if _EXECUTION_ID.fullmatch(args.execution_id) is None:
raise RuntimeError("LAB E7 execution ID is invalid")
profile, profile_sha256 = _read_profile(args.profile)
session = args.session.resolve(strict=True)
preparation_started = time.perf_counter()
e6 = validate_tracked_fusion_qualification_result(
args.job,
args.tracking,
args.semantic,
args.e6_result,
)
if session.name != e6.job.session_id:
raise RuntimeError("LAB E7 session does not match the E6 result")
selection = profile["selection"]
replay_frames = _prepare_frames(
e6=e6,
session=session,
selection_start=int(selection["start_frame_index"]),
selection_count=int(selection["frame_count"]),
)
preparation_wall = time.perf_counter() - preparation_started
identity = {
"schema_version": IDENTITY_SCHEMA,
"execution_id": args.execution_id,
"pipeline": PIPELINE_ID,
"e6_result_id": e6.result_id,
"job_id": e6.job.job_id,
"input_sha256": e6.job.input_sha256,
"session_id": e6.job.session_id,
"source_id": e6.job.source_id,
"calibration_sha256": e6.semantic.calibration_sha256,
"camera_slot": e6.semantic.calibration_slot,
"selection": selection,
"configuration": profile,
"profile_sha256": profile_sha256,
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
"runtime_contract_sha256": _sha256(
Path(live_perception_module.__file__).resolve(strict=True)
),
"result_validator_sha256": _sha256(
Path(live_validator_module.__file__).resolve(strict=True)
),
"e6_adapter_sha256": _sha256(
Path(__file__).with_name("fuse_e6_tracking_lidar.py").resolve(strict=True)
),
}
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
result_id = f"e7-live-replay-{identity_sha256}"
parent = args.output_root.resolve()
parent.mkdir(mode=0o700, parents=True, exist_ok=True)
output = parent / result_id
if output.exists():
print(json.dumps({"result_id": result_id, "output": str(output), "reused": True}))
return 0
staging = parent / f".{result_id}.{os.getpid()}.incomplete"
staging.mkdir(mode=0o700, exist_ok=False)
world_path = staging / "world-state.jsonl"
telemetry_path = staging / "telemetry.jsonl"
rrd_path = staging / "world-state.rrd"
report_path = staging / "run-report.json"
queue = LatestWinsQueue[QueueEnvelope](int(profile["replay"]["queue_capacity"]))
producer_error: list[BaseException] = []
recording: rr.RecordingStream | None = None
try:
recording = rr.RecordingStream("nodedc_mission_core_e7", recording_id=result_id)
recording.set_sinks(rr.FileSink(rrd_path, write_footer=True))
recording.log("/world", rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True)
recording.log(
"/contract",
rr.TextDocument(
"LAB E7 replay-as-live: bounded latest-wins derived queue, explicit age and "
"degradation, E6 diagnostic objects, no vehicle-control authority."
),
static=True,
)
replay_speed = float(profile["replay"]["speed"])
first_session = float(replay_frames[0].frame["session_seconds"])
replay_started = time.perf_counter() + 0.25
def produce() -> None:
try:
for replay_frame in replay_frames:
offset = (
float(replay_frame.frame["session_seconds"]) - first_session
) / replay_speed
deadline = replay_started + offset
remaining = deadline - time.perf_counter()
if remaining > 0:
time.sleep(remaining)
enqueued = time.perf_counter()
queue.publish(
QueueEnvelope(
replay_frame=replay_frame,
scheduled_monotonic=deadline,
enqueued_monotonic=enqueued,
producer_lag_ms=max(0.0, (enqueued - deadline) * 1000.0),
)
)
except BaseException as exc: # noqa: BLE001 - forwarded to main thread
producer_error.append(exc)
finally:
queue.close()
producer = threading.Thread(target=produce, name="e7-replay-producer", daemon=True)
producer.start()
projector = WorldStateProjector(
velocity_history_limit_s=float(
profile["world_state"]["velocity_history_limit_s"]
)
)
health_counts: Counter[str] = Counter()
end_to_end: list[float] = []
queue_wait: list[float] = []
processing: list[float] = []
rerun_latency: list[float] = []
producer_lag: list[float] = []
published_indices: list[int] = []
consumer_delay = float(profile["replay"]["consumer_delay_ms"]) / 1000.0
with world_path.open("w", encoding="utf-8") as world_stream, telemetry_path.open(
"w", encoding="utf-8"
) as telemetry_stream:
while True:
envelope = queue.take_next(timeout=0.5)
if envelope is None:
if queue.snapshot().closed:
break
continue
dequeued = time.perf_counter()
if consumer_delay > 0:
time.sleep(consumer_delay)
stage_started = time.perf_counter()
replay_frame = envelope.replay_frame
cloud_lidar = _transform_to_lidar(replay_frame.cloud_map, replay_frame)
accepted = [
item
for item in replay_frame.frame.get("objects", [])
if str(item.get("cuboid_status", "")).startswith("accepted-")
]
centers_map = np.asarray(
[item["cuboid_center_map"] for item in accepted], dtype=np.float64
).reshape((-1, 3))
centers_lidar = _transform_to_lidar(centers_map, replay_frame)
lidar_positions = {
int(item["track_id"]): centers_lidar[index]
for index, item in enumerate(accepted)
}
world = projector.project(
frame=replay_frame.frame,
lidar_positions=lidar_positions,
clearance=_clearance(
cloud_lidar,
profile["world_state"]["clearance"],
),
)
projection_finished = time.perf_counter()
relative_seconds = (
float(replay_frame.frame["session_seconds"]) - first_session
)
recording.set_time(
"replay_time",
duration=np.timedelta64(round(relative_seconds * 1e9), "ns"),
)
if bool(profile["presentation"]["log_point_cloud"]) and replay_frame.cloud_map.size:
recording.log(
"/world/lidar",
rr.Points3D(
replay_frame.cloud_map,
colors=np.tile(
np.asarray([[145, 145, 145]], dtype=np.uint8),
(replay_frame.cloud_map.shape[0], 1),
),
radii=rr.Radius.ui_points(
float(profile["presentation"]["point_radius_ui"])
),
),
)
elif not replay_frame.cloud_map.size:
recording.log("/world/lidar", rr.Clear(recursive=False))
if accepted:
colors = []
labels = []
for item in accepted:
group = str(item["association_group"])
colors.append(
{
"person": [255, 70, 170, 88],
"vehicle": [130, 90, 255, 88],
"bicycle": [80, 220, 255, 88],
"motorcycle": [255, 180, 65, 88],
}.get(group, [220, 220, 220, 88])
)
labels.append(
f"{group} #{item['track_id']} · "
f"{float(item['distance_smoothed_m']):.1f} m"
)
recording.log(
"/world/perception/boxes3d",
rr.Boxes3D(
centers=centers_map.astype(np.float32),
half_sizes=np.asarray(
[item["cuboid_half_size"] for item in accepted],
dtype=np.float32,
),
quaternions=np.asarray(
[item["cuboid_quaternion_xyzw"] for item in accepted],
dtype=np.float32,
),
colors=np.asarray(colors, dtype=np.uint8),
labels=labels,
fill_mode=FillMode.Solid,
show_labels=True,
),
)
else:
recording.log("/world/perception/boxes3d", rr.Clear(recursive=False))
rerun_finished = time.perf_counter()
age_ms = (rerun_finished - envelope.scheduled_monotonic) * 1000.0
health, reasons = classify_health(
source_available=True,
fusion_state=str(replay_frame.frame["state"]),
result_age_ms=age_ms,
stale_after_ms=float(profile["health"]["stale_after_ms"]),
unavailable_after_ms=float(profile["health"]["unavailable_after_ms"]),
)
health_counts[health] += 1
world["delivery"] = {
"health": health,
"reasons": list(reasons),
"result_age_ms": age_ms,
"queue_policy": "bounded-latest-wins",
}
world_stream.write(json.dumps(world, ensure_ascii=False, allow_nan=False) + "\n")
published = time.perf_counter()
latency_ms = (published - envelope.scheduled_monotonic) * 1000.0
recording.log("/telemetry/end_to_end_latency_ms", rr.Scalars([latency_ms]))
recording.log("/telemetry/queue_depth", rr.Scalars([queue.snapshot().depth]))
recording.log(
"/telemetry/health_code",
rr.Scalars(
[{"healthy": 0, "degraded": 1, "stale": 2, "unavailable": 3}[health]]
),
)
telemetry = {
"schema_version": TELEMETRY_SCHEMA,
"frame_index": int(replay_frame.frame["frame_index"]),
"source_frame_index": int(replay_frame.frame["source_frame_index"]),
"session_seconds": float(replay_frame.frame["session_seconds"]),
"producer_lag_ms": envelope.producer_lag_ms,
"queue_wait_ms": (dequeued - envelope.enqueued_monotonic) * 1000.0,
"world_projection_ms": (projection_finished - stage_started) * 1000.0,
"rerun_publish_ms": (rerun_finished - projection_finished) * 1000.0,
"end_to_end_latency_ms": latency_ms,
"health": health,
"queue": _queue_document(queue),
}
telemetry_stream.write(
json.dumps(telemetry, ensure_ascii=False, allow_nan=False) + "\n"
)
end_to_end.append(latency_ms)
queue_wait.append(telemetry["queue_wait_ms"])
processing.append(telemetry["world_projection_ms"])
rerun_latency.append(telemetry["rerun_publish_ms"])
producer_lag.append(envelope.producer_lag_ms)
published_indices.append(int(replay_frame.frame["frame_index"]))
producer.join(timeout=5)
if producer.is_alive():
raise RuntimeError("LAB E7 replay producer did not stop")
if producer_error:
raise RuntimeError("LAB E7 replay producer failed") from producer_error[0]
if recording is not None:
recording.flush()
recording.disconnect()
recording = None
finished = time.perf_counter()
queue_metrics = queue.snapshot()
replay_span = (
float(replay_frames[-1].frame["session_seconds"])
- float(replay_frames[0].frame["session_seconds"])
) / replay_speed
replay_wall = finished - replay_started
attempted = len(replay_frames)
processed = len(published_indices)
metrics = {
"frames_attempted": attempted,
"frames_processed": processed,
"frames_dropped": queue_metrics.dropped_total,
"drop_fraction": queue_metrics.dropped_total / attempted,
"delivery_rate_hz": processed / replay_span,
"replay_span_seconds": replay_span,
"replay_wall_seconds": replay_wall,
"preparation_wall_seconds": preparation_wall,
"process_peak_rss_mib": _peak_rss_mib(),
"health_counts": dict(health_counts),
"queue": {
"capacity": queue_metrics.capacity,
"maximum_depth": queue_metrics.maximum_depth,
"final_depth": queue_metrics.depth,
"published": queue_metrics.published,
"consumed": queue_metrics.consumed,
"dropped_overflow": queue_metrics.dropped_overflow,
"dropped_superseded": queue_metrics.dropped_superseded,
},
"end_to_end_latency_ms": {
"mean": float(np.mean(end_to_end)),
"p50": _percentile(end_to_end, 50),
"p95": _percentile(end_to_end, 95),
"max": max(end_to_end),
},
"queue_wait_ms": {
"mean": float(np.mean(queue_wait)),
"p95": _percentile(queue_wait, 95),
"max": max(queue_wait),
},
"world_projection_ms": {
"mean": float(np.mean(processing)),
"p95": _percentile(processing, 95),
"max": max(processing),
},
"rerun_publish_ms": {
"mean": float(np.mean(rerun_latency)),
"p95": _percentile(rerun_latency, 95),
"max": max(rerun_latency),
},
"producer_lag_ms": {
"mean": float(np.mean(producer_lag)),
"p95": _percentile(producer_lag, 95),
"max": max(producer_lag),
},
"published_frame_index_start": min(published_indices),
"published_frame_index_end": max(published_indices),
}
accepted, rejection_reasons = _accepted(profile, metrics)
report = {
"schema_version": REPORT_SCHEMA,
"result_id": result_id,
"created_at_utc": datetime.now(UTC).isoformat(timespec="milliseconds").replace(
"+00:00", "Z"
),
"identity": identity,
"acceptance": {
"accepted": accepted,
"mode": profile["mode"],
"rejection_reasons": rejection_reasons,
"navigation_or_safety_accepted": False,
"scope": "replay-scheduler-and-world-state-not-live-inference",
},
"metrics": metrics,
}
report_path.write_text(
json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
artifacts = [
_artifact(
world_path,
"live-world-state",
"application/x-ndjson",
"missioncore.live-perception-world-state/v1",
),
_artifact(
telemetry_path,
"live-telemetry",
"application/x-ndjson",
TELEMETRY_SCHEMA,
),
_artifact(rrd_path, "live-rerun", "application/vnd.rerun.rrd"),
_artifact(report_path, "live-run-report", "application/json", REPORT_SCHEMA),
]
result = {
"schema_version": RESULT_SCHEMA,
"result_id": result_id,
"identity_sha256": identity_sha256,
"identity": identity,
"ground_truth": False,
"publication_scope": "replay-qualification-only",
"acceptance_state": "accepted" if accepted else "rejected",
"metrics": metrics,
"artifacts": artifacts,
}
(staging / "result.json").write_text(
json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
staging.rename(output)
validated = validate_live_replay_qualification_result(
args.job,
args.tracking,
args.semantic,
args.e6_result,
output,
)
if validated.result_id != result_id or validated.accepted != accepted:
raise RuntimeError("LAB E7 published result failed identity validation")
print(
json.dumps(
{
"result_id": result_id,
"output": str(output),
"accepted": accepted,
"metrics": metrics,
},
ensure_ascii=False,
)
)
return 0 if accepted else 2
except BaseException:
if recording is not None:
recording.disconnect()
queue.close()
shutil.rmtree(staging, ignore_errors=True)
raise
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,153 @@
[CmdletBinding()]
param(
[ValidateRange(1, 1000)] [int]$FreeGiBFloor = 360,
[string]$RuntimeRoot = "D:\NDC_MISSIONCORE\runtime",
[string]$ContainerImage = "nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794",
[string]$RuntimeName = "perception-e15-media-pyav180-lz445-v1"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-LastExitCode([string]$Operation) {
if ($LASTEXITCODE -ne 0) { throw "$Operation failed with exit code $LASTEXITCODE" }
}
function Resolve-DDirectory([string]$Path, [string]$Label) {
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
$root = [IO.Path]::GetPathRoot($item.FullName).TrimEnd("\")
if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $root -ine "D:") {
throw "$Label must be a real D: directory"
}
return $item.FullName
}
function Convert-ToDockerPath([string]$Path) { return $Path.Replace("\", "/") }
function Assert-FreeSpace([string]$Phase) {
$free = [int64](Get-PSDrive -Name D).Free
$floor = [int64]$FreeGiBFloor * 1GB
Write-Host (
"DISK_GUARD PHASE={0} DRIVE=D FREE_BYTES={1} FREE_GIB={2} FLOOR_GIB={3}" -f
$Phase, $free, [math]::Round($free / 1GB, 3), $FreeGiBFloor
)
if ($free -lt ($floor + 2GB)) {
throw "D: lacks the guarded LAB E15 media-runtime reserve during $Phase"
}
return $free
}
function Get-PayloadDigest([string]$Root) {
$resolved = Resolve-DDirectory $Root "E15 media runtime"
$lines = @(
Get-ChildItem -LiteralPath $resolved -Recurse -File -Force |
Where-Object { $_.Name -ne "manifest.json" } |
Sort-Object FullName |
ForEach-Object {
$relative = $_.FullName.Substring($resolved.Length).TrimStart("\").Replace("\", "/")
$hash = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
"{0}`t{1}`t{2}" -f $relative, $_.Length, $hash
}
)
if ($lines.Count -lt 4) { throw "E15 media runtime payload is incomplete" }
$bytes = [Text.Encoding]::UTF8.GetBytes(($lines -join "`n") + "`n")
$hasher = [Security.Cryptography.SHA256]::Create()
try {
return ([BitConverter]::ToString($hasher.ComputeHash($bytes))).Replace("-", "").ToLowerInvariant()
}
finally { $hasher.Dispose() }
}
function Assert-Runtime([string]$Path) {
$root = Resolve-DDirectory $Path "E15 media runtime"
$manifestPath = Join-Path $root "manifest.json"
if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
throw "E15 media runtime manifest is missing"
}
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
if (
$manifest.schema_version -ne "missioncore.e15-media-runtime/v1" -or
$manifest.runtime_name -ne $RuntimeName -or
$manifest.container_image -ne $ContainerImage -or
$manifest.packages.av -ne "18.0.0" -or
$manifest.packages.lz4 -ne "4.4.5" -or
$manifest.payload_sha256 -ne (Get-PayloadDigest $root)
) { throw "E15 media runtime identity changed" }
$dockerRoot = Convert-ToDockerPath $root
$verifyCode = "import av,lz4.version;print(av.__version__);print(lz4.version.version)"
$verifyOutput = @(& docker run --rm --network none --read-only `
--security-opt "no-new-privileges:true" --cap-drop ALL --pids-limit 64 `
--tmpfs "/tmp:rw,noexec,nosuid,size=64m" `
-e "PYTHONPATH=/opt/media" -e "PYTHONDONTWRITEBYTECODE=1" `
-v ("{0}:/opt/media:ro" -f $dockerRoot) `
--entrypoint python3 $ContainerImage -c $verifyCode)
Assert-LastExitCode "E15 media runtime verification"
if ($verifyOutput.Count -ne 2 -or $verifyOutput[0] -ne "18.0.0" -or $verifyOutput[1] -ne "4.4.5") {
throw "E15 media runtime package versions changed"
}
Write-Host "MEDIA_RUNTIME_OK pyav=18.0.0 lz4=4.4.5"
return $root
}
$runtime = Resolve-DDirectory $RuntimeRoot "Runtime root"
$derived = Resolve-DDirectory (Join-Path $runtime "derived") "Runtime derived root"
$destination = Join-Path $derived $RuntimeName
$freeBefore = Assert-FreeSpace "media-runtime-preflight"
& docker image inspect $ContainerImage *> $null
Assert-LastExitCode "Pinned container image inspection"
if (Test-Path -LiteralPath $destination) {
$resolved = Assert-Runtime $destination
Write-Output "STATE=existing-verified"
Write-Output ("MEDIA_RUNTIME_ROOT={0}" -f $resolved)
Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBefore)
Write-Output ("DISK_FREE_BYTES_AFTER={0}" -f ([int64](Get-PSDrive -Name D).Free))
return
}
$token = [Guid]::NewGuid().ToString("N")
$staging = Join-Path $derived (".{0}-{1}.tmp" -f $RuntimeName, $token)
$null = New-Item -ItemType Directory -Path $staging
$completed = $false
try {
$dockerStaging = Convert-ToDockerPath $staging
Write-Output "PHASE=media-runtime-install-start"
& docker run --rm --network bridge --read-only `
--security-opt "no-new-privileges:true" --cap-drop ALL --pids-limit 128 `
--tmpfs "/tmp:rw,nosuid,size=1g" `
-e "PIP_DISABLE_PIP_VERSION_CHECK=1" -e "PYTHONDONTWRITEBYTECODE=1" `
-v ("{0}:/target:rw" -f $dockerStaging) `
--entrypoint python3 $ContainerImage -m pip install `
--no-cache-dir --only-binary ":all:" --target /target `
"av==18.0.0" "lz4==4.4.5"
Assert-LastExitCode "E15 media runtime installation"
$payloadSha256 = Get-PayloadDigest $staging
$manifest = [ordered]@{
schema_version = "missioncore.e15-media-runtime/v1"
runtime_name = $RuntimeName
created_at_utc = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
container_image = $ContainerImage
packages = [ordered]@{ av = "18.0.0"; lz4 = "4.4.5" }
payload_sha256 = $payloadSha256
storage_scope = "D-only-immutable-runtime"
}
$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $staging "manifest.json") -Encoding utf8
$null = Assert-Runtime $staging
Move-Item -LiteralPath $staging -Destination $destination
$completed = $true
$resolved = Assert-Runtime $destination
$freeAfter = Assert-FreeSpace "media-runtime-published"
Write-Output "STATE=created-verified"
Write-Output ("MEDIA_RUNTIME_ROOT={0}" -f $resolved)
Write-Output ("PAYLOAD_SHA256={0}" -f $payloadSha256)
Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBefore)
Write-Output ("DISK_FREE_BYTES_AFTER={0}" -f $freeAfter)
}
finally {
if (-not $completed -and (Test-Path -LiteralPath $staging)) {
Remove-Item -LiteralPath $staging -Recurse -Force
}
}

View File

@ -0,0 +1,313 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)] [string]$JobRoot,
[Parameter(Mandatory = $true)] [string]$RunnerPath,
[Parameter(Mandatory = $true)] [string]$ProfilePath,
[Parameter(Mandatory = $true)] [string]$DetectorProfilePath,
[Parameter(Mandatory = $true)] [string]$SemanticProfilePath,
[Parameter(Mandatory = $true)] [string]$ValidFovRoot,
[Parameter(Mandatory = $true)] [string]$LidarPackRoot,
[ValidateRange(0, 1000000)] [int]$StartFrame = 1000,
[ValidateRange(0, 1000000)] [int]$EndFrame = 1600,
[ValidateRange(1, 1000)] [int]$FreeGiBFloor = 360,
[string]$RuntimeRoot = "D:\NDC_MISSIONCORE\runtime",
[string]$ModelRoot = "D:\NDC_MISSIONCORE\runtime\models\yolox_s",
[string]$TritonContainer = "mission-core-triton",
[string]$ContainerImage = "nvcr.io/nvidia/tritonserver:26.06-py3"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-LastExitCode([string]$Operation) {
if ($LASTEXITCODE -ne 0) { throw "$Operation failed with exit code $LASTEXITCODE" }
}
function Resolve-DDirectory([string]$Path, [string]$Label) {
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
$root = [IO.Path]::GetPathRoot($item.FullName).TrimEnd("\")
if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $root -ine "D:") {
throw "$Label must be a real D: directory"
}
return $item.FullName
}
function Resolve-DFile([string]$Path, [string]$Label) {
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
$root = [IO.Path]::GetPathRoot($item.FullName).TrimEnd("\")
if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $root -ine "D:") {
throw "$Label must be a regular D: file"
}
return $item.FullName
}
function Convert-ToDockerPath([string]$Path) { return $Path.Replace("\", "/") }
function Get-DFreeBytes { return [int64](Get-PSDrive -Name D).Free }
function Assert-FreeSpace([string]$Phase, [int64]$RequiredAdditionalBytes = 0) {
$free = Get-DFreeBytes
$floor = [int64]$FreeGiBFloor * 1GB
Write-Host (
"DISK_GUARD PHASE={0} DRIVE=D FREE_BYTES={1} FREE_GIB={2} FLOOR_GIB={3} REQUIRED_ADDITIONAL_GIB={4}" -f
$Phase, $free, [math]::Round($free / 1GB, 3), $FreeGiBFloor,
[math]::Round($RequiredAdditionalBytes / 1GB, 3)
)
if ($free -lt ($floor + $RequiredAdditionalBytes)) {
throw "D: lacks the guarded LAB E10 reserve during $Phase"
}
return $free
}
function Test-TritonModelReady {
try {
$response = Invoke-WebRequest -UseBasicParsing -TimeoutSec 10 `
-Uri "http://127.0.0.1:8000/v2/models/yolox_s/ready"
return $response.StatusCode -eq 200
}
catch { return $false }
}
$jobDirectory = Resolve-DDirectory $JobRoot "Job root"
$runner = Resolve-DFile $RunnerPath "LAB E10 runner"
$profile = Resolve-DFile $ProfilePath "LAB E10 profile"
$detectorProfile = Resolve-DFile $DetectorProfilePath "Detector profile"
$semanticProfile = Resolve-DFile $SemanticProfilePath "Semantic profile"
$validFov = Resolve-DDirectory $ValidFovRoot "Valid-FOV root"
$lidarPack = Resolve-DDirectory $LidarPackRoot "LiDAR replay pack"
$runtime = Resolve-DDirectory $RuntimeRoot "Runtime root"
$model = Resolve-DDirectory $ModelRoot "YOLOX model root"
$runnerRoot = Split-Path $runner -Parent
foreach ($path in @($profile, $detectorProfile, $semanticProfile)) {
if ((Split-Path $path -Parent) -ne $runnerRoot) { throw "Runner and profiles must share one mount" }
}
foreach ($dependency in @(
"e10_fusion_runtime.py",
"run_e9_multirate_perception.py",
"run_e8_realtime_tracking.py",
"run_e5_instance_tracking.py",
"run_e4_full_session_segmentation.py",
"run_recorded_perception_epoch.py",
"run_e3_rectified_segmentation.py",
"run_evaluation_prelabels.py"
)) {
$null = Resolve-DFile (Join-Path $runnerRoot $dependency) "LAB E10 dependency"
}
$job = Get-Content -LiteralPath (Join-Path $jobDirectory "job.json") -Raw | ConvertFrom-Json
if ($job.schema_version -ne "missioncore.compute-job/v1" -or $job.job_id -ne (Split-Path $jobDirectory -Leaf)) {
throw "Compute job manifest is incompatible"
}
$sourceId = [string]$job.input.source_id
$epoch = [int]$job.input.codec_epoch
$fullFrameCount = [int]$job.input.segment_count
$clipFrameCount = $EndFrame - $StartFrame + 1
$timelineStart = [double]$job.input.timeline.start_seconds
if ($sourceId -ne "sensor.camera.right" -or $StartFrame -lt 0 -or $EndFrame -lt $StartFrame -or $EndFrame -ge $fullFrameCount -or $clipFrameCount -lt 2) {
throw "LAB E10 clip escapes the camera job"
}
$lidarManifest = Get-Content -LiteralPath (Join-Path $lidarPack "manifest.json") -Raw | ConvertFrom-Json
if (
$lidarManifest.schema_version -ne "missioncore.e10-lidar-replay-pack/v1" -or
$lidarManifest.identity.job_id -ne $job.job_id -or
[int]$lidarManifest.identity.source_start_frame_index -ne $StartFrame -or
[int]$lidarManifest.identity.source_end_frame_index -ne $EndFrame -or
[int]$lidarManifest.identity.frame_count -ne $clipFrameCount
) { throw "LAB E10 LiDAR pack differs from the selected camera clip" }
Write-Output ("PHASE=inputs-validated JOB={0} CLIP={1}-{2} FRAMES={3} LIDAR_PACK={4}" -f $job.job_id, $StartFrame, $EndFrame, $clipFrameCount, $lidarManifest.pack_id)
$epochRoot = Resolve-DDirectory (Join-Path $jobDirectory ("input\camera\{0}\epoch-{1}" -f $sourceId, $epoch)) "Camera epoch"
$initPath = Resolve-DFile (Join-Path $epochRoot "init.mp4") "Camera init"
$segmentsRoot = Resolve-DDirectory (Join-Path $epochRoot "segments") "Camera segments"
$derivedRoot = Join-Path $runtime "derived"
$tmpRoot = Join-Path $runtime "tmp"
$cacheRoot = Resolve-DDirectory (Join-Path $runtime "cache\perception-e3-models-v1") "EoMT cache"
$e3Environment = Resolve-DDirectory (Join-Path $derivedRoot "perception-e3-opencv413092-v1") "E3 environment"
$torchEnvironment = Resolve-DDirectory (Join-Path $derivedRoot "perception-p0-env-v1") "Torch environment"
$transformersEnvironment = Resolve-DDirectory (Join-Path $derivedRoot "perception-p0-transformers4576-v1") "Transformers environment"
$pixelWorkingSet = [int64]$clipFrameCount * 800 * 600 * 3
$partialStreamReserve = [int64][math]::Ceiling(([int64]$job.input.byte_length * ($EndFrame + 1) / $fullFrameCount) * 1.2)
$workingSetReserve = [int64][math]::Ceiling($pixelWorkingSet * 1.4) + $partialStreamReserve + 3GB
$freeBytesBefore = Assert-FreeSpace "preflight" $workingSetReserve
& docker image inspect $ContainerImage *> $null
Assert-LastExitCode "Existing container image inspection"
$tritonState = docker inspect --format "{{.State.Running}}" $TritonContainer
Assert-LastExitCode "Triton container inspection"
if ($tritonState.Trim().ToLowerInvariant() -ne "true") { throw "LAB E10 requires the existing Triton container" }
$runnerName = Split-Path $runner -Leaf
$profileName = Split-Path $profile -Leaf
$detectorProfileName = Split-Path $detectorProfile -Leaf
$semanticProfileName = Split-Path $semanticProfile -Leaf
$lidarMount = "/" + (Split-Path $lidarPack -Leaf)
$orchestratorSha256 = (Get-FileHash -LiteralPath $PSCommandPath -Algorithm SHA256).Hash.ToLowerInvariant()
$commonMounts = @(
"-e", "PYTHONPATH=/runner:/opt/transformers:/opt/env",
"-e", "TORCH_HOME=/cache/torch",
"-e", "HF_HOME=/cache/huggingface",
"-e", "HF_HUB_OFFLINE=1",
"-e", "TRANSFORMERS_OFFLINE=1",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"-v", ((Convert-ToDockerPath $lidarPack) + (":{0}:ro" -f $lidarMount)),
"-v", ((Convert-ToDockerPath $model) + ":/model:ro"),
"-v", ((Convert-ToDockerPath $cacheRoot) + ":/cache:ro"),
"-v", ((Convert-ToDockerPath $e3Environment) + ":/environment:ro"),
"-v", ((Convert-ToDockerPath $torchEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $transformersEnvironment) + ":/opt/transformers:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro")
)
$commonRunnerArgs = @(
"--job", "/job",
"--profile", ("/runner/{0}" -f $profileName),
"--detector-profile", ("/runner/{0}" -f $detectorProfileName),
"--semantic-profile", ("/runner/{0}" -f $semanticProfileName),
"--valid-fov-root", "/valid-fov",
"--model-root", "/model",
"--cache", "/cache",
"--environment", "/environment",
"--lidar-pack", $lidarMount
)
$preflightArgs = @(
"run", "--rm", "--gpus", "all", "--network", "none", "--read-only",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "512", "--shm-size", "4g",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=2g"
) + $commonMounts + @(
"--entrypoint", "python3", $ContainerImage,
("/runner/{0}" -f $runnerName), "preflight"
) + $commonRunnerArgs
Write-Output "PHASE=e10-preflight-start"
& docker @preflightArgs
Assert-LastExitCode "LAB E10 preflight"
Write-Output "PHASE=e10-preflight-complete"
$modelWasReady = Test-TritonModelReady
$loadedByRun = $false
if (-not $modelWasReady) {
Invoke-WebRequest -UseBasicParsing -TimeoutSec 60 -Uri "http://127.0.0.1:8000/v2/repository/models/yolox_s/load" -Method Post -ContentType "application/json" -Body "{}" *> $null
$loadedByRun = $true
$deadline = [DateTime]::UtcNow.AddSeconds(60)
while (-not (Test-TritonModelReady)) {
if ([DateTime]::UtcNow -ge $deadline) { throw "YOLOX-S did not become ready" }
Start-Sleep -Milliseconds 500
}
}
Write-Output ("PHASE=e10-model-ready LOADED_BY_RUN={0}" -f $loadedByRun)
$token = [Guid]::NewGuid().ToString("N")
$workRoot = Join-Path $tmpRoot ("{0}-e10-{1}" -f $job.job_id, $token)
$framesRoot = Join-Path $workRoot "frames"
$streamPath = Join-Path $workRoot "camera.mp4"
$ptsPath = Join-Path $workRoot "pts.json"
$timelinePath = Join-Path $workRoot "timeline.jsonl"
$publishRoot = Join-Path $derivedRoot (".{0}-e10-{1}.publish" -f $job.job_id, $token)
$stagingRoot = Join-Path $publishRoot "output"
$null = New-Item -ItemType Directory -Path $framesRoot
$null = New-Item -ItemType Directory -Path $publishRoot
$completed = $false
$totalWatch = [Diagnostics.Stopwatch]::StartNew()
try {
$stream = [IO.File]::Open($streamPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
try {
$input = [IO.File]::OpenRead($initPath)
try { $input.CopyTo($stream) } finally { $input.Dispose() }
for ($sequence = 1; $sequence -le ($EndFrame + 1); $sequence++) {
$path = Resolve-DFile (Join-Path $segmentsRoot ("{0}.m4s" -f $sequence)) "Camera segment"
$input = [IO.File]::OpenRead($path)
try { $input.CopyTo($stream) } finally { $input.Dispose() }
}
$stream.Flush($true)
}
finally { $stream.Dispose() }
$null = Assert-FreeSpace "post-stream-reconstruction"
$extractWatch = [Diagnostics.Stopwatch]::StartNew()
$selectFilter = "select='between(n,{0},{1})'" -f $StartFrame, $EndFrame
& ffmpeg -hide_banner -loglevel fatal -i $streamPath -map 0:v:0 -vf $selectFilter -fps_mode passthrough (Join-Path $framesRoot "frame-%06d.png")
Assert-LastExitCode "LAB E10 camera extraction"
& ffprobe -v error -select_streams v:0 -show_entries frame=best_effort_timestamp_time -of json $streamPath | Set-Content -LiteralPath $ptsPath -Encoding utf8
Assert-LastExitCode "LAB E10 timestamp probe"
$decoded = @(Get-ChildItem -LiteralPath $framesRoot -File -Filter "frame-*.png")
$pts = @((Get-Content -LiteralPath $ptsPath -Raw | ConvertFrom-Json).frames)
if ($decoded.Count -ne $clipFrameCount -or $pts.Count -ne ($EndFrame + 1)) { throw "LAB E10 decoded frame count changed" }
$firstEpochSeconds = [double]::Parse(([string]$pts[0].best_effort_timestamp_time).Trim(), [Globalization.CultureInfo]::InvariantCulture)
$writer = [IO.StreamWriter]::new($timelinePath, $false, [Text.UTF8Encoding]::new($false))
try {
for ($local = 0; $local -lt $clipFrameCount; $local++) {
$source = $StartFrame + $local
$epochSeconds = [double]::Parse(([string]$pts[$source].best_effort_timestamp_time).Trim(), [Globalization.CultureInfo]::InvariantCulture) - $firstEpochSeconds
$row = [ordered]@{
frame_index = $local
sequence = $local + 1
source_frame_index = $source
source_sequence = $source + 1
epoch_seconds = $epochSeconds
session_seconds = $timelineStart + $epochSeconds
}
$writer.WriteLine(($row | ConvertTo-Json -Compress))
}
$writer.Flush()
}
finally { $writer.Dispose() }
$extractWatch.Stop()
$freePostExtract = Assert-FreeSpace "post-frame-extraction"
$runArgs = @(
"run", "--rm", "--gpus", "all", "--network", ("container:{0}" -f $TritonContainer),
"--read-only", "--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "512", "--shm-size", "4g",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=2g"
) + $commonMounts + @(
"-v", ((Convert-ToDockerPath $framesRoot) + ":/frames:ro"),
"-v", ((Convert-ToDockerPath $workRoot) + ":/work:ro"),
"-v", ((Convert-ToDockerPath $publishRoot) + ":/publish:rw"),
"--entrypoint", "python3", $ContainerImage,
("/runner/{0}" -f $runnerName), "run"
) + $commonRunnerArgs + @(
"--frames", "/frames",
"--timeline", "/work/timeline.jsonl",
"--output", "/publish/output",
"--triton-url", "http://127.0.0.1:8000",
"--free-bytes-floor", [string]([int64]$FreeGiBFloor * 1GB),
"--orchestrator-sha256", $orchestratorSha256,
"--container-image", $ContainerImage
)
Write-Output ("PHASE=e10-integrated-replay-start FRAMES={0}" -f $clipFrameCount)
& docker @runArgs
Assert-LastExitCode "LAB E10 integrated replay"
$freePostReplay = Assert-FreeSpace "post-replay"
$result = Get-Content -LiteralPath (Join-Path $stagingRoot "result.json") -Raw | ConvertFrom-Json
if ($result.schema_version -ne "missioncore.e10-integrated-perception-result/v1" -or $result.result_id -notmatch "^e10-integrated-perception-[a-f0-9]{64}$") {
throw "LAB E10 result manifest is incompatible"
}
$finalRoot = Join-Path $derivedRoot ([string]$result.result_id)
if (Test-Path -LiteralPath $finalRoot) { throw "Immutable LAB E10 result already exists" }
Move-Item -LiteralPath $stagingRoot -Destination $finalRoot
Remove-Item -LiteralPath $publishRoot -Force
$completed = $true
$totalWatch.Stop()
$freeFinal = Assert-FreeSpace "post-publication"
Write-Output ("RESULT_ROOT={0}" -f $finalRoot)
Write-Output ("RESULT_ID={0}" -f $result.result_id)
Write-Output ("ACCEPTANCE_STATE={0}" -f $result.acceptance_state)
Write-Output ("PREPARATION_SECONDS={0}" -f $extractWatch.Elapsed.TotalSeconds)
Write-Output ("ORCHESTRATOR_WALL_SECONDS={0}" -f $totalWatch.Elapsed.TotalSeconds)
Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBytesBefore)
Write-Output ("DISK_FREE_BYTES_POST_EXTRACT={0}" -f $freePostExtract)
Write-Output ("DISK_FREE_BYTES_POST_REPLAY={0}" -f $freePostReplay)
Write-Output ("DISK_FREE_BYTES_FINAL={0}" -f $freeFinal)
}
finally {
if (Test-Path -LiteralPath $workRoot) { Remove-Item -LiteralPath $workRoot -Recurse -Force }
if (-not $completed -and (Test-Path -LiteralPath $publishRoot)) { Remove-Item -LiteralPath $publishRoot -Recurse -Force }
if ($loadedByRun) {
try {
Invoke-WebRequest -UseBasicParsing -TimeoutSec 60 -Uri "http://127.0.0.1:8000/v2/repository/models/yolox_s/unload" -Method Post -ContentType "application/json" -Body "{}" *> $null
Write-Output "PHASE=e10-model-state-restored"
}
catch { Write-Warning "LAB E10 could not restore the prior YOLOX-S state" }
}
}

View File

@ -0,0 +1,100 @@
[CmdletBinding()]
param(
[switch]$TokenStdin,
[ValidatePattern("^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")]
[string]$RequestId = ("physical-k1-shadow-{0}" -f [Guid]::NewGuid().ToString("N")),
[string]$PersistentContainer = "mission-core-perception-worker",
[string]$PersistentOutputRoot = "D:\NDC_MISSIONCORE\runtime\derived\.perception-persistent-publish",
[ValidateRange(1024, 65535)] [int]$PersistentPort = 18020,
[ValidateRange(1, 1000)] [int]$FreeGiBFloor = 360
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-LastExitCode([string]$Operation) {
if ($LASTEXITCODE -ne 0) { throw "$Operation failed with exit code $LASTEXITCODE" }
}
function Resolve-DDirectory([string]$Path, [string]$Label) {
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
$root = [IO.Path]::GetPathRoot($item.FullName).TrimEnd("\")
if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $root -ine "D:") {
throw "$Label must be a real D: directory"
}
return $item.FullName
}
function Get-DFreeBytes { return [int64](Get-PSDrive -Name D).Free }
if (-not $TokenStdin) { throw "Persistent shadow run requires the token through stdin" }
$token = [Console]::In.ReadLine()
if (-not $token -or $token.Length -lt 40 -or $token.Length -gt 512) {
throw "Persistent shadow token is missing or malformed"
}
$outputRoot = Resolve-DDirectory $PersistentOutputRoot "Persistent output root"
$freeBefore = Get-DFreeBytes
if ($freeBefore -lt ([int64]$FreeGiBFloor * 1GB + 512MB)) {
throw "D: lacks the guarded persistent-run reserve"
}
$containerRunning = docker inspect --format "{{.State.Running}}" $PersistentContainer
Assert-LastExitCode "Persistent worker inspection"
if ($containerRunning.Trim().ToLowerInvariant() -ne "true") {
throw "Persistent worker is not running"
}
$outputName = $RequestId
$request = @{
request_id = $RequestId
output_name = $outputName
token = $token
} | ConvertTo-Json -Compress
$token = $null
$client = (
"import sys,urllib.request,urllib.error;data=sys.stdin.buffer.read();" +
"request=urllib.request.Request('http://127.0.0.1:{0}/run',data=data," -f $PersistentPort
) + "headers={'Content-Type':'application/json'},method='POST');" +
"`ntry:`n response=urllib.request.urlopen(request,timeout=3600); body=response.read(); code=response.status" +
"`nexcept urllib.error.HTTPError as exc:`n body=exc.read(); code=exc.code" +
"`nsys.stdout.buffer.write(body);raise SystemExit(0 if code==200 else 22)"
try {
$responseJson = $request | & docker exec -i $PersistentContainer python3 -c $client
$request = $null
Assert-LastExitCode "Persistent worker request"
}
finally {
$token = $null
$request = $null
}
$response = $responseJson | ConvertFrom-Json
if ($response.request_id -ne $RequestId -or $response.models_reused -ne $true) {
throw "Persistent worker response contract changed"
}
$staging = Join-Path $outputRoot $outputName
$resultPath = Join-Path $staging "result.json"
if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) {
throw "Persistent worker result manifest is missing"
}
$result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json
if (
$result.schema_version -ne "missioncore.e15-shadow-inference-result/v1" -or
$result.result_id -notmatch "^e15-shadow-inference-[a-f0-9]{64}$" -or
$result.publication_scope -ne "live-shadow-diagnostic-only"
) { throw "Persistent worker result manifest is incompatible" }
$derivedRoot = Resolve-DDirectory (Split-Path $outputRoot -Parent) "Runtime derived root"
$finalRoot = Join-Path $derivedRoot ([string]$result.result_id)
if (Test-Path -LiteralPath $finalRoot) {
throw "Immutable persistent-worker result already exists"
}
Move-Item -LiteralPath $staging -Destination $finalRoot
$freeFinal = Get-DFreeBytes
if ($freeFinal -lt ([int64]$FreeGiBFloor * 1GB)) {
throw "D: crossed the guarded floor after persistent run"
}
Write-Output ("RESULT_ROOT={0}" -f $finalRoot)
Write-Output ("RESULT_ID={0}" -f $result.result_id)
Write-Output ("ACCEPTANCE_STATE={0}" -f $result.acceptance_state)
Write-Output ("RUNNER_EXIT_CODE={0}" -f $response.exit_code)
Write-Output "MODELS_REUSED=true"
Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBefore)
Write-Output ("DISK_FREE_BYTES_FINAL={0}" -f $freeFinal)

View File

@ -0,0 +1,432 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)] [string]$JobRoot,
[Parameter(Mandatory = $true)] [string]$RunnerPath,
[Parameter(Mandatory = $true)] [string]$LiveProfilePath,
[Parameter(Mandatory = $true)] [string]$E14ProfilePath,
[Parameter(Mandatory = $true)] [string]$DetectorProfilePath,
[Parameter(Mandatory = $true)] [string]$SemanticProfilePath,
[Parameter(Mandatory = $true)] [string]$ValidFovRoot,
[Parameter(Mandatory = $true)] [string]$ProjectionPackRoot,
[Parameter(Mandatory = $true)] [string]$PackageRoot,
[switch]$PreflightOnly,
[switch]$PersistentService,
[switch]$TokenStdin,
[ValidateRange(5, 3600)] [int]$MaximumDurationSeconds = 20,
[ValidateRange(1, 1000)] [int]$FreeGiBFloor = 360,
[ValidateRange(1024, 65535)] [int]$SourcePort = 18012,
[string]$SourceHost = "host.docker.internal",
[string]$SourcePath = "/api/v1/device-plugins/nodedc.device.xgrids-lixelkity-k1/live-perception-shadow",
[string]$RuntimeRoot = "D:\NDC_MISSIONCORE\runtime",
[string]$ModelRoot = "D:\NDC_MISSIONCORE\runtime\models\yolox_s",
[string]$MediaRuntimeRoot = "D:\NDC_MISSIONCORE\runtime\derived\perception-e15-media-pyav180-lz445-v1",
[string]$TritonContainer = "mission-core-triton",
[string]$PersistentContainer = "mission-core-perception-worker",
[string]$PersistentOutputRoot = "D:\NDC_MISSIONCORE\runtime\derived\.perception-persistent-publish",
[ValidateRange(1024, 65535)] [int]$PersistentPort = 18020,
[string]$ContainerImage = "nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-LastExitCode([string]$Operation) {
if ($LASTEXITCODE -ne 0) { throw "$Operation failed with exit code $LASTEXITCODE" }
}
function Resolve-DDirectory([string]$Path, [string]$Label) {
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
$root = [IO.Path]::GetPathRoot($item.FullName).TrimEnd("\")
if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $root -ine "D:") {
throw "$Label must be a real D: directory"
}
return $item.FullName
}
function Resolve-DFile([string]$Path, [string]$Label) {
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
$root = [IO.Path]::GetPathRoot($item.FullName).TrimEnd("\")
if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $root -ine "D:") {
throw "$Label must be a regular D: file"
}
return $item.FullName
}
function Convert-ToDockerPath([string]$Path) { return $Path.Replace("\", "/") }
function Get-DFreeBytes { return [int64](Get-PSDrive -Name D).Free }
function Assert-FreeSpace([string]$Phase, [int64]$RequiredAdditionalBytes = 0) {
$free = Get-DFreeBytes
$floor = [int64]$FreeGiBFloor * 1GB
Write-Host (
"DISK_GUARD PHASE={0} DRIVE=D FREE_BYTES={1} FREE_GIB={2} FLOOR_GIB={3} REQUIRED_ADDITIONAL_GIB={4}" -f
$Phase, $free, [math]::Round($free / 1GB, 3), $FreeGiBFloor,
[math]::Round($RequiredAdditionalBytes / 1GB, 3)
)
if ($free -lt ($floor + $RequiredAdditionalBytes)) {
throw "D: lacks the guarded LAB E15 reserve during $Phase"
}
return $free
}
function Test-TritonModelReady {
try {
$response = Invoke-WebRequest -UseBasicParsing -TimeoutSec 10 `
-Uri "http://127.0.0.1:8000/v2/models/yolox_s/ready"
return $response.StatusCode -eq 200
}
catch { return $false }
}
$jobDirectory = Resolve-DDirectory $JobRoot "Job root"
$runner = Resolve-DFile $RunnerPath "LAB E15 runner"
$liveProfile = Resolve-DFile $LiveProfilePath "LAB E15 live profile"
$e14Profile = Resolve-DFile $E14ProfilePath "Accepted E14 profile"
$detectorProfile = Resolve-DFile $DetectorProfilePath "Detector profile"
$semanticProfile = Resolve-DFile $SemanticProfilePath "Semantic profile"
$validFov = Resolve-DDirectory $ValidFovRoot "Valid-FOV root"
$projectionPack = Resolve-DDirectory $ProjectionPackRoot "E15 projection pack"
$package = Resolve-DDirectory $PackageRoot "Mission Core package root"
$runtime = Resolve-DDirectory $RuntimeRoot "Runtime root"
$model = Resolve-DDirectory $ModelRoot "YOLOX model root"
$mediaRuntime = Resolve-DDirectory $MediaRuntimeRoot "E15 media runtime"
$runnerRoot = Split-Path $runner -Parent
foreach ($path in @($liveProfile, $e14Profile, $detectorProfile, $semanticProfile)) {
if ((Split-Path $path -Parent) -ne $runnerRoot) {
throw "Runner and profiles must share one immutable mount"
}
}
foreach ($dependency in @(
"e10_fusion_runtime.py",
"e15_shadow_runtime.py",
"run_e10_integrated_perception.py",
"run_e12_shadow_transport_probe.py",
"run_e9_multirate_perception.py",
"run_e8_realtime_tracking.py",
"run_e5_instance_tracking.py",
"run_e4_full_session_segmentation.py",
"run_recorded_perception_epoch.py",
"run_e3_rectified_segmentation.py",
"run_evaluation_prelabels.py"
)) {
$null = Resolve-DFile (Join-Path $runnerRoot $dependency) "LAB E15 dependency"
}
if (-not (Test-Path -LiteralPath (Join-Path $package "k1link\compute\live_perception.py") -PathType Leaf)) {
throw "Mission Core package mount lacks live perception synchronization"
}
$job = Get-Content -LiteralPath (Join-Path $jobDirectory "job.json") -Raw | ConvertFrom-Json
if (
$job.schema_version -ne "missioncore.compute-job/v1" -or
$job.job_id -ne (Split-Path $jobDirectory -Leaf) -or
$job.input.source_id -ne "sensor.camera.right"
) { throw "Compute bootstrap job manifest is incompatible" }
$live = Get-Content -LiteralPath $liveProfile -Raw | ConvertFrom-Json
if (
$live.schema_version -ne "missioncore.e15-shadow-inference-profile/v1" -or
$live.mode -ne "replay-shadow-gate" -or
[bool]$live.authority.commands_enabled -or
[bool]$live.authority.navigation_or_safety_accepted -or
$live.transport.pyav_version -ne "18.0.0"
) { throw "LAB E15 replay-shadow authority contract changed" }
$projection = Get-Content -LiteralPath (Join-Path $projectionPack "manifest.json") -Raw | ConvertFrom-Json
if (
$projection.schema_version -ne "missioncore.e15-live-projection-pack/v1" -or
$projection.identity.source_id -ne "sensor.camera.right" -or
$projection.identity.calibration_slot -ne "camera_1" -or
$projection.identity.calibration_sha256 -ne $live.source.calibration_sha256
) { throw "LAB E15 projection pack binding changed" }
$mediaManifest = Get-Content -LiteralPath (Join-Path $mediaRuntime "manifest.json") -Raw | ConvertFrom-Json
if (
$mediaManifest.schema_version -ne "missioncore.e15-media-runtime/v1" -or
$mediaManifest.container_image -ne $ContainerImage -or
$mediaManifest.packages.av -ne "18.0.0" -or
$mediaManifest.packages.lz4 -ne "4.4.5"
) { throw "LAB E15 media runtime identity changed" }
$derivedRoot = Resolve-DDirectory (Join-Path $runtime "derived") "Runtime derived root"
$cacheRoot = Resolve-DDirectory (Join-Path $runtime "cache\perception-e3-models-v1") "EoMT cache"
$e3Environment = Resolve-DDirectory (Join-Path $derivedRoot "perception-e3-opencv413092-v1") "E3 environment"
$torchEnvironment = Resolve-DDirectory (Join-Path $derivedRoot "perception-p0-env-v1") "Torch environment"
$transformersEnvironment = Resolve-DDirectory (Join-Path $derivedRoot "perception-p0-transformers4576-v1") "Transformers environment"
$freeBytesBefore = Assert-FreeSpace "preflight" 512MB
& docker image inspect $ContainerImage *> $null
Assert-LastExitCode "Pinned container image inspection"
$tritonState = docker inspect --format "{{.State.Running}}" $TritonContainer
Assert-LastExitCode "Triton container inspection"
if ($tritonState.Trim().ToLowerInvariant() -ne "true") {
throw "LAB E15 requires the existing Triton container"
}
$runnerName = Split-Path $runner -Leaf
$liveProfileName = Split-Path $liveProfile -Leaf
$e14ProfileName = Split-Path $e14Profile -Leaf
$detectorProfileName = Split-Path $detectorProfile -Leaf
$semanticProfileName = Split-Path $semanticProfile -Leaf
$projectionMount = "/" + (Split-Path $projectionPack -Leaf)
$packageMount = "/" + (Split-Path $package -Leaf)
$orchestratorSha256 = (Get-FileHash -LiteralPath $PSCommandPath -Algorithm SHA256).Hash.ToLowerInvariant()
$commonMounts = @(
"-e", ("PYTHONPATH=/runner:{0}:/opt/media:/opt/transformers:/opt/env" -f $packageMount),
"-e", "PYTHONDONTWRITEBYTECODE=1",
"-e", "TORCH_HOME=/cache/torch",
"-e", "HF_HOME=/cache/huggingface",
"-e", "HF_HUB_OFFLINE=1",
"-e", "TRANSFORMERS_OFFLINE=1",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"-v", ((Convert-ToDockerPath $projectionPack) + (":{0}:ro" -f $projectionMount)),
"-v", ((Convert-ToDockerPath $model) + ":/model:ro"),
"-v", ((Convert-ToDockerPath $cacheRoot) + ":/cache:ro"),
"-v", ((Convert-ToDockerPath $e3Environment) + ":/environment:ro"),
"-v", ((Convert-ToDockerPath $torchEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $transformersEnvironment) + ":/opt/transformers:ro"),
"-v", ((Convert-ToDockerPath $mediaRuntime) + ":/opt/media:ro"),
"-v", ((Convert-ToDockerPath $package) + (":{0}:ro" -f $packageMount)),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro")
)
$commonRunnerArgs = @(
"--job", "/job",
"--live-profile", ("/runner/{0}" -f $liveProfileName),
"--e14-profile", ("/runner/{0}" -f $e14ProfileName),
"--detector-profile", ("/runner/{0}" -f $detectorProfileName),
"--semantic-profile", ("/runner/{0}" -f $semanticProfileName),
"--valid-fov-root", "/valid-fov",
"--projection-pack", $projectionMount,
"--model-root", "/model",
"--cache", "/cache",
"--environment", "/environment",
"--worker-package", $packageMount
)
if ($PersistentService) {
if ($PreflightOnly -or $TokenStdin) {
throw "Persistent service cannot be combined with one-shot switches"
}
$persistentParent = Resolve-DDirectory (Split-Path $PersistentOutputRoot -Parent) "Persistent output parent"
if (-not (Test-Path -LiteralPath $PersistentOutputRoot)) {
$null = New-Item -ItemType Directory -Path $PersistentOutputRoot
}
$persistentOutput = Resolve-DDirectory $PersistentOutputRoot "Persistent output root"
if ((Split-Path $persistentOutput -Parent) -ne $persistentParent) {
throw "Persistent output root must be a direct child of its guarded D: parent"
}
$runnerSha256 = (Get-FileHash -LiteralPath $runner -Algorithm SHA256).Hash.ToLowerInvariant()
$existingContainerId = docker ps -a --filter ("name=^{0}$" -f $PersistentContainer) --format "{{.ID}}"
Assert-LastExitCode "Persistent worker container lookup"
if ($existingContainerId) {
$labels = (docker inspect --format "{{json .Config.Labels}}" $PersistentContainer) | ConvertFrom-Json
Assert-LastExitCode "Persistent worker label inspection"
if (
$labels.'missioncore.role' -ne "perception-persistent-worker" -or
$labels.'missioncore.runner.sha256' -ne $runnerSha256
) { throw "Existing persistent worker has a different immutable identity" }
$running = docker inspect --format "{{.State.Running}}" $PersistentContainer
Assert-LastExitCode "Persistent worker state inspection"
if ($running.Trim().ToLowerInvariant() -ne "true") {
throw "Matching persistent worker exists but is not running"
}
$healthProbe = (
"import json,urllib.request;d=json.load(urllib.request.urlopen(" +
"'http://127.0.0.1:{0}/health',timeout=5));" -f $PersistentPort
) + "raise SystemExit(0 if d.get('ok') and d.get('models_loaded') else 2)"
$previousErrorPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
& docker exec $PersistentContainer python3 -c $healthProbe 2>$null
$healthExitCode = $LASTEXITCODE
$ErrorActionPreference = $previousErrorPreference
if ($healthExitCode -ne 0) { throw "Existing persistent worker health probe failed" }
Write-Output "STATE=persistent-worker-reused"
Write-Output ("CONTAINER={0}" -f $PersistentContainer)
return
}
if (-not (Test-TritonModelReady)) {
Invoke-WebRequest -UseBasicParsing -TimeoutSec 60 `
-Uri "http://127.0.0.1:8000/v2/repository/models/yolox_s/load" `
-Method Post -ContentType "application/json" -Body "{}" *> $null
$deadline = [DateTime]::UtcNow.AddSeconds(60)
while (-not (Test-TritonModelReady)) {
if ([DateTime]::UtcNow -ge $deadline) { throw "YOLOX-S did not become ready" }
Start-Sleep -Milliseconds 500
}
}
$serveArgs = @(
"run", "--detach", "--name", $PersistentContainer,
"--label", "missioncore.role=perception-persistent-worker",
"--label", ("missioncore.runner.sha256={0}" -f $runnerSha256),
"--gpus", "all", "--network", ("container:{0}" -f $TritonContainer),
"--read-only", "--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "512", "--shm-size", "4g",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=2g"
) + $commonMounts + @(
"-v", ((Convert-ToDockerPath $persistentOutput) + ":/publish:rw"),
"--entrypoint", "python3", $ContainerImage,
("/runner/{0}" -f $runnerName), "serve"
) + $commonRunnerArgs + @(
"--host", $SourceHost,
"--port", [string]$SourcePort,
"--path", $SourcePath,
"--triton-url", "http://127.0.0.1:8000",
"--output-root", "/publish",
"--listen-host", "127.0.0.1",
"--listen-port", [string]$PersistentPort,
"--max-duration-seconds", [string]$MaximumDurationSeconds,
"--free-bytes-floor", [string]([int64]$FreeGiBFloor * 1GB),
"--orchestrator-sha256", $orchestratorSha256,
"--container-image", $ContainerImage
)
Write-Output "PHASE=persistent-worker-model-load-start"
& docker @serveArgs
Assert-LastExitCode "Persistent worker container start"
$healthProbe = (
"import json,urllib.request;d=json.load(urllib.request.urlopen(" +
"'http://127.0.0.1:{0}/health',timeout=5));" -f $PersistentPort
) + "print(json.dumps(d,sort_keys=True));raise SystemExit(0 if d.get('ok') and d.get('models_loaded') else 2)"
$deadline = [DateTime]::UtcNow.AddSeconds(240)
do {
Start-Sleep -Seconds 2
$running = docker inspect --format "{{.State.Running}}" $PersistentContainer
if ($LASTEXITCODE -ne 0 -or $running.Trim().ToLowerInvariant() -ne "true") {
& docker logs --tail 80 $PersistentContainer
throw "Persistent worker exited during model load"
}
$previousErrorPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
$health = & docker exec $PersistentContainer python3 -c $healthProbe 2>$null
$healthExitCode = $LASTEXITCODE
$ErrorActionPreference = $previousErrorPreference
$healthy = $healthExitCode -eq 0
} while (-not $healthy -and [DateTime]::UtcNow -lt $deadline)
if (-not $healthy) {
& docker logs --tail 80 $PersistentContainer
throw "Persistent worker model load did not become ready"
}
Write-Output "STATE=persistent-worker-ready"
Write-Output ("CONTAINER={0}" -f $PersistentContainer)
Write-Output ("HEALTH={0}" -f $health)
Write-Output ("DISK_FREE_BYTES={0}" -f (Get-DFreeBytes))
return
}
$preflightArgs = @(
"run", "--rm", "--gpus", "all", "--network", "none", "--read-only",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "512", "--shm-size", "4g",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=2g"
) + $commonMounts + @(
"--entrypoint", "python3", $ContainerImage,
("/runner/{0}" -f $runnerName), "preflight"
) + $commonRunnerArgs
Write-Output "PHASE=e15-preflight-start"
& docker @preflightArgs
Assert-LastExitCode "LAB E15 preflight"
Write-Output "PHASE=e15-preflight-complete"
if ($PreflightOnly) {
Write-Output "STATE=preflight-ready"
Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBytesBefore)
Write-Output ("DISK_FREE_BYTES_AFTER={0}" -f (Get-DFreeBytes))
return
}
if (-not $TokenStdin) { throw "LAB E15 requires the shadow token through stdin" }
$shadowToken = [Console]::In.ReadLine()
if (-not $shadowToken -or $shadowToken.Length -lt 40 -or $shadowToken.Length -gt 512) {
throw "LAB E15 shadow token is missing or malformed"
}
$modelWasReady = Test-TritonModelReady
$loadedByRun = $false
if (-not $modelWasReady) {
Invoke-WebRequest -UseBasicParsing -TimeoutSec 60 `
-Uri "http://127.0.0.1:8000/v2/repository/models/yolox_s/load" `
-Method Post -ContentType "application/json" -Body "{}" *> $null
$loadedByRun = $true
$deadline = [DateTime]::UtcNow.AddSeconds(60)
while (-not (Test-TritonModelReady)) {
if ([DateTime]::UtcNow -ge $deadline) { throw "YOLOX-S did not become ready" }
Start-Sleep -Milliseconds 500
}
}
Write-Output ("PHASE=e15-model-ready LOADED_BY_RUN={0}" -f $loadedByRun)
$token = [Guid]::NewGuid().ToString("N")
$publishRoot = Join-Path $derivedRoot (".{0}-e15-{1}.publish" -f $job.job_id, $token)
$stagingRoot = Join-Path $publishRoot "output"
$null = New-Item -ItemType Directory -Path $publishRoot
$completed = $false
$totalWatch = [Diagnostics.Stopwatch]::StartNew()
try {
$runArgs = @(
"run", "--rm", "--interactive", "--gpus", "all",
"--network", ("container:{0}" -f $TritonContainer),
"--read-only", "--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "512", "--shm-size", "4g",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=2g"
) + $commonMounts + @(
"-v", ((Convert-ToDockerPath $publishRoot) + ":/publish:rw"),
"--entrypoint", "python3", $ContainerImage,
("/runner/{0}" -f $runnerName), "run"
) + $commonRunnerArgs + @(
"--host", $SourceHost,
"--port", [string]$SourcePort,
"--path", $SourcePath,
"--token-stdin",
"--triton-url", "http://127.0.0.1:8000",
"--output", "/publish/output",
"--max-duration-seconds", [string]$MaximumDurationSeconds,
"--free-bytes-floor", [string]([int64]$FreeGiBFloor * 1GB),
"--orchestrator-sha256", $orchestratorSha256,
"--container-image", $ContainerImage
)
Write-Output ("PHASE=e15-shadow-inference-start MAX_DURATION_SECONDS={0}" -f $MaximumDurationSeconds)
$shadowToken | & docker @runArgs
$runnerExitCode = $LASTEXITCODE
$shadowToken = $null
if ($runnerExitCode -ne 0 -and $runnerExitCode -ne 2) {
throw "LAB E15 shadow inference failed with exit code $runnerExitCode"
}
$null = Assert-FreeSpace "post-shadow-inference"
$resultPath = Join-Path $stagingRoot "result.json"
if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) {
throw "LAB E15 result manifest is missing"
}
$result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json
if (
$result.schema_version -ne "missioncore.e15-shadow-inference-result/v1" -or
$result.result_id -notmatch "^e15-shadow-inference-[a-f0-9]{64}$" -or
$result.publication_scope -ne "live-shadow-diagnostic-only"
) { throw "LAB E15 result manifest is incompatible" }
$finalRoot = Join-Path $derivedRoot ([string]$result.result_id)
if (Test-Path -LiteralPath $finalRoot) { throw "Immutable LAB E15 result already exists" }
Move-Item -LiteralPath $stagingRoot -Destination $finalRoot
Remove-Item -LiteralPath $publishRoot -Force
$completed = $true
$totalWatch.Stop()
$freeFinal = Assert-FreeSpace "post-publication"
Write-Output ("RESULT_ROOT={0}" -f $finalRoot)
Write-Output ("RESULT_ID={0}" -f $result.result_id)
Write-Output ("ACCEPTANCE_STATE={0}" -f $result.acceptance_state)
Write-Output ("RUNNER_EXIT_CODE={0}" -f $runnerExitCode)
Write-Output ("ORCHESTRATOR_WALL_SECONDS={0}" -f $totalWatch.Elapsed.TotalSeconds)
Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBytesBefore)
Write-Output ("DISK_FREE_BYTES_FINAL={0}" -f $freeFinal)
}
finally {
$shadowToken = $null
if (-not $completed -and (Test-Path -LiteralPath $publishRoot)) {
Remove-Item -LiteralPath $publishRoot -Recurse -Force
}
if ($loadedByRun) {
try {
Invoke-WebRequest -UseBasicParsing -TimeoutSec 60 `
-Uri "http://127.0.0.1:8000/v2/repository/models/yolox_s/unload" `
-Method Post -ContentType "application/json" -Body "{}" *> $null
Write-Output "PHASE=e15-model-state-restored"
}
catch { Write-Warning "LAB E15 could not restore the prior YOLOX-S state" }
}
}

View File

@ -0,0 +1,251 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$EvaluationPack,
[Parameter(Mandatory = $true)]
[string]$E2Prelabels,
[Parameter(Mandatory = $true)]
[string]$ValidFovRoot,
[Parameter(Mandatory = $true)]
[string]$RunnerPath,
[Parameter(Mandatory = $true)]
[string]$BaselineRunnerPath,
[Parameter(Mandatory = $true)]
[string]$ProfilePath,
[ValidateRange(0, 64)]
[int]$MaxFrames = 0,
[ValidateRange(1, 1000)]
[int]$FreeGiBFloor = 360,
[string]$RuntimeRoot = "D:\NDC_MISSIONCORE\runtime",
[string]$ContainerImage = "nvcr.io/nvidia/tritonserver:26.06-py3"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-LastExitCode {
param([string]$Operation)
if ($LASTEXITCODE -ne 0) {
throw "$Operation failed with exit code $LASTEXITCODE"
}
}
function Assert-RegularFile {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if (-not $item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a regular file"
}
function Assert-Directory {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if ($item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a real directory"
}
function Assert-DDrivePath {
param([string]$Path, [string]$Label)
$fullPath = [IO.Path]::GetFullPath($Path)
if ([IO.Path]::GetPathRoot($fullPath).TrimEnd("\") -ine "D:") {
throw "$Label must be stored on D:"
}
return $fullPath
}
function Assert-FreeSpace {
param([string]$Phase)
$freeBytes = [int64](Get-PSDrive -Name D).Free
$floorBytes = [int64]$FreeGiBFloor * 1GB
$freeGiB = [math]::Round($freeBytes / 1GB, 3)
Write-Output ("DISK_GUARD PHASE={0} DRIVE=D FREE_GIB={1} FLOOR_GIB={2}" -f $Phase, $freeGiB, $FreeGiBFloor)
if ($freeBytes -lt $floorBytes) {
throw "D: free-space floor was crossed during $Phase"
}
}
function Convert-ToDockerPath {
param([string]$Path)
return $Path.Replace("\", "/")
}
$packRoot = Assert-DDrivePath (Assert-Directory (Resolve-Path -LiteralPath $EvaluationPack).Path "Evaluation pack") "Evaluation pack"
$prelabelsRoot = Assert-DDrivePath (Assert-Directory (Resolve-Path -LiteralPath $E2Prelabels).Path "E2 prelabels") "E2 prelabels"
$validFov = Assert-DDrivePath (Assert-Directory (Resolve-Path -LiteralPath $ValidFovRoot).Path "Valid-FOV root") "Valid-FOV root"
$runner = Assert-DDrivePath (Assert-RegularFile (Resolve-Path -LiteralPath $RunnerPath).Path "LAB E3 runner") "LAB E3 runner"
$baselineRunner = Assert-DDrivePath (Assert-RegularFile (Resolve-Path -LiteralPath $BaselineRunnerPath).Path "Baseline runner") "Baseline runner"
$profile = Assert-DDrivePath (Assert-RegularFile (Resolve-Path -LiteralPath $ProfilePath).Path "LAB E3 profile") "LAB E3 profile"
$runtime = Assert-DDrivePath (Assert-Directory (Resolve-Path -LiteralPath $RuntimeRoot).Path "Runtime root") "Runtime root"
if (
(Split-Path $runner -Parent) -ne (Split-Path $baselineRunner -Parent) -or
(Split-Path $runner -Parent) -ne (Split-Path $profile -Parent)
) {
throw "LAB E3 runner, baseline runner, and profile must share one read-only mount"
}
$pack = Get-Content -LiteralPath (Join-Path $packRoot "manifest.json") -Raw | ConvertFrom-Json
$prelabels = Get-Content -LiteralPath (Join-Path $prelabelsRoot "result.json") -Raw | ConvertFrom-Json
$profileDocument = Get-Content -LiteralPath $profile -Raw | ConvertFrom-Json
$packFrameCount = @($pack.identity.frames).Count
if (
$pack.schema_version -ne "missioncore.perception-evaluation-pack/v1" -or
$pack.generation_id -ne (Split-Path $packRoot -Leaf) -or
$pack.identity.source_id -ne "sensor.camera.right" -or
$pack.identity.calibration_slot -ne "camera_1" -or
$packFrameCount -ne 64 -or
$prelabels.schema_version -ne "missioncore.perception-evaluation-prelabels/v1" -or
$prelabels.identity.evaluation_pack_id -ne $pack.generation_id -or
$profileDocument.schema_version -ne "missioncore.k1-e3-rectified-segmentation-profile/v1" -or
$profileDocument.source.source_id -ne $pack.identity.source_id -or
$profileDocument.source.calibration_slot -ne $pack.identity.calibration_slot -or
$profileDocument.source.calibration_sha256 -ne $pack.identity.calibration_sha256
) {
throw "LAB E3 inputs are incompatible"
}
$null = Assert-RegularFile (Join-Path $validFov "manifest.json") "Valid-FOV manifest"
$null = Assert-RegularFile (Join-Path $validFov "mask.png") "Valid-FOV mask"
& docker image inspect $ContainerImage *> $null
Assert-LastExitCode "Existing container image inspection"
Assert-FreeSpace "preflight"
$cacheRoot = Join-Path $runtime "cache\perception-e3-models-v1"
$derivedRoot = Join-Path $runtime "derived\e3-segmentation"
$environmentRoot = Join-Path $runtime "derived\perception-e3-opencv413092-v1"
$torchEnvironment = Assert-Directory (Join-Path $runtime "derived\perception-p0-env-v1") "Torch environment"
$transformersEnvironment = Assert-Directory (Join-Path $runtime "derived\perception-p0-transformers4576-v1") "Transformers environment"
$runnerRoot = Split-Path $runner -Parent
$runnerName = Split-Path $runner -Leaf
$profileName = Split-Path $profile -Leaf
$null = New-Item -ItemType Directory -Path $cacheRoot -Force
$null = New-Item -ItemType Directory -Path $derivedRoot -Force
if (-not (Test-Path -LiteralPath $environmentRoot)) {
$environmentToken = [Guid]::NewGuid().ToString("N")
$environmentStaging = Join-Path (Split-Path $environmentRoot -Parent) (".e3-environment-{0}.publish" -f $environmentToken)
$null = New-Item -ItemType Directory -Path $environmentStaging
$null = New-Item -ItemType Directory -Path (Join-Path $environmentStaging "tmp")
try {
$prepareArgs = @(
"run", "--rm", "--network", "bridge", "--read-only",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "512", "--tmpfs", "/tmp:rw,noexec,nosuid,size=1g",
"-e", "PYTHONPATH=/opt/transformers:/opt/env",
"-e", "HF_HOME=/cache/huggingface",
"-e", "PIP_NO_CACHE_DIR=1",
"-e", "HOME=/environment/tmp",
"-e", "TMPDIR=/environment/tmp",
"-v", ((Convert-ToDockerPath $cacheRoot) + ":/cache:rw"),
"-v", ((Convert-ToDockerPath $environmentStaging) + ":/environment:rw"),
"-v", ((Convert-ToDockerPath $torchEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $transformersEnvironment) + ":/opt/transformers:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3",
$ContainerImage,
("/runner/{0}" -f $runnerName), "prepare",
"--profile", ("/runner/{0}" -f $profileName),
"--cache", "/cache",
"--environment", "/environment"
)
Write-Output "PHASE=e3-dependency-prepare-start"
& docker @prepareArgs
Assert-LastExitCode "LAB E3 dependency preparation"
$null = Assert-RegularFile (Join-Path $environmentStaging "manifest.json") "LAB E3 dependency manifest"
Remove-Item -LiteralPath (Join-Path $environmentStaging "tmp") -Recurse -Force
Move-Item -LiteralPath $environmentStaging -Destination $environmentRoot
Write-Output "PHASE=e3-dependency-prepare-complete"
}
finally {
if (Test-Path -LiteralPath $environmentStaging) {
Remove-Item -LiteralPath $environmentStaging -Recurse -Force
}
}
}
$environment = Assert-Directory $environmentRoot "LAB E3 dependency environment"
$null = Assert-RegularFile (Join-Path $environment "manifest.json") "LAB E3 dependency manifest"
Assert-FreeSpace "post-dependency-prepare"
$runToken = [Guid]::NewGuid().ToString("N")
$publishRoot = Join-Path $derivedRoot (".{0}-{1}.publish" -f $pack.generation_id, $runToken)
$stagingRoot = Join-Path $publishRoot "output"
$null = New-Item -ItemType Directory -Path $publishRoot
try {
$runArgs = @(
"run", "--rm", "--gpus", "all", "--network", "none", "--read-only",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "512", "--shm-size", "4g",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=2g",
"-e", "PYTHONPATH=/opt/transformers:/opt/env",
"-e", "TORCH_HOME=/cache/torch",
"-e", "HF_HOME=/cache/huggingface",
"-e", "HF_HUB_OFFLINE=1",
"-e", "TRANSFORMERS_OFFLINE=1",
"-e", "HOME=/tmp",
"-v", ((Convert-ToDockerPath $packRoot) + ":/evaluation-pack:ro"),
"-v", ((Convert-ToDockerPath $prelabelsRoot) + ":/e2-prelabels:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"-v", ((Convert-ToDockerPath $publishRoot) + ":/publish:rw"),
"-v", ((Convert-ToDockerPath $cacheRoot) + ":/cache:ro"),
"-v", ((Convert-ToDockerPath $environment) + ":/environment:ro"),
"-v", ((Convert-ToDockerPath $torchEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $transformersEnvironment) + ":/opt/transformers:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3",
$ContainerImage,
("/runner/{0}" -f $runnerName), "run",
"--profile", ("/runner/{0}" -f $profileName),
"--evaluation-pack", "/evaluation-pack",
"--e2-prelabels", "/e2-prelabels",
"--valid-fov-root", "/valid-fov",
"--cache", "/cache",
"--environment", "/environment",
"--output", "/publish/output",
"--telemetry-interval-seconds", "1"
)
if ($MaxFrames -gt 0) {
$runArgs += @("--max-frames", [string]$MaxFrames)
}
$expectedFrames = if ($MaxFrames -gt 0) { $MaxFrames } else { $packFrameCount }
Write-Output ("PHASE=e3-run-start PACK={0} FRAMES={1}" -f $pack.generation_id, $expectedFrames)
& docker @runArgs
Assert-LastExitCode "LAB E3 segmentation"
$result = Get-Content -LiteralPath (Join-Path $stagingRoot "result.json") -Raw | ConvertFrom-Json
if (
$result.schema_version -ne "missioncore.k1-e3-rectified-segmentation-result/v1" -or
$result.result_id -notmatch "^e3-segmentation-[a-f0-9]{64}$" -or
$result.identity.evaluation_pack_id -ne $pack.generation_id -or
[int]$result.identity.frame_count -ne $expectedFrames -or
$result.ground_truth -ne $false
) {
throw "LAB E3 result manifest is incompatible"
}
$finalRoot = Join-Path $derivedRoot ([string]$result.result_id)
if (Test-Path -LiteralPath $finalRoot) {
throw "An immutable LAB E3 result with the same identity already exists: $finalRoot"
}
Move-Item -LiteralPath $stagingRoot -Destination $finalRoot
Remove-Item -LiteralPath $publishRoot -Force
Assert-FreeSpace "post-run"
Write-Output ("RESULT_ROOT={0}" -f $finalRoot)
Write-Output ("RESULT_ID={0}" -f $result.result_id)
}
finally {
if (Test-Path -LiteralPath $publishRoot) {
Remove-Item -LiteralPath $publishRoot -Recurse -Force
}
}

View File

@ -0,0 +1,421 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$JobRoot,
[Parameter(Mandatory = $true)]
[string]$RunnerPath,
[Parameter(Mandatory = $true)]
[string]$ProfilePath,
[Parameter(Mandatory = $true)]
[string]$ValidFovRoot,
[ValidateRange(0, 256)]
[int]$PilotFrames = 0,
[ValidateRange(1, 1000)]
[int]$FreeGiBFloor = 360,
[string]$RuntimeRoot = "D:\NDC_MISSIONCORE\runtime",
[string]$ContainerImage = "nvcr.io/nvidia/tritonserver:26.06-py3"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-LastExitCode {
param([string]$Operation)
if ($LASTEXITCODE -ne 0) {
throw "$Operation failed with exit code $LASTEXITCODE"
}
}
function Assert-RegularFile {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if (-not $item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a regular file"
}
function Assert-Directory {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if ($item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a real directory"
}
function Assert-DDrivePath {
param([string]$Path, [string]$Label)
$fullPath = [IO.Path]::GetFullPath($Path)
if ([IO.Path]::GetPathRoot($fullPath).TrimEnd("\") -ine "D:") {
throw "$Label must be stored on D:"
}
return $fullPath
}
function Get-DFreeBytes {
return [int64](Get-PSDrive -Name D).Free
}
function Assert-FreeSpace {
param(
[string]$Phase,
[int64]$RequiredAdditionalBytes = 0
)
$freeBytes = Get-DFreeBytes
$floorBytes = [int64]$FreeGiBFloor * 1GB
$requiredBytes = $floorBytes + $RequiredAdditionalBytes
Write-Host (
"DISK_GUARD PHASE={0} DRIVE=D FREE_BYTES={1} FREE_GIB={2} FLOOR_GIB={3} REQUIRED_ADDITIONAL_GIB={4}" -f
$Phase,
$freeBytes,
[math]::Round($freeBytes / 1GB, 3),
$FreeGiBFloor,
[math]::Round($RequiredAdditionalBytes / 1GB, 3)
)
if ($freeBytes -lt $requiredBytes) {
throw "D: does not have the guarded LAB E4 working-set reserve during $Phase"
}
return $freeBytes
}
function Convert-ToDockerPath {
param([string]$Path)
return $Path.Replace("\", "/")
}
$jobDirectory = Assert-DDrivePath (Assert-Directory (Resolve-Path -LiteralPath $JobRoot).Path "Job root") "Job root"
$runner = Assert-DDrivePath (Assert-RegularFile (Resolve-Path -LiteralPath $RunnerPath).Path "LAB E4 runner") "LAB E4 runner"
$profile = Assert-DDrivePath (Assert-RegularFile (Resolve-Path -LiteralPath $ProfilePath).Path "LAB E4 profile") "LAB E4 profile"
$validFov = Assert-DDrivePath (Assert-Directory (Resolve-Path -LiteralPath $ValidFovRoot).Path "Valid-FOV root") "Valid-FOV root"
$runtime = Assert-DDrivePath (Assert-Directory (Resolve-Path -LiteralPath $RuntimeRoot).Path "Runtime root") "Runtime root"
$runnerRoot = Split-Path $runner -Parent
if ((Split-Path $profile -Parent) -ne $runnerRoot) {
throw "LAB E4 runner and profile must share one read-only mount"
}
foreach ($dependency in @(
"run_recorded_perception_epoch.py",
"run_e3_rectified_segmentation.py",
"run_evaluation_prelabels.py"
)) {
$null = Assert-RegularFile (Join-Path $runnerRoot $dependency) "LAB E4 runner dependency"
}
$job = Get-Content -LiteralPath (Join-Path $jobDirectory "job.json") -Raw | ConvertFrom-Json
if ($job.schema_version -ne "missioncore.compute-job/v1" -or $job.job_id -ne (Split-Path $jobDirectory -Leaf)) {
throw "Compute job manifest is incompatible"
}
$sourceId = [string]$job.input.source_id
$epoch = [int]$job.input.codec_epoch
$fullFrameCount = [int]$job.input.segment_count
$activeFrameCount = if ($PilotFrames -gt 0) { $PilotFrames } else { $fullFrameCount }
$timelineStart = [double]$job.input.timeline.start_seconds
$timelineEnd = [double]$job.input.timeline.end_seconds
$timelineDuration = $timelineEnd - $timelineStart
if (
$sourceId -ne "sensor.camera.right" -or
$fullFrameCount -lt 1 -or
$activeFrameCount -gt $fullFrameCount -or
$timelineDuration -le 0
) {
throw "LAB E4 camera job contract is invalid"
}
Write-Output ("PHASE=job-manifest-validated JOB={0} FRAMES={1}/{2}" -f $job.job_id, $activeFrameCount, $fullFrameCount)
$epochRoot = Join-Path $jobDirectory ("input\camera\{0}\epoch-{1}" -f $sourceId, $epoch)
$epochRoot = Assert-DDrivePath (Assert-Directory $epochRoot "Camera epoch") "Camera epoch"
$initPath = Assert-RegularFile (Join-Path $epochRoot "init.mp4") "Camera init"
$segmentsRoot = Assert-Directory (Join-Path $epochRoot "segments") "Camera segments"
$derivedRoot = Join-Path $runtime "derived"
$tmpRoot = Join-Path $runtime "tmp"
$cacheRoot = Assert-DDrivePath (Assert-Directory (Join-Path $runtime "cache\perception-e3-models-v1") "EoMT cache") "EoMT cache"
$e3Environment = Assert-DDrivePath (Assert-Directory (Join-Path $derivedRoot "perception-e3-opencv413092-v1") "E3 dependency environment") "E3 dependency environment"
$torchEnvironment = Assert-DDrivePath (Assert-Directory (Join-Path $derivedRoot "perception-p0-env-v1") "Torch environment") "Torch environment"
$transformersEnvironment = Assert-DDrivePath (Assert-Directory (Join-Path $derivedRoot "perception-p0-transformers4576-v1") "Transformers environment") "Transformers environment"
$null = New-Item -ItemType Directory -Path $derivedRoot -Force
$null = New-Item -ItemType Directory -Path $tmpRoot -Force
# Conservative upper bound: decoded RGB + RGB overlay + one-byte semantic mask,
# plus 20% filesystem/encoding overhead and the reconstructed source stream.
$pixelWorkingSet = [int64]$activeFrameCount * 800 * 600 * 7
$workingSetReserve = [int64][math]::Ceiling(($pixelWorkingSet * 1.2) + [int64]$job.input.byte_length)
$freeBytesBefore = Assert-FreeSpace "preflight" $workingSetReserve
& docker image inspect $ContainerImage *> $null
Assert-LastExitCode "Existing container image inspection"
$runnerName = Split-Path $runner -Leaf
$profileName = Split-Path $profile -Leaf
$orchestratorSha256 = (Get-FileHash -LiteralPath $PSCommandPath -Algorithm SHA256).Hash.ToLowerInvariant()
$preflightArgs = @(
"run", "--rm", "--gpus", "all", "--network", "none", "--read-only",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "512", "--shm-size", "4g",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=2g",
"-e", "PYTHONPATH=/opt/transformers:/opt/env",
"-e", "TORCH_HOME=/cache/torch",
"-e", "HF_HOME=/cache/huggingface",
"-e", "HF_HUB_OFFLINE=1",
"-e", "TRANSFORMERS_OFFLINE=1",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"-v", ((Convert-ToDockerPath $cacheRoot) + ":/cache:ro"),
"-v", ((Convert-ToDockerPath $e3Environment) + ":/environment:ro"),
"-v", ((Convert-ToDockerPath $torchEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $transformersEnvironment) + ":/opt/transformers:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3",
$ContainerImage,
("/runner/{0}" -f $runnerName), "preflight",
"--job", "/job",
"--profile", ("/runner/{0}" -f $profileName),
"--valid-fov-root", "/valid-fov",
"--cache", "/cache",
"--environment", "/environment"
)
Write-Output "PHASE=e4-preflight-start"
& docker @preflightArgs
Assert-LastExitCode "LAB E4 preflight"
Write-Output "PHASE=e4-preflight-complete"
$runToken = [Guid]::NewGuid().ToString("N")
$workRoot = Join-Path $tmpRoot ("{0}-e4-{1}" -f $job.job_id, $runToken)
$framesRoot = Join-Path $workRoot "frames"
$streamPath = Join-Path $workRoot "camera.mp4"
$ptsPath = Join-Path $workRoot "pts.json"
$timelinePath = Join-Path $workRoot "timeline.jsonl"
$publishRoot = Join-Path $derivedRoot (".{0}-e4-{1}.publish" -f $job.job_id, $runToken)
$stagingRoot = Join-Path $publishRoot "output"
$null = New-Item -ItemType Directory -Path $framesRoot
$null = New-Item -ItemType Directory -Path $publishRoot
$totalWatch = [Diagnostics.Stopwatch]::StartNew()
$completed = $false
Write-Output "PHASE=e4-private-staging-created"
try {
$stream = [IO.File]::Open($streamPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
try {
$input = [IO.File]::OpenRead($initPath)
try {
$input.CopyTo($stream)
}
finally {
$input.Dispose()
}
for ($sequence = 1; $sequence -le $activeFrameCount; $sequence++) {
$path = Assert-RegularFile (Join-Path $segmentsRoot ("{0}.m4s" -f $sequence)) "Camera segment"
$input = [IO.File]::OpenRead($path)
try {
$input.CopyTo($stream)
}
finally {
$input.Dispose()
}
if ($sequence % 500 -eq 0 -or $sequence -eq $activeFrameCount) {
Write-Output ("PHASE=e4-stream-reconstruction SEGMENTS={0}/{1}" -f $sequence, $activeFrameCount)
}
}
$stream.Flush($true)
}
finally {
$stream.Dispose()
}
$null = Assert-FreeSpace "post-stream-reconstruction"
$extractWatch = [Diagnostics.Stopwatch]::StartNew()
Write-Output "PHASE=e4-frame-extraction-start"
& ffmpeg -hide_banner -loglevel fatal -i $streamPath -map 0:v:0 -fps_mode passthrough -frames:v $activeFrameCount (Join-Path $framesRoot "frame-%06d.png")
Assert-LastExitCode "LAB E4 camera extraction"
& ffprobe -v error -select_streams v:0 -show_entries frame=best_effort_timestamp_time -of json $streamPath | Set-Content -LiteralPath $ptsPath -Encoding utf8
Assert-LastExitCode "LAB E4 camera timestamp probe"
$decodedFrames = @(Get-ChildItem -LiteralPath $framesRoot -File -Filter "frame-*.png")
$ptsDocument = Get-Content -LiteralPath $ptsPath -Raw | ConvertFrom-Json
$pts = @($ptsDocument.frames)
if ($decodedFrames.Count -ne $activeFrameCount -or $pts.Count -lt $activeFrameCount) {
throw "Decoded LAB E4 frame count differs from the requested camera epoch"
}
$firstEpochSeconds = [double]::Parse(
([string]$pts[0].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
)
$previousEpochSeconds = -1.0
$timelineWriter = [IO.StreamWriter]::new($timelinePath, $false, [Text.UTF8Encoding]::new($false))
try {
for ($index = 0; $index -lt $activeFrameCount; $index++) {
$epochSeconds = [double]::Parse(
([string]$pts[$index].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
) - $firstEpochSeconds
if ($epochSeconds -le $previousEpochSeconds -or $epochSeconds -gt ($timelineDuration + 0.001)) {
throw "Decoded LAB E4 timestamps are not strictly monotonic inside the camera timeline"
}
$row = [ordered]@{
frame_index = $index
epoch_seconds = $epochSeconds
session_seconds = $timelineStart + $epochSeconds
}
$timelineWriter.WriteLine(($row | ConvertTo-Json -Compress))
$previousEpochSeconds = $epochSeconds
}
$timelineWriter.Flush()
}
finally {
$timelineWriter.Dispose()
}
$extractWatch.Stop()
$freeBytesPostExtract = Assert-FreeSpace "post-frame-extraction"
Write-Output ("PHASE=e4-frame-extraction-complete FRAMES={0}" -f $activeFrameCount)
$runArgs = @(
"run", "--rm", "--gpus", "all", "--network", "none", "--read-only",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "512", "--shm-size", "4g",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=2g",
"-e", "PYTHONPATH=/opt/transformers:/opt/env",
"-e", "TORCH_HOME=/cache/torch",
"-e", "HF_HOME=/cache/huggingface",
"-e", "HF_HUB_OFFLINE=1",
"-e", "TRANSFORMERS_OFFLINE=1",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"-v", ((Convert-ToDockerPath $framesRoot) + ":/frames:ro"),
"-v", ((Convert-ToDockerPath $workRoot) + ":/work:ro"),
"-v", ((Convert-ToDockerPath $publishRoot) + ":/publish:rw"),
"-v", ((Convert-ToDockerPath $cacheRoot) + ":/cache:ro"),
"-v", ((Convert-ToDockerPath $e3Environment) + ":/environment:ro"),
"-v", ((Convert-ToDockerPath $torchEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $transformersEnvironment) + ":/opt/transformers:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3",
$ContainerImage,
("/runner/{0}" -f $runnerName), "run",
"--job", "/job",
"--profile", ("/runner/{0}" -f $profileName),
"--valid-fov-root", "/valid-fov",
"--frames", "/frames",
"--timeline", "/work/timeline.jsonl",
"--cache", "/cache",
"--environment", "/environment",
"--output", "/publish/output",
"--frame-limit", [string]$activeFrameCount,
"--free-bytes-floor", [string]([int64]$FreeGiBFloor * 1GB),
"--orchestrator-sha256", $orchestratorSha256,
"--container-image", $ContainerImage,
"--telemetry-interval-seconds", "1"
)
Write-Output ("PHASE=e4-inference-start FRAMES={0}" -f $activeFrameCount)
& docker @runArgs
Assert-LastExitCode "LAB E4 semantic inference"
$freeBytesPostInference = Assert-FreeSpace "post-inference"
Write-Output "PHASE=e4-inference-complete"
if ($PilotFrames -gt 0) {
$pilotParent = Join-Path $derivedRoot "e4-pilots"
$null = New-Item -ItemType Directory -Path $pilotParent -Force
$pilotRoot = Join-Path $pilotParent ("pilot-{0}-{1}" -f $activeFrameCount, $runToken)
Move-Item -LiteralPath $stagingRoot -Destination $pilotRoot
Remove-Item -LiteralPath $publishRoot -Force
$completed = $true
Write-Output ("PILOT_ROOT={0}" -f $pilotRoot)
Write-Output ("PILOT_FRAMES={0}" -f $activeFrameCount)
Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBytesBefore)
Write-Output ("DISK_FREE_BYTES_POST_EXTRACT={0}" -f $freeBytesPostExtract)
Write-Output ("DISK_FREE_BYTES_POST_INFERENCE={0}" -f $freeBytesPostInference)
return
}
$videoPath = Join-Path $stagingRoot "perception.mp4"
$encodeWatch = [Diagnostics.Stopwatch]::StartNew()
$averageFps = $fullFrameCount / $timelineDuration
$fpsText = $averageFps.ToString("0.#########", [Globalization.CultureInfo]::InvariantCulture)
$durationText = $timelineDuration.ToString("0.#########", [Globalization.CultureInfo]::InvariantCulture)
& ffmpeg -hide_banner -loglevel error -framerate $fpsText -i (Join-Path $stagingRoot "overlay-frames\frame-%06d.png") -t $durationText -c:v h264_nvenc -preset p4 -tune hq -rc vbr -cq 21 -b:v 0 -pix_fmt yuv420p -movflags +faststart $videoPath
Assert-LastExitCode "LAB E4 video encoding"
$encodeWatch.Stop()
$null = Assert-FreeSpace "post-video-encoding"
Write-Output "PHASE=e4-video-encoding-complete"
$masksPath = Join-Path $stagingRoot "masks.tar.gz"
$archiveWatch = [Diagnostics.Stopwatch]::StartNew()
& tar.exe -czf $masksPath -C $stagingRoot semantic-masks
Assert-LastExitCode "LAB E4 mask archive publication"
$archiveWatch.Stop()
$freeBytesPostArtifacts = Assert-FreeSpace "post-mask-archive"
Write-Output "PHASE=e4-mask-archive-complete"
$totalWatch.Stop()
$finalizeArgs = @(
"run", "--rm", "--network", "none", "--read-only",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "128", "--tmpfs", "/tmp:rw,noexec,nosuid,size=256m",
"-e", "PYTHONPATH=/opt/transformers:/opt/env",
"-v", ((Convert-ToDockerPath $stagingRoot) + ":/output:rw"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3",
$ContainerImage,
("/runner/{0}" -f $runnerName), "finalize",
"--output", "/output",
"--video", "/output/perception.mp4",
"--masks", "/output/masks.tar.gz",
"--extract-seconds", $extractWatch.Elapsed.TotalSeconds.ToString("0.######", [Globalization.CultureInfo]::InvariantCulture),
"--encode-seconds", $encodeWatch.Elapsed.TotalSeconds.ToString("0.######", [Globalization.CultureInfo]::InvariantCulture),
"--archive-seconds", $archiveWatch.Elapsed.TotalSeconds.ToString("0.######", [Globalization.CultureInfo]::InvariantCulture),
"--wall-seconds", $totalWatch.Elapsed.TotalSeconds.ToString("0.######", [Globalization.CultureInfo]::InvariantCulture),
"--encoder", "ffmpeg-h264_nvenc-p4-cq21-yuv420p-faststart",
"--disk-free-before-bytes", [string]$freeBytesBefore,
"--disk-free-post-extract-bytes", [string]$freeBytesPostExtract,
"--disk-free-post-inference-bytes", [string]$freeBytesPostInference,
"--disk-free-post-artifacts-bytes", [string]$freeBytesPostArtifacts,
"--disk-floor-bytes", [string]([int64]$FreeGiBFloor * 1GB),
"--working-set-reserve-bytes", [string]$workingSetReserve
)
& docker @finalizeArgs
Assert-LastExitCode "LAB E4 result finalization"
$result = Get-Content -LiteralPath (Join-Path $stagingRoot "result.json") -Raw | ConvertFrom-Json
if (
$result.schema_version -ne "missioncore.recorded-perception-result/v2" -or
$result.result_id -notmatch "^result-[a-f0-9]{64}$" -or
[int]$result.frames_processed -ne $fullFrameCount -or
$result.ground_truth -ne $false
) {
throw "Final LAB E4 result manifest is incompatible"
}
$finalRoot = Join-Path $derivedRoot ([string]$result.result_id)
if (Test-Path -LiteralPath $finalRoot) {
throw "An immutable result with the same LAB E4 identity already exists: $finalRoot"
}
foreach ($temporaryChild in @("overlay-frames", "semantic-masks")) {
$temporaryPath = Join-Path $stagingRoot $temporaryChild
if (Test-Path -LiteralPath $temporaryPath) {
Remove-Item -LiteralPath $temporaryPath -Recurse -Force
}
}
Move-Item -LiteralPath $stagingRoot -Destination $finalRoot
Remove-Item -LiteralPath $publishRoot -Force
$completed = $true
$freeBytesFinal = Assert-FreeSpace "post-publication"
Write-Output ("RESULT_ROOT={0}" -f $finalRoot)
Write-Output ("RESULT_ID={0}" -f $result.result_id)
Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBytesBefore)
Write-Output ("DISK_FREE_BYTES_POST_EXTRACT={0}" -f $freeBytesPostExtract)
Write-Output ("DISK_FREE_BYTES_POST_INFERENCE={0}" -f $freeBytesPostInference)
Write-Output ("DISK_FREE_BYTES_POST_ARTIFACTS={0}" -f $freeBytesPostArtifacts)
Write-Output ("DISK_FREE_BYTES_FINAL={0}" -f $freeBytesFinal)
}
finally {
if (Test-Path -LiteralPath $workRoot) {
Remove-Item -LiteralPath $workRoot -Recurse -Force
}
if (-not $completed -and (Test-Path -LiteralPath $publishRoot)) {
Remove-Item -LiteralPath $publishRoot -Recurse -Force
}
}

View File

@ -0,0 +1,544 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$JobRoot,
[Parameter(Mandatory = $true)]
[string]$RunnerPath,
[Parameter(Mandatory = $true)]
[string]$ProfilePath,
[Parameter(Mandatory = $true)]
[string]$ValidFovRoot,
[ValidateRange(0, 1000000)]
[int]$StartFrame = 1000,
[ValidateRange(0, 1000000)]
[int]$EndFrame = 1600,
[ValidateRange(1, 1000)]
[int]$FreeGiBFloor = 360,
[string]$RuntimeRoot = "D:\NDC_MISSIONCORE\runtime",
[string]$ModelRoot = "D:\NDC_MISSIONCORE\runtime\models\yolox_s",
[string]$TritonContainer = "mission-core-triton",
[string]$ContainerImage = "nvcr.io/nvidia/tritonserver:26.06-py3"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-LastExitCode {
param([string]$Operation)
if ($LASTEXITCODE -ne 0) {
throw "$Operation failed with exit code $LASTEXITCODE"
}
}
function Assert-RegularFile {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if (-not $item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a regular file"
}
function Assert-Directory {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if ($item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a real directory"
}
function Assert-DDrivePath {
param([string]$Path, [string]$Label)
$fullPath = [IO.Path]::GetFullPath($Path)
if ([IO.Path]::GetPathRoot($fullPath).TrimEnd("\") -ine "D:") {
throw "$Label must be stored on D:"
}
return $fullPath
}
function Get-DFreeBytes {
return [int64](Get-PSDrive -Name D).Free
}
function Assert-FreeSpace {
param(
[string]$Phase,
[int64]$RequiredAdditionalBytes = 0
)
$freeBytes = Get-DFreeBytes
$floorBytes = [int64]$FreeGiBFloor * 1GB
Write-Host (
"DISK_GUARD PHASE={0} DRIVE=D FREE_BYTES={1} FREE_GIB={2} FLOOR_GIB={3} REQUIRED_ADDITIONAL_GIB={4}" -f
$Phase,
$freeBytes,
[math]::Round($freeBytes / 1GB, 3),
$FreeGiBFloor,
[math]::Round($RequiredAdditionalBytes / 1GB, 3)
)
if ($freeBytes -lt ($floorBytes + $RequiredAdditionalBytes)) {
throw "D: does not have the guarded LAB E5 working-set reserve during $Phase"
}
return $freeBytes
}
function Convert-ToDockerPath {
param([string]$Path)
return $Path.Replace("\", "/")
}
function Test-TritonModelReady {
param([string]$ModelName)
try {
$response = Invoke-WebRequest `
-Uri ("http://127.0.0.1:8000/v2/models/{0}/ready" -f $ModelName) `
-Method Get `
-UseBasicParsing `
-TimeoutSec 10
return $response.StatusCode -eq 200
}
catch {
return $false
}
}
$jobDirectory = Assert-DDrivePath (
Assert-Directory (Resolve-Path -LiteralPath $JobRoot).Path "Job root"
) "Job root"
$runner = Assert-DDrivePath (
Assert-RegularFile (Resolve-Path -LiteralPath $RunnerPath).Path "LAB E5 runner"
) "LAB E5 runner"
$profile = Assert-DDrivePath (
Assert-RegularFile (Resolve-Path -LiteralPath $ProfilePath).Path "LAB E5 profile"
) "LAB E5 profile"
$validFov = Assert-DDrivePath (
Assert-Directory (Resolve-Path -LiteralPath $ValidFovRoot).Path "Valid-FOV root"
) "Valid-FOV root"
$runtime = Assert-DDrivePath (
Assert-Directory (Resolve-Path -LiteralPath $RuntimeRoot).Path "Runtime root"
) "Runtime root"
$model = Assert-DDrivePath (
Assert-Directory (Resolve-Path -LiteralPath $ModelRoot).Path "YOLOX model root"
) "YOLOX model root"
$runnerRoot = Split-Path $runner -Parent
if ((Split-Path $profile -Parent) -ne $runnerRoot) {
throw "LAB E5 runner and profile must share one read-only mount"
}
foreach ($dependency in @(
"run_recorded_perception_epoch.py",
"run_e3_rectified_segmentation.py",
"run_evaluation_prelabels.py"
)) {
$null = Assert-RegularFile (Join-Path $runnerRoot $dependency) "LAB E5 runner dependency"
}
$job = Get-Content -LiteralPath (Join-Path $jobDirectory "job.json") -Raw | ConvertFrom-Json
if ($job.schema_version -ne "missioncore.compute-job/v1" -or $job.job_id -ne (Split-Path $jobDirectory -Leaf)) {
throw "Compute job manifest is incompatible"
}
$sourceId = [string]$job.input.source_id
$epoch = [int]$job.input.codec_epoch
$fullFrameCount = [int]$job.input.segment_count
$clipFrameCount = $EndFrame - $StartFrame + 1
$timelineStart = [double]$job.input.timeline.start_seconds
if (
$sourceId -ne "sensor.camera.right" -or
$StartFrame -lt 0 -or
$EndFrame -lt $StartFrame -or
$EndFrame -ge $fullFrameCount -or
$clipFrameCount -lt 2
) {
throw "LAB E5 clip escapes the camera job"
}
Write-Output (
"PHASE=job-manifest-validated JOB={0} CLIP={1}-{2} FRAMES={3}" -f
$job.job_id, $StartFrame, $EndFrame, $clipFrameCount
)
$epochRoot = Join-Path $jobDirectory ("input\camera\{0}\epoch-{1}" -f $sourceId, $epoch)
$epochRoot = Assert-DDrivePath (Assert-Directory $epochRoot "Camera epoch") "Camera epoch"
$initPath = Assert-RegularFile (Join-Path $epochRoot "init.mp4") "Camera init"
$segmentsRoot = Assert-Directory (Join-Path $epochRoot "segments") "Camera segments"
$derivedRoot = Join-Path $runtime "derived"
$tmpRoot = Join-Path $runtime "tmp"
$pythonEnvironment = Assert-DDrivePath (
Assert-Directory (Join-Path $derivedRoot "perception-p0-env-v1") "Python environment"
) "Python environment"
$null = New-Item -ItemType Directory -Path $derivedRoot -Force
$null = New-Item -ItemType Directory -Path $tmpRoot -Force
# Conservative bound: partial reconstructed stream, decoded RGB PNG payload,
# RGB overlays, encoded result and 2 GiB of filesystem/codec overhead.
$pixelWorkingSet = [int64]$clipFrameCount * 800 * 600 * 6
$partialStreamReserve = [int64][math]::Ceiling(
([int64]$job.input.byte_length * ($EndFrame + 1) / $fullFrameCount) * 1.2
)
$workingSetReserve = [int64][math]::Ceiling($pixelWorkingSet * 1.4) + $partialStreamReserve + 2GB
$freeBytesBefore = Assert-FreeSpace "preflight" $workingSetReserve
& docker image inspect $ContainerImage *> $null
Assert-LastExitCode "Existing container image inspection"
$tritonState = docker inspect --format "{{.State.Running}}" $TritonContainer
Assert-LastExitCode "Triton container inspection"
if ($tritonState.Trim().ToLowerInvariant() -ne "true") {
throw "LAB E5 requires the existing Triton container"
}
$runnerName = Split-Path $runner -Leaf
$profileName = Split-Path $profile -Leaf
$orchestratorSha256 = (Get-FileHash -LiteralPath $PSCommandPath -Algorithm SHA256).Hash.ToLowerInvariant()
$preflightArgs = @(
"run", "--rm", "--network", "none", "--read-only",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "128", "--tmpfs", "/tmp:rw,noexec,nosuid,size=256m",
"-e", "PYTHONPATH=/runner:/opt/env",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"-v", ((Convert-ToDockerPath $model) + ":/model:ro"),
"-v", ((Convert-ToDockerPath $pythonEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3",
$ContainerImage,
("/runner/{0}" -f $runnerName), "preflight",
"--job", "/job",
"--profile", ("/runner/{0}" -f $profileName),
"--valid-fov-root", "/valid-fov",
"--model-root", "/model"
)
Write-Output "PHASE=e5-preflight-start"
& docker @preflightArgs
Assert-LastExitCode "LAB E5 preflight"
Write-Output "PHASE=e5-preflight-complete"
$modelWasReady = Test-TritonModelReady "yolox_s"
$loadedByRun = $false
if (-not $modelWasReady) {
Invoke-WebRequest `
-Uri "http://127.0.0.1:8000/v2/repository/models/yolox_s/load" `
-Method Post `
-ContentType "application/json" `
-Body "{}" `
-UseBasicParsing `
-TimeoutSec 60 *> $null
$loadedByRun = $true
$deadline = [DateTime]::UtcNow.AddSeconds(60)
while (-not (Test-TritonModelReady "yolox_s")) {
if ([DateTime]::UtcNow -ge $deadline) {
throw "YOLOX-S did not become ready in Triton"
}
Start-Sleep -Milliseconds 500
}
}
Write-Output ("PHASE=e5-model-ready LOADED_BY_RUN={0}" -f $loadedByRun)
$runToken = [Guid]::NewGuid().ToString("N")
$workRoot = Join-Path $tmpRoot ("{0}-e5-{1}" -f $job.job_id, $runToken)
$framesRoot = Join-Path $workRoot "frames"
$streamPath = Join-Path $workRoot "camera.mp4"
$ptsPath = Join-Path $workRoot "pts.json"
$timelinePath = Join-Path $workRoot "timeline.jsonl"
$publishRoot = Join-Path $derivedRoot (".{0}-e5-{1}.publish" -f $job.job_id, $runToken)
$stagingRoot = Join-Path $publishRoot "output"
$null = New-Item -ItemType Directory -Path $framesRoot
$null = New-Item -ItemType Directory -Path $publishRoot
$totalWatch = [Diagnostics.Stopwatch]::StartNew()
$completed = $false
Write-Output "PHASE=e5-private-staging-created"
try {
$stream = [IO.File]::Open(
$streamPath,
[IO.FileMode]::CreateNew,
[IO.FileAccess]::Write,
[IO.FileShare]::None
)
try {
$input = [IO.File]::OpenRead($initPath)
try {
$input.CopyTo($stream)
}
finally {
$input.Dispose()
}
for ($sequence = 1; $sequence -le ($EndFrame + 1); $sequence++) {
$path = Assert-RegularFile (
Join-Path $segmentsRoot ("{0}.m4s" -f $sequence)
) "Camera segment"
$input = [IO.File]::OpenRead($path)
try {
$input.CopyTo($stream)
}
finally {
$input.Dispose()
}
if ($sequence % 500 -eq 0 -or $sequence -eq ($EndFrame + 1)) {
Write-Output (
"PHASE=e5-stream-reconstruction SEGMENTS={0}/{1}" -f
$sequence, ($EndFrame + 1)
)
}
}
$stream.Flush($true)
}
finally {
$stream.Dispose()
}
$null = Assert-FreeSpace "post-stream-reconstruction"
$extractWatch = [Diagnostics.Stopwatch]::StartNew()
Write-Output "PHASE=e5-frame-extraction-start"
$selectFilter = "select='between(n,{0},{1})'" -f $StartFrame, $EndFrame
& ffmpeg `
-hide_banner -loglevel fatal `
-i $streamPath `
-map 0:v:0 `
-vf $selectFilter `
-fps_mode passthrough `
(Join-Path $framesRoot "frame-%06d.png")
Assert-LastExitCode "LAB E5 camera clip extraction"
& ffprobe `
-v error `
-select_streams v:0 `
-show_entries frame=best_effort_timestamp_time `
-of json `
$streamPath | Set-Content -LiteralPath $ptsPath -Encoding utf8
Assert-LastExitCode "LAB E5 camera timestamp probe"
$decodedFrames = @(Get-ChildItem -LiteralPath $framesRoot -File -Filter "frame-*.png")
$ptsDocument = Get-Content -LiteralPath $ptsPath -Raw | ConvertFrom-Json
$pts = @($ptsDocument.frames)
if ($decodedFrames.Count -ne $clipFrameCount -or $pts.Count -ne ($EndFrame + 1)) {
throw "Decoded LAB E5 frame/timestamp count differs from the selected clip"
}
$firstEpochSeconds = [double]::Parse(
([string]$pts[0].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
)
$previousSessionSeconds = -1.0
$timelineWriter = [IO.StreamWriter]::new(
$timelinePath,
$false,
[Text.UTF8Encoding]::new($false)
)
try {
for ($localIndex = 0; $localIndex -lt $clipFrameCount; $localIndex++) {
$sourceIndex = $StartFrame + $localIndex
$epochSeconds = [double]::Parse(
([string]$pts[$sourceIndex].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
) - $firstEpochSeconds
$sessionSeconds = $timelineStart + $epochSeconds
if ($sessionSeconds -le $previousSessionSeconds) {
throw "Decoded LAB E5 clip timestamps are not strictly monotonic"
}
$row = [ordered]@{
frame_index = $localIndex
sequence = $localIndex + 1
source_frame_index = $sourceIndex
source_sequence = $sourceIndex + 1
epoch_seconds = $epochSeconds
session_seconds = $sessionSeconds
}
$timelineWriter.WriteLine(($row | ConvertTo-Json -Compress))
$previousSessionSeconds = $sessionSeconds
}
$timelineWriter.Flush()
}
finally {
$timelineWriter.Dispose()
}
$extractWatch.Stop()
$freeBytesPostExtract = Assert-FreeSpace "post-frame-extraction"
Write-Output ("PHASE=e5-frame-extraction-complete FRAMES={0}" -f $clipFrameCount)
$runArgs = @(
"run", "--rm", "--gpus", "all",
"--network", ("container:{0}" -f $TritonContainer),
"--read-only",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "256", "--shm-size", "1g",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=512m",
"-e", "PYTHONPATH=/runner:/opt/env",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"-v", ((Convert-ToDockerPath $model) + ":/model:ro"),
"-v", ((Convert-ToDockerPath $pythonEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $framesRoot) + ":/frames:ro"),
"-v", ((Convert-ToDockerPath $workRoot) + ":/work:ro"),
"-v", ((Convert-ToDockerPath $publishRoot) + ":/publish:rw"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3",
$ContainerImage,
("/runner/{0}" -f $runnerName), "run",
"--job", "/job",
"--profile", ("/runner/{0}" -f $profileName),
"--valid-fov-root", "/valid-fov",
"--model-root", "/model",
"--frames", "/frames",
"--timeline", "/work/timeline.jsonl",
"--output", "/publish/output",
"--triton-url", "http://127.0.0.1:8000",
"--free-bytes-floor", [string]([int64]$FreeGiBFloor * 1GB),
"--orchestrator-sha256", $orchestratorSha256,
"--container-image", $ContainerImage,
"--telemetry-interval-seconds", "1"
)
Write-Output ("PHASE=e5-inference-start FRAMES={0}" -f $clipFrameCount)
& docker @runArgs
Assert-LastExitCode "LAB E5 detector/tracker inference"
$freeBytesPostInference = Assert-FreeSpace "post-inference"
Write-Output "PHASE=e5-inference-complete"
$timelineRows = @(Get-Content -LiteralPath $timelinePath | ForEach-Object {
$_ | ConvertFrom-Json
})
$clipSpanSeconds = [double]$timelineRows[-1].session_seconds - [double]$timelineRows[0].session_seconds
if ($clipSpanSeconds -le 0) {
throw "LAB E5 clip duration is invalid"
}
$averageFps = ($clipFrameCount - 1) / $clipSpanSeconds
$fpsText = $averageFps.ToString("0.#########", [Globalization.CultureInfo]::InvariantCulture)
$videoPath = Join-Path $stagingRoot "tracking.mp4"
$encodeWatch = [Diagnostics.Stopwatch]::StartNew()
& ffmpeg `
-hide_banner -loglevel error `
-framerate $fpsText `
-i (Join-Path $stagingRoot "overlay-frames\frame-%06d.png") `
-c:v h264_nvenc `
-preset p4 `
-tune hq `
-rc vbr `
-cq 21 `
-b:v 0 `
-pix_fmt yuv420p `
-movflags +faststart `
$videoPath
Assert-LastExitCode "LAB E5 video encoding"
$encodeWatch.Stop()
Write-Output "PHASE=e5-video-encoding-complete"
$contactSheetPath = Join-Path $stagingRoot "contact-sheet.png"
$tileColumns = if ($clipFrameCount -le 60) { 4 } else { 3 }
$tileRows = 2
$sampleInterval = [math]::Max(0.25, $clipSpanSeconds / ($tileColumns * $tileRows))
$sampleText = $sampleInterval.ToString("0.######", [Globalization.CultureInfo]::InvariantCulture)
$contactFilter = "fps=1/{0},scale=400:-1,tile={1}x{2}:padding=8:margin=8:color=black" -f `
$sampleText, $tileColumns, $tileRows
& ffmpeg `
-hide_banner -loglevel error `
-i $videoPath `
-vf $contactFilter `
-frames:v 1 `
$contactSheetPath
Assert-LastExitCode "LAB E5 contact-sheet generation"
$probe = & ffprobe `
-v error `
-select_streams v:0 `
-count_frames `
-show_entries stream=codec_name,width,height,nb_read_frames `
-of json `
$videoPath | ConvertFrom-Json
Assert-LastExitCode "LAB E5 result video probe"
$videoStream = @($probe.streams)[0]
if (
$videoStream.codec_name -ne "h264" -or
[int]$videoStream.width -ne 800 -or
[int]$videoStream.height -ne 600 -or
[int]$videoStream.nb_read_frames -ne $clipFrameCount
) {
throw "LAB E5 result video contract changed"
}
$freeBytesPostArtifacts = Assert-FreeSpace "post-artifacts"
$totalWatch.Stop()
$finalizeArgs = @(
"run", "--rm", "--network", "none", "--read-only",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "128", "--tmpfs", "/tmp:rw,noexec,nosuid,size=256m",
"-e", "PYTHONPATH=/runner",
"-v", ((Convert-ToDockerPath $stagingRoot) + ":/output:rw"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3",
$ContainerImage,
("/runner/{0}" -f $runnerName), "finalize",
"--output", "/output",
"--video", "/output/tracking.mp4",
"--contact-sheet", "/output/contact-sheet.png",
"--extract-seconds", $extractWatch.Elapsed.TotalSeconds.ToString("0.######", [Globalization.CultureInfo]::InvariantCulture),
"--encode-seconds", $encodeWatch.Elapsed.TotalSeconds.ToString("0.######", [Globalization.CultureInfo]::InvariantCulture),
"--wall-seconds", $totalWatch.Elapsed.TotalSeconds.ToString("0.######", [Globalization.CultureInfo]::InvariantCulture),
"--encoder", "ffmpeg-h264_nvenc-p4-cq21-yuv420p-faststart",
"--disk-free-before-bytes", [string]$freeBytesBefore,
"--disk-free-post-extract-bytes", [string]$freeBytesPostExtract,
"--disk-free-post-inference-bytes", [string]$freeBytesPostInference,
"--disk-free-post-artifacts-bytes", [string]$freeBytesPostArtifacts,
"--disk-floor-bytes", [string]([int64]$FreeGiBFloor * 1GB),
"--working-set-reserve-bytes", [string]$workingSetReserve
)
& docker @finalizeArgs
Assert-LastExitCode "LAB E5 result finalization"
$result = Get-Content -LiteralPath (Join-Path $stagingRoot "result.json") -Raw | ConvertFrom-Json
if (
$result.schema_version -ne "missioncore.e5-tracking-result/v1" -or
$result.result_id -notmatch "^e5-tracking-[a-f0-9]{64}$" -or
[int]$result.frames_processed -ne $clipFrameCount -or
$result.ground_truth -ne $false -or
$result.publication_scope -ne "qualification-clip-only"
) {
throw "Final LAB E5 result manifest is incompatible"
}
$finalRoot = Join-Path $derivedRoot ([string]$result.result_id)
if (Test-Path -LiteralPath $finalRoot) {
throw "An immutable result with the same LAB E5 identity already exists: $finalRoot"
}
$overlayFrames = Join-Path $stagingRoot "overlay-frames"
if (Test-Path -LiteralPath $overlayFrames) {
Remove-Item -LiteralPath $overlayFrames -Recurse -Force
}
Move-Item -LiteralPath $stagingRoot -Destination $finalRoot
Remove-Item -LiteralPath $publishRoot -Force
$completed = $true
$freeBytesFinal = Assert-FreeSpace "post-publication"
Write-Output ("RESULT_ROOT={0}" -f $finalRoot)
Write-Output ("RESULT_ID={0}" -f $result.result_id)
Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBytesBefore)
Write-Output ("DISK_FREE_BYTES_POST_EXTRACT={0}" -f $freeBytesPostExtract)
Write-Output ("DISK_FREE_BYTES_POST_INFERENCE={0}" -f $freeBytesPostInference)
Write-Output ("DISK_FREE_BYTES_POST_ARTIFACTS={0}" -f $freeBytesPostArtifacts)
Write-Output ("DISK_FREE_BYTES_FINAL={0}" -f $freeBytesFinal)
}
finally {
if (Test-Path -LiteralPath $workRoot) {
Remove-Item -LiteralPath $workRoot -Recurse -Force
}
if (-not $completed -and (Test-Path -LiteralPath $publishRoot)) {
Remove-Item -LiteralPath $publishRoot -Recurse -Force
}
if ($loadedByRun) {
try {
Invoke-WebRequest `
-Uri "http://127.0.0.1:8000/v2/repository/models/yolox_s/unload" `
-Method Post `
-ContentType "application/json" `
-Body "{}" `
-UseBasicParsing `
-TimeoutSec 60 *> $null
Write-Output "PHASE=e5-model-state-restored"
}
catch {
Write-Warning "LAB E5 could not restore the prior unloaded YOLOX-S state"
}
}
}

View File

@ -0,0 +1,411 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$JobRoot,
[Parameter(Mandatory = $true)]
[string]$RunnerPath,
[Parameter(Mandatory = $true)]
[string]$ProfilePath,
[Parameter(Mandatory = $true)]
[string]$ValidFovRoot,
[ValidateRange(0, 1000000)]
[int]$StartFrame = 1000,
[ValidateRange(0, 1000000)]
[int]$EndFrame = 1600,
[ValidateRange(1, 1000)]
[int]$FreeGiBFloor = 360,
[string]$RuntimeRoot = "D:\NDC_MISSIONCORE\runtime",
[string]$ModelRoot = "D:\NDC_MISSIONCORE\runtime\models\yolox_s",
[string]$TritonContainer = "mission-core-triton",
[string]$ContainerImage = "nvcr.io/nvidia/tritonserver:26.06-py3"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-LastExitCode {
param([string]$Operation)
if ($LASTEXITCODE -ne 0) {
throw "$Operation failed with exit code $LASTEXITCODE"
}
}
function Assert-RegularFile {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if (-not $item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a regular file"
}
function Assert-Directory {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if ($item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a real directory"
}
function Assert-DDrivePath {
param([string]$Path, [string]$Label)
$fullPath = [IO.Path]::GetFullPath($Path)
if ([IO.Path]::GetPathRoot($fullPath).TrimEnd("\") -ine "D:") {
throw "$Label must be stored on D:"
}
return $fullPath
}
function Convert-ToDockerPath {
param([string]$Path)
return $Path.Replace("\", "/")
}
function Get-DFreeBytes {
return [int64](Get-PSDrive -Name D).Free
}
function Assert-FreeSpace {
param([string]$Phase, [int64]$RequiredAdditionalBytes = 0)
$freeBytes = Get-DFreeBytes
$floorBytes = [int64]$FreeGiBFloor * 1GB
Write-Host (
"DISK_GUARD PHASE={0} DRIVE=D FREE_BYTES={1} FREE_GIB={2} FLOOR_GIB={3} REQUIRED_ADDITIONAL_GIB={4}" -f
$Phase,
$freeBytes,
[math]::Round($freeBytes / 1GB, 3),
$FreeGiBFloor,
[math]::Round($RequiredAdditionalBytes / 1GB, 3)
)
if ($freeBytes -lt ($floorBytes + $RequiredAdditionalBytes)) {
throw "D: does not have the guarded LAB E8 working-set reserve during $Phase"
}
return $freeBytes
}
function Test-TritonModelReady {
try {
$response = Invoke-WebRequest `
-Uri "http://127.0.0.1:8000/v2/models/yolox_s/ready" `
-Method Get -UseBasicParsing -TimeoutSec 10
return $response.StatusCode -eq 200
}
catch {
return $false
}
}
$jobDirectory = Assert-DDrivePath (
Assert-Directory (Resolve-Path -LiteralPath $JobRoot).Path "Job root"
) "Job root"
$runner = Assert-DDrivePath (
Assert-RegularFile (Resolve-Path -LiteralPath $RunnerPath).Path "LAB E8 runner"
) "LAB E8 runner"
$profile = Assert-DDrivePath (
Assert-RegularFile (Resolve-Path -LiteralPath $ProfilePath).Path "LAB E8 profile"
) "LAB E8 profile"
$validFov = Assert-DDrivePath (
Assert-Directory (Resolve-Path -LiteralPath $ValidFovRoot).Path "Valid-FOV root"
) "Valid-FOV root"
$runtime = Assert-DDrivePath (
Assert-Directory (Resolve-Path -LiteralPath $RuntimeRoot).Path "Runtime root"
) "Runtime root"
$model = Assert-DDrivePath (
Assert-Directory (Resolve-Path -LiteralPath $ModelRoot).Path "YOLOX model root"
) "YOLOX model root"
$runnerRoot = Split-Path $runner -Parent
if ((Split-Path $profile -Parent) -ne $runnerRoot) {
throw "LAB E8 runner and profile must share one read-only mount"
}
foreach ($dependency in @(
"run_e5_instance_tracking.py",
"run_recorded_perception_epoch.py",
"run_e3_rectified_segmentation.py",
"run_evaluation_prelabels.py"
)) {
$null = Assert-RegularFile (Join-Path $runnerRoot $dependency) "LAB E8 runner dependency"
}
$job = Get-Content -LiteralPath (Join-Path $jobDirectory "job.json") -Raw | ConvertFrom-Json
if ($job.schema_version -ne "missioncore.compute-job/v1" -or $job.job_id -ne (Split-Path $jobDirectory -Leaf)) {
throw "Compute job manifest is incompatible"
}
$sourceId = [string]$job.input.source_id
$epoch = [int]$job.input.codec_epoch
$fullFrameCount = [int]$job.input.segment_count
$clipFrameCount = $EndFrame - $StartFrame + 1
$timelineStart = [double]$job.input.timeline.start_seconds
if (
$sourceId -ne "sensor.camera.right" -or
$StartFrame -lt 0 -or
$EndFrame -lt $StartFrame -or
$EndFrame -ge $fullFrameCount -or
$clipFrameCount -lt 2
) {
throw "LAB E8 clip escapes the camera job"
}
Write-Output (
"PHASE=job-manifest-validated JOB={0} CLIP={1}-{2} FRAMES={3}" -f
$job.job_id, $StartFrame, $EndFrame, $clipFrameCount
)
$epochRoot = Assert-DDrivePath (
Assert-Directory (
Join-Path $jobDirectory ("input\camera\{0}\epoch-{1}" -f $sourceId, $epoch)
) "Camera epoch"
) "Camera epoch"
$initPath = Assert-RegularFile (Join-Path $epochRoot "init.mp4") "Camera init"
$segmentsRoot = Assert-Directory (Join-Path $epochRoot "segments") "Camera segments"
$derivedRoot = Join-Path $runtime "derived"
$tmpRoot = Join-Path $runtime "tmp"
$pythonEnvironment = Assert-DDrivePath (
Assert-Directory (Join-Path $derivedRoot "perception-p0-env-v1") "Python environment"
) "Python environment"
$null = New-Item -ItemType Directory -Path $derivedRoot -Force
$null = New-Item -ItemType Directory -Path $tmpRoot -Force
# LAB E8 has temporary decoded input but no per-frame output images. Reserve
# decoded RGB payload, partial stream and two GiB for filesystem/codec overhead.
$pixelWorkingSet = [int64]$clipFrameCount * 800 * 600 * 3
$partialStreamReserve = [int64][math]::Ceiling(
([int64]$job.input.byte_length * ($EndFrame + 1) / $fullFrameCount) * 1.2
)
$workingSetReserve = [int64][math]::Ceiling($pixelWorkingSet * 1.4) + $partialStreamReserve + 2GB
$freeBytesBefore = Assert-FreeSpace "preflight" $workingSetReserve
& docker image inspect $ContainerImage *> $null
Assert-LastExitCode "Existing container image inspection"
$tritonState = docker inspect --format "{{.State.Running}}" $TritonContainer
Assert-LastExitCode "Triton container inspection"
if ($tritonState.Trim().ToLowerInvariant() -ne "true") {
throw "LAB E8 requires the existing Triton container"
}
$runnerName = Split-Path $runner -Leaf
$profileName = Split-Path $profile -Leaf
$orchestratorSha256 = (Get-FileHash -LiteralPath $PSCommandPath -Algorithm SHA256).Hash.ToLowerInvariant()
$preflightArgs = @(
"run", "--rm", "--network", "none", "--read-only",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "128", "--tmpfs", "/tmp:rw,noexec,nosuid,size=256m",
"-e", "PYTHONPATH=/runner:/opt/env",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"-v", ((Convert-ToDockerPath $model) + ":/model:ro"),
"-v", ((Convert-ToDockerPath $pythonEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3", $ContainerImage,
("/runner/{0}" -f $runnerName), "preflight",
"--job", "/job",
"--profile", ("/runner/{0}" -f $profileName),
"--valid-fov-root", "/valid-fov",
"--model-root", "/model"
)
Write-Output "PHASE=e8-preflight-start"
& docker @preflightArgs
Assert-LastExitCode "LAB E8 preflight"
Write-Output "PHASE=e8-preflight-complete"
$modelWasReady = Test-TritonModelReady
$loadedByRun = $false
if (-not $modelWasReady) {
Invoke-WebRequest `
-Uri "http://127.0.0.1:8000/v2/repository/models/yolox_s/load" `
-Method Post -ContentType "application/json" -Body "{}" `
-UseBasicParsing -TimeoutSec 60 *> $null
$loadedByRun = $true
$deadline = [DateTime]::UtcNow.AddSeconds(60)
while (-not (Test-TritonModelReady)) {
if ([DateTime]::UtcNow -ge $deadline) {
throw "YOLOX-S did not become ready in Triton"
}
Start-Sleep -Milliseconds 500
}
}
Write-Output ("PHASE=e8-model-ready LOADED_BY_RUN={0}" -f $loadedByRun)
$runToken = [Guid]::NewGuid().ToString("N")
$workRoot = Join-Path $tmpRoot ("{0}-e8-{1}" -f $job.job_id, $runToken)
$framesRoot = Join-Path $workRoot "frames"
$streamPath = Join-Path $workRoot "camera.mp4"
$ptsPath = Join-Path $workRoot "pts.json"
$timelinePath = Join-Path $workRoot "timeline.jsonl"
$publishRoot = Join-Path $derivedRoot (".{0}-e8-{1}.publish" -f $job.job_id, $runToken)
$stagingRoot = Join-Path $publishRoot "output"
$null = New-Item -ItemType Directory -Path $framesRoot
$null = New-Item -ItemType Directory -Path $publishRoot
$totalWatch = [Diagnostics.Stopwatch]::StartNew()
$completed = $false
Write-Output "PHASE=e8-private-staging-created"
try {
$stream = [IO.File]::Open(
$streamPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None
)
try {
$input = [IO.File]::OpenRead($initPath)
try { $input.CopyTo($stream) } finally { $input.Dispose() }
for ($sequence = 1; $sequence -le ($EndFrame + 1); $sequence++) {
$path = Assert-RegularFile (
Join-Path $segmentsRoot ("{0}.m4s" -f $sequence)
) "Camera segment"
$input = [IO.File]::OpenRead($path)
try { $input.CopyTo($stream) } finally { $input.Dispose() }
}
$stream.Flush($true)
}
finally {
$stream.Dispose()
}
$null = Assert-FreeSpace "post-stream-reconstruction"
$extractWatch = [Diagnostics.Stopwatch]::StartNew()
Write-Output "PHASE=e8-frame-extraction-start"
$selectFilter = "select='between(n,{0},{1})'" -f $StartFrame, $EndFrame
& ffmpeg -hide_banner -loglevel fatal -i $streamPath -map 0:v:0 `
-vf $selectFilter -fps_mode passthrough (Join-Path $framesRoot "frame-%06d.png")
Assert-LastExitCode "LAB E8 camera clip extraction"
& ffprobe -v error -select_streams v:0 `
-show_entries frame=best_effort_timestamp_time -of json $streamPath |
Set-Content -LiteralPath $ptsPath -Encoding utf8
Assert-LastExitCode "LAB E8 camera timestamp probe"
$decodedFrames = @(Get-ChildItem -LiteralPath $framesRoot -File -Filter "frame-*.png")
$ptsDocument = Get-Content -LiteralPath $ptsPath -Raw | ConvertFrom-Json
$pts = @($ptsDocument.frames)
if ($decodedFrames.Count -ne $clipFrameCount -or $pts.Count -ne ($EndFrame + 1)) {
throw "Decoded LAB E8 frame/timestamp count differs from the selected clip"
}
$firstEpochSeconds = [double]::Parse(
([string]$pts[0].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
)
$previousSessionSeconds = -1.0
$timelineWriter = [IO.StreamWriter]::new(
$timelinePath, $false, [Text.UTF8Encoding]::new($false)
)
try {
for ($localIndex = 0; $localIndex -lt $clipFrameCount; $localIndex++) {
$sourceIndex = $StartFrame + $localIndex
$epochSeconds = [double]::Parse(
([string]$pts[$sourceIndex].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
) - $firstEpochSeconds
$sessionSeconds = $timelineStart + $epochSeconds
if ($sessionSeconds -le $previousSessionSeconds) {
throw "Decoded LAB E8 clip timestamps are not strictly monotonic"
}
$row = [ordered]@{
frame_index = $localIndex
sequence = $localIndex + 1
source_frame_index = $sourceIndex
source_sequence = $sourceIndex + 1
epoch_seconds = $epochSeconds
session_seconds = $sessionSeconds
}
$timelineWriter.WriteLine(($row | ConvertTo-Json -Compress))
$previousSessionSeconds = $sessionSeconds
}
$timelineWriter.Flush()
}
finally {
$timelineWriter.Dispose()
}
$extractWatch.Stop()
$freeBytesPostExtract = Assert-FreeSpace "post-frame-extraction"
Write-Output ("PHASE=e8-frame-extraction-complete FRAMES={0}" -f $clipFrameCount)
$runArgs = @(
"run", "--rm", "--gpus", "all",
"--network", ("container:{0}" -f $TritonContainer),
"--read-only", "--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "256", "--shm-size", "1g",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=512m",
"-e", "PYTHONPATH=/runner:/opt/env",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"-v", ((Convert-ToDockerPath $model) + ":/model:ro"),
"-v", ((Convert-ToDockerPath $pythonEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $framesRoot) + ":/frames:ro"),
"-v", ((Convert-ToDockerPath $workRoot) + ":/work:ro"),
"-v", ((Convert-ToDockerPath $publishRoot) + ":/publish:rw"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3", $ContainerImage,
("/runner/{0}" -f $runnerName), "run",
"--job", "/job",
"--profile", ("/runner/{0}" -f $profileName),
"--valid-fov-root", "/valid-fov",
"--model-root", "/model",
"--frames", "/frames",
"--timeline", "/work/timeline.jsonl",
"--output", "/publish/output",
"--triton-url", "http://127.0.0.1:8000",
"--free-bytes-floor", [string]([int64]$FreeGiBFloor * 1GB),
"--orchestrator-sha256", $orchestratorSha256,
"--container-image", $ContainerImage,
"--telemetry-interval-seconds", "1"
)
Write-Output ("PHASE=e8-source-paced-replay-start FRAMES={0}" -f $clipFrameCount)
& docker @runArgs
Assert-LastExitCode "LAB E8 source-paced detector/tracker"
$freeBytesPostReplay = Assert-FreeSpace "post-replay"
Write-Output "PHASE=e8-source-paced-replay-complete"
$result = Get-Content -LiteralPath (Join-Path $stagingRoot "result.json") -Raw | ConvertFrom-Json
if (
$result.schema_version -ne "missioncore.e8-realtime-tracking-result/v1" -or
$result.result_id -notmatch "^e8-realtime-tracking-[a-f0-9]{64}$" -or
$result.acceptance_state -ne "accepted" -or
$result.ground_truth -ne $false
) {
throw "Final LAB E8 result manifest is incompatible or rejected"
}
$finalRoot = Join-Path $derivedRoot ([string]$result.result_id)
if (Test-Path -LiteralPath $finalRoot) {
throw "An immutable result with the same LAB E8 identity already exists: $finalRoot"
}
Move-Item -LiteralPath $stagingRoot -Destination $finalRoot
Remove-Item -LiteralPath $publishRoot -Force
$totalWatch.Stop()
$completed = $true
$freeBytesFinal = Assert-FreeSpace "post-publication"
Write-Output ("RESULT_ROOT={0}" -f $finalRoot)
Write-Output ("RESULT_ID={0}" -f $result.result_id)
Write-Output ("PREPARATION_SECONDS={0}" -f $extractWatch.Elapsed.TotalSeconds)
Write-Output ("ORCHESTRATOR_WALL_SECONDS={0}" -f $totalWatch.Elapsed.TotalSeconds)
Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBytesBefore)
Write-Output ("DISK_FREE_BYTES_POST_EXTRACT={0}" -f $freeBytesPostExtract)
Write-Output ("DISK_FREE_BYTES_POST_REPLAY={0}" -f $freeBytesPostReplay)
Write-Output ("DISK_FREE_BYTES_FINAL={0}" -f $freeBytesFinal)
}
finally {
if (Test-Path -LiteralPath $workRoot) {
Remove-Item -LiteralPath $workRoot -Recurse -Force
}
if (-not $completed -and (Test-Path -LiteralPath $publishRoot)) {
Remove-Item -LiteralPath $publishRoot -Recurse -Force
}
if ($loadedByRun) {
try {
Invoke-WebRequest `
-Uri "http://127.0.0.1:8000/v2/repository/models/yolox_s/unload" `
-Method Post -ContentType "application/json" -Body "{}" `
-UseBasicParsing -TimeoutSec 60 *> $null
Write-Output "PHASE=e8-model-state-restored"
}
catch {
Write-Warning "LAB E8 could not restore the prior unloaded YOLOX-S state"
}
}
}

View File

@ -0,0 +1,349 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)] [string]$JobRoot,
[Parameter(Mandatory = $true)] [string]$RunnerPath,
[Parameter(Mandatory = $true)] [string]$ProfilePath,
[Parameter(Mandatory = $true)] [string]$DetectorProfilePath,
[Parameter(Mandatory = $true)] [string]$SemanticProfilePath,
[Parameter(Mandatory = $true)] [string]$ValidFovRoot,
[ValidateRange(0, 1000000)] [int]$StartFrame = 1000,
[ValidateRange(0, 1000000)] [int]$EndFrame = 1600,
[ValidateRange(1, 1000)] [int]$FreeGiBFloor = 360,
[string]$RuntimeRoot = "D:\NDC_MISSIONCORE\runtime",
[string]$ModelRoot = "D:\NDC_MISSIONCORE\runtime\models\yolox_s",
[string]$TritonContainer = "mission-core-triton",
[string]$ContainerImage = "nvcr.io/nvidia/tritonserver:26.06-py3"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-LastExitCode {
param([string]$Operation)
if ($LASTEXITCODE -ne 0) { throw "$Operation failed with exit code $LASTEXITCODE" }
}
function Resolve-DDirectory {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
$root = [IO.Path]::GetPathRoot($item.FullName).TrimEnd("\")
if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $root -ine "D:") {
throw "$Label must be a real D: directory"
}
return $item.FullName
}
function Resolve-DFile {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
$root = [IO.Path]::GetPathRoot($item.FullName).TrimEnd("\")
if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $root -ine "D:") {
throw "$Label must be a regular D: file"
}
return $item.FullName
}
function Convert-ToDockerPath {
param([string]$Path)
return $Path.Replace("\", "/")
}
function Get-DFreeBytes { return [int64](Get-PSDrive -Name D).Free }
function Assert-FreeSpace {
param([string]$Phase, [int64]$RequiredAdditionalBytes = 0)
$freeBytes = Get-DFreeBytes
$floorBytes = [int64]$FreeGiBFloor * 1GB
Write-Host (
"DISK_GUARD PHASE={0} DRIVE=D FREE_BYTES={1} FREE_GIB={2} FLOOR_GIB={3} REQUIRED_ADDITIONAL_GIB={4}" -f
$Phase, $freeBytes, [math]::Round($freeBytes / 1GB, 3), $FreeGiBFloor,
[math]::Round($RequiredAdditionalBytes / 1GB, 3)
)
if ($freeBytes -lt ($floorBytes + $RequiredAdditionalBytes)) {
throw "D: does not have the guarded LAB E9 working-set reserve during $Phase"
}
return $freeBytes
}
function Test-TritonModelReady {
try {
$response = Invoke-WebRequest -UseBasicParsing -TimeoutSec 10 `
-Uri "http://127.0.0.1:8000/v2/models/yolox_s/ready"
return $response.StatusCode -eq 200
}
catch { return $false }
}
$jobDirectory = Resolve-DDirectory $JobRoot "Job root"
$runner = Resolve-DFile $RunnerPath "LAB E9 runner"
$profile = Resolve-DFile $ProfilePath "LAB E9 profile"
$detectorProfile = Resolve-DFile $DetectorProfilePath "LAB E9 detector profile"
$semanticProfile = Resolve-DFile $SemanticProfilePath "LAB E9 semantic profile"
$validFov = Resolve-DDirectory $ValidFovRoot "Valid-FOV root"
$runtime = Resolve-DDirectory $RuntimeRoot "Runtime root"
$model = Resolve-DDirectory $ModelRoot "YOLOX model root"
$runnerRoot = Split-Path $runner -Parent
foreach ($path in @($profile, $detectorProfile, $semanticProfile)) {
if ((Split-Path $path -Parent) -ne $runnerRoot) {
throw "LAB E9 runner and profiles must share one read-only mount"
}
}
foreach ($dependency in @(
"run_e8_realtime_tracking.py",
"run_e5_instance_tracking.py",
"run_e4_full_session_segmentation.py",
"run_recorded_perception_epoch.py",
"run_e3_rectified_segmentation.py",
"run_evaluation_prelabels.py"
)) {
$null = Resolve-DFile (Join-Path $runnerRoot $dependency) "LAB E9 runner dependency"
}
$job = Get-Content -LiteralPath (Join-Path $jobDirectory "job.json") -Raw | ConvertFrom-Json
if ($job.schema_version -ne "missioncore.compute-job/v1" -or $job.job_id -ne (Split-Path $jobDirectory -Leaf)) {
throw "Compute job manifest is incompatible"
}
$sourceId = [string]$job.input.source_id
$epoch = [int]$job.input.codec_epoch
$fullFrameCount = [int]$job.input.segment_count
$clipFrameCount = $EndFrame - $StartFrame + 1
$timelineStart = [double]$job.input.timeline.start_seconds
if (
$sourceId -ne "sensor.camera.right" -or $StartFrame -lt 0 -or
$EndFrame -lt $StartFrame -or $EndFrame -ge $fullFrameCount -or $clipFrameCount -lt 2
) { throw "LAB E9 clip escapes the camera job" }
Write-Output (
"PHASE=job-manifest-validated JOB={0} CLIP={1}-{2} FRAMES={3}" -f
$job.job_id, $StartFrame, $EndFrame, $clipFrameCount
)
$epochRoot = Resolve-DDirectory (
Join-Path $jobDirectory ("input\camera\{0}\epoch-{1}" -f $sourceId, $epoch)
) "Camera epoch"
$initPath = Resolve-DFile (Join-Path $epochRoot "init.mp4") "Camera init"
$segmentsRoot = Resolve-DDirectory (Join-Path $epochRoot "segments") "Camera segments"
$derivedRoot = Join-Path $runtime "derived"
$tmpRoot = Join-Path $runtime "tmp"
$cacheRoot = Resolve-DDirectory (Join-Path $runtime "cache\perception-e3-models-v1") "EoMT cache"
$e3Environment = Resolve-DDirectory (Join-Path $derivedRoot "perception-e3-opencv413092-v1") "E3 environment"
$torchEnvironment = Resolve-DDirectory (Join-Path $derivedRoot "perception-p0-env-v1") "Torch environment"
$transformersEnvironment = Resolve-DDirectory (Join-Path $derivedRoot "perception-p0-transformers4576-v1") "Transformers environment"
$null = New-Item -ItemType Directory -Path $derivedRoot -Force
$null = New-Item -ItemType Directory -Path $tmpRoot -Force
$pixelWorkingSet = [int64]$clipFrameCount * 800 * 600 * 3
$partialStreamReserve = [int64][math]::Ceiling(
([int64]$job.input.byte_length * ($EndFrame + 1) / $fullFrameCount) * 1.2
)
$workingSetReserve = [int64][math]::Ceiling($pixelWorkingSet * 1.4) + $partialStreamReserve + 3GB
$freeBytesBefore = Assert-FreeSpace "preflight" $workingSetReserve
& docker image inspect $ContainerImage *> $null
Assert-LastExitCode "Existing container image inspection"
$tritonState = docker inspect --format "{{.State.Running}}" $TritonContainer
Assert-LastExitCode "Triton container inspection"
if ($tritonState.Trim().ToLowerInvariant() -ne "true") {
throw "LAB E9 requires the existing Triton container"
}
$runnerName = Split-Path $runner -Leaf
$profileName = Split-Path $profile -Leaf
$detectorProfileName = Split-Path $detectorProfile -Leaf
$semanticProfileName = Split-Path $semanticProfile -Leaf
$orchestratorSha256 = (Get-FileHash -LiteralPath $PSCommandPath -Algorithm SHA256).Hash.ToLowerInvariant()
$commonMounts = @(
"-e", "PYTHONPATH=/runner:/opt/transformers:/opt/env",
"-e", "TORCH_HOME=/cache/torch",
"-e", "HF_HOME=/cache/huggingface",
"-e", "HF_HUB_OFFLINE=1",
"-e", "TRANSFORMERS_OFFLINE=1",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"-v", ((Convert-ToDockerPath $model) + ":/model:ro"),
"-v", ((Convert-ToDockerPath $cacheRoot) + ":/cache:ro"),
"-v", ((Convert-ToDockerPath $e3Environment) + ":/environment:ro"),
"-v", ((Convert-ToDockerPath $torchEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $transformersEnvironment) + ":/opt/transformers:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro")
)
$commonRunnerArgs = @(
"--job", "/job",
"--profile", ("/runner/{0}" -f $profileName),
"--detector-profile", ("/runner/{0}" -f $detectorProfileName),
"--semantic-profile", ("/runner/{0}" -f $semanticProfileName),
"--valid-fov-root", "/valid-fov",
"--model-root", "/model",
"--cache", "/cache",
"--environment", "/environment"
)
$preflightArgs = @(
"run", "--rm", "--gpus", "all", "--network", "none", "--read-only",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "512", "--shm-size", "4g",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=2g"
) + $commonMounts + @(
"--entrypoint", "python3", $ContainerImage,
("/runner/{0}" -f $runnerName), "preflight"
) + $commonRunnerArgs
Write-Output "PHASE=e9-preflight-start"
& docker @preflightArgs
Assert-LastExitCode "LAB E9 preflight"
Write-Output "PHASE=e9-preflight-complete"
$modelWasReady = Test-TritonModelReady
$loadedByRun = $false
if (-not $modelWasReady) {
Invoke-WebRequest -UseBasicParsing -TimeoutSec 60 `
-Uri "http://127.0.0.1:8000/v2/repository/models/yolox_s/load" `
-Method Post -ContentType "application/json" -Body "{}" *> $null
$loadedByRun = $true
$deadline = [DateTime]::UtcNow.AddSeconds(60)
while (-not (Test-TritonModelReady)) {
if ([DateTime]::UtcNow -ge $deadline) { throw "YOLOX-S did not become ready" }
Start-Sleep -Milliseconds 500
}
}
Write-Output ("PHASE=e9-model-ready LOADED_BY_RUN={0}" -f $loadedByRun)
$runToken = [Guid]::NewGuid().ToString("N")
$workRoot = Join-Path $tmpRoot ("{0}-e9-{1}" -f $job.job_id, $runToken)
$framesRoot = Join-Path $workRoot "frames"
$streamPath = Join-Path $workRoot "camera.mp4"
$ptsPath = Join-Path $workRoot "pts.json"
$timelinePath = Join-Path $workRoot "timeline.jsonl"
$publishRoot = Join-Path $derivedRoot (".{0}-e9-{1}.publish" -f $job.job_id, $runToken)
$stagingRoot = Join-Path $publishRoot "output"
$null = New-Item -ItemType Directory -Path $framesRoot
$null = New-Item -ItemType Directory -Path $publishRoot
$completed = $false
$totalWatch = [Diagnostics.Stopwatch]::StartNew()
try {
$stream = [IO.File]::Open($streamPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
try {
$input = [IO.File]::OpenRead($initPath)
try { $input.CopyTo($stream) } finally { $input.Dispose() }
for ($sequence = 1; $sequence -le ($EndFrame + 1); $sequence++) {
$path = Resolve-DFile (Join-Path $segmentsRoot ("{0}.m4s" -f $sequence)) "Camera segment"
$input = [IO.File]::OpenRead($path)
try { $input.CopyTo($stream) } finally { $input.Dispose() }
}
$stream.Flush($true)
}
finally { $stream.Dispose() }
$null = Assert-FreeSpace "post-stream-reconstruction"
$extractWatch = [Diagnostics.Stopwatch]::StartNew()
$selectFilter = "select='between(n,{0},{1})'" -f $StartFrame, $EndFrame
& ffmpeg -hide_banner -loglevel fatal -i $streamPath -map 0:v:0 `
-vf $selectFilter -fps_mode passthrough (Join-Path $framesRoot "frame-%06d.png")
Assert-LastExitCode "LAB E9 camera clip extraction"
& ffprobe -v error -select_streams v:0 -show_entries frame=best_effort_timestamp_time `
-of json $streamPath | Set-Content -LiteralPath $ptsPath -Encoding utf8
Assert-LastExitCode "LAB E9 camera timestamp probe"
$decodedFrames = @(Get-ChildItem -LiteralPath $framesRoot -File -Filter "frame-*.png")
$pts = @((Get-Content -LiteralPath $ptsPath -Raw | ConvertFrom-Json).frames)
if ($decodedFrames.Count -ne $clipFrameCount -or $pts.Count -ne ($EndFrame + 1)) {
throw "Decoded LAB E9 frame/timestamp count changed"
}
$firstEpochSeconds = [double]::Parse(
([string]$pts[0].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
)
$previousSessionSeconds = -1.0
$timelineWriter = [IO.StreamWriter]::new(
$timelinePath, $false, [Text.UTF8Encoding]::new($false)
)
try {
for ($localIndex = 0; $localIndex -lt $clipFrameCount; $localIndex++) {
$sourceIndex = $StartFrame + $localIndex
$epochSeconds = [double]::Parse(
([string]$pts[$sourceIndex].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
) - $firstEpochSeconds
$sessionSeconds = $timelineStart + $epochSeconds
if ($sessionSeconds -le $previousSessionSeconds) { throw "LAB E9 timeline is not monotonic" }
$row = [ordered]@{
frame_index = $localIndex
sequence = $localIndex + 1
source_frame_index = $sourceIndex
source_sequence = $sourceIndex + 1
epoch_seconds = $epochSeconds
session_seconds = $sessionSeconds
}
$timelineWriter.WriteLine(($row | ConvertTo-Json -Compress))
$previousSessionSeconds = $sessionSeconds
}
$timelineWriter.Flush()
}
finally { $timelineWriter.Dispose() }
$extractWatch.Stop()
$freeBytesPostExtract = Assert-FreeSpace "post-frame-extraction"
Write-Output ("PHASE=e9-frame-extraction-complete FRAMES={0}" -f $clipFrameCount)
$runArgs = @(
"run", "--rm", "--gpus", "all", "--network", ("container:{0}" -f $TritonContainer),
"--read-only", "--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "512", "--shm-size", "4g",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=2g"
) + $commonMounts + @(
"-v", ((Convert-ToDockerPath $framesRoot) + ":/frames:ro"),
"-v", ((Convert-ToDockerPath $workRoot) + ":/work:ro"),
"-v", ((Convert-ToDockerPath $publishRoot) + ":/publish:rw"),
"--entrypoint", "python3", $ContainerImage,
("/runner/{0}" -f $runnerName), "run"
) + $commonRunnerArgs + @(
"--frames", "/frames",
"--timeline", "/work/timeline.jsonl",
"--output", "/publish/output",
"--triton-url", "http://127.0.0.1:8000",
"--free-bytes-floor", [string]([int64]$FreeGiBFloor * 1GB),
"--orchestrator-sha256", $orchestratorSha256,
"--container-image", $ContainerImage,
"--telemetry-interval-seconds", "1"
)
Write-Output ("PHASE=e9-multirate-replay-start FRAMES={0}" -f $clipFrameCount)
& docker @runArgs
Assert-LastExitCode "LAB E9 multirate replay"
$freeBytesPostReplay = Assert-FreeSpace "post-replay"
$result = Get-Content -LiteralPath (Join-Path $stagingRoot "result.json") -Raw | ConvertFrom-Json
if (
$result.schema_version -ne "missioncore.e9-multirate-perception-result/v1" -or
$result.result_id -notmatch "^e9-multirate-perception-[a-f0-9]{64}$" -or
$result.acceptance_state -notin @("accepted", "rejected") -or
$result.ground_truth -ne $false
) { throw "LAB E9 result manifest is incompatible" }
$finalRoot = Join-Path $derivedRoot ([string]$result.result_id)
if (Test-Path -LiteralPath $finalRoot) { throw "Immutable LAB E9 result already exists" }
Move-Item -LiteralPath $stagingRoot -Destination $finalRoot
Remove-Item -LiteralPath $publishRoot -Force
$completed = $true
$totalWatch.Stop()
$freeBytesFinal = Assert-FreeSpace "post-publication"
Write-Output ("RESULT_ROOT={0}" -f $finalRoot)
Write-Output ("RESULT_ID={0}" -f $result.result_id)
Write-Output ("ACCEPTANCE_STATE={0}" -f $result.acceptance_state)
Write-Output ("PREPARATION_SECONDS={0}" -f $extractWatch.Elapsed.TotalSeconds)
Write-Output ("ORCHESTRATOR_WALL_SECONDS={0}" -f $totalWatch.Elapsed.TotalSeconds)
Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBytesBefore)
Write-Output ("DISK_FREE_BYTES_POST_EXTRACT={0}" -f $freeBytesPostExtract)
Write-Output ("DISK_FREE_BYTES_POST_REPLAY={0}" -f $freeBytesPostReplay)
Write-Output ("DISK_FREE_BYTES_FINAL={0}" -f $freeBytesFinal)
}
finally {
if (Test-Path -LiteralPath $workRoot) { Remove-Item -LiteralPath $workRoot -Recurse -Force }
if (-not $completed -and (Test-Path -LiteralPath $publishRoot)) {
Remove-Item -LiteralPath $publishRoot -Recurse -Force
}
if ($loadedByRun) {
try {
Invoke-WebRequest -UseBasicParsing -TimeoutSec 60 `
-Uri "http://127.0.0.1:8000/v2/repository/models/yolox_s/unload" `
-Method Post -ContentType "application/json" -Body "{}" *> $null
Write-Output "PHASE=e9-model-state-restored"
}
catch { Write-Warning "LAB E9 could not restore the prior unloaded YOLOX-S state" }
}
}

View File

@ -0,0 +1,133 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$EvaluationPack,
[Parameter(Mandatory = $true)]
[string]$ValidFovRoot,
[Parameter(Mandatory = $true)]
[string]$RunnerPath,
[Parameter(Mandatory = $true)]
[string]$BaselineRunnerPath,
[string]$RuntimeRoot = "D:\NDC_MISSIONCORE\runtime"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-LastExitCode {
param([string]$Operation)
if ($LASTEXITCODE -ne 0) {
throw "$Operation failed with exit code $LASTEXITCODE"
}
}
function Assert-RegularFile {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if (-not $item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a regular file"
}
function Assert-Directory {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if ($item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a real directory"
}
function Convert-ToDockerPath {
param([string]$Path)
return $Path.Replace("\", "/")
}
$packRoot = Assert-Directory (Resolve-Path -LiteralPath $EvaluationPack).Path "Evaluation pack"
$validFov = Assert-Directory (Resolve-Path -LiteralPath $ValidFovRoot).Path "Valid-FOV root"
$runner = Assert-RegularFile (Resolve-Path -LiteralPath $RunnerPath).Path "Prelabel runner"
$baselineRunner = Assert-RegularFile (Resolve-Path -LiteralPath $BaselineRunnerPath).Path "Baseline runner"
if ((Split-Path $runner -Parent) -ne (Split-Path $baselineRunner -Parent)) {
throw "Prelabel and baseline runners must share one read-only mount"
}
$runtime = Assert-Directory (Resolve-Path -LiteralPath $RuntimeRoot).Path "Runtime root"
$pack = Get-Content -LiteralPath (Join-Path $packRoot "manifest.json") -Raw | ConvertFrom-Json
if (
$pack.schema_version -ne "missioncore.perception-evaluation-pack/v1" -or
$pack.generation_id -ne (Split-Path $packRoot -Leaf) -or
$pack.identity.preprocessing_profile -ne "fixed-valid-fov-fill/v1"
) {
throw "Evaluation pack manifest is incompatible"
}
$null = Assert-RegularFile (Join-Path $validFov "manifest.json") "Valid-FOV manifest"
$null = Assert-RegularFile (Join-Path $validFov "mask.png") "Valid-FOV mask"
$cacheRoot = Assert-Directory (Join-Path $runtime "cache\perception-p0-models-v1") "Model cache"
$torchEnvironment = Assert-Directory (Join-Path $runtime "derived\perception-p0-env-v1") "Torch environment"
$transformersEnvironment = Assert-Directory (Join-Path $runtime "derived\perception-p0-transformers4576-v1") "Transformers environment"
$runnerRoot = Split-Path $runner -Parent
$runnerName = Split-Path $runner -Leaf
$derivedRoot = Join-Path $runtime "derived\evaluation-prelabels"
$null = New-Item -ItemType Directory -Path $derivedRoot -Force
$runToken = [Guid]::NewGuid().ToString("N")
$publishRoot = Join-Path $derivedRoot (".{0}-{1}.publish" -f $pack.generation_id, $runToken)
$stagingRoot = Join-Path $publishRoot "output"
$null = New-Item -ItemType Directory -Path $publishRoot
try {
$dockerArgs = @(
"run", "--rm", "--gpus", "all", "--network", "none",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--shm-size", "2g",
"-e", "PYTHONPATH=/opt/transformers:/opt/env",
"-e", "TORCH_HOME=/cache/torch",
"-e", "HF_HOME=/cache/huggingface",
"-e", "HF_HUB_OFFLINE=1",
"-e", "TRANSFORMERS_OFFLINE=1",
"-v", ((Convert-ToDockerPath $packRoot) + ":/evaluation-pack:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"-v", ((Convert-ToDockerPath $publishRoot) + ":/publish:rw"),
"-v", ((Convert-ToDockerPath $cacheRoot) + ":/cache:rw"),
"-v", ((Convert-ToDockerPath $torchEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $transformersEnvironment) + ":/opt/transformers:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3",
"nvcr.io/nvidia/tritonserver:26.06-py3",
("/runner/{0}" -f $runnerName),
"--evaluation-pack", "/evaluation-pack",
"--valid-fov-root", "/valid-fov",
"--output", "/publish/output",
"--cache", "/cache",
"--telemetry-interval-seconds", "1"
)
Write-Output ("PHASE=e2-prelabels-start PACK={0}" -f $pack.generation_id)
& docker @dockerArgs
Assert-LastExitCode "E2 prelabel generation"
$result = Get-Content -LiteralPath (Join-Path $stagingRoot "result.json") -Raw | ConvertFrom-Json
if (
$result.schema_version -ne "missioncore.perception-evaluation-prelabels/v1" -or
$result.result_id -notmatch "^evaluation-prelabels-[a-f0-9]{64}$" -or
$result.identity.evaluation_pack_id -ne $pack.generation_id
) {
throw "E2 prelabel result manifest is incompatible"
}
$finalRoot = Join-Path $derivedRoot ([string]$result.result_id)
if (Test-Path -LiteralPath $finalRoot) {
throw "An immutable E2 prelabel result with the same identity already exists: $finalRoot"
}
Move-Item -LiteralPath $stagingRoot -Destination $finalRoot
Remove-Item -LiteralPath $publishRoot -Force
Write-Output ("RESULT_ROOT={0}" -f $finalRoot)
Write-Output ("RESULT_ID={0}" -f $result.result_id)
}
finally {
if (Test-Path -LiteralPath $publishRoot) {
Remove-Item -LiteralPath $publishRoot -Recurse -Force
}
}

View File

@ -0,0 +1,276 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$JobRoot,
[Parameter(Mandatory = $true)]
[string]$RunnerPath,
[Parameter(Mandatory = $true)]
[string]$BaselineRunnerPath,
[Parameter(Mandatory = $true)]
[string]$QualificationManifest,
[Parameter(Mandatory = $true)]
[string]$ValidFovRoot,
[string]$RuntimeRoot = "D:\NDC_MISSIONCORE\runtime",
[string]$CalibrationSha256 = "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9",
[string]$CalibrationSlot = "camera_1"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-LastExitCode {
param([string]$Operation)
if ($LASTEXITCODE -ne 0) {
throw "$Operation failed with exit code $LASTEXITCODE"
}
}
function Assert-RegularFile {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if (-not $item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a regular file"
}
function Assert-Directory {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if ($item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a real directory"
}
function Convert-ToDockerPath {
param([string]$Path)
return $Path.Replace("\", "/")
}
$jobDirectory = Assert-Directory (Resolve-Path -LiteralPath $JobRoot).Path "Job root"
$runner = Assert-RegularFile (Resolve-Path -LiteralPath $RunnerPath).Path "Qualification runner"
$baselineRunner = Assert-RegularFile (Resolve-Path -LiteralPath $BaselineRunnerPath).Path "Baseline runner"
$qualification = Assert-RegularFile (Resolve-Path -LiteralPath $QualificationManifest).Path "Qualification manifest"
$validFov = Assert-Directory (Resolve-Path -LiteralPath $ValidFovRoot).Path "Valid-FOV root"
$null = Assert-RegularFile (Join-Path $validFov "manifest.json") "Valid-FOV manifest"
$null = Assert-RegularFile (Join-Path $validFov "mask.png") "Valid-FOV mask"
if ((Split-Path $runner -Parent) -ne (Split-Path $baselineRunner -Parent)) {
throw "Qualification and baseline runners must share one read-only mount"
}
$runtime = Assert-Directory (Resolve-Path -LiteralPath $RuntimeRoot).Path "Runtime root"
$job = Get-Content -LiteralPath (Join-Path $jobDirectory "job.json") -Raw | ConvertFrom-Json
if ($job.schema_version -ne "missioncore.compute-job/v1" -or $job.job_id -ne (Split-Path $jobDirectory -Leaf)) {
throw "Compute job manifest is incompatible"
}
if ($CalibrationSha256 -notmatch "^[a-f0-9]{64}$" -or $CalibrationSlot -notmatch "^[A-Za-z0-9._-]+$") {
throw "Calibration binding is invalid"
}
$sourceId = [string]$job.input.source_id
$epoch = [int]$job.input.codec_epoch
$frameCount = [int]$job.input.segment_count
$timelineStart = [double]$job.input.timeline.start_seconds
$timelineEnd = [double]$job.input.timeline.end_seconds
$timelineDuration = $timelineEnd - $timelineStart
if ($frameCount -lt 1 -or $timelineDuration -le 0) {
throw "Compute job frame/timeline contract is invalid"
}
$qualificationDocument = Get-Content -LiteralPath $qualification -Raw | ConvertFrom-Json
if (
$qualificationDocument.schema_version -ne "missioncore.recorded-qualification-slice/v1" -or
$qualificationDocument.identity.job_id -ne $job.job_id -or
$qualificationDocument.identity.input_sha256 -ne $job.input_sha256
) {
throw "Qualification manifest is not bound to this job"
}
Write-Output ("PHASE=inputs-validated QUALIFICATION_FRAMES={0}" -f @($qualificationDocument.frames).Count)
$epochRoot = Join-Path $jobDirectory ("input\camera\{0}\epoch-{1}" -f $sourceId, $epoch)
$epochRoot = Assert-Directory $epochRoot "Camera epoch"
$initPath = Assert-RegularFile (Join-Path $epochRoot "init.mp4") "Camera init"
$segmentsRoot = Assert-Directory (Join-Path $epochRoot "segments") "Camera segments"
$derivedRoot = Join-Path $runtime "derived\qualification"
$tmpRoot = Join-Path $runtime "tmp"
$cacheRoot = Assert-Directory (Join-Path $runtime "cache\perception-p0-models-v1") "Model cache"
$torchEnvironment = Assert-Directory (Join-Path $runtime "derived\perception-p0-env-v1") "Torch environment"
$transformersEnvironment = Assert-Directory (Join-Path $runtime "derived\perception-p0-transformers4576-v1") "Transformers environment"
$runnerRoot = Split-Path $runner -Parent
$runnerName = Split-Path $runner -Leaf
$baselineRunnerName = Split-Path $baselineRunner -Leaf
$qualificationRoot = Split-Path $qualification -Parent
$qualificationName = Split-Path $qualification -Leaf
$null = New-Item -ItemType Directory -Path $derivedRoot -Force
$null = New-Item -ItemType Directory -Path $tmpRoot -Force
$preflightArgs = @(
"run", "--rm", "--gpus", "all", "--network", "none",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"-e", "PYTHONPATH=/opt/transformers:/opt/env",
"-e", "TORCH_HOME=/cache/torch",
"-e", "HF_HOME=/cache/huggingface",
"-e", "HF_HUB_OFFLINE=1",
"-e", "TRANSFORMERS_OFFLINE=1",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $cacheRoot) + ":/cache:rw"),
"-v", ((Convert-ToDockerPath $torchEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $transformersEnvironment) + ":/opt/transformers:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3",
"nvcr.io/nvidia/tritonserver:26.06-py3",
("/runner/{0}" -f $baselineRunnerName), "preflight",
"--job", "/job",
"--cache", "/cache"
)
Write-Output "PHASE=worker-preflight-started"
& docker @preflightArgs
Assert-LastExitCode "Worker preflight"
Write-Output "PHASE=worker-preflight-complete"
$runToken = [Guid]::NewGuid().ToString("N")
$workRoot = Join-Path $tmpRoot ("{0}-e1-{1}" -f $job.job_id, $runToken)
$framesRoot = Join-Path $workRoot "frames"
$streamPath = Join-Path $workRoot "camera.mp4"
$ptsPath = Join-Path $workRoot "pts.json"
$timelinePath = Join-Path $workRoot "timeline.jsonl"
$publishRoot = Join-Path $derivedRoot (".{0}-e1-{1}.publish" -f $job.job_id, $runToken)
$stagingRoot = Join-Path $publishRoot "output"
$null = New-Item -ItemType Directory -Path $framesRoot
$null = New-Item -ItemType Directory -Path $publishRoot
Write-Output "PHASE=private-staging-created"
try {
$stream = [IO.File]::Open($streamPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
try {
$input = [IO.File]::OpenRead($initPath)
try {
$input.CopyTo($stream)
}
finally {
$input.Dispose()
}
for ($sequence = 1; $sequence -le $frameCount; $sequence++) {
$path = Assert-RegularFile (Join-Path $segmentsRoot ("{0}.m4s" -f $sequence)) "Camera segment"
$input = [IO.File]::OpenRead($path)
try {
$input.CopyTo($stream)
}
finally {
$input.Dispose()
}
if ($sequence % 500 -eq 0 -or $sequence -eq $frameCount) {
Write-Output ("PHASE=stream-reconstruction SEGMENTS={0}/{1}" -f $sequence, $frameCount)
}
}
$stream.Flush($true)
}
finally {
$stream.Dispose()
}
Write-Output "PHASE=frame-extraction-started"
& ffmpeg -hide_banner -loglevel fatal -i $streamPath -map 0:v:0 -fps_mode passthrough (Join-Path $framesRoot "frame-%06d.png")
Assert-LastExitCode "Camera extraction"
& ffprobe -v error -select_streams v:0 -show_entries frame=best_effort_timestamp_time -of json $streamPath | Set-Content -LiteralPath $ptsPath -Encoding utf8
Assert-LastExitCode "Camera timestamp probe"
$decodedFrames = @(Get-ChildItem -LiteralPath $framesRoot -File -Filter "frame-*.png")
$ptsDocument = Get-Content -LiteralPath $ptsPath -Raw | ConvertFrom-Json
$pts = @($ptsDocument.frames)
if ($decodedFrames.Count -ne $frameCount -or $pts.Count -ne $frameCount) {
throw "Decoded frame count differs from the compute job"
}
$firstEpochSeconds = [double]::Parse(
([string]$pts[0].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
)
$previousEpochSeconds = -1.0
$timelineWriter = [IO.StreamWriter]::new($timelinePath, $false, [Text.UTF8Encoding]::new($false))
try {
for ($index = 0; $index -lt $frameCount; $index++) {
$epochSeconds = [double]::Parse(
([string]$pts[$index].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
) - $firstEpochSeconds
if ($epochSeconds -le $previousEpochSeconds -or $epochSeconds -gt ($timelineDuration + 0.001)) {
throw "Decoded camera timestamps are not strictly monotonic inside the compute job timeline"
}
$row = [ordered]@{
frame_index = $index
epoch_seconds = $epochSeconds
session_seconds = $timelineStart + $epochSeconds
}
$timelineWriter.WriteLine(($row | ConvertTo-Json -Compress))
$previousEpochSeconds = $epochSeconds
}
$timelineWriter.Flush()
}
finally {
$timelineWriter.Dispose()
}
Write-Output ("PHASE=frame-extraction-complete FRAMES={0}" -f $frameCount)
$dockerArgs = @(
"run", "--rm", "--gpus", "all", "--network", "none",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--shm-size", "2g",
"-e", "PYTHONPATH=/opt/transformers:/opt/env",
"-e", "TORCH_HOME=/cache/torch",
"-e", "HF_HOME=/cache/huggingface",
"-e", "HF_HUB_OFFLINE=1",
"-e", "TRANSFORMERS_OFFLINE=1",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $framesRoot) + ":/frames:ro"),
"-v", ((Convert-ToDockerPath $workRoot) + ":/work:ro"),
"-v", ((Convert-ToDockerPath $publishRoot) + ":/publish:rw"),
"-v", ((Convert-ToDockerPath $cacheRoot) + ":/cache:rw"),
"-v", ((Convert-ToDockerPath $torchEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $transformersEnvironment) + ":/opt/transformers:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"-v", ((Convert-ToDockerPath $qualificationRoot) + ":/qualification:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"--entrypoint", "python3",
"nvcr.io/nvidia/tritonserver:26.06-py3",
("/runner/{0}" -f $runnerName),
"--job", "/job",
"--frames", "/frames",
"--timeline", "/work/timeline.jsonl",
"--qualification", ("/qualification/{0}" -f $qualificationName),
"--valid-fov-root", "/valid-fov",
"--output", "/publish/output",
"--cache", "/cache",
"--calibration-sha256", $CalibrationSha256,
"--calibration-slot", $CalibrationSlot,
"--telemetry-interval-seconds", "1"
)
& docker @dockerArgs
Assert-LastExitCode "Preprocessing qualification"
$result = Get-Content -LiteralPath (Join-Path $stagingRoot "result.json") -Raw | ConvertFrom-Json
if (
$result.schema_version -ne "missioncore.perception-preprocessing-qualification-result/v1" -or
$result.result_id -notmatch "^qualification-result-[a-f0-9]{64}$"
) {
throw "Qualification result manifest is incompatible"
}
$finalRoot = Join-Path $derivedRoot ([string]$result.result_id)
if (Test-Path -LiteralPath $finalRoot) {
throw "An immutable qualification result with the same identity already exists: $finalRoot"
}
Move-Item -LiteralPath $stagingRoot -Destination $finalRoot
Remove-Item -LiteralPath $publishRoot -Force
Write-Output ("RESULT_ROOT={0}" -f $finalRoot)
Write-Output ("RESULT_ID={0}" -f $result.result_id)
}
finally {
if (Test-Path -LiteralPath $workRoot) {
Remove-Item -LiteralPath $workRoot -Recurse -Force
}
}

View File

@ -0,0 +1,292 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$JobRoot,
[Parameter(Mandatory = $true)]
[string]$RunnerPath,
[string]$RuntimeRoot = "D:\NDC_MISSIONCORE\runtime",
[string]$CalibrationSha256 = "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9",
[string]$CalibrationSlot = "camera_1"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-LastExitCode {
param([string]$Operation)
if ($LASTEXITCODE -ne 0) {
throw "$Operation failed with exit code $LASTEXITCODE"
}
}
function Assert-RegularFile {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if (-not $item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a regular file"
}
function Assert-Directory {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if ($item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a real directory"
}
function Convert-ToDockerPath {
param([string]$Path)
return $Path.Replace("\", "/")
}
$jobDirectory = Assert-Directory (Resolve-Path -LiteralPath $JobRoot).Path "Job root"
$runner = Assert-RegularFile (Resolve-Path -LiteralPath $RunnerPath).Path "Runner"
$runtime = Assert-Directory (Resolve-Path -LiteralPath $RuntimeRoot).Path "Runtime root"
$job = Get-Content -LiteralPath (Join-Path $jobDirectory "job.json") -Raw | ConvertFrom-Json
if ($job.schema_version -ne "missioncore.compute-job/v1" -or $job.job_id -ne (Split-Path $jobDirectory -Leaf)) {
throw "Compute job manifest is incompatible"
}
if ($CalibrationSha256 -notmatch "^[a-f0-9]{64}$" -or $CalibrationSlot -notmatch "^[A-Za-z0-9._-]+$") {
throw "Calibration binding is invalid"
}
Write-Output "PHASE=job-manifest-validated"
$sourceId = [string]$job.input.source_id
$epoch = [int]$job.input.codec_epoch
$frameCount = [int]$job.input.segment_count
$timelineStart = [double]$job.input.timeline.start_seconds
$timelineEnd = [double]$job.input.timeline.end_seconds
$timelineDuration = $timelineEnd - $timelineStart
if ($frameCount -lt 1 -or $timelineDuration -le 0) {
throw "Compute job frame/timeline contract is invalid"
}
$epochRoot = Join-Path $jobDirectory ("input\camera\{0}\epoch-{1}" -f $sourceId, $epoch)
$epochRoot = Assert-Directory $epochRoot "Camera epoch"
$initPath = Assert-RegularFile (Join-Path $epochRoot "init.mp4") "Camera init"
$segmentsRoot = Assert-Directory (Join-Path $epochRoot "segments") "Camera segments"
$derivedRoot = Join-Path $runtime "derived"
$tmpRoot = Join-Path $runtime "tmp"
$cacheRoot = Assert-Directory (Join-Path $runtime "cache\perception-p0-models-v1") "Model cache"
$torchEnvironment = Assert-Directory (Join-Path $derivedRoot "perception-p0-env-v1") "Torch environment"
$transformersEnvironment = Assert-Directory (Join-Path $derivedRoot "perception-p0-transformers4576-v1") "Transformers environment"
$runnerRoot = Split-Path $runner -Parent
$runnerName = Split-Path $runner -Leaf
$null = New-Item -ItemType Directory -Path $derivedRoot -Force
$null = New-Item -ItemType Directory -Path $tmpRoot -Force
$preflightArgs = @(
"run", "--rm", "--gpus", "all", "--network", "none",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"-e", "PYTHONPATH=/opt/transformers:/opt/env",
"-e", "TORCH_HOME=/cache/torch",
"-e", "HF_HOME=/cache/huggingface",
"-e", "HF_HUB_OFFLINE=1",
"-e", "TRANSFORMERS_OFFLINE=1",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $cacheRoot) + ":/cache:rw"),
"-v", ((Convert-ToDockerPath $torchEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $transformersEnvironment) + ":/opt/transformers:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3",
"nvcr.io/nvidia/tritonserver:26.06-py3",
("/runner/{0}" -f $runnerName), "preflight",
"--job", "/job",
"--cache", "/cache"
)
Write-Output "PHASE=worker-preflight-started"
& docker @preflightArgs
Assert-LastExitCode "Worker preflight"
Write-Output "PHASE=worker-preflight-complete"
$runToken = [Guid]::NewGuid().ToString("N")
$workRoot = Join-Path $tmpRoot ("{0}-panoptic-{1}" -f $job.job_id, $runToken)
$framesRoot = Join-Path $workRoot "frames"
$streamPath = Join-Path $workRoot "camera.mp4"
$ptsPath = Join-Path $workRoot "pts.json"
$timelinePath = Join-Path $workRoot "timeline.jsonl"
$publishRoot = Join-Path $derivedRoot (".{0}-panoptic-{1}.publish" -f $job.job_id, $runToken)
$stagingRoot = Join-Path $publishRoot "output"
$null = New-Item -ItemType Directory -Path $framesRoot
$null = New-Item -ItemType Directory -Path $publishRoot
$totalWatch = [Diagnostics.Stopwatch]::StartNew()
Write-Output "PHASE=private-staging-created"
try {
$stream = [IO.File]::Open($streamPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
try {
foreach ($path in @($initPath)) {
$input = [IO.File]::OpenRead($path)
try {
$input.CopyTo($stream)
}
finally {
$input.Dispose()
}
}
for ($sequence = 1; $sequence -le $frameCount; $sequence++) {
$path = Assert-RegularFile (Join-Path $segmentsRoot ("{0}.m4s" -f $sequence)) "Camera segment"
$input = [IO.File]::OpenRead($path)
try {
$input.CopyTo($stream)
}
finally {
$input.Dispose()
}
if ($sequence % 500 -eq 0 -or $sequence -eq $frameCount) {
Write-Output ("PHASE=stream-reconstruction SEGMENTS={0}/{1}" -f $sequence, $frameCount)
}
}
$stream.Flush($true)
}
finally {
$stream.Dispose()
}
$extractWatch = [Diagnostics.Stopwatch]::StartNew()
Write-Output "PHASE=frame-extraction-started"
# This archive contains valid strictly-increasing PTS but image2 rounds DTS
# to its own time base and reports harmless duplicate-DTS diagnostics. Exact
# frame-count and JSON timestamp checks below are the admission boundary.
& ffmpeg -hide_banner -loglevel fatal -i $streamPath -map 0:v:0 -fps_mode passthrough (Join-Path $framesRoot "frame-%06d.png")
Assert-LastExitCode "Camera extraction"
& ffprobe -v error -select_streams v:0 -show_entries frame=best_effort_timestamp_time -of json $streamPath | Set-Content -LiteralPath $ptsPath -Encoding utf8
Assert-LastExitCode "Camera timestamp probe"
$decodedFrames = @(Get-ChildItem -LiteralPath $framesRoot -File -Filter "frame-*.png")
$ptsDocument = Get-Content -LiteralPath $ptsPath -Raw | ConvertFrom-Json
$pts = @($ptsDocument.frames)
if ($decodedFrames.Count -ne $frameCount -or $pts.Count -ne $frameCount) {
throw "Decoded frame count differs from the compute job"
}
$firstEpochSeconds = [double]::Parse(
([string]$pts[0].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
)
$previousEpochSeconds = -1.0
$timelineWriter = [IO.StreamWriter]::new($timelinePath, $false, [Text.UTF8Encoding]::new($false))
try {
for ($index = 0; $index -lt $frameCount; $index++) {
$epochSeconds = [double]::Parse(
([string]$pts[$index].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
) - $firstEpochSeconds
if ($epochSeconds -le $previousEpochSeconds -or $epochSeconds -gt ($timelineDuration + 0.001)) {
throw "Decoded camera timestamps are not strictly monotonic inside the compute job timeline"
}
$sessionSeconds = $timelineStart + $epochSeconds
$row = [ordered]@{
frame_index = $index
epoch_seconds = $epochSeconds
session_seconds = $sessionSeconds
}
$timelineWriter.WriteLine(($row | ConvertTo-Json -Compress))
$previousEpochSeconds = $epochSeconds
}
$timelineWriter.Flush()
}
finally {
$timelineWriter.Dispose()
}
$extractWatch.Stop()
Write-Output ("PHASE=frame-extraction-complete FRAMES={0}" -f $frameCount)
$dockerArgs = @(
"run", "--rm", "--gpus", "all", "--network", "none",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--shm-size", "2g",
"-e", "PYTHONPATH=/opt/transformers:/opt/env",
"-e", "TORCH_HOME=/cache/torch",
"-e", "HF_HOME=/cache/huggingface",
"-e", "HF_HUB_OFFLINE=1",
"-e", "TRANSFORMERS_OFFLINE=1",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $framesRoot) + ":/frames:ro"),
"-v", ((Convert-ToDockerPath $workRoot) + ":/work:ro"),
"-v", ((Convert-ToDockerPath $publishRoot) + ":/publish:rw"),
"-v", ((Convert-ToDockerPath $cacheRoot) + ":/cache:rw"),
"-v", ((Convert-ToDockerPath $torchEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $transformersEnvironment) + ":/opt/transformers:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3",
"nvcr.io/nvidia/tritonserver:26.06-py3",
("/runner/{0}" -f $runnerName), "run",
"--job", "/job",
"--frames", "/frames",
"--timeline", "/work/timeline.jsonl",
"--output", "/publish/output",
"--cache", "/cache",
"--calibration-sha256", $CalibrationSha256,
"--calibration-slot", $CalibrationSlot,
"--telemetry-interval-seconds", "1"
)
& docker @dockerArgs
Assert-LastExitCode "Panoptic inference"
Write-Output "PHASE=panoptic-inference-complete"
$videoPath = Join-Path $stagingRoot "perception.mp4"
$encodeWatch = [Diagnostics.Stopwatch]::StartNew()
$averageFps = $frameCount / $timelineDuration
$fpsText = $averageFps.ToString("0.#########", [Globalization.CultureInfo]::InvariantCulture)
$durationText = $timelineDuration.ToString("0.#########", [Globalization.CultureInfo]::InvariantCulture)
& ffmpeg -hide_banner -loglevel error -framerate $fpsText -i (Join-Path $stagingRoot "overlay-frames\frame-%06d.png") -t $durationText -c:v h264_nvenc -preset p4 -tune hq -rc vbr -cq 21 -b:v 0 -pix_fmt yuv420p -movflags +faststart $videoPath
Assert-LastExitCode "Panoptic video encoding"
$encodeWatch.Stop()
Write-Output "PHASE=video-encoding-complete"
$masksPath = Join-Path $stagingRoot "masks.tar.gz"
& tar.exe -czf $masksPath -C $stagingRoot instance-masks semantic-masks
Assert-LastExitCode "Mask archive publication"
$totalWatch.Stop()
$finalizeArgs = @(
"run", "--rm", "--network", "none",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"-v", ((Convert-ToDockerPath $stagingRoot) + ":/output:rw"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3",
"nvcr.io/nvidia/tritonserver:26.06-py3",
("/runner/{0}" -f $runnerName), "finalize",
"--output", "/output",
"--video", "/output/perception.mp4",
"--masks", "/output/masks.tar.gz",
"--extract-seconds", $extractWatch.Elapsed.TotalSeconds.ToString("0.######", [Globalization.CultureInfo]::InvariantCulture),
"--encode-seconds", $encodeWatch.Elapsed.TotalSeconds.ToString("0.######", [Globalization.CultureInfo]::InvariantCulture),
"--wall-seconds", $totalWatch.Elapsed.TotalSeconds.ToString("0.######", [Globalization.CultureInfo]::InvariantCulture),
"--encoder", "ffmpeg-h264_nvenc-p4-cq21-yuv420p-faststart"
)
& docker @finalizeArgs
Assert-LastExitCode "Result finalization"
$result = Get-Content -LiteralPath (Join-Path $stagingRoot "result.json") -Raw | ConvertFrom-Json
if ($result.schema_version -ne "missioncore.recorded-perception-result/v2" -or $result.result_id -notmatch "^result-[a-f0-9]{64}$") {
throw "Final result manifest is incompatible"
}
$finalRoot = Join-Path $derivedRoot ([string]$result.result_id)
if (Test-Path -LiteralPath $finalRoot) {
throw "An immutable result with the same identity already exists: $finalRoot"
}
foreach ($temporaryChild in @("overlay-frames", "instance-masks", "semantic-masks")) {
$temporaryPath = Join-Path $stagingRoot $temporaryChild
if (Test-Path -LiteralPath $temporaryPath) {
Remove-Item -LiteralPath $temporaryPath -Recurse -Force
}
}
Move-Item -LiteralPath $stagingRoot -Destination $finalRoot
Remove-Item -LiteralPath $publishRoot -Force
Write-Output ("RESULT_ROOT={0}" -f $finalRoot)
Write-Output ("RESULT_ID={0}" -f $result.result_id)
}
finally {
if (Test-Path -LiteralPath $workRoot) {
Remove-Item -LiteralPath $workRoot -Recurse -Force
}
}

View File

@ -0,0 +1,62 @@
# Mission Core CVAT D-only profile
This profile installs the annotation control plane in a dedicated WSL2
distribution named `MissionCore-CVAT`. Its VHDX, Docker image store, CVAT
source, persistent volumes, reports, and imported datasets live below
`D:\NDC_MISSIONCORE`. It does not use the Docker Desktop image store that backs
the Triton, Frigate, and Ollama containers.
Pinned inputs:
- Ubuntu 24.04.4 WSL AMD64 image, SHA-256
`9b2f7730dc68227dd04a9f3e5eab86ad85caf556b8606ad94f1f29ff5c4fd3f5`
- CVAT `v2.70.0`
- D free-space floor: `360 GiB`
- WSL VHD logical ceiling: `32 GB` (sparse allocation)
The official CVAT Compose topology is retained. The override only replaces its
named volumes with explicit bind-backed directories inside the D-hosted WSL
VHD. Images are pulled serially and the D free-space floor is checked before and
after every image.
Run from the Windows host:
```powershell
wsl -d MissionCore-CVAT -u root -- bash /mnt/d/NDC_MISSIONCORE/workspace/mission-core-compute/cvat/provision_cvat_wsl.sh
wsl -d MissionCore-CVAT -u root -- bash /mnt/d/NDC_MISSIONCORE/workspace/mission-core-compute/cvat/ensure_cvat_admin.sh
wsl -d MissionCore-CVAT -u root -- bash /mnt/d/NDC_MISSIONCORE/workspace/mission-core-compute/cvat/import_e2_workspace.sh
```
After a Windows reboot, start the existing deployment without pulling images or
re-provisioning it:
```powershell
& D:\NDC_MISSIONCORE\workspace\mission-core-compute\cvat\Start-Cvat.ps1
```
The launcher checks the `360 GiB` D-drive floor, starts the pinned Compose
topology in the D-hosted WSL distribution, waits for the API, and keeps the WSL
runtime alive. It writes startup logs only below
`D:\NDC_MISSIONCORE\runtime\annotation\cvat\logs`.
From the Mission Core repository on the Mac, start CVAT if needed, discover the
current WSL address, and open the SSH tunnel without a hard-coded IP:
```bash
bash experiments/perception/worker/cvat/open_cvat_tunnel.sh
```
Keep that terminal open and use `http://localhost:18080`. The same SSH session
keeps the WSL runtime alive. Pass `--background` when detached runtime and
tunnel sessions are preferred; rerun the helper after a Mac or Windows reboot.
The generated administrator password is stored only in
`D:\NDC_MISSIONCORE\secrets\cvat\admin.env`; it is not printed by the script or
committed to Git.
Traefik binds inside the dedicated WSL environment to `127.0.0.1:8080` and
`127.0.0.1:8090`. It also publishes container port `8080` as WSL-internal port
`18080` so the existing Windows SSH service can forward it without relying on
Windows-to-WSL localhost forwarding. Remote review still uses the exact SSH
host alias and a local forward; this profile does not add routes, Windows port
proxies, DNS changes, or firewall rules.

View File

@ -0,0 +1,58 @@
[CmdletBinding()]
param(
[int]$TimeoutSeconds = 180
)
$ErrorActionPreference = 'Stop'
$Distro = 'MissionCore-CVAT'
$Root = 'D:\NDC_MISSIONCORE'
$RuntimeRoot = Join-Path $Root 'runtime\annotation\cvat'
$LogRoot = Join-Path $RuntimeRoot 'logs'
$LinuxStartScript = '/mnt/d/NDC_MISSIONCORE/workspace/mission-core-compute/cvat/start_cvat_runtime.sh'
$FreeGiBFloor = 360
$drive = Get-PSDrive -Name D
$freeGiB = [math]::Floor($drive.Free / 1GB)
if ($freeGiB -lt $FreeGiBFloor) {
throw "D: free-space floor crossed ($freeGiB GiB free; $FreeGiBFloor GiB required)"
}
try {
$about = Invoke-RestMethod -Uri 'http://localhost:8080/api/server/about' -TimeoutSec 3
if ($about.version -eq '2.70.0') {
Write-Output "CVAT already ready version=$($about.version) free_gib=$freeGiB"
exit 0
}
} catch {
# A stopped WSL distribution is the normal condition after a Windows reboot.
}
New-Item -ItemType Directory -Path $LogRoot -Force | Out-Null
$timestamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')
$stdoutPath = Join-Path $LogRoot "start-$timestamp.stdout.log"
$stderrPath = Join-Path $LogRoot "start-$timestamp.stderr.log"
$arguments = @(
'-d', $Distro,
'-u', 'root',
'--', 'bash', $LinuxStartScript, '--keepalive'
)
Start-Process -FilePath 'wsl.exe' -ArgumentList $arguments -WindowStyle Hidden `
-RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
do {
Start-Sleep -Seconds 2
try {
$about = Invoke-RestMethod -Uri 'http://localhost:8080/api/server/about' -TimeoutSec 3
if ($about.version -eq '2.70.0') {
Write-Output "CVAT ready version=$($about.version) free_gib=$freeGiB"
Write-Output "startup_log=$stdoutPath"
exit 0
}
} catch {
# Continue until the complete CVAT Compose topology is ready.
}
} while ((Get-Date) -lt $deadline)
throw "CVAT did not become ready within $TimeoutSeconds seconds; inspect $stderrPath"

View File

@ -0,0 +1,50 @@
services:
traefik:
ports: !override
- 127.0.0.1:8080:8080
- 127.0.0.1:8090:8090
- 0.0.0.0:18080:8080
volumes:
cvat_db:
driver: local
driver_opts:
type: none
o: bind
device: /srv/mission-core-cvat/volumes/cvat_db
cvat_data:
driver: local
driver_opts:
type: none
o: bind
device: /srv/mission-core-cvat/volumes/cvat_data
cvat_keys:
driver: local
driver_opts:
type: none
o: bind
device: /srv/mission-core-cvat/volumes/cvat_keys
cvat_logs:
driver: local
driver_opts:
type: none
o: bind
device: /srv/mission-core-cvat/volumes/cvat_logs
cvat_inmem_db:
driver: local
driver_opts:
type: none
o: bind
device: /srv/mission-core-cvat/volumes/cvat_inmem_db
cvat_events_db:
driver: local
driver_opts:
type: none
o: bind
device: /srv/mission-core-cvat/volumes/cvat_events_db
cvat_cache_db:
driver: local
driver_opts:
type: none
o: bind
device: /srv/mission-core-cvat/volumes/cvat_cache_db

View File

@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -Eeuo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
readonly SECRET_ROOT="/mnt/d/NDC_MISSIONCORE/secrets/cvat"
readonly SECRET_FILE="${SECRET_ROOT}/admin.env"
if [[ "${EUID}" -ne 0 ]]; then
echo "ensure_cvat_admin.sh must run as root" >&2
exit 2
fi
install -d -m 0700 "${SECRET_ROOT}"
if [[ ! -f "${SECRET_FILE}" ]]; then
umask 077
password="$(openssl rand -hex 24)"
{
printf 'CVAT_ADMIN_USERNAME=missioncore\n'
printf 'CVAT_ADMIN_EMAIL=missioncore@local.invalid\n'
printf 'CVAT_ADMIN_PASSWORD=%s\n' "${password}"
} >"${SECRET_FILE}"
fi
set -a
# shellcheck disable=SC1090
. "${SECRET_FILE}"
set +a
docker exec \
-e "CVAT_ADMIN_USERNAME=${CVAT_ADMIN_USERNAME}" \
-e "CVAT_ADMIN_EMAIL=${CVAT_ADMIN_EMAIL}" \
-e "CVAT_ADMIN_PASSWORD=${CVAT_ADMIN_PASSWORD}" \
cvat_server \
python3 /home/django/manage.py shell -c \
'import os; from django.contrib.auth import get_user_model; User = get_user_model(); user, _ = User.objects.get_or_create(username=os.environ["CVAT_ADMIN_USERNAME"]); user.email = os.environ["CVAT_ADMIN_EMAIL"]; user.is_staff = True; user.is_superuser = True; user.set_password(os.environ["CVAT_ADMIN_PASSWORD"]); user.save()'
printf 'admin_ready=true\nusername=%s\nsecret_file=%s\n' \
"${CVAT_ADMIN_USERNAME}" "${SECRET_FILE}"

View File

@ -0,0 +1,284 @@
from __future__ import annotations
import argparse
import hashlib
import json
import os
import time
from collections import Counter
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from cvat_sdk import make_client, models
from cvat_sdk.api_client.exceptions import ServiceException
from cvat_sdk.core.proxies.tasks import ResourceType
WORKSPACE_ID = (
"annotation-workspace-"
"9a950d1c37d56dc12cc285b13c5addd7795285879cbcb1fbb2d5811c3c69821a"
)
EVALUATION_PACK_ID = (
"evaluation-pack-"
"7a983bba75d46c7c260252cb2d461e1384dcb92cda9e164397e841e6ebb37789"
)
EXPECTED_FRAME_COUNT = 64
EXPECTED_INSTANCE_COUNT = 775
BACKGROUND_LABEL = {
"name": "background",
"color": "#000000",
"attributes": [],
}
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _label_specs(raw_labels: list[dict[str, Any]]) -> list[dict[str, Any]]:
labels = [
{
"name": item["name"],
"color": item["color"],
"attributes": [],
}
for item in raw_labels
]
if not any(label["name"] == BACKGROUND_LABEL["name"] for label in labels):
labels.insert(0, dict(BACKGROUND_LABEL))
return labels
def _annotation_counts(task: Any) -> dict[str, int]:
annotations = task.get_annotations()
return {
"shape_count": len(annotations.shapes),
"tag_count": len(annotations.tags),
"track_count": len(annotations.tracks),
}
def _list_tasks_when_ready(client: Any, *, attempts: int = 60) -> list[Any]:
for attempt in range(1, attempts + 1):
try:
return list(client.tasks.list())
except ServiceException as error:
if error.status not in {500, 502, 503} or attempt == attempts:
raise
time.sleep(2)
raise AssertionError("unreachable")
def _task_record(task: Any, *, disposition: str, expected_labels: list[str]) -> dict[str, Any]:
task.fetch()
task_labels = list(task.get_labels())
actual_labels = sorted(label.name for label in task_labels)
if task.size != EXPECTED_FRAME_COUNT:
raise RuntimeError(
f"Task {task.id} has {task.size} frames, expected {EXPECTED_FRAME_COUNT}"
)
if actual_labels != sorted(expected_labels):
raise RuntimeError(
f"Task {task.id} labels differ: actual={actual_labels!r} "
f"expected={sorted(expected_labels)!r}"
)
annotation_counts = _annotation_counts(task)
label_names_by_id = {label.id: label.name for label in task_labels}
annotations = task.get_annotations()
shape_counts_by_label = dict(
sorted(
Counter(
label_names_by_id.get(shape.label_id, f"unknown:{shape.label_id}")
for shape in annotations.shapes
).items()
)
)
return {
"id": task.id,
"name": task.name,
"disposition": disposition,
"frame_count": task.size,
"label_names": actual_labels,
**annotation_counts,
"shape_counts_by_label": shape_counts_by_label,
"url_path": f"/tasks/{task.id}",
}
def _ensure_task(
client: Any,
*,
name: str,
labels: list[dict[str, Any]],
images_path: Path,
annotation_path: Path,
annotation_format: str,
expected_shape_count: int | None,
) -> dict[str, Any]:
matches = [task for task in _list_tasks_when_ready(client) if task.name == name]
if len(matches) > 1:
raise RuntimeError(f"Multiple CVAT tasks have the reserved name {name!r}")
expected_labels = [label["name"] for label in labels]
if matches:
task = matches[0]
task.fetch()
actual_labels = sorted(label.name for label in task.get_labels())
expected_without_background = sorted(
label for label in expected_labels if label != BACKGROUND_LABEL["name"]
)
disposition = "reused"
if (
actual_labels == expected_without_background
and BACKGROUND_LABEL["name"] in expected_labels
):
task.update(
models.PatchedTaskWriteRequest(
labels=[models.PatchedLabelRequest(**BACKGROUND_LABEL)]
)
)
disposition = "reused-and-background-label-added"
record = _task_record(
task,
disposition=disposition,
expected_labels=expected_labels,
)
if record["shape_count"] == 0:
task.import_annotations(annotation_format, annotation_path)
record = _task_record(
task,
disposition=f"{disposition}-and-annotations-imported",
expected_labels=expected_labels,
)
if expected_shape_count is not None and record["shape_count"] != expected_shape_count:
raise RuntimeError(
f"Task {task.id} has {record['shape_count']} shapes, "
f"expected {expected_shape_count}"
)
return record
task = client.tasks.create_from_data(
spec=models.TaskWriteRequest(
name=name,
labels=labels,
segment_size=EXPECTED_FRAME_COUNT,
overlap=0,
),
resources=[images_path],
resource_type=ResourceType.LOCAL,
data_params={
"image_quality": 100,
"sorting_method": "lexicographical",
"use_cache": False,
},
annotation_path=annotation_path,
annotation_format=annotation_format,
status_check_period=2,
)
record = _task_record(task, disposition="created", expected_labels=expected_labels)
if expected_shape_count is not None and record["shape_count"] != expected_shape_count:
raise RuntimeError(
f"Task {task.id} has {record['shape_count']} shapes, "
f"expected {expected_shape_count}"
)
return record
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--server", required=True)
parser.add_argument("--username", required=True)
parser.add_argument("--password-env", required=True)
parser.add_argument("--workspace", type=Path, required=True)
parser.add_argument("--report", type=Path, required=True)
args = parser.parse_args()
password = os.environ.get(args.password_env)
if not password:
raise RuntimeError(f"Password environment variable {args.password_env!r} is empty")
workspace = args.workspace.resolve()
if workspace.name != WORKSPACE_ID:
raise RuntimeError(f"Unexpected annotation workspace: {workspace}")
manifest_path = workspace / "manifest.json"
labels_path = workspace / "cvat" / "labels.json"
images_path = workspace / "cvat" / "images.zip"
instance_path = workspace / "cvat" / "instance-coco.zip"
semantic_path = workspace / "cvat" / "semantic-mask.zip"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
labels = json.loads(labels_path.read_text(encoding="utf-8"))
if manifest["ground_truth"] is not False:
raise RuntimeError("LAB E2 import must remain an unreviewed model draft")
if manifest["identity"]["frame_count"] != EXPECTED_FRAME_COUNT:
raise RuntimeError("Unexpected LAB E2 frame count")
if manifest["identity"]["draft_instance_count"] != EXPECTED_INSTANCE_COUNT:
raise RuntimeError("Unexpected LAB E2 instance count")
task_specs = [
{
"name": "LAB E2 | K1 | instance prelabels | pack 7a983bba",
"labels": _label_specs(labels["instance_task"]),
"annotation_path": instance_path,
"annotation_format": "COCO 1.0",
"expected_shape_count": EXPECTED_INSTANCE_COUNT,
},
{
"name": "LAB E2 | K1 | dense semantic prelabels | pack 7a983bba",
"labels": _label_specs(labels["semantic_task"]),
"annotation_path": semantic_path,
"annotation_format": "Segmentation mask 1.1",
"expected_shape_count": None,
},
]
with make_client(args.server, credentials=(args.username, password)) as client:
client.check_server_version(fail_if_unsupported=True)
task_records = [
_ensure_task(
client,
name=task_spec["name"],
labels=task_spec["labels"],
images_path=images_path,
annotation_path=task_spec["annotation_path"],
annotation_format=task_spec["annotation_format"],
expected_shape_count=task_spec["expected_shape_count"],
)
for task_spec in task_specs
]
report = {
"schema_version": "missioncore.lab-e2-cvat-import/v1",
"created_at_utc": datetime.now(UTC).isoformat(timespec="milliseconds").replace(
"+00:00", "Z"
),
"server": args.server,
"cvat_version": "v2.70.0",
"workspace_id": WORKSPACE_ID,
"evaluation_pack_id": EVALUATION_PACK_ID,
"workspace_manifest_sha256": _sha256(manifest_path),
"ground_truth": False,
"inputs": {
"images_zip_sha256": _sha256(images_path),
"instance_coco_zip_sha256": _sha256(instance_path),
"semantic_mask_zip_sha256": _sha256(semantic_path),
},
"tasks": task_records,
"next_gate": (
"two-pass human review and reviewed export; "
"do not treat drafts as accuracy evidence"
),
}
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,89 @@
#!/usr/bin/env bash
set -Eeuo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
readonly FREE_GIB_FLOOR="${FREE_GIB_FLOOR:-360}"
readonly WINDOWS_ROOT="/mnt/d/NDC_MISSIONCORE"
readonly COMPUTE_ROOT="${WINDOWS_ROOT}/workspace/mission-core-compute"
readonly TOOL_ROOT="/srv/mission-core-cvat/tools/e2-import"
readonly SECRET_FILE="${WINDOWS_ROOT}/secrets/cvat/admin.env"
readonly WORKSPACE_ROOT="${WINDOWS_ROOT}/runtime/annotation/imports/LAB-E2/annotation-workspace-9a950d1c37d56dc12cc285b13c5addd7795285879cbcb1fbb2d5811c3c69821a"
readonly REPORT_ROOT="${WINDOWS_ROOT}/runtime/annotation/cvat/reports"
if [[ "${EUID}" -ne 0 ]]; then
echo "import_e2_workspace.sh must run as root" >&2
exit 2
fi
free_gib() {
df -BG --output=avail "${WINDOWS_ROOT}" | tail -n 1 | tr -dc '0-9'
}
guard_disk() {
local stage="$1"
local free
free="$(free_gib)"
printf 'disk_guard stage=%s free_gib=%s floor_gib=%s\n' \
"${stage}" "${free}" "${FREE_GIB_FLOOR}"
if (( free < FREE_GIB_FLOOR )); then
echo "D: free-space floor crossed; refusing further writes" >&2
exit 3
fi
}
wait_for_cvat() {
local attempt http_code
for attempt in $(seq 1 60); do
http_code="$(
curl --silent --output /dev/null --write-out '%{http_code}' \
"http://localhost:8080/api/server/about" || true
)"
if [[ "${http_code}" == "200" ]]; then
http_code="$(
curl --silent --output /dev/null --write-out '%{http_code}' \
"http://localhost:8080/api/tasks" || true
)"
if [[ "${http_code}" == "200" || "${http_code}" == "401" || "${http_code}" == "403" ]]; then
printf 'cvat_ready attempt=%s tasks_http=%s\n' "${attempt}" "${http_code}"
return 0
fi
fi
sleep 2
done
echo "CVAT API did not become ready within 120 seconds" >&2
return 1
}
test -f "${SECRET_FILE}"
test -f "${WORKSPACE_ROOT}/manifest.json"
test -f "${COMPUTE_ROOT}/cvat/import_e2_workspace.py"
guard_disk preflight
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y python3-venv
install -d -m 0750 "${TOOL_ROOT}" "${REPORT_ROOT}"
if [[ ! -x "${TOOL_ROOT}/venv/bin/python" ]]; then
python3 -m venv "${TOOL_ROOT}/venv"
"${TOOL_ROOT}/venv/bin/pip" install --disable-pip-version-check \
"cvat-sdk==2.70.0"
fi
set -a
# shellcheck disable=SC1090
. "${SECRET_FILE}"
set +a
wait_for_cvat
report="${REPORT_ROOT}/lab-e2-cvat-import-$(date -u +%Y%m%dT%H%M%SZ).json"
"${TOOL_ROOT}/venv/bin/python" "${COMPUTE_ROOT}/cvat/import_e2_workspace.py" \
--server "http://localhost:8080" \
--username "${CVAT_ADMIN_USERNAME}" \
--password-env CVAT_ADMIN_PASSWORD \
--workspace "${WORKSPACE_ROOT}" \
--report "${report}"
guard_disk imported
printf 'import_report=%s\n' "${report}"

View File

@ -0,0 +1,179 @@
from __future__ import annotations
import argparse
import hashlib
import json
import os
from datetime import UTC, datetime
from pathlib import Path, PurePosixPath
from typing import Any
from cvat_sdk import make_client
from import_e2_workspace import _ensure_task, _label_specs, _sha256
WORKSPACE_SCHEMA = "missioncore.lab-e3-cvat-review-workspace/v1"
IDENTITY_SCHEMA = "missioncore.lab-e3-cvat-review-identity/v1"
EXPECTED_PACK_ID = (
"evaluation-pack-"
"7a983bba75d46c7c260252cb2d461e1384dcb92cda9e164397e841e6ebb37789"
)
EXPECTED_RESULT_ID = (
"e3-segmentation-"
"01bd497c44c2b940add145ec784d3418010327bce0baddd4420b0925317e8a16"
)
EXPECTED_FRAMES = 64
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode()
def _valid_sha256(value: object) -> bool:
return (
isinstance(value, str)
and len(value) == 64
and all(character in "0123456789abcdef" for character in value)
)
def _read_object(path: Path) -> dict[str, Any]:
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise RuntimeError(f"JSON root is not an object: {path}")
return value
def _safe_artifact(root: Path, encoded: object) -> Path:
if not isinstance(encoded, str):
raise RuntimeError("artifact path is not a string")
relative = PurePosixPath(encoded)
if relative.is_absolute() or ".." in relative.parts or not relative.parts:
raise RuntimeError("artifact path is unsafe")
path = root.joinpath(*relative.parts).resolve(strict=True)
if not path.is_file() or not path.is_relative_to(root):
raise RuntimeError("artifact path escaped its root")
return path
def _workspace(root: Path, images_zip: Path) -> tuple[dict[str, Any], dict[str, Path]]:
manifest_path = root / "manifest.json"
manifest = _read_object(manifest_path)
identity = manifest.get("identity")
identity_sha256 = manifest.get("identity_sha256")
if (
manifest.get("schema_version") != WORKSPACE_SCHEMA
or manifest.get("ground_truth") is not False
or not isinstance(identity, dict)
or identity.get("schema_version") != IDENTITY_SCHEMA
or not _valid_sha256(identity_sha256)
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
or manifest.get("workspace_id") != f"e3-cvat-review-{identity_sha256}"
or root.name != manifest.get("workspace_id")
or identity.get("evaluation_pack_id") != EXPECTED_PACK_ID
or identity.get("e3_result_id") != EXPECTED_RESULT_ID
or identity.get("frame_count") != EXPECTED_FRAMES
or _sha256(images_zip) != identity.get("images_zip_sha256")
):
raise RuntimeError("LAB E3 CVAT review workspace is incompatible")
artifacts: dict[str, Path] = {}
descriptors = manifest.get("artifacts")
if not isinstance(descriptors, list):
raise RuntimeError("LAB E3 CVAT artifact list is invalid")
for descriptor in descriptors:
if not isinstance(descriptor, dict):
raise RuntimeError("LAB E3 CVAT artifact descriptor is invalid")
path = _safe_artifact(root, descriptor.get("path"))
if (
path.stat().st_size != descriptor.get("bytes")
or not _valid_sha256(descriptor.get("sha256"))
or _sha256(path) != descriptor["sha256"]
):
raise RuntimeError(f"LAB E3 CVAT artifact changed: {path.name}")
artifacts[path.name] = path
if set(artifacts) != {
"labels.json",
"control-fisheye-mask.zip",
"challenger-kb4-cubemap5.zip",
}:
raise RuntimeError("LAB E3 CVAT artifact set changed")
return manifest, artifacts
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--server", required=True)
parser.add_argument("--username", required=True)
parser.add_argument("--password-env", required=True)
parser.add_argument("--workspace", type=Path, required=True)
parser.add_argument("--images-zip", type=Path, required=True)
parser.add_argument("--report", type=Path, required=True)
args = parser.parse_args()
password = os.environ.get(args.password_env)
if not password:
raise RuntimeError(f"Password environment variable {args.password_env!r} is empty")
workspace = args.workspace.resolve(strict=True)
images_zip = args.images_zip.resolve(strict=True)
manifest, artifacts = _workspace(workspace, images_zip)
labels_document = _read_object(artifacts["labels.json"])
semantic_labels = labels_document.get("semantic_task")
if not isinstance(semantic_labels, list):
raise RuntimeError("semantic labels are absent")
labels = _label_specs(semantic_labels)
task_specs = (
{
"name": "LAB E3 | K1 | EoMT fisheye control | pack 7a983bba",
"annotation": artifacts["control-fisheye-mask.zip"],
"role": "control",
},
{
"name": "LAB E3 | K1 | EoMT KB4 cubemap5 challenger | pack 7a983bba",
"annotation": artifacts["challenger-kb4-cubemap5.zip"],
"role": "challenger",
},
)
with make_client(args.server, credentials=(args.username, password)) as client:
client.check_server_version(fail_if_unsupported=True)
tasks = []
for spec in task_specs:
record = _ensure_task(
client,
name=spec["name"],
labels=labels,
images_path=images_zip,
annotation_path=spec["annotation"],
annotation_format="Segmentation mask 1.1",
expected_shape_count=None,
)
record["role"] = spec["role"]
tasks.append(record)
report = {
"schema_version": "missioncore.lab-e3-cvat-import/v1",
"created_at_utc": datetime.now(UTC)
.isoformat(timespec="milliseconds")
.replace("+00:00", "Z"),
"server": args.server,
"cvat_version": "v2.70.0",
"workspace_id": manifest["workspace_id"],
"workspace_manifest_sha256": _sha256(workspace / "manifest.json"),
"evaluation_pack_id": EXPECTED_PACK_ID,
"e3_result_id": EXPECTED_RESULT_ID,
"ground_truth": False,
"priority_image_ids": manifest["identity"]["priority_image_ids"],
"tasks": tasks,
"next_gate": "two-pass human review and reviewed export",
}
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -Eeuo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
readonly FREE_GIB_FLOOR="${FREE_GIB_FLOOR:-360}"
readonly WINDOWS_ROOT="/mnt/d/NDC_MISSIONCORE"
readonly COMPUTE_ROOT="${WINDOWS_ROOT}/workspace/mission-core-compute"
readonly TOOL_ROOT="/srv/mission-core-cvat/tools/e2-import"
readonly SECRET_FILE="${WINDOWS_ROOT}/secrets/cvat/admin.env"
readonly WORKSPACE_ID="e3-cvat-review-ca599521e345446ca8e9af5a9013062099278e7317f83ff89740c6b092ddc52f"
readonly WORKSPACE_ROOT="${WINDOWS_ROOT}/runtime/annotation/imports/LAB-E3/${WORKSPACE_ID}"
readonly E2_WORKSPACE_ID="annotation-workspace-9a950d1c37d56dc12cc285b13c5addd7795285879cbcb1fbb2d5811c3c69821a"
readonly IMAGES_ZIP="${WINDOWS_ROOT}/runtime/annotation/imports/LAB-E2/${E2_WORKSPACE_ID}/cvat/images.zip"
readonly REPORT_ROOT="${WINDOWS_ROOT}/runtime/annotation/cvat/reports"
if [[ "${EUID}" -ne 0 ]]; then
echo "import_e3_review.sh must run as root" >&2
exit 2
fi
free_gib() {
df -BG --output=avail "${WINDOWS_ROOT}" | tail -n 1 | tr -dc '0-9'
}
guard_disk() {
local stage="$1"
local free
free="$(free_gib)"
printf 'disk_guard stage=%s free_gib=%s floor_gib=%s\n' \
"${stage}" "${free}" "${FREE_GIB_FLOOR}"
if (( free < FREE_GIB_FLOOR )); then
echo "D: free-space floor crossed; refusing further writes" >&2
exit 3
fi
}
wait_for_cvat() {
local attempt http_code
for attempt in $(seq 1 60); do
http_code="$(
curl --silent --output /dev/null --write-out '%{http_code}' \
"http://localhost:8080/api/server/about" || true
)"
if [[ "${http_code}" == "200" ]]; then
printf 'cvat_ready attempt=%s\n' "${attempt}"
return 0
fi
sleep 2
done
echo "CVAT API did not become ready within 120 seconds" >&2
return 1
}
test -x "${TOOL_ROOT}/venv/bin/python"
test -f "${SECRET_FILE}"
test -f "${WORKSPACE_ROOT}/manifest.json"
test -f "${IMAGES_ZIP}"
test -f "${COMPUTE_ROOT}/cvat/import_e2_workspace.py"
test -f "${COMPUTE_ROOT}/cvat/import_e3_review.py"
guard_disk preflight
set -a
# shellcheck disable=SC1090
. "${SECRET_FILE}"
set +a
wait_for_cvat
report="${REPORT_ROOT}/lab-e3-cvat-import-$(date -u +%Y%m%dT%H%M%SZ).json"
"${TOOL_ROOT}/venv/bin/python" "${COMPUTE_ROOT}/cvat/import_e3_review.py" \
--server "http://localhost:8080" \
--username "${CVAT_ADMIN_USERNAME}" \
--password-env CVAT_ADMIN_PASSWORD \
--workspace "${WORKSPACE_ROOT}" \
--images-zip "${IMAGES_ZIP}" \
--report "${report}"
guard_disk imported
printf 'import_report=%s\n' "${report}"

View File

@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -Eeuo pipefail
readonly SSH_ALIAS="mission-gpu"
readonly LOCAL_PORT="${CVAT_LOCAL_PORT:-18080}"
readonly REMOTE_WSL_PORT="18080"
readonly LINUX_START_SCRIPT='/mnt/d/NDC_MISSIONCORE/workspace/mission-core-compute/cvat/start_cvat_runtime.sh'
if curl --max-time 2 --fail --silent \
"http://localhost:${LOCAL_PORT}/api/server/about" >/dev/null 2>&1; then
printf 'cvat_tunnel_ready url=http://localhost:%s\n' "${LOCAL_PORT}"
exit 0
fi
if command -v lsof >/dev/null \
&& lsof -nP -iTCP:"${LOCAL_PORT}" -sTCP:LISTEN >/dev/null 2>&1; then
echo "Local port ${LOCAL_PORT} is already occupied by a non-responsive process" >&2
exit 2
fi
runtime_command="wsl.exe -d MissionCore-CVAT -u root -- bash ${LINUX_START_SCRIPT} --keepalive"
background=false
if [[ "${1:-}" == "--background" ]]; then
background=true
ssh -f "${SSH_ALIAS}" "${runtime_command}"
else
ssh "${SSH_ALIAS}" "${runtime_command}" &
runtime_ssh_pid=$!
cleanup() {
if [[ -n "${tunnel_ssh_pid:-}" ]]; then
kill "${tunnel_ssh_pid}" 2>/dev/null || true
fi
kill "${runtime_ssh_pid}" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
fi
for attempt in $(seq 1 30); do
cvat_ip="$(
ssh "${SSH_ALIAS}" \
"wsl.exe -d MissionCore-CVAT -u root -- hostname -I" \
| tr -d '\r' | awk '{print $1}'
)"
if [[ "${cvat_ip}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
break
fi
sleep 1
done
if [[ ! "${cvat_ip:-}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "MissionCore-CVAT did not return a valid WSL address" >&2
if [[ "${background}" == false ]]; then
kill "${runtime_ssh_pid}" 2>/dev/null || true
fi
exit 3
fi
forward=(
-N
-L "${LOCAL_PORT}:${cvat_ip}:${REMOTE_WSL_PORT}"
-o ExitOnForwardFailure=yes
"${SSH_ALIAS}"
)
if [[ "${background}" == true ]]; then
ssh -f "${forward[@]}"
else
ssh "${forward[@]}" &
tunnel_ssh_pid=$!
fi
for attempt in $(seq 1 60); do
if curl --max-time 2 --fail --silent \
"http://localhost:${LOCAL_PORT}/api/server/about" >/dev/null 2>&1; then
printf 'cvat_tunnel_ready url=http://localhost:%s wsl_ip=%s\n' \
"${LOCAL_PORT}" "${cvat_ip}"
if [[ "${background}" == true ]]; then
exit 0
fi
wait "${tunnel_ssh_pid}"
tunnel_status=$?
kill "${runtime_ssh_pid}" 2>/dev/null || true
exit "${tunnel_status}"
fi
sleep 2
done
echo "CVAT tunnel did not become ready within 120 seconds" >&2
if [[ "${background}" == false ]]; then
kill "${tunnel_ssh_pid}" "${runtime_ssh_pid}" 2>/dev/null || true
fi
exit 4

View File

@ -0,0 +1,132 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Do not inherit Windows executables into this D-only runtime. In particular,
# Docker Desktop's docker.exe belongs to a different image store.
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
readonly CVAT_TAG="${CVAT_TAG:-v2.70.0}"
readonly FREE_GIB_FLOOR="${FREE_GIB_FLOOR:-360}"
readonly WINDOWS_ROOT="/mnt/d/NDC_MISSIONCORE"
readonly COMPUTE_ROOT="${WINDOWS_ROOT}/workspace/mission-core-compute"
readonly RUNTIME_ROOT="${WINDOWS_ROOT}/runtime/annotation/cvat"
readonly CVAT_ROOT="${RUNTIME_ROOT}/source/cvat-${CVAT_TAG}"
readonly STATE_ROOT="/srv/mission-core-cvat"
readonly OVERRIDE_FILE="${COMPUTE_ROOT}/cvat/docker-compose.override.yml"
if [[ "${EUID}" -ne 0 ]]; then
echo "provision_cvat_wsl.sh must run as root" >&2
exit 2
fi
free_gib() {
df -BG --output=avail "${WINDOWS_ROOT}" | tail -n 1 | tr -dc '0-9'
}
guard_disk() {
local stage="$1"
local free
free="$(free_gib)"
printf 'disk_guard stage=%s free_gib=%s floor_gib=%s\n' \
"${stage}" "${free}" "${FREE_GIB_FLOOR}"
if (( free < FREE_GIB_FLOOR )); then
echo "D: free-space floor crossed; refusing further writes" >&2
exit 3
fi
}
guard_disk preflight
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y ca-certificates curl git
if ! dpkg-query -W -f='${Status}' docker-ce 2>/dev/null \
| grep -qx 'install ok installed'; then
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
# shellcheck disable=SC1091
. /etc/os-release
arch="$(dpkg --print-architecture)"
printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu %s stable\n' \
"${arch}" "${VERSION_CODENAME}" >/etc/apt/sources.list.d/docker.list
apt-get update
apt-get install -y \
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
fi
systemctl enable --now docker
docker version
docker compose version
guard_disk docker-engine
install -d -m 0750 \
"${STATE_ROOT}/volumes/cvat_db" \
"${STATE_ROOT}/volumes/cvat_data" \
"${STATE_ROOT}/volumes/cvat_keys" \
"${STATE_ROOT}/volumes/cvat_logs" \
"${STATE_ROOT}/volumes/cvat_inmem_db" \
"${STATE_ROOT}/volumes/cvat_events_db" \
"${STATE_ROOT}/volumes/cvat_cache_db" \
"${RUNTIME_ROOT}/source" \
"${RUNTIME_ROOT}/reports"
if [[ ! -d "${CVAT_ROOT}/.git" ]]; then
git clone --depth 1 --branch "${CVAT_TAG}" \
https://github.com/cvat-ai/cvat.git "${CVAT_ROOT}"
fi
test "$(git -C "${CVAT_ROOT}" describe --tags --exact-match)" = "${CVAT_TAG}"
test -f "${OVERRIDE_FILE}"
guard_disk cvat-source
export CVAT_VERSION="${CVAT_TAG}"
export CVAT_HOST="localhost"
export CVAT_HTTP_PORT="8080"
export COMPOSE_PROJECT_NAME="missioncore-cvat"
compose=(
docker compose
--project-directory "${CVAT_ROOT}"
-f "${CVAT_ROOT}/docker-compose.yml"
-f "${OVERRIDE_FILE}"
)
"${compose[@]}" config --quiet
mapfile -t images < <("${compose[@]}" config --images | sort -u)
for image in "${images[@]}"; do
guard_disk "before-pull:${image}"
docker pull "${image}"
guard_disk "after-pull:${image}"
done
"${compose[@]}" up -d --no-build
guard_disk cvat-started
deadline=$((SECONDS + 600))
until curl --fail --silent --show-error http://localhost:8080/api/server/about \
>/dev/null; do
if (( SECONDS >= deadline )); then
"${compose[@]}" ps
echo "CVAT did not become ready within 600 seconds" >&2
exit 4
fi
sleep 5
done
report="${RUNTIME_ROOT}/reports/deployment-$(date -u +%Y%m%dT%H%M%SZ).txt"
{
printf 'cvat_tag=%s\n' "${CVAT_TAG}"
printf 'cvat_commit=%s\n' "$(git -C "${CVAT_ROOT}" rev-parse HEAD)"
printf 'docker_version=%s\n' "$(docker version --format '{{.Server.Version}}')"
printf 'compose_version=%s\n' "$(docker compose version --short)"
printf 'free_gib=%s\n' "$(free_gib)"
printf 'generated_at_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
docker image inspect "${images[@]}" \
--format 'image={{index .RepoTags 0}} id={{.Id}} size={{.Size}}'
"${compose[@]}" ps
} | tee "${report}"
printf 'CVAT ready at http://localhost:8080\nreport=%s\n' "${report}"

View File

@ -0,0 +1,69 @@
#!/usr/bin/env bash
set -Eeuo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
readonly FREE_GIB_FLOOR="${FREE_GIB_FLOOR:-360}"
readonly WINDOWS_ROOT="/mnt/d/NDC_MISSIONCORE"
readonly COMPUTE_ROOT="${WINDOWS_ROOT}/workspace/mission-core-compute"
readonly CVAT_ROOT="${WINDOWS_ROOT}/runtime/annotation/cvat/source/cvat-v2.70.0"
readonly OVERRIDE_FILE="${COMPUTE_ROOT}/cvat/docker-compose.override.yml"
if [[ "${EUID}" -ne 0 ]]; then
echo "start_cvat_runtime.sh must run as root" >&2
exit 2
fi
free_gib="$(df -BG --output=avail "${WINDOWS_ROOT}" | tail -n 1 | tr -dc '0-9')"
printf 'disk_guard stage=start free_gib=%s floor_gib=%s\n' \
"${free_gib}" "${FREE_GIB_FLOOR}"
if (( free_gib < FREE_GIB_FLOOR )); then
echo "D: free-space floor crossed; refusing to start CVAT" >&2
exit 3
fi
test -f "${CVAT_ROOT}/docker-compose.yml"
test -f "${OVERRIDE_FILE}"
systemctl start docker
export CVAT_VERSION="v2.70.0"
export CVAT_HOST="localhost"
export CVAT_HTTP_PORT="8080"
export COMPOSE_PROJECT_NAME="missioncore-cvat"
compose=(
docker compose
--project-directory "${CVAT_ROOT}"
-f "${CVAT_ROOT}/docker-compose.yml"
-f "${OVERRIDE_FILE}"
)
"${compose[@]}" up -d --no-build
deadline=$((SECONDS + 180))
while true; do
about_http="$(
curl --silent --output /dev/null --write-out '%{http_code}' \
http://localhost:8080/api/server/about || true
)"
tasks_http="$(
curl --silent --output /dev/null --write-out '%{http_code}' \
http://localhost:8080/api/tasks || true
)"
if [[ "${about_http}" == "200" ]] \
&& [[ "${tasks_http}" == "200" || "${tasks_http}" == "401" || "${tasks_http}" == "403" ]]; then
break
fi
if (( SECONDS >= deadline )); then
"${compose[@]}" ps
echo "CVAT did not become ready within 180 seconds" >&2
exit 4
fi
sleep 2
done
printf 'cvat_ready about_http=%s tasks_http=%s free_gib=%s\n' \
"${about_http}" "${tasks_http}" "${free_gib}"
if [[ "${1:-}" == "--keepalive" ]]; then
exec sleep infinity
fi

View File

@ -0,0 +1,87 @@
{
"schema_version": "missioncore.e10-integrated-perception-profile/v1",
"mode": "full-session-qualification",
"source": {
"source_id": "sensor.camera.right",
"resolution": [800, 600],
"calibration_slot": "camera_1",
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
},
"selection": {
"required_frame_count": 4489,
"required_source_start_frame_index": 0,
"required_source_end_frame_index": 4488,
"minimum_source_span_seconds": 448.0
},
"replay": {
"speed": 1.0,
"detector_queue_capacity": 2,
"semantic_queue_capacity": 1,
"semantic_sample_every_frames": 5,
"semantic_ttl_ms": 750.0
},
"association": {
"box_inset": {
"bottom_fraction": 0.02,
"horizontal_fraction": 0.05,
"top_fraction": 0.04
},
"depth_cluster_gap_fraction": 0.06,
"depth_cluster_minimum_gap_m": 0.5,
"group_nms_iou_threshold": 0.55,
"maximum_cuboid_span_m": 12.0,
"maximum_distance_innovation_fraction": 0.25,
"maximum_distance_innovation_m": 1.5,
"maximum_oriented_extent_m": {
"bicycle": [3.5, 2.0, 2.5],
"motorcycle": [3.5, 2.0, 2.5],
"person": [1.5, 1.5, 2.8],
"vehicle": [6.5, 3.5, 4.0]
},
"minimum_cuboid_extent_m": 0.15,
"minimum_support_points": {
"bicycle": 3,
"motorcycle": 3,
"person": 3,
"vehicle": 5
},
"semantic_ids": {
"bicycle": [2],
"motorcycle": [3],
"person": [1],
"vehicle": [4, 5]
},
"spatial_cluster_radius_m": {
"bicycle": 0.9,
"motorcycle": 0.9,
"person": 0.9,
"vehicle": 1.5
},
"vehicle_labels": ["car", "truck", "bus"],
"distance_history_frames": 5
},
"world_state": {
"velocity_history_limit_s": 1.0,
"clearance": {
"sector_count": 72,
"minimum_range_m": 0.5,
"maximum_range_m": 30.0,
"ground_percentile": 5.0,
"minimum_height_above_ground_m": 0.2,
"maximum_height_above_ground_m": 3.0,
"front_half_angle_degrees": 15.0
}
},
"acceptance": {
"detector_minimum_effective_fps": 9.5,
"detector_maximum_drop_fraction": 0.0,
"maximum_p95_world_state_age_ms": 175.0,
"semantic_minimum_effective_fps": 1.8,
"semantic_maximum_drop_fraction": 0.05,
"semantic_maximum_p95_completion_age_ms": 400.0,
"minimum_fresh_semantic_coverage": 0.9,
"minimum_lidar_fused_frames": 3500,
"minimum_accepted_cuboids": 500,
"require_zero_failures": true
}
}

View File

@ -0,0 +1,81 @@
{
"schema_version": "missioncore.e10-integrated-perception-profile/v1",
"mode": "qualification",
"source": {
"source_id": "sensor.camera.right",
"resolution": [800, 600],
"calibration_slot": "camera_1",
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
},
"replay": {
"speed": 1.0,
"detector_queue_capacity": 2,
"semantic_queue_capacity": 1,
"semantic_sample_every_frames": 5,
"semantic_ttl_ms": 750.0
},
"association": {
"box_inset": {
"bottom_fraction": 0.02,
"horizontal_fraction": 0.05,
"top_fraction": 0.04
},
"depth_cluster_gap_fraction": 0.06,
"depth_cluster_minimum_gap_m": 0.5,
"group_nms_iou_threshold": 0.55,
"maximum_cuboid_span_m": 12.0,
"maximum_distance_innovation_fraction": 0.25,
"maximum_distance_innovation_m": 1.5,
"maximum_oriented_extent_m": {
"bicycle": [3.5, 2.0, 2.5],
"motorcycle": [3.5, 2.0, 2.5],
"person": [1.5, 1.5, 2.8],
"vehicle": [6.5, 3.5, 4.0]
},
"minimum_cuboid_extent_m": 0.15,
"minimum_support_points": {
"bicycle": 3,
"motorcycle": 3,
"person": 3,
"vehicle": 5
},
"semantic_ids": {
"bicycle": [2],
"motorcycle": [3],
"person": [1],
"vehicle": [4, 5]
},
"spatial_cluster_radius_m": {
"bicycle": 0.9,
"motorcycle": 0.9,
"person": 0.9,
"vehicle": 1.5
},
"vehicle_labels": ["car", "truck", "bus"],
"distance_history_frames": 5
},
"world_state": {
"velocity_history_limit_s": 1.0,
"clearance": {
"sector_count": 72,
"minimum_range_m": 0.5,
"maximum_range_m": 30.0,
"ground_percentile": 5.0,
"minimum_height_above_ground_m": 0.2,
"maximum_height_above_ground_m": 3.0,
"front_half_angle_degrees": 15.0
}
},
"acceptance": {
"detector_minimum_effective_fps": 9.5,
"detector_maximum_drop_fraction": 0.005,
"maximum_p95_world_state_age_ms": 175.0,
"semantic_minimum_effective_fps": 1.8,
"semantic_maximum_drop_fraction": 0.05,
"semantic_maximum_p95_completion_age_ms": 400.0,
"minimum_fresh_semantic_coverage": 0.9,
"minimum_lidar_fused_frames": 450,
"minimum_accepted_cuboids": 100,
"require_zero_failures": true
}
}

View File

@ -0,0 +1,78 @@
{
"schema_version": "missioncore.e10-integrated-perception-profile/v1",
"mode": "semantic-loss-negative-control",
"source": {
"source_id": "sensor.camera.right",
"resolution": [800, 600],
"calibration_slot": "camera_1",
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
},
"replay": {
"speed": 1.0,
"detector_queue_capacity": 2,
"semantic_queue_capacity": 1,
"semantic_sample_every_frames": 5,
"semantic_ttl_ms": 750.0
},
"semantic_loss": {
"stop_after_completed_results": 20
},
"association": {
"box_inset": {
"bottom_fraction": 0.02,
"horizontal_fraction": 0.05,
"top_fraction": 0.04
},
"depth_cluster_gap_fraction": 0.06,
"depth_cluster_minimum_gap_m": 0.5,
"group_nms_iou_threshold": 0.55,
"maximum_cuboid_span_m": 12.0,
"maximum_distance_innovation_fraction": 0.25,
"maximum_distance_innovation_m": 1.5,
"maximum_oriented_extent_m": {
"bicycle": [3.5, 2.0, 2.5],
"motorcycle": [3.5, 2.0, 2.5],
"person": [1.5, 1.5, 2.8],
"vehicle": [6.5, 3.5, 4.0]
},
"minimum_cuboid_extent_m": 0.15,
"minimum_support_points": {
"bicycle": 3,
"motorcycle": 3,
"person": 3,
"vehicle": 5
},
"semantic_ids": {
"bicycle": [2],
"motorcycle": [3],
"person": [1],
"vehicle": [4, 5]
},
"spatial_cluster_radius_m": {
"bicycle": 0.9,
"motorcycle": 0.9,
"person": 0.9,
"vehicle": 1.5
},
"vehicle_labels": ["car", "truck", "bus"],
"distance_history_frames": 5
},
"world_state": {
"velocity_history_limit_s": 1.0,
"clearance": {
"sector_count": 72,
"minimum_range_m": 0.5,
"maximum_range_m": 30.0,
"ground_percentile": 5.0,
"minimum_height_above_ground_m": 0.2,
"maximum_height_above_ground_m": 3.0,
"front_half_angle_degrees": 15.0
}
},
"acceptance": {
"detector_minimum_effective_fps": 9.5,
"detector_maximum_drop_fraction": 0.005,
"maximum_p95_world_state_age_ms": 175.0,
"minimum_stale_detector_frames": 450
}
}

View File

@ -0,0 +1,144 @@
{
"schema_version": "missioncore.e10-integrated-perception-profile/v1",
"mode": "pilot",
"source": {
"source_id": "sensor.camera.right",
"resolution": [800, 600],
"calibration_slot": "camera_1",
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
},
"replay": {
"speed": 1.0,
"detector_queue_capacity": 2,
"semantic_queue_capacity": 1,
"semantic_sample_every_frames": 5,
"semantic_ttl_ms": 750.0
},
"association": {
"box_inset": {
"bottom_fraction": 0.02,
"horizontal_fraction": 0.05,
"top_fraction": 0.04
},
"depth_cluster_gap_fraction": 0.06,
"depth_cluster_minimum_gap_m": 0.5,
"group_nms_iou_threshold": 0.5,
"maximum_cuboid_span_m": 15.0,
"maximum_distance_innovation_fraction": 0.25,
"maximum_distance_innovation_m": 1.5,
"maximum_oriented_extent_m": {
"bicycle": [3.5, 2.0, 2.5],
"motorcycle": [3.5, 2.0, 2.5],
"person": [1.5, 1.5, 2.8],
"vehicle": [12.5, 4.0, 4.5]
},
"minimum_cuboid_extent_m": 0.15,
"minimum_support_points": {
"bicycle": 4,
"motorcycle": 4,
"person": 4,
"vehicle": 8
},
"semantic_ids": {
"bicycle": [2],
"motorcycle": [3],
"person": [1],
"vehicle": [4, 5]
},
"spatial_cluster_radius_m": {
"bicycle": 0.9,
"motorcycle": 0.9,
"person": 0.9,
"vehicle": 1.5
},
"vehicle_labels": ["car", "truck", "bus"],
"distance_history_frames": 5
},
"cuboid_completion": {
"mode": "class-prior-amodal-v1",
"failure_policy": "reject",
"classes": {
"person": {
"nominal_size_m": [0.55, 0.55, 1.72],
"minimum_size_m": [0.35, 0.35, 1.3],
"maximum_size_m": [1.2, 1.2, 2.3],
"support_padding_m": [0.12, 0.12, 0.12]
},
"bicycle": {
"nominal_size_m": [1.8, 0.65, 1.5],
"minimum_size_m": [1.2, 0.4, 1.0],
"maximum_size_m": [2.5, 1.2, 2.2],
"support_padding_m": [0.18, 0.12, 0.12]
},
"motorcycle": {
"nominal_size_m": [2.1, 0.8, 1.45],
"minimum_size_m": [1.4, 0.5, 1.0],
"maximum_size_m": [3.0, 1.4, 2.2],
"support_padding_m": [0.2, 0.14, 0.14]
},
"car": {
"nominal_size_m": [4.5, 1.85, 1.55],
"minimum_size_m": [3.2, 1.45, 1.2],
"maximum_size_m": [5.8, 2.4, 2.3],
"support_padding_m": [0.25, 0.18, 0.15]
},
"truck": {
"nominal_size_m": [7.0, 2.5, 3.0],
"minimum_size_m": [4.8, 1.8, 1.8],
"maximum_size_m": [12.5, 3.2, 4.2],
"support_padding_m": [0.35, 0.22, 0.2]
},
"bus": {
"nominal_size_m": [10.5, 2.55, 3.2],
"minimum_size_m": [7.0, 2.1, 2.5],
"maximum_size_m": [13.5, 3.2, 4.2],
"support_padding_m": [0.4, 0.24, 0.2]
}
},
"ground": {
"local_radius_m": 2.5,
"lower_percentile": 8.0,
"maximum_below_support_m": 1.2,
"maximum_above_support_m": 0.15,
"fallback_below_support_m": 0.25
},
"orientation": {
"minimum_anisotropy_ratio": 1.35,
"face_width_switch_fraction": 1.05
},
"temporal": {
"center_alpha": 0.4,
"size_alpha": 0.2,
"yaw_alpha": 0.25,
"maximum_center_innovation_m": 2.0,
"maximum_yaw_innovation_degrees": 55.0,
"maximum_idle_s": 1.0,
"confirmation_hits": 3
},
"minimum_support_coverage_fraction": 0.75
},
"world_state": {
"velocity_history_limit_s": 1.0,
"clearance": {
"sector_count": 72,
"minimum_range_m": 0.5,
"maximum_range_m": 30.0,
"ground_percentile": 5.0,
"minimum_height_above_ground_m": 0.2,
"maximum_height_above_ground_m": 3.0,
"front_half_angle_degrees": 15.0
}
},
"acceptance": {
"detector_minimum_effective_fps": 9.5,
"detector_maximum_drop_fraction": 0.005,
"maximum_p95_world_state_age_ms": 175.0,
"semantic_minimum_effective_fps": 1.8,
"semantic_maximum_drop_fraction": 0.05,
"semantic_maximum_p95_completion_age_ms": 400.0,
"minimum_fresh_semantic_coverage": 0.9,
"minimum_lidar_fused_frames": 450,
"minimum_accepted_cuboids": 100,
"require_zero_failures": true
}
}

View File

@ -0,0 +1,150 @@
{
"schema_version": "missioncore.e10-integrated-perception-profile/v1",
"mode": "full-session-qualification",
"source": {
"source_id": "sensor.camera.right",
"resolution": [800, 600],
"calibration_slot": "camera_1",
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
},
"selection": {
"required_frame_count": 4489,
"required_source_start_frame_index": 0,
"required_source_end_frame_index": 4488,
"minimum_source_span_seconds": 448.0
},
"replay": {
"speed": 1.0,
"detector_queue_capacity": 2,
"semantic_queue_capacity": 1,
"semantic_sample_every_frames": 5,
"semantic_ttl_ms": 750.0
},
"association": {
"box_inset": {
"bottom_fraction": 0.02,
"horizontal_fraction": 0.05,
"top_fraction": 0.04
},
"depth_cluster_gap_fraction": 0.06,
"depth_cluster_minimum_gap_m": 0.5,
"group_nms_iou_threshold": 0.5,
"maximum_cuboid_span_m": 15.0,
"maximum_distance_innovation_fraction": 0.25,
"maximum_distance_innovation_m": 1.5,
"maximum_oriented_extent_m": {
"bicycle": [3.5, 2.0, 2.5],
"motorcycle": [3.5, 2.0, 2.5],
"person": [1.5, 1.5, 2.8],
"vehicle": [12.5, 4.0, 4.5]
},
"minimum_cuboid_extent_m": 0.15,
"minimum_support_points": {
"bicycle": 4,
"motorcycle": 4,
"person": 4,
"vehicle": 8
},
"semantic_ids": {
"bicycle": [2],
"motorcycle": [3],
"person": [1],
"vehicle": [4, 5]
},
"spatial_cluster_radius_m": {
"bicycle": 0.9,
"motorcycle": 0.9,
"person": 0.9,
"vehicle": 1.5
},
"vehicle_labels": ["car", "truck", "bus"],
"distance_history_frames": 5
},
"cuboid_completion": {
"mode": "class-prior-amodal-v1",
"failure_policy": "reject",
"classes": {
"person": {
"nominal_size_m": [0.55, 0.55, 1.72],
"minimum_size_m": [0.35, 0.35, 1.3],
"maximum_size_m": [1.2, 1.2, 2.3],
"support_padding_m": [0.12, 0.12, 0.12]
},
"bicycle": {
"nominal_size_m": [1.8, 0.65, 1.5],
"minimum_size_m": [1.2, 0.4, 1.0],
"maximum_size_m": [2.5, 1.2, 2.2],
"support_padding_m": [0.18, 0.12, 0.12]
},
"motorcycle": {
"nominal_size_m": [2.1, 0.8, 1.45],
"minimum_size_m": [1.4, 0.5, 1.0],
"maximum_size_m": [3.0, 1.4, 2.2],
"support_padding_m": [0.2, 0.14, 0.14]
},
"car": {
"nominal_size_m": [4.5, 1.85, 1.55],
"minimum_size_m": [3.2, 1.45, 1.2],
"maximum_size_m": [5.8, 2.4, 2.3],
"support_padding_m": [0.25, 0.18, 0.15]
},
"truck": {
"nominal_size_m": [7.0, 2.5, 3.0],
"minimum_size_m": [4.8, 1.8, 1.8],
"maximum_size_m": [12.5, 3.2, 4.2],
"support_padding_m": [0.35, 0.22, 0.2]
},
"bus": {
"nominal_size_m": [10.5, 2.55, 3.2],
"minimum_size_m": [7.0, 2.1, 2.5],
"maximum_size_m": [13.5, 3.2, 4.2],
"support_padding_m": [0.4, 0.24, 0.2]
}
},
"ground": {
"local_radius_m": 2.5,
"lower_percentile": 8.0,
"maximum_below_support_m": 1.2,
"maximum_above_support_m": 0.15,
"fallback_below_support_m": 0.25
},
"orientation": {
"minimum_anisotropy_ratio": 1.35,
"face_width_switch_fraction": 1.05
},
"temporal": {
"center_alpha": 0.4,
"size_alpha": 0.2,
"yaw_alpha": 0.25,
"maximum_center_innovation_m": 2.0,
"maximum_yaw_innovation_degrees": 55.0,
"maximum_idle_s": 1.0,
"confirmation_hits": 3
},
"minimum_support_coverage_fraction": 0.75
},
"world_state": {
"velocity_history_limit_s": 1.0,
"clearance": {
"sector_count": 72,
"minimum_range_m": 0.5,
"maximum_range_m": 30.0,
"ground_percentile": 5.0,
"minimum_height_above_ground_m": 0.2,
"maximum_height_above_ground_m": 3.0,
"front_half_angle_degrees": 15.0
}
},
"acceptance": {
"detector_minimum_effective_fps": 9.5,
"detector_maximum_drop_fraction": 0.005,
"maximum_p95_world_state_age_ms": 175.0,
"semantic_minimum_effective_fps": 1.8,
"semantic_maximum_drop_fraction": 0.05,
"semantic_maximum_p95_completion_age_ms": 400.0,
"minimum_fresh_semantic_coverage": 0.9,
"minimum_lidar_fused_frames": 3500,
"minimum_accepted_cuboids": 1500,
"require_zero_failures": true
}
}

View File

@ -0,0 +1,51 @@
{
"schema_version": "missioncore.e15-shadow-inference-profile/v1",
"mode": "replay-shadow-gate",
"authority": {
"commands_enabled": false,
"navigation_or_safety_accepted": false
},
"source": {
"source_id": "sensor.camera.right",
"resolution": [800, 600],
"calibration_slot": "camera_1",
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
},
"transport": {
"wire_schema": "missioncore.live-perception-wire/v1",
"camera_media": "persistent-fmp4-pyav",
"pyav_version": "18.0.0",
"maximum_media_buffer_bytes": 8388608,
"camera_metadata_capacity": 16
},
"scheduling": {
"detector_queue_capacity": 2,
"semantic_queue_capacity": 1,
"semantic_sample_every_frames": 5,
"semantic_ttl_ms": 750.0,
"sensor_wait_ms": 90.0
},
"temporal": {
"binding": "nearest-recorded-host-arrival-best-effort",
"maximum_lidar_camera_delta_ms": 100.0,
"maximum_pose_point_delta_ms": 100.0,
"buffer_capacity_per_modality": 32,
"retention_seconds": 3.0,
"clock_qualification": "not-hardware-synchronized"
},
"acceptance": {
"minimum_camera_frames": 140,
"detector_minimum_effective_fps": 9.5,
"detector_maximum_drop_fraction": 0.01,
"semantic_minimum_effective_fps": 1.8,
"semantic_maximum_drop_fraction": 0.05,
"semantic_maximum_p95_completion_age_ms": 400.0,
"minimum_fresh_semantic_coverage": 0.9,
"minimum_fused_fraction": 0.85,
"maximum_p95_decode_age_ms": 80.0,
"maximum_p95_world_state_age_ms": 200.0,
"require_zero_transport_gaps": true,
"require_zero_camera_sequence_gaps": true,
"require_zero_failures": true
}
}

Some files were not shown because too many files have changed in this diff Show More