Initial Seg Data Server Net platform

This commit is contained in:
2026-06-30 11:49:36 +08:00
commit 98abafa7cc
48 changed files with 6020 additions and 0 deletions

2
backend/app/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
"""Seg Data Server backend."""

140
backend/app/catalog.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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()

View 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

View File

@@ -0,0 +1,2 @@
"""Analysis task wrappers."""

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

View File

@@ -0,0 +1,2 @@
"""Dataset task wrappers."""

View 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

View File

@@ -0,0 +1,2 @@
"""MMSeg task wrappers."""

View 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

View File

@@ -0,0 +1,2 @@
"""SegModel task wrappers."""

View 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

View File

@@ -0,0 +1,2 @@
"""System task wrappers."""

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

View 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

View File

@@ -0,0 +1,2 @@
"""Weight sync and verification."""

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

View File

@@ -0,0 +1,2 @@
"""YOLO task wrappers."""

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