SysOps: voice-agy-webbuilder-backup — 2026-06-23 10:04 UTC
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
"""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 "")}
|
||||
Reference in New Issue
Block a user