Michele Dolfi commited on
Commit
e6a25a6
·
unverified ·
1 Parent(s): 503b885

feat: Add new docling-serve cli (#50)

Browse files

Signed-off-by: Michele Dolfi <[email protected]>

.github/workflows/ci-images-dryrun.yml CHANGED
@@ -20,7 +20,7 @@ jobs:
20
  with:
21
  publish: false
22
  build_args: |
23
- CPU_ONLY=true
24
  ghcr_image_name: ds4sd/docling-serve-cpu
25
  quay_image_name: ""
26
 
@@ -37,7 +37,7 @@ jobs:
37
  with:
38
  publish: false
39
  build_args: |
40
- CPU_ONLY=false
41
  platforms: linux/amd64
42
  ghcr_image_name: ds4sd/docling-serve
43
  quay_image_name: ""
 
20
  with:
21
  publish: false
22
  build_args: |
23
+ UV_SYNC_EXTRA_ARGS=--no-extra cu124
24
  ghcr_image_name: ds4sd/docling-serve-cpu
25
  quay_image_name: ""
26
 
 
37
  with:
38
  publish: false
39
  build_args: |
40
+ UV_SYNC_EXTRA_ARGS=--no-extra cpu
41
  platforms: linux/amd64
42
  ghcr_image_name: ds4sd/docling-serve
43
  quay_image_name: ""
.github/workflows/images.yml CHANGED
@@ -34,7 +34,7 @@ jobs:
34
  publish: true
35
  environment: registry-creds
36
  build_args: |
37
- CPU_ONLY=true
38
  ghcr_image_name: ds4sd/docling-serve-cpu
39
  quay_image_name: ds4sd/docling-serve-cpu
40
 
@@ -53,7 +53,7 @@ jobs:
53
  publish: true
54
  environment: registry-creds
55
  build_args: |
56
- CPU_ONLY=false
57
  platforms: linux/amd64
58
  ghcr_image_name: ds4sd/docling-serve
59
  quay_image_name: ds4sd/docling-serve
 
34
  publish: true
35
  environment: registry-creds
36
  build_args: |
37
+ UV_SYNC_EXTRA_ARGS=--no-extra cu124
38
  ghcr_image_name: ds4sd/docling-serve-cpu
39
  quay_image_name: ds4sd/docling-serve-cpu
40
 
 
53
  publish: true
54
  environment: registry-creds
55
  build_args: |
56
+ UV_SYNC_EXTRA_ARGS=--no-extra cpu
57
  platforms: linux/amd64
58
  ghcr_image_name: ds4sd/docling-serve
59
  quay_image_name: ds4sd/docling-serve
.github/workflows/job-image.yml CHANGED
@@ -105,8 +105,6 @@ jobs:
105
  cache-to: type=gha,mode=max
106
  file: Containerfile
107
  build-args: ${{ inputs.build_args }}
108
- # |
109
- # --build-arg CPU_ONLY=true
110
 
111
  - name: Generate artifact attestation
112
  if: ${{ inputs.publish }}
@@ -137,8 +135,6 @@ jobs:
137
  cache-to: type=gha,mode=max
138
  file: Containerfile
139
  build-args: ${{ inputs.build_args }}
140
- # |
141
- # --build-arg CPU_ONLY=true
142
 
143
  - name: Remove Local Docker Images
144
  run: |
 
105
  cache-to: type=gha,mode=max
106
  file: Containerfile
107
  build-args: ${{ inputs.build_args }}
 
 
108
 
109
  - name: Generate artifact attestation
110
  if: ${{ inputs.publish }}
 
135
  cache-to: type=gha,mode=max
136
  file: Containerfile
137
  build-args: ${{ inputs.build_args }}
 
 
138
 
139
  - name: Remove Local Docker Images
140
  run: |
Containerfile CHANGED
@@ -2,8 +2,8 @@ ARG BASE_IMAGE=quay.io/sclorg/python-312-c9s:c9s
2
 
3
  FROM ${BASE_IMAGE}
4
 
5
- ARG CPU_ONLY=false
6
  ARG MODELS_LIST="layout tableformer picture_classifier easyocr"
 
7
 
8
  USER 0
9
 
@@ -41,17 +41,10 @@ ENV PYTHONIOENCODING=utf-8
41
  ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
42
  ENV UV_PROJECT_ENVIRONMENT=/opt/app-root
43
 
44
- ENV WITH_UI=True
45
-
46
  COPY --chown=1001:0 pyproject.toml uv.lock README.md ./
47
 
48
  RUN --mount=type=cache,target=/opt/app-root/src/.cache/uv,uid=1001 \
49
- if [ "$CPU_ONLY" = "true" ]; then \
50
- NO_EXTRA=cu124; \
51
- else \
52
- NO_EXTRA=cpu; \
53
- fi && \
54
- uv sync --frozen --no-install-project --no-dev --all-extras --no-extra ${NO_EXTRA}
55
 
56
  RUN echo "Downloading models..." && \
57
  docling-tools models download ${MODELS_LIST} && \
@@ -59,8 +52,9 @@ RUN echo "Downloading models..." && \
59
  chmod -R g=u /opt/app-root/src/.cache
60
 
61
  COPY --chown=1001:0 --chmod=664 ./docling_serve ./docling_serve
62
-
 
63
 
64
  EXPOSE 5001
65
 
66
- CMD ["python", "-m", "docling_serve"]
 
2
 
3
  FROM ${BASE_IMAGE}
4
 
 
5
  ARG MODELS_LIST="layout tableformer picture_classifier easyocr"
6
+ ARG UV_SYNC_EXTRA_ARGS=""
7
 
8
  USER 0
9
 
 
41
  ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
42
  ENV UV_PROJECT_ENVIRONMENT=/opt/app-root
43
 
 
 
44
  COPY --chown=1001:0 pyproject.toml uv.lock README.md ./
45
 
46
  RUN --mount=type=cache,target=/opt/app-root/src/.cache/uv,uid=1001 \
47
+ uv sync --frozen --no-install-project --no-dev --all-extras ${UV_SYNC_EXTRA_ARGS} # --no-extra ${NO_EXTRA}
 
 
 
 
 
48
 
49
  RUN echo "Downloading models..." && \
50
  docling-tools models download ${MODELS_LIST} && \
 
52
  chmod -R g=u /opt/app-root/src/.cache
53
 
54
  COPY --chown=1001:0 --chmod=664 ./docling_serve ./docling_serve
55
+ RUN --mount=type=cache,target=/opt/app-root/src/.cache/uv,uid=1001 \
56
+ uv sync --frozen --no-dev --all-extras ${UV_SYNC_EXTRA_ARGS} # --no-extra ${NO_EXTRA}
57
 
58
  EXPOSE 5001
59
 
60
+ CMD ["docling-serve", "run"]
Makefile CHANGED
@@ -26,15 +26,15 @@ md-lint-file:
26
 
27
  .PHONY: docling-serve-cpu-image
28
  docling-serve-cpu-image: Containerfile ## Build docling-serve "cpu only" container image
29
- $(ECHO_PREFIX) printf " %-12s Containerfile\n" "[docling-serve CPU ONLY]"
30
- $(CMD_PREFIX) docker build --build-arg CPU_ONLY=true -f Containerfile --platform linux/amd64 -t ghcr.io/ds4sd/docling-serve-cpu:$(TAG) .
31
  $(CMD_PREFIX) docker tag ghcr.io/ds4sd/docling-serve-cpu:$(TAG) ghcr.io/ds4sd/docling-serve-cpu:main
32
  $(CMD_PREFIX) docker tag ghcr.io/ds4sd/docling-serve-cpu:$(TAG) quay.io/ds4sd/docling-serve-cpu:main
33
 
34
  .PHONY: docling-serve-gpu-image
35
  docling-serve-gpu-image: Containerfile ## Build docling-serve container image with GPU support
36
  $(ECHO_PREFIX) printf " %-12s Containerfile\n" "[docling-serve with GPU]"
37
- $(CMD_PREFIX) docker build --build-arg CPU_ONLY=false -f Containerfile --platform linux/amd64 -t ghcr.io/ds4sd/docling-serve:$(TAG) .
38
  $(CMD_PREFIX) docker tag ghcr.io/ds4sd/docling-serve:$(TAG) ghcr.io/ds4sd/docling-serve:main
39
  $(CMD_PREFIX) docker tag ghcr.io/ds4sd/docling-serve:$(TAG) quay.io/ds4sd/docling-serve:main
40
 
 
26
 
27
  .PHONY: docling-serve-cpu-image
28
  docling-serve-cpu-image: Containerfile ## Build docling-serve "cpu only" container image
29
+ $(ECHO_PREFIX) printf " %-12s Containerfile\n" "[docling-serve CPU]"
30
+ $(CMD_PREFIX) docker build --load --build-arg "UV_SYNC_EXTRA_ARGS=--no-extra cu124" -f Containerfile -t ghcr.io/ds4sd/docling-serve-cpu:$(TAG) .
31
  $(CMD_PREFIX) docker tag ghcr.io/ds4sd/docling-serve-cpu:$(TAG) ghcr.io/ds4sd/docling-serve-cpu:main
32
  $(CMD_PREFIX) docker tag ghcr.io/ds4sd/docling-serve-cpu:$(TAG) quay.io/ds4sd/docling-serve-cpu:main
33
 
34
  .PHONY: docling-serve-gpu-image
35
  docling-serve-gpu-image: Containerfile ## Build docling-serve container image with GPU support
36
  $(ECHO_PREFIX) printf " %-12s Containerfile\n" "[docling-serve with GPU]"
37
+ $(CMD_PREFIX) docker build --load --build-arg "UV_SYNC_EXTRA_ARGS=--no-extra cpu" -f Containerfile --platform linux/amd64 -t ghcr.io/ds4sd/docling-serve:$(TAG) .
38
  $(CMD_PREFIX) docker tag ghcr.io/ds4sd/docling-serve:$(TAG) ghcr.io/ds4sd/docling-serve:main
39
  $(CMD_PREFIX) docker tag ghcr.io/ds4sd/docling-serve:$(TAG) quay.io/ds4sd/docling-serve:main
40
 
README.md CHANGED
@@ -327,25 +327,83 @@ See `[project.optional-dependencies]` section in `pyproject.toml` for full list
327
 
328
  ### Run the server
329
 
330
- The [start_server.sh](./start_server.sh) executable is a convenient script for launching the local webserver.
 
331
 
332
  ```sh
333
- # Run the server
334
- bash start_server.sh
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
- # Run the server with live reload
337
- RELOAD=true bash start_server.sh
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  ```
339
 
340
- ### Environment variables
 
 
 
 
 
 
 
341
 
342
- The following variables are available:
 
 
 
 
 
343
 
344
  - `DOCLING_ARTIFACTS_PATH`: if set Docling will use only the local weights of models, for example `/opt/app-root/.cache/docling/cache`.
345
  - `TESSDATA_PREFIX`: Tesseract data location, example `/usr/share/tesseract/tessdata/`.
346
- - `UVICORN_WORKERS`: Number of workers to use.
347
- - `RELOAD`: If `True`, this will enable auto-reload when you modify files, useful for development.
348
- - `WITH_UI`: If `True`, The Gradio UI will be available at `/ui`.
349
 
350
  ## Get help and support
351
 
 
327
 
328
  ### Run the server
329
 
330
+ The `docling-serve` executable is a convenient script for launching the webserver both in
331
+ development and production mode.
332
 
333
  ```sh
334
+ # Run the server in development mode
335
+ # - reload is enabled by default
336
+ # - listening on the 127.0.0.1 address
337
+ # - ui is enabled by default
338
+ docling-serve dev
339
+
340
+ # Run the server in production mode
341
+ # - reload is disabled by default
342
+ # - listening on the 0.0.0.0 address
343
+ # - ui is disabled by default
344
+ docling-serve run
345
+ ```
346
+
347
+ ### Options
348
 
349
+ The `docling-serve` executable allows is controlled with both command line
350
+ options and environment variables.
351
+
352
+ <details>
353
+ <summary>`docling-serve` help message</summary>
354
+
355
+ ```sh
356
+ $ docling-serve dev --help
357
+
358
+ Usage: docling-serve dev [OPTIONS]
359
+
360
+ Run a Docling Serve app in development mode. 🧪
361
+ This is equivalent to docling-serve run but with reload
362
+ enabled and listening on the 127.0.0.1 address.
363
+
364
+ Options can be set also with the corresponding ENV variable, with the exception
365
+ of --enable-ui, --host and --reload.
366
+
367
+ ╭─ Options ──────────────────────────────────────────────────────────────────────────────────────────────────╮
368
+ │ --host TEXT The host to serve on. For local development in localhost │
369
+ │ use 127.0.0.1. To enable public access, e.g. in a │
370
+ │ container, use all the IP addresses available with │
371
+ │ 0.0.0.0. │
372
+ │ [default: 127.0.0.1] │
373
+ │ --port INTEGER The port to serve on. [default: 5001] │
374
+ │ --reload --no-reload Enable auto-reload of the server when (code) files │
375
+ │ change. This is resource intensive, use it only during │
376
+ │ development. │
377
+ │ [default: reload] │
378
+ │ --root-path TEXT The root path is used to tell your app that it is being │
379
+ │ served to the outside world with some path prefix set up │
380
+ │ in some termination proxy or similar. │
381
+ │ --proxy-headers --no-proxy-headers Enable/Disable X-Forwarded-Proto, X-Forwarded-For, │
382
+ │ X-Forwarded-Port to populate remote address info. │
383
+ │ [default: proxy-headers] │
384
+ │ --enable-ui --no-enable-ui Enable the development UI. [default: enable-ui] │
385
+ │ --help Show this message and exit. │
386
+ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
387
  ```
388
 
389
+ </details>
390
+
391
+ #### Environment variables
392
+
393
+ The environment variables controlling the `uvicorn` execution can be specified with the `UVICORN_` prefix:
394
+
395
+ - `UVICORN_WORKERS`: Number of workers to use.
396
+ - `UVICORN_RELOAD`: If `True`, this will enable auto-reload when you modify files, useful for development.
397
 
398
+ The environment variables controlling specifics of the Docling Serve app can be specified with the
399
+ `DOCLING_SERVE_` prefix:
400
+
401
+ - `DOCLING_SERVE_ENABLE_UI`: If `True`, The Gradio UI will be available at `/ui`.
402
+
403
+ Others:
404
 
405
  - `DOCLING_ARTIFACTS_PATH`: if set Docling will use only the local weights of models, for example `/opt/app-root/.cache/docling/cache`.
406
  - `TESSDATA_PREFIX`: Tesseract data location, example `/usr/share/tesseract/tessdata/`.
 
 
 
407
 
408
  ## Get help and support
409
 
docling_serve/.env.example CHANGED
@@ -1,3 +1,3 @@
1
  TESSDATA_PREFIX=/usr/share/tesseract/tessdata/
2
  UVICORN_WORKERS=2
3
- RELOAD=True
 
1
  TESSDATA_PREFIX=/usr/share/tesseract/tessdata/
2
  UVICORN_WORKERS=2
3
+ UVICORN_RELOAD=True
docling_serve/__main__.py CHANGED
@@ -1,20 +1,281 @@
1
- import os
 
 
 
 
 
2
 
3
- from docling_serve.helper_functions import _str_to_bool
 
 
4
 
5
- # Launch the FastAPI server
6
- if __name__ == "__main__":
7
- from uvicorn import run
8
-
9
- port = int(os.getenv("PORT", "5001"))
10
- workers = int(os.getenv("UVICORN_WORKERS", "1"))
11
- reload = _str_to_bool(os.getenv("RELOAD", "False"))
12
-
13
- run(
14
- "docling_serve.app:app",
15
- host="0.0.0.0",
16
- port=port,
17
- workers=workers,
18
- timeout_keep_alive=600,
19
- reload=reload,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  )
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib
2
+ import logging
3
+ import platform
4
+ import sys
5
+ import warnings
6
+ from typing import Annotated, Any, Union
7
 
8
+ import typer
9
+ import uvicorn
10
+ from rich.console import Console
11
 
12
+ from docling_serve.settings import docling_serve_settings, uvicorn_settings
13
+
14
+ warnings.filterwarnings(action="ignore", category=UserWarning, module="pydantic|torch")
15
+ warnings.filterwarnings(action="ignore", category=FutureWarning, module="easyocr")
16
+
17
+
18
+ err_console = Console(stderr=True)
19
+ console = Console()
20
+
21
+ app = typer.Typer(
22
+ no_args_is_help=True,
23
+ rich_markup_mode="rich",
24
+ )
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def version_callback(value: bool) -> None:
30
+ if value:
31
+ docling_serve_version = importlib.metadata.version("docling_serve")
32
+ docling_version = importlib.metadata.version("docling")
33
+ docling_core_version = importlib.metadata.version("docling-core")
34
+ docling_ibm_models_version = importlib.metadata.version("docling-ibm-models")
35
+ docling_parse_version = importlib.metadata.version("docling-parse")
36
+ platform_str = platform.platform()
37
+ py_impl_version = sys.implementation.cache_tag
38
+ py_lang_version = platform.python_version()
39
+ console.print(f"Docling Serve version: {docling_serve_version}")
40
+ console.print(f"Docling version: {docling_version}")
41
+ console.print(f"Docling Core version: {docling_core_version}")
42
+ console.print(f"Docling IBM Models version: {docling_ibm_models_version}")
43
+ console.print(f"Docling Parse version: {docling_parse_version}")
44
+ console.print(f"Python: {py_impl_version} ({py_lang_version})")
45
+ console.print(f"Platform: {platform_str}")
46
+ raise typer.Exit()
47
+
48
+
49
+ @app.callback()
50
+ def callback(
51
+ version: Annotated[
52
+ Union[bool, None],
53
+ typer.Option(
54
+ "--version", help="Show the version and exit.", callback=version_callback
55
+ ),
56
+ ] = None,
57
+ verbose: Annotated[
58
+ int,
59
+ typer.Option(
60
+ "--verbose",
61
+ "-v",
62
+ count=True,
63
+ help="Set the verbosity level. -v for info logging, -vv for debug logging.",
64
+ ),
65
+ ] = 0,
66
+ ) -> None:
67
+ if verbose == 0:
68
+ logging.basicConfig(level=logging.WARNING)
69
+ elif verbose == 1:
70
+ logging.basicConfig(level=logging.INFO)
71
+ elif verbose == 2:
72
+ logging.basicConfig(level=logging.DEBUG)
73
+
74
+
75
+ def _run(
76
+ *,
77
+ command: str,
78
+ ) -> None:
79
+ server_type = "development" if command == "dev" else "production"
80
+
81
+ console.print(f"Starting {server_type} server 🚀")
82
+
83
+ url = f"http://{uvicorn_settings.host}:{uvicorn_settings.port}"
84
+ url_docs = f"{url}/docs"
85
+ url_ui = f"{url}/ui"
86
+
87
+ console.print("")
88
+ console.print(f"Server started at [link={url}]{url}[/]")
89
+ console.print(f"Documentation at [link={url_docs}]{url_docs}[/]")
90
+ if docling_serve_settings.enable_ui:
91
+ console.print(f"UI at [link={url_ui}]{url_ui}[/]")
92
+
93
+ if command == "dev":
94
+ console.print("")
95
+ console.print(
96
+ "Running in development mode, for production use: "
97
+ "[bold]docling-serve run[/]",
98
+ )
99
+
100
+ console.print("")
101
+ console.print("Logs:")
102
+
103
+ uvicorn.run(
104
+ app="docling_serve.app:create_app",
105
+ factory=True,
106
+ host=uvicorn_settings.host,
107
+ port=uvicorn_settings.port,
108
+ reload=uvicorn_settings.reload,
109
+ workers=uvicorn_settings.workers,
110
+ root_path=uvicorn_settings.root_path,
111
+ proxy_headers=uvicorn_settings.proxy_headers,
112
+ )
113
+
114
+
115
+ @app.command()
116
+ def dev(
117
+ *,
118
+ # uvicorn options
119
+ host: Annotated[
120
+ str,
121
+ typer.Option(
122
+ help=(
123
+ "The host to serve on. For local development in localhost "
124
+ "use [blue]127.0.0.1[/blue]. To enable public access, "
125
+ "e.g. in a container, use all the IP addresses "
126
+ "available with [blue]0.0.0.0[/blue]."
127
+ )
128
+ ),
129
+ ] = "127.0.0.1",
130
+ port: Annotated[
131
+ int,
132
+ typer.Option(help="The port to serve on."),
133
+ ] = uvicorn_settings.port,
134
+ reload: Annotated[
135
+ bool,
136
+ typer.Option(
137
+ help=(
138
+ "Enable auto-reload of the server when (code) files change. "
139
+ "This is [bold]resource intensive[/bold], "
140
+ "use it only during development."
141
+ )
142
+ ),
143
+ ] = True,
144
+ root_path: Annotated[
145
+ str,
146
+ typer.Option(
147
+ help=(
148
+ "The root path is used to tell your app that it is being served "
149
+ "to the outside world with some [bold]path prefix[/bold] "
150
+ "set up in some termination proxy or similar."
151
+ )
152
+ ),
153
+ ] = uvicorn_settings.root_path,
154
+ proxy_headers: Annotated[
155
+ bool,
156
+ typer.Option(
157
+ help=(
158
+ "Enable/Disable X-Forwarded-Proto, X-Forwarded-For, "
159
+ "X-Forwarded-Port to populate remote address info."
160
+ )
161
+ ),
162
+ ] = uvicorn_settings.proxy_headers,
163
+ # docling options
164
+ enable_ui: Annotated[bool, typer.Option(help="Enable the development UI.")] = True,
165
+ ) -> Any:
166
+ """
167
+ Run a [bold]Docling Serve[/bold] app in [yellow]development[/yellow] mode. 🧪
168
+
169
+ This is equivalent to [bold]docling-serve run[/bold] but with [bold]reload[/bold]
170
+ enabled and listening on the [blue]127.0.0.1[/blue] address.
171
+
172
+ Options can be set also with the corresponding ENV variable, with the exception
173
+ of --enable-ui, --host and --reload.
174
+ """
175
+
176
+ uvicorn_settings.host = host
177
+ uvicorn_settings.port = port
178
+ uvicorn_settings.reload = reload
179
+ uvicorn_settings.root_path = root_path
180
+ uvicorn_settings.proxy_headers = proxy_headers
181
+
182
+ docling_serve_settings.enable_ui = enable_ui
183
+
184
+ _run(
185
+ command="dev",
186
+ )
187
+
188
+
189
+ @app.command()
190
+ def run(
191
+ *,
192
+ host: Annotated[
193
+ str,
194
+ typer.Option(
195
+ help=(
196
+ "The host to serve on. For local development in localhost "
197
+ "use [blue]127.0.0.1[/blue]. To enable public access, "
198
+ "e.g. in a container, use all the IP addresses "
199
+ "available with [blue]0.0.0.0[/blue]."
200
+ )
201
+ ),
202
+ ] = uvicorn_settings.host,
203
+ port: Annotated[
204
+ int,
205
+ typer.Option(help="The port to serve on."),
206
+ ] = uvicorn_settings.port,
207
+ reload: Annotated[
208
+ bool,
209
+ typer.Option(
210
+ help=(
211
+ "Enable auto-reload of the server when (code) files change. "
212
+ "This is [bold]resource intensive[/bold], "
213
+ "use it only during development."
214
+ )
215
+ ),
216
+ ] = uvicorn_settings.reload,
217
+ workers: Annotated[
218
+ Union[int, None],
219
+ typer.Option(
220
+ help=(
221
+ "Use multiple worker processes. "
222
+ "Mutually exclusive with the --reload flag."
223
+ )
224
+ ),
225
+ ] = uvicorn_settings.workers,
226
+ root_path: Annotated[
227
+ str,
228
+ typer.Option(
229
+ help=(
230
+ "The root path is used to tell your app that it is being served "
231
+ "to the outside world with some [bold]path prefix[/bold] "
232
+ "set up in some termination proxy or similar."
233
+ )
234
+ ),
235
+ ] = uvicorn_settings.root_path,
236
+ proxy_headers: Annotated[
237
+ bool,
238
+ typer.Option(
239
+ help=(
240
+ "Enable/Disable X-Forwarded-Proto, X-Forwarded-For, "
241
+ "X-Forwarded-Port to populate remote address info."
242
+ )
243
+ ),
244
+ ] = uvicorn_settings.proxy_headers,
245
+ # docling options
246
+ enable_ui: Annotated[
247
+ bool, typer.Option(help="Enable the development UI.")
248
+ ] = docling_serve_settings.enable_ui,
249
+ ) -> Any:
250
+ """
251
+ Run a [bold]Docling Serve[/bold] app in [green]production[/green] mode. 🚀
252
+
253
+ This is equivalent to [bold]docling-serve dev[/bold] but with [bold]reload[/bold]
254
+ disabled and listening on the [blue]0.0.0.0[/blue] address.
255
+
256
+ Options can be set also with the corresponding ENV variable, e.g. UVICORN_PORT
257
+ or DOCLING_SERVE_ENABLE_UI.
258
+ """
259
+
260
+ uvicorn_settings.host = host
261
+ uvicorn_settings.port = port
262
+ uvicorn_settings.reload = reload
263
+ uvicorn_settings.workers = workers
264
+ uvicorn_settings.root_path = root_path
265
+ uvicorn_settings.proxy_headers = proxy_headers
266
+
267
+ docling_serve_settings.enable_ui = enable_ui
268
+
269
+ _run(
270
+ command="run",
271
  )
272
+
273
+
274
+ def main() -> None:
275
+ app()
276
+
277
+
278
+ # Launch the CLI when calling python -m docling_serve
279
+ if __name__ == "__main__":
280
+
281
+ main()
docling_serve/app.py CHANGED
@@ -1,5 +1,4 @@
1
  import logging
2
- import os
3
  import tempfile
4
  from contextlib import asynccontextmanager
5
  from io import BytesIO
@@ -8,7 +7,6 @@ from typing import Annotated, Any, Dict, List, Optional, Union
8
 
9
  from docling.datamodel.base_models import DocumentStream, InputFormat
10
  from docling.document_converter import DocumentConverter
11
- from dotenv import load_dotenv
12
  from fastapi import BackgroundTasks, FastAPI, UploadFile
13
  from fastapi.middleware.cors import CORSMiddleware
14
  from fastapi.responses import RedirectResponse
@@ -22,17 +20,9 @@ from docling_serve.docling_conversion import (
22
  converters,
23
  get_pdf_pipeline_opts,
24
  )
25
- from docling_serve.helper_functions import FormDepends, _str_to_bool
26
  from docling_serve.response_preparation import ConvertDocumentResponse, process_results
27
-
28
- # Load local env vars if present
29
- load_dotenv()
30
-
31
- WITH_UI = _str_to_bool(os.getenv("WITH_UI", "False"))
32
- if WITH_UI:
33
- import gradio as gr
34
-
35
- from docling_serve.gradio_ui import ui as gradio_ui
36
 
37
 
38
  # Set up custom logging as we'll be intermixes with FastAPI/Uvicorn's logging
@@ -70,7 +60,6 @@ _log = logging.getLogger(__name__)
70
  # Context manager to initialize and clean up the lifespan of the FastAPI app
71
  @asynccontextmanager
72
  async def lifespan(app: FastAPI):
73
- # settings = Settings()
74
 
75
  # Converter with default options
76
  pdf_format_option, options_hash = get_pdf_pipeline_opts(ConvertDocumentsOptions())
@@ -86,143 +75,156 @@ async def lifespan(app: FastAPI):
86
  yield
87
 
88
  converters.clear()
89
- if WITH_UI:
90
- gradio_ui.close()
91
 
92
 
93
  ##################################
94
  # App creation and configuration #
95
  ##################################
96
 
97
- app = FastAPI(
98
- title="Docling Serve",
99
- lifespan=lifespan,
100
- )
101
-
102
- origins = ["*"]
103
- methods = ["*"]
104
- headers = ["*"]
105
-
106
- app.add_middleware(
107
- CORSMiddleware,
108
- allow_origins=origins,
109
- allow_credentials=True,
110
- allow_methods=methods,
111
- allow_headers=headers,
112
- )
113
 
114
- # Mount the Gradio app
115
- if WITH_UI:
116
- tmp_output_dir = Path(tempfile.mkdtemp())
117
- gradio_ui.gradio_output_dir = tmp_output_dir
118
- app = gr.mount_gradio_app(
119
- app,
120
- gradio_ui,
121
- path="/ui",
122
- allowed_paths=["./logo.png", tmp_output_dir],
123
- root_path="/ui",
124
  )
125
 
 
 
 
126
 
127
- #############################
128
- # API Endpoints definitions #
129
- #############################
130
-
131
-
132
- # Favicon
133
- @app.get("/favicon.ico", include_in_schema=False)
134
- async def favicon():
135
- response = RedirectResponse(url="https://ds4sd.github.io/docling/assets/logo.png")
136
- return response
137
-
138
-
139
- # Status
140
- class HealthCheckResponse(BaseModel):
141
- status: str = "ok"
142
-
143
-
144
- @app.get("/health")
145
- def health() -> HealthCheckResponse:
146
- return HealthCheckResponse()
147
-
148
-
149
- # API readiness compatibility for OpenShift AI Workbench
150
- @app.get("/api", include_in_schema=False)
151
- def api_check() -> HealthCheckResponse:
152
- return HealthCheckResponse()
153
-
154
-
155
- # Convert a document from URL(s)
156
- @app.post(
157
- "/v1alpha/convert/source",
158
- response_model=ConvertDocumentResponse,
159
- responses={
160
- 200: {
161
- "content": {"application/zip": {}},
162
- # "description": "Return the JSON item or an image.",
163
- }
164
- },
165
- )
166
- def process_url(
167
- background_tasks: BackgroundTasks, conversion_request: ConvertDocumentsRequest
168
- ):
169
- sources: List[Union[str, DocumentStream]] = []
170
- headers: Optional[Dict[str, Any]] = None
171
- if isinstance(conversion_request, ConvertDocumentFileSourcesRequest):
172
- for file_source in conversion_request.file_sources:
173
- sources.append(file_source.to_document_stream())
174
- else:
175
- for http_source in conversion_request.http_sources:
176
- sources.append(http_source.url)
177
- if headers is None and http_source.headers:
178
- headers = http_source.headers
179
-
180
- # Note: results are only an iterator->lazy evaluation
181
- results = convert_documents(
182
- sources=sources, options=conversion_request.options, headers=headers
183
  )
184
 
185
- # The real processing will happen here
186
- response = process_results(
187
- background_tasks=background_tasks,
188
- conversion_options=conversion_request.options,
189
- conv_results=results,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  )
191
-
192
- return response
193
-
194
-
195
- # Convert a document from file(s)
196
- @app.post(
197
- "/v1alpha/convert/file",
198
- response_model=ConvertDocumentResponse,
199
- responses={
200
- 200: {
201
- "content": {"application/zip": {}},
202
- }
203
- },
204
- )
205
- async def process_file(
206
- background_tasks: BackgroundTasks,
207
- files: List[UploadFile],
208
- options: Annotated[ConvertDocumentsOptions, FormDepends(ConvertDocumentsOptions)],
209
- ):
210
-
211
- _log.info(f"Received {len(files)} files for processing.")
212
-
213
- # Load the uploaded files to Docling DocumentStream
214
- file_sources = []
215
- for file in files:
216
- buf = BytesIO(file.file.read())
217
- name = file.filename if file.filename else "file.pdf"
218
- file_sources.append(DocumentStream(name=name, stream=buf))
219
-
220
- results = convert_documents(sources=file_sources, options=options)
221
-
222
- response = process_results(
223
- background_tasks=background_tasks,
224
- conversion_options=options,
225
- conv_results=results,
 
 
226
  )
227
-
228
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import logging
 
2
  import tempfile
3
  from contextlib import asynccontextmanager
4
  from io import BytesIO
 
7
 
8
  from docling.datamodel.base_models import DocumentStream, InputFormat
9
  from docling.document_converter import DocumentConverter
 
10
  from fastapi import BackgroundTasks, FastAPI, UploadFile
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from fastapi.responses import RedirectResponse
 
20
  converters,
21
  get_pdf_pipeline_opts,
22
  )
