diff --git a/.env.example b/.env.example index 8ff39a2..0821ea4 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,13 @@ +# Conversion engine: "local" (in-process Docling) or "remote" (Docling Serve) +# CONVERSION_ENGINE=local + +# Docling Serve settings (remote mode only) +# DOCLING_SERVE_URL=http://localhost:5001 +# DOCLING_SERVE_API_KEY= + +# Max seconds per conversion (default: 600) +# CONVERSION_TIMEOUT=600 + # CORS (comma-separated origins, only needed for custom deployments) # CORS_ORIGINS=http://localhost:3000,https://your-domain.com diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9066a50..a50c6ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release Docker Image +name: Release Docker Images on: push: @@ -11,12 +11,16 @@ env: jobs: release: - name: Build & push Docker image + name: Build & push — ${{ matrix.target }} runs-on: ubuntu-latest permissions: contents: read packages: write + strategy: + matrix: + target: [remote, local] + steps: - uses: actions/checkout@v4 @@ -28,15 +32,15 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract version from tag + - name: Docker metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest + type=semver,pattern={{version}},suffix=-${{ matrix.target }} + type=semver,pattern={{major}}.{{minor}},suffix=-${{ matrix.target }} + type=raw,value=latest-${{ matrix.target }} - name: Extract version from tag id: version @@ -45,6 +49,7 @@ jobs: - uses: docker/build-push-action@v6 with: context: . + target: ${{ matrix.target }} push: true platforms: linux/amd64,linux/arm64 build-args: APP_VERSION=${{ steps.version.outputs.value }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bea8895..50b4605 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,13 @@ Thank you for your interest in contributing to Docling Studio! This guide will h ```bash cd document-parser python -m venv .venv && source .venv/bin/activate + +# Remote mode (lightweight — delegates to Docling Serve) pip install -r requirements.txt + +# Local mode (full — runs Docling in-process) +pip install -r requirements-local.txt + pip install ruff pytest pytest-asyncio httpx # dev tools uvicorn main:app --reload --port 8000 ``` @@ -129,11 +135,16 @@ We use [Semantic Versioning](https://semver.org/): `MAJOR.MINOR.PATCH`. ### Docker Image Tags +Each release produces two image variants: + | Tag | Description | |-----|-------------| -| `X.Y.Z` | Exact version | -| `X.Y` | Latest patch of this minor | -| `latest` | Latest stable release | +| `X.Y.Z-remote` | Exact version — lightweight (Docling Serve) | +| `X.Y.Z-local` | Exact version — full (in-process Docling) | +| `X.Y-remote` | Latest patch of this minor — lightweight | +| `X.Y-local` | Latest patch of this minor — full | +| `latest-remote` | Latest stable — lightweight | +| `latest-local` | Latest stable — full | ### Hotfix diff --git a/Dockerfile b/Dockerfile index bbfbe36..ff3e37b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ # syntax=docker/dockerfile:1 # ============================================================================= -# Docling Studio — single-image build (frontend + backend) +# Docling Studio — single-image build (frontend + backend, multi-target) # # Usage: -# docker build -t docling-studio . -# docker run -p 3000:3000 docling-studio +# docker build --target remote -t docling-studio:remote . +# docker build --target local -t docling-studio:local . # ============================================================================= # --- Stage 1: Build frontend assets --- @@ -18,21 +18,19 @@ RUN npm ci COPY frontend/ . RUN VITE_APP_VERSION=${APP_VERSION} npm run build -# --- Stage 2: Runtime (Python + Nginx) --- -FROM python:3.12-slim +# --- Stage 2: Base runtime (Python + Nginx) --- +FROM python:3.12-slim AS base ARG APP_VERSION=dev ENV APP_VERSION=${APP_VERSION} -# System deps: poppler (pdf2image), nginx, and OpenCV runtime libs +# System deps: poppler (pdf2image), nginx RUN apt-get update && apt-get install -y --no-install-recommends \ poppler-utils \ - libgl1 \ - libglib2.0-0 \ nginx \ && rm -rf /var/lib/apt/lists/* -# Python deps +# Python deps (common) WORKDIR /app COPY document-parser/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt @@ -57,5 +55,24 @@ ENV DB_PATH=/app/data/docling_studio.db EXPOSE 3000 -# nginx needs to run as root for port binding, then drops to appuser for uvicorn CMD ["sh", "-c", "nginx && exec su appuser -c 'uvicorn main:app --host 127.0.0.1 --port 8000'"] + +# --- Remote: lightweight, delegates to Docling Serve --- +FROM base AS remote +ENV CONVERSION_ENGINE=remote + +# --- Local: full Docling in-process --- +FROM base AS local + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgl1 \ + libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* + +COPY document-parser/requirements-local.txt . +RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu \ + && pip install --no-cache-dir -r requirements-local.txt + +RUN chown -R appuser:appuser /app \ + && chown -R appuser:appuser /usr/local/lib/python3.12/site-packages/rapidocr/models +ENV CONVERSION_ENGINE=local diff --git a/README.md b/README.md index 5ac1323..fda0a1a 100644 --- a/README.md +++ b/README.md @@ -85,22 +85,42 @@ frontend/src/ ## Quick Start -### Docker (fastest) +Docling Studio ships two Docker image variants: + +| Variant | Image tag | Size | Description | +|---------|-----------|------|-------------| +| **remote** | `latest-remote` | ~270 MB | Lightweight — delegates to an external [Docling Serve](https://github.com/DS4SD/docling-serve) instance | +| **local** | `latest-local` | ~1.9 GB | Full — runs Docling in-process, CPU-only (downloads ML models on first run) | + +### Docker — remote mode (fastest) ```bash -docker run -p 3000:3000 ghcr.io/scub-france/docling-studio:latest +docker run -p 3000:3000 \ + -e DOCLING_SERVE_URL=http://your-docling-serve:5001 \ + ghcr.io/scub-france/docling-studio:latest-remote ``` -Open [http://localhost:3000](http://localhost:3000) +### Docker — local mode (self-contained) + +```bash +docker run -p 3000:3000 ghcr.io/scub-france/docling-studio:latest-local +``` > **Note:** The first analysis takes longer as Docling downloads its ML models (~400 MB). Subsequent runs are fast. +Open [http://localhost:3000](http://localhost:3000) + ### Docker Compose (for development) ```bash git clone https://github.com/scub-france/Docling-Studio.git cd Docling-Studio + +# Local mode (default) docker compose up --build + +# Remote mode +CONVERSION_MODE=remote DOCLING_SERVE_URL=http://your-docling-serve:5001 docker compose up --build ``` ### Local Development @@ -109,7 +129,13 @@ docker compose up --build ```bash cd document-parser python -m venv .venv && source .venv/bin/activate + +# Remote mode (lightweight) pip install -r requirements.txt + +# Local mode (with Docling) +pip install -r requirements-local.txt + uvicorn main:app --reload --port 8000 ``` @@ -156,6 +182,9 @@ All configuration is done via environment variables. See [`.env.example`](.env.e | Variable | Default | Description | |----------|---------|-------------| +| `CONVERSION_ENGINE` | `local` | `local` (in-process Docling) or `remote` (Docling Serve) | +| `DOCLING_SERVE_URL` | `http://localhost:5001` | Docling Serve endpoint (remote mode only) | +| `DOCLING_SERVE_API_KEY` | — | API key for Docling Serve (optional) | | `CORS_ORIGINS` | `http://localhost:3000,...` | CORS allowed origins (comma-separated) | | `UPLOAD_DIR` | `./uploads` | File storage directory | | `DB_PATH` | `./data/docling_studio.db` | SQLite database path | @@ -168,7 +197,7 @@ GitHub Actions pipelines (see [`.github/workflows/`](.github/workflows/)): | Workflow | Trigger | What it does | |----------|---------|--------------| | **CI** | push to `main`, pull requests | Lint + type check + Backend tests + Frontend tests + build | -| **Release** | push tag `v*` | Build & push multi-arch Docker image to `ghcr.io` | +| **Release** | push tag `v*` | Build & push **two** multi-arch Docker images (`remote` + `local`) to `ghcr.io` | | **Docs** | push to `main` (docs changes) | Build & deploy MkDocs to GitHub Pages | We follow [Semantic Versioning](https://semver.org/) with a simplified Git Flow. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full release process. @@ -183,12 +212,11 @@ We follow [Semantic Versioning](https://semver.org/) with a simplified Git Flow. ### Docker Desktop settings -The document parser needs **at least 4 GB of RAM**: - -| Resource | Minimum | Recommended | -|----------|---------|-------------| -| Memory | 6 GB | 8 GB+ | -| CPUs | 4 | 8+ | +| | Remote image | Local image | +|---|---|---| +| **Image size** | ~270 MB | ~1.9 GB | +| **Memory** | 2 GB | 6 GB (recommended 8 GB+) | +| **CPUs** | 2 | 4 (recommended 8+) | ### Platform support diff --git a/docker-compose.yml b/docker-compose.yml index 6e75d5e..f1f80aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,7 @@ services: document-parser: build: context: ./document-parser + target: ${CONVERSION_MODE:-local} expose: - "8000" volumes: @@ -9,6 +10,8 @@ services: - db_data:/app/data environment: CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000,http://localhost:5173} + DOCLING_SERVE_URL: ${DOCLING_SERVE_URL:-} + DOCLING_SERVE_API_KEY: ${DOCLING_SERVE_API_KEY:-} deploy: resources: limits: diff --git a/docs/contributing.md b/docs/contributing.md index 17c6e7d..44412e3 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -20,7 +20,13 @@ ```bash cd document-parser python -m venv .venv && source .venv/bin/activate + + # Remote mode (lightweight — delegates to Docling Serve) pip install -r requirements.txt + + # Local mode (full — runs Docling in-process) + pip install -r requirements-local.txt + pip install ruff pytest pytest-asyncio httpx uvicorn main:app --reload --port 8000 ``` diff --git a/docs/getting-started.md b/docs/getting-started.md index cd38397..539dd4f 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,33 +1,67 @@ # Getting Started -## Docker Compose (recommended) +Docling Studio ships two Docker image variants: + +| Variant | Image tag | Size | Description | +|---------|-----------|------|-------------| +| **remote** | `latest-remote` | ~270 MB | Lightweight — delegates to an external [Docling Serve](https://github.com/DS4SD/docling-serve) instance | +| **local** | `latest-local` | ~1.9 GB | Full — runs Docling in-process, CPU-only (downloads ML models on first run) | + +## Docker — remote mode (fastest) + +```bash +docker run -p 3000:3000 \ + -e DOCLING_SERVE_URL=http://your-docling-serve:5001 \ + ghcr.io/scub-france/docling-studio:latest-remote +``` + +## Docker — local mode (self-contained) + +```bash +docker run -p 3000:3000 ghcr.io/scub-france/docling-studio:latest-local +``` + +> **Note:** The first analysis takes longer as Docling downloads its ML models (~400 MB). Subsequent runs are fast. + +Open [http://localhost:3000](http://localhost:3000). + +## Docker Compose (recommended for development) ```bash git clone https://github.com/scub-france/Docling-Studio.git cd Docling-Studio -docker compose up --build -``` -Open [http://localhost:3000](http://localhost:3000). +# Local mode (default) +docker compose up --build + +# Remote mode +CONVERSION_MODE=remote DOCLING_SERVE_URL=http://your-docling-serve:5001 docker compose up --build +``` ## Local Development -### Backend (Python 3.12+) +=== "Backend (Python 3.12+)" -```bash -cd document-parser -python -m venv .venv && source .venv/bin/activate -pip install -r requirements.txt -uvicorn main:app --reload --port 8000 -``` + ```bash + cd document-parser + python -m venv .venv && source .venv/bin/activate -### Frontend (Node 20+) + # Remote mode (lightweight) + pip install -r requirements.txt -```bash -cd frontend -npm install -npm run dev -``` + # Local mode (with Docling) + pip install -r requirements-local.txt + + uvicorn main:app --reload --port 8000 + ``` + +=== "Frontend (Node 20+)" + + ```bash + cd frontend + npm install + npm run dev + ``` The frontend runs on `http://localhost:3000` and proxies API calls to `http://localhost:8000`. @@ -71,6 +105,9 @@ All configuration is done via environment variables: | Variable | Default | Description | |----------|---------|-------------| +| `CONVERSION_ENGINE` | `local` | `local` (in-process Docling) or `remote` (Docling Serve) | +| `DOCLING_SERVE_URL` | `http://localhost:5001` | Docling Serve endpoint (remote mode only) | +| `DOCLING_SERVE_API_KEY` | — | API key for Docling Serve (optional) | | `CORS_ORIGINS` | `http://localhost:3000,...` | CORS allowed origins | | `UPLOAD_DIR` | `./uploads` | File storage directory | | `DB_PATH` | `./data/docling_studio.db` | SQLite database path | @@ -78,9 +115,10 @@ All configuration is done via environment variables: ## System Requirements -| Resource | Minimum | Recommended | -|----------|---------|-------------| -| Memory | 6 GB | 8 GB+ | -| CPUs | 4 | 8+ | +| | Remote image | Local image | +|---|---|---| +| **Image size** | ~270 MB | ~1.9 GB | +| **Memory** | 2 GB | 6 GB (recommended 8 GB+) | +| **CPUs** | 2 | 4 (recommended 8+) | All Docker images are multi-arch (`linux/amd64` + `linux/arm64`). No GPU required. diff --git a/document-parser/Dockerfile b/document-parser/Dockerfile index 76cc697..3c21f9a 100644 --- a/document-parser/Dockerfile +++ b/document-parser/Dockerfile @@ -1,9 +1,17 @@ -FROM python:3.12-slim +# syntax=docker/dockerfile:1 +# ============================================================================= +# Docling Studio — backend image (multi-target: remote / local) +# +# Usage: +# docker build --target remote -t docling-studio-backend:remote . +# docker build --target local -t docling-studio-backend:local . +# ============================================================================= + +# --- Base: common deps for both targets --- +FROM python:3.12-slim AS base RUN apt-get update && apt-get install -y --no-install-recommends \ poppler-utils \ - libgl1 \ - libglib2.0-0 \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -24,3 +32,25 @@ ENV DB_PATH=/app/data/docling_studio.db USER appuser CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] + +# --- Remote: lightweight, delegates to Docling Serve --- +FROM base AS remote +ENV CONVERSION_ENGINE=remote + +# --- Local: full Docling in-process --- +FROM base AS local + +USER root +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgl1 \ + libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements-local.txt . +RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu \ + && pip install --no-cache-dir -r requirements-local.txt + +RUN chown -R appuser:appuser /app \ + && chown -R appuser:appuser /usr/local/lib/python3.12/site-packages/rapidocr/models +USER appuser +ENV CONVERSION_ENGINE=local diff --git a/document-parser/main.py b/document-parser/main.py index aaf0275..5927c46 100644 --- a/document-parser/main.py +++ b/document-parser/main.py @@ -103,7 +103,7 @@ app.include_router(documents_router) app.include_router(analyses_router) -@app.get("/health") +@app.get("/api/health") def health(): """Health check endpoint.""" return { diff --git a/document-parser/requirements-local.txt b/document-parser/requirements-local.txt new file mode 100644 index 0000000..82cf543 --- /dev/null +++ b/document-parser/requirements-local.txt @@ -0,0 +1,3 @@ +-r requirements.txt +docling>=2.80.0,<3.0.0 +docling-core>=2.0.0,<3.0.0 diff --git a/document-parser/requirements.txt b/document-parser/requirements.txt index 4b8cc74..24aeedb 100644 --- a/document-parser/requirements.txt +++ b/document-parser/requirements.txt @@ -1,5 +1,3 @@ -docling>=2.80.0,<3.0.0 -docling-core>=2.0.0,<3.0.0 fastapi>=0.115.0,<1.0.0 uvicorn[standard]>=0.32.0,<1.0.0 python-multipart>=0.0.12 diff --git a/document-parser/tests/test_api_endpoints.py b/document-parser/tests/test_api_endpoints.py index 2f34518..9826e5c 100644 --- a/document-parser/tests/test_api_endpoints.py +++ b/document-parser/tests/test_api_endpoints.py @@ -26,7 +26,7 @@ def mock_analysis_service(client): class TestHealthEndpoint: def test_health(self, client): - resp = client.get("/health") + resp = client.get("/api/health") assert resp.status_code == 200 data = resp.json() assert data["status"] == "ok" diff --git a/frontend/src/features/feature-flags/store.ts b/frontend/src/features/feature-flags/store.ts index 568609d..b3b3721 100644 --- a/frontend/src/features/feature-flags/store.ts +++ b/frontend/src/features/feature-flags/store.ts @@ -44,7 +44,7 @@ export const useFeatureFlagStore = defineStore('feature-flags', () => { async function load(): Promise { try { - const data = await apiFetch('/health') + const data = await apiFetch('/api/health') engine.value = data.engine loaded.value = true error.value = null