refactor(ui): reuse canonical graph for M4.8 regression
This commit is contained in:
@@ -184,6 +184,7 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.m4-replay-threat-visual__review-controls,
|
||||||
.m4-replay-threat-visual__pane-layer-controls,
|
.m4-replay-threat-visual__pane-layer-controls,
|
||||||
.m4-replay-threat-visual__single-pane-controls {
|
.m4-replay-threat-visual__single-pane-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -195,11 +196,14 @@
|
|||||||
scrollbar-width: none;
|
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,
|
||||||
.m4-replay-threat-visual__single-pane-controls > * {
|
.m4-replay-threat-visual__single-pane-controls > * {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.m4-replay-threat-visual__review-controls::-webkit-scrollbar,
|
||||||
.m4-replay-threat-visual__pane-layer-controls::-webkit-scrollbar,
|
.m4-replay-threat-visual__pane-layer-controls::-webkit-scrollbar,
|
||||||
.m4-replay-threat-visual__single-pane-controls::-webkit-scrollbar {
|
.m4-replay-threat-visual__single-pane-controls::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
+6
-5
@@ -5,7 +5,7 @@ import {
|
|||||||
LaboratoryWorkTemplate,
|
LaboratoryWorkTemplate,
|
||||||
} from "../../components/laboratory/LaboratoryPresentation";
|
} from "../../components/laboratory/LaboratoryPresentation";
|
||||||
import type { M48SmallStaticRegressionResult } from "../../core/laboratory/m48SmallStaticRegression";
|
import type { M48SmallStaticRegressionResult } from "../../core/laboratory/m48SmallStaticRegression";
|
||||||
import { M48SmallStaticRegressionVisual } from "./M48SmallStaticRegressionVisual";
|
import { M48SmallStaticRegressionEvidence } from "./M48SmallStaticRegressionEvidence";
|
||||||
|
|
||||||
function percent(value: number): string {
|
function percent(value: number): string {
|
||||||
return `${(value * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
|
return `${(value * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
|
||||||
@@ -30,7 +30,7 @@ export function M48SmallStaticPassageRegressionResultView({
|
|||||||
status={status}
|
status={status}
|
||||||
statusTone={result.accepted ? "success" : "warning"}
|
statusTone={result.accepted ? "success" : "warning"}
|
||||||
facts={[
|
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.pipelineId },
|
||||||
{ label: "Эксперимент", value: result.experimentId },
|
{ label: "Эксперимент", value: result.experimentId },
|
||||||
{ label: "Прогон", value: `${result.runLabel} · immutable ${result.resultId}` },
|
{ label: "Прогон", value: `${result.runLabel} · immutable ${result.resultId}` },
|
||||||
@@ -47,7 +47,8 @@ export function M48SmallStaticPassageRegressionResultView({
|
|||||||
executionClass: "deterministic",
|
executionClass: "deterministic",
|
||||||
pipelineId: result.pipelineId,
|
pipelineId: result.pipelineId,
|
||||||
components: [
|
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: "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 },
|
{ 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={(
|
evidence={(
|
||||||
<LaboratoryEvidence eyebrow="M4.8R1 VISUAL EVIDENCE · ASSISTED ANCHOR + WORKER" title="Точный кадр: ручной якорь и frozen-ответ Worker 006" kind="recorded-replay" resizable>
|
<LaboratoryEvidence eyebrow="M4.8R1 VISUAL EVIDENCE · CANONICAL REFERENCE GRAPH" title="Полный граф и 14 точных assisted-якорей на общем recorded timeline" kind="recorded-replay" resizable>
|
||||||
<M48SmallStaticRegressionVisual resultId={result.resultId} />
|
<M48SmallStaticRegressionEvidence result={result} />
|
||||||
</LaboratoryEvidence>
|
</LaboratoryEvidence>
|
||||||
)}
|
)}
|
||||||
result={(
|
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,
|
Button,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
Select,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
SplitPane,
|
SplitPane,
|
||||||
type SplitPaneOrientation,
|
type SplitPaneOrientation,
|
||||||
@@ -90,12 +91,23 @@ export interface M4ReplayThreatSemanticLayer {
|
|||||||
taxonomy: readonly E47SemanticClass[];
|
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({
|
export function M4ReplayThreatVisual({
|
||||||
resultId,
|
resultId,
|
||||||
semantic,
|
semantic,
|
||||||
|
reviewAnchors = EMPTY_REVIEW_ANCHORS,
|
||||||
}: {
|
}: {
|
||||||
resultId: string;
|
resultId: string;
|
||||||
semantic?: M4ReplayThreatSemanticLayer;
|
semantic?: M4ReplayThreatSemanticLayer;
|
||||||
|
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
|
||||||
}) {
|
}) {
|
||||||
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
|
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
|
||||||
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(null);
|
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(null);
|
||||||
@@ -111,6 +123,7 @@ export function M4ReplayThreatVisual({
|
|||||||
: "vertical"
|
: "vertical"
|
||||||
));
|
));
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0);
|
||||||
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
|
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
|
||||||
const metadata = useM4ThreatTimelineMetadata(resultId);
|
const metadata = useM4ThreatTimelineMetadata(resultId);
|
||||||
const playbackRange = useMemo(() => metadata.timeline ? ({
|
const playbackRange = useMemo(() => metadata.timeline ? ({
|
||||||
@@ -120,6 +133,8 @@ export function M4ReplayThreatVisual({
|
|||||||
const playbackController = useRecordedEvidencePlayback(playbackRange, {
|
const playbackController = useRecordedEvidencePlayback(playbackRange, {
|
||||||
clock: mediaMode === "video" ? "external" : "animation",
|
clock: mediaMode === "video" ? "external" : "animation",
|
||||||
});
|
});
|
||||||
|
const seekPlayback = playbackController.seek;
|
||||||
|
const setPlaybackPlaying = playbackController.setPlaying;
|
||||||
const timelineFrame = useM4ThreatTimelineFrame({
|
const timelineFrame = useM4ThreatTimelineFrame({
|
||||||
resultId,
|
resultId,
|
||||||
timeline: metadata.timeline,
|
timeline: metadata.timeline,
|
||||||
@@ -209,7 +224,50 @@ export function M4ReplayThreatVisual({
|
|||||||
&& timelineFrame.activeSequence !== null
|
&& timelineFrame.activeSequence !== null
|
||||||
&& frame.sequence !== timelineFrame.activeSequence,
|
&& 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[]>(
|
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
|
||||||
() => semantic?.taxonomy.map((item) => ({
|
() => semantic?.taxonomy.map((item) => ({
|
||||||
id: item.classId,
|
id: item.classId,
|
||||||
@@ -429,11 +487,54 @@ export function M4ReplayThreatVisual({
|
|||||||
? spatialMode ? spatialLayerControls : mediaMode ? mediaLayerControls : null
|
? spatialMode ? spatialLayerControls : mediaMode ? mediaLayerControls : null
|
||||||
: 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="l3-visual-audit__actions">
|
||||||
<div className="m4-replay-threat-visual__single-pane-controls">
|
{reviewControls}
|
||||||
{singlePaneControls}
|
{singlePaneControls ? (
|
||||||
</div>
|
<div className="m4-replay-threat-visual__single-pane-controls">
|
||||||
|
{singlePaneControls}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
|
|||||||
},
|
},
|
||||||
"m48-small-static-passage-regression": {
|
"m48-small-static-passage-regression": {
|
||||||
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
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",
|
experimentId: "m48-small-static-passage-regression",
|
||||||
experimentName: "M4.8 · small static passage regression",
|
experimentName: "M4.8 · small static passage regression",
|
||||||
variantName: "M4.8R1 · Worker 006 small-static assisted baseline",
|
variantName: "M4.8R1 · Worker 006 small-static assisted baseline",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import assert from "node:assert/strict";
|
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 { after, before, test } from "node:test";
|
||||||
import { createServer } from "vite";
|
import { createServer } from "vite";
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ let fetchM48SmallStaticRegressionCase;
|
|||||||
|
|
||||||
const resultId = `m48-small-static-passage-regression-${"a".repeat(64)}`;
|
const resultId = `m48-small-static-passage-regression-${"a".repeat(64)}`;
|
||||||
const packId = `m48-object-quality-pack-${"b".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 anchorId = `anchor-${"c".repeat(24)}`;
|
||||||
const authority = {
|
const authority = {
|
||||||
mode: "replay-simulated",
|
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",
|
schema_version: "missioncore.m48-small-static-passage-regression-result-view/v1",
|
||||||
result_id: resultId,
|
result_id: resultId,
|
||||||
pack_id: packId,
|
pack_id: packId,
|
||||||
|
reference_graph_lab_result_id: referenceGraphLabResultId,
|
||||||
created_at_utc: "2026-08-24T12:00:00Z",
|
created_at_utc: "2026-08-24T12:00:00Z",
|
||||||
run_label: "M4.8R1",
|
run_label: "M4.8R1",
|
||||||
pipeline_id: "m48-class-free-object-quality/v1",
|
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.pipelineId, "m48-class-free-object-quality/v1");
|
||||||
assert.equal(summary.experimentId, "m48-small-static-passage-regression/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.metrics.workerMissedAnchorCount, 14);
|
||||||
assert.equal(summary.independentTruth, false);
|
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,
|
anchor_id: anchorId,
|
||||||
clip_id: "m48-clip-03",
|
clip_id: "m48-clip-03",
|
||||||
sequence: 256,
|
sequence: 256,
|
||||||
|
anchor_extent_xyxy: [0.2, 0.3, 0.4, 0.7],
|
||||||
requires_avoidance_or_clearance: true,
|
requires_avoidance_or_clearance: true,
|
||||||
worker_candidate_count: 1,
|
worker_candidate_count: 1,
|
||||||
best_iou: 0.01,
|
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.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, {
|
const item = await fetchM48SmallStaticRegressionCase(resultId, anchorId, {
|
||||||
fetcher: async () => response({
|
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);
|
assert.equal(item.groundTruth, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("M4.8R1 visual reuses the held viewer and keeps manual boxes exact-frame only", () => {
|
test("M4.8R1 deletes the third viewer and projects anchors through the canonical graph", () => {
|
||||||
const visual = readFileSync(
|
const deletedVisual = new URL(
|
||||||
new URL("../src/workspaces/laboratory/M48SmallStaticRegressionVisual.tsx", import.meta.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",
|
"utf8",
|
||||||
);
|
);
|
||||||
assert.match(visual, /<LaboratoryEvidenceViewer/);
|
const canonicalVisual = readFileSync(
|
||||||
assert.match(visual, /<M48BlindClipPlayer/);
|
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
|
||||||
assert.match(visual, /<M48EvidenceModeRail/);
|
"utf8",
|
||||||
assert.match(visual, /firstSequence: sequence,[\s\S]*lastSequence: sequence/);
|
);
|
||||||
assert.doesNotMatch(visual, /Rerun|MediaSource|setInterval/);
|
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/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -160,6 +160,29 @@ history of rejected approaches belong in Ops.
|
|||||||
|
|
||||||
Primary visual evidence is hosted in one reusable viewer frame.
|
Primary visual evidence is hosted in one reusable viewer frame.
|
||||||
|
|
||||||
|
### Frozen Milestone 4 perception instruments
|
||||||
|
|
||||||
|
Milestone 4 admits exactly two operator instruments inside the shared LAB page
|
||||||
|
template:
|
||||||
|
|
||||||
|
1. the M4.8 assisted-correction instrument for editing frozen Worker 006
|
||||||
|
proposals and adding exact-frame misses;
|
||||||
|
2. the M4.7 Canonical Reference Graph instrument for synchronized
|
||||||
|
`VIDEO/CAMERA/3D/PLAN/SEMANTICS`, Worker proposals, current LiDAR, bounded
|
||||||
|
local SLAM, rolling occupancy and metric-obstacle inspection.
|
||||||
|
|
||||||
|
A later M4.8 regression or quality run supplies typed case navigation, manual
|
||||||
|
anchor overlays and result copy to one of these instruments. It does not create
|
||||||
|
an experiment-named player, viewer, Rerun configuration, timeline, splitter,
|
||||||
|
spatial renderer or loading grammar. A manual exact-frame annotation is shown
|
||||||
|
only on the annotated frame unless a separately evidenced and reviewable
|
||||||
|
tracking result exists; ordinary model proposals and graph layers continue on
|
||||||
|
their own full recorded timeline.
|
||||||
|
|
||||||
|
A third perception instrument is a product-surface change. It requires explicit
|
||||||
|
product-owner agreement before implementation and cannot be introduced by a LAB
|
||||||
|
adapter, result component or experiment configuration.
|
||||||
|
|
||||||
The viewer frame must:
|
The viewer frame must:
|
||||||
|
|
||||||
- provide the canonical expand/restore `IconButton`;
|
- provide the canonical expand/restore `IconButton`;
|
||||||
|
|||||||
@@ -1384,12 +1384,66 @@ clearance and passability remain functions of admitted LiDAR/local occupancy,
|
|||||||
uncertainty and the configured vehicle footprint. Physical live, navigation,
|
uncertainty and the configured vehicle footprint. Physical live, navigation,
|
||||||
commands, actuation and collision-safety authority remain false.
|
commands, actuation and collision-safety authority remain false.
|
||||||
|
|
||||||
The regression reuses the held M4.8 recorded viewer and its CAMERA/3D/PLAN,
|
The regression reuses the accepted M4.7 Canonical Reference Graph instrument,
|
||||||
single media clock, timeline and bounded spatial cache. Each assisted anchor and
|
not an experiment-local M4.8 viewer. It therefore retains the complete
|
||||||
the frozen Worker objects are shown only on their exact source frame; the UI does
|
VIDEO/CAMERA/3D/PLAN/SEMANTICS graph, one media clock, Worker proposals, current
|
||||||
not drift a manually drawn rectangle across subsequent frames or fabricate a
|
LiDAR, bounded local SLAM, rolling occupancy and metric obstacles. Each assisted
|
||||||
track. A later Worker candidate must publish another immutable M4.8R run against
|
anchor is an additional exact-frame overlay and is shown only on its source
|
||||||
the frozen seed, leaving this baseline available for before/after comparison.
|
frame; the UI does not drift a manually drawn rectangle across subsequent
|
||||||
|
frames or fabricate a track. A later Worker candidate must publish another
|
||||||
|
immutable M4.8R run against the frozen seed, leaving this baseline available for
|
||||||
|
before/after comparison.
|
||||||
|
|
||||||
|
### 2026-08-25 — perception interface freeze and dataset/training boundary
|
||||||
|
|
||||||
|
Mission Core now has two and only two admitted Milestone 4 perception
|
||||||
|
instruments:
|
||||||
|
|
||||||
|
1. the M4.8 assisted-correction instrument used to accept, edit, delete and add
|
||||||
|
class-free exact-frame boxes over frozen Worker 006 proposals;
|
||||||
|
2. the M4.7 Canonical Reference Graph instrument used to inspect detector,
|
||||||
|
camera, semantic, LiDAR, local-SLAM, rolling and threat evidence on one full
|
||||||
|
recorded timeline.
|
||||||
|
|
||||||
|
`M48SmallStaticRegressionVisual` is removed. M4.8R results bind to the immutable
|
||||||
|
M4.7 LAB identity stored in their source pack and project only typed regression
|
||||||
|
anchors and navigation into the second instrument. Creating another player or
|
||||||
|
viewer for an experiment is prohibited unless the product owner first approves
|
||||||
|
a third reusable instrument. This is enforced by the M4.8R frontend contract
|
||||||
|
test as well as the product UI canon.
|
||||||
|
|
||||||
|
Worker 006 remains an inference-serving boundary. Its frozen `5,236` M4.8 boxes
|
||||||
|
are model output, not an online-learning state. `0/14` in M4.8R1 means that none
|
||||||
|
of the fourteen operator-added miss anchors reached IoU `0.50` against those
|
||||||
|
frozen boxes; it does not mean that Worker 006 emitted no detections.
|
||||||
|
|
||||||
|
The assisted-correction instrument is a valid annotation front end but is not,
|
||||||
|
by itself, a train-ready dataset system. A released camera-obstacle dataset must
|
||||||
|
add the following immutable control plane around its corrections:
|
||||||
|
|
||||||
|
- exact source frame, raw/corrected optical representation, calibration,
|
||||||
|
preprocessing and frozen model identities;
|
||||||
|
- explicit annotation completeness and negative-frame state rather than
|
||||||
|
treating absence of a box as background truth;
|
||||||
|
- revisioned reviewer and adjudication identity, with candidate-assisted data
|
||||||
|
kept distinct from independent truth;
|
||||||
|
- route/time-block train, development and sealed validation partitions to
|
||||||
|
prevent adjacent-frame leakage;
|
||||||
|
- one versioned task taxonomy. The first task is class-free
|
||||||
|
`obstacle-presence`; `critical_corridor_obstacle` remains a planning/clearance
|
||||||
|
attribute and does not become a detector-class-specific collider rule;
|
||||||
|
- a content-addressed internal manifest plus COCO detection export and hashes.
|
||||||
|
A lossy YOLO text folder is not the source of truth.
|
||||||
|
|
||||||
|
The already deployed CVAT `v2.70.0` remains the offline review/export control
|
||||||
|
plane behind this contract; it is not another Mission Core evidence viewer.
|
||||||
|
Mission Core owns the two product instruments and immutable LAB publication.
|
||||||
|
Training runs in a separate bounded Worker job/container, initially reusing the
|
||||||
|
current YOLOX/PyTorch family, and may not mutate the serving Triton repository
|
||||||
|
or the frozen Worker 006 process in place. A successful job seals weights,
|
||||||
|
configuration, dataset release, metrics and ONNX identity; only then is a
|
||||||
|
candidate loaded through the existing Triton seam and measured by a new
|
||||||
|
append-only M4.8R run plus an independent validation gate.
|
||||||
|
|
||||||
## Implementation order
|
## Implementation order
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user