Add real workspace acceptance
This commit is contained in:
@@ -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
|
||||||
|
|||||||
76
README.md
76
README.md
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
13
backend/tests/test_acceptance.py
Normal file
13
backend/tests/test_acceptance.py
Normal 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()
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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[@]}"
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
Reference in New Issue
Block a user