ocr-ebola / app.py
Joseph Pollack
Track image and font assets with Git LFS so Hugging Face Spaces accepts the push.
8839278 unverified
Raw
History Blame Contribute Delete
6.94 kB
from __future__ import annotations
import mimetypes
import os
from collections.abc import Iterator
from typing import Any
from urllib.parse import urlsplit
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
from backend.spaces_shim import spaces # noqa: E402 (must precede torch)
import gradio as gr # noqa: E402
from fastapi import HTTPException # noqa: E402
from fastapi.middleware.cors import CORSMiddleware # noqa: E402
from fastapi.responses import FileResponse, JSONResponse, Response # noqa: E402
from gradio.data_classes import FileData # noqa: E402
from starlette.staticfiles import StaticFiles # noqa: E402
from backend.config import ( # noqa: E402
DIST_DIR,
GPU_DURATION_CEILING,
GPU_DURATION_FLOOR,
GPU_SECONDS_PER_PAGE,
MAX_PDF_PAGES,
MODEL_GLM,
MODEL_OVIS,
PAGES_PER_GPU_REQUEST,
TEST_MODE,
)
from backend.documents import document_info # noqa: E402
from backend.load_models import load_models, models_loaded # noqa: E402
from backend.orchestrator import _file_path, run_ocr_batch # noqa: E402
def server_config() -> tuple[int, str | None, str | None]:
port = int(os.getenv("PORT", os.getenv("GRADIO_SERVER_PORT", "7860")))
configured_root = (
os.getenv("OCR_ROOT_PATH", "").strip() or os.getenv("GRADIO_ROOT_PATH", "").strip()
)
public_url = None
root_path = None
if configured_root.startswith(("http://", "https://")):
public_url = configured_root
path = urlsplit(configured_root).path.rstrip("/")
root_path = path or None
elif configured_root:
root_path = configured_root.rstrip("/") or None
return port, root_path, public_url
SERVER_PORT, ROOT_PATH, PUBLIC_URL = server_config()
class CachedStaticFiles(StaticFiles):
"""Serve immutable production assets from the browser cache after first load."""
async def get_response(self, path: str, scope: dict[str, Any]) -> Any:
response = await super().get_response(path, scope)
if response.status_code == 200:
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
return response
EXAMPLE_ASSETS = (
{
path.name: (
path.read_bytes(),
mimetypes.guess_type(path.name)[0] or "application/octet-stream",
)
for path in (DIST_DIR / "examples").iterdir()
if path.is_file()
}
if (DIST_DIR / "examples").is_dir()
else {}
)
load_models()
app = gr.Server()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://127.0.0.1:4173", "http://localhost:4173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def _gpu_duration(
image_path: FileData | dict[str, Any],
page_index: int = 0,
page_count: int = PAGES_PER_GPU_REQUEST,
model_choice: str = "OvisOCR2",
prompt: str = "",
) -> int:
del model_choice, prompt
configured_duration = os.getenv("OCR_GPU_DURATION", "").strip()
if configured_duration:
return int(configured_duration)
requested_count = max(1, min(PAGES_PER_GPU_REQUEST, int(page_count)))
try:
path = _file_path(image_path)
_, total_pages = document_info(path)
remaining_pages = max(1, total_pages - int(page_index))
requested_count = min(requested_count, remaining_pages)
except Exception:
pass
return max(
GPU_DURATION_FLOOR,
min(GPU_DURATION_CEILING, requested_count * GPU_SECONDS_PER_PAGE),
)
@spaces.GPU(duration=_gpu_duration)
def _run_ocr_gpu(
image_path: FileData,
page_index: int = 0,
page_count: int = PAGES_PER_GPU_REQUEST,
model_choice: str = "OvisOCR2",
prompt: str = "",
) -> Iterator[dict[str, Any]]:
yield from run_ocr_batch(
image_path,
page_index=page_index,
page_count=page_count,
model_choice=model_choice,
prompt=prompt,
)
@app.api(name="run_ocr", concurrency_limit=1, time_limit=300)
@app.mcp.tool(name="run_ocr")
def run_ocr(
image_path: FileData,
page_index: int = 0,
page_count: int = PAGES_PER_GPU_REQUEST,
model_choice: str = "OvisOCR2",
prompt: str = "",
) -> Iterator[dict[str, Any]]:
"""Stream OCR Markdown for a bounded batch of document pages."""
yield from _run_ocr_gpu(
image_path,
page_index=page_index,
page_count=page_count,
model_choice=model_choice,
prompt=prompt,
)
@app.get("/healthz")
def healthz() -> JSONResponse:
loaded = models_loaded()
return JSONResponse(
{
"status": "ok",
"models": {
"OvisOCR2": MODEL_OVIS,
"GLM-OCR": MODEL_GLM,
},
"loaded": loaded,
"backend": "mock" if TEST_MODE else "transformers",
"max_pdf_pages": MAX_PDF_PAGES,
"pages_per_gpu_request": PAGES_PER_GPU_REQUEST,
"gpu_seconds_per_page": GPU_SECONDS_PER_PAGE,
"gpu_duration_floor": GPU_DURATION_FLOOR,
"gpu_duration_ceiling": GPU_DURATION_CEILING,
"root_path": ROOT_PATH,
"public_url": PUBLIC_URL,
}
)
@app.get("/examples/{filename}")
def example_asset(filename: str) -> Response:
asset = EXAMPLE_ASSETS.get(filename)
if asset is None:
raise HTTPException(status_code=404, detail="Example not found")
content, media_type = asset
return Response(
content=content,
media_type=media_type,
headers={"Cache-Control": "public, max-age=31536000, immutable"},
)
if DIST_DIR.is_dir():
for route, directory in (
("/assets", DIST_DIR / "assets"),
("/brand", DIST_DIR / "brand"),
("/vendor", DIST_DIR / "vendor"),
):
if directory.is_dir():
app.mount(
route,
CachedStaticFiles(directory=directory),
name=route.strip("/").replace("/", "-"),
)
@app.get("/")
def homepage() -> FileResponse:
index_path = DIST_DIR / "index.html"
if not index_path.is_file():
raise RuntimeError(
"Frontend build missing. Run `cd frontend && npm ci && npm run build` "
"before launching app.py."
)
return FileResponse(index_path, headers={"Cache-Control": "no-cache"})
@app.get("/favicon.ico")
def favicon() -> FileResponse:
favicon_path = DIST_DIR / "favicon.ico"
if not favicon_path.is_file():
raise HTTPException(status_code=404, detail="Favicon not found")
return FileResponse(
favicon_path,
headers={"Cache-Control": "public, max-age=31536000, immutable"},
)
if __name__ == "__main__":
app.launch(
server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),
server_port=SERVER_PORT,
root_path=ROOT_PATH,
show_error=True,
)