Add Command Center v2: DQ/RAG integration, S3 browser, Jupyter, GPU matrix.
Mirror mo/atc-GPU layout with config/, docs/, scripts/ for Gitea deploy.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
"""Upload PPT/PPTX decks and convert to presentation JSON + HTML."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
PRESENTATIONS_DIR = Path(os.getenv("PRESENTATIONS_DIR", "/data/presentations"))
|
||||
DOCLING_URL = os.getenv("DOCLING_URL", "http://docling-serve:5001").rstrip("/")
|
||||
|
||||
|
||||
def _ensure_dir() -> Path:
|
||||
PRESENTATIONS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
return PRESENTATIONS_DIR
|
||||
|
||||
|
||||
def list_decks() -> list[dict[str, Any]]:
|
||||
_ensure_dir()
|
||||
decks = []
|
||||
for meta_path in sorted(PRESENTATIONS_DIR.glob("*/meta.json"), key=lambda p: p.stat().st_mtime, reverse=True):
|
||||
try:
|
||||
meta = json.loads(meta_path.read_text())
|
||||
decks.append(meta)
|
||||
except Exception:
|
||||
continue
|
||||
return decks
|
||||
|
||||
|
||||
def _safe_name(name: str) -> str:
|
||||
return re.sub(r"[^a-zA-Z0-9._-]+", "_", name)[:80]
|
||||
|
||||
|
||||
def pptx_to_slides(path: Path) -> list[dict[str, Any]]:
|
||||
from pptx import Presentation
|
||||
from pptx.enum.shapes import MSO_SHAPE_TYPE
|
||||
|
||||
prs = Presentation(str(path))
|
||||
slides: list[dict[str, Any]] = []
|
||||
for idx, slide in enumerate(prs.slides, start=1):
|
||||
bullets: list[str] = []
|
||||
title = ""
|
||||
for shape in slide.shapes:
|
||||
if not hasattr(shape, "text"):
|
||||
continue
|
||||
text = (shape.text or "").strip()
|
||||
if not text:
|
||||
continue
|
||||
if shape.shape_type == MSO_SHAPE_TYPE.PLACEHOLDER and not title:
|
||||
title = text.split("\n")[0][:120]
|
||||
else:
|
||||
for line in text.split("\n"):
|
||||
line = line.strip()
|
||||
if line and line != title:
|
||||
bullets.append(line[:240])
|
||||
if not title:
|
||||
title = f"Slide {idx}"
|
||||
slides.append({
|
||||
"id": f"upload-{idx}",
|
||||
"title": title,
|
||||
"subtitle": "",
|
||||
"bullets": bullets[:12] or ["(empty slide)"],
|
||||
"kind": "upload",
|
||||
})
|
||||
return slides
|
||||
|
||||
|
||||
async def docling_enrich(path: Path) -> list[dict[str, Any]] | None:
|
||||
"""Optional: parse via Docling for richer structure."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||
with path.open("rb") as f:
|
||||
r = await client.post(
|
||||
f"{DOCLING_URL}/v1/convert/file",
|
||||
files={"files": (path.name, f, "application/octet-stream")},
|
||||
data={"to_formats": "md"},
|
||||
)
|
||||
if r.status_code >= 400:
|
||||
return None
|
||||
data = r.json()
|
||||
md = ""
|
||||
if isinstance(data, dict):
|
||||
doc = data.get("document") or data.get("result") or data
|
||||
if isinstance(doc, dict):
|
||||
md = doc.get("md_content") or doc.get("markdown") or ""
|
||||
elif isinstance(doc, str):
|
||||
md = doc
|
||||
if not md:
|
||||
return None
|
||||
slides = []
|
||||
chunks = [c.strip() for c in re.split(r"\n#{1,2}\s+", md) if c.strip()]
|
||||
for i, chunk in enumerate(chunks[:40], start=1):
|
||||
lines = [ln.strip() for ln in chunk.split("\n") if ln.strip()]
|
||||
title = lines[0][:120] if lines else f"Slide {i}"
|
||||
bullets = [ln.lstrip("-•* ").strip() for ln in lines[1:13] if ln.strip()]
|
||||
slides.append({
|
||||
"id": f"docling-{i}",
|
||||
"title": title,
|
||||
"subtitle": "Docling parsed",
|
||||
"bullets": bullets or ["—"],
|
||||
"kind": "upload",
|
||||
})
|
||||
return slides if slides else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def save_upload(filename: str, content: bytes) -> dict[str, Any]:
|
||||
_ensure_dir()
|
||||
deck_id = str(uuid.uuid4())[:8]
|
||||
deck_dir = PRESENTATIONS_DIR / deck_id
|
||||
deck_dir.mkdir(parents=True, exist_ok=True)
|
||||
safe = _safe_name(filename)
|
||||
dest = deck_dir / safe
|
||||
dest.write_bytes(content)
|
||||
|
||||
slides: list[dict[str, Any]] = []
|
||||
source = "pptx"
|
||||
if safe.lower().endswith((".pptx", ".ppt")):
|
||||
slides = pptx_to_slides(dest)
|
||||
docling_slides = await docling_enrich(dest)
|
||||
if docling_slides and len(docling_slides) >= len(slides):
|
||||
slides = docling_slides
|
||||
source = "docling+pptx"
|
||||
else:
|
||||
docling_slides = await docling_enrich(dest)
|
||||
if docling_slides:
|
||||
slides = docling_slides
|
||||
source = "docling"
|
||||
|
||||
if not slides:
|
||||
slides = [{
|
||||
"id": "upload-1",
|
||||
"title": safe,
|
||||
"subtitle": "Uploaded file",
|
||||
"bullets": [f"File stored at {dest.name}", "Could not auto-parse slides — open in editor or re-upload PPTX"],
|
||||
"kind": "upload",
|
||||
}]
|
||||
|
||||
payload = {
|
||||
"id": deck_id,
|
||||
"filename": safe,
|
||||
"source": source,
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"title": safe.rsplit(".", 1)[0],
|
||||
"subtitle": "Uploaded presentation",
|
||||
"slide_count": len(slides),
|
||||
"slides": slides,
|
||||
}
|
||||
(deck_dir / "meta.json").write_text(json.dumps(payload, indent=2, default=str))
|
||||
(deck_dir / "deck.json").write_text(json.dumps(payload, default=str))
|
||||
return payload
|
||||
|
||||
|
||||
def get_deck(deck_id: str) -> dict[str, Any] | None:
|
||||
path = PRESENTATIONS_DIR / deck_id / "meta.json"
|
||||
if not path.exists():
|
||||
return None
|
||||
return json.loads(path.read_text())
|
||||
Reference in New Issue
Block a user