23
+ from docling_serve.helper_functions import FormDepends
24
  from docling_serve.response_preparation import ConvertDocumentResponse, process_results
25
+ from docling_serve.settings import docling_serve_settings
 
 
 
 
 
 
 
 
26
 
27
 
28
  # Set up custom logging as we'll be intermixes with FastAPI/Uvicorn's logging
 
60
  # Context manager to initialize and clean up the lifespan of the FastAPI app
61
  @asynccontextmanager
62
  async def lifespan(app: FastAPI):
 
63
 
64
  # Converter with default options
65
  pdf_format_option, options_hash = get_pdf_pipeline_opts(ConvertDocumentsOptions())
 
75
  yield
76
 
77
  converters.clear()
78
+ # if WITH_UI:
79
+ # gradio_ui.close()
80
 
81
 
82
  ##################################
83
  # App creation and configuration #
84
  ##################################
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
+ def create_app():
88
+ app = FastAPI(
89
+ title="Docling Serve",
90
+ lifespan=lifespan,
 
 
 
 
 
 
91
  )
92
 
93
+ origins = ["*"]
94
+ methods = ["*"]
95
+ headers = ["*"]
96
 
97
+ app.add_middleware(
98
+ CORSMiddleware,
99
+ allow_origins=origins,
100
+ allow_credentials=True,
101
+ allow_methods=methods,
102
+ allow_headers=headers,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  )
104
 
