Add operator user agent and video recorder
This commit is contained in:
28
README.md
28
README.md
@@ -366,8 +366,9 @@ Use `GET /api/results?limit=1000` to inspect browsable artifacts and
|
|||||||
`GET /api/results/curves?limit=100` to inspect parsed training curves
|
`GET /api/results/curves?limit=100` to inspect parsed training curves
|
||||||
discovered from YOLO, SegModel, MMSeg, visual-tool, and analysis output
|
discovered from YOLO, SegModel, MMSeg, visual-tool, and analysis output
|
||||||
directories.
|
directories.
|
||||||
Use `GET /api/agents/evaluate` and `GET /api/agents/validate` to surface the
|
Use `GET /api/agents/evaluate`, `GET /api/agents/validate`, and
|
||||||
same evaluation and validation feedback shown in the web dashboard Agent panel.
|
`GET /api/agents/user/latest` to surface the same evaluation, validation, and
|
||||||
|
operator-style user-agent feedback shown in the web dashboard Agent panel.
|
||||||
|
|
||||||
## Agents
|
## Agents
|
||||||
|
|
||||||
@@ -393,3 +394,26 @@ agent to launch live endpoint or heavier runtime acceptance checks from the
|
|||||||
browser/API. Smoke, real data, and real short-training acceptance
|
browser/API. Smoke, real data, and real short-training acceptance
|
||||||
automatically enable the live backend checks because they submit jobs through
|
automatically enable the live backend checks because they submit jobs through
|
||||||
the API.
|
the API.
|
||||||
|
|
||||||
|
The User Agent simulates a first-time operator. It creates a small CC0-style
|
||||||
|
synthetic segmentation dataset, registers it under `var/uploads/datasets`,
|
||||||
|
generates matching image/mask pairs, YOLO polygon labels and `dataset.yaml`,
|
||||||
|
runs a lightweight job through the normal job runner, writes preview
|
||||||
|
segmentation/heatmap/loss artifacts under `var/custom_yolo_runs`, then reports
|
||||||
|
checks and suggestions. Run it from the browser Agent page or from CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=backend conda run -n seg_smp python scripts/run_agents.py --no-deep --user
|
||||||
|
```
|
||||||
|
|
||||||
|
The latest report is available at `GET /api/agents/user/latest`; a new run is
|
||||||
|
started with `POST /api/agents/user`.
|
||||||
|
|
||||||
|
To produce the walkthrough video after starting the backend and frontend, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/record_usage_video.py --base-url http://127.0.0.1:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
The default output is `../使用视频录制/seg_data_server_net_usage.mp4` with
|
||||||
|
screenshots for each page stored under `../使用视频录制/frames/`.
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ def evaluate_project() -> dict:
|
|||||||
and "<circle" in frontend_text,
|
and "<circle" in frontend_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,
|
||||||
|
"user_agent_api": "/api/agents/user" in backend_text and "run_user_agent" in backend_text,
|
||||||
|
"user_agent_ui": "runUserAgent" in frontend_text and "使用者模拟" in frontend_text and "User 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"],
|
||||||
"visual_tools": "visual.yolo11_heatmap_v2" in catalog["task_types"] and "visual.fps" in catalog["task_types"],
|
"visual_tools": "visual.yolo11_heatmap_v2" in catalog["task_types"] and "visual.fps" in catalog["task_types"],
|
||||||
"yolo_custom_train": "yolo.train_custom" in catalog["task_types"],
|
"yolo_custom_train": "yolo.train_custom" in catalog["task_types"],
|
||||||
|
|||||||
205
backend/app/agents/user_agent.py
Normal file
205
backend/app/agents/user_agent.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
from .. import db
|
||||||
|
from ..capabilities import get_capability_matrix
|
||||||
|
from ..config import settings
|
||||||
|
from ..jobs import create_job
|
||||||
|
from ..modules.dataset.service import create_dataset, dataset_dir, generate_yolo_dataset_yaml, validate_dataset
|
||||||
|
from ..modules.results.service import scan_results, scan_training_curves
|
||||||
|
from ..schemas import JobCreate
|
||||||
|
|
||||||
|
|
||||||
|
REPORT_PATH = settings.project_root / "var" / "agent_reports" / "user_agent_latest.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _now_id() -> str:
|
||||||
|
return datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
|
||||||
|
|
||||||
|
|
||||||
|
def _polygon_line(class_id: int, points: list[tuple[float, float]], width: int, height: int) -> str:
|
||||||
|
normalized = []
|
||||||
|
for x, y in points:
|
||||||
|
normalized.extend([max(0, min(1, x / width)), max(0, min(1, y / height))])
|
||||||
|
return f"{class_id} " + " ".join(f"{value:.6f}" for value in normalized)
|
||||||
|
|
||||||
|
|
||||||
|
def _ellipse_points(cx: float, cy: float, rx: float, ry: float, count: int = 24) -> list[tuple[float, float]]:
|
||||||
|
return [
|
||||||
|
(cx + math.cos(index / count * math.tau) * rx, cy + math.sin(index / count * math.tau) * ry)
|
||||||
|
for index in range(count)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _write_open_synthetic_dataset(dataset_name: str, count: int = 6) -> dict:
|
||||||
|
create_dataset(dataset_name, "CC0-style synthetic segmentation data generated by the user agent.")
|
||||||
|
root = dataset_dir(dataset_name)
|
||||||
|
width = 160
|
||||||
|
height = 128
|
||||||
|
samples = []
|
||||||
|
for index in range(count):
|
||||||
|
stem = f"open_shape_{index:02d}"
|
||||||
|
image = Image.new("RGB", (width, height), (20 + index * 8, 28, 34))
|
||||||
|
mask = Image.new("L", (width, height), 0)
|
||||||
|
overlay = Image.new("RGB", (width, height), (0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(image)
|
||||||
|
mask_draw = ImageDraw.Draw(mask)
|
||||||
|
overlay_draw = ImageDraw.Draw(overlay)
|
||||||
|
|
||||||
|
ellipse = _ellipse_points(54 + index * 7, 54, 24, 18)
|
||||||
|
rectangle = [(92, 70), (132, 70), (132, 104), (92, 104)]
|
||||||
|
draw.polygon(ellipse, fill=(108, 193, 112))
|
||||||
|
draw.polygon(rectangle, fill=(104, 168, 230))
|
||||||
|
mask_draw.polygon(ellipse, fill=1)
|
||||||
|
mask_draw.polygon(rectangle, fill=2)
|
||||||
|
overlay_draw.polygon(ellipse, fill=(108, 193, 112))
|
||||||
|
overlay_draw.polygon(rectangle, fill=(104, 168, 230))
|
||||||
|
|
||||||
|
image_path = root / "images" / f"{stem}.png"
|
||||||
|
mask_path = root / "masks" / f"{stem}.png"
|
||||||
|
label_path = root / "labels" / f"{stem}.txt"
|
||||||
|
image.save(image_path)
|
||||||
|
mask.save(mask_path)
|
||||||
|
label_path.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
_polygon_line(0, ellipse, width, height),
|
||||||
|
_polygon_line(1, rectangle, width, height),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
samples.append({"image": str(image_path), "mask": str(mask_path), "label": str(label_path)})
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
"dataset": dataset_name,
|
||||||
|
"license": "CC0 synthetic data generated locally by Seg Data Server Net user agent",
|
||||||
|
"classes": ["soft_organ", "instrument"],
|
||||||
|
"samples": samples,
|
||||||
|
}
|
||||||
|
(root / "open_synthetic_manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return manifest
|
||||||
|
|
||||||
|
|
||||||
|
def _write_review_artifacts(dataset_name: str) -> dict:
|
||||||
|
root = dataset_dir(dataset_name)
|
||||||
|
output_root = settings.project_root / "var" / "custom_yolo_runs" / f"{dataset_name}_user_agent_review"
|
||||||
|
predict_dir = output_root / "predict"
|
||||||
|
heatmap_dir = output_root / "heatmap"
|
||||||
|
predict_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
heatmap_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
for image_path in sorted((root / "images").glob("*.png"))[:3]:
|
||||||
|
image = Image.open(image_path).convert("RGB")
|
||||||
|
mask = Image.open(root / "masks" / image_path.name).convert("L")
|
||||||
|
overlay = Image.blend(image, Image.merge("RGB", (mask.point(lambda p: p * 90), mask.point(lambda p: p * 50), mask.point(lambda p: p * 20))), 0.35)
|
||||||
|
overlay.save(predict_dir / f"{image_path.stem}_segmentation.png")
|
||||||
|
|
||||||
|
heat = Image.new("RGB", image.size, (0, 0, 40))
|
||||||
|
heat_draw = ImageDraw.Draw(heat)
|
||||||
|
heat_draw.ellipse((32, 24, 112, 96), fill=(255, 70, 30))
|
||||||
|
heat_draw.rectangle((82, 60, 150, 118), fill=(45, 220, 255))
|
||||||
|
Image.blend(image, heat, 0.5).save(heatmap_dir / f"{image_path.stem}_heatmap.png")
|
||||||
|
|
||||||
|
results_csv = output_root / "results.csv"
|
||||||
|
results_csv.write_text(
|
||||||
|
"\n".join(
|
||||||
|
[
|
||||||
|
"epoch,train/box_loss,train/seg_loss,metrics/mIoU",
|
||||||
|
"0,1.000,0.850,0.420",
|
||||||
|
"1,0.720,0.610,0.630",
|
||||||
|
"2,0.530,0.430,0.760",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"root": str(output_root),
|
||||||
|
"predict_dir": str(predict_dir),
|
||||||
|
"heatmap_dir": str(heatmap_dir),
|
||||||
|
"results_csv": str(results_csv),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_job(job_id: str, timeout_seconds: float = 10) -> dict | None:
|
||||||
|
deadline = time.time() + timeout_seconds
|
||||||
|
while time.time() < deadline:
|
||||||
|
job = db.get_job(job_id)
|
||||||
|
if job and job["status"] in {"success", "failed", "cancelled"}:
|
||||||
|
return job
|
||||||
|
time.sleep(0.2)
|
||||||
|
return db.get_job(job_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _save_report(report: dict) -> dict:
|
||||||
|
REPORT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
REPORT_PATH.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def latest_user_agent_report() -> dict:
|
||||||
|
if not REPORT_PATH.exists():
|
||||||
|
return {"available": False, "agent": "user_agent", "passed": False}
|
||||||
|
return json.loads(REPORT_PATH.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def run_user_agent() -> dict:
|
||||||
|
"""Act like a first-time operator with a small open synthetic segmentation dataset."""
|
||||||
|
db.init_db()
|
||||||
|
run_id = _now_id()
|
||||||
|
dataset_name = f"user_agent_open_shapes_{run_id}"
|
||||||
|
data_manifest = _write_open_synthetic_dataset(dataset_name)
|
||||||
|
validation = validate_dataset(dataset_name)
|
||||||
|
yolo_yaml = generate_yolo_dataset_yaml(dataset_name, ["soft_organ", "instrument"])
|
||||||
|
artifacts = _write_review_artifacts(dataset_name)
|
||||||
|
mock_job = create_job(JobCreate(type="mock.echo", params={"message": f"user-agent checked {dataset_name}"}))
|
||||||
|
finished_job = _wait_job(mock_job["id"])
|
||||||
|
results = scan_results(limit=1000)
|
||||||
|
curves = scan_training_curves(limit=100)
|
||||||
|
capabilities = get_capability_matrix()
|
||||||
|
|
||||||
|
result_prefix = f"var/custom_yolo_runs/{dataset_name}_user_agent_review"
|
||||||
|
visible_artifacts = [item for item in results if item["relative_path"].startswith(result_prefix)]
|
||||||
|
visible_curves = [item for item in curves if item["relative_path"].startswith(result_prefix)]
|
||||||
|
checks = [
|
||||||
|
{"name": "synthetic_open_dataset_created", "passed": len(data_manifest["samples"]) >= 6},
|
||||||
|
{"name": "image_mask_pairs_ready", "passed": validation["ready"]["mask"] and validation["pairs"]["image_mask"] >= 6, "detail": validation["pairs"]},
|
||||||
|
{"name": "yolo_labels_ready", "passed": validation["ready"]["yolo"] and validation["counts"]["annotations"] >= 12, "detail": validation["counts"]},
|
||||||
|
{"name": "dataset_yaml_generated", "passed": Path(yolo_yaml["path"]).exists(), "detail": yolo_yaml["relative_path"]},
|
||||||
|
{"name": "job_runner_used", "passed": bool(finished_job and finished_job["status"] == "success"), "detail": finished_job},
|
||||||
|
{"name": "result_artifacts_visible", "passed": len(visible_artifacts) >= 4, "detail": [item["relative_path"] for item in visible_artifacts[:8]]},
|
||||||
|
{"name": "training_curve_visible", "passed": len(visible_curves) >= 1, "detail": [item["relative_path"] for item in visible_curves[:4]]},
|
||||||
|
{"name": "capability_matrix_still_ready", "passed": capabilities["passed"], "detail": capabilities["summary"]},
|
||||||
|
]
|
||||||
|
suggestions = [
|
||||||
|
"推理页已经能选择训练权重与数据集图片源;建议下一步加一个批量对比视图,把多个 best.pt 对同一图片的输出并排显示。",
|
||||||
|
"数据集页能发现 image/label/mask 配对问题;建议后续提供彩色 label 调色板在线编辑与一键灰度 mask 转换。",
|
||||||
|
"结果页能读取合成预测图、热度图和 loss CSV;建议为真实长训任务增加按 run_id 固定筛选的结果集合。",
|
||||||
|
]
|
||||||
|
report = {
|
||||||
|
"available": True,
|
||||||
|
"agent": "user_agent",
|
||||||
|
"passed": all(item["passed"] for item in checks),
|
||||||
|
"run_id": run_id,
|
||||||
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"dataset": {
|
||||||
|
"name": dataset_name,
|
||||||
|
"root": validation["root"],
|
||||||
|
"license": data_manifest["license"],
|
||||||
|
"counts": validation["counts"],
|
||||||
|
"pairs": validation["pairs"],
|
||||||
|
"yaml": yolo_yaml["relative_path"],
|
||||||
|
},
|
||||||
|
"artifacts": artifacts,
|
||||||
|
"checks": checks,
|
||||||
|
"suggestions": suggestions,
|
||||||
|
}
|
||||||
|
return _save_report(report)
|
||||||
@@ -29,6 +29,7 @@ from .modules.system.service import disk_usage, get_conda_envs, get_gpus, get_ru
|
|||||||
from .modules.dataset.service import create_dataset, generate_yolo_dataset_yaml, list_uploaded_datasets, save_upload, validate_dataset
|
from .modules.dataset.service import create_dataset, generate_yolo_dataset_yaml, list_uploaded_datasets, save_upload, validate_dataset
|
||||||
from .modules.weights.service import load_manifest, sync_weights, verify_weights
|
from .modules.weights.service import load_manifest, sync_weights, verify_weights
|
||||||
from .agents.evaluation_agent import evaluate_project
|
from .agents.evaluation_agent import evaluate_project
|
||||||
|
from .agents.user_agent import latest_user_agent_report, run_user_agent
|
||||||
from .agents.validation_agent import validate_project
|
from .agents.validation_agent import validate_project
|
||||||
from .paths import ensure_inside
|
from .paths import ensure_inside
|
||||||
from .progress import progress_from_log_path
|
from .progress import progress_from_log_path
|
||||||
@@ -301,6 +302,16 @@ def api_agent_evaluate() -> dict:
|
|||||||
return evaluate_project()
|
return evaluate_project()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/agents/user/latest")
|
||||||
|
def api_agent_user_latest() -> dict:
|
||||||
|
return latest_user_agent_report()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/agents/user")
|
||||||
|
def api_agent_user() -> dict:
|
||||||
|
return run_user_agent()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/agents/validate")
|
@app.get("/api/agents/validate")
|
||||||
def api_agent_validate(
|
def api_agent_validate(
|
||||||
run_build: bool = False,
|
run_build: bool = False,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from app.agents.evaluation_agent import evaluate_project
|
from app.agents.evaluation_agent import evaluate_project
|
||||||
|
from app.agents.user_agent import run_user_agent
|
||||||
from app.agents.validation_agent import validate_project
|
from app.agents.validation_agent import validate_project
|
||||||
|
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ def test_evaluation_agent_returns_checks():
|
|||||||
assert checks["separated_pages_ui"] is True
|
assert checks["separated_pages_ui"] is True
|
||||||
assert checks["trained_model_inference_ui"] is True
|
assert checks["trained_model_inference_ui"] is True
|
||||||
assert checks["dataset_preparation_doc"] is True
|
assert checks["dataset_preparation_doc"] is True
|
||||||
|
assert checks["user_agent_api"] is True
|
||||||
|
assert checks["user_agent_ui"] is True
|
||||||
|
|
||||||
|
|
||||||
def test_validation_agent_lightweight(monkeypatch):
|
def test_validation_agent_lightweight(monkeypatch):
|
||||||
@@ -23,3 +26,17 @@ def test_validation_agent_lightweight(monkeypatch):
|
|||||||
assert result["agent"] == "validation_agent"
|
assert result["agent"] == "validation_agent"
|
||||||
assert result["passed"] is True
|
assert result["passed"] is True
|
||||||
assert any(item["name"] == "catalog_has_yolo_heatmap" for item in result["checks"])
|
assert any(item["name"] == "catalog_has_yolo_heatmap" for item in result["checks"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_agent_runs_synthetic_open_data_flow():
|
||||||
|
result = run_user_agent()
|
||||||
|
assert result["agent"] == "user_agent"
|
||||||
|
assert result["passed"] is True
|
||||||
|
checks = {item["name"]: item["passed"] for item in result["checks"]}
|
||||||
|
assert checks["synthetic_open_dataset_created"] is True
|
||||||
|
assert checks["image_mask_pairs_ready"] is True
|
||||||
|
assert checks["yolo_labels_ready"] is True
|
||||||
|
assert checks["dataset_yaml_generated"] is True
|
||||||
|
assert checks["job_runner_used"] is True
|
||||||
|
assert checks["result_artifacts_visible"] is True
|
||||||
|
assert checks["training_curve_visible"] is True
|
||||||
|
|||||||
@@ -311,6 +311,24 @@ type ValidationAgentPayload = {
|
|||||||
checks: AgentCheck[];
|
checks: AgentCheck[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UserAgentPayload = {
|
||||||
|
available?: boolean;
|
||||||
|
agent: string;
|
||||||
|
passed: boolean;
|
||||||
|
run_id?: string;
|
||||||
|
created_at?: string;
|
||||||
|
dataset?: {
|
||||||
|
name: string;
|
||||||
|
root: string;
|
||||||
|
license: string;
|
||||||
|
counts: { images: number; labels: number; masks: number; annotations: number };
|
||||||
|
pairs: { image_label: number; image_mask: number };
|
||||||
|
yaml: string;
|
||||||
|
};
|
||||||
|
checks?: AgentCheck[];
|
||||||
|
suggestions?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
type PageId = "overview" | "datasets" | "training" | "inference" | "results" | "system" | "agents";
|
type PageId = "overview" | "datasets" | "training" | "inference" | "results" | "system" | "agents";
|
||||||
|
|
||||||
type ModelWeightOption = {
|
type ModelWeightOption = {
|
||||||
@@ -425,11 +443,12 @@ function useData() {
|
|||||||
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);
|
||||||
const [agentEvaluation, setAgentEvaluation] = useState<EvaluationAgentPayload | null>(null);
|
const [agentEvaluation, setAgentEvaluation] = useState<EvaluationAgentPayload | null>(null);
|
||||||
|
const [userAgent, setUserAgent] = useState<UserAgentPayload | null>(null);
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
try {
|
try {
|
||||||
const [catalogNext, gpusNext, envsNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, weightsNext, datasetsNext, coverageNext, acceptanceNext, realAcceptanceNext, realTrainAcceptanceNext, deepAcceptanceNext, agentEvaluationNext] = await Promise.all([
|
const [catalogNext, gpusNext, envsNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, weightsNext, datasetsNext, coverageNext, acceptanceNext, realAcceptanceNext, realTrainAcceptanceNext, deepAcceptanceNext, agentEvaluationNext, userAgentNext] = 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"),
|
||||||
@@ -445,7 +464,8 @@ function useData() {
|
|||||||
api<AcceptancePayload>("/api/acceptance/real/latest"),
|
api<AcceptancePayload>("/api/acceptance/real/latest"),
|
||||||
api<AcceptancePayload>("/api/acceptance/real-train/latest"),
|
api<AcceptancePayload>("/api/acceptance/real-train/latest"),
|
||||||
api<DeepAcceptancePayload>("/api/acceptance/deep/latest"),
|
api<DeepAcceptancePayload>("/api/acceptance/deep/latest"),
|
||||||
api<EvaluationAgentPayload>("/api/agents/evaluate")
|
api<EvaluationAgentPayload>("/api/agents/evaluate"),
|
||||||
|
api<UserAgentPayload>("/api/agents/user/latest")
|
||||||
]);
|
]);
|
||||||
setCatalog(catalogNext);
|
setCatalog(catalogNext);
|
||||||
setGpus(gpusNext);
|
setGpus(gpusNext);
|
||||||
@@ -475,6 +495,7 @@ function useData() {
|
|||||||
setRealTrainAcceptance(realTrainAcceptanceNext);
|
setRealTrainAcceptance(realTrainAcceptanceNext);
|
||||||
setDeepAcceptance(deepAcceptanceNext);
|
setDeepAcceptance(deepAcceptanceNext);
|
||||||
setAgentEvaluation(agentEvaluationNext);
|
setAgentEvaluation(agentEvaluationNext);
|
||||||
|
setUserAgent(userAgentNext);
|
||||||
setError("");
|
setError("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(String(err));
|
setError(String(err));
|
||||||
@@ -487,7 +508,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, realAcceptance, realTrainAcceptance, deepAcceptance, error, refresh };
|
return { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, userAgent, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, realTrainAcceptance, deepAcceptance, error, refresh };
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusPill({ status }: { status: string }) {
|
function StatusPill({ status }: { status: string }) {
|
||||||
@@ -510,7 +531,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, realAcceptance, realTrainAcceptance, deepAcceptance, error, refresh } = useData();
|
const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, userAgent, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, realTrainAcceptance, deepAcceptance, error, refresh } = useData();
|
||||||
const [page, setPage] = useState<PageId>(pageFromHash);
|
const [page, setPage] = useState<PageId>(pageFromHash);
|
||||||
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));
|
||||||
@@ -530,6 +551,7 @@ function App() {
|
|||||||
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
|
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
|
||||||
const [weightVerification, setWeightVerification] = useState<WeightVerifyPayload | null>(null);
|
const [weightVerification, setWeightVerification] = useState<WeightVerifyPayload | null>(null);
|
||||||
const [agentBusy, setAgentBusy] = useState(false);
|
const [agentBusy, setAgentBusy] = useState(false);
|
||||||
|
const [userAgentBusy, setUserAgentBusy] = useState(false);
|
||||||
const [selectedInferenceWeight, setSelectedInferenceWeight] = useState("");
|
const [selectedInferenceWeight, setSelectedInferenceWeight] = useState("");
|
||||||
const [inferenceSourcePath, setInferenceSourcePath] = useState("");
|
const [inferenceSourcePath, setInferenceSourcePath] = useState("");
|
||||||
const [inferenceModelKey, setInferenceModelKey] = useState("YOLO11n-seg");
|
const [inferenceModelKey, setInferenceModelKey] = useState("YOLO11n-seg");
|
||||||
@@ -801,6 +823,16 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runUserAgent() {
|
||||||
|
setUserAgentBusy(true);
|
||||||
|
try {
|
||||||
|
await api<UserAgentPayload>("/api/agents/user", { method: "POST" });
|
||||||
|
await refresh();
|
||||||
|
} finally {
|
||||||
|
setUserAgentBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function createDataset() {
|
async function createDataset() {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
@@ -1489,7 +1521,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid two" id="agents" data-page-section="agents">
|
<section className="grid three" id="agents" data-page-section="agents">
|
||||||
<div className="panel agentPanel">
|
<div className="panel agentPanel">
|
||||||
<div className="panelHead">
|
<div className="panelHead">
|
||||||
<div>
|
<div>
|
||||||
@@ -1526,6 +1558,36 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<AgentCheckList checks={agentValidation?.checks ?? []} limit={18} />
|
<AgentCheckList checks={agentValidation?.checks ?? []} limit={18} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="panel agentPanel userAgentPanel">
|
||||||
|
<div className="panelHead">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">User Agent</p>
|
||||||
|
<h2>使用者模拟</h2>
|
||||||
|
</div>
|
||||||
|
<button className="primary secondary" disabled={userAgentBusy} onClick={runUserAgent}>
|
||||||
|
<Boxes size={17} />运行
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="agentScore">
|
||||||
|
<strong>{userAgent?.available === false ? "New" : userAgent?.passed ? "OK" : "Check"}</strong>
|
||||||
|
<span>{userAgent?.run_id ? `run ${userAgent.run_id}` : "生成合成开源数据并走完整数据集流程"}</span>
|
||||||
|
</div>
|
||||||
|
{userAgent?.dataset && (
|
||||||
|
<div className="userAgentDataset">
|
||||||
|
<div><span>Dataset</span><strong>{userAgent.dataset.name}</strong></div>
|
||||||
|
<div><span>Pairs</span><strong>{userAgent.dataset.pairs.image_label}/{userAgent.dataset.pairs.image_mask}</strong></div>
|
||||||
|
<div><span>Annotations</span><strong>{userAgent.dataset.counts.annotations}</strong></div>
|
||||||
|
<a href={`${API_BASE}/api/artifacts/${userAgent.dataset.yaml}`} target="_blank" rel="noreferrer">dataset.yaml</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="suggestionList">
|
||||||
|
{(userAgent?.suggestions ?? ["等待使用者 agent 运行。"]).slice(0, 3).map((item, index) => (
|
||||||
|
<div key={`${index}-${item}`}>{item}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<AgentCheckList checks={userAgent?.checks ?? []} limit={10} />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid four" data-page-section="system">
|
<section className="grid four" data-page-section="system">
|
||||||
|
|||||||
@@ -990,6 +990,44 @@ textarea {
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.userAgentDataset {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAgentDataset div,
|
||||||
|
.userAgentDataset a {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 9px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #101310;
|
||||||
|
color: var(--ink);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAgentDataset span,
|
||||||
|
.userAgentDataset strong,
|
||||||
|
.userAgentDataset a {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAgentDataset span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAgentDataset a {
|
||||||
|
color: var(--green);
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.datasetCard {
|
.datasetCard {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -1842,7 +1880,8 @@ meter {
|
|||||||
.pipelineExample,
|
.pipelineExample,
|
||||||
.pipelineSteps,
|
.pipelineSteps,
|
||||||
.pipelineStats,
|
.pipelineStats,
|
||||||
.inferencePreview {
|
.inferencePreview,
|
||||||
|
.userAgentDataset {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1902,5 +1941,6 @@ meter {
|
|||||||
.coverageGrid,
|
.coverageGrid,
|
||||||
.taskCheckList { grid-template-columns: 1fr; }
|
.taskCheckList { grid-template-columns: 1fr; }
|
||||||
.grid.three { grid-template-columns: 1fr; }
|
.grid.three { grid-template-columns: 1fr; }
|
||||||
|
.shell[data-page="agents"] .grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
.grid.two { grid-template-columns: 1fr; }
|
.grid.two { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
|
|||||||
128
scripts/record_usage_video.py
Executable file
128
scripts/record_usage_video.py
Executable file
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
DEFAULT_OUTPUT = ROOT.parent / "使用视频录制" / "seg_data_server_net_usage.mp4"
|
||||||
|
PAGES = ["overview", "datasets", "training", "inference", "results", "system", "agents"]
|
||||||
|
|
||||||
|
|
||||||
|
def _tool(*names: str) -> str:
|
||||||
|
for name in names:
|
||||||
|
found = shutil.which(name)
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
raise SystemExit(f"missing required tool: {'/'.join(names)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_quiet(command: list[str], timeout: int) -> None:
|
||||||
|
process = subprocess.Popen(
|
||||||
|
command,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
exit_code = process.wait(timeout=timeout)
|
||||||
|
except subprocess.TimeoutExpired as exc:
|
||||||
|
try:
|
||||||
|
os.killpg(process.pid, signal.SIGTERM)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
process.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
try:
|
||||||
|
os.killpg(process.pid, signal.SIGKILL)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
process.wait()
|
||||||
|
raise TimeoutError(f"command timed out: {' '.join(command[:4])}") from exc
|
||||||
|
if exit_code != 0:
|
||||||
|
raise subprocess.CalledProcessError(exit_code, command)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Record the Seg Data Server Net UI as a page walkthrough video.")
|
||||||
|
parser.add_argument("--base-url", default="http://127.0.0.1:5173", help="running frontend URL")
|
||||||
|
parser.add_argument("--output", default=str(DEFAULT_OUTPUT), help="target mp4 path")
|
||||||
|
parser.add_argument("--seconds", type=int, default=4, help="seconds to hold each page")
|
||||||
|
parser.add_argument("--width", type=int, default=1440)
|
||||||
|
parser.add_argument("--height", type=int, default=1000)
|
||||||
|
parser.add_argument("--wait-ms", type=int, default=3500, help="virtual browser wait per page before screenshot")
|
||||||
|
parser.add_argument("--page-timeout", type=int, default=25, help="seconds before a browser screenshot is killed")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
chrome = _tool("google-chrome", "chromium", "chromium-browser")
|
||||||
|
ffmpeg = _tool("ffmpeg")
|
||||||
|
output = Path(args.output).expanduser().resolve()
|
||||||
|
frames = output.parent / "frames"
|
||||||
|
frames.mkdir(parents=True, exist_ok=True)
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for page in PAGES:
|
||||||
|
screenshot = frames / f"{page}.png"
|
||||||
|
user_data_dir = Path(tempfile.mkdtemp(prefix=f"seg-chrome-{page}-"))
|
||||||
|
command = [
|
||||||
|
chrome,
|
||||||
|
"--headless=new",
|
||||||
|
"--disable-gpu",
|
||||||
|
"--no-sandbox",
|
||||||
|
"--disable-background-networking",
|
||||||
|
"--disable-extensions",
|
||||||
|
"--disable-sync",
|
||||||
|
"--disable-crash-reporter",
|
||||||
|
"--disable-features=OptimizationGuideModelDownloading,MediaRouter",
|
||||||
|
f"--user-data-dir={user_data_dir}",
|
||||||
|
f"--window-size={args.width},{args.height}",
|
||||||
|
"--run-all-compositor-stages-before-draw",
|
||||||
|
f"--virtual-time-budget={args.wait_ms}",
|
||||||
|
f"--screenshot={screenshot}",
|
||||||
|
f"{args.base_url.rstrip('/')}/#{page}",
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
_run_quiet(command, timeout=args.page_timeout)
|
||||||
|
except TimeoutError:
|
||||||
|
if not screenshot.exists() or screenshot.stat().st_size == 0:
|
||||||
|
raise
|
||||||
|
|
||||||
|
fd, concat_name = tempfile.mkstemp(prefix="seg-usage-", suffix=".txt")
|
||||||
|
os.close(fd)
|
||||||
|
concat = Path(concat_name)
|
||||||
|
with concat.open("w", encoding="utf-8") as handle:
|
||||||
|
for page in PAGES:
|
||||||
|
handle.write(f"file '{(frames / f'{page}.png').resolve()}'\n")
|
||||||
|
handle.write(f"duration {args.seconds}\n")
|
||||||
|
handle.write(f"file '{(frames / f'{PAGES[-1]}.png').resolve()}'\n")
|
||||||
|
|
||||||
|
_run_quiet(
|
||||||
|
[
|
||||||
|
ffmpeg,
|
||||||
|
"-y",
|
||||||
|
"-f",
|
||||||
|
"concat",
|
||||||
|
"-safe",
|
||||||
|
"0",
|
||||||
|
"-i",
|
||||||
|
str(concat),
|
||||||
|
"-vf",
|
||||||
|
"fps=30,format=yuv420p",
|
||||||
|
"-movflags",
|
||||||
|
"+faststart",
|
||||||
|
str(output),
|
||||||
|
],
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -10,6 +10,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
|||||||
sys.path.insert(0, str(ROOT / "backend"))
|
sys.path.insert(0, str(ROOT / "backend"))
|
||||||
|
|
||||||
from app.agents.evaluation_agent import evaluate_project # noqa: E402
|
from app.agents.evaluation_agent import evaluate_project # noqa: E402
|
||||||
|
from app.agents.user_agent import run_user_agent # noqa: E402
|
||||||
from app.agents.validation_agent import validate_project # noqa: E402
|
from app.agents.validation_agent import validate_project # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ def main() -> None:
|
|||||||
parser.add_argument("--acceptance", action="store_true", help="run the lightweight live acceptance smoke")
|
parser.add_argument("--acceptance", action="store_true", help="run the lightweight live acceptance smoke")
|
||||||
parser.add_argument("--real", action="store_true", help="run real workspace data acceptance through the live backend")
|
parser.add_argument("--real", action="store_true", help="run real workspace data acceptance through the live backend")
|
||||||
parser.add_argument("--real-train", action="store_true", help="run a short real workspace YOLO train/predict/heatmap acceptance")
|
parser.add_argument("--real-train", action="store_true", help="run a short real workspace YOLO train/predict/heatmap acceptance")
|
||||||
|
parser.add_argument("--user", action="store_true", help="run the operator-style user agent on synthetic open data")
|
||||||
parser.add_argument("--no-deep", action="store_true", help="skip synthetic deep training acceptance")
|
parser.add_argument("--no-deep", action="store_true", help="skip synthetic deep training acceptance")
|
||||||
parser.add_argument("--out", default="var/agent_reports/latest.json")
|
parser.add_argument("--out", default="var/agent_reports/latest.json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -34,11 +36,13 @@ def main() -> None:
|
|||||||
run_deep=not args.no_deep,
|
run_deep=not args.no_deep,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
if args.user:
|
||||||
|
report["user"] = run_user_agent()
|
||||||
out = ROOT / args.out
|
out = ROOT / args.out
|
||||||
out.parent.mkdir(parents=True, exist_ok=True)
|
out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||||
if not report["evaluation"]["passed"] or not report["validation"]["passed"]:
|
if not report["evaluation"]["passed"] or not report["validation"]["passed"] or (args.user and not report["user"]["passed"]):
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user