diff --git a/.env.example b/.env.example index 5a239f6..768c9f3 100644 --- a/.env.example +++ b/.env.example @@ -2,10 +2,15 @@ SEG_SOURCE_ROOT=../Seg SEG_DATA_SERVER_ROOT=. SEG_BACKEND_DB=var/seg_data_server.sqlite3 SEG_BACKEND_LOG_DIR=var/job_logs +SEG_BACKEND_HOST=0.0.0.0 +SEG_BACKEND_PORT=8010 +SEG_BACKEND_RELOAD=1 SEG_TASK_CONDA_ENV=seg_smp SEG_MMSEG_CONDA_ENV=seg_mmcv SEG_BACKEND_CONDA_ENV=seg_smp SEG_WEIGHT_MODE=copy SEG_ENABLE_SHELL_TASKS=1 SEG_VALIDATE_DEEP=1 +SEG_FRONTEND_HOST=0.0.0.0 +SEG_FRONTEND_PORT=5173 VITE_API_BASE=http://localhost:8010 diff --git a/README.md b/README.md index fdcc2db..5158b94 100644 --- a/README.md +++ b/README.md @@ -69,15 +69,39 @@ SEG_SOURCE_ROOT=../Seg SEG_DATA_SERVER_ROOT=. SEG_BACKEND_DB=var/seg_data_server.sqlite3 SEG_BACKEND_LOG_DIR=var/job_logs +SEG_BACKEND_HOST=0.0.0.0 +SEG_BACKEND_PORT=8010 +SEG_BACKEND_RELOAD=1 SEG_TASK_CONDA_ENV=seg_smp SEG_MMSEG_CONDA_ENV=seg_mmcv SEG_BACKEND_CONDA_ENV=seg_smp SEG_WEIGHT_MODE=copy SEG_ENABLE_SHELL_TASKS=1 SEG_VALIDATE_DEEP=1 +SEG_FRONTEND_HOST=0.0.0.0 +SEG_FRONTEND_PORT=5173 VITE_API_BASE=http://localhost:8010 ``` +Environment variables used during deployment: + +| Variable | Purpose | +| --- | --- | +| `SEG_SOURCE_ROOT` | Path to the original `Seg/` algorithm workspace. | +| `SEG_DATA_SERVER_ROOT` | Runtime root for this web project. Keep `.` for normal deployments. | +| `SEG_BACKEND_DB` | SQLite database used for datasets, jobs, and profiles. | +| `SEG_BACKEND_LOG_DIR` | Directory for job logs streamed through SSE. | +| `SEG_BACKEND_HOST` / `SEG_BACKEND_PORT` | FastAPI listen address. | +| `SEG_BACKEND_RELOAD` | Set `1` for development reload, `0` for long-running production workers. | +| `SEG_BACKEND_CONDA_ENV` | Conda env used to run FastAPI. | +| `SEG_TASK_CONDA_ENV` | Default env for dataset, SegModel, YOLO, visual, and analysis jobs. | +| `SEG_MMSEG_CONDA_ENV` | Dedicated env for full MMSeg/mmcv jobs. | +| `SEG_WEIGHT_MODE` | `copy` or `reflink` when syncing weights. | +| `SEG_ENABLE_SHELL_TASKS` | Enables execution of the wrapped legacy shell/Python tasks. | +| `SEG_VALIDATE_DEEP` | Enables deep acceptance by default for local agent runs. | +| `SEG_FRONTEND_HOST` / `SEG_FRONTEND_PORT` | Vite development server listen address. | +| `VITE_API_BASE` | Backend URL embedded into the frontend build or used by the dev server. | + Install system prerequisites first: Git, Conda or Miniconda, Node.js/npm, a working NVIDIA driver and `nvidia-smi` for GPU discovery, and enough free disk space for the copied weights. Full weight sync currently needs about 35 GB @@ -129,11 +153,38 @@ scripts/run_backend.sh scripts/run_frontend.sh ``` -Defaults are `0.0.0.0:8010` for the backend and Vite's dev port for the -frontend. Override backend binding with `SEG_BACKEND_HOST` and -`SEG_BACKEND_PORT`. For a production process manager such as systemd, -supervisor, or Docker Compose, call the same two scripts so `.env` and the -configured conda environments are used consistently. +Defaults are `0.0.0.0:8010` for the backend and `0.0.0.0:5173` for the +frontend development server. Override backend binding with `SEG_BACKEND_HOST` +and `SEG_BACKEND_PORT`; override the Vite dev server with `SEG_FRONTEND_HOST` +and `SEG_FRONTEND_PORT`. + +For long-running production service management, disable backend reload and +start the same script from systemd, supervisor, or Docker Compose so `.env` +and the configured conda environments are used consistently: + +```bash +SEG_BACKEND_RELOAD=0 scripts/run_backend.sh +``` + +For a static frontend deployment, set `VITE_API_BASE` to the public backend +URL before building, then serve `frontend/dist/` with Nginx or another static +file server. A quick local preview is: + +```bash +cd frontend +npm run build +npm run preview -- --host 0.0.0.0 --port 4173 +``` + +If systemd is used, a minimal backend unit can call the script directly: + +```ini +[Service] +WorkingDirectory=/home/wkmgc/Desktop/Data_Disk_1/Seg/Seg_Data_Server_Net +Environment=SEG_BACKEND_RELOAD=0 +ExecStart=/home/wkmgc/Desktop/Data_Disk_1/Seg/Seg_Data_Server_Net/scripts/run_backend.sh +Restart=always +``` Validate a deployment before handing it to operators: @@ -208,6 +259,16 @@ discovery. MMSeg full-model readiness is validated in `SEG_MMSEG_CONDA_ENV` by importing `mmcv._ext` and building a local MMSeg `EncoderDecoder` from the existing config tree. +`POST /api/acceptance/real` runs the same operator-facing path on existing +non-synthetic workspace files. It discovers a real `DataSet_Own/*_Ori` image +with a matching mask, discovers a real YOLO image/txt pair from +`Seg_All_In_One_YoloModel/Yolo数据集构建/Data`, uploads those files through the +dataset API, validates YOLO and mask readiness, generates `dataset.yaml`, runs +YOLO prediction and heatmap jobs, and runs the legacy stack job on the uploaded +real image/mask pair. The latest report is available from +`GET /api/acceptance/real/latest` and is shown in the coverage panel as +`真实数据`. + For stronger runtime proof, `POST /api/acceptance/deep` runs minimal training loops for the three model families: one SegModel optimizer step, one YOLO segmentation epoch on a synthetic 64x64 dataset, one YOLO GradCAM heatmap @@ -308,5 +369,6 @@ non-training validation pass is needed. The web dashboard calls validation in light mode by default: `/api/agents/validate?run_build=false&run_acceptance=false&run_deep=false`. -Pass `run_acceptance=true` or `run_deep=true` only when you explicitly want the -agent to launch the heavier runtime acceptance checks from the browser/API. +Pass `run_acceptance=true`, `run_real=true`, or `run_deep=true` only when you +explicitly want the agent to launch the heavier runtime acceptance checks from +the browser/API. diff --git a/backend/app/acceptance.py b/backend/app/acceptance.py index 273a2c4..1eaa4ec 100644 --- a/backend/app/acceptance.py +++ b/backend/app/acceptance.py @@ -12,6 +12,8 @@ from typing import Any from .config import settings +IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff"} + def _run_command(command: list[str], cwd: Path | None = None, timeout: int = 60) -> dict[str, Any]: try: @@ -183,6 +185,23 @@ def _request_text(url: str, timeout: int = 10) -> dict[str, Any]: return {"passed": False, "error": str(exc)} +def _content_type(path: Path) -> str: + suffix = path.suffix.lower() + if suffix in {".jpg", ".jpeg"}: + return "image/jpeg" + if suffix == ".png": + return "image/png" + if suffix in {".tif", ".tiff"}: + return "image/tiff" + if suffix == ".txt": + return "text/plain" + return "application/octet-stream" + + +def _post_file(url: str, path: Path, timeout: int = 30) -> dict[str, Any]: + return _post_multipart(url, "files", path.name, path.read_bytes(), _content_type(path), timeout=timeout) + + def _post_multipart(url: str, field: str, filename: str, content: bytes, content_type: str = "text/plain", timeout: int = 10) -> dict[str, Any]: boundary = f"----SegAcceptance{uuid.uuid4().hex}" body = b"".join( @@ -280,6 +299,61 @@ def _result_files(root: Path, suffixes: set[str]) -> list[Path]: return sorted(path for path in root.rglob("*") if path.is_file() and path.suffix.lower() in suffixes) +def _files_by_stem(root: Path, suffixes: set[str], nonempty: bool = True) -> dict[str, Path]: + if not root.exists(): + return {} + files: dict[str, Path] = {} + for path in sorted(root.iterdir()): + if not path.is_file() or path.suffix.lower() not in suffixes: + continue + if nonempty and path.stat().st_size <= 0: + continue + files.setdefault(path.stem, path) + return files + + +def _find_stem_pair(left_root: Path, left_suffixes: set[str], right_root: Path, right_suffixes: set[str]) -> tuple[Path, Path] | None: + left = _files_by_stem(left_root, left_suffixes) + right = _files_by_stem(right_root, right_suffixes) + for stem in sorted(set(left) & set(right)): + return left[stem], right[stem] + return None + + +def find_real_workspace_samples() -> dict[str, Any]: + """Find existing non-synthetic samples from the checked-out Seg workspace.""" + source = settings.source_root + mask_pair = None + mask_candidates = [] + for prefix in ("A", "B", "C"): + image_root = source / "DataSet_Own" / f"{prefix}_Ori" + mask_root = source / "DataSet_Own" / f"{prefix}_Label_Ori" + mask_candidates.append({"image_root": str(image_root), "mask_root": str(mask_root)}) + pair = _find_stem_pair(image_root, IMAGE_SUFFIXES, mask_root, IMAGE_SUFFIXES) + if pair: + mask_pair = {"image": str(pair[0]), "mask": str(pair[1]), "dataset": prefix} + break + + yolo_pair = None + yolo_candidates = [] + yolo_dataset = source / "Seg_All_In_One_YoloModel" / "Yolo数据集构建" / "Data" + for split in ("train", "val"): + image_root = yolo_dataset / "images" / split + label_root = yolo_dataset / "labels" / split + yolo_candidates.append({"image_root": str(image_root), "label_root": str(label_root)}) + pair = _find_stem_pair(image_root, IMAGE_SUFFIXES, label_root, {".txt"}) + if pair: + yolo_pair = {"image": str(pair[0]), "label": str(pair[1]), "split": split} + break + + return { + "passed": bool(mask_pair and yolo_pair), + "mask_pair": mask_pair, + "yolo_pair": yolo_pair, + "candidates": {"mask": mask_candidates, "yolo": yolo_candidates}, + } + + def run_model_family_readiness() -> dict[str, Any]: """Exercise the model-family runtime stack without launching full training.""" source = settings.source_root @@ -366,6 +440,186 @@ def latest_deep_acceptance_report() -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) +def latest_real_acceptance_report() -> dict[str, Any]: + path = settings.project_root / "var" / "acceptance" / "real_latest.json" + if not path.exists(): + return {"available": False, "path": str(path)} + return json.loads(path.read_text(encoding="utf-8")) + + +def run_real_dataset_acceptance(base_url: str = "http://127.0.0.1:8010") -> dict[str, Any]: + """Run the upload/predict/heatmap path against existing non-synthetic Seg data.""" + acceptance_root = settings.project_root / "var" / "acceptance" + run_id = uuid.uuid4().hex[:8] + fixture_root = acceptance_root / f"real_{run_id}" + fixture_root.mkdir(parents=True, exist_ok=True) + + samples = find_real_workspace_samples() + checks: list[dict[str, Any]] = [ + {"name": "real_workspace_samples_discovered", "passed": samples["passed"], "detail": samples} + ] + if not samples["passed"]: + report = { + "available": True, + "run_id": run_id, + "base_url": base_url, + "fixture_root": str(fixture_root), + "passed": False, + "checks": checks, + "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + } + (acceptance_root / "real_latest.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") + return report + + dataset_name = f"real_acceptance_{run_id}" + created_dataset = _request_json("POST", f"{base_url}/api/datasets", {"name": dataset_name, "description": "real workspace acceptance"}, timeout=10) + checks.append({"name": "create_real_upload_dataset", "passed": created_dataset.get("passed", False), "detail": created_dataset}) + + mask_image = Path(samples["mask_pair"]["image"]) + mask_file = Path(samples["mask_pair"]["mask"]) + yolo_image = Path(samples["yolo_pair"]["image"]) + yolo_label = Path(samples["yolo_pair"]["label"]) + + uploads = { + "real_mask_image_upload": _post_file(f"{base_url}/api/datasets/{dataset_name}/upload/images", mask_image, timeout=30), + "real_mask_upload": _post_file(f"{base_url}/api/datasets/{dataset_name}/upload/masks", mask_file, timeout=30), + "real_yolo_image_upload": _post_file(f"{base_url}/api/datasets/{dataset_name}/upload/images", yolo_image, timeout=30), + "real_yolo_label_upload": _post_file(f"{base_url}/api/datasets/{dataset_name}/upload/labels", yolo_label, timeout=30), + } + for name, detail in uploads.items(): + checks.append({"name": name, "passed": detail.get("passed", False), "detail": detail}) + + validation = _request_json("GET", f"{base_url}/api/datasets/{dataset_name}/validate", timeout=20) + validation_json = validation.get("json") if validation.get("passed") else {} + checks.append( + { + "name": "real_dataset_validate_yolo_and_mask", + "passed": validation.get("passed", False) + and validation_json.get("ready", {}).get("yolo") + and validation_json.get("ready", {}).get("mask"), + "detail": validation, + } + ) + + yolo_yaml = _request_json("POST", f"{base_url}/api/datasets/{dataset_name}/yolo-yaml", {"class_names": ["object"]}, timeout=20) + checks.append({"name": "real_dataset_yolo_yaml", "passed": yolo_yaml.get("passed", False), "detail": yolo_yaml}) + + yolo_image_upload = uploads["real_yolo_image_upload"].get("json", {}) + mask_image_upload = uploads["real_mask_image_upload"].get("json", {}) + mask_upload = uploads["real_mask_upload"].get("json", {}) + uploaded_yolo_image = yolo_image_upload.get("saved", [{}])[0].get("relative_path") + uploaded_mask_image = mask_image_upload.get("saved", [{}])[0].get("relative_path") + uploaded_mask = mask_upload.get("saved", [{}])[0].get("relative_path") + + artifact_label = _request_text(f"{base_url}/api/artifacts/{uploads['real_yolo_label_upload'].get('json', {}).get('saved', [{}])[0].get('relative_path')}", timeout=10) + checks.append( + { + "name": "real_uploaded_label_artifact", + "passed": artifact_label.get("passed", False) and bool(artifact_label.get("body", "").strip()), + "detail": artifact_label, + } + ) + + yolo_weight = settings.source_root / "Seg_All_In_One_YoloModel" / "yolo11n-seg.pt" + predict_name = f"{dataset_name}_predict_real" + if uploaded_yolo_image: + predict = _create_job_and_wait( + base_url, + "yolo.predict_custom", + { + "weights": str(yolo_weight), + "source": uploaded_yolo_image, + "project": "var/custom_yolo_runs", + "name": predict_name, + "imgsz": 96, + "conf": 0.05, + "device": "cpu", + "exist_ok": True, + }, + timeout=120, + ) + else: + predict = {"passed": False, "error": "skipped because real_yolo_image_upload did not return a saved path"} + predict_root = settings.project_root / "var" / "custom_yolo_runs" / predict_name + predict_outputs = _result_files(predict_root, {".png", ".jpg", ".jpeg"}) + checks.append( + { + "name": "real_workspace_yolo_predict_job_runner", + "passed": predict.get("passed", False) and bool(predict_outputs), + "detail": {**predict, "output_count": len(predict_outputs), "outputs": [_relative_to_project(path) for path in predict_outputs[:8]]}, + } + ) + + heatmap_name = f"{dataset_name}_heatmap_real" + if uploaded_yolo_image: + heatmap = _create_job_and_wait( + base_url, + "yolo.heatmap_custom", + { + "weights": str(yolo_weight), + "source": uploaded_yolo_image, + "project": "var/custom_yolo_runs", + "name": heatmap_name, + "model_key": "YOLO11n-seg", + "pt_name": "best.pt", + "cam_method": "GradCAM", + "target_layers": "model.model.model[9]", + "limit": 1, + }, + timeout=120, + ) + else: + heatmap = {"passed": False, "error": "skipped because real_yolo_image_upload did not return a saved path"} + heatmap_root = settings.project_root / "var" / "custom_yolo_runs" / heatmap_name / "HeartMap_Visual" + heatmap_outputs = _result_files(heatmap_root, {".jpg", ".jpeg", ".png"}) + checks.append( + { + "name": "real_workspace_yolo_heatmap_job_runner", + "passed": heatmap.get("passed", False) and len(heatmap_outputs) >= 2, + "detail": {**heatmap, "output_count": len(heatmap_outputs), "outputs": [_relative_to_project(path) for path in heatmap_outputs[:8]]}, + } + ) + + stack_dir = fixture_root / "real_stack" + if uploaded_mask_image and uploaded_mask: + stack = _create_job_with_retry( + base_url, + "dataset.stack_single", + { + "image_path": str(settings.project_root / uploaded_mask_image), + "label_path": str(settings.project_root / uploaded_mask), + "result_dir": str(stack_dir), + "alpha": 0.35, + }, + attempts=2, + timeout=90, + ) + else: + stack = {"passed": False, "error": "skipped because real mask upload did not return saved paths"} + stack_outputs = _result_files(stack_dir, {".png", ".jpg", ".jpeg"}) + checks.append( + { + "name": "real_workspace_stack_job_runner", + "passed": stack.get("passed", False) and bool(stack_outputs), + "detail": {**stack, "output_count": len(stack_outputs), "outputs": [_relative_to_project(path) for path in stack_outputs[:8]]}, + } + ) + + report = { + "available": True, + "run_id": run_id, + "base_url": base_url, + "fixture_root": str(fixture_root), + "dataset_name": dataset_name, + "samples": samples, + "passed": all(item["passed"] for item in checks), + "checks": checks, + "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + } + (acceptance_root / "real_latest.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") + return report + + def run_deep_acceptance() -> dict[str, Any]: """Run minimal training loops for each model family without full datasets.""" acceptance_root = settings.project_root / "var" / "acceptance" diff --git a/backend/app/agents/evaluation_agent.py b/backend/app/agents/evaluation_agent.py index a414f4b..761f22a 100644 --- a/backend/app/agents/evaluation_agent.py +++ b/backend/app/agents/evaluation_agent.py @@ -102,6 +102,12 @@ def evaluate_project() -> dict: "deep_acceptance_ui": "runDeepAcceptance" in frontend_text and "深度训练" in frontend_text, "deep_yolo_heatmap_validation": "yolo_tiny_heatmap_generation" in acceptance_text, "uploaded_yolo_workflow_acceptance": "uploaded_yolo_predict_job_runner" in acceptance_text and "uploaded_yolo_heatmap_job_runner" in acceptance_text, + "real_workspace_acceptance": "/api/acceptance/real" in backend_text + and "runRealAcceptance" in frontend_text + and "真实数据" in frontend_text + and "real_workspace_yolo_predict_job_runner" in acceptance_text + and "real_workspace_yolo_heatmap_job_runner" in acceptance_text + and "real_workspace_stack_job_runner" in acceptance_text, "agent_api": "/api/agents/evaluate" in backend_text and "/api/agents/validate" in backend_text, "agent_panel_ui": "runAgentValidation" in frontend_text and "评价建议" in frontend_text and "Validation Agent" in frontend_text, "coverage_api": "/api/coverage" in backend_text and coverage["task_build_passed"], @@ -126,7 +132,7 @@ def evaluate_project() -> dict: if coverage["unmapped_user_scripts"]: suggestions.append(f"Map remaining user-facing scripts: {len(coverage['unmapped_user_scripts'])}") if not suggestions: - suggestions.append("Current platform covers the requested control-plane features, uploaded YOLO dataset train/predict/heatmap actions, live uploaded-data YOLO predict/heatmap acceptance, and synthetic deep training acceptance; next focus is a real non-synthetic dataset run.") + suggestions.append("Current platform covers the requested control-plane features, uploaded YOLO dataset train/predict/heatmap actions, live uploaded-data YOLO predict/heatmap acceptance, real workspace data acceptance, and synthetic deep training acceptance; next focus is a longer operator-run task on a full dataset.") score = sum(1 for item in checks if item["passed"]) / max(len(checks), 1) return { diff --git a/backend/app/agents/validation_agent.py b/backend/app/agents/validation_agent.py index a76799c..6be8be9 100644 --- a/backend/app/agents/validation_agent.py +++ b/backend/app/agents/validation_agent.py @@ -8,7 +8,7 @@ import urllib.error import urllib.request from pathlib import Path -from ..acceptance import run_deep_acceptance, run_live_acceptance +from ..acceptance import run_deep_acceptance, run_live_acceptance, run_real_dataset_acceptance from ..capabilities import get_capability_matrix from ..catalog import get_catalog from ..config import settings @@ -42,7 +42,12 @@ def _fetch(url: str, timeout: int = 5) -> dict: return {"url": url, "error": str(exc), "passed": False} -def validate_project(run_build: bool = False, run_acceptance: bool | None = None, run_deep: bool | None = None) -> dict: +def validate_project( + run_build: bool = False, + run_acceptance: bool | None = None, + run_deep: bool | None = None, + run_real: bool | None = None, +) -> dict: """Validate current runtime readiness without launching heavy training.""" checks = [] catalog = get_catalog() @@ -143,9 +148,13 @@ def validate_project(run_build: bool = False, run_acceptance: bool | None = None checks.append({"name": "live_frontend_index", "passed": frontend["passed"] and "Seg Data Server" in frontend.get("body", ""), "detail": frontend}) acceptance_enabled = run_acceptance if run_acceptance is not None else os.getenv("SEG_VALIDATE_ACCEPTANCE", "1") == "1" deep_enabled = run_deep if run_deep is not None else os.getenv("SEG_VALIDATE_DEEP", "1") == "1" + real_enabled = run_real if run_real is not None else os.getenv("SEG_VALIDATE_REAL", "0") == "1" if acceptance_enabled: acceptance = run_live_acceptance(backend_url) checks.append({"name": "live_acceptance_smoke", "passed": acceptance["passed"], "detail": acceptance}) + if real_enabled: + real_acceptance = run_real_dataset_acceptance(backend_url) + checks.append({"name": "real_workspace_acceptance", "passed": real_acceptance["passed"], "detail": real_acceptance}) if deep_enabled: deep_acceptance = run_deep_acceptance() checks.append({"name": "deep_training_acceptance", "passed": deep_acceptance["passed"], "detail": deep_acceptance}) diff --git a/backend/app/main.py b/backend/app/main.py index aa27ae7..e16c575 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -9,7 +9,14 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, StreamingResponse from . import db -from .acceptance import latest_acceptance_report, latest_deep_acceptance_report, run_deep_acceptance, run_live_acceptance +from .acceptance import ( + latest_acceptance_report, + latest_deep_acceptance_report, + latest_real_acceptance_report, + run_deep_acceptance, + run_live_acceptance, + run_real_dataset_acceptance, +) from .capabilities import get_capability_matrix from .catalog import get_catalog from .config import settings @@ -114,6 +121,16 @@ def api_deep_acceptance() -> dict: return run_deep_acceptance() +@app.get("/api/acceptance/real/latest") +def api_real_acceptance_latest() -> dict: + return latest_real_acceptance_report() + + +@app.post("/api/acceptance/real") +def api_real_acceptance(base_url: str = "http://127.0.0.1:8010") -> dict: + return run_real_dataset_acceptance(base_url) + + @app.get("/api/datasets") def api_datasets() -> list[dict]: return list_uploaded_datasets() @@ -273,5 +290,5 @@ def api_agent_evaluate() -> dict: @app.get("/api/agents/validate") -def api_agent_validate(run_build: bool = False, run_acceptance: bool = False, run_deep: bool = False) -> dict: - return validate_project(run_build=run_build, run_acceptance=run_acceptance, run_deep=run_deep) +def api_agent_validate(run_build: bool = False, run_acceptance: bool = False, run_deep: bool = False, run_real: bool = False) -> dict: + return validate_project(run_build=run_build, run_acceptance=run_acceptance, run_deep=run_deep, run_real=run_real) diff --git a/backend/tests/test_acceptance.py b/backend/tests/test_acceptance.py new file mode 100644 index 0000000..35b9c39 --- /dev/null +++ b/backend/tests/test_acceptance.py @@ -0,0 +1,13 @@ +from pathlib import Path + +from app.acceptance import find_real_workspace_samples + + +def test_find_real_workspace_samples_uses_existing_seg_data(): + samples = find_real_workspace_samples() + + assert samples["passed"] is True + assert Path(samples["mask_pair"]["image"]).exists() + assert Path(samples["mask_pair"]["mask"]).exists() + assert Path(samples["yolo_pair"]["image"]).exists() + assert Path(samples["yolo_pair"]["label"]).exists() diff --git a/backend/tests/test_agents.py b/backend/tests/test_agents.py index 26f76a7..82d5fc1 100644 --- a/backend/tests/test_agents.py +++ b/backend/tests/test_agents.py @@ -6,6 +6,8 @@ def test_evaluation_agent_returns_checks(): result = evaluate_project() assert result["agent"] == "evaluation_suggestion_agent" assert result["checks"] + checks = {item["name"]: item["passed"] for item in result["checks"]} + assert checks["real_workspace_acceptance"] is True def test_validation_agent_lightweight(monkeypatch): diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index b798015..7ebef81 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -373,6 +373,7 @@ function useData() { const [datasetValidations, setDatasetValidations] = useState>({}); const [coverage, setCoverage] = useState(null); const [acceptance, setAcceptance] = useState(null); + const [realAcceptance, setRealAcceptance] = useState(null); const [deepAcceptance, setDeepAcceptance] = useState(null); const [runtimeReadiness, setRuntimeReadiness] = useState(null); const [capabilities, setCapabilities] = useState(null); @@ -381,7 +382,7 @@ function useData() { async function refresh() { try { - const [catalogNext, gpusNext, envsNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, weightsNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext, agentEvaluationNext] = await Promise.all([ + const [catalogNext, gpusNext, envsNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, weightsNext, datasetsNext, coverageNext, acceptanceNext, realAcceptanceNext, deepAcceptanceNext, agentEvaluationNext] = await Promise.all([ api("/api/catalog"), api("/api/system/gpus"), api("/api/system/envs"), @@ -394,6 +395,7 @@ function useData() { api("/api/datasets"), api("/api/coverage"), api("/api/acceptance/latest"), + api("/api/acceptance/real/latest"), api("/api/acceptance/deep/latest"), api("/api/agents/evaluate") ]); @@ -421,6 +423,7 @@ function useData() { setDatasetValidations(Object.fromEntries(validationEntries)); setCoverage(coverageNext); setAcceptance(acceptanceNext); + setRealAcceptance(realAcceptanceNext); setDeepAcceptance(deepAcceptanceNext); setAgentEvaluation(agentEvaluationNext); setError(""); @@ -435,7 +438,7 @@ function useData() { return () => window.clearInterval(timer); }, []); - return { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh }; + return { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, deepAcceptance, error, refresh }; } function StatusPill({ status }: { status: string }) { @@ -458,7 +461,7 @@ function JobProgressBar({ progress }: { progress?: JobProgress }) { } function App() { - const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData(); + const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, deepAcceptance, error, refresh } = useData(); const [taskType, setTaskType] = useState("mock.echo"); const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2)); const [selectedJob, setSelectedJob] = useState(null); @@ -652,6 +655,16 @@ function App() { } } + async function runRealAcceptance() { + setBusy(true); + try { + await api("/api/acceptance/real", { method: "POST" }); + await refresh(); + } finally { + setBusy(false); + } + } + async function runAgentValidation() { setAgentBusy(true); try { @@ -1111,6 +1124,9 @@ function App() { + @@ -1137,6 +1153,10 @@ function App() { 模型族 {acceptance?.model_family_readiness?.passed ? "OK" : "Check"} +
+ 真实数据 + {realAcceptance?.available === false ? "New" : realAcceptance?.passed ? "OK" : "Check"} +
深度训练 {deepAcceptance?.available === false ? "New" : deepAcceptance?.passed ? "OK" : "Check"} @@ -1147,6 +1167,7 @@ function App() { <> 当前用户侧脚本已全部映射到网页任务。 最近验收:{acceptance?.created_at ?? "尚未运行"} {acceptance?.run_id ? `#${acceptance.run_id}` : ""} + 真实数据:{realAcceptance?.created_at ?? "尚未运行"} {realAcceptance?.run_id ? `#${realAcceptance.run_id}` : ""},通过 {realAcceptance?.checks?.filter((item) => item.passed).length ?? 0}/{realAcceptance?.checks?.length ?? 0} 深度验收:{deepAcceptance?.created_at ?? "尚未运行"} {deepAcceptance?.run_id ? `#${deepAcceptance.run_id}` : ""},通过 {deepAcceptance?.checks?.filter((item) => item.passed).length ?? 0}/{deepAcceptance?.checks?.length ?? 0} 模型族 readiness:{acceptance?.model_family_readiness?.checks?.filter((item) => item.passed).length ?? 0}/{acceptance?.model_family_readiness?.checks?.length ?? 0},warnings {acceptance?.model_family_readiness?.warnings?.length ?? 0} diff --git a/scripts/run_backend.sh b/scripts/run_backend.sh index e0b9ffd..73aaca4 100755 --- a/scripts/run_backend.sh +++ b/scripts/run_backend.sh @@ -12,6 +12,14 @@ fi BACKEND_ENV="${SEG_BACKEND_CONDA_ENV:-seg_smp}" HOST="${SEG_BACKEND_HOST:-0.0.0.0}" PORT="${SEG_BACKEND_PORT:-8010}" +RELOAD="${SEG_BACKEND_RELOAD:-1}" cd "${ROOT_DIR}" -exec conda run -n "${BACKEND_ENV}" uvicorn app.main:app --app-dir backend --host "${HOST}" --port "${PORT}" --reload +args=(uvicorn app.main:app --app-dir backend --host "${HOST}" --port "${PORT}") +case "${RELOAD,,}" in + 1|true|yes|on) + args+=(--reload) + ;; +esac + +exec conda run -n "${BACKEND_ENV}" "${args[@]}" diff --git a/scripts/run_frontend.sh b/scripts/run_frontend.sh index 658601e..78fca8d 100755 --- a/scripts/run_frontend.sh +++ b/scripts/run_frontend.sh @@ -9,6 +9,9 @@ if [[ -f "${ROOT_DIR}/.env" ]]; then set +a fi +HOST="${SEG_FRONTEND_HOST:-0.0.0.0}" +PORT="${SEG_FRONTEND_PORT:-5173}" + cd "${ROOT_DIR}/frontend" npm install -exec npm run dev -- --host 0.0.0.0 +exec npm run dev -- --host "${HOST}" --port "${PORT}"