105
+ # Mount the Gradio app
106
+ if docling_serve_settings.enable_ui:
107
+
108
+ try:
109
+ import gradio as gr
110
+
111
+ from docling_serve.gradio_ui import ui as gradio_ui
112
+
113
+ tmp_output_dir = Path(tempfile.mkdtemp())
114
+ gradio_ui.gradio_output_dir = tmp_output_dir
115
+ app = gr.mount_gradio_app(
116
+ app,
117
+ gradio_ui,
118
+ path="/ui",
119
+ allowed_paths=["./logo.png", tmp_output_dir],
120
+ root_path="/ui",
121
+ )
122
+ except ImportError:
123
+ _log.warning(
124
+ "Docling Serve enable_ui is activated, but gradio is not installed. "
125
+ "Install it with `pip install docling-serve[ui]` "
126
+ "or `pip install gradio`"
127
+ )
128
+
129
+ #############################
130
+ # API Endpoints definitions #
131
+ #############################
132
+
133
+ # Favicon
134
+ @app.get("/favicon.ico", include_in_schema=False)
135
+ async def favicon():
136
+ response = RedirectResponse(
137
+ url="https://ds4sd.github.io/docling/assets/logo.png"
138
+ )
139
+ return response
140
+
141
+ # Status
142
+ class HealthCheckResponse(BaseModel):
143
+ status: str = "ok"
144
+
145
+ @app.get("/health")
146
+ def health() -> HealthCheckResponse:
147
+ return HealthCheckResponse()
148
+
149
+ # API readiness compatibility for OpenShift AI Workbench
150
+ @app.get("/api", include_in_schema=False)
151
+ def api_check() -> HealthCheckResponse:
152
+ return HealthCheckResponse()
153
+
154
+ # Convert a document from URL(s)
155
+ @app.post(
156
+ "/v1alpha/convert/source",
157
+ response_model=ConvertDocumentResponse,
158
+ responses={
159
+ 200: {
160
+ "content": {"application/zip": {}},
161
+ # "description": "Return the JSON item or an image.",
162
+ }
163
+ },
164
  )
