Files

213 lines
7.4 KiB
Python

"""Docling API routes for doc-ingest service."""
from __future__ import annotations
import os
from pathlib import Path
from typing import Any, Optional
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from app.docling_service import capabilities, convert_nas_batch, convert_nas_file, get_result, list_history
router = APIRouter(prefix="/docling", tags=["docling"])
NAS_ROOT = Path(os.getenv("NAS_ROOT", "/nas"))
class DoclingConvertBody(BaseModel):
path: str = Field(..., min_length=1, description="Relatief NAS-pad")
ocr: bool = True
tables: bool = True
page_images: bool = False
picture_images: bool = True
force_full_page_ocr: bool = False
formats: list[str] = Field(default_factory=lambda: ["markdown", "html", "json", "text"])
class DoclingBatchBody(BaseModel):
paths: list[str] = Field(default_factory=list)
limit: int = Field(default=10, ge=1, le=50)
ext: Optional[str] = None
ocr: bool = True
tables: bool = True
page_images: bool = False
picture_images: bool = True
force_full_page_ocr: bool = False
formats: list[str] = Field(default_factory=lambda: ["markdown", "json"])
def _options(body: DoclingConvertBody | DoclingBatchBody) -> dict[str, Any]:
return {
"ocr": body.ocr,
"tables": body.tables,
"page_images": body.page_images,
"picture_images": body.picture_images,
"force_full_page_ocr": body.force_full_page_ocr,
"formats": body.formats,
}
@router.get("/capabilities")
def docling_capabilities() -> dict[str, Any]:
return {"ok": True, **capabilities()}
@router.post("/convert")
def docling_convert(body: DoclingConvertBody) -> dict[str, Any]:
if not NAS_ROOT.is_dir():
raise HTTPException(503, f"NAS not mounted at {NAS_ROOT}")
caps = capabilities()
if not caps.get("installed"):
raise HTTPException(503, "Docling not installed on doc-ingest service")
out = convert_nas_file(NAS_ROOT, body.path.lstrip("/"), _options(body))
if not out.get("ok"):
raise HTTPException(502, out.get("error") or "Conversion failed")
return out
@router.post("/batch")
def docling_batch(body: DoclingBatchBody) -> dict[str, Any]:
if not NAS_ROOT.is_dir():
raise HTTPException(503, f"NAS not mounted at {NAS_ROOT}")
caps = capabilities()
if not caps.get("installed"):
raise HTTPException(503, "Docling not installed on doc-ingest service")
paths = list(body.paths or [])
if not paths:
from app.ingest import scan_share_files
files = scan_share_files(NAS_ROOT)
if body.ext:
want = body.ext if body.ext.startswith(".") else f".{body.ext}"
files = [f for f in files if f.suffix.lower() == want.lower()]
paths = [str(f.relative_to(NAS_ROOT)) for f in files[: body.limit]]
if not paths:
return {"ok": True, "converted": 0, "total": 0, "results": []}
return convert_nas_batch(NAS_ROOT, paths, _options(body))
@router.get("/history")
def docling_history(limit: int = Query(default=30, ge=1, le=100)) -> dict[str, Any]:
return {"ok": True, "items": list_history(limit)}
@router.get("/result")
def docling_result(
path: str = Query(..., min_length=1),
file_sig: Optional[str] = None,
) -> dict[str, Any]:
row = get_result(path.lstrip("/"), file_sig)
if not row:
raise HTTPException(404, "No Docling result for this file")
return {"ok": True, "result": row}
class NasWriteBody(BaseModel):
path: str = Field(..., min_length=1, description="Bronbestand (voor bestandsnaam)")
content: str = ""
subdir: str = Field(default="Telegram/Exports", max_length=256)
@router.get("/nas/diagnostics")
def docling_nas_diagnostics() -> dict[str, Any]:
if not NAS_ROOT.is_dir():
return {"ok": False, "nas_mounted": False, "error": f"NAS not at {NAS_ROOT}"}
from app.ingest import scan_share_files, SKIP_DIR_NAMES
all_files = scan_share_files(NAS_ROOT)
folder_counts: dict[str, int] = {}
for p in all_files:
top = p.relative_to(NAS_ROOT).parts[0] if p.relative_to(NAS_ROOT).parts else "."
folder_counts[top] = folder_counts.get(top, 0) + 1
empty_tops = []
for child in NAS_ROOT.iterdir():
if not child.is_dir() or child.name.startswith(("#", ".")) or child.name in SKIP_DIR_NAMES:
continue
if child.name not in folder_counts:
empty_tops.append(child.name)
return {
"ok": True,
"nas_mounted": True,
"total_files": len(all_files),
"by_top_folder": folder_counts,
"empty_folder_trees": empty_tops,
"hint": (
"Mappen zonder zichtbare bestanden = Synology ACL probleem. "
"Geef SMB-gebruiker aissa Read/Write op share + submappen in DSM."
) if empty_tops else "",
}
TEXT_SUFFIXES = {
".txt", ".md", ".csv", ".json", ".xml", ".html", ".htm", ".yaml", ".yml", ".log", ".rtf",
}
IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tiff", ".tif"}
PREVIEW_SUFFIXES = TEXT_SUFFIXES | IMAGE_SUFFIXES | {".pdf"}
@router.get("/nas/read")
def docling_nas_read(path: str = Query(..., min_length=1)) -> dict[str, Any]:
if not NAS_ROOT.is_dir():
raise HTTPException(503, f"NAS not mounted at {NAS_ROOT}")
rel = path.lstrip("/")
full = (NAS_ROOT / rel).resolve()
if not str(full).startswith(str(NAS_ROOT.resolve())) or not full.is_file():
raise HTTPException(404, "File not found")
ext = full.suffix.lower()
if ext not in TEXT_SUFFIXES and ext not in {".csv", ".rtf"}:
raise HTTPException(400, f"Not a plain-text file: {ext}")
try:
content = full.read_text(encoding="utf-8")
except UnicodeDecodeError:
content = full.read_text(encoding="latin-1", errors="replace")
return {
"ok": True,
"path": rel,
"filename": full.name,
"extension": ext,
"content": content,
"bytes": full.stat().st_size,
"editable": True,
}
@router.get("/nas/file")
def docling_nas_file(path: str = Query(..., min_length=1)):
from fastapi.responses import FileResponse
if not NAS_ROOT.is_dir():
raise HTTPException(503, f"NAS not mounted at {NAS_ROOT}")
rel = path.lstrip("/")
full = (NAS_ROOT / rel).resolve()
if not str(full).startswith(str(NAS_ROOT.resolve())) or not full.is_file():
raise HTTPException(404, "File not found")
media = "application/octet-stream"
ext = full.suffix.lower()
if ext == ".pdf":
media = "application/pdf"
elif ext in IMAGE_SUFFIXES:
media = f"image/{ext.lstrip('.')}"
if ext == ".jpg":
media = "image/jpeg"
elif ext == ".html":
media = "text/html"
return FileResponse(full, media_type=media, filename=full.name)
@router.post("/nas/write")
def docling_nas_write(body: NasWriteBody) -> dict[str, Any]:
if not NAS_ROOT.is_dir():
raise HTTPException(503, f"NAS not mounted at {NAS_ROOT}")
subdir = body.subdir.strip().strip("/")
out_dir = (NAS_ROOT / subdir).resolve()
if not str(out_dir).startswith(str(NAS_ROOT.resolve())):
raise HTTPException(400, "Invalid subdir")
out_dir.mkdir(parents=True, exist_ok=True)
stem = Path(body.path.lstrip("/")).stem
fname = f"{stem}-edited.md"
full = out_dir / fname
full.write_text(body.content or "", encoding="utf-8")
rel = str(full.relative_to(NAS_ROOT))
return {"ok": True, "path": rel, "filename": fname, "bytes": len(body.content or "")}