Add full capability readiness matrix
This commit is contained in:
@@ -44,10 +44,12 @@ def evaluate_project() -> dict:
|
||||
"loss_result_ui": "loss" in frontend_text.lower() and "heatmap" in frontend_text.lower() and "CurvePanel" in frontend_text,
|
||||
"job_progress_ui": "JobProgressBar" in frontend_text and "progressTrack" in frontend_text,
|
||||
"runtime_readiness_ui": "runtimeReadiness" in frontend_text and "环境就绪" in frontend_text,
|
||||
"capability_matrix_ui": "capabilities" in frontend_text and "全功能矩阵" in frontend_text,
|
||||
"dataset_api": "/api/datasets" in backend_text and "api_upload_dataset_files" in backend_text,
|
||||
"dataset_quality_api": "/api/datasets/{dataset_name}/validate" in backend_text and "/api/datasets/{dataset_name}/yolo-yaml" in backend_text,
|
||||
"job_progress_api": "progress_from_log_path" in backend_text and '"progress"' in backend_text,
|
||||
"runtime_readiness_api": "/api/system/readiness" in backend_text,
|
||||
"capability_matrix_api": "/api/capabilities" in backend_text,
|
||||
"runtime_bootstrap_scripts": (settings.project_root / "scripts" / "bootstrap_conda_envs.sh").exists()
|
||||
and (settings.project_root / "scripts" / "verify_runtime_envs.py").exists(),
|
||||
"curve_api": "/api/results/curves" in backend_text,
|
||||
|
||||
@@ -9,6 +9,7 @@ import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from ..acceptance import run_deep_acceptance, run_live_acceptance
|
||||
from ..capabilities import get_capability_matrix
|
||||
from ..catalog import get_catalog
|
||||
from ..config import settings
|
||||
from ..coverage import get_coverage_report
|
||||
@@ -66,6 +67,12 @@ def validate_project(run_build: bool = False) -> dict:
|
||||
checks.append({"name": "mmseg_env_exists", "passed": settings.mmseg_conda_env in env_names, "detail": {"env": settings.mmseg_conda_env}})
|
||||
runtime_readiness = get_runtime_readiness(force=True)
|
||||
checks.append({"name": "runtime_env_readiness", "passed": runtime_readiness["passed"], "detail": runtime_readiness})
|
||||
capability_matrix = get_capability_matrix()
|
||||
checks.append({
|
||||
"name": "capability_matrix_ready",
|
||||
"passed": capability_matrix["passed"] and capability_matrix["summary"]["ready_domains"] == capability_matrix["summary"]["total_domains"],
|
||||
"detail": capability_matrix,
|
||||
})
|
||||
|
||||
smoke = _run(
|
||||
[
|
||||
@@ -105,6 +112,7 @@ def validate_project(run_build: bool = False) -> dict:
|
||||
datasets = _fetch(f"{backend_url}/api/datasets")
|
||||
live_jobs = _fetch(f"{backend_url}/api/jobs")
|
||||
live_readiness = _fetch(f"{backend_url}/api/system/readiness")
|
||||
live_capabilities = _fetch(f"{backend_url}/api/capabilities")
|
||||
live_coverage = _fetch(f"{backend_url}/api/coverage")
|
||||
live_curves = _fetch(f"{backend_url}/api/results/curves")
|
||||
frontend = _fetch(frontend_url)
|
||||
@@ -124,6 +132,11 @@ def validate_project(run_build: bool = False) -> dict:
|
||||
"passed": live_readiness["passed"] and '"passed":true' in live_readiness.get("body", "").replace(" ", ""),
|
||||
"detail": live_readiness,
|
||||
})
|
||||
checks.append({
|
||||
"name": "live_capability_matrix_api",
|
||||
"passed": live_capabilities["passed"] and '"passed":true' in live_capabilities.get("body", "").replace(" ", ""),
|
||||
"detail": live_capabilities,
|
||||
})
|
||||
checks.append({"name": "live_coverage_api", "passed": live_coverage["passed"] and '"task_build_passed":true' in live_coverage.get("body", "").replace(" ", ""), "detail": live_coverage})
|
||||
checks.append({"name": "live_training_curves_api", "passed": live_curves["passed"] and live_curves.get("body", "").lstrip().startswith("["), "detail": live_curves})
|
||||
checks.append({"name": "live_frontend_index", "passed": frontend["passed"] and "Seg Data Server" in frontend.get("body", ""), "detail": frontend})
|
||||
|
||||
299
backend/app/capabilities.py
Normal file
299
backend/app/capabilities.py
Normal file
@@ -0,0 +1,299 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from .acceptance import latest_acceptance_report, latest_deep_acceptance_report
|
||||
from .catalog import get_catalog
|
||||
from .coverage import get_coverage_report
|
||||
from .modules.dataset.service import list_uploaded_datasets
|
||||
from .modules.results.service import scan_results, scan_training_curves
|
||||
from .modules.system.service import get_gpus, get_runtime_readiness
|
||||
from .modules.weights.service import load_manifest
|
||||
|
||||
|
||||
CAPABILITY_GROUPS = [
|
||||
{
|
||||
"id": "dataset",
|
||||
"label": "Dataset",
|
||||
"description": "上传图片、label、mask,并执行预处理、配对、叠加、拼接、视频抽帧和 YOLO 数据构建。",
|
||||
"required_tasks": [
|
||||
"dataset.rename",
|
||||
"dataset.to_png",
|
||||
"dataset.resize",
|
||||
"dataset.pair",
|
||||
"dataset.rebuild_labels",
|
||||
"dataset.stack",
|
||||
"dataset.stitch",
|
||||
"dataset.video_frames",
|
||||
"dataset.yolo_txt_sort",
|
||||
"dataset.yolo_convert_png",
|
||||
"dataset.yolo_resize",
|
||||
],
|
||||
"task_prefixes": ["dataset."],
|
||||
"evidence_roles": ["segmentation"],
|
||||
},
|
||||
{
|
||||
"id": "segmodel",
|
||||
"label": "SegModel",
|
||||
"description": "SMP/SegModel 单模型和批量训练、预测、raw mask、指标、FLOPs/FPS。",
|
||||
"required_tasks": [
|
||||
"segmodel.train",
|
||||
"segmodel.batch_train",
|
||||
"segmodel.predict",
|
||||
"segmodel.batch_predict",
|
||||
"segmodel.flops",
|
||||
"segmodel.benchmark",
|
||||
"segmodel.raw_mask_check",
|
||||
"segmodel.metrics",
|
||||
],
|
||||
"task_prefixes": ["segmodel."],
|
||||
"runtime_roles": ["backend_task"],
|
||||
"families": ["segmodel"],
|
||||
},
|
||||
{
|
||||
"id": "yolo",
|
||||
"label": "YOLO",
|
||||
"description": "YOLOv8/v9/v11/v12 分割训练、上传数据集训练、预测、热度图、对比、视频预测和 raw mask。",
|
||||
"required_tasks": [
|
||||
"yolo.train",
|
||||
"yolo.train_custom",
|
||||
"yolo.predict",
|
||||
"yolo.batch_predict",
|
||||
"yolo.heatmap",
|
||||
"yolo.compare",
|
||||
"yolo.raw_mask_check",
|
||||
"yolo.video_visible",
|
||||
"yolo.video_unvisible",
|
||||
],
|
||||
"task_prefixes": ["yolo."],
|
||||
"runtime_roles": ["backend_task"],
|
||||
"families": ["yolo"],
|
||||
"evidence_roles": ["heatmap", "segmentation", "curve", "video"],
|
||||
},
|
||||
{
|
||||
"id": "mmseg",
|
||||
"label": "MMSeg",
|
||||
"description": "MMSeg 数据/算法配置生成、31 个算法、训练、预测、指标、FLOPs/FPS、绘图和 loss/mIoU 提取。",
|
||||
"required_tasks": [
|
||||
"mmseg.generate_data",
|
||||
"mmseg.generate_alg",
|
||||
"mmseg.train",
|
||||
"mmseg.predict_v1",
|
||||
"mmseg.predict_v2",
|
||||
"mmseg.metrics",
|
||||
"mmseg.flops_fps",
|
||||
"mmseg.draw",
|
||||
"mmseg.extract_loss_miou",
|
||||
],
|
||||
"task_prefixes": ["mmseg."],
|
||||
"runtime_roles": ["mmseg_full"],
|
||||
"families": ["mmseg"],
|
||||
},
|
||||
{
|
||||
"id": "visual",
|
||||
"label": "Visual Tools",
|
||||
"description": "可视化工具训练、推理、FPS、YOLO11 热度图、label 转换和 8-bit PNG 生成。",
|
||||
"required_tasks": [
|
||||
"visual.train",
|
||||
"visual.inference",
|
||||
"visual.fps",
|
||||
"visual.yolo11_heatmap_v1",
|
||||
"visual.yolo11_heatmap_v2",
|
||||
"visual.deal_labels",
|
||||
],
|
||||
"task_prefixes": ["visual."],
|
||||
"runtime_roles": ["backend_task"],
|
||||
"families": ["tool"],
|
||||
"evidence_roles": ["heatmap", "segmentation"],
|
||||
},
|
||||
{
|
||||
"id": "analysis",
|
||||
"label": "Analysis",
|
||||
"description": "合并 SegModel/MMSeg/YOLO 指标,生成 CSV、表格、图表和性能摘要。",
|
||||
"required_tasks": ["analysis.all"],
|
||||
"task_prefixes": ["analysis."],
|
||||
"families": ["analysis"],
|
||||
},
|
||||
{
|
||||
"id": "system",
|
||||
"label": "System",
|
||||
"description": "GPU 查询、环境检查、权重同步、任务日志/进度、取消和备份入口。",
|
||||
"required_tasks": ["system.backup", "system.check_graph_card"],
|
||||
"task_prefixes": ["system.", "mock."],
|
||||
"runtime_roles": ["backend_task", "mmseg_full"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _tasks_for_group(all_tasks: list[str], prefixes: list[str]) -> list[str]:
|
||||
return sorted(task for task in all_tasks if any(task.startswith(prefix) for prefix in prefixes))
|
||||
|
||||
|
||||
def _artifact_matches(item: dict[str, Any], group: dict[str, Any]) -> bool:
|
||||
families = set(group.get("families", []))
|
||||
roles = set(group.get("evidence_roles", []))
|
||||
return (not families or item.get("family") in families) and (not roles or item.get("role") in roles)
|
||||
|
||||
|
||||
def _coverage_build_task_set(coverage: dict[str, Any]) -> set[str]:
|
||||
return {item["task"] for item in coverage.get("task_build_checks", []) if item.get("passed")}
|
||||
|
||||
|
||||
def _runtime_map(readiness: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
return {item["role"]: item for item in readiness.get("envs", [])}
|
||||
|
||||
|
||||
def get_capability_matrix() -> dict[str, Any]:
|
||||
catalog = get_catalog()
|
||||
coverage = get_coverage_report()
|
||||
readiness = get_runtime_readiness(force=False)
|
||||
results = scan_results(limit=1000)
|
||||
curves = scan_training_curves(limit=50)
|
||||
datasets = list_uploaded_datasets()
|
||||
manifest = load_manifest()
|
||||
gpus = get_gpus()
|
||||
acceptance = latest_acceptance_report()
|
||||
deep_acceptance = latest_deep_acceptance_report()
|
||||
|
||||
all_tasks = catalog["task_types"]
|
||||
buildable_tasks = _coverage_build_task_set(coverage)
|
||||
runtime_by_role = _runtime_map(readiness)
|
||||
domains = []
|
||||
|
||||
for group in CAPABILITY_GROUPS:
|
||||
group_tasks = _tasks_for_group(all_tasks, group["task_prefixes"])
|
||||
required_tasks = group["required_tasks"]
|
||||
missing_required = [task for task in required_tasks if task not in all_tasks]
|
||||
unbuildable_required = [task for task in required_tasks if task in all_tasks and task not in buildable_tasks]
|
||||
runtime_roles = group.get("runtime_roles", [])
|
||||
runtime_reports = [runtime_by_role.get(role) for role in runtime_roles if runtime_by_role.get(role)]
|
||||
runtime_ready = all(item.get("passed") for item in runtime_reports) if runtime_roles else True
|
||||
artifacts = [item for item in results if _artifact_matches(item, group)]
|
||||
group_curves = [item for item in curves if not group.get("families") or item.get("family") in group["families"]]
|
||||
evidence_count = len(artifacts) + len(group_curves)
|
||||
|
||||
if group["id"] == "dataset":
|
||||
evidence_count += sum(dataset["counts"]["images"] + dataset["counts"]["labels"] + dataset["counts"]["masks"] for dataset in datasets)
|
||||
if group["id"] == "mmseg":
|
||||
evidence_count += len(catalog.get("mmseg_algorithms", []))
|
||||
if group["id"] == "system":
|
||||
evidence_count += int(bool(gpus.get("available"))) + int(manifest.get("count", 0) > 0)
|
||||
|
||||
gaps = []
|
||||
if missing_required:
|
||||
gaps.append(f"missing tasks: {', '.join(missing_required[:4])}")
|
||||
if unbuildable_required:
|
||||
gaps.append(f"unbuildable tasks: {', '.join(unbuildable_required[:4])}")
|
||||
if runtime_roles and not runtime_ready:
|
||||
gaps.append("runtime env check failed")
|
||||
|
||||
domains.append(
|
||||
{
|
||||
"id": group["id"],
|
||||
"label": group["label"],
|
||||
"description": group["description"],
|
||||
"ready": not missing_required and not unbuildable_required and runtime_ready,
|
||||
"tasks": {
|
||||
"total": len(group_tasks),
|
||||
"required": len(required_tasks),
|
||||
"required_ready": len(required_tasks) - len(missing_required) - len(unbuildable_required),
|
||||
"examples": group_tasks[:10],
|
||||
"missing_required": missing_required,
|
||||
"unbuildable_required": unbuildable_required,
|
||||
},
|
||||
"runtime": [
|
||||
{"role": item["role"], "name": item["name"], "passed": item["passed"]}
|
||||
for item in runtime_reports
|
||||
],
|
||||
"evidence": {
|
||||
"count": evidence_count,
|
||||
"artifacts": [
|
||||
{
|
||||
"name": item["name"],
|
||||
"relative_path": item["relative_path"],
|
||||
"role": item.get("role"),
|
||||
"family": item.get("family"),
|
||||
}
|
||||
for item in artifacts[:6]
|
||||
],
|
||||
"curves": [
|
||||
{
|
||||
"name": item["name"],
|
||||
"relative_path": item["relative_path"],
|
||||
"family": item.get("family"),
|
||||
"row_count": item.get("row_count"),
|
||||
}
|
||||
for item in group_curves[:4]
|
||||
],
|
||||
},
|
||||
"gaps": gaps,
|
||||
}
|
||||
)
|
||||
|
||||
requirements = [
|
||||
{
|
||||
"id": "user_script_mapping",
|
||||
"label": "用户侧脚本映射",
|
||||
"passed": coverage["mapped_user_scripts"] == coverage["user_scripts_total"] and not coverage["unmapped_user_scripts"],
|
||||
"detail": f"{coverage['mapped_user_scripts']}/{coverage['user_scripts_total']}",
|
||||
},
|
||||
{
|
||||
"id": "runtime_readiness",
|
||||
"label": "运行环境就绪",
|
||||
"passed": readiness.get("passed", False),
|
||||
"detail": ", ".join(f"{item['name']}={item['passed']}" for item in readiness.get("envs", [])),
|
||||
},
|
||||
{
|
||||
"id": "dataset_upload",
|
||||
"label": "数据集上传/Label/Mask",
|
||||
"passed": len(datasets) >= 1,
|
||||
"detail": f"{len(datasets)} uploaded dataset(s)",
|
||||
},
|
||||
{
|
||||
"id": "yolo_heatmap",
|
||||
"label": "YOLO 热度图证据",
|
||||
"passed": any(item.get("family") == "yolo" and item.get("role") == "heatmap" for item in results),
|
||||
"detail": "heatmap artifact discovered",
|
||||
},
|
||||
{
|
||||
"id": "training_curves",
|
||||
"label": "训练 loss/指标曲线",
|
||||
"passed": len(curves) >= 1,
|
||||
"detail": f"{len(curves)} curve file(s)",
|
||||
},
|
||||
{
|
||||
"id": "deep_acceptance",
|
||||
"label": "深度训练验收",
|
||||
"passed": bool(deep_acceptance.get("passed")),
|
||||
"detail": deep_acceptance.get("run_id", "not run"),
|
||||
},
|
||||
{
|
||||
"id": "weights_manifest",
|
||||
"label": "权重清单",
|
||||
"passed": manifest.get("count", 0) >= 1,
|
||||
"detail": f"{manifest.get('count', 0)} weights indexed",
|
||||
},
|
||||
]
|
||||
|
||||
ready_domains = sum(1 for item in domains if item["ready"])
|
||||
return {
|
||||
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"passed": ready_domains == len(domains) and all(item["passed"] for item in requirements),
|
||||
"summary": {
|
||||
"ready_domains": ready_domains,
|
||||
"total_domains": len(domains),
|
||||
"mapped_user_scripts": coverage["mapped_user_scripts"],
|
||||
"user_scripts_total": coverage["user_scripts_total"],
|
||||
"task_build_passed": coverage["task_build_passed"],
|
||||
"uploaded_datasets": len(datasets),
|
||||
"artifacts": len(results),
|
||||
"curves": len(curves),
|
||||
"weights": manifest.get("count", 0),
|
||||
"gpus_available": bool(gpus.get("available")),
|
||||
"acceptance_passed": bool(acceptance.get("passed")),
|
||||
"deep_acceptance_passed": bool(deep_acceptance.get("passed")),
|
||||
},
|
||||
"requirements": requirements,
|
||||
"domains": domains,
|
||||
}
|
||||
@@ -10,6 +10,7 @@ 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 .capabilities import get_capability_matrix
|
||||
from .catalog import get_catalog
|
||||
from .config import settings
|
||||
from .coverage import get_coverage_report
|
||||
@@ -86,6 +87,11 @@ def api_coverage() -> dict:
|
||||
return get_coverage_report()
|
||||
|
||||
|
||||
@app.get("/api/capabilities")
|
||||
def api_capabilities() -> dict:
|
||||
return get_capability_matrix()
|
||||
|
||||
|
||||
@app.get("/api/acceptance/latest")
|
||||
def api_acceptance_latest() -> dict:
|
||||
return latest_acceptance_report()
|
||||
|
||||
Reference in New Issue
Block a user