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

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"))
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
@@ -20,6 +21,7 @@ def main() -> None:
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-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("--out", default="var/agent_reports/latest.json")
args = parser.parse_args()
@@ -34,11 +36,13 @@ def main() -> None:
run_deep=not args.no_deep,
),
}
if args.user:
report["user"] = run_user_agent()
out = ROOT / args.out
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
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)