Initial portal commit: landing + 9 AI-powered apps

Apps:
- dwg-rooms: extract room numbers from DWG/DXF
- dwg-counting: count symbols in PDF drawings (OpenCV template matching)
- contract-check: review PDF contracts against a checklist (Claude vision + Tesseract OCR fallback)
- email-drafter: bullet notes → polished Czech/English business emails
- invoice-extractor: PDF/image invoice → structured data → Excel
- translator: Czech-first translator across 19 languages with tone control
- vv-check: find inconsistent unit prices across VV sheets in one workbook
- vv-compare: diff original vs new VV files (changes / added / removed)
- feature-request: portal users submit ideas + sample files

Infrastructure:
- LiteLLM gateway with per-app virtual keys + budgets
- Langfuse observability
- Geist font, shared theme, cross-subdomain back link + theme sync via cookie/URL
- Caddy reverse proxy on *.klas.chat
This commit is contained in:
Ondřej Glaser
2026-05-13 15:25:04 +02:00
commit 48cef99257
139 changed files with 20171 additions and 0 deletions

143
dwg-rooms/main.py Normal file
View File

@@ -0,0 +1,143 @@
"""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")