mirror of
https://github.com/docling-project/docling-serve.git
synced 2025-11-29 08:33:50 +00:00
Compare commits
10 Commits
vku/oc-any
...
v0.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40bb21d347 | ||
|
|
ee89ee4dae | ||
|
|
6b3d281f02 | ||
|
|
b598872e5c | ||
|
|
087417e5c2 | ||
|
|
57f9073bc0 | ||
|
|
525a43ff6f | ||
|
|
c1ce4719c9 | ||
|
|
5dfb75d3b9 | ||
|
|
420162e674 |
@@ -3,7 +3,7 @@ config:
|
||||
no-emphasis-as-header: false
|
||||
first-line-heading: false
|
||||
MD033:
|
||||
allowed_elements: ["details", "summary", "br", "a", "p", "img"]
|
||||
allowed_elements: ["details", "summary", "br", "a", "b", "p", "img"]
|
||||
MD024:
|
||||
siblings_only: true
|
||||
globs:
|
||||
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,3 +1,23 @@
|
||||
## [v0.8.0](https://github.com/docling-project/docling-serve/releases/tag/v0.8.0) - 2025-04-22
|
||||
|
||||
### Feature
|
||||
|
||||
* Add option for vlm pipeline ([#143](https://github.com/docling-project/docling-serve/issues/143)) ([`ee89ee4`](https://github.com/docling-project/docling-serve/commit/ee89ee4daee5e916bd6a3bdb452f78934cd03f60))
|
||||
* Expose more conversion options ([#142](https://github.com/docling-project/docling-serve/issues/142)) ([`6b3d281`](https://github.com/docling-project/docling-serve/commit/6b3d281f02905c195ab75f25bb39f5c4d4e7b680))
|
||||
* **UI:** Change UI to use async endpoints ([#131](https://github.com/docling-project/docling-serve/issues/131)) ([`b598872`](https://github.com/docling-project/docling-serve/commit/b598872e5c48928ac44417a11bb7acc0e5c3f0c6))
|
||||
|
||||
### Fix
|
||||
|
||||
* **UI:** Use https when calling the api ([#139](https://github.com/docling-project/docling-serve/issues/139)) ([`57f9073`](https://github.com/docling-project/docling-serve/commit/57f9073bc0daf72428b068ea28e2bec7cd76c37b))
|
||||
* Fix permissions in docker image ([#136](https://github.com/docling-project/docling-serve/issues/136)) ([`c1ce471`](https://github.com/docling-project/docling-serve/commit/c1ce4719c933179ba3c59d73d0584853bbd6fa6a))
|
||||
* Picture caption visuals ([#129](https://github.com/docling-project/docling-serve/issues/129)) ([`5dfb75d`](https://github.com/docling-project/docling-serve/commit/5dfb75d3b9a7022d1daad12edbb8ec7bbf9aa264))
|
||||
|
||||
### Documentation
|
||||
|
||||
* Fix required permissions for oauth2-proxy requests ([#141](https://github.com/docling-project/docling-serve/issues/141)) ([`087417e`](https://github.com/docling-project/docling-serve/commit/087417e5c2387d4ed95500222058f34d8a8702aa))
|
||||
* Update deployment examples ([#135](https://github.com/docling-project/docling-serve/issues/135)) ([`525a43f`](https://github.com/docling-project/docling-serve/commit/525a43ff6f04b7cc80f9dd6a0e653a8d8c4ab317))
|
||||
* Fix image tag ([#124](https://github.com/docling-project/docling-serve/issues/124)) ([`420162e`](https://github.com/docling-project/docling-serve/commit/420162e674cc38b4c3c13673ffbee4c20a1b15f1))
|
||||
|
||||
## [v0.7.0](https://github.com/docling-project/docling-serve/releases/tag/v0.7.0) - 2025-03-31
|
||||
|
||||
### Feature
|
||||
|
||||
@@ -17,6 +17,8 @@ RUN --mount=type=bind,source=os-packages.txt,target=/tmp/os-packages.txt \
|
||||
dnf -y clean all && \
|
||||
rm -rf /var/cache/dnf
|
||||
|
||||
RUN /usr/bin/fix-permissions /opt/app-root/src/.cache
|
||||
|
||||
ENV TESSDATA_PREFIX=/usr/share/tesseract/tessdata/
|
||||
|
||||
###################################################################################################
|
||||
@@ -44,7 +46,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.6.1,source=/uv,target=/bin/uv \
|
||||
--mount=type=cache,target=/opt/app-root/src/.cache/uv,uid=1001 \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --no-install-project --no-dev --all-extras ${UV_SYNC_EXTRA_ARGS}
|
||||
umask 002 && uv sync --frozen --no-install-project --no-dev --all-extras ${UV_SYNC_EXTRA_ARGS}
|
||||
|
||||
ARG MODELS_LIST="layout tableformer picture_classifier easyocr"
|
||||
|
||||
@@ -52,15 +54,15 @@ RUN echo "Downloading models..." && \
|
||||
HF_HUB_DOWNLOAD_TIMEOUT="90" \
|
||||
HF_HUB_ETAG_TIMEOUT="90" \
|
||||
docling-tools models download -o "${DOCLING_SERVE_ARTIFACTS_PATH}" ${MODELS_LIST} && \
|
||||
chown -R 1001:0 /opt/app-root/src/.cache && \
|
||||
chmod -R g=u /opt/app-root/src/.cache
|
||||
chown -R 1001:0 ${DOCLING_SERVE_ARTIFACTS_PATH} && \
|
||||
chmod -R g=u ${DOCLING_SERVE_ARTIFACTS_PATH}
|
||||
|
||||
COPY --chown=1001:0 ./docling_serve ./docling_serve
|
||||
RUN --mount=from=ghcr.io/astral-sh/uv:0.6.1,source=/uv,target=/bin/uv \
|
||||
--mount=type=cache,target=/opt/app-root/src/.cache/uv,uid=1001 \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --no-dev --all-extras ${UV_SYNC_EXTRA_ARGS}
|
||||
umask 002 && uv sync --frozen --no-dev --all-extras ${UV_SYNC_EXTRA_ARGS}
|
||||
|
||||
EXPOSE 5001
|
||||
|
||||
|
||||
13
Makefile
13
Makefile
@@ -17,6 +17,7 @@ else
|
||||
endif
|
||||
|
||||
TAG=$(shell git rev-parse HEAD)
|
||||
BRANCH_TAG=$(shell git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
action-lint-file:
|
||||
$(CMD_PREFIX) touch .action-lint
|
||||
@@ -28,22 +29,22 @@ md-lint-file:
|
||||
docling-serve-image: Containerfile
|
||||
$(ECHO_PREFIX) printf " %-12s Containerfile\n" "[docling-serve]"
|
||||
$(CMD_PREFIX) docker build --load --build-arg "UV_SYNC_EXTRA_ARGS=--no-extra cu124 --no-extra cpu" -f Containerfile -t ghcr.io/docling-project/docling-serve:$(TAG) .
|
||||
$(CMD_PREFIX) docker tag ghcr.io/docling-project/docling-serve:$(TAG) ghcr.io/docling-project/docling-serve:main
|
||||
$(CMD_PREFIX) docker tag ghcr.io/docling-project/docling-serve:$(TAG) quay.io/docling-project/docling-serve:main
|
||||
$(CMD_PREFIX) docker tag ghcr.io/docling-project/docling-serve:$(TAG) ghcr.io/docling-project/docling-serve:$(BRANCH_TAG)
|
||||
$(CMD_PREFIX) docker tag ghcr.io/docling-project/docling-serve:$(TAG) quay.io/docling-project/docling-serve:$(BRANCH_TAG)
|
||||
|
||||
.PHONY: docling-serve-cpu-image
|
||||
docling-serve-cpu-image: Containerfile ## Build docling-serve "cpu only" container image
|
||||
$(ECHO_PREFIX) printf " %-12s Containerfile\n" "[docling-serve CPU]"
|
||||
$(CMD_PREFIX) docker build --load --build-arg "UV_SYNC_EXTRA_ARGS=--no-extra cu124" -f Containerfile -t ghcr.io/docling-project/docling-serve-cpu:$(TAG) .
|
||||
$(CMD_PREFIX) docker tag ghcr.io/docling-project/docling-serve-cpu:$(TAG) ghcr.io/docling-project/docling-serve-cpu:main
|
||||
$(CMD_PREFIX) docker tag ghcr.io/docling-project/docling-serve-cpu:$(TAG) quay.io/docling-project/docling-serve-cpu:main
|
||||
$(CMD_PREFIX) docker tag ghcr.io/docling-project/docling-serve-cpu:$(TAG) ghcr.io/docling-project/docling-serve-cpu:$(BRANCH_TAG)
|
||||
$(CMD_PREFIX) docker tag ghcr.io/docling-project/docling-serve-cpu:$(TAG) quay.io/docling-project/docling-serve-cpu:$(BRANCH_TAG)
|
||||
|
||||
.PHONY: docling-serve-cu124-image
|
||||
docling-serve-cu124-image: Containerfile ## Build docling-serve container image with GPU support
|
||||
$(ECHO_PREFIX) printf " %-12s Containerfile\n" "[docling-serve with Cuda 12.4]"
|
||||
$(CMD_PREFIX) docker build --load --build-arg "UV_SYNC_EXTRA_ARGS=--no-extra cpu" -f Containerfile --platform linux/amd64 -t ghcr.io/docling-project/docling-serve-cu124:$(TAG) .
|
||||
$(CMD_PREFIX) docker tag ghcr.io/docling-project/docling-serve-cu124:$(TAG) ghcr.io/docling-project/docling-serve-cu124:main
|
||||
$(CMD_PREFIX) docker tag ghcr.io/docling-project/docling-serve-cu124:$(TAG) quay.io/docling-project/docling-serve-cu124:main
|
||||
$(CMD_PREFIX) docker tag ghcr.io/docling-project/docling-serve-cu124:$(TAG) ghcr.io/docling-project/docling-serve-cu124:$(BRANCH_TAG)
|
||||
$(CMD_PREFIX) docker tag ghcr.io/docling-project/docling-serve-cu124:$(TAG) quay.io/docling-project/docling-serve-cu124:$(BRANCH_TAG)
|
||||
|
||||
.PHONY: action-lint
|
||||
action-lint: .action-lint ## Lint GitHub Action workflows
|
||||
|
||||
@@ -7,7 +7,13 @@ from docling.datamodel.base_models import InputFormat, OutputFormat
|
||||
from docling.datamodel.pipeline_options import (
|
||||
EasyOcrOptions,
|
||||
PdfBackend,
|
||||
PdfPipeline,
|
||||
TableFormerMode,
|
||||
TableStructureOptions,
|
||||
)
|
||||
from docling.datamodel.settings import (
|
||||
DEFAULT_PAGE_RANGE,
|
||||
PageRange,
|
||||
)
|
||||
from docling.models.factories import get_ocr_factory
|
||||
from docling_core.types.doc import ImageRefMode
|
||||
@@ -121,16 +127,37 @@ class ConvertDocumentsOptions(BaseModel):
|
||||
table_mode: Annotated[
|
||||
TableFormerMode,
|
||||
Field(
|
||||
TableFormerMode.FAST,
|
||||
description=(
|
||||
"Mode to use for table structure, String. "
|
||||
f"Allowed values: {', '.join([v.value for v in TableFormerMode])}. "
|
||||
"Optional, defaults to fast."
|
||||
),
|
||||
examples=[TableFormerMode.FAST],
|
||||
examples=[TableStructureOptions().mode],
|
||||
# pattern="fast|accurate",
|
||||
),
|
||||
] = TableFormerMode.FAST
|
||||
] = TableStructureOptions().mode
|
||||
|
||||
pipeline: Annotated[
|
||||
PdfPipeline,
|
||||
Field(description="Choose the pipeline to process PDF or image files."),
|
||||
] = PdfPipeline.STANDARD
|
||||
|
||||
page_range: Annotated[
|
||||
PageRange,
|
||||
Field(
|
||||
description="Only convert a range of pages. The page number starts at 1.",
|
||||
examples=[(1, 4)],
|
||||
),
|
||||
] = DEFAULT_PAGE_RANGE
|
||||
|
||||
document_timeout: Annotated[
|
||||
float,
|
||||
Field(
|
||||
description="The timeout for processing each document, in seconds.",
|
||||
gt=0,
|
||||
le=docling_serve_settings.max_document_timeout,
|
||||
),
|
||||
] = docling_serve_settings.max_document_timeout
|
||||
|
||||
abort_on_error: Annotated[
|
||||
bool,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from collections.abc import Iterable, Iterator
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
@@ -18,10 +19,15 @@ from docling.datamodel.document import ConversionResult
|
||||
from docling.datamodel.pipeline_options import (
|
||||
OcrOptions,
|
||||
PdfBackend,
|
||||
PdfPipeline,
|
||||
PdfPipelineOptions,
|
||||
TableFormerMode,
|
||||
VlmPipelineOptions,
|
||||
smoldocling_vlm_conversion_options,
|
||||
smoldocling_vlm_mlx_conversion_options,
|
||||
)
|
||||
from docling.document_converter import DocumentConverter, FormatOption, PdfFormatOption
|
||||
from docling.pipeline.vlm_pipeline import VlmPipeline
|
||||
from docling_core.types.doc import ImageRefMode
|
||||
|
||||
from docling_serve.datamodel.convert import ConvertDocumentsOptions, ocr_factory
|
||||
@@ -84,10 +90,9 @@ def get_converter(pdf_format_option: PdfFormatOption) -> DocumentConverter:
|
||||
return _get_converter_from_hash(options_hash)
|
||||
|
||||
|
||||
# Computes the PDF pipeline options and returns the PdfFormatOption and its hash
|
||||
def get_pdf_pipeline_opts(
|
||||
request: ConvertDocumentsOptions,
|
||||
) -> PdfFormatOption:
|
||||
def _parse_standard_pdf_opts(
|
||||
request: ConvertDocumentsOptions, artifacts_path: Optional[Path]
|
||||
) -> PdfPipelineOptions:
|
||||
try:
|
||||
ocr_options: OcrOptions = ocr_factory.create_options(
|
||||
kind=request.ocr_engine.value, # type: ignore
|
||||
@@ -110,6 +115,8 @@ def get_pdf_pipeline_opts(
|
||||
ocr_options.lang = request.ocr_lang
|
||||
|
||||
pipeline_options = PdfPipelineOptions(
|
||||
artifacts_path=artifacts_path,
|
||||
document_timeout=request.document_timeout,
|
||||
do_ocr=request.do_ocr,
|
||||
ocr_options=ocr_options,
|
||||
do_table_structure=request.do_table_structure,
|
||||
@@ -118,7 +125,6 @@ def get_pdf_pipeline_opts(
|
||||
do_picture_classification=request.do_picture_classification,
|
||||
do_picture_description=request.do_picture_description,
|
||||
)
|
||||
pipeline_options.table_structure_options.do_cell_matching = True # do_cell_matching
|
||||
pipeline_options.table_structure_options.mode = TableFormerMode(request.table_mode)
|
||||
|
||||
if request.image_export_mode != ImageRefMode.PLACEHOLDER:
|
||||
@@ -126,6 +132,10 @@ def get_pdf_pipeline_opts(
|
||||
if request.images_scale:
|
||||
pipeline_options.images_scale = request.images_scale
|
||||
|
||||
return pipeline_options
|
||||
|
||||
|
||||
def _parse_backend(request: ConvertDocumentsOptions) -> type[PdfDocumentBackend]:
|
||||
if request.pdf_backend == PdfBackend.DLPARSE_V1:
|
||||
backend: type[PdfDocumentBackend] = DoclingParseDocumentBackend
|
||||
elif request.pdf_backend == PdfBackend.DLPARSE_V2:
|
||||
@@ -137,35 +147,78 @@ def get_pdf_pipeline_opts(
|
||||
else:
|
||||
raise RuntimeError(f"Unexpected PDF backend type {request.pdf_backend}")
|
||||
|
||||
return backend
|
||||
|
||||
|
||||
def _parse_vlm_pdf_opts(
|
||||
request: ConvertDocumentsOptions, artifacts_path: Optional[Path]
|
||||
) -> VlmPipelineOptions:
|
||||
pipeline_options = VlmPipelineOptions(
|
||||
artifacts_path=artifacts_path,
|
||||
document_timeout=request.document_timeout,
|
||||
)
|
||||
pipeline_options.vlm_options = smoldocling_vlm_conversion_options
|
||||
if sys.platform == "darwin":
|
||||
try:
|
||||
import mlx_vlm # noqa: F401
|
||||
|
||||
pipeline_options.vlm_options = smoldocling_vlm_mlx_conversion_options
|
||||
except ImportError:
|
||||
_log.warning(
|
||||
"To run SmolDocling faster, please install mlx-vlm:\n"
|
||||
"pip install mlx-vlm"
|
||||
)
|
||||
return pipeline_options
|
||||
|
||||
|
||||
# Computes the PDF pipeline options and returns the PdfFormatOption and its hash
|
||||
def get_pdf_pipeline_opts(
|
||||
request: ConvertDocumentsOptions,
|
||||
) -> PdfFormatOption:
|
||||
artifacts_path: Optional[Path] = None
|
||||
if docling_serve_settings.artifacts_path is not None:
|
||||
if str(docling_serve_settings.artifacts_path.absolute()) == "":
|
||||
_log.info(
|
||||
"artifacts_path is an empty path, model weights will be dowloaded "
|
||||
"at runtime."
|
||||
)
|
||||
pipeline_options.artifacts_path = None
|
||||
artifacts_path = None
|
||||
elif docling_serve_settings.artifacts_path.is_dir():
|
||||
_log.info(
|
||||
"artifacts_path is set to a valid directory. "
|
||||
"No model weights will be downloaded at runtime."
|
||||
)
|
||||
pipeline_options.artifacts_path = docling_serve_settings.artifacts_path
|
||||
artifacts_path = docling_serve_settings.artifacts_path
|
||||
else:
|
||||
_log.warning(
|
||||
"artifacts_path is set to an invalid directory. "
|
||||
"The system will download the model weights at runtime."
|
||||
)
|
||||
pipeline_options.artifacts_path = None
|
||||
artifacts_path = None
|
||||
else:
|
||||
_log.info(
|
||||
"artifacts_path is unset. "
|
||||
"The system will download the model weights at runtime."
|
||||
)
|
||||
|
||||
pdf_format_option = PdfFormatOption(
|
||||
pipeline_options=pipeline_options,
|
||||
backend=backend,
|
||||
)
|
||||
pipeline_options: Union[PdfPipelineOptions, VlmPipelineOptions]
|
||||
if request.pipeline == PdfPipeline.STANDARD:
|
||||
pipeline_options = _parse_standard_pdf_opts(request, artifacts_path)
|
||||
backend = _parse_backend(request)
|
||||
pdf_format_option = PdfFormatOption(
|
||||
pipeline_options=pipeline_options,
|
||||
backend=backend,
|
||||
)
|
||||
|
||||
elif request.pipeline == PdfPipeline.VLM:
|
||||
pipeline_options = _parse_vlm_pdf_opts(request, artifacts_path)
|
||||
pdf_format_option = PdfFormatOption(
|
||||
pipeline_cls=VlmPipeline, pipeline_options=pipeline_options
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"The pipeline {request.pipeline} is not implemented."
|
||||
)
|
||||
|
||||
return pdf_format_option
|
||||
|
||||
@@ -180,6 +233,9 @@ def convert_documents(
|
||||
results: Iterator[ConversionResult] = converter.convert_all(
|
||||
sources,
|
||||
headers=headers,
|
||||
page_range=options.page_range,
|
||||
max_file_size=docling_serve_settings.max_file_size,
|
||||
max_num_pages=docling_serve_settings.max_num_pages,
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import base64
|
||||
import importlib
|
||||
import json
|
||||
import logging
|
||||
import ssl
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import certifi
|
||||
import gradio as gr
|
||||
import requests
|
||||
import httpx
|
||||
|
||||
from docling.datamodel.pipeline_options import (
|
||||
PdfBackend,
|
||||
PdfPipeline,
|
||||
TableFormerMode,
|
||||
TableStructureOptions,
|
||||
)
|
||||
|
||||
from docling_serve.helper_functions import _to_list_of_strings
|
||||
from docling_serve.settings import docling_serve_settings, uvicorn_settings
|
||||
@@ -17,7 +28,7 @@ logger = logging.getLogger(__name__)
|
||||
############################
|
||||
|
||||
logo_path = "https://raw.githubusercontent.com/docling-project/docling/refs/heads/main/docs/assets/logo.svg"
|
||||
js_components_url = "https://unpkg.com/@docling/docling-components@0.0.3"
|
||||
js_components_url = "https://unpkg.com/@docling/docling-components@0.0.6"
|
||||
if (
|
||||
docling_serve_settings.static_path is not None
|
||||
and docling_serve_settings.static_path.is_dir()
|
||||
@@ -109,8 +120,29 @@ file_output_path = None # Will be set when a new file is generated
|
||||
#############
|
||||
|
||||
|
||||
def get_api_endpoint() -> str:
|
||||
protocol = "http"
|
||||
if uvicorn_settings.ssl_keyfile is not None:
|
||||
protocol = "https"
|
||||
return f"{protocol}://{docling_serve_settings.api_host}:{uvicorn_settings.port}"
|
||||
|
||||
|
||||
def get_ssl_context() -> ssl.SSLContext:
|
||||
ctx = ssl.create_default_context(cafile=certifi.where())
|
||||
kube_sa_ca_cert_path = Path(
|
||||
"/run/secrets/kubernetes.io/serviceaccount/service-ca.crt"
|
||||
)
|
||||
if (
|
||||
uvicorn_settings.ssl_keyfile is not None
|
||||
and ".svc." in docling_serve_settings.api_host
|
||||
and kube_sa_ca_cert_path.exists()
|
||||
):
|
||||
ctx.load_verify_locations(cafile=kube_sa_ca_cert_path)
|
||||
return ctx
|
||||
|
||||
|
||||
def health_check():
|
||||
response = requests.get(f"http://localhost:{uvicorn_settings.port}/health")
|
||||
response = httpx.get(f"{get_api_endpoint()}/health")
|
||||
if response.status_code == 200:
|
||||
return "Healthy"
|
||||
return "Unhealthy"
|
||||
@@ -126,6 +158,11 @@ def set_outputs_visibility_direct(x, y):
|
||||
return content, file
|
||||
|
||||
|
||||
def set_task_id_visibility(x):
|
||||
task_id_row = gr.Row(visible=x)
|
||||
return task_id_row
|
||||
|
||||
|
||||
def set_outputs_visibility_process(x):
|
||||
content = gr.Row(visible=not x)
|
||||
file = gr.Row(visible=x)
|
||||
@@ -137,6 +174,7 @@ def set_download_button_label(label_text: gr.State):
|
||||
|
||||
|
||||
def clear_outputs():
|
||||
task_id_rendered = ""
|
||||
markdown_content = ""
|
||||
json_content = ""
|
||||
json_rendered_content = ""
|
||||
@@ -145,6 +183,7 @@ def clear_outputs():
|
||||
doctags_content = ""
|
||||
|
||||
return (
|
||||
task_id_rendered,
|
||||
markdown_content,
|
||||
markdown_content,
|
||||
json_content,
|
||||
@@ -187,10 +226,56 @@ def change_ocr_lang(ocr_engine):
|
||||
return "english,chinese"
|
||||
|
||||
|
||||
def wait_task_finish(task_id: str, return_as_file: bool):
|
||||
conversion_sucess = False
|
||||
task_finished = False
|
||||
task_status = ""
|
||||
ssl_ctx = get_ssl_context()
|
||||
while not task_finished:
|
||||
try:
|
||||
response = httpx.get(
|
||||
f"{get_api_endpoint()}/v1alpha/status/poll/{task_id}?wait=5",
|
||||
verify=ssl_ctx,
|
||||
timeout=15,
|
||||
)
|
||||
task_status = response.json()["task_status"]
|
||||
if task_status == "success":
|
||||
conversion_sucess = True
|
||||
task_finished = True
|
||||
|
||||
if task_status in ("failure", "revoked"):
|
||||
conversion_sucess = False
|
||||
task_finished = True
|
||||
raise RuntimeError(f"Task failed with status {task_status!r}")
|
||||
time.sleep(5)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing file(s): {e}")
|
||||
conversion_sucess = False
|
||||
task_finished = True
|
||||
raise gr.Error(f"Error processing file(s): {e}", print_exception=False)
|
||||
|
||||
if conversion_sucess:
|
||||
try:
|
||||
response = httpx.get(
|
||||
f"{get_api_endpoint()}/v1alpha/result/{task_id}",
|
||||
timeout=15,
|
||||
verify=ssl_ctx,
|
||||
)
|
||||
output = response_to_output(response, return_as_file)
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting task result: {e}")
|
||||
|
||||
raise gr.Error(
|
||||
f"Error getting task result, conversion finished with status: {task_status}"
|
||||
)
|
||||
|
||||
|
||||
def process_url(
|
||||
input_sources,
|
||||
to_formats,
|
||||
image_export_mode,
|
||||
pipeline,
|
||||
ocr,
|
||||
force_ocr,
|
||||
ocr_engine,
|
||||
@@ -209,6 +294,7 @@ def process_url(
|
||||
"options": {
|
||||
"to_formats": to_formats,
|
||||
"image_export_mode": image_export_mode,
|
||||
"pipeline": pipeline,
|
||||
"ocr": ocr,
|
||||
"force_ocr": force_ocr,
|
||||
"ocr_engine": ocr_engine,
|
||||
@@ -231,9 +317,12 @@ def process_url(
|
||||
logger.error("No input sources provided.")
|
||||
raise gr.Error("No input sources provided.", print_exception=False)
|
||||
try:
|
||||
response = requests.post(
|
||||
f"http://localhost:{uvicorn_settings.port}/v1alpha/convert/source",
|
||||
ssl_ctx = get_ssl_context()
|
||||
response = httpx.post(
|
||||
f"{get_api_endpoint()}/v1alpha/convert/source/async",
|
||||
json=parameters,
|
||||
verify=ssl_ctx,
|
||||
timeout=60,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing URL: {e}")
|
||||
@@ -243,14 +332,22 @@ def process_url(
|
||||
error_message = data.get("detail", "An unknown error occurred.")
|
||||
logger.error(f"Error processing file: {error_message}")
|
||||
raise gr.Error(f"Error processing file: {error_message}", print_exception=False)
|
||||
output = response_to_output(response, return_as_file)
|
||||
return output
|
||||
|
||||
task_id_rendered = response.json()["task_id"]
|
||||
return task_id_rendered
|
||||
|
||||
|
||||
def file_to_base64(file):
|
||||
with open(file.name, "rb") as f:
|
||||
encoded_string = base64.b64encode(f.read()).decode("utf-8")
|
||||
return encoded_string
|
||||
|
||||
|
||||
def process_file(
|
||||
files,
|
||||
file,
|
||||
to_formats,
|
||||
image_export_mode,
|
||||
pipeline,
|
||||
ocr,
|
||||
force_ocr,
|
||||
ocr_engine,
|
||||
@@ -264,33 +361,39 @@ def process_file(
|
||||
do_picture_classification,
|
||||
do_picture_description,
|
||||
):
|
||||
if not files or len(files) == 0 or files[0] == "":
|
||||
if not file or file == "":
|
||||
logger.error("No files provided.")
|
||||
raise gr.Error("No files provided.", print_exception=False)
|
||||
files_data = [("files", (file.name, open(file.name, "rb"))) for file in files]
|
||||
files_data = [{"base64_string": file_to_base64(file), "filename": file.name}]
|
||||
|
||||
parameters = {
|
||||
"to_formats": to_formats,
|
||||
"image_export_mode": image_export_mode,
|
||||
"ocr": str(ocr).lower(),
|
||||
"force_ocr": str(force_ocr).lower(),
|
||||
"ocr_engine": ocr_engine,
|
||||
"ocr_lang": _to_list_of_strings(ocr_lang),
|
||||
"pdf_backend": pdf_backend,
|
||||
"table_mode": table_mode,
|
||||
"abort_on_error": str(abort_on_error).lower(),
|
||||
"return_as_file": str(return_as_file).lower(),
|
||||
"do_code_enrichment": str(do_code_enrichment).lower(),
|
||||
"do_formula_enrichment": str(do_formula_enrichment).lower(),
|
||||
"do_picture_classification": str(do_picture_classification).lower(),
|
||||
"do_picture_description": str(do_picture_description).lower(),
|
||||
"file_sources": files_data,
|
||||
"options": {
|
||||
"to_formats": to_formats,
|
||||
"image_export_mode": image_export_mode,
|
||||
"pipeline": pipeline,
|
||||
"ocr": ocr,
|
||||
"force_ocr": force_ocr,
|
||||
"ocr_engine": ocr_engine,
|
||||
"ocr_lang": _to_list_of_strings(ocr_lang),
|
||||
"pdf_backend": pdf_backend,
|
||||
"table_mode": table_mode,
|
||||
"abort_on_error": abort_on_error,
|
||||
"return_as_file": return_as_file,
|
||||
"do_code_enrichment": do_code_enrichment,
|
||||
"do_formula_enrichment": do_formula_enrichment,
|
||||
"do_picture_classification": do_picture_classification,
|
||||
"do_picture_description": do_picture_description,
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"http://localhost:{uvicorn_settings.port}/v1alpha/convert/file",
|
||||
files=files_data,
|
||||
data=parameters,
|
||||
ssl_ctx = get_ssl_context()
|
||||
response = httpx.post(
|
||||
f"{get_api_endpoint()}/v1alpha/convert/source/async",
|
||||
json=parameters,
|
||||
verify=ssl_ctx,
|
||||
timeout=60,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing file(s): {e}")
|
||||
@@ -300,8 +403,9 @@ def process_file(
|
||||
error_message = data.get("detail", "An unknown error occurred.")
|
||||
logger.error(f"Error processing file: {error_message}")
|
||||
raise gr.Error(f"Error processing file: {error_message}", print_exception=False)
|
||||
output = response_to_output(response, return_as_file)
|
||||
return output
|
||||
|
||||
task_id_rendered = response.json()["task_id"]
|
||||
return task_id_rendered
|
||||
|
||||
|
||||
def response_to_output(response, return_as_file):
|
||||
@@ -415,30 +519,31 @@ with gr.Blocks(
|
||||
)
|
||||
|
||||
# URL Processing Tab
|
||||
with gr.Tab("Convert URL(s)"):
|
||||
with gr.Tab("Convert URL"):
|
||||
with gr.Row():
|
||||
with gr.Column(scale=4):
|
||||
url_input = gr.Textbox(
|
||||
label="Input Sources (comma-separated URLs)",
|
||||
placeholder="https://arxiv.org/pdf/2206.01062",
|
||||
label="URL Input Source",
|
||||
placeholder="https://arxiv.org/pdf/2501.17887",
|
||||
)
|
||||
with gr.Column(scale=1):
|
||||
url_process_btn = gr.Button("Process URL(s)", scale=1)
|
||||
url_process_btn = gr.Button("Process URL", scale=1)
|
||||
url_reset_btn = gr.Button("Reset", scale=1)
|
||||
|
||||
# File Processing Tab
|
||||
with gr.Tab("Convert File(s)"):
|
||||
with gr.Tab("Convert File"):
|
||||
with gr.Row():
|
||||
with gr.Column(scale=4):
|
||||
file_input = gr.File(
|
||||
elem_id="file_input_zone",
|
||||
label="Upload Files",
|
||||
label="Upload File",
|
||||
file_types=[
|
||||
".pdf",
|
||||
".docx",
|
||||
".pptx",
|
||||
".html",
|
||||
".xlsx",
|
||||
".json",
|
||||
".asciidoc",
|
||||
".txt",
|
||||
".md",
|
||||
@@ -447,11 +552,11 @@ with gr.Blocks(
|
||||
".png",
|
||||
".gif",
|
||||
],
|
||||
file_count="multiple",
|
||||
file_count="single",
|
||||
scale=4,
|
||||
)
|
||||
with gr.Column(scale=1):
|
||||
file_process_btn = gr.Button("Process File(s)", scale=1)
|
||||
file_process_btn = gr.Button("Process File", scale=1)
|
||||
file_reset_btn = gr.Button("Reset", scale=1)
|
||||
|
||||
# Options
|
||||
@@ -460,14 +565,14 @@ with gr.Blocks(
|
||||
with gr.Column(scale=1):
|
||||
to_formats = gr.CheckboxGroup(
|
||||
[
|
||||
("Markdown", "md"),
|
||||
("Docling (JSON)", "json"),
|
||||
("Markdown", "md"),
|
||||
("HTML", "html"),
|
||||
("Plain Text", "text"),
|
||||
("Doc Tags", "doctags"),
|
||||
],
|
||||
label="To Formats",
|
||||
value=["md"],
|
||||
value=["json", "md"],
|
||||
)
|
||||
with gr.Column(scale=1):
|
||||
image_export_mode = gr.Radio(
|
||||
@@ -479,6 +584,13 @@ with gr.Blocks(
|
||||
label="Image Export Mode",
|
||||
value="embedded",
|
||||
)
|
||||
with gr.Row():
|
||||
with gr.Column(scale=1, min_width=200):
|
||||
pipeline = gr.Radio(
|
||||
[(v.value.capitalize(), v.value) for v in PdfPipeline],
|
||||
label="Pipeline type",
|
||||
value=PdfPipeline.STANDARD.value,
|
||||
)
|
||||
with gr.Row():
|
||||
with gr.Column(scale=1, min_width=200):
|
||||
ocr = gr.Checkbox(label="Enable OCR", value=True)
|
||||
@@ -499,19 +611,23 @@ with gr.Blocks(
|
||||
)
|
||||
ocr_engine.change(change_ocr_lang, inputs=[ocr_engine], outputs=[ocr_lang])
|
||||
with gr.Row():
|
||||
with gr.Column(scale=2):
|
||||
with gr.Column(scale=4):
|
||||
pdf_backend = gr.Radio(
|
||||
["pypdfium2", "dlparse_v1", "dlparse_v2"],
|
||||
[v.value for v in PdfBackend],
|
||||
label="PDF Backend",
|
||||
value="dlparse_v2",
|
||||
value=PdfBackend.DLPARSE_V4.value,
|
||||
)
|
||||
with gr.Column(scale=2):
|
||||
table_mode = gr.Radio(
|
||||
["fast", "accurate"], label="Table Mode", value="fast"
|
||||
[(v.value.capitalize(), v.value) for v in TableFormerMode],
|
||||
label="Table Mode",
|
||||
value=TableStructureOptions().mode.value,
|
||||
)
|
||||
with gr.Column(scale=1):
|
||||
abort_on_error = gr.Checkbox(label="Abort on Error", value=False)
|
||||
return_as_file = gr.Checkbox(label="Return as File", value=False)
|
||||
return_as_file = gr.Checkbox(
|
||||
label="Return as File", visible=False, value=False
|
||||
) # Disable until async handle output as file
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
do_code_enrichment = gr.Checkbox(
|
||||
@@ -528,18 +644,22 @@ with gr.Blocks(
|
||||
label="Enable picture description", value=False
|
||||
)
|
||||
|
||||
# Task id output
|
||||
with gr.Row(visible=False) as task_id_output:
|
||||
task_id_rendered = gr.Textbox(label="Task id", interactive=False)
|
||||
|
||||
# Document output
|
||||
with gr.Row(visible=False) as content_output:
|
||||
with gr.Tab("Docling (JSON)"):
|
||||
output_json = gr.Code(language="json", wrap_lines=True, show_label=False)
|
||||
with gr.Tab("Docling-Rendered"):
|
||||
output_json_rendered = gr.HTML(label="Response")
|
||||
with gr.Tab("Markdown"):
|
||||
output_markdown = gr.Code(
|
||||
language="markdown", wrap_lines=True, show_label=False
|
||||
)
|
||||
with gr.Tab("Markdown-Rendered"):
|
||||
output_markdown_rendered = gr.Markdown(label="Response")
|
||||
with gr.Tab("Docling (JSON)"):
|
||||
output_json = gr.Code(language="json", wrap_lines=True, show_label=False)
|
||||
with gr.Tab("Docling-Rendered"):
|
||||
output_json_rendered = gr.HTML()
|
||||
with gr.Tab("HTML"):
|
||||
output_html = gr.Code(language="html", wrap_lines=True, show_label=False)
|
||||
with gr.Tab("HTML-Rendered"):
|
||||
@@ -557,36 +677,34 @@ with gr.Blocks(
|
||||
# UI Actions #
|
||||
##############
|
||||
|
||||
# Disable until async handle output as file
|
||||
# Handle Return as File
|
||||
url_input.change(
|
||||
auto_set_return_as_file,
|
||||
inputs=[url_input, file_input, image_export_mode],
|
||||
outputs=[return_as_file],
|
||||
)
|
||||
file_input.change(
|
||||
auto_set_return_as_file,
|
||||
inputs=[url_input, file_input, image_export_mode],
|
||||
outputs=[return_as_file],
|
||||
)
|
||||
image_export_mode.change(
|
||||
auto_set_return_as_file,
|
||||
inputs=[url_input, file_input, image_export_mode],
|
||||
outputs=[return_as_file],
|
||||
)
|
||||
# url_input.change(
|
||||
# auto_set_return_as_file,
|
||||
# inputs=[url_input, file_input, image_export_mode],
|
||||
# outputs=[return_as_file],
|
||||
# )
|
||||
# file_input.change(
|
||||
# auto_set_return_as_file,
|
||||
# inputs=[url_input, file_input, image_export_mode],
|
||||
# outputs=[return_as_file],
|
||||
# )
|
||||
# image_export_mode.change(
|
||||
# auto_set_return_as_file,
|
||||
# inputs=[url_input, file_input, image_export_mode],
|
||||
# outputs=[return_as_file],
|
||||
# )
|
||||
|
||||
# URL processing
|
||||
url_process_btn.click(
|
||||
set_options_visibility, inputs=[false_bool], outputs=[options]
|
||||
).then(
|
||||
set_download_button_label, inputs=[processing_text], outputs=[download_file_btn]
|
||||
).then(
|
||||
set_outputs_visibility_process,
|
||||
inputs=[return_as_file],
|
||||
outputs=[content_output, file_output],
|
||||
).then(
|
||||
clear_outputs,
|
||||
inputs=None,
|
||||
outputs=[
|
||||
task_id_rendered,
|
||||
output_markdown,
|
||||
output_markdown_rendered,
|
||||
output_json,
|
||||
@@ -596,12 +714,17 @@ with gr.Blocks(
|
||||
output_text,
|
||||
output_doctags,
|
||||
],
|
||||
).then(
|
||||
set_task_id_visibility,
|
||||
inputs=[true_bool],
|
||||
outputs=[task_id_output],
|
||||
).then(
|
||||
process_url,
|
||||
inputs=[
|
||||
url_input,
|
||||
to_formats,
|
||||
image_export_mode,
|
||||
pipeline,
|
||||
ocr,
|
||||
force_ocr,
|
||||
ocr_engine,
|
||||
@@ -615,6 +738,16 @@ with gr.Blocks(
|
||||
do_picture_classification,
|
||||
do_picture_description,
|
||||
],
|
||||
outputs=[
|
||||
task_id_rendered,
|
||||
],
|
||||
).then(
|
||||
set_outputs_visibility_process,
|
||||
inputs=[return_as_file],
|
||||
outputs=[content_output, file_output],
|
||||
).then(
|
||||
wait_task_finish,
|
||||
inputs=[task_id_rendered, return_as_file],
|
||||
outputs=[
|
||||
output_markdown,
|
||||
output_markdown_rendered,
|
||||
@@ -645,21 +778,20 @@ with gr.Blocks(
|
||||
set_outputs_visibility_direct,
|
||||
inputs=[false_bool, false_bool],
|
||||
outputs=[content_output, file_output],
|
||||
).then(clear_url_input, inputs=None, outputs=[url_input])
|
||||
).then(set_task_id_visibility, inputs=[false_bool], outputs=[task_id_output]).then(
|
||||
clear_url_input, inputs=None, outputs=[url_input]
|
||||
)
|
||||
|
||||
# File processing
|
||||
file_process_btn.click(
|
||||
set_options_visibility, inputs=[false_bool], outputs=[options]
|
||||
).then(
|
||||
set_download_button_label, inputs=[processing_text], outputs=[download_file_btn]
|
||||
).then(
|
||||
set_outputs_visibility_process,
|
||||
inputs=[return_as_file],
|
||||
outputs=[content_output, file_output],
|
||||
).then(
|
||||
clear_outputs,
|
||||
inputs=None,
|
||||
outputs=[
|
||||
task_id_rendered,
|
||||
output_markdown,
|
||||
output_markdown_rendered,
|
||||
output_json,
|
||||
@@ -669,12 +801,17 @@ with gr.Blocks(
|
||||
output_text,
|
||||
output_doctags,
|
||||
],
|
||||
).then(
|
||||
set_task_id_visibility,
|
||||
inputs=[true_bool],
|
||||
outputs=[task_id_output],
|
||||
).then(
|
||||
process_file,
|
||||
inputs=[
|
||||
file_input,
|
||||
to_formats,
|
||||
image_export_mode,
|
||||
pipeline,
|
||||
ocr,
|
||||
force_ocr,
|
||||
ocr_engine,
|
||||
@@ -688,6 +825,16 @@ with gr.Blocks(
|
||||
do_picture_classification,
|
||||
do_picture_description,
|
||||
],
|
||||
outputs=[
|
||||
task_id_rendered,
|
||||
],
|
||||
).then(
|
||||
set_outputs_visibility_process,
|
||||
inputs=[return_as_file],
|
||||
outputs=[content_output, file_output],
|
||||
).then(
|
||||
wait_task_finish,
|
||||
inputs=[task_id_rendered, return_as_file],
|
||||
outputs=[
|
||||
output_markdown,
|
||||
output_markdown_rendered,
|
||||
@@ -718,4 +865,6 @@ with gr.Blocks(
|
||||
set_outputs_visibility_direct,
|
||||
inputs=[false_bool, false_bool],
|
||||
outputs=[content_output, file_output],
|
||||
).then(clear_file_input, inputs=None, outputs=[file_input])
|
||||
).then(set_task_id_visibility, inputs=[false_bool], outputs=[task_id_output]).then(
|
||||
clear_file_input, inputs=None, outputs=[file_input]
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
@@ -32,11 +33,16 @@ class DoclingServeSettings(BaseSettings):
|
||||
)
|
||||
|
||||
enable_ui: bool = False
|
||||
api_host: str = "localhost"
|
||||
artifacts_path: Optional[Path] = None
|
||||
static_path: Optional[Path] = None
|
||||
options_cache_size: int = 2
|
||||
allow_external_plugins: bool = False
|
||||
|
||||
max_document_timeout: float = 3_600 * 24 * 7 # 7 days
|
||||
max_num_pages: int = sys.maxsize
|
||||
max_file_size: int = sys.maxsize
|
||||
|
||||
cors_origins: list[str] = ["*"]
|
||||
cors_methods: list[str] = ["*"]
|
||||
cors_headers: list[str] = ["*"]
|
||||
|
||||
15
docs/deploy-examples/compose-gpu.yaml
Normal file
15
docs/deploy-examples/compose-gpu.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
docling:
|
||||
image: ghcr.io/docling-project/docling-serve-cu124
|
||||
container_name: docling-serve
|
||||
ports:
|
||||
- 5001:5001
|
||||
environment:
|
||||
- DOCLING_SERVE_ENABLE_UI=true
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all # nvidia-smi
|
||||
capabilities: [gpu]
|
||||
@@ -9,41 +9,18 @@ metadata:
|
||||
annotations:
|
||||
serviceaccounts.openshift.io/oauth-redirectreference.primary: '{"kind":"OAuthRedirectReference","apiVersion":"v1","reference":{"kind":"Route","name":"docling-serve"}}'
|
||||
---
|
||||
kind: Role
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: docling-serve-oauth
|
||||
labels:
|
||||
app: docling-serve
|
||||
component: docling-serve-api
|
||||
rules:
|
||||
- verbs:
|
||||
- create
|
||||
apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
- subjectaccessreviews
|
||||
- verbs:
|
||||
- create
|
||||
apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
- tokenreviews
|
||||
---
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: docling-serve-oauth
|
||||
labels:
|
||||
app: docling-serve
|
||||
component: docling-serve-api
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: docling-serve
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: docling-serve-oauth
|
||||
kind: ClusterRole
|
||||
name: system:auth-delegator
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: docling-serve
|
||||
namespace: docling
|
||||
---
|
||||
apiVersion: route.openshift.io/v1
|
||||
kind: Route
|
||||
@@ -107,10 +84,10 @@ spec:
|
||||
- name: api
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
cpu: 2000m
|
||||
memory: 2Gi
|
||||
requests:
|
||||
cpu: 250m
|
||||
cpu: 800m
|
||||
memory: 1Gi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
@@ -128,13 +105,19 @@ spec:
|
||||
port: http
|
||||
scheme: HTTPS
|
||||
initialDelaySeconds: 3
|
||||
timeoutSeconds: 2
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 4
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
failureThreshold: 3
|
||||
failureThreshold: 5
|
||||
env:
|
||||
- name: NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: DOCLING_SERVE_ENABLE_UI
|
||||
value: 'true'
|
||||
- name: DOCLING_SERVE_API_HOST
|
||||
value: 'docling-serve.$(NAMESPACE).svc.cluster.local'
|
||||
- name: UVICORN_SSL_CERTFILE
|
||||
value: '/etc/tls/private/tls.crt'
|
||||
- name: UVICORN_SSL_KEYFILE
|
||||
@@ -147,7 +130,7 @@ spec:
|
||||
- name: proxy-tls
|
||||
mountPath: /etc/tls/private
|
||||
imagePullPolicy: Always
|
||||
image: 'ghcr.io/docling-project/docling-serve:dev-ssl'
|
||||
image: 'ghcr.io/docling-project/docling-serve-cpu:fix-ui-with-https'
|
||||
- name: oauth-proxy
|
||||
resources:
|
||||
limits:
|
||||
|
||||
58
docs/deploy-examples/docling-serve-simple.yaml
Normal file
58
docs/deploy-examples/docling-serve-simple.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
# This example deployment configures Docling Serve with a Service and cuda image
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: docling-serve
|
||||
labels:
|
||||
app: docling-serve
|
||||
component: docling-serve-api
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 5001
|
||||
targetPort: http
|
||||
selector:
|
||||
app: docling-serve
|
||||
component: docling-serve-api
|
||||
---
|
||||
kind: Deployment
|
||||
apiVersion: apps/v1
|
||||
metadata:
|
||||
name: docling-serve
|
||||
labels:
|
||||
app: docling-serve
|
||||
component: docling-serve-api
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: docling-serve
|
||||
component: docling-serve-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: docling-serve
|
||||
component: docling-serve-api
|
||||
spec:
|
||||
restartPolicy: Always
|
||||
containers:
|
||||
- name: api
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 2Gi
|
||||
nvidia.com/gpu: 1 # Limit to one GPU
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 1Gi
|
||||
nvidia.com/gpu: 1 # Limit to one GPU
|
||||
env:
|
||||
- name: DOCLING_SERVE_ENABLE_UI
|
||||
value: 'true'
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 5001
|
||||
protocol: TCP
|
||||
imagePullPolicy: Always
|
||||
image: 'ghcr.io/docling-project/docling-serve-cu124'
|
||||
@@ -1,7 +1,161 @@
|
||||
# Deployment
|
||||
# Deployment Examples
|
||||
|
||||
This document provides deployment examples for running the application in different environments.
|
||||
|
||||
Choose the deployment option that best fits your setup.
|
||||
|
||||
- **[Local GPU](#local-gpu)**: For deploying the application locally on a machine with a NVIDIA GPU (using Docker Compose).
|
||||
- **[OpenShift](#openshift)**: For deploying the application on an OpenShift cluster, designed for cloud-native environments.
|
||||
|
||||
---
|
||||
|
||||
## Local GPU
|
||||
|
||||
### Docker compose
|
||||
|
||||
Manifest example: [compose-gpu.yaml](./deploy-examples/compose-gpu.yaml)
|
||||
|
||||
This deployment has the following features:
|
||||
|
||||
- NVIDIA cuda enabled
|
||||
|
||||
Install the app with:
|
||||
|
||||
```sh
|
||||
docker compose -f docs/deploy-examples/compose-gpu.yaml up -d
|
||||
```
|
||||
|
||||
For using the API:
|
||||
|
||||
```sh
|
||||
# Make a test query
|
||||
curl -X 'POST' \
|
||||
"localhost:5001/v1alpha/convert/source/async" \
|
||||
-H "accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"http_sources": [{"url": "https://arxiv.org/pdf/2501.17887"}]
|
||||
}'
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><b>Requirements</b></summary>
|
||||
|
||||
- debian/ubuntu/rhel/fedora/opensuse
|
||||
- docker
|
||||
- nvidia drivers >=550.54.14
|
||||
- nvidia-container-toolkit
|
||||
|
||||
Docs:
|
||||
|
||||
- [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/supported-platforms.html)
|
||||
- [CUDA Toolkit Release Notes](https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html#id6)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Steps</b></summary>
|
||||
|
||||
1. Check driver version and which GPU you want to use (0/1/2/3.. and update [compose-gpu.yaml](./deploy-examples/compose-gpu.yaml) file or use `count: all`)
|
||||
|
||||
```sh
|
||||
nvidia-smi
|
||||
```
|
||||
|
||||
2. Check if the NVIDIA Container Toolkit is installed/updated
|
||||
|
||||
```sh
|
||||
# debian
|
||||
dpkg -l | grep nvidia-container-toolkit
|
||||
```
|
||||
|
||||
```sh
|
||||
# rhel
|
||||
rpm -q nvidia-container-toolkit
|
||||
```
|
||||
|
||||
NVIDIA Container Toolkit install steps can be found here:
|
||||
|
||||
<https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html>
|
||||
|
||||
3. Check which runtime is being used by Docker
|
||||
|
||||
```sh
|
||||
# docker
|
||||
docker info | grep -i runtime
|
||||
```
|
||||
|
||||
4. If the default Docker runtime changes back from 'nvidia' to 'default' after restarting the Docker service (optional):
|
||||
|
||||
Backup the daemon.json file:
|
||||
|
||||
```sh
|
||||
sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bak
|
||||
```
|
||||
|
||||
Update the daemon.json file:
|
||||
|
||||
```sh
|
||||
echo '{
|
||||
"runtimes": {
|
||||
"nvidia": {
|
||||
"path": "nvidia-container-runtime"
|
||||
}
|
||||
},
|
||||
"default-runtime": "nvidia"
|
||||
}' | sudo tee /etc/docker/daemon.json > /dev/null
|
||||
```
|
||||
|
||||
Restart the Docker service:
|
||||
|
||||
```sh
|
||||
sudo systemctl restart docker
|
||||
```
|
||||
|
||||
Confirm 'nvidia' is the default runtime used by Docker by repeating step 3.
|
||||
|
||||
5. Run the container:
|
||||
|
||||
```sh
|
||||
docker compose -f docs/deploy-examples/compose-gpu.yaml up -d
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## OpenShift
|
||||
|
||||
### Simple deployment
|
||||
|
||||
Manifest example: [docling-serve-simple.yaml](./deploy-examples/docling-serve-simple.yaml)
|
||||
|
||||
This deployment example has the following features:
|
||||
|
||||
- Deployment configuration
|
||||
- Service configuration
|
||||
- NVIDIA cuda enabled
|
||||
|
||||
Install the app with:
|
||||
|
||||
```sh
|
||||
oc apply -f docs/deploy-examples/docling-serve-simple.yaml
|
||||
```
|
||||
|
||||
For using the API:
|
||||
|
||||
```sh
|
||||
# Port-forward the service
|
||||
oc port-forward svc/docling-serve 5001:5001
|
||||
|
||||
# Make a test query
|
||||
curl -X 'POST' \
|
||||
"localhost:5001/v1alpha/convert/source/async" \
|
||||
-H "accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"http_sources": [{"url": "https://arxiv.org/pdf/2501.17887"}]
|
||||
}'
|
||||
```
|
||||
|
||||
### Secure deployment with `oauth-proxy`
|
||||
|
||||
Manifest example: [docling-serve-oauth.yaml](./deploy-examples/docling-serve-oauth.yaml)
|
||||
@@ -15,7 +169,7 @@ This deployment has the following features:
|
||||
Install the app with:
|
||||
|
||||
```sh
|
||||
kubectl apply -f docs/deploy-examples/docling-serve-oauth.yaml
|
||||
oc apply -f docs/deploy-examples/docling-serve-oauth.yaml
|
||||
```
|
||||
|
||||
For using the API:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "docling-serve"
|
||||
version = "0.7.0" # DO NOT EDIT, updated automatically
|
||||
version = "0.8.0" # DO NOT EDIT, updated automatically
|
||||
description = "Running Docling as a service"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
@@ -30,7 +30,8 @@ classifiers = [
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"docling~=2.28",
|
||||
"docling[vlm]~=2.28",
|
||||
"mlx-vlm~=0.1.12; sys_platform == 'darwin' and platform_machine == 'arm64'",
|
||||
"fastapi[standard]~=0.115",
|
||||
"httpx~=0.28",
|
||||
"pydantic~=2.10",
|
||||
@@ -43,7 +44,8 @@ dependencies = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
ui = [
|
||||
"gradio~=5.9"
|
||||
"gradio~=5.9",
|
||||
"pydantic<2.11.0", # fix compatibility between gradio and new pydantic 2.11
|
||||
]
|
||||
tesserocr = [
|
||||
"tesserocr~=2.7"
|
||||
@@ -195,6 +197,7 @@ module = [
|
||||
"tesserocr.*",
|
||||
"rapidocr_onnxruntime.*",
|
||||
"requests.*",
|
||||
"mlx_vlm.*",
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
|
||||
@@ -92,16 +92,11 @@ async def test_convert_file(async_client):
|
||||
msg=f'JSON document should contain \'{{\\n "schema_name": "DoclingDocument\'". Received: {safe_slice(data["document"]["json_content"])}',
|
||||
)
|
||||
# HTML check
|
||||
check.is_in(
|
||||
"html_content",
|
||||
data.get("document", {}),
|
||||
msg=f"Response should contain 'html_content' key. Received keys: {list(data.get('document', {}).keys())}",
|
||||
)
|
||||
if data.get("document", {}).get("html_content") is not None:
|
||||
check.is_in(
|
||||
'<!DOCTYPE html>\n<html lang="en">\n<head>',
|
||||
"<!DOCTYPE html>\n<html>\n<head>",
|
||||
data["document"]["html_content"],
|
||||
msg=f"HTML document should contain '<!DOCTYPE html>\\n<html lang=\"en'>. Received: {safe_slice(data['document']['html_content'])}",
|
||||
msg=f"HTML document should contain '<!DOCTYPE html>\\n<html>'. Received: {safe_slice(data['document']['html_content'])}",
|
||||
)
|
||||
# Text check
|
||||
check.is_in(
|
||||
@@ -123,7 +118,7 @@ async def test_convert_file(async_client):
|
||||
)
|
||||
if data.get("document", {}).get("doctags_content") is not None:
|
||||
check.is_in(
|
||||
"<document>\n<section_header_level_1><location>",
|
||||
"<doctag><page_header><loc",
|
||||
data["document"]["doctags_content"],
|
||||
msg=f"DocTags document should contain '<document>\\n<section_header_level_1><location>'. Received: {safe_slice(data['document']['doctags_content'])}",
|
||||
msg=f"DocTags document should contain '<doctag><page_header><loc'. Received: {safe_slice(data['document']['doctags_content'])}",
|
||||
)
|
||||
|
||||
@@ -93,9 +93,9 @@ async def test_convert_url(async_client):
|
||||
)
|
||||
if data.get("document", {}).get("html_content") is not None:
|
||||
check.is_in(
|
||||
'<!DOCTYPE html>\n<html lang="en">\n<head>',
|
||||
"<!DOCTYPE html>\n<html>\n<head>",
|
||||
data["document"]["html_content"],
|
||||
msg=f"HTML document should contain '<!DOCTYPE html>\\n<html lang=\"en'>. Received: {safe_slice(data['document']['html_content'])}",
|
||||
msg=f"HTML document should contain '<!DOCTYPE html>\\n<html>'. Received: {safe_slice(data['document']['html_content'])}",
|
||||
)
|
||||
# Text check
|
||||
check.is_in(
|
||||
@@ -117,7 +117,7 @@ async def test_convert_url(async_client):
|
||||
)
|
||||
if data.get("document", {}).get("doctags_content") is not None:
|
||||
check.is_in(
|
||||
"<document>\n<section_header_level_1><location>",
|
||||
"<doctag><page_header><loc",
|
||||
data["document"]["doctags_content"],
|
||||
msg=f"DocTags document should contain '<document>\\n<section_header_level_1><location>'. Received: {safe_slice(data['document']['doctags_content'])}",
|
||||
msg=f"DocTags document should contain '<doctag><page_header><loc'. Received: {safe_slice(data['document']['doctags_content'])}",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user