fix(m4): retain compact obstacle evidence
This commit is contained in:
@@ -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/);
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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"
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
Reference in New Issue
Block a user