Deploybot
Deploy from stable branch
082393b
Raw
History Blame Contribute Delete
6.83 kB
"""V1 API routes — browser-memory storage mode.
Upload returns all figure images inline as base64; backend retains nothing
after the response is sent. No next_figure/prev_figure endpoints needed.
"""
import base64
import os
import uuid
from collections.abc import Callable
from io import BytesIO
from pathlib import Path
from typing import Any
from fastapi import File, UploadFile
from fastapi.responses import StreamingResponse
from api_helpers import _stream_with_progress
from storage import (
cleanup_session_files,
get_temp_dir,
load_parse_cache,
save_image,
save_parse_cache,
save_upload,
use_disk_images,
)
def _image_to_b64(img: "Path | Any | None") -> "str | None":
"""Encode a PIL Image or Path to a base64 PNG string."""
if img is None:
return None
if isinstance(img, Path):
return base64.b64encode(img.read_bytes()).decode("utf-8")
buf = BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
return base64.b64encode(buf.getvalue()).decode("utf-8")
def create_document_routes_v1(
app: Any,
session_states: dict[str, dict[str, Any]],
) -> None:
"""Register v1 document processing API routes."""
@app.post("/api/v1/upload")
async def api_v1_upload_file(file: UploadFile = File(...)) -> StreamingResponse:
"""V1 upload: returns all figures inline, then cleans up."""
from PIL import Image
from crops import extract_figures
from document_parser import parse_document
from pdf_io import load_pdf_pages
from ui_state import create_initial_state, hash_bytes, page_cache, parse_cache
file_bytes = await file.read()
session_id = str(uuid.uuid4())
session_states[session_id] = create_initial_state()
suffix = Path(file.filename).suffix if file.filename else ".tmp"
upload_path = save_upload(session_id, file_bytes, suffix)
if upload_path is not None:
temp_path = str(upload_path)
_cleanup_temp = False
else:
temp_path = str(get_temp_dir() / f"{uuid.uuid4().hex}{suffix}")
Path(temp_path).write_bytes(file_bytes)
_cleanup_temp = True
image_exts = {".jpg", ".jpeg", ".jfif", ".png", ".bmp", ".dib", ".gif", ".tif", ".tiff", ".webp"}
office_exts = {".docx", ".xlsx", ".pptx"}
max_pages = 20
def _work(on_progress: Callable[[str], None]) -> dict:
try:
ext = Path(temp_path).suffix.lower()
state = session_states[session_id]
state["current_figure_index"] = 0
state["conversation_history"] = []
state["current_image_path"] = None
with open(temp_path, "rb") as f:
raw = f.read()
file_hash = hash_bytes(raw)
state["uploaded_file_hash"] = file_hash
if ext in image_exts:
on_progress("Loading image...")
image = Image.open(temp_path).convert("RGB")
figures = [
{
"image": _image_to_b64(image),
"page": 0,
"caption": "",
"status": "Figure 1 of 1 (Page 1)",
}
]
return {
"status": "Image loaded successfully.\nNumber of figures: 1.",
"html_content": "Image uploaded directly (no document parsing needed)",
"figures": figures,
"session_id": session_id,
"v1": True,
}
fmt_label = ext.lstrip(".").upper()
status_lines = [f"{fmt_label} loaded successfully."]
if ext in office_exts:
page_images: list = []
else:
on_progress("Rendering PDF pages...")
cache_key = f"{file_hash}_{max_pages}"
if cache_key in page_cache:
page_images = page_cache[cache_key]
else:
page_images = load_pdf_pages(raw, max_pages=max_pages)
if not use_disk_images():
page_cache[cache_key] = page_images
status_lines.append(f"Number of pages rendered: {len(page_images)} (max {max_pages}).")
on_progress("Parsing document with Docling...")
if not use_disk_images() and file_hash in parse_cache:
parse_result = parse_cache[file_hash]
else:
parse_result = load_parse_cache(file_hash, session_id=session_id)
if parse_result is None:
parse_result = parse_document(raw, file_ext=ext, on_progress=on_progress)
save_parse_cache(file_hash, parse_result, session_id=session_id)
if not use_disk_images():
parse_cache[file_hash] = parse_result
status_lines.append("Document parsing done using Docling.")
on_progress("Extracting figures...")
figures_info = extract_figures(page_images, parse_result.get("figures", []))
status_lines.append(f"Number of figures extracted: {len(figures_info)}.")
on_progress("Finalizing...")
figures = []
for i, fig in enumerate(figures_info):
fig_b64 = _image_to_b64(fig["image"])
page_num = fig.get("page", 0)
caption = fig.get("caption", "")
fig_status = f"Figure {i + 1} of {len(figures_info)} (Page {page_num + 1})"
figures.append({
"image": fig_b64,
"page": page_num,
"caption": caption,
"status": fig_status,
})
return {
"status": "\n".join(status_lines),
"html_content": parse_result.get("html", "No content available"),
"figures": figures,
"session_id": session_id,
"v1": True,
}
finally:
if _cleanup_temp:
try:
os.remove(temp_path)
except OSError:
pass
cleanup_session_files(session_id)
session_states.pop(session_id, None)
return StreamingResponse(
_stream_with_progress(_work, session_id),
media_type="text/event-stream",
)