refactor(ui): reuse canonical graph for M4.8 regression
This commit is contained in:
@@ -184,6 +184,7 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.m4-replay-threat-visual__review-controls,
|
||||
.m4-replay-threat-visual__pane-layer-controls,
|
||||
.m4-replay-threat-visual__single-pane-controls {
|
||||
display: flex;
|
||||
@@ -195,11 +196,14 @@
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.m4-replay-threat-visual__review-controls,
|
||||
.m4-replay-threat-visual__review-controls > *,
|
||||
.m4-replay-threat-visual__single-pane-controls,
|
||||
.m4-replay-threat-visual__single-pane-controls > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.m4-replay-threat-visual__review-controls::-webkit-scrollbar,
|
||||
.m4-replay-threat-visual__pane-layer-controls::-webkit-scrollbar,
|
||||
.m4-replay-threat-visual__single-pane-controls::-webkit-scrollbar {
|
||||
display: none;
|
||||
|
||||
+6
-5
@@ -5,7 +5,7 @@ import {
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type { M48SmallStaticRegressionResult } from "../../core/laboratory/m48SmallStaticRegression";
|
||||
import { M48SmallStaticRegressionVisual } from "./M48SmallStaticRegressionVisual";
|
||||
import { M48SmallStaticRegressionEvidence } from "./M48SmallStaticRegressionEvidence";
|
||||
|
||||
function percent(value: number): string {
|
||||
return `${(value * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
|
||||
@@ -30,7 +30,7 @@ export function M48SmallStaticPassageRegressionResultView({
|
||||
status={status}
|
||||
statusTone={result.accepted ? "success" : "warning"}
|
||||
facts={[
|
||||
{ label: "Конфигурация", value: `${rigLabel} RIGHT · camera + prediction-free current spatial evidence` },
|
||||
{ label: "Конфигурация", value: `${rigLabel} RIGHT · Canonical Reference Graph · VIDEO/CAMERA/3D/PLAN/SEMANTICS` },
|
||||
{ label: "Пайплайн", value: result.pipelineId },
|
||||
{ label: "Эксперимент", value: result.experimentId },
|
||||
{ label: "Прогон", value: `${result.runLabel} · immutable ${result.resultId}` },
|
||||
@@ -47,7 +47,8 @@ export function M48SmallStaticPassageRegressionResultView({
|
||||
executionClass: "deterministic",
|
||||
pipelineId: result.pipelineId,
|
||||
components: [
|
||||
{ kind: "source", name: result.packId, version: "immutable Worker 006 pack", role: "frozen candidate output + exact camera/current-spatial evidence", identitySha256: result.packId.split("-").at(-1) ?? null },
|
||||
{ kind: "source", name: result.packId, version: "immutable Worker 006 pack", role: "frozen candidate output + exact assisted anchors", identitySha256: result.packId.split("-").at(-1) ?? null },
|
||||
{ kind: "source", name: result.referenceGraphLabResultId, version: "approved canonical template", role: "synchronized VIDEO/CAMERA/3D/PLAN/SEMANTICS evidence", identitySha256: result.referenceGraphLabResultId.split("-").at(-1) ?? null },
|
||||
{ kind: "source", name: "M4.8 assisted correction snapshot", version: "revision-bound", role: "operator-added anchors · not independent truth", identitySha256: null },
|
||||
{ kind: "algorithm", name: "small-static assisted-anchor comparator", version: "v1", role: "exact-frame class-free IoU regression", identitySha256: result.resultId.split("-").at(-1) ?? null },
|
||||
],
|
||||
@@ -55,8 +56,8 @@ export function M48SmallStaticPassageRegressionResultView({
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence eyebrow="M4.8R1 VISUAL EVIDENCE · ASSISTED ANCHOR + WORKER" title="Точный кадр: ручной якорь и frozen-ответ Worker 006" kind="recorded-replay" resizable>
|
||||
<M48SmallStaticRegressionVisual resultId={result.resultId} />
|
||||
<LaboratoryEvidence eyebrow="M4.8R1 VISUAL EVIDENCE · CANONICAL REFERENCE GRAPH" title="Полный граф и 14 точных assisted-якорей на общем recorded timeline" kind="recorded-replay" resizable>
|
||||
<M48SmallStaticRegressionEvidence result={result} />
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Icon } from "@nodedc/ui-react";
|
||||
|
||||
import { fetchM47ReferenceGraphLab, type M47ReferenceGraphLabResult } from "../../core/laboratory/m47ReferenceGraph";
|
||||
import {
|
||||
fetchM48SmallStaticRegressionCases,
|
||||
type M48SmallStaticRegressionCaseSummary,
|
||||
type M48SmallStaticRegressionResult,
|
||||
} from "../../core/laboratory/m48SmallStaticRegression";
|
||||
import {
|
||||
M4ReplayThreatVisual,
|
||||
type M4ReplayThreatReviewAnchor,
|
||||
} from "./M4ReplayThreatVisual";
|
||||
|
||||
function message(error: unknown): string {
|
||||
return error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
: "Каноническое graph evidence M4.8R1 недоступно.";
|
||||
}
|
||||
|
||||
export function M48SmallStaticRegressionEvidence({
|
||||
result,
|
||||
}: {
|
||||
result: M48SmallStaticRegressionResult;
|
||||
}) {
|
||||
const [graph, setGraph] = useState<M47ReferenceGraphLabResult | null>(null);
|
||||
const [cases, setCases] = useState<readonly M48SmallStaticRegressionCaseSummary[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
void Promise.all([
|
||||
fetchM47ReferenceGraphLab({
|
||||
resultId: result.referenceGraphLabResultId,
|
||||
signal: controller.signal,
|
||||
}),
|
||||
fetchM48SmallStaticRegressionCases(result.resultId, {
|
||||
signal: controller.signal,
|
||||
}),
|
||||
])
|
||||
.then(([nextGraph, nextCases]) => {
|
||||
if (controller.signal.aborted) return;
|
||||
if (nextCases.some((item) => item.sequence < 1 || item.sequence > nextGraph.frames.expected)) {
|
||||
throw new Error("M4.8R1 anchor вышел за immutable timeline Canonical Reference Graph.");
|
||||
}
|
||||
setGraph(nextGraph);
|
||||
setCases(nextCases);
|
||||
})
|
||||
.catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted) setError(message(caught));
|
||||
})
|
||||
.finally(() => {
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [result.referenceGraphLabResultId, result.resultId]);
|
||||
|
||||
const reviewAnchors = useMemo<readonly M4ReplayThreatReviewAnchor[]>(() => (
|
||||
cases.map((item) => ({
|
||||
id: item.anchorId,
|
||||
sourceSequence: item.sequence - 1,
|
||||
extentXyxyNormalized: item.anchorExtentXyxy,
|
||||
matchedAtThreshold: item.matchedAtThreshold,
|
||||
}))
|
||||
), [cases]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="l3-visual-audit__state" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
<span>Открываем утверждённый Canonical Reference Graph</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (error || !graph) {
|
||||
return (
|
||||
<div className="l3-visual-audit__state" role="alert">
|
||||
<Icon name="alert" size={18} />
|
||||
<span>{error ?? "Canonical Reference Graph не связан с прогоном M4.8R1."}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<M4ReplayThreatVisual
|
||||
resultId={graph.visual.resultId}
|
||||
semantic={{
|
||||
resultId: graph.semantic.resultId,
|
||||
taxonomy: graph.semantic.taxonomy,
|
||||
}}
|
||||
reviewAnchors={reviewAnchors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||
import {
|
||||
fetchM48ReviewSourceCatalog,
|
||||
type M48ReviewTracklet,
|
||||
} from "../../core/laboratory/m48ObjectCentricQuality";
|
||||
import {
|
||||
fetchM48SmallStaticRegressionCase,
|
||||
fetchM48SmallStaticRegressionCases,
|
||||
type M48SmallStaticRegressionCase,
|
||||
type M48SmallStaticRegressionCaseSummary,
|
||||
} from "../../core/laboratory/m48SmallStaticRegression";
|
||||
import {
|
||||
M48BlindClipPlayer,
|
||||
type M48BlindEvidenceMode,
|
||||
} from "./annotation/M48BlindClipPlayer";
|
||||
import { M48EvidenceModeRail } from "./annotation/M48EvidenceModeControls";
|
||||
import { useM48SpatialClipPlayback } from "./annotation/useM48SpatialClipPlayback";
|
||||
|
||||
function message(error: unknown): string {
|
||||
return error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
: "M4.8R1 evidence недоступно.";
|
||||
}
|
||||
|
||||
function exactFrameTracklets(item: M48SmallStaticRegressionCase): readonly M48ReviewTracklet[] {
|
||||
const sequence = item.anchor.sequence;
|
||||
const state = (
|
||||
objectId: string,
|
||||
extentXyxy: readonly [number, number, number, number],
|
||||
geometryAssociation: M48ReviewTracklet["stateSegments"][number]["geometryAssociation"],
|
||||
freshness: M48ReviewTracklet["stateSegments"][number]["freshness"],
|
||||
motion: M48ReviewTracklet["stateSegments"][number]["motion"],
|
||||
threat: M48ReviewTracklet["stateSegments"][number]["threat"],
|
||||
criticalCorridorObstacle: boolean,
|
||||
): M48ReviewTracklet => ({
|
||||
objectId,
|
||||
firstSequence: sequence,
|
||||
lastSequence: sequence,
|
||||
keyframes: [{ sequence, extentXyxy, visibility: "visible" }],
|
||||
stateSegments: [{
|
||||
startSequence: sequence,
|
||||
endSequence: sequence,
|
||||
geometryAssociation,
|
||||
freshness,
|
||||
motion,
|
||||
threat,
|
||||
criticalCorridorObstacle,
|
||||
}],
|
||||
notes: null,
|
||||
});
|
||||
|
||||
return [
|
||||
state(
|
||||
`ASSISTED · ${item.anchor.objectId}`,
|
||||
item.anchor.extentXyxy,
|
||||
item.anchor.geometryAssociation,
|
||||
item.anchor.freshness,
|
||||
item.anchor.motion,
|
||||
item.anchor.threat,
|
||||
item.anchor.requiresAvoidanceOrClearance,
|
||||
),
|
||||
...item.comparison.workerObjects.map((object) => state(
|
||||
`WORKER · ${object.predictionId}`,
|
||||
object.extentXyxy,
|
||||
object.geometryAssociation,
|
||||
object.freshness,
|
||||
object.motion,
|
||||
object.threat,
|
||||
false,
|
||||
)),
|
||||
];
|
||||
}
|
||||
|
||||
export function M48SmallStaticRegressionVisual({ resultId }: { resultId: string }) {
|
||||
const [cases, setCases] = useState<readonly M48SmallStaticRegressionCaseSummary[]>([]);
|
||||
const [caseIndex, setCaseIndex] = useState(0);
|
||||
const [item, setItem] = useState<M48SmallStaticRegressionCase | null>(null);
|
||||
const [catalog, setCatalog] = useState<Awaited<ReturnType<typeof fetchM48ReviewSourceCatalog>> | null>(null);
|
||||
const [sequence, setSequence] = useState(1);
|
||||
const [mode, setMode] = useState<M48BlindEvidenceMode>("camera");
|
||||
const [cameraVisible, setCameraVisible] = useState(true);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
void fetchM48SmallStaticRegressionCases(resultId, { signal: controller.signal })
|
||||
.then((next) => {
|
||||
if (!controller.signal.aborted) setCases(next);
|
||||
})
|
||||
.catch((caught: unknown) => !controller.signal.aborted && setError(message(caught)))
|
||||
.finally(() => !controller.signal.aborted && setLoading(false));
|
||||
return () => controller.abort();
|
||||
}, [resultId]);
|
||||
|
||||
useEffect(() => {
|
||||
const selected = cases[caseIndex];
|
||||
if (!selected) {
|
||||
setItem(null);
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
void fetchM48SmallStaticRegressionCase(resultId, selected.anchorId, { signal: controller.signal })
|
||||
.then((next) => {
|
||||
if (controller.signal.aborted) return;
|
||||
setItem(next);
|
||||
setSequence(next.anchor.sequence);
|
||||
})
|
||||
.catch((caught: unknown) => !controller.signal.aborted && setError(message(caught)))
|
||||
.finally(() => !controller.signal.aborted && setLoading(false));
|
||||
return () => controller.abort();
|
||||
}, [caseIndex, cases, resultId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!item) return;
|
||||
const controller = new AbortController();
|
||||
setCatalog(null);
|
||||
void fetchM48ReviewSourceCatalog(item.packId, { signal: controller.signal })
|
||||
.then((next) => !controller.signal.aborted && setCatalog(next))
|
||||
.catch((caught: unknown) => !controller.signal.aborted && setError(message(caught)));
|
||||
return () => controller.abort();
|
||||
}, [item?.packId]);
|
||||
|
||||
const clip = item && catalog
|
||||
? catalog.clips.find((candidate) => candidate.clipId === item.anchor.clipId) ?? null
|
||||
: null;
|
||||
const spatialEnabled = Boolean(
|
||||
catalog?.evidenceCapabilities.currentPointCloudBodyXyzM
|
||||
&& catalog.evidenceCapabilities.rig
|
||||
&& catalog.evidenceCapabilities.virtualCorridor,
|
||||
);
|
||||
const spatial = useM48SpatialClipPlayback({
|
||||
packId: item?.packId ?? "",
|
||||
clip,
|
||||
sequence,
|
||||
enabled: mode !== "camera" && spatialEnabled,
|
||||
});
|
||||
const tracklets = useMemo(
|
||||
() => item ? exactFrameTracklets(item) : [],
|
||||
[item],
|
||||
);
|
||||
|
||||
return (
|
||||
<LaboratoryEvidenceViewer
|
||||
label="M4.8R1 assisted-anchor regression"
|
||||
className="m48-atlas-visual"
|
||||
mode={mode}
|
||||
modes={[]}
|
||||
expanded={expanded}
|
||||
onModeChange={setMode}
|
||||
onExpandedChange={setExpanded}
|
||||
modeControlsVisible={false}
|
||||
chromeLayout="stacked"
|
||||
actions={(
|
||||
<>
|
||||
<IconButton
|
||||
label="Предыдущий assisted-якорь"
|
||||
disabled={!cases.length}
|
||||
onClick={() => setCaseIndex((current) => (current - 1 + cases.length) % cases.length)}
|
||||
>
|
||||
<Icon name="chevron-left" size={16} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
label="Следующий assisted-якорь"
|
||||
disabled={!cases.length}
|
||||
onClick={() => setCaseIndex((current) => (current + 1) % cases.length)}
|
||||
>
|
||||
<Icon name="chevron-right" size={16} />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
overlay={item ? (
|
||||
<div className="m48-atlas-visual__case">
|
||||
<StatusBadge tone={item.comparison.matchedAtThreshold ? "success" : "warning"}>
|
||||
{item.comparison.matchedAtThreshold ? "WORKER RECALL" : "WORKER MISS"}
|
||||
</StatusBadge>
|
||||
<strong>{caseIndex + 1}/{cases.length} · {item.anchor.clipId} · кадр {item.anchor.sequence}</strong>
|
||||
<small>ASSISTED-якорь, не independent truth · best IoU {item.comparison.bestIou.toFixed(3)}</small>
|
||||
</div>
|
||||
) : null}
|
||||
>
|
||||
<div className="m48-evidence-stage">
|
||||
{loading ? (
|
||||
<div className="m48-atlas-visual__state" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
Загружаем M4.8R1 bounded case
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="m48-atlas-visual__state" role="alert"><Icon name="alert" size={18} />{error}</div>
|
||||
) : clip && catalog?.cameraPlayback && item ? (
|
||||
<M48BlindClipPlayer
|
||||
cameraPlayback={catalog.cameraPlayback}
|
||||
clip={clip}
|
||||
sequence={sequence}
|
||||
mode={mode}
|
||||
cameraVisible={cameraVisible}
|
||||
tracklets={tracklets}
|
||||
selectedObjectId={`ASSISTED · ${item.anchor.objectId}`}
|
||||
editable={false}
|
||||
drawing={false}
|
||||
spatialFrame={spatial.frame}
|
||||
spatialLoading={spatial.loading}
|
||||
spatialError={spatial.error}
|
||||
spatialEvidenceAvailable={spatialEnabled}
|
||||
onSequenceChange={setSequence}
|
||||
onDrawingChange={() => undefined}
|
||||
onSelectedObjectIdChange={() => undefined}
|
||||
onTrackletsChange={() => undefined}
|
||||
/>
|
||||
) : (
|
||||
<div className="m48-atlas-visual__state" role="status">
|
||||
<Icon name="alert" size={18} />Точный источник M4.8R1 недоступен.
|
||||
</div>
|
||||
)}
|
||||
{catalog ? (
|
||||
<M48EvidenceModeRail
|
||||
mode={mode}
|
||||
cameraVisible={cameraVisible}
|
||||
spatialAvailable={spatialEnabled}
|
||||
onModeChange={setMode}
|
||||
onCameraVisibleChange={setCameraVisible}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</LaboratoryEvidenceViewer>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Button,
|
||||
Icon,
|
||||
IconButton,
|
||||
Select,
|
||||
SegmentedControl,
|
||||
SplitPane,
|
||||
type SplitPaneOrientation,
|
||||
@@ -90,12 +91,23 @@ export interface M4ReplayThreatSemanticLayer {
|
||||
taxonomy: readonly E47SemanticClass[];
|
||||
}
|
||||
|
||||
export interface M4ReplayThreatReviewAnchor {
|
||||
id: string;
|
||||
sourceSequence: number;
|
||||
extentXyxyNormalized: readonly [number, number, number, number];
|
||||
matchedAtThreshold: boolean;
|
||||
}
|
||||
|
||||
const EMPTY_REVIEW_ANCHORS: readonly M4ReplayThreatReviewAnchor[] = [];
|
||||
|
||||
export function M4ReplayThreatVisual({
|
||||
resultId,
|
||||
semantic,
|
||||
reviewAnchors = EMPTY_REVIEW_ANCHORS,
|
||||
}: {
|
||||
resultId: string;
|
||||
semantic?: M4ReplayThreatSemanticLayer;
|
||||
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
|
||||
}) {
|
||||
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
|
||||
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(null);
|
||||
@@ -111,6 +123,7 @@ export function M4ReplayThreatVisual({
|
||||
: "vertical"
|
||||
));
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0);
|
||||
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
|
||||
const metadata = useM4ThreatTimelineMetadata(resultId);
|
||||
const playbackRange = useMemo(() => metadata.timeline ? ({
|
||||
@@ -120,6 +133,8 @@ export function M4ReplayThreatVisual({
|
||||
const playbackController = useRecordedEvidencePlayback(playbackRange, {
|
||||
clock: mediaMode === "video" ? "external" : "animation",
|
||||
});
|
||||
const seekPlayback = playbackController.seek;
|
||||
const setPlaybackPlaying = playbackController.setPlaying;
|
||||
const timelineFrame = useM4ThreatTimelineFrame({
|
||||
resultId,
|
||||
timeline: metadata.timeline,
|
||||
@@ -209,7 +224,50 @@ export function M4ReplayThreatVisual({
|
||||
&& timelineFrame.activeSequence !== null
|
||||
&& frame.sequence !== timelineFrame.activeSequence,
|
||||
);
|
||||
const activeBoxes = useMemo(() => boxes(frame?.cameraProposals ?? []), [frame]);
|
||||
const reviewAnchorIdentity = useMemo(
|
||||
() => reviewAnchors.map((anchor) => `${anchor.id}:${anchor.sourceSequence}`).join("|"),
|
||||
[reviewAnchors],
|
||||
);
|
||||
const initializedReviewAnchorRef = useRef("");
|
||||
useEffect(() => {
|
||||
const timeline = metadata.timeline;
|
||||
if (!timeline || !reviewAnchors.length) return;
|
||||
const identity = `${resultId}:${reviewAnchorIdentity}`;
|
||||
if (initializedReviewAnchorRef.current === identity) return;
|
||||
initializedReviewAnchorRef.current = identity;
|
||||
setSelectedReviewAnchorIndex(0);
|
||||
const startTimeNs = timeline.frameTimesNs[reviewAnchors[0]!.sourceSequence];
|
||||
if (typeof startTimeNs === "number") {
|
||||
setPlaybackPlaying(false);
|
||||
seekPlayback(startTimeNs / 1_000_000_000);
|
||||
}
|
||||
}, [metadata.timeline, resultId, reviewAnchorIdentity, reviewAnchors, seekPlayback, setPlaybackPlaying]);
|
||||
const reviewAnchorBoxes = useMemo<readonly RecordedEvidenceBox[]>(() => {
|
||||
const timeline = metadata.timeline;
|
||||
if (!frame || !timeline) return [];
|
||||
return reviewAnchors
|
||||
.filter((anchor) => anchor.sourceSequence === frame.sequence)
|
||||
.map((anchor) => {
|
||||
const [left, top, right, bottom] = anchor.extentXyxyNormalized;
|
||||
return {
|
||||
boxXyxy: [
|
||||
left * timeline.imageWidth,
|
||||
top * timeline.imageHeight,
|
||||
right * timeline.imageWidth,
|
||||
bottom * timeline.imageHeight,
|
||||
] as const,
|
||||
label: anchor.matchedAtThreshold
|
||||
? "РУЧНОЙ ЯКОРЬ · ПОКРЫТ WORKER 006"
|
||||
: "РУЧНОЙ ЯКОРЬ · ПРОПУСК WORKER 006",
|
||||
tone: anchor.matchedAtThreshold ? "success" as const : "danger" as const,
|
||||
dashed: true,
|
||||
};
|
||||
});
|
||||
}, [frame, metadata.timeline, reviewAnchors]);
|
||||
const activeBoxes = useMemo(
|
||||
() => [...boxes(frame?.cameraProposals ?? []), ...reviewAnchorBoxes],
|
||||
[frame, reviewAnchorBoxes],
|
||||
);
|
||||
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
|
||||
() => semantic?.taxonomy.map((item) => ({
|
||||
id: item.classId,
|
||||
@@ -429,11 +487,54 @@ export function M4ReplayThreatVisual({
|
||||
? spatialMode ? spatialLayerControls : mediaMode ? mediaLayerControls : null
|
||||
: null;
|
||||
|
||||
const actions = singlePaneControls ? (
|
||||
const selectReviewAnchor = (index: number) => {
|
||||
const timeline = metadata.timeline;
|
||||
if (!timeline || !reviewAnchors.length) return;
|
||||
const normalizedIndex = (index + reviewAnchors.length) % reviewAnchors.length;
|
||||
const anchor = reviewAnchors[normalizedIndex]!;
|
||||
const sourceTimeNs = timeline.frameTimesNs[anchor.sourceSequence];
|
||||
if (typeof sourceTimeNs !== "number") return;
|
||||
setSelectedReviewAnchorIndex(normalizedIndex);
|
||||
setPlaybackPlaying(false);
|
||||
seekPlayback(sourceTimeNs / 1_000_000_000);
|
||||
};
|
||||
|
||||
const reviewControls = reviewAnchors.length ? (
|
||||
<div className="m4-replay-threat-visual__review-controls">
|
||||
<IconButton
|
||||
label="Предыдущий контрольный пример"
|
||||
onClick={() => selectReviewAnchor(selectedReviewAnchorIndex - 1)}
|
||||
>
|
||||
<Icon name="chevron-left" size={16} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
label="Следующий контрольный пример"
|
||||
onClick={() => selectReviewAnchor(selectedReviewAnchorIndex + 1)}
|
||||
>
|
||||
<Icon name="chevron-right" size={16} />
|
||||
</IconButton>
|
||||
<Select
|
||||
label="Контрольные примеры M4.8R1"
|
||||
value={String(selectedReviewAnchorIndex)}
|
||||
options={reviewAnchors.map((anchor, index) => ({
|
||||
value: String(index),
|
||||
label: `${index + 1}/${reviewAnchors.length} · кадр ${anchor.sourceSequence + 1} · ${anchor.matchedAtThreshold ? "покрыт" : "пропуск"}`,
|
||||
}))}
|
||||
variant="split"
|
||||
menuWidth="anchor"
|
||||
onChange={(value) => selectReviewAnchor(Number(value))}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const actions = reviewControls || singlePaneControls ? (
|
||||
<div className="l3-visual-audit__actions">
|
||||
<div className="m4-replay-threat-visual__single-pane-controls">
|
||||
{singlePaneControls}
|
||||
</div>
|
||||
{reviewControls}
|
||||
{singlePaneControls ? (
|
||||
<div className="m4-replay-threat-visual__single-pane-controls">
|
||||
{singlePaneControls}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : undefined;
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
|
||||
},
|
||||
"m48-small-static-passage-regression": {
|
||||
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + prediction-free spatial evidence`,
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Canonical Reference Graph`,
|
||||
experimentId: "m48-small-static-passage-regression",
|
||||
experimentName: "M4.8 · small static passage regression",
|
||||
variantName: "M4.8R1 · Worker 006 small-static assisted baseline",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { after, before, test } from "node:test";
|
||||
import { createServer } from "vite";
|
||||
|
||||
@@ -10,6 +10,7 @@ let fetchM48SmallStaticRegressionCase;
|
||||
|
||||
const resultId = `m48-small-static-passage-regression-${"a".repeat(64)}`;
|
||||
const packId = `m48-object-quality-pack-${"b".repeat(64)}`;
|
||||
const referenceGraphLabResultId = `m47-reference-graph-lab-${"d".repeat(64)}`;
|
||||
const anchorId = `anchor-${"c".repeat(24)}`;
|
||||
const authority = {
|
||||
mode: "replay-simulated",
|
||||
@@ -40,6 +41,7 @@ test("M4.8R1 keeps pipeline identity and assisted evidence authority explicit",
|
||||
schema_version: "missioncore.m48-small-static-passage-regression-result-view/v1",
|
||||
result_id: resultId,
|
||||
pack_id: packId,
|
||||
reference_graph_lab_result_id: referenceGraphLabResultId,
|
||||
created_at_utc: "2026-08-24T12:00:00Z",
|
||||
run_label: "M4.8R1",
|
||||
pipeline_id: "m48-class-free-object-quality/v1",
|
||||
@@ -73,6 +75,7 @@ test("M4.8R1 keeps pipeline identity and assisted evidence authority explicit",
|
||||
});
|
||||
assert.equal(summary.pipelineId, "m48-class-free-object-quality/v1");
|
||||
assert.equal(summary.experimentId, "m48-small-static-passage-regression/v1");
|
||||
assert.equal(summary.referenceGraphLabResultId, referenceGraphLabResultId);
|
||||
assert.equal(summary.metrics.workerMissedAnchorCount, 14);
|
||||
assert.equal(summary.independentTruth, false);
|
||||
});
|
||||
@@ -86,6 +89,7 @@ test("M4.8R1 bounded case binds one exact assisted anchor and frozen objects", a
|
||||
anchor_id: anchorId,
|
||||
clip_id: "m48-clip-03",
|
||||
sequence: 256,
|
||||
anchor_extent_xyxy: [0.2, 0.3, 0.4, 0.7],
|
||||
requires_avoidance_or_clearance: true,
|
||||
worker_candidate_count: 1,
|
||||
best_iou: 0.01,
|
||||
@@ -98,6 +102,7 @@ test("M4.8R1 bounded case binds one exact assisted anchor and frozen objects", a
|
||||
}),
|
||||
});
|
||||
assert.equal(cases[0].outcome, "missed-assisted-anchor");
|
||||
assert.deepEqual(cases[0].anchorExtentXyxy, [0.2, 0.3, 0.4, 0.7]);
|
||||
|
||||
const item = await fetchM48SmallStaticRegressionCase(resultId, anchorId, {
|
||||
fetcher: async () => response({
|
||||
@@ -150,14 +155,26 @@ test("M4.8R1 bounded case binds one exact assisted anchor and frozen objects", a
|
||||
assert.equal(item.groundTruth, false);
|
||||
});
|
||||
|
||||
test("M4.8R1 visual reuses the held viewer and keeps manual boxes exact-frame only", () => {
|
||||
const visual = readFileSync(
|
||||
new URL("../src/workspaces/laboratory/M48SmallStaticRegressionVisual.tsx", import.meta.url),
|
||||
test("M4.8R1 deletes the third viewer and projects anchors through the canonical graph", () => {
|
||||
const deletedVisual = new URL(
|
||||
"../src/workspaces/laboratory/M48SmallStaticRegressionVisual.tsx",
|
||||
import.meta.url,
|
||||
);
|
||||
assert.equal(existsSync(deletedVisual), false);
|
||||
const evidence = readFileSync(
|
||||
new URL("../src/workspaces/laboratory/M48SmallStaticRegressionEvidence.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(visual, /<LaboratoryEvidenceViewer/);
|
||||
assert.match(visual, /<M48BlindClipPlayer/);
|
||||
assert.match(visual, /<M48EvidenceModeRail/);
|
||||
assert.match(visual, /firstSequence: sequence,[\s\S]*lastSequence: sequence/);
|
||||
assert.doesNotMatch(visual, /Rerun|MediaSource|setInterval/);
|
||||
const canonicalVisual = readFileSync(
|
||||
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(evidence, /fetchM47ReferenceGraphLab/);
|
||||
assert.match(evidence, /<M4ReplayThreatVisual/);
|
||||
assert.match(evidence, /sourceSequence: item\.sequence - 1/);
|
||||
assert.doesNotMatch(evidence, /M48BlindClipPlayer|M48EvidenceModeRail|Rerun|MediaSource/);
|
||||
assert.match(canonicalVisual, /reviewAnchors/);
|
||||
assert.match(canonicalVisual, /РУЧНОЙ ЯКОРЬ · ПРОПУСК WORKER 006/);
|
||||
assert.match(canonicalVisual, /showSpatialSemantic/);
|
||||
assert.match(canonicalVisual, /showRollingMap/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user