Add operator user agent and video recorder

This commit is contained in:
2026-07-01 00:44:10 +08:00
parent dcd6f7fd41
commit 1d43efd5b4
9 changed files with 502 additions and 9 deletions

View File

@@ -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/`.

View File

@@ -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"],

View 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)

View File

@@ -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,

View File

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

View File

@@ -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">

View File

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

View File

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