165
+ def process_url(
166
+ background_tasks: BackgroundTasks, conversion_request: ConvertDocumentsRequest
167
+ ):
168
+ sources: List[Union[str, DocumentStream]] = []
169
+ headers: Optional[Dict[str, Any]] = None
170
+ if isinstance(conversion_request, ConvertDocumentFileSourcesRequest):
171
+ for file_source in conversion_request.file_sources:
172
+ sources.append(file_source.to_document_stream())
173
+ else:
174
+ for http_source in conversion_request.http_sources:
175
+ sources.append(http_source.url)
176
+ if headers is None and http_source.headers:
177
+ headers = http_source.headers
178
+
179
+ # Note: results are only an iterator->lazy evaluation
180
+ results = convert_documents(
181
+ sources=sources, options=conversion_request.options, headers=headers
182
+ )
183
+
184
+ # The real processing will happen here
185
+ response = process_results(
186
+ background_tasks=background_tasks,
187
+ conversion_options=conversion_request.options,
188
+ conv_results=results,
189
+ )
190
+
191
+ return response
192
+
193
+ # Convert a document from file(s)
194
+ @app.post(
195
+ "/v1alpha/convert/file",
196
+ response_model=ConvertDocumentResponse,
197
+ responses={
198
+ 200: {
199
+ "content": {"application/zip": {}},
200
+ }
201
+ },
202
  )
