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, U extends string = string,
>({ >({
label, label,
className,
mode, mode,
modes, modes,
expanded, expanded,
@@ -31,6 +32,7 @@ export function LaboratoryEvidenceViewer<
children, children,
}: { }: {
label: string; label: string;
className?: string;
mode: T; mode: T;
modes: readonly LaboratoryEvidenceViewerMode<T>[]; modes: readonly LaboratoryEvidenceViewerMode<T>[];
expanded: boolean; expanded: boolean;
@@ -62,7 +64,10 @@ export function LaboratoryEvidenceViewer<
const viewer = ( const viewer = (
<section <section
className="laboratory-evidence-viewer" className={[
"laboratory-evidence-viewer",
className,
].filter(Boolean).join(" ")}
data-expanded={expanded ? "true" : undefined} data-expanded={expanded ? "true" : undefined}
aria-label={label} 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 { import {
RecordedFmp4Player, RecordedFmp4Player,
type RecordedObservationPlayback, type RecordedObservationPlayback,
} from "../RecordedFmp4Player"; } from "../RecordedFmp4Player";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts"; import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
import {
RecordedEvidenceBoxOverlay,
type RecordedEvidenceBox,
type RecordedEvidenceBoxTone,
} from "./RecordedEvidenceBoxOverlay";
export type RecordedEvidenceBoxTone = export type { RecordedEvidenceBox, 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 RecordedEvidenceVideoScene({ export function RecordedEvidenceVideoScene({
source, source,
@@ -61,66 +28,8 @@ export function RecordedEvidenceVideoScene({
ariaLabel: string; ariaLabel: string;
onPlaybackChange: (playback: RecordedObservationPlayback) => void; 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 ( return (
<div className="recorded-evidence-video-scene" ref={hostRef}> <div className="recorded-evidence-video-scene">
<RecordedFmp4Player <RecordedFmp4Player
source={source} source={source}
playback={playback} playback={playback}
@@ -128,7 +37,12 @@ export function RecordedEvidenceVideoScene({
prepare prepare
onPlaybackChange={onPlaybackChange} onPlaybackChange={onPlaybackChange}
/> />
<canvas ref={canvasRef} role="img" aria-label={ariaLabel} /> <RecordedEvidenceBoxOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
boxes={boxes}
ariaLabel={ariaLabel}
/>
</div> </div>
); );
} }
@@ -90,6 +90,7 @@ export interface M4ThreatCameraProposal {
export interface M4ThreatVisualFrame { export interface M4ThreatVisualFrame {
resultId: string; resultId: string;
cameraUrl: string;
ordinal: number; ordinal: number;
sequence: number; sequence: number;
frameId: string; frameId: string;
@@ -408,6 +409,7 @@ export async function fetchM4ThreatVisual(
const corridor = object(item.corridor, "M4.6 visual corridor"); const corridor = object(item.corridor, "M4.6 visual corridor");
return { return {
resultId: result, resultId: result,
cameraUrl: text(item.camera_url, "M4.6 camera URL"),
ordinal: integer(item.ordinal, "M4.6 ordinal"), ordinal: integer(item.ordinal, "M4.6 ordinal"),
sequence: integer(item.sequence, "M4.6 sequence"), sequence: integer(item.sequence, "M4.6 sequence"),
frameId: text(item.frame_id, "M4.6 frame id"), frameId: text(item.frame_id, "M4.6 frame id"),
@@ -46,7 +46,8 @@
user-select: none; user-select: none;
} }
.recorded-evidence-video-scene { .recorded-evidence-video-scene,
.recorded-evidence-image-scene {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -55,12 +56,23 @@
background: var(--nodedc-canvas); 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 { .recorded-evidence-video-scene > .recorded-media-player {
position: absolute; position: absolute;
inset: 0; inset: 0;
} }
.recorded-evidence-video-scene > canvas { .recorded-evidence-video-scene > canvas,
.recorded-evidence-image-scene > canvas {
position: absolute; position: absolute;
z-index: 2; z-index: 2;
inset: 0; inset: 0;
@@ -70,6 +82,18 @@
pointer-events: none; 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 { .e46e-ready-stack-video {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -26,7 +26,7 @@ export function M4ReplayThreatResultView({
<LaboratorySummary <LaboratorySummary
title="M4.6 · dual-evidence threat replay" title="M4.6 · dual-evidence threat replay"
description="Camera и LiDAR дают независимые доказательства, после чего один source-neutral слой оценивает пересечение виртуального коридора, ближайшее сближение и TTC. Ни один сенсор не назначен first." description="Camera и LiDAR дают независимые доказательства, после чего один source-neutral слой оценивает пересечение виртуального коридора, ближайшее сближение и TTC. Ни один сенсор не назначен first."
status="4489/4489 · replay-simulated · accepted" status="Replay contract passed · CV quality gate open"
statusTone="warning" statusTone="warning"
facts={[ facts={[
{ {
@@ -53,7 +53,7 @@ export function M4ReplayThreatResultView({
brief={{ brief={{
question: "Может ли единый слой обнаруживать потенциальное препятствие по двум независимым источникам, не теряя LiDAR-only объекты и не объявляя camera-only наблюдение безопасным?", 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.", 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.", limitation: "Корпус и коридор пока виртуальные, replay не является live-проходом или физическим collision test. На машине виртуальная привязка должна замениться измеренным rigid T_body_from_sensor; independent object truth остаётся следующим gate.",
}} }}
method={{ method={{
@@ -105,8 +105,8 @@ export function M4ReplayThreatResultView({
)} )}
result={( result={(
<LaboratoryResultSummary <LaboratoryResultSummary
title="Dual-evidence слой готов к следующей CV-итерации на recorded replay" title="Replay-инфраструктура принята; object recall ещё проверяется"
status="Replay gate accepted · physical authority withheld" status="Жёлтый: pipeline целостен, независимый object-truth gate не пройден"
statusTone="warning" statusTone="warning"
metrics={[ metrics={[
{ {
@@ -131,7 +131,7 @@ export function M4ReplayThreatResultView({
}, },
]} ]}
conclusion={{ 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-пригодность и выдача команд.", notProved: "Не доказаны live realtime, измеренный T_body_from_sensor и геометрия физического корпуса, независимая object-level правильность, навигационная или safety-пригодность и выдача команд.",
decision: "Сохранить dual-evidence provider как канонический replay seam и переходить к независимому object-centric gate; физическую геометрию и live/actuation authority не смешивать с дальнейшей CV-разработкой.", decision: "Сохранить dual-evidence provider как канонический replay seam и переходить к независимому object-centric gate; физическую геометрию и live/actuation authority не смешивать с дальнейшей CV-разработкой.",
}} }}
@@ -7,6 +7,7 @@ import {
type LaboratoryMetricSceneMode, type LaboratoryMetricSceneMode,
} from "../../components/laboratory/LaboratoryMetricEvidenceScene"; } from "../../components/laboratory/LaboratoryMetricEvidenceScene";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer"; import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import { RecordedEvidenceImageScene } from "../../components/laboratory/RecordedEvidenceImageScene";
import { import {
RecordedEvidenceVideoScene, RecordedEvidenceVideoScene,
type RecordedEvidenceBox, type RecordedEvidenceBox,
@@ -108,7 +109,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
}, [ordinal, resultId]); }, [ordinal, resultId]);
useEffect(() => { useEffect(() => {
if ((mode !== "video" && mode !== "camera") || (videoOverlay && videoSource)) return; if (mode !== "video" || (videoOverlay && videoSource)) return;
const controller = new AbortController(); const controller = new AbortController();
setVideoLoading(true); setVideoLoading(true);
setVideoError(null); setVideoError(null);
@@ -157,14 +158,6 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
return () => controller.abort(); return () => controller.abort();
}, [mode, resultId, videoOverlay, videoSource]); }, [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( const activeVideoFrame = useMemo(
() => videoOverlay () => videoOverlay
? selectM4ThreatVideoFrame(videoOverlay.frames, videoPlayback.currentSeconds) ? selectM4ThreatVideoFrame(videoOverlay.frames, videoPlayback.currentSeconds)
@@ -317,7 +310,7 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
) : undefined; ) : undefined;
let content; let content;
if (mode === "video" || mode === "camera") { if (mode === "video") {
content = videoLoading ? ( content = videoLoading ? (
<div className="l3-visual-audit__state" role="status"> <div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" /> <span className="busy-indicator" aria-hidden="true" />
@@ -335,14 +328,30 @@ export function M4ReplayThreatVisual({ resultId }: { resultId: string }) {
imageWidth={videoOverlay.imageWidth} imageWidth={videoOverlay.imageWidth}
imageHeight={videoOverlay.imageHeight} imageHeight={videoOverlay.imageHeight}
boxes={activeBoxes} boxes={activeBoxes}
ariaLabel={ ariaLabel={`M4.6 full video frame ${activeVideoFrame?.frameIndex ?? 0}: ${activeBoxes.length} proposals`}
mode === "camera"
? `M4.6 exact camera sample ${ordinal}: ${activeBoxes.length} proposals`
: `M4.6 full video frame ${activeVideoFrame?.frameIndex ?? 0}: ${activeBoxes.length} proposals`
}
onPlaybackChange={setVideoPlayback} 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 { } else {
content = sampleLoading ? ( content = sampleLoading ? (
<div className="l3-visual-audit__state" role="status"> <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"> <div className="l3-visual-audit m4-replay-threat-visual">
<LaboratoryEvidenceViewer <LaboratoryEvidenceViewer
label="M4.6 dual-evidence replay: video, camera and metric 3D" label="M4.6 dual-evidence replay: video, camera and metric 3D"
className="m4-replay-threat-evidence-viewer"
mode={mode} mode={mode}
modes={[ modes={[
{ value: "video", label: "VIDEO" }, { value: "video", label: "VIDEO" },
@@ -63,7 +63,7 @@ test("M4.6 decodes accepted dual-evidence result without physical authority", as
}, },
metrics: { metrics: {
decisions: { threat: 8010, "not-threat": 6610, unknown: 60832 }, 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 }, fixtures: { critical: 4, critical_false_not_threat: 0, passed: 9, total: 9 },
runtime: { runtime: {
frames_per_second: 116.4, frames_per_second: 116.4,
@@ -99,7 +99,7 @@ test("M4.6 decodes accepted dual-evidence result without physical authority", as
}), { status: 200 }), }), { status: 200 }),
}); });
assert.equal(result.resultId, resultId); 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.fixtures.criticalFalseNotThreat, 0);
assert.equal(result.metrics.bodyFrame.qualified, 3861); assert.equal(result.metrics.bodyFrame.qualified, 3861);
assert.equal(result.metrics.bodyFrame.cameraForwardAlignmentDeg.p95, 8.439); 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({ fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.perception-threat-visual-frame/v1", schema_version: "missioncore.perception-threat-visual-frame/v1",
result_id: resultId, result_id: resultId,
camera_url: `/api/v1/laboratory/m4-threat/results/${resultId}/visuals/1/camera`,
ordinal: 1, ordinal: 1,
sequence: 20, sequence: 20,
frame_id: "frame-000020", 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 }), }), { status: 200 }),
}); });
assert.equal(frame.sequence, 20); assert.equal(frame.sequence, 20);
assert.match(frame.cameraUrl, /\/visuals\/1\/camera$/);
assert.equal(frame.metricObstacles[0].assessment.decision, "threat"); assert.equal(frame.metricObstacles[0].assessment.decision, "threat");
assert.equal(frame.cameraProposals[0].threatDecision, "unknown"); assert.equal(frame.cameraProposals[0].threatDecision, "unknown");
assert.equal(frame.pointCloudSampleCount, 2); 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({ fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.perception-threat-visual-frame/v2", schema_version: "missioncore.perception-threat-visual-frame/v2",
result_id: resultId, result_id: resultId,
camera_url: `/api/v1/laboratory/m4-threat/results/${resultId}/visuals/14/camera`,
ordinal: 14, ordinal: 14,
sequence: 1880, sequence: 1880,
frame_id: "frame-001880", 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); assert.equal(selectM4ThreatVideoFrame(overlay.frames, 35.50).frameIndex, 1);
}); });
test("M4.6 viewer reuses shared video and metric evidence renderers", async () => { test("M4.6 viewer reuses shared camera, video and metric evidence renderers", async () => {
const [visual, videoScene, metricScene] = await Promise.all([ const [visual, imageScene, videoScene, metricScene] = await Promise.all([
readFile(new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url), "utf8"), 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/RecordedEvidenceVideoScene.tsx", import.meta.url), "utf8"),
readFile(new URL("../src/components/laboratory/LaboratoryMetricEvidenceScene.tsx", import.meta.url), "utf8"), readFile(new URL("../src/components/laboratory/LaboratoryMetricEvidenceScene.tsx", import.meta.url), "utf8"),
]); ]);
assert.match(visual, /<RecordedEvidenceVideoScene/); assert.match(visual, /<RecordedEvidenceVideoScene/);
assert.match(visual, /<RecordedEvidenceImageScene/);
assert.match(visual, /<LaboratoryMetricEvidenceScene/); assert.match(visual, /<LaboratoryMetricEvidenceScene/);
assert.match(visual, /label: "VIDEO"/); assert.match(visual, /label: "VIDEO"/);
assert.match(visual, /label: "CAMERA"/); assert.match(visual, /label: "CAMERA"/);
assert.match(visual, /label: "3D"/); assert.match(visual, /label: "3D"/);
assert.match(videoScene, /<RecordedFmp4Player/); assert.match(videoScene, /<RecordedFmp4Player/);
assert.match(imageScene, /<RecordedEvidenceBoxOverlay/);
assert.match(metricScene, /OrbitControls/); assert.match(metricScene, /OrbitControls/);
assert.match(metricScene, /CURRENT INCREMENT/); assert.match(metricScene, /CURRENT INCREMENT/);
assert.match(metricScene, /ROLLING MAP/); assert.match(metricScene, /ROLLING MAP/);
+1 -1
View File
@@ -193,7 +193,7 @@
}, },
{ {
"catalog_id": "m4-replay-threat", "catalog_id": "m4-replay-threat",
"evidence_id": "m4-threat-replay-ef521b23eee704dee99856b6e93d5047a9b358e21ffda3ea9cacc2ef768164d9", "evidence_id": "m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324",
"signal": "progress", "signal": "progress",
"lifecycle": "current", "lifecycle": "current",
"visual_evidence": "available" "visual_evidence": "available"
@@ -34,7 +34,7 @@
"geometry_local_radius_m": 10.0, "geometry_local_radius_m": 10.0,
"geometry_voxel_size_m": 0.45, "geometry_voxel_size_m": 0.45,
"geometry_minimum_cluster_points": 4, "geometry_minimum_cluster_points": 4,
"geometry_minimum_cluster_voxels": 2, "geometry_minimum_cluster_voxels": 1,
"maximum_geometry_clusters_per_frame": 64 "maximum_geometry_clusters_per_frame": 64
}, },
"policy": { "policy": {
+4 -4
View File
@@ -5,10 +5,10 @@
"source": { "source": {
"source_id": "RAVNOVES00", "source_id": "RAVNOVES00",
"session_id": "20260720T065719Z_viewer_live", "session_id": "20260720T065719Z_viewer_live",
"temporal_result_id": "m4-temporal-replay-b8611526dfcd2b9be9049560d751bbd23a9ad54b7dda8e9dc48a17374d46266e", "temporal_result_id": "m4-temporal-replay-81e13d5654ac8d1219f6937dd425f7dbcdbacd5748d23fe653297134f0de6c22",
"temporal_frames_sha256": "08a03669d3487ecf7bd62b047ae5b8d1ff586a27e7435e5f235907a4d709dd25", "temporal_frames_sha256": "e83b80ea06b3462c2d04c5a1b74289a0ec401596c7150ae90f5a1b748b639c3a",
"geometry_result_id": "m4-geometry-replay-8daf3109e3cf30b960b4b376032ff3b5ec58ca42a1e5899b841cf29fbcf14ad8", "geometry_result_id": "m4-geometry-replay-b7d72e9e411fd576243d10ab39717e90daa10703b67c6e32ab284f6bd78d344a",
"geometry_frames_sha256": "b4db5d0ebaba4d6268a1006707dc313c229f3dbdfd73b1d863cad5d853be8ac4", "geometry_frames_sha256": "d4d4c0a98e09c0f26251284747108651cf4b21e9c9733e3a14963999b0300772",
"detector_result_id": "m4-detector-replay-11f83f2e0b81758ac2a5a5fc54e9d293b501678df5f6ef97b5c6069ba08605c5", "detector_result_id": "m4-detector-replay-11f83f2e0b81758ac2a5a5fc54e9d293b501678df5f6ef97b5c6069ba08605c5",
"detector_frames_sha256": "9bf5ae17938cd57c112278781b38d54a7187cf2dabc7bb0332acdb1efad721f5", "detector_frames_sha256": "9bf5ae17938cd57c112278781b38d54a7187cf2dabc7bb0332acdb1efad721f5",
"source_pack_id": "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b", "source_pack_id": "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b",
+2 -2
View File
@@ -6,8 +6,8 @@
"source": { "source": {
"source_id": "RAVNOVES00", "source_id": "RAVNOVES00",
"session_id": "20260720T065719Z_viewer_live", "session_id": "20260720T065719Z_viewer_live",
"geometry_result_id": "m4-geometry-replay-8daf3109e3cf30b960b4b376032ff3b5ec58ca42a1e5899b841cf29fbcf14ad8", "geometry_result_id": "m4-geometry-replay-b7d72e9e411fd576243d10ab39717e90daa10703b67c6e32ab284f6bd78d344a",
"geometry_frames_sha256": "b4db5d0ebaba4d6268a1006707dc313c229f3dbdfd73b1d863cad5d853be8ac4" "geometry_frames_sha256": "d4d4c0a98e09c0f26251284747108651cf4b21e9c9733e3a14963999b0300772"
}, },
"temporal": { "temporal": {
"coordinate_frame": "map", "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 `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. an intersecting corridor but cannot claim current motion or safe clearance.
The accepted resealed M4.5R result is Review of exact CAMERA frame `2584` then exposed a separate geometry defect:
`m4-temporal-replay-b8611526dfcd2b9be9049560d751bbd23a9ad54b7dda8e9dc48a17374d46266e`: 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 - frame ledger SHA-256
`08a03669d3487ecf7bd62b047ae5b8d1ff586a27e7435e5f235907a4d709dd25`; `e83b80ea06b3462c2d04c5a1b74289a0ec401596c7150ae90f5a1b748b639c3a`;
- manifest/report SHA-256 - manifest/report SHA-256
`5df4c31b1d3a9f834895af999c55829b40afeca89d50ffe04f7e23bd85e2bd8c` / `540640dacb10f0215133f3a2c833b0502856e132350fa262751894a7f62cdb84` /
`2e708fb30f35b34ed99cbe107055772bfb61553a3321c0ff2fee725d3a07bdc9`; `461bedc3ae03cda90630c92e6b4c0c637ae4b6514a90d956abfb1be592230ed0`;
- `4,489 / 4,489` frames, zero failed; - `4,489 / 4,489` frames, zero failed;
- `848,868` current increment cells; - `849,650` current increment cells;
- `69,855` retained component and `1,776,145` retained cell publications; - `70,989` retained component and `1,779,135` retained cell publications;
- peak `907` active cells and `35` retained components; - 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`. maximum retained age exactly `3.0 s`.
The accepted resealed M4.6 result is The accepted corrected M4.6 result is
`m4-threat-replay-ef521b23eee704dee99856b6e93d5047a9b358e21ffda3ea9cacc2ef768164d9`: `m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324`:
- manifest/report SHA-256 - manifest/report SHA-256
`199ed52211f3d50ecce0e3f0bc1efc094b665e9a0c5e0426e01640a6b9a5f0bb` / `87c58e8123deede1de2908af98752fbe03b94785858df2492e817c77cf8b02c5` /
`e32402fd6d52b1c22e3a5ee5b794bbe8facaf132e02cdaa902175395fa6b9db8`; `8def86903c8d37e3112cf6452d6a22a5a835ccc542e9e01714868dd3a9831864`;
- frame/visual SHA-256 - frame/visual SHA-256
`57219acd7dfe1cf04b12d4a415d1819e0a4947bc3173333bb56eea8fd2bd47b9` / `b57be1839f5915e3b80b54355b694e0bd8c9ac318d0cbe6de2bff713082cfa4e` /
`bda54e144a1b4c878ba645a8dafedcdc0332e16a1489b62b7100670e8eae6f25`; `14b9679b1df4d50aaceeb3e018e46398a92b47727aa83339179395db4dfcd005`;
- evidence: `27,299` current metric, `69,855` rolling-map, `37,995` - evidence: `28,081` current metric, `70,989` rolling-map, `38,025`
stale/held and `10,158` camera-only publications; 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; - deterministic fixtures `10 / 10`, five critical, zero critical false-safe;
- body-frame qualification unchanged at `3,861 / 3,928`, confirming the - body-frame qualification unchanged at `3,861 / 3,928`, confirming the
representation fix did not regress the earlier route/gravity correction; compact-component fix did not regress the earlier route/gravity correction;
- frame `1880` now binds two distinct engineering anchors to retained occupied - frames `1880` and `2584` bind reviewed body-frame windows to produced occupied
components; the near anchor asserts threat. The anchors are not independent cells/components. At `2584` the compact near hemisphere survives as retained
truth. 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 The common viewer keeps VIDEO/CAMERA/3D/PLAN synchronized and independently
toggles `CURRENT INCREMENT` and `ROLLING MAP`. Regression frames are now `138`, toggles `CURRENT INCREMENT` and `ROLLING MAP`. CAMERA requests one exact JPEG
`274` and `1880`. M4.7 may proceed only from the resealed identities above; decoded from the bounded fMP4 GOP instead of preparing the complete video.
physical-live, collision, navigation and actuation authority remain false. 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 ## Implementation order
@@ -52,24 +52,24 @@ is not a map-clearing policy.
## Evidence and acceptance ## Evidence and acceptance
The accepted M4.5R result is The current accepted M4.5R result is
`m4-temporal-replay-b8611526dfcd2b9be9049560d751bbd23a9ad54b7dda8e9dc48a17374d46266e`. `m4-temporal-replay-81e13d5654ac8d1219f6937dd425f7dbcdbacd5748d23fe653297134f0de6c22`.
Its frame ledger SHA-256 is Its frame ledger SHA-256 is
`08a03669d3487ecf7bd62b047ae5b8d1ff586a27e7435e5f235907a4d709dd25`. `e83b80ea06b3462c2d04c5a1b74289a0ec401596c7150ae90f5a1b748b639c3a`.
Across all `4,489` source frames it records: Across all `4,489` source frames it records:
- `848,868` current increment occupied cells; - `849,650` current increment occupied cells;
- `69,855` retained component publications and `1,776,145` retained cell - `70,989` retained component publications and `1,779,135` retained cell
publications; publications;
- peak `907` active cells and `35` retained components; - 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. - maximum retained age exactly `3.0 s` and `587` active cells at replay end.
The accepted M4.6 result is The current accepted M4.6 result is
`m4-threat-replay-ef521b23eee704dee99856b6e93d5047a9b358e21ffda3ea9cacc2ef768164d9`. `m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324`.
All `4,489` frames completed with `27,299` current metric, `69,855` rolling-map, All `4,489` frames completed with `28,081` current metric, `70,989` rolling-map,
`37,995` stale/held and `10,158` camera-only evidence publications assessed `38,025` stale/held and `10,158` camera-only evidence publications assessed
exactly once. Decisions are `6,626 threat`, `10,700 not-threat` and `127,981 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 unknown`. All `10/10` deterministic fixtures pass with zero critical false-safe
outcomes. 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 assert threat; both regions are present in the accepted result. These anchors
encode a reproducible owner-reviewed visual case, not independent object truth. 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 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 layers over the same selected frame. The current point count is not relabelled
as a complete scan, and retained occupied cells remain independently hideable. 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 - 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 scan/ray provider must add explicit observation and clearing semantics rather
than weakening this contract. 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. detector accuracy or physical collision acceptance.
- Live K1, measured `T_body_from_sensor`, physical body geometry, navigation, - Live K1, measured `T_body_from_sensor`, physical body geometry, navigation,
commands and actuation remain outside M4.5R/M4.6 authority. 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" TEMPORAL_REPLAY_MANIFEST_NAME: Final = "manifest.json"
E34_RESULT_ID: Final = ( E34_RESULT_ID: Final = (
"e34-temporal-occupied-" "e34-temporal-occupied-8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73"
"8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73"
) )
E34_MANIFEST_SHA256: Final = "88285f44d0316913881cc0948a4dfb300d56b460c51be36d51b4dd717cbf4170" E34_MANIFEST_SHA256: Final = "88285f44d0316913881cc0948a4dfb300d56b460c51be36d51b4dd717cbf4170"
E51_RESULT_ID: Final = ( E51_RESULT_ID: Final = (
"e51-motion-semantic-" "e51-motion-semantic-1abb7eb9940608fc5af95a1f318cadfbc42ac2412a8662b6622e000e03da1555"
"1abb7eb9940608fc5af95a1f318cadfbc42ac2412a8662b6622e000e03da1555"
) )
E51_MANIFEST_SHA256: Final = "a38ecad59765d1439ac432a34c053362195ec4b493d8e8f1d4a1731f99836437" E51_MANIFEST_SHA256: Final = "a38ecad59765d1439ac432a34c053362195ec4b493d8e8f1d4a1731f99836437"
E46B_RESULT_ID: Final = ( E46B_RESULT_ID: Final = (
"e46b-temporal-motion-" "e46b-temporal-motion-78d038912273364e36f996401873a8ee178a94641350021c2cd35bcb301ba36d"
"78d038912273364e36f996401873a8ee178a94641350021c2cd35bcb301ba36d"
) )
E46B_MANIFEST_SHA256: Final = "b0d0b6bdfa23f0475106c1870771ee90dd6de85719396a665dd7311f93da3d59" E46B_MANIFEST_SHA256: Final = "b0d0b6bdfa23f0475106c1870771ee90dd6de85719396a665dd7311f93da3d59"
E46B_CASES_SHA256: Final = "95b58300f10dc796f7576bffbcc670ac76b74d6f25b13c0768f57331cba49074" E46B_CASES_SHA256: Final = "95b58300f10dc796f7576bffbcc670ac76b74d6f25b13c0768f57331cba49074"
@@ -100,9 +97,7 @@ def build_temporal_replay(
store = RecordedGeometryStore.from_repository(repository) store = RecordedGeometryStore.from_repository(repository)
temporal = BoundedSpatialTemporalProvider(point_resolver=store, profile=profile) temporal = BoundedSpatialTemporalProvider(point_resolver=store, profile=profile)
motion = ClassIndependentMotionEstimator(profile=profile) motion = ClassIndependentMotionEstimator(profile=profile)
rolling_profile = load_rolling_map_profile( rolling_profile = load_rolling_map_profile(repository / DEFAULT_ROLLING_MAP_PROFILE_PATH)
repository / DEFAULT_ROLLING_MAP_PROFILE_PATH
)
rolling = RollingLocalObstacleMapProvider( rolling = RollingLocalObstacleMapProvider(
pose_resolver=store, pose_resolver=store,
profile=rolling_profile, profile=rolling_profile,
@@ -147,9 +142,7 @@ def build_temporal_replay(
obstacles = motion.estimate(packet, temporal_obstacles) obstacles = motion.estimate(packet, temporal_obstacles)
rolling_retained = rolling.update(packet, obstacles) rolling_retained = rolling.update(packet, obstacles)
latencies_ms.append((time.perf_counter_ns() - frame_started_ns) / 1_000_000) latencies_ms.append((time.perf_counter_ns() - frame_started_ns) / 1_000_000)
current = tuple( current = tuple(item for item in obstacles if item.state is TemporalState.CURRENT)
item for item in obstacles if item.state is TemporalState.CURRENT
)
held = tuple(item for item in obstacles if item.state is TemporalState.HELD) 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) expired = tuple(item for item in obstacles if item.state is TemporalState.EXPIRED)
motion_counts = _motion_counts(current) motion_counts = _motion_counts(current)
@@ -158,13 +151,10 @@ def build_temporal_replay(
"sequence": frame_count, "sequence": frame_count,
"frame_id": packet.envelope.frame_id, "frame_id": packet.envelope.frame_id,
"source_time_ns": packet.envelope.timestamps.source_ns, "source_time_ns": packet.envelope.timestamps.source_ns,
"source_available": ( "source_available": (packet.envelope.registered_point_increment.available),
packet.envelope.registered_point_increment.available
),
"input_observation_count": len(observations), "input_observation_count": len(observations),
"current_occupied_input_count": sum( "current_occupied_input_count": sum(
item.occupied_support item.occupied_support and item.currentness.value == "current"
and item.currentness.value == "current"
for item in observations for item in observations
), ),
"nonmetric_uncertainty_input_count": sum( "nonmetric_uncertainty_input_count": sum(
@@ -173,13 +163,10 @@ def build_temporal_replay(
"current": [item.to_dict() for item in current], "current": [item.to_dict() for item in current],
"held": [item.to_dict() for item in held], "held": [item.to_dict() for item in held],
"expired": [item.to_dict() for item in expired], "expired": [item.to_dict() for item in expired],
"rolling_retained": [ "rolling_retained": [item.to_dict() for item in rolling_retained],
item.to_dict() for item in rolling_retained
],
"motion_counts": motion_counts, "motion_counts": motion_counts,
"map_frame_jump_candidate": any( "map_frame_jump_candidate": any(
item.association_basis == "map-frame-discontinuity" item.association_basis == "map-frame-discontinuity" for item in current
for item in current
), ),
"policy": _frame_policy(), "policy": _frame_policy(),
"authority": _false_authority(), "authority": _false_authority(),
@@ -216,9 +203,7 @@ def build_temporal_replay(
identity = { identity = {
"schema_version": TEMPORAL_REPLAY_SCHEMA_V2, "schema_version": TEMPORAL_REPLAY_SCHEMA_V2,
"geometry_result_id": geometry.result_id, "geometry_result_id": geometry.result_id,
"geometry_manifest_sha256": _file_sha256( "geometry_manifest_sha256": _file_sha256(geometry.result_root / "manifest.json"),
geometry.result_root / "manifest.json"
),
"geometry_frames_sha256": profile.geometry_frames_sha256, "geometry_frames_sha256": profile.geometry_frames_sha256,
"profile_id": profile.profile_id, "profile_id": profile.profile_id,
"profile_sha256": profile.profile_sha256, "profile_sha256": profile.profile_sha256,
@@ -398,18 +383,11 @@ def read_temporal_replay_result(root: Path) -> TemporalReplayResult:
if is_v2 if is_v2
else _requirements(metrics, ttl_ns / 1_000_000_000) else _requirements(metrics, ttl_ns / 1_000_000_000)
) )
if ( if ttl_ns != 750_000_000 or requirements != expected_requirements:
ttl_ns != 750_000_000
or requirements != expected_requirements
):
raise TemporalReplayError("temporal replay acceptance was not derived from metrics") raise TemporalReplayError("temporal replay acceptance was not derived from metrics")
if ( if (
report.get("schema_version") 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("result_id") != resolved.name
or report.get("identity_sha256") != identity_sha256 or report.get("identity_sha256") != identity_sha256
or report.get("metrics") != metrics or report.get("metrics") != metrics
@@ -490,9 +468,7 @@ def _requirements_v2(
requirements.update( requirements.update(
{ {
"registered_increment_is_not_treated_as_complete_scan": True, "registered_increment_is_not_treated_as_complete_scan": True,
"rolling_map_processed_every_source_frame": ( "rolling_map_processed_every_source_frame": (rolling.get("input_frames") == 4489),
rolling.get("input_frames") == 4489
),
"rolling_map_materialized_retained_occupancy": ( "rolling_map_materialized_retained_occupancy": (
_integer( _integer(
rolling.get("retained_component_publications"), rolling.get("retained_component_publications"),
@@ -506,8 +482,7 @@ def _requirements_v2(
"maximum rolling retained age", "maximum rolling retained age",
) )
<= round(rolling_retention_seconds * 1_000_000_000) <= round(rolling_retention_seconds * 1_000_000_000)
and rolling.get("retention_ns") and rolling.get("retention_ns") == round(rolling_retention_seconds * 1_000_000_000)
== round(rolling_retention_seconds * 1_000_000_000)
and rolling.get("maximum_cells") == rolling_maximum_cells and rolling.get("maximum_cells") == rolling_maximum_cells
and _integer( and _integer(
rolling.get("capacity_evicted_cells"), rolling.get("capacity_evicted_cells"),
@@ -571,22 +546,24 @@ def _requirements(metrics: dict[str, object], ttl_seconds: float) -> dict[str, b
) )
return { return {
"full_frame_accounting": frames == {"total": 4489, "failed": 0}, "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": ( "metric_and_nonmetric_partition_closed": (
temporal.get("current_occupied_observations") == 27299 temporal_input == current_input + uncertainty_input
and temporal.get("nonmetric_uncertainty_observations") == 10158 and current_input > 0
and uncertainty_input > 0
), ),
"camera_uncertainty_never_created_occupied_state": ( "camera_uncertainty_never_created_occupied_state": (
temporal_input == current_input + uncertainty_input temporal_input == current_input + uncertainty_input
), ),
"detector_identity_changes_survive_spatial_reassociation": identity_changes > 0, "detector_identity_changes_survive_spatial_reassociation": identity_changes > 0,
"temporal_state_is_bounded": ( "temporal_state_is_bounded": (
peak_components <= 256 peak_components <= 256 and peak_cells <= 4096 and maximum_history <= 8
and peak_cells <= 4096
and maximum_history <= 8
), ),
"held_and_expired_states_materialized": held_publications > 0 "held_and_expired_states_materialized": held_publications > 0 and expired_publications > 0,
and expired_publications > 0,
"no_occupied_cells_survive_ttl": ( "no_occupied_cells_survive_ttl": (
retention.get("past_ttl_occupied_publications") == 0 retention.get("past_ttl_occupied_publications") == 0
and retention.get("ghost_occupancy_past_ttl_count") == 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) value > 0 for value in (moving, stationary, unknown)
), ),
"motion_accounting_closed": ( "motion_accounting_closed": (
motion_input motion_input == current_publications + held_publications + expired_publications
== current_publications + held_publications + expired_publications
), ),
"bounded_labeled_engineering_checks_recorded": ( "bounded_labeled_engineering_checks_recorded": (
clips clips
@@ -651,9 +627,8 @@ def _validate_frame_ledger(
frame.get("nonmetric_uncertainty_input_count"), frame.get("nonmetric_uncertainty_input_count"),
"frame nonmetric uncertainty inputs", "frame nonmetric uncertainty inputs",
) )
if ( if frame_current_inputs + frame_uncertainty_inputs != frame.get(
frame_current_inputs + frame_uncertainty_inputs "input_observation_count"
!= frame.get("input_observation_count")
): ):
raise TemporalReplayError("temporal frame input partition is open") raise TemporalReplayError("temporal frame input partition is open")
current_inputs += frame_current_inputs current_inputs += frame_current_inputs
@@ -673,11 +648,7 @@ def _validate_frame_ledger(
groups[state] = items groups[state] = items
for item in items: for item in items:
motion_counts[item.motion.value] += 1 motion_counts[item.motion.value] += 1
component_ids = [ component_ids = [item.component_id for group in groups.values() for item in group]
item.component_id
for group in groups.values()
for item in group
]
if len(component_ids) != len(set(component_ids)): if len(component_ids) != len(set(component_ids)):
raise TemporalReplayError("temporal frame duplicated a component") raise TemporalReplayError("temporal frame duplicated a component")
if is_v2: if is_v2:
@@ -689,13 +660,9 @@ def _validate_frame_ledger(
) )
) )
if any(item.state is not TemporalState.RETAINED for item in rolling): if any(item.state is not TemporalState.RETAINED for item in rolling):
raise TemporalReplayError( raise TemporalReplayError("rolling map published a non-retained component")
"rolling map published a non-retained component"
)
if set(component_ids) & {item.component_id for item in rolling}: if set(component_ids) & {item.component_id for item in rolling}:
raise TemporalReplayError( raise TemporalReplayError("rolling and temporal component identities overlap")
"rolling and temporal component identities overlap"
)
rolling_publications += len(rolling) rolling_publications += len(rolling)
current_publications += len(groups[TemporalState.CURRENT]) current_publications += len(groups[TemporalState.CURRENT])
held_publications += len(groups[TemporalState.HELD]) held_publications += len(groups[TemporalState.HELD])
@@ -706,11 +673,7 @@ def _validate_frame_ledger(
frame_metrics = _object(metrics.get("frames"), "temporal frames") frame_metrics = _object(metrics.get("frames"), "temporal frames")
temporal = _object(metrics.get("temporal"), "temporal metrics") temporal = _object(metrics.get("temporal"), "temporal metrics")
motion = _object(metrics.get("motion"), "motion metrics") motion = _object(metrics.get("motion"), "motion metrics")
rolling_metrics = ( rolling_metrics = _object(metrics.get("rolling_map"), "rolling map metrics") if is_v2 else None
_object(metrics.get("rolling_map"), "rolling map metrics")
if is_v2
else None
)
if ( if (
frames != frame_metrics.get("total") frames != frame_metrics.get("total")
or observations != metrics.get("input_observations") or observations != metrics.get("input_observations")
@@ -724,8 +687,7 @@ def _validate_frame_ledger(
or motion_counts[MotionState.UNKNOWN.value] != motion.get("unknown") or motion_counts[MotionState.UNKNOWN.value] != motion.get("unknown")
or ( or (
rolling_metrics is not None rolling_metrics is not None
and rolling_publications and rolling_publications != rolling_metrics.get("retained_component_publications")
!= rolling_metrics.get("retained_component_publications")
) )
): ):
raise TemporalReplayError("temporal frame ledger and metrics disagree") raise TemporalReplayError("temporal frame ledger and metrics disagree")
@@ -801,10 +763,7 @@ def _clip_check(
def _motion_counts(obstacles: tuple[TemporalObstacle, ...]) -> dict[str, int]: def _motion_counts(obstacles: tuple[TemporalObstacle, ...]) -> dict[str, int]:
return { return {state.value: sum(item.motion is state for item in obstacles) for state in MotionState}
state.value: sum(item.motion is state for item in obstacles)
for state in MotionState
}
def _frame_policy() -> dict[str, object]: def _frame_policy() -> dict[str, object]:
@@ -836,9 +795,7 @@ def _read_geometry_frame(line: bytes, sequence: int) -> dict[str, object]:
try: try:
frame = _object(json.loads(line), "geometry replay frame") frame = _object(json.loads(line), "geometry replay frame")
except json.JSONDecodeError as exc: except json.JSONDecodeError as exc:
raise TemporalReplayError( raise TemporalReplayError(f"geometry replay frame {sequence + 1} is invalid JSON") from exc
f"geometry replay frame {sequence + 1} is invalid JSON"
) from exc
if frame.get("sequence") != sequence: if frame.get("sequence") != sequence:
raise TemporalReplayError("geometry replay frame sequence is incomplete") raise TemporalReplayError("geometry replay frame sequence is incomplete")
return frame return frame
@@ -853,25 +810,23 @@ def _read_frame(
try: try:
frame = _object(json.loads(line), "temporal replay frame") frame = _object(json.loads(line), "temporal replay frame")
except json.JSONDecodeError as exc: except json.JSONDecodeError as exc:
raise TemporalReplayError( raise TemporalReplayError(f"temporal replay frame {sequence + 1} is invalid JSON") from exc
f"temporal replay frame {sequence + 1} is invalid JSON"
) from exc
expected_keys = { expected_keys = {
"schema_version", "schema_version",
"sequence", "sequence",
"frame_id", "frame_id",
"source_time_ns", "source_time_ns",
"source_available", "source_available",
"input_observation_count", "input_observation_count",
"current_occupied_input_count", "current_occupied_input_count",
"nonmetric_uncertainty_input_count", "nonmetric_uncertainty_input_count",
"current", "current",
"held", "held",
"expired", "expired",
"motion_counts", "motion_counts",
"map_frame_jump_candidate", "map_frame_jump_candidate",
"policy", "policy",
"authority", "authority",
} }
if is_v2: if is_v2:
expected_keys.add("rolling_retained") 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" THREAT_REPLAY_MANIFEST_NAME: Final = "manifest.json"
VISUAL_FRAME_COUNT: Final = 32 VISUAL_FRAME_COUNT: Final = 32
VISUAL_POINT_LIMIT: Final = 4_000 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 = ( FRAME_1880_ENGINEERING_ANCHORS: Final = (
{ {
"anchor_id": "near-concrete-hemisphere", "anchor_id": "near-concrete-hemisphere",
@@ -83,6 +83,22 @@ FRAME_1880_ENGINEERING_ANCHORS: Final = (
"must_assert_threat": False, "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): class ThreatReplayError(RuntimeError):
@@ -143,6 +159,7 @@ def build_threat_replay(
latencies_ms: list[float] = [] latencies_ms: list[float] = []
visual_count = 0 visual_count = 0
frame_1880_regression: dict[str, object] | None = None frame_1880_regression: dict[str, object] | None = None
frame_2584_regression: dict[str, object] | None = None
try: try:
temporal_frames_path = temporal.result_root / "frames.jsonl" temporal_frames_path = temporal.result_root / "frames.jsonl"
geometry_frames_path = geometry.result_root / "frames.jsonl" geometry_frames_path = geometry.result_root / "frames.jsonl"
@@ -188,13 +205,8 @@ def build_threat_replay(
"rolling retained obstacles", "rolling retained obstacles",
) )
) )
if any( if any(item.state is not TemporalState.RETAINED for item in rolling_retained):
item.state is not TemporalState.RETAINED raise ThreatReplayError("temporal replay rolling map escaped retained state")
for item in rolling_retained
):
raise ThreatReplayError(
"temporal replay rolling map escaped retained state"
)
geometry_observations = _array( geometry_observations = _array(
geometry_frame.get("observations"), "geometry observations" 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) latencies_ms.append((time.perf_counter_ns() - frame_started_ns) / 1_000_000)
by_id = {item.component_id: item for item in assessments} by_id = {item.component_id: item for item in assessments}
expected_ids = { expected_ids = {
item.component_id item.component_id for item in (*current, *rolling_retained, *unknown)
for item in (*current, *rolling_retained, *unknown) } | {item.proposal_id for item in camera_uncertainty}
} | {
item.proposal_id for item in camera_uncertainty
}
if set(by_id) != expected_ids: if set(by_id) != expected_ids:
raise ThreatReplayError("threat assessment coverage is incomplete") raise ThreatReplayError("threat assessment coverage is incomplete")
camera_rows = _camera_rows( camera_rows = _camera_rows(
@@ -248,9 +257,13 @@ def build_threat_replay(
if frame_count == 1880: if frame_count == 1880:
frame_1880_regression = _frame_1880_regression( frame_1880_regression = _frame_1880_regression(
metric_rows, metric_rows,
body_frame_resolver.body_frame_for_frame( body_frame_resolver.body_frame_for_frame(packet.envelope.frame_id),
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: for item in assessments:
assessment_counts[item.decision.value] += 1 assessment_counts[item.decision.value] += 1
@@ -327,6 +340,7 @@ def build_threat_replay(
fixtures=fixtures, fixtures=fixtures,
body_frame=body_frame_resolver.qualification_summary(), body_frame=body_frame_resolver.qualification_summary(),
frame_1880_regression=frame_1880_regression, frame_1880_regression=frame_1880_regression,
frame_2584_regression=frame_2584_regression,
) )
requirements = _requirements_v2(metrics, fixtures) requirements = _requirements_v2(metrics, fixtures)
accepted = all(value is True for value in requirements.values()) 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"]) fixtures = _read_json(paths["threat-deterministic-fixtures"])
accepted = all(value is True for value in requirements.values()) accepted = all(value is True for value in requirements.values())
expected_requirements = ( expected_requirements = (
_requirements_v2(metrics, fixtures) _requirements_v2(metrics, fixtures) if is_v2 else _requirements_v1(metrics, fixtures)
if is_v2
else _requirements_v1(metrics, fixtures)
) )
if ( if (
report.get("schema_version") report.get("schema_version")
@@ -687,8 +699,7 @@ def _visual_frame(
"point_cloud_sample_count": int(sampled.shape[0]), "point_cloud_sample_count": int(sampled.shape[0]),
"point_cloud_layer": "current-increment", "point_cloud_layer": "current-increment",
"rolling_map_component_count": sum( "rolling_map_component_count": sum(
row.get("state") == TemporalState.RETAINED.value row.get("state") == TemporalState.RETAINED.value for row in metric_rows
for row in metric_rows
), ),
"metric_obstacles": metric_visuals, "metric_obstacles": metric_visuals,
"camera_proposals": camera_rows, "camera_proposals": camera_rows,
@@ -778,22 +789,15 @@ def _frame_1880_regression(
"must_assert_threat": anchor["must_assert_threat"], "must_assert_threat": anchor["must_assert_threat"],
"matched": match is not None, "matched": match is not None,
"component_id": component_id, "component_id": component_id,
"centroid_body_xyz_m": ( "centroid_body_xyz_m": (None if match is None else list(match[1])),
None if match is None else list(match[1])
),
"decision": ( "decision": (
None None if anchor_assessment is None else anchor_assessment.get("decision")
if anchor_assessment is None
else anchor_assessment.get("decision")
), ),
} }
) )
required_threats_passed = all( required_threats_passed = all(
item["matched"] is True item["matched"] is True
and ( and (item["must_assert_threat"] is False or item["decision"] == ThreatDecision.THREAT.value)
item["must_assert_threat"] is False
or item["decision"] == ThreatDecision.THREAT.value
)
for item in anchors for item in anchors
) )
return { 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: class _FixtureBodyFrames:
def body_frame_for_frame(self, frame_id: str) -> ReplayBodyFrame: def body_frame_for_frame(self, frame_id: str) -> ReplayBodyFrame:
return ReplayBodyFrame( return ReplayBodyFrame(
@@ -982,11 +1080,7 @@ def _fixture_obstacle(
component_id=component_id, component_id=component_id,
identity_scope="ephemeral", identity_scope="ephemeral",
state=state, state=state,
ttl_ns=( ttl_ns=(3_000_000_000 if state is TemporalState.RETAINED else 750_000_000),
3_000_000_000
if state is TemporalState.RETAINED
else 750_000_000
),
last_hit_ns=last.evidence_time_ns, last_hit_ns=last.evidence_time_ns,
age_ns=0 if state is TemporalState.CURRENT else 100_000_000, age_ns=0 if state is TemporalState.CURRENT else 100_000_000,
association_basis="deterministic-fixture", association_basis="deterministic-fixture",
@@ -1096,6 +1190,7 @@ def _metrics(
fixtures: dict[str, object], fixtures: dict[str, object],
body_frame: dict[str, object], body_frame: dict[str, object],
frame_1880_regression: dict[str, object] | None, frame_1880_regression: dict[str, object] | None,
frame_2584_regression: dict[str, object] | None,
) -> dict[str, object]: ) -> dict[str, object]:
values = np.asarray(latencies_ms, dtype=np.float64) values = np.asarray(latencies_ms, dtype=np.float64)
return { return {
@@ -1116,6 +1211,7 @@ def _metrics(
"qualified_base_footprint_available": True, "qualified_base_footprint_available": True,
"geometry_regression_sequences": list(VISUAL_GEOMETRY_REGRESSION_SEQUENCES), "geometry_regression_sequences": list(VISUAL_GEOMETRY_REGRESSION_SEQUENCES),
"frame_1880_regression": frame_1880_regression, "frame_1880_regression": frame_1880_regression,
"frame_2584_regression": frame_2584_regression,
}, },
"fixtures": { "fixtures": {
"passed": fixtures["passed_count"], "passed": fixtures["passed_count"],
@@ -1156,8 +1252,7 @@ def _requirements_v1(
stale_cases = [ stale_cases = [
_object(item, "fixture") _object(item, "fixture")
for item in cases for item in cases
if isinstance(item, dict) if isinstance(item, dict) and item.get("name") in {"occluded-held", "stale-expired"}
and item.get("name") in {"occluded-held", "stale-expired"}
] ]
return { return {
"full_ravnoves00_replay_completed": ( "full_ravnoves00_replay_completed": (
@@ -1168,8 +1263,7 @@ def _requirements_v1(
), ),
"camera_only_is_unknown_never_safe": camera_case.get("actual") == "unknown", "camera_only_is_unknown_never_safe": camera_case.get("actual") == "unknown",
"held_and_stale_are_unknown_never_safe": ( "held_and_stale_are_unknown_never_safe": (
len(stale_cases) == 2 len(stale_cases) == 2 and all(item.get("actual") == "unknown" for item in stale_cases)
and all(item.get("actual") == "unknown" for item in stale_cases)
), ),
"geometry_only_evidence_is_assessed": ( "geometry_only_evidence_is_assessed": (
_integer( _integer(
@@ -1206,13 +1300,10 @@ def _requirements_v1(
== _integer(body_frame.get("qualified"), "qualified body frames") == _integer(body_frame.get("qualified"), "qualified body frames")
+ _integer(body_frame.get("rejected"), "rejected body frames") + _integer(body_frame.get("rejected"), "rejected body frames")
and _integer(body_frame.get("qualified"), "qualified body frames") and _integer(body_frame.get("qualified"), "qualified body frames")
>= math.ceil( >= math.ceil(_integer(body_frame.get("available"), "available body frames") * 0.95)
_integer(body_frame.get("available"), "available body frames") * 0.95
)
and body_frame.get("origin") == "local-surface-vertical-projection" and body_frame.get("origin") == "local-surface-vertical-projection"
and body_frame.get("up") == "vendor-slam-map-gravity-axis" and body_frame.get("up") == "vendor-slam-map-gravity-axis"
and body_frame.get("forward") and body_frame.get("forward") == "smoothed-slam-trajectory-validated-by-camera-axis"
== "smoothed-slam-trajectory-validated-by-camera-axis"
and _number_value( and _number_value(
_object( _object(
body_frame.get("camera_forward_alignment_deg"), body_frame.get("camera_forward_alignment_deg"),
@@ -1308,6 +1399,19 @@ def _requirements_v2(
).get("required_threats_passed") ).get("required_threats_passed")
is True 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_is_grounded_gravity_stable_and_route_aligned": (
body_frame.get("available") body_frame.get("available")
== _integer(body_frame.get("qualified"), "qualified body frames") == _integer(body_frame.get("qualified"), "qualified body frames")
+3
View File
@@ -5,6 +5,7 @@ from .active import (
ActiveSessionLeaseError, ActiveSessionLeaseError,
recover_stale_active_session_marker, recover_stale_active_session_marker,
) )
from .camera_frame import RecordedCameraFrame, RecordedCameraFrameService
from .lab_cache import publish_lab_replay_cache from .lab_cache import publish_lab_replay_cache
from .media import ( from .media import (
RECORDED_MEDIA_MANIFEST_SCHEMA, RECORDED_MEDIA_MANIFEST_SCHEMA,
@@ -65,6 +66,8 @@ __all__ = [
"publish_lab_replay_cache", "publish_lab_replay_cache",
"RecordingMaterializationCancelled", "RecordingMaterializationCancelled",
"RecordedMediaArtifact", "RecordedMediaArtifact",
"RecordedCameraFrame",
"RecordedCameraFrameService",
"RECORDED_MEDIA_MANIFEST_SCHEMA", "RECORDED_MEDIA_MANIFEST_SCHEMA",
"RecordedMediaFile", "RecordedMediaFile",
"RecordedMediaInspector", "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 ( from k1link.sessions import (
MaterializedRecording, MaterializedRecording,
RecordedCameraFrameService,
RecordedMediaInspector, RecordedMediaInspector,
RecordedMediaManifest, RecordedMediaManifest,
RecordingPreparationQueueFull, RecordingPreparationQueueFull,
@@ -191,6 +192,16 @@ session_recorded_media_inspector = RecordedMediaInspector(
) )
_ffmpeg = _resolve_media_tool("ffmpeg") _ffmpeg = _resolve_media_tool("ffmpeg")
_ffprobe = _resolve_media_tool("ffprobe") _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 = ( session_legacy_perception_overlay_store = (
RecordedPerceptionOverlayStore( RecordedPerceptionOverlayStore(
jobs_root=REPOSITORY_ROOT / ".runtime" / "compute-jobs", jobs_root=REPOSITORY_ROOT / ".runtime" / "compute-jobs",
@@ -586,9 +597,7 @@ app.include_router(
app.include_router( app.include_router(
build_advanced_laboratory_router( build_advanced_laboratory_router(
evidence_registry=LABORATORY_EVIDENCE_REGISTRY, evidence_registry=LABORATORY_EVIDENCE_REGISTRY,
evidence_runtime_root_provider=lambda: ( evidence_runtime_root_provider=lambda: REPOSITORY_ROOT / ".runtime" / "compute-experiments",
REPOSITORY_ROOT / ".runtime" / "compute-experiments"
),
e31_root_provider=lambda: ( e31_root_provider=lambda: (
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e31" / "source-qualifications" REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e31" / "source-qualifications"
), ),
@@ -756,22 +765,19 @@ app.include_router(
app.include_router( app.include_router(
build_m4_threat_replay_router( build_m4_threat_replay_router(
root_provider=lambda: ( root_provider=lambda: (
REPOSITORY_ROOT REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "m4" / "replay-threat"
/ ".runtime" ),
/ "compute-experiments" camera_frame_provider=(
/ "m4" session_recorded_camera_frame_service.extract
/ "replay-threat" if session_recorded_camera_frame_service is not None
else None
), ),
) )
) )
app.include_router( app.include_router(
build_e46e_ready_stack_router( build_e46e_ready_stack_router(
root_provider=lambda: ( root_provider=lambda: (
REPOSITORY_ROOT REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46e" / "ready-stack-results"
/ ".runtime"
/ "compute-experiments"
/ "e46e"
/ "ready-stack-results"
), ),
) )
) )
@@ -785,11 +791,7 @@ app.include_router(
/ "dashcam-bakeoff-results" / "dashcam-bakeoff-results"
), ),
e46e_root_provider=lambda: ( e46e_root_provider=lambda: (
REPOSITORY_ROOT REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46e" / "ready-stack-results"
/ ".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 pathlib import Path
from typing import Final from typing import Final
from fastapi import APIRouter, HTTPException, Query from fastapi import APIRouter, HTTPException, Query, Response
from k1link.perception.threat_replay import ( from k1link.perception.threat_replay import (
THREAT_REPLAY_FRAME_SCHEMA, THREAT_REPLAY_FRAME_SCHEMA,
@@ -22,20 +22,21 @@ from k1link.perception.threat_replay import (
ThreatReplayResult, ThreatReplayResult,
read_threat_replay_result, read_threat_replay_result,
) )
from k1link.sessions import RecordedCameraFrame, SessionIntegrityError
M4_THREAT_CATALOG_SCHEMA: Final = "missioncore.m4-threat-replay-catalog/v1" M4_THREAT_CATALOG_SCHEMA: Final = "missioncore.m4-threat-replay-catalog/v1"
M4_THREAT_VIEW_SCHEMA: Final = "missioncore.m4-threat-replay-view/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_VIDEO_SCHEMA: Final = "missioncore.m4-threat-video-overlay/v1"
M4_THREAT_VISUAL_CATALOG_SCHEMA: Final = ( M4_THREAT_VISUAL_CATALOG_SCHEMA: Final = "missioncore.m4-threat-visual-catalog/v1"
"missioncore.m4-threat-visual-catalog/v1"
)
_RESULT_ID = re.compile(rf"^{THREAT_REPLAY_RESULT_PREFIX}[a-f0-9]{{64}}$") _RESULT_ID = re.compile(rf"^{THREAT_REPLAY_RESULT_PREFIX}[a-f0-9]{{64}}$")
RootProvider = Callable[[], Path | None] RootProvider = Callable[[], Path | None]
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
def build_m4_threat_replay_router( def build_m4_threat_replay_router(
*, *,
root_provider: RootProvider = lambda: None, root_provider: RootProvider = lambda: None,
camera_frame_provider: CameraFrameProvider | None = None,
) -> APIRouter: ) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory/m4-threat", tags=["laboratory"]) 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]), **copy.deepcopy(frames[ordinal - 1]),
"result_id": result_id, "result_id": result_id,
"ordinal": ordinal, "ordinal": ordinal,
"camera_url": (
f"/api/v1/laboratory/m4-threat/results/{result_id}/visuals/{ordinal}/camera"
),
"ground_truth": False, "ground_truth": False,
"access": "read-only-replay-simulated", "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") @router.get("/results/{result_id}/video-overlay")
def get_video_overlay(result_id: str) -> dict[str, object]: def get_video_overlay(result_id: str) -> dict[str, object]:
frozen = result(result_id) frozen = result(result_id)
@@ -160,9 +203,7 @@ def _cached_video_overlay(
frames.append( frames.append(
{ {
"frame_index": expected_sequence, "frame_index": expected_sequence,
"session_seconds": _nonnegative_int( "session_seconds": _nonnegative_int(row.get("source_time_ns"), "source time")
row.get("source_time_ns"), "source time"
)
/ 1_000_000_000, / 1_000_000_000,
"source_available": row["source_available"], "source_available": row["source_available"],
"camera_proposals": copy.deepcopy(row["camera_proposals"]), "camera_proposals": copy.deepcopy(row["camera_proposals"]),
@@ -209,9 +250,7 @@ def _project_result(result: ThreatReplayResult) -> dict[str, object]:
}, },
"metrics": copy.deepcopy(result.metrics), "metrics": copy.deepcopy(result.metrics),
"configuration": copy.deepcopy(result.report["configuration"]), "configuration": copy.deepcopy(result.report["configuration"]),
"acceptance_requirements": copy.deepcopy( "acceptance_requirements": copy.deepcopy(result.report["acceptance_requirements"]),
result.report["acceptance_requirements"]
),
"limitations": copy.deepcopy(result.report["limitations"]), "limitations": copy.deepcopy(result.report["limitations"]),
"accepted": result.accepted, "accepted": result.accepted,
"ground_truth": False, "ground_truth": False,
@@ -271,9 +310,7 @@ def _candidates(provider: RootProvider) -> list[Path]:
( (
item item
for item in root.iterdir() for item in root.iterdir()
if item.is_dir() if item.is_dir() and not item.is_symlink() and _RESULT_ID.fullmatch(item.name)
and not item.is_symlink()
and _RESULT_ID.fullmatch(item.name)
), ),
key=lambda item: item.stat().st_mtime_ns, key=lambda item: item.stat().st_mtime_ns,
reverse=True, reverse=True,
+7 -12
View File
@@ -8,7 +8,7 @@ REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
RESULT_ROOT = ( RESULT_ROOT = (
REPOSITORY_ROOT REPOSITORY_ROOT
/ ".runtime/perception-m4/geometry-results" / ".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["with_range"] == 5341
assert proposals["eligible_for_range"] == 13298 assert proposals["eligible_for_range"] == 13298
assert proposals["ownership_collision"] == 146 assert proposals["ownership_collision"] == 146
assert result.metrics["geometry_only_observations"] == 21958 assert result.metrics["geometry_only_observations"] == 22740
assert result.metrics["published_source_point_rows"] == 2164767 assert result.metrics["published_source_point_rows"] == 2173778
def test_geometry_result_binds_the_accepted_m4_3_e32_and_e53_evidence() -> None: 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 isinstance(identity, dict)
assert identity["detector_result_id"] == ( assert identity["detector_result_id"] == (
"m4-detector-replay-" "m4-detector-replay-11f83f2e0b81758ac2a5a5fc54e9d293b501678df5f6ef97b5c6069ba08605c5"
"11f83f2e0b81758ac2a5a5fc54e9d293b501678df5f6ef97b5c6069ba08605c5"
) )
assert identity["geometry_profile_sha256"] == ( assert identity["geometry_profile_sha256"] == (
"420d989aab5918e0f98e3439cadb8b7251332d51b48f4f2c25d77a385bea49f8" "cc666c9389a5e221957faddec89584709b66918d14abaf646f1832e001421999"
) )
assert identity["historical_references"] == { assert identity["historical_references"] == {
"e32": { "e32": {
"manifest_sha256": ( "manifest_sha256": ("f4b57c9f7619c43414adf1d10488b05df466226a523a0c001671102ced0e9ab8"),
"f4b57c9f7619c43414adf1d10488b05df466226a523a0c001671102ced0e9ab8"
),
"result_id": ( "result_id": (
"e32-track-geometry-" "e32-track-geometry-"
"a14ca0e7fb3850ca0dfa3c41634e1b490a2d58ab74d101afc6d6921fbdb0e6fd" "a14ca0e7fb3850ca0dfa3c41634e1b490a2d58ab74d101afc6d6921fbdb0e6fd"
), ),
}, },
"e53": { "e53": {
"manifest_sha256": ( "manifest_sha256": ("fc5b4ae69aae0098b7075c539d25b563dff1bbb09209a49030edda6cba9ee544"),
"fc5b4ae69aae0098b7075c539d25b563dff1bbb09209a49030edda6cba9ee544"
),
"result_id": ( "result_id": (
"e53-camera-first-shadow-" "e53-camera-first-shadow-"
"e6f03cf8bfb15db86100239b060e13f914532618b7b99e811866e4e6a555186c" "e6f03cf8bfb15db86100239b060e13f914532618b7b99e811866e4e6a555186c"
+58 -49
View File
@@ -5,15 +5,19 @@ from pathlib import Path
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from k1link.perception.threat_replay import read_threat_replay_result 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 from k1link.web.m4_threat_replay_api import build_m4_threat_replay_router
REPOSITORY_ROOT = Path(__file__).resolve().parents[1] 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" RESULTS_ROOT = REPOSITORY_ROOT / ".runtime/compute-experiments/m4/replay-threat"
def _endpoint(path: str): def _endpoint(path: str, *, camera_frame_provider=None):
router = build_m4_threat_replay_router(root_provider=lambda: RESULTS_ROOT) router = build_m4_threat_replay_router(
root_provider=lambda: RESULTS_ROOT,
camera_frame_provider=camera_frame_provider,
)
return next( return next(
route.endpoint route.endpoint
for route in router.routes 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["frames"] == {"total": 4489, "failed": 0}
assert result.metrics["evidence"] == { assert result.metrics["evidence"] == {
"camera-only": 10158, "camera-only": 10158,
"current-metric": 27299, "current-metric": 28081,
"rolling-map-retained": 69855, "rolling-map-retained": 70989,
"stale-or-held": 37995, "stale-or-held": 38025,
} }
assert result.metrics["decisions"] == { assert result.metrics["decisions"] == {
"not-threat": 10700, "not-threat": 10769,
"threat": 6626, "threat": 7606,
"unknown": 127981, "unknown": 128878,
} }
assert result.metrics["fixtures"] == { assert result.metrics["fixtures"] == {
"critical": 5, "critical": 5,
@@ -51,10 +55,10 @@ def test_threat_result_is_content_bound_and_visual_evidence_is_complete() -> Non
assert isinstance(identity, dict) assert isinstance(identity, dict)
assert identity["frames_sha256"] == ( assert identity["frames_sha256"] == (
"57219acd7dfe1cf04b12d4a415d1819e0a4947bc3173333bb56eea8fd2bd47b9" "b57be1839f5915e3b80b54355b694e0bd8c9ac318d0cbe6de2bff713082cfa4e"
) )
assert identity["visuals_sha256"] == ( assert identity["visuals_sha256"] == (
"bda54e144a1b4c878ba645a8dafedcdc0332e16a1489b62b7100670e8eae6f25" "14b9679b1df4d50aaceeb3e018e46398a92b47727aa83339179395db4dfcd005"
) )
visual = result.metrics["visual_evidence"] visual = result.metrics["visual_evidence"]
assert isinstance(visual, dict) 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", "qualified_base_footprint_available",
) )
) )
assert visual["geometry_regression_sequences"] == [138, 274, 1880] assert visual["geometry_regression_sequences"] == [138, 274, 1880, 2584]
assert visual["frame_1880_regression"] == { frame_1880 = visual["frame_1880_regression"]
"camera_visible_hemispheres_independent_truth": False, assert frame_1880["matched_anchor_count"] == 2
"engineering_anchors": [ assert frame_1880["required_threats_passed"] is True
{ assert frame_1880["camera_visible_hemispheres_independent_truth"] is False
"anchor_id": "near-concrete-hemisphere", frame_2584 = visual["frame_2584_regression"]
"bounds_body_xyz_m": [[0.3, 1.2], [-0.8, 0.2], [-0.1, 0.9]], assert frame_2584["matched_anchor_count"] == 2
"centroid_body_xyz_m": [ assert frame_2584["required_threats_passed"] is True
0.7594228459267565, assert frame_2584["matching_basis"] == (
-0.2681278641873485, "produced-occupied-cell-inside-camera-reviewed-body-window"
0.3753253937774115, )
], near, far = frame_2584["engineering_anchors"]
"component_id": "rolling-5329b5d2ef498e6250c931ff", assert near["anchor_id"] == "near-compact-concrete-hemisphere"
"decision": "threat", assert near["component_state"] == "retained"
"matched": True, assert near["decision"] == "threat"
"must_assert_threat": True, assert far["anchor_id"] == "far-concrete-hemisphere-occupancy"
}, assert far["matched"] is 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,
}
body_frame = result.metrics["body_frame"] body_frame = result.metrics["body_frame"]
assert body_frame["qualified"] == 3861 assert body_frame["qualified"] == 3861
assert body_frame["rejected"] == 67 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["schema_version"] == "missioncore.perception-threat-visual-frame/v2"
assert frame["point_cloud_layer"] == "current-increment" assert frame["point_cloud_layer"] == "current-increment"
assert frame["point_cloud_sample_count"] > 0 assert frame["point_cloud_sample_count"] > 0
assert frame["camera_url"].endswith(f"/{RESULT_ID}/visuals/1/camera")
assert frame["rig"] == { assert frame["rig"] == {
"length_m": 1.0, "length_m": 1.0,
"nominal_sensor_height_m": 1.25, "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"][0]["frame_index"] == 0
assert overlay["frames"][-1]["frame_index"] == 4488 assert overlay["frames"][-1]["frame_index"] == 4488
assert overlay["authority"] == "replay-simulated" 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 = ( ROLLING_RESULT_ROOT = (
REPOSITORY_ROOT REPOSITORY_ROOT
/ ".runtime/perception-m4/temporal-results" / ".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"] identity = result.manifest["identity"]
assert isinstance(identity, dict) assert isinstance(identity, dict)
assert identity["geometry_result_id"] == ( assert identity["geometry_result_id"] == (
"m4-geometry-replay-" "m4-geometry-replay-8daf3109e3cf30b960b4b376032ff3b5ec58ca42a1e5899b841cf29fbcf14ad8"
"8daf3109e3cf30b960b4b376032ff3b5ec58ca42a1e5899b841cf29fbcf14ad8"
) )
assert identity["frames_sha256"] == ( assert identity["frames_sha256"] == (
"1bf1365bdb3f20214443d3f8b87a0fa88f9848af8ca0456b7ca364d37631c3fc" "1bf1365bdb3f20214443d3f8b87a0fa88f9848af8ca0456b7ca364d37631c3fc"
@@ -97,18 +96,18 @@ def test_rolling_temporal_result_separates_increment_from_retained_map() -> None
assert result.metrics["rolling_map"] == { assert result.metrics["rolling_map"] == {
"active_cells_at_end": 587, "active_cells_at_end": 587,
"capacity_evicted_cells": 0, "capacity_evicted_cells": 0,
"current_increment_cells": 848868, "current_increment_cells": 849650,
"input_frames": 4489, "input_frames": 4489,
"local_radius_m": 12.0, "local_radius_m": 12.0,
"maximum_cells": 65536, "maximum_cells": 65536,
"maximum_retained_age_ns": 3000000000, "maximum_retained_age_ns": 3000000000,
"peak_active_cells": 907, "peak_active_cells": 907,
"peak_retained_components": 35, "peak_retained_components": 35,
"radius_evicted_cells": 6512, "radius_evicted_cells": 6513,
"retained_cell_publications": 1776145, "retained_cell_publications": 1779135,
"retained_component_publications": 69855, "retained_component_publications": 70989,
"retention_ns": 3000000000, "retention_ns": 3000000000,
"time_evicted_cells": 29620, "time_evicted_cells": 29720,
"voxel_size_m": 0.45, "voxel_size_m": 0.45,
} }
frames = result.result_root / "frames.jsonl" 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 else: # pragma: no cover - immutable artifact guarantees this branch is unreachable
raise AssertionError("frame 1880 missing") raise AssertionError("frame 1880 missing")
assert frame["schema_version"] == "missioncore.perception-temporal-replay-frame/v2" 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"]) assert all(item["state"] == "retained" for item in frame["rolling_retained"])