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:
90
vv-compare/main.py
Normal file
90
vv-compare/main.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""FastAPI: two Excel VVs (original + new) → comparison report Excel."""
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, File, HTTPException, UploadFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from vv_compare import compare, write_compare_report
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="VV Compare")
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"],
|
||||
allow_methods=["*"], allow_headers=["*"])
|
||||
|
||||
WORK_DIR = Path(os.getenv("WORK_DIR", "/tmp/vv-compare"))
|
||||
WORK_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
jobs: dict[str, dict] = {}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return FileResponse("static/index.html")
|
||||
|
||||
|
||||
@app.post("/api/compare")
|
||||
async def do_compare(
|
||||
original: UploadFile = File(...),
|
||||
new: UploadFile = File(...),
|
||||
):
|
||||
for f in (original, new):
|
||||
suffix = Path(f.filename or "").suffix.lower()
|
||||
if suffix not in (".xlsx", ".xlsm"):
|
||||
raise HTTPException(400, "Podporované formáty: .xlsx, .xlsm")
|
||||
|
||||
job_id = str(uuid.uuid4())
|
||||
job_dir = WORK_DIR / job_id
|
||||
job_dir.mkdir()
|
||||
orig_path = job_dir / f"original{Path(original.filename).suffix}"
|
||||
new_path = job_dir / f"new{Path(new.filename).suffix}"
|
||||
orig_path.write_bytes(await original.read())
|
||||
new_path.write_bytes(await new.read())
|
||||
logger.info("Job %s: original=%s, new=%s", job_id, original.filename, new.filename)
|
||||
|
||||
try:
|
||||
result = compare(orig_path, new_path)
|
||||
except Exception as exc:
|
||||
logger.exception("Compare failed")
|
||||
raise HTTPException(500, f"Porovnání selhalo: {exc}")
|
||||
|
||||
jobs[job_id] = {
|
||||
"original_filename": original.filename,
|
||||
"new_filename": new.filename,
|
||||
"job_dir": str(job_dir),
|
||||
"result": result,
|
||||
}
|
||||
return {"job_id": job_id, "result": result}
|
||||
|
||||
|
||||
@app.get("/api/report/{job_id}")
|
||||
async def report(job_id: str):
|
||||
if job_id not in jobs:
|
||||
raise HTTPException(404, "Úloha nenalezena")
|
||||
job = jobs[job_id]
|
||||
out_path = Path(job["job_dir"]) / "report.xlsx"
|
||||
write_compare_report(
|
||||
job["result"],
|
||||
job.get("original_filename") or "puvodni.xlsx",
|
||||
job.get("new_filename") or "novy.xlsx",
|
||||
out_path,
|
||||
)
|
||||
return FileResponse(
|
||||
str(out_path),
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
filename="Porovnani_VV_puvodni_vs_novy.xlsx",
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
Reference in New Issue
Block a user