commit 98abafa7cc4298033c94dc27b6cedd462516078e Author: admin <572701190@qq.com> Date: Tue Jun 30 11:49:36 2026 +0800 Initial Seg Data Server Net platform diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..25bc061 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +SEG_SOURCE_ROOT=../Seg +SEG_DATA_SERVER_ROOT=. +SEG_BACKEND_DB=var/seg_data_server.sqlite3 +SEG_BACKEND_LOG_DIR=var/job_logs +SEG_TASK_CONDA_ENV=seg_smp +SEG_BACKEND_CONDA_ENV=seg_server +SEG_WEIGHT_MODE=copy +SEG_ENABLE_SHELL_TASKS=1 +VITE_API_BASE=http://localhost:8000 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7d5b6f4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.engine filter=lfs diff=lfs merge=lfs -text + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f52548 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Runtime state +.env +var/ +.pytest_cache/ +backend/.pytest_cache/ +backend/__pycache__/ +frontend/node_modules/ +frontend/dist/ +frontend/*.tsbuildinfo + +# Python / JS caches +__pycache__/ +*.py[cod] +.mypy_cache/ +.ruff_cache/ +.vite/ + +# Large runtime assets. Track through Git LFS or release/package storage. +weights/files/ +*.pt +*.pth +*.onnx +*.engine + +# Keep the manifest and placeholder. +!weights/.gitkeep +!weights/manifest.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d2d5ef --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# Seg Data Server Net + +Modular web control plane for the existing Seg image segmentation workspace. + +The platform keeps the current training and analysis scripts as the compute +core, then adds: + +- a FastAPI backend for catalog discovery, job orchestration, logs, results, + GPU status, and weight management; +- a React/Vite frontend for launching jobs and inspecting progress; +- a unified `weights/` area with a generated manifest for `.pt`, `.pth`, + `.onnx`, and `.engine` assets. + +## Layout + +```text +Seg_Data_Server_Net/ + backend/ FastAPI API, job runner, module wrappers + frontend/ React + Vite operator UI + scripts/ helper scripts for running services and syncing weights + weights/ copied model weights and manifest.json +``` + +## Quick Start + +```bash +cd Seg_Data_Server_Net +cp .env.example .env + +# Backend. The existing machine already has a seg_server env with FastAPI. +conda run -n seg_server uvicorn app.main:app --app-dir backend --host 0.0.0.0 --port 8000 + +# Frontend. +cd frontend +npm install +npm run dev -- --host 0.0.0.0 +``` + +Open the Vite URL shown in the terminal. The frontend expects the backend at +`http://localhost:8000` by default. + +## Weight Sync + +The current workspace contains tens of GB of pretrained and trained weights. +They are copied into `weights/files/` and indexed in +`weights/manifest.json`. + +```bash +cd Seg_Data_Server_Net +python scripts/sync_weights.py --mode copy --hash +``` + +For repository storage, use Git LFS or a Gitea release/package store: + +```bash +git lfs install +git lfs track "*.pt" "*.pth" "*.onnx" "*.engine" +``` + +If Git LFS is not available on the host or server, keep the copied weights on +the deployment volume and commit only `weights/manifest.json`. + +## Job Types + +The backend exposes all current Seg capabilities as job types. Examples: + +- `dataset.rename`, `dataset.resize`, `dataset.pair`, `dataset.rebuild_labels`, + `dataset.stack`, `dataset.stitch`, `dataset.video_frames` +- `segmodel.train`, `segmodel.batch_train`, `segmodel.predict`, + `segmodel.batch_predict`, `segmodel.flops`, `segmodel.raw_mask_check` +- `yolo.train`, `yolo.batch_train`, `yolo.predict`, `yolo.batch_predict`, + `yolo.heatmap`, `yolo.compare`, `yolo.raw_mask_check`, `yolo.video_visible` +- `mmseg.generate_data`, `mmseg.generate_alg`, `mmseg.train`, + `mmseg.metrics`, `mmseg.flops_fps`, `mmseg.draw`, `mmseg.extract_loss_miou` +- `analysis.all`, `system.backup`, `mock.echo` + +Use `GET /api/catalog` to inspect supported models, algorithms, datasets, and +task types discovered from the existing `Seg/` workspace. + diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..62a2513 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1,2 @@ +"""Seg Data Server backend.""" + diff --git a/backend/app/catalog.py b/backend/app/catalog.py new file mode 100644 index 0000000..a6ee4e6 --- /dev/null +++ b/backend/app/catalog.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from .config import settings +from .paths import rel + + +SEGMODEL_ARCHS = [ + "Unet", + "UnetPlusPlus", + "FPN", + "PSPNet", + "DeepLabV3", + "DeepLabV3Plus", + "Linknet", + "MAnet", + "PAN", + "UPerNet", + "Segformer", + "DPT", +] + + +YOLO_MODELS = [ + "YOLOv8n-seg", + "YOLOv8s-seg", + "YOLOv8m-seg", + "YOLOv8l-seg", + "YOLOv8x-seg", + "YOLOv9c-seg", + "YOLOv9e-seg", + "YOLO11n-seg", + "YOLO11s-seg", + "YOLO11m-seg", + "YOLO11l-seg", + "YOLO11x-seg", + "YOLO12-seg", +] + + +TASK_TYPES = [ + "mock.echo", + "system.backup", + "dataset.rename", + "dataset.to_png", + "dataset.resize", + "dataset.pair", + "dataset.rebuild_labels", + "dataset.stack", + "dataset.stitch", + "dataset.video_frames", + "segmodel.train", + "segmodel.batch_train", + "segmodel.predict", + "segmodel.batch_predict", + "segmodel.flops", + "segmodel.raw_mask_check", + "segmodel.metrics", + "yolo.train", + "yolo.batch_train", + "yolo.predict", + "yolo.batch_predict", + "yolo.heatmap", + "yolo.compare", + "yolo.raw_mask_check", + "yolo.video_visible", + "yolo.video_unvisible", + "mmseg.init_weights", + "mmseg.generate_data", + "mmseg.generate_alg", + "mmseg.train", + "mmseg.metrics", + "mmseg.flops_fps", + "mmseg.draw", + "mmseg.extract_loss_miou", + "analysis.all", +] + + +def _read_json(path: Path) -> Any | None: + try: + return json.loads(path.read_text(encoding="utf-8")) + except Exception: + return None + + +def discover_datasets() -> list[dict[str, Any]]: + root = settings.source_root + candidates: list[dict[str, Any]] = [] + for base in ["DataSet_Public", "BestMode_Predict_Results_DataSet_Public", "Hardisk"]: + parent = root / base + if not parent.exists(): + continue + for item in sorted(parent.iterdir()): + if item.is_dir(): + candidates.append({"name": item.name, "path": rel(item, root), "source": base}) + mmseg_params = root / "Seg_All_In_One_MMSeg" / "My_All_In_One" / "1_Data_Parameter" + for item in sorted(mmseg_params.glob("*.json")): + data = _read_json(item) + if item.name == "All_Data_Record.json" or not data: + continue + candidates.append({"name": item.stem, "path": rel(item, root), "source": "mmseg_parameter"}) + return candidates + + +def discover_mmseg_algorithms() -> list[str]: + alg_dir = settings.source_root / "Seg_All_In_One_MMSeg" / "My_All_In_One" / "2_Alg_Program" + if not alg_dir.exists(): + return [] + return sorted(path.stem for path in alg_dir.glob("*.py")) + + +def discover_weights_summary() -> dict[str, Any]: + manifest = settings.weights_root / "manifest.json" + if not manifest.exists(): + return {"manifest": None, "count": 0, "total_bytes": 0} + data = _read_json(manifest) or {} + return { + "manifest": rel(manifest, settings.project_root), + "count": len(data.get("files", [])), + "total_bytes": data.get("total_bytes", 0), + "updated_at": data.get("updated_at"), + } + + +def get_catalog() -> dict[str, Any]: + return { + "source_root": str(settings.source_root), + "project_root": str(settings.project_root), + "task_types": TASK_TYPES, + "segmodel_architectures": SEGMODEL_ARCHS, + "yolo_models": YOLO_MODELS, + "mmseg_algorithms": discover_mmseg_algorithms(), + "datasets": discover_datasets(), + "weights": discover_weights_summary(), + } + diff --git a/backend/app/commands.py b/backend/app/commands.py new file mode 100644 index 0000000..06d932e --- /dev/null +++ b/backend/app/commands.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path + + +@dataclass +class CommandSpec: + command: list[str] + cwd: Path + description: str = "" + env: dict[str, str] = field(default_factory=dict) + stdin_text: str | None = None + + +TaskFactory = callable + + +def conda_python(env_name: str, script: Path, *args: object) -> list[str]: + return ["conda", "run", "-n", env_name, "python", str(script), *[str(a) for a in args]] + + +def python(script: Path, *args: object) -> list[str]: + return ["python", str(script), *[str(a) for a in args]] + + +def bash(script: Path, *args: object) -> list[str]: + return ["bash", str(script), *[str(a) for a in args]] + + +def option(params: dict, name: str, default=None): + value = params.get(name, default) + return value + + +def required(params: dict, name: str): + if name not in params or params[name] in (None, ""): + raise ValueError(f"missing required parameter: {name}") + return params[name] + + +def append_flag(args: list[str], flag: str, value): + if value not in (None, ""): + args.extend([flag, str(value)]) + diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..c12ece5 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass +from pathlib import Path + + +def _resolve_path(value: str | None, default: Path) -> Path: + base = Path(value).expanduser() if value else default + if not base.is_absolute(): + base = default.parent / base + return base.resolve() + + +@dataclass(frozen=True) +class Settings: + project_root: Path + source_root: Path + db_path: Path + log_dir: Path + weights_root: Path + task_conda_env: str + backend_conda_env: str + weight_mode: str + enable_shell_tasks: bool + + +def get_settings() -> Settings: + project_root = Path(os.getenv("SEG_DATA_SERVER_ROOT", Path(__file__).resolve().parents[2])).expanduser() + if not project_root.is_absolute(): + project_root = (Path(__file__).resolve().parents[2] / project_root).resolve() + else: + project_root = project_root.resolve() + + sibling_source = project_root.parent / "Seg" + default_source = sibling_source if sibling_source.exists() else project_root.parent + source_root = _resolve_path(os.getenv("SEG_SOURCE_ROOT"), default_source) + db_path = _resolve_path(os.getenv("SEG_BACKEND_DB"), project_root / "var" / "seg_data_server.sqlite3") + log_dir = _resolve_path(os.getenv("SEG_BACKEND_LOG_DIR"), project_root / "var" / "job_logs") + weights_root = (project_root / "weights").resolve() + + return Settings( + project_root=project_root, + source_root=source_root, + db_path=db_path, + log_dir=log_dir, + weights_root=weights_root, + task_conda_env=os.getenv("SEG_TASK_CONDA_ENV", "seg_smp"), + backend_conda_env=os.getenv("SEG_BACKEND_CONDA_ENV", "seg_server"), + weight_mode=os.getenv("SEG_WEIGHT_MODE", "copy"), + enable_shell_tasks=os.getenv("SEG_ENABLE_SHELL_TASKS", "1") == "1", + ) + + +settings = get_settings() diff --git a/backend/app/db.py b/backend/app/db.py new file mode 100644 index 0000000..074fa1f --- /dev/null +++ b/backend/app/db.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +import json +import sqlite3 +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from .config import settings + + +def utc_now() -> str: + return datetime.now(timezone.utc).isoformat() + + +def connect() -> sqlite3.Connection: + settings.db_path.parent.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(settings.db_path) + conn.row_factory = sqlite3.Row + return conn + + +def init_db() -> None: + with connect() as conn: + conn.execute( + """ + create table if not exists jobs ( + id text primary key, + type text not null, + status text not null, + params_json text not null, + command_json text not null, + cwd text not null, + description text not null default '', + pid integer, + exit_code integer, + created_at text not null, + started_at text, + finished_at text, + log_path text not null, + error text + ) + """ + ) + conn.execute( + """ + create table if not exists profiles ( + id integer primary key autoincrement, + name text not null, + kind text not null, + data_json text not null, + updated_at text not null, + unique(name, kind) + ) + """ + ) + + +def _job_from_row(row: sqlite3.Row) -> dict[str, Any]: + data = dict(row) + data["params"] = json.loads(data.pop("params_json")) + data["command"] = json.loads(data.pop("command_json")) + return data + + +def insert_job(job: dict[str, Any]) -> None: + with connect() as conn: + conn.execute( + """ + insert into jobs ( + id, type, status, params_json, command_json, cwd, description, + pid, exit_code, created_at, started_at, finished_at, log_path, error + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + job["id"], + job["type"], + job["status"], + json.dumps(job["params"], ensure_ascii=False), + json.dumps(job["command"], ensure_ascii=False), + job["cwd"], + job.get("description", ""), + job.get("pid"), + job.get("exit_code"), + job["created_at"], + job.get("started_at"), + job.get("finished_at"), + job["log_path"], + job.get("error"), + ), + ) + + +def update_job(job_id: str, **fields: Any) -> None: + if not fields: + return + allowed = { + "status", + "pid", + "exit_code", + "started_at", + "finished_at", + "error", + } + updates = {key: value for key, value in fields.items() if key in allowed} + if not updates: + return + assignments = ", ".join(f"{key}=?" for key in updates) + with connect() as conn: + conn.execute(f"update jobs set {assignments} where id=?", [*updates.values(), job_id]) + + +def get_job(job_id: str) -> dict[str, Any] | None: + with connect() as conn: + row = conn.execute("select * from jobs where id=?", (job_id,)).fetchone() + return _job_from_row(row) if row else None + + +def list_jobs(limit: int = 100) -> list[dict[str, Any]]: + with connect() as conn: + rows = conn.execute( + "select * from jobs order by created_at desc limit ?", (limit,) + ).fetchall() + return [_job_from_row(row) for row in rows] + + +def upsert_profile(name: str, kind: str, data: dict[str, Any]) -> dict[str, Any]: + updated_at = utc_now() + with connect() as conn: + conn.execute( + """ + insert into profiles (name, kind, data_json, updated_at) + values (?, ?, ?, ?) + on conflict(name, kind) do update set + data_json=excluded.data_json, + updated_at=excluded.updated_at + """, + (name, kind, json.dumps(data, ensure_ascii=False), updated_at), + ) + return {"name": name, "kind": kind, "data": data, "updated_at": updated_at} + + +def list_profiles(kind: str | None = None) -> list[dict[str, Any]]: + sql = "select name, kind, data_json, updated_at from profiles" + params: tuple = () + if kind: + sql += " where kind=?" + params = (kind,) + sql += " order by kind, name" + with connect() as conn: + rows = conn.execute(sql, params).fetchall() + return [ + { + "name": row["name"], + "kind": row["kind"], + "data": json.loads(row["data_json"]), + "updated_at": row["updated_at"], + } + for row in rows + ] + + +def log_tail(log_path: str | Path, max_bytes: int = 8192) -> str: + path = Path(log_path) + if not path.exists(): + return "" + size = path.stat().st_size + with path.open("rb") as handle: + if size > max_bytes: + handle.seek(size - max_bytes) + return handle.read().decode("utf-8", errors="replace") + diff --git a/backend/app/jobs.py b/backend/app/jobs.py new file mode 100644 index 0000000..68b6e9d --- /dev/null +++ b/backend/app/jobs.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import os +import signal +import subprocess +import threading +import uuid +from pathlib import Path + +from . import db +from .commands import CommandSpec +from .config import settings +from .modules import build_module_task +from .schemas import JobCreate + +_running: dict[str, subprocess.Popen] = {} +_lock = threading.Lock() + + +def _build_task(request: JobCreate) -> CommandSpec: + conda_env = request.conda_env or settings.task_conda_env + spec = build_module_task(request.type, request.params, conda_env) + if spec is None: + raise ValueError(f"unsupported job type: {request.type}") + env = dict(spec.env) + if request.gpus: + env["CUDA_VISIBLE_DEVICES"] = ",".join(str(gpu) for gpu in request.gpus) + return CommandSpec( + command=spec.command, + cwd=spec.cwd, + description=spec.description, + env=env, + stdin_text=spec.stdin_text, + ) + + +def create_job(request: JobCreate) -> dict: + spec = _build_task(request) + job_id = uuid.uuid4().hex + settings.log_dir.mkdir(parents=True, exist_ok=True) + log_path = settings.log_dir / f"{job_id}.log" + job = { + "id": job_id, + "type": request.type, + "status": "queued", + "params": request.params, + "command": spec.command, + "cwd": str(spec.cwd), + "description": spec.description, + "pid": None, + "exit_code": None, + "created_at": db.utc_now(), + "started_at": None, + "finished_at": None, + "log_path": str(log_path), + "error": None, + } + db.insert_job(job) + thread = threading.Thread(target=_run_job, args=(job_id, spec, log_path), daemon=True) + thread.start() + return db.get_job(job_id) + + +def _run_job(job_id: str, spec: CommandSpec, log_path: Path) -> None: + env = os.environ.copy() + env.update(spec.env) + db.update_job(job_id, status="running", started_at=db.utc_now()) + try: + with log_path.open("ab") as log_file: + log_file.write(("COMMAND: " + " ".join(spec.command) + "\n").encode("utf-8")) + log_file.flush() + process = subprocess.Popen( + spec.command, + cwd=str(spec.cwd), + env=env, + stdin=subprocess.PIPE if spec.stdin_text is not None else None, + stdout=log_file, + stderr=subprocess.STDOUT, + start_new_session=True, + ) + with _lock: + _running[job_id] = process + db.update_job(job_id, pid=process.pid) + if spec.stdin_text is not None and process.stdin is not None: + process.stdin.write(spec.stdin_text.encode("utf-8")) + process.stdin.close() + exit_code = process.wait() + with _lock: + _running.pop(job_id, None) + current = db.get_job(job_id) + if current and current["status"] == "cancelled": + db.update_job(job_id, exit_code=exit_code, finished_at=db.utc_now()) + elif exit_code == 0: + db.update_job(job_id, status="success", exit_code=exit_code, finished_at=db.utc_now()) + else: + db.update_job(job_id, status="failed", exit_code=exit_code, finished_at=db.utc_now()) + except Exception as exc: + with _lock: + _running.pop(job_id, None) + db.update_job(job_id, status="failed", error=str(exc), finished_at=db.utc_now()) + + +def cancel_job(job_id: str) -> dict | None: + job = db.get_job(job_id) + if not job: + return None + db.update_job(job_id, status="cancelled", finished_at=db.utc_now()) + with _lock: + process = _running.get(job_id) + if process and process.poll() is None: + try: + os.killpg(process.pid, signal.SIGTERM) + except ProcessLookupError: + pass + return db.get_job(job_id) + diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..4f103ee --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +import asyncio +import json +from pathlib import Path + +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, StreamingResponse + +from . import db +from .catalog import get_catalog +from .config import settings +from .jobs import cancel_job, create_job +from .modules.system.service import disk_usage, get_conda_envs, get_gpus, scan_results +from .modules.weights.service import load_manifest, sync_weights, verify_weights +from .paths import ensure_inside +from .schemas import JobCreate, ProfileCreate, WeightSyncRequest + +app = FastAPI(title="Seg Data Server Net", version="0.1.0") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.on_event("startup") +def startup() -> None: + db.init_db() + settings.log_dir.mkdir(parents=True, exist_ok=True) + settings.weights_root.mkdir(parents=True, exist_ok=True) + + +@app.get("/api/health") +def health() -> dict: + return { + "ok": True, + "source_root": str(settings.source_root), + "project_root": str(settings.project_root), + "disk": disk_usage(), + } + + +@app.get("/api/system/gpus") +def api_gpus() -> dict: + return get_gpus() + + +@app.get("/api/system/envs") +def api_envs() -> dict: + return get_conda_envs() + + +@app.get("/api/catalog") +def api_catalog() -> dict: + return get_catalog() + + +@app.get("/api/profiles") +def api_profiles(kind: str | None = None) -> list[dict]: + return db.list_profiles(kind) + + +@app.post("/api/profiles") +def api_save_profile(profile: ProfileCreate) -> dict: + return db.upsert_profile(profile.name, profile.kind, profile.data) + + +@app.post("/api/jobs") +def api_create_job(request: JobCreate) -> dict: + try: + return create_job(request) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@app.get("/api/jobs") +def api_jobs(limit: int = 100) -> list[dict]: + return db.list_jobs(limit) + + +@app.get("/api/jobs/{job_id}") +def api_job(job_id: str) -> dict: + job = db.get_job(job_id) + if not job: + raise HTTPException(status_code=404, detail="job not found") + job["log_tail"] = db.log_tail(job["log_path"]) + return job + + +@app.post("/api/jobs/{job_id}/cancel") +def api_cancel_job(job_id: str) -> dict: + job = cancel_job(job_id) + if not job: + raise HTTPException(status_code=404, detail="job not found") + return job + + +@app.get("/api/jobs/{job_id}/events") +async def api_job_events(job_id: str): + async def stream(): + last_size = 0 + while True: + job = db.get_job(job_id) + if not job: + yield "event: error\ndata: job not found\n\n" + return + path = Path(job["log_path"]) + chunk = "" + if path.exists(): + size = path.stat().st_size + if size > last_size: + with path.open("rb") as handle: + handle.seek(last_size) + chunk = handle.read(size - last_size).decode("utf-8", errors="replace") + last_size = size + payload = {"job": job, "chunk": chunk} + yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n" + if job["status"] in {"success", "failed", "cancelled"}: + return + await asyncio.sleep(1) + + return StreamingResponse(stream(), media_type="text/event-stream") + + +@app.get("/api/results") +def api_results() -> list[dict]: + return scan_results() + + +@app.get("/api/artifacts/{artifact_path:path}") +def api_artifact(artifact_path: str): + candidate = Path(artifact_path) + if not candidate.is_absolute(): + candidate = settings.source_root / candidate + try: + resolved = candidate.resolve() + allowed = False + for root in (settings.source_root, settings.project_root): + try: + ensure_inside(resolved, root) + allowed = True + break + except Exception: + continue + if not allowed: + raise ValueError("artifact path is outside allowed roots") + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + if not resolved.exists() or not resolved.is_file(): + raise HTTPException(status_code=404, detail="artifact not found") + return FileResponse(resolved) + + +@app.get("/api/weights") +def api_weights() -> dict: + return load_manifest() + + +@app.post("/api/weights/sync") +def api_weight_sync(request: WeightSyncRequest) -> dict: + return sync_weights(request.mode, request.hash_files, request.skip_existing) + + +@app.post("/api/weights/verify") +def api_weight_verify() -> dict: + return verify_weights() + diff --git a/backend/app/modules/__init__.py b/backend/app/modules/__init__.py new file mode 100644 index 0000000..cdd7629 --- /dev/null +++ b/backend/app/modules/__init__.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from .analysis.tasks import build_analysis_task +from .dataset.tasks import build_dataset_task +from .mmseg.tasks import build_mmseg_task +from .segmodel.tasks import build_segmodel_task +from .system.tasks import build_system_task +from .yolo.tasks import build_yolo_task + + +def build_module_task(job_type: str, params: dict, conda_env: str): + for builder in ( + build_dataset_task, + build_segmodel_task, + build_yolo_task, + build_mmseg_task, + build_analysis_task, + build_system_task, + ): + spec = builder(job_type, params, conda_env) + if spec is not None: + return spec + return None + diff --git a/backend/app/modules/analysis/__init__.py b/backend/app/modules/analysis/__init__.py new file mode 100644 index 0000000..86174a9 --- /dev/null +++ b/backend/app/modules/analysis/__init__.py @@ -0,0 +1,2 @@ +"""Analysis task wrappers.""" + diff --git a/backend/app/modules/analysis/tasks.py b/backend/app/modules/analysis/tasks.py new file mode 100644 index 0000000..a9617b8 --- /dev/null +++ b/backend/app/modules/analysis/tasks.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from ...commands import CommandSpec, append_flag, conda_python +from ...config import settings + + +ANALYSIS_DIR = settings.source_root / "Seg_All_In_One_Analysis" + + +def build_analysis_task(job_type: str, params: dict, conda_env: str) -> CommandSpec | None: + if job_type != "analysis.all": + return None + args = conda_python(conda_env, ANALYSIS_DIR / "1_Analysis_All.py") + append_flag(args, "--input_dir", params.get("input_dir", "../BestMode_Predict_Results_DataSet_Public")) + append_flag(args, "--output_dir", params.get("output_dir", "./")) + stdin = f"{params.get('dataset_choice', 1)}\n" + return CommandSpec(args, ANALYSIS_DIR, "merge SegModel/MMSeg metrics and generate plots", stdin_text=stdin) + diff --git a/backend/app/modules/dataset/__init__.py b/backend/app/modules/dataset/__init__.py new file mode 100644 index 0000000..5d6b5ec --- /dev/null +++ b/backend/app/modules/dataset/__init__.py @@ -0,0 +1,2 @@ +"""Dataset task wrappers.""" + diff --git a/backend/app/modules/dataset/tasks.py b/backend/app/modules/dataset/tasks.py new file mode 100644 index 0000000..53b6811 --- /dev/null +++ b/backend/app/modules/dataset/tasks.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from pathlib import Path + +from ...commands import CommandSpec, append_flag, bash, conda_python, required +from ...config import settings + + +DATASET_TOOL_DIR = settings.source_root / "DataSet_Own" / "1. 图片预处理(内含使用手册)" +STACK_TOOL_DIR = settings.source_root / "Tool-图片堆叠" +VIDEO_DIR = settings.source_root / "Seg_Predict_Own_Video_V2" +YOLO_DATASET_DIR = settings.source_root / "Seg_All_In_One_YoloModel" / "Yolo数据集构建" + + +def _dataset_script(name: str) -> Path: + return DATASET_TOOL_DIR / name + + +def build_dataset_task(job_type: str, params: dict, conda_env: str) -> CommandSpec | None: + if job_type == "dataset.rename": + args = bash(_dataset_script("1_rename_pics.sh")) + append_flag(args, "-i", required(params, "image_dir")) + append_flag(args, "-l", required(params, "label_dir")) + return CommandSpec(args, DATASET_TOOL_DIR, "rename and normalize image/label names") + + if job_type == "dataset.to_png": + script = _dataset_script("2_1_Trans_to_png.py") + args = conda_python(conda_env, script) + append_flag(args, "-i", params.get("input_dir")) + append_flag(args, "-o", params.get("output_dir")) + return CommandSpec(args, DATASET_TOOL_DIR, "convert images to png") + + if job_type == "dataset.resize": + args = bash(_dataset_script("2_reformate_pics.sh")) + append_flag(args, "-i", params.get("image_dir")) + append_flag(args, "-l", params.get("label_dir")) + append_flag(args, "-w", params.get("width", 1920)) + append_flag(args, "-h", params.get("height", 1080)) + return CommandSpec(args, DATASET_TOOL_DIR, "resize and reformat image/label folders") + + if job_type == "dataset.pair": + args = bash(_dataset_script("3_pair_ori_label.sh")) + append_flag(args, "-i", required(params, "image_dir")) + append_flag(args, "-l", required(params, "label_dir")) + append_flag(args, "-p", params.get("prefix", "")) + append_flag(args, "-s", params.get("suffix", "")) + return CommandSpec(args, DATASET_TOOL_DIR, "check image and label pairing") + + if job_type == "dataset.rebuild_labels": + args = bash(_dataset_script("4_rebuild_labels.sh")) + append_flag(args, "-l", required(params, "label_dir")) + return CommandSpec(args, DATASET_TOOL_DIR, "rebuild color labels into GT masks") + + if job_type == "dataset.stack": + args = bash(_dataset_script("5_TOOL_stack_pics.sh")) + append_flag(args, "-i", required(params, "image_dir")) + append_flag(args, "-l", required(params, "label_dir")) + append_flag(args, "-r", required(params, "result_dir")) + append_flag(args, "-a", params.get("alpha", 0.3)) + append_flag(args, "-p", params.get("prefix", "")) + append_flag(args, "-s", params.get("suffix", "")) + return CommandSpec(args, DATASET_TOOL_DIR, "overlay image and label for inspection") + + if job_type == "dataset.stitch": + args = bash(_dataset_script("6_TOOL_stitch_pics.sh")) + append_flag(args, "-i", required(params, "image_dir")) + append_flag(args, "-l", required(params, "label_dir")) + append_flag(args, "-r", required(params, "result_dir")) + return CommandSpec(args, DATASET_TOOL_DIR, "stitch image and label panels") + + if job_type == "dataset.video_frames": + script = VIDEO_DIR / "1_Save_Frame_V2.py" + args = conda_python(conda_env, script) + append_flag(args, "--video", required(params, "video")) + append_flag(args, "--interval", params.get("interval", 0.5)) + append_flag(args, "--resize", params.get("resize")) + append_flag(args, "--output_dir", params.get("output_dir")) + return CommandSpec(args, VIDEO_DIR, "extract video frames into DataSet_Public layout") + + if job_type == "dataset.yolo_check_pairs": + script = YOLO_DATASET_DIR / "0_1_check_picture_pair.py" + args = conda_python(conda_env, script) + append_flag(args, "-i", required(params, "image_dir")) + append_flag(args, "-l", required(params, "label_dir")) + return CommandSpec(args, YOLO_DATASET_DIR, "check YOLO image/label pairs") + + return None + diff --git a/backend/app/modules/mmseg/__init__.py b/backend/app/modules/mmseg/__init__.py new file mode 100644 index 0000000..e4f38f2 --- /dev/null +++ b/backend/app/modules/mmseg/__init__.py @@ -0,0 +1,2 @@ +"""MMSeg task wrappers.""" + diff --git a/backend/app/modules/mmseg/tasks.py b/backend/app/modules/mmseg/tasks.py new file mode 100644 index 0000000..abbb2c8 --- /dev/null +++ b/backend/app/modules/mmseg/tasks.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from ...commands import CommandSpec, append_flag, conda_python, required +from ...config import settings + + +MMSEG_DIR = settings.source_root / "Seg_All_In_One_MMSeg" +MY_DIR = MMSEG_DIR / "My_All_In_One" + + +def _stdin_for_generate_alg(params: dict) -> str: + lines = [ + str(params.get("dataset_choice", 1)), + str(params.get("gpu_count", 1)), + ] + gpu_ids = params.get("gpu_ids", [0]) + if isinstance(gpu_ids, str): + gpu_ids = [part.strip() for part in gpu_ids.split(",") if part.strip()] + for index in range(int(params.get("gpu_count", len(gpu_ids) or 1))): + lines.append(str(gpu_ids[index] if index < len(gpu_ids) else 0)) + + mode = str(params.get("schedule_mode", 2)) + lines.append(mode) + if mode == "1": + lines.extend( + [ + str(params.get("train_k", 40)), + str(params.get("check_count", 10)), + str(params.get("logger_interval", 50)), + ] + ) + else: + lines.extend( + [ + str(params.get("max_epochs", 300)), + str(params.get("val_interval", 1)), + str(params.get("checkpoint_interval", 10)), + str(params.get("logger_interval", "")), + ] + ) + lines.append(str(params.get("algorithm_choice", 1))) + return "\n".join(lines) + "\n" + + +def build_mmseg_task(job_type: str, params: dict, conda_env: str) -> CommandSpec | None: + if job_type == "mmseg.init_weights": + return CommandSpec(conda_python(conda_env, MY_DIR / "0_Initial_Save_All_Model_locally.py"), MMSEG_DIR, "download/save MMSeg pretrained weights locally") + + if job_type == "mmseg.generate_data": + return CommandSpec(conda_python(conda_env, MY_DIR / "1_Initial_Data_All_data_from_1_Data_Parameter-V2.py"), MMSEG_DIR, "generate MMSeg dataset configs from JSON parameters") + + if job_type == "mmseg.generate_alg": + script = MY_DIR / "2_Initial_Alg_All_data_from_2_Alg_Program-V2.py" + return CommandSpec( + conda_python(conda_env, script), + MMSEG_DIR, + "generate MMSeg algorithm config and training command", + stdin_text=_stdin_for_generate_alg(params), + ) + + if job_type == "mmseg.train": + config_path = required(params, "config") + args = conda_python(conda_env, MMSEG_DIR / "tools" / "train.py", config_path) + append_flag(args, "--work-dir", params.get("work_dir")) + return CommandSpec(args, MMSEG_DIR, "train MMSeg model") + + if job_type == "mmseg.metrics": + args = conda_python(conda_env, MY_DIR / "4_2_predict_matrics_from_log_V2.py") + append_flag(args, "--input_dir", params.get("input_dir", "../Hardisk")) + append_flag(args, "--output_dir", params.get("output_dir", "../BestMode_Predict_Results_DataSet_Public")) + stdin = f"{params.get('dataset_choice', 1)}\n{params.get('algorithm_choice', 0)}\n" + return CommandSpec(args, MMSEG_DIR, "extract best MMSeg metrics from logs", stdin_text=stdin) + + if job_type == "mmseg.flops_fps": + args = conda_python(conda_env, MY_DIR / "4_1_predict_params_FLOPs_FPS_V2.py") + append_flag(args, "--input_dir", params.get("input_dir", "../Hardisk")) + append_flag(args, "--output_dir", params.get("output_dir", "../BestMode_Predict_Results_DataSet_Public")) + append_flag(args, "--repeat-times", params.get("repeat_times", 3)) + stdin = f"{params.get('dataset_choice', 1)}\n{params.get('algorithm_choice', 0)}\n" + if "shape_h" in params and "shape_w" in params: + stdin += f"{params['shape_h']}\n{params['shape_w']}\n" + return CommandSpec(args, MMSEG_DIR, "calculate MMSeg FLOPs/Params/FPS", stdin_text=stdin) + + if job_type == "mmseg.draw": + return CommandSpec(conda_python(conda_env, MY_DIR / "4_3_predict_draw_pictures_and_tabels.py"), MMSEG_DIR, "generate MMSeg prediction pictures and tables") + + if job_type == "mmseg.extract_loss_miou": + return CommandSpec(conda_python(conda_env, MY_DIR / "4_4_extract_loss_and_best_miou.py"), MMSEG_DIR, "extract MMSeg loss and best mIoU curves") + + if job_type == "mmseg.delete_epoch": + return CommandSpec(conda_python(conda_env, MY_DIR / "3_Find_And_Delete_Special_Epoch.py"), MMSEG_DIR, "find and delete selected epoch checkpoints") + + return None + diff --git a/backend/app/modules/segmodel/__init__.py b/backend/app/modules/segmodel/__init__.py new file mode 100644 index 0000000..1ed492a --- /dev/null +++ b/backend/app/modules/segmodel/__init__.py @@ -0,0 +1,2 @@ +"""SegModel task wrappers.""" + diff --git a/backend/app/modules/segmodel/tasks.py b/backend/app/modules/segmodel/tasks.py new file mode 100644 index 0000000..b006d74 --- /dev/null +++ b/backend/app/modules/segmodel/tasks.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from ...commands import CommandSpec, append_flag, bash, conda_python, required +from ...config import settings + + +SEGMODEL_DIR = settings.source_root / "Seg_All_In_One_SegModel" + + +def build_segmodel_task(job_type: str, params: dict, conda_env: str) -> CommandSpec | None: + env = {"SEG_CONDA_ENV": conda_env} + + if job_type == "segmodel.train": + args = conda_python(conda_env, SEGMODEL_DIR / "train.py") + append_flag(args, "-a", required(params, "architecture")) + return CommandSpec(args, SEGMODEL_DIR, "train one segmentation_models_pytorch architecture") + + if job_type == "segmodel.batch_train": + return CommandSpec(bash(SEGMODEL_DIR / "train.sh"), SEGMODEL_DIR, "run legacy SegModel batch training", env=env) + + if job_type == "segmodel.predict": + args = conda_python(conda_env, SEGMODEL_DIR / "1_predict.py") + append_flag(args, "-a", required(params, "architecture")) + choice = str(params.get("run_choice", 1)) + return CommandSpec(args, SEGMODEL_DIR, "predict with one SegModel run", stdin_text=f"{choice}\n") + + if job_type == "segmodel.batch_predict": + return CommandSpec(bash(SEGMODEL_DIR / "predict.sh"), SEGMODEL_DIR, "run legacy SegModel batch prediction", env=env) + + if job_type == "segmodel.flops": + script = SEGMODEL_DIR / params.get("script", "2_predict_params_and_FLOPs_V2.py") + return CommandSpec(conda_python(conda_env, script), SEGMODEL_DIR, "calculate SegModel params/FLOPs/FPS") + + if job_type == "segmodel.raw_mask_check": + return CommandSpec(conda_python(conda_env, SEGMODEL_DIR / "1_predict_raw_masks_check.py"), SEGMODEL_DIR, "check SegModel raw mask completeness") + + if job_type == "segmodel.metrics": + return CommandSpec(conda_python(conda_env, SEGMODEL_DIR / "3_predict_matrics_from_log.py"), SEGMODEL_DIR, "parse SegModel training/prediction metrics") + + return None + diff --git a/backend/app/modules/system/__init__.py b/backend/app/modules/system/__init__.py new file mode 100644 index 0000000..e120f6e --- /dev/null +++ b/backend/app/modules/system/__init__.py @@ -0,0 +1,2 @@ +"""System task wrappers.""" + diff --git a/backend/app/modules/system/service.py b/backend/app/modules/system/service.py new file mode 100644 index 0000000..7c981b2 --- /dev/null +++ b/backend/app/modules/system/service.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + +from ...config import settings + + +def parse_nvidia_smi_csv(output: str) -> list[dict]: + gpus: list[dict] = [] + for line in output.splitlines(): + if not line.strip(): + continue + parts = [part.strip() for part in line.split(",")] + if len(parts) < 7: + continue + index, name, total, used, free, util, temp = parts[:7] + try: + gpus.append( + { + "index": int(index), + "name": name, + "memory_total_mb": int(total), + "memory_used_mb": int(used), + "memory_free_mb": int(free), + "utilization_gpu_percent": int(util), + "temperature_c": int(temp), + } + ) + except ValueError: + continue + return gpus + + +def get_gpus() -> dict: + cmd = [ + "nvidia-smi", + "--query-gpu=index,name,memory.total,memory.used,memory.free,utilization.gpu,temperature.gpu", + "--format=csv,noheader,nounits", + ] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return {"available": True, "gpus": parse_nvidia_smi_csv(result.stdout)} + except Exception as exc: + return {"available": False, "gpus": [], "error": str(exc)} + + +def get_conda_envs() -> dict: + try: + result = subprocess.run(["conda", "env", "list"], capture_output=True, text=True, check=True) + except Exception as exc: + return {"available": False, "envs": [], "error": str(exc)} + envs = [] + for line in result.stdout.splitlines(): + raw = line.strip() + if not raw or raw.startswith("#"): + continue + marker = "*" in raw.split() + parts = raw.replace("*", " ").split() + if len(parts) >= 2: + envs.append({"name": parts[0], "path": parts[-1], "active": marker}) + return {"available": True, "envs": envs, "task_default": settings.task_conda_env} + + +def disk_usage() -> dict: + usage = shutil.disk_usage(settings.source_root) + return { + "path": str(settings.source_root), + "total": usage.total, + "used": usage.used, + "free": usage.free, + } + + +def scan_results() -> list[dict]: + roots = [ + settings.source_root / "DataSet_Public_outputs", + settings.source_root / "BestMode_Predict_Results_DataSet_Public", + settings.source_root / "Hardisk", + settings.source_root / "Seg_All_In_One_Analysis", + ] + exts = {".csv", ".png", ".jpg", ".jpeg", ".svg", ".log", ".pth", ".pt"} + results: list[dict] = [] + for root in roots: + if not root.exists(): + continue + for path in root.rglob("*"): + if path.is_file() and path.suffix.lower() in exts: + try: + stat = path.stat() + results.append( + { + "name": path.name, + "path": str(path.resolve()), + "relative_path": str(path.resolve().relative_to(settings.source_root)), + "size": stat.st_size, + "modified": stat.st_mtime, + "kind": path.suffix.lower().lstrip("."), + } + ) + except OSError: + continue + results.sort(key=lambda item: item["modified"], reverse=True) + return results[:1000] + diff --git a/backend/app/modules/system/tasks.py b/backend/app/modules/system/tasks.py new file mode 100644 index 0000000..b4e60d5 --- /dev/null +++ b/backend/app/modules/system/tasks.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from ...commands import CommandSpec, bash +from ...config import settings + + +def build_system_task(job_type: str, params: dict, conda_env: str) -> CommandSpec | None: + if job_type == "system.backup": + return CommandSpec(bash(settings.source_root / "Back_Up.sh"), settings.source_root, "run legacy backup script") + if job_type == "mock.echo": + message = params.get("message", "Seg Data Server mock job") + return CommandSpec(["python", "-c", f"print({message!r})"], settings.project_root, "test job runner") + return None + diff --git a/backend/app/modules/weights/__init__.py b/backend/app/modules/weights/__init__.py new file mode 100644 index 0000000..b9bc8a0 --- /dev/null +++ b/backend/app/modules/weights/__init__.py @@ -0,0 +1,2 @@ +"""Weight sync and verification.""" + diff --git a/backend/app/modules/weights/service.py b/backend/app/modules/weights/service.py new file mode 100644 index 0000000..02c324a --- /dev/null +++ b/backend/app/modules/weights/service.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +import hashlib +import json +import os +import shutil +import subprocess +from datetime import datetime, timezone +from pathlib import Path +from typing import Iterable + +from ...config import settings + +WEIGHT_EXTS = {".pt", ".pth", ".onnx", ".engine"} + + +def sha256_file(path: Path, chunk_size: int = 1024 * 1024 * 8) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + while True: + chunk = handle.read(chunk_size) + if not chunk: + break + digest.update(chunk) + return digest.hexdigest() + + +def iter_source_weights() -> Iterable[Path]: + project_root = settings.project_root.resolve() + for path in settings.source_root.rglob("*"): + if not path.is_file() or path.suffix.lower() not in WEIGHT_EXTS: + continue + try: + path.resolve().relative_to(project_root) + continue + except ValueError: + yield path + + +def classify_weight(path: Path) -> dict[str, str]: + try: + rel = str(path.resolve().relative_to(settings.source_root)) + except ValueError: + rel = str(path) + lower = rel.lower() + if "yolo" in lower: + family = "yolo" + elif "mmseg" in lower or "my_local_model" in lower: + family = "mmseg" + elif "segmodel" in lower: + family = "segmodel" + else: + family = "misc" + if "best.pt" in lower or "best.pth" in lower: + role = "trained_best" + elif "last.pt" in lower or "last.pth" in lower: + role = "trained_last" + elif "pretrain" in lower or "my_local_model" in lower: + role = "pretrained" + else: + role = "weight" + return {"family": family, "role": role} + + +def copy_weight(src: Path, dst: Path, mode: str) -> None: + dst.parent.mkdir(parents=True, exist_ok=True) + if mode == "hardlink": + if dst.exists(): + dst.unlink() + os.link(src, dst) + elif mode == "reflink": + subprocess.run(["cp", "--reflink=auto", "--preserve=timestamps", str(src), str(dst)], check=True) + else: + shutil.copy2(src, dst) + + +def sync_weights(mode: str = "copy", hash_files: bool = True, skip_existing: bool = True) -> dict: + files_dir = settings.weights_root / "files" + settings.weights_root.mkdir(parents=True, exist_ok=True) + entries = [] + total_bytes = 0 + for src in sorted(iter_source_weights()): + rel = src.resolve().relative_to(settings.source_root) + dst = files_dir / rel + stat = src.stat() + total_bytes += stat.st_size + copied = False + if not (skip_existing and dst.exists() and dst.stat().st_size == stat.st_size): + copy_weight(src, dst, mode) + copied = True + meta = classify_weight(src) + entry = { + "source_path": str(rel), + "stored_path": str(dst.resolve().relative_to(settings.project_root)), + "size": stat.st_size, + "family": meta["family"], + "role": meta["role"], + "copied": copied, + } + if hash_files: + entry["sha256"] = sha256_file(dst) + entries.append(entry) + + manifest = { + "generated_at": datetime.now(timezone.utc).isoformat(), + "updated_at": datetime.now(timezone.utc).isoformat(), + "source_root": str(settings.source_root), + "mode": mode, + "count": len(entries), + "total_bytes": total_bytes, + "files": entries, + } + manifest_path = settings.weights_root / "manifest.json" + manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8") + return manifest + + +def load_manifest() -> dict: + manifest_path = settings.weights_root / "manifest.json" + if not manifest_path.exists(): + return { + "generated_at": None, + "source_root": str(settings.source_root), + "count": 0, + "total_bytes": 0, + "files": [], + } + return json.loads(manifest_path.read_text(encoding="utf-8")) + + +def verify_weights() -> dict: + manifest = load_manifest() + checked = [] + ok_count = 0 + for entry in manifest.get("files", []): + path = settings.project_root / entry["stored_path"] + exists = path.exists() + size_ok = exists and path.stat().st_size == entry.get("size") + hash_ok = None + if exists and "sha256" in entry: + hash_ok = sha256_file(path) == entry["sha256"] + ok = bool(exists and size_ok and (hash_ok is not False)) + ok_count += int(ok) + checked.append( + { + "stored_path": entry["stored_path"], + "exists": exists, + "size_ok": size_ok, + "hash_ok": hash_ok, + "ok": ok, + } + ) + return {"count": len(checked), "ok_count": ok_count, "items": checked} diff --git a/backend/app/modules/yolo/__init__.py b/backend/app/modules/yolo/__init__.py new file mode 100644 index 0000000..ae4513d --- /dev/null +++ b/backend/app/modules/yolo/__init__.py @@ -0,0 +1,2 @@ +"""YOLO task wrappers.""" + diff --git a/backend/app/modules/yolo/tasks.py b/backend/app/modules/yolo/tasks.py new file mode 100644 index 0000000..ff1ad71 --- /dev/null +++ b/backend/app/modules/yolo/tasks.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from ...commands import CommandSpec, append_flag, bash, conda_python, required +from ...config import settings + + +YOLO_DIR = settings.source_root / "Seg_All_In_One_YoloModel" +VIDEO_YOLO_DIR = settings.source_root / "Seg_Predict_YoloModel" + + +def build_yolo_task(job_type: str, params: dict, conda_env: str) -> CommandSpec | None: + env = {"SEG_CONDA_ENV": conda_env} + + if job_type == "yolo.train": + args = conda_python(conda_env, YOLO_DIR / "yolo_train.py") + append_flag(args, "--model", required(params, "model")) + return CommandSpec(args, YOLO_DIR, "train one Ultralytics YOLO segmentation model") + + if job_type == "yolo.batch_train": + return CommandSpec(bash(YOLO_DIR / "yolo_train.sh"), YOLO_DIR, "run legacy YOLO batch training", env=env) + + if job_type == "yolo.predict": + args = conda_python(conda_env, YOLO_DIR / "yolo_predict_V2.py") + append_flag(args, "--model", required(params, "model")) + append_flag(args, "--source", params.get("source")) + append_flag(args, "--pt_name", params.get("pt_name", "best.pt")) + append_flag(args, "--conf", params.get("conf", 0.2)) + choice = str(params.get("run_choice", 1)) + return CommandSpec(args, YOLO_DIR, "predict with one YOLO model", stdin_text=f"{choice}\n") + + if job_type == "yolo.batch_predict": + args = bash(YOLO_DIR / "yolo_predict.sh") + append_flag(args, "--pt_name", params.get("pt_name", "best.pt")) + append_flag(args, "--conf", params.get("conf", 0.2)) + append_flag(args, "--heatmap_method", params.get("heatmap_method")) + return CommandSpec(args, YOLO_DIR, "run legacy YOLO batch prediction", env=env) + + if job_type == "yolo.heatmap": + args = conda_python(conda_env, YOLO_DIR / "yolo_predict_visualize_nn.py") + append_flag(args, "--model", required(params, "model")) + append_flag(args, "--target_layers", params.get("target_layers", "default")) + append_flag(args, "--cam_method", params.get("cam_method", "All")) + append_flag(args, "--pt_name", params.get("pt_name", "best.pt")) + choice = str(params.get("run_choice", 1)) + return CommandSpec(args, YOLO_DIR, "generate YOLO heatmaps", stdin_text=f"{choice}\n") + + if job_type == "yolo.compare": + args = conda_python(conda_env, YOLO_DIR / "yolo_predict_V2_compare_all.py") + append_flag(args, "--pt_name", params.get("pt_name", "all")) + return CommandSpec(args, YOLO_DIR, "compare all YOLO prediction outputs") + + if job_type == "yolo.raw_mask_check": + args = conda_python(conda_env, YOLO_DIR / "yolo_predict_raw_masks_check.py") + append_flag(args, "--pt_name", params.get("pt_name", "best.pt")) + return CommandSpec(args, YOLO_DIR, "check YOLO raw mask completeness") + + if job_type == "yolo.copy_best": + args = bash(YOLO_DIR / "Tool_Yolo_Copy_Best_Model.sh") + append_flag(args, "--pt_name", params.get("pt_name", "best.pt")) + return CommandSpec(args, YOLO_DIR, "copy YOLO best weights into prediction area") + + if job_type == "yolo.video_visible": + return CommandSpec(conda_python(conda_env, VIDEO_YOLO_DIR / "yolo_Seg_Video-V1-Visible.py"), VIDEO_YOLO_DIR, "render visible YOLO video prediction") + + if job_type == "yolo.video_unvisible": + return CommandSpec(conda_python(conda_env, VIDEO_YOLO_DIR / "yolo_Seg_Video-V2-UnVisible.py"), VIDEO_YOLO_DIR, "render invisible/headless YOLO video prediction") + + return None + diff --git a/backend/app/paths.py b/backend/app/paths.py new file mode 100644 index 0000000..ddae6e2 --- /dev/null +++ b/backend/app/paths.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from pathlib import Path + + +class PathSecurityError(ValueError): + pass + + +def ensure_inside(path: Path, root: Path) -> Path: + resolved = path.expanduser().resolve() + root_resolved = root.expanduser().resolve() + try: + resolved.relative_to(root_resolved) + except ValueError as exc: + raise PathSecurityError(f"path escapes root: {resolved}") from exc + return resolved + + +def resolve_user_path(value: str | None, default: Path, root: Path) -> Path: + if value: + candidate = Path(value).expanduser() + if not candidate.is_absolute(): + candidate = root / candidate + else: + candidate = default + return ensure_inside(candidate, root) + + +def rel(path: Path, root: Path) -> str: + try: + return str(path.resolve().relative_to(root.resolve())) + except ValueError: + return str(path.resolve()) + diff --git a/backend/app/schemas.py b/backend/app/schemas.py new file mode 100644 index 0000000..b259a6e --- /dev/null +++ b/backend/app/schemas.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, Field + + +JobStatus = Literal["queued", "running", "success", "failed", "cancelled"] + + +class JobCreate(BaseModel): + type: str = Field(..., examples=["segmodel.train", "yolo.predict"]) + params: dict[str, Any] = Field(default_factory=dict) + gpus: list[int] | None = None + conda_env: str | None = None + + +class JobRecord(BaseModel): + id: str + type: str + status: JobStatus + params: dict[str, Any] + command: list[str] + cwd: str + description: str = "" + pid: int | None = None + exit_code: int | None = None + created_at: str + started_at: str | None = None + finished_at: str | None = None + log_path: str + error: str | None = None + + +class ProfileCreate(BaseModel): + name: str + kind: str + data: dict[str, Any] = Field(default_factory=dict) + + +class WeightSyncRequest(BaseModel): + mode: Literal["copy", "reflink", "hardlink"] = "copy" + hash_files: bool = True + skip_existing: bool = True + diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..2de758c --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,6 @@ +fastapi>=0.110 +uvicorn[standard]>=0.27 +pydantic>=2 +python-multipart>=0.0.9 +pytest>=8 + diff --git a/backend/tests/test_catalog.py b/backend/tests/test_catalog.py new file mode 100644 index 0000000..2b3967c --- /dev/null +++ b/backend/tests/test_catalog.py @@ -0,0 +1,15 @@ +from app.catalog import SEGMODEL_ARCHS, TASK_TYPES, YOLO_MODELS + + +def test_catalog_contains_required_capabilities(): + assert "Unet" in SEGMODEL_ARCHS + assert "YOLOv9e-seg" in YOLO_MODELS + for task in [ + "dataset.video_frames", + "segmodel.train", + "yolo.predict", + "mmseg.flops_fps", + "analysis.all", + ]: + assert task in TASK_TYPES + diff --git a/backend/tests/test_system_service.py b/backend/tests/test_system_service.py new file mode 100644 index 0000000..7c3441f --- /dev/null +++ b/backend/tests/test_system_service.py @@ -0,0 +1,18 @@ +from app.modules.system.service import parse_nvidia_smi_csv + + +def test_parse_nvidia_smi_csv(): + output = "0, NVIDIA GeForce RTX 4090, 24564, 15, 24069, 0, 37\n" + gpus = parse_nvidia_smi_csv(output) + assert gpus == [ + { + "index": 0, + "name": "NVIDIA GeForce RTX 4090", + "memory_total_mb": 24564, + "memory_used_mb": 15, + "memory_free_mb": 24069, + "utilization_gpu_percent": 0, + "temperature_c": 37, + } + ] + diff --git a/backend/tests/test_weights_service.py b/backend/tests/test_weights_service.py new file mode 100644 index 0000000..c571bba --- /dev/null +++ b/backend/tests/test_weights_service.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from app.modules.weights.service import classify_weight + + +def test_classify_weight(): + item = classify_weight(Path("Seg_All_In_One_YoloModel/yolov8n-seg.pt")) + assert item["family"] == "yolo" + assert item["role"] == "weight" + + best = classify_weight(Path("Seg_Predict_YoloModel/YOLOv9e-seg/weights/best.pt")) + assert best["family"] == "yolo" + assert best["role"] == "trained_best" + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..3e60698 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + Seg Data Server Net + + +
+ + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..16028e6 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1721 @@ +{ + "name": "seg-data-server-net-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "seg-data-server-net-frontend", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-react": "^4.3.4", + "lucide-react": "^0.468.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "typescript": "^5.7.2", + "vite": "^6.0.7" + }, + "devDependencies": { + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.381", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.381.tgz", + "integrity": "sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==", + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..b7daf7e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "seg-data-server-net-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@vitejs/plugin-react": "^4.3.4", + "lucide-react": "^0.468.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "typescript": "^5.7.2", + "vite": "^6.0.7" + }, + "devDependencies": { + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3" + } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..7218dc2 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,400 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { createRoot } from "react-dom/client"; +import { + Activity, + BarChart3, + Cpu, + Database, + FileSearch, + Gauge, + HardDrive, + Layers3, + Play, + RefreshCcw, + ShieldCheck, + Square, + Terminal, + UploadCloud, + Zap +} from "lucide-react"; +import "./styles.css"; + +const API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8000"; + +type Job = { + id: string; + type: string; + status: string; + description: string; + created_at: string; + started_at?: string; + finished_at?: string; + log_tail?: string; + params: Record; +}; + +type Catalog = { + task_types: string[]; + segmodel_architectures: string[]; + yolo_models: string[]; + mmseg_algorithms: string[]; + datasets: Array<{ name: string; path: string; source: string }>; + weights: { count: number; total_bytes: number; updated_at?: string }; +}; + +type GpuPayload = { + available: boolean; + gpus: Array<{ + index: number; + name: string; + memory_total_mb: number; + memory_used_mb: number; + memory_free_mb: number; + utilization_gpu_percent: number; + temperature_c: number; + }>; +}; + +async function api(path: string, init?: RequestInit): Promise { + const res = await fetch(`${API_BASE}${path}`, { + headers: { "Content-Type": "application/json" }, + ...init + }); + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} + +const defaultParams: Record> = { + "mock.echo": { message: "hello from Seg Data Server" }, + "dataset.video_frames": { video: "../Seg_Predict_Own_Video_V2/LC_Video_1.mp4", interval: 0.5, resize: "1920x1080" }, + "segmodel.train": { architecture: "Unet" }, + "segmodel.predict": { architecture: "Unet", run_choice: 1 }, + "yolo.train": { model: "YOLOv8n-seg" }, + "yolo.predict": { model: "YOLOv8n-seg", pt_name: "best.pt", conf: 0.2, run_choice: 1 }, + "yolo.heatmap": { model: "YOLOv8n-seg", cam_method: "All", pt_name: "best.pt", run_choice: 1 }, + "mmseg.generate_alg": { dataset_choice: 1, gpu_count: 1, gpu_ids: [0], schedule_mode: 2, max_epochs: 300, algorithm_choice: 1 }, + "mmseg.train": { config: "configs/example.py", work_dir: "../DataSet_Public_outputs/example" }, + "mmseg.metrics": { input_dir: "../Hardisk", output_dir: "../BestMode_Predict_Results_DataSet_Public", dataset_choice: 1, algorithm_choice: 0 }, + "mmseg.flops_fps": { input_dir: "../Hardisk", output_dir: "../BestMode_Predict_Results_DataSet_Public", repeat_times: 3, dataset_choice: 1, algorithm_choice: 0 }, + "analysis.all": { input_dir: "../BestMode_Predict_Results_DataSet_Public", output_dir: "./", dataset_choice: 1 } +}; + +function formatBytes(value?: number) { + if (!value) return "0 B"; + const units = ["B", "KB", "MB", "GB", "TB"]; + let next = value; + let unit = 0; + while (next >= 1024 && unit < units.length - 1) { + next /= 1024; + unit += 1; + } + return `${next.toFixed(unit > 1 ? 2 : 0)} ${units[unit]}`; +} + +function useData() { + const [catalog, setCatalog] = useState(null); + const [gpus, setGpus] = useState(null); + const [jobs, setJobs] = useState([]); + const [results, setResults] = useState>>([]); + const [error, setError] = useState(""); + + async function refresh() { + try { + const [catalogNext, gpusNext, jobsNext, resultsNext] = await Promise.all([ + api("/api/catalog"), + api("/api/system/gpus"), + api("/api/jobs"), + api>>("/api/results") + ]); + setCatalog(catalogNext); + setGpus(gpusNext); + setJobs(jobsNext); + setResults(resultsNext.slice(0, 80)); + setError(""); + } catch (err) { + setError(String(err)); + } + } + + useEffect(() => { + refresh(); + const timer = window.setInterval(refresh, 5000); + return () => window.clearInterval(timer); + }, []); + + return { catalog, gpus, jobs, results, error, refresh }; +} + +function StatusPill({ status }: { status: string }) { + return {status}; +} + +function App() { + const { catalog, gpus, jobs, results, error, refresh } = useData(); + const [taskType, setTaskType] = useState("mock.echo"); + const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2)); + const [selectedJob, setSelectedJob] = useState(null); + const [log, setLog] = useState(""); + const [busy, setBusy] = useState(false); + + const runningCount = jobs.filter((job) => job.status === "running").length; + const successCount = jobs.filter((job) => job.status === "success").length; + const failedCount = jobs.filter((job) => job.status === "failed").length; + + const taskGroups = useMemo>(() => { + const items = catalog?.task_types ?? []; + return { + dataset: items.filter((task) => task.startsWith("dataset.")), + segmodel: items.filter((task) => task.startsWith("segmodel.")), + yolo: items.filter((task) => task.startsWith("yolo.")), + mmseg: items.filter((task) => task.startsWith("mmseg.")), + analysis: items.filter((task) => task.startsWith("analysis.") || task.startsWith("system.") || task.startsWith("mock.")) + }; + }, [catalog]); + + function pickTask(next: string) { + setTaskType(next); + setParams(JSON.stringify(defaultParams[next] ?? {}, null, 2)); + } + + async function createJob() { + setBusy(true); + try { + await api("/api/jobs", { + method: "POST", + body: JSON.stringify({ type: taskType, params: JSON.parse(params) }) + }); + await refresh(); + } finally { + setBusy(false); + } + } + + async function syncWeights() { + setBusy(true); + try { + await api("/api/weights/sync", { + method: "POST", + body: JSON.stringify({ mode: "copy", hash_files: true, skip_existing: true }) + }); + await refresh(); + } finally { + setBusy(false); + } + } + + async function inspectJob(job: Job) { + const detail = await api(`/api/jobs/${job.id}`); + setSelectedJob(detail); + setLog(detail.log_tail ?? ""); + const source = new EventSource(`${API_BASE}/api/jobs/${job.id}/events`); + source.onmessage = (event) => { + const payload = JSON.parse(event.data); + if (payload.chunk) setLog((prev) => `${prev}${payload.chunk}`); + setSelectedJob(payload.job); + if (["success", "failed", "cancelled"].includes(payload.job.status)) source.close(); + }; + } + + async function cancelSelectedJob() { + if (!selectedJob) return; + await api(`/api/jobs/${selectedJob.id}/cancel`, { method: "POST" }); + await refresh(); + } + + return ( +
+ + +
+
+
+

Segmentation Operations

+

训练、预测、分析与权重资产控制台

+
+ +
+ + {error &&
{error}
} + +
+
+ + 运行中 + {runningCount} +
+
+ + 成功 + {successCount} +
+
+ + 失败 + {failedCount} +
+
+ + 数据集 + {catalog?.datasets.length ?? 0} +
+
+ +
+
+
+
+

Job Builder

+

创建任务

+
+ +
+
+ {Object.entries(taskGroups).map(([group, values]) => ( +
+ {group} + {values.map((task) => ( + + ))} +
+ ))} +
+