Show dataset YOLO output readiness

This commit is contained in:
2026-06-30 15:29:30 +08:00
parent 826027629b
commit b60fcc5112
6 changed files with 175 additions and 12 deletions

View File

@@ -115,6 +115,14 @@ type TrainingCurve = {
series: CurveSeries[];
};
type DatasetYoloOutputsPayload = {
bestWeight?: ResultItem;
artifacts: ResultItem[];
curves: TrainingCurve[];
predictions: ResultItem[];
heatmaps: ResultItem[];
};
type CoveragePayload = {
scripts_total: number;
user_scripts_total: number;
@@ -352,7 +360,7 @@ function useData() {
setRuntimeReadiness(readinessNext);
setCapabilities(capabilitiesNext);
setJobs(jobsNext);
setResults(resultsNext.slice(0, 80));
setResults(resultsNext.slice(0, 240));
setCurves(curvesNext.slice(0, 12));
setDatasets(datasetsNext);
const validationEntries: Array<[string, DatasetValidation]> = [];
@@ -444,6 +452,26 @@ function App() {
);
const selectedValidation = selectedDataset ? datasetValidations[selectedDataset.name] : undefined;
const selectedCurve = curves.find((curve) => curve.relative_path === selectedCurvePath) ?? curves[0];
const selectedYoloOutputs = useMemo<DatasetYoloOutputsPayload>(() => {
if (!selectedDataset) {
return { artifacts: [], curves: [], predictions: [], heatmaps: [] };
}
const prefixes = [
`var/custom_yolo_runs/${selectedDataset.name}`,
`var/custom_yolo_runs/${selectedDataset.name}_predict`,
`var/custom_yolo_runs/${selectedDataset.name}_heatmap`
];
const matches = (relativePath: string) => prefixes.some((prefix) => relativePath === prefix || relativePath.startsWith(`${prefix}/`));
const artifacts = results.filter((item) => matches(item.relative_path));
return {
bestWeight: artifacts.find((item) => item.relative_path.endsWith("/weights/best.pt")),
artifacts,
curves: curves.filter((curve) => matches(curve.relative_path)),
predictions: artifacts.filter((item) => item.role === "segmentation" && item.previewable),
heatmaps: artifacts.filter((item) => item.role === "heatmap" && item.previewable)
};
}, [curves, results, selectedDataset]);
const selectedYoloWeightReady = Boolean(selectedYoloOutputs.bestWeight);
function pickTask(next: string) {
setTaskType(next);
@@ -562,7 +590,7 @@ function App() {
function customYoloWeightPath(dataset: UploadedDataset) {
const expected = `var/custom_yolo_runs/${dataset.name}/weights/best.pt`;
return results.find((item) => item.relative_path === expected || item.relative_path.endsWith(`/custom_yolo_runs/${dataset.name}/weights/best.pt`))?.relative_path ?? expected;
return selectedYoloOutputs.bestWeight?.relative_path ?? results.find((item) => item.relative_path === expected || item.relative_path.endsWith(`/custom_yolo_runs/${dataset.name}/weights/best.pt`))?.relative_path ?? expected;
}
async function createSelectedYoloYaml() {
@@ -879,10 +907,10 @@ function App() {
<button className="iconButton" disabled={busy || !selectedValidation?.ready.yolo} onClick={startSelectedYoloTrain} title="启动自定义 YOLO 训练">
<Play size={18} />
</button>
<button className="iconButton" disabled={busy || !selectedDataset?.absolute_layout} onClick={startSelectedYoloPredict} title="使用自定义 best.pt 预测">
<button className="iconButton" disabled={busy || !selectedDataset?.absolute_layout || !selectedYoloWeightReady} onClick={startSelectedYoloPredict} title={selectedYoloWeightReady ? "使用自定义 best.pt 预测" : "best.pt 尚未生成"}>
<FileImage size={18} />
</button>
<button className="iconButton" disabled={busy || !selectedDataset?.absolute_layout} onClick={startSelectedYoloHeatmap} title="使用自定义 best.pt 生成热度图">
<button className="iconButton" disabled={busy || !selectedDataset?.absolute_layout || !selectedYoloWeightReady} onClick={startSelectedYoloHeatmap} title={selectedYoloWeightReady ? "使用自定义 best.pt 生成热度图" : "best.pt 尚未生成"}>
<Zap size={18} />
</button>
<FileImage size={22} />
@@ -929,6 +957,7 @@ function App() {
))}
</div>
{selectedValidation && <DatasetQuality validation={selectedValidation} />}
{selectedDataset && <DatasetYoloOutputs dataset={selectedDataset} outputs={selectedYoloOutputs} />}
</div>
</section>
@@ -1258,6 +1287,48 @@ function DatasetQuality({ validation }: { validation: DatasetValidation }) {
);
}
function DatasetYoloOutputs({ dataset, outputs }: { dataset: UploadedDataset; outputs: DatasetYoloOutputsPayload }) {
const previewItems = [...outputs.heatmaps.slice(0, 3), ...outputs.predictions.slice(0, 3)].slice(0, 6);
return (
<div className="datasetOutputBox">
<div className="qualityHead">
<strong>{dataset.name} · YOLO</strong>
<span>{outputs.bestWeight ? "BEST.PT READY" : "BEST.PT MISSING"}</span>
</div>
<div className="qualityStats">
<div><span>Weights</span><strong>{outputs.bestWeight ? 1 : 0}</strong></div>
<div><span>Predict</span><strong>{outputs.predictions.length}</strong></div>
<div><span>Heatmap</span><strong>{outputs.heatmaps.length}</strong></div>
<div><span>Curves</span><strong>{outputs.curves.length}</strong></div>
</div>
<div className="datasetOutputLinks">
{outputs.bestWeight && (
<a href={`${API_BASE}/api/artifacts/${outputs.bestWeight.relative_path}`} target="_blank" rel="noreferrer">
<span>best.pt</span>
<small>{formatBytes(outputs.bestWeight.size)}</small>
</a>
)}
{outputs.curves.slice(0, 2).map((curve) => (
<a key={curve.relative_path} href={`${API_BASE}/api/artifacts/${curve.relative_path}`} target="_blank" rel="noreferrer">
<span>{curve.name}</span>
<small>{curve.row_count} epochs</small>
</a>
))}
</div>
{!!previewItems.length && (
<div className="datasetOutputPreview">
{previewItems.map((item) => (
<a key={item.relative_path} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
<img src={`${API_BASE}/api/artifacts/${item.relative_path}`} alt={item.name} />
<span>{item.role}</span>
</a>
))}
</div>
)}
</div>
);
}
function CurvePanel({
curves,
selected,

View File

@@ -761,6 +761,16 @@ textarea {
background: #0b0d0b;
}
.datasetOutputBox {
display: grid;
gap: 10px;
margin-top: 12px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 7px;
background: #0b0d0b;
}
.qualityHead {
display: flex;
justify-content: space-between;
@@ -817,6 +827,65 @@ textarea {
border-color: rgba(240, 113, 103, 0.55);
}
.datasetOutputLinks {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.datasetOutputLinks a {
min-width: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
padding: 8px;
border-radius: 6px;
border: 1px solid var(--line);
background: #101310;
color: var(--ink);
text-decoration: none;
}
.datasetOutputLinks span,
.datasetOutputLinks small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.datasetOutputPreview {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.datasetOutputPreview a {
min-width: 0;
overflow: hidden;
border-radius: 6px;
border: 1px solid var(--line);
background: #101310;
color: var(--muted);
text-decoration: none;
}
.datasetOutputPreview img {
display: block;
width: 100%;
aspect-ratio: 1.35 / 1;
object-fit: cover;
background: #060806;
}
.datasetOutputPreview span {
display: block;
padding: 6px;
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.jobList, .resultList {
display: grid;
gap: 8px;
@@ -1201,6 +1270,8 @@ meter {
.opGrid,
.sampleStrip,
.datasetOutputLinks,
.datasetOutputPreview,
.taskCheckList,
.agentCheckList,
.qualityStats,