fix(m4): retain compact obstacle evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 22:16:33 +03:00
parent 13ed096f80
commit aacc6dc43b
24 changed files with 981 additions and 410 deletions
@@ -20,6 +20,7 @@ export function LaboratoryEvidenceViewer<
U extends string = string,
>({
label,
className,
mode,
modes,
expanded,
@@ -31,6 +32,7 @@ export function LaboratoryEvidenceViewer<
children,
}: {
label: string;
className?: string;
mode: T;
modes: readonly LaboratoryEvidenceViewerMode<T>[];
expanded: boolean;
@@ -62,7 +64,10 @@ export function LaboratoryEvidenceViewer<
const viewer = (
<section
className="laboratory-evidence-viewer"
className={[
"laboratory-evidence-viewer",
className,
].filter(Boolean).join(" ")}
data-expanded={expanded ? "true" : undefined}
aria-label={label}
>
@@ -0,0 +1,110 @@
import { useEffect, useRef } from "react";
export type RecordedEvidenceBoxTone =
| "accent"
| "danger"
| "success"
| "warning"
| "neutral";
export interface RecordedEvidenceBox {
boxXyxy: readonly [number, number, number, number];
label: string;
tone: RecordedEvidenceBoxTone;
dashed?: boolean;
}
function rgba(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
alpha = 1,
): string {
const channels = getComputedStyle(host)
.getPropertyValue(token)
.trim()
.match(/[\d.]+/g)
?.slice(0, 3)
.map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
function toneColor(host: HTMLElement, tone: RecordedEvidenceBoxTone): string {
if (tone === "danger") return rgba(host, "--nodedc-danger-rgb", [255, 104, 112]);
if (tone === "success") return rgba(host, "--nodedc-success-rgb", [181, 255, 90]);
if (tone === "warning") return rgba(host, "--nodedc-warning-rgb", [255, 197, 92]);
if (tone === "neutral") return rgba(host, "--nodedc-foreground-rgb", [245, 245, 245], 0.7);
return rgba(host, "--nodedc-accent-rgb", [232, 56, 126]);
}
export function RecordedEvidenceBoxOverlay({
imageWidth,
imageHeight,
boxes,
ariaLabel,
}: {
imageWidth: number;
imageHeight: number;
boxes: readonly RecordedEvidenceBox[];
ariaLabel: string;
}) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
const host = canvas?.parentElement;
if (!host || !canvas) return;
const context = canvas.getContext("2d");
if (!context) return;
const render = () => {
const width = Math.max(host.clientWidth, 1);
const height = Math.max(host.clientHeight, 1);
const pixelRatio = Math.min(window.devicePixelRatio, 1.5);
canvas.width = Math.round(width * pixelRatio);
canvas.height = Math.round(height * pixelRatio);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
context.clearRect(0, 0, width, height);
const scale = Math.min(width / imageWidth, height / imageHeight);
const drawWidth = imageWidth * scale;
const drawHeight = imageHeight * scale;
const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
for (const item of boxes) {
const [left, top, right, bottom] = item.boxXyxy;
const x = offsetX + left * scale;
const y = offsetY + top * scale;
const boxWidth = (right - left) * scale;
const boxHeight = (bottom - top) * scale;
const stroke = toneColor(host, item.tone);
context.strokeStyle = stroke;
context.lineWidth = Math.max(1.5, 2 * scale);
context.setLineDash(item.dashed ? [5, 4] : []);
context.strokeRect(x, y, boxWidth, boxHeight);
context.setLineDash([]);
const fontSize = Math.max(9, 10 * scale);
context.font = `650 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = Math.min(drawWidth, context.measureText(item.label).width + 8);
const labelHeight = fontSize + 6;
const labelX = Math.min(offsetX + drawWidth - labelWidth, Math.max(offsetX, x));
const labelY = Math.max(offsetY, y - labelHeight);
context.fillStyle = rgba(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.9);
context.fillRect(labelX, labelY, labelWidth, labelHeight);
context.fillStyle = stroke;
context.fillText(item.label, labelX + 4, labelY + fontSize + 1, labelWidth - 8);
}
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [boxes, imageHeight, imageWidth]);
return <canvas ref={canvasRef} role="img" aria-label={ariaLabel} />;
}
@@ -0,0 +1,58 @@
import { useEffect, useState } from "react";
import { Icon } from "@nodedc/ui-react";
import {
RecordedEvidenceBoxOverlay,
type RecordedEvidenceBox,
} from "./RecordedEvidenceBoxOverlay";
export function RecordedEvidenceImageScene({
src,
imageWidth,
imageHeight,
boxes,
ariaLabel,
}: {
src: string;
imageWidth: number;
imageHeight: number;
boxes: readonly RecordedEvidenceBox[];
ariaLabel: string;
}) {
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
useEffect(() => setState("loading"), [src]);
return (
<div className="recorded-evidence-image-scene">
<img
src={src}
alt=""
draggable={false}
onLoad={() => setState("ready")}
onError={() => setState("error")}
/>
{state === "ready" ? (
<RecordedEvidenceBoxOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
boxes={boxes}
ariaLabel={ariaLabel}
/>
) : (
<div className="l3-visual-audit__state" role="status">
{state === "loading" ? (
<span className="busy-indicator" aria-hidden="true" />
) : (
<Icon name="alert" size={18} />
)}
<span>
{state === "loading"
? "Декодируем один точный CAMERA-кадр"
: "Точный CAMERA-кадр недоступен."}
</span>
</div>
)}
</div>
);
}
@@ -1,48 +1,15 @@
import { useEffect, useRef } from "react";
import {
RecordedFmp4Player,
type RecordedObservationPlayback,
} from "../RecordedFmp4Player";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
import {
RecordedEvidenceBoxOverlay,
type RecordedEvidenceBox,
type RecordedEvidenceBoxTone,
} from "./RecordedEvidenceBoxOverlay";
export type RecordedEvidenceBoxTone =
| "accent"
| "danger"
| "success"
| "warning"
| "neutral";
export interface RecordedEvidenceBox {
boxXyxy: readonly [number, number, number, number];
label: string;
tone: RecordedEvidenceBoxTone;
dashed?: boolean;
}
function rgba(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
alpha = 1,
): string {
const channels = getComputedStyle(host)
.getPropertyValue(token)
.trim()
.match(/[\d.]+/g)
?.slice(0, 3)
.map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
function toneColor(host: HTMLElement, tone: RecordedEvidenceBoxTone): string {
if (tone === "danger") return rgba(host, "--nodedc-danger-rgb", [255, 104, 112]);
if (tone === "success") return rgba(host, "--nodedc-success-rgb", [181, 255, 90]);
if (tone === "warning") return rgba(host, "--nodedc-warning-rgb", [255, 197, 92]);
if (tone === "neutral") return rgba(host, "--nodedc-foreground-rgb", [245, 245, 245], 0.7);
return rgba(host, "--nodedc-accent-rgb", [232, 56, 126]);
}
export type { RecordedEvidenceBox, RecordedEvidenceBoxTone };
export function RecordedEvidenceVideoScene({
source,
@@ -61,66 +28,8 @@ export function RecordedEvidenceVideoScene({
ariaLabel: string;
onPlaybackChange: (playback: RecordedObservationPlayback) => void;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const host = hostRef.current;
const canvas = canvasRef.current;
if (!host || !canvas) return;
const context = canvas.getContext("2d");
if (!context) return;
const render = () => {
const width = Math.max(host.clientWidth, 1);
const height = Math.max(host.clientHeight, 1);
const pixelRatio = Math.min(window.devicePixelRatio, 1.5);
canvas.width = Math.round(width * pixelRatio);
canvas.height = Math.round(height * pixelRatio);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
context.clearRect(0, 0, width, height);
const scale = Math.min(width / imageWidth, height / imageHeight);
const drawWidth = imageWidth * scale;
const drawHeight = imageHeight * scale;
const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
for (const item of boxes) {
const [left, top, right, bottom] = item.boxXyxy;
const x = offsetX + left * scale;
const y = offsetY + top * scale;
const boxWidth = (right - left) * scale;
const boxHeight = (bottom - top) * scale;
const stroke = toneColor(host, item.tone);
context.strokeStyle = stroke;
context.lineWidth = Math.max(1.5, 2 * scale);
context.setLineDash(item.dashed ? [5, 4] : []);
context.strokeRect(x, y, boxWidth, boxHeight);
context.setLineDash([]);
const fontSize = Math.max(9, 10 * scale);
context.font = `650 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = Math.min(drawWidth, context.measureText(item.label).width + 8);
const labelHeight = fontSize + 6;
const labelX = Math.min(offsetX + drawWidth - labelWidth, Math.max(offsetX, x));
const labelY = Math.max(offsetY, y - labelHeight);
context.fillStyle = rgba(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.9);
context.fillRect(labelX, labelY, labelWidth, labelHeight);
context.fillStyle = stroke;
context.fillText(item.label, labelX + 4, labelY + fontSize + 1, labelWidth - 8);
}
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [boxes, imageHeight, imageWidth]);
return (
<div className="recorded-evidence-video-scene" ref={hostRef}>
<div className="recorded-evidence-video-scene">
<RecordedFmp4Player
source={source}
playback={playback}
@@ -128,7 +37,12 @@ export function RecordedEvidenceVideoScene({
prepare
onPlaybackChange={onPlaybackChange}
/>
<canvas ref={canvasRef} role="img" aria-label={ariaLabel} />
<RecordedEvidenceBoxOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
boxes={boxes}
ariaLabel={ariaLabel}
/>
</div>
);
}
@@ -90,6 +90,7 @@ export interface M4ThreatCameraProposal {
export interface M4ThreatVisualFrame {
resultId: string;
cameraUrl: string;
ordinal: number;
sequence: number;
frameId: string;
@@ -408,6 +409,7 @@ export async function fetchM4ThreatVisual(
const corridor = object(item.corridor, "M4.6 visual corridor");
return {
resultId: result,
cameraUrl: text(item.camera_url, "M4.6 camera URL"),
ordinal: integer(item.ordinal, "M4.6 ordinal"),
sequence: integer(item.sequence, "M4.6 sequence"),
frameId: text(item.frame_id, "M4.6 frame id"),
@@ -46,7 +46,8 @@
user-select: none;
}
.recorded-evidence-video-scene {
.recorded-evidence-video-scene,
.recorded-evidence-image-scene {
position: relative;
width: 100%;
height: 100%;
@@ -55,12 +56,23 @@
background: var(--nodedc-canvas);
}
.recorded-evidence-image-scene > img {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
object-fit: contain;
user-select: none;
}
.recorded-evidence-video-scene > .recorded-media-player {
position: absolute;
inset: 0;
}
.recorded-evidence-video-scene > canvas {
.recorded-evidence-video-scene > canvas,
.recorded-evidence-image-scene > canvas {
position: absolute;
z-index: 2;
inset: 0;
@@ -70,6 +82,18 @@
pointer-events: none;
}
.recorded-evidence-image-scene > .l3-visual-audit__state {
position: absolute;
z-index: 3;
inset: 0;
}
.m4-replay-threat-evidence-viewer .laboratory-metric-evidence-scene__toolbar {
top: 5.4rem;
max-width: calc(100% - 1.2rem);
flex-wrap: wrap;
}
.e46e-ready-stack-video {
width: 100%;
height: 100%;
@@ -26,7 +26,7 @@ export function M4ReplayThreatResultView({
<LaboratorySummary
title="M4.6 · dual-evidence threat replay"
description="Camera и LiDAR дают независимые доказательства, после чего один source-neutral слой оценивает пересечение виртуального коридора, ближайшее сближение и TTC. Ни один сенсор не назначен first."
status="4489/4489 · replay-simulated · accepted"
status="Replay contract passed · CV quality gate open"
statusTone="warning"
facts={[
{
@@ -53,7 +53,7 @@ export function M4ReplayThreatResultView({
brief={{
question: "Может ли единый слой обнаруживать потенциальное препятствие по двум независимым источникам, не теряя LiDAR-only объекты и не объявляя camera-only наблюдение безопасным?",
approach: "Все 4489 кадров RAVNOVES00 повторно пропущены через неизменяемые detector, metric geometry и temporal ledgers. Текущий lio_pcl increment хранится отдельно от bounded rolling map: отсутствие повторной публикации точки не считается свободным пространством. Виртуальный base_footprint привязан к gravity-оси SLAM map и направлению сглаженной траектории, проверенному camera extrinsic.",
principalResult: `${metrics.evidence.currentMetric.toLocaleString("ru-RU")} current metric, ${metrics.evidence.rollingMapRetained.toLocaleString("ru-RU")} rolling-map и ${metrics.evidence.cameraOnly.toLocaleString("ru-RU")} camera-only публикаций учтены. Кадр 1880 удерживает обе видимые бетонные полусферы; критические fixtures: ${metrics.fixtures.passed}/${metrics.fixtures.total}, ложных safe: ${metrics.fixtures.criticalFalseNotThreat}.`,
principalResult: `${metrics.evidence.currentMetric.toLocaleString("ru-RU")} current metric, ${metrics.evidence.rollingMapRetained.toLocaleString("ru-RU")} rolling-map и ${metrics.evidence.cameraOnly.toLocaleString("ru-RU")} camera-only публикаций учтены. Кадры 1880 и 2584 закрепляют компактные бетонные полусферы как автоматические метрические регрессии; критические fixtures: ${metrics.fixtures.passed}/${metrics.fixtures.total}, ложных safe: ${metrics.fixtures.criticalFalseNotThreat}.`,
limitation: "Корпус и коридор пока виртуальные, replay не является live-проходом или физическим collision test. На машине виртуальная привязка должна замениться измеренным rigid T_body_from_sensor; independent object truth остаётся следующим gate.",
}}
method={{
@@ -105,8 +105,8 @@ export function M4ReplayThreatResultView({
)}
result={(
<LaboratoryResultSummary
title="Dual-evidence слой готов к следующей CV-итерации на recorded replay"
status="Replay gate accepted · physical authority withheld"
title="Replay-инфраструктура принята; object recall ещё проверяется"
status="Жёлтый: pipeline целостен, независимый object-truth gate не пройден"
statusTone="warning"
metrics={[
{
@@ -131,7 +131,7 @@ export function M4ReplayThreatResultView({
},
]}
conclusion={{
proved: `На неизменяемом RAVNOVES00 каждый current, rolling-map, stale/held и camera-only объект получил ровно одну консервативную оценку. CURRENT INCREMENT и ROLLING MAP независимо включаются в viewer; кадр 1880 закреплён как регрессия двух бетонных полусфер. ${metrics.bodyFrame.qualified}/${metrics.bodyFrame.available} body frames квалифицированы без переноса handheld roll/pitch на SLAM-мир.`,
proved: `На неизменяемом RAVNOVES00 каждый current, rolling-map, stale/held и camera-only объект получил ровно одну консервативную оценку. CURRENT INCREMENT и ROLLING MAP независимо включаются в viewer; кадры 1880 и 2584 закреплены как регрессии компактных бетонных полусфер. ${metrics.bodyFrame.qualified}/${metrics.bodyFrame.available} body frames квалифицированы без переноса handheld roll/pitch на SLAM-мир.`,
notProved: "Не доказаны live realtime, измеренный T_body_from_sensor и геометрия физического корпуса, независимая object-level правильность, навигационная или safety-пригодность и выдача команд.",
decision: "Сохранить dual-evidence provider как канонический replay seam и переходить к независимому object-centric gate; физическую геометрию и live/actuation authority не смешивать с дальнейшей CV-разработкой.",
}}
@@ -7,6 +7,7 @@ import {
type LaboratoryMetricSceneMode,
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import { RecordedEvidenceImageScene } from "../../components/laboratory/RecordedEvidenceImageScene";
import {
RecordedEvidenceVideoScene,
type RecordedEvidenceBox,
@@ -108,7 +109,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
}, [ordinal, resultId]);
useEffect(() => {
if ((mode !== "video" && mode !== "camera") || (videoOverlay && videoSource)) return;
if (mode !== "video" || (videoOverlay && videoSource)) return;
const controller = new AbortController();
setVideoLoading(true);
setVideoError(null);
@@ -157,14 +158,6 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
return () => controller.abort();
}, [mode, resultId, videoOverlay, videoSource]);
useEffect(() => {
if (mode !== "camera" || !frame) return;
setVideoPlayback({
currentSeconds: frame.sourceTimeNs / 1_000_000_000,
playing: false,
});
}, [frame, mode]);
const activeVideoFrame = useMemo(
() => videoOverlay
? selectM4ThreatVideoFrame(videoOverlay.frames, videoPlayback.currentSeconds)
@@ -317,7 +310,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
) : undefined;
let content;
if (mode === "video" || mode === "camera") {
if (mode === "video") {
content = videoLoading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
@@ -335,14 +328,30 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
imageWidth={videoOverlay.imageWidth}
imageHeight={videoOverlay.imageHeight}
boxes={activeBoxes}
ariaLabel={
mode === "camera"
? `M4.6 exact camera sample ${ordinal}: ${activeBoxes.length} proposals`
: `M4.6 full video frame ${activeVideoFrame?.frameIndex ?? 0}: ${activeBoxes.length} proposals`
}
ariaLabel={`M4.6 full video frame ${activeVideoFrame?.frameIndex ?? 0}: ${activeBoxes.length} proposals`}
onPlaybackChange={setVideoPlayback}
/>
);
} else if (mode === "camera") {
content = sampleLoading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем точный CAMERA-кадр M4.6</span>
</div>
) : sampleError || !frame ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{sampleError ?? "CAMERA-кадр M4.6 недоступен."}</span>
</div>
) : (
<RecordedEvidenceImageScene
src={frame.cameraUrl}
imageWidth={800}
imageHeight={600}
boxes={activeBoxes}
ariaLabel={`M4.6 exact camera sample ${ordinal}: ${activeBoxes.length} proposals`}
/>
);
} else {
content = sampleLoading ? (
<div className="l3-visual-audit__state" role="status">
@@ -376,6 +385,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
<div className="l3-visual-audit m4-replay-threat-visual">
<LaboratoryEvidenceViewer
label="M4.6 dual-evidence replay: video, camera and metric 3D"
className="m4-replay-threat-evidence-viewer"
mode={mode}
modes={[
{ value: "video", label: "VIDEO" },
@@ -63,7 +63,7 @@ test("M4.6 decodes accepted dual-evidence result without physical authority", as
},
metrics: {
decisions: { threat: 8010, "not-threat": 6610, unknown: 60832 },
evidence: { "camera-only": 10158, "current-metric": 27299, "stale-or-held": 37995 },
evidence: { "camera-only": 10158, "current-metric": 28081, "rolling-map-retained": 70989, "stale-or-held": 38025 },
fixtures: { critical: 4, critical_false_not_threat: 0, passed: 9, total: 9 },
runtime: {
frames_per_second: 116.4,
@@ -99,7 +99,7 @@ test("M4.6 decodes accepted dual-evidence result without physical authority", as
}), { status: 200 }),
});
assert.equal(result.resultId, resultId);
assert.equal(result.metrics.evidence.currentMetric, 27299);
assert.equal(result.metrics.evidence.currentMetric, 28081);
assert.equal(result.metrics.fixtures.criticalFalseNotThreat, 0);
assert.equal(result.metrics.bodyFrame.qualified, 3861);
assert.equal(result.metrics.bodyFrame.cameraForwardAlignmentDeg.p95, 8.439);
@@ -112,6 +112,7 @@ test("M4.6 binds exact CAMERA and metric 3D evidence to one replay frame", async
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.perception-threat-visual-frame/v1",
result_id: resultId,
camera_url: `/api/v1/laboratory/m4-threat/results/${resultId}/visuals/1/camera`,
ordinal: 1,
sequence: 20,
frame_id: "frame-000020",
@@ -146,6 +147,7 @@ test("M4.6 binds exact CAMERA and metric 3D evidence to one replay frame", async
}), { status: 200 }),
});
assert.equal(frame.sequence, 20);
assert.match(frame.cameraUrl, /\/visuals\/1\/camera$/);
assert.equal(frame.metricObstacles[0].assessment.decision, "threat");
assert.equal(frame.cameraProposals[0].threatDecision, "unknown");
assert.equal(frame.pointCloudSampleCount, 2);
@@ -156,6 +158,7 @@ test("M4.6 v2 keeps CURRENT INCREMENT separate from ROLLING MAP", async () => {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.perception-threat-visual-frame/v2",
result_id: resultId,
camera_url: `/api/v1/laboratory/m4-threat/results/${resultId}/visuals/14/camera`,
ordinal: 14,
sequence: 1880,
frame_id: "frame-001880",
@@ -231,18 +234,21 @@ test("M4.6 full video preserves camera-only unknown and nearest-frame selection"
assert.equal(selectM4ThreatVideoFrame(overlay.frames, 35.50).frameIndex, 1);
});
test("M4.6 viewer reuses shared video and metric evidence renderers", async () => {
const [visual, videoScene, metricScene] = await Promise.all([
test("M4.6 viewer reuses shared camera, video and metric evidence renderers", async () => {
const [visual, imageScene, videoScene, metricScene] = await Promise.all([
readFile(new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/RecordedEvidenceImageScene.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/RecordedEvidenceVideoScene.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/LaboratoryMetricEvidenceScene.tsx", import.meta.url), "utf8"),
]);
assert.match(visual, /<RecordedEvidenceVideoScene/);
assert.match(visual, /<RecordedEvidenceImageScene/);
assert.match(visual, /<LaboratoryMetricEvidenceScene/);
assert.match(visual, /label: "VIDEO"/);
assert.match(visual, /label: "CAMERA"/);
assert.match(visual, /label: "3D"/);
assert.match(videoScene, /<RecordedFmp4Player/);
assert.match(imageScene, /<RecordedEvidenceBoxOverlay/);
assert.match(metricScene, /OrbitControls/);
assert.match(metricScene, /CURRENT INCREMENT/);
assert.match(metricScene, /ROLLING MAP/);
+1 -1
View File
@@ -193,7 +193,7 @@
},
{
"catalog_id": "m4-replay-threat",
"evidence_id": "m4-threat-replay-ef521b23eee704dee99856b6e93d5047a9b358e21ffda3ea9cacc2ef768164d9",
"evidence_id": "m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324",
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
@@ -34,7 +34,7 @@
"geometry_local_radius_m": 10.0,
"geometry_voxel_size_m": 0.45,
"geometry_minimum_cluster_points": 4,
"geometry_minimum_cluster_voxels": 2,
"geometry_minimum_cluster_voxels": 1,
"maximum_geometry_clusters_per_frame": 64
},
"policy": {
+4 -4
View File
@@ -5,10 +5,10 @@
"source": {
"source_id": "RAVNOVES00",
"session_id": "20260720T065719Z_viewer_live",
"temporal_result_id": "m4-temporal-replay-b8611526dfcd2b9be9049560d751bbd23a9ad54b7dda8e9dc48a17374d46266e",
"temporal_frames_sha256": "08a03669d3487ecf7bd62b047ae5b8d1ff586a27e7435e5f235907a4d709dd25",
"geometry_result_id": "m4-geometry-replay-8daf3109e3cf30b960b4b376032ff3b5ec58ca42a1e5899b841cf29fbcf14ad8",
"geometry_frames_sha256": "b4db5d0ebaba4d6268a1006707dc313c229f3dbdfd73b1d863cad5d853be8ac4",
"temporal_result_id": "m4-temporal-replay-81e13d5654ac8d1219f6937dd425f7dbcdbacd5748d23fe653297134f0de6c22",
"temporal_frames_sha256": "e83b80ea06b3462c2d04c5a1b74289a0ec401596c7150ae90f5a1b748b639c3a",
"geometry_result_id": "m4-geometry-replay-b7d72e9e411fd576243d10ab39717e90daa10703b67c6e32ab284f6bd78d344a",
"geometry_frames_sha256": "d4d4c0a98e09c0f26251284747108651cf4b21e9c9733e3a14963999b0300772",
"detector_result_id": "m4-detector-replay-11f83f2e0b81758ac2a5a5fc54e9d293b501678df5f6ef97b5c6069ba08605c5",
"detector_frames_sha256": "9bf5ae17938cd57c112278781b38d54a7187cf2dabc7bb0332acdb1efad721f5",
"source_pack_id": "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b",
+2 -2
View File
@@ -6,8 +6,8 @@
"source": {
"source_id": "RAVNOVES00",
"session_id": "20260720T065719Z_viewer_live",
"geometry_result_id": "m4-geometry-replay-8daf3109e3cf30b960b4b376032ff3b5ec58ca42a1e5899b841cf29fbcf14ad8",
"geometry_frames_sha256": "b4db5d0ebaba4d6268a1006707dc313c229f3dbdfd73b1d863cad5d853be8ac4"
"geometry_result_id": "m4-geometry-replay-b7d72e9e411fd576243d10ab39717e90daa10703b67c6e32ab284f6bd78d344a",
"geometry_frames_sha256": "d4d4c0a98e09c0f26251284747108651cf4b21e9c9733e3a14963999b0300772"
},
"temporal": {
"coordinate_frame": "map",
@@ -1007,44 +1007,60 @@ Missing republication never clears a cell. Retained occupancy is bounded to
`3.0 s`, `12.0 m`, `65,536` cells and zero allowed capacity drops; it can block
an intersecting corridor but cannot claim current motion or safe clearance.
The accepted resealed M4.5R result is
`m4-temporal-replay-b8611526dfcd2b9be9049560d751bbd23a9ad54b7dda8e9dc48a17374d46266e`:
Review of exact CAMERA frame `2584` then exposed a separate geometry defect:
the visible near concrete hemisphere produced six occupied points in one
`0.45 m` voxel at frame `2572`, but `geometry_minimum_cluster_voxels = 2`
discarded it before both temporal products. This was a real false negative.
The minimum remains four source points, while a compact component may occupy
one voxel. The former M4.5R/M4.6 results are withdrawn as current evidence.
The accepted corrected M4.4 geometry result is
`m4-geometry-replay-b7d72e9e411fd576243d10ab39717e90daa10703b67c6e32ab284f6bd78d344a`.
It publishes `22,740` geometry-only observations and `2,173,778` source-point
rows. Its frame ledger SHA-256 is
`d4d4c0a98e09c0f26251284747108651cf4b21e9c9733e3a14963999b0300772`.
The accepted corrected M4.5R result is
`m4-temporal-replay-81e13d5654ac8d1219f6937dd425f7dbcdbacd5748d23fe653297134f0de6c22`:
- frame ledger SHA-256
`08a03669d3487ecf7bd62b047ae5b8d1ff586a27e7435e5f235907a4d709dd25`;
`e83b80ea06b3462c2d04c5a1b74289a0ec401596c7150ae90f5a1b748b639c3a`;
- manifest/report SHA-256
`5df4c31b1d3a9f834895af999c55829b40afeca89d50ffe04f7e23bd85e2bd8c` /
`2e708fb30f35b34ed99cbe107055772bfb61553a3321c0ff2fee725d3a07bdc9`;
`540640dacb10f0215133f3a2c833b0502856e132350fa262751894a7f62cdb84` /
`461bedc3ae03cda90630c92e6b4c0c637ae4b6514a90d956abfb1be592230ed0`;
- `4,489 / 4,489` frames, zero failed;
- `848,868` current increment cells;
- `69,855` retained component and `1,776,145` retained cell publications;
- `849,650` current increment cells;
- `70,989` retained component and `1,779,135` retained cell publications;
- peak `907` active cells and `35` retained components;
- `29,620` time evictions, `6,512` radius evictions, zero capacity drops and
- `29,720` time evictions, `6,513` radius evictions, zero capacity drops and
maximum retained age exactly `3.0 s`.
The accepted resealed M4.6 result is
`m4-threat-replay-ef521b23eee704dee99856b6e93d5047a9b358e21ffda3ea9cacc2ef768164d9`:
The accepted corrected M4.6 result is
`m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324`:
- manifest/report SHA-256
`199ed52211f3d50ecce0e3f0bc1efc094b665e9a0c5e0426e01640a6b9a5f0bb` /
`e32402fd6d52b1c22e3a5ee5b794bbe8facaf132e02cdaa902175395fa6b9db8`;
`87c58e8123deede1de2908af98752fbe03b94785858df2492e817c77cf8b02c5` /
`8def86903c8d37e3112cf6452d6a22a5a835ccc542e9e01714868dd3a9831864`;
- frame/visual SHA-256
`57219acd7dfe1cf04b12d4a415d1819e0a4947bc3173333bb56eea8fd2bd47b9` /
`bda54e144a1b4c878ba645a8dafedcdc0332e16a1489b62b7100670e8eae6f25`;
- evidence: `27,299` current metric, `69,855` rolling-map, `37,995`
`b57be1839f5915e3b80b54355b694e0bd8c9ac318d0cbe6de2bff713082cfa4e` /
`14b9679b1df4d50aaceeb3e018e46398a92b47727aa83339179395db4dfcd005`;
- evidence: `28,081` current metric, `70,989` rolling-map, `38,025`
stale/held and `10,158` camera-only publications;
- decisions: `6,626 threat`, `10,700 not-threat`, `127,981 unknown`;
- decisions: `7,606 threat`, `10,769 not-threat`, `128,878 unknown`;
- deterministic fixtures `10 / 10`, five critical, zero critical false-safe;
- body-frame qualification unchanged at `3,861 / 3,928`, confirming the
representation fix did not regress the earlier route/gravity correction;
- frame `1880` now binds two distinct engineering anchors to retained occupied
components; the near anchor asserts threat. The anchors are not independent
truth.
compact-component fix did not regress the earlier route/gravity correction;
- frames `1880` and `2584` bind reviewed body-frame windows to produced occupied
cells/components. At `2584` the compact near hemisphere survives as retained
threat and the far hemisphere is accounted for inside a larger current
component. These windows are regression evidence, not independent truth.
The common viewer keeps VIDEO/CAMERA/3D/PLAN synchronized and independently
toggles `CURRENT INCREMENT` and `ROLLING MAP`. Regression frames are now `138`,
`274` and `1880`. M4.7 may proceed only from the resealed identities above;
physical-live, collision, navigation and actuation authority remain false.
toggles `CURRENT INCREMENT` and `ROLLING MAP`. CAMERA requests one exact JPEG
decoded from the bounded fMP4 GOP instead of preparing the complete video.
Regression frames are now `138`, `274`, `1880` and `2584`. M4.7 may proceed only
from the corrected identities above; physical-live, collision, navigation and
actuation authority remain false.
## Implementation order
@@ -52,24 +52,24 @@ is not a map-clearing policy.
## Evidence and acceptance
The accepted M4.5R result is
`m4-temporal-replay-b8611526dfcd2b9be9049560d751bbd23a9ad54b7dda8e9dc48a17374d46266e`.
The current accepted M4.5R result is
`m4-temporal-replay-81e13d5654ac8d1219f6937dd425f7dbcdbacd5748d23fe653297134f0de6c22`.
Its frame ledger SHA-256 is
`08a03669d3487ecf7bd62b047ae5b8d1ff586a27e7435e5f235907a4d709dd25`.
`e83b80ea06b3462c2d04c5a1b74289a0ec401596c7150ae90f5a1b748b639c3a`.
Across all `4,489` source frames it records:
- `848,868` current increment occupied cells;
- `69,855` retained component publications and `1,776,145` retained cell
- `849,650` current increment occupied cells;
- `70,989` retained component publications and `1,779,135` retained cell
publications;
- peak `907` active cells and `35` retained components;
- `29,620` time evictions, `6,512` radius evictions and zero capacity drops;
- `29,720` time evictions, `6,513` radius evictions and zero capacity drops;
- maximum retained age exactly `3.0 s` and `587` active cells at replay end.
The accepted M4.6 result is
`m4-threat-replay-ef521b23eee704dee99856b6e93d5047a9b358e21ffda3ea9cacc2ef768164d9`.
All `4,489` frames completed with `27,299` current metric, `69,855` rolling-map,
`37,995` stale/held and `10,158` camera-only evidence publications assessed
exactly once. Decisions are `6,626 threat`, `10,700 not-threat` and `127,981
The current accepted M4.6 result is
`m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324`.
All `4,489` frames completed with `28,081` current metric, `70,989` rolling-map,
`38,025` stale/held and `10,158` camera-only evidence publications assessed
exactly once. Decisions are `7,606 threat`, `10,769 not-threat` and `128,878
unknown`. All `10/10` deterministic fixtures pass with zero critical false-safe
outcomes.
@@ -78,6 +78,14 @@ separate body-frame regions to two retained components. The near region must
assert threat; both regions are present in the accepted result. These anchors
encode a reproducible owner-reviewed visual case, not independent object truth.
Frame `2584` is a second mandatory visual regression added after operator
review found that a compact hemisphere had six valid points in one voxel and
was discarded by a two-voxel geometry filter. The corrected geometry contract
retains the four-point minimum but permits one-voxel compact components. At
`2584` the near hemisphere is a retained threat and occupied cells for the far
hemisphere are present inside a larger current component. This is still an
engineering regression window, not independent truth.
The common LAB viewer exposes `CURRENT INCREMENT` and `ROLLING MAP` as separate
layers over the same selected frame. The current point count is not relabelled
as a complete scan, and retained occupied cells remain independently hideable.
@@ -92,7 +100,7 @@ as a complete scan, and retained occupied cells remain independently hideable.
- No ray-level free-space clearing is available in this recording. A future raw
scan/ray provider must add explicit observation and clearing semantics rather
than weakening this contract.
- The two frame-1880 anchors are regression evidence, not independent truth,
- The frame-1880 and frame-2584 anchors are regression evidence, not independent truth,
detector accuracy or physical collision acceptance.
- Live K1, measured `T_body_from_sensor`, physical body geometry, navigation,
commands and actuation remain outside M4.5R/M4.6 authority.
+50 -95
View File
@@ -46,18 +46,15 @@ TEMPORAL_REPLAY_REPORT_NAME: Final = "report.json"
TEMPORAL_REPLAY_MANIFEST_NAME: Final = "manifest.json"
E34_RESULT_ID: Final = (
"e34-temporal-occupied-"
"8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73"
"e34-temporal-occupied-8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73"
)
E34_MANIFEST_SHA256: Final = "88285f44d0316913881cc0948a4dfb300d56b460c51be36d51b4dd717cbf4170"
E51_RESULT_ID: Final = (
"e51-motion-semantic-"
"1abb7eb9940608fc5af95a1f318cadfbc42ac2412a8662b6622e000e03da1555"
"e51-motion-semantic-1abb7eb9940608fc5af95a1f318cadfbc42ac2412a8662b6622e000e03da1555"
)
E51_MANIFEST_SHA256: Final = "a38ecad59765d1439ac432a34c053362195ec4b493d8e8f1d4a1731f99836437"
E46B_RESULT_ID: Final = (
"e46b-temporal-motion-"
"78d038912273364e36f996401873a8ee178a94641350021c2cd35bcb301ba36d"
"e46b-temporal-motion-78d038912273364e36f996401873a8ee178a94641350021c2cd35bcb301ba36d"
)
E46B_MANIFEST_SHA256: Final = "b0d0b6bdfa23f0475106c1870771ee90dd6de85719396a665dd7311f93da3d59"
E46B_CASES_SHA256: Final = "95b58300f10dc796f7576bffbcc670ac76b74d6f25b13c0768f57331cba49074"
@@ -100,9 +97,7 @@ def build_temporal_replay(
store = RecordedGeometryStore.from_repository(repository)
temporal = BoundedSpatialTemporalProvider(point_resolver=store, profile=profile)
motion = ClassIndependentMotionEstimator(profile=profile)
rolling_profile = load_rolling_map_profile(
repository / DEFAULT_ROLLING_MAP_PROFILE_PATH
)
rolling_profile = load_rolling_map_profile(repository / DEFAULT_ROLLING_MAP_PROFILE_PATH)
rolling = RollingLocalObstacleMapProvider(
pose_resolver=store,
profile=rolling_profile,
@@ -147,9 +142,7 @@ def build_temporal_replay(
obstacles = motion.estimate(packet, temporal_obstacles)
rolling_retained = rolling.update(packet, obstacles)
latencies_ms.append((time.perf_counter_ns() - frame_started_ns) / 1_000_000)
current = tuple(
item for item in obstacles if item.state is TemporalState.CURRENT
)
current = tuple(item for item in obstacles if item.state is TemporalState.CURRENT)
held = tuple(item for item in obstacles if item.state is TemporalState.HELD)
expired = tuple(item for item in obstacles if item.state is TemporalState.EXPIRED)
motion_counts = _motion_counts(current)
@@ -158,13 +151,10 @@ def build_temporal_replay(
"sequence": frame_count,
"frame_id": packet.envelope.frame_id,
"source_time_ns": packet.envelope.timestamps.source_ns,
"source_available": (
packet.envelope.registered_point_increment.available
),
"source_available": (packet.envelope.registered_point_increment.available),
"input_observation_count": len(observations),
"current_occupied_input_count": sum(
item.occupied_support
and item.currentness.value == "current"
item.occupied_support and item.currentness.value == "current"
for item in observations
),
"nonmetric_uncertainty_input_count": sum(
@@ -173,13 +163,10 @@ def build_temporal_replay(
"current": [item.to_dict() for item in current],
"held": [item.to_dict() for item in held],
"expired": [item.to_dict() for item in expired],
"rolling_retained": [
item.to_dict() for item in rolling_retained
],
"rolling_retained": [item.to_dict() for item in rolling_retained],
"motion_counts": motion_counts,
"map_frame_jump_candidate": any(
item.association_basis == "map-frame-discontinuity"
for item in current
item.association_basis == "map-frame-discontinuity" for item in current
),
"policy": _frame_policy(),
"authority": _false_authority(),
@@ -216,9 +203,7 @@ def build_temporal_replay(
identity = {
"schema_version": TEMPORAL_REPLAY_SCHEMA_V2,
"geometry_result_id": geometry.result_id,
"geometry_manifest_sha256": _file_sha256(
geometry.result_root / "manifest.json"
),
"geometry_manifest_sha256": _file_sha256(geometry.result_root / "manifest.json"),
"geometry_frames_sha256": profile.geometry_frames_sha256,
"profile_id": profile.profile_id,
"profile_sha256": profile.profile_sha256,
@@ -398,18 +383,11 @@ def read_temporal_replay_result(root: Path) -> TemporalReplayResult:
if is_v2
else _requirements(metrics, ttl_ns / 1_000_000_000)
)
if (
ttl_ns != 750_000_000
or requirements != expected_requirements
):
if ttl_ns != 750_000_000 or requirements != expected_requirements:
raise TemporalReplayError("temporal replay acceptance was not derived from metrics")
if (
report.get("schema_version")
!= (
TEMPORAL_REPLAY_REPORT_SCHEMA_V2
if is_v2
else TEMPORAL_REPLAY_REPORT_SCHEMA
)
!= (TEMPORAL_REPLAY_REPORT_SCHEMA_V2 if is_v2 else TEMPORAL_REPLAY_REPORT_SCHEMA)
or report.get("result_id") != resolved.name
or report.get("identity_sha256") != identity_sha256
or report.get("metrics") != metrics
@@ -490,9 +468,7 @@ def _requirements_v2(
requirements.update(
{
"registered_increment_is_not_treated_as_complete_scan": True,
"rolling_map_processed_every_source_frame": (
rolling.get("input_frames") == 4489
),
"rolling_map_processed_every_source_frame": (rolling.get("input_frames") == 4489),
"rolling_map_materialized_retained_occupancy": (
_integer(
rolling.get("retained_component_publications"),
@@ -506,8 +482,7 @@ def _requirements_v2(
"maximum rolling retained age",
)
<= round(rolling_retention_seconds * 1_000_000_000)
and rolling.get("retention_ns")
== round(rolling_retention_seconds * 1_000_000_000)
and rolling.get("retention_ns") == round(rolling_retention_seconds * 1_000_000_000)
and rolling.get("maximum_cells") == rolling_maximum_cells
and _integer(
rolling.get("capacity_evicted_cells"),
@@ -571,22 +546,24 @@ def _requirements(metrics: dict[str, object], ttl_seconds: float) -> dict[str, b
)
return {
"full_frame_accounting": frames == {"total": 4489, "failed": 0},
"geometry_observation_accounting": metrics.get("input_observations") == 37457,
"geometry_observation_accounting": (
metrics.get("input_observations") == temporal_input
and temporal.get("input_frames") == frames.get("total")
and temporal_input > 0
),
"metric_and_nonmetric_partition_closed": (
temporal.get("current_occupied_observations") == 27299
and temporal.get("nonmetric_uncertainty_observations") == 10158
temporal_input == current_input + uncertainty_input
and current_input > 0
and uncertainty_input > 0
),
"camera_uncertainty_never_created_occupied_state": (
temporal_input == current_input + uncertainty_input
),
"detector_identity_changes_survive_spatial_reassociation": identity_changes > 0,
"temporal_state_is_bounded": (
peak_components <= 256
and peak_cells <= 4096
and maximum_history <= 8
peak_components <= 256 and peak_cells <= 4096 and maximum_history <= 8
),
"held_and_expired_states_materialized": held_publications > 0
and expired_publications > 0,
"held_and_expired_states_materialized": held_publications > 0 and expired_publications > 0,
"no_occupied_cells_survive_ttl": (
retention.get("past_ttl_occupied_publications") == 0
and retention.get("ghost_occupancy_past_ttl_count") == 0
@@ -600,8 +577,7 @@ def _requirements(metrics: dict[str, object], ttl_seconds: float) -> dict[str, b
value > 0 for value in (moving, stationary, unknown)
),
"motion_accounting_closed": (
motion_input
== current_publications + held_publications + expired_publications
motion_input == current_publications + held_publications + expired_publications
),
"bounded_labeled_engineering_checks_recorded": (
clips
@@ -651,9 +627,8 @@ def _validate_frame_ledger(
frame.get("nonmetric_uncertainty_input_count"),
"frame nonmetric uncertainty inputs",
)
if (
frame_current_inputs + frame_uncertainty_inputs
!= frame.get("input_observation_count")
if frame_current_inputs + frame_uncertainty_inputs != frame.get(
"input_observation_count"
):
raise TemporalReplayError("temporal frame input partition is open")
current_inputs += frame_current_inputs
@@ -673,11 +648,7 @@ def _validate_frame_ledger(
groups[state] = items
for item in items:
motion_counts[item.motion.value] += 1
component_ids = [
item.component_id
for group in groups.values()
for item in group
]
component_ids = [item.component_id for group in groups.values() for item in group]
if len(component_ids) != len(set(component_ids)):
raise TemporalReplayError("temporal frame duplicated a component")
if is_v2:
@@ -689,13 +660,9 @@ def _validate_frame_ledger(
)
)
if any(item.state is not TemporalState.RETAINED for item in rolling):
raise TemporalReplayError(
"rolling map published a non-retained component"
)
raise TemporalReplayError("rolling map published a non-retained component")
if set(component_ids) & {item.component_id for item in rolling}:
raise TemporalReplayError(
"rolling and temporal component identities overlap"
)
raise TemporalReplayError("rolling and temporal component identities overlap")
rolling_publications += len(rolling)
current_publications += len(groups[TemporalState.CURRENT])
held_publications += len(groups[TemporalState.HELD])
@@ -706,11 +673,7 @@ def _validate_frame_ledger(
frame_metrics = _object(metrics.get("frames"), "temporal frames")
temporal = _object(metrics.get("temporal"), "temporal metrics")
motion = _object(metrics.get("motion"), "motion metrics")
rolling_metrics = (
_object(metrics.get("rolling_map"), "rolling map metrics")
if is_v2
else None
)
rolling_metrics = _object(metrics.get("rolling_map"), "rolling map metrics") if is_v2 else None
if (
frames != frame_metrics.get("total")
or observations != metrics.get("input_observations")
@@ -724,8 +687,7 @@ def _validate_frame_ledger(
or motion_counts[MotionState.UNKNOWN.value] != motion.get("unknown")
or (
rolling_metrics is not None
and rolling_publications
!= rolling_metrics.get("retained_component_publications")
and rolling_publications != rolling_metrics.get("retained_component_publications")
)
):
raise TemporalReplayError("temporal frame ledger and metrics disagree")
@@ -801,10 +763,7 @@ def _clip_check(
def _motion_counts(obstacles: tuple[TemporalObstacle, ...]) -> dict[str, int]:
return {
state.value: sum(item.motion is state for item in obstacles)
for state in MotionState
}
return {state.value: sum(item.motion is state for item in obstacles) for state in MotionState}
def _frame_policy() -> dict[str, object]:
@@ -836,9 +795,7 @@ def _read_geometry_frame(line: bytes, sequence: int) -> dict[str, object]:
try:
frame = _object(json.loads(line), "geometry replay frame")
except json.JSONDecodeError as exc:
raise TemporalReplayError(
f"geometry replay frame {sequence + 1} is invalid JSON"
) from exc
raise TemporalReplayError(f"geometry replay frame {sequence + 1} is invalid JSON") from exc
if frame.get("sequence") != sequence:
raise TemporalReplayError("geometry replay frame sequence is incomplete")
return frame
@@ -853,25 +810,23 @@ def _read_frame(
try:
frame = _object(json.loads(line), "temporal replay frame")
except json.JSONDecodeError as exc:
raise TemporalReplayError(
f"temporal replay frame {sequence + 1} is invalid JSON"
) from exc
raise TemporalReplayError(f"temporal replay frame {sequence + 1} is invalid JSON") from exc
expected_keys = {
"schema_version",
"sequence",
"frame_id",
"source_time_ns",
"source_available",
"input_observation_count",
"current_occupied_input_count",
"nonmetric_uncertainty_input_count",
"current",
"held",
"expired",
"motion_counts",
"map_frame_jump_candidate",
"policy",
"authority",
"schema_version",
"sequence",
"frame_id",
"source_time_ns",
"source_available",
"input_observation_count",
"current_occupied_input_count",
"nonmetric_uncertainty_input_count",
"current",
"held",
"expired",
"motion_counts",
"map_frame_jump_candidate",
"policy",
"authority",
}
if is_v2:
expected_keys.add("rolling_retained")
+149 -45
View File
@@ -66,7 +66,7 @@ THREAT_REPLAY_REPORT_NAME: Final = "report.json"
THREAT_REPLAY_MANIFEST_NAME: Final = "manifest.json"
VISUAL_FRAME_COUNT: Final = 32
VISUAL_POINT_LIMIT: Final = 4_000
VISUAL_GEOMETRY_REGRESSION_SEQUENCES: Final = (138, 274, 1880)
VISUAL_GEOMETRY_REGRESSION_SEQUENCES: Final = (138, 274, 1880, 2584)
FRAME_1880_ENGINEERING_ANCHORS: Final = (
{
"anchor_id": "near-concrete-hemisphere",
@@ -83,6 +83,22 @@ FRAME_1880_ENGINEERING_ANCHORS: Final = (
"must_assert_threat": False,
},
)
FRAME_2584_ENGINEERING_ANCHORS: Final = (
{
"anchor_id": "near-compact-concrete-hemisphere",
"x_bounds_m": (1.5, 2.4),
"y_bounds_m": (-0.7, 0.2),
"z_bounds_m": (-0.1, 0.9),
"must_assert_threat": True,
},
{
"anchor_id": "far-concrete-hemisphere-occupancy",
"x_bounds_m": (2.8, 4.0),
"y_bounds_m": (1.2, 2.4),
"z_bounds_m": (-0.1, 1.0),
"must_assert_threat": False,
},
)
class ThreatReplayError(RuntimeError):
@@ -143,6 +159,7 @@ def build_threat_replay(
latencies_ms: list[float] = []
visual_count = 0
frame_1880_regression: dict[str, object] | None = None
frame_2584_regression: dict[str, object] | None = None
try:
temporal_frames_path = temporal.result_root / "frames.jsonl"
geometry_frames_path = geometry.result_root / "frames.jsonl"
@@ -188,13 +205,8 @@ def build_threat_replay(
"rolling retained obstacles",
)
)
if any(
item.state is not TemporalState.RETAINED
for item in rolling_retained
):
raise ThreatReplayError(
"temporal replay rolling map escaped retained state"
)
if any(item.state is not TemporalState.RETAINED for item in rolling_retained):
raise ThreatReplayError("temporal replay rolling map escaped retained state")
geometry_observations = _array(
geometry_frame.get("observations"), "geometry observations"
)
@@ -229,11 +241,8 @@ def build_threat_replay(
latencies_ms.append((time.perf_counter_ns() - frame_started_ns) / 1_000_000)
by_id = {item.component_id: item for item in assessments}
expected_ids = {
item.component_id
for item in (*current, *rolling_retained, *unknown)
} | {
item.proposal_id for item in camera_uncertainty
}
item.component_id for item in (*current, *rolling_retained, *unknown)
} | {item.proposal_id for item in camera_uncertainty}
if set(by_id) != expected_ids:
raise ThreatReplayError("threat assessment coverage is incomplete")
camera_rows = _camera_rows(
@@ -248,9 +257,13 @@ def build_threat_replay(
if frame_count == 1880:
frame_1880_regression = _frame_1880_regression(
metric_rows,
body_frame_resolver.body_frame_for_frame(
packet.envelope.frame_id
),
body_frame_resolver.body_frame_for_frame(packet.envelope.frame_id),
)
if frame_count == 2584:
frame_2584_regression = _frame_2584_regression(
metric_rows,
body_frame_resolver.body_frame_for_frame(packet.envelope.frame_id),
voxel_size_m=profile.corridor.occupied_voxel_size_m,
)
for item in assessments:
assessment_counts[item.decision.value] += 1
@@ -327,6 +340,7 @@ def build_threat_replay(
fixtures=fixtures,
body_frame=body_frame_resolver.qualification_summary(),
frame_1880_regression=frame_1880_regression,
frame_2584_regression=frame_2584_regression,
)
requirements = _requirements_v2(metrics, fixtures)
accepted = all(value is True for value in requirements.values())
@@ -488,9 +502,7 @@ def read_threat_replay_result(root: Path) -> ThreatReplayResult:
fixtures = _read_json(paths["threat-deterministic-fixtures"])
accepted = all(value is True for value in requirements.values())
expected_requirements = (
_requirements_v2(metrics, fixtures)
if is_v2
else _requirements_v1(metrics, fixtures)
_requirements_v2(metrics, fixtures) if is_v2 else _requirements_v1(metrics, fixtures)
)
if (
report.get("schema_version")
@@ -687,8 +699,7 @@ def _visual_frame(
"point_cloud_sample_count": int(sampled.shape[0]),
"point_cloud_layer": "current-increment",
"rolling_map_component_count": sum(
row.get("state") == TemporalState.RETAINED.value
for row in metric_rows
row.get("state") == TemporalState.RETAINED.value for row in metric_rows
),
"metric_obstacles": metric_visuals,
"camera_proposals": camera_rows,
@@ -778,22 +789,15 @@ def _frame_1880_regression(
"must_assert_threat": anchor["must_assert_threat"],
"matched": match is not None,
"component_id": component_id,
"centroid_body_xyz_m": (
None if match is None else list(match[1])
),
"centroid_body_xyz_m": (None if match is None else list(match[1])),
"decision": (
None
if anchor_assessment is None
else anchor_assessment.get("decision")
None if anchor_assessment is None else anchor_assessment.get("decision")
),
}
)
required_threats_passed = all(
item["matched"] is True
and (
item["must_assert_threat"] is False
or item["decision"] == ThreatDecision.THREAT.value
)
and (item["must_assert_threat"] is False or item["decision"] == ThreatDecision.THREAT.value)
for item in anchors
)
return {
@@ -808,6 +812,100 @@ def _frame_1880_regression(
}
def _frame_2584_regression(
metric_rows: list[dict[str, object]],
body_frame: ReplayBodyFrame | None,
*,
voxel_size_m: float,
) -> dict[str, object]:
"""Bind both visible hemispheres to produced occupancy without injecting it."""
if body_frame is None:
raise ThreatReplayError("frame 2584 has no qualified body frame")
candidates: list[
tuple[dict[str, object], tuple[float, float, float], tuple[float, float, float]]
] = []
for row in metric_rows:
assessment = _object(row.get("assessment"), "frame 2584 assessment")
centroid_map = _array(row.get("centroid_map_xyz_m"), "frame 2584 centroid")
if len(centroid_map) != 3:
raise ThreatReplayError("frame 2584 centroid is invalid")
centroid_body = body_frame.map_point_to_body(
tuple(_number_value(value, "frame 2584 centroid") for value in centroid_map)
)
for raw_cell in _array(row.get("cells"), "frame 2584 cells"):
cell = _object(raw_cell, "frame 2584 cell")
point_map = tuple(
(_signed_integer(cell.get(key), f"frame 2584 cell {key}") + 0.5) * voxel_size_m
for key in ("x", "y", "z")
)
candidates.append(
(
row,
centroid_body,
body_frame.map_point_to_body(point_map),
)
)
if not row.get("cells"):
raise ThreatReplayError("frame 2584 metric component has no occupied cells")
if assessment.get("component_id") != row.get("component_id"):
raise ThreatReplayError("frame 2584 assessment identity changed")
anchors: list[dict[str, object]] = []
matched_ids: set[str] = set()
for raw_anchor in FRAME_2584_ENGINEERING_ANCHORS:
anchor = _object(raw_anchor, "frame 2584 engineering anchor")
x_bounds = _bounds(anchor.get("x_bounds_m"), "frame 2584 x bounds")
y_bounds = _bounds(anchor.get("y_bounds_m"), "frame 2584 y bounds")
z_bounds = _bounds(anchor.get("z_bounds_m"), "frame 2584 z bounds")
match = next(
(
(row, centroid, cell)
for row, centroid, cell in candidates
if row.get("component_id") not in matched_ids
and x_bounds[0] <= cell[0] <= x_bounds[1]
and y_bounds[0] <= cell[1] <= y_bounds[1]
and z_bounds[0] <= cell[2] <= z_bounds[1]
),
None,
)
component_id = None if match is None else str(match[0]["component_id"])
if component_id is not None:
matched_ids.add(component_id)
assessment = (
None
if match is None
else _object(match[0].get("assessment"), "frame 2584 anchor assessment")
)
anchors.append(
{
"anchor_id": anchor["anchor_id"],
"bounds_body_xyz_m": [list(x_bounds), list(y_bounds), list(z_bounds)],
"must_assert_threat": anchor["must_assert_threat"],
"matched": match is not None,
"component_id": component_id,
"component_state": None if match is None else match[0].get("state"),
"centroid_body_xyz_m": None if match is None else list(match[1]),
"matched_cell_body_xyz_m": None if match is None else list(match[2]),
"decision": None if assessment is None else assessment.get("decision"),
}
)
required_threats_passed = all(
item["matched"] is True
and (item["must_assert_threat"] is False or item["decision"] == ThreatDecision.THREAT.value)
for item in anchors
)
return {
"sequence": 2584,
"engineering_anchors": anchors,
"matched_anchor_count": sum(item["matched"] is True for item in anchors),
"required_threats_passed": required_threats_passed,
"camera_visible_hemispheres_independent_truth": False,
"matching_basis": "produced-occupied-cell-inside-camera-reviewed-body-window",
"gate": "compact-and-merged-hemisphere-occupancy-regression",
}
class _FixtureBodyFrames:
def body_frame_for_frame(self, frame_id: str) -> ReplayBodyFrame:
return ReplayBodyFrame(
@@ -982,11 +1080,7 @@ def _fixture_obstacle(
component_id=component_id,
identity_scope="ephemeral",
state=state,
ttl_ns=(
3_000_000_000
if state is TemporalState.RETAINED
else 750_000_000
),
ttl_ns=(3_000_000_000 if state is TemporalState.RETAINED else 750_000_000),
last_hit_ns=last.evidence_time_ns,
age_ns=0 if state is TemporalState.CURRENT else 100_000_000,
association_basis="deterministic-fixture",
@@ -1096,6 +1190,7 @@ def _metrics(
fixtures: dict[str, object],
body_frame: dict[str, object],
frame_1880_regression: dict[str, object] | None,
frame_2584_regression: dict[str, object] | None,
) -> dict[str, object]:
values = np.asarray(latencies_ms, dtype=np.float64)
return {
@@ -1116,6 +1211,7 @@ def _metrics(
"qualified_base_footprint_available": True,
"geometry_regression_sequences": list(VISUAL_GEOMETRY_REGRESSION_SEQUENCES),
"frame_1880_regression": frame_1880_regression,
"frame_2584_regression": frame_2584_regression,
},
"fixtures": {
"passed": fixtures["passed_count"],
@@ -1156,8 +1252,7 @@ def _requirements_v1(
stale_cases = [
_object(item, "fixture")
for item in cases
if isinstance(item, dict)
and item.get("name") in {"occluded-held", "stale-expired"}
if isinstance(item, dict) and item.get("name") in {"occluded-held", "stale-expired"}
]
return {
"full_ravnoves00_replay_completed": (
@@ -1168,8 +1263,7 @@ def _requirements_v1(
),
"camera_only_is_unknown_never_safe": camera_case.get("actual") == "unknown",
"held_and_stale_are_unknown_never_safe": (
len(stale_cases) == 2
and all(item.get("actual") == "unknown" for item in stale_cases)
len(stale_cases) == 2 and all(item.get("actual") == "unknown" for item in stale_cases)
),
"geometry_only_evidence_is_assessed": (
_integer(
@@ -1206,13 +1300,10 @@ def _requirements_v1(
== _integer(body_frame.get("qualified"), "qualified body frames")
+ _integer(body_frame.get("rejected"), "rejected body frames")
and _integer(body_frame.get("qualified"), "qualified body frames")
>= math.ceil(
_integer(body_frame.get("available"), "available body frames") * 0.95
)
>= math.ceil(_integer(body_frame.get("available"), "available body frames") * 0.95)
and body_frame.get("origin") == "local-surface-vertical-projection"
and body_frame.get("up") == "vendor-slam-map-gravity-axis"
and body_frame.get("forward")
== "smoothed-slam-trajectory-validated-by-camera-axis"
and body_frame.get("forward") == "smoothed-slam-trajectory-validated-by-camera-axis"
and _number_value(
_object(
body_frame.get("camera_forward_alignment_deg"),
@@ -1308,6 +1399,19 @@ def _requirements_v2(
).get("required_threats_passed")
is True
),
"frame_2584_retains_compact_hemisphere_and_accounts_for_far_occupancy": (
isinstance(visual.get("frame_2584_regression"), dict)
and _object(
visual.get("frame_2584_regression"),
"frame 2584 regression",
).get("matched_anchor_count")
== len(FRAME_2584_ENGINEERING_ANCHORS)
and _object(
visual.get("frame_2584_regression"),
"frame 2584 regression",
).get("required_threats_passed")
is True
),
"body_frame_is_grounded_gravity_stable_and_route_aligned": (
body_frame.get("available")
== _integer(body_frame.get("qualified"), "qualified body frames")
+3
View File
@@ -5,6 +5,7 @@ from .active import (
ActiveSessionLeaseError,
recover_stale_active_session_marker,
)
from .camera_frame import RecordedCameraFrame, RecordedCameraFrameService
from .lab_cache import publish_lab_replay_cache
from .media import (
RECORDED_MEDIA_MANIFEST_SCHEMA,
@@ -65,6 +66,8 @@ __all__ = [
"publish_lab_replay_cache",
"RecordingMaterializationCancelled",
"RecordedMediaArtifact",
"RecordedCameraFrame",
"RecordedCameraFrameService",
"RECORDED_MEDIA_MANIFEST_SCHEMA",
"RecordedMediaFile",
"RecordedMediaInspector",
+314
View File
@@ -0,0 +1,314 @@
from __future__ import annotations
import hashlib
import os
import struct
import subprocess
import threading
from contextlib import suppress
from dataclasses import dataclass
from pathlib import Path
from .media import RecordedMediaEpoch, RecordedMediaInspector, RecordedMediaManifest
from .models import SessionIntegrityError
from .store import SessionStore
_MAX_KEYFRAME_DISTANCE = 120
_FFMPEG_TIMEOUT_SECONDS = 15.0
@dataclass(frozen=True, slots=True)
class RecordedCameraFrame:
payload: bytes
media_type: str
width: int
height: int
sha256: str
class RecordedCameraFrameService:
"""Decode one exact archived camera frame without preparing the full video.
Canonical camera archives contain one video sample per fMP4 fragment. The
service validates the sealed manifest, walks back only to the preceding IDR
fragment and gives that bounded GOP to ffmpeg. This keeps CAMERA review
independent from the 4489-row VIDEO overlay and full-player preparation.
"""
def __init__(
self,
store: SessionStore,
inspector: RecordedMediaInspector,
*,
ffmpeg_path: Path,
cache_root: Path,
) -> None:
resolved_ffmpeg = ffmpeg_path.expanduser().resolve(strict=True)
if not resolved_ffmpeg.is_file() or not os.access(resolved_ffmpeg, os.X_OK):
raise SessionIntegrityError("ffmpeg is unavailable for camera frame review")
self._store = store
self._inspector = inspector
self._ffmpeg_path = resolved_ffmpeg
self._cache_root = cache_root.expanduser().absolute()
self._cache_root.mkdir(parents=True, exist_ok=True)
if self._cache_root.is_symlink() or not self._cache_root.is_dir():
raise SessionIntegrityError("camera frame cache root is invalid")
self._lock = threading.Lock()
def extract(
self,
session_id: str,
frame_index: int,
*,
expected_source_name: str = "sensor.camera.right",
) -> RecordedCameraFrame:
if frame_index < 0:
raise SessionIntegrityError("camera frame index is invalid")
replay = self._store.prepare_replay(session_id, speed=1.0, loop=False)
matches = tuple(
artifact
for artifact in self._store.list_recorded_media(session_id)
if artifact.source_path.name == expected_source_name
)
if len(matches) != 1:
raise SessionIntegrityError("recorded camera source is unavailable")
artifact = matches[0]
manifest = self._inspector.inspect(artifact, replay)
epoch, sequence = _frame_location(manifest, frame_index)
cache_key = hashlib.sha256(
(
f"{manifest.generation_sha256}\0{artifact.artifact_id}\0"
f"{expected_source_name}\0{frame_index}\0jpeg-q2-v1"
).encode()
).hexdigest()
cache_path = self._cache_root / f"{cache_key}.jpg"
with self._lock:
cached = _read_cached_jpeg(cache_path)
if cached is not None:
return cached
frame = self._decode(manifest, epoch, sequence)
_publish_cached_jpeg(cache_path, frame.payload)
return frame
def _decode(
self,
manifest: RecordedMediaManifest,
epoch: RecordedMediaEpoch,
sequence: int,
) -> RecordedCameraFrame:
target = self._inspector.get_segment(manifest, epoch.ordinal, sequence)
fragments: list[bytes] = [target.payload]
key_sequence = sequence
while not _fragment_is_sync(fragments[0]):
key_sequence -= 1
if key_sequence < 1 or sequence - key_sequence > _MAX_KEYFRAME_DISTANCE:
raise SessionIntegrityError("camera frame has no bounded sync fragment")
previous = self._inspector.get_segment(
manifest,
epoch.ordinal,
key_sequence,
)
fragments.insert(0, previous.payload)
init = self._inspector.get_init(manifest, epoch.ordinal)
select_index = sequence - key_sequence
try:
completed = subprocess.run(
[
str(self._ffmpeg_path),
"-hide_banner",
"-loglevel",
"error",
"-f",
"mp4",
"-i",
"pipe:0",
"-vf",
f"select=eq(n\\,{select_index})",
"-fps_mode",
"passthrough",
"-frames:v",
"1",
"-f",
"image2pipe",
"-c:v",
"mjpeg",
"-q:v",
"2",
"pipe:1",
],
input=b"".join((init.payload, *fragments)),
capture_output=True,
timeout=_FFMPEG_TIMEOUT_SECONDS,
check=False,
)
except (OSError, subprocess.TimeoutExpired) as exc:
raise SessionIntegrityError("camera frame decoder failed") from exc
if completed.returncode != 0:
detail = completed.stderr.decode("utf-8", errors="replace").strip()[-500:]
raise SessionIntegrityError(f"camera frame decoder rejected sealed media: {detail}")
width, height = _jpeg_dimensions(completed.stdout)
digest = hashlib.sha256(completed.stdout).hexdigest()
return RecordedCameraFrame(
payload=completed.stdout,
media_type="image/jpeg",
width=width,
height=height,
sha256=digest,
)
def _frame_location(
manifest: RecordedMediaManifest,
frame_index: int,
) -> tuple[RecordedMediaEpoch, int]:
remaining = frame_index
for epoch in manifest.epochs:
if remaining < len(epoch.segments):
return epoch, remaining + 1
remaining -= len(epoch.segments)
raise SessionIntegrityError("camera frame is outside the recorded media manifest")
def _fragment_is_sync(payload: bytes) -> bool:
tfhd_default_flags: int | None = None
sample_flags: int | None = None
sample_count: int | None = None
for box_type, body in _walk_boxes(payload):
if box_type == b"tfhd":
if len(body) < 8:
raise SessionIntegrityError("camera fragment tfhd is truncated")
flags = int.from_bytes(body[1:4], "big")
offset = 8
for mask, size in ((0x000001, 8), (0x000002, 4), (0x000008, 4), (0x000010, 4)):
if flags & mask:
offset += size
if flags & 0x000020:
if offset + 4 > len(body):
raise SessionIntegrityError("camera fragment default flags are truncated")
tfhd_default_flags = struct.unpack_from(">I", body, offset)[0]
elif box_type == b"trun":
if len(body) < 8:
raise SessionIntegrityError("camera fragment trun is truncated")
flags = int.from_bytes(body[1:4], "big")
sample_count = struct.unpack_from(">I", body, 4)[0]
if sample_count != 1:
raise SessionIntegrityError("camera fragment must contain exactly one sample")
offset = 8
if flags & 0x000001:
offset += 4
if flags & 0x000004:
if offset + 4 > len(body):
raise SessionIntegrityError("camera fragment first flags are truncated")
sample_flags = struct.unpack_from(">I", body, offset)[0]
offset += 4
per_sample_sizes = (
(0x000100, 4),
(0x000200, 4),
(0x000400, 4),
(0x000800, 4),
)
for mask, size in per_sample_sizes:
if flags & mask:
if offset + size > len(body):
raise SessionIntegrityError("camera fragment sample data is truncated")
if mask == 0x000400:
sample_flags = struct.unpack_from(">I", body, offset)[0]
offset += size
if sample_count != 1:
raise SessionIntegrityError("camera fragment has no unique video sample")
effective_flags = sample_flags if sample_flags is not None else tfhd_default_flags
if effective_flags is None:
raise SessionIntegrityError("camera fragment sample flags are unavailable")
return (effective_flags & 0x00010000) == 0
def _walk_boxes(payload: bytes):
containers = {b"moof", b"traf"}
pending = [(0, len(payload))]
boxes = 0
while pending:
start, end = pending.pop()
offset = start
while offset + 8 <= end:
boxes += 1
if boxes > 64:
raise SessionIntegrityError("camera fragment box budget was exceeded")
size = struct.unpack_from(">I", payload, offset)[0]
box_type = payload[offset + 4 : offset + 8]
header = 8
if size == 1:
if offset + 16 > end:
raise SessionIntegrityError("camera fragment extended box is truncated")
size = struct.unpack_from(">Q", payload, offset + 8)[0]
header = 16
elif size == 0:
size = end - offset
if size < header or offset + size > end:
raise SessionIntegrityError("camera fragment box size is invalid")
body_start = offset + header
body_end = offset + size
yield box_type, payload[body_start:body_end]
if box_type in containers:
pending.append((body_start, body_end))
offset = body_end
if offset != end:
raise SessionIntegrityError("camera fragment box boundary is invalid")
def _jpeg_dimensions(payload: bytes) -> tuple[int, int]:
if len(payload) < 4 or payload[:2] != b"\xff\xd8" or payload[-2:] != b"\xff\xd9":
raise SessionIntegrityError("camera frame decoder returned an invalid JPEG")
offset = 2
while offset + 4 <= len(payload):
if payload[offset] != 0xFF:
offset += 1
continue
marker = payload[offset + 1]
offset += 2
if marker in {0xD8, 0xD9} or 0xD0 <= marker <= 0xD7:
continue
if offset + 2 > len(payload):
break
length = struct.unpack_from(">H", payload, offset)[0]
if length < 2 or offset + length > len(payload):
break
if marker in {0xC0, 0xC1, 0xC2} and length >= 7:
height, width = struct.unpack_from(">HH", payload, offset + 3)
if width > 0 and height > 0:
return width, height
offset += length
raise SessionIntegrityError("camera frame JPEG dimensions are unavailable")
def _read_cached_jpeg(path: Path) -> RecordedCameraFrame | None:
try:
if path.is_symlink() or not path.is_file():
return None
payload = path.read_bytes()
width, height = _jpeg_dimensions(payload)
except (OSError, SessionIntegrityError):
return None
return RecordedCameraFrame(
payload=payload,
media_type="image/jpeg",
width=width,
height=height,
sha256=hashlib.sha256(payload).hexdigest(),
)
def _publish_cached_jpeg(path: Path, payload: bytes) -> None:
temporary = path.parent / f".{path.name}.{os.getpid()}.{threading.get_ident()}.tmp"
try:
with temporary.open("xb") as stream:
stream.write(payload)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
except OSError as exc:
raise SessionIntegrityError("camera frame cache could not be published") from exc
finally:
with suppress(FileNotFoundError):
temporary.unlink()
+20 -18
View File
@@ -31,6 +31,7 @@ from k1link.laboratory import (
)
from k1link.sessions import (
MaterializedRecording,
RecordedCameraFrameService,
RecordedMediaInspector,
RecordedMediaManifest,
RecordingPreparationQueueFull,
@@ -191,6 +192,16 @@ session_recorded_media_inspector = RecordedMediaInspector(
)
_ffmpeg = _resolve_media_tool("ffmpeg")
_ffprobe = _resolve_media_tool("ffprobe")
session_recorded_camera_frame_service = (
RecordedCameraFrameService(
session_store,
session_recorded_media_inspector,
ffmpeg_path=_ffmpeg,
cache_root=session_store.data_dir / "camera-frame-cache",
)
if _ffmpeg is not None
else None
)
session_legacy_perception_overlay_store = (
RecordedPerceptionOverlayStore(
jobs_root=REPOSITORY_ROOT / ".runtime" / "compute-jobs",
@@ -586,9 +597,7 @@ app.include_router(
app.include_router(
build_advanced_laboratory_router(
evidence_registry=LABORATORY_EVIDENCE_REGISTRY,
evidence_runtime_root_provider=lambda: (
REPOSITORY_ROOT / ".runtime" / "compute-experiments"
),
evidence_runtime_root_provider=lambda: REPOSITORY_ROOT / ".runtime" / "compute-experiments",
e31_root_provider=lambda: (
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e31" / "source-qualifications"
),
@@ -756,22 +765,19 @@ app.include_router(
app.include_router(
build_m4_threat_replay_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "m4"
/ "replay-threat"
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "m4" / "replay-threat"
),
camera_frame_provider=(
session_recorded_camera_frame_service.extract
if session_recorded_camera_frame_service is not None
else None
),
)
)
app.include_router(
build_e46e_ready_stack_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e46e"
/ "ready-stack-results"
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46e" / "ready-stack-results"
),
)
)
@@ -785,11 +791,7 @@ app.include_router(
/ "dashcam-bakeoff-results"
),
e46e_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e46e"
/ "ready-stack-results"
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46e" / "ready-stack-results"
),
)
)
+50 -13
View File
@@ -10,7 +10,7 @@ from functools import lru_cache
from pathlib import Path
from typing import Final
from fastapi import APIRouter, HTTPException, Query
from fastapi import APIRouter, HTTPException, Query, Response
from k1link.perception.threat_replay import (
THREAT_REPLAY_FRAME_SCHEMA,
@@ -22,20 +22,21 @@ from k1link.perception.threat_replay import (
ThreatReplayResult,
read_threat_replay_result,
)
from k1link.sessions import RecordedCameraFrame, SessionIntegrityError
M4_THREAT_CATALOG_SCHEMA: Final = "missioncore.m4-threat-replay-catalog/v1"
M4_THREAT_VIEW_SCHEMA: Final = "missioncore.m4-threat-replay-view/v1"
M4_THREAT_VIDEO_SCHEMA: Final = "missioncore.m4-threat-video-overlay/v1"
M4_THREAT_VISUAL_CATALOG_SCHEMA: Final = (
"missioncore.m4-threat-visual-catalog/v1"
)
M4_THREAT_VISUAL_CATALOG_SCHEMA: Final = "missioncore.m4-threat-visual-catalog/v1"
_RESULT_ID = re.compile(rf"^{THREAT_REPLAY_RESULT_PREFIX}[a-f0-9]{{64}}$")
RootProvider = Callable[[], Path | None]
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
def build_m4_threat_replay_router(
*,
root_provider: RootProvider = lambda: None,
camera_frame_provider: CameraFrameProvider | None = None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory/m4-threat", tags=["laboratory"])
@@ -109,10 +110,52 @@ def build_m4_threat_replay_router(
**copy.deepcopy(frames[ordinal - 1]),
"result_id": result_id,
"ordinal": ordinal,
"camera_url": (
f"/api/v1/laboratory/m4-threat/results/{result_id}/visuals/{ordinal}/camera"
),
"ground_truth": False,
"access": "read-only-replay-simulated",
}
@router.get("/results/{result_id}/visuals/{ordinal}/camera")
def get_visual_camera(result_id: str, ordinal: int) -> Response:
if camera_frame_provider is None:
raise HTTPException(status_code=503, detail="M4.6 camera decoder недоступен")
frozen = result(result_id)
if not 1 <= ordinal <= 32:
raise HTTPException(status_code=404, detail="M4.6 visual frame не найден")
frames = _read_jsonl(frozen.result_root / "visual-frames.jsonl")
if len(frames) != 32:
raise HTTPException(status_code=404, detail="M4.6 visual frame не найден")
identity = frozen.manifest.get("identity")
if not isinstance(identity, dict):
raise HTTPException(status_code=404, detail="M4.6 source identity не найдена")
session_id = identity.get("source_session_id")
sequence = frames[ordinal - 1].get("sequence")
if not isinstance(session_id, str) or not isinstance(sequence, int):
raise HTTPException(status_code=404, detail="M4.6 camera identity не найдена")
try:
camera = camera_frame_provider(session_id, sequence)
except (OSError, SessionIntegrityError, ValueError):
raise HTTPException(
status_code=503,
detail="M4.6 exact camera frame недоступен",
) from None
if camera.width != 800 or camera.height != 600:
raise HTTPException(
status_code=503,
detail="M4.6 camera frame нарушил размерный контракт",
)
return Response(
content=camera.payload,
media_type=camera.media_type,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{camera.sha256}"',
"X-Content-Type-Options": "nosniff",
},
)
@router.get("/results/{result_id}/video-overlay")
def get_video_overlay(result_id: str) -> dict[str, object]:
frozen = result(result_id)
@@ -160,9 +203,7 @@ def _cached_video_overlay(
frames.append(
{
"frame_index": expected_sequence,
"session_seconds": _nonnegative_int(
row.get("source_time_ns"), "source time"
)
"session_seconds": _nonnegative_int(row.get("source_time_ns"), "source time")
/ 1_000_000_000,
"source_available": row["source_available"],
"camera_proposals": copy.deepcopy(row["camera_proposals"]),
@@ -209,9 +250,7 @@ def _project_result(result: ThreatReplayResult) -> dict[str, object]:
},
"metrics": copy.deepcopy(result.metrics),
"configuration": copy.deepcopy(result.report["configuration"]),
"acceptance_requirements": copy.deepcopy(
result.report["acceptance_requirements"]
),
"acceptance_requirements": copy.deepcopy(result.report["acceptance_requirements"]),
"limitations": copy.deepcopy(result.report["limitations"]),
"accepted": result.accepted,
"ground_truth": False,
@@ -271,9 +310,7 @@ def _candidates(provider: RootProvider) -> list[Path]:
(
item
for item in root.iterdir()
if item.is_dir()
and not item.is_symlink()
and _RESULT_ID.fullmatch(item.name)
if item.is_dir() and not item.is_symlink() and _RESULT_ID.fullmatch(item.name)
),
key=lambda item: item.stat().st_mtime_ns,
reverse=True,
+7 -12
View File
@@ -8,7 +8,7 @@ REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
RESULT_ROOT = (
REPOSITORY_ROOT
/ ".runtime/perception-m4/geometry-results"
/ "m4-geometry-replay-8daf3109e3cf30b960b4b376032ff3b5ec58ca42a1e5899b841cf29fbcf14ad8"
/ "m4-geometry-replay-b7d72e9e411fd576243d10ab39717e90daa10703b67c6e32ab284f6bd78d344a"
)
@@ -28,8 +28,8 @@ def test_full_source_geometry_result_closes_m4_4_contract() -> None:
assert proposals["with_range"] == 5341
assert proposals["eligible_for_range"] == 13298
assert proposals["ownership_collision"] == 146
assert result.metrics["geometry_only_observations"] == 21958
assert result.metrics["published_source_point_rows"] == 2164767
assert result.metrics["geometry_only_observations"] == 22740
assert result.metrics["published_source_point_rows"] == 2173778
def test_geometry_result_binds_the_accepted_m4_3_e32_and_e53_evidence() -> None:
@@ -38,26 +38,21 @@ def test_geometry_result_binds_the_accepted_m4_3_e32_and_e53_evidence() -> None:
assert isinstance(identity, dict)
assert identity["detector_result_id"] == (
"m4-detector-replay-"
"11f83f2e0b81758ac2a5a5fc54e9d293b501678df5f6ef97b5c6069ba08605c5"
"m4-detector-replay-11f83f2e0b81758ac2a5a5fc54e9d293b501678df5f6ef97b5c6069ba08605c5"
)
assert identity["geometry_profile_sha256"] == (
"420d989aab5918e0f98e3439cadb8b7251332d51b48f4f2c25d77a385bea49f8"
"cc666c9389a5e221957faddec89584709b66918d14abaf646f1832e001421999"
)
assert identity["historical_references"] == {
"e32": {
"manifest_sha256": (
"f4b57c9f7619c43414adf1d10488b05df466226a523a0c001671102ced0e9ab8"
),
"manifest_sha256": ("f4b57c9f7619c43414adf1d10488b05df466226a523a0c001671102ced0e9ab8"),
"result_id": (
"e32-track-geometry-"
"a14ca0e7fb3850ca0dfa3c41634e1b490a2d58ab74d101afc6d6921fbdb0e6fd"
),
},
"e53": {
"manifest_sha256": (
"fc5b4ae69aae0098b7075c539d25b563dff1bbb09209a49030edda6cba9ee544"
),
"manifest_sha256": ("fc5b4ae69aae0098b7075c539d25b563dff1bbb09209a49030edda6cba9ee544"),
"result_id": (
"e53-camera-first-shadow-"
"e6f03cf8bfb15db86100239b060e13f914532618b7b99e811866e4e6a555186c"
+58 -49
View File
@@ -5,15 +5,19 @@ from pathlib import Path
from fastapi.routing import APIRoute
from k1link.perception.threat_replay import read_threat_replay_result
from k1link.sessions import RecordedCameraFrame
from k1link.web.m4_threat_replay_api import build_m4_threat_replay_router
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
RESULT_ID = "m4-threat-replay-ef521b23eee704dee99856b6e93d5047a9b358e21ffda3ea9cacc2ef768164d9"
RESULT_ID = "m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324"
RESULTS_ROOT = REPOSITORY_ROOT / ".runtime/compute-experiments/m4/replay-threat"
def _endpoint(path: str):
router = build_m4_threat_replay_router(root_provider=lambda: RESULTS_ROOT)
def _endpoint(path: str, *, camera_frame_provider=None):
router = build_m4_threat_replay_router(
root_provider=lambda: RESULTS_ROOT,
camera_frame_provider=camera_frame_provider,
)
return next(
route.endpoint
for route in router.routes
@@ -28,14 +32,14 @@ def test_full_source_threat_result_closes_m4_6_contract() -> None:
assert result.metrics["frames"] == {"total": 4489, "failed": 0}
assert result.metrics["evidence"] == {
"camera-only": 10158,
"current-metric": 27299,
"rolling-map-retained": 69855,
"stale-or-held": 37995,
"current-metric": 28081,
"rolling-map-retained": 70989,
"stale-or-held": 38025,
}
assert result.metrics["decisions"] == {
"not-threat": 10700,
"threat": 6626,
"unknown": 127981,
"not-threat": 10769,
"threat": 7606,
"unknown": 128878,
}
assert result.metrics["fixtures"] == {
"critical": 5,
@@ -51,10 +55,10 @@ def test_threat_result_is_content_bound_and_visual_evidence_is_complete() -> Non
assert isinstance(identity, dict)
assert identity["frames_sha256"] == (
"57219acd7dfe1cf04b12d4a415d1819e0a4947bc3173333bb56eea8fd2bd47b9"
"b57be1839f5915e3b80b54355b694e0bd8c9ac318d0cbe6de2bff713082cfa4e"
)
assert identity["visuals_sha256"] == (
"bda54e144a1b4c878ba645a8dafedcdc0332e16a1489b62b7100670e8eae6f25"
"14b9679b1df4d50aaceeb3e018e46398a92b47727aa83339179395db4dfcd005"
)
visual = result.metrics["visual_evidence"]
assert isinstance(visual, dict)
@@ -70,44 +74,23 @@ def test_threat_result_is_content_bound_and_visual_evidence_is_complete() -> Non
"qualified_base_footprint_available",
)
)
assert visual["geometry_regression_sequences"] == [138, 274, 1880]
assert visual["frame_1880_regression"] == {
"camera_visible_hemispheres_independent_truth": False,
"engineering_anchors": [
{
"anchor_id": "near-concrete-hemisphere",
"bounds_body_xyz_m": [[0.3, 1.2], [-0.8, 0.2], [-0.1, 0.9]],
"centroid_body_xyz_m": [
0.7594228459267565,
-0.2681278641873485,
0.3753253937774115,
],
"component_id": "rolling-5329b5d2ef498e6250c931ff",
"decision": "threat",
"matched": True,
"must_assert_threat": True,
},
{
"anchor_id": "far-concrete-hemisphere",
"bounds_body_xyz_m": [[1.5, 2.7], [0.6, 1.7], [-0.1, 0.9]],
"centroid_body_xyz_m": [
2.058019871618555,
1.1276051922616088,
0.2553253937774115,
],
"component_id": "rolling-7ba116b07683abeca7c8005b",
"decision": "threat",
"matched": True,
"must_assert_threat": False,
},
],
"gate": "two-visible-hemisphere-regression",
"matched_anchor_count": 2,
"required_threats_passed": True,
"retained_components": 10,
"retained_threat_components": 2,
"sequence": 1880,
}
assert visual["geometry_regression_sequences"] == [138, 274, 1880, 2584]
frame_1880 = visual["frame_1880_regression"]
assert frame_1880["matched_anchor_count"] == 2
assert frame_1880["required_threats_passed"] is True
assert frame_1880["camera_visible_hemispheres_independent_truth"] is False
frame_2584 = visual["frame_2584_regression"]
assert frame_2584["matched_anchor_count"] == 2
assert frame_2584["required_threats_passed"] is True
assert frame_2584["matching_basis"] == (
"produced-occupied-cell-inside-camera-reviewed-body-window"
)
near, far = frame_2584["engineering_anchors"]
assert near["anchor_id"] == "near-compact-concrete-hemisphere"
assert near["component_state"] == "retained"
assert near["decision"] == "threat"
assert far["anchor_id"] == "far-concrete-hemisphere-occupancy"
assert far["matched"] is True
body_frame = result.metrics["body_frame"]
assert body_frame["qualified"] == 3861
assert body_frame["rejected"] == 67
@@ -129,6 +112,7 @@ def test_m4_6_lab_api_projects_report_and_exact_visual_frame() -> None:
assert frame["schema_version"] == "missioncore.perception-threat-visual-frame/v2"
assert frame["point_cloud_layer"] == "current-increment"
assert frame["point_cloud_sample_count"] > 0
assert frame["camera_url"].endswith(f"/{RESULT_ID}/visuals/1/camera")
assert frame["rig"] == {
"length_m": 1.0,
"nominal_sensor_height_m": 1.25,
@@ -146,3 +130,28 @@ def test_m4_6_video_overlay_covers_the_exact_recorded_camera_timeline() -> None:
assert overlay["frames"][0]["frame_index"] == 0
assert overlay["frames"][-1]["frame_index"] == 4488
assert overlay["authority"] == "replay-simulated"
def test_m4_6_exact_camera_endpoint_is_bound_to_selected_visual_sequence() -> None:
calls: list[tuple[str, int]] = []
def provide(session_id: str, frame_index: int) -> RecordedCameraFrame:
calls.append((session_id, frame_index))
return RecordedCameraFrame(
payload=b"sealed-jpeg",
media_type="image/jpeg",
width=800,
height=600,
sha256="a" * 64,
)
endpoint = _endpoint(
"/api/v1/laboratory/m4-threat/results/{result_id}/visuals/{ordinal}/camera",
camera_frame_provider=provide,
)
response = endpoint(RESULT_ID, 19)
assert response.body == b"sealed-jpeg"
assert response.media_type == "image/jpeg"
assert response.headers["etag"] == f'"{"a" * 64}"'
assert calls == [("20260720T065719Z_viewer_live", 2584)]
+8 -9
View File
@@ -19,7 +19,7 @@ RESULT_ROOT = (
ROLLING_RESULT_ROOT = (
REPOSITORY_ROOT
/ ".runtime/perception-m4/temporal-results"
/ "m4-temporal-replay-b8611526dfcd2b9be9049560d751bbd23a9ad54b7dda8e9dc48a17374d46266e"
/ "m4-temporal-replay-81e13d5654ac8d1219f6937dd425f7dbcdbacd5748d23fe653297134f0de6c22"
)
@@ -77,8 +77,7 @@ def test_temporal_result_is_digest_bound_to_m4_4_e34_e51_and_e46b(
identity = result.manifest["identity"]
assert isinstance(identity, dict)
assert identity["geometry_result_id"] == (
"m4-geometry-replay-"
"8daf3109e3cf30b960b4b376032ff3b5ec58ca42a1e5899b841cf29fbcf14ad8"
"m4-geometry-replay-8daf3109e3cf30b960b4b376032ff3b5ec58ca42a1e5899b841cf29fbcf14ad8"
)
assert identity["frames_sha256"] == (
"1bf1365bdb3f20214443d3f8b87a0fa88f9848af8ca0456b7ca364d37631c3fc"
@@ -97,18 +96,18 @@ def test_rolling_temporal_result_separates_increment_from_retained_map() -> None
assert result.metrics["rolling_map"] == {
"active_cells_at_end": 587,
"capacity_evicted_cells": 0,
"current_increment_cells": 848868,
"current_increment_cells": 849650,
"input_frames": 4489,
"local_radius_m": 12.0,
"maximum_cells": 65536,
"maximum_retained_age_ns": 3000000000,
"peak_active_cells": 907,
"peak_retained_components": 35,
"radius_evicted_cells": 6512,
"retained_cell_publications": 1776145,
"retained_component_publications": 69855,
"radius_evicted_cells": 6513,
"retained_cell_publications": 1779135,
"retained_component_publications": 70989,
"retention_ns": 3000000000,
"time_evicted_cells": 29620,
"time_evicted_cells": 29720,
"voxel_size_m": 0.45,
}
frames = result.result_root / "frames.jsonl"
@@ -120,5 +119,5 @@ def test_rolling_temporal_result_separates_increment_from_retained_map() -> None
else: # pragma: no cover - immutable artifact guarantees this branch is unreachable
raise AssertionError("frame 1880 missing")
assert frame["schema_version"] == "missioncore.perception-temporal-replay-frame/v2"
assert len(frame["rolling_retained"]) == 10
assert len(frame["rolling_retained"]) == 12
assert all(item["state"] == "retained" for item in frame["rolling_retained"])