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

View File

@@ -0,0 +1,93 @@
"""Render annotated PDF with color-coded symbol match highlights."""
import logging
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
logger = logging.getLogger(__name__)
# Distinct visually-separable colors (RGB) for up to 12 symbol types
PALETTE = [
(228, 26, 28), # red
(55, 126, 184), # blue
(77, 175, 74), # green
(152, 78, 163), # purple
(255, 127, 0), # orange
(0, 184, 184), # teal
(247, 129, 191), # pink
(153, 153, 0), # olive
(166, 86, 40), # brown
(102, 102, 102), # grey
(0, 0, 0), # black
(190, 81, 51), # rust
]
def _load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
for path in (
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
):
try:
return ImageFont.truetype(path, size)
except OSError:
continue
return ImageFont.load_default()
def render_annotated_pdf(
drawing_path: Path,
results: list[dict],
out_path: Path,
filename: str = "drawing",
) -> Path:
"""Build a multi-page PDF: page 1 = legend, page 2 = annotated drawing.
results: [{id, description, count, matches: [{x,y,w,h,score,rot}, ...]}, ...]
"""
base = Image.open(drawing_path).convert("RGB")
annotated = base.copy()
draw = ImageDraw.Draw(annotated)
# Scale stroke width by image size so boxes are visible at any zoom
stroke = max(2, min(annotated.size) // 800)
label_font = _load_font(max(14, min(annotated.size) // 200))
for idx, r in enumerate(results):
color = PALETTE[idx % len(PALETTE)]
for m in r.get("matches", []):
x, y, w, h = m["x"], m["y"], m["w"], m["h"]
draw.rectangle([x, y, x + w, y + h], outline=color, width=stroke)
# Legend page
legend_w = annotated.size[0]
row_h = max(36, legend_w // 30)
legend_h = row_h * (len(results) + 4)
legend = Image.new("RGB", (legend_w, legend_h), "white")
ldraw = ImageDraw.Draw(legend)
title_font = _load_font(max(28, legend_w // 50))
ldraw.text((40, 30), f"Počítání symbolů — {filename}", font=title_font, fill="black")
ldraw.text(
(40, 30 + row_h),
f"Nalezeno {sum(r['count'] for r in results)} symbolů v {len(results)} kategoriích.",
font=label_font, fill="black",
)
y = 30 + 3 * row_h
swatch = row_h - 10
for idx, r in enumerate(results):
color = PALETTE[idx % len(PALETTE)]
ldraw.rectangle([40, y, 40 + swatch, y + swatch], fill=color, outline="black", width=2)
text = f"{r.get('description', r['id'])}{r['count']}×"
ldraw.text((40 + swatch + 16, y), text, font=label_font, fill="black")
y += row_h
# Save as multi-page PDF
legend.save(
out_path,
"PDF",
resolution=150.0,
save_all=True,
append_images=[annotated],
)
return out_path