Add dataset bench and validation agents

This commit is contained in:
2026-06-30 12:38:25 +08:00
parent 69f9a8e29b
commit dd7b7384ec
16 changed files with 853 additions and 24 deletions

View File

@@ -3,8 +3,10 @@ import { createRoot } from "react-dom/client";
import {
Activity,
BarChart3,
Boxes,
Cpu,
Database,
FileImage,
FileSearch,
Gauge,
HardDrive,
@@ -15,11 +17,12 @@ import {
Square,
Terminal,
UploadCloud,
Wand2,
Zap
} from "lucide-react";
import "./styles.css";
const API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8000";
const API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8010";
type Job = {
id: string;
@@ -42,6 +45,22 @@ type Catalog = {
weights: { count: number; total_bytes: number; updated_at?: string };
};
type UploadedDataset = {
name: string;
description?: string;
counts: { images: number; labels: number; masks: number };
samples: Record<string, Array<{ name: string; relative_path: string; size: number; previewable: boolean }>>;
};
type ResultItem = {
name: string;
path: string;
relative_path: string;
size: number;
modified: number;
kind: string;
};
type GpuPayload = {
available: boolean;
gpus: Array<{
@@ -66,6 +85,13 @@ async function api<T>(path: string, init?: RequestInit): Promise<T> {
const defaultParams: Record<string, Record<string, unknown>> = {
"mock.echo": { message: "hello from Seg Data Server" },
"dataset.rename": { input_dir: "../DataSet_Own", prefix: "image" },
"dataset.to_png": { input_dir: "../DataSet_Own", output_dir: "../DataSet_Own_png" },
"dataset.resize": { input_dir: "../DataSet_Own", output_dir: "../DataSet_Own_resize", size: "512x512" },
"dataset.pair": { image_dir: "../DataSet_Own/images", label_dir: "../DataSet_Own/labels" },
"dataset.rebuild_labels": { label_dir: "../DataSet_Own/labels", output_dir: "../DataSet_Own/rebuilt_labels" },
"dataset.stack": { image_dir: "../DataSet_Own/images", mask_dir: "../DataSet_Own/masks", output_dir: "../DataSet_Own/stacked" },
"dataset.stitch": { input_dir: "../DataSet_Own/stacked", output_dir: "../DataSet_Own/stitch" },
"dataset.video_frames": { video: "../Seg_Predict_Own_Video_V2/LC_Video_1.mp4", interval: 0.5, resize: "1920x1080" },
"segmodel.train": { architecture: "Unet" },
"segmodel.predict": { architecture: "Unet", run_choice: 1 },
@@ -79,6 +105,17 @@ const defaultParams: Record<string, Record<string, unknown>> = {
"analysis.all": { input_dir: "../BestMode_Predict_Results_DataSet_Public", output_dir: "./", dataset_choice: 1 }
};
const taskLabels: Record<string, string> = {
"dataset.rename": "重命名",
"dataset.to_png": "转 PNG",
"dataset.resize": "Resize",
"dataset.pair": "图片/Label 配对",
"dataset.rebuild_labels": "重建 Label",
"dataset.stack": "透明叠加",
"dataset.stitch": "拼接检查",
"dataset.video_frames": "视频抽帧"
};
function formatBytes(value?: number) {
if (!value) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
@@ -95,21 +132,24 @@ function useData() {
const [catalog, setCatalog] = useState<Catalog | null>(null);
const [gpus, setGpus] = useState<GpuPayload | null>(null);
const [jobs, setJobs] = useState<Job[]>([]);
const [results, setResults] = useState<Array<Record<string, unknown>>>([]);
const [results, setResults] = useState<ResultItem[]>([]);
const [datasets, setDatasets] = useState<UploadedDataset[]>([]);
const [error, setError] = useState<string>("");
async function refresh() {
try {
const [catalogNext, gpusNext, jobsNext, resultsNext] = await Promise.all([
const [catalogNext, gpusNext, jobsNext, resultsNext, datasetsNext] = await Promise.all([
api<Catalog>("/api/catalog"),
api<GpuPayload>("/api/system/gpus"),
api<Job[]>("/api/jobs"),
api<Array<Record<string, unknown>>>("/api/results")
api<ResultItem[]>("/api/results"),
api<UploadedDataset[]>("/api/datasets")
]);
setCatalog(catalogNext);
setGpus(gpusNext);
setJobs(jobsNext);
setResults(resultsNext.slice(0, 80));
setDatasets(datasetsNext);
setError("");
} catch (err) {
setError(String(err));
@@ -122,7 +162,7 @@ function useData() {
return () => window.clearInterval(timer);
}, []);
return { catalog, gpus, jobs, results, error, refresh };
return { catalog, gpus, jobs, results, datasets, error, refresh };
}
function StatusPill({ status }: { status: string }) {
@@ -130,12 +170,16 @@ function StatusPill({ status }: { status: string }) {
}
function App() {
const { catalog, gpus, jobs, results, error, refresh } = useData();
const { catalog, gpus, jobs, results, datasets, error, refresh } = useData();
const [taskType, setTaskType] = useState("mock.echo");
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
const [log, setLog] = useState("");
const [busy, setBusy] = useState(false);
const [datasetName, setDatasetName] = useState("demo_dataset");
const [datasetDescription, setDatasetDescription] = useState("");
const [uploadKind, setUploadKind] = useState<"images" | "labels" | "masks">("images");
const [uploadFiles, setUploadFiles] = useState<FileList | null>(null);
const runningCount = jobs.filter((job) => job.status === "running").length;
const successCount = jobs.filter((job) => job.status === "success").length;
@@ -152,11 +196,18 @@ function App() {
};
}, [catalog]);
const datasetOps = taskGroups.dataset.filter((task) => task in taskLabels);
function pickTask(next: string) {
setTaskType(next);
setParams(JSON.stringify(defaultParams[next] ?? {}, null, 2));
}
function pickDatasetTask(next: string) {
pickTask(next);
window.location.hash = "jobs";
}
async function createJob() {
setBusy(true);
try {
@@ -183,6 +234,36 @@ function App() {
}
}
async function createDataset() {
setBusy(true);
try {
await api("/api/datasets", {
method: "POST",
body: JSON.stringify({ name: datasetName, description: datasetDescription })
});
await refresh();
} finally {
setBusy(false);
}
}
async function uploadDatasetFiles() {
if (!uploadFiles || uploadFiles.length === 0) return;
setBusy(true);
try {
const body = new FormData();
Array.from(uploadFiles).forEach((file) => body.append("files", file));
const res = await fetch(`${API_BASE}/api/datasets/${encodeURIComponent(datasetName)}/upload/${uploadKind}`, {
method: "POST",
body
});
if (!res.ok) throw new Error(await res.text());
await refresh();
} finally {
setBusy(false);
}
}
async function inspectJob(job: Job) {
const detail = await api<Job>(`/api/jobs/${job.id}`);
setSelectedJob(detail);
@@ -214,6 +295,7 @@ function App() {
</div>
<nav>
<a href="#jobs"><Terminal size={18} /></a>
<a href="#datasets"><Boxes size={18} /></a>
<a href="#gpu"><Cpu size={18} />GPU</a>
<a href="#weights"><HardDrive size={18} /></a>
<a href="#results"><BarChart3 size={18} /></a>
@@ -251,8 +333,8 @@ function App() {
</div>
<div className="metric">
<Database size={20} />
<span></span>
<strong>{catalog?.datasets.length ?? 0}</strong>
<span></span>
<strong>{datasets.length}</strong>
</div>
</section>
@@ -305,6 +387,82 @@ function App() {
</div>
</section>
<section className="grid two" id="datasets">
<div className="panel">
<div className="panelHead">
<div>
<p className="eyebrow">Dataset Bench</p>
<h2>LabelMask </h2>
</div>
<Database size={22} />
</div>
<div className="datasetForm">
<label className="field compact">
<span></span>
<input value={datasetName} onChange={(event) => setDatasetName(event.target.value)} />
</label>
<label className="field compact">
<span></span>
<input value={datasetDescription} onChange={(event) => setDatasetDescription(event.target.value)} />
</label>
<div className="segmented">
{(["images", "labels", "masks"] as const).map((kind) => (
<button key={kind} className={uploadKind === kind ? "active" : ""} onClick={() => setUploadKind(kind)}>
{kind}
</button>
))}
</div>
<label className="drop">
<UploadCloud size={24} />
<span>{uploadFiles?.length ? `${uploadFiles.length} files selected` : "选择图片、label 或 mask 文件"}</span>
<input multiple type="file" accept="image/*,.txt,.json,.yaml,.yml" onChange={(event) => setUploadFiles(event.target.files)} />
</label>
<div className="buttonRow">
<button className="primary" disabled={busy} onClick={createDataset}><Boxes size={17} /></button>
<button className="primary secondary" disabled={busy || !uploadFiles?.length} onClick={uploadDatasetFiles}><UploadCloud size={17} /></button>
</div>
<div className="opGrid">
{datasetOps.map((task) => (
<button key={task} type="button" onClick={() => pickDatasetTask(task)}>
<Wand2 size={16} />
<span>{taskLabels[task]}</span>
</button>
))}
</div>
</div>
</div>
<div className="panel">
<div className="panelHead">
<div>
<p className="eyebrow">Files</p>
<h2></h2>
</div>
<FileImage size={22} />
</div>
<div className="datasetList">
{datasets.map((dataset) => (
<div className="datasetCard" key={dataset.name}>
<div className="datasetCardHead">
<strong>{dataset.name}</strong>
<span>{dataset.counts.images} image · {dataset.counts.labels} label · {dataset.counts.masks} mask</span>
</div>
<div className="sampleStrip">
{["images", "labels", "masks"].flatMap((kind) =>
(dataset.samples[kind] ?? []).slice(0, 4).map((sample) => (
<a key={`${kind}-${sample.relative_path}`} href={`${API_BASE}/api/artifacts/${sample.relative_path}`} target="_blank" rel="noreferrer">
<span>{kind}</span>
<small>{sample.name}</small>
</a>
))
)}
</div>
</div>
))}
</div>
</div>
</section>
<section className="grid three">
<div className="panel" id="gpu">
<div className="panelHead">
@@ -356,6 +514,46 @@ function App() {
</div>
</section>
<section className="grid three">
<div className="panel insight">
<div className="panelHead">
<div>
<p className="eyebrow">Segmentation</p>
<h2></h2>
</div>
<Wand2 size={22} />
</div>
<ResultPreview results={results.filter((item) => /predict|mask|comparison|prediction/i.test(item.relative_path) && ["png", "jpg", "jpeg"].includes(item.kind)).slice(0, 6)} />
</div>
<div className="panel insight">
<div className="panelHead">
<div>
<p className="eyebrow">Heatmap</p>
<h2>YOLO </h2>
</div>
<Zap size={22} />
</div>
<ResultPreview results={results.filter((item) => /heat|cam|grad/i.test(item.relative_path) && ["png", "jpg", "jpeg"].includes(item.kind)).slice(0, 6)} />
</div>
<div className="panel insight">
<div className="panelHead">
<div>
<p className="eyebrow">Curves</p>
<h2>Loss / </h2>
</div>
<BarChart3 size={22} />
</div>
<div className="resultList tight">
{results.filter((item) => /loss|metric|miou|iou|csv|curve/i.test(item.relative_path)).slice(0, 10).map((item) => (
<a key={item.path} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
<span>{item.name}</span>
<small>{formatBytes(item.size)}</small>
</a>
))}
</div>
</div>
</section>
<section className="grid two">
<div className="panel logPanel">
<div className="panelHead">
@@ -380,9 +578,9 @@ function App() {
</div>
<div className="resultList">
{results.map((item) => (
<a key={String(item.path)} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
<span>{String(item.name)}</span>
<small>{formatBytes(Number(item.size))}</small>
<a key={item.path} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
<span>{item.name}</span>
<small>{formatBytes(item.size)}</small>
</a>
))}
</div>
@@ -393,6 +591,22 @@ function App() {
);
}
function ResultPreview({ results }: { results: ResultItem[] }) {
if (!results.length) {
return <p className="muted"></p>;
}
return (
<div className="previewGrid">
{results.map((item) => (
<a key={item.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.name}</span>
</a>
))}
</div>
);
}
createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />

View File

@@ -32,6 +32,11 @@ button, textarea, select {
font: inherit;
}
input {
font: inherit;
color: var(--ink);
}
button {
border: 0;
color: inherit;
@@ -154,6 +159,10 @@ h2 {
font-weight: 700;
}
.primary.secondary {
background: var(--cyan);
}
.primary:disabled, .iconButton:disabled {
opacity: 0.5;
cursor: not-allowed;
@@ -283,6 +292,159 @@ textarea {
font-size: 13px;
}
.field.compact {
margin-top: 0;
}
.field input {
height: 38px;
padding: 0 10px;
border-radius: 6px;
border: 1px solid var(--line);
background: var(--field);
}
.datasetForm {
display: grid;
gap: 12px;
}
.segmented {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
padding: 4px;
border: 1px solid var(--line);
background: #101310;
border-radius: 7px;
}
.segmented button {
height: 34px;
border-radius: 5px;
background: transparent;
color: var(--muted);
}
.segmented button.active {
background: var(--green);
color: #0b0d0b;
font-weight: 760;
}
.drop {
min-height: 118px;
display: grid;
place-items: center;
gap: 8px;
position: relative;
border: 1px dashed #526052;
border-radius: 8px;
background: rgba(13, 16, 13, 0.8);
color: var(--muted);
text-align: center;
overflow: hidden;
}
.drop input {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
}
.buttonRow {
display: flex;
gap: 10px;
}
.opGrid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.opGrid button {
min-width: 0;
height: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 0 8px;
border-radius: 6px;
border: 1px solid var(--line);
background: #101310;
color: var(--muted);
font-size: 12px;
}
.opGrid button:hover {
color: var(--ink);
border-color: var(--green);
}
.opGrid span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.datasetList {
display: grid;
gap: 10px;
max-height: 420px;
overflow: auto;
}
.datasetCard {
padding: 12px;
border: 1px solid var(--line);
border-radius: 7px;
background: #101310;
}
.datasetCardHead {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.datasetCardHead span {
color: var(--muted);
font-size: 12px;
}
.sampleStrip {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.sampleStrip a {
min-width: 0;
padding: 8px;
border-radius: 5px;
border: 1px solid var(--line);
text-decoration: none;
color: var(--ink);
background: #0b0d0b;
}
.sampleStrip span,
.sampleStrip small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sampleStrip span {
color: var(--green);
font-size: 11px;
}
.jobList, .resultList {
display: grid;
gap: 8px;
@@ -382,11 +544,53 @@ meter {
border-color: var(--green);
}
.resultList.tight {
max-height: 290px;
}
.insight {
min-height: 350px;
}
.previewGrid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.previewGrid a {
min-width: 0;
border: 1px solid var(--line);
border-radius: 7px;
overflow: hidden;
color: var(--ink);
text-decoration: none;
background: #0b0d0b;
}
.previewGrid img {
display: block;
width: 100%;
aspect-ratio: 16 / 10;
object-fit: cover;
background: #060806;
}
.previewGrid span {
display: block;
padding: 8px;
color: var(--muted);
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 1180px) {
body { min-width: 960px; }
.shell { grid-template-columns: 220px 1fr; }
.taskColumns { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.opGrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid.three { grid-template-columns: 1fr; }
.grid.two { grid-template-columns: 1fr; }
}