Add dataset bench and validation agents
This commit is contained in:
@@ -3,7 +3,7 @@ SEG_DATA_SERVER_ROOT=.
|
|||||||
SEG_BACKEND_DB=var/seg_data_server.sqlite3
|
SEG_BACKEND_DB=var/seg_data_server.sqlite3
|
||||||
SEG_BACKEND_LOG_DIR=var/job_logs
|
SEG_BACKEND_LOG_DIR=var/job_logs
|
||||||
SEG_TASK_CONDA_ENV=seg_smp
|
SEG_TASK_CONDA_ENV=seg_smp
|
||||||
SEG_BACKEND_CONDA_ENV=seg_server
|
SEG_BACKEND_CONDA_ENV=seg_smp
|
||||||
SEG_WEIGHT_MODE=copy
|
SEG_WEIGHT_MODE=copy
|
||||||
SEG_ENABLE_SHELL_TASKS=1
|
SEG_ENABLE_SHELL_TASKS=1
|
||||||
VITE_API_BASE=http://localhost:8000
|
VITE_API_BASE=http://localhost:8010
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -27,8 +27,9 @@ Seg_Data_Server_Net/
|
|||||||
cd Seg_Data_Server_Net
|
cd Seg_Data_Server_Net
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
|
||||||
# Backend. The existing machine already has a seg_server env with FastAPI.
|
# Backend. The deployment env is seg_smp so the API and task wrappers share
|
||||||
conda run -n seg_server uvicorn app.main:app --app-dir backend --host 0.0.0.0 --port 8000
|
# the same segmentation dependency stack.
|
||||||
|
conda run -n seg_smp uvicorn app.main:app --app-dir backend --host 0.0.0.0 --port 8010
|
||||||
|
|
||||||
# Frontend.
|
# Frontend.
|
||||||
cd frontend
|
cd frontend
|
||||||
@@ -37,7 +38,13 @@ npm run dev -- --host 0.0.0.0
|
|||||||
```
|
```
|
||||||
|
|
||||||
Open the Vite URL shown in the terminal. The frontend expects the backend at
|
Open the Vite URL shown in the terminal. The frontend expects the backend at
|
||||||
`http://localhost:8000` by default.
|
`http://localhost:8010` by default.
|
||||||
|
|
||||||
|
The web UI includes a dataset bench for creating upload workspaces, uploading
|
||||||
|
images/labels/masks, and jumping into the existing rename, PNG conversion,
|
||||||
|
resize, pair-check, label rebuild, transparent overlay, stitch, and video-frame
|
||||||
|
jobs. Segmentation previews, YOLO heatmaps, and loss/metric artifacts are
|
||||||
|
grouped on the results dashboard.
|
||||||
|
|
||||||
## Weight Sync
|
## Weight Sync
|
||||||
|
|
||||||
@@ -77,3 +84,15 @@ The backend exposes all current Seg capabilities as job types. Examples:
|
|||||||
|
|
||||||
Use `GET /api/catalog` to inspect supported models, algorithms, datasets, and
|
Use `GET /api/catalog` to inspect supported models, algorithms, datasets, and
|
||||||
task types discovered from the existing `Seg/` workspace.
|
task types discovered from the existing `Seg/` workspace.
|
||||||
|
|
||||||
|
## Agents
|
||||||
|
|
||||||
|
Run the local evaluation and validation agents before publishing changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=backend conda run -n seg_smp python scripts/run_agents.py --build
|
||||||
|
```
|
||||||
|
|
||||||
|
The validation agent checks catalog coverage, the new `seg_smp` env, GPU
|
||||||
|
visibility, no-weight Git safety, backend tests, frontend build, and live
|
||||||
|
backend/frontend endpoints when the services are running.
|
||||||
|
|||||||
2
backend/app/agents/__init__.py
Normal file
2
backend/app/agents/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""Local deterministic agents for app evaluation and validation."""
|
||||||
|
|
||||||
61
backend/app/agents/evaluation_agent.py
Normal file
61
backend/app/agents/evaluation_agent.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ..catalog import get_catalog
|
||||||
|
from ..config import settings
|
||||||
|
|
||||||
|
|
||||||
|
REQUIRED_TASKS = {
|
||||||
|
"dataset.upload": "covered_by_api",
|
||||||
|
"dataset.video_frames": "job",
|
||||||
|
"segmodel.train": "job",
|
||||||
|
"segmodel.predict": "job",
|
||||||
|
"yolo.heatmap": "job",
|
||||||
|
"mmseg.flops_fps": "job",
|
||||||
|
"analysis.all": "job",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_project() -> dict:
|
||||||
|
"""Return product/implementation suggestions for the current web platform."""
|
||||||
|
frontend = settings.project_root / "frontend" / "src" / "main.tsx"
|
||||||
|
backend = settings.project_root / "backend" / "app" / "main.py"
|
||||||
|
readme = settings.project_root / "README.md"
|
||||||
|
catalog = get_catalog()
|
||||||
|
checks = []
|
||||||
|
suggestions = []
|
||||||
|
|
||||||
|
frontend_text = frontend.read_text(encoding="utf-8") if frontend.exists() else ""
|
||||||
|
backend_text = backend.read_text(encoding="utf-8") if backend.exists() else ""
|
||||||
|
readme_text = readme.read_text(encoding="utf-8") if readme.exists() else ""
|
||||||
|
|
||||||
|
expectations = {
|
||||||
|
"left_nav_dataset": "数据集" in frontend_text and "#datasets" in frontend_text,
|
||||||
|
"upload_ui": "uploadDatasetFiles" in frontend_text and "labels" in frontend_text and "masks" in frontend_text,
|
||||||
|
"loss_result_ui": "loss" in frontend_text.lower() and "heatmap" in frontend_text.lower(),
|
||||||
|
"dataset_api": "/api/datasets" in backend_text and "api_upload_dataset_files" in backend_text,
|
||||||
|
"no_weight_to_gitea": "Do not push" in readme_text and "check_no_weight_git" in readme_text,
|
||||||
|
"all_core_tasks": all(task in catalog["task_types"] for task in REQUIRED_TASKS if REQUIRED_TASKS[task] == "job"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, passed in expectations.items():
|
||||||
|
checks.append({"name": name, "passed": bool(passed)})
|
||||||
|
if not passed:
|
||||||
|
suggestions.append(f"Improve missing capability: {name}")
|
||||||
|
|
||||||
|
if len(catalog["mmseg_algorithms"]) < 31:
|
||||||
|
suggestions.append("MMSeg algorithm catalog should expose all 31 algorithm generators.")
|
||||||
|
if len(catalog["segmodel_architectures"]) < 12:
|
||||||
|
suggestions.append("SegModel catalog should expose all 12 supported architectures.")
|
||||||
|
if not suggestions:
|
||||||
|
suggestions.append("Current platform covers the requested control-plane features; next focus is real dataset/training acceptance tests.")
|
||||||
|
|
||||||
|
score = sum(1 for item in checks if item["passed"]) / max(len(checks), 1)
|
||||||
|
return {
|
||||||
|
"agent": "evaluation_suggestion_agent",
|
||||||
|
"score": round(score, 3),
|
||||||
|
"checks": checks,
|
||||||
|
"suggestions": suggestions,
|
||||||
|
}
|
||||||
|
|
||||||
98
backend/app/agents/validation_agent.py
Normal file
98
backend/app/agents/validation_agent.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ..catalog import get_catalog
|
||||||
|
from ..config import settings
|
||||||
|
from ..modules.system.service import get_conda_envs, get_gpus
|
||||||
|
from ..modules.weights.service import load_manifest
|
||||||
|
|
||||||
|
|
||||||
|
def _run(command: list[str], cwd: Path | None = None, timeout: int = 60) -> dict:
|
||||||
|
result = subprocess.run(command, cwd=cwd, capture_output=True, text=True, timeout=timeout)
|
||||||
|
return {
|
||||||
|
"command": command,
|
||||||
|
"returncode": result.returncode,
|
||||||
|
"stdout": result.stdout[-4000:],
|
||||||
|
"stderr": result.stderr[-4000:],
|
||||||
|
"passed": result.returncode == 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch(url: str, timeout: int = 5) -> dict:
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url, timeout=timeout) as response:
|
||||||
|
body = response.read(20000).decode("utf-8", errors="replace")
|
||||||
|
return {"url": url, "status": response.status, "body": body, "passed": 200 <= response.status < 300}
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
body = exc.read(20000).decode("utf-8", errors="replace")
|
||||||
|
return {"url": url, "status": exc.code, "body": body, "passed": False}
|
||||||
|
except Exception as exc:
|
||||||
|
return {"url": url, "error": str(exc), "passed": False}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_project(run_build: bool = False) -> dict:
|
||||||
|
"""Validate current runtime readiness without launching heavy training."""
|
||||||
|
checks = []
|
||||||
|
catalog = get_catalog()
|
||||||
|
manifest = load_manifest()
|
||||||
|
|
||||||
|
checks.append({"name": "catalog_has_yolo_heatmap", "passed": "yolo.heatmap" in catalog["task_types"]})
|
||||||
|
checks.append({"name": "catalog_has_mmseg_31_algs", "passed": len(catalog["mmseg_algorithms"]) >= 31})
|
||||||
|
checks.append({"name": "weights_manifest_present", "passed": manifest.get("count", 0) >= 1})
|
||||||
|
checks.append({"name": "gpus_query", "passed": bool(get_gpus().get("available"))})
|
||||||
|
env_names = [item["name"] for item in get_conda_envs().get("envs", [])]
|
||||||
|
checks.append({"name": "seg_smp_env_exists", "passed": "seg_smp" in env_names})
|
||||||
|
|
||||||
|
smoke = _run(
|
||||||
|
[
|
||||||
|
"conda",
|
||||||
|
"run",
|
||||||
|
"-n",
|
||||||
|
"seg_smp",
|
||||||
|
"python",
|
||||||
|
"-c",
|
||||||
|
"import fastapi, uvicorn, torch, cv2, segmentation_models_pytorch, ultralytics, albumentations, mmengine, mmseg, mmcv; print(torch.__version__, torch.cuda.is_available())",
|
||||||
|
],
|
||||||
|
cwd=settings.project_root,
|
||||||
|
)
|
||||||
|
checks.append({"name": "seg_smp_backend_smoke", "passed": smoke["passed"], "detail": smoke})
|
||||||
|
|
||||||
|
no_weight = _run(["bash", "scripts/check_no_weight_git.sh"], cwd=settings.project_root)
|
||||||
|
checks.append({"name": "no_weight_in_git", "passed": no_weight["passed"], "detail": no_weight})
|
||||||
|
|
||||||
|
if os.getenv("SEG_VALIDATE_LIVE", "1") == "1":
|
||||||
|
backend_url = os.getenv("SEG_VALIDATE_BACKEND_URL", "http://127.0.0.1:8010")
|
||||||
|
frontend_url = os.getenv("SEG_VALIDATE_FRONTEND_URL", "http://127.0.0.1:5173")
|
||||||
|
health = _fetch(f"{backend_url}/api/health")
|
||||||
|
datasets = _fetch(f"{backend_url}/api/datasets")
|
||||||
|
frontend = _fetch(frontend_url)
|
||||||
|
checks.append({"name": "live_backend_health", "passed": health["passed"] and '"ok":true' in health.get("body", "").replace(" ", ""), "detail": health})
|
||||||
|
checks.append({"name": "live_dataset_api", "passed": datasets["passed"] and datasets.get("body", "").lstrip().startswith("["), "detail": datasets})
|
||||||
|
checks.append({"name": "live_frontend_index", "passed": frontend["passed"] and "Seg Data Server" in frontend.get("body", ""), "detail": frontend})
|
||||||
|
|
||||||
|
if run_build:
|
||||||
|
tests = _run(["conda", "run", "-n", settings.backend_conda_env, "python", "-m", "pytest", "-q"], cwd=settings.project_root, timeout=120)
|
||||||
|
checks.append({"name": "backend_tests", "passed": tests["passed"], "detail": tests})
|
||||||
|
build = _run(["npm", "run", "build"], cwd=settings.project_root / "frontend", timeout=120)
|
||||||
|
checks.append({"name": "frontend_build", "passed": build["passed"], "detail": build})
|
||||||
|
|
||||||
|
passed = all(item["passed"] for item in checks)
|
||||||
|
return {
|
||||||
|
"agent": "validation_agent",
|
||||||
|
"passed": passed,
|
||||||
|
"checks": checks,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_validation_report(path: Path, run_build: bool = False) -> dict:
|
||||||
|
result = validate_project(run_build=run_build)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return result
|
||||||
@@ -103,6 +103,11 @@ def discover_datasets() -> list[dict[str, Any]]:
|
|||||||
if item.name == "All_Data_Record.json" or not data:
|
if item.name == "All_Data_Record.json" or not data:
|
||||||
continue
|
continue
|
||||||
candidates.append({"name": item.stem, "path": rel(item, root), "source": "mmseg_parameter"})
|
candidates.append({"name": item.stem, "path": rel(item, root), "source": "mmseg_parameter"})
|
||||||
|
uploaded_root = settings.project_root / "var" / "uploads" / "datasets"
|
||||||
|
if uploaded_root.exists():
|
||||||
|
for item in sorted(uploaded_root.iterdir()):
|
||||||
|
if item.is_dir():
|
||||||
|
candidates.append({"name": item.name, "path": rel(item, settings.project_root), "source": "uploaded"})
|
||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
@@ -137,4 +142,3 @@ def get_catalog() -> dict[str, Any]:
|
|||||||
"datasets": discover_datasets(),
|
"datasets": discover_datasets(),
|
||||||
"weights": discover_weights_summary(),
|
"weights": discover_weights_summary(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ def get_settings() -> Settings:
|
|||||||
log_dir=log_dir,
|
log_dir=log_dir,
|
||||||
weights_root=weights_root,
|
weights_root=weights_root,
|
||||||
task_conda_env=os.getenv("SEG_TASK_CONDA_ENV", "seg_smp"),
|
task_conda_env=os.getenv("SEG_TASK_CONDA_ENV", "seg_smp"),
|
||||||
backend_conda_env=os.getenv("SEG_BACKEND_CONDA_ENV", "seg_server"),
|
backend_conda_env=os.getenv("SEG_BACKEND_CONDA_ENV", "seg_smp"),
|
||||||
weight_mode=os.getenv("SEG_WEIGHT_MODE", "copy"),
|
weight_mode=os.getenv("SEG_WEIGHT_MODE", "copy"),
|
||||||
enable_shell_tasks=os.getenv("SEG_ENABLE_SHELL_TASKS", "1") == "1",
|
enable_shell_tasks=os.getenv("SEG_ENABLE_SHELL_TASKS", "1") == "1",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, File, HTTPException, UploadFile
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
|
|
||||||
@@ -13,9 +13,12 @@ from .catalog import get_catalog
|
|||||||
from .config import settings
|
from .config import settings
|
||||||
from .jobs import cancel_job, create_job
|
from .jobs import cancel_job, create_job
|
||||||
from .modules.system.service import disk_usage, get_conda_envs, get_gpus, scan_results
|
from .modules.system.service import disk_usage, get_conda_envs, get_gpus, scan_results
|
||||||
|
from .modules.dataset.service import create_dataset, list_uploaded_datasets, save_upload
|
||||||
from .modules.weights.service import load_manifest, sync_weights, verify_weights
|
from .modules.weights.service import load_manifest, sync_weights, verify_weights
|
||||||
|
from .agents.evaluation_agent import evaluate_project
|
||||||
|
from .agents.validation_agent import validate_project
|
||||||
from .paths import ensure_inside
|
from .paths import ensure_inside
|
||||||
from .schemas import JobCreate, ProfileCreate, WeightSyncRequest
|
from .schemas import DatasetCreate, JobCreate, ProfileCreate, WeightSyncRequest
|
||||||
|
|
||||||
app = FastAPI(title="Seg Data Server Net", version="0.1.0")
|
app = FastAPI(title="Seg Data Server Net", version="0.1.0")
|
||||||
|
|
||||||
@@ -60,6 +63,24 @@ def api_catalog() -> dict:
|
|||||||
return get_catalog()
|
return get_catalog()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/datasets")
|
||||||
|
def api_datasets() -> list[dict]:
|
||||||
|
return list_uploaded_datasets()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/datasets")
|
||||||
|
def api_create_dataset(dataset: DatasetCreate) -> dict:
|
||||||
|
return create_dataset(dataset.name, dataset.description)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/datasets/{dataset_name}/upload/{kind}")
|
||||||
|
async def api_upload_dataset_files(dataset_name: str, kind: str, files: list[UploadFile] = File(...)) -> dict:
|
||||||
|
try:
|
||||||
|
return await save_upload(dataset_name, kind, files)
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/profiles")
|
@app.get("/api/profiles")
|
||||||
def api_profiles(kind: str | None = None) -> list[dict]:
|
def api_profiles(kind: str | None = None) -> list[dict]:
|
||||||
return db.list_profiles(kind)
|
return db.list_profiles(kind)
|
||||||
@@ -170,3 +191,12 @@ def api_weight_sync(request: WeightSyncRequest) -> dict:
|
|||||||
def api_weight_verify() -> dict:
|
def api_weight_verify() -> dict:
|
||||||
return verify_weights()
|
return verify_weights()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/agents/evaluate")
|
||||||
|
def api_agent_evaluate() -> dict:
|
||||||
|
return evaluate_project()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/agents/validate")
|
||||||
|
def api_agent_validate(run_build: bool = False) -> dict:
|
||||||
|
return validate_project(run_build=run_build)
|
||||||
|
|||||||
132
backend/app/modules/dataset/service.py
Normal file
132
backend/app/modules/dataset/service.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
from fastapi import UploadFile
|
||||||
|
|
||||||
|
from ...config import settings
|
||||||
|
|
||||||
|
DATASET_KINDS = ("images", "labels", "masks")
|
||||||
|
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff"}
|
||||||
|
|
||||||
|
|
||||||
|
def uploads_root() -> Path:
|
||||||
|
root = settings.project_root / "var" / "uploads" / "datasets"
|
||||||
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def slugify(value: str) -> str:
|
||||||
|
text = re.sub(r"[^A-Za-z0-9_.\-\u4e00-\u9fff]+", "_", value.strip())
|
||||||
|
return text.strip("._") or "dataset"
|
||||||
|
|
||||||
|
|
||||||
|
def safe_filename(value: str | None) -> str:
|
||||||
|
original = Path(value or "upload.bin").name
|
||||||
|
suffix = Path(original).suffix.lower()
|
||||||
|
stem = slugify(Path(original).stem or "upload")
|
||||||
|
if suffix and re.fullmatch(r"\.[A-Za-z0-9]{1,12}", suffix):
|
||||||
|
return f"{stem}{suffix}"
|
||||||
|
return stem
|
||||||
|
|
||||||
|
|
||||||
|
def dataset_dir(name: str) -> Path:
|
||||||
|
return uploads_root() / slugify(name)
|
||||||
|
|
||||||
|
|
||||||
|
def metadata_path(name: str) -> Path:
|
||||||
|
return dataset_dir(name) / "dataset.json"
|
||||||
|
|
||||||
|
|
||||||
|
def create_dataset(name: str, description: str = "") -> dict:
|
||||||
|
safe_name = slugify(name)
|
||||||
|
root = dataset_dir(safe_name)
|
||||||
|
for kind in DATASET_KINDS:
|
||||||
|
(root / kind).mkdir(parents=True, exist_ok=True)
|
||||||
|
meta = {
|
||||||
|
"name": safe_name,
|
||||||
|
"description": description,
|
||||||
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"root": str(root.relative_to(settings.project_root)),
|
||||||
|
"layout": {
|
||||||
|
"images": str((root / "images").relative_to(settings.project_root)),
|
||||||
|
"labels": str((root / "labels").relative_to(settings.project_root)),
|
||||||
|
"masks": str((root / "masks").relative_to(settings.project_root)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
metadata_path(safe_name).write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return describe_dataset(safe_name)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_meta(name: str) -> dict:
|
||||||
|
path = metadata_path(name)
|
||||||
|
if path.exists():
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
return {"name": slugify(name), "description": "", "root": str(dataset_dir(name).relative_to(settings.project_root))}
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_files(root: Path) -> Iterable[Path]:
|
||||||
|
if not root.exists():
|
||||||
|
return []
|
||||||
|
return sorted(path for path in root.rglob("*") if path.is_file())
|
||||||
|
|
||||||
|
|
||||||
|
def describe_dataset(name: str) -> dict:
|
||||||
|
safe_name = slugify(name)
|
||||||
|
root = dataset_dir(safe_name)
|
||||||
|
meta = _load_meta(safe_name)
|
||||||
|
counts = {}
|
||||||
|
samples = {}
|
||||||
|
for kind in sorted(DATASET_KINDS):
|
||||||
|
files = list(_iter_files(root / kind))
|
||||||
|
counts[kind] = len(files)
|
||||||
|
samples[kind] = [
|
||||||
|
{
|
||||||
|
"name": path.name,
|
||||||
|
"path": str(path.resolve()),
|
||||||
|
"relative_path": str(path.resolve().relative_to(settings.project_root)),
|
||||||
|
"size": path.stat().st_size,
|
||||||
|
"previewable": path.suffix.lower() in IMAGE_EXTS,
|
||||||
|
}
|
||||||
|
for path in files[:80]
|
||||||
|
]
|
||||||
|
return {**meta, "counts": counts, "samples": samples}
|
||||||
|
|
||||||
|
|
||||||
|
def list_uploaded_datasets() -> list[dict]:
|
||||||
|
root = uploads_root()
|
||||||
|
datasets = []
|
||||||
|
for item in sorted(root.iterdir()):
|
||||||
|
if item.is_dir():
|
||||||
|
datasets.append(describe_dataset(item.name))
|
||||||
|
return datasets
|
||||||
|
|
||||||
|
|
||||||
|
async def save_upload(dataset: str, kind: str, files: list[UploadFile]) -> dict:
|
||||||
|
if kind not in DATASET_KINDS:
|
||||||
|
raise ValueError(f"unsupported dataset file kind: {kind}")
|
||||||
|
safe_name = slugify(dataset)
|
||||||
|
if not metadata_path(safe_name).exists():
|
||||||
|
create_dataset(safe_name)
|
||||||
|
target = dataset_dir(safe_name) / kind
|
||||||
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
|
saved = []
|
||||||
|
for upload in files:
|
||||||
|
filename = safe_filename(upload.filename)
|
||||||
|
dst = target / filename
|
||||||
|
if dst.exists():
|
||||||
|
stem = dst.stem
|
||||||
|
suffix = dst.suffix
|
||||||
|
counter = 1
|
||||||
|
while dst.exists():
|
||||||
|
dst = target / f"{stem}_{counter}{suffix}"
|
||||||
|
counter += 1
|
||||||
|
with dst.open("wb") as handle:
|
||||||
|
shutil.copyfileobj(upload.file, handle)
|
||||||
|
saved.append({"name": dst.name, "relative_path": str(dst.relative_to(settings.project_root)), "size": dst.stat().st_size})
|
||||||
|
return {"dataset": describe_dataset(safe_name), "saved": saved}
|
||||||
@@ -43,3 +43,7 @@ class WeightSyncRequest(BaseModel):
|
|||||||
hash_files: bool = True
|
hash_files: bool = True
|
||||||
skip_existing: bool = True
|
skip_existing: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetCreate(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: str = ""
|
||||||
|
|||||||
14
backend/tests/test_agents.py
Normal file
14
backend/tests/test_agents.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from app.agents.evaluation_agent import evaluate_project
|
||||||
|
from app.agents.validation_agent import validate_project
|
||||||
|
|
||||||
|
|
||||||
|
def test_evaluation_agent_returns_checks():
|
||||||
|
result = evaluate_project()
|
||||||
|
assert result["agent"] == "evaluation_suggestion_agent"
|
||||||
|
assert result["checks"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_agent_lightweight():
|
||||||
|
result = validate_project(run_build=False)
|
||||||
|
assert result["agent"] == "validation_agent"
|
||||||
|
assert any(item["name"] == "catalog_has_yolo_heatmap" for item in result["checks"])
|
||||||
13
backend/tests/test_dataset_service.py
Normal file
13
backend/tests/test_dataset_service.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from app.modules.dataset.service import create_dataset, describe_dataset
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_dataset_layout(tmp_path, monkeypatch):
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from app.modules.dataset import service
|
||||||
|
|
||||||
|
monkeypatch.setattr(service, "settings", SimpleNamespace(project_root=tmp_path))
|
||||||
|
created = create_dataset("case 01", "demo")
|
||||||
|
assert created["name"] == "case_01"
|
||||||
|
assert created["counts"] == {"images": 0, "labels": 0, "masks": 0}
|
||||||
|
described = describe_dataset("case_01")
|
||||||
|
assert described["layout"]["images"].endswith("images")
|
||||||
@@ -3,8 +3,10 @@ import { createRoot } from "react-dom/client";
|
|||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
|
Boxes,
|
||||||
Cpu,
|
Cpu,
|
||||||
Database,
|
Database,
|
||||||
|
FileImage,
|
||||||
FileSearch,
|
FileSearch,
|
||||||
Gauge,
|
Gauge,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
@@ -15,11 +17,12 @@ import {
|
|||||||
Square,
|
Square,
|
||||||
Terminal,
|
Terminal,
|
||||||
UploadCloud,
|
UploadCloud,
|
||||||
|
Wand2,
|
||||||
Zap
|
Zap
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8000";
|
const API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8010";
|
||||||
|
|
||||||
type Job = {
|
type Job = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -42,6 +45,22 @@ type Catalog = {
|
|||||||
weights: { count: number; total_bytes: number; updated_at?: string };
|
weights: { count: number; total_bytes: number; updated_at?: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UploadedDataset = {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
counts: { images: number; labels: number; masks: number };
|
||||||
|
samples: Record<string, Array<{ name: string; relative_path: string; size: number; previewable: boolean }>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResultItem = {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
relative_path: string;
|
||||||
|
size: number;
|
||||||
|
modified: number;
|
||||||
|
kind: string;
|
||||||
|
};
|
||||||
|
|
||||||
type GpuPayload = {
|
type GpuPayload = {
|
||||||
available: boolean;
|
available: boolean;
|
||||||
gpus: Array<{
|
gpus: Array<{
|
||||||
@@ -66,6 +85,13 @@ async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
|||||||
|
|
||||||
const defaultParams: Record<string, Record<string, unknown>> = {
|
const defaultParams: Record<string, Record<string, unknown>> = {
|
||||||
"mock.echo": { message: "hello from Seg Data Server" },
|
"mock.echo": { message: "hello from Seg Data Server" },
|
||||||
|
"dataset.rename": { input_dir: "../DataSet_Own", prefix: "image" },
|
||||||
|
"dataset.to_png": { input_dir: "../DataSet_Own", output_dir: "../DataSet_Own_png" },
|
||||||
|
"dataset.resize": { input_dir: "../DataSet_Own", output_dir: "../DataSet_Own_resize", size: "512x512" },
|
||||||
|
"dataset.pair": { image_dir: "../DataSet_Own/images", label_dir: "../DataSet_Own/labels" },
|
||||||
|
"dataset.rebuild_labels": { label_dir: "../DataSet_Own/labels", output_dir: "../DataSet_Own/rebuilt_labels" },
|
||||||
|
"dataset.stack": { image_dir: "../DataSet_Own/images", mask_dir: "../DataSet_Own/masks", output_dir: "../DataSet_Own/stacked" },
|
||||||
|
"dataset.stitch": { input_dir: "../DataSet_Own/stacked", output_dir: "../DataSet_Own/stitch" },
|
||||||
"dataset.video_frames": { video: "../Seg_Predict_Own_Video_V2/LC_Video_1.mp4", interval: 0.5, resize: "1920x1080" },
|
"dataset.video_frames": { video: "../Seg_Predict_Own_Video_V2/LC_Video_1.mp4", interval: 0.5, resize: "1920x1080" },
|
||||||
"segmodel.train": { architecture: "Unet" },
|
"segmodel.train": { architecture: "Unet" },
|
||||||
"segmodel.predict": { architecture: "Unet", run_choice: 1 },
|
"segmodel.predict": { architecture: "Unet", run_choice: 1 },
|
||||||
@@ -79,6 +105,17 @@ const defaultParams: Record<string, Record<string, unknown>> = {
|
|||||||
"analysis.all": { input_dir: "../BestMode_Predict_Results_DataSet_Public", output_dir: "./", dataset_choice: 1 }
|
"analysis.all": { input_dir: "../BestMode_Predict_Results_DataSet_Public", output_dir: "./", dataset_choice: 1 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const taskLabels: Record<string, string> = {
|
||||||
|
"dataset.rename": "重命名",
|
||||||
|
"dataset.to_png": "转 PNG",
|
||||||
|
"dataset.resize": "Resize",
|
||||||
|
"dataset.pair": "图片/Label 配对",
|
||||||
|
"dataset.rebuild_labels": "重建 Label",
|
||||||
|
"dataset.stack": "透明叠加",
|
||||||
|
"dataset.stitch": "拼接检查",
|
||||||
|
"dataset.video_frames": "视频抽帧"
|
||||||
|
};
|
||||||
|
|
||||||
function formatBytes(value?: number) {
|
function formatBytes(value?: number) {
|
||||||
if (!value) return "0 B";
|
if (!value) return "0 B";
|
||||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||||
@@ -95,21 +132,24 @@ function useData() {
|
|||||||
const [catalog, setCatalog] = useState<Catalog | null>(null);
|
const [catalog, setCatalog] = useState<Catalog | null>(null);
|
||||||
const [gpus, setGpus] = useState<GpuPayload | null>(null);
|
const [gpus, setGpus] = useState<GpuPayload | null>(null);
|
||||||
const [jobs, setJobs] = useState<Job[]>([]);
|
const [jobs, setJobs] = useState<Job[]>([]);
|
||||||
const [results, setResults] = useState<Array<Record<string, unknown>>>([]);
|
const [results, setResults] = useState<ResultItem[]>([]);
|
||||||
|
const [datasets, setDatasets] = useState<UploadedDataset[]>([]);
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
try {
|
try {
|
||||||
const [catalogNext, gpusNext, jobsNext, resultsNext] = await Promise.all([
|
const [catalogNext, gpusNext, jobsNext, resultsNext, datasetsNext] = await Promise.all([
|
||||||
api<Catalog>("/api/catalog"),
|
api<Catalog>("/api/catalog"),
|
||||||
api<GpuPayload>("/api/system/gpus"),
|
api<GpuPayload>("/api/system/gpus"),
|
||||||
api<Job[]>("/api/jobs"),
|
api<Job[]>("/api/jobs"),
|
||||||
api<Array<Record<string, unknown>>>("/api/results")
|
api<ResultItem[]>("/api/results"),
|
||||||
|
api<UploadedDataset[]>("/api/datasets")
|
||||||
]);
|
]);
|
||||||
setCatalog(catalogNext);
|
setCatalog(catalogNext);
|
||||||
setGpus(gpusNext);
|
setGpus(gpusNext);
|
||||||
setJobs(jobsNext);
|
setJobs(jobsNext);
|
||||||
setResults(resultsNext.slice(0, 80));
|
setResults(resultsNext.slice(0, 80));
|
||||||
|
setDatasets(datasetsNext);
|
||||||
setError("");
|
setError("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(String(err));
|
setError(String(err));
|
||||||
@@ -122,7 +162,7 @@ function useData() {
|
|||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { catalog, gpus, jobs, results, error, refresh };
|
return { catalog, gpus, jobs, results, datasets, error, refresh };
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusPill({ status }: { status: string }) {
|
function StatusPill({ status }: { status: string }) {
|
||||||
@@ -130,12 +170,16 @@ function StatusPill({ status }: { status: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { catalog, gpus, jobs, results, error, refresh } = useData();
|
const { catalog, gpus, jobs, results, datasets, error, refresh } = useData();
|
||||||
const [taskType, setTaskType] = useState("mock.echo");
|
const [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);
|
||||||
const [log, setLog] = useState("");
|
const [log, setLog] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [datasetName, setDatasetName] = useState("demo_dataset");
|
||||||
|
const [datasetDescription, setDatasetDescription] = useState("");
|
||||||
|
const [uploadKind, setUploadKind] = useState<"images" | "labels" | "masks">("images");
|
||||||
|
const [uploadFiles, setUploadFiles] = useState<FileList | null>(null);
|
||||||
|
|
||||||
const runningCount = jobs.filter((job) => job.status === "running").length;
|
const 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;
|
||||||
@@ -152,11 +196,18 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, [catalog]);
|
}, [catalog]);
|
||||||
|
|
||||||
|
const datasetOps = taskGroups.dataset.filter((task) => task in taskLabels);
|
||||||
|
|
||||||
function pickTask(next: string) {
|
function pickTask(next: string) {
|
||||||
setTaskType(next);
|
setTaskType(next);
|
||||||
setParams(JSON.stringify(defaultParams[next] ?? {}, null, 2));
|
setParams(JSON.stringify(defaultParams[next] ?? {}, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pickDatasetTask(next: string) {
|
||||||
|
pickTask(next);
|
||||||
|
window.location.hash = "jobs";
|
||||||
|
}
|
||||||
|
|
||||||
async function createJob() {
|
async function createJob() {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
@@ -183,6 +234,36 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createDataset() {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await api("/api/datasets", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ name: datasetName, description: datasetDescription })
|
||||||
|
});
|
||||||
|
await refresh();
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadDatasetFiles() {
|
||||||
|
if (!uploadFiles || uploadFiles.length === 0) return;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const body = new FormData();
|
||||||
|
Array.from(uploadFiles).forEach((file) => body.append("files", file));
|
||||||
|
const res = await fetch(`${API_BASE}/api/datasets/${encodeURIComponent(datasetName)}/upload/${uploadKind}`, {
|
||||||
|
method: "POST",
|
||||||
|
body
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(await res.text());
|
||||||
|
await refresh();
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function inspectJob(job: Job) {
|
async function inspectJob(job: Job) {
|
||||||
const detail = await api<Job>(`/api/jobs/${job.id}`);
|
const detail = await api<Job>(`/api/jobs/${job.id}`);
|
||||||
setSelectedJob(detail);
|
setSelectedJob(detail);
|
||||||
@@ -214,6 +295,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="#jobs"><Terminal size={18} />任务</a>
|
<a href="#jobs"><Terminal size={18} />任务</a>
|
||||||
|
<a href="#datasets"><Boxes size={18} />数据集</a>
|
||||||
<a href="#gpu"><Cpu size={18} />GPU</a>
|
<a href="#gpu"><Cpu size={18} />GPU</a>
|
||||||
<a href="#weights"><HardDrive size={18} />权重</a>
|
<a href="#weights"><HardDrive size={18} />权重</a>
|
||||||
<a href="#results"><BarChart3 size={18} />结果</a>
|
<a href="#results"><BarChart3 size={18} />结果</a>
|
||||||
@@ -251,8 +333,8 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="metric">
|
<div className="metric">
|
||||||
<Database size={20} />
|
<Database size={20} />
|
||||||
<span>数据集</span>
|
<span>上传集</span>
|
||||||
<strong>{catalog?.datasets.length ?? 0}</strong>
|
<strong>{datasets.length}</strong>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -305,6 +387,82 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="grid two" id="datasets">
|
||||||
|
<div className="panel">
|
||||||
|
<div className="panelHead">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Dataset Bench</p>
|
||||||
|
<h2>数据集、Label、Mask 上传</h2>
|
||||||
|
</div>
|
||||||
|
<Database size={22} />
|
||||||
|
</div>
|
||||||
|
<div className="datasetForm">
|
||||||
|
<label className="field compact">
|
||||||
|
<span>数据集名称</span>
|
||||||
|
<input value={datasetName} onChange={(event) => setDatasetName(event.target.value)} />
|
||||||
|
</label>
|
||||||
|
<label className="field compact">
|
||||||
|
<span>说明</span>
|
||||||
|
<input value={datasetDescription} onChange={(event) => setDatasetDescription(event.target.value)} />
|
||||||
|
</label>
|
||||||
|
<div className="segmented">
|
||||||
|
{(["images", "labels", "masks"] as const).map((kind) => (
|
||||||
|
<button key={kind} className={uploadKind === kind ? "active" : ""} onClick={() => setUploadKind(kind)}>
|
||||||
|
{kind}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<label className="drop">
|
||||||
|
<UploadCloud size={24} />
|
||||||
|
<span>{uploadFiles?.length ? `${uploadFiles.length} files selected` : "选择图片、label 或 mask 文件"}</span>
|
||||||
|
<input multiple type="file" accept="image/*,.txt,.json,.yaml,.yml" onChange={(event) => setUploadFiles(event.target.files)} />
|
||||||
|
</label>
|
||||||
|
<div className="buttonRow">
|
||||||
|
<button className="primary" disabled={busy} onClick={createDataset}><Boxes size={17} />创建</button>
|
||||||
|
<button className="primary secondary" disabled={busy || !uploadFiles?.length} onClick={uploadDatasetFiles}><UploadCloud size={17} />上传</button>
|
||||||
|
</div>
|
||||||
|
<div className="opGrid">
|
||||||
|
{datasetOps.map((task) => (
|
||||||
|
<button key={task} type="button" onClick={() => pickDatasetTask(task)}>
|
||||||
|
<Wand2 size={16} />
|
||||||
|
<span>{taskLabels[task]}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="panel">
|
||||||
|
<div className="panelHead">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Files</p>
|
||||||
|
<h2>数据集浏览</h2>
|
||||||
|
</div>
|
||||||
|
<FileImage size={22} />
|
||||||
|
</div>
|
||||||
|
<div className="datasetList">
|
||||||
|
{datasets.map((dataset) => (
|
||||||
|
<div className="datasetCard" key={dataset.name}>
|
||||||
|
<div className="datasetCardHead">
|
||||||
|
<strong>{dataset.name}</strong>
|
||||||
|
<span>{dataset.counts.images} image · {dataset.counts.labels} label · {dataset.counts.masks} mask</span>
|
||||||
|
</div>
|
||||||
|
<div className="sampleStrip">
|
||||||
|
{["images", "labels", "masks"].flatMap((kind) =>
|
||||||
|
(dataset.samples[kind] ?? []).slice(0, 4).map((sample) => (
|
||||||
|
<a key={`${kind}-${sample.relative_path}`} href={`${API_BASE}/api/artifacts/${sample.relative_path}`} target="_blank" rel="noreferrer">
|
||||||
|
<span>{kind}</span>
|
||||||
|
<small>{sample.name}</small>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="grid three">
|
<section className="grid three">
|
||||||
<div className="panel" id="gpu">
|
<div className="panel" id="gpu">
|
||||||
<div className="panelHead">
|
<div className="panelHead">
|
||||||
@@ -356,6 +514,46 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="grid three">
|
||||||
|
<div className="panel insight">
|
||||||
|
<div className="panelHead">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Segmentation</p>
|
||||||
|
<h2>分割结果</h2>
|
||||||
|
</div>
|
||||||
|
<Wand2 size={22} />
|
||||||
|
</div>
|
||||||
|
<ResultPreview results={results.filter((item) => /predict|mask|comparison|prediction/i.test(item.relative_path) && ["png", "jpg", "jpeg"].includes(item.kind)).slice(0, 6)} />
|
||||||
|
</div>
|
||||||
|
<div className="panel insight">
|
||||||
|
<div className="panelHead">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Heatmap</p>
|
||||||
|
<h2>YOLO 热度图</h2>
|
||||||
|
</div>
|
||||||
|
<Zap size={22} />
|
||||||
|
</div>
|
||||||
|
<ResultPreview results={results.filter((item) => /heat|cam|grad/i.test(item.relative_path) && ["png", "jpg", "jpeg"].includes(item.kind)).slice(0, 6)} />
|
||||||
|
</div>
|
||||||
|
<div className="panel insight">
|
||||||
|
<div className="panelHead">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Curves</p>
|
||||||
|
<h2>Loss / 指标</h2>
|
||||||
|
</div>
|
||||||
|
<BarChart3 size={22} />
|
||||||
|
</div>
|
||||||
|
<div className="resultList tight">
|
||||||
|
{results.filter((item) => /loss|metric|miou|iou|csv|curve/i.test(item.relative_path)).slice(0, 10).map((item) => (
|
||||||
|
<a key={item.path} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
|
||||||
|
<span>{item.name}</span>
|
||||||
|
<small>{formatBytes(item.size)}</small>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="grid two">
|
<section className="grid two">
|
||||||
<div className="panel logPanel">
|
<div className="panel logPanel">
|
||||||
<div className="panelHead">
|
<div className="panelHead">
|
||||||
@@ -380,9 +578,9 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="resultList">
|
<div className="resultList">
|
||||||
{results.map((item) => (
|
{results.map((item) => (
|
||||||
<a key={String(item.path)} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
|
<a key={item.path} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
|
||||||
<span>{String(item.name)}</span>
|
<span>{item.name}</span>
|
||||||
<small>{formatBytes(Number(item.size))}</small>
|
<small>{formatBytes(item.size)}</small>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -393,6 +591,22 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ResultPreview({ results }: { results: ResultItem[] }) {
|
||||||
|
if (!results.length) {
|
||||||
|
return <p className="muted">暂无结果,运行预测、热度图或分析任务后会自动出现在这里。</p>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="previewGrid">
|
||||||
|
{results.map((item) => (
|
||||||
|
<a key={item.path} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
|
||||||
|
<img src={`${API_BASE}/api/artifacts/${item.relative_path}`} alt={item.name} />
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ button, textarea, select {
|
|||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font: inherit;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: 0;
|
border: 0;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -154,6 +159,10 @@ h2 {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.primary.secondary {
|
||||||
|
background: var(--cyan);
|
||||||
|
}
|
||||||
|
|
||||||
.primary:disabled, .iconButton:disabled {
|
.primary:disabled, .iconButton:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@@ -283,6 +292,159 @@ textarea {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field.compact {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input {
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--field);
|
||||||
|
}
|
||||||
|
|
||||||
|
.datasetForm {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #101310;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented button {
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented button.active {
|
||||||
|
background: var(--green);
|
||||||
|
color: #0b0d0b;
|
||||||
|
font-weight: 760;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop {
|
||||||
|
min-height: 118px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
position: relative;
|
||||||
|
border: 1px dashed #526052;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(13, 16, 13, 0.8);
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop input {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opGrid button {
|
||||||
|
min-width: 0;
|
||||||
|
height: 42px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #101310;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opGrid button:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
border-color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.opGrid span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datasetList {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
max-height: 420px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datasetCard {
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #101310;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datasetCardHead {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datasetCardHead span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sampleStrip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sampleStrip a {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--ink);
|
||||||
|
background: #0b0d0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sampleStrip span,
|
||||||
|
.sampleStrip small {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sampleStrip span {
|
||||||
|
color: var(--green);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.jobList, .resultList {
|
.jobList, .resultList {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -382,11 +544,53 @@ meter {
|
|||||||
border-color: var(--green);
|
border-color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resultList.tight {
|
||||||
|
max-height: 290px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight {
|
||||||
|
min-height: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewGrid a {
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 7px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--ink);
|
||||||
|
text-decoration: none;
|
||||||
|
background: #0b0d0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewGrid img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 10;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #060806;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewGrid span {
|
||||||
|
display: block;
|
||||||
|
padding: 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
body { min-width: 960px; }
|
body { min-width: 960px; }
|
||||||
.shell { grid-template-columns: 220px 1fr; }
|
.shell { grid-template-columns: 220px 1fr; }
|
||||||
.taskColumns { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
.taskColumns { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
.opGrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
.grid.three { grid-template-columns: 1fr; }
|
.grid.three { grid-template-columns: 1fr; }
|
||||||
.grid.two { grid-template-columns: 1fr; }
|
.grid.two { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
35
scripts/run_agents.py
Normal file
35
scripts/run_agents.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / "backend"))
|
||||||
|
|
||||||
|
from app.agents.evaluation_agent import evaluate_project # noqa: E402
|
||||||
|
from app.agents.validation_agent import validate_project # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Run local evaluation and validation agents.")
|
||||||
|
parser.add_argument("--build", action="store_true", help="also run pytest and frontend build")
|
||||||
|
parser.add_argument("--out", default="var/agent_reports/latest.json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
report = {
|
||||||
|
"evaluation": evaluate_project(),
|
||||||
|
"validation": validate_project(run_build=args.build),
|
||||||
|
}
|
||||||
|
out = ROOT / args.out
|
||||||
|
out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||||
|
if not report["validation"]["passed"]:
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
@@ -2,10 +2,9 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
BACKEND_ENV="${SEG_BACKEND_CONDA_ENV:-seg_server}"
|
BACKEND_ENV="${SEG_BACKEND_CONDA_ENV:-seg_smp}"
|
||||||
HOST="${SEG_BACKEND_HOST:-0.0.0.0}"
|
HOST="${SEG_BACKEND_HOST:-0.0.0.0}"
|
||||||
PORT="${SEG_BACKEND_PORT:-8000}"
|
PORT="${SEG_BACKEND_PORT:-8010}"
|
||||||
|
|
||||||
cd "${ROOT_DIR}"
|
cd "${ROOT_DIR}"
|
||||||
exec conda run -n "${BACKEND_ENV}" uvicorn app.main:app --app-dir backend --host "${HOST}" --port "${PORT}" --reload
|
exec conda run -n "${BACKEND_ENV}" uvicorn app.main:app --app-dir backend --host "${HOST}" --port "${PORT}" --reload
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user