diff --git a/README.md b/README.md index 00e3bcf..bea7031 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,15 @@ classified as supporting artifacts rather than direct web actions. The same panel can run `POST /api/acceptance/smoke`, a lightweight live smoke that creates an upload dataset, uploads a label, downloads it through the artifact API, runs a mock job, checks SSE log streaming, and executes one -legacy image/label overlay job on tiny generated PNGs. +legacy image/label overlay job on tiny generated PNGs. It also runs model +family readiness checks: a SegModel/SMP forward pass, a YOLO segmentation +prediction on a tiny image, MMSeg config parsing, and local MMSeg pretrained +weight discovery. + +Current `seg_smp` uses `mmcv-lite` because no `torch 2.6/cu124` full `mmcv` +wheel is available on this machine and `nvcc` is not installed for source +builds. The acceptance smoke reports MMSeg full model construction as a +warning until a full `mmcv` build with `mmcv._ext` is installed. ## Weight Sync diff --git a/backend/app/acceptance.py b/backend/app/acceptance.py index b600589..0eba587 100644 --- a/backend/app/acceptance.py +++ b/backend/app/acceptance.py @@ -1,6 +1,8 @@ from __future__ import annotations import json +import subprocess +import sys import time import uuid import urllib.error @@ -11,6 +13,22 @@ from typing import Any from .config import settings +def _run_snippet(code: str, cwd: Path | None = None, timeout: int = 60) -> dict[str, Any]: + result = subprocess.run( + [sys.executable, "-c", code], + cwd=str(cwd or settings.project_root), + capture_output=True, + text=True, + timeout=timeout, + ) + return { + "passed": result.returncode == 0, + "returncode": result.returncode, + "stdout": result.stdout[-4000:], + "stderr": result.stderr[-4000:], + } + + def _request_json(method: str, url: str, payload: dict[str, Any] | None = None, timeout: int = 10) -> dict[str, Any]: data = None headers = {"Accept": "application/json"} @@ -122,6 +140,68 @@ def _write_acceptance_images(root: Path) -> tuple[Path, Path, Path]: return image_path, label_path, result_dir +def run_model_family_readiness() -> dict[str, Any]: + """Exercise the model-family runtime stack without launching full training.""" + source = settings.source_root + yolo_weight = source / "Seg_All_In_One_YoloModel" / "yolo11n-seg.pt" + mmseg_config = source / "Seg_All_In_One_MMSeg" / "configs" / "fcn" / "fcn_r18-d8_4xb2-80k_cityscapes-512x1024.py" + mmseg_pretrained = source / "Seg_All_In_One_MMSeg" / "My_Local_Model" / "mmcls" / "resnet18.pth" + + checks = [ + { + "name": "segmodel_smp_forward", + "required": True, + "detail": _run_snippet( + "import torch, segmentation_models_pytorch as smp; " + "m=smp.Unet(encoder_name='resnet18', encoder_weights=None, classes=2).eval(); " + "torch.set_grad_enabled(False); y=m(torch.randn(1,3,64,64)); " + "print(tuple(y.shape))" + ), + }, + { + "name": "yolo_seg_predict_cpu", + "required": True, + "detail": _run_snippet( + "from ultralytics import YOLO; import numpy as np; " + f"model=YOLO({str(yolo_weight)!r}); " + "r=model.predict(np.zeros((64,64,3), dtype=np.uint8), imgsz=64, verbose=False, save=False, device='cpu'); " + "print(len(r), r[0].orig_shape)" + ), + }, + { + "name": "mmseg_config_parse", + "required": True, + "detail": _run_snippet( + "from mmengine.config import Config; " + f"cfg=Config.fromfile({str(mmseg_config)!r}); " + "print(cfg.model.type, cfg.train_dataloader.batch_size)" + ), + }, + { + "name": "mmseg_local_pretrained_weight", + "required": True, + "detail": {"passed": mmseg_pretrained.exists(), "path": str(mmseg_pretrained), "size": mmseg_pretrained.stat().st_size if mmseg_pretrained.exists() else 0}, + }, + { + "name": "mmseg_full_model_build", + "required": False, + "detail": _run_snippet( + "from mmengine.config import Config; from mmseg.registry import MODELS; " + f"cfg=Config.fromfile({str(mmseg_config)!r}); " + "model=MODELS.build(cfg.model); print(type(model).__name__)", + timeout=90, + ), + }, + ] + for check in checks: + check["passed"] = bool(check["detail"].get("passed")) + return { + "passed": all(item["passed"] for item in checks if item["required"]), + "warnings": [item for item in checks if not item["required"] and not item["passed"]], + "checks": checks, + } + + def latest_acceptance_report() -> dict[str, Any]: path = settings.project_root / "var" / "acceptance" / "latest.json" if not path.exists(): @@ -188,12 +268,16 @@ def run_live_acceptance(base_url: str = "http://127.0.0.1:8010") -> dict[str, An } ) + readiness = run_model_family_readiness() + checks.append({"name": "model_family_readiness", "passed": readiness["passed"], "detail": readiness}) + report = { "available": True, "run_id": run_id, "base_url": base_url, "passed": all(item["passed"] for item in checks), "checks": checks, + "model_family_readiness": readiness, "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), } latest = acceptance_root / "latest.json" diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 3f4b7e1..58c19e8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -78,6 +78,11 @@ type AcceptancePayload = { run_id?: string; created_at?: string; checks?: Array<{ name: string; passed: boolean }>; + model_family_readiness?: { + passed: boolean; + warnings: Array<{ name: string; passed: boolean }>; + checks: Array<{ name: string; passed: boolean; required: boolean }>; + }; }; type GpuPayload = { @@ -539,12 +544,17 @@ function App() { 轻量验收 {acceptance?.available === false ? "New" : acceptance?.passed ? "OK" : "Check"} +
+ 模型族 + {acceptance?.model_family_readiness?.passed ? "OK" : "Check"} +
{(coverage?.unmapped_user_scripts.length ?? 0) === 0 ? ( <> 当前用户侧脚本已全部映射到网页任务。 最近验收:{acceptance?.created_at ?? "尚未运行"} {acceptance?.run_id ? `#${acceptance.run_id}` : ""} + 模型族 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} ) : ( coverage?.unmapped_user_scripts.slice(0, 8).map((item) => {item}) diff --git a/frontend/src/styles.css b/frontend/src/styles.css index c41016e..2b47ec3 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -399,7 +399,7 @@ textarea { .coverageGrid { display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; }