Split UI pages and add inference workflow

This commit is contained in:
2026-07-01 00:13:05 +08:00
parent f77ba2888c
commit dcd6f7fd41
6 changed files with 618 additions and 37 deletions

View File

@@ -213,6 +213,8 @@ checks YOLO txt labels and mask dimensions, and can generate a `dataset.yaml`
for the `yolo.train_custom` task. The selected upload dataset also exposes
direct YOLO custom train, predict, and heatmap actions; custom outputs are
written under `var/custom_yolo_runs` and are scanned by the results dashboard.
The detailed color-label to mask/txt conversion path is documented in
`docs/DATASET_PREPARATION.md`.
When a dataset is selected, the dataset panel shows its custom YOLO `best.pt`,
prediction previews, heatmap previews, and inline training curve previews.
Segmentation previews, YOLO heatmaps, and loss/metric artifacts are grouped on

View File

@@ -30,6 +30,7 @@ def evaluate_project() -> dict:
backend = settings.project_root / "backend" / "app" / "main.py"
jobs_backend = settings.project_root / "backend" / "app" / "jobs.py"
readme = settings.project_root / "README.md"
dataset_doc = settings.project_root / "docs" / "DATASET_PREPARATION.md"
results_service = settings.project_root / "backend" / "app" / "modules" / "results" / "service.py"
catalog = get_catalog()
coverage = get_coverage_report()
@@ -42,12 +43,26 @@ def evaluate_project() -> dict:
acceptance_text = (settings.project_root / "backend" / "app" / "acceptance.py").read_text(encoding="utf-8")
results_service_text = results_service.read_text(encoding="utf-8") if results_service.exists() else ""
readme_text = readme.read_text(encoding="utf-8") if readme.exists() else ""
dataset_doc_text = dataset_doc.read_text(encoding="utf-8") if dataset_doc.exists() else ""
expectations = {
"left_nav_dataset": "数据集" in frontend_text and "#datasets" in frontend_text,
"left_nav_dataset": "数据集" in frontend_text and '"datasets"' in frontend_text and "navigate(item.id)" in frontend_text,
"separated_pages_ui": "type PageId" in frontend_text
and "data-page-section" in frontend_text
and 'data-page={page}' in frontend_text
and '"inference"' in frontend_text
and '"results"' in frontend_text,
"upload_ui": "uploadDatasetFiles" in frontend_text and "labels" in frontend_text and "masks" in frontend_text,
"dataset_quality_ui": "DatasetQuality" in frontend_text and "generateSelectedYoloYaml" in frontend_text,
"dataset_preparation_doc": "DATASET_PREPARATION.md" in readme_text
and "彩色 label 到训练 mask" in dataset_doc_text
and "YOLO txt" in dataset_doc_text,
"uploaded_yolo_workflow_ui": "startSelectedYoloTrain" in frontend_text and "startSelectedYoloPredict" in frontend_text and "startSelectedYoloHeatmap" in frontend_text,
"trained_model_inference_ui": "modelWeightOptions" in frontend_text
and "selectedInferenceWeight" in frontend_text
and "startInferenceJob" in frontend_text
and "yolo.predict_custom" in frontend_text
and "yolo.heatmap_custom" in frontend_text,
"dataset_yolo_outputs_ui": "DatasetYoloOutputs" in frontend_text
and "selectedYoloOutputs" in frontend_text
and "BEST.PT READY" in frontend_text

View File

@@ -12,6 +12,9 @@ def test_evaluation_agent_returns_checks():
assert checks["real_workspace_acceptance"] is True
assert checks["real_train_acceptance"] is True
assert checks["single_epoch_curve_support"] is True
assert checks["separated_pages_ui"] is True
assert checks["trained_model_inference_ui"] is True
assert checks["dataset_preparation_doc"] is True
def test_validation_agent_lightweight(monkeypatch):

View File

@@ -0,0 +1,86 @@
# 数据集、Label、Mask 处理流程
本项目保留 `Seg/` 里的原始处理逻辑,并在网页端把上传数据集统一组织成:
```text
var/uploads/datasets/<dataset_name>/
images/ 原始图片或待推理图片
labels/ YOLO txt/json/yaml 标注
masks/ 语义分割灰度 mask 或彩色 label/mask
```
## 现有数据集样例
可以参考两个现有路径:
- `Seg/DataSet_Own/A_Ori``Seg/DataSet_Own/A_Label_Ori`
- `Seg/Seg_All_In_One_YoloModel/Yolo数据集构建/ORI``Label`
它们的核心要求是:原图和标注图用同一个文件 stem 配对。比如:
```text
images/sample_001.png
masks/sample_001.png
labels/sample_001.txt
```
如果彩色 label 带 `_label` 后缀,原脚本会在配对或转换时剥离这个后缀。
## 彩色 label 到训练 mask
`Seg/DataSet_Own/1. 图片预处理(内含使用手册)/4_deal_labels.py` 的流程是:
1. 从人工彩色 label 读取 RGB。
2. 用边缘检测、连通域检测、分水岭填充清理标注区域,输出 `*_label.png``*_pro_label_fold`
3.`Annotate_CLASSES``Annotate_PALETTE` 精确匹配颜色。
4. 背景写成 `0`,第一个类别从 `1` 开始写入灰度 GT mask。
5. 输出训练用 mask 到 `*_GT_label_fold`,默认后缀是 `_gtFine_labelTrainIds.png`
因此,新数据如果是 MMSeg/SegModel 语义分割训练,推荐准备:
```text
images/*.png
masks/*.png # 单通道灰度0 是背景1..N 是类别 id
```
如果手里只有彩色 label需要先按调色板转换成灰度 mask。新增类别时要同步维护
- `Annotate_CLASSES`
- `Annotate_PALETTE`
- `bg_PALETTE`
## 彩色 label 或 GT mask 到 YOLO txt
YOLO 分割训练使用 polygon txt。现有脚本路径
```text
Seg/Seg_All_In_One_YoloModel/Yolo数据集构建/2_Check_and_Gen_Txt_Label_sort_label.py
```
该脚本会读取 `ORI_GT_label_fold` 中的灰度 GT mask
- `0` 是背景;
- 非背景灰度值代表类别;
- 每个类别区域提取轮廓;
- 坐标按图片宽高归一化;
- 输出 `class_id x1 y1 x2 y2 ...``Data/labels/train``Data/labels/val`
网页端的上传数据集如果要直接训练 YOLO需要
```text
images/sample_001.png
labels/sample_001.txt
```
然后在数据集页生成 `dataset.yaml`,再启动 `yolo.train_custom`
## 新图片推理
如果只是对新图片做推理,不需要 mask 或 label
1. 在网页数据集页创建一个新数据集。
2. 把新图片上传到 `images`
3. 进入推理页,选择一个已经训练好的权重,例如 `best.pt`
4. 图片来源选择这个数据集,启动预测或热度图任务。
如果要把新图片加入训练,则必须额外提供同名的 `mask``label txt`,否则只能用于推理或人工复核。

View File

@@ -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,

View File

@@ -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));
}
}