203
+ async def process_file(
204
+ background_tasks: BackgroundTasks,
205
+ files: List[UploadFile],
206
+ options: Annotated[
207
+ ConvertDocumentsOptions, FormDepends(ConvertDocumentsOptions)
208
+ ],
209
+ ):
210
+
211
+ _log.info(f"Received {len(files)} files for processing.")
212
+
213
+ # Load the uploaded files to Docling DocumentStream
214
+ file_sources = []
215
+ for file in files:
216
+ buf = BytesIO(file.file.read())
217
+ name = file.filename if file.filename else "file.pdf"
218
+ file_sources.append(DocumentStream(name=name, stream=buf))
219
+
220
+ results = convert_documents(sources=file_sources, options=options)
221
+
222
+ response = process_results(
223
+ background_tasks=background_tasks,
224
+ conversion_options=options,
225
+ conv_results=results,
226
+ )
227
+
228
+ return response
229
+
230
+ return app
docling_serve/settings.py CHANGED
@@ -1,6 +1,28 @@
 
 
1
  from pydantic_settings import BaseSettings, SettingsConfigDict
2
 
3
 
4
- class Settings(BaseSettings):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- model_config = SettingsConfigDict(env_prefix="DOCLING_")
 
 
1
+ from typing import Union
2
+
3
  from pydantic_settings import BaseSettings, SettingsConfigDict
