feat: HSAP platform v2 — modular navigation, quality review, audit log, world model simulation

Major changes:
- New frontend (platform/web/): Vite + React 18 + TypeScript + Tailwind
- 4-module navigation: 数据送标 / 模型管理 / 车队管理 / 系统管理
- Data catalog with charts (DMS/ADAS/Lane 3-tab view)
- Quality review workflow (标注质检): Good/Fine/Bad scoring with auto-advance
- Audit enhancements: batch operations, rejection categories, Feishu notifications
- Operation audit log (操作日志)
- World model simulation studio (仿真工坊)
- Dataset version management with snapshots and diff
- ADAS 7-class dataset integration (138K images organized + compressed)
- User management with Feishu integration and pagination
- CRUD/search/filter on all pages, card layout redesign
- PIL-optimized image overlay rendering
- Auto-snapshot on build, in_review workflow stage
- Removed embedded algorithm code (now in workspace)
This commit is contained in:
2026-06-03 11:40:21 +08:00
parent 7c43b44c57
commit e72bc061c5
5487 changed files with 979207 additions and 6197 deletions

View File

@@ -0,0 +1,179 @@
"""世界模型仿真数据生成 — API 接口层。
实际生成逻辑由外部世界模型引擎完成(通过 subprocess / HTTP 调用)。
本模块提供:任务提交、队列管理、状态追踪、结果入库。
"""
from __future__ import annotations
import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from as_platform.config import WORKSPACE
from as_platform.db.engine import session_scope
SIM_JOBS_DIR = WORKSPACE / "manifests" / "simulation_jobs"
SIM_OUTPUT_ROOT = WORKSPACE / "datasets" / "dms" / "simulated"
SCENE_TEMPLATES = {
"urban_highway": {"label": "城市快速路", "desc": "多车道高速公路场景"},
"urban_street": {"label": "城市街道", "desc": "有交通灯和路口的城市道路"},
"rural_road": {"label": "乡村道路", "desc": "双车道乡村公路"},
"tunnel": {"label": "隧道", "desc": "隧道内光照变化场景"},
"night_city": {"label": "夜间城市", "desc": "低光照城市道路"},
"rain_highway": {"label": "雨天高速", "desc": "雨天湿滑路面"},
"fog_rural": {"label": "雾天乡村", "desc": "大雾低能见度"},
}
CAMERA_PRESETS = {
"truck_front": {"label": "卡车前视", "height": 2.5, "fov": 75, "pitch": -5},
"truck_side": {"label": "卡车侧视", "height": 2.5, "fov": 100, "pitch": 0},
"car_front": {"label": "轿车前视", "height": 1.2, "fov": 60, "pitch": -3},
"car_wide": {"label": "轿车广角", "height": 1.2, "fov": 120, "pitch": 0},
}
OBJECT_CLASSES = ["Pedestrain", "Car", "Truck", "Bus", "Motor-vehicles", "Tricycle", "cones"]
def list_jobs(offset: int = 0, limit: int = 20) -> dict[str, Any]:
SIM_JOBS_DIR.mkdir(parents=True, exist_ok=True)
jobs = []
for f in sorted(SIM_JOBS_DIR.glob("*.json"), reverse=True):
try:
data = json.loads(f.read_text())
data["_id"] = f.stem
if data.get("status") != "archived":
jobs.append(data)
except Exception:
pass
total = len(jobs)
return {"items": jobs[offset:offset + limit], "total": total}
def submit_job(params: dict[str, Any], user_name: str = "") -> dict[str, Any]:
job_id = f"sim-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:8]}"
now = datetime.now(timezone.utc).isoformat()
scene = params.get("scene", "urban_highway")
camera = params.get("camera", "truck_front")
weather = params.get("weather", "clear")
objects = params.get("objects", OBJECT_CLASSES[:4])
density = params.get("density", "medium")
count = min(params.get("count", 100), 5000)
note = params.get("note", "")
fov_variant = params.get("fov_variant", False) # 是否生成多FOV变体
scene_info = SCENE_TEMPLATES.get(scene, {"label": scene})
cam_info = CAMERA_PRESETS.get(camera, {"label": camera})
job = {
"id": job_id,
"status": "queued",
"created_at": now,
"submitted_by": user_name,
"params": {
"scene": scene,
"scene_label": scene_info["label"],
"camera": camera,
"camera_label": cam_info["label"],
"camera_height": cam_info.get("height", 2.5),
"camera_fov": cam_info.get("fov", 75),
"weather": weather,
"objects": objects,
"density": density,
"count": count,
"fov_variant": fov_variant,
"note": note,
},
"result": None,
"batch_registered": False,
}
SIM_JOBS_DIR.mkdir(parents=True, exist_ok=True)
(SIM_JOBS_DIR / f"{job_id}.json").write_text(json.dumps(job, ensure_ascii=False, indent=2))
# Queue actual generation (mock for now — replace with real world model call)
_trigger_generation(job_id)
return job
def get_job(job_id: str) -> dict[str, Any] | None:
f = SIM_JOBS_DIR / f"{job_id}.json"
if not f.is_file():
return None
data = json.loads(f.read_text())
data["_id"] = f.stem
return data
def get_job_images(job_id: str, offset: int = 0, limit: int = 60) -> dict[str, Any]:
out_dir = SIM_OUTPUT_ROOT / job_id / "images"
if not out_dir.is_dir():
return {"items": [], "total": 0}
imgs = sorted(out_dir.glob("*.jpg")) + sorted(out_dir.glob("*.png"))
total = len(imgs)
page = imgs[offset:offset + limit]
return {
"items": [{"name": p.name, "path": str(p.relative_to(SIM_OUTPUT_ROOT))} for p in page],
"total": total,
}
def ingest_job_to_batch(job_id: str, task: str = "adas", user_name: str = "") -> dict[str, Any]:
"""将仿真生成的数据注册为批次,直接入库。"""
job = get_job(job_id)
if not job:
return {"ok": False, "error": "Job not found"}
out_dir = SIM_OUTPUT_ROOT / job_id
if not out_dir.is_dir():
return {"ok": False, "error": "生成数据不存在"}
# Register as batch
from as_platform.data.core import register_batch
batch_name = f"sim_{job_id}"
try:
register_batch(None, "dms", task, batch_name, stage="returned", location="inbox")
# Update job status
job["status"] = "ingested"
job["batch_registered"] = True
job["batch_name"] = batch_name
(SIM_JOBS_DIR / f"{job_id}.json").write_text(json.dumps(job, ensure_ascii=False, indent=2))
return {"ok": True, "batch": batch_name, "task": task}
except Exception as e:
return {"ok": False, "error": str(e)}
def _trigger_generation(job_id: str) -> None:
"""触发实际生成(当前为 mock。替换为真实世界模型调用。"""
import threading
def _run():
try:
# TODO: 替换为真实世界模型调用
# 例如: subprocess.run(["python", "world_model/generate.py", "--job", job_id])
_update_status(job_id, "running")
# Mock: create output directory but no actual images
out_dir = SIM_OUTPUT_ROOT / job_id / "images"
out_dir.mkdir(parents=True, exist_ok=True)
# Mock 完成
_update_status(job_id, "completed")
except Exception as e:
_update_status(job_id, "failed", str(e))
threading.Thread(target=_run, daemon=True, name=f"sim-{job_id}").start()
def _update_status(job_id: str, status: str, error: str = "") -> None:
f = SIM_JOBS_DIR / f"{job_id}.json"
if f.is_file():
data = json.loads(f.read_text())
data["status"] = status
if error:
data["error"] = error
if status == "completed":
data["completed_at"] = datetime.now(timezone.utc).isoformat()
f.write_text(json.dumps(data, ensure_ascii=False, indent=2))