Add real workspace acceptance

This commit is contained in:
2026-06-30 17:33:15 +08:00
parent 4eb9452760
commit 53b81dd04d
11 changed files with 418 additions and 18 deletions

View File

@@ -2,10 +2,15 @@ SEG_SOURCE_ROOT=../Seg
SEG_DATA_SERVER_ROOT=. 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_BACKEND_HOST=0.0.0.0
SEG_BACKEND_PORT=8010
SEG_BACKEND_RELOAD=1
SEG_TASK_CONDA_ENV=seg_smp SEG_TASK_CONDA_ENV=seg_smp
SEG_MMSEG_CONDA_ENV=seg_mmcv SEG_MMSEG_CONDA_ENV=seg_mmcv
SEG_BACKEND_CONDA_ENV=seg_smp SEG_BACKEND_CONDA_ENV=seg_smp
SEG_WEIGHT_MODE=copy SEG_WEIGHT_MODE=copy
SEG_ENABLE_SHELL_TASKS=1 SEG_ENABLE_SHELL_TASKS=1
SEG_VALIDATE_DEEP=1 SEG_VALIDATE_DEEP=1
SEG_FRONTEND_HOST=0.0.0.0
SEG_FRONTEND_PORT=5173
VITE_API_BASE=http://localhost:8010 VITE_API_BASE=http://localhost:8010

View File

@@ -69,15 +69,39 @@ SEG_SOURCE_ROOT=../Seg
SEG_DATA_SERVER_ROOT=. 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_BACKEND_HOST=0.0.0.0
SEG_BACKEND_PORT=8010
SEG_BACKEND_RELOAD=1
SEG_TASK_CONDA_ENV=seg_smp SEG_TASK_CONDA_ENV=seg_smp
SEG_MMSEG_CONDA_ENV=seg_mmcv SEG_MMSEG_CONDA_ENV=seg_mmcv
SEG_BACKEND_CONDA_ENV=seg_smp SEG_BACKEND_CONDA_ENV=seg_smp
SEG_WEIGHT_MODE=copy SEG_WEIGHT_MODE=copy
SEG_ENABLE_SHELL_TASKS=1 SEG_ENABLE_SHELL_TASKS=1
SEG_VALIDATE_DEEP=1 SEG_VALIDATE_DEEP=1
SEG_FRONTEND_HOST=0.0.0.0
SEG_FRONTEND_PORT=5173
VITE_API_BASE=http://localhost:8010 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 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 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 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 scripts/run_frontend.sh
``` ```
Defaults are `0.0.0.0:8010` for the backend and Vite's dev port for the Defaults are `0.0.0.0:8010` for the backend and `0.0.0.0:5173` for the
frontend. Override backend binding with `SEG_BACKEND_HOST` and frontend development server. Override backend binding with `SEG_BACKEND_HOST`
`SEG_BACKEND_PORT`. For a production process manager such as systemd, and `SEG_BACKEND_PORT`; override the Vite dev server with `SEG_FRONTEND_HOST`
supervisor, or Docker Compose, call the same two scripts so `.env` and the and `SEG_FRONTEND_PORT`.
configured conda environments are used consistently.
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: 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 `SEG_MMSEG_CONDA_ENV` by importing `mmcv._ext` and building a local MMSeg
`EncoderDecoder` from the existing config tree. `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 For stronger runtime proof, `POST /api/acceptance/deep` runs minimal training
loops for the three model families: one SegModel optimizer step, one YOLO loops for the three model families: one SegModel optimizer step, one YOLO
segmentation epoch on a synthetic 64x64 dataset, one YOLO GradCAM heatmap 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: The web dashboard calls validation in light mode by default:
`/api/agents/validate?run_build=false&run_acceptance=false&run_deep=false`. `/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 Pass `run_acceptance=true`, `run_real=true`, or `run_deep=true` only when you
agent to launch the heavier runtime acceptance checks from the browser/API. explicitly want the agent to launch the heavier runtime acceptance checks from
the browser/API.

View File

@@ -12,6 +12,8 @@ from typing import Any
from .config import settings 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]: def _run_command(command: list[str], cwd: Path | None = None, timeout: int = 60) -> dict[str, Any]:
try: try:
@@ -183,6 +185,23 @@ def _request_text(url: str, timeout: int = 10) -> dict[str, Any]:
return {"passed": False, "error": str(exc)} 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]: 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}" boundary = f"----SegAcceptance{uuid.uuid4().hex}"
body = b"".join( 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) 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]: def run_model_family_readiness() -> dict[str, Any]:
"""Exercise the model-family runtime stack without launching full training.""" """Exercise the model-family runtime stack without launching full training."""
source = settings.source_root 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")) 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]: def run_deep_acceptance() -> dict[str, Any]:
"""Run minimal training loops for each model family without full datasets.""" """Run minimal training loops for each model family without full datasets."""
acceptance_root = settings.project_root / "var" / "acceptance" acceptance_root = settings.project_root / "var" / "acceptance"

View File

