feat(lab): present chronological perception evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 07:49:49 +03:00
parent 12c8a2d74c
commit 7a72a206f1
102 changed files with 20122 additions and 222 deletions
@@ -23,6 +23,12 @@ export interface WorkspaceNavigation {
activateAutomaticSpatialSource: () => void;
}
export interface LaboratoryAnnotationAction {
label: string;
disabled?: boolean;
onClick: () => void;
}
export interface WorkspaceRendererProps {
definition: WorkspaceDefinition;
state: MissionRuntimeState | null;
@@ -56,4 +62,7 @@ export interface WorkspaceRendererProps {
disabled: boolean;
blockedReason: string | null;
};
onLaboratoryAnnotationActionChange: (
action: LaboratoryAnnotationAction | null,
) => void;
}
@@ -1,9 +1,7 @@
import type { ComponentType } from "react";
import type { LaboratoryOption } from "../../components/laboratory/LaboratoryPresentation";
import {
isAdvancedLaboratoryWorkId,
type AdvancedLaboratoryIndexItem,
type AdvancedLaboratoryWorkId,
} from "../../core/laboratory/advancedIndex";
import type { AdvancedLaboratoryResults } from "../../core/laboratory/advancedResults";
@@ -20,7 +18,27 @@ import { E39Result } from "./E39Result";
import { E40Result } from "./E40Result";
import { L3PointPillarsResult } from "./L3PointPillarsResult";
import { L31PointPillarsRavnovesResult } from "./L31PointPillarsRavnovesResult";
import { L32PointPillarsCameraReviewResult } from "./L32PointPillarsCameraReviewResult";
import { L33CameraFirstDetectorReviewResult } from "./L33CameraFirstDetectorReviewResult";
import { L34RightYoloxTruthIslandResultView } from "./L34RightYoloxTruthIslandResult";
import { L34AAssistedYoloxErrorResultView } from "./L34AAssistedYoloxErrorResult";
import { L34BNestedBoxConsolidationResultView } from "./L34BNestedBoxConsolidationResult";
import { L34CTileSeamStitchResultView } from "./L34CTileSeamStitchResult";
import { L34DCumulativePostprocessingResultView } from "./L34DCumulativePostprocessingResult";
import { L34ESelfReviewDiagnosticResultView } from "./L34ESelfReviewDiagnosticResult";
import { L34FAdjudicatedReferenceResultView } from "./L34FAdjudicatedReferenceResult";
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
import { E46BlindReviewResultView } from "./E46BlindReviewResult";
import { E46AAiEngineeringPreannotationResultView } from "./E46AAiEngineeringPreannotationResult";
import { E46BTemporalMotionResultView } from "./E46BTemporalMotionResult";
import { E46CFullReplayWorldTracksResultView } from "./E46CFullReplayWorldTracksResult";
import { E46DTemporalFailureAuditResultView } from "./E46DTemporalFailureAuditResult";
import { E46EReadyStackResultView } from "./E46EReadyStackResult";
import { E46FDashCamBakeoffResultView } from "./E46FDashCamBakeoffResult";
import { E46GRectifiedDetectorBakeoffResultView } from "./E46GRectifiedDetectorBakeoffResult";
import { E46HFullRectifiedFrontReplayResultView } from "./E46HFullRectifiedFrontReplayResult";
import { E46IGroundingDinoFullReplayResultView } from "./E46IGroundingDinoFullReplayResult";
import { E46JRawFisheyeRealtimeResultView } from "./E46JRawFisheyeRealtimeResult";
export { isAdvancedLaboratoryWorkId };
export type { AdvancedLaboratoryWorkId };
@@ -29,35 +47,6 @@ type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
SpatialView: ComponentType<WorkspaceRendererProps>;
};
export function advancedLaboratoryWorkOptions(
index: readonly AdvancedLaboratoryIndexItem[],
): readonly LaboratoryOption<AdvancedLaboratoryWorkId>[] {
const available = new Set(index.map(({ workId }) => workId));
const options: readonly LaboratoryOption<AdvancedLaboratoryWorkId>[] = [
{
id: "l31-pointpillars-ravnoves",
label: "L3.1 · PointPillars на RAVNOVES00",
},
{
id: "l3-pointpillars-visual-audit",
label: "L3 · KITTI · внешний PointPillars benchmark",
},
{ id: "e31-source-binding", label: "LAB E31 · source binding" },
{ id: "e32-track-geometry", label: "LAB E32 · TrackGeometry v1" },
{ id: "e33-worker-shadow", label: "LAB E33 · worker shadow 1×" },
{ id: "e34-temporal-layer", label: "LAB E34 · temporal occupied/unknown" },
{ id: "e35-degradation-recovery", label: "LAB E35 · degradation recovery" },
{ id: "e37-ravnoves-acceptance", label: "LAB E37 · RAVNOVES00 acceptance R0" },
{ id: "e38-perception-baseline", label: "LAB E38 · perception quality R1" },
{ id: "e39-perception-refinement", label: "LAB E39 · perception refinement R1" },
{
id: "e40-perception-product-gate",
label: "LAB E40 · historical visible engineering evaluation",
},
];
return options.filter(({ id }) => available.has(id));
}
export function advancedLaboratorySourceSession(
workId: AdvancedLaboratoryWorkId,
results: AdvancedLaboratoryResults,
@@ -98,9 +87,69 @@ export function AdvancedLaboratoryResult({
if (workId === "l31-pointpillars-ravnoves" && results.l31) {
return <L31PointPillarsRavnovesResult result={results.l31} />;
}
if (workId === "l32-pointpillars-camera-review" && results.l32) {
return <L32PointPillarsCameraReviewResult result={results.l32} />;
}
if (workId === "l33-camera-first-detector-review" && results.l33) {
return <L33CameraFirstDetectorReviewResult result={results.l33} />;
}
if (workId === "e40-perception-product-gate" && results.e40) {
return <E40Result rigLabel={rigLabel} result={results.e40} />;
}
if (workId === "e46-detector-truth-island" && results.e46) {
return <E46BlindReviewResultView rigLabel={rigLabel} result={results.e46} />;
}
if (workId === "e46a-ai-engineering-preannotation" && results.e46a) {
return <E46AAiEngineeringPreannotationResultView rigLabel={rigLabel} result={results.e46a} />;
}
if (workId === "e46b-temporal-motion" && results.e46b) {
return <E46BTemporalMotionResultView rigLabel={rigLabel} result={results.e46b} />;
}
if (workId === "e46c-full-replay-world-tracks" && results.e46c) {
return <E46CFullReplayWorldTracksResultView rigLabel={rigLabel} result={results.e46c} />;
}
if (workId === "e46d-temporal-failure-audit" && results.e46d) {
return <E46DTemporalFailureAuditResultView rigLabel={rigLabel} result={results.e46d} />;
}
if (workId === "e46e-ready-stack" && results.e46e) {
return <E46EReadyStackResultView rigLabel={rigLabel} result={results.e46e} />;
}
if (workId === "e46f-dashcam-bakeoff" && results.e46f) {
return <E46FDashCamBakeoffResultView rigLabel={rigLabel} result={results.e46f} />;
}
if (workId === "e46g-rectified-detector-bakeoff" && results.e46g) {
return <E46GRectifiedDetectorBakeoffResultView rigLabel={rigLabel} result={results.e46g} />;
}
if (workId === "e46h-full-rectified-front-replay" && results.e46h) {
return <E46HFullRectifiedFrontReplayResultView rigLabel={rigLabel} result={results.e46h} />;
}
if (workId === "e46i-grounding-dino-full-replay" && results.e46i) {
return <E46IGroundingDinoFullReplayResultView rigLabel={rigLabel} result={results.e46i} />;
}
if (workId === "e46j-raw-fisheye-realtime" && results.e46j) {
return <E46JRawFisheyeRealtimeResultView rigLabel={rigLabel} result={results.e46j} />;
}
if (workId === "l34-right-yolox-truth-island-freeze" && results.l34) {
return <L34RightYoloxTruthIslandResultView rigLabel={rigLabel} result={results.l34} />;
}
if (workId === "l34a-assisted-yolox-error-audit" && results.l34a) {
return <L34AAssistedYoloxErrorResultView rigLabel={rigLabel} result={results.l34a} />;
}
if (workId === "l34b-nested-box-consolidation-shadow" && results.l34b) {
return <L34BNestedBoxConsolidationResultView rigLabel={rigLabel} result={results.l34b} />;
}
if (workId === "l34c-tile-seam-stitch-shadow" && results.l34c) {
return <L34CTileSeamStitchResultView rigLabel={rigLabel} result={results.l34c} />;
}
if (workId === "l34d-cumulative-postprocessing-candidate" && results.l34d) {
return <L34DCumulativePostprocessingResultView rigLabel={rigLabel} result={results.l34d} />;
}
if (workId === "l34e-self-review-diagnostic" && results.l34e) {
return <L34ESelfReviewDiagnosticResultView rigLabel={rigLabel} result={results.l34e} />;
}
if (workId === "l34f-adjudicated-reference" && results.l34f) {
return <L34FAdjudicatedReferenceResultView rigLabel={rigLabel} result={results.l34f} />;
}
if (workId === "e39-perception-refinement" && results.e39) {
return <E39Result rigLabel={rigLabel} result={results.e39} />;
}
@@ -0,0 +1,75 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { E46AAiEngineeringPreannotationResult } from "../../core/laboratory/e46aAiEngineeringPreannotation";
import { formatNumber } from "../../presentation";
import { E46AAiEngineeringPreannotationVisual } from "./E46AAiEngineeringPreannotationVisual";
export function E46AAiEngineeringPreannotationResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: E46AAiEngineeringPreannotationResult;
}) {
const counts = result.metrics.classCounts;
return (
<LaboratoryWorkTemplate
summary={<LaboratorySummary
title="LAB E46A · object-level preannotation QA"
description="Все 32 exact E46 RIGHT-camera кадра повторно проверены на уровне отдельных объектов. Ложные рамки удалены, а грубая геометрия привязана к замороженному детекторному кандидату и затем просмотрена на исходных кадрах."
status={`32/32 object-level QA · ${result.metrics.deletedFalseBoxCount} false boxes · not truth`}
statusTone="warning"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · RIGHT camera · recorded replay` },
{ label: "Разметка", value: `${result.metrics.frameCount}/32 кадров · ${formatNumber(result.metrics.objectCount, 0)} объектов` },
{ label: "QA", value: `${formatNumber(result.metrics.geometrySnappedObjectCount, 0)} geometry snaps · ${formatNumber(result.metrics.deletedFalseBoxCount, 0)} удалено · ${formatNumber(result.metrics.categoryCorrectedObjectCount, 0)} класс исправлен` },
{ label: "Полномочия", value: "Reviewer A/B не затронуты · E48 closed · L3.5 closed" },
]}
brief={{
question: "Можно ли убрать очевидную дрочню из ручной разметки и оставить человеку только спорные correction/approval?",
approach: "Исходные 260 candidate-visible рамок сопоставлены с замороженной Mask R-CNN геометрией на тех же hash-bound кадрах. После NMS и one-to-one snap все 32 кадра просмотрены целиком; ложные фоновые рамки удалены, custom-классы сохранены.",
principalResult: `${formatNumber(result.metrics.sourceObjectCount, 0)} исходных рамок → ${formatNumber(result.metrics.objectCount, 0)} после QA: ${formatNumber(counts.car ?? 0, 0)} авто, ${formatNumber(counts.heavy_vehicle ?? 0, 0)} тяжёлых ТС, ${formatNumber(counts.person ?? 0, 0)} людей, ${formatNumber(counts.stroller ?? 0, 0)} коляски, ${formatNumber(counts.laptop ?? 0, 0)} ноутбука и ${formatNumber(counts.bicycle ?? 0, 0)} велосипед.`,
limitation: "Наличие объекта и финальное решение всё ещё candidate-visible и AI-assisted. Это инженерная предразметка, не independent truth и не основание считать AP/precision/recall.",
}}
method={{
completeness: "complete",
executionClass: "hybrid",
pipelineId: "e46a-object-level-preannotation-qa/v2",
components: [
{ kind: "source", name: result.sourceE46ResultId, version: "E46 immutable 32-frame source", role: "hash-bound sensor.camera.right frames", identitySha256: result.sourceE46ResultId.split("-").at(-1) ?? null },
{ kind: "model", name: "Mask R-CNN geometry candidate", version: "valid-fov-fill frozen", role: "geometry-only snap; no truth authority", identitySha256: null },
{ kind: "tool", name: "AI object-level visual audit", version: "all-32/v2", role: "remove false boxes, verify one instance per frame", identitySha256: null },
],
}}
/>}
evidence={<LaboratoryEvidence
eyebrow="E46A VISUAL EVIDENCE · PREANNOTATION / SOURCE"
title="Все 32 кадра открываются с рамками и без них"
kind="diagnostic-model"
resizable
>
<E46AAiEngineeringPreannotationVisual resultId={result.resultId} />
</LaboratoryEvidence>}
result={<LaboratoryResultSummary
title="Очевидные ошибки предразметки сняты; ручная работа оставлена только для спорных случаев"
status="Object-level QA complete · independent gate unchanged"
statusTone="warning"
metrics={[
{ label: "Reviewed frames", value: "32/32", hint: "Exact E46 source" },
{ label: "Geometry snapped", value: formatNumber(result.metrics.geometrySnappedObjectCount, 0), hint: "Frozen candidate · visually checked" },
{ label: "False boxes removed", value: formatNumber(result.metrics.deletedFalseBoxCount, 0), hint: `${formatNumber(result.metrics.sourceObjectCount, 0)}${formatNumber(result.metrics.objectCount, 0)} objects` },
{ label: "Independent truth", value: "0/2", hint: "E46 Reviewer A/B untouched" },
]}
conclusion={{
proved: `На всех 32 кадрах выполнен object-level engineering QA: ${formatNumber(result.metrics.deletedFalseBoxCount, 0)} фоновых/дублирующих рамок удалены, ${formatNumber(result.metrics.geometrySnappedObjectCount, 0)} рамок геометрически уточнены, один SUV исправлен из heavy_vehicle в car.`,
notProved: "Не доказаны независимость, correctness неоднозначных дальних объектов, detector AP/precision/recall, движение/статика, live/hardware или safety-пригодность. Количество рамок не является метрикой качества модели.",
decision: "Использовать обновлённый E46A как assisted correction/approval. Следующий архитектурный шаг — temporal motion-state для размеченных объектов; независимый benchmark остаётся отдельным Reviewer A/B → adjudication → E48 seal.",
}}
/>}
/>
);
}
@@ -0,0 +1,120 @@
import { useEffect, useRef, useState } from "react";
import type { E46ACase } from "../../core/laboratory/e46aAiEngineeringPreannotation";
export type E46ASceneMode = "preannotation" | "source";
function color(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
alpha = 1,
): string {
const value = getComputedStyle(host).getPropertyValue(token).trim();
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
function objectColor(host: HTMLElement, category: string): string {
if (category === "person" || category === "stroller" || category === "laptop") {
return color(host, "--nodedc-accent-rgb", [232, 56, 126]);
}
if (category === "heavy_vehicle") {
return color(host, "--nodedc-warning-rgb", [255, 197, 92]);
}
if (category === "bicycle" || category === "motorcycle") {
return color(host, "--nodedc-success-rgb", [181, 255, 90]);
}
return color(host, "--nodedc-foreground-rgb", [245, 245, 245]);
}
export function E46AAiEngineeringPreannotationScene({
frame,
mode,
}: {
frame: E46ACase;
mode: E46ASceneMode;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [image, setImage] = useState<HTMLImageElement | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
const next = new Image();
next.decoding = "async";
next.onload = () => { setImage(next); setFailed(false); };
next.onerror = () => { setImage(null); setFailed(true); };
next.src = frame.cameraUrl;
return () => { next.onload = null; next.onerror = null; };
}, [frame.cameraUrl]);
useEffect(() => {
const host = hostRef.current;
const canvas = canvasRef.current;
if (!host || !canvas || !image) 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.fillStyle = color(host, "--nodedc-canvas-rgb", [5, 5, 6]);
context.fillRect(0, 0, width, height);
const scale = Math.min(width / 800, height / 600);
const drawWidth = 800 * scale;
const drawHeight = 600 * scale;
const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
context.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
if (mode === "source") return;
frame.objects.forEach((item) => {
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 = objectColor(host, item.category);
context.strokeStyle = stroke;
context.lineWidth = Math.max(1.5, 2 * scale);
context.strokeRect(x, y, boxWidth, boxHeight);
const label = item.displayCategory;
const fontSize = Math.max(9, 10 * scale);
context.font = `600 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = context.measureText(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 = color(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.9);
context.fillRect(labelX, labelY, labelWidth, labelHeight);
context.fillStyle = stroke;
context.fillText(label, labelX + 4, labelY + fontSize + 1);
});
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [frame, image, mode]);
return (
<div className="l32-camera-scene" ref={hostRef}>
<canvas
ref={canvasRef}
role="img"
aria-label={`E46A frame ${frame.frameIndex}: ${mode}, ${frame.objectCount} preannotations`}
/>
{failed ? (
<div className="l3-visual-audit__state" role="status">
Точный source-кадр E46A недоступен.
</div>
) : null}
</div>
);
}
@@ -0,0 +1,120 @@
import { useEffect, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchE46ACase,
type E46ACase,
} from "../../core/laboratory/e46aAiEngineeringPreannotation";
import {
E46AAiEngineeringPreannotationScene,
type E46ASceneMode,
} from "./E46AAiEngineeringPreannotationScene";
const SEQUENCES = Array.from({ length: 32 }, (_, index) => index + 1);
export function E46AAiEngineeringPreannotationVisual({
resultId,
}: {
resultId: string;
}) {
const [selectedSequence, setSelectedSequence] = useState(1);
const [frame, setFrame] = useState<E46ACase | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<E46ASceneMode>("preannotation");
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
setFrame(null);
void fetchE46ACase(resultId, selectedSequence, { signal: controller.signal })
.then((next) => { if (!controller.signal.aborted) setFrame(next); })
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(caught instanceof Error ? caught.message : "E46A frame недоступен.");
}
})
.finally(() => { if (!controller.signal.aborted) setLoading(false); });
return () => controller.abort();
}, [resultId, selectedSequence]);
const navigate = (offset: -1 | 1) => {
setSelectedSequence((current) => ((current - 1 + offset + 32) % 32) + 1);
};
const actions = (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущий кадр E46A" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующий кадр E46A" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать кадр E46A"
value={String(selectedSequence)}
options={SEQUENCES.map((sequence) => ({
value: String(sequence),
label: frame?.truthIslandSequence === sequence
? `${sequence}/32 · frame ${frame.frameIndex} · ${frame.groupId}`
: `${sequence}/32 · E46 source frame`,
}))}
variant="split"
menuWidth="anchor"
searchable
searchPlaceholder="Найти номер кадра"
onChange={(value) => setSelectedSequence(Number(value))}
/>
</div>
);
const overlay = frame ? (
<div className="l3-visual-audit__overlay">
<div>
<span>RAVNOVES00 · sensor.camera.right</span>
<strong>{frame.sessionSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с · frame {frame.frameIndex}</strong>
<small>Sequence {frame.truthIslandSequence}/32 · {frame.groupId}</small>
</div>
<div>
<span>E46A · AI engineering preannotation</span>
<strong>{frame.objectCount} объектов · визуально проверено</strong>
<small>Not independent · not truth · image {frame.sourceImageSha256.slice(0, 12)}</small>
</div>
</div>
) : undefined;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="E46A AI engineering preannotation and exact source"
mode={mode}
modes={[
{ value: "preannotation", label: "PREANNOTATION" },
{ value: "source", label: "SOURCE" },
]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
{loading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем hash-bound E46A preannotation</span>
</div>
) : error || !frame ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{error ?? "E46A frame недоступен."}</span>
</div>
) : (
<E46AAiEngineeringPreannotationScene frame={frame} mode={mode} />
)}
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,12 @@
import { LaboratoryEvidence, LaboratoryResultSummary, LaboratorySummary, LaboratoryWorkTemplate } from "../../components/laboratory/LaboratoryPresentation";
import type { E46BTemporalMotionResult } from "../../core/laboratory/e46bTemporalMotion";
import { E46BTemporalMotionVisual } from "./E46BTemporalMotionVisual";
export function E46BTemporalMotionResultView({ rigLabel, result }: { rigLabel: string; result: E46BTemporalMotionResult }) {
const metrics = result.metrics;
return <LaboratoryWorkTemplate
summary={<LaboratorySummary title="LAB E46B · RIGHT temporal tracks + motion-state" description="Четыре записанных temporal-клипа E46A связаны в устойчивые source-scoped ID. Состояние движения публикуется только при решающем E26 ego-motion/LiDAR evidence; остальные объекты честно остаются unknown." status={`16/16 visual evidence · ${metrics.trackCount} tracks · recorded RIGHT`} statusTone="warning" facts={[{ label: "Конфигурация", value: `${rigLabel} · RIGHT camera · recorded replay` }, { label: "Temporal", value: `${metrics.temporalGroupCount} клипа · ${metrics.frameCount} кадров · ${metrics.objectObservationCount} наблюдений` }, { label: "Motion", value: `${metrics.dynamicTrackCount} dynamic · ${metrics.staticTrackCount} static · ${metrics.unknownTrackCount} unknown` }, { label: "Полномочия", value: "Engineering evidence · no metric velocity · no safety" }]} brief={{ question: "Можно ли превратить отдельные рамки в стабильные объекты и различить движение/статику на записи?", approach: "Внутри каждого фиксированного четырёхкадрового клипа объекты связаны по классу и пространственному порядку. Каждая рамка отдельно сопоставлена с hash-bound E26 KB4 ego-motion/LiDAR fusion на том же source frame; конфликт или отсутствие evidence даёт unknown.", principalResult: `${metrics.trackCount} устойчивых source-scoped ID: ${metrics.dynamicTrackCount} движется, ${metrics.staticTrackCount} стоит, ${metrics.unknownTrackCount} остаются неизвестными. Визуал показывает рамку, ID и накопленный след на всех 16 кадрах.`, limitation: "ID действуют только внутри четырёх коротких клипов. Это не route-global tracking, не метрическая скорость, не truth и не acceptance детектора." }} method={{ completeness: "complete", executionClass: "hybrid", pipelineId: "e46b-source-scoped-temporal-motion/v1", components: [{ kind: "source", name: result.sourceE46AResultId, version: "E46A immutable", role: "object geometry on exact RIGHT frames", identitySha256: result.sourceE46AResultId.split("-").at(-1) ?? null }, { kind: "algorithm", name: "Source-scoped temporal association", version: "category-x-rank/v1", role: "stable IDs within four-frame clips", identitySha256: null }, { kind: "algorithm", name: result.sourceE26ResultId, version: "KB4 ego-motion + LiDAR fusion", role: "conservative dynamic/static/unknown evidence", identitySha256: result.sourceE26ResultId.split("-").at(-1) ?? null }] }} />}
evidence={<LaboratoryEvidence eyebrow="E46B VISUAL EVIDENCE · TRACKS / SOURCE" title="Устойчивый ID, след и motion-state на каждом temporal-кадре" kind="diagnostic-model" resizable><E46BTemporalMotionVisual resultId={result.resultId} /></LaboratoryEvidence>}
result={<LaboratoryResultSummary title="Объектный слой получил temporal identity и консервативный motion-state" status="Source-scoped temporal layer complete · route-global tracking next" statusTone="warning" metrics={[{ label: "Temporal frames", value: "16/16", hint: "4 fixed clips" }, { label: "Stable IDs", value: String(metrics.trackCount), hint: `${metrics.objectObservationCount} observations` }, { label: "Decisive motion", value: String(metrics.dynamicTrackCount + metrics.staticTrackCount), hint: `${metrics.dynamicTrackCount} dynamic · ${metrics.staticTrackCount} static` }, { label: "Unknown", value: String(metrics.unknownTrackCount), hint: "no guess without evidence" }]} conclusion={{ proved: `На 16 recorded RIGHT кадрах опубликованы ${metrics.trackCount} стабильных ID с визуальными следами; ${metrics.dynamicTrackCount + metrics.staticTrackCount} треков имеют непротиворечивое E26 motion evidence.`, notProved: "Не доказаны route-global identity, метрическая скорость для camera-only объектов, detector AP/recall, live/hardware или safety-пригодность.", decision: "Принять E46B как recorded temporal object layer. Следующий архитектурный шаг — расширить ассоциацию с коротких truth-island клипов на полный recorded replay и затем связать confirmed tracks с temporal occupied/unknown world layer." }} />}
/>;
}
@@ -0,0 +1,65 @@
import { useEffect, useRef, useState } from "react";
import type { E46BCase, E46BMotionState } from "../../core/laboratory/e46bTemporalMotion";
export type E46BSceneMode = "tracks" | "source";
function color(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 stateColor(host: HTMLElement, state: E46BMotionState): string {
return state === "dynamic" ? color(host, "--nodedc-accent-rgb", [232, 56, 126])
: state === "static" ? color(host, "--nodedc-success-rgb", [181, 255, 90])
: color(host, "--nodedc-warning-rgb", [255, 197, 92]);
}
const stateLabel = (state: E46BMotionState) => state === "dynamic" ? "движется" : state === "static" ? "стоит" : "неизвестно";
export function E46BTemporalMotionScene({ frame, mode }: { frame: E46BCase; mode: E46BSceneMode }) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [image, setImage] = useState<HTMLImageElement | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
const next = new Image(); next.decoding = "async";
next.onload = () => { setImage(next); setFailed(false); };
next.onerror = () => { setImage(null); setFailed(true); };
next.src = frame.cameraUrl;
return () => { next.onload = null; next.onerror = null; };
}, [frame.cameraUrl]);
useEffect(() => {
const host = hostRef.current; const canvas = canvasRef.current;
if (!host || !canvas || !image) 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.fillStyle = color(host, "--nodedc-canvas-rgb", [5, 5, 6]); context.fillRect(0, 0, width, height);
const scale = Math.min(width / 800, height / 600); const drawWidth = 800 * scale; const drawHeight = 600 * scale;
const offsetX = (width - drawWidth) / 2; const offsetY = (height - drawHeight) / 2;
context.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
if (mode === "source") return;
frame.objects.forEach((item) => {
const stroke = stateColor(host, item.motionState);
if (item.trailCentersXy.length > 1) {
context.beginPath(); context.strokeStyle = stroke; context.lineWidth = Math.max(1.5, 2 * scale);
item.trailCentersXy.forEach(([px, py], index) => index === 0 ? context.moveTo(offsetX + px * scale, offsetY + py * scale) : context.lineTo(offsetX + px * scale, offsetY + py * scale));
context.stroke();
item.trailCentersXy.forEach(([px, py]) => { context.beginPath(); context.fillStyle = stroke; context.arc(offsetX + px * scale, offsetY + py * scale, Math.max(2, 3 * scale), 0, Math.PI * 2); context.fill(); });
}
const [left, top, right, bottom] = item.boxXyxy; const x = offsetX + left * scale; const y = offsetY + top * scale;
context.strokeStyle = stroke; context.lineWidth = Math.max(1.5, 2 * scale); context.strokeRect(x, y, (right - left) * scale, (bottom - top) * scale);
const label = `#${String(item.trackIndex).padStart(2, "0")} · ${item.displayCategory} · ${stateLabel(item.motionState)}`;
const fontSize = Math.max(9, 10 * scale); context.font = `600 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = context.measureText(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 = color(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.9); context.fillRect(labelX, labelY, labelWidth, labelHeight);
context.fillStyle = stroke; context.fillText(label, labelX + 4, labelY + fontSize + 1);
});
};
const observer = new ResizeObserver(render); observer.observe(host); render(); return () => observer.disconnect();
}, [frame, image, mode]);
return <div className="l32-camera-scene" ref={hostRef}><canvas ref={canvasRef} role="img" aria-label={`E46B frame ${frame.frameIndex}: ${frame.objectCount} temporal tracks`} />{failed ? <div className="l3-visual-audit__state" role="status">Точный source-кадр E46B недоступен.</div> : null}</div>;
}
@@ -0,0 +1,21 @@
import { useEffect, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import { fetchE46BCase, type E46BCase } from "../../core/laboratory/e46bTemporalMotion";
import { E46BTemporalMotionScene, type E46BSceneMode } from "./E46BTemporalMotionScene";
const SEQUENCES = [5, 6, 7, 8, 14, 15, 16, 17, 23, 24, 25, 26, 29, 30, 31, 32] as const;
export function E46BTemporalMotionVisual({ resultId }: { resultId: string }) {
const [selectedSequence, setSelectedSequence] = useState<number>(5); const [frame, setFrame] = useState<E46BCase | null>(null);
const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<E46BSceneMode>("tracks"); const [expanded, setExpanded] = useState(false);
useEffect(() => {
const controller = new AbortController(); setLoading(true); setError(null); setFrame(null);
void fetchE46BCase(resultId, selectedSequence, { signal: controller.signal }).then((next) => { if (!controller.signal.aborted) setFrame(next); }).catch((caught: unknown) => { if (!controller.signal.aborted) setError(caught instanceof Error ? caught.message : "E46B frame недоступен."); }).finally(() => { if (!controller.signal.aborted) setLoading(false); });
return () => controller.abort();
}, [resultId, selectedSequence]);
const navigate = (offset: -1 | 1) => { const index = SEQUENCES.indexOf(selectedSequence as typeof SEQUENCES[number]); setSelectedSequence(SEQUENCES[(index + offset + SEQUENCES.length) % SEQUENCES.length]); };
const actions = <div className="l3-visual-audit__actions"><div className="l3-visual-audit__pagination"><IconButton label="Предыдущий temporal кадр" onClick={() => navigate(-1)}><Icon name="chevron-left" size={16} /></IconButton><IconButton label="Следующий temporal кадр" onClick={() => navigate(1)}><Icon name="chevron-right" size={16} /></IconButton></div><Select label="Выбрать temporal кадр" value={String(selectedSequence)} options={SEQUENCES.map((sequence, index) => ({ value: String(sequence), label: `${index + 1}/16 · E46 seq ${sequence}${frame?.truthIslandSequence === sequence ? ` · frame ${frame.frameIndex}` : ""}` }))} variant="split" menuWidth="anchor" onChange={(value) => setSelectedSequence(Number(value))} /></div>;
const overlay = frame ? <div className="l3-visual-audit__overlay"><div><span>RAVNOVES00 · sensor.camera.right</span><strong>{frame.groupId} · frame {frame.frameIndex}</strong><small>Sequence {frame.truthIslandSequence} · {frame.sessionSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с</small></div><div><span>E46B · temporal tracks</span><strong>{frame.objectCount} ID · {frame.motionCounts.dynamic} движется · {frame.motionCounts.static} стоит</strong><small>{frame.motionCounts.unknown} неизвестно · image {frame.sourceImageSha256.slice(0, 12)}</small></div></div> : undefined;
return <div className="l3-visual-audit"><LaboratoryEvidenceViewer label="E46B temporal tracks and exact source" mode={mode} modes={[{ value: "tracks", label: "TRACKS" }, { value: "source", label: "SOURCE" }]} expanded={expanded} onModeChange={setMode} onExpandedChange={setExpanded} actions={actions} overlay={overlay}>{loading ? <div className="l3-visual-audit__state" role="status"><span className="busy-indicator" aria-hidden="true" /><span>Открываем E46B temporal evidence</span></div> : error || !frame ? <div className="l3-visual-audit__state" role="status"><Icon name="alert" size={18} /><span>{error ?? "E46B frame недоступен."}</span></div> : <E46BTemporalMotionScene frame={frame} mode={mode} />}</LaboratoryEvidenceViewer></div>;
}
@@ -0,0 +1,79 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { E46BlindReviewResult } from "../../core/laboratory/e46BlindReview";
import { E46BlindReviewVisual } from "./E46BlindReviewVisual";
export function E46BlindReviewResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: E46BlindReviewResult;
}) {
const metrics = result.metrics;
const complete = result.decision.twoIndependentReviewsComplete;
return (
<LaboratoryWorkTemplate
summary={<LaboratorySummary
title="LAB E46 · independent source-only detector review"
description="Два раздельных reviewer-слота получают те же 32 immutable кадра RAVNOVES00 без identity YOLOX-кандидата, predictions, scores, prelabels и разметки другого рецензента."
status={complete ? "2/2 frozen · нужна adjudication" : `${metrics.completedSubmissionCount}/2 independent reviews frozen`}
statusTone="warning"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · RIGHT camera · recorded replay` },
{ label: "Источник", value: `${metrics.frameCount} кадров · ${metrics.anchorCount} anchors + ${metrics.temporalFrameCount} temporal` },
{ label: "Reviewer slots", value: `${metrics.reviewSessionCount}/2 созданы · ${metrics.completedSubmissionCount}/2 frozen` },
{ label: "Полномочия", value: "Not truth · E48 unsealed · L3.5 closed" },
]}
brief={{
question: "Как собрать две реально независимые разметки E46, не подсказывая людям результат модели или первого reviewer?",
approach: "Каждый рецензент получает приватный capability-bound слот, фиксированную семиклассовую taxonomy и 32 exact source frame. Чужой слот сервер не раскрывает; freeze разрешён только после 32/32 и трёх явных attestations о независимости и model blindness.",
principalResult: `Контур готов; заморожено ${metrics.completedSubmissionCount} из 2 обязательных review. До второго независимого freeze никаких AP/recall или выводов о качестве детектора не существует.`,
limitation: "Интерфейс и криптографическая привязка источника контролируют данные, но утверждение о том, что reviewer физически не видел модель вне системы, остаётся человеческой аттестацией.",
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: "e46-source-only-independent-review/v1",
components: [
{ kind: "source", name: result.resultId, version: "E46 immutable references", role: "32 hash-bound right-camera frames", identitySha256: result.resultId.split("-").at(-1) ?? null },
{ kind: "algorithm", name: "Capability-separated reviewer slots", version: "2-slot/v1", role: "prevent cross-review label access", identitySha256: null },
{ kind: "tool", name: "Fullscreen canonical box editor", version: "source-only", role: "manual move, resize, add, delete and classify", identitySha256: null },
],
}}
/>}
evidence={<LaboratoryEvidence
eyebrow="E46 VISUAL SOURCE · CANDIDATE IDENTITY EXCLUDED"
title="Все 32 кадра доступны как source-only evidence"
kind="diagnostic-model"
resizable
>
<E46BlindReviewVisual resultId={result.resultId} />
</LaboratoryEvidence>}
result={<LaboratoryResultSummary
title={complete
? "Два независимых входа собраны; seal всё ещё закрыт до adjudication"
: "Контур готов; независимые человеческие проходы ещё не собраны"}
status={complete ? "Adjudication required before E48" : "Reviewer A + Reviewer B required"}
statusTone="warning"
metrics={[
{ label: "Source frames", value: "32/32", hint: "16 anchors · 4 temporal groups" },
{ label: "Reviewer slots", value: `${metrics.reviewSessionCount}/2`, hint: "Capability separated" },
{ label: "Frozen inputs", value: `${metrics.completedSubmissionCount}/2`, hint: "E48 inputs · not truth" },
{ label: "Model material", value: "0", hint: "No candidate id, boxes, scores or prelabels" },
]}
conclusion={{
proved: "Источник для независимого контроля воспроизводим, визуально доступен и отделён от model material и чужих reviewer labels. Сервер не примет неполный, assisted или произвольный-class проход.",
notProved: "Пока не доказаны correctness разметки, межэкспертное согласие, качество YOLOX, AP/recall, metric-grade truth или пригодность системы к live/hardware/safety.",
decision: complete
? "Выполнить отдельную adjudication двух immutable reviews; только затем разрешить E48 truth seal и один acceptance-прогон L3.5."
: "Передать Reviewer A и Reviewer B их отдельные слоты. Не показывать им L3.4/L3.4D/L3.4F до freeze собственного прохода.",
}}
/>}
/>
);
}
@@ -0,0 +1,147 @@
import { useEffect, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchL34AnnotationSourceCatalog,
fetchL34AnnotationSourceFrame,
type L34AnnotationSourceCatalog,
type L34AnnotationSourceFrame,
} from "../../core/laboratory/l34Annotation";
function frameLabel(frame: L34AnnotationSourceCatalog["frames"][number]): string {
return `${frame.truthIslandSequence}/32 · frame ${frame.frameIndex} · ${frame.groupId}`;
}
export function E46BlindReviewVisual({ resultId }: { resultId: string }) {
const [catalog, setCatalog] = useState<L34AnnotationSourceCatalog | null>(null);
const [selectedSequence, setSelectedSequence] = useState(1);
const [frame, setFrame] = useState<L34AnnotationSourceFrame | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
void fetchL34AnnotationSourceCatalog(resultId, {
signal: controller.signal,
}).then((next) => {
if (controller.signal.aborted) return;
setCatalog(next);
setSelectedSequence(next.frames[0]?.truthIslandSequence ?? 1);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(caught instanceof Error ? caught.message : "E46 source недоступен.");
}
}).finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [resultId]);
useEffect(() => {
if (!catalog) return;
const controller = new AbortController();
setLoading(true);
setFrame(null);
void fetchL34AnnotationSourceFrame(resultId, selectedSequence, {
signal: controller.signal,
}).then((next) => {
if (!controller.signal.aborted) setFrame(next);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(caught instanceof Error ? caught.message : "E46 frame недоступен.");
}
}).finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [catalog, resultId, selectedSequence]);
const selectedIndex = catalog?.frames.findIndex(
(item) => item.truthIslandSequence === selectedSequence,
) ?? -1;
const navigate = (offset: -1 | 1) => {
if (!catalog || selectedIndex < 0) return;
const next = (
selectedIndex + offset + catalog.frames.length
) % catalog.frames.length;
setSelectedSequence(catalog.frames[next].truthIslandSequence);
};
const actions = catalog ? (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущий source-only кадр E46" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующий source-only кадр E46" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать source-only кадр E46"
value={String(selectedSequence)}
options={catalog.frames.map((item) => ({
value: String(item.truthIslandSequence),
label: frameLabel(item),
}))}
variant="split"
menuWidth="anchor"
searchable
searchPlaceholder="Найти кадр или группу"
onChange={(value) => setSelectedSequence(Number(value))}
/>
</div>
) : null;
const overlay = frame ? (
<div className="l3-visual-audit__overlay">
<div>
<span>RAVNOVES00 · sensor.camera.right</span>
<strong>{frame.sessionSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с · frame {frame.frameIndex}</strong>
<small>Sequence {frame.truthIslandSequence}/32 · {frame.groupId}</small>
</div>
<div>
<span>E46 independent review source</span>
<strong>0 boxes · 0 labels · 0 scores</strong>
<small>Candidate identity не включена · image {frame.cameraSha256.slice(0, 12)}</small>
</div>
</div>
) : undefined;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="E46 source-only independent reviewer evidence"
mode="source"
modes={[{ value: "source", label: "SOURCE ONLY" }]}
expanded={expanded}
onModeChange={() => undefined}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
{loading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем hash-bound E46 source без model material</span>
</div>
) : error || !frame ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{error ?? "E46 source frame недоступен."}</span>
</div>
) : (
<div className="l32-camera-scene">
<img
src={frame.cameraUrl}
alt={`E46 source-only кадр ${frame.frameIndex} без model overlay`}
draggable={false}
/>
</div>
)}
</LaboratoryEvidenceViewer>
</div>
);
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,24 @@
import { useEffect, useRef, useState } from "react";
import type { E46CCase, E46CMotionState } from "../../core/laboratory/e46cFullReplayWorldTracks";
export type E46CSceneMode = "camera" | "world";
function color(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 stateColor(host: HTMLElement, state: E46CMotionState): string { return state === "dynamic" ? color(host, "--nodedc-accent-rgb", [232, 56, 126]) : state === "static" ? color(host, "--nodedc-success-rgb", [181, 255, 90]) : color(host, "--nodedc-warning-rgb", [255, 197, 92]); }
const stateLabel = (state: E46CMotionState) => state === "dynamic" ? "движется" : state === "static" ? "стоит" : "unknown";
export function E46CFullReplayWorldTracksScene({ frame, mode }: { frame: E46CCase; mode: E46CSceneMode }) {
const hostRef = useRef<HTMLDivElement | null>(null); const canvasRef = useRef<HTMLCanvasElement | null>(null); const [image, setImage] = useState<HTMLImageElement | null>(null); const [failed, setFailed] = useState(false);
useEffect(() => { const next = new Image(); next.decoding = "async"; next.onload = () => { setImage(next); setFailed(false); }; next.onerror = () => { setImage(null); setFailed(true); }; next.src = frame.cameraUrl; return () => { next.onload = null; next.onerror = null; }; }, [frame.cameraUrl]);
useEffect(() => {
const host = hostRef.current; const canvas = canvasRef.current; if (!host || !canvas || (mode === "camera" && !image)) 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.fillStyle = color(host, "--nodedc-canvas-rgb", [5, 5, 6]); context.fillRect(0, 0, width, height);
if (mode === "camera" && image) { const scale = Math.min(width / 800, height / 600); const drawWidth = 800 * scale; const drawHeight = 600 * scale; const offsetX = (width - drawWidth) / 2; const offsetY = (height - drawHeight) / 2; context.drawImage(image, offsetX, offsetY, drawWidth, drawHeight); frame.objects.forEach((item) => { const [left, top, right, bottom] = item.boxXyxy; const x = offsetX + left * scale; const y = offsetY + top * scale; const stroke = stateColor(host, item.motionState); context.strokeStyle = stroke; context.lineWidth = Math.max(1.5, 2 * scale); context.strokeRect(x, y, (right - left) * scale, (bottom - top) * scale); const identity = item.routeTrackId === null ? "S—" : `S${item.routeTrackId}${item.worldTrackId === null ? "" : `→W${item.worldTrackId}`}`; const label = `${identity} · ${item.displayCategory} · ${stateLabel(item.motionState)}`; const fontSize = Math.max(9, 10 * scale); context.font = `600 ${fontSize}px Inter, system-ui, sans-serif`; const labelWidth = context.measureText(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 = color(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.9); context.fillRect(labelX, labelY, labelWidth, labelHeight); context.fillStyle = stroke; context.fillText(label, labelX + 4, labelY + fontSize + 1); }); return; }
const footprints = frame.worldObjects.flatMap((item) => item.occupancyFootprintMapXy); if (!footprints.length) { context.fillStyle = color(host, "--nodedc-foreground-rgb", [245, 245, 245], 0.64); context.font = "600 14px Inter, system-ui, sans-serif"; context.textAlign = "center"; context.fillText("На этом sample-кадре world-state отсутствует", width / 2, height / 2); context.textAlign = "start"; return; }
const xs = footprints.map((point) => point[0]); const ys = footprints.map((point) => point[1]); const minX = Math.min(...xs); const maxX = Math.max(...xs); const minY = Math.min(...ys); const maxY = Math.max(...ys); const padding = 56; const spanX = Math.max(maxX - minX, 8); const spanY = Math.max(maxY - minY, 8); const scale = Math.min((width - padding * 2) / spanX, (height - padding * 2) / spanY); const project = ([x, y]: readonly [number, number]) => [padding + (x - minX) * scale, height - padding - (y - minY) * scale] as const;
context.strokeStyle = color(host, "--nodedc-foreground-rgb", [245, 245, 245], 0.1); context.lineWidth = 1; for (let gx = Math.floor(minX / 5) * 5; gx <= maxX; gx += 5) { const [x] = project([gx, minY]); context.beginPath(); context.moveTo(x, padding); context.lineTo(x, height - padding); context.stroke(); } for (let gy = Math.floor(minY / 5) * 5; gy <= maxY; gy += 5) { const [, y] = project([minX, gy]); context.beginPath(); context.moveTo(padding, y); context.lineTo(width - padding, y); context.stroke(); }
frame.worldObjects.forEach((item) => { const stroke = stateColor(host, item.motionState); const projected = item.occupancyFootprintMapXy.map(project); context.beginPath(); projected.forEach(([x, y], index) => index === 0 ? context.moveTo(x, y) : context.lineTo(x, y)); context.closePath(); context.fillStyle = item.occupancyEvidenceCurrent ? stroke.replace(", 1)", ", 0.28)") : stroke.replace(", 1)", ", 0.12)"); context.fill(); context.strokeStyle = stroke; context.setLineDash(item.occupancyEvidenceCurrent ? [] : [5, 4]); context.lineWidth = 2; context.stroke(); context.setLineDash([]); const [labelX, labelY] = project([item.positionMapM[0], item.positionMapM[1]]); const label = `S${item.routeTrackId}→W${item.worldTrackId} · ${stateLabel(item.motionState)} · ${item.occupancyEvidenceCurrent ? "current" : "held"}`; context.font = "600 11px Inter, system-ui, sans-serif"; const labelWidth = context.measureText(label).width + 8; const safeLabelX = Math.min(width - labelWidth - 18, Math.max(18, labelX + 5)); const safeLabelY = Math.min(height - 42, Math.max(22, labelY - 16)); context.fillStyle = color(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.88); context.fillRect(safeLabelX, safeLabelY, labelWidth, 18); context.fillStyle = stroke; context.fillText(label, safeLabelX + 4, safeLabelY + 13); });
context.fillStyle = color(host, "--nodedc-foreground-rgb", [245, 245, 245], 0.58); context.font = "600 11px Inter, system-ui, sans-serif"; context.fillText("world map · occupied footprints · solid=current · dashed=held", 18, height - 18);
}; const observer = new ResizeObserver(render); observer.observe(host); render(); return () => observer.disconnect();
}, [frame, image, mode]);
return <div className="l32-camera-scene" ref={hostRef}><canvas ref={canvasRef} role="img" aria-label={`E46C frame ${frame.frameIndex}: ${mode}, ${frame.objectCount} camera objects, ${frame.worldObjectCount} world objects`} />{failed && mode === "camera" ? <div className="l3-visual-audit__state" role="status">Точный source-кадр E46C недоступен.</div> : null}</div>;
}
@@ -0,0 +1,358 @@
import { useEffect, useMemo, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import type { RecordedObservationPlayback } from "../../components/RecordedFmp4Player";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchE46CCase,
fetchE46CVideoOverlay,
selectE46CVideoFrame,
type E46CCase,
type E46CVideoOverlay,
} from "../../core/laboratory/e46cFullReplayWorldTracks";
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
import { replayObservationSession } from "../../core/observation/sessionArchive";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
import { E46CRecordedVideoScene } from "./E46CRecordedVideoScene";
import {
E46CFullReplayWorldTracksScene,
type E46CSceneMode,
} from "./E46CFullReplayWorldTracksScene";
const SEQUENCES = Array.from({ length: 32 }, (_, index) => index + 1);
type E46CViewMode = "video" | E46CSceneMode;
export interface E46CTemporalAuditWindow {
id: string;
label: string;
startSeconds: number;
endSeconds: number;
title: string;
detail: string;
}
function reviewWindowLabel(
window: E46CVideoOverlay["reviewWindows"][number],
): string {
const kind = window.kind === "static-control" ? "статика" : "динамика";
const group = window.classGroup === "vehicle"
? "транспорт"
: window.classGroup === "person"
? "люди"
: window.classGroup === "vulnerable_road_user"
? "люди / велосипеды"
: window.classGroup;
const tracks = window.targetSourceTrackIds.length
? ` · S${window.targetSourceTrackIds.join(", S")}`
: "";
return `${window.startSeconds.toFixed(0)}${window.endSeconds.toFixed(0)} с · ${kind} · ${group}${tracks}`;
}
export function E46CFullReplayWorldTracksVisual({
resultId,
auditWindows,
}: {
resultId: string;
auditWindows?: readonly E46CTemporalAuditWindow[];
}) {
const [selectedSequence, setSelectedSequence] = useState(1);
const [frame, setFrame] = useState<E46CCase | null>(null);
const [sampleLoading, setSampleLoading] = useState(true);
const [sampleError, setSampleError] = useState<string | null>(null);
const [mode, setMode] = useState<E46CViewMode>("video");
const [expanded, setExpanded] = useState(false);
const [videoOverlay, setVideoOverlay] = useState<E46CVideoOverlay | null>(null);
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
const [videoLoading, setVideoLoading] = useState(false);
const [videoError, setVideoError] = useState<string | null>(null);
const [selectedWindow, setSelectedWindow] = useState(auditWindows?.[0]?.id ?? "");
const [videoPlayback, setVideoPlayback] = useState<RecordedObservationPlayback>({
currentSeconds: 0,
playing: false,
});
useEffect(() => {
const controller = new AbortController();
setSampleLoading(true);
setSampleError(null);
setFrame(null);
void fetchE46CCase(resultId, selectedSequence, { signal: controller.signal })
.then((next) => {
if (!controller.signal.aborted) setFrame(next);
})
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
setSampleError(caught instanceof Error ? caught.message : "E46C frame недоступен.");
}
})
.finally(() => {
if (!controller.signal.aborted) setSampleLoading(false);
});
return () => controller.abort();
}, [resultId, selectedSequence]);
useEffect(() => {
if (mode !== "video" || (videoOverlay && videoSource)) return;
const controller = new AbortController();
setVideoLoading(true);
setVideoError(null);
void (async () => {
const overlay = await fetchE46CVideoOverlay(resultId, {
signal: controller.signal,
});
const replay = await replayObservationSession(overlay.recordedSourceSessionId, {
signal: controller.signal,
});
if (replay.kind !== "ready") {
throw new Error("Физический RIGHT-архив ещё готовится к воспроизведению.");
}
const source = recordedObservationSources(replay.launch).find(
(candidate) =>
candidate.modality === "video" &&
candidate.semanticChannelId === "camera.video.recorded",
);
const delivery = source?.delivery?.kind === "recorded-fmp4-manifest"
? source.delivery
: null;
if (
!source ||
!delivery ||
delivery.timelineStartSeconds !== overlay.timelineStartSeconds ||
delivery.timelineEndSeconds < overlay.timelineEndSeconds
) {
throw new Error("RIGHT-видео не совпало с временным контрактом E46C.");
}
if (controller.signal.aborted) return;
setVideoOverlay(overlay);
setVideoSource(source);
const initialWindow = auditWindows?.find((item) => item.id === selectedWindow);
setVideoPlayback({
currentSeconds: initialWindow?.startSeconds ?? overlay.timelineStartSeconds,
playing: false,
});
})()
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
setVideoError(
caught instanceof Error ? caught.message : "E46C video replay недоступен.",
);
}
})
.finally(() => {
if (!controller.signal.aborted) setVideoLoading(false);
});
return () => controller.abort();
}, [auditWindows, mode, resultId, selectedWindow, videoOverlay, videoSource]);
const activeVideoFrame = useMemo(
() =>
videoOverlay
? selectE46CVideoFrame(videoOverlay.frames, videoPlayback.currentSeconds)
: null,
[videoOverlay, videoPlayback.currentSeconds],
);
const activeAuditWindow = auditWindows?.find((item) => item.id === selectedWindow) ?? null;
const navigate = (offset: -1 | 1) => {
setSelectedSequence((current) => ((current - 1 + offset + 32) % 32) + 1);
};
const seekVideo = (seconds: number) => {
if (!videoOverlay) return;
setVideoPlayback({
currentSeconds: Math.min(
videoOverlay.timelineEndSeconds,
Math.max(videoOverlay.timelineStartSeconds, seconds),
),
playing: false,
});
};
const actions = mode === "video" ? (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton
label="Назад на 5 секунд"
onClick={() => seekVideo(videoPlayback.currentSeconds - 5)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Вперёд на 5 секунд"
onClick={() => seekVideo(videoPlayback.currentSeconds + 5)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
{auditWindows?.length || videoOverlay?.reviewWindows.length ? (
<Select
label={auditWindows?.length
? "Перейти к автоматически найденному эпизоду E46D"
: "Перейти к проверочному окну E26"}
value={selectedWindow || "full-route"}
options={[
{ value: "full-route", label: "Весь проход · с начала" },
...(auditWindows?.map((window) => ({
value: window.id,
label: window.label,
})) ?? videoOverlay?.reviewWindows.map((window) => ({
value: window.id,
label: reviewWindowLabel(window),
})) ?? []),
]}
variant="split"
menuWidth="anchor"
searchable
searchPlaceholder="Найти окно"
onChange={(value) => {
if (!videoOverlay) return;
setSelectedWindow(value);
if (value === "full-route") {
seekVideo(videoOverlay.timelineStartSeconds);
return;
}
const window = auditWindows?.find((item) => item.id === value)
?? videoOverlay.reviewWindows.find((item) => item.id === value);
if (window) seekVideo(window.startSeconds);
}}
/>
) : null}
</div>
) : (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущий sample E46C" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующий sample E46C" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать sample E46C"
value={String(selectedSequence)}
options={SEQUENCES.map((sequence) => ({
value: String(sequence),
label:
frame?.truthIslandSequence === sequence
? `${sequence}/32 · frame ${frame.frameIndex} · ${frame.groupId}`
: `${sequence}/32 · exact E46 sample`,
}))}
variant="split"
menuWidth="anchor"
searchable
searchPlaceholder="Найти sample"
onChange={(value) => setSelectedSequence(Number(value))}
/>
</div>
);
const overlay = mode === "video" && videoOverlay ? (
<div className="l3-visual-audit__overlay l3-visual-audit__overlay--video">
<div>
<span>RAVNOVES00 · recorded RIGHT video</span>
<strong>
+{Math.max(0, videoPlayback.currentSeconds - videoOverlay.timelineStartSeconds).toFixed(1)} с
{activeVideoFrame ? ` · frame ${activeVideoFrame.frameIndex}` : ""}
</strong>
<small>{videoPlayback.playing ? "воспроизведение" : "пауза / seek"}</small>
</div>
<div>
<span>E26 · temporal route tracks</span>
<strong>
{activeVideoFrame?.objects.length ?? 0} boxes
{activeVideoFrame?.objects.length
? ` · ${activeVideoFrame.objects.map((item) => `S${item.routeTrackId}`).join(", ")}`
: " · объектов нет"}
</strong>
<small>{activeVideoFrame?.fusionState ?? "ожидаем frame binding"}</small>
</div>
{activeAuditWindow ? (
<div>
<span>E46D · temporal exception</span>
<strong>{activeAuditWindow.title}</strong>
<small>{activeAuditWindow.detail}</small>
</div>
) : (
<div>
<span>Что проверяем</span>
<strong>удержание рамки · смена S-ID · потеря / повторный захват</strong>
<small>Цвет: dynamic / static / unknown · это diagnostic, не truth</small>
</div>
)}
</div>
) : frame ? (
<div className="l3-visual-audit__overlay">
<div>
<span>RAVNOVES00 · full recorded RIGHT</span>
<strong>frame {frame.frameIndex} · {frame.groupId}</strong>
<small>
Sample {frame.truthIslandSequence}/32 · {frame.sessionSeconds.toLocaleString("ru-RU", {
maximumFractionDigits: 1,
})} с
</small>
</div>
<div>
<span>E46C · route / world binding</span>
<strong>
{frame.matchedRouteObjectCount}/{frame.objectCount} route-bound · {frame.worldObjectCount} world objects
</strong>
<small>{frame.fusionState} · image {frame.sourceImageSha256.slice(0, 12)}</small>
</div>
</div>
) : undefined;
let content;
if (mode === "video") {
content = videoLoading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Связываем 4489 temporal frames с RIGHT-видео</span>
</div>
) : videoError || !videoOverlay || !videoSource ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{videoError ?? "E46C video replay недоступен."}</span>
</div>
) : (
<E46CRecordedVideoScene
source={videoSource}
overlay={videoOverlay}
playback={videoPlayback}
onPlaybackChange={setVideoPlayback}
/>
);
} else {
content = sampleLoading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем E46C route/world sample</span>
</div>
) : sampleError || !frame ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{sampleError ?? "E46C frame недоступен."}</span>
</div>
) : (
<E46CFullReplayWorldTracksScene frame={frame} mode={mode} />
);
}
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="E46C full replay temporal video, route and world evidence"
mode={mode}
modes={[
{ value: "video", label: "VIDEO" },
{ value: "camera", label: "CAMERA" },
{ value: "world", label: "WORLD" },
]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
{content}
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,146 @@
import { useEffect, useMemo, useRef } from "react";
import {
RecordedFmp4Player,
type RecordedObservationPlayback,
} from "../../components/RecordedFmp4Player";
import {
selectE46CVideoFrame,
type E46CMotionState,
type E46CVideoOverlay,
} from "../../core/laboratory/e46cFullReplayWorldTracks";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
function color(
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 stateColor(host: HTMLElement, state: E46CMotionState): string {
if (state === "dynamic") return color(host, "--nodedc-accent-rgb", [232, 56, 126]);
if (state === "static") return color(host, "--nodedc-success-rgb", [181, 255, 90]);
return color(host, "--nodedc-warning-rgb", [255, 197, 92]);
}
function stateLabel(state: E46CMotionState): string {
if (state === "dynamic") return "движется";
if (state === "static") return "стоит";
return "unknown";
}
export function E46CRecordedVideoScene({
source,
overlay,
playback,
onPlaybackChange,
}: {
source: ObservationSourceDescriptor;
overlay: E46CVideoOverlay;
playback: RecordedObservationPlayback;
onPlaybackChange: (playback: RecordedObservationPlayback) => void;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const frame = useMemo(
() => selectE46CVideoFrame(overlay.frames, playback.currentSeconds),
[overlay.frames, playback.currentSeconds],
);
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);
if (!frame) return;
const scale = Math.min(
width / overlay.imageWidth,
height / overlay.imageHeight,
);
const drawWidth = overlay.imageWidth * scale;
const drawHeight = overlay.imageHeight * scale;
const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
for (const item of frame.objects) {
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 = stateColor(host, item.motionState);
context.strokeStyle = stroke;
context.lineWidth = Math.max(1.5, 2 * scale);
context.setLineDash(item.cameraEvidenceCurrent ? [] : [5, 4]);
context.strokeRect(x, y, boxWidth, boxHeight);
context.setLineDash([]);
const identity = `S${item.routeTrackId}${
item.worldTrackId === null ? "" : `→W${item.worldTrackId}`
}`;
const label = `${identity} · ${item.displayCategory} · ${stateLabel(
item.motionState,
)} · ${Math.round(item.score * 100)}%`;
const fontSize = Math.max(9, 10 * scale);
context.font = `650 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = context.measureText(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 = color(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.9);
context.fillRect(labelX, labelY, labelWidth, labelHeight);
context.fillStyle = stroke;
context.fillText(label, labelX + 4, labelY + fontSize + 1);
}
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [frame, overlay.imageHeight, overlay.imageWidth]);
return (
<div className="e46c-video-scene" ref={hostRef}>
<RecordedFmp4Player
source={source}
playback={playback}
interactive
prepare
onPlaybackChange={onPlaybackChange}
/>
<canvas
ref={canvasRef}
role="img"
aria-label={
frame
? `E46C video frame ${frame.frameIndex}: ${frame.objects.length} route objects`
: "E46C recorded video overlay"
}
/>
</div>
);
}
@@ -0,0 +1,206 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type {
E46DReviewClip,
E46DTemporalFailureAuditResult,
E46DTemporalSignalKind,
} from "../../core/laboratory/e46dTemporalFailureAudit";
import { formatNumber } from "../../presentation";
import {
E46CFullReplayWorldTracksVisual,
type E46CTemporalAuditWindow,
} from "./E46CFullReplayWorldTracksVisual";
function evidenceNumber(clip: E46DReviewClip, key: string): number | null {
const value = clip.evidence[key];
return typeof value === "number" ? value : null;
}
function kindTitle(kind: E46DTemporalSignalKind): string {
if (kind === "layer-blackout") return "Объектный слой исчез";
if (kind === "camera-evidence-hold") return "Tracker держит объект без свежей рамки";
if (kind === "route-layer-gap") return "Route-track пропал и вернулся";
if (kind === "route-id-rebirth-candidate") return "Похожий объект сменил S-ID";
if (kind === "bbox-jump") return "Рамка скачком сменила положение";
if (kind === "motion-state-flap") return "Статус движения нестабилен";
if (kind === "world-binding-flap") return "Привязка к W-ID нестабильна";
return "Всплеск короткоживущих S-ID";
}
function clipTitle(clip: E46DReviewClip): string {
const tracks = clip.routeTrackIds.length
? ` · S${clip.routeTrackIds.slice(0, 4).join(", S")}${clip.routeTrackIds.length > 4 ? "…" : ""}`
: "";
if (clip.kind === "layer-blackout") {
const before = evidenceNumber(clip, "before_object_count") ?? 0;
const after = evidenceNumber(clip, "after_object_count") ?? 0;
return `${kindTitle(clip.kind)}: ${before} → 0 → ${after}`;
}
if (clip.kind === "route-id-rebirth-candidate" && clip.routeTrackIds.length >= 2) {
return `${kindTitle(clip.kind)}: S${clip.routeTrackIds[0]} → S${clip.routeTrackIds[1]}`;
}
return `${kindTitle(clip.kind)}${tracks}`;
}
function clipDetail(clip: E46DReviewClip): string {
const duration = Math.max(0, clip.eventEndSeconds - clip.eventStartSeconds);
if (clip.kind === "layer-blackout") {
return `${evidenceNumber(clip, "zero_frame_count") ?? 0} пустых кадров · ${duration.toFixed(1)} с · published layer, не truth`;
}
if (clip.kind === "route-layer-gap") {
return `${evidenceNumber(clip, "missing_frame_count") ?? 0} кадров без S-ID · тот же ID затем вернулся`;
}
if (clip.kind === "camera-evidence-hold") {
return `${evidenceNumber(clip, "held_frame_count") ?? 0} кадров без current detector evidence`;
}
if (clip.kind === "route-id-rebirth-candidate" || clip.kind === "bbox-jump") {
const overlap = evidenceNumber(clip, "bbox_iou");
return `frame ${clip.startFrame}${clip.endFrame}${overlap === null ? "" : ` · IoU ${overlap.toFixed(2)}`} · нужна визуальная проверка`;
}
if (clip.kind === "short-track-burst") {
return `${evidenceNumber(clip, "short_track_count") ?? clip.routeTrackIds.length} ID с ≤3 наблюдениями за короткое окно`;
}
return `${evidenceNumber(clip, "transition_count") ?? 0} переключения за ${Math.max(0, duration).toFixed(1)} с`;
}
function auditWindows(
clips: readonly E46DReviewClip[],
): readonly E46CTemporalAuditWindow[] {
return clips.map((clip) => ({
id: clip.clipId,
label: `#${String(clip.rank).padStart(2, "0")} · ${clip.eventStartSeconds.toFixed(1)} с · ${clipTitle(clip)}`,
startSeconds: clip.startSeconds,
endSeconds: clip.endSeconds,
title: clipTitle(clip),
detail: clipDetail(clip),
}));
}
export function E46DTemporalFailureAuditResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: E46DTemporalFailureAuditResult;
}) {
const metrics = result.metrics;
const windows = auditWindows(result.reviewClips);
const zeroPercent = metrics.zeroObjectFrameFraction * 100;
const shortPercent = metrics.shortRouteTrackFraction * 100;
const heldPercent = metrics.cameraHeldObservationFraction * 100;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E46D · full replay temporal failure audit"
description="Полный recorded RIGHT replay автоматически просканирован на обвалы object-layer, разрывы и перерождение S-ID, скачки рамок, motion-flap и нестабильную W-привязку. Приоритетные эпизоды открываются как видео с точным временным контекстом."
status={`Temporal continuity failed · ${metrics.failureSignalCount} signals · ${metrics.reviewClipCount} clips`}
statusTone="danger"
facts={[
{
label: "Конфигурация",
value: `${rigLabel} · RIGHT camera · recorded replay · E26/E46C frozen`,
},
{
label: "Покрытие",
value: `${metrics.routeFrameCount}/4489 кадров · ${formatNumber(metrics.routeSpanSeconds, 1)} с · полный маршрут`,
},
{
label: "Continuity",
value: `${metrics.layerBlackoutEpisodeCount} обвалов слоя · ${metrics.routeLayerGapEpisodeCount} route-gap · ${metrics.routeIdRebirthCandidateCount} ID-candidates`,
},
{
label: "Визуал",
value: `${metrics.reviewClipCount} автоматически выбранных VIDEO-эпизодов · CAMERA/WORLD доступны в том же viewer`,
},
]}
brief={{
question: "Удерживает ли полный recorded perception replay рамки и идентичности во времени без скрытых провалов между удачными статичными кадрами?",
approach: "Все 4489 hash-bound E26 fusion-кадров проверены детерминированным temporal scanner. Он не пересчитывает модель: фиксирует только наблюдаемые разрывы опубликованного слоя и связывает их с исходным RIGHT-видео через ту же session-time шкалу.",
principalResult: `${metrics.zeroObjectFrameCount} кадров (${zeroPercent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%) имеют пустой object-layer; найдено ${metrics.layerBlackoutEpisodeCount} обвалов между непустыми кадрами. ${metrics.shortRouteTrackCount} из ${metrics.routeTrackCount} S-ID (${shortPercent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%) живут не дольше трёх наблюдений.`,
limitation: "Это честная диагностика опубликованного temporal layer, а не independent truth. Клип доказывает разрыв данных; семантический false positive или пропуск конкретного физического объекта подтверждается визуально в том же видео.",
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: "e46d-full-replay-temporal-failure-audit/v1",
components: [
{
kind: "source",
name: result.sourceE46CResultId,
version: "4489-frame full recorded RIGHT replay",
role: "video, route-track and world-binding substrate",
identitySha256: result.sourceE46CResultId.split("-").at(-1) ?? null,
},
{
kind: "source",
name: result.sourceE26ResultId,
version: "accepted diagnostic fusion",
role: "camera evidence, S-ID, W-ID and motion state",
identitySha256: result.sourceE26ResultId.split("-").at(-1) ?? null,
},
{
kind: "algorithm",
name: "Deterministic temporal exception scanner",
version: "e46d/v1",
role: "full-route detection, ranking and clip selection",
identitySha256: null,
},
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="E46D VISUAL EVIDENCE · AUTOMATIC TEMPORAL EXCEPTIONS"
title="Приоритетные видеоэпизоды: до сбоя, сам разрыв и восстановление"
kind="diagnostic-model"
resizable
>
<E46CFullReplayWorldTracksVisual
resultId={result.sourceE46CResultId}
auditWindows={windows}
/>
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Полный temporal слой не прошёл continuity gate"
status="Regression confirmed in published layer · detector/tracker/world causes separated"
statusTone="danger"
metrics={[
{
label: "Empty layer",
value: `${metrics.zeroObjectFrameCount}/${metrics.routeFrameCount}`,
hint: `${zeroPercent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}% frames · ${metrics.layerBlackoutEpisodeCount} bracketed episodes`,
},
{
label: "Short S-ID",
value: `${metrics.shortRouteTrackCount}/${metrics.routeTrackCount}`,
hint: `≤3 observations · ${shortPercent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`,
},
{
label: "Held evidence",
value: formatNumber(metrics.cameraHeldObservationCount, 0),
hint: `${heldPercent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}% observations without fresh camera evidence`,
},
{
label: "Identity / world",
value: `${metrics.routeIdRebirthCandidateCount} / ${metrics.worldBindingFlapEpisodeCount}`,
hint: "S-ID rebirth candidates / W-binding flap episodes",
},
]}
conclusion={{
proved: `Полный маршрут учтён без пропуска входных строк, но опубликованный temporal layer нестабилен: ${metrics.layerBlackoutEpisodeCount} раз он полностью обнуляется между непустыми кадрами, ${metrics.routeLayerGapEpisodeCount} раз тот же S-ID исчезает и возвращается, а ${metrics.bboxJumpEpisodeCount} current/current перехода имеют резкий bbox-разрыв.`,
notProved: "Без независимой покадровой truth не доказано, что каждый короткий S-ID является false positive, а каждый overlap-кандидат — физический ID-switch. Не доказаны live/hardware, free-space, navigation или safety-пригодность.",
decision: "Не продвигать текущий E26/E46C temporal layer. Сначала устранить глобальные object-layer blackouts и route fragmentation, затем стабилизировать W-binding и motion-state и сравнить старый/новый pipeline на том же recorded RIGHT replay.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,102 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { E46EReadyStackResult } from "../../core/laboratory/e46eReadyStack";
import { formatNumber } from "../../presentation";
import { E46EReadyStackVisual } from "./E46EReadyStackVisual";
export function E46EReadyStackResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: E46EReadyStackResult;
}) {
const metrics = result.metrics;
const zeroTrackPercent = (metrics.zeroTrackFrameCount / metrics.frameCount) * 100;
const shortPercent = metrics.shortTrackFraction * 100;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E46E · NVIDIA ready-stack bake-off R1"
description="Тот же полный recorded RIGHT проход пересчитан готовым NVIDIA-контуром: TrafficCamNet Transformer Lite (RT-DETR ResNet50) выдаёт детекции, штатный NvDCF — временные ID. Наш код только проверяет и публикует результат."
status={`Ready stack frozen · ${metrics.frameCount}/4489 · custom tracking 0`}
statusTone="accent"
facts={[
{
label: "Конфигурация",
value: `${rigLabel} · RIGHT camera · recorded replay · 800×600`,
},
{
label: "Модель",
value: "NVIDIA TrafficCamNet Transformer Lite · RT-DETR ResNet50 · FP16",
},
{
label: "Tracker",
value: "NVIDIA NvDCF performance profile · штатные ID · без ReID и hold/stitch Mission Core",
},
{
label: "Визуал",
value: `полное overlay-видео · ${formatNumber(result.video.byteLength / 1_048_576, 1)} MiB · hash-bound`,
},
]}
brief={{
question: "Даст ли готовый NVIDIA detector + tracker более честную и устойчивую основу компьютерного зрения, чем текущий YOLOX-контур с самописной временной склейкой?",
approach: "На Worker 006 один раз прогнан весь неизменный RIGHT-архив из 4489 кадров. DeepStream 9.1 работал без сети; RT-DETR и NvDCF не дообучались и не подстраивались под этот маршрут. Для каждого кадра опубликованы проверяемые detector/NvDCF-наблюдения, исходные LTRB-координаты и то же полноразмерное overlay-видео.",
principalResult: `${formatNumber(metrics.detectionObservationCount, 0)} detector-наблюдений превратились в ${formatNumber(metrics.trackObservationCount, 0)} NvDCF-наблюдений и ${formatNumber(metrics.uniqueTrackCount, 0)} route-local ID. Tracker пережил ${formatNumber(metrics.trackerRecoveredFrameCount, 0)} кадров без свежей детекции; зафиксировано ${metrics.fullLayerBlackoutEventCount} полных обвалов слоя.`,
limitation: "Набор классов ограничен car/person/bicycle/road_sign, а независимой покадровой truth для всего маршрута нет. Поэтому LAB доказывает воспроизводимый ready-stack и его временное поведение, но не заявляет precision/recall, motion-state, navigation или safety.",
}}
method={result.method}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="E46E VISUAL EVIDENCE · STOCK NVIDIA FULL REPLAY"
title="Полный RIGHT-видос: рамки RT-DETR и устойчивость NvDCF ID во времени"
kind="diagnostic-model"
resizable
>
<E46EReadyStackVisual videoUrl={result.video.url} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Готовый NVIDIA-контур зафиксирован как новый сравнительный baseline"
status="Full replay available · objective continuity metrics · visual review in the same artifact"
statusTone="accent"
metrics={[
{
label: "Detector / NvDCF",
value: `${formatNumber(metrics.detectionObservationCount, 0)} / ${formatNumber(metrics.trackObservationCount, 0)}`,
hint: `mean ${formatNumber(metrics.meanTrackedObjectsPerFrame, 2)} tracked objects/frame`,
},
{
label: "Empty layer",
value: `${metrics.zeroTrackFrameCount}/${metrics.frameCount}`,
hint: `${formatNumber(zeroTrackPercent, 1)}% frames · ${metrics.fullLayerBlackoutEventCount} bracketed blackouts`,
},
{
label: "NvDCF recovery",
value: formatNumber(metrics.trackerRecoveredFrameCount, 0),
hint: "frames with track output and no current detector box",
},
{
label: "Short / gap",
value: `${metrics.shortTrackCount} / ${metrics.routeIdGapEventCount}`,
hint: `≤3 observations: ${formatNumber(shortPercent, 1)}% · ${metrics.trackBoxClippedCount} display-only box clips`,
},
]}
conclusion={{
proved: `Все ${metrics.frameCount} кадров одного immutable RIGHT-маршрута обработаны официальным NVIDIA stack; готовое видео и покадровые ID связаны одной identity. Mission Core не выполнял association, hold, stitch или detector postprocessing; ${metrics.trackBoxClippedCount} выходов рамок за границу только обрезаны для отображения с сохранением raw-координат.`,
notProved: "Без независимой full-route truth не доказаны абсолютные precision/recall и корректность каждого ID. Не реализованы классификация dynamic/static, LiDAR-range fusion, live hardware, free-space, commands, navigation или safety.",
decision: "Считать E46E готовым A/B baseline и оценивать его по полному видео и тем же continuity-метрикам против E46D. Дальнейшую работу вести через готовые модели/трекеры и адаптеры контрактов, а не через подгонку самописного tracker под один ролик.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,37 @@
import { useState } from "react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
export function E46EReadyStackVisual({
videoUrl,
label = "E46E NVIDIA ready-stack full recorded RIGHT overlay",
ariaLabel = "E46E full recorded RIGHT video with NVIDIA RT-DETR detections and NvDCF track IDs",
}: {
videoUrl: string;
label?: string;
ariaLabel?: string;
}) {
const [expanded, setExpanded] = useState(false);
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label={label}
mode="video"
modes={[{ value: "video", label: "VIDEO" }]}
expanded={expanded}
onModeChange={() => undefined}
onExpandedChange={setExpanded}
>
<div className="e46e-ready-stack-video">
<video
controls
playsInline
preload="metadata"
src={videoUrl}
aria-label={ariaLabel}
/>
</div>
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,114 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { E46FDashCamBakeoffResult } from "../../core/laboratory/e46fDashCamBakeoff";
import { formatNumber } from "../../presentation";
import { E46EReadyStackVisual } from "./E46EReadyStackVisual";
export function E46FDashCamBakeoffResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: E46FDashCamBakeoffResult;
}) {
const metrics = result.metrics;
const baseline = result.comparison.baselineMetrics;
const triage = result.comparison.largeBoxVisualTriage;
const candidateLargePercent = (triage.candidate.frameCount / metrics.frameCount) * 100;
const baselineLargePercent = (triage.baseline.frameCount / baseline.frameCount) * 100;
const shortPercent = metrics.shortTrackFraction * 100;
const baselineShortPercent = baseline.shortTrackFraction * 100;
const reviewSeconds = result.comparison.visualReview.sampleVideoSeconds
.map((value) => `${formatNumber(value, 1)} c`)
.join(" · ");
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E46F · NVIDIA mobile-detector bake-off R2"
description="Контролируемая замена только детектора: тот же RIGHT-архив, DeepStream 9.1, FP16, NvDCF и 800×600. TrafficCamNet E46E заменён на готовый DashCamNet DetectNet_v2, обученный под движущуюся камеру."
status="Rejected · temporal progress, semantic regression on unrectified fisheye"
statusTone="warning"
facts={[
{
label: "Конфигурация",
value: `${rigLabel} · RIGHT recorded replay · detector-only A/B · 4489/4489`,
},
{
label: "Кандидат",
value: "NVIDIA DashCamNet · DetectNet_v2 ResNet18 · signed ONNX v1.0.4 · FP16",
},
{
label: "Контроль",
value: "NVIDIA NvDCF performance profile · без Mission Core NMS, association, hold или stitch",
},
{
label: "Предквалификация",
value: "v1.0.5 отклонён до LAB: на реальных кадрах ненулевым был только один confidence-канал",
},
]}
brief={{
question: "Станет ли готовый moving-camera DashCamNet лучшим detector drop-in для нашего сырого fisheye RIGHT-потока, если весь остальной NVIDIA-контур оставить неизменным?",
approach: "На Worker 006 весь проход пересчитан при одном контролируемом изменении — TrafficCamNet RT-DETR заменён на DashCamNet DetectNet_v2. Официальные NMS-параметры NVIDIA и точный model hash заморожены. Результат проверен не только числами: опубликован полный overlay-видос и отдельно просмотрены повторяющиеся крупные рамки на краях fisheye.",
principalResult: `Continuity немного улучшилась: пустые track-кадры ${metrics.zeroTrackFrameCount} против ${baseline.zeroTrackFrameCount}, blackout ${metrics.fullLayerBlackoutEventCount} против ${baseline.fullLayerBlackoutEventCount}, route-ID ${metrics.uniqueTrackCount} против ${baseline.uniqueTrackCount}. Но крупные рамки ≥20% кадра появились на ${triage.candidate.frameCount} кадрах против ${triage.baseline.frameCount} у E46E; визуально это повторяющиеся person-треки на чёрной кайме и краевой геометрии.`,
limitation: "Large-box triage — диагностический указатель для навигации по видео, а не precision/recall. Независимой full-route truth нет; LAB не оценивает motion-state, LiDAR range, free-space, live hardware, navigation или safety.",
}}
method={result.method}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="E46F VISUAL EVIDENCE · DETECTOR-ONLY FULL-ROUTE A/B"
title="Полный RIGHT-видос: DashCamNet + NvDCF и семантическая регрессия на краях fisheye"
kind="diagnostic-model"
resizable
>
<E46EReadyStackVisual
videoUrl={result.video.url}
label="E46F DashCamNet detector-only full recorded RIGHT overlay"
ariaLabel="E46F full recorded RIGHT video with DashCamNet detections and NvDCF track IDs"
/>
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="DashCamNet отклонён на сыром fisheye; следующий контракт — rectification перед detector"
status={`Visual reject · контрольные эпизоды: ${reviewSeconds}`}
statusTone="warning"
metrics={[
{
label: "Empty / blackout",
value: `${metrics.zeroTrackFrameCount} / ${metrics.fullLayerBlackoutEventCount}`,
hint: `E46E: ${baseline.zeroTrackFrameCount} / ${baseline.fullLayerBlackoutEventCount} · temporal progress`,
},
{
label: "Route ID",
value: `${metrics.uniqueTrackCount}`,
hint: `E46E: ${baseline.uniqueTrackCount} · short ${formatNumber(shortPercent, 1)}% vs ${formatNumber(baselineShortPercent, 1)}%`,
},
{
label: "Large-box frames",
value: `${triage.candidate.frameCount}/${metrics.frameCount}`,
hint: `${formatNumber(candidateLargePercent, 1)}% · E46E ${triage.baseline.frameCount} (${formatNumber(baselineLargePercent, 1)}%)`,
},
{
label: "Large-box class",
value: Object.keys(triage.candidate.classObservations).join(", "),
hint: `${triage.candidate.observationCount} observations · ${triage.candidate.trackIdCount} route IDs`,
},
]}
conclusion={{
proved: `Готовый DashCamNet действительно сокращает фрагментацию: ${metrics.uniqueTrackCount} ID вместо ${baseline.uniqueTrackCount}, ${formatNumber(shortPercent, 1)}% коротких треков вместо ${formatNumber(baselineShortPercent, 1)}%. Все ${metrics.frameCount} кадров и полное видео связаны immutable identity; E46F не содержит собственной detector/tracker-логики Mission Core.`,
notProved: `Числа continuity не означают корректное зрение. На ${triage.candidate.frameCount} кадрах видны очень крупные person-рамки, регулярно захватывающие чёрную кайму и край fisheye. Поэтому E46F не принят как perception candidate и не получает motion, free-space, navigation или safety authority.`,
decision: "Не чинить DashCamNet порогами под этот ролик. Сохранить E46E текущим готовым baseline. Следующий detector bake-off запускать только после одной calibration-derived rectification/valid-FOV projection, одинаковой для всех перспективных моделей.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,108 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { E46GRectifiedDetectorBakeoffResult } from "../../core/laboratory/e46gRectifiedDetectorBakeoff";
import { formatNumber } from "../../presentation";
import { E46GRectifiedDetectorBakeoffVisual } from "./E46GRectifiedDetectorBakeoffVisual";
export function E46GRectifiedDetectorBakeoffResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: E46GRectifiedDetectorBakeoffResult;
}) {
const traffic = result.metrics.trafficcamnet;
const dash = result.metrics.dashcamnet;
const trafficFront = traffic.views.front;
const dashFront = dash.views.front;
const trafficSideLarge = traffic.views.left.largeTrackObservationCount
+ traffic.views.right.largeTrackObservationCount;
const dashSideLarge = dash.views.left.largeTrackObservationCount
+ dash.views.right.largeTrackObservationCount;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E46G · calibrated ready-detector bake-off R3"
description="Один физический RIGHT-поток recorded rig, factory KB4 и штатный NVIDIA nvdewarper. TrafficCamNet и DashCamNet пересчитаны на одних 600 кадрах и одних LEFT / FRONT / RIGHT проекциях с неизменным NvDCF."
status="Selected for next diagnostic · TrafficCamNet FRONT only"
statusTone="accent"
facts={[
{
label: "Конфигурация",
value: `${rigLabel} · одна RIGHT-запись · frames ${result.selection.firstSourceFrameIndex}${result.selection.lastSourceFrameIndex}`,
},
{
label: "Геометрия",
value: `Factory camera_1 KB4 · NVIDIA nvdewarper · 3 × ${result.rectification.outputResolution[0]}×${result.rectification.outputResolution[1]} · FOV ${result.rectification.horizontalFovDegrees}°`,
},
{
label: "Кандидаты",
value: "TrafficCamNet Transformer Lite RT-DETR vs DashCamNet DetectNet_v2 · FP16",
},
{
label: "Temporal",
value: "Stock NVIDIA NvDCF performance profile · identity остаётся view-local",
},
]}
brief={{
question: "Какой готовый NVIDIA-детектор лучше переносится на калиброванную перспективную проекцию нашего единственного RIGHT-потока и годится для следующего полного recorded replay?",
approach: "На Worker 006 factory KB4 подан в официальный Gst-nvdewarper. Оба детектора получили одни и те же 600 последовательных кадров, три синхронные проекции и один stock NvDCF. Решение принято по двум полным 60-секундным видео, а counts использованы только как навигация по визуалу.",
principalResult: `FRONT пригоден: TrafficCamNet удерживает видимые машины и людей заметно полнее — ${trafficFront.trackObservationCount} track-observations и ${trafficFront.zeroTrackFrameCount} пустых track-кадров против ${dashFront.trackObservationCount} и ${dashFront.zeroTrackFrameCount} у DashCamNet. LEFT/RIGHT непригодны: в них доминирует корпус крепления, на котором TrafficCamNet создаёт крупные ложные треки.`,
limitation: "Независимой исчерпывающей truth нет. У TrafficCamNet остаются дубли и stale boxes вокруг отдельных машин; E46G выбирает только следующий диагностический full-route кандидат и не принимает perception, motion-state, free-space, navigation или safety.",
}}
method={result.method}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="E46G VISUAL EVIDENCE · CALIBRATED DETECTOR A/B"
title="Два синхронных 60-секундных видео · колонки LEFT / FRONT / RIGHT"
kind="diagnostic-model"
resizable
>
<E46GRectifiedDetectorBakeoffVisual videos={result.videos} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="TrafficCamNet выбран для полного FRONT replay; боковые проекции исключены"
status="Diagnostic selection · no perception authority"
statusTone="accent"
metrics={[
{
label: "Traffic FRONT",
value: `${trafficFront.trackObservationCount} tracks`,
hint: `${trafficFront.zeroTrackFrameCount}/600 empty · ${trafficFront.uniqueTrackCount} view-local ID`,
},
{
label: "Dash FRONT",
value: `${dashFront.trackObservationCount} tracks`,
hint: `${dashFront.zeroTrackFrameCount}/600 empty · ${dashFront.uniqueTrackCount} view-local ID`,
},
{
label: "FRONT large-box",
value: `${formatNumber(trafficFront.largeTrackFraction * 100, 2)}% / ${formatNumber(dashFront.largeTrackFraction * 100, 2)}%`,
hint: "TrafficCamNet / DashCamNet · диагностический порог ≥20% plane",
},
{
label: "LEFT+RIGHT large",
value: `${trafficSideLarge} / ${dashSideLarge}`,
hint: "TrafficCamNet / DashCamNet · визуально связано с корпусом крепления",
},
]}
conclusion={{
proved: "Factory KB4 и штатный NVIDIA nvdewarper дают читаемую FRONT-перспективу из одной физической RIGHT-камеры. На одинаковом gate TrafficCamNet визуально сохраняет значительно больше реальных транспортных объектов и людей, тогда как DashCamNet часто оставляет видимые объекты без рамок.",
notProved: "Counts не являются accuracy. TrafficCamNet ещё дублирует отдельные машины, а LEFT/RIGHT содержат сильную self-occlusion корпуса. E46G не доказывает cross-view identity, динамику/статику, LiDAR range, free-space или работу на другом маршруте.",
decision: "Не тюнить модель под ролик и не использовать боковые проекции. E46H считать на полном retained route только для FRONT: готовый TrafficCamNet + stock NvDCF, затем проверить непрерывность всего видео и отдельно разобрать дубли/stale tracks.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,45 @@
import { useState } from "react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import type {
E46GCandidate,
E46GVideo,
} from "../../core/laboratory/e46gRectifiedDetectorBakeoff";
const MODES = [
{ value: "trafficcamnet", label: "TRAFFICCAMNET" },
{ value: "dashcamnet", label: "DASHCAMNET" },
] as const;
export function E46GRectifiedDetectorBakeoffVisual({
videos,
}: {
videos: Readonly<Record<E46GCandidate, E46GVideo>>;
}) {
const [candidate, setCandidate] = useState<E46GCandidate>("trafficcamnet");
const [expanded, setExpanded] = useState(false);
const selected = videos[candidate];
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="E46G synchronized calibrated detector comparison"
mode={candidate}
modes={MODES}
expanded={expanded}
onModeChange={setCandidate}
onExpandedChange={setExpanded}
>
<div className="e46e-ready-stack-video">
<video
key={selected.url}
controls
playsInline
preload="metadata"
src={selected.url}
aria-label={`E46G ${candidate} synchronized left front right comparison video`}
/>
</div>
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,106 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type {
E46HFullRectifiedFrontReplayResult,
} from "../../core/laboratory/e46hFullRectifiedFrontReplay";
import { formatNumber } from "../../presentation";
import { E46HFullRectifiedFrontReplayVisual } from "./E46HFullRectifiedFrontReplayVisual";
export function E46HFullRectifiedFrontReplayResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: E46HFullRectifiedFrontReplayResult;
}) {
const metrics = result.metrics;
const falseSemanticWindows = result.visualReview.reviewWindows.filter(
({ verdict }) => verdict === "semantic-false-positive",
);
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E46H · full calibrated FRONT replay R1"
description="Полный retained prefix единственной RIGHT-записи: factory KB4 → штатный NVIDIA nvdewarper FRONT → готовый TrafficCamNet → stock NvDCF. Без собственного детектора, трекера и route-specific постобработки."
status="Diagnostic regression · large semantic false tracks"
statusTone="warning"
facts={[
{
label: "Конфигурация",
value: `${rigLabel} · одна RIGHT-запись · frames 0…4487 из 4489`,
},
{
label: "Геометрия",
value: `FRONT ${result.rectification.outputResolution[0]}×${result.rectification.outputResolution[1]} · FOV ${result.rectification.horizontalFovDegrees}° · ${result.rectification.providerVersion}`,
},
{
label: "Provider stack",
value: "TrafficCamNet Transformer Lite RT-DETR + NVIDIA NvDCF performance profile",
},
{
label: "Длительность",
value: `448,8 с seekable overlay · ${metrics.frameCount} последовательных кадров`,
},
]}
brief={{
question: "Сохраняет ли выбранный готовый NVIDIA FRONT-stack полезные объекты на всём recorded route и можно ли после короткого bake-off продвигать его в perception?",
approach: "На Worker 006 полностью пересчитан неизменяемый retained prefix. В интерфейс опубликован весь seekable overlay, а числовые исключения связаны с конкретными окнами видео: пять крупных ложных semantic tracks и два zero-track интервала.",
principalResult: `FRONT читаем и большую часть маршрута держит видимые машины и людей. Но обнаружено ${falseSemanticWindows.length} крупных ложных car-треков на стене, кусте, дороге и террасе. Оба zero-track события в конце показывают реально пустую сцену — это не отказ object layer.`,
limitation: "Независимой полной truth нет, поэтому counts не являются precision/recall. Класс, route-local ID, physical identity, LiDAR range и dynamic/static — разные слои доказательств; E46H принимает только диагностический recorded replay.",
}}
method={result.method}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="E46H VISUAL EVIDENCE · FULL RETAINED FRONT ROUTE"
title="Полное 448,8-секундное видео + навигация по проверенным исключениям"
kind="diagnostic-model"
resizable
>
<E46HFullRectifiedFrontReplayVisual result={result} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="FRONT continuity подтверждена; текущий ready-provider не принят из-за semantic false tracks"
status="Blocked for promotion · no perception authority"
statusTone="warning"
metrics={[
{
label: "Tracked observations",
value: metrics.trackObservationCount.toLocaleString("ru-RU"),
hint: `${formatNumber(metrics.meanTrackedObjectsPerFrame, 2)} на кадр · ${metrics.uniqueTrackCount} route-local ID`,
},
{
label: "Пустые track-кадры",
value: `${metrics.zeroTrackFrameCount}/${metrics.frameCount}`,
hint: "два интервала · визуально реальная пустая сцена",
},
{
label: "Short tracks",
value: `${formatNumber(metrics.shortTrackFraction * 100, 2)}%`,
hint: `${metrics.shortTrackCount} из ${metrics.uniqueTrackCount} ID`,
},
{
label: "Large-box observations",
value: metrics.largeTrackObservationCount.toLocaleString("ru-RU"),
hint: `${formatNumber(metrics.largeTrackFraction * 100, 3)}% · пять ложных semantic tracks`,
},
]}
conclusion={{
proved: "Один физический RIGHT-поток можно штатно преобразовать в читаемый FRONT и полностью воспроизвести готовым NVIDIA detector/tracker stack. Object layer не исчезает в двух tail-интервалах: там действительно нет объектов.",
notProved: "Stack не доказал достаточную семантическую точность: длительные крупные car-треки закрепляются за фоном и местами перекрывают значительную часть кадра. E46H не принимает perception, motion-state, free-space, navigation или safety.",
decision: "Не подгонять TrafficCamNet под этот маршрут. Сохранить калиброванный FRONT adapter и full-route replay как неизменный benchmark; дальше сравнить на нём другой готовый provider или provider-level semantic suppression, затем снова проверить полное видео.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,103 @@
import { useEffect, useRef, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import type {
E46HFullRectifiedFrontReplayResult,
} from "../../core/laboratory/e46hFullRectifiedFrontReplay";
const MODES = [{ value: "video", label: "VIDEO" }] as const;
export function E46HFullRectifiedFrontReplayVisual({
result,
}: {
result: E46HFullRectifiedFrontReplayResult;
}) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const [selectedWindowId, setSelectedWindowId] = useState("full-route");
const [expanded, setExpanded] = useState(false);
const windows = result.visualReview.reviewWindows;
const selectedWindow = windows.find(({ id }) => id === selectedWindowId) ?? null;
useEffect(() => {
const video = videoRef.current;
if (!video || !selectedWindow) return;
video.pause();
video.currentTime = selectedWindow.startSeconds;
}, [selectedWindow]);
const navigate = (offset: -1 | 1) => {
const currentIndex = windows.findIndex(({ id }) => id === selectedWindowId);
const nextIndex = currentIndex < 0
? (offset > 0 ? 0 : windows.length - 1)
: (currentIndex + offset + windows.length) % windows.length;
setSelectedWindowId(windows[nextIndex]?.id ?? "full-route");
};
const actions = (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущее окно E46H" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующее окно E46H" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать окно полного видео E46H"
value={selectedWindowId}
options={[
{ value: "full-route", label: "Весь проход · 0.0448.8 с" },
...windows.map((window) => ({
value: window.id,
label: window.label,
})),
]}
variant="split"
menuWidth="anchor"
onChange={setSelectedWindowId}
/>
</div>
);
const overlay = selectedWindow ? (
<div className="l3-visual-audit__overlay">
<div>
<span>E46H · визуальная проверка</span>
<strong>{selectedWindow.label}</strong>
<small>
{selectedWindow.verdict === "empty-scene-expected"
? "Пустая сцена подтверждена визуально"
: `Ложный крупный semantic track ${selectedWindow.sourceTrackId ?? "—"}`}
</small>
</div>
</div>
) : null;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="E46H full FRONT tracked replay"
mode="video"
modes={MODES}
expanded={expanded}
onModeChange={() => undefined}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
<div className="e46e-ready-stack-video">
<video
ref={videoRef}
controls
playsInline
preload="metadata"
src={result.video.url}
aria-label="E46H full FRONT tracked replay video"
/>
</div>
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,101 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { E46IGroundingDinoFullReplayResult } from "../../core/laboratory/e46iGroundingDinoFullReplay";
import { formatNumber } from "../../presentation";
import { E46IGroundingDinoFullReplayVisual } from "./E46IGroundingDinoFullReplayVisual";
export function E46IGroundingDinoFullReplayResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: E46IGroundingDinoFullReplayResult;
}) {
const metrics = result.metrics;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E46I · Grounding DINO full FRONT shadow R1"
description="Тот же неизменяемый RIGHT → KB4 FRONT источник E46H, но другой готовый NVIDIA provider: коммерческий Grounding DINO Swin-Tiny через TAO Deploy/TensorRT FP16. Без route-specific тюнинга и собственного детекторного postprocessing."
status="Semantic progress · temporal layer pending"
statusTone="success"
facts={[
{
label: "Конфигурация",
value: `${rigLabel} · одна RIGHT-запись · FRONT 960×544 · ${result.source.frameCount} кадров`,
},
{
label: "Provider",
value: `${result.provider.name} ${result.provider.version} · ${result.provider.precision}`,
},
{
label: "Фиксированный контракт",
value: `${result.inference.captions.join(" · ")} · threshold ${result.inference.confidenceThreshold}`,
},
{
label: "Recorded throughput",
value: `${formatNumber(metrics.workerMeanFramesPerSecond, 2)} fps на RTX 4090 · ${formatNumber(metrics.workerElapsedSeconds, 0)} с`,
},
]}
brief={{
question: "Убирает ли готовый open-vocabulary NVIDIA detector крупные фоновые car-галлюцинации E46H, не теряя полезные машины и людей на полном recorded route?",
approach: "Сначала выполнен фиксированный 11-кадровый shadow gate, затем без изменения threshold пересчитаны все 4488 FRONT-кадров. В LAB опубликованы полный seekable overlay, обзор всего маршрута и отдельная сетка старых failure windows.",
principalResult: `Все ${result.shadowGate.suppressedCases}/${result.shadowGate.legacyCases} крупных фоновых случаев E46H подавлены. Все ${result.shadowGate.positiveCasesWithRelevantDetection}/${result.shadowGate.positiveCases} позитивных anchor-кадров сохранили релевантный объект. Это материальная семантическая прогрессия, а не финальное принятие perception.`,
limitation: "Выход пока покадровый: постоянных ID, dynamic/static и LiDAR range здесь нет. Caption ontology ограничена четырьмя классами; коляска названа bicycle, один частично обрезанный person пропущен. Tokenizer во время этого запуска скачивался из сети.",
}}
method={result.method}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="E46I VISUAL EVIDENCE · FULL ROUTE + LEGACY FAILURE WINDOWS"
title="Полное 448,8-секундное видео, route overview и shadow-gate"
kind="diagnostic-model"
resizable
>
<E46IGroundingDinoFullReplayVisual result={result} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Семантическая регрессия E46H снята; следующий блокер — temporal identity"
status="Progress confirmed · not perception authority"
statusTone="success"
metrics={[
{
label: "Legacy false backgrounds",
value: `${result.shadowGate.suppressedCases}/${result.shadowGate.legacyCases} suppressed`,
hint: "стена · куст · земля · дорога · терраса",
},
{
label: "Detection observations",
value: metrics.detectionObservationCount.toLocaleString("ru-RU"),
hint: `${formatNumber(metrics.meanDetectionsPerFrame, 2)} на кадр · max ${metrics.maxDetectionsPerFrame}`,
},
{
label: "Zero-detection frames",
value: `${metrics.zeroDetectionFrameCount}/${metrics.frameCount}`,
hint: `longest run ${metrics.longestZeroDetectionRunFrames} кадров`,
},
{
label: "Large boxes ≥25%",
value: metrics.largeBoxObservationCount.toLocaleString("ru-RU"),
hint: `${formatNumber(metrics.largeBoxObservationFraction * 100, 3)}% · визуально близкие реальные машины`,
},
]}
conclusion={{
proved: "Готовый NVIDIA Grounding DINO на том же FRONT adapter принципиально чище TrafficCamNet на этом recorded route: пять известных крупных фоновых false-car случаев исчезли, а полезные объекты остались видимыми.",
notProved: "Покадровый detector ещё не доказывает стабильную identity, удержание рамки, dynamic/static, физическую природу неизвестного объекта, LiDAR range, free-space или безопасность. Полной независимой truth по маршруту нет.",
decision: "Зафиксировать Grounding DINO как прошедший semantic provider shadow, не подгонять его под ролик. Следом сделать offline-seal tokenizer и подключить готовый temporal tracker; после этого публиковать второе полное видео уже с постоянными ID и motion-state.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,126 @@
import { useEffect, useRef, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import type { E46IGroundingDinoFullReplayResult } from "../../core/laboratory/e46iGroundingDinoFullReplay";
const MODES = [
{ value: "video", label: "VIDEO" },
{ value: "targeted", label: "FAILURES" },
{ value: "full-route", label: "ROUTE" },
{ value: "shadow-gate", label: "GATE" },
] as const;
type Mode = (typeof MODES)[number]["value"];
export function E46IGroundingDinoFullReplayVisual({
result,
}: {
result: E46IGroundingDinoFullReplayResult;
}) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const [mode, setMode] = useState<Mode>("video");
const [selectedWindowId, setSelectedWindowId] = useState("full-route");
const [expanded, setExpanded] = useState(false);
const windows = result.visualReview.reviewWindows;
const selectedWindow = windows.find(({ id }) => id === selectedWindowId) ?? null;
useEffect(() => {
const video = videoRef.current;
if (!video || !selectedWindow) return;
video.pause();
video.currentTime = selectedWindow.startSeconds;
}, [selectedWindow]);
const navigate = (offset: -1 | 1) => {
const currentIndex = windows.findIndex(({ id }) => id === selectedWindowId);
const nextIndex = currentIndex < 0
? (offset > 0 ? 0 : windows.length - 1)
: (currentIndex + offset + windows.length) % windows.length;
setSelectedWindowId(windows[nextIndex]?.id ?? "full-route");
setMode("video");
};
const actions = mode === "video" ? (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущее окно E46I" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующее окно E46I" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать окно полного видео E46I"
value={selectedWindowId}
options={[
{ value: "full-route", label: "Весь проход · 0.0448.8 с" },
...windows.map((window) => ({ value: window.id, label: window.label })),
]}
variant="split"
menuWidth="anchor"
onChange={setSelectedWindowId}
/>
</div>
) : null;
const overlay = mode === "video" && selectedWindow ? (
<div className="l3-visual-audit__overlay">
<div>
<span>E46I · provider comparison</span>
<strong>{selectedWindow.label}</strong>
<small>
{selectedWindow.verdict === "empty-scene-mostly-preserved"
? "Пустая сцена в основном сохранена"
: "Крупная фоновая car-галлюцинация E46H подавлена"}
</small>
</div>
</div>
) : null;
const image = mode === "targeted"
? result.visuals.targetedWindows
: mode === "full-route"
? result.visuals.fullRoute
: result.visuals.shadowGate;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="E46I Grounding DINO full FRONT replay"
mode={mode}
modes={MODES}
expanded={expanded}
onModeChange={(value) => setMode(value as Mode)}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
<div className="e46e-ready-stack-video">
{mode === "video" ? (
<video
ref={videoRef}
controls
playsInline
preload="metadata"
src={result.video.url}
aria-label="E46I Grounding DINO full FRONT replay video"
/>
) : (
<img
src={image.url}
alt={
mode === "targeted"
? "E46I targeted legacy failure windows"
: mode === "full-route"
? "E46I full route ten-second contact sheet"
: "E46I eleven-frame semantic shadow gate"
}
/>
)}
</div>
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,101 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { E46JRawFisheyeRealtimeResult } from "../../core/laboratory/e46jRawFisheyeRealtime";
import { formatNumber } from "../../presentation";
import { E46JRawFisheyeRealtimeVisual } from "./E46JRawFisheyeRealtimeVisual";
export function E46JRawFisheyeRealtimeResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: E46JRawFisheyeRealtimeResult;
}) {
const metrics = result.metrics;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E46J · full raw fisheye realtime gate"
description="Одна физическая RIGHT-камера, один полный сырой KB4-кадр и один YOLOX-S inference. Без кропов, dewarp, виртуальных камер, тайлов, route-specific фильтров и собственного detector logic."
status="Realtime capacity passed · temporal layer pending"
statusTone="success"
facts={[
{
label: "Конфигурация",
value: `${rigLabel} · RIGHT raw KB4 800×600 · ${result.source.frameCount} кадров`,
},
{
label: "Provider",
value: `${result.detector.architecture} · ${result.detector.license} · ${result.detector.runtime}`,
},
{
label: "Фиксированный контракт",
value: `1 кадр → 1 infer · score ${result.detection.minimumScore} · NMS ${result.detection.nmsIouThreshold}`,
},
{
label: "Realtime capacity",
value: `${formatNumber(metrics.coreCapacityFps, 2)} fps · core p95 ${formatNumber(metrics.corePathP95Ms, 2)} мс`,
},
]}
brief={{
question: "Может ли готовый YOLOX-S работать одним проходом на полном сыром fisheye и сохранить исходный FOV при целевых 10 FPS?",
approach: "Все 4489 записанных RIGHT-кадров прошли через один co-located Triton inference. Видео опубликовано целиком; отдельно проверены маршрутная сетка, прежние failure windows и окно с тенью оператора.",
principalResult: `4489/4489 кадров, 0 ошибок, ${formatNumber(metrics.coreCapacityFps, 2)} fps вычислительной ёмкости. Полный fisheye сохранён; пять прежних крупных фоновых car-срабатываний в целевых окнах не наблюдаются.`,
limitation: `Это покадровый detector без ID и motion-state. В окне тени оператора person-box присутствует на ${metrics.operatorShadowPersonFrameCount}/${metrics.operatorShadowWindowFrameCount} кадров. Независимой полной truth нет.`,
}}
method={result.method}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="E46J VISUAL EVIDENCE · FULL RAW FISHEYE + TARGETED WINDOWS"
title="Полное 448,723-секундное видео без обрезки FOV"
kind="diagnostic-model"
resizable
>
<E46JRawFisheyeRealtimeVisual result={result} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Полный fisheye проходит realtime-gate; следующий блок — temporal identity"
status="Compute accepted · detector not promoted"
statusTone="success"
metrics={[
{
label: "Core capacity",
value: `${formatNumber(metrics.coreCapacityFps, 2)} fps`,
hint: `mean ${formatNumber(metrics.corePathMeanMs, 2)} мс · p95 ${formatNumber(metrics.corePathP95Ms, 2)} мс`,
},
{
label: "Inference request",
value: `${formatNumber(metrics.inferenceRequestP95Ms, 2)} мс p95`,
hint: `mean ${formatNumber(metrics.inferenceRequestMeanMs, 2)} мс · local GPU path`,
},
{
label: "Detection observations",
value: metrics.detectionObservationCount.toLocaleString("ru-RU"),
hint: `${formatNumber(metrics.meanDetectionsPerFrame, 2)} на кадр · max ${metrics.maxDetectionsPerFrame}`,
},
{
label: "Known shadow FP",
value: `${metrics.operatorShadowPersonFrameCount}/${metrics.operatorShadowWindowFrameCount} кадров`,
hint: "419.4426.9 с · операторская тень → person",
},
]}
conclusion={{
proved: "Готовый YOLOX-S способен работать одним проходом по полному raw KB4 fisheye с запасом относительно 10 FPS. Обрезать исходный FOV или считать несколько виртуальных камер для detector-stage не требуется.",
notProved: "Покадровые рамки ещё не доказывают устойчивые ID, удержание объекта, drop/recovery, dynamic/static, LiDAR range или безопасность. Полной независимой разметки маршрута нет; известен false person на тени оператора.",
decision: result.decision.nextAction,
}}
/>
)}
/>
);
}
@@ -0,0 +1,126 @@
import { useEffect, useRef, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import type { E46JRawFisheyeRealtimeResult } from "../../core/laboratory/e46jRawFisheyeRealtime";
const MODES = [
{ value: "video", label: "VIDEO" },
{ value: "targeted", label: "FAILURES" },
{ value: "full-route", label: "ROUTE" },
{ value: "operator-shadow", label: "SHADOW" },
] as const;
type Mode = (typeof MODES)[number]["value"];
export function E46JRawFisheyeRealtimeVisual({
result,
}: {
result: E46JRawFisheyeRealtimeResult;
}) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const [mode, setMode] = useState<Mode>("video");
const [selectedWindowId, setSelectedWindowId] = useState("full-route");
const [expanded, setExpanded] = useState(false);
const windows = result.visualReview.reviewWindows;
const selectedWindow = windows.find(({ id }) => id === selectedWindowId) ?? null;
useEffect(() => {
const video = videoRef.current;
if (mode !== "video" || !video || !selectedWindow) return;
video.pause();
video.currentTime = selectedWindow.startSeconds;
}, [mode, selectedWindow]);
const navigate = (offset: -1 | 1) => {
const currentIndex = windows.findIndex(({ id }) => id === selectedWindowId);
const nextIndex = currentIndex < 0
? (offset > 0 ? 0 : windows.length - 1)
: (currentIndex + offset + windows.length) % windows.length;
setSelectedWindowId(windows[nextIndex]?.id ?? "full-route");
setMode("video");
};
const actions = mode === "video" ? (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущее окно E46J" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующее окно E46J" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать окно полного видео E46J"
value={selectedWindowId}
options={[
{ value: "full-route", label: "Весь проход · 0.0448.723 с" },
...windows.map((window) => ({ value: window.id, label: window.label })),
]}
variant="split"
menuWidth="anchor"
onChange={setSelectedWindowId}
/>
</div>
) : null;
const overlay = mode === "video" && selectedWindow ? (
<div className="l3-visual-audit__overlay">
<div>
<span>E46J · raw fisheye one-pass</span>
<strong>{selectedWindow.label}</strong>
<small>
{selectedWindow.verdict === "operator-shadow-person-false-positive-observed"
? "Известное исключение · тень оператора → person"
: "Прежняя крупная фоновая car-ошибка не наблюдается"}
</small>
</div>
</div>
) : null;
const image = mode === "targeted"
? result.visuals.targetedWindows
: mode === "full-route"
? result.visuals.fullRoute
: result.visuals.operatorShadow;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="E46J full raw fisheye YOLOX-S realtime gate"
mode={mode}
modes={MODES}
expanded={expanded}
onModeChange={(value) => setMode(value as Mode)}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
<div className="e46e-ready-stack-video">
{mode === "video" ? (
<video
ref={videoRef}
controls
playsInline
preload="metadata"
src={result.video.url}
aria-label="E46J full raw fisheye YOLOX-S video"
/>
) : (
<img
src={image.url}
alt={
mode === "targeted"
? "E46J targeted legacy failure windows"
: mode === "full-route"
? "E46J full raw fisheye route contact sheet"
: "E46J operator shadow false-person exception"
}
/>
)}
</div>
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,77 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { L32PointPillarsCameraReviewResult } from "../../core/laboratory/l32PointPillarsCameraReview";
import { L32PointPillarsCameraReviewVisual } from "./L32PointPillarsCameraReviewVisual";
function percent(value: number): string {
return `${(value * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
}
export function L32PointPillarsCameraReviewResult({
result,
}: {
result: L32PointPillarsCameraReviewResult;
}) {
const metrics = result.metrics;
return (
<LaboratoryWorkTemplate
summary={<LaboratorySummary
title="L3.2 · Камера + LiDAR · PointPillars на RAVNOVES00"
description="Семантическая проверка запечатанного PointPillars-прогона: точный кадр правой камеры, калиброванные точки LiDAR и те же модельные 3D-гипотезы в одном viewer."
status="Проверено · текущий кандидат отклонён"
statusTone="danger"
facts={[
{ label: "Источник", value: `RAVNOVES00 · ${metrics.reviewFrameCount} camera-bound кадров` },
{ label: "Синхронизация", value: `p95 ${metrics.cameraBindingAbsoluteDeltaMs.p95.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} мс` },
{ label: "Режимы", value: "CAM / 3D / BEV · LiDAR overlay отключаемый" },
{ label: "Полномочия", value: "Shadow-only · accuracy и safety не приняты" },
]}
brief={{
question: "Соответствуют ли cross-domain гипотезы PointPillars видимым объектам RAVNOVES00?",
approach: "17 route-wide кадров старой L3.1 привязаны к ближайшему точному кадру правой камеры. Точки и 3D-боксы спроецированы неизменённой заводской KB4-калибровкой; повторный inference не выполнялся.",
principalResult: `${percent(metrics.scoreBelow025Fraction)} опубликованных гипотез имеют score ниже 0,25, ${percent(metrics.scoreBelow050Fraction)} — ниже 0,50. Камера делает семантическое несоответствие визуально проверяемым.`,
limitation: "Камера не заменяет независимый 3D ground truth, поэтому precision/recall не вычисляются. Эта работа доказательно отклоняет текущий кандидат, но не измеряет точность будущего детектора.",
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: "l32-pointpillars-camera-review/v1",
components: [
{ kind: "source", name: result.sourceL31ResultId, version: "sealed L3.1", role: "неизменённые точки и гипотезы PointPillars", identitySha256: result.sourceL31ResultId.split("-").at(-1) ?? null },
{ kind: "source", name: "sensor.camera.right", version: result.sourceSessionId, role: "семантический camera reference", identitySha256: null },
{ kind: "algorithm", name: "Factory KB4 projection", version: "camera_1 · 800×600", role: "проекция LiDAR и 3D-боксов в кадр", identitySha256: null },
{ kind: "runtime", name: "Mission Core local evidence builder", version: result.resultId, role: "последовательная immutable-материализация", identitySha256: result.resultId.split("-").at(-1) ?? null },
],
}}
/>}
evidence={<LaboratoryEvidence
eyebrow="RAVNOVES00 → CAMERA-BOUND ДОКАЗАТЕЛЬСТВО"
title="Камера, точки LiDAR и гипотезы PointPillars"
kind="diagnostic-model"
resizable
>
<L32PointPillarsCameraReviewVisual result={result} />
</LaboratoryEvidence>}
result={<LaboratoryResultSummary
title="Runtime работает, но текущая модель семантически непригодна"
status="Кандидат отклонён"
statusTone="danger"
metrics={[
{ label: "Camera-bound frames", value: metrics.reviewFrameCount.toLocaleString("ru-RU"), hint: `из 18 старых visual frames; 1 был до начала камеры` },
{ label: "Camera sync p95", value: `${metrics.cameraBindingAbsoluteDeltaMs.p95.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} мс`, hint: `max ${metrics.cameraBindingAbsoluteDeltaMs.maximum.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} мс` },
{ label: "Score < 0,25", value: percent(metrics.scoreBelow025Fraction), hint: `${metrics.reviewPredictionCount.toLocaleString("ru-RU")} гипотез в ревизии` },
{ label: "Видимые projected boxes", value: metrics.reviewVisibleProjectedBoxCount.toLocaleString("ru-RU"), hint: "контуры, попавшие в camera FOV" },
]}
conclusion={{
proved: "Точки сенсорного рига, правая камера и PointPillars-гипотезы воспроизводимо совмещаются в одной системе просмотра; источник и калибровка привязаны к RAVNOVES00.",
notProved: "Текущий PointPillars не доказал семантическую корректность и не получает эксплуатационных полномочий. Независимой 3D-разметки по-прежнему нет.",
decision: "Старую L3.1 убрать из продуктового каталога, сохранить как audit trail. L3.2 считать отрицательным baseline; следующий detector-кандидат проверять тем же CAM → LiDAR → target evidence-контрактом.",
}}
/>}
/>
);
}
@@ -0,0 +1,156 @@
import { useEffect, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchL32PointPillarsCameraReviewFrame,
L32_REVIEW_SCORE_THRESHOLD,
type L32PointPillarsCameraReviewResult,
type L32VisualFrame,
} from "../../core/laboratory/l32PointPillarsCameraReview";
import { L3PointPillarsScene } from "./L3PointPillarsScene";
import { L32PointPillarsCameraScene } from "./L32PointPillarsCameraScene";
type ViewerMode = "cam" | "3d" | "bev";
type LidarMode = "on" | "off";
function frameLabel(frame: L32PointPillarsCameraReviewResult["frames"][number]) {
return `${frame.sessionSeconds.toLocaleString("ru-RU", {
maximumFractionDigits: 1,
})} с · кадр ${frame.frameId} · Vehicle ${frame.classCounts.Vehicle}`;
}
export function L32PointPillarsCameraReviewVisual({
result,
}: {
result: L32PointPillarsCameraReviewResult;
}) {
const [selectedFrameId, setSelectedFrameId] = useState(result.frames[0]?.frameId ?? "");
const [frame, setFrame] = useState<L32VisualFrame | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<ViewerMode>("cam");
const [lidarMode, setLidarMode] = useState<LidarMode>("on");
const [expanded, setExpanded] = useState(false);
useEffect(() => {
if (!selectedFrameId) return;
const controller = new AbortController();
setFrame(null);
setLoading(true);
setError(null);
void fetchL32PointPillarsCameraReviewFrame(
result.resultId,
selectedFrameId,
{ signal: controller.signal },
).then((next) => {
if (!controller.signal.aborted) setFrame(next);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(caught instanceof Error ? caught.message : "Кадр L3.2 недоступен.");
}
}).finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [result.resultId, selectedFrameId]);
const selectedIndex = result.frames.findIndex(({ frameId }) => frameId === selectedFrameId);
const navigate = (offset: -1 | 1) => {
if (selectedIndex < 0) return;
const index = (selectedIndex + offset + result.frames.length) % result.frames.length;
setSelectedFrameId(result.frames[index].frameId);
};
const actions = (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущий кадр RAVNOVES00" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующий кадр RAVNOVES00" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать camera-bound кадр RAVNOVES00"
value={selectedFrameId}
options={result.frames.map((item) => ({ value: item.frameId, label: frameLabel(item) }))}
variant="split"
menuWidth="anchor"
onChange={setSelectedFrameId}
/>
</div>
);
const reviewableBoxCount = frame?.projectedBoxes.filter(
({ score }) => score >= L32_REVIEW_SCORE_THRESHOLD,
).length ?? 0;
const rejectedBoxCount = (frame?.projectedBoxes.length ?? 0) - reviewableBoxCount;
const overlay = frame ? (
<div className="l3-visual-audit__overlay">
<div>
<span>RAVNOVES00 · правая камера</span>
<strong>{frame.summary.sessionSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с · кадр {frame.frameId}</strong>
<small>Δ camera/LiDAR {Math.abs(frame.summary.cameraDeltaMs).toLocaleString("ru-RU", { maximumFractionDigits: 1 })} мс</small>
</div>
<div>
<span>PointPillars · кандидаты, не truth</span>
<strong>Vehicle {frame.summary.classCounts.Vehicle} · Pedestrian {frame.summary.classCounts.Pedestrian} · Cyclist {frame.summary.classCounts.Cyclist}</strong>
<small>{reviewableBoxCount} для ревизии · {rejectedBoxCount} ниже confidence 25%</small>
</div>
<div className="l3-visual-audit__legend">
<span data-tone="warning">Жёлтый пунктир · слабый кандидат 2550%</span>
<span data-tone="prediction">Белый контур · кандидат от 50%</span>
<span>Цветные точки · метрический LiDAR overlay</span>
</div>
</div>
) : undefined;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="Camera-bound PointPillars на RAVNOVES00"
mode={mode}
modes={[
{ value: "cam", label: "CAM" },
{ value: "3d", label: "3D" },
{ value: "bev", label: "BEV" },
]}
secondaryMode={mode === "cam" ? {
value: lidarMode,
modes: [
{ value: "on", label: "L on" },
{ value: "off", label: "L off" },
],
label: "LiDAR поверх камеры",
onChange: setLidarMode,
} : undefined}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
{loading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем camera-bound кадр RAVNOVES00</span>
</div>
) : error || !frame ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{error ?? "Кадр L3.2 недоступен."}</span>
</div>
) : mode === "cam" ? (
<L32PointPillarsCameraScene frame={frame} lidarVisible={lidarMode === "on"} />
) : (
<L3PointPillarsScene frame={{
frameId: frame.frameId,
pointsXyzi: frame.pointsXyzi,
truthBoxes: [],
predictionBoxes: frame.predictionBoxes,
}} mode={mode} bevCenterX={0} bevHalfExtent={55} />
)}
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,140 @@
import { useEffect, useRef, useState } from "react";
import {
L32_REVIEW_SCORE_THRESHOLD,
type L32VisualFrame,
} from "../../core/laboratory/l32PointPillarsCameraReview";
export function L32PointPillarsCameraScene({
frame,
lidarVisible,
}: {
frame: L32VisualFrame;
lidarVisible: boolean;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [image, setImage] = useState<HTMLImageElement | null>(null);
const [error, setError] = useState(false);
useEffect(() => {
const next = new Image();
next.decoding = "async";
next.onload = () => {
setError(false);
setImage(next);
};
next.onerror = () => {
setImage(null);
setError(true);
};
next.src = frame.cameraUrl;
return () => {
next.onload = null;
next.onerror = null;
};
}, [frame.cameraUrl]);
useEffect(() => {
const host = hostRef.current;
const canvas = canvasRef.current;
if (!host || !canvas || !image) 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 ratio = Math.min(window.devicePixelRatio, 1.5);
canvas.width = Math.round(width * ratio);
canvas.height = Math.round(height * ratio);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
context.setTransform(ratio, 0, 0, ratio, 0, 0);
context.fillStyle = "#050506";
context.fillRect(0, 0, width, height);
const scale = Math.min(width / frame.cameraWidth, height / frame.cameraHeight);
const drawWidth = frame.cameraWidth * scale;
const drawHeight = frame.cameraHeight * scale;
const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
context.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
if (lidarVisible) {
for (let index = 0; index < frame.projectedPointsXyd.length; index += 3) {
const x = offsetX + frame.projectedPointsXyd[index] * scale;
const y = offsetY + frame.projectedPointsXyd[index + 1] * scale;
const depth = frame.projectedPointsXyd[index + 2];
const hue = Math.max(188, Math.min(226, 226 - depth * 1.1));
context.fillStyle = `hsla(${hue} 92% 68% / 0.72)`;
context.beginPath();
context.arc(x, y, Math.max(1.2, 1.65 * scale), 0, Math.PI * 2);
context.fill();
}
}
frame.projectedBoxes.filter(
({ score }) => score >= L32_REVIEW_SCORE_THRESHOLD,
).forEach((box) => {
const admitted = box.score >= 0.5;
context.lineWidth = Math.max(1.25, (admitted ? 1.8 : 1.55) * scale);
context.strokeStyle = admitted
? "rgba(255, 248, 232, 0.94)"
: "rgba(255, 196, 71, 0.9)";
context.setLineDash(admitted
? []
: [Math.max(4, 6 * scale), Math.max(3, 4 * scale)]);
const segments = box.segmentsXyxy;
for (let index = 0; index < segments.length; index += 4) {
context.beginPath();
context.moveTo(
offsetX + segments[index] * scale,
offsetY + segments[index + 1] * scale,
);
context.lineTo(
offsetX + segments[index + 2] * scale,
offsetY + segments[index + 3] * scale,
);
context.stroke();
}
const xValues = segments.filter((_, index) => index % 2 === 0);
const yValues = segments.filter((_, index) => index % 2 === 1);
if (xValues.length && yValues.length) {
const label = `${box.modelClass} ${(box.score * 100).toFixed(0)}%`;
const fontSize = Math.max(10, 11 * scale);
const x = offsetX + Math.min(...xValues) * scale;
const y = offsetY + Math.min(...yValues) * scale;
context.font = `600 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = context.measureText(label).width;
context.setLineDash([]);
context.fillStyle = "rgba(5, 5, 6, 0.82)";
context.fillRect(
x,
Math.max(offsetY, y - fontSize - 7),
labelWidth + 12,
fontSize + 7,
);
context.fillStyle = admitted ? "#fff8e8" : "#ffc447";
context.fillText(label, x + 6, Math.max(offsetY + fontSize, y - 5));
}
});
context.setLineDash([]);
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [frame, image, lidarVisible]);
return (
<div className="l32-camera-scene" ref={hostRef}>
<canvas
ref={canvasRef}
role="img"
aria-label={`RAVNOVES00 camera frame ${frame.frameId}${lidarVisible ? ", LiDAR on" : ", LiDAR off"}`}
/>
{error ? (
<div className="l3-visual-audit__state" role="status">
Точный кадр правой камеры недоступен.
</div>
) : null}
</div>
);
}
@@ -0,0 +1,78 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { L33CameraFirstDetectorReviewResult } from "../../core/laboratory/l33CameraFirstDetectorReview";
import { L33CameraFirstDetectorVisual } from "./L33CameraFirstDetectorVisual";
function percent(value: number): string {
return `${(value * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
}
export function L33CameraFirstDetectorReviewResult({
result,
}: {
result: L33CameraFirstDetectorReviewResult;
}) {
const metrics = result.metrics;
return (
<LaboratoryWorkTemplate
summary={<LaboratorySummary
title="L3.3 · Camera-first detector + LiDAR range · RAVNOVES00"
description="Полный записанный маршрут проходит через rectified YOLOX-S, ByteTrack, EoMT, калиброванную LiDAR-геометрию, 3D-кубоиды и ограниченную temporal-стабилизацию. Камера сохраняет объект без дальности; LiDAR добавляет только доказанную метрическую геометрию."
status="Полный replay принят · runtime ещё не подключён"
statusTone="warning"
facts={[
{ label: "Источник", value: `RAVNOVES00 · ${metrics.framesProcessed.toLocaleString("ru-RU")} кадров полного маршрута` },
{ label: "Контур", value: `${result.detector.architecture} + ByteTrack + EoMT + LiDAR + E23` },
{ label: "Производительность", value: `${metrics.effectiveComposedFps.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} кадр/с · p95 ${metrics.composedP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} мс` },
{ label: "Полномочия", value: "Shadow-only · accuracy, navigation и safety не приняты" },
]}
brief={{
question: "Собирается ли из правой камеры сенсорного рига и LiDAR единый объектный world state на всём маршруте, а не только на выбранных красивых кадрах?",
approach: "Все 4 489 кадров RAVNOVES00 обработаны rectified core-3 YOLOX-S и ByteTrack. Принятый EoMT работает как multi-rate семантическая поддержка 2 Гц; калиброванный LiDAR назначает дальность и завершает кубоид, E23 ограниченно удерживает состояние между наблюдениями. Ни один camera-only объект не получает выдуманную дальность.",
principalResult: `${metrics.routeDetectionCount.toLocaleString("ru-RU")} camera-track наблюдений получены на ${metrics.routeDetectionFrameCount.toLocaleString("ru-RU")} кадрах. LiDAR-фьюжн доступен на ${metrics.lidarFusedFrames.toLocaleString("ru-RU")} кадрах; принято ${metrics.rawAcceptedCuboids.toLocaleString("ru-RU")} исходных и ${metrics.temporalAcceptedCuboids.toLocaleString("ru-RU")} temporal-наблюдений 3D-кубоидов. ${metrics.duplicateSupportRejections.toLocaleString("ru-RU")} конфликтующих повторных назначений одной LiDAR-опоры заблокированы.`,
limitation: "Это полный recorded replay, а не живой транспорт и не независимая accuracy-разметка. EoMT остаётся multi-rate 2 Гц; следующий gate — тот же контур в тёплом worker runtime.",
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: "kb4-core3-yolox-eomt-lidar-e23-temporal/v1",
components: [
{ kind: "model", name: result.detector.architecture, version: result.detector.modelSha256, role: "camera semantic class and 2D track", identitySha256: result.detector.modelSha256 },
{ kind: "model", name: "EoMT semantic segmentation", version: `${metrics.semanticEffectiveFps.toFixed(2)} Hz multi-rate`, role: "semantic support for LiDAR association", identitySha256: null },
{ kind: "algorithm", name: "Calibrated LiDAR cuboid completion", version: result.sourceWorldStateResultId, role: "metric range and bounded amodal 3D cuboid", identitySha256: result.sourceWorldStateResultId.split("-").at(-1) ?? null },
{ kind: "algorithm", name: "E23 inline temporal state", version: "bounded hold and stitching", role: "preserve object state between qualified observations", identitySha256: null },
{ kind: "source", name: result.sourceL32ResultId, version: result.sourceSessionId, role: "exact camera frames and calibrated LiDAR overlay", identitySha256: result.sourceL32ResultId.split("-").at(-1) ?? null },
],
}}
/>}
evidence={<LaboratoryEvidence
eyebrow="RAVNOVES00 → CAMERA-FIRST ДОКАЗАТЕЛЬСТВО"
title="Объекты камеры, LiDAR-поддержка и метрическая дальность"
kind="diagnostic-model"
resizable
>
<L33CameraFirstDetectorVisual result={result} />
</LaboratoryEvidence>}
result={<LaboratoryResultSummary
title="Полный camera-first world state собран на записанном маршруте"
status="Replay gate принят · live gate следующий"
statusTone="warning"
metrics={[
{ label: "Полный маршрут", value: metrics.framesProcessed.toLocaleString("ru-RU"), hint: `${metrics.effectiveComposedFps.toFixed(2)} кадр/с effective` },
{ label: "LiDAR fusion", value: metrics.lidarFusedFrames.toLocaleString("ru-RU"), hint: `${percent(metrics.lidarFusedFrames / metrics.framesProcessed)} кадров` },
{ label: "3D cuboids", value: metrics.rawAcceptedCuboids.toLocaleString("ru-RU"), hint: "приняты до temporal hold" },
{ label: "Конфликты геометрии", value: metrics.duplicateSupportRejections.toLocaleString("ru-RU"), hint: "одна LiDAR-опора не размножает объекты" },
]}
conclusion={{
proved: "На всех 4 489 кадрах одной записи детектор, трекер, multi-rate сегментация, LiDAR-дальность, кубоид и temporal world state выполняются как один ограниченный контур с полным учётом кадров и принятым latency gate. Одна LiDAR-опора больше не может одновременно породить геометрию нескольких camera-track объектов.",
notProved: "Пока не доказаны live transport, длительная эксплуатационная устойчивость, precision/recall на независимой разметке и пригодность для navigation/safety. Визуальные пустые кадры остаются частью проверки, а не скрываются.",
decision: "Не создавать новый LAB. Эту L3.3 использовать как визуальный аудит полного replay; следующий инженерный шаг — подключить тот же состав в тёплый worker и измерить live cadence без изменения device acquisition sequence.",
}}
/>}
/>
);
}
@@ -0,0 +1,238 @@
import { useEffect, useRef, useState } from "react";
import type { L33VisualFrame } from "../../core/laboratory/l33CameraFirstDetectorReview";
function tokenColor(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
alpha = 1,
): string {
const value = getComputedStyle(host).getPropertyValue(token).trim();
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
function labelForDetection(
label: string,
rangeM: number | null,
score: number,
): string {
const names: Readonly<Record<string, string>> = {
car: "Авто",
truck: "Грузовик",
bus: "Автобус",
person: "Человек",
bicycle: "Велосипед",
motorcycle: "Мотоцикл",
};
const qualifier = rangeM === null
? `${(score * 100).toFixed(0)}%`
: `${rangeM.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} м`;
return `${names[label] ?? label} · ${qualifier}`;
}
type LabelRect = Readonly<{
x: number;
y: number;
width: number;
height: number;
}>;
function overlaps(left: LabelRect, right: LabelRect): boolean {
const margin = 3;
return !(
left.x + left.width + margin <= right.x
|| right.x + right.width + margin <= left.x
|| left.y + left.height + margin <= right.y
|| right.y + right.height + margin <= left.y
);
}
function placeLabel(
box: LabelRect,
labelWidth: number,
labelHeight: number,
imageBounds: LabelRect,
occupied: readonly LabelRect[],
): LabelRect | null {
const clampX = (value: number) => Math.min(
imageBounds.x + imageBounds.width - labelWidth,
Math.max(imageBounds.x, value),
);
const candidates: LabelRect[] = [];
const horizontal = [box.x, box.x + box.width - labelWidth];
for (let row = 0; row < 4; row += 1) {
const above = box.y - labelHeight - 3 - row * (labelHeight + 3);
const below = box.y + box.height + 3 + row * (labelHeight + 3);
for (const x of horizontal) {
candidates.push({ x: clampX(x), y: above, width: labelWidth, height: labelHeight });
candidates.push({ x: clampX(x), y: below, width: labelWidth, height: labelHeight });
}
}
candidates.push({
x: clampX(box.x + 3),
y: box.y + 3,
width: labelWidth,
height: labelHeight,
});
return candidates.find((candidate) => (
candidate.y >= imageBounds.y
&& candidate.y + candidate.height <= imageBounds.y + imageBounds.height
&& occupied.every((current) => !overlaps(candidate, current))
)) ?? null;
}
export function L33CameraFirstDetectorScene({
frame,
lidarVisible,
}: {
frame: L33VisualFrame;
lidarVisible: boolean;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [image, setImage] = useState<HTMLImageElement | null>(null);
const [error, setError] = useState(false);
useEffect(() => {
const next = new Image();
next.decoding = "async";
next.onload = () => {
setError(false);
setImage(next);
};
next.onerror = () => {
setImage(null);
setError(true);
};
next.src = frame.cameraUrl;
return () => {
next.onload = null;
next.onerror = null;
};
}, [frame.cameraUrl]);
useEffect(() => {
const host = hostRef.current;
const canvas = canvasRef.current;
if (!host || !canvas || !image) 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 ratio = Math.min(window.devicePixelRatio, 1.5);
canvas.width = Math.round(width * ratio);
canvas.height = Math.round(height * ratio);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
context.setTransform(ratio, 0, 0, ratio, 0, 0);
context.fillStyle = tokenColor(host, "--nodedc-canvas-rgb", [5, 5, 6]);
context.fillRect(0, 0, width, height);
const scale = Math.min(width / frame.cameraWidth, height / frame.cameraHeight);
const drawWidth = frame.cameraWidth * scale;
const drawHeight = frame.cameraHeight * scale;
const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
context.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
if (lidarVisible) {
for (let index = 0; index < frame.projectedPointsXyd.length; index += 3) {
const x = offsetX + frame.projectedPointsXyd[index] * scale;
const y = offsetY + frame.projectedPointsXyd[index + 1] * scale;
const depth = frame.projectedPointsXyd[index + 2];
const hue = Math.max(188, Math.min(226, 226 - depth * 1.1));
context.fillStyle = `hsla(${hue} 92% 68% / 0.56)`;
context.beginPath();
context.arc(x, y, Math.max(1.0, 1.45 * scale), 0, Math.PI * 2);
context.fill();
}
}
const rangeColor = tokenColor(host, "--nodedc-success-rgb", [143, 255, 93], 0.96);
const cameraColor = tokenColor(host, "--nodedc-text-primary-rgb", [247, 248, 244], 0.92);
const glassColor = tokenColor(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.82);
const boxes = frame.detections.map((detection) => {
const [left, top, right, bottom] = detection.bboxXyxy;
const x = offsetX + left * scale;
const y = offsetY + top * scale;
const boxWidth = (right - left) * scale;
const boxHeight = (bottom - top) * scale;
const ranged = detection.rangeM !== null;
context.strokeStyle = ranged ? rangeColor : cameraColor;
context.lineWidth = Math.max(1.25, 1.8 * scale);
context.setLineDash(
detection.semanticProvenance === "bounded-track-interpolation"
|| detection.semanticProvenance === "e23-temporal-hold"
? [Math.max(4, 6 * scale), Math.max(3, 4 * scale)]
: [],
);
context.strokeRect(x, y, boxWidth, boxHeight);
return { detection, x, y, width: boxWidth, height: boxHeight, ranged };
});
const occupied: LabelRect[] = [];
const imageBounds = { x: offsetX, y: offsetY, width: drawWidth, height: drawHeight };
[...boxes]
.sort((left, right) => (
Number(right.ranged) - Number(left.ranged)
|| right.width * right.height - left.width * left.height
))
.forEach(({ detection, x, y, width: boxWidth, height: boxHeight, ranged }) => {
const label = labelForDetection(
detection.label,
detection.rangeM,
detection.score,
);
const fontSize = Math.max(9, 10 * scale);
const labelHeight = fontSize + 7;
context.font = `600 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = context.measureText(label).width + 12;
const placement = placeLabel(
{ x, y, width: boxWidth, height: boxHeight },
labelWidth,
labelHeight,
imageBounds,
occupied,
);
if (!placement) return;
occupied.push(placement);
context.setLineDash([]);
context.fillStyle = glassColor;
context.fillRect(
placement.x,
placement.y,
placement.width,
placement.height,
);
context.fillStyle = ranged ? rangeColor : cameraColor;
context.fillText(
label,
placement.x + 6,
placement.y + fontSize + 1,
);
});
context.setLineDash([]);
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [frame, image, lidarVisible]);
return (
<div className="l32-camera-scene" ref={hostRef}>
<canvas
ref={canvasRef}
role="img"
aria-label={`RAVNOVES00 camera-first frame ${frame.frameId}${lidarVisible ? ", LiDAR on" : ", LiDAR off"}`}
/>
{error ? (
<div className="l3-visual-audit__state" role="status">
Точный кадр правой камеры недоступен.
</div>
) : null}
</div>
);
}
@@ -0,0 +1,169 @@
import { useEffect, useMemo, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchL33CameraFirstDetectorReviewFrame,
type L33CameraFirstDetectorReviewResult,
type L33VisualFrame,
} from "../../core/laboratory/l33CameraFirstDetectorReview";
import { L3PointPillarsScene } from "./L3PointPillarsScene";
import { L33CameraFirstDetectorScene } from "./L33CameraFirstDetectorScene";
type ViewerMode = "cam" | "3d" | "bev";
type LidarMode = "on" | "off";
function frameLabel(frame: L33CameraFirstDetectorReviewResult["frames"][number]) {
return `${frame.sessionSeconds.toLocaleString("ru-RU", {
maximumFractionDigits: 1,
})} с · кадр ${frame.frameId} · объектов ${frame.detectionCount}`;
}
export function L33CameraFirstDetectorVisual({
result,
}: {
result: L33CameraFirstDetectorReviewResult;
}) {
const [selectedFrameId, setSelectedFrameId] = useState(result.frames[0]?.frameId ?? "");
const [frame, setFrame] = useState<L33VisualFrame | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<ViewerMode>("cam");
const [lidarMode, setLidarMode] = useState<LidarMode>("on");
const [expanded, setExpanded] = useState(false);
useEffect(() => {
if (!selectedFrameId) return;
const controller = new AbortController();
setFrame(null);
setLoading(true);
setError(null);
void fetchL33CameraFirstDetectorReviewFrame(
result.resultId,
selectedFrameId,
{ signal: controller.signal },
).then((next) => {
if (!controller.signal.aborted) setFrame(next);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(caught instanceof Error ? caught.message : "Кадр L3.3 недоступен.");
}
}).finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [result.resultId, selectedFrameId]);
const selectedIndex = result.frames.findIndex(({ frameId }) => frameId === selectedFrameId);
const associationRays = useMemo(() => frame?.detections.map((detection) => ({
originXyzM: detection.cameraRayLidar.originXyzM,
directionXyz: detection.cameraRayLidar.directionXyz,
anchorXyzM: detection.geometryAnchorXyzM,
temporalHeld: detection.semanticProvenance === "e23-temporal-hold",
})) ?? [], [frame]);
const navigate = (offset: -1 | 1) => {
if (selectedIndex < 0) return;
const index = (selectedIndex + offset + result.frames.length) % result.frames.length;
setSelectedFrameId(result.frames[index].frameId);
};
const actions = (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущий camera-first кадр" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующий camera-first кадр" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать camera-first кадр RAVNOVES00"
value={selectedFrameId}
options={result.frames.map((item) => ({ value: item.frameId, label: frameLabel(item) }))}
variant="split"
menuWidth="anchor"
onChange={setSelectedFrameId}
/>
</div>
);
const overlay = frame ? (
<div className="l3-visual-audit__overlay">
<div>
<span>RAVNOVES00 · правая камера</span>
<strong>{frame.summary.sessionSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с · кадр {frame.frameId}</strong>
<small>{frame.summary.semanticProvenance === "e23-temporal-hold" ? "E23 удерживает состояние между наблюдениями" : "Текущий rectified camera track"}</small>
</div>
<div>
<span>YOLOX-S · камера владеет семантикой</span>
<strong>{frame.detections.length} объектов · {frame.summary.rangedDetectionCount} с LiDAR-дистанцией</strong>
<small>Порог detector score {(result.metrics.minimumDetectorScore * 100).toFixed(0)}%</small>
</div>
{mode === "cam" ? (
<div className="l3-visual-audit__legend">
<span data-tone="success">Зелёный контур · LiDAR подтвердил дальность</span>
<span data-tone="prediction">Белый контур · объект камеры без метрической дальности</span>
<span>Пунктир · E23 временно удерживает camera track</span>
</div>
) : (
<div className="l3-visual-audit__legend">
<span data-tone="success">Зелёный луч и точка · LiDAR-дальность; пунктир · E23 hold</span>
<span data-tone="prediction">Белый пунктирный луч · camera-only направление</span>
<span>Светлый куб · world-state объект; пунктир · E23 hold</span>
</div>
)}
</div>
) : undefined;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="Camera-first detector на RAVNOVES00"
mode={mode}
modes={[
{ value: "cam", label: "CAM" },
{ value: "3d", label: "3D" },
{ value: "bev", label: "BEV" },
]}
secondaryMode={mode === "cam" ? {
value: lidarMode,
modes: [
{ value: "on", label: "L on" },
{ value: "off", label: "L off" },
],
label: "LiDAR поверх камеры",
onChange: setLidarMode,
} : undefined}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
{loading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем camera-first кадр RAVNOVES00</span>
</div>
) : error || !frame ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{error ?? "Кадр L3.3 недоступен."}</span>
</div>
) : mode === "cam" ? (
<L33CameraFirstDetectorScene frame={frame} lidarVisible={lidarMode === "on"} />
) : (
<L3PointPillarsScene frame={{
frameId: frame.frameId,
pointsXyzi: frame.pointsXyzi,
truthBoxes: [],
predictionBoxes: frame.predictionBoxes,
}}
mode={mode}
associationRays={associationRays}
fitToEvidence
/>
)}
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,126 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type {
L34AAssistedYoloxErrorAuditResult,
} from "../../core/laboratory/l34aAssistedYoloxErrorAudit";
import { formatNumber } from "../../presentation";
import { L34AAssistedYoloxErrorVisual } from "./L34AAssistedYoloxErrorVisual";
function digest(value: string): string | null {
const candidate = value.split("-").at(-1) ?? "";
return /^[a-f0-9]{64}$/.test(candidate) ? candidate : null;
}
function formatPercent(value: number): string {
return `${(value * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
}
export function L34AAssistedYoloxErrorResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: L34AAssistedYoloxErrorAuditResult;
}) {
const metrics = result.metrics;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB L3.4A · assisted YOLOX error audit"
description="Детерминированное сопоставление точного L3.4 candidate freeze с полностью проверенной candidate-seeded разметкой: инженерная диагностика ошибок, но не независимая truth."
status="Assisted diagnostic · не truth"
statusTone="warning"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · RIGHT camera · recorded replay` },
{ label: "Сопоставление", value: "Greedy max-IoU · threshold 0.50" },
{ label: "Покрытие", value: `${formatNumber(metrics.frameCount, 0)} кадров · ${formatNumber(metrics.referenceCount, 0)} объектов` },
{ label: "Полномочия", value: "Диагностика · candidate не принят" },
]}
brief={{
question: "Какие конкретные FP, FN, дубли и ошибки класса делает замороженный YOLOX-S на 32 кадрах RAVNOVES00?",
approach: "Точные 268 L3.4 predictions пространственно сопоставлены при IoU ≥ 0.50 с 278 объектами сохранённой AI-assisted review. Каждый результат связан с исходным кадром, prediction box и reference box.",
principalResult: `${formatNumber(metrics.truePositive, 0)} совпадения, ${formatNumber(metrics.falsePositive, 0)} FP, ${formatNumber(metrics.falseNegative, 0)} FN, ${formatNumber(metrics.classMismatch, 0)} ошибок класса и ${formatNumber(metrics.duplicateFalsePositive, 0)} дублей; визуальные ошибки есть на ${formatNumber(metrics.errorCaseCount, 0)} из ${formatNumber(metrics.frameCount, 0)} кадров.`,
limitation: "Разметка создавалась поверх frozen candidate, поэтому alignment-метрики смещены в пользу модели и не являются blind accuracy, AP или основанием для acceptance. L3.5 остаётся закрыт до independent truth seal.",
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: result.pipelineId,
components: [
{
kind: "source",
name: "L3.4 RIGHT YOLOX freeze",
version: "268 immutable candidate boxes",
role: "prediction substrate",
identitySha256: null,
},
{
kind: "source",
name: result.assistedAnnotation.sessionId,
version: `revision ${result.assistedAnnotation.revision} · not truth`,
role: "candidate-seeded engineering reference",
identitySha256: result.assistedAnnotation.sessionSha256,
},
{
kind: "algorithm",
name: result.profileId,
version: "greedy max-IoU 0.50 · spatial match before class verdict",
role: "TP/FP/FN, duplicate and class-mismatch audit",
identitySha256: digest(result.resultId),
},
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="ASSISTED ERROR AUDIT · SOURCE-BOUND VISUALS"
title="FP, FN, дубли и неверные классы на точных кадрах"
kind="diagnostic-model"
resizable
>
<L34AAssistedYoloxErrorVisual result={result} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Детектор требует раздельной коррекции post-processing, классов и данных"
status="Engineering diagnosis only"
statusTone="warning"
metrics={[
{
label: "Assisted precision@0.50",
value: formatPercent(metrics.precisionIou50),
hint: `${formatNumber(metrics.truePositive, 0)} TP · ${formatNumber(metrics.falsePositive, 0)} FP · не blind`,
},
{
label: "Assisted recall@0.50",
value: formatPercent(metrics.recallIou50),
hint: `${formatNumber(metrics.falseNegative, 0)} FN · не acceptance`,
},
{
label: "Классы",
value: formatNumber(metrics.classMismatch, 0),
hint: `${formatNumber(metrics.customReferenceCount, 0)} объектов с proposed ontology labels`,
},
{
label: "Дубли",
value: formatNumber(metrics.duplicateFalsePositive, 0),
hint: "отдельный post-processing сигнал",
},
]}
conclusion={{
proved: "На этих записанных кадрах воспроизводимо локализованы конкретные классы ошибок: дубли и лишние боксы, пропуски людей/статических препятствий и несовпадения stroller/scooter/laptop с текущей онтологией кандидата.",
notProved: "Не доказаны blind AP/recall, перенос на другой маршрут, работа LiDAR range, live/hardware качество, navigation или safety. Assisted alignment нельзя использовать как независимую truth.",
decision: "Использовать кейсы для отдельной коррекции NMS/post-processing, class mapping и подготовки detector-data. Не тюнить пороги по агрегату и не открывать L3.5 до двух независимых review и adjudication.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,264 @@
import { useEffect, useRef, useState } from "react";
import type {
L34AAuditAnnotation,
L34AAuditCase,
L34AAuditPrediction,
} from "../../core/laboratory/l34aAssistedYoloxErrorAudit";
function tokenColor(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
alpha = 1,
): string {
const value = getComputedStyle(host).getPropertyValue(token).trim();
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
const CATEGORY_NAMES: Readonly<Record<string, string>> = {
car: "Авто",
heavy_vehicle: "Тяжёлый транспорт",
person: "Человек",
bicycle: "Велосипед",
motorcycle: "Мотоцикл",
static_obstacle: "Препятствие",
animal: "Животное",
};
function categoryName(value: string): string {
if (value.startsWith("unmapped:")) return value.slice("unmapped:".length);
return CATEGORY_NAMES[value] ?? value;
}
function predictionLabel(
prediction: L34AAuditPrediction,
annotation: L34AAuditAnnotation | undefined,
): string | null {
const category = categoryName(prediction.category);
const score = `${(prediction.score * 100).toFixed(0)}%`;
if (prediction.verdict === "true_positive") return null;
if (prediction.verdict === "duplicate_false_positive") {
return `Дубль · ${category} · ${score}`;
}
if (prediction.verdict === "class_mismatch") {
return `Класс · ${category}${categoryName(annotation?.displayCategory ?? "?")}`;
}
return `FP · ${category} · ${score}`;
}
function annotationLabel(annotation: L34AAuditAnnotation): string | null {
if (annotation.verdict !== "false_negative") return null;
return `FN · ${categoryName(annotation.displayCategory)}`;
}
type LabelRect = Readonly<{
x: number;
y: number;
width: number;
height: number;
}>;
function overlaps(left: LabelRect, right: LabelRect): boolean {
const margin = 3;
return !(
left.x + left.width + margin <= right.x
|| right.x + right.width + margin <= left.x
|| left.y + left.height + margin <= right.y
|| right.y + right.height + margin <= left.y
);
}
function placeLabel(
box: LabelRect,
labelWidth: number,
labelHeight: number,
imageBounds: LabelRect,
occupied: readonly LabelRect[],
): LabelRect | null {
const x = Math.min(
imageBounds.x + imageBounds.width - labelWidth,
Math.max(imageBounds.x, box.x),
);
const candidates = [
box.y - labelHeight - 3,
box.y + box.height + 3,
box.y + 3,
].map((y) => ({ x, y, width: labelWidth, height: labelHeight }));
return candidates.find((candidate) => (
candidate.y >= imageBounds.y
&& candidate.y + candidate.height <= imageBounds.y + imageBounds.height
&& occupied.every((current) => !overlaps(candidate, current))
)) ?? null;
}
export function L34AAssistedYoloxErrorScene({
auditCase,
errorsVisible,
}: {
auditCase: L34AAuditCase;
errorsVisible: boolean;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [image, setImage] = useState<HTMLImageElement | null>(null);
const [error, setError] = useState(false);
useEffect(() => {
const next = new Image();
next.decoding = "async";
next.onload = () => {
setError(false);
setImage(next);
};
next.onerror = () => {
setImage(null);
setError(true);
};
next.src = auditCase.cameraUrl;
return () => {
next.onload = null;
next.onerror = null;
};
}, [auditCase.cameraUrl]);
useEffect(() => {
const host = hostRef.current;
const canvas = canvasRef.current;
if (!host || !canvas || !image) 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 ratio = Math.min(window.devicePixelRatio, 1.5);
canvas.width = Math.round(width * ratio);
canvas.height = Math.round(height * ratio);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
context.setTransform(ratio, 0, 0, ratio, 0, 0);
context.fillStyle = tokenColor(host, "--nodedc-canvas-rgb", [5, 5, 6]);
context.fillRect(0, 0, width, height);
const scale = Math.min(
width / auditCase.cameraWidth,
height / auditCase.cameraHeight,
);
const drawWidth = auditCase.cameraWidth * scale;
const drawHeight = auditCase.cameraHeight * scale;
const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
context.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
if (!errorsVisible) return;
const colors = {
tp: tokenColor(host, "--nodedc-success-rgb", [181, 255, 90], 0.66),
fp: tokenColor(host, "--nodedc-danger-rgb", [255, 116, 116], 0.98),
fn: tokenColor(host, "--nodedc-warning-rgb", [255, 209, 102], 0.98),
mismatch: tokenColor(host, "--nodedc-accent-rgb", [255, 47, 146], 0.98),
label: tokenColor(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.86),
};
const imageBounds = {
x: offsetX,
y: offsetY,
width: drawWidth,
height: drawHeight,
};
const occupied: LabelRect[] = [];
const annotationById = new Map(
auditCase.annotations.map((annotation) => [annotation.objectId, annotation]),
);
const drawBox = (
rawBox: readonly [number, number, number, number],
color: string,
dashed: boolean,
): LabelRect => {
const [left, top, right, bottom] = rawBox;
const box = {
x: offsetX + left * scale,
y: offsetY + top * scale,
width: (right - left) * scale,
height: (bottom - top) * scale,
};
context.strokeStyle = color;
context.lineWidth = Math.max(1.35, 1.9 * scale);
context.setLineDash(dashed ? [6, 4] : []);
context.strokeRect(box.x, box.y, box.width, box.height);
context.setLineDash([]);
return box;
};
const drawLabel = (label: string | null, box: LabelRect, color: string) => {
if (!label) return;
const fontSize = Math.max(9, 10 * scale);
const labelHeight = fontSize + 7;
context.font = `600 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = context.measureText(label).width + 12;
const placement = placeLabel(
box,
labelWidth,
labelHeight,
imageBounds,
occupied,
);
if (!placement) return;
occupied.push(placement);
context.fillStyle = colors.label;
context.fillRect(
placement.x,
placement.y,
placement.width,
placement.height,
);
context.fillStyle = color;
context.fillText(label, placement.x + 6, placement.y + fontSize + 1);
};
auditCase.predictions.forEach((prediction) => {
const color = prediction.verdict === "true_positive"
? colors.tp
: prediction.verdict === "class_mismatch"
? colors.mismatch
: colors.fp;
const box = drawBox(prediction.boxXyxy, color, false);
drawLabel(
predictionLabel(
prediction,
prediction.matchedObjectId
? annotationById.get(prediction.matchedObjectId)
: undefined,
),
box,
color,
);
});
auditCase.annotations.forEach((annotation) => {
if (annotation.verdict === "true_positive") return;
const color = annotation.verdict === "class_mismatch"
? colors.mismatch
: colors.fn;
const box = drawBox(annotation.boxXyxy, color, true);
drawLabel(annotationLabel(annotation), box, color);
});
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [auditCase, errorsVisible, image]);
return (
<div className="l32-camera-scene" ref={hostRef}>
<canvas
ref={canvasRef}
role="img"
aria-label={`L3.4A assisted error case frame ${auditCase.frameIndex}: ${auditCase.summary.truePositive} TP, ${auditCase.summary.falsePositive} FP, ${auditCase.summary.falseNegative} FN`}
/>
{error ? (
<div className="l3-visual-audit__state" role="status">
Точный кадр правой камеры L3.4A недоступен.
</div>
) : null}
</div>
);
}
@@ -0,0 +1,153 @@
import { useEffect, useMemo, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchL34AAuditCase,
type L34AAssistedYoloxErrorAuditResult,
type L34AAuditCase,
type L34ACaseSummary,
} from "../../core/laboratory/l34aAssistedYoloxErrorAudit";
import { L34AAssistedYoloxErrorScene } from "./L34AAssistedYoloxErrorScene";
type ViewerMode = "errors" | "source";
function caseLabel(item: L34ACaseSummary): string {
const metrics = item.summary;
return `${item.truthIslandSequence}/32 · frame ${item.frameIndex} · ${item.groupId} · ${metrics.falsePositive} FP · ${metrics.falseNegative} FN · ${metrics.classMismatch} class`;
}
export function L34AAssistedYoloxErrorVisual({
result,
}: {
result: L34AAssistedYoloxErrorAuditResult;
}) {
const orderedCases = useMemo(() => {
const bySequence = new Map(
result.cases.map((item) => [item.truthIslandSequence, item]),
);
return result.caseOrder.map((sequence) => bySequence.get(sequence)).filter(
(item): item is L34ACaseSummary => item !== undefined,
);
}, [result.caseOrder, result.cases]);
const [selectedSequence, setSelectedSequence] = useState(
orderedCases[0]?.truthIslandSequence ?? 1,
);
const [auditCase, setAuditCase] = useState<L34AAuditCase | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<ViewerMode>("errors");
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const controller = new AbortController();
setAuditCase(null);
setLoading(true);
setError(null);
void fetchL34AAuditCase(result.resultId, selectedSequence, {
signal: controller.signal,
}).then((next) => {
if (!controller.signal.aborted) setAuditCase(next);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(
caught instanceof Error
? caught.message
: "Визуальный кейс L3.4A недоступен.",
);
}
}).finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [result.resultId, selectedSequence]);
const selectedIndex = orderedCases.findIndex(
({ truthIslandSequence }) => truthIslandSequence === selectedSequence,
);
const navigate = (offset: -1 | 1) => {
if (selectedIndex < 0 || !orderedCases.length) return;
const index = (selectedIndex + offset + orderedCases.length) % orderedCases.length;
setSelectedSequence(orderedCases[index].truthIslandSequence);
};
const actions = (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущий error-case L3.4A" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующий error-case L3.4A" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать error-case L3.4A"
value={String(selectedSequence)}
options={orderedCases.map((item) => ({
value: String(item.truthIslandSequence),
label: caseLabel(item),
}))}
variant="split"
menuWidth="anchor"
searchable
searchPlaceholder="Найти кадр, группу или ошибку"
onChange={(value) => setSelectedSequence(Number(value))}
/>
</div>
);
const overlay = auditCase ? (
<div className="l3-visual-audit__overlay">
<div>
<span>RAVNOVES00 · sensor.camera.right</span>
<strong>{auditCase.sessionSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с · frame {auditCase.frameIndex}</strong>
<small>Sequence {auditCase.truthIslandSequence}/32 · {auditCase.groupId}</small>
</div>
<div>
<span>Assisted IoU 0.50 diagnostic</span>
<strong>{auditCase.summary.truePositive} TP · {auditCase.summary.falsePositive} FP · {auditCase.summary.falseNegative} FN</strong>
<small>{auditCase.summary.classMismatch} class mismatch · {auditCase.summary.duplicateFalsePositive} duplicate</small>
</div>
<div className="l3-visual-audit__legend">
<span data-tone="tp">Зелёный · совпало</span>
<span data-tone="fp">Красный · FP/дубль</span>
<span data-tone="fn">Жёлтый пунктир · FN</span>
<span data-tone="prediction">Акцент · неверный класс</span>
</div>
</div>
) : undefined;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="L3.4A assisted YOLOX error audit"
mode={mode}
modes={[
{ value: "errors", label: "ERRORS" },
{ value: "source", label: "SOURCE" },
]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
{loading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем hash-bound кадр и error layers</span>
</div>
) : error || !auditCase ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{error ?? "Визуальный кейс L3.4A недоступен."}</span>
</div>
) : (
<L34AAssistedYoloxErrorScene
auditCase={auditCase}
errorsVisible={mode === "errors"}
/>
)}
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,66 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { L34BResult } from "../../core/laboratory/l34bNestedBoxConsolidation";
import { formatNumber } from "../../presentation";
import { L34BNestedBoxConsolidationVisual } from "./L34BNestedBoxConsolidationVisual";
function formatPercent(value: number): string {
return `${(value * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
}
export function L34BNestedBoxConsolidationResultView({ rigLabel, result }: { rigLabel: string; result: L34BResult }) {
const { before, after } = result.metrics;
return (
<LaboratoryWorkTemplate
summary={<LaboratorySummary
title="LAB L3.4B · nested-box consolidation shadow"
description="Ограниченная post-normalization проверка: только почти полностью вложенные рамки одного класса объединяются; global IoU NMS не меняется."
status="Regression-free assisted shadow · не truth"
statusTone="success"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · RIGHT camera · recorded replay` },
{ label: "Правило", value: "Same class · overlap-over-smaller ≥ 0.95 · union box" },
{ label: "Покрытие", value: `${formatNumber(after.frameCount, 0)} кадров · ${formatNumber(result.metrics.affectedCaseCount, 0)} изменён` },
{ label: "Полномочия", value: "Shadow post-processing · candidate не принят" },
]}
brief={{
question: "Можно ли убрать подтверждённые вложенные рамки, не потеряв корректные объекты?",
approach: "К точным 268 L3.4 predictions применено одно детерминированное правило: совпадающий нормализованный класс и покрытие меньшей рамки не менее 95%. Геометрия объединяется, score берётся максимальный; результат повторно сопоставляется с той же assisted review.",
principalResult: `${formatNumber(before.predictionCount, 0)}${formatNumber(after.predictionCount, 0)} рамок, ${formatNumber(before.falsePositive, 0)}${formatNumber(after.falsePositive, 0)} assisted FP; TP ${formatNumber(after.truePositive, 0)} и FN ${formatNumber(after.falseNegative, 0)} не изменились.`,
limitation: "Это source-scoped assisted shadow, а не blind accuracy. Четыре left/front seam split и semantic stroller collision этим правилом не исправляются.",
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: result.pipelineId,
components: [
{ kind: "source", name: "L3.4 immutable freeze", version: "268 normalized YOLOX boxes", role: "before substrate", identitySha256: null },
{ kind: "source", name: "L3.4A assisted audit", version: "32 reviewed frames · not truth", role: "diagnostic comparison", identitySha256: null },
{ kind: "algorithm", name: result.profile.profileId, version: "same class · overlap 0.95 · union", role: "nested-box consolidation shadow", identitySha256: result.resultId.split("-").at(-1) ?? null },
],
}}
/>}
evidence={<LaboratoryEvidence eyebrow="POST-PROCESSING SHADOW · BEFORE / AFTER / SOURCE" title="Одна вложенная пара объединена без assisted-регрессии" kind="diagnostic-model" resizable><L34BNestedBoxConsolidationVisual result={result} /></LaboratoryEvidence>}
result={<LaboratoryResultSummary
title="Узкое nested-box правило прошло; глобальный NMS — нет"
status="Bounded shadow accepted · not candidate acceptance"
statusTone="success"
metrics={[
{ label: "Assisted precision", value: `${formatPercent(before.precisionIou50)}${formatPercent(after.precisionIou50)}`, hint: "+0,3 п.п. · не blind" },
{ label: "Assisted recall", value: formatPercent(after.recallIou50), hint: "Без изменения" },
{ label: "FP", value: `${formatNumber(before.falsePositive, 0)}${formatNumber(after.falsePositive, 0)}`, hint: "Одна рамка удалена объединением" },
{ label: "Осталось seam-сигналов", value: formatNumber(result.decision.remainingL34aDuplicateSignals, 0), hint: "Нужен tile-aware stitching" },
]}
conclusion={{
proved: "На текущих 32 записанных кадрах правило вложенности ≥95% объединяет ровно одну пару одного класса и не ухудшает assisted TP, FN, recall или class mismatch. BEFORE/AFTER связан с исходным кадром.",
notProved: "Не доказаны blind AP/recall, перенос на другой маршрут, live/hardware, LiDAR range, navigation или safety. Четыре seam split не являются обычными IoU-дублями.",
decision: "Оставить nested-box consolidation как bounded shadow. Не снижать глобальный IoU NMS: тест 0.30 удалял корректные соседние машины. Следующая работа — сохранить rectification_tile в prediction contract и проверить temporal left/front seam stitching.",
}}
/>}
/>
);
}
@@ -0,0 +1,115 @@
import { useEffect, useRef, useState } from "react";
import type { L34BCase } from "../../core/laboratory/l34bNestedBoxConsolidation";
type SceneMode = "after" | "before" | "source";
function color(host: HTMLElement, token: string, fallback: readonly [number, number, number], alpha = 1): string {
const value = getComputedStyle(host).getPropertyValue(token).trim();
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
export function L34BNestedBoxConsolidationScene({
shadowCase,
mode,
}: {
shadowCase: L34BCase;
mode: SceneMode;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [image, setImage] = useState<HTMLImageElement | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
const next = new Image();
next.decoding = "async";
next.onload = () => { setImage(next); setFailed(false); };
next.onerror = () => { setImage(null); setFailed(true); };
next.src = shadowCase.cameraUrl;
return () => { next.onload = null; next.onerror = null; };
}, [shadowCase.cameraUrl]);
useEffect(() => {
const host = hostRef.current;
const canvas = canvasRef.current;
if (!host || !canvas || !image) 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 ratio = Math.min(window.devicePixelRatio, 1.5);
canvas.width = Math.round(width * ratio);
canvas.height = Math.round(height * ratio);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
context.setTransform(ratio, 0, 0, ratio, 0, 0);
context.fillStyle = color(host, "--nodedc-canvas-rgb", [5, 5, 6]);
context.fillRect(0, 0, width, height);
const scale = Math.min(width / shadowCase.cameraWidth, height / shadowCase.cameraHeight);
const drawWidth = shadowCase.cameraWidth * scale;
const drawHeight = shadowCase.cameraHeight * scale;
const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
context.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
if (mode === "source") return;
const sourceIndices = new Set(shadowCase.consolidations.flatMap((item) => item.sourcePredictionIndices));
const mergedIndices = new Set(shadowCase.consolidations.map((item) => item.outputPredictionIndex));
const predictions = mode === "before" ? shadowCase.beforePredictions : shadowCase.afterPredictions;
let highlightedLabelIndex = 0;
predictions.forEach((prediction) => {
const highlighted = mode === "before"
? sourceIndices.has(prediction.predictionIndex)
: mergedIndices.has(prediction.predictionIndex);
const [left, top, right, bottom] = prediction.boxXyxy;
const x = offsetX + left * scale;
const y = offsetY + top * scale;
const boxWidth = (right - left) * scale;
const boxHeight = (bottom - top) * scale;
const stroke = highlighted
? mode === "before"
? color(host, "--nodedc-danger-rgb", [255, 116, 116], 1)
: color(host, "--nodedc-success-rgb", [181, 255, 90], 1)
: color(host, "--nodedc-foreground-rgb", [240, 240, 240], 0.46);
context.strokeStyle = stroke;
context.lineWidth = highlighted ? Math.max(2, 2.7 * scale) : Math.max(1, 1.2 * scale);
context.setLineDash(mode === "before" && highlighted ? [7, 4] : []);
context.strokeRect(x, y, boxWidth, boxHeight);
context.setLineDash([]);
if (!highlighted) return;
const label = `${mode === "before" ? "BEFORE" : "AFTER"} · ${prediction.category} · ${(prediction.score * 100).toFixed(0)}%`;
const fontSize = Math.max(10, 11 * scale);
context.font = `600 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = context.measureText(label).width + 12;
const labelHeight = fontSize + 8;
const labelX = Math.min(
offsetX + drawWidth - labelWidth,
Math.max(offsetX, x),
);
const labelY = Math.max(
offsetY,
y - labelHeight - 3 - highlightedLabelIndex * (labelHeight + 3),
);
highlightedLabelIndex += 1;
context.fillStyle = color(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.9);
context.fillRect(labelX, labelY, labelWidth, labelHeight);
context.fillStyle = stroke;
context.fillText(label, labelX + 6, labelY + fontSize + 1);
});
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [image, mode, shadowCase]);
return (
<div className="l32-camera-scene" ref={hostRef}>
<canvas ref={canvasRef} role="img" aria-label={`L3.4B frame ${shadowCase.frameIndex}: ${mode} nested-box consolidation`} />
{failed ? <div className="l3-visual-audit__state" role="status">Точный кадр L3.4B недоступен.</div> : null}
</div>
);
}
@@ -0,0 +1,92 @@
import { useEffect, useMemo, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchL34BCase,
type L34BCase,
type L34BCaseSummary,
type L34BResult,
} from "../../core/laboratory/l34bNestedBoxConsolidation";
import { L34BNestedBoxConsolidationScene } from "./L34BNestedBoxConsolidationScene";
type ViewerMode = "after" | "before" | "source";
function caseLabel(item: L34BCaseSummary): string {
return `${item.truthIslandSequence}/32 · frame ${item.frameIndex} · ${item.groupId} · ${item.consolidationCount ? "CONSOLIDATED" : "UNCHANGED"}`;
}
export function L34BNestedBoxConsolidationVisual({ result }: { result: L34BResult }) {
const orderedCases = useMemo(() => {
const bySequence = new Map(result.cases.map((item) => [item.truthIslandSequence, item]));
return result.caseOrder.map((sequence) => bySequence.get(sequence)).filter((item): item is L34BCaseSummary => item !== undefined);
}, [result.caseOrder, result.cases]);
const [selectedSequence, setSelectedSequence] = useState(orderedCases[0]?.truthIslandSequence ?? 1);
const [shadowCase, setShadowCase] = useState<L34BCase | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<ViewerMode>("after");
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
setShadowCase(null);
void fetchL34BCase(result.resultId, selectedSequence, { signal: controller.signal })
.then((next) => { if (!controller.signal.aborted) setShadowCase(next); })
.catch((caught: unknown) => { if (!controller.signal.aborted) setError(caught instanceof Error ? caught.message : "Визуальный кейс L3.4B недоступен."); })
.finally(() => { if (!controller.signal.aborted) setLoading(false); });
return () => controller.abort();
}, [result.resultId, selectedSequence]);
const selectedIndex = orderedCases.findIndex((item) => item.truthIslandSequence === selectedSequence);
const navigate = (offset: -1 | 1) => {
if (selectedIndex < 0 || !orderedCases.length) return;
setSelectedSequence(orderedCases[(selectedIndex + offset + orderedCases.length) % orderedCases.length].truthIslandSequence);
};
const actions = (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущий L3.4B case" onClick={() => navigate(-1)}><Icon name="chevron-left" size={16} /></IconButton>
<IconButton label="Следующий L3.4B case" onClick={() => navigate(1)}><Icon name="chevron-right" size={16} /></IconButton>
</div>
<Select
label="Выбрать L3.4B case"
value={String(selectedSequence)}
options={orderedCases.map((item) => ({ value: String(item.truthIslandSequence), label: caseLabel(item) }))}
variant="split"
menuWidth="anchor"
searchable
searchPlaceholder="Найти кадр или группу"
onChange={(value) => setSelectedSequence(Number(value))}
/>
</div>
);
const overlay = shadowCase ? (
<div className="l3-visual-audit__overlay">
<div><span>RAVNOVES00 · sensor.camera.right</span><strong>{shadowCase.sessionSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с · frame {shadowCase.frameIndex}</strong><small>Sequence {shadowCase.truthIslandSequence}/32 · {shadowCase.groupId}</small></div>
<div><span>Nested same-class overlap 95%</span><strong>{shadowCase.beforeSummary.predictionCount} {shadowCase.afterSummary.predictionCount} boxes</strong><small>{shadowCase.beforeSummary.falsePositive} {shadowCase.afterSummary.falsePositive} assisted FP · recall unchanged</small></div>
<div className="l3-visual-audit__legend"><span data-tone="fp">Красный пунктир · исходные вложенные рамки</span><span data-tone="tp">Зелёный · объединённая геометрия</span><span data-tone="prediction">SOURCE · чистый hash-bound кадр</span></div>
</div>
) : undefined;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="L3.4B nested-box consolidation before/after"
mode={mode}
modes={[{ value: "after", label: "AFTER" }, { value: "before", label: "BEFORE" }, { value: "source", label: "SOURCE" }]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
{loading ? <div className="l3-visual-audit__state" role="status"><span className="busy-indicator" aria-hidden="true" /><span>Открываем BEFORE/AFTER evidence</span></div>
: error || !shadowCase ? <div className="l3-visual-audit__state" role="status"><Icon name="alert" size={18} /><span>{error ?? "Визуальный кейс L3.4B недоступен."}</span></div>
: <L34BNestedBoxConsolidationScene shadowCase={shadowCase} mode={mode} />}
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,79 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { L34CResult } from "../../core/laboratory/l34cTileSeamStitch";
import { formatNumber } from "../../presentation";
import { L34CTileSeamStitchVisual } from "./L34CTileSeamStitchVisual";
function formatPercent(value: number): string {
return `${(value * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
}
export function L34CTileSeamStitchResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: L34CResult;
}) {
const { before, after } = result.metrics;
return (
<LaboratoryWorkTemplate
summary={<LaboratorySummary
title="LAB L3.4C · temporal tile-seam stitch shadow"
description="Tile provenance восстановлен exact join с qualification; front/left split объединяется только при устойчивой геометрии минимум на трёх последовательных кадрах."
status="Regression-free assisted shadow · не truth"
statusTone="success"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · RIGHT camera · recorded replay` },
{ label: "Provenance", value: "Exact L3.4 ↔ qualification · front / left tile" },
{ label: "Временной контракт", value: `${result.profile.minimumConsecutiveFrames} кадров · union IoU ≥${result.profile.temporalUnionIouThreshold.toLocaleString("ru-RU")}` },
{ label: "Полномочия", value: "Shadow post-processing · candidate не принят" },
]}
brief={{
question: "Можно ли убрать устойчивый разрыв одной машины между front и left rectification tiles, не склеивая соседние объекты?",
approach: "Каждая из 268 frozen YOLOX-рамок exact-join связана с raw label, class id, center и rectification tile из immutable qualification. Склейка допускается только для одного класса, front/left, крупной пересекающейся seam-геометрии и серии минимум из трёх последовательных кадров с union-IoU не ниже 0,80.",
principalResult: `${formatNumber(before.predictionCount, 0)}${formatNumber(after.predictionCount, 0)} рамок, ${formatNumber(before.falsePositive, 0)}${formatNumber(after.falsePositive, 0)} assisted FP и ${formatNumber(before.duplicateFalsePositive, 0)}${formatNumber(after.duplicateFalsePositive, 0)} duplicate-сигналов; TP и FN не изменились.`,
limitation: "Это source-scoped assisted shadow, а не blind accuracy. Подтверждён только один четырёхкадровый front/left run; одиночные случаи, front/right, другой маршрут и live/hardware не допущены.",
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: result.pipelineId,
components: [
{ kind: "source", name: "L3.4 immutable freeze", version: "268 normalized YOLOX boxes", role: "prediction substrate", identitySha256: null },
{ kind: "source", name: "rectified YOLOX qualification", version: "4489 frames", role: "exact tile provenance", identitySha256: null },
{ kind: "algorithm", name: result.profile.profileId, version: "front/left · temporal ≥3 · union", role: "fail-closed seam stitch shadow", identitySha256: result.resultId.split("-").at(-1) ?? null },
],
}}
/>}
evidence={<LaboratoryEvidence
eyebrow="TILE-AWARE SHADOW · BEFORE / AFTER / SOURCE"
title="Одна front/left seam-композиция подтверждена на четырёх кадрах"
kind="diagnostic-model"
resizable
>
<L34CTileSeamStitchVisual result={result} />
</LaboratoryEvidence>}
result={<LaboratoryResultSummary
title="Temporal front/left stitch прошёл assisted-проверку"
status="Bounded shadow accepted · not candidate acceptance"
statusTone="success"
metrics={[
{ label: "Assisted precision", value: `${formatPercent(before.precisionIou50)}${formatPercent(after.precisionIou50)}`, hint: "+1,3 п.п. · не blind" },
{ label: "Assisted recall", value: formatPercent(after.recallIou50), hint: "Без изменения" },
{ label: "Duplicate FP", value: `${formatNumber(before.duplicateFalsePositive, 0)}${formatNumber(after.duplicateFalsePositive, 0)}`, hint: "Четыре seam-рамки объединены" },
{ label: "Temporal admission", value: `${formatNumber(result.metrics.stitchCount, 0)} / ${formatNumber(result.metrics.staticCandidateCount, 0)}`, hint: "Один статический кандидат отклонён" },
]}
conclusion={{
proved: "На текущих 32 записанных RIGHT-кадрах exact tile provenance однозначно восстанавливается для каждой frozen prediction. Один front/left seam-run на кадрах 2620–2623 проходит временной контракт; четыре duplicate FP исчезают без ухудшения assisted TP, FN, recall или class mismatch.",
notProved: "Не доказаны blind AP/recall, перенос на front/right, другой маршрут, live/hardware, LiDAR range, navigation или safety. Assisted review не является независимой truth.",
decision: "Принять temporal seam stitch как отдельный bounded shadow. Глобальный NMS оставить неизменным. Следом композиционно проверить принятые L3.4B nested-box и L3.4C seam правила и только затем замораживать единый cumulative candidate.",
}}
/>}
/>
);
}
@@ -0,0 +1,152 @@
import { useEffect, useRef, useState } from "react";
import type { L34CCase } from "../../core/laboratory/l34cTileSeamStitch";
type SceneMode = "after" | "before" | "source";
function color(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
alpha = 1,
): string {
const value = getComputedStyle(host).getPropertyValue(token).trim();
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
export function L34CTileSeamStitchScene({
shadowCase,
mode,
}: {
shadowCase: L34CCase;
mode: SceneMode;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [image, setImage] = useState<HTMLImageElement | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
const next = new Image();
next.decoding = "async";
next.onload = () => { setImage(next); setFailed(false); };
next.onerror = () => { setImage(null); setFailed(true); };
next.src = shadowCase.cameraUrl;
return () => { next.onload = null; next.onerror = null; };
}, [shadowCase.cameraUrl]);
useEffect(() => {
const host = hostRef.current;
const canvas = canvasRef.current;
if (!host || !canvas || !image) 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 ratio = Math.min(window.devicePixelRatio, 1.5);
canvas.width = Math.round(width * ratio);
canvas.height = Math.round(height * ratio);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
context.setTransform(ratio, 0, 0, ratio, 0, 0);
context.fillStyle = color(host, "--nodedc-canvas-rgb", [5, 5, 6]);
context.fillRect(0, 0, width, height);
const scale = Math.min(
width / shadowCase.cameraWidth,
height / shadowCase.cameraHeight,
);
const drawWidth = shadowCase.cameraWidth * scale;
const drawHeight = shadowCase.cameraHeight * scale;
const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
context.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
if (mode === "source") return;
const sourceIndices = new Set(
shadowCase.stitches.flatMap((item) => item.sourcePredictionIndices),
);
const predictions = mode === "before"
? shadowCase.beforePredictions
: shadowCase.afterPredictions;
const mergedIndices = new Set(
predictions
.filter((prediction) => (
"sourcePredictionIndices" in prediction
&& prediction.sourcePredictionIndices.length > 1
))
.map((prediction) => prediction.predictionIndex),
);
let highlightedLabelIndex = 0;
predictions.forEach((prediction) => {
const highlighted = mode === "before"
? sourceIndices.has(prediction.predictionIndex)
: mergedIndices.has(prediction.predictionIndex);
const [left, top, right, bottom] = prediction.boxXyxy;
const x = offsetX + left * scale;
const y = offsetY + top * scale;
const boxWidth = (right - left) * scale;
const boxHeight = (bottom - top) * scale;
const tile = "rectificationTile" in prediction
? prediction.rectificationTile
: prediction.sourceRectificationTiles.join("+");
const stroke = highlighted
? mode === "after"
? color(host, "--nodedc-success-rgb", [181, 255, 90])
: tile === "front"
? color(host, "--nodedc-accent-rgb", [232, 56, 126])
: color(host, "--nodedc-warning-rgb", [255, 197, 92])
: color(host, "--nodedc-foreground-rgb", [240, 240, 240], 0.35);
context.strokeStyle = stroke;
context.lineWidth = highlighted
? Math.max(2, 2.7 * scale)
: Math.max(1, 1.1 * scale);
context.setLineDash(mode === "before" && highlighted ? [7, 4] : []);
context.strokeRect(x, y, boxWidth, boxHeight);
context.setLineDash([]);
if (!highlighted) return;
const label = mode === "before"
? `BEFORE · ${tile} · ${prediction.category} · ${(prediction.score * 100).toFixed(0)}%`
: `AFTER · front+left union · ${prediction.category} · ${(prediction.score * 100).toFixed(0)}%`;
const fontSize = Math.max(10, 11 * scale);
context.font = `600 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = context.measureText(label).width + 12;
const labelHeight = fontSize + 8;
const labelX = Math.min(
offsetX + drawWidth - labelWidth,
Math.max(offsetX, x),
);
const labelY = Math.max(
offsetY,
y - labelHeight - 3 - highlightedLabelIndex * (labelHeight + 3),
);
highlightedLabelIndex += 1;
context.fillStyle = color(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.9);
context.fillRect(labelX, labelY, labelWidth, labelHeight);
context.fillStyle = stroke;
context.fillText(label, labelX + 6, labelY + fontSize + 1);
});
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [image, mode, shadowCase]);
return (
<div className="l32-camera-scene" ref={hostRef}>
<canvas
ref={canvasRef}
role="img"
aria-label={`L3.4C frame ${shadowCase.frameIndex}: ${mode} tile-seam stitch`}
/>
{failed ? (
<div className="l3-visual-audit__state" role="status">
Точный кадр L3.4C недоступен.
</div>
) : null}
</div>
);
}
@@ -0,0 +1,142 @@
import { useEffect, useMemo, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchL34CCase,
type L34CCase,
type L34CCaseSummary,
type L34CResult,
} from "../../core/laboratory/l34cTileSeamStitch";
import { L34CTileSeamStitchScene } from "./L34CTileSeamStitchScene";
type ViewerMode = "after" | "before" | "source";
function caseLabel(item: L34CCaseSummary): string {
return `${item.truthIslandSequence}/32 · frame ${item.frameIndex} · ${item.groupId} · ${item.stitchCount ? "STITCHED" : "UNCHANGED"}`;
}
export function L34CTileSeamStitchVisual({ result }: { result: L34CResult }) {
const orderedCases = useMemo(() => {
const bySequence = new Map(result.cases.map((item) => [item.truthIslandSequence, item]));
return result.caseOrder
.map((sequence) => bySequence.get(sequence))
.filter((item): item is L34CCaseSummary => item !== undefined);
}, [result.caseOrder, result.cases]);
const [selectedSequence, setSelectedSequence] = useState(
orderedCases[0]?.truthIslandSequence ?? 1,
);
const [shadowCase, setShadowCase] = useState<L34CCase | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<ViewerMode>("after");
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
setShadowCase(null);
void fetchL34CCase(result.resultId, selectedSequence, {
signal: controller.signal,
}).then((next) => {
if (!controller.signal.aborted) setShadowCase(next);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(caught instanceof Error
? caught.message
: "Визуальный кейс L3.4C недоступен.");
}
}).finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [result.resultId, selectedSequence]);
const selectedIndex = orderedCases.findIndex(
(item) => item.truthIslandSequence === selectedSequence,
);
const navigate = (offset: -1 | 1) => {
if (selectedIndex < 0 || !orderedCases.length) return;
const next = (selectedIndex + offset + orderedCases.length) % orderedCases.length;
setSelectedSequence(orderedCases[next].truthIslandSequence);
};
const actions = (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущий L3.4C case" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующий L3.4C case" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать L3.4C case"
value={String(selectedSequence)}
options={orderedCases.map((item) => ({
value: String(item.truthIslandSequence),
label: caseLabel(item),
}))}
variant="split"
menuWidth="anchor"
searchable
searchPlaceholder="Найти кадр или группу"
onChange={(value) => setSelectedSequence(Number(value))}
/>
</div>
);
const stitch = shadowCase?.stitches[0];
const overlay = shadowCase ? (
<div className="l3-visual-audit__overlay">
<div>
<span>RAVNOVES00 · sensor.camera.right</span>
<strong>{shadowCase.sessionSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с · frame {shadowCase.frameIndex}</strong>
<small>Sequence {shadowCase.truthIslandSequence}/32 · {shadowCase.groupId}</small>
</div>
<div>
<span>Exact tile provenance · temporal seam contract</span>
<strong>{stitch ? `${stitch.sourceTiles.join(" + ")} → union` : "UNCHANGED"}</strong>
<small>{stitch ? `${stitch.temporalRunLength} consecutive frames · source IoU ${stitch.sourceIou.toLocaleString("ru-RU", { maximumFractionDigits: 2 })}` : "Не прошёл fail-closed admission"}</small>
</div>
<div className="l3-visual-audit__legend">
<span data-tone="fp">BEFORE · front и left source boxes</span>
<span data-tone="tp">AFTER · единая union geometry</span>
<span data-tone="prediction">SOURCE · чистый hash-bound кадр</span>
</div>
</div>
) : undefined;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="L3.4C temporal tile-seam stitch before/after"
mode={mode}
modes={[
{ value: "after", label: "AFTER" },
{ value: "before", label: "BEFORE" },
{ value: "source", label: "SOURCE" },
]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
{loading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем tile-aware BEFORE/AFTER evidence</span>
</div>
) : error || !shadowCase ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{error ?? "Визуальный кейс L3.4C недоступен."}</span>
</div>
) : (
<L34CTileSeamStitchScene shadowCase={shadowCase} mode={mode} />
)}
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,79 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { L34DResult } from "../../core/laboratory/l34dCumulativePostprocessing";
import { formatNumber } from "../../presentation";
import { L34DCumulativePostprocessingVisual } from "./L34DCumulativePostprocessingVisual";
function formatPercent(value: number): string {
return `${(value * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
}
export function L34DCumulativePostprocessingResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: L34DResult;
}) {
const { before, after } = result.metrics;
return (
<LaboratoryWorkTemplate
summary={<LaboratorySummary
title="LAB L3.4D · cumulative B+C candidate freeze"
description="Принятые nested-box и temporal seam операции применены одновременно к исходным L3.4 indices; любое пересечение политик отклоняет сборку."
status="Cumulative candidate frozen · не truth"
statusTone="success"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · RIGHT camera · recorded replay` },
{ label: "Композиция", value: "L3.4B nested-box + L3.4C temporal seam" },
{ label: "Инвариант", value: "Original source indices · 0 policy conflicts" },
{ label: "Полномочия", value: "Candidate frozen · blind gate закрыт" },
]}
brief={{
question: "Можно ли объединить уже принятые L3.4B и L3.4C правила в один детерминированный candidate без зависимости от порядка и без assisted-регрессии?",
approach: "Операции B и C повторно связаны с исходными L3.4 prediction indices и проверены против immutable payload. Они применяются одновременно; пересечение source indices, drift рамок, score, класса или tile provenance приводит к отказу сборки.",
principalResult: `${formatNumber(before.predictionCount, 0)}${formatNumber(after.predictionCount, 0)} рамки и ${formatNumber(before.falsePositive, 0)}${formatNumber(after.falsePositive, 0)} assisted FP. Одна nested-box и четыре seam-операции дали точную сумму count-effects; TP, FN и recall не изменились.`,
limitation: "Candidate только заморожен, но не принят: сравнение использует ту же candidate-seeded assisted review. Independent prediction-hidden labels ещё не получены; live, hardware, left camera и другой маршрут не проверялись.",
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: result.pipelineId,
components: [
{ kind: "source", name: "L3.4B immutable shadow", version: "nested-box v1", role: "source-indexed consolidation operations", identitySha256: null },
{ kind: "source", name: "L3.4C immutable shadow", version: "temporal seam v1", role: "tile provenance and seam operations", identitySha256: null },
{ kind: "algorithm", name: result.profile.profileId, version: "fail-closed source-index composition", role: "freeze one cumulative post-processing candidate", identitySha256: result.resultId.split("-").at(-1) ?? null },
],
}}
/>}
evidence={<LaboratoryEvidence
eyebrow="CUMULATIVE CANDIDATE · BEFORE / AFTER / SOURCE"
title="Пять изменённых кадров: одна nested-box и четыре temporal seam операции"
kind="diagnostic-model"
resizable
>
<L34DCumulativePostprocessingVisual result={result} />
</LaboratoryEvidence>}
result={<LaboratoryResultSummary
title="B+C композиция заморожена без assisted-регрессии"
status="Frozen candidate · not independent acceptance"
statusTone="success"
metrics={[
{ label: "Assisted precision", value: `${formatPercent(before.precisionIou50)}${formatPercent(after.precisionIou50)}`, hint: "+1,7 п.п. · не blind" },
{ label: "Assisted recall", value: formatPercent(after.recallIou50), hint: "Без изменения" },
{ label: "False positive", value: `${formatNumber(before.falsePositive, 0)}${formatNumber(after.falsePositive, 0)}`, hint: "5 на 32 кадрах" },
{ label: "B / C / conflicts", value: `${result.metrics.nestedConsolidationCount} / ${result.metrics.temporalStitchCount} / ${result.metrics.operationConflictCount}`, hint: "Count-effects additive" },
]}
conclusion={{
proved: "На текущем immutable RIGHT-наборе L3.4B и L3.4C работают на непересекающихся исходных predictions. Совместная проекция воспроизводит одну nested-box и четыре temporal seam операции, убирает пять assisted FP и не меняет TP, FN, recall или class mismatch.",
notProved: "Не доказаны blind AP/recall, независимость assisted labels, перенос на другой маршрут или камеру, live/hardware, LiDAR range, navigation и safety. Замороженный candidate ещё не является принятым detector pipeline.",
decision: "Зафиксировать этот exact B+C состав как единственный cumulative candidate. Не менять правила, thresholds, global NMS или модель до одного prediction-hidden независимого label round и одноразовой оценки после раскрытия.",
}}
/>}
/>
);
}
@@ -0,0 +1,161 @@
import { useEffect, useRef, useState } from "react";
import type {
L34DCase,
L34DOperationType,
} from "../../core/laboratory/l34dCumulativePostprocessing";
type SceneMode = "after" | "before" | "source";
function color(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
alpha = 1,
): string {
const value = getComputedStyle(host).getPropertyValue(token).trim();
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
function shortOperation(type: L34DOperationType | undefined): string {
return type === "nested-box-consolidation" ? "B nested union"
: type === "temporal-tile-seam-stitch" ? "C seam union"
: "unchanged";
}
export function L34DCumulativePostprocessingScene({
candidateCase,
mode,
}: {
candidateCase: L34DCase;
mode: SceneMode;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [image, setImage] = useState<HTMLImageElement | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
const next = new Image();
next.decoding = "async";
next.onload = () => { setImage(next); setFailed(false); };
next.onerror = () => { setImage(null); setFailed(true); };
next.src = candidateCase.cameraUrl;
return () => { next.onload = null; next.onerror = null; };
}, [candidateCase.cameraUrl]);
useEffect(() => {
const host = hostRef.current;
const canvas = canvasRef.current;
if (!host || !canvas || !image) 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 ratio = Math.min(window.devicePixelRatio, 1.5);
canvas.width = Math.round(width * ratio);
canvas.height = Math.round(height * ratio);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
context.setTransform(ratio, 0, 0, ratio, 0, 0);
context.fillStyle = color(host, "--nodedc-canvas-rgb", [5, 5, 6]);
context.fillRect(0, 0, width, height);
const scale = Math.min(
width / candidateCase.cameraWidth,
height / candidateCase.cameraHeight,
);
const drawWidth = candidateCase.cameraWidth * scale;
const drawHeight = candidateCase.cameraHeight * scale;
const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
context.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
if (mode === "source") return;
const operationBySourceIndex = new Map<number, L34DOperationType>();
candidateCase.operations.forEach((operation) => {
operation.sourcePredictionIndices.forEach((index) => {
operationBySourceIndex.set(index, operation.operationType);
});
});
const predictions = mode === "before"
? candidateCase.beforePredictions
: candidateCase.afterPredictions;
let highlightedLabelIndex = 0;
predictions.forEach((prediction) => {
const operation = mode === "before"
? operationBySourceIndex.get(prediction.predictionIndex)
: "operationTypes" in prediction
? prediction.operationTypes[0]
: undefined;
const highlighted = operation !== undefined;
const [left, top, right, bottom] = prediction.boxXyxy;
const x = offsetX + left * scale;
const y = offsetY + top * scale;
const boxWidth = (right - left) * scale;
const boxHeight = (bottom - top) * scale;
const tile = "rectificationTile" in prediction
? prediction.rectificationTile
: prediction.sourceRectificationTiles.join("+");
const stroke = highlighted
? mode === "after"
? color(host, "--nodedc-success-rgb", [181, 255, 90])
: operation === "nested-box-consolidation"
? color(host, "--nodedc-warning-rgb", [255, 197, 92])
: tile === "front"
? color(host, "--nodedc-accent-rgb", [232, 56, 126])
: color(host, "--nodedc-warning-rgb", [255, 197, 92])
: color(host, "--nodedc-foreground-rgb", [240, 240, 240], 0.35);
context.strokeStyle = stroke;
context.lineWidth = highlighted
? Math.max(2, 2.7 * scale)
: Math.max(1, 1.1 * scale);
context.setLineDash(mode === "before" && highlighted ? [7, 4] : []);
context.strokeRect(x, y, boxWidth, boxHeight);
context.setLineDash([]);
if (!highlighted) return;
const label = mode === "before"
? `BEFORE · ${operation === "nested-box-consolidation" ? "B nested" : tile} · ${prediction.category} · ${(prediction.score * 100).toFixed(0)}%`
: `AFTER · ${shortOperation(operation)} · ${prediction.category} · ${(prediction.score * 100).toFixed(0)}%`;
const fontSize = Math.max(10, 11 * scale);
context.font = `600 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = context.measureText(label).width + 12;
const labelHeight = fontSize + 8;
const labelX = Math.min(
offsetX + drawWidth - labelWidth,
Math.max(offsetX, x),
);
const labelY = Math.max(
offsetY,
y - labelHeight - 3 - highlightedLabelIndex * (labelHeight + 3),
);
highlightedLabelIndex += 1;
context.fillStyle = color(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.9);
context.fillRect(labelX, labelY, labelWidth, labelHeight);
context.fillStyle = stroke;
context.fillText(label, labelX + 6, labelY + fontSize + 1);
});
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [candidateCase, image, mode]);
return (
<div className="l32-camera-scene" ref={hostRef}>
<canvas
ref={canvasRef}
role="img"
aria-label={`L3.4D frame ${candidateCase.frameIndex}: ${mode} cumulative candidate`}
/>
{failed ? (
<div className="l3-visual-audit__state" role="status">
Точный кадр L3.4D недоступен.
</div>
) : null}
</div>
);
}
@@ -0,0 +1,173 @@
import { useEffect, useMemo, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchL34DCase,
type L34DCase,
type L34DCaseSummary,
type L34DOperationType,
type L34DResult,
} from "../../core/laboratory/l34dCumulativePostprocessing";
import { L34DCumulativePostprocessingScene } from "./L34DCumulativePostprocessingScene";
type ViewerMode = "after" | "before" | "source";
function operationLabel(types: readonly L34DOperationType[]): string {
return types.includes("nested-box-consolidation")
? "B · NESTED"
: types.includes("temporal-tile-seam-stitch")
? "C · SEAM"
: "UNCHANGED";
}
function caseLabel(item: L34DCaseSummary): string {
return `${item.truthIslandSequence}/32 · frame ${item.frameIndex} · ${item.groupId} · ${operationLabel(item.operationTypes)}`;
}
export function L34DCumulativePostprocessingVisual({
result,
}: {
result: L34DResult;
}) {
const orderedCases = useMemo(() => {
const bySequence = new Map(
result.cases.map((item) => [item.truthIslandSequence, item]),
);
return result.caseOrder
.map((sequence) => bySequence.get(sequence))
.filter((item): item is L34DCaseSummary => item !== undefined);
}, [result.caseOrder, result.cases]);
const [selectedSequence, setSelectedSequence] = useState(
orderedCases[0]?.truthIslandSequence ?? 1,
);
const [candidateCase, setCandidateCase] = useState<L34DCase | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<ViewerMode>("after");
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
setCandidateCase(null);
void fetchL34DCase(result.resultId, selectedSequence, {
signal: controller.signal,
}).then((next) => {
if (!controller.signal.aborted) setCandidateCase(next);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(caught instanceof Error
? caught.message
: "Визуальный кейс L3.4D недоступен.");
}
}).finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [result.resultId, selectedSequence]);
const selectedIndex = orderedCases.findIndex(
(item) => item.truthIslandSequence === selectedSequence,
);
const navigate = (offset: -1 | 1) => {
if (selectedIndex < 0 || !orderedCases.length) return;
const next = (selectedIndex + offset + orderedCases.length)
% orderedCases.length;
setSelectedSequence(orderedCases[next].truthIslandSequence);
};
const actions = (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущий L3.4D case" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующий L3.4D case" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать L3.4D case"
value={String(selectedSequence)}
options={orderedCases.map((item) => ({
value: String(item.truthIslandSequence),
label: caseLabel(item),
}))}
variant="split"
menuWidth="anchor"
searchable
searchPlaceholder="Найти кадр или операцию"
onChange={(value) => setSelectedSequence(Number(value))}
/>
</div>
);
const operation = candidateCase?.operations[0];
const operationSummary = operation?.operationType
=== "nested-box-consolidation"
? "L3.4B nested-box → union"
: operation?.operationType === "temporal-tile-seam-stitch"
? `${operation.sourceTiles.join(" + ")} → union`
: "UNCHANGED";
const operationDetail = operation?.operationType
=== "nested-box-consolidation"
? `${operation.sourcePredictionIndices.length} nested source boxes · ${operation.category}`
: operation?.operationType === "temporal-tile-seam-stitch"
? `${operation.temporalRunLength} consecutive frames · ${operation.category}`
: "Ни одно принятое правило не изменяет этот кадр";
const overlay = candidateCase ? (
<div className="l3-visual-audit__overlay">
<div>
<span>RAVNOVES00 · sensor.camera.right</span>
<strong>{candidateCase.sessionSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с · frame {candidateCase.frameIndex}</strong>
<small>Sequence {candidateCase.truthIslandSequence}/32 · {candidateCase.groupId}</small>
</div>
<div>
<span>Frozen source-index cumulative candidate</span>
<strong>{operationSummary}</strong>
<small>{operationDetail}</small>
</div>
<div className="l3-visual-audit__legend">
<span data-tone="fp">BEFORE · исходные B/C boxes</span>
<span data-tone="tp">AFTER · frozen cumulative geometry</span>
<span data-tone="prediction">SOURCE · чистый hash-bound кадр</span>
</div>
</div>
) : undefined;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="L3.4D cumulative post-processing before/after"
mode={mode}
modes={[
{ value: "after", label: "AFTER" },
{ value: "before", label: "BEFORE" },
{ value: "source", label: "SOURCE" },
]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
{loading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем cumulative BEFORE/AFTER evidence</span>
</div>
) : error || !candidateCase ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{error ?? "Визуальный кейс L3.4D недоступен."}</span>
</div>
) : (
<L34DCumulativePostprocessingScene
candidateCase={candidateCase}
mode={mode}
/>
)}
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,80 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { L34EResult } from "../../core/laboratory/l34eSelfReviewDiagnostic";
import { formatNumber } from "../../presentation";
import { L34ESelfReviewDiagnosticVisual } from "./L34ESelfReviewDiagnosticVisual";
function percent(value: number): string {
return `${(value * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
}
export function L34ESelfReviewDiagnosticResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: L34EResult;
}) {
const strict = result.metrics.strictIou50;
const diagnostic = result.metrics.diagnosticAssociation;
return (
<LaboratoryWorkTemplate
summary={<LaboratorySummary
title="LAB L3.4E · self-review diagnostic disagreement audit"
description="Ручной prediction-hidden проход сопоставлен с exact L3.4D candidate. Строгий IoU50 сохранён как технический срез, но отделён от объектных совпадений и ошибок локализации."
status="Diagnostic complete · reference не metric-grade"
statusTone="warning"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · RIGHT camera · recorded replay` },
{ label: "Разметка", value: "32/32 · 260 manual objects · 0 prelabels" },
{ label: "Сопоставление", value: "IoU50 strict → loose visual association" },
{ label: "Полномочия", value: "Not truth · retuning запрещён · blind gate закрыт" },
]}
brief={{
question: "Что именно расходится между замороженным L3.4D candidate и ручным self-review: существование объекта, класс или геометрия рамки?",
approach: "Сначала выполнен воспроизводимый greedy IoU50. Затем только для оставшихся объектов проведена диагностическая spatial-association по IoU ≥ 0,10 или overlap-over-smaller ≥ 0,30. Каждый конфликт опубликован поверх точного hash-bound source frame.",
principalResult: `${formatNumber(diagnostic.associatedPairCount, 0)} объектных пар сопоставлены: ${formatNumber(diagnostic.strictAlignment, 0)} строгих, ${formatNumber(diagnostic.localizationDisagreement, 0)} с разъехавшимися рамками и ${formatNumber(diagnostic.strictClassMismatch + diagnostic.classAndLocalizationDisagreement, 0)} с конфликтом класса. Без пары остались ${formatNumber(diagnostic.predictionOnly, 0)} candidate и ${formatNumber(diagnostic.referenceOnly, 0)} self-review объектов.`,
limitation: `Strict IoU50 F1 = ${percent(strict.f1Iou50)} не является качеством детектора: ручные рамки грубые, особенно на дальних машинах, а reviewer видел identity candidate. Нужны refinement/adjudication и независимый reviewer.`,
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: result.pipelineId,
components: [
{ kind: "source", name: "L3.4D immutable candidate", version: "cumulative B+C freeze", role: "candidate boxes and exact source binding", identitySha256: null },
{ kind: "source", name: "Manual self-review", version: "revision 3 · prediction-hidden", role: "coarse diagnostic reference · not truth", identitySha256: null },
{ kind: "algorithm", name: result.profile.profileId, version: "strict IoU50 + loose spatial association", role: "classify alignment, localization and unmatched objects", identitySha256: result.resultId.split("-").at(-1) ?? null },
],
}}
/>}
evidence={<LaboratoryEvidence
eyebrow="DIAGNOSTIC MISMATCH GALLERY · OVERLAY / CANDIDATE / SELF-REVIEW / SOURCE"
title="Все 32 кадра отсортированы по тяжести расхождения"
kind="diagnostic-model"
resizable
>
<L34ESelfReviewDiagnosticVisual result={result} />
</LaboratoryEvidence>}
result={<LaboratoryResultSummary
title="Self-review выявил расхождение, но не дал metric-grade benchmark"
status="Refine labels before detector decision"
statusTone="warning"
metrics={[
{ label: "Object associations", value: `${formatNumber(diagnostic.associatedPairCount, 0)} / ${formatNumber(diagnostic.referenceCount, 0)}`, hint: `${percent(diagnostic.referenceAssociationCoverage)} self-review coverage` },
{ label: "Localization", value: formatNumber(diagnostic.localizationDisagreement, 0), hint: "Object paired · geometry disagrees" },
{ label: "Unmatched P / R", value: `${formatNumber(diagnostic.predictionOnly, 0)} / ${formatNumber(diagnostic.referenceOnly, 0)}`, hint: "Requires visual adjudication" },
{ label: "Strict IoU50 F1", value: percent(strict.f1Iou50), hint: "Diagnostic only · not detector accuracy" },
]}
conclusion={{
proved: "Ручной проход завершён без prelabels, exact L3.4D source binding сохранён, и каждый конфликт можно визуально проверить. Основная масса strict FP/FN вызвана не только наличием объектов, но и сильным расхождением геометрии ручных и модельных рамок.",
notProved: "Не доказаны detector precision/recall/AP, регрессия или прогрессия L3.4D, пригодность ручных рамок как metric-grade truth и независимость review. Loose associations являются только маршрутизацией визуального аудита.",
decision: "Не перетюнивать модель, thresholds или post-processing по этим агрегатам. Сначала пройти mismatch gallery, уточнить спорные ручные рамки и классы, провести adjudication, затем отдать exact frozen candidate независимому reviewer и выполнить один acceptance calculation.",
}}
/>}
/>
);
}
@@ -0,0 +1,216 @@
import { useEffect, useRef, useState } from "react";
import type {
L34ECase,
L34EDiagnosticVerdict,
} from "../../core/laboratory/l34eSelfReviewDiagnostic";
export type L34ESceneMode = "overlay" | "candidate" | "review" | "source";
function color(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
alpha = 1,
): string {
const value = getComputedStyle(host).getPropertyValue(token).trim();
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
function verdictColor(
host: HTMLElement,
verdict: L34EDiagnosticVerdict,
reference: boolean,
): string {
if (verdict === "strict_alignment") {
return color(host, "--nodedc-success-rgb", [181, 255, 90], 0.42);
}
if (verdict === "localization_disagreement") {
return color(host, "--nodedc-warning-rgb", [255, 197, 92], 0.95);
}
if (verdict === "strict_class_mismatch"
|| verdict === "class_and_localization_disagreement") {
return color(host, "--nodedc-accent-rgb", [232, 56, 126], 0.98);
}
return reference
? color(host, "--nodedc-accent-rgb", [232, 56, 126], 1)
: color(host, "--nodedc-danger-rgb", [255, 94, 94], 1);
}
function shortVerdict(verdict: L34EDiagnosticVerdict): string {
return verdict === "strict_alignment" ? "STRICT"
: verdict === "localization_disagreement" ? "LOC"
: verdict === "strict_class_mismatch" ? "CLASS"
: verdict === "class_and_localization_disagreement" ? "CLASS+LOC"
: verdict === "prediction_only" ? "P-ONLY"
: "R-ONLY";
}
export function L34ESelfReviewDiagnosticScene({
diagnosticCase,
mode,
}: {
diagnosticCase: L34ECase;
mode: L34ESceneMode;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [image, setImage] = useState<HTMLImageElement | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
const next = new Image();
next.decoding = "async";
next.onload = () => { setImage(next); setFailed(false); };
next.onerror = () => { setImage(null); setFailed(true); };
next.src = diagnosticCase.cameraUrl;
return () => { next.onload = null; next.onerror = null; };
}, [diagnosticCase.cameraUrl]);
useEffect(() => {
const host = hostRef.current;
const canvas = canvasRef.current;
if (!host || !canvas || !image) 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.fillStyle = color(host, "--nodedc-canvas-rgb", [5, 5, 6]);
context.fillRect(0, 0, width, height);
const scale = Math.min(
width / diagnosticCase.cameraWidth,
height / diagnosticCase.cameraHeight,
);
const drawWidth = diagnosticCase.cameraWidth * scale;
const drawHeight = diagnosticCase.cameraHeight * scale;
const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
context.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
if (mode === "source") return;
const labelRects: { x: number; y: number; width: number; height: number }[] = [];
const drawBox = ({
box,
category,
verdict,
reference,
}: {
box: readonly [number, number, number, number];
category: string;
verdict: L34EDiagnosticVerdict;
reference: boolean;
}) => {
const [left, top, right, bottom] = box;
const x = offsetX + left * scale;
const y = offsetY + top * scale;
const boxWidth = (right - left) * scale;
const boxHeight = (bottom - top) * scale;
const stroke = verdictColor(host, verdict, reference);
context.strokeStyle = stroke;
context.lineWidth = verdict === "strict_alignment"
? Math.max(1, 1.1 * scale)
: Math.max(2, 2.4 * scale);
context.setLineDash(reference ? [7, 4] : []);
context.strokeRect(x, y, boxWidth, boxHeight);
context.setLineDash([]);
if (verdict === "strict_alignment" || mode === "overlay") return;
const label = `${shortVerdict(verdict)} · ${category}`;
const fontSize = Math.max(10, 11 * scale);
context.font = `600 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = context.measureText(label).width + 12;
const labelHeight = fontSize + 8;
const labelX = Math.min(
offsetX + drawWidth - labelWidth,
Math.max(offsetX, x),
);
const candidateY = [
y - labelHeight - 2,
y + boxHeight + 2,
...Array.from({ length: 8 }, (_, index) => (
y - (index + 2) * (labelHeight + 2)
)),
...Array.from({ length: 8 }, (_, index) => (
y + boxHeight + (index + 2) * (labelHeight + 2)
)),
];
const fits = (candidate: number) => {
if (candidate < offsetY
|| candidate + labelHeight > offsetY + drawHeight) return false;
return labelRects.every((rect) => (
labelX + labelWidth <= rect.x
|| rect.x + rect.width <= labelX
|| candidate + labelHeight <= rect.y
|| rect.y + rect.height <= candidate
));
};
const labelY = candidateY.find(fits)
?? Math.min(offsetY + drawHeight - labelHeight, Math.max(offsetY, y));
labelRects.push({ x: labelX, y: labelY, width: labelWidth, height: labelHeight });
const boxAnchorX = x + boxWidth / 2;
const boxAnchorY = y;
const labelAnchorX = Math.min(
labelX + labelWidth,
Math.max(labelX, boxAnchorX),
);
const labelAnchorY = labelY > boxAnchorY
? labelY
: labelY + labelHeight;
context.strokeStyle = stroke;
context.lineWidth = 1;
context.beginPath();
context.moveTo(boxAnchorX, boxAnchorY);
context.lineTo(labelAnchorX, labelAnchorY);
context.stroke();
context.fillStyle = color(host, "--nodedc-canvas-rgb", [5, 5, 6], 0.88);
context.fillRect(labelX, labelY, labelWidth, labelHeight);
context.fillStyle = stroke;
context.fillText(label, labelX + 6, labelY + fontSize + 1);
};
if (mode === "overlay" || mode === "candidate") {
diagnosticCase.predictions.forEach((prediction) => drawBox({
box: prediction.boxXyxy,
category: prediction.category,
verdict: prediction.diagnosticVerdict,
reference: false,
}));
}
if (mode === "overlay" || mode === "review") {
diagnosticCase.references.forEach((reference) => drawBox({
box: reference.boxXyxy,
category: reference.displayCategory,
verdict: reference.diagnosticVerdict,
reference: true,
}));
}
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [diagnosticCase, image, mode]);
return (
<div className="l32-camera-scene" ref={hostRef}>
<canvas
ref={canvasRef}
role="img"
aria-label={`L3.4E frame ${diagnosticCase.frameIndex}: ${mode} self-review diagnostic`}
/>
{failed ? (
<div className="l3-visual-audit__state" role="status">
Точный кадр L3.4E недоступен.
</div>
) : null}
</div>
);
}
@@ -0,0 +1,156 @@
import { useEffect, useMemo, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchL34ECase,
type L34ECase,
type L34ECaseSummary,
type L34EResult,
} from "../../core/laboratory/l34eSelfReviewDiagnostic";
import {
L34ESelfReviewDiagnosticScene,
type L34ESceneMode,
} from "./L34ESelfReviewDiagnosticScene";
function caseLabel(item: L34ECaseSummary): string {
const summary = item.diagnosticSummary;
return `${item.truthIslandSequence}/32 · frame ${item.frameIndex} · LOC ${summary.localizationDisagreement} · P ${summary.predictionOnly} · R ${summary.referenceOnly}`;
}
export function L34ESelfReviewDiagnosticVisual({
result,
}: {
result: L34EResult;
}) {
const orderedCases = useMemo(() => {
const bySequence = new Map(
result.cases.map((item) => [item.truthIslandSequence, item]),
);
return result.caseOrder
.map((sequence) => bySequence.get(sequence))
.filter((item): item is L34ECaseSummary => item !== undefined);
}, [result.caseOrder, result.cases]);
const [selectedSequence, setSelectedSequence] = useState(
orderedCases[0]?.truthIslandSequence ?? 1,
);
const [diagnosticCase, setDiagnosticCase] = useState<L34ECase | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<L34ESceneMode>("overlay");
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
setDiagnosticCase(null);
void fetchL34ECase(result.resultId, selectedSequence, {
signal: controller.signal,
}).then((next) => {
if (!controller.signal.aborted) setDiagnosticCase(next);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(caught instanceof Error
? caught.message
: "Визуальный кейс L3.4E недоступен.");
}
}).finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [result.resultId, selectedSequence]);
const selectedIndex = orderedCases.findIndex(
(item) => item.truthIslandSequence === selectedSequence,
);
const navigate = (offset: -1 | 1) => {
if (selectedIndex < 0 || !orderedCases.length) return;
const next = (selectedIndex + offset + orderedCases.length)
% orderedCases.length;
setSelectedSequence(orderedCases[next].truthIslandSequence);
};
const actions = (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущий конфликт L3.4E" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующий конфликт L3.4E" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать конфликтный кадр L3.4E"
value={String(selectedSequence)}
options={orderedCases.map((item) => ({
value: String(item.truthIslandSequence),
label: caseLabel(item),
}))}
variant="split"
menuWidth="anchor"
searchable
searchPlaceholder="Найти кадр или тип расхождения"
onChange={(value) => setSelectedSequence(Number(value))}
/>
</div>
);
const summary = diagnosticCase?.diagnosticSummary;
const overlay = diagnosticCase && summary ? (
<div className="l3-visual-audit__overlay">
<div>
<span>RAVNOVES00 · sensor.camera.right</span>
<strong>{diagnosticCase.sessionSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с · frame {diagnosticCase.frameIndex}</strong>
<small>Sequence {diagnosticCase.truthIslandSequence}/32 · {diagnosticCase.groupId}</small>
</div>
<div>
<span>Diagnostic self-review · не truth</span>
<strong>{summary.associatedPairCount} пар · {summary.localizationDisagreement} LOC · {summary.strictClassMismatch + summary.classAndLocalizationDisagreement} CLASS</strong>
<small>{summary.predictionOnly} candidate-only · {summary.referenceOnly} review-only</small>
</div>
<div className="l3-visual-audit__legend">
<span data-tone="tp">STRICT · совпавшая геометрия</span>
<span data-tone="warning">LOC · объект совпал, рамка разъехалась</span>
<span data-tone="fp">P-ONLY · только candidate</span>
<span data-tone="fn">R-ONLY · только self-review</span>
</div>
</div>
) : undefined;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="L3.4E candidate and manual self-review disagreement gallery"
mode={mode}
modes={[
{ value: "overlay", label: "OVERLAY" },
{ value: "candidate", label: "CANDIDATE" },
{ value: "review", label: "SELF-REVIEW" },
{ value: "source", label: "SOURCE" },
]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
{loading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем hash-bound mismatch evidence</span>
</div>
) : error || !diagnosticCase ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{error ?? "Визуальный кейс L3.4E недоступен."}</span>
</div>
) : (
<L34ESelfReviewDiagnosticScene
diagnosticCase={diagnosticCase}
mode={mode}
/>
)}
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -0,0 +1,112 @@
import { useEffect, useState } from "react";
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import { fetchL34ESelfReviewDiagnostic, type L34EResult } from "../../core/laboratory/l34eSelfReviewDiagnostic";
import type { L34FFrozenResult } from "../../core/laboratory/l34fAdjudication";
import { formatNumber } from "../../presentation";
import { L34ESelfReviewDiagnosticVisual } from "./L34ESelfReviewDiagnosticVisual";
function FrozenEvidence({ result }: { result: L34FFrozenResult }) {
const [diagnostic, setDiagnostic] = useState<L34EResult | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
void fetchL34ESelfReviewDiagnostic({ signal: controller.signal })
.then((value) => {
if (controller.signal.aborted) return;
if (value.resultId !== result.diagnosticResultId) {
setError("Hash-bound diagnostic source L3.4E не совпал с L3.4F.");
return;
}
setDiagnostic(value);
})
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(caught instanceof Error ? caught.message : "Evidence L3.4F недоступен.");
}
});
return () => controller.abort();
}, [result.diagnosticResultId]);
if (error) return <div className="l3-visual-audit__state" role="status">{error}</div>;
if (!diagnostic) {
return <div className="l3-visual-audit__state" role="status"><span className="busy-indicator" aria-hidden="true" />Открываем frozen evidence L3.4F</div>;
}
return <L34ESelfReviewDiagnosticVisual result={diagnostic} />;
}
export function L34FAdjudicatedReferenceResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: L34FFrozenResult;
}) {
const changedObjects = result.metrics.geometryChangedObjectCount
+ result.metrics.classChangedObjectCount
+ result.metrics.attributeChangedObjectCount
+ result.metrics.addedObjectCount
+ result.metrics.deletedObjectCount;
return (
<LaboratoryWorkTemplate
summary={<LaboratorySummary
title="LAB L3.4F · candidate-visible adjudicated engineering reference"
description="Все 32 конфликтных кадра L3.4E повторно просмотрены в полноэкранном редакторе: candidate оставался read-only, human-reference можно было двигать, ресайзить, удалять, добавлять и переклассифицировать."
status="Adjudication complete · engineering reference · not truth"
statusTone="warning"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · RIGHT camera · recorded replay` },
{ label: "Проход", value: `${result.metrics.frameCount}/32 · ${result.metrics.adjudicatedReferenceCount} human objects` },
{ label: "Правки", value: `${result.metrics.changedFrameCount} кадров · ${changedObjects} операций` },
{ label: "Полномочия", value: "Candidate не принят · retuning запрещён · L3.5 закрыт" },
]}
brief={{
question: "Нужно ли исправлять человеческую self-review разметку после визуального разбора расхождений с candidate L3.4D?",
approach: "Каждый конфликт открыт на exact hash-bound source frame в четырёх слоях: OVERLAY, CANDIDATE, HUMAN и SOURCE. Candidate был видим, но неизменяем; сохранение и freeze разрешались только после отметки 32/32 кадров.",
principalResult: `${formatNumber(result.metrics.unchangedObjectCount, 0)} из ${formatNumber(result.metrics.sourceReferenceCount, 0)} исходных human-объектов оставлены без изменений. Добавлено ${formatNumber(result.metrics.addedObjectCount, 0)}, удалено ${formatNumber(result.metrics.deletedObjectCount, 0)}, геометрия изменена у ${formatNumber(result.metrics.geometryChangedObjectCount, 0)} объектов, класс — у ${formatNumber(result.metrics.classChangedObjectCount, 0)}.`,
limitation: "Reviewer видел candidate и ранее создавал исходную self-review разметку. Поэтому результат полезен как инженерная reference-разметка и визуальный протокол, но не является independent или metric-grade truth.",
}}
method={{
completeness: "complete",
executionClass: "hybrid",
pipelineId: "l34e-candidate-visible-adjudication/v1",
components: [
{ kind: "source", name: "L3.4E mismatch gallery", version: result.diagnosticResultId, role: "candidate + self-review + exact source", identitySha256: result.diagnosticResultId.split("-").at(-1) ?? null },
{ kind: "algorithm", name: "Fullscreen box adjudication", version: "revisioned 32/32 gate", role: "move, resize, add, delete, relabel, approve", identitySha256: null },
{ kind: "model", name: "Frozen L3.4D candidate", version: "scores hidden", role: "visible read-only comparison layer", identitySha256: null },
],
}}
/>}
evidence={<LaboratoryEvidence
eyebrow="FROZEN VISUAL EVIDENCE · OVERLAY / CANDIDATE / HUMAN / SOURCE"
title="32/32 кадров доступны для повторной визуальной проверки"
kind="diagnostic-model"
resizable
>
<FrozenEvidence result={result} />
</LaboratoryEvidence>}
result={<LaboratoryResultSummary
title="Разметка выдержала adjudication, но detector gate всё ещё закрыт"
status="Two independent blind reviews required before E48 / L3.5"
statusTone="warning"
metrics={[
{ label: "Reviewed", value: `${result.metrics.frameCount}/32`, hint: "Все конфликтные кадры" },
{ label: "Human objects", value: formatNumber(result.metrics.adjudicatedReferenceCount, 0), hint: `${formatNumber(result.metrics.unchangedObjectCount, 0)} unchanged` },
{ label: "Changed frames", value: formatNumber(result.metrics.changedFrameCount, 0), hint: `${formatNumber(changedObjects, 0)} edit operations` },
{ label: "Blind authority", value: "0", hint: "Not independent truth" },
]}
conclusion={{
proved: "Полный candidate-visible разбор завершён и заморожен как воспроизводимый immutable artifact. На просмотренных кадрах не нашлось честных оснований менять исходные human boxes только ради лучшего совпадения с моделью.",
notProved: "Не доказаны точность детектора, AP/precision/recall, прогрессия L3.4D, независимость разметки или пригодность результата для safety/navigation. Ноль правок не означает ноль ошибок модели.",
decision: "Не трогать модель по этому набору. Следующий шаг — два разных реальных reviewer получают те же 32 source frames без candidate identity, predictions и этой разметки. После двух freeze выполняется отдельная adjudication, затем E48 seal и только потом одноразовый L3.5 acceptance calculation.",
}}
/>}
/>
);
}
@@ -0,0 +1,122 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type {
L34RightYoloxTruthIslandResult,
} from "../../core/laboratory/l34RightYoloxTruthIsland";
import { formatNumber } from "../../presentation";
import { L34RightYoloxTruthIslandVisual } from "./L34RightYoloxTruthIslandVisual";
function digest(value: string): string | null {
const candidate = value.split("-").at(-1) ?? "";
return /^[a-f0-9]{64}$/.test(candidate) ? candidate : null;
}
export function L34RightYoloxTruthIslandResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: L34RightYoloxTruthIslandResult;
}) {
const metrics = result.metrics;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB L3.4 · RIGHT YOLOX truth-island freeze"
description="Первый честный benchmark-прогон текущего camera-first YOLOX pipeline: точные предсказания заморожены на 32 скрытых кадрах RAVNOVES00 до раскрытия независимой человеческой разметки."
status="Ожидает независимую truth"
statusTone="warning"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · RIGHT camera · recorded replay` },
{ label: "Кандидат", value: `${result.candidate.architecture} · score ≥ ${result.candidate.minimumScore}` },
{ label: "Truth island", value: `${formatNumber(metrics.frameCount, 0)} кадров · ${formatNumber(metrics.temporalGroupCount, 0)} групп` },
{ label: "Состояние", value: "Predictions frozen · labels unread" },
]}
brief={{
question: "Как текущий YOLOX-S распознаёт task-relevant объекты на заранее выбранных скрытых кадрах RAVNOVES00?",
approach: "Использован только записанный sensor.camera.right. Из E46 взяты 32 model-independent кадра; из полного YOLOX replay зафиксированы все допущенные detection boxes при score ≥ 0.25. Human labels не читались и не использовались для настройки.",
principalResult: `Заморожено ${formatNumber(metrics.predictionCount, 0)} предсказаний на ${formatNumber(metrics.framesWithPredictions, 0)} из ${formatNumber(metrics.frameCount, 0)} кадров. Это полный candidate freeze, а не метрика качества.`,
limitation: "AP, recall, misses и false positives пока не существуют: их можно вычислить только после двух независимых blind-review и adjudication. Live, hardware, левая камера и второй маршрут исключены из этого прогона.",
}}
method={{
completeness: "complete",
executionClass: "ai-inference",
pipelineId: result.pipelineId,
components: [
{
kind: "source",
name: result.truthIsland.resultId,
version: "E46 blind truth island · labels unavailable",
role: "model-independent frame selection",
identitySha256: digest(result.truthIsland.resultId),
},
{
kind: "model",
name: result.candidate.architecture,
version: `COCO-80 · score ≥ ${result.candidate.minimumScore}`,
role: "right-camera task-relevant 2D detection candidate",
identitySha256: result.candidate.modelSha256,
},
{
kind: "algorithm",
name: result.profileId,
version: "candidate-freeze/v1",
role: "freeze predictions before independent truth reveal",
identitySha256: digest(result.resultId),
},
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="FROZEN CANDIDATE · NO TRUTH REVEAL"
title="Правые кадры RAVNOVES00 и точные frozen YOLOX boxes"
kind="diagnostic-model"
resizable
>
<L34RightYoloxTruthIslandVisual result={result} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Кандидат зафиксирован; оценка качества ещё закрыта"
status="Human truth required"
statusTone="warning"
metrics={[
{
label: "Кадры",
value: formatNumber(metrics.frameCount, 0),
hint: `${formatNumber(metrics.temporalGroupCount, 0)} независимых temporal/anchor групп`,
},
{
label: "Предсказания",
value: formatNumber(metrics.predictionCount, 0),
hint: "content-addressed freeze",
},
{
label: "Truth read",
value: result.decision.truthLabelsRead ? "Да" : "Нет",
hint: "blindness contract preserved",
},
{
label: "Accuracy",
value: metrics.accuracyMetricsAvailable ? "Доступна" : "Недоступна",
hint: "не подменяется descriptive counts",
},
]}
conclusion={{
proved: "Текущий YOLOX-кандидат воспроизводимо привязан к одному записанному источнику, одной правой камере и заранее выбранным 32 кадрам; предсказания заморожены до раскрытия truth.",
notProved: "Не доказаны accuracy, recall, FP/FN, качество LiDAR range, перенос на другой маршрут, live transport, hardware runtime, navigation или safety.",
decision: "Не тюнить модель по этим кадрам. Следующий gate — две независимые слепые разметки E46, adjudication и вычисление AP/recall по этой точной prediction generation.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,212 @@
import { useEffect, useRef, useState } from "react";
import type {
L34Prediction,
L34VisualFrame,
} from "../../core/laboratory/l34RightYoloxTruthIsland";
function tokenColor(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
alpha = 1,
): string {
const value = getComputedStyle(host).getPropertyValue(token).trim();
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}
function predictionLabel(prediction: L34Prediction): string {
const names: Readonly<Record<string, string>> = {
car: "Авто",
heavy_vehicle: "Тяжёлый транспорт",
person: "Человек",
bicycle: "Велосипед",
motorcycle: "Мотоцикл",
static_obstacle: "Препятствие",
animal: "Животное",
};
return `${names[prediction.label] ?? prediction.label} · ${(prediction.score * 100).toFixed(0)}%`;
}
type LabelRect = Readonly<{
x: number;
y: number;
width: number;
height: number;
}>;
function overlaps(left: LabelRect, right: LabelRect): boolean {
const margin = 3;
return !(
left.x + left.width + margin <= right.x
|| right.x + right.width + margin <= left.x
|| left.y + left.height + margin <= right.y
|| right.y + right.height + margin <= left.y
);
}
function placeLabel(
box: LabelRect,
labelWidth: number,
labelHeight: number,
imageBounds: LabelRect,
occupied: readonly LabelRect[],
): LabelRect | null {
const x = Math.min(
imageBounds.x + imageBounds.width - labelWidth,
Math.max(imageBounds.x, box.x),
);
const candidates = [
box.y - labelHeight - 3,
box.y + box.height + 3,
box.y + 3,
].map((y) => ({ x, y, width: labelWidth, height: labelHeight }));
return candidates.find((candidate) => (
candidate.y >= imageBounds.y
&& candidate.y + candidate.height <= imageBounds.y + imageBounds.height
&& occupied.every((current) => !overlaps(candidate, current))
)) ?? null;
}
export function L34RightYoloxTruthIslandScene({
frame,
predictionsVisible,
}: {
frame: L34VisualFrame;
predictionsVisible: boolean;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [image, setImage] = useState<HTMLImageElement | null>(null);
const [error, setError] = useState(false);
useEffect(() => {
const next = new Image();
next.decoding = "async";
next.onload = () => {
setError(false);
setImage(next);
};
next.onerror = () => {
setImage(null);
setError(true);
};
next.src = frame.cameraUrl;
return () => {
next.onload = null;
next.onerror = null;
};
}, [frame.cameraUrl]);
useEffect(() => {
const host = hostRef.current;
const canvas = canvasRef.current;
if (!host || !canvas || !image) 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 ratio = Math.min(window.devicePixelRatio, 1.5);
canvas.width = Math.round(width * ratio);
canvas.height = Math.round(height * ratio);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
context.setTransform(ratio, 0, 0, ratio, 0, 0);
context.fillStyle = tokenColor(host, "--nodedc-canvas-rgb", [5, 5, 6]);
context.fillRect(0, 0, width, height);
const scale = Math.min(width / frame.cameraWidth, height / frame.cameraHeight);
const drawWidth = frame.cameraWidth * scale;
const drawHeight = frame.cameraHeight * scale;
const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
context.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
if (!predictionsVisible) return;
const predictionColor = tokenColor(
host,
"--nodedc-accent-rgb",
[174, 255, 88],
0.96,
);
const labelSurface = tokenColor(
host,
"--nodedc-canvas-rgb",
[5, 5, 6],
0.84,
);
const boxes = frame.predictions.map((prediction) => {
const [left, top, right, bottom] = prediction.bboxXyxy;
const box = {
x: offsetX + left * scale,
y: offsetY + top * scale,
width: (right - left) * scale,
height: (bottom - top) * scale,
};
context.strokeStyle = predictionColor;
context.lineWidth = Math.max(1.25, 1.8 * scale);
context.strokeRect(box.x, box.y, box.width, box.height);
return { prediction, box };
});
const occupied: LabelRect[] = [];
const imageBounds = {
x: offsetX,
y: offsetY,
width: drawWidth,
height: drawHeight,
};
[...boxes]
.sort((left, right) => right.prediction.score - left.prediction.score)
.forEach(({ prediction, box }) => {
const label = predictionLabel(prediction);
const fontSize = Math.max(9, 10 * scale);
const labelHeight = fontSize + 7;
context.font = `600 ${fontSize}px Inter, system-ui, sans-serif`;
const labelWidth = context.measureText(label).width + 12;
const placement = placeLabel(
box,
labelWidth,
labelHeight,
imageBounds,
occupied,
);
if (!placement) return;
occupied.push(placement);
context.fillStyle = labelSurface;
context.fillRect(
placement.x,
placement.y,
placement.width,
placement.height,
);
context.fillStyle = predictionColor;
context.fillText(
label,
placement.x + 6,
placement.y + fontSize + 1,
);
});
};
const observer = new ResizeObserver(render);
observer.observe(host);
render();
return () => observer.disconnect();
}, [frame, image, predictionsVisible]);
return (
<div className="l32-camera-scene" ref={hostRef}>
<canvas
ref={canvasRef}
role="img"
aria-label={`L3.4 right-camera frame ${frame.frameIndex}, ${predictionsVisible ? `${frame.predictions.length} frozen predictions` : "immutable source only"}`}
/>
{error ? (
<div className="l3-visual-audit__state" role="status">
Точный кадр правой камеры L3.4 недоступен.
</div>
) : null}
</div>
);
}
@@ -0,0 +1,150 @@
import { useEffect, useState } from "react";
import { Icon, IconButton, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchL34RightYoloxTruthIslandFrame,
type L34FrameSummary,
type L34RightYoloxTruthIslandResult,
type L34VisualFrame,
} from "../../core/laboratory/l34RightYoloxTruthIsland";
import { L34RightYoloxTruthIslandScene } from "./L34RightYoloxTruthIslandScene";
type ViewerMode = "predictions" | "source";
function frameLabel(frame: L34FrameSummary): string {
const score = frame.maximumScore > 0
? ` · max ${(frame.maximumScore * 100).toFixed(0)}%`
: "";
return `${frame.truthIslandSequence}/32 · frame ${frame.frameIndex} · ${frame.groupId} · ${frame.predictionCount} boxes${score}`;
}
export function L34RightYoloxTruthIslandVisual({
result,
}: {
result: L34RightYoloxTruthIslandResult;
}) {
const initialSequence = result.frames.find(
({ groupId }) => groupId === "clip-stroller-person",
)?.truthIslandSequence ?? result.frames[0]?.truthIslandSequence ?? 1;
const [selectedSequence, setSelectedSequence] = useState(initialSequence);
const [frame, setFrame] = useState<L34VisualFrame | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<ViewerMode>("predictions");
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const controller = new AbortController();
setFrame(null);
setLoading(true);
setError(null);
void fetchL34RightYoloxTruthIslandFrame(
result.resultId,
selectedSequence,
{ signal: controller.signal },
).then((next) => {
if (!controller.signal.aborted) setFrame(next);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setError(
caught instanceof Error
? caught.message
: "Визуальный кадр L3.4 недоступен.",
);
}
}).finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [result.resultId, selectedSequence]);
const selectedIndex = result.frames.findIndex(
({ truthIslandSequence }) => truthIslandSequence === selectedSequence,
);
const navigate = (offset: -1 | 1) => {
if (selectedIndex < 0) return;
const index = (
selectedIndex + offset + result.frames.length
) % result.frames.length;
setSelectedSequence(result.frames[index].truthIslandSequence);
};
const actions = (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton label="Предыдущий визуальный кадр L3.4" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующий визуальный кадр L3.4" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать визуальный кадр L3.4"
value={String(selectedSequence)}
options={result.frames.map((item) => ({
value: String(item.truthIslandSequence),
label: frameLabel(item),
}))}
variant="split"
menuWidth="anchor"
searchable
searchPlaceholder="Найти кадр или группу"
onChange={(value) => setSelectedSequence(Number(value))}
/>
</div>
);
const overlay = frame ? (
<div className="l3-visual-audit__overlay">
<div>
<span>RAVNOVES00 · sensor.camera.right</span>
<strong>{frame.sessionSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с · frame {frame.frameIndex}</strong>
<small>{frame.role === "temporal" ? "Temporal sequence" : "Independent anchor"} · image {frame.imageId}</small>
</div>
<div>
<span>Frozen YOLOX-S candidate</span>
<strong>{frame.predictions.length} boxes · labels скрыты</strong>
<small>image {frame.cameraSha256.slice(0, 12)} · predictions {frame.predictionRowsSha256.slice(0, 12)}</small>
</div>
<div className="l3-visual-audit__legend">
<span data-tone="prediction">Акцентный контур · frozen prediction</span>
<span>Источник переключается без нового backend-расчёта</span>
</div>
</div>
) : undefined;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="L3.4 visual candidate review"
mode={mode}
modes={[
{ value: "predictions", label: "BOXES" },
{ value: "source", label: "SOURCE" },
]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={actions}
overlay={overlay}
>
{loading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем hash-bound кадр и frozen boxes</span>
</div>
) : error || !frame ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{error ?? "Визуальный кадр L3.4 недоступен."}</span>
</div>
) : (
<L34RightYoloxTruthIslandScene
frame={frame}
predictionsVisible={mode === "predictions"}
/>
)}
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -9,6 +9,13 @@ import type {
export type L3VisualMode = "3d" | "bev";
export interface L3VisualAssociationRay {
originXyzM: readonly [number, number, number];
directionXyz: readonly [number, number, number];
anchorXyzM: readonly [number, number, number] | null;
temporalHeld?: boolean;
}
function tokenColor(
host: HTMLElement,
token: string,
@@ -66,6 +73,90 @@ function boxSegments(box: L3VisualBox): Float32Array {
return positions;
}
function scenePoint(value: readonly [number, number, number]): THREE.Vector3 {
return new THREE.Vector3(value[0], value[2], -value[1]);
}
function evidenceBounds(
boxes: readonly L3VisualBox[],
rays: readonly L3VisualAssociationRay[],
): THREE.Box3 | null {
const bounds = new THREE.Box3();
boxes.forEach((box) => {
const segments = boxSegments(box);
for (let index = 0; index < segments.length; index += 3) {
bounds.expandByPoint(new THREE.Vector3(
segments[index],
segments[index + 1],
segments[index + 2],
));
}
});
rays.forEach((ray) => {
bounds.expandByPoint(scenePoint(ray.originXyzM));
if (ray.anchorXyzM) bounds.expandByPoint(scenePoint(ray.anchorXyzM));
});
return bounds.isEmpty() ? null : bounds;
}
function addAssociationRays(
scene: THREE.Scene,
rays: readonly L3VisualAssociationRay[],
rangedColor: THREE.Color,
bearingColor: THREE.Color,
): { lines: THREE.Line[]; anchors: THREE.Mesh[] } {
const lines: THREE.Line[] = [];
const anchors: THREE.Mesh[] = [];
for (const ray of rays) {
const origin = scenePoint(ray.originXyzM);
const endpoint = ray.anchorXyzM
? scenePoint(ray.anchorXyzM)
: scenePoint([
ray.originXyzM[0] + ray.directionXyz[0] * 25,
ray.originXyzM[1] + ray.directionXyz[1] * 25,
ray.originXyzM[2] + ray.directionXyz[2] * 25,
]);
const geometry = new THREE.BufferGeometry().setFromPoints([origin, endpoint]);
const material = ray.anchorXyzM
? ray.temporalHeld
? new THREE.LineDashedMaterial({
color: rangedColor,
dashSize: 0.55,
gapSize: 0.34,
transparent: true,
opacity: 0.88,
depthWrite: false,
})
: new THREE.LineBasicMaterial({
color: rangedColor,
transparent: true,
opacity: 0.96,
depthWrite: false,
})
: new THREE.LineDashedMaterial({
color: bearingColor,
dashSize: 0.75,
gapSize: 0.5,
transparent: true,
opacity: 0.56,
depthWrite: false,
});
const line = new THREE.Line(geometry, material);
if (!ray.anchorXyzM || ray.temporalHeld) line.computeLineDistances();
scene.add(line);
lines.push(line);
if (ray.anchorXyzM) {
const anchorGeometry = new THREE.SphereGeometry(0.28, 14, 10);
const anchorMaterial = new THREE.MeshBasicMaterial({ color: rangedColor });
const anchor = new THREE.Mesh(anchorGeometry, anchorMaterial);
anchor.position.copy(endpoint);
scene.add(anchor);
anchors.push(anchor);
}
}
return { lines, anchors };
}
function addBoxes(
scene: THREE.Scene,
boxes: readonly L3VisualBox[],
@@ -78,14 +169,26 @@ function addBoxes(
"position",
new THREE.BufferAttribute(boxSegments(box), 3),
);
const material = new THREE.LineBasicMaterial({
color: colors[box.status],
transparent: true,
opacity,
depthTest: true,
depthWrite: false,
});
const held = box.temporalStatus?.startsWith("e23-held") ?? false;
const material = held
? new THREE.LineDashedMaterial({
color: colors[box.status],
dashSize: 0.48,
gapSize: 0.3,
transparent: true,
opacity: Math.min(opacity, 0.62),
depthTest: true,
depthWrite: false,
})
: new THREE.LineBasicMaterial({
color: colors[box.status],
transparent: true,
opacity,
depthTest: true,
depthWrite: false,
});
const lines = new THREE.LineSegments(geometry, material);
if (held) lines.computeLineDistances();
scene.add(lines);
return lines;
});
@@ -96,6 +199,8 @@ export function L3PointPillarsScene({
mode,
bevCenterX = 30,
bevHalfExtent = 42,
associationRays = [],
fitToEvidence = false,
}: {
frame: Pick<
L3VisualFrame,
@@ -104,6 +209,8 @@ export function L3PointPillarsScene({
mode: L3VisualMode;
bevCenterX?: number;
bevHalfExtent?: number;
associationRays?: readonly L3VisualAssociationRay[];
fitToEvidence?: boolean;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const [renderError, setRenderError] = useState<string | null>(null);
@@ -183,6 +290,19 @@ export function L3PointPillarsScene({
colors,
0.72,
);
const associationGeometry = addAssociationRays(
scene,
associationRays,
colors["true-positive"],
colors.matched,
);
const fittedBounds = fitToEvidence
? evidenceBounds(frame.predictionBoxes, associationRays)
: null;
const fittedCenter = fittedBounds?.getCenter(new THREE.Vector3())
?? new THREE.Vector3(mode === "bev" ? bevCenterX : 30, 0, 0);
const fittedSize = fittedBounds?.getSize(new THREE.Vector3())
?? new THREE.Vector3(bevHalfExtent * 2, 8, bevHalfExtent * 2);
const grid = new THREE.GridHelper(
80,
40,
@@ -197,15 +317,32 @@ export function L3PointPillarsScene({
material.opacity = 0.22;
material.depthWrite = false;
});
if (fitToEvidence) {
grid.position.x = fittedCenter.x;
grid.position.z = fittedCenter.z;
}
scene.add(grid);
const perspective = new THREE.PerspectiveCamera(52, 1, 0.1, 500);
perspective.position.set(-12, 18, 36);
if (fitToEvidence) {
const span = Math.max(fittedSize.x, fittedSize.z, fittedSize.y * 2, 10);
perspective.position.copy(fittedCenter).add(new THREE.Vector3(
-span * 0.9,
span * 0.72,
span * 1.2,
));
} else {
perspective.position.set(-12, 18, 36);
}
const orthographic = new THREE.OrthographicCamera(-40, 40, 40, -40, 0.1, 500);
orthographic.position.set(bevCenterX + 5, 100, 0);
orthographic.position.set(
fitToEvidence ? fittedCenter.x : bevCenterX + 5,
100,
fitToEvidence ? fittedCenter.z : 0,
);
orthographic.up.set(1, 0, 0);
const camera = mode === "bev" ? orthographic : perspective;
camera.lookAt(mode === "bev" ? bevCenterX : 30, 0, 0);
camera.lookAt(fittedCenter);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = false;
@@ -213,7 +350,7 @@ export function L3PointPillarsScene({
controls.enablePan = true;
controls.enableZoom = true;
controls.screenSpacePanning = true;
controls.target.set(mode === "bev" ? bevCenterX : 30, 0, 0);
controls.target.copy(fittedCenter);
controls.update();
const render = () => renderer.render(scene, camera);
@@ -226,11 +363,18 @@ export function L3PointPillarsScene({
camera.aspect = width / height;
camera.updateProjectionMatrix();
} else {
const horizontal = bevHalfExtent;
const aspect = width / height;
const horizontal = fitToEvidence
? Math.max(
8,
fittedSize.z * 0.64,
fittedSize.x * aspect * 0.64,
)
: bevHalfExtent;
camera.left = -horizontal;
camera.right = horizontal;
camera.top = horizontal / (width / height);
camera.bottom = -horizontal / (width / height);
camera.top = horizontal / aspect;
camera.bottom = -horizontal / aspect;
camera.updateProjectionMatrix();
}
render();
@@ -249,12 +393,20 @@ export function L3PointPillarsScene({
lines.geometry.dispose();
(lines.material as THREE.Material).dispose();
});
associationGeometry.lines.forEach((line) => {
line.geometry.dispose();
(line.material as THREE.Material).dispose();
});
associationGeometry.anchors.forEach((anchor) => {
anchor.geometry.dispose();
(anchor.material as THREE.Material).dispose();
});
grid.geometry.dispose();
gridMaterials.forEach((material) => material.dispose());
renderer.dispose();
renderer.domElement.remove();
};
}, [bevCenterX, bevHalfExtent, frame, mode]);
}, [associationRays, bevCenterX, bevHalfExtent, fitToEvidence, frame, mode]);
return (
<div className="l3-visual-audit__scene" ref={hostRef}>
@@ -6,7 +6,6 @@ import {
type ComponentType,
} from "react";
import { Icon, StatusBadge } from "@nodedc/ui-react";
import {
LaboratoryEvidence,
LaboratorySelector,
@@ -14,7 +13,6 @@ import {
LaboratoryWorkTemplate,
type LaboratoryMethod,
type LaboratoryMethodComponent,
type LaboratoryOption,
} from "../../components/laboratory/LaboratoryPresentation";
import type { ObservationSessionSummary } from "../../core/observation/sessionArchive";
import { useObservationSessions } from "../../core/observation/useObservationSessions";
@@ -42,7 +40,6 @@ import type { WorkspaceRendererProps } from "../contracts";
import {
AdvancedLaboratoryResult,
advancedLaboratorySourceSession,
advancedLaboratoryWorkOptions,
isAdvancedLaboratoryWorkId,
} from "./AdvancedLaboratoryResult";
import {
@@ -50,15 +47,22 @@ import {
e30LaboratoryBrief, PUBLISHED_LABORATORY_BRIEF,
} from "./laboratoryArchiveBriefs";
import { useAdvancedLaboratoryCatalog } from "./useAdvancedLaboratoryCatalog";
import { buildLaboratoryProfiles, workOptionsForProfile } from "./laboratoryArchiveProfiles";
import type { LaboratoryProfileId, LaboratoryWorkId } from "./laboratoryArchiveProfiles";
import { useL34AnnotationCapability } from "./annotation/useL34AnnotationCapability";
import {
buildLaboratoryCatalog,
buildLaboratoryProfiles,
experimentOptionsForProfile,
workOptionsForExperiment,
} from "./laboratoryArchiveProfiles";
import type {
LaboratoryCatalogSeed,
LaboratoryExperimentId,
LaboratoryProfileId,
LaboratoryWorkId,
} from "./laboratoryArchiveProfiles";
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
SpatialView: ComponentType<WorkspaceRendererProps>;
};
function laboratoryWorkOrdinal(value: string): number {
const match = value.match(/\bE(\d+)\b/i);
return match ? Number(match[1]) : -1;
}
function digestFromContentId(value: string | null | undefined): string | null {
const digest = value?.split("-").at(-1) ?? "";
@@ -546,8 +550,15 @@ function PublishedLaboratoryResult({
}
export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const [profileId, setProfileId] = useState<LaboratoryProfileId>("sensor-fusion");
const [workId, setWorkId] = useState<LaboratoryWorkId>("e28-local-surface");
const [profileId, setProfileId] = useState<LaboratoryProfileId>(
"rig-right-yolox-lidar-range-v1",
);
const [experimentId, setExperimentId] = useState<LaboratoryExperimentId>(
"ravnoves00-perception-benchmark-v1",
);
const [workId, setWorkId] = useState<LaboratoryWorkId>(
"l34-right-yolox-truth-island-freeze",
);
const initialWorkSelectedRef = useRef(false);
const [e28Model, setE28Model] = useState<LidarLocalSurfaceModel | null>(null);
const [e29Result, setE29Result] = useState<E29EvidenceResult | null>(null);
@@ -569,12 +580,10 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
&& session.status === "ready"
&& session.replayable
&& session.modalities.includes("point-cloud")
)).sort((left, right) => {
const ordinalDelta = laboratoryWorkOrdinal(right.lab?.labId ?? "")
- laboratoryWorkOrdinal(left.lab?.labId ?? "");
if (ordinalDelta !== 0) return ordinalDelta;
return (right.startedAtUtc ?? "").localeCompare(left.startedAtUtc ?? "");
}),
)).sort((left, right) => (
Date.parse(right.lab?.runCreatedAtUtc ?? right.startedAtUtc)
- Date.parse(left.lab?.runCreatedAtUtc ?? left.startedAtUtc)
)),
[sessions.items],
);
const sourceSessions = useMemo(
@@ -593,7 +602,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
},
});
const advancedResults: AdvancedLaboratoryResults = advanced.results;
const annotationWorkspace = useL34AnnotationCapability({ selectedWorkId: workId, l34Result: advancedResults.l34, l34dResult: advancedResults.l34d, l34eResult: advancedResults.l34e, e46Result: advancedResults.e46, e46aResult: advancedResults.e46a, onActionChange: props.onLaboratoryAnnotationActionChange });
useEffect(() => {
const controller = new AbortController();
setEvidenceLoading(true);
@@ -636,12 +645,12 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
: "";
return sensorToken ? sensorToken.toLocaleUpperCase("ru-RU") : "Сенсорный риг";
}, [props.deviceLabel, publishedWorks]);
const sensorWorks = useMemo(() => {
const items: LaboratoryOption<LaboratoryWorkId>[] = [];
const knownWorks = useMemo(() => {
const items: LaboratoryCatalogSeed[] = [];
if (e28Model) {
items.push({
id: "e28-local-surface",
label: "LAB E28 · локальная поверхность L2.6",
createdAtUtc: e28Model.createdAtUtc ?? "",
});
}
if (
@@ -650,50 +659,42 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
) {
items.push({
id: "e29-camera-geometry",
label: "LAB E29 · camera-first + geometry",
createdAtUtc: e29Result.createdAtUtc ?? "",
});
}
if (e30Result && sourceSessions.has(e30Result.sourceSessionId)) {
items.push({
id: "e30-evidence-review",
label: "LAB E30 · evidence review A2",
createdAtUtc: e30Result.createdAtUtc ?? "",
});
}
items.push(
...advancedLaboratoryWorkOptions(advanced.index).filter(
({ id }) => id !== "l3-pointpillars-visual-audit",
),
);
return items.sort(
(left, right) => laboratoryWorkOrdinal(right.label) - laboratoryWorkOrdinal(left.label),
);
return items.filter(({ createdAtUtc }) => createdAtUtc.trim());
}, [
advanced.index,
e28Model,
e29Result,
e30Result,
sourceSessions,
]);
const publicBenchmarkWorks = useMemo(
() => advancedLaboratoryWorkOptions(advanced.index).filter(
({ id }) => id === "l3-pointpillars-visual-audit",
),
[advanced.index],
const catalog = useMemo(
() => buildLaboratoryCatalog({
rigLabel,
knownWorks,
advancedIndex: advanced.index,
publishedWorks,
}),
[advanced.index, knownWorks, publishedWorks, rigLabel],
);
const profiles = useMemo(
() => buildLaboratoryProfiles({
rigLabel,
sensorAvailable: sensorWorks.length > 0,
publicBenchmarkAvailable: publicBenchmarkWorks.length > 0,
publishedAvailable: publishedWorks.length > 0,
}),
[publicBenchmarkWorks.length, publishedWorks.length, rigLabel, sensorWorks.length],
() => buildLaboratoryProfiles(catalog),
[catalog],
);
const publishedWorkOptions = publishedWorks.map((session) => ({
id: `session:${session.id}` as const,
label: `${session.lab?.labId ?? "LAB"} · ${laboratorySessionTitle(session)}`,
}));
const workOptions = workOptionsForProfile(
profileId, sensorWorks, publicBenchmarkWorks, publishedWorkOptions,
const experimentOptions = useMemo(
() => experimentOptionsForProfile(profileId, catalog),
[catalog, profileId],
);
const workOptions = useMemo(
() => workOptionsForExperiment(profileId, experimentId, catalog),
[catalog, experimentId, profileId],
);
const selectedSessionId = workId.startsWith("session:")
? workId.slice("session:".length)
@@ -719,25 +720,11 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const firstProfile = profiles[0];
if (!firstProfile) return;
setProfileId(firstProfile.id);
if (firstProfile.id === "sensor-fusion") {
const firstWork = sensorWorks[0];
if (firstWork) {
setWorkId(firstWork.id);
initialWorkSelectedRef.current = true;
}
} else if (firstProfile.id === "public-benchmarks") {
const firstWork = publicBenchmarkWorks[0];
if (firstWork) {
setWorkId(firstWork.id);
initialWorkSelectedRef.current = true;
}
} else {
const first = publishedWorks[0];
if (first) {
setWorkId(`session:${first.id}`);
initialWorkSelectedRef.current = true;
}
}
return;
}
if (!experimentOptions.some((experiment) => experiment.id === experimentId)) {
const firstExperiment = experimentOptions[0];
if (firstExperiment) setExperimentId(firstExperiment.id);
return;
}
if (!initialWorkSelectedRef.current) {
@@ -754,11 +741,10 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
}, [
evidenceLoading,
advanced.indexLoading,
experimentId,
experimentOptions,
profileId,
profiles,
publishedWorks,
publicBenchmarkWorks,
sensorWorks,
sessions.state,
workId,
workOptions,
@@ -767,21 +753,18 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const selectProfile = (next: LaboratoryProfileId) => {
setProfileId(next);
initialWorkSelectedRef.current = true;
if (next === "sensor-fusion") {
const first = sensorWorks[0];
if (first) setWorkId(first.id);
return;
}
if (next === "public-benchmarks") {
const first = publicBenchmarkWorks[0];
if (first) setWorkId(first.id);
return;
}
const first = publishedWorks[0];
if (!first) return;
const nextWork = `session:${first.id}` as const;
setWorkId(nextWork);
void sessions.replay(first.id);
const firstExperiment = experimentOptionsForProfile(next, catalog)[0];
if (!firstExperiment) return;
setExperimentId(firstExperiment.id);
const firstWork = workOptionsForExperiment(next, firstExperiment.id, catalog)[0];
if (firstWork) selectWork(firstWork.id);
};
const selectExperiment = (next: LaboratoryExperimentId) => {
setExperimentId(next);
initialWorkSelectedRef.current = true;
const firstWork = workOptionsForExperiment(profileId, next, catalog)[0];
if (firstWork) selectWork(firstWork.id);
};
const selectWork = (next: LaboratoryWorkId) => {
@@ -851,23 +834,39 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
data-viewer-focused={viewerFocused ? "true" : undefined}
>
<LaboratorySelector
eyebrow="ПРОФИЛЬ ЛАБОРАТОРНОГО КОНТУРА"
eyebrow="PIPELINE-КОНТУР"
title={profiles.find((profile) => profile.id === profileId)?.label ?? rigLabel}
description="Профиль фиксирует объект исследования, сенсорные модули и вычислительный контур. Исходные данные остаются read-only; профиль объединяет серию сопоставимых лабораторных работ."
label="Профиль"
description="Датированный снимок конкретного perception pipeline. Контуры отсортированы по времени последнего зафиксированного результата; источник остаётся read-only."
label="Pipeline"
value={profileId}
options={profiles}
searchable
onChange={selectProfile}
/>
<LaboratorySelector
eyebrow="ЛАБОРАТОРНАЯ РАБОТА"
eyebrow="ЭКСПЕРИМЕНТ"
title={experimentOptions.find((experiment) => (
experiment.id === experimentId
))?.label ?? "Эксперимент не выбран"}
description="Эксперимент объединяет сопоставимые прогоны и модификации только внутри выбранного pipeline-контракта."
label="Эксперимент"
value={experimentId}
options={experimentOptions}
disabled={experimentOptions.length === 0}
searchable
onChange={selectExperiment}
/>
<LaboratorySelector
eyebrow="ПРОГОН / ВАРИАНТ"
title={workOptions.find((work) => work.id === workId)?.label ?? "Работа не выбрана"}
description="Выберите один зафиксированный эксперимент. Ниже откроются его задача и структурированный результат; viewer появляется только у опубликованного серверного доказательства."
label="Работа"
description="Строго хронологический список immutable-прогонов: дата запуска, LAB-id и конкретная модификация. Ниже открывается только опубликованное серверное доказательство."
label="Прогон"
value={workId}
options={workOptions}
disabled={workOptions.length === 0}
searchable
onChange={selectWork}
/>
@@ -994,6 +993,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
</div>
)}
</div>
{annotationWorkspace}
</div>
);
}
@@ -0,0 +1,488 @@
import {
useEffect,
useRef,
useState,
type PointerEvent as ReactPointerEvent,
} from "react";
import { Select } from "@nodedc/ui-react";
import {
L34_ANNOTATION_CLASS_OPTIONS,
L34_ANNOTATION_UNMAPPED_OPTION,
annotationOperationKey,
type L34AnnotationCategory,
type L34AnnotationObject,
type L34AnnotationSourceFrame,
} from "../../../core/laboratory/l34Annotation";
export interface L34AnnotationObjectDraft
extends Omit<L34AnnotationObject, "category" | "origin"> {
category: L34AnnotationCategory | null;
origin: string;
}
export interface L34AnnotationComparisonObject {
objectId: string;
category: string;
boxXyxy: readonly [number, number, number, number];
}
export type L34AnnotationLayerMode = "overlay" | "candidate" | "review" | "source";
type Point = Readonly<{ x: number; y: number }>;
type PlaneSize = Readonly<{ width: number; height: number }>;
type ResizeHandle = "nw" | "ne" | "sw" | "se";
type Interaction =
| Readonly<{
kind: "draw";
pointerId: number;
start: Point;
end: Point;
}>
| Readonly<{
kind: "move" | "resize";
pointerId: number;
objectId: string;
start: Point;
originalBox: readonly [number, number, number, number];
handle?: ResizeHandle;
}>;
function boundedPoint(
clientX: number,
clientY: number,
svg: SVGSVGElement,
frame: L34AnnotationSourceFrame,
): Point {
const bounds = svg.getBoundingClientRect();
const x = (clientX - bounds.left) * frame.cameraWidth / bounds.width;
const y = (clientY - bounds.top) * frame.cameraHeight / bounds.height;
return {
x: Math.max(0, Math.min(frame.cameraWidth, x)),
y: Math.max(0, Math.min(frame.cameraHeight, y)),
};
}
function movedBox(
original: readonly [number, number, number, number],
start: Point,
end: Point,
frame: L34AnnotationSourceFrame,
): readonly [number, number, number, number] {
const width = original[2] - original[0];
const height = original[3] - original[1];
const left = Math.max(
0,
Math.min(frame.cameraWidth - width, original[0] + end.x - start.x),
);
const top = Math.max(
0,
Math.min(frame.cameraHeight - height, original[1] + end.y - start.y),
);
return [left, top, left + width, top + height];
}
function resizedBox(
original: readonly [number, number, number, number],
handle: ResizeHandle,
point: Point,
): readonly [number, number, number, number] {
let [left, top, right, bottom] = original;
if (handle.includes("n")) top = Math.min(point.y, bottom - 4);
if (handle.includes("s")) bottom = Math.max(point.y, top + 4);
if (handle.includes("w")) left = Math.min(point.x, right - 4);
if (handle.includes("e")) right = Math.max(point.x, left + 4);
return [left, top, right, bottom];
}
function boxesNearlyEqual(
left: readonly [number, number, number, number],
right: readonly [number, number, number, number],
): boolean {
return left.every((value, index) => Math.abs(value - right[index]) < 0.5);
}
function customLabelValue(label: string): string {
return `custom:${encodeURIComponent(label)}`;
}
function objectLabel(object: L34AnnotationObjectDraft): string {
if (object.category === null) return "Выберите класс";
if (object.category === "unmapped") {
return object.proposedLabel ?? L34_ANNOTATION_UNMAPPED_OPTION.label;
}
return L34_ANNOTATION_CLASS_OPTIONS.find(
({ value }) => value === object.category,
)?.label ?? object.category;
}
function boxFromPoints(
start: Point,
end: Point,
): readonly [number, number, number, number] {
return [
Math.min(start.x, end.x),
Math.min(start.y, end.y),
Math.max(start.x, end.x),
Math.max(start.y, end.y),
];
}
export function L34AnnotationCanvas({
frame,
objects,
drawingEnabled,
selectedObjectId,
unmappedLabels,
allowUnmapped = true,
comparisonObjects = [],
layerMode = "review",
newObjectOrigin = "manual",
onObjectsChange,
onSelectedObjectIdChange,
onRequestUnmappedLabel,
}: {
frame: L34AnnotationSourceFrame;
objects: readonly L34AnnotationObjectDraft[];
drawingEnabled: boolean;
selectedObjectId: string | null;
unmappedLabels: readonly string[];
allowUnmapped?: boolean;
comparisonObjects?: readonly L34AnnotationComparisonObject[];
layerMode?: L34AnnotationLayerMode;
newObjectOrigin?: string;
onObjectsChange: (objects: readonly L34AnnotationObjectDraft[]) => void;
onSelectedObjectIdChange: (objectId: string | null) => void;
onRequestUnmappedLabel: (objectId: string) => void;
}) {
const stageRef = useRef<HTMLDivElement | null>(null);
const [planeSize, setPlaneSize] = useState<PlaneSize>({ width: 1, height: 1 });
const [interaction, setInteraction] = useState<Interaction | null>(null);
useEffect(() => {
const host = stageRef.current;
if (!host) return;
const measure = () => {
const width = Math.max(host.clientWidth, 1);
const height = Math.max(host.clientHeight, 1);
const scale = Math.min(
width / frame.cameraWidth,
height / frame.cameraHeight,
);
setPlaneSize({
width: Math.max(frame.cameraWidth * scale, 1),
height: Math.max(frame.cameraHeight * scale, 1),
});
};
const observer = new ResizeObserver(measure);
observer.observe(host);
measure();
return () => observer.disconnect();
}, [frame.cameraHeight, frame.cameraWidth]);
useEffect(() => setInteraction(null), [frame.truthIslandSequence]);
const updateObject = (
objectId: string,
patch: Partial<L34AnnotationObjectDraft>,
) => {
let changed = false;
const next = objects.map((object) => {
if (object.objectId !== objectId) return object;
const keys = Object.keys(patch) as Array<keyof L34AnnotationObjectDraft>;
const scalarChanged = keys.some((key) => (
key !== "boxXyxy" && object[key] !== patch[key]
));
const boxChanged = patch.boxXyxy !== undefined
&& !boxesNearlyEqual(object.boxXyxy, patch.boxXyxy);
if (!scalarChanged && !boxChanged) return object;
changed = true;
return { ...object, ...patch };
});
if (changed) onObjectsChange(next);
};
const finishInteraction = (event: ReactPointerEvent<SVGSVGElement>) => {
if (!interaction || interaction.pointerId !== event.pointerId) return;
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
const end = boundedPoint(
event.clientX,
event.clientY,
event.currentTarget,
frame,
);
if (interaction.kind !== "draw") {
const box = interaction.kind === "move"
? movedBox(interaction.originalBox, interaction.start, end, frame)
: resizedBox(
interaction.originalBox,
interaction.handle ?? "se",
end,
);
updateObject(interaction.objectId, { boxXyxy: box });
setInteraction(null);
return;
}
const box = boxFromPoints(interaction.start, end);
setInteraction(null);
if (box[2] - box[0] < 4 || box[3] - box[1] < 4) return;
const objectId = annotationOperationKey("box").replace(":", "-");
onObjectsChange([
...objects,
{
objectId,
category: null,
proposedLabel: null,
origin: newObjectOrigin,
boxXyxy: box,
occluded: false,
truncated: false,
},
]);
onSelectedObjectIdChange(objectId);
};
const humanVisible = layerMode === "overlay" || layerMode === "review";
const candidateVisible = layerMode === "overlay" || layerMode === "candidate";
const humanLabelsVisible = layerMode === "review";
const candidateLabelsVisible = layerMode === "candidate";
const editable = humanVisible;
const effectiveDrawingEnabled = drawingEnabled && editable;
const draftBox = interaction?.kind === "draw"
? boxFromPoints(interaction.start, interaction.end)
: null;
const selectedObject = objects.find(
({ objectId }) => objectId === selectedObjectId,
) ?? null;
return (
<div className="l34-annotation-canvas" ref={stageRef}>
<div
className="l34-annotation-canvas__plane"
style={{ width: planeSize.width, height: planeSize.height }}
>
<img
src={frame.cameraUrl}
alt={`Исходный кадр ${frame.frameIndex} правой камеры без model overlay`}
draggable={false}
/>
<svg
viewBox={`0 0 ${frame.cameraWidth} ${frame.cameraHeight}`}
role="img"
aria-label={`Разметка кадра ${frame.frameIndex}: ${objects.length} рамок`}
data-drawing={effectiveDrawingEnabled ? "true" : undefined}
onPointerDown={(event) => {
if (!effectiveDrawingEnabled || event.button !== 0) return;
const point = boundedPoint(
event.clientX,
event.clientY,
event.currentTarget,
frame,
);
event.currentTarget.setPointerCapture(event.pointerId);
setInteraction({
kind: "draw",
pointerId: event.pointerId,
start: point,
end: point,
});
onSelectedObjectIdChange(null);
}}
onPointerMove={(event) => {
if (!interaction || interaction.pointerId !== event.pointerId) return;
const point = boundedPoint(
event.clientX,
event.clientY,
event.currentTarget,
frame,
);
if (interaction.kind === "draw") {
setInteraction({ ...interaction, end: point });
return;
}
const box = interaction.kind === "move"
? movedBox(interaction.originalBox, interaction.start, point, frame)
: resizedBox(
interaction.originalBox,
interaction.handle ?? "se",
point,
);
updateObject(interaction.objectId, { boxXyxy: box });
}}
onPointerUp={finishInteraction}
onPointerCancel={() => setInteraction(null)}
>
{candidateVisible ? comparisonObjects.map((object) => {
const [left, top, right, bottom] = object.boxXyxy;
return (
<rect
className="l34-annotation-canvas__comparison"
key={object.objectId}
x={left}
y={top}
width={right - left}
height={bottom - top}
/>
);
}) : null}
{humanVisible ? objects.map((object) => {
const [left, top, right, bottom] = object.boxXyxy;
return (
<rect
key={object.objectId}
x={left}
y={top}
width={right - left}
height={bottom - top}
data-selected={object.objectId === selectedObjectId ? "true" : undefined}
onPointerDown={(event) => {
if (effectiveDrawingEnabled || event.button !== 0) return;
event.stopPropagation();
const svg = event.currentTarget.ownerSVGElement;
if (!svg) return;
const point = boundedPoint(
event.clientX,
event.clientY,
svg,
frame,
);
svg.setPointerCapture(event.pointerId);
onSelectedObjectIdChange(object.objectId);
setInteraction({
kind: "move",
pointerId: event.pointerId,
objectId: object.objectId,
start: point,
originalBox: object.boxXyxy,
});
}}
/>
);
}) : null}
{draftBox ? (
<rect
className="l34-annotation-canvas__draft"
x={draftBox[0]}
y={draftBox[1]}
width={draftBox[2] - draftBox[0]}
height={draftBox[3] - draftBox[1]}
/>
) : null}
{selectedObject && editable && !effectiveDrawingEnabled ? ([
["nw", selectedObject.boxXyxy[0], selectedObject.boxXyxy[1]],
["ne", selectedObject.boxXyxy[2], selectedObject.boxXyxy[1]],
["sw", selectedObject.boxXyxy[0], selectedObject.boxXyxy[3]],
["se", selectedObject.boxXyxy[2], selectedObject.boxXyxy[3]],
] as const).map(([handle, x, y]) => (
<circle
className="l34-annotation-canvas__resize-handle"
data-handle={handle}
key={`${selectedObject.objectId}-${handle}`}
cx={x}
cy={y}
r={5}
onPointerDown={(event) => {
if (event.button !== 0) return;
event.stopPropagation();
const svg = event.currentTarget.ownerSVGElement;
if (!svg) return;
svg.setPointerCapture(event.pointerId);
setInteraction({
kind: "resize",
pointerId: event.pointerId,
objectId: selectedObject.objectId,
start: boundedPoint(
event.clientX,
event.clientY,
svg,
frame,
),
originalBox: selectedObject.boxXyxy,
handle,
});
}}
/>
)) : null}
</svg>
{candidateLabelsVisible ? comparisonObjects.map((object) => {
const [left, top] = object.boxXyxy;
return (
<div
className="l34-annotation-canvas__comparison-label"
key={`${object.objectId}-comparison-label`}
style={{
left: `${left / frame.cameraWidth * 100}%`,
top: `${top / frame.cameraHeight * 100}%`,
transform: top < 42 ? "none" : "translateY(-100%)",
}}
>
Candidate · {object.category}
</div>
);
}) : null}
{humanLabelsVisible ? objects.map((object) => {
const [left, top] = object.boxXyxy;
const below = top < 42;
const selected = object.objectId === selectedObjectId;
return (
<div
className="l34-annotation-canvas__label"
data-unclassified={object.category === null ? "true" : undefined}
data-unmapped={object.category === "unmapped" ? "true" : undefined}
data-selected={selected ? "true" : undefined}
key={`${object.objectId}-label`}
style={{
left: `${left / frame.cameraWidth * 100}%`,
top: `${top / frame.cameraHeight * 100}%`,
transform: below ? "none" : "translateY(-100%)",
}}
onPointerDown={() => onSelectedObjectIdChange(object.objectId)}
>
{selected ? <Select
label={`Класс рамки ${object.objectId}`}
value={object.category === "unmapped" && object.proposedLabel
? customLabelValue(object.proposedLabel)
: object.category ?? ""}
options={[
{ value: "", label: "Выберите класс", disabled: true },
...L34_ANNOTATION_CLASS_OPTIONS,
...(allowUnmapped ? [
...Array.from(new Set([
...unmappedLabels,
...(object.proposedLabel ? [object.proposedLabel] : []),
])).map((label) => ({
value: customLabelValue(label),
label: `Другой · ${label}`,
})),
L34_ANNOTATION_UNMAPPED_OPTION,
] : []),
]}
menuWidth={210}
minMenuWidth={210}
placement={below ? "bottom-start" : "top-start"}
onChange={(value) => {
if (value.startsWith("custom:")) {
updateObject(object.objectId, {
category: "unmapped",
proposedLabel: decodeURIComponent(value.slice(7)),
});
return;
}
if (value === L34_ANNOTATION_UNMAPPED_OPTION.value) {
onRequestUnmappedLabel(object.objectId);
return;
}
updateObject(object.objectId, {
category: value as L34AnnotationCategory,
proposedLabel: null,
});
}}
/> : <span>{objectLabel(object)}</span>}
</div>
);
}) : null}
</div>
</div>
);
}
@@ -0,0 +1,635 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { createPortal } from "react-dom";
import {
Button,
Checker,
Icon,
IconButton,
SegmentedControl,
Select,
StatusBadge,
TextField,
ToastStack,
Window,
WindowFooterActions,
type ToastItem,
} from "@nodedc/ui-react";
import {
type L34AnnotationSourceFrame,
} from "../../../core/laboratory/l34Annotation";
import type { L34EResult } from "../../../core/laboratory/l34eSelfReviewDiagnostic";
import {
createL34FSession,
fetchL34FCase,
fetchL34FSession,
fetchL34FSessions,
freezeL34FSession,
saveL34FSession,
type L34FCase,
type L34FFrame,
type L34FObject,
type L34FSession,
type L34FSessionSummary,
} from "../../../core/laboratory/l34fAdjudication";
import {
L34AnnotationCanvas,
type L34AnnotationLayerMode,
type L34AnnotationObjectDraft,
} from "./L34AnnotationCanvas";
interface FrameDraft extends Omit<L34FFrame, "objects"> {
objects: readonly L34AnnotationObjectDraft[];
}
const MODES: ReadonlyArray<{ value: L34AnnotationLayerMode; label: string }> = [
{ value: "overlay", label: "ВМЕСТЕ" },
{ value: "candidate", label: "CANDIDATE" },
{ value: "review", label: "HUMAN" },
{ value: "source", label: "SOURCE" },
];
function errorMessage(error: unknown): string {
return error instanceof Error && error.message.trim()
? error.message
: "Операция L3.4F не выполнена.";
}
function draftsFromSession(session: L34FSession): Map<number, FrameDraft> {
return new Map(session.frames.map((frame) => [
frame.truthIslandSequence,
{
...frame,
objects: frame.objects.map((object) => ({ ...object })),
},
]));
}
function sessionLabel(session: L34FSessionSummary): string {
return `${session.title} · ${session.progress.reviewedFrameCount}/32`;
}
export function L34FAdjudicationWorkspace({
result,
onClose,
}: {
result: L34EResult;
onClose: () => void;
}) {
const [sessions, setSessions] = useState<readonly L34FSessionSummary[]>([]);
const [session, setSession] = useState<L34FSession | null>(null);
const [drafts, setDrafts] = useState<ReadonlyMap<number, FrameDraft>>(new Map());
const [selectedSequence, setSelectedSequence] = useState(result.caseOrder[0] ?? 1);
const [diagnosticCase, setDiagnosticCase] = useState<L34FCase | null>(null);
const [selectedObjectId, setSelectedObjectId] = useState<string | null>(null);
const [mode, setMode] = useState<L34AnnotationLayerMode>("overlay");
const [drawingEnabled, setDrawingEnabled] = useState(false);
const [loading, setLoading] = useState(true);
const [frameLoading, setFrameLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [dirty, setDirty] = useState(false);
const [error, setError] = useState<string | null>(null);
const [titleDraft, setTitleDraft] = useState("");
const [saveOpen, setSaveOpen] = useState(false);
const [discardOpen, setDiscardOpen] = useState(false);
const [customLabelOpen, setCustomLabelOpen] = useState(false);
const [customLabelObjectId, setCustomLabelObjectId] = useState<string | null>(null);
const [customLabelDraft, setCustomLabelDraft] = useState("");
const [toasts, setToasts] = useState<ToastItem[]>([]);
const previousFocusRef = useRef<HTMLElement | null>(null);
const notify = useCallback((item: Omit<ToastItem, "id">) => {
setToasts((current) => [
...current.filter(({ tone }) => tone !== "loading"),
{ ...item, id: `${Date.now()}-${Math.random()}` },
]);
}, []);
useEffect(() => {
previousFocusRef.current = document.activeElement instanceof HTMLElement
? document.activeElement
: null;
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = previousOverflow;
previousFocusRef.current?.focus();
};
}, []);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
void fetchL34FSessions(result.resultId, { signal: controller.signal })
.then(async (items) => {
if (controller.signal.aborted) return;
setSessions(items);
const latest = items.at(-1);
if (!latest) return;
const loaded = await fetchL34FSession(result.resultId, latest.sessionId, {
signal: controller.signal,
});
if (controller.signal.aborted) return;
setSession(loaded);
setTitleDraft(loaded.title);
setDrafts(draftsFromSession(loaded));
})
.catch((caught: unknown) => {
if (!controller.signal.aborted) setError(errorMessage(caught));
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [result.resultId]);
useEffect(() => {
const controller = new AbortController();
setFrameLoading(true);
setDiagnosticCase(null);
void fetchL34FCase(result.resultId, selectedSequence, {
signal: controller.signal,
}).then((value) => {
if (!controller.signal.aborted) setDiagnosticCase(value);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) setError(errorMessage(caught));
}).finally(() => {
if (!controller.signal.aborted) setFrameLoading(false);
});
return () => controller.abort();
}, [result.resultId, selectedSequence]);
useEffect(() => setSelectedObjectId(null), [selectedSequence]);
const currentDraft = drafts.get(selectedSequence) ?? null;
const selectedObject = currentDraft?.objects.find(
({ objectId }) => objectId === selectedObjectId,
) ?? null;
const reviewedCount = [...drafts.values()].filter(({ reviewed }) => reviewed).length;
const objectCount = [...drafts.values()].reduce(
(total, frame) => total + frame.objects.length,
0,
);
const unresolvedCount = [...drafts.values()].reduce(
(total, frame) => total + frame.objects.filter((object) => (
object.category === null
|| (object.category === "unmapped" && !object.proposedLabel?.trim())
)).length,
0,
);
const customLabels = useMemo(() => Array.from(new Set(
[...drafts.values()].flatMap(({ objects }) => objects
.filter((object) => object.category === "unmapped" && object.proposedLabel)
.map((object) => object.proposedLabel!)),
)).sort((left, right) => left.localeCompare(right, "ru-RU")), [drafts]);
const sourceFrame = useMemo<L34AnnotationSourceFrame | null>(() => (
diagnosticCase ? {
resultId: diagnosticCase.diagnosticResultId,
truthIslandSequence: diagnosticCase.truthIslandSequence,
imageId: diagnosticCase.imageId,
frameIndex: diagnosticCase.frameIndex,
groupId: diagnosticCase.groupId,
role: "temporal",
sessionSeconds: diagnosticCase.sessionSeconds,
cameraWidth: diagnosticCase.cameraWidth,
cameraHeight: diagnosticCase.cameraHeight,
cameraSha256: diagnosticCase.sourceImageSha256,
cameraUrl: diagnosticCase.cameraUrl,
modelMaterialIncluded: false,
} : null
), [diagnosticCase]);
const setCurrentObjects = (objects: readonly L34AnnotationObjectDraft[]) => {
if (!currentDraft) return;
setDrafts((current) => {
const next = new Map(current);
next.set(selectedSequence, { ...currentDraft, objects, reviewed: false });
return next;
});
setDirty(true);
};
const createSession = async () => {
if (dirty) return;
setBusy(true);
setError(null);
try {
const created = await createL34FSession(result.resultId);
setSession(created);
setSessions((current) => [
...current.filter(({ sessionId }) => sessionId !== created.sessionId),
created,
]);
setDrafts(draftsFromSession(created));
setTitleDraft(created.title);
setDirty(false);
notify({
tone: "success",
title: "Сессия L3.4F создана",
description: "Human-слой скопирован из self-review; candidate остаётся read-only.",
});
} catch (caught) {
setError(errorMessage(caught));
} finally {
setBusy(false);
}
};
const selectSession = async (sessionId: string) => {
if (dirty || sessionId === session?.sessionId) return;
setBusy(true);
try {
const loaded = await fetchL34FSession(result.resultId, sessionId);
setSession(loaded);
setDrafts(draftsFromSession(loaded));
setTitleDraft(loaded.title);
} catch (caught) {
setError(errorMessage(caught));
} finally {
setBusy(false);
}
};
const savableFrames = (): readonly L34FFrame[] | null => {
if (drafts.size !== 32) return null;
const frames: L34FFrame[] = [];
for (const frame of drafts.values()) {
if (frame.objects.some(({ category }) => category === null)) return null;
frames.push({
...frame,
objects: frame.objects as readonly L34FObject[],
});
}
return frames.sort((left, right) => left.truthIslandSequence - right.truthIslandSequence);
};
const save = async () => {
if (!session || !titleDraft.trim()) return;
const frames = savableFrames();
if (!frames) return;
setBusy(true);
setError(null);
try {
const saved = await saveL34FSession(session, titleDraft.trim(), frames);
setSession(saved);
setDrafts(draftsFromSession(saved));
setSessions((current) => [
...current.filter(({ sessionId }) => sessionId !== saved.sessionId),
saved,
]);
setDirty(false);
setSaveOpen(false);
notify({
tone: "success",
title: "Ревизия L3.4F сохранена",
description: `${saved.progress.reviewedFrameCount}/32 кадров · revision ${saved.revision}.`,
});
} catch (caught) {
setError(errorMessage(caught));
} finally {
setBusy(false);
}
};
const freeze = async () => {
if (!session || dirty || !session.progress.complete) return;
setBusy(true);
setError(null);
try {
const frozen = await freezeL34FSession(session);
notify({
tone: "success",
title: "L3.4F зафиксирован",
description: `${frozen.metrics.changedFrameCount} изменённых кадров · не independent truth.`,
});
} catch (caught) {
setError(errorMessage(caught));
} finally {
setBusy(false);
}
};
const updateSelectedObject = (patch: Partial<L34AnnotationObjectDraft>) => {
if (!currentDraft || !selectedObject) return;
setCurrentObjects(currentDraft.objects.map((object) => (
object.objectId === selectedObject.objectId ? { ...object, ...patch } : object
)));
};
const requestCustomLabel = (objectId: string) => {
const object = currentDraft?.objects.find((item) => item.objectId === objectId);
if (!object) return;
setSelectedObjectId(objectId);
setCustomLabelObjectId(objectId);
setCustomLabelDraft(object.proposedLabel ?? "");
setCustomLabelOpen(true);
};
const confirmCustomLabel = () => {
const label = customLabelDraft.trim();
if (!currentDraft || !customLabelObjectId || !label) return;
setCurrentObjects(currentDraft.objects.map((object) => (
object.objectId === customLabelObjectId
? { ...object, category: "unmapped", proposedLabel: label }
: object
)));
setCustomLabelOpen(false);
setCustomLabelObjectId(null);
};
const orderedCases = result.caseOrder.map((sequence) => result.cases.find(
(item) => item.truthIslandSequence === sequence,
)).filter((item): item is L34EResult["cases"][number] => Boolean(item));
const selectedIndex = result.caseOrder.indexOf(selectedSequence);
const navigate = (offset: -1 | 1) => {
const index = (selectedIndex + offset + result.caseOrder.length) % result.caseOrder.length;
setSelectedSequence(result.caseOrder[index]);
};
const requestClose = () => dirty ? setDiscardOpen(true) : onClose();
useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.key !== "Escape" || saveOpen || customLabelOpen || discardOpen) return;
event.preventDefault();
requestClose();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
});
const workspace = (
<section
className="l34-annotation-workspace"
role="dialog"
aria-modal="true"
aria-label="Разбор конфликтов LAB L3.4F"
>
<header className="l34-annotation-workspace__toolbar">
<div className="l34-annotation-workspace__session-tools">
<Select
label="Сессия разбора конфликтов"
value={session?.sessionId ?? ""}
options={sessions.length ? sessions.map((item) => ({
value: item.sessionId,
label: sessionLabel(item),
})) : [{ value: "", label: "Сессия не создана", disabled: true }]}
disabled={busy}
searchable
menuWidth={380}
minMenuWidth={300}
onChange={(value) => void selectSession(value)}
/>
<IconButton
label="Создать сессию L3.4F"
disabled={busy || dirty}
onClick={() => void createSession()}
>
<Icon name="plus" size={16} />
</IconButton>
<Button
size="compact"
variant={drawingEnabled ? "accent" : "secondary"}
icon={<Icon name="edit" size={16} />}
aria-pressed={drawingEnabled}
disabled={!session || busy || mode === "candidate" || mode === "source"}
onClick={() => setDrawingEnabled((value) => !value)}
>
Рамка
</Button>
<Button
size="compact"
variant="primary"
icon={<Icon name="save" size={16} />}
disabled={!session || busy}
onClick={() => setSaveOpen(true)}
>
Сохранить
</Button>
<Button
size="compact"
variant="secondary"
disabled={busy || dirty || !session?.progress.complete}
onClick={() => void freeze()}
>
Зафиксировать L3.4F
</Button>
</div>
<div className="l34-annotation-workspace__frame-tools">
<IconButton label="Предыдущий конфликт" onClick={() => navigate(-1)}>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton label="Следующий конфликт" onClick={() => navigate(1)}>
<Icon name="chevron-right" size={16} />
</IconButton>
<Select
label="Конфликтный кадр"
value={String(selectedSequence)}
options={orderedCases.map((item) => ({
value: String(item.truthIslandSequence),
label: `${item.truthIslandSequence}/32 · frame ${item.frameIndex} · severity ${item.diagnosticSummary.severityScore}`,
}))}
searchable
menuWidth={380}
minMenuWidth={300}
onChange={(value) => setSelectedSequence(Number(value))}
/>
<StatusBadge tone={dirty ? "warning" : reviewedCount === 32 ? "success" : "neutral"}>
{dirty ? "Не сохранено" : `${reviewedCount}/32 · не truth`}
</StatusBadge>
<IconButton label="Закрыть L3.4F" onClick={requestClose}>
<Icon name="close" size={16} />
</IconButton>
</div>
</header>
<div className="l34-annotation-workspace__stage">
{loading || frameLoading ? (
<div className="l34-annotation-workspace__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<strong>Открываем candidate и human слоя без model scores</strong>
</div>
) : error || !sourceFrame || !diagnosticCase ? (
<div className="l34-annotation-workspace__state" role="alert">
<Icon name="alert" size={18} />
<strong>{error ?? "Кадр L3.4F недоступен."}</strong>
</div>
) : (
<L34AnnotationCanvas
frame={sourceFrame}
objects={currentDraft?.objects ?? []}
comparisonObjects={diagnosticCase.candidateObjects.map((object) => ({
objectId: object.candidateId,
category: object.category,
boxXyxy: object.boxXyxy,
}))}
layerMode={mode}
newObjectOrigin="adjudicated_manual"
drawingEnabled={drawingEnabled && Boolean(session)}
selectedObjectId={selectedObjectId}
unmappedLabels={customLabels}
onObjectsChange={setCurrentObjects}
onSelectedObjectIdChange={setSelectedObjectId}
onRequestUnmappedLabel={requestCustomLabel}
/>
)}
</div>
<footer className="l34-annotation-workspace__inspector">
<div className="l34-annotation-workspace__source-state">
<span>RAVNOVES00 · sensor.camera.right · L3.4F</span>
<strong>
{diagnosticCase
? `frame ${diagnosticCase.frameIndex} · severity ${diagnosticCase.severityScore}`
: "Источник проверяется"}
</strong>
<small>Candidate visible · scores скрыты · engineering reference · не independent truth</small>
</div>
<SegmentedControl
value={mode}
items={[...MODES]}
label="Слои L3.4F"
onChange={setMode}
/>
<div className="l34f-adjudication-workspace__legend">
<span>Candidate read-only</span>
<span>Human editable</span>
</div>
{session && currentDraft ? (
<Checker
checked={currentDraft.reviewed}
label={currentDraft.objects.length ? "Конфликт разобран" : "Кадр подтверждён пустым"}
onChange={(reviewed) => {
setDrafts((current) => {
const next = new Map(current);
next.set(selectedSequence, { ...currentDraft, reviewed });
return next;
});
setDirty(true);
}}
/>
) : null}
{selectedObject && (mode === "overlay" || mode === "review") ? (
<div className="l34-annotation-workspace__object-tools">
<Checker
checked={selectedObject.occluded}
label="Перекрыт"
onChange={(occluded) => updateSelectedObject({ occluded })}
/>
<Checker
checked={selectedObject.truncated}
label="Обрезан"
onChange={(truncated) => updateSelectedObject({ truncated })}
/>
<IconButton
label="Удалить human-рамку"
onClick={() => {
if (!currentDraft) return;
setCurrentObjects(currentDraft.objects.filter(
({ objectId }) => objectId !== selectedObject.objectId,
));
setSelectedObjectId(null);
}}
>
<Icon name="trash" size={16} />
</IconButton>
</div>
) : null}
</footer>
<Window
open={customLabelOpen}
title="Другой объект"
subtitle="Название останется предложением до отдельной нормализации taxonomy."
size="sm"
onClose={() => setCustomLabelOpen(false)}
footer={(
<WindowFooterActions>
<Button onClick={() => setCustomLabelOpen(false)}>Отмена</Button>
<Button variant="primary" disabled={!customLabelDraft.trim()} onClick={confirmCustomLabel}>
Добавить название
</Button>
</WindowFooterActions>
)}
>
<TextField
label="Название объекта"
value={customLabelDraft}
maxLength={80}
autoFocus
placeholder="Например, детская коляска"
onChange={(event) => setCustomLabelDraft(event.target.value)}
/>
</Window>
<Window
open={saveOpen}
title="Сохранить ревизию L3.4F"
subtitle="Сохранение не превращает candidate-visible adjudication в ground truth."
size="md"
closeOnBackdrop={!busy}
closeOnEscape={!busy}
onClose={() => !busy && setSaveOpen(false)}
footer={(
<WindowFooterActions>
<Button disabled={busy} onClick={() => setSaveOpen(false)}>Отмена</Button>
<Button
variant="primary"
disabled={busy || !titleDraft.trim() || unresolvedCount > 0}
onClick={() => void save()}
>
{busy ? "Сохраняем" : "Сохранить ревизию"}
</Button>
</WindowFooterActions>
)}
>
<div className="l34-annotation-save-form">
<TextField
label="Название сессии"
value={titleDraft}
maxLength={160}
autoFocus
onChange={(event) => setTitleDraft(event.target.value)}
/>
<div className="l34-annotation-save-form__summary">
<span>Разобрано кадров</span><strong>{reviewedCount} / 32</strong>
<span>Human-объектов</span><strong>{objectCount}</strong>
<span>Без класса</span><strong>{unresolvedCount}</strong>
</div>
<p>
После 32/32 сохраните ревизию и нажмите «Зафиксировать L3.4F». Следующий независимый gate reviewer-B без candidate overlay.
</p>
</div>
</Window>
<Window
open={discardOpen}
title="Закрыть без сохранения?"
subtitle="Несохранённые изменения human-слоя будут потеряны."
size="sm"
onClose={() => setDiscardOpen(false)}
footer={(
<WindowFooterActions>
<Button onClick={() => setDiscardOpen(false)}>Остаться</Button>
<Button variant="danger" onClick={onClose}>Закрыть без сохранения</Button>
</WindowFooterActions>
)}
>
Сохранённая серверная ревизия останется неизменной.
</Window>
<ToastStack
items={toasts}
onDismiss={(id) => setToasts((current) => current.filter((item) => item.id !== id))}
/>
</section>
);
return typeof document === "undefined" ? null : createPortal(workspace, document.body);
}
@@ -0,0 +1,82 @@
import { useCallback, useEffect, useState, type ReactNode } from "react";
import type { L34RightYoloxTruthIslandResult } from "../../../core/laboratory/l34RightYoloxTruthIsland";
import type { L34DResult } from "../../../core/laboratory/l34dCumulativePostprocessing";
import type { L34EResult } from "../../../core/laboratory/l34eSelfReviewDiagnostic";
import type { E46BlindReviewResult } from "../../../core/laboratory/e46BlindReview";
import type { E46AAiEngineeringPreannotationResult } from "../../../core/laboratory/e46aAiEngineeringPreannotation";
import type { LaboratoryAnnotationAction } from "../../contracts";
import { L34AnnotationWorkspace } from "./L34AnnotationWorkspace";
import { L34FAdjudicationWorkspace } from "./L34FAdjudicationWorkspace";
export function useL34AnnotationCapability({
selectedWorkId,
l34Result,
l34dResult,
l34eResult,
e46Result,
e46aResult,
onActionChange,
}: {
selectedWorkId: string;
l34Result: L34RightYoloxTruthIslandResult | null;
l34dResult: L34DResult | null;
l34eResult: L34EResult | null;
e46Result: E46BlindReviewResult | null;
e46aResult: E46AAiEngineeringPreannotationResult | null;
onActionChange: (action: LaboratoryAnnotationAction | null) => void;
}): ReactNode {
const [open, setOpen] = useState(false);
const openWorkspace = useCallback(() => setOpen(true), []);
const available = selectedWorkId === "l34-right-yolox-truth-island-freeze"
&& l34Result
? { resultId: l34Result.resultId, workflow: "assisted-candidate" as const }
: selectedWorkId === "e46-detector-truth-island" && e46Result
? { resultId: e46Result.resultId, workflow: "independent-blind" as const }
: selectedWorkId === "e46a-ai-engineering-preannotation" && e46aResult
? { resultId: e46aResult.resultId, workflow: "engineering-preannotation" as const }
: selectedWorkId === "l34d-cumulative-postprocessing-candidate"
&& l34dResult
? { resultId: l34dResult.resultId, workflow: "prediction-hidden" as const }
: selectedWorkId === "l34e-self-review-diagnostic" && l34eResult
? { resultId: l34eResult.resultId, workflow: "adjudication" as const }
: null;
useEffect(() => {
if (!available) {
onActionChange(null);
setOpen(false);
return;
}
onActionChange({
label: open
? "Рабочая область открыта"
: available.workflow === "adjudication"
? "Разобрать конфликты"
: available.workflow === "independent-blind"
? "Независимая разметка"
: available.workflow === "engineering-preannotation"
? "Проверить и исправить"
: "Разметить данные",
disabled: open,
onClick: openWorkspace,
});
return () => onActionChange(null);
}, [available, onActionChange, open, openWorkspace]);
if (open && available?.workflow === "adjudication" && l34eResult) {
return (
<L34FAdjudicationWorkspace
result={l34eResult}
onClose={() => setOpen(false)}
/>
);
}
return open && available && available.workflow !== "adjudication" ? (
<L34AnnotationWorkspace
resultId={available.resultId}
workflow={available.workflow}
onClose={() => setOpen(false)}
/>
) : null;
}
@@ -1,10 +1,25 @@
import type { LaboratoryOption } from "../../components/laboratory/LaboratoryPresentation";
import type { AdvancedLaboratoryWorkId } from "../../core/laboratory/advancedIndex";
import type {
AdvancedLaboratoryIndexItem,
AdvancedLaboratoryWorkId,
} from "../../core/laboratory/advancedIndex";
import type { ObservationSessionSummary } from "../../core/observation/sessionArchive";
export type LaboratoryProfileId =
| "sensor-fusion"
| "public-benchmarks"
| "published-perception";
| "rig-camera-local-surface-v1"
| "rig-track-geometry-temporal-v1"
| "rig-ravnoves-perception-gate-v1"
| "rig-pointpillars-transfer-v1"
| "rig-right-yolox-lidar-range-v1"
| "rig-nvidia-ready-stack-v1"
| "rig-nvidia-dashcam-ready-stack-v1"
| "rig-nvidia-rectified-ready-stack-v1"
| "rig-nvidia-grounding-dino-front-v1"
| "rig-right-raw-fisheye-yolox-realtime-v1"
| "kitti-pointpillars-benchmark-v1"
| `published:${string}`;
export type LaboratoryExperimentId = string;
export type LaboratoryWorkId =
| "e28-local-surface"
@@ -13,48 +28,430 @@ export type LaboratoryWorkId =
| AdvancedLaboratoryWorkId
| `session:${string}`;
export function buildLaboratoryProfiles({
rigLabel,
sensorAvailable,
publicBenchmarkAvailable,
publishedAvailable,
}: {
rigLabel: string;
sensorAvailable: boolean;
publicBenchmarkAvailable: boolean;
publishedAvailable: boolean;
}): readonly LaboratoryOption<LaboratoryProfileId>[] {
const profiles: LaboratoryOption<LaboratoryProfileId>[] = [];
if (sensorAvailable) {
profiles.push({
id: "sensor-fusion",
label: `${rigLabel} · камера + LiDAR · control plane`,
});
}
if (publicBenchmarkAvailable) {
profiles.push({
id: "public-benchmarks",
label: "Публичные датасеты · внешний benchmark-контур",
});
}
if (publishedAvailable) {
profiles.push({
id: "published-perception",
label: `${rigLabel} · опубликованный perception pipeline`,
});
}
return profiles;
export interface LaboratoryCatalogSeed {
id: LaboratoryWorkId;
createdAtUtc: string;
}
export function workOptionsForProfile(
export interface LaboratoryCatalogEntry {
id: LaboratoryWorkId;
createdAtUtc: string;
profileId: LaboratoryProfileId;
profileName: string;
experimentId: LaboratoryExperimentId;
experimentName: string;
variantName: string;
}
interface KnownWorkDefinition {
profileId: Exclude<LaboratoryProfileId, `published:${string}`>;
profileName: (rigLabel: string) => string;
experimentId: LaboratoryExperimentId;
experimentName: string;
variantName: string;
}
const rig = (rigLabel: string): string => rigLabel.trim() || "Сенсорный риг";
const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`>, KnownWorkDefinition>> = {
"e28-local-surface": {
profileId: "rig-camera-local-surface-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera-first + local-surface LiDAR`,
experimentId: "local-surface-semantic-geometry",
experimentName: "Local surface + semantic geometry",
variantName: "E28 · Local surface L2.6",
},
"e29-camera-geometry": {
profileId: "rig-camera-local-surface-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera-first + local-surface LiDAR`,
experimentId: "local-surface-semantic-geometry",
experimentName: "Local surface + semantic geometry",
variantName: "E29 · Camera-first + geometry",
},
"e30-evidence-review": {
profileId: "rig-camera-local-surface-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera-first + local-surface LiDAR`,
experimentId: "local-surface-semantic-geometry",
experimentName: "Local surface + semantic geometry",
variantName: "E30 · Evidence review A2",
},
"e31-source-binding": {
profileId: "rig-track-geometry-temporal-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · TrackGeometry + temporal occupied/unknown`,
experimentId: "track-geometry-source-contract",
experimentName: "Source contract + TrackGeometry",
variantName: "E31 · Source binding",
},
"e32-track-geometry": {
profileId: "rig-track-geometry-temporal-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · TrackGeometry + temporal occupied/unknown`,
experimentId: "track-geometry-source-contract",
experimentName: "Source contract + TrackGeometry",
variantName: "E32 · TrackGeometry v1",
},
"e33-worker-shadow": {
profileId: "rig-track-geometry-temporal-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · TrackGeometry + temporal occupied/unknown`,
experimentId: "temporal-shadow-envelope",
experimentName: "Temporal shadow envelope",
variantName: "E33 · Worker shadow 1×",
},
"e34-temporal-layer": {
profileId: "rig-track-geometry-temporal-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · TrackGeometry + temporal occupied/unknown`,
experimentId: "temporal-shadow-envelope",
experimentName: "Temporal shadow envelope",
variantName: "E34 · Occupied/unknown layer",
},
"e35-degradation-recovery": {
profileId: "rig-track-geometry-temporal-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · TrackGeometry + temporal occupied/unknown`,
experimentId: "temporal-shadow-envelope",
experimentName: "Temporal shadow envelope",
variantName: "E35 · Degradation recovery",
},
"e37-ravnoves-acceptance": {
profileId: "rig-ravnoves-perception-gate-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 perception gate`,
experimentId: "ravnoves00-quality-gate",
experimentName: "RAVNOVES00 quality gate",
variantName: "E37 · Acceptance R0",
},
"e38-perception-baseline": {
profileId: "rig-ravnoves-perception-gate-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 perception gate`,
experimentId: "ravnoves00-quality-gate",
experimentName: "RAVNOVES00 quality gate",
variantName: "E38 · Perception quality R1",
},
"e39-perception-refinement": {
profileId: "rig-ravnoves-perception-gate-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 perception gate`,
experimentId: "ravnoves00-quality-gate",
experimentName: "RAVNOVES00 quality gate",
variantName: "E39 · Perception refinement R1",
},
"e40-perception-product-gate": {
profileId: "rig-ravnoves-perception-gate-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 perception gate`,
experimentId: "ravnoves00-quality-gate",
experimentName: "RAVNOVES00 quality gate",
variantName: "E40 · Historical visible evaluation",
},
"e46-detector-truth-island": {
profileId: "rig-right-yolox-lidar-range-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · YOLOX camera-first + LiDAR range`,
experimentId: "ravnoves00-perception-benchmark-v1",
experimentName: "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1",
variantName: "E46 · Independent source-only reviewer collection · not truth",
},
"e46a-ai-engineering-preannotation": {
profileId: "rig-right-yolox-lidar-range-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · YOLOX camera-first + LiDAR range`,
experimentId: "ravnoves00-perception-benchmark-v1",
experimentName: "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1",
variantName: "E46A · AI engineering preannotation · 32/32 · not truth",
},
"e46b-temporal-motion": {
profileId: "rig-right-yolox-lidar-range-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · YOLOX camera-first + LiDAR range`,
experimentId: "ravnoves00-perception-benchmark-v1",
experimentName: "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1",
variantName: "E46B · RIGHT temporal tracks + motion-state · 16/16",
},
"e46c-full-replay-world-tracks": {
profileId: "rig-right-yolox-lidar-range-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · YOLOX camera-first + LiDAR range`,
experimentId: "ravnoves00-perception-benchmark-v1",
experimentName: "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1",
variantName: "E46C · full recorded RIGHT route/world tracks · 4489/4489",
},
"e46d-temporal-failure-audit": {
profileId: "rig-right-yolox-lidar-range-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · YOLOX camera-first + LiDAR range`,
experimentId: "ravnoves00-perception-benchmark-v1",
experimentName: "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1",
variantName: "E46D · full replay temporal failure audit · automatic clips",
},
"e46e-ready-stack": {
profileId: "rig-nvidia-ready-stack-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · NVIDIA RT-DETR + NvDCF ready stack`,
experimentId: "nvidia-ready-stack-bakeoff-r1",
experimentName: "NVIDIA ready-stack bake-off R1",
variantName: "E46E · TrafficCamNet RT-DETR + NvDCF · full replay",
},
"e46f-dashcam-bakeoff": {
profileId: "rig-nvidia-dashcam-ready-stack-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · DashCamNet DetectNet_v2 + NvDCF`,
experimentId: "nvidia-mobile-detector-bakeoff-r2",
experimentName: "NVIDIA mobile-detector bake-off R2",
variantName: "E46F · DashCamNet DetectNet_v2 + NvDCF · rejected on raw fisheye",
},
"e46g-rectified-detector-bakeoff": {
profileId: "rig-nvidia-rectified-ready-stack-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · NVIDIA KB4 FRONT + TrafficCamNet`,
experimentId: "nvidia-calibrated-detector-bakeoff-r3",
experimentName: "NVIDIA calibrated ready-detector bake-off R3",
variantName: "E46G · KB4 nvdewarper · TrafficCamNet FRONT selected",
},
"e46h-full-rectified-front-replay": {
profileId: "rig-nvidia-rectified-ready-stack-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · NVIDIA KB4 FRONT + TrafficCamNet`,
experimentId: "nvidia-front-full-route-qualification-r1",
experimentName: "NVIDIA FRONT full-route qualification R1",
variantName: "E46H · full FRONT replay · large semantic false tracks",
},
"e46i-grounding-dino-full-replay": {
profileId: "rig-nvidia-grounding-dino-front-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · NVIDIA KB4 FRONT + Grounding DINO`,
experimentId: "nvidia-grounding-dino-full-route-shadow-r1",
experimentName: "NVIDIA Grounding DINO full-route shadow R1",
variantName: "E46I · semantic regression suppressed · awaiting temporal layer",
},
"e46j-raw-fisheye-realtime": {
profileId: "rig-right-raw-fisheye-yolox-realtime-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · raw KB4 fisheye + YOLOX-S`,
experimentId: "raw-fisheye-one-pass-realtime-r1",
experimentName: "Raw fisheye one-pass realtime qualification R1",
variantName: "E46J · full raw fisheye · realtime capacity passed",
},
"l34-right-yolox-truth-island-freeze": {
profileId: "rig-right-yolox-lidar-range-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · YOLOX camera-first + LiDAR range`,
experimentId: "ravnoves00-perception-benchmark-v1",
experimentName: "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1",
variantName: "L3.4 · RIGHT YOLOX predictions frozen · awaiting truth",
},
"l34a-assisted-yolox-error-audit": {
profileId: "rig-right-yolox-lidar-range-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · YOLOX camera-first + LiDAR range`,
experimentId: "ravnoves00-perception-benchmark-v1",
experimentName: "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1",
variantName: "L3.4A · Assisted YOLOX error audit · not truth",
},
"l34b-nested-box-consolidation-shadow": {
profileId: "rig-right-yolox-lidar-range-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · YOLOX camera-first + LiDAR range`,
experimentId: "ravnoves00-perception-benchmark-v1",
experimentName: "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1",
variantName: "L3.4B · Nested-box consolidation shadow · not truth",
},
"l34c-tile-seam-stitch-shadow": {
profileId: "rig-right-yolox-lidar-range-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · YOLOX camera-first + LiDAR range`,
experimentId: "ravnoves00-perception-benchmark-v1",
experimentName: "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1",
variantName: "L3.4C · Temporal front/left seam stitch · not truth",
},
"l34d-cumulative-postprocessing-candidate": {
profileId: "rig-right-yolox-lidar-range-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · YOLOX camera-first + LiDAR range`,
experimentId: "ravnoves00-perception-benchmark-v1",
experimentName: "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1",
variantName: "L3.4D · Cumulative B+C candidate freeze · not truth",
},
"l34e-self-review-diagnostic": {
profileId: "rig-right-yolox-lidar-range-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · YOLOX camera-first + LiDAR range`,
experimentId: "ravnoves00-perception-benchmark-v1",
experimentName: "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1",
variantName: "L3.4E · Self-review diagnostic disagreement audit · not truth",
},
"l34f-adjudicated-reference": {
profileId: "rig-right-yolox-lidar-range-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · YOLOX camera-first + LiDAR range`,
experimentId: "ravnoves00-perception-benchmark-v1",
experimentName: "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1",
variantName: "L3.4F · Candidate-visible adjudicated engineering reference · not truth",
},
"l3-pointpillars-visual-audit": {
profileId: "kitti-pointpillars-benchmark-v1",
profileName: () => "KITTI · PointPillars external benchmark",
experimentId: "kitti-pointpillars-visual-audit",
experimentName: "PointPillars external validation",
variantName: "L3 · KITTI visual audit",
},
"l31-pointpillars-ravnoves": {
profileId: "rig-pointpillars-transfer-v1",
profileName: (rigLabel) => `${rig(rigLabel)} · PointPillars LiDAR transfer`,
experimentId: "pointpillars-ravnoves00-transfer",
experimentName: "RAVNOVES00 transfer",
variantName: "L3.1 · PointPillars hypotheses",
},
"l32-pointpillars-camera-review": {
profileId: "rig-pointpillars-transfer-v1",
profileName: (rigLabel) => `${rig(rigLabel)} · PointPillars LiDAR transfer`,
experimentId: "pointpillars-ravnoves00-transfer",
experimentName: "RAVNOVES00 transfer",
variantName: "L3.2 · RIGHT camera review",
},
"l33-camera-first-detector-review": {
profileId: "rig-right-yolox-lidar-range-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · YOLOX camera-first + LiDAR range`,
experimentId: "right-camera-detector-admission",
experimentName: "Detector admission + metric range",
variantName: "L3.3 · Detector review + LiDAR range",
},
};
const PUBLISHED_PIPELINE_NAMES: Readonly<Record<string, string>> = {
"e10-integrated-perception": "Camera semantics + LiDAR metric fusion",
"e21-realtime-envelope": "Bounded real-time perception replay",
"e22-temporal-stability": "Temporal 2D/3D stabilization",
"e23-inline-temporal-stability": "Inline warm-worker stabilization",
"e24-world-motion": "World-frame motion tracking",
"e25-persistent-support-motion": "Persistent occupied-support tracking",
"e26-camera-ego-motion-fusion": "Camera + ego-motion fusion",
};
function timestamp(value: string): number {
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : 0;
}
export function laboratoryDate(value: string): string {
const parsed = new Date(value);
if (!Number.isFinite(parsed.getTime())) return "0000-00-00";
return new Intl.DateTimeFormat("sv-SE", {
timeZone: "Europe/Moscow",
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(parsed);
}
export function laboratoryTimestamp(value: string): string {
const parsed = new Date(value);
if (!Number.isFinite(parsed.getTime())) return "0000-00-00 00:00:00 MSK";
const parts = Object.fromEntries(new Intl.DateTimeFormat("en-CA", {
timeZone: "Europe/Moscow",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23",
}).formatToParts(parsed).map(({ type, value: part }) => [type, part]));
return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second} MSK`;
}
function newestFirst(
left: Pick<LaboratoryCatalogEntry, "id" | "createdAtUtc">,
right: Pick<LaboratoryCatalogEntry, "id" | "createdAtUtc">,
): number {
return timestamp(right.createdAtUtc) - timestamp(left.createdAtUtc)
|| right.id.localeCompare(left.id);
}
function latestTimestamp(entries: readonly LaboratoryCatalogEntry[]): string {
const latest = [...entries].sort(newestFirst)[0];
return laboratoryTimestamp(latest?.createdAtUtc ?? "");
}
function pipelineIdForSession(session: ObservationSessionSummary): string {
const method = session.lab?.provenance.method;
if (method && typeof method === "object" && !Array.isArray(method)) {
const pipelineId = (method as Record<string, unknown>).pipeline_id;
if (typeof pipelineId === "string" && pipelineId.trim()) return pipelineId.trim();
}
return session.lab?.resultKind ?? "legacy-perception";
}
export function buildLaboratoryCatalog({
rigLabel,
knownWorks,
advancedIndex,
publishedWorks,
}: {
rigLabel: string;
knownWorks: readonly LaboratoryCatalogSeed[];
advancedIndex: readonly AdvancedLaboratoryIndexItem[];
publishedWorks: readonly ObservationSessionSummary[];
}): readonly LaboratoryCatalogEntry[] {
const seeded = new Map<LaboratoryWorkId, string>();
for (const work of knownWorks) seeded.set(work.id, work.createdAtUtc);
for (const work of advancedIndex) seeded.set(work.workId, work.createdAtUtc);
const entries: LaboratoryCatalogEntry[] = [];
for (const [id, createdAtUtc] of seeded) {
if (id.startsWith("session:")) continue;
const definition = KNOWN_WORKS[id as Exclude<LaboratoryWorkId, `session:${string}`>];
if (!definition) continue;
entries.push({
id,
createdAtUtc,
profileId: definition.profileId,
profileName: definition.profileName(rigLabel),
experimentId: definition.experimentId,
experimentName: definition.experimentName,
variantName: definition.variantName,
});
}
for (const session of publishedWorks) {
const pipelineId = pipelineIdForSession(session);
const profileId = `published:${pipelineId}` as const;
const pipelineName = PUBLISHED_PIPELINE_NAMES[pipelineId] ?? pipelineId;
entries.push({
id: `session:${session.id}`,
createdAtUtc: session.lab?.runCreatedAtUtc ?? session.startedAtUtc,
profileId,
profileName: `${rig(rigLabel)} RIGHT · ${pipelineName}`,
experimentId: `${profileId}:ravnoves00`,
experimentName: `RAVNOVES00 · ${pipelineName}`,
variantName: session.label,
});
}
return entries.sort(newestFirst);
}
export function buildLaboratoryProfiles(
catalog: readonly LaboratoryCatalogEntry[],
): readonly LaboratoryOption<LaboratoryProfileId>[] {
const groups = new Map<LaboratoryProfileId, LaboratoryCatalogEntry[]>();
for (const entry of catalog) {
const group = groups.get(entry.profileId) ?? [];
group.push(entry);
groups.set(entry.profileId, group);
}
return [...groups.entries()].map(([id, entries]) => ({
id,
label: `${latestTimestamp(entries)} · ${entries[0]?.profileName ?? id}`,
latest: Math.max(...entries.map(({ createdAtUtc }) => timestamp(createdAtUtc))),
})).sort((left, right) => right.latest - left.latest || left.label.localeCompare(right.label))
.map(({ id, label }) => ({ id, label }));
}
export function experimentOptionsForProfile(
profileId: LaboratoryProfileId,
sensorWorks: readonly LaboratoryOption<LaboratoryWorkId>[],
publicBenchmarkWorks: readonly LaboratoryOption<LaboratoryWorkId>[],
publishedWorks: readonly LaboratoryOption<LaboratoryWorkId>[],
catalog: readonly LaboratoryCatalogEntry[],
): readonly LaboratoryOption<LaboratoryExperimentId>[] {
const groups = new Map<LaboratoryExperimentId, LaboratoryCatalogEntry[]>();
for (const entry of catalog) {
if (entry.profileId !== profileId) continue;
const group = groups.get(entry.experimentId) ?? [];
group.push(entry);
groups.set(entry.experimentId, group);
}
return [...groups.entries()].map(([id, entries]) => ({
id,
label: `${latestTimestamp(entries)} · ${entries[0]?.experimentName ?? id}`,
latest: Math.max(...entries.map(({ createdAtUtc }) => timestamp(createdAtUtc))),
})).sort((left, right) => right.latest - left.latest || left.label.localeCompare(right.label))
.map(({ id, label }) => ({ id, label }));
}
export function workOptionsForExperiment(
profileId: LaboratoryProfileId,
experimentId: LaboratoryExperimentId,
catalog: readonly LaboratoryCatalogEntry[],
): readonly LaboratoryOption<LaboratoryWorkId>[] {
return profileId === "sensor-fusion"
? sensorWorks
: profileId === "public-benchmarks"
? publicBenchmarkWorks
: publishedWorks;
return catalog.filter((entry) => (
entry.profileId === profileId && entry.experimentId === experimentId
)).sort(newestFirst).map((entry) => ({
id: entry.id,
label: `${laboratoryTimestamp(entry.createdAtUtc)} · ${entry.variantName}`,
}));
}
@@ -20,6 +20,8 @@ function mergeResults(
return {
l3: next.l3 ?? current.l3,
l31: next.l31 ?? current.l31,
l32: next.l32 ?? current.l32,
l33: next.l33 ?? current.l33,
e31: next.e31 ?? current.e31,
e32: next.e32 ?? current.e32,
e33: next.e33 ?? current.e33,
@@ -29,6 +31,24 @@ function mergeResults(
e38: next.e38 ?? current.e38,
e39: next.e39 ?? current.e39,
e40: next.e40 ?? current.e40,
e46: next.e46 ?? current.e46,
e46a: next.e46a ?? current.e46a,
e46b: next.e46b ?? current.e46b,
e46c: next.e46c ?? current.e46c,
e46d: next.e46d ?? current.e46d,
e46e: next.e46e ?? current.e46e,
e46f: next.e46f ?? current.e46f,
e46g: next.e46g ?? current.e46g,
e46h: next.e46h ?? current.e46h,
e46i: next.e46i ?? current.e46i,
e46j: next.e46j ?? current.e46j,
l34: next.l34 ?? current.l34,
l34a: next.l34a ?? current.l34a,
l34b: next.l34b ?? current.l34b,
l34c: next.l34c ?? current.l34c,
l34d: next.l34d ?? current.l34d,
l34e: next.l34e ?? current.l34e,
l34f: next.l34f ?? current.l34f,
};
}