"""FastAPI app: upload DWG/DXF → extract rooms → export Excel.""" import json import logging import os import subprocess import uuid from pathlib import Path from fastapi import FastAPI, File, Form, HTTPException, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from excel_export import export_to_excel from extractor import DEFAULT_EXAMPLES, extract_rooms from llm_helper import enhance_with_llm logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI(title="DWG Room Extractor") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) WORK_DIR = Path(os.getenv("WORK_DIR", "/tmp/dwg-rooms")) WORK_DIR.mkdir(parents=True, exist_ok=True) jobs: dict[str, dict] = {} def _dwg_to_dxf(dwg_path: Path, out_dir: Path) -> Path: """Convert DWG → DXF using LibreDWG dwgread. Returns DXF path.""" dxf_path = out_dir / f"{dwg_path.stem}.dxf" r = subprocess.run( ["dwgread", "-O", "DXF", "-o", str(dxf_path), str(dwg_path)], capture_output=True, text=True, timeout=120, ) if not dxf_path.exists(): raise RuntimeError( f"DWG conversion failed (exit {r.returncode}): {r.stderr or r.stdout or 'no output'}" ) return dxf_path @app.get("/") async def root(): return FileResponse("static/index.html") @app.get("/api/defaults") async def defaults(): return {"examples": DEFAULT_EXAMPLES} @app.post("/api/upload") async def upload( file: UploadFile = File(...), examples: str = Form(default=""), # JSON array of strings ): suffix = Path(file.filename or "input.dxf").suffix.lower() if suffix not in (".dwg", ".dxf"): raise HTTPException(400, "Supported formats: .dwg, .dxf") try: ex_list = json.loads(examples) if examples else None if ex_list is not None and not isinstance(ex_list, list): ex_list = None except json.JSONDecodeError: ex_list = None job_id = str(uuid.uuid4()) job_dir = WORK_DIR / job_id job_dir.mkdir() input_path = job_dir / f"input{suffix}" content = await file.read() input_path.write_bytes(content) logger.info("Job %s: %s (%d bytes), examples=%r", job_id, file.filename, len(content), ex_list) steps: list[str] = [] try: if suffix == ".dwg": steps.append("Konverze DWG → DXF") dxf_path = _dwg_to_dxf(input_path, job_dir) else: dxf_path = input_path steps.append("Extrakce místností (pravidla)") rooms, unmatched = extract_rooms(str(dxf_path), examples=ex_list) logger.info("Job %s: %d rooms, %d unmatched texts", job_id, len(rooms), len(unmatched)) if unmatched and os.getenv("LITELLM_API_KEY", ""): steps.append("AI rozšíření") llm_rooms = await enhance_with_llm(unmatched) logger.info("Job %s: LLM added %d rooms", job_id, len(llm_rooms)) rooms.extend(llm_rooms) jobs[job_id] = {"filename": file.filename, "rooms": rooms} return {"job_id": job_id, "room_count": len(rooms), "steps": steps} except Exception as exc: logger.error("Job %s failed: %s", job_id, exc) raise HTTPException(500, str(exc)) @app.get("/api/rooms/{job_id}") async def get_rooms(job_id: str): if job_id not in jobs: raise HTTPException(404, "Not found") return jobs[job_id]["rooms"] @app.put("/api/rooms/{job_id}") async def update_rooms(job_id: str, rooms: list[dict]): if job_id not in jobs: raise HTTPException(404, "Not found") jobs[job_id]["rooms"] = rooms return {"ok": True, "count": len(rooms)} @app.get("/api/export/{job_id}") async def export(job_id: str): if job_id not in jobs: raise HTTPException(404, "Not found") job = jobs[job_id] excel_path = WORK_DIR / job_id / "rooms.xlsx" export_to_excel(job["rooms"], str(excel_path)) stem = Path(job["filename"]).stem return FileResponse( str(excel_path), media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", filename=f"mistnosti_{stem}.xlsx", ) @app.get("/health") async def health(): return {"status": "ok"} app.mount("/static", StaticFiles(directory="static"), name="static")