@@ -102,6 +102,12 @@ def evaluate_project() -> dict:
"deep_acceptance_ui": "runDeepAcceptance" in frontend_text and "深度训练" in frontend_text, "deep_acceptance_ui": "runDeepAcceptance" in frontend_text and "深度训练" in frontend_text,
"deep_yolo_heatmap_validation": "yolo_tiny_heatmap_generation" in acceptance_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, "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_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, "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"], "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"]: if coverage["unmapped_user_scripts"]:
suggestions.append(f"Map remaining user-facing scripts: {len(coverage['unmapped_user_scripts'])}") suggestions.append(f"Map remaining user-facing scripts: {len(coverage['unmapped_user_scripts'])}")
if not suggestions: 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) score = sum(1 for item in checks if item["passed"]) / max(len(checks), 1)
return { return {

View File

@@ -8,7 +8,7 @@ import urllib.error
import urllib.request import urllib.request
from pathlib import Path 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 ..capabilities import get_capability_matrix
from ..catalog import get_catalog from ..catalog import get_catalog
from ..config import settings from ..config import settings
@@ -42,7 +42,12 @@ def _fetch(url: str, timeout: int = 5) -> dict:
return {"url": url, "error": str(exc), "passed": False} 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.""" """Validate current runtime readiness without launching heavy training."""
checks = [] checks = []
catalog = get_catalog() 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}) 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" 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" 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: if acceptance_enabled:
acceptance = run_live_acceptance(backend_url) acceptance = run_live_acceptance(backend_url)
checks.append({"name": "live_acceptance_smoke", "passed": acceptance["passed"], "detail": acceptance}) 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: if deep_enabled:
deep_acceptance = run_deep_acceptance() deep_acceptance = run_deep_acceptance()
checks.append({"name": "deep_training_acceptance", "passed": deep_acceptance["passed"], "detail": deep_acceptance}) checks.append({"name": "deep_training_acceptance", "passed": deep_acceptance["passed"], "detail": deep_acceptance})

View File

@@ -9,7 +9,14 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
from . import db 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 .capabilities import get_capability_matrix
from .catalog import get_catalog from .catalog import get_catalog
from .config import settings from .config import settings
@@ -114,6 +121,16 @@ def api_deep_acceptance() -> dict:
return run_deep_acceptance() 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") @app.get("/api/datasets")
def api_datasets() -> list[dict]: def api_datasets() -> list[dict]:
return list_uploaded_datasets() return list_uploaded_datasets()
@@ -273,5 +290,5 @@ def api_agent_evaluate() -> dict:
@app.get("/api/agents/validate") @app.get("/api/agents/validate")
def api_agent_validate(run_build: bool = False, run_acceptance: bool = False, run_deep: bool = False) -> dict: 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) return validate_project(run_build=run_build, run_acceptance=run_acceptance, run_deep=run_deep, run_real=run_real)

View File

@@ -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()

View File

@@ -6,6 +6,8 @@ def test_evaluation_agent_returns_checks():
result = evaluate_project() result = evaluate_project()
assert result["agent"] == "evaluation_suggestion_agent" assert result["agent"] == "evaluation_suggestion_agent"
assert result["checks"] 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): def test_validation_agent_lightweight(monkeypatch):

View File

@@ -373,6 +373,7 @@ function useData() {
const [datasetValidations, setDatasetValidations] = useState<Record<string, DatasetValidation>>({}); const [datasetValidations, setDatasetValidations] = useState<Record<string, DatasetValidation>>({});
const [coverage, setCoverage] = useState<CoveragePayload | null>(null); const [coverage, setCoverage] = useState<CoveragePayload | null>(null);
const [acceptance, setAcceptance] = useState<AcceptancePayload | null>(null); const [acceptance, setAcceptance] = useState<AcceptancePayload | null>(null);
const [realAcceptance, setRealAcceptance] = useState<AcceptancePayload | null>(null);
const [deepAcceptance, setDeepAcceptance] = useState<DeepAcceptancePayload | null>(null); const [deepAcceptance, setDeepAcceptance] = useState<DeepAcceptancePayload | null>(null);
const [runtimeReadiness, setRuntimeReadiness] = useState<RuntimeReadinessPayload | null>(null); const [runtimeReadiness, setRuntimeReadiness] = useState<RuntimeReadinessPayload | null>(null);
const [capabilities, setCapabilities] = useState<CapabilityPayload | null>(null); const [capabilities, setCapabilities] = useState<CapabilityPayload | null>(null);
@@ -381,7 +382,7 @@ function useData() {
async function refresh() { async function refresh() {
try { 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<Catalog>("/api/catalog"), api<Catalog>("/api/catalog"),
api<GpuPayload>("/api/system/gpus"), api<GpuPayload>("/api/system/gpus"),
api<CondaEnvPayload>("/api/system/envs"), api<CondaEnvPayload>("/api/system/envs"),
@@ -394,6 +395,7 @@ function useData() {
api<UploadedDataset[]>("/api/datasets"), api<UploadedDataset[]>("/api/datasets"),
api<CoveragePayload>("/api/coverage"), api<CoveragePayload>("/api/coverage"),
api<AcceptancePayload>("/api/acceptance/latest"), api<AcceptancePayload>("/api/acceptance/latest"),
api<AcceptancePayload>("/api/acceptance/real/latest"),
api<DeepAcceptancePayload>("/api/acceptance/deep/latest"), api<DeepAcceptancePayload>("/api/acceptance/deep/latest"),
api<EvaluationAgentPayload>("/api/agents/evaluate") api<EvaluationAgentPayload>("/api/agents/evaluate")
]); ]);
@@ -421,6 +423,7 @@ function useData() {
setDatasetValidations(Object.fromEntries(validationEntries)); setDatasetValidations(Object.fromEntries(validationEntries));
setCoverage(coverageNext); setCoverage(coverageNext);
setAcceptance(acceptanceNext); setAcceptance(acceptanceNext);
setRealAcceptance(realAcceptanceNext);
setDeepAcceptance(deepAcceptanceNext); setDeepAcceptance(deepAcceptanceNext);
setAgentEvaluation(agentEvaluationNext); setAgentEvaluation(agentEvaluationNext);
setError(""); setError("");
@@ -435,7 +438,7 @@ function useData() {
return () => window.clearInterval(timer); 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 }) { function StatusPill({ status }: { status: string }) {
@@ -458,7 +461,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, 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 [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);
@@ -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() { async function runAgentValidation() {
setAgentBusy(true); setAgentBusy(true);
try { try {
@@ -1111,6 +1124,9 @@ function App() {
<button className="iconButton" disabled={busy} onClick={runAcceptanceSmoke} title="运行轻量验收"> <button className="iconButton" disabled={busy} onClick={runAcceptanceSmoke} title="运行轻量验收">
<ClipboardCheck size={18} /> <ClipboardCheck size={18} />
</button> </button>
<button className="iconButton" disabled={busy} onClick={runRealAcceptance} title="运行真实数据验收">
<FileSearch size={18} />
</button>
<button className="iconButton" disabled={busy} onClick={runDeepAcceptance} title="运行深度训练验收"> <button className="iconButton" disabled={busy} onClick={runDeepAcceptance} title="运行深度训练验收">
<Activity size={18} /> <Activity size={18} />
</button> </button>
@@ -1137,6 +1153,10 @@ function App() {
<span></span> <span></span>
<strong>{acceptance?.model_family_readiness?.passed ? "OK" : "Check"}</strong> <strong>{acceptance?.model_family_readiness?.passed ? "OK" : "Check"}</strong>
</div> </div>
<div>
<span></span>
<strong>{realAcceptance?.available === false ? "New" : realAcceptance?.passed ? "OK" : "Check"}</strong>
</div>
<div> <div>
<span></span> <span></span>
<strong>{deepAcceptance?.available === false ? "New" : deepAcceptance?.passed ? "OK" : "Check"}</strong> <strong>{deepAcceptance?.available === false ? "New" : deepAcceptance?.passed ? "OK" : "Check"}</strong>
@@ -1147,6 +1167,7 @@ function App() {
<> <>
<span></span> <span></span>
<span>{acceptance?.created_at ?? "尚未运行"} {acceptance?.run_id ? `#${acceptance.run_id}` : ""}</span> <span>{acceptance?.created_at ?? "尚未运行"} {acceptance?.run_id ? `#${acceptance.run_id}` : ""}</span>
<span>{realAcceptance?.created_at ?? "尚未运行"} {realAcceptance?.run_id ? `#${realAcceptance.run_id}` : ""} {realAcceptance?.checks?.filter((item) => item.passed).length ?? 0}/{realAcceptance?.checks?.length ?? 0}</span>
<span>{deepAcceptance?.created_at ?? "尚未运行"} {deepAcceptance?.run_id ? `#${deepAcceptance.run_id}` : ""} {deepAcceptance?.checks?.filter((item) => item.passed).length ?? 0}/{deepAcceptance?.checks?.length ?? 0}</span> <span>{deepAcceptance?.created_at ?? "尚未运行"} {deepAcceptance?.run_id ? `#${deepAcceptance.run_id}` : ""} {deepAcceptance?.checks?.filter((item) => item.passed).length ?? 0}/{deepAcceptance?.checks?.length ?? 0}</span>
<span> 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}</span> <span> 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}</span>
</> </>

View File

@@ -12,6 +12,14 @@ fi
BACKEND_ENV="${SEG_BACKEND_CONDA_ENV:-seg_smp}" 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:-8010}" PORT="${SEG_BACKEND_PORT:-8010}"
RELOAD="${SEG_BACKEND_RELOAD:-1}"
cd "${ROOT_DIR}" 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[@]}"

View File

@@ -9,6 +9,9 @@ if [[ -f "${ROOT_DIR}/.env" ]]; then
set +a set +a
fi fi
HOST="${SEG_FRONTEND_HOST:-0.0.0.0}"
PORT="${SEG_FRONTEND_PORT:-5173}"
cd "${ROOT_DIR}/frontend" cd "${ROOT_DIR}/frontend"
npm install npm install
exec npm run dev -- --host 0.0.0.0 exec npm run dev -- --host "${HOST}" --port "${PORT}"