Split UI pages and add inference workflow
This commit is contained in:
@@ -311,6 +311,34 @@ type ValidationAgentPayload = {
|
||||
checks: AgentCheck[];
|
||||
};
|
||||
|
||||
type PageId = "overview" | "datasets" | "training" | "inference" | "results" | "system" | "agents";
|
||||
|
||||
type ModelWeightOption = {
|
||||
key: string;
|
||||
label: string;
|
||||
path: string;
|
||||
artifactPath: string;
|
||||
family: string;
|
||||
role: string;
|
||||
size: number;
|
||||
source: string;
|
||||
};
|
||||
|
||||
const pages: Array<{ id: PageId; label: string; eyebrow: string; title: string; description: string; icon: React.ReactNode }> = [
|
||||
{ id: "overview", label: "总览", eyebrow: "Operations Map", title: "分割平台运行总览", description: "能力矩阵、关键资产和最近产物集中看板。", icon: <Gauge size={18} /> },
|
||||
{ id: "datasets", label: "数据集", eyebrow: "Dataset Bench", title: "数据集、Label、Mask 工作台", description: "按数据集管理上传、配对校验、训练数据生成和样例预览。", icon: <Boxes size={18} /> },
|
||||
{ id: "training", label: "训练", eyebrow: "Training Queue", title: "训练任务与实时日志", description: "选择 SegModel、YOLO、MMSeg 或数据处理任务并跟踪进度。", icon: <Terminal size={18} /> },
|
||||
{ id: "inference", label: "推理", eyebrow: "Model Inference", title: "选择训练模型进行图片推理", description: "从已有 best.pt/last.pt/manifest 权重中选择模型,对上传图片或数据集运行预测与热度图。", icon: <FileImage size={18} /> },
|
||||
{ id: "results", label: "结果", eyebrow: "Result Studio", title: "分割结果、热度图与 Loss 曲线", description: "按模型族和产物类型浏览训练结果、推理图、热度图和 CSV 曲线。", icon: <BarChart3 size={18} /> },
|
||||
{ id: "system", label: "系统", eyebrow: "Runtime Control", title: "GPU、环境、权重与覆盖验收", description: "检查 GPU、conda 环境、权重 manifest、脚本覆盖和验收任务。", icon: <Cpu size={18} /> },
|
||||
{ id: "agents", label: "Agent", eyebrow: "Review Agents", title: "评价建议与验证 Agent", description: "持续审查网页平台覆盖度、运行状态和下一步风险。", icon: <Bot size={18} /> }
|
||||
];
|
||||
|
||||
function pageFromHash(): PageId {
|
||||
const raw = window.location.hash.replace(/^#/, "");
|
||||
return pages.some((item) => item.id === raw) ? (raw as PageId) : "overview";
|
||||
}
|
||||
|
||||
async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -483,6 +511,7 @@ function JobProgressBar({ progress }: { progress?: JobProgress }) {
|
||||
|
||||
function App() {
|
||||
const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, realTrainAcceptance, deepAcceptance, error, refresh } = useData();
|
||||
const [page, setPage] = useState<PageId>(pageFromHash);
|
||||
const [taskType, setTaskType] = useState("mock.echo");
|
||||
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
|
||||
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
|
||||
@@ -501,12 +530,21 @@ function App() {
|
||||
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
|
||||
const [weightVerification, setWeightVerification] = useState<WeightVerifyPayload | null>(null);
|
||||
const [agentBusy, setAgentBusy] = useState(false);
|
||||
const [selectedInferenceWeight, setSelectedInferenceWeight] = useState("");
|
||||
const [inferenceSourcePath, setInferenceSourcePath] = useState("");
|
||||
const [inferenceModelKey, setInferenceModelKey] = useState("YOLO11n-seg");
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
|
||||
useEffect(() => () => {
|
||||
eventSourceRef.current?.close();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const syncPage = () => setPage(pageFromHash());
|
||||
window.addEventListener("hashchange", syncPage);
|
||||
return () => window.removeEventListener("hashchange", syncPage);
|
||||
}, []);
|
||||
|
||||
const runningCount = jobs.filter((job) => job.status === "running").length;
|
||||
const successCount = jobs.filter((job) => job.status === "success").length;
|
||||
const failedCount = jobs.filter((job) => job.status === "failed").length;
|
||||
@@ -552,7 +590,7 @@ function App() {
|
||||
const selectedYoloWeightReady = Boolean(selectedYoloOutputs.bestWeight);
|
||||
const availableGpus = gpus?.gpus ?? [];
|
||||
const condaEnvOptions = useMemo(() => ["auto", ...Array.from(new Set((condaEnvs?.envs ?? []).map((item) => item.name))).sort()], [condaEnvs]);
|
||||
const selectedGpuDevice = selectedGpuIds.length ? selectedGpuIds.map((_, index) => index).join(",") : "cpu";
|
||||
const selectedGpuDevice = selectedGpuIds.length ? selectedGpuIds.join(",") : "cpu";
|
||||
const resultFamilyOptions = useMemo(() => ["all", ...Array.from(new Set(results.map((item) => item.family ?? "artifact"))).sort()], [results]);
|
||||
const resultRoleOptions = useMemo(() => ["all", ...Array.from(new Set(results.map((item) => item.role ?? "artifact"))).sort()], [results]);
|
||||
const filteredResults = useMemo(
|
||||
@@ -562,6 +600,63 @@ function App() {
|
||||
),
|
||||
[resultFamilyFilter, resultRoleFilter, results]
|
||||
);
|
||||
const modelWeightOptions = useMemo<ModelWeightOption[]>(() => {
|
||||
const options = new Map<string, ModelWeightOption>();
|
||||
for (const item of results) {
|
||||
if (item.role !== "weight" || !["pt", "pth", "onnx", "engine"].includes(item.kind)) continue;
|
||||
options.set(item.path, {
|
||||
key: item.path,
|
||||
label: `${item.family ?? "model"} · ${item.name}`,
|
||||
path: item.path,
|
||||
artifactPath: item.relative_path,
|
||||
family: item.family ?? "model",
|
||||
role: item.role ?? "weight",
|
||||
size: item.size,
|
||||
source: "result"
|
||||
});
|
||||
}
|
||||
for (const item of weightManifest?.files ?? []) {
|
||||
if (!/\.(pt|pth|onnx|engine)$/i.test(item.stored_path)) continue;
|
||||
options.set(item.stored_path, {
|
||||
key: item.stored_path,
|
||||
label: `${item.family} · ${fileName(item.source_path)}`,
|
||||
path: item.stored_path,
|
||||
artifactPath: item.stored_path,
|
||||
family: item.family,
|
||||
role: item.role,
|
||||
size: item.size,
|
||||
source: "manifest"
|
||||
});
|
||||
}
|
||||
return Array.from(options.values()).sort((a, b) => {
|
||||
const score = (item: ModelWeightOption) => (item.label.toLowerCase().includes("best.pt") ? 3 : item.label.toLowerCase().includes("last.pt") ? 2 : item.source === "result" ? 1 : 0);
|
||||
return score(b) - score(a) || a.label.localeCompare(b.label);
|
||||
});
|
||||
}, [results, weightManifest]);
|
||||
const activePage = pages.find((item) => item.id === page) ?? pages[0];
|
||||
const inferenceSource = inferenceSourcePath.trim() || selectedDataset?.absolute_layout?.images || "";
|
||||
const inferenceOutputs = useMemo(() => {
|
||||
const predictions = results.filter((item) => item.role === "segmentation" && item.previewable).slice(0, 8);
|
||||
const heatmaps = results.filter((item) => item.role === "heatmap" && item.previewable).slice(0, 8);
|
||||
return { predictions, heatmaps };
|
||||
}, [results]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedInferenceWeight && modelWeightOptions.length) {
|
||||
setSelectedInferenceWeight(modelWeightOptions[0].path);
|
||||
}
|
||||
}, [modelWeightOptions, selectedInferenceWeight]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedDatasetName && datasets.length) {
|
||||
setSelectedDatasetName(datasets[0].name);
|
||||
}
|
||||
}, [datasets, selectedDatasetName]);
|
||||
|
||||
function navigate(next: PageId) {
|
||||
setPage(next);
|
||||
window.location.hash = next;
|
||||
}
|
||||
|
||||
function pickTask(next: string) {
|
||||
setTaskType(next);
|
||||
@@ -571,7 +666,7 @@ function App() {
|
||||
function pickDatasetTask(next: string) {
|
||||
setTaskType(next);
|
||||
setParams(JSON.stringify(datasetParamsForTask(next), null, 2));
|
||||
window.location.hash = "jobs";
|
||||
navigate("training");
|
||||
}
|
||||
|
||||
function toggleGpu(index: number) {
|
||||
@@ -776,7 +871,7 @@ function App() {
|
||||
body: JSON.stringify(jobPayload("yolo.train_custom", { model: "YOLO11n-seg", data: generated.path, epochs: 10, imgsz: 640, batch: 1, workers: 0, device: selectedGpuDevice, project: "var/custom_yolo_runs", name: selectedDataset.name, exist_ok: true }))
|
||||
});
|
||||
await inspectJob(job);
|
||||
window.location.hash = "jobs";
|
||||
navigate("training");
|
||||
await refresh();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
@@ -792,7 +887,7 @@ function App() {
|
||||
body: JSON.stringify(jobPayload("yolo.predict_custom", { weights: customYoloWeightPath(selectedDataset), source: selectedDataset.absolute_layout.images, imgsz: 640, conf: 0.25, device: selectedGpuDevice, project: "var/custom_yolo_runs", name: `${selectedDataset.name}_predict`, exist_ok: true }))
|
||||
});
|
||||
await inspectJob(job);
|
||||
window.location.hash = "jobs";
|
||||
navigate("inference");
|
||||
await refresh();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
@@ -808,7 +903,47 @@ function App() {
|
||||
body: JSON.stringify(jobPayload("yolo.heatmap_custom", { weights: customYoloWeightPath(selectedDataset), source: selectedDataset.absolute_layout.images, model_key: "YOLO11n-seg", cam_method: "GradCAM", target_layers: "model.model.model[9]", limit: 3, device: selectedGpuDevice, project: "var/custom_yolo_runs", name: `${selectedDataset.name}_heatmap` }))
|
||||
});
|
||||
await inspectJob(job);
|
||||
window.location.hash = "jobs";
|
||||
navigate("inference");
|
||||
await refresh();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function startInferenceJob(kind: "predict" | "heatmap") {
|
||||
if (!selectedInferenceWeight || !inferenceSource) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const baseName = selectedDataset?.name ?? "manual_source";
|
||||
const jobType = kind === "predict" ? "yolo.predict_custom" : "yolo.heatmap_custom";
|
||||
const rawParams = kind === "predict"
|
||||
? {
|
||||
weights: selectedInferenceWeight,
|
||||
source: inferenceSource,
|
||||
imgsz: 640,
|
||||
conf: 0.25,
|
||||
device: selectedGpuDevice,
|
||||
project: "var/custom_yolo_runs",
|
||||
name: `${baseName}_model_infer`,
|
||||
exist_ok: true
|
||||
}
|
||||
: {
|
||||
weights: selectedInferenceWeight,
|
||||
source: inferenceSource,
|
||||
model_key: inferenceModelKey,
|
||||
cam_method: "GradCAM",
|
||||
target_layers: "model.model.model[9]",
|
||||
limit: 3,
|
||||
device: selectedGpuDevice,
|
||||
project: "var/custom_yolo_runs",
|
||||
name: `${baseName}_model_heatmap`
|
||||
};
|
||||
const job = await api<Job>("/api/jobs", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(jobPayload(jobType, rawParams))
|
||||
});
|
||||
await inspectJob(job);
|
||||
navigate("inference");
|
||||
await refresh();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
@@ -844,7 +979,7 @@ function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="shell">
|
||||
<main className="shell" data-page={page}>
|
||||
<aside className="rail">
|
||||
<div className="brand">
|
||||
<div className="mark"><Layers3 size={24} /></div>
|
||||
@@ -854,23 +989,21 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="#jobs"><Terminal size={18} />任务</a>
|
||||
<a href="#capabilities"><Gauge size={18} />能力</a>
|
||||
<a href="#datasets"><Boxes size={18} />数据集</a>
|
||||
<a href="#gpu"><Cpu size={18} />GPU</a>
|
||||
<a href="#runtime"><ShieldCheck size={18} />环境</a>
|
||||
<a href="#coverage"><ClipboardCheck size={18} />覆盖</a>
|
||||
<a href="#agents"><Bot size={18} />Agent</a>
|
||||
<a href="#weights"><HardDrive size={18} />权重</a>
|
||||
<a href="#results"><BarChart3 size={18} />结果</a>
|
||||
{pages.map((item) => (
|
||||
<button key={item.id} type="button" className={page === item.id ? "active" : ""} onClick={() => navigate(item.id)}>
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<section className="workspace">
|
||||
<header className="topbar">
|
||||
<div>
|
||||
<p className="eyebrow">Segmentation Operations</p>
|
||||
<h1>训练、预测、分析与权重资产控制台</h1>
|
||||
<p className="eyebrow">{activePage.eyebrow}</p>
|
||||
<h1>{activePage.title}</h1>
|
||||
<span className="pageHint">{activePage.description}</span>
|
||||
</div>
|
||||
<button className="iconButton" onClick={refresh} title="刷新">
|
||||
<RefreshCcw size={18} />
|
||||
@@ -879,7 +1012,7 @@ function App() {
|
||||
|
||||
{error && <div className="alert">{error}</div>}
|
||||
|
||||
<section className="metrics">
|
||||
<section className="metrics" data-page-section="overview">
|
||||
<div className="metric">
|
||||
<Activity size={20} />
|
||||
<span>运行中</span>
|
||||
@@ -907,7 +1040,7 @@ function App() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel" id="capabilities">
|
||||
<section className="panel" id="capabilities" data-page-section="overview">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Capability Matrix</p>
|
||||
@@ -949,7 +1082,7 @@ function App() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid two" id="jobs">
|
||||
<section className="grid two" id="jobs" data-page-section="training">
|
||||
<div className="panel taskPanel">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
@@ -1032,7 +1165,7 @@ function App() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid two" id="datasets">
|
||||
<section className="grid two" id="datasets" data-page-section="datasets">
|
||||
<div className="panel">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
@@ -1144,7 +1277,127 @@ function App() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid two" id="coverage">
|
||||
<section className="grid two" data-page-section="datasets">
|
||||
<DatasetPipelineGuide selectedDataset={selectedDataset} validation={selectedValidation} />
|
||||
<div className="panel">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Current Layout</p>
|
||||
<h2>当前选中数据集路径</h2>
|
||||
</div>
|
||||
<FileSearch size={22} />
|
||||
</div>
|
||||
{selectedDataset ? (
|
||||
<div className="pathStack">
|
||||
<div><span>images</span><code>{selectedDataset.absolute_layout?.images}</code></div>
|
||||
<div><span>labels</span><code>{selectedDataset.absolute_layout?.labels}</code></div>
|
||||
<div><span>masks</span><code>{selectedDataset.absolute_layout?.masks}</code></div>
|
||||
<div><span>pairing</span><code>同名 stem 配对,例如 sample.png + sample.txt + sample.png mask</code></div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="muted">先创建或选择一个上传数据集。</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid two" id="inference" data-page-section="inference">
|
||||
<div className="panel">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Model Picker</p>
|
||||
<h2>选择训练好的模型</h2>
|
||||
</div>
|
||||
<FileImage size={22} />
|
||||
</div>
|
||||
<div className="inferenceForm">
|
||||
<label className="field compact">
|
||||
<span>模型权重</span>
|
||||
<select value={selectedInferenceWeight} onChange={(event) => setSelectedInferenceWeight(event.target.value)} aria-label="trained model weight">
|
||||
{modelWeightOptions.length ? modelWeightOptions.map((item) => (
|
||||
<option key={item.key} value={item.path}>
|
||||
{item.label} · {formatBytes(item.size)} · {item.source}
|
||||
</option>
|
||||
)) : (
|
||||
<option value="">等待权重 manifest 或训练结果</option>
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="field compact">
|
||||
<span>图片来源</span>
|
||||
<select
|
||||
value={selectedDataset?.name ?? ""}
|
||||
onChange={(event) => {
|
||||
setSelectedDatasetName(event.target.value);
|
||||
setDatasetName(event.target.value);
|
||||
setInferenceSourcePath("");
|
||||
}}
|
||||
aria-label="inference dataset"
|
||||
>
|
||||
<option value="">手动填写图片路径</option>
|
||||
{datasets.map((dataset) => (
|
||||
<option key={dataset.name} value={dataset.name}>
|
||||
{dataset.name} · {dataset.counts.images} images
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="field compact">
|
||||
<span>图片文件或目录</span>
|
||||
<input
|
||||
value={inferenceSource}
|
||||
onChange={(event) => setInferenceSourcePath(event.target.value)}
|
||||
placeholder="选择数据集后自动使用 images 目录,也可填写绝对路径"
|
||||
/>
|
||||
</label>
|
||||
<label className="field compact">
|
||||
<span>热度图模型族</span>
|
||||
<select value={inferenceModelKey} onChange={(event) => setInferenceModelKey(event.target.value)} aria-label="heatmap model key">
|
||||
{(catalog?.yolo_models ?? ["YOLO11n-seg"]).map((item) => (
|
||||
<option key={item} value={item}>{item}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div className="buttonRow">
|
||||
<button className="primary" disabled={busy || !selectedInferenceWeight || !inferenceSource} onClick={() => startInferenceJob("predict")}>
|
||||
<FileImage size={17} />预测
|
||||
</button>
|
||||
<button className="primary secondary" disabled={busy || !selectedInferenceWeight || !inferenceSource} onClick={() => startInferenceJob("heatmap")}>
|
||||
<Zap size={17} />热度图
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modelList">
|
||||
{modelWeightOptions.slice(0, 10).map((item) => (
|
||||
<a key={item.key} href={`${API_BASE}/api/artifacts/${item.artifactPath}`} target="_blank" rel="noreferrer" className={selectedInferenceWeight === item.path ? "selected" : ""}>
|
||||
<span>{item.label}</span>
|
||||
<small>{item.family} · {item.role} · {formatBytes(item.size)}</small>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Inference Outputs</p>
|
||||
<h2>最近推理产物</h2>
|
||||
</div>
|
||||
<Wand2 size={22} />
|
||||
</div>
|
||||
<div className="inferencePreview">
|
||||
<div>
|
||||
<strong>分割结果</strong>
|
||||
<ResultPreview results={inferenceOutputs.predictions.slice(0, 4)} />
|
||||
</div>
|
||||
<div>
|
||||
<strong>热度图</strong>
|
||||
<ResultPreview results={inferenceOutputs.heatmaps.slice(0, 4)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid two" id="coverage" data-page-section="system">
|
||||
<div className="panel">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
@@ -1236,7 +1489,7 @@ function App() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid two" id="agents">
|
||||
<section className="grid two" id="agents" data-page-section="agents">
|
||||
<div className="panel agentPanel">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
@@ -1275,7 +1528,7 @@ function App() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid four">
|
||||
<section className="grid four" data-page-section="system">
|
||||
<div className="panel" id="gpu">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
@@ -1351,7 +1604,7 @@ function App() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid three">
|
||||
<section className="grid three" data-page-section="overview results">
|
||||
<div className="panel insight">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
@@ -1386,7 +1639,7 @@ function App() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid two">
|
||||
<section className="grid two" data-page-section="training inference results">
|
||||
<div className="panel logPanel">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
@@ -1458,6 +1711,45 @@ function JobDiagnostics({ job }: { job: Job }) {
|
||||
);
|
||||
}
|
||||
|
||||
function DatasetPipelineGuide({ selectedDataset, validation }: { selectedDataset?: UploadedDataset; validation?: DatasetValidation }) {
|
||||
const activeName = selectedDataset?.name ?? "未选择";
|
||||
return (
|
||||
<div className="panel pipelinePanel">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Label Pipeline</p>
|
||||
<h2>彩色 Label 到训练数据</h2>
|
||||
</div>
|
||||
<Database size={22} />
|
||||
</div>
|
||||
<div className="pipelineExample">
|
||||
<div>
|
||||
<span>现有样例</span>
|
||||
<strong>DataSet_Own/A_Ori + A_Label_Ori</strong>
|
||||
<small>同名图片先配对,再生成 A_Label_pro_label_fold 与 A_Label_GT_label_fold。</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>YOLO 样例</span>
|
||||
<strong>Seg_All_In_One_YoloModel/Yolo数据集构建</strong>
|
||||
<small>ORI/Label 生成 ORI_GT_label_fold,再输出 Data/images 与 Data/labels/*.txt。</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pipelineSteps">
|
||||
<div><span>1</span><strong>原图与彩色标注同名</strong><small>例如 `xxx.png` 对 `xxx.png`,或 label 后缀为 `_label` 时由配对脚本剥离后缀匹配。</small></div>
|
||||
<div><span>2</span><strong>彩色 label 先清理成 pro label</strong><small>`4_deal_labels.py` 使用边缘检测、连通域和分水岭填充,输出 `*_label.png`。</small></div>
|
||||
<div><span>3</span><strong>pro label 转训练 mask</strong><small>按 `Annotate_PALETTE` 精确匹配 RGB,背景为 0,类别从 1 开始写入灰度 GT mask。</small></div>
|
||||
<div><span>4</span><strong>YOLO 再转多边形 txt</strong><small>`2_Check_and_Gen_Txt_Label_sort_label.py` 从灰度 GT 提轮廓,写入归一化 polygon 坐标。</small></div>
|
||||
</div>
|
||||
<div className="pipelineStats">
|
||||
<div><span>当前数据集</span><strong>{activeName}</strong></div>
|
||||
<div><span>Image/Label</span><strong>{validation?.pairs.image_label ?? 0}</strong></div>
|
||||
<div><span>Image/Mask</span><strong>{validation?.pairs.image_mask ?? 0}</strong></div>
|
||||
<div><span>YOLO Ready</span><strong>{validation?.ready.yolo ? "OK" : "Check"}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WeightPanel({
|
||||
catalog,
|
||||
manifest,
|
||||
|
||||
@@ -89,21 +89,39 @@ nav {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
nav a {
|
||||
nav a,
|
||||
nav button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
color: var(--muted);
|
||||
padding: 11px 10px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
nav a:hover,
|
||||
nav button:hover,
|
||||
nav button.active {
|
||||
color: var(--ink);
|
||||
background: var(--panel-2);
|
||||
}
|
||||
|
||||
nav button.active {
|
||||
border: 1px solid rgba(157, 226, 111, 0.42);
|
||||
background: rgba(157, 226, 111, 0.08);
|
||||
}
|
||||
|
||||
nav button span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
padding: 26px;
|
||||
}
|
||||
@@ -137,6 +155,23 @@ h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.pageHint {
|
||||
display: block;
|
||||
margin-top: 7px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.shell[data-page="overview"] [data-page-section]:not([data-page-section~="overview"]),
|
||||
.shell[data-page="datasets"] [data-page-section]:not([data-page-section~="datasets"]),
|
||||
.shell[data-page="training"] [data-page-section]:not([data-page-section~="training"]),
|
||||
.shell[data-page="inference"] [data-page-section]:not([data-page-section~="inference"]),
|
||||
.shell[data-page="results"] [data-page-section]:not([data-page-section~="results"]),
|
||||
.shell[data-page="system"] [data-page-section]:not([data-page-section~="system"]),
|
||||
.shell[data-page="agents"] [data-page-section]:not([data-page-section~="agents"]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.iconButton, .primary {
|
||||
height: 38px;
|
||||
display: inline-flex;
|
||||
@@ -529,7 +564,9 @@ textarea {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.field input {
|
||||
.field input,
|
||||
.field select {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
@@ -537,6 +574,145 @@ textarea {
|
||||
background: var(--field);
|
||||
}
|
||||
|
||||
.pathStack,
|
||||
.inferenceForm,
|
||||
.modelList,
|
||||
.pipelinePanel,
|
||||
.pipelineSteps,
|
||||
.pipelineStats,
|
||||
.pipelineExample {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pathStack div {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.pathStack span,
|
||||
.pipelineExample span,
|
||||
.pipelineStats span {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.pathStack code {
|
||||
display: block;
|
||||
padding: 9px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: #080a08;
|
||||
color: var(--ink);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.pipelineExample {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.pipelineExample div,
|
||||
.pipelineSteps div,
|
||||
.pipelineStats div {
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
border-radius: 7px;
|
||||
border: 1px solid var(--line);
|
||||
background: #101310;
|
||||
}
|
||||
|
||||
.pipelineExample strong,
|
||||
.pipelineExample small,
|
||||
.pipelineSteps strong,
|
||||
.pipelineSteps small,
|
||||
.pipelineStats strong {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pipelineExample strong,
|
||||
.pipelineStats strong {
|
||||
margin-top: 3px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pipelineExample small,
|
||||
.pipelineSteps small {
|
||||
margin-top: 5px;
|
||||
color: var(--muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.pipelineSteps {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.pipelineSteps div {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pipelineSteps span {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 6px;
|
||||
background: var(--green);
|
||||
color: #0b0d0b;
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.pipelineStats {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.modelList {
|
||||
margin-top: 14px;
|
||||
max-height: 330px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modelList a {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 9px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--line);
|
||||
background: #101310;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.modelList a.selected {
|
||||
border-color: var(--green);
|
||||
background: rgba(157, 226, 111, 0.08);
|
||||
}
|
||||
|
||||
.modelList span,
|
||||
.modelList small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inferencePreview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.inferencePreview > div {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.datasetForm {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
@@ -1614,16 +1790,19 @@ meter {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
nav a,
|
||||
nav button {
|
||||
justify-content: center;
|
||||
padding: 10px 6px;
|
||||
}
|
||||
|
||||
nav a svg {
|
||||
nav a svg,
|
||||
nav button svg {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
nav a {
|
||||
nav a,
|
||||
nav button {
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
@@ -1656,10 +1835,14 @@ meter {
|
||||
.datasetOutputPreview,
|
||||
.gpuPicker,
|
||||
.taskCheckList,
|
||||
.agentCheckList,
|
||||
.jobDetailGrid,
|
||||
.qualityStats,
|
||||
.qualityChecks {
|
||||
.agentCheckList,
|
||||
.jobDetailGrid,
|
||||
.qualityStats,
|
||||
.qualityChecks,
|
||||
.pipelineExample,
|
||||
.pipelineSteps,
|
||||
.pipelineStats,
|
||||
.inferencePreview {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user