Implement Datalab-compatible API with async endpoints and deepseek OCR backend
- New endpoints: /api/v1/convert, /api/v1/extract, /api/v1/segment, /api/v1/ocr, /api/v1/table_rec, /api/v1/marker (deprecated), /api/v1/create-document, /api/v1/files/*, /api/v1/thumbnails - Async submit-and-poll pattern matching Datalab API spec - X-API-Key authentication via API_KEY env var - Filesystem-based request store for multi-worker support - OCR_BACKEND=deepseek mode: uses deepseek-ocr via ollama for OCR (prompt: <|grounding|>Free OCR.) - build_options() now propagates output_format to parsed_opts - Fixed docs page CSS curly brace conflict with .format() - Renamed marker_endpoint_referece.md -> marker_endpoint_reference.md - Containerfile: added poppler-utils, pandoc; 3 gunicorn workers - gunicorn.conf: post_fork hook for background worker threads
This commit is contained in:
+72
-10
@@ -1,29 +1,91 @@
|
||||
# gunicorn.conf.py -- production WSGI config for marker-api
|
||||
import os
|
||||
import multiprocessing
|
||||
import threading
|
||||
|
||||
port = int(os.environ.get("PORT", "8000"))
|
||||
workers = min(multiprocessing.cpu_count() * 2 + 1, 8)
|
||||
port = int(os.environ.get("PORT", "8001"))
|
||||
workers = int(os.environ.get("GUNICORN_WORKERS", "3"))
|
||||
threads = int(os.environ.get("GUNICORN_THREADS", "4"))
|
||||
timeout = int(os.environ.get("GUNICORN_TIMEOUT", "600"))
|
||||
|
||||
bind = f"0.0.0.0:{port}"
|
||||
workers = int(os.environ.get("GUNICORN_WORKERS", "1"))
|
||||
workers = min(workers, 8)
|
||||
|
||||
worker_class = "gthread"
|
||||
threads = int(os.environ.get("GUNICORN_THREADS", "4"))
|
||||
timeout = int(os.environ.get("GUNICORN_TIMEOUT", "300"))
|
||||
graceful_timeout = 60
|
||||
|
||||
accesslog = "-"
|
||||
errorlog = "-"
|
||||
loglevel = os.environ.get("GUNICORN_LOGLEVEL", "info")
|
||||
|
||||
# Preload so models are loaded once in master process (not duplicated per-worker)
|
||||
preload_app = True
|
||||
# Do NOT preload — each worker starts its own background thread and loads models
|
||||
preload_app = False
|
||||
|
||||
# Enable worker recycling to prevent memory leaks
|
||||
max_requests = 1000
|
||||
max_requests_jitter = 100
|
||||
|
||||
# Tempdir for marker uploads
|
||||
tempdir = "/app/uploads"
|
||||
|
||||
|
||||
def post_fork(server, worker):
|
||||
"""Start background worker thread after fork (filesystem-based store works across workers)."""
|
||||
t = threading.Thread(target=_bg_worker, daemon=True)
|
||||
t.start()
|
||||
|
||||
|
||||
def _bg_worker():
|
||||
import app as marker_api
|
||||
from request_store import _job_queue, _job_cond, get_request, complete_request, fail_request, get_result
|
||||
|
||||
while True:
|
||||
import time
|
||||
import traceback
|
||||
with _job_cond:
|
||||
while not _job_queue:
|
||||
_job_cond.wait()
|
||||
job = _job_queue.pop(0)
|
||||
request_id, endpoint, raw_bytes, filename, opts = job
|
||||
try:
|
||||
if endpoint == "convert":
|
||||
result = marker_api.convert_with_ocr_backend(raw_bytes, filename, **opts)
|
||||
elif endpoint == "ocr":
|
||||
from deepseek_ocr import ocr_pdf
|
||||
result = ocr_pdf(raw_bytes, max_pages=opts.get("max_pages"), page_range=opts.get("page_range"))
|
||||
elif endpoint == "extract":
|
||||
conv_result = marker_api.convert_with_ocr_backend(raw_bytes, filename, **opts)
|
||||
if conv_result["success"]:
|
||||
import json
|
||||
schema_json = opts.get("page_schema") or "{}"
|
||||
try:
|
||||
schema = json.loads(schema_json) if isinstance(schema_json, str) else schema_json
|
||||
except json.JSONDecodeError:
|
||||
schema = {}
|
||||
extraction = marker_api._apply_extraction_schema(conv_result["output"], schema)
|
||||
result = {**conv_result, "extraction": extraction}
|
||||
else:
|
||||
result = conv_result
|
||||
elif endpoint == "segment":
|
||||
conv_result = marker_api.convert_with_ocr_backend(raw_bytes, filename, **opts)
|
||||
if conv_result["success"]:
|
||||
import json
|
||||
schema_json = opts.get("segmentation_schema") or "{}"
|
||||
try:
|
||||
schema = json.loads(schema_json) if isinstance(schema_json, str) else schema_json
|
||||
except json.JSONDecodeError:
|
||||
schema = {}
|
||||
segments = marker_api._apply_segmentation_schema(conv_result["output"], schema)
|
||||
result = {**conv_result, "segments": segments}
|
||||
else:
|
||||
result = conv_result
|
||||
elif endpoint == "table_rec":
|
||||
conv_result = marker_api.convert_with_ocr_backend(raw_bytes, filename, **opts, output_format="json")
|
||||
tables = marker_api._extract_tables(conv_result)
|
||||
result = {**conv_result, "tables": tables}
|
||||
else:
|
||||
result = {"success": False, "error": f"Unknown endpoint: {endpoint}"}
|
||||
if result["success"]:
|
||||
complete_request(request_id, result)
|
||||
else:
|
||||
fail_request(request_id, result.get("error", "Unknown error"))
|
||||
except Exception as exc:
|
||||
traceback.print_exc()
|
||||
fail_request(request_id, str(exc))
|
||||
|
||||
Reference in New Issue
Block a user