93773f3887
Includes FastAPI backend, vendored step2urdf frontend, and A7 handtuned arm JSON for URDF generation.
156 lines
4.6 KiB
Python
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()
|