4
 
5
 
6
+ class UvicornSettings(BaseSettings):
7
+ model_config = SettingsConfigDict(
8
+ env_prefix="UVICORN_", env_file=".env", extra="allow"
9
+ )
10
+
11
+ host: str = "0.0.0.0"
12
+ port: int = 5001
13
+ reload: bool = False
14
+ root_path: str = ""
15
+ proxy_headers: bool = True
16
+ workers: Union[int, None] = None
17
+
18
+
19
+ class DoclingServeSettings(BaseSettings):
20
+ model_config = SettingsConfigDict(
21
+ env_prefix="DOCLING_SERVE_", env_file=".env", extra="allow"
22
+ )
23
+
24
+ enable_ui: bool = False
25
+
26
 
27
+ uvicorn_settings = UvicornSettings()
28
+ docling_serve_settings = DoclingServeSettings()
pyproject.toml CHANGED
@@ -36,6 +36,7 @@ dependencies = [
36
  "pydantic~=2.10",
37
  "pydantic-settings~=2.4",
38
  "python-multipart>=0.0.14,<0.1.0",
 
39
  "uvicorn[standard]>=0.29.0,<1.0.0",
40
  ]
41
 
@@ -74,6 +75,7 @@ dev = [
74
  ]
75
 
76
  [tool.uv]
 
