Split UI pages and add inference workflow
This commit is contained in:
@@ -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
|
for the `yolo.train_custom` task. The selected upload dataset also exposes
|
||||||
direct YOLO custom train, predict, and heatmap actions; custom outputs are
|
direct YOLO custom train, predict, and heatmap actions; custom outputs are
|
||||||
written under `var/custom_yolo_runs` and are scanned by the results dashboard.
|
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`,
|
When a dataset is selected, the dataset panel shows its custom YOLO `best.pt`,
|
||||||
prediction previews, heatmap previews, and inline training curve previews.
|
prediction previews, heatmap previews, and inline training curve previews.
|
||||||
Segmentation previews, YOLO heatmaps, and loss/metric artifacts are grouped on
|
Segmentation previews, YOLO heatmaps, and loss/metric artifacts are grouped on
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ def evaluate_project() -> dict:
|
|||||||
backend = settings.project_root / "backend" / "app" / "main.py"
|
backend = settings.project_root / "backend" / "app" / "main.py"
|
||||||
jobs_backend = settings.project_root / "backend" / "app" / "jobs.py"
|
jobs_backend = settings.project_root / "backend" / "app" / "jobs.py"
|
||||||
readme = settings.project_root / "README.md"
|
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"
|
results_service = settings.project_root / "backend" / "app" / "modules" / "results" / "service.py"
|
||||||
catalog = get_catalog()
|
catalog = get_catalog()
|
||||||
coverage = get_coverage_report()
|
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")
|
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 ""
|
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 ""
|
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 = {
|
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,
|
"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_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,
|
"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
|
"dataset_yolo_outputs_ui": "DatasetYoloOutputs" in frontend_text
|
||||||
and "selectedYoloOutputs" in frontend_text
|
and "selectedYoloOutputs" in frontend_text
|
||||||
and "BEST.PT READY" in frontend_text
|
and "BEST.PT READY" in frontend_text
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ def test_evaluation_agent_returns_checks():
|
|||||||
assert checks["real_workspace_acceptance"] is True
|
assert checks["real_workspace_acceptance"] is True
|
||||||
assert checks["real_train_acceptance"] is True
|
assert checks["real_train_acceptance"] is True
|
||||||
assert checks["single_epoch_curve_support"] 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):
|
def test_validation_agent_lightweight(monkeypatch):
|
||||||
|
|||||||
86
docs/DATASET_PREPARATION.md
Normal file
86
docs/DATASET_PREPARATION.md
Normal 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`,否则只能用于推理或人工复核。
|
||||||
@@ -311,6 +311,34 @@ type ValidationAgentPayload = {
|
|||||||
checks: AgentCheck[];
|
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> {
|
async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const res = await fetch(`${API_BASE}${path}`, {
|
const res = await fetch(`${API_BASE}${path}`, {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -483,6 +511,7 @@ function JobProgressBar({ progress }: { progress?: JobProgress }) {
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, realTrainAcceptance, deepAcceptance, error, refresh } = useData();
|
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 [taskType, setTaskType] = useState("mock.echo");
|
||||||
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
|
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
|
||||||
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
|
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
|
||||||
@@ -501,12 +530,21 @@ function App() {
|
|||||||
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
|
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
|
||||||
const [weightVerification, setWeightVerification] = useState<WeightVerifyPayload | null>(null);
|
const [weightVerification, setWeightVerification] = useState<WeightVerifyPayload | null>(null);
|
||||||
const [agentBusy, setAgentBusy] = useState(false);
|
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);
|
const eventSourceRef = useRef<EventSource | null>(null);
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
eventSourceRef.current?.close();
|
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 runningCount = jobs.filter((job) => job.status === "running").length;
|
||||||
const successCount = jobs.filter((job) => job.status === "success").length;
|
const successCount = jobs.filter((job) => job.status === "success").length;
|
||||||
const failedCount = jobs.filter((job) => job.status === "failed").length;
|
const failedCount = jobs.filter((job) => job.status === "failed").length;
|
||||||
@@ -552,7 +590,7 @@ function App() {
|
|||||||
const selectedYoloWeightReady = Boolean(selectedYoloOutputs.bestWeight);
|
const selectedYoloWeightReady = Boolean(selectedYoloOutputs.bestWeight);
|
||||||
const availableGpus = gpus?.gpus ?? [];
|
const availableGpus = gpus?.gpus ?? [];
|
||||||
const condaEnvOptions = useMemo(() => ["auto", ...Array.from(new Set((condaEnvs?.envs ?? []).map((item) => item.name))).sort()], [condaEnvs]);
|
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 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 resultRoleOptions = useMemo(() => ["all", ...Array.from(new Set(results.map((item) => item.role ?? "artifact"))).sort()], [results]);
|
||||||
const filteredResults = useMemo(
|
const filteredResults = useMemo(
|
||||||
@@ -562,6 +600,63 @@ function App() {
|
|||||||
),
|
),
|
||||||
[resultFamilyFilter, resultRoleFilter, results]
|
[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) {
|
function pickTask(next: string) {
|
||||||
setTaskType(next);
|
setTaskType(next);
|
||||||
@@ -571,7 +666,7 @@ function App() {
|
|||||||
function pickDatasetTask(next: string) {
|
function pickDatasetTask(next: string) {
|
||||||
setTaskType(next);
|
setTaskType(next);
|
||||||
setParams(JSON.stringify(datasetParamsForTask(next), null, 2));
|
setParams(JSON.stringify(datasetParamsForTask(next), null, 2));
|
||||||
window.location.hash = "jobs";
|
navigate("training");
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleGpu(index: number) {
|
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 }))
|
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);
|
await inspectJob(job);
|
||||||
window.location.hash = "jobs";
|
navigate("training");
|
||||||
await refresh();
|
await refresh();
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
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 }))
|
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);
|
await inspectJob(job);
|
||||||
window.location.hash = "jobs";
|
navigate("inference");
|
||||||
await refresh();
|
await refresh();
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
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` }))
|
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);
|
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();
|
await refresh();
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
@@ -844,7 +979,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="shell">
|
<main className="shell" data-page={page}>
|
||||||
<aside className="rail">
|
<aside className="rail">
|
||||||
<div className="brand">
|
<div className="brand">
|
||||||
<div className="mark"><Layers3 size={24} /></div>
|
<div className="mark"><Layers3 size={24} /></div>
|
||||||
@@ -854,23 +989,21 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="#jobs"><Terminal size={18} />任务</a>
|
{pages.map((item) => (
|
||||||
<a href="#capabilities"><Gauge size={18} />能力</a>
|
<button key={item.id} type="button" className={page === item.id ? "active" : ""} onClick={() => navigate(item.id)}>
|
||||||
<a href="#datasets"><Boxes size={18} />数据集</a>
|
{item.icon}
|
||||||
<a href="#gpu"><Cpu size={18} />GPU</a>
|
<span>{item.label}</span>
|
||||||
<a href="#runtime"><ShieldCheck size={18} />环境</a>
|
</button>
|
||||||
<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>
|
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section className="workspace">
|
<section className="workspace">
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Segmentation Operations</p>
|
<p className="eyebrow">{activePage.eyebrow}</p>
|
||||||
<h1>训练、预测、分析与权重资产控制台</h1>
|
<h1>{activePage.title}</h1>
|
||||||
|
<span className="pageHint">{activePage.description}</span>
|
||||||
</div>
|
</div>
|
||||||
<button className="iconButton" onClick={refresh} title="刷新">
|
<button className="iconButton" onClick={refresh} title="刷新">
|
||||||
<RefreshCcw size={18} />
|
<RefreshCcw size={18} />
|
||||||
@@ -879,7 +1012,7 @@ function App() {
|
|||||||
|
|
||||||
{error && <div className="alert">{error}</div>}
|
{error && <div className="alert">{error}</div>}
|
||||||
|
|
||||||
<section className="metrics">
|
<section className="metrics" data-page-section="overview">
|
||||||
<div className="metric">
|
<div className="metric">
|
||||||
<Activity size={20} />
|
<Activity size={20} />
|
||||||
<span>运行中</span>
|
<span>运行中</span>
|
||||||
@@ -907,7 +1040,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="panel" id="capabilities">
|
<section className="panel" id="capabilities" data-page-section="overview">
|
||||||
<div className="panelHead">
|
<div className="panelHead">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Capability Matrix</p>
|
<p className="eyebrow">Capability Matrix</p>
|
||||||
@@ -949,7 +1082,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid two" id="jobs">
|
<section className="grid two" id="jobs" data-page-section="training">
|
||||||
<div className="panel taskPanel">
|
<div className="panel taskPanel">
|
||||||
<div className="panelHead">
|
<div className="panelHead">
|
||||||
<div>
|
<div>
|
||||||
@@ -1032,7 +1165,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid two" id="datasets">
|
<section className="grid two" id="datasets" data-page-section="datasets">
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<div className="panelHead">
|
<div className="panelHead">
|
||||||
<div>
|
<div>
|
||||||
@@ -1144,7 +1277,127 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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="panel">
|
||||||
<div className="panelHead">
|
<div className="panelHead">
|
||||||
<div>
|
<div>
|
||||||
@@ -1236,7 +1489,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid two" id="agents">
|
<section className="grid two" id="agents" data-page-section="agents">
|
||||||
<div className="panel agentPanel">
|
<div className="panel agentPanel">
|
||||||
<div className="panelHead">
|
<div className="panelHead">
|
||||||
<div>
|
<div>
|
||||||
@@ -1275,7 +1528,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid four">
|
<section className="grid four" data-page-section="system">
|
||||||
<div className="panel" id="gpu">
|
<div className="panel" id="gpu">
|
||||||
<div className="panelHead">
|
<div className="panelHead">
|
||||||
<div>
|
<div>
|
||||||
@@ -1351,7 +1604,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid three">
|
<section className="grid three" data-page-section="overview results">
|
||||||
<div className="panel insight">
|
<div className="panel insight">
|
||||||
<div className="panelHead">
|
<div className="panelHead">
|
||||||
<div>
|
<div>
|
||||||
@@ -1386,7 +1639,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid two">
|
<section className="grid two" data-page-section="training inference results">
|
||||||
<div className="panel logPanel">
|
<div className="panel logPanel">
|
||||||
<div className="panelHead">
|
<div className="panelHead">
|
||||||
<div>
|
<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({
|
function WeightPanel({
|
||||||
catalog,
|
catalog,
|
||||||
manifest,
|
manifest,
|
||||||
|
|||||||
@@ -89,21 +89,39 @@ nav {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a {
|
nav a,
|
||||||
|
nav button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
padding: 11px 10px;
|
padding: 11px 10px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a:hover {
|
nav a:hover,
|
||||||
|
nav button:hover,
|
||||||
|
nav button.active {
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
background: var(--panel-2);
|
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 {
|
.workspace {
|
||||||
padding: 26px;
|
padding: 26px;
|
||||||
}
|
}
|
||||||
@@ -137,6 +155,23 @@ h2 {
|
|||||||
font-size: 18px;
|
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 {
|
.iconButton, .primary {
|
||||||
height: 38px;
|
height: 38px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -529,7 +564,9 @@ textarea {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field input {
|
.field input,
|
||||||
|
.field select {
|
||||||
|
width: 100%;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -537,6 +574,145 @@ textarea {
|
|||||||
background: var(--field);
|
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 {
|
.datasetForm {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -1614,16 +1790,19 @@ meter {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a {
|
nav a,
|
||||||
|
nav button {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 10px 6px;
|
padding: 10px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a svg {
|
nav a svg,
|
||||||
|
nav button svg {
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a {
|
nav a,
|
||||||
|
nav button {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1656,10 +1835,14 @@ meter {
|
|||||||
.datasetOutputPreview,
|
.datasetOutputPreview,
|
||||||
.gpuPicker,
|
.gpuPicker,
|
||||||
.taskCheckList,
|
.taskCheckList,
|
||||||
.agentCheckList,
|
.agentCheckList,
|
||||||
.jobDetailGrid,
|
.jobDetailGrid,
|
||||||
.qualityStats,
|
.qualityStats,
|
||||||
.qualityChecks {
|
.qualityChecks,
|
||||||
|
.pipelineExample,
|
||||||
|
.pipelineSteps,
|
||||||
|
.pipelineStats,
|
||||||
|
.inferencePreview {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user