Files
step2urdf/app/main.py
T
sunxianghui 93773f3887 Initial commit: step2urdf tool with handtuned JSON import.
Includes FastAPI backend, vendored step2urdf frontend, and A7 handtuned arm JSON for URDF generation.
2026-08-26 15:32:47 +08:00

156 lines
4.6 KiB
Python

from __future__ import annotations
import shutil
import tempfile
from pathlib import Path
from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from app.config import settings
from app.models import ExportRequest, ProposeRequest
from app.services.mesh_export import geometry_backend_status
from app.services.pipeline import create_job_from_upload, export_package, load_parse, propose_draft
ROOT = Path(__file__).resolve().parents[1]
LEGACY_STATIC = ROOT / "app" / "static"
FRONTEND_DIST = ROOT / "frontend" / "dist"
app = FastAPI(title="step2urdf-tool", version="0.2.0")
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://127.0.0.1:5678",
"http://localhost:5678",
"http://127.0.0.1:8787",
"http://localhost:8787",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/health")
async def health():
return {
"ok": True,
"zhipu_configured": bool(settings.zhipu_api_key),
"zhipu_model": settings.zhipu_model,
"geometry": geometry_backend_status(),
"primary_ui": "step2urdf frontend (Vite :5678 in dev)",
}
@app.post("/api/parse")
async def parse_step(file: UploadFile = File(...)):
"""Optional server-side STEP text parse (legacy / debug). Geometry stays in the browser."""
suffix = Path(file.filename or "model.STEP").suffix or ".STEP"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
tmp_path = Path(tmp.name)
shutil.copyfileobj(file.file, tmp)
try:
result = create_job_from_upload(tmp_path, file.filename or "model.STEP")
finally:
tmp_path.unlink(missing_ok=True)
return result
@app.get("/api/jobs/{job_id}")
async def get_job(job_id: str):
try:
return load_parse(job_id)
except FileNotFoundError as e:
raise HTTPException(404, str(e)) from e
@app.post("/api/propose")
async def propose(req: ProposeRequest):
"""Zhipu-assisted link/joint proposal.
Preferred body (from step2urdf UI)::
{"part_names": [...], "profile": "a7", "robot_name": "A7", "extra_hint": "..."}
Legacy: ``job_id`` from ``/api/parse``.
"""
names = list(req.part_names or [])
if req.solid_names:
# Deduplicate while preserving order
seen: set[str] = set()
merged: list[str] = []
for n in names + list(req.solid_names):
if n and n not in seen:
seen.add(n)
merged.append(n)
names = merged
try:
draft = await propose_draft(
req.job_id,
profile=req.profile,
robot_name=req.robot_name,
extra_hint=req.extra_hint,
part_names=names or None,
solids_geom=list(req.solids or []),
)
except FileNotFoundError as e:
raise HTTPException(404, str(e)) from e
except ValueError as e:
raise HTTPException(400, str(e)) from e
except Exception as e: # noqa: BLE001
raise HTTPException(502, f"Zhipu/propose failed: {e}") from e
return draft
@app.post("/api/export")
async def export(req: ExportRequest):
"""Legacy server-side URDF write. Prefer Export URDF in the step2urdf UI."""
try:
meta = export_package(req.job_id, req.draft, include_meshes=req.include_meshes)
except FileNotFoundError as e:
raise HTTPException(404, str(e)) from e
return meta
@app.get("/api/jobs/{job_id}/download/{robot_name}")
async def download_urdf(job_id: str, robot_name: str):
path = settings.jobs_dir / job_id / "export" / robot_name / f"{robot_name}.urdf"
if not path.exists():
raise HTTPException(404, "URDF not found; export first")
return FileResponse(path, filename=f"{robot_name}.urdf", media_type="application/xml")
@app.get("/")
async def root():
if FRONTEND_DIST.is_dir() and (FRONTEND_DIST / "index.html").exists():
return FileResponse(FRONTEND_DIST / "index.html")
return RedirectResponse(url="/legacy/")
# Built step2urdf UI (after `pnpm build` in frontend/)
if FRONTEND_DIST.is_dir():
app.mount("/assets", StaticFiles(directory=str(FRONTEND_DIST / "assets")), name="frontend-assets")
# Previous MVP static UI kept under /legacy only
if LEGACY_STATIC.is_dir():
app.mount("/legacy", StaticFiles(directory=str(LEGACY_STATIC), html=True), name="legacy")
def main() -> None:
import uvicorn
uvicorn.run(
"app.main:app",
host=settings.host,
port=settings.port,
reload=True,
)
if __name__ == "__main__":
main()