77
  conflicts = [
78
  [
79
  { extra = "cpu" },
@@ -104,6 +106,9 @@ explicit = true
104
  [tool.setuptools.packages.find]
105
  include = ["docling_serve"]
106
 
 
 
 
107
  [project.urls]
108
  Homepage = "https://github.com/DS4SD/docling-serve"
109
  # Documentation = "https://ds4sd.github.io/docling"
@@ -118,6 +123,7 @@ include = '\.pyi?$'
118
 
119
  [tool.isort]
120
  profile = "black"
 
121
  line_length = 88
122
  py_version=311
123
 
 
36
  "pydantic~=2.10",
37
  "pydantic-settings~=2.4",
38
  "python-multipart>=0.0.14,<0.1.0",
39
+ "typer~=0.12",
40
  "uvicorn[standard]>=0.29.0,<1.0.0",
41
  ]
42
 
 
75
  ]
76
 
77
  [tool.uv]
78
+ package = true
79
  conflicts = [
80
  [
81
  { extra = "cpu" },
 
106
  [tool.setuptools.packages.find]
107
  include = ["docling_serve"]
108
 
109
+ [project.scripts]
110
+ docling-serve = "docling_serve.__main__:main"
111
+
112
  [project.urls]
113
  Homepage = "https://github.com/DS4SD/docling-serve"
114
  # Documentation = "https://ds4sd.github.io/docling"
 
123
 
124
  [tool.isort]
125
  profile = "black"
126
+ known_third_party = ["docling", "docling_core"]
127
  line_length = 88
128
  py_version=311
129
 
start_server.sh DELETED
@@ -1,30 +0,0 @@
1
- #!/bin/bash
2
- set -Eeuo pipefail
3
-
4
- # Network settings
5
- export PORT="${PORT:-5001}"
6
- export HOST="${HOST:-"0.0.0.0"}"
7
-
8
- # Performance settings
9
- UVICORN_WORKERS="${UVICORN_WORKERS:-1}"
10
-
11
- # Development settings
12
- export WITH_UI="${WITH_UI:-"true"}"
13
- export RELOAD=${RELOAD:-"false"}
14
-
15
- # --------------------------------------
16
- # Process env settings
17
-
18
- EXTRA_ARGS=""
19
- if [ "$RELOAD" == "true" ]; then
20
- EXTRA_ARGS="$EXTRA_ARGS --reload"
21
- fi
22
-
23
- # Launch
24
- exec uv run uvicorn \
25
- docling_serve.app:app \
26
- --host=${HOST} \
27
- --port=${PORT} \
28
- --timeout-keep-alive=600 \
29
- ${EXTRA_ARGS} \
30
- --workers=${UVICORN_WORKERS}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
uv.lock CHANGED
@@ -583,7 +583,7 @@ wheels = [
583
  [[package]]
584
  name = "docling-serve"
585
  version = "0.2.0"
586
- source = { virtual = "." }
587
  dependencies = [
588
  { name = "docling" },
589
  { name = "fastapi", extra = ["standard"] },
@@ -591,6 +591,7 @@ dependencies = [
591
  { name = "pydantic" },
592
  { name = "pydantic-settings" },
593
  { name = "python-multipart" },
 
594
  { name = "uvicorn", extra = ["standard"] },
595
  ]
596
 
@@ -646,6 +647,7 @@ requires-dist = [
646
  { name = "torch", marker = "extra == 'cu124'", specifier = ">=2.6.0", index = "https://download.pytorch.org/whl/cu124", conflict = { package = "docling-serve", extra = "cu124" } },
647
  { name = "torchvision", marker = "extra == 'cpu'", specifier = ">=0.21.0", index = "https://download.pytorch.org/whl/cpu", conflict = { package = "docling-serve", extra = "cpu" } },
648
  { name = "torchvision", marker = "extra == 'cu124'", specifier = ">=0.21.0", index = "https://download.pytorch.org/whl/cu124", conflict = { package = "docling-serve", extra = "cu124" } },
 
649
  { name = "uvicorn", extras = ["standard"], specifier = ">=0.29.0,<1.0.0" },
650
  ]
651
  provides-extras = ["ui", "tesserocr", "rapidocr", "cpu", "cu124"]
 
583
  [[package]]
584
  name = "docling-serve"
585
  version = "0.2.0"
586
+ source = { editable = "." }
587
  dependencies = [
588
  { name = "docling" },
589
  { name = "fastapi", extra = ["standard"] },
 
591
  { name = "pydantic" },
592
  { name = "pydantic-settings" },
593
  { name = "python-multipart" },
594
+ { name = "typer" },
595
  { name = "uvicorn", extra = ["standard"] },
596
  ]
597
 
 
647
  { name = "torch", marker = "extra == 'cu124'", specifier = ">=2.6.0", index = "https://download.pytorch.org/whl/cu124", conflict = { package = "docling-serve", extra = "cu124" } },
648
  { name = "torchvision", marker = "extra == 'cpu'", specifier = ">=0.21.0", index = "https://download.pytorch.org/whl/cpu", conflict = { package = "docling-serve", extra = "cpu" } },
649
  { name = "torchvision", marker = "extra == 'cu124'", specifier = ">=0.21.0", index = "https://download.pytorch.org/whl/cu124", conflict = { package = "docling-serve", extra = "cu124" } },
650
+ { name = "typer", specifier = "~=0.12" },
651
  { name = "uvicorn", extras = ["standard"], specifier = ">=0.29.0,<1.0.0" },
652
  ]
653
  provides-extras = ["ui", "tesserocr", "rapidocr", "cpu", "cu124"]