Initial Seg Data Server Net platform
This commit is contained in:
9
.env.example
Normal file
9
.env.example
Normal file
@@ -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
|
||||
5
.gitattributes
vendored
Normal file
5
.gitattributes
vendored
Normal file
@@ -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
|
||||
|
||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -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
|
||||
79
README.md
Normal file
79
README.md
Normal file
@@ -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/<original-relative-path>` 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.
|
||||
|
||||
2
backend/app/__init__.py
Normal file
2
backend/app/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Seg Data Server backend."""
|
||||
|
||||
140
backend/app/catalog.py
Normal file
140
backend/app/catalog.py
Normal file
@@ -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(),
|
||||
}
|
||||
|
||||
45
backend/app/commands.py
Normal file
45
backend/app/commands.py
Normal file
@@ -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)])
|
||||
|
||||
55
backend/app/config.py
Normal file
55
backend/app/config.py
Normal file
@@ -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()
|
||||
172
backend/app/db.py
Normal file
172
backend/app/db.py
Normal file
@@ -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")
|
||||
|
||||
116
backend/app/jobs.py
Normal file
116
backend/app/jobs.py
Normal file
@@ -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)
|
||||
|
||||
172
backend/app/main.py
Normal file
172
backend/app/main.py
Normal file
@@ -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()
|
||||
|
||||
24
backend/app/modules/__init__.py
Normal file
24
backend/app/modules/__init__.py
Normal file
@@ -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
|
||||
|
||||
2
backend/app/modules/analysis/__init__.py
Normal file
2
backend/app/modules/analysis/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Analysis task wrappers."""
|
||||
|
||||
18
backend/app/modules/analysis/tasks.py
Normal file
18
backend/app/modules/analysis/tasks.py
Normal file
@@ -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)
|
||||
|
||||
2
backend/app/modules/dataset/__init__.py
Normal file
2
backend/app/modules/dataset/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Dataset task wrappers."""
|
||||
|
||||
88
backend/app/modules/dataset/tasks.py
Normal file
88
backend/app/modules/dataset/tasks.py
Normal file
@@ -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
|
||||
|
||||
2
backend/app/modules/mmseg/__init__.py
Normal file
2
backend/app/modules/mmseg/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""MMSeg task wrappers."""
|
||||
|
||||
94
backend/app/modules/mmseg/tasks.py
Normal file
94
backend/app/modules/mmseg/tasks.py
Normal file
@@ -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
|
||||
|
||||
2
backend/app/modules/segmodel/__init__.py
Normal file
2
backend/app/modules/segmodel/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""SegModel task wrappers."""
|
||||
|
||||
41
backend/app/modules/segmodel/tasks.py
Normal file
41
backend/app/modules/segmodel/tasks.py
Normal file
@@ -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
|
||||
|
||||
2
backend/app/modules/system/__init__.py
Normal file
2
backend/app/modules/system/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""System task wrappers."""
|
||||
|
||||
106
backend/app/modules/system/service.py
Normal file
106
backend/app/modules/system/service.py
Normal file
@@ -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]
|
||||
|
||||
14
backend/app/modules/system/tasks.py
Normal file
14
backend/app/modules/system/tasks.py
Normal file
@@ -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
|
||||
|
||||
2
backend/app/modules/weights/__init__.py
Normal file
2
backend/app/modules/weights/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Weight sync and verification."""
|
||||
|
||||
153
backend/app/modules/weights/service.py
Normal file
153
backend/app/modules/weights/service.py
Normal file
@@ -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}
|
||||
2
backend/app/modules/yolo/__init__.py
Normal file
2
backend/app/modules/yolo/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""YOLO task wrappers."""
|
||||
|
||||
69
backend/app/modules/yolo/tasks.py
Normal file
69
backend/app/modules/yolo/tasks.py
Normal file
@@ -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
|
||||
|
||||
35
backend/app/paths.py
Normal file
35
backend/app/paths.py
Normal file
@@ -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())
|
||||
|
||||
45
backend/app/schemas.py
Normal file
45
backend/app/schemas.py
Normal file
@@ -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
|
||||
|
||||
6
backend/requirements.txt
Normal file
6
backend/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
fastapi>=0.110
|
||||
uvicorn[standard]>=0.27
|
||||
pydantic>=2
|
||||
python-multipart>=0.0.9
|
||||
pytest>=8
|
||||
|
||||
15
backend/tests/test_catalog.py
Normal file
15
backend/tests/test_catalog.py
Normal file
@@ -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
|
||||
|
||||
18
backend/tests/test_system_service.py
Normal file
18
backend/tests/test_system_service.py
Normal file
@@ -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,
|
||||
}
|
||||
]
|
||||
|
||||
14
backend/tests/test_weights_service.py
Normal file
14
backend/tests/test_weights_service.py
Normal file
@@ -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"
|
||||
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Seg Data Server Net</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
1721
frontend/package-lock.json
generated
Normal file
1721
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
frontend/package.json
Normal file
23
frontend/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
400
frontend/src/main.tsx
Normal file
400
frontend/src/main.tsx
Normal file
@@ -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<string, unknown>;
|
||||
};
|
||||
|
||||
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<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
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<string, Record<string, unknown>> = {
|
||||
"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<Catalog | null>(null);
|
||||
const [gpus, setGpus] = useState<GpuPayload | null>(null);
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
const [results, setResults] = useState<Array<Record<string, unknown>>>([]);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const [catalogNext, gpusNext, jobsNext, resultsNext] = await Promise.all([
|
||||
api<Catalog>("/api/catalog"),
|
||||
api<GpuPayload>("/api/system/gpus"),
|
||||
api<Job[]>("/api/jobs"),
|
||||
api<Array<Record<string, unknown>>>("/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 <span className={`pill pill-${status}`}>{status}</span>;
|
||||
}
|
||||
|
||||
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<Job | null>(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<Record<string, string[]>>(() => {
|
||||
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<Job>("/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<Job>(`/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 (
|
||||
<main className="shell">
|
||||
<aside className="rail">
|
||||
<div className="brand">
|
||||
<div className="mark"><Layers3 size={24} /></div>
|
||||
<div>
|
||||
<strong>Seg Data Server</strong>
|
||||
<span>Net Console</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="#jobs"><Terminal size={18} />任务</a>
|
||||
<a href="#gpu"><Cpu size={18} />GPU</a>
|
||||
<a href="#weights"><HardDrive size={18} />权重</a>
|
||||
<a href="#results"><BarChart3 size={18} />结果</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<section className="workspace">
|
||||
<header className="topbar">
|
||||
<div>
|
||||
<p className="eyebrow">Segmentation Operations</p>
|
||||
<h1>训练、预测、分析与权重资产控制台</h1>
|
||||
</div>
|
||||
<button className="iconButton" onClick={refresh} title="刷新">
|
||||
<RefreshCcw size={18} />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{error && <div className="alert">{error}</div>}
|
||||
|
||||
<section className="metrics">
|
||||
<div className="metric">
|
||||
<Activity size={20} />
|
||||
<span>运行中</span>
|
||||
<strong>{runningCount}</strong>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<ShieldCheck size={20} />
|
||||
<span>成功</span>
|
||||
<strong>{successCount}</strong>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<Zap size={20} />
|
||||
<span>失败</span>
|
||||
<strong>{failedCount}</strong>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<Database size={20} />
|
||||
<span>数据集</span>
|
||||
<strong>{catalog?.datasets.length ?? 0}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid two" id="jobs">
|
||||
<div className="panel taskPanel">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Job Builder</p>
|
||||
<h2>创建任务</h2>
|
||||
</div>
|
||||
<button className="primary" disabled={busy} onClick={createJob}>
|
||||
<Play size={17} />启动
|
||||
</button>
|
||||
</div>
|
||||
<div className="taskColumns">
|
||||
{Object.entries(taskGroups).map(([group, values]) => (
|
||||
<div key={group} className="taskGroup">
|
||||
<span>{group}</span>
|
||||
{values.map((task) => (
|
||||
<button key={task} className={task === taskType ? "selected" : ""} onClick={() => pickTask(task)}>
|
||||
{task}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<label className="field">
|
||||
<span>参数 JSON</span>
|
||||
<textarea value={params} onChange={(event) => setParams(event.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Queue</p>
|
||||
<h2>最近任务</h2>
|
||||
</div>
|
||||
<Gauge size={22} />
|
||||
</div>
|
||||
<div className="jobList">
|
||||
{jobs.slice(0, 12).map((job) => (
|
||||
<button key={job.id} className="jobRow" onClick={() => inspectJob(job)}>
|
||||
<span>{job.type}</span>
|
||||
<StatusPill status={job.status} />
|
||||
<small>{job.description || job.id.slice(0, 8)}</small>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid three">
|
||||
<div className="panel" id="gpu">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Hardware</p>
|
||||
<h2>GPU</h2>
|
||||
</div>
|
||||
<Cpu size={22} />
|
||||
</div>
|
||||
{(gpus?.gpus ?? []).map((gpu) => (
|
||||
<div className="gpu" key={gpu.index}>
|
||||
<div>
|
||||
<strong>GPU {gpu.index}</strong>
|
||||
<span>{gpu.name}</span>
|
||||
</div>
|
||||
<meter value={gpu.memory_used_mb} max={gpu.memory_total_mb} />
|
||||
<small>{gpu.memory_free_mb} MB free · {gpu.utilization_gpu_percent}% · {gpu.temperature_c}C</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="panel" id="weights">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Assets</p>
|
||||
<h2>权重</h2>
|
||||
</div>
|
||||
<button className="iconButton" disabled={busy} onClick={syncWeights} title="同步权重">
|
||||
<UploadCloud size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bigNumber">{catalog?.weights.count ?? 0}</div>
|
||||
<p className="muted">{formatBytes(catalog?.weights.total_bytes)} indexed</p>
|
||||
<p className="muted">{catalog?.weights.updated_at ?? "manifest not generated"}</p>
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Catalog</p>
|
||||
<h2>模型</h2>
|
||||
</div>
|
||||
<FileSearch size={22} />
|
||||
</div>
|
||||
<p className="muted">SegModel {catalog?.segmodel_architectures.length ?? 0} · YOLO {catalog?.yolo_models.length ?? 0} · MMSeg {catalog?.mmseg_algorithms.length ?? 0}</p>
|
||||
<div className="chips">
|
||||
{(catalog?.segmodel_architectures ?? []).slice(0, 8).map((item) => <span key={item}>{item}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid two">
|
||||
<div className="panel logPanel">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Live Log</p>
|
||||
<h2>{selectedJob?.type ?? "选择一个任务"}</h2>
|
||||
</div>
|
||||
<button className="iconButton" disabled={!selectedJob} onClick={cancelSelectedJob} title="取消任务">
|
||||
<Square size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<pre>{log || "No log selected."}</pre>
|
||||
</div>
|
||||
|
||||
<div className="panel" id="results">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Artifacts</p>
|
||||
<h2>最近结果</h2>
|
||||
</div>
|
||||
<BarChart3 size={22} />
|
||||
</div>
|
||||
<div className="resultList">
|
||||
{results.map((item) => (
|
||||
<a key={String(item.path)} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
|
||||
<span>{String(item.name)}</span>
|
||||
<small>{formatBytes(Number(item.size))}</small>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
392
frontend/src/styles.css
Normal file
392
frontend/src/styles.css
Normal file
@@ -0,0 +1,392 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--ink: #eef2e8;
|
||||
--muted: #9aa89a;
|
||||
--panel: #151815;
|
||||
--panel-2: #1d211d;
|
||||
--line: #333b32;
|
||||
--field: #0d100d;
|
||||
--green: #9de26f;
|
||||
--amber: #d3b35b;
|
||||
--red: #f07167;
|
||||
--cyan: #73d2de;
|
||||
--blue: #7aa2ff;
|
||||
--shadow: 0 18px 50px rgba(0, 0, 0, 0.32);
|
||||
font-family: "Aptos", "Segoe UI", "Noto Sans CJK SC", sans-serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 1100px;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(157, 226, 111, 0.07) 1px, transparent 1px),
|
||||
linear-gradient(180deg, rgba(115, 210, 222, 0.045) 1px, transparent 1px),
|
||||
#0b0d0b;
|
||||
background-size: 52px 52px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
button, textarea, select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.rail {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
padding: 24px 18px;
|
||||
border-right: 1px solid var(--line);
|
||||
background: rgba(12, 15, 12, 0.92);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 34px;
|
||||
}
|
||||
|
||||
.mark {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #0a0d0a;
|
||||
background: var(--green);
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.brand strong, .brand span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.brand span {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--muted);
|
||||
padding: 11px 10px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: var(--ink);
|
||||
background: var(--panel-2);
|
||||
}
|
||||
|
||||
.workspace {
|
||||
padding: 26px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 6px;
|
||||
color: var(--green);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
margin: 0;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 720;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.iconButton, .primary {
|
||||
height: 38px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.iconButton {
|
||||
width: 38px;
|
||||
background: var(--panel-2);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.primary {
|
||||
padding: 0 14px;
|
||||
background: var(--green);
|
||||
color: #0b0d0b;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.primary:disabled, .iconButton:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgba(240, 113, 103, 0.5);
|
||||
background: rgba(240, 113, 103, 0.12);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.metrics, .grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.metric, .panel {
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(21, 24, 21, 0.94);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: grid;
|
||||
grid-template-columns: 28px 1fr auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.metric span, .muted, small {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.metric strong {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.grid.two {
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(360px, 0.65fr);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.grid.three {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 18px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panelHead {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.taskColumns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.taskGroup {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.taskGroup > span {
|
||||
display: block;
|
||||
color: var(--amber);
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.taskGroup button {
|
||||
width: 100%;
|
||||
display: block;
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 5px;
|
||||
background: #101310;
|
||||
border: 1px solid transparent;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.taskGroup button.selected {
|
||||
border-color: var(--green);
|
||||
color: var(--ink);
|
||||
background: rgba(157, 226, 111, 0.08);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.field span {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 180px;
|
||||
resize: vertical;
|
||||
padding: 12px;
|
||||
color: var(--ink);
|
||||
background: var(--field);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
font-family: "Cascadia Code", "JetBrains Mono", monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.jobList, .resultList {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-height: 460px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.jobRow, .resultList a {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
background: #101310;
|
||||
border: 1px solid var(--line);
|
||||
text-decoration: none;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.jobRow small {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.pill {
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.pill-running { color: var(--cyan); }
|
||||
.pill-success { color: var(--green); }
|
||||
.pill-failed { color: var(--red); }
|
||||
.pill-cancelled { color: var(--amber); }
|
||||
|
||||
.gpu {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.gpu strong, .gpu span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.gpu span {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
meter {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
accent-color: var(--green);
|
||||
}
|
||||
|
||||
.bigNumber {
|
||||
font-size: 54px;
|
||||
font-weight: 760;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.chips span {
|
||||
padding: 6px 9px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
background: #101310;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.logPanel pre {
|
||||
min-height: 340px;
|
||||
max-height: 520px;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
color: #dbe7d8;
|
||||
background: #080a08;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
font-family: "Cascadia Code", "JetBrains Mono", monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.resultList a:hover, .jobRow:hover {
|
||||
border-color: var(--green);
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
body { min-width: 960px; }
|
||||
.shell { grid-template-columns: 220px 1fr; }
|
||||
.taskColumns { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.grid.three { grid-template-columns: 1fr; }
|
||||
.grid.two { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
2
frontend/src/vite-env.d.ts
vendored
Normal file
2
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
22
frontend/tsconfig.json
Normal file
22
frontend/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": []
|
||||
}
|
||||
|
||||
10
frontend/vite.config.ts
Normal file
10
frontend/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173
|
||||
}
|
||||
});
|
||||
|
||||
3
pytest.ini
Normal file
3
pytest.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
pythonpath = backend
|
||||
testpaths = backend/tests
|
||||
13
scripts/init_git_lfs.sh
Executable file
13
scripts/init_git_lfs.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v git-lfs >/dev/null 2>&1 && ! git lfs version >/dev/null 2>&1; then
|
||||
echo "git-lfs is not installed on this host." >&2
|
||||
echo "Install git-lfs or publish weights through Gitea releases/packages." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git lfs install
|
||||
git lfs track "*.pt" "*.pth" "*.onnx" "*.engine"
|
||||
git add .gitattributes
|
||||
|
||||
11
scripts/run_backend.sh
Executable file
11
scripts/run_backend.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
BACKEND_ENV="${SEG_BACKEND_CONDA_ENV:-seg_server}"
|
||||
HOST="${SEG_BACKEND_HOST:-0.0.0.0}"
|
||||
PORT="${SEG_BACKEND_PORT:-8000}"
|
||||
|
||||
cd "${ROOT_DIR}"
|
||||
exec conda run -n "${BACKEND_ENV}" uvicorn app.main:app --app-dir backend --host "${HOST}" --port "${PORT}" --reload
|
||||
|
||||
8
scripts/run_frontend.sh
Executable file
8
scripts/run_frontend.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${ROOT_DIR}/frontend"
|
||||
npm install
|
||||
exec npm run dev -- --host 0.0.0.0
|
||||
|
||||
33
scripts/sync_weights.py
Executable file
33
scripts/sync_weights.py
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BACKEND = ROOT / "backend"
|
||||
sys.path.insert(0, str(BACKEND))
|
||||
|
||||
from app.modules.weights.service import sync_weights # noqa: E402
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Sync Seg weight files into Seg_Data_Server_Net/weights.")
|
||||
parser.add_argument("--mode", choices=["copy", "reflink", "hardlink"], default="copy")
|
||||
parser.add_argument("--hash", action="store_true", help="calculate sha256 for every copied weight")
|
||||
parser.add_argument("--no-skip-existing", action="store_true", help="recopy files even when size matches")
|
||||
args = parser.parse_args()
|
||||
manifest = sync_weights(
|
||||
mode=args.mode,
|
||||
hash_files=args.hash,
|
||||
skip_existing=not args.no_skip_existing,
|
||||
)
|
||||
total_gb = manifest["total_bytes"] / 1024 / 1024 / 1024
|
||||
print(f"synced {manifest['count']} weights, total {total_gb:.2f} GB")
|
||||
print("manifest:", ROOT / "weights" / "manifest.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
1
weights/.gitkeep
Normal file
1
weights/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1792
weights/manifest.json
Normal file
1792
weights/manifest.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user