Spaces:
Running on Zero
Running on Zero
| """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.""" | |
| 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", | |
| ) | |