From e36daad3d3bf2c80aa91c2123455db6deaaa85a4 Mon Sep 17 00:00:00 2001 From: Ayan-josh-05 Date: Mon, 24 Aug 2026 11:57:05 +0530 Subject: [PATCH 1/9] Setup locally --- lending-poc/.gitignore | 7 +- lending-poc/docker-compose.yml | 2 +- lending-poc/document_processing/ocr/README.md | 4 ++ lending-poc/document_processing/ocr/api.py | 64 ++++++++++++++---- .../ocr/extraction_input/test-image.jpg | Bin 0 -> 19332 bytes .../ocr/extractor/engines/surya_engine.py | 11 ++- .../translation/requirements.txt | 2 +- .../translation/translation_service/config.py | 3 +- lending-poc/field_mapping_poc/api.py | 4 +- lending-poc/field_mapping_poc/config.py | 2 +- lending-poc/frontend/package-lock.json | 54 --------------- lending-poc/frontend/src/api/extract.ts | 7 +- lending-poc/frontend/src/config/env.ts | 1 + lending-poc/gateway/main.py | 8 ++- lending-poc/scripts/start-backend.sh | 2 +- 15 files changed, 91 insertions(+), 80 deletions(-) create mode 100644 lending-poc/document_processing/ocr/extraction_input/test-image.jpg diff --git a/lending-poc/.gitignore b/lending-poc/.gitignore index a9f203b..e159458 100644 --- a/lending-poc/.gitignore +++ b/lending-poc/.gitignore @@ -3,10 +3,15 @@ __pycache__/ *.pyo .venv/ .env +**/.env *.egg-info/ dist/ build/ .mypy_cache/ .pytest_cache/ .ruff_cache/ -venv/ \ No newline at end of file +venv/ +**/.venv/ +.venv-windows/ +local.env +*.log \ No newline at end of file diff --git a/lending-poc/docker-compose.yml b/lending-poc/docker-compose.yml index 38239e6..9beda1a 100644 --- a/lending-poc/docker-compose.yml +++ b/lending-poc/docker-compose.yml @@ -8,7 +8,7 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} POSTGRES_DB: ${POSTGRES_DB:-lending_poc} ports: - - "${POSTGRES_HOST_PORT:-55432}:5432" + - "${POSTGRES_HOST_PORT:-55439}:5432" volumes: - pgdata:/var/lib/postgresql/data healthcheck: diff --git a/lending-poc/document_processing/ocr/README.md b/lending-poc/document_processing/ocr/README.md index 210ca46..15725e6 100644 --- a/lending-poc/document_processing/ocr/README.md +++ b/lending-poc/document_processing/ocr/README.md @@ -200,6 +200,10 @@ make status # Show project status ### Prerequisites - Python 3.11+ - macOS/Linux (Surya requires local inference backend) +- A Surya inference backend: on CPU-only WSL install `llama.cpp` so that + `llama-server` is on `PATH`; on an NVIDIA WSL setup configure Surya's vLLM + backend and Docker/GPU passthrough. The API now verifies this at startup, + before reporting `/health` as healthy. ### Dependencies diff --git a/lending-poc/document_processing/ocr/api.py b/lending-poc/document_processing/ocr/api.py index bba47e4..4ea26e4 100644 --- a/lending-poc/document_processing/ocr/api.py +++ b/lending-poc/document_processing/ocr/api.py @@ -5,7 +5,7 @@ Built as a thin wrapper around the existing Extractor pipeline. Usage: - uvicorn api:app --host 0.0.0.0 --port 8000 --reload + uvicorn api:app --host 0.0.0.0 --port 8010 --reload Endpoints: POST /extract - Upload and process a document @@ -17,6 +17,7 @@ from pathlib import Path from typing import Any, Dict import os +from contextlib import asynccontextmanager from fastapi import FastAPI, File, UploadFile, HTTPException from fastapi.concurrency import run_in_threadpool @@ -25,16 +26,40 @@ from extractor import Extractor, DEFAULT_ENGINE from extractor.loader import SUPPORTED_EXTENSIONS +# Initialize extractor (reused across requests for efficiency) +extractor = Extractor(engine=DEFAULT_ENGINE) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Start Surya without making the HTTP service unavailable during warm-up.""" + app.state.ocr_ready = False + app.state.ocr_error = None + + async def warm_up() -> None: + try: + await run_in_threadpool(extractor.engine.warm_up) + except Exception as exc: + # Keep the API available for diagnostics. /extract will return a + # useful 503 instead of holding an upload open while Surya retries + # a missing/misconfigured WSL inference runtime. + app.state.ocr_error = str(exc) + else: + app.state.ocr_ready = True + + warm_up_task = asyncio.create_task(warm_up()) + yield + warm_up_task.cancel() + + # Initialize FastAPI app app = FastAPI( title="OCR Text Extraction API", description="Upload documents (PDF, PNG, JPEG) for OCR text extraction using Surya", - version="1.0.0" + version="1.0.0", + lifespan=lifespan, ) -# Initialize extractor (reused across requests for efficiency) -extractor = Extractor(engine=DEFAULT_ENGINE) - # File size limit (50MB) MAX_FILE_SIZE = 50 * 1024 * 1024 @@ -43,13 +68,18 @@ _extract_lock = asyncio.Lock() @app.get("/health") -async def health_check() -> Dict[str, str]: - """Health check endpoint to verify API is running.""" - return { - "status": "healthy", +async def health_check() -> Dict[str, Any]: + """Health check that distinguishes an online API from ready OCR inference.""" + response: Dict[str, Any] = { + "status": "healthy" if app.state.ocr_ready else "initializing", "service": "OCR Text Extraction API", - "engine": DEFAULT_ENGINE + "engine": DEFAULT_ENGINE, + "ocr_ready": app.state.ocr_ready, } + if app.state.ocr_error: + response["status"] = "unhealthy" + response["ocr_error"] = app.state.ocr_error + return response @app.post("/extract") async def extract_text(file: UploadFile = File(...)) -> Dict[str, Any]: @@ -60,6 +90,14 @@ async def extract_text(file: UploadFile = File(...)) -> Dict[str, Any]: Returns: Extracted text, HTML representation, and metadata """ + # Never accept an upload when the OCR runtime is not available. Without + # this guard Surya can block the request for its full backend timeout. + if not app.state.ocr_ready: + detail = "OCR inference is still initializing" + if app.state.ocr_error: + detail = f"OCR inference is unavailable: {app.state.ocr_error}" + raise HTTPException(status_code=503, detail=detail) + # Validate file type if not file.filename: raise HTTPException(status_code=400, detail="No filename provided") @@ -126,7 +164,7 @@ async def extract_text(file: UploadFile = File(...)) -> Dict[str, Any]: finally: # Clean up temporary file - if temp_file and os.path.exists(temp_file_path): + if temp_file_path and os.path.exists(temp_file_path): try: os.unlink(temp_file_path) except OSError: @@ -152,6 +190,6 @@ async def root() -> Dict[str, Any]: uvicorn.run( "api:app", host="0.0.0.0", - port=8000, + port=8010, reload=True - ) \ No newline at end of file + ) diff --git a/lending-poc/document_processing/ocr/extraction_input/test-image.jpg b/lending-poc/document_processing/ocr/extraction_input/test-image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..84c5ec2653e51701a84f582677a94124be19f6e1 GIT binary patch literal 19332 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 difnS27#%!*-jHGMW6B@;ejXE!5 None: def _ensure_ready(self) -> None: if self._manager is None: - self._manager = SuryaInferenceManager() # auto-spawns vllm or llama-server + self._manager = SuryaInferenceManager() + # Surya creates its inference manager lazily. Starting it here + # means an API startup check can fail fast when WSL is missing its + # backend (llama-server on CPU, or vLLM/Docker on CUDA), instead + # of making the first uploaded document appear to hang. + self._manager.start() self._recognizer = RecognitionPredictor(self._manager) + def warm_up(self) -> None: + """Start and validate Surya's inference backend without processing a file.""" + self._ensure_ready() + def run(self, images: List[Image.Image]) -> List[PageResult]: self._ensure_ready() raw_predictions = self._recognizer(images) diff --git a/lending-poc/document_processing/translation/requirements.txt b/lending-poc/document_processing/translation/requirements.txt index c7d3d78..b29dbac 100644 --- a/lending-poc/document_processing/translation/requirements.txt +++ b/lending-poc/document_processing/translation/requirements.txt @@ -1,5 +1,5 @@ # Translation service dependencies -ollama==0.4.7 +ollama==0.6.2 # FastAPI and server fastapi==0.115.6 diff --git a/lending-poc/document_processing/translation/translation_service/config.py b/lending-poc/document_processing/translation/translation_service/config.py index 79f938d..9bc7be7 100644 --- a/lending-poc/document_processing/translation/translation_service/config.py +++ b/lending-poc/document_processing/translation/translation_service/config.py @@ -8,6 +8,7 @@ Nothing outside this file needs to change for those operations. """ +import os from pathlib import Path # --------------------------------------------------------------------------- @@ -67,7 +68,7 @@ def get_kb_path(domain: str) -> Path: MODEL_ADAPTER = "ollama" # Model identifier passed to the chosen adapter. -MODEL_NAME = "gemma4:e4b" +MODEL_NAME = os.getenv("OLLAMA_MODEL", "gemma4:e4b-it-qat") # --------------------------------------------------------------------------- # Model options (adapter-specific — passed through as-is) diff --git a/lending-poc/field_mapping_poc/api.py b/lending-poc/field_mapping_poc/api.py index 29a7a01..d3e7b24 100644 --- a/lending-poc/field_mapping_poc/api.py +++ b/lending-poc/field_mapping_poc/api.py @@ -5,7 +5,7 @@ Built as a wrapper around the existing FieldMapper. Usage: - uvicorn api:app --host 0.0.0.0 --port 8000 --reload + uvicorn api:app --host 0.0.0.0 --port 8002 --reload Endpoints: POST /map - Map OCR text to the provided JSON schema @@ -109,6 +109,6 @@ async def root() -> Dict[str, Any]: uvicorn.run( "api:app", host="0.0.0.0", - port=8000, + port=8002, reload=True ) diff --git a/lending-poc/field_mapping_poc/config.py b/lending-poc/field_mapping_poc/config.py index cda02f2..7a19041 100644 --- a/lending-poc/field_mapping_poc/config.py +++ b/lending-poc/field_mapping_poc/config.py @@ -12,7 +12,7 @@ @dataclass(frozen=True) class OllamaConfig: host: str = os.getenv("OLLAMA_HOST", "http://localhost:11434") - model: str = os.getenv("OLLAMA_MODEL", "gemma4:e4b") + model: str = os.getenv("OLLAMA_MODEL", "gemma4:e4b-it-qat") temperature: float = float(os.getenv("OLLAMA_TEMPERATURE", "0.0")) num_ctx: int = int(os.getenv("OLLAMA_NUM_CTX", "8192")) request_timeout: int = int(os.getenv("OLLAMA_TIMEOUT_SECONDS", "120")) diff --git a/lending-poc/frontend/package-lock.json b/lending-poc/frontend/package-lock.json index 57e38c8..7ebcb70 100644 --- a/lending-poc/frontend/package-lock.json +++ b/lending-poc/frontend/package-lock.json @@ -626,9 +626,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -646,9 +643,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -666,9 +660,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -686,9 +677,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -706,9 +694,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -726,9 +711,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -955,9 +937,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -979,9 +958,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1003,9 +979,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1027,9 +1000,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1202,9 +1172,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1222,9 +1189,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1242,9 +1206,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1262,9 +1223,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2877,9 +2835,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2901,9 +2856,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2925,9 +2877,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2949,9 +2898,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/lending-poc/frontend/src/api/extract.ts b/lending-poc/frontend/src/api/extract.ts index 10cbcc5..f4aa2c1 100644 --- a/lending-poc/frontend/src/api/extract.ts +++ b/lending-poc/frontend/src/api/extract.ts @@ -4,9 +4,10 @@ import { extractResponseSchema, type ExtractResponse } from '@/schemas/extract.s // OCR extraction is CPU-bound and can take well over the client's default // 30s timeout, especially for multi-page documents or several concurrent // uploads (the backend also serializes concurrent extractions, so later -// documents in a batch wait on earlier ones). 5 minutes gives real -// documents room to finish instead of erroring out mid-processing. -const EXTRACT_TIMEOUT_MS = 5 * 60 * 1000 +// documents in a batch wait on earlier ones). CPU-only Surya can take +// several minutes per handwritten page, so this must stay aligned with the +// gateway's OCR_REQUEST_TIMEOUT_SECONDS default. +const EXTRACT_TIMEOUT_MS = 2 * 60 * 1000 /** * POSTs a single file to /extract. The backend processes one file per call, diff --git a/lending-poc/frontend/src/config/env.ts b/lending-poc/frontend/src/config/env.ts index 322b5c3..19ad126 100644 --- a/lending-poc/frontend/src/config/env.ts +++ b/lending-poc/frontend/src/config/env.ts @@ -1,5 +1,6 @@ import { z } from 'zod' + const envSchema = z.object({ VITE_API_BASE_URL: z.string().url().or(z.string().startsWith('/')), VITE_TRANSLATION_API_BASE_URL: z.string().url().or(z.string().startsWith('/')), diff --git a/lending-poc/gateway/main.py b/lending-poc/gateway/main.py index 6e7b551..4a5755d 100644 --- a/lending-poc/gateway/main.py +++ b/lending-poc/gateway/main.py @@ -23,6 +23,10 @@ OCR_BASE_URL = os.environ.get("OCR_BASE_URL", "http://127.0.0.1:8010") TRANSLATION_BASE_URL = os.environ.get("TRANSLATION_BASE_URL", "http://127.0.0.1:8001") FIELD_MAPPING_BASE_URL = os.environ.get("FIELD_MAPPING_BASE_URL", "http://127.0.0.1:8002") +# Surya on a CPU-only WSL host can take several minutes per handwritten page. +# Keep this aligned with the browser timeout so the gateway does not terminate +# a valid OCR request while the OCR worker is still generating text. +OCR_REQUEST_TIMEOUT_SECONDS = float(os.environ.get("OCR_REQUEST_TIMEOUT_SECONDS", "1800")) # Headers that must not be forwarded as-is between hops (RFC 7230) plus a few # that httpx/Starlette will recompute themselves and that would otherwise @@ -37,7 +41,7 @@ @asynccontextmanager async def lifespan(app: FastAPI): - app.state.http = httpx.AsyncClient(timeout=120.0) + app.state.http = httpx.AsyncClient(timeout=httpx.Timeout(30.0)) yield await app.state.http.aclose() @@ -58,12 +62,14 @@ async def _proxy(request: Request, base_url: str, path: str) -> Response: headers = {k: v for k, v in request.headers.items() if k.lower() not in REQUEST_STRIP_HEADERS} body = await request.body() try: + timeout = OCR_REQUEST_TIMEOUT_SECONDS if base_url == OCR_BASE_URL else None upstream = await client.request( request.method, f"{base_url}{path}", headers=headers, params=list(request.query_params.multi_items()), content=body, + timeout=timeout, ) except httpx.RequestError as exc: return JSONResponse( diff --git a/lending-poc/scripts/start-backend.sh b/lending-poc/scripts/start-backend.sh index 6b33855..645b480 100755 --- a/lending-poc/scripts/start-backend.sh +++ b/lending-poc/scripts/start-backend.sh @@ -10,7 +10,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PYTHON_BIN="python3.12" -OLLAMA_MODEL="${OLLAMA_MODEL:-gemma4:e4b}" +OLLAMA_MODEL="${OLLAMA_MODEL:-gemma4:e4b-it-qat}" GATEWAY_PORT=8000 OCR_PORT=8010 From f12b726a554effcd20e65ae70a844af8f0a69cb0 Mon Sep 17 00:00:00 2001 From: Ayan-josh-05 Date: Tue, 25 Aug 2026 10:47:59 +0530 Subject: [PATCH 2/9] Dockerized the application --- lending-poc/.dockerignore | 14 +++ lending-poc/app/config.py | 2 +- lending-poc/docker-compose.yml | 87 +++++++++++++++++++ .../document_processing/ocr/.dockerignore | 10 +++ .../document_processing/ocr/Dockerfile | 17 ++++ .../translation/.dockerignore | 7 ++ .../translation/Dockerfile | 12 +++ lending-poc/field_mapping_poc/.dockerignore | 6 ++ lending-poc/field_mapping_poc/Dockerfile | 12 +++ lending-poc/frontend/.dockerignore | 6 ++ lending-poc/frontend/Dockerfile | 12 +++ lending-poc/gateway/.dockerignore | 5 ++ lending-poc/gateway/Dockerfile | 12 +++ lending-poc/surya-inference/Dockerfile | 43 +++++++++ lending-poc/surya-inference/entrypoint.sh | 31 +++++++ 15 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 lending-poc/.dockerignore create mode 100644 lending-poc/document_processing/ocr/.dockerignore create mode 100644 lending-poc/document_processing/ocr/Dockerfile create mode 100644 lending-poc/document_processing/translation/.dockerignore create mode 100644 lending-poc/document_processing/translation/Dockerfile create mode 100644 lending-poc/field_mapping_poc/.dockerignore create mode 100644 lending-poc/field_mapping_poc/Dockerfile create mode 100644 lending-poc/frontend/.dockerignore create mode 100644 lending-poc/frontend/Dockerfile create mode 100644 lending-poc/gateway/.dockerignore create mode 100644 lending-poc/gateway/Dockerfile create mode 100644 lending-poc/surya-inference/Dockerfile create mode 100644 lending-poc/surya-inference/entrypoint.sh diff --git a/lending-poc/.dockerignore b/lending-poc/.dockerignore new file mode 100644 index 0000000..02dfa58 --- /dev/null +++ b/lending-poc/.dockerignore @@ -0,0 +1,14 @@ +.git +.venv +**/.venv +**/.venv-windows +**/surya-env +**/node_modules +**/__pycache__ +document_processing +field_mapping_poc +gateway +frontend +scripts +docs +*.log diff --git a/lending-poc/app/config.py b/lending-poc/app/config.py index 90c1779..ce7fab6 100644 --- a/lending-poc/app/config.py +++ b/lending-poc/app/config.py @@ -4,7 +4,7 @@ class Settings(BaseSettings): - model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") APP_NAME: str = "lending-poc" APP_VERSION: str = "0.1.0" diff --git a/lending-poc/docker-compose.yml b/lending-poc/docker-compose.yml index 9beda1a..84e31c2 100644 --- a/lending-poc/docker-compose.yml +++ b/lending-poc/docker-compose.yml @@ -32,5 +32,92 @@ services: db: condition: service_healthy + ollama: + image: ollama/ollama:latest + ports: + - "11434:11434" + volumes: + - ollama_models:/root/.ollama + + field_mapping: + build: ./field_mapping_poc + command: uvicorn api:app --host 0.0.0.0 --port 8002 --reload + ports: + - "8002:8002" + environment: + OLLAMA_HOST: ${OLLAMA_HOST:-http://ollama:11434} + OLLAMA_MODEL: ${OLLAMA_MODEL:-gemma4:e4b-it-qat} + volumes: + - ./field_mapping_poc:/app + depends_on: + - ollama + + translation: + build: ./document_processing/translation + command: uvicorn api_server:app --host 0.0.0.0 --port 8001 --reload + ports: + - "8001:8001" + environment: + OLLAMA_HOST: ${OLLAMA_HOST:-http://ollama:11434} + OLLAMA_MODEL: ${OLLAMA_MODEL:-gemma4:e4b-it-qat} + volumes: + - ./document_processing/translation:/app + depends_on: + - ollama + + surya-inference: + build: ./surya-inference + ports: + - "8500:8000" + environment: + SURYA_INFERENCE_PARALLEL: ${SURYA_INFERENCE_PARALLEL:-4} + SURYA_INFERENCE_CTX_SIZE: ${SURYA_INFERENCE_CTX_SIZE:-49152} + volumes: + - surya_models:/models + + ocr: + build: ./document_processing/ocr + command: uvicorn api:app --host 0.0.0.0 --port 8010 --reload + ports: + - "8010:8010" + environment: + SURYA_INFERENCE_URL: http://surya-inference:8000/v1 + SURYA_INFERENCE_AUTOSTART: "false" + volumes: + - ./document_processing/ocr:/app + depends_on: + - surya-inference + + gateway: + build: ./gateway + command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload + ports: + - "8080:8000" + environment: + FIELD_MAPPING_BASE_URL: http://field_mapping:8002 + OCR_BASE_URL: http://ocr:8010 + TRANSLATION_BASE_URL: http://translation:8001 + volumes: + - ./gateway:/app + depends_on: + - field_mapping + - ocr + - translation + + frontend: + build: ./frontend + ports: + - "5173:5173" + environment: + # Browser-facing: must be host-reachable (localhost + published port), + # not a Docker-internal service name. + VITE_API_BASE_URL: http://localhost:8080 + VITE_TRANSLATION_API_BASE_URL: http://localhost:8080 + volumes: + - ./frontend:/app + - /app/node_modules + volumes: pgdata: + ollama_models: + surya_models: diff --git a/lending-poc/document_processing/ocr/.dockerignore b/lending-poc/document_processing/ocr/.dockerignore new file mode 100644 index 0000000..3071a79 --- /dev/null +++ b/lending-poc/document_processing/ocr/.dockerignore @@ -0,0 +1,10 @@ +__pycache__ +*.pyc +.venv +.venv-windows +surya-env +.env +.git +extraction_output +*.stdout.log +*.stderr.log diff --git a/lending-poc/document_processing/ocr/Dockerfile b/lending-poc/document_processing/ocr/Dockerfile new file mode 100644 index 0000000..7f63ad4 --- /dev/null +++ b/lending-poc/document_processing/ocr/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Surya's recognition/layout models run in-process here (CPU). The LLM half +# runs in the separate surya-inference container, reached via +# SURYA_INFERENCE_URL — this image does not need llama-server. +RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8010 + +CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8010"] diff --git a/lending-poc/document_processing/translation/.dockerignore b/lending-poc/document_processing/translation/.dockerignore new file mode 100644 index 0000000..677b168 --- /dev/null +++ b/lending-poc/document_processing/translation/.dockerignore @@ -0,0 +1,7 @@ +__pycache__ +*.pyc +.venv +.venv-windows +.env +.git +output diff --git a/lending-poc/document_processing/translation/Dockerfile b/lending-poc/document_processing/translation/Dockerfile new file mode 100644 index 0000000..819520c --- /dev/null +++ b/lending-poc/document_processing/translation/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8001 + +CMD ["uvicorn", "api_server:app", "--host", "0.0.0.0", "--port", "8001"] diff --git a/lending-poc/field_mapping_poc/.dockerignore b/lending-poc/field_mapping_poc/.dockerignore new file mode 100644 index 0000000..d372f23 --- /dev/null +++ b/lending-poc/field_mapping_poc/.dockerignore @@ -0,0 +1,6 @@ +__pycache__ +*.pyc +.venv +.env +samples +.git diff --git a/lending-poc/field_mapping_poc/Dockerfile b/lending-poc/field_mapping_poc/Dockerfile new file mode 100644 index 0000000..e98c885 --- /dev/null +++ b/lending-poc/field_mapping_poc/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8002 + +CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8002"] diff --git a/lending-poc/frontend/.dockerignore b/lending-poc/frontend/.dockerignore new file mode 100644 index 0000000..c071f14 --- /dev/null +++ b/lending-poc/frontend/.dockerignore @@ -0,0 +1,6 @@ +node_modules +dist +.env +.env.local +*.log +.git diff --git a/lending-poc/frontend/Dockerfile b/lending-poc/frontend/Dockerfile new file mode 100644 index 0000000..0163475 --- /dev/null +++ b/lending-poc/frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:22-slim + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/lending-poc/gateway/.dockerignore b/lending-poc/gateway/.dockerignore new file mode 100644 index 0000000..2c46cf5 --- /dev/null +++ b/lending-poc/gateway/.dockerignore @@ -0,0 +1,5 @@ +__pycache__ +*.pyc +.venv +.env +.git diff --git a/lending-poc/gateway/Dockerfile b/lending-poc/gateway/Dockerfile new file mode 100644 index 0000000..b3a66cf --- /dev/null +++ b/lending-poc/gateway/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/lending-poc/surya-inference/Dockerfile b/lending-poc/surya-inference/Dockerfile new file mode 100644 index 0000000..94ef34b --- /dev/null +++ b/lending-poc/surya-inference/Dockerfile @@ -0,0 +1,43 @@ +# Builds llama.cpp's CPU `llama-server` from source and serves it standalone, +# so ocr-api can point SURYA_INFERENCE_URL here instead of spawning its own +# copy in-process (see surya/inference/backends/llamacpp.py in the ocr-api +# image for the equivalent in-process spawn logic this mirrors). +FROM python:3.12-slim AS build + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake git ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN git clone --depth 1 https://github.com/ggml-org/llama.cpp /llama.cpp +WORKDIR /llama.cpp +RUN cmake -B build -DGGML_NATIVE=OFF -DLLAMA_CURL=OFF -DCMAKE_BUILD_TYPE=Release \ + && cmake --build build -j"$(nproc)" --target llama-server + +FROM python:3.12-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl ca-certificates libgomp1 \ + && rm -rf /var/lib/apt/lists/* + +# llama-server is dynamically linked against the other .so files built +# alongside it (libllama-server-impl.so, libllama-common.so, libmtmd.so, ...), +# so the whole bin/ directory needs to come along, not just the executable. +COPY --from=build /llama.cpp/build/bin /opt/llama.cpp/bin +ENV LD_LIBRARY_PATH=/opt/llama.cpp/bin +RUN ln -s /opt/llama.cpp/bin/llama-server /usr/local/bin/llama-server + +ENV SURYA_GGUF_REPO=datalab-to/surya-ocr-2-gguf \ + SURYA_GGUF_MODEL_FILE=surya-2.gguf \ + SURYA_GGUF_MMPROJ_FILE=surya-2-mmproj.gguf \ + SURYA_MODEL_ALIAS=datalab-to/surya-ocr-2 \ + MODEL_DIR=/models \ + PORT=8000 + +RUN mkdir -p /models + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 8000 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/lending-poc/surya-inference/entrypoint.sh b/lending-poc/surya-inference/entrypoint.sh new file mode 100644 index 0000000..7d640bd --- /dev/null +++ b/lending-poc/surya-inference/entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# Downloads the same GGUF files Surya's own llamacpp backend fetches when it +# spawns its own server (surya/inference/backends/llamacpp.py: +# SURYA_GGUF_REPO / SURYA_GGUF_MODEL_FILE / SURYA_GGUF_MMPROJ_FILE), then +# starts llama-server with the equivalent flags so ocr-api can attach to it +# via SURYA_INFERENCE_URL instead of spawning its own. +set -e + +MODEL_PATH="${MODEL_DIR}/${SURYA_GGUF_MODEL_FILE}" +MMPROJ_PATH="${MODEL_DIR}/${SURYA_GGUF_MMPROJ_FILE}" + +if [ ! -f "$MODEL_PATH" ]; then + echo "Downloading ${SURYA_GGUF_MODEL_FILE} from ${SURYA_GGUF_REPO}..." + curl -fL -o "$MODEL_PATH" "https://huggingface.co/${SURYA_GGUF_REPO}/resolve/main/${SURYA_GGUF_MODEL_FILE}" +fi + +if [ ! -f "$MMPROJ_PATH" ]; then + echo "Downloading ${SURYA_GGUF_MMPROJ_FILE} from ${SURYA_GGUF_REPO}..." + curl -fL -o "$MMPROJ_PATH" "https://huggingface.co/${SURYA_GGUF_REPO}/resolve/main/${SURYA_GGUF_MMPROJ_FILE}" +fi + +exec llama-server \ + -m "$MODEL_PATH" \ + --mmproj "$MMPROJ_PATH" \ + -ngl 0 \ + --host 0.0.0.0 \ + --port "${PORT:-8000}" \ + --parallel "${SURYA_INFERENCE_PARALLEL:-4}" \ + --ctx-size "${SURYA_INFERENCE_CTX_SIZE:-49152}" \ + --alias "$SURYA_MODEL_ALIAS" \ + --jinja From d293b13c0a1365ab350b02a042daf1012dbfea1d Mon Sep 17 00:00:00 2001 From: Ayan-josh-05 Date: Tue, 25 Aug 2026 13:22:56 +0530 Subject: [PATCH 3/9] fix: keep Ollama model resident to prevent /map timeouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ollama unloaded gemma4:e4b-it-qat after 5m idle, and reloading it took ~140-150s — longer than field_mapping's 120s request timeout, so every call after a gap timed out on all retries. Set OLLAMA_KEEP_ALIVE=-1 to keep the model loaded, and raise field_mapping's timeout to 300s to cover the first cold load. Co-Authored-By: Claude Sonnet 5 --- lending-poc/docker-compose.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lending-poc/docker-compose.yml b/lending-poc/docker-compose.yml index 84e31c2..4924b92 100644 --- a/lending-poc/docker-compose.yml +++ b/lending-poc/docker-compose.yml @@ -36,6 +36,12 @@ services: image: ollama/ollama:latest ports: - "11434:11434" + environment: + # Keep the model resident once loaded instead of unloading after the + # default 5m idle timeout — cold-loading this model takes 2+ minutes, + # which otherwise blows past callers' request timeouts on every call + # after a short idle gap. + OLLAMA_KEEP_ALIVE: -1 volumes: - ollama_models:/root/.ollama @@ -47,6 +53,9 @@ services: environment: OLLAMA_HOST: ${OLLAMA_HOST:-http://ollama:11434} OLLAMA_MODEL: ${OLLAMA_MODEL:-gemma4:e4b-it-qat} + # Cold model load alone can take 2+ minutes; give the first call after + # an idle gap enough headroom instead of timing out mid-load. + OLLAMA_TIMEOUT_SECONDS: ${OLLAMA_TIMEOUT_SECONDS:-300} volumes: - ./field_mapping_poc:/app depends_on: From 83c04ade622050b3b6f4cbaea06422df9ddba076 Mon Sep 17 00:00:00 2001 From: Ayan-josh-05 Date: Tue, 25 Aug 2026 13:30:20 +0530 Subject: [PATCH 4/9] Undo some changes --- .../document_processing/translation/requirements.txt | 2 +- lending-poc/frontend/src/api/extract.ts | 2 +- lending-poc/gateway/main.py | 8 +------- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/lending-poc/document_processing/translation/requirements.txt b/lending-poc/document_processing/translation/requirements.txt index b29dbac..c7d3d78 100644 --- a/lending-poc/document_processing/translation/requirements.txt +++ b/lending-poc/document_processing/translation/requirements.txt @@ -1,5 +1,5 @@ # Translation service dependencies -ollama==0.6.2 +ollama==0.4.7 # FastAPI and server fastapi==0.115.6 diff --git a/lending-poc/frontend/src/api/extract.ts b/lending-poc/frontend/src/api/extract.ts index f4aa2c1..4c787cb 100644 --- a/lending-poc/frontend/src/api/extract.ts +++ b/lending-poc/frontend/src/api/extract.ts @@ -7,7 +7,7 @@ import { extractResponseSchema, type ExtractResponse } from '@/schemas/extract.s // documents in a batch wait on earlier ones). CPU-only Surya can take // several minutes per handwritten page, so this must stay aligned with the // gateway's OCR_REQUEST_TIMEOUT_SECONDS default. -const EXTRACT_TIMEOUT_MS = 2 * 60 * 1000 +const EXTRACT_TIMEOUT_MS = 5 * 60 * 1000 /** * POSTs a single file to /extract. The backend processes one file per call, diff --git a/lending-poc/gateway/main.py b/lending-poc/gateway/main.py index 4a5755d..6e7b551 100644 --- a/lending-poc/gateway/main.py +++ b/lending-poc/gateway/main.py @@ -23,10 +23,6 @@ OCR_BASE_URL = os.environ.get("OCR_BASE_URL", "http://127.0.0.1:8010") TRANSLATION_BASE_URL = os.environ.get("TRANSLATION_BASE_URL", "http://127.0.0.1:8001") FIELD_MAPPING_BASE_URL = os.environ.get("FIELD_MAPPING_BASE_URL", "http://127.0.0.1:8002") -# Surya on a CPU-only WSL host can take several minutes per handwritten page. -# Keep this aligned with the browser timeout so the gateway does not terminate -# a valid OCR request while the OCR worker is still generating text. -OCR_REQUEST_TIMEOUT_SECONDS = float(os.environ.get("OCR_REQUEST_TIMEOUT_SECONDS", "1800")) # Headers that must not be forwarded as-is between hops (RFC 7230) plus a few # that httpx/Starlette will recompute themselves and that would otherwise @@ -41,7 +37,7 @@ @asynccontextmanager async def lifespan(app: FastAPI): - app.state.http = httpx.AsyncClient(timeout=httpx.Timeout(30.0)) + app.state.http = httpx.AsyncClient(timeout=120.0) yield await app.state.http.aclose() @@ -62,14 +58,12 @@ async def _proxy(request: Request, base_url: str, path: str) -> Response: headers = {k: v for k, v in request.headers.items() if k.lower() not in REQUEST_STRIP_HEADERS} body = await request.body() try: - timeout = OCR_REQUEST_TIMEOUT_SECONDS if base_url == OCR_BASE_URL else None upstream = await client.request( request.method, f"{base_url}{path}", headers=headers, params=list(request.query_params.multi_items()), content=body, - timeout=timeout, ) except httpx.RequestError as exc: return JSONResponse( From baa515edd76ea8911acee20a3346db8fc3fb7289 Mon Sep 17 00:00:00 2001 From: Ayan-josh-05 Date: Tue, 25 Aug 2026 16:46:12 +0530 Subject: [PATCH 5/9] feat: make GPU acceleration opt-in for Surya OCR and Ollama via compose profiles Adds a "gpu" compose profile (alongside the default "cpu" one, toggled via COMPOSE_PROFILES in .env) that requests GPU device access for surya-inference, ocr, and ollama. Each pair of variants shares a network alias so downstream services never need to know which is active. - surya-inference: optional CUDA build of llama.cpp (build args), with entrypoint.sh probing nvidia-smi at startup to pick -ngl and falling back to CPU even if GPU access was requested but isn't actually there. - ocr: optional CUDA torch wheel (build arg) instead of the pinned CPU one. - ollama: GPU device reservation only; the official image already auto-detects CUDA and falls back to CPU on its own. - Fixes entrypoint.sh CRLF line endings (broke its shebang when checked out on Windows) and adds .gitattributes to keep shell scripts LF going forward. --- .gitattributes | 1 + lending-poc/docker-compose.yml | 105 +++++++++++++++++- .../document_processing/ocr/Dockerfile | 19 +++- lending-poc/document_processing/ocr/README.md | 18 +++ lending-poc/surya-inference/Dockerfile | 27 +++-- lending-poc/surya-inference/entrypoint.sh | 14 ++- 6 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/lending-poc/docker-compose.yml b/lending-poc/docker-compose.yml index 4924b92..965aaea 100644 --- a/lending-poc/docker-compose.yml +++ b/lending-poc/docker-compose.yml @@ -32,8 +32,14 @@ services: db: condition: service_healthy + # Ollama's own image already auto-detects CUDA at runtime and falls back + # to CPU on its own — no custom build needed here, just the same cpu/gpu + # profile split used for surya-inference/ocr below, so GPU access is only + # requested when COMPOSE_PROFILES=gpu. ollama: + &ollama image: ollama/ollama:latest + profiles: ["cpu"] ports: - "11434:11434" environment: @@ -45,6 +51,21 @@ services: volumes: - ollama_models:/root/.ollama + ollama-gpu: + <<: *ollama + profiles: ["gpu"] + networks: + default: + aliases: + - ollama + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + field_mapping: build: ./field_mapping_poc command: uvicorn api:app --host 0.0.0.0 --port 8002 --reload @@ -59,7 +80,12 @@ services: volumes: - ./field_mapping_poc:/app depends_on: - - ollama + ollama: + condition: service_started + required: false + ollama-gpu: + condition: service_started + required: false translation: build: ./document_processing/translation @@ -72,10 +98,23 @@ services: volumes: - ./document_processing/translation:/app depends_on: - - ollama + ollama: + condition: service_started + required: false + ollama-gpu: + condition: service_started + required: false + # surya-inference and ocr each come in a "cpu" and "gpu" variant, selected + # via COMPOSE_PROFILES in .env (defaults to "cpu" so plain `docker compose + # up` always works). Both variants of a pair share a network alias so + # downstream services (SURYA_INFERENCE_URL, OCR_BASE_URL) don't need to + # know which one is active. See document_processing/ocr/README.md for GPU + # prerequisites (NVIDIA Container Toolkit / WSL GPU passthrough). surya-inference: + &surya-inference build: ./surya-inference + profiles: ["cpu"] ports: - "8500:8000" environment: @@ -84,8 +123,31 @@ services: volumes: - surya_models:/models + surya-inference-gpu: + <<: *surya-inference + profiles: ["gpu"] + build: + context: ./surya-inference + args: + BASE_IMAGE: nvidia/cuda:12.4.1-devel-ubuntu22.04 + RUNTIME_IMAGE: nvidia/cuda:12.4.1-runtime-ubuntu22.04 + GGML_CUDA: "ON" + networks: + default: + aliases: + - surya-inference + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + ocr: + &ocr build: ./document_processing/ocr + profiles: ["cpu"] command: uvicorn api:app --host 0.0.0.0 --port 8010 --reload ports: - "8010:8010" @@ -95,7 +157,31 @@ services: volumes: - ./document_processing/ocr:/app depends_on: - - surya-inference + surya-inference: + condition: service_started + required: false + surya-inference-gpu: + condition: service_started + required: false + + ocr-gpu: + <<: *ocr + profiles: ["gpu"] + build: + context: ./document_processing/ocr + args: + GPU: "1" + networks: + default: + aliases: + - ocr + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] gateway: build: ./gateway @@ -109,9 +195,16 @@ services: volumes: - ./gateway:/app depends_on: - - field_mapping - - ocr - - translation + field_mapping: + condition: service_started + ocr: + condition: service_started + required: false + ocr-gpu: + condition: service_started + required: false + translation: + condition: service_started frontend: build: ./frontend diff --git a/lending-poc/document_processing/ocr/Dockerfile b/lending-poc/document_processing/ocr/Dockerfile index 7f63ad4..b4a2da8 100644 --- a/lending-poc/document_processing/ocr/Dockerfile +++ b/lending-poc/document_processing/ocr/Dockerfile @@ -2,10 +2,21 @@ FROM python:3.12-slim WORKDIR /app -# Surya's recognition/layout models run in-process here (CPU). The LLM half -# runs in the separate surya-inference container, reached via -# SURYA_INFERENCE_URL — this image does not need llama-server. -RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu +# Surya's recognition/layout models run in-process here. The LLM half runs +# in the separate surya-inference container, reached via SURYA_INFERENCE_URL +# — this image does not need llama-server. +# +# CPU by default: pin the CPU-only torch wheel so the image doesn't pull +# CUDA deps it can't use. Paired with the "gpu" compose profile, build with +# --build-arg GPU=1 to skip the pin and let requirements.txt's surya-ocr +# pull the default (CUDA-enabled) torch wheel instead — PyTorch's GPU wheels +# bundle their own CUDA runtime libs, so no CUDA base image is needed here, +# only GPU device access from the container (which the compose profile +# grants) and a host NVIDIA driver. +ARG GPU=0 +RUN if [ "$GPU" = "0" ]; then \ + pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu; \ + fi COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt diff --git a/lending-poc/document_processing/ocr/README.md b/lending-poc/document_processing/ocr/README.md index 15725e6..c0a24b5 100644 --- a/lending-poc/document_processing/ocr/README.md +++ b/lending-poc/document_processing/ocr/README.md @@ -205,6 +205,24 @@ make status # Show project status backend and Docker/GPU passthrough. The API now verifies this at startup, before reporting `/health` as healthy. +### Running via Docker Compose (GPU or CPU) + +The top-level `docker-compose.yml` runs this service (and its `surya-inference` +backend) in a CPU or GPU variant, picked by `COMPOSE_PROFILES` in `.env`: + +- `COMPOSE_PROFILES=cpu` (default) — always works, no GPU required. +- `COMPOSE_PROFILES=gpu` — requires an NVIDIA GPU on the host plus the + [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) + (and, on Windows, WSL2 GPU passthrough). Both containers detect GPU access + at startup and fall back to CPU automatically if it isn't actually there, + so setting the profile without a working toolkit degrades to CPU rather + than failing outright — but `docker compose up` itself will fail to create + the containers if the toolkit isn't installed at all, since the GPU device + reservation can't be satisfied. + +Switch profiles by editing `COMPOSE_PROFILES` in `lending-poc/.env`, then +`docker compose up --build`. + ### Dependencies The project includes both core OCR dependencies and API dependencies: diff --git a/lending-poc/surya-inference/Dockerfile b/lending-poc/surya-inference/Dockerfile index 94ef34b..100edbe 100644 --- a/lending-poc/surya-inference/Dockerfile +++ b/lending-poc/surya-inference/Dockerfile @@ -1,8 +1,21 @@ -# Builds llama.cpp's CPU `llama-server` from source and serves it standalone, -# so ocr-api can point SURYA_INFERENCE_URL here instead of spawning its own -# copy in-process (see surya/inference/backends/llamacpp.py in the ocr-api -# image for the equivalent in-process spawn logic this mirrors). -FROM python:3.12-slim AS build +# Builds llama.cpp's `llama-server` from source and serves it standalone, so +# ocr-api can point SURYA_INFERENCE_URL here instead of spawning its own copy +# in-process (see surya/inference/backends/llamacpp.py in the ocr-api image +# for the equivalent in-process spawn logic this mirrors). +# +# CPU by default. For a CUDA-capable build (paired with the "gpu" compose +# profile), pass: +# --build-arg BASE_IMAGE=nvidia/cuda:12.4.1-devel-ubuntu22.04 +# --build-arg RUNTIME_IMAGE=nvidia/cuda:12.4.1-runtime-ubuntu22.04 +# --build-arg GGML_CUDA=ON +# entrypoint.sh still probes for a visible GPU at container start and falls +# back to -ngl 0 (CPU) if none is found, so this image works either way. +ARG BASE_IMAGE=python:3.12-slim +ARG RUNTIME_IMAGE=python:3.12-slim + +FROM ${BASE_IMAGE} AS build + +ARG GGML_CUDA=OFF RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential cmake git ca-certificates \ @@ -10,10 +23,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN git clone --depth 1 https://github.com/ggml-org/llama.cpp /llama.cpp WORKDIR /llama.cpp -RUN cmake -B build -DGGML_NATIVE=OFF -DLLAMA_CURL=OFF -DCMAKE_BUILD_TYPE=Release \ +RUN cmake -B build -DGGML_NATIVE=OFF -DGGML_CUDA=${GGML_CUDA} -DLLAMA_CURL=OFF -DCMAKE_BUILD_TYPE=Release \ && cmake --build build -j"$(nproc)" --target llama-server -FROM python:3.12-slim +FROM ${RUNTIME_IMAGE} RUN apt-get update && apt-get install -y --no-install-recommends \ curl ca-certificates libgomp1 \ diff --git a/lending-poc/surya-inference/entrypoint.sh b/lending-poc/surya-inference/entrypoint.sh index 7d640bd..84a64c1 100644 --- a/lending-poc/surya-inference/entrypoint.sh +++ b/lending-poc/surya-inference/entrypoint.sh @@ -19,10 +19,22 @@ if [ ! -f "$MMPROJ_PATH" ]; then curl -fL -o "$MMPROJ_PATH" "https://huggingface.co/${SURYA_GGUF_REPO}/resolve/main/${SURYA_GGUF_MMPROJ_FILE}" fi +# nvidia-smi only shows up here if the container was actually started with +# GPU access (via the "gpu" compose profile + NVIDIA Container Toolkit) - +# so this doubles as the CPU/GPU decision regardless of how the image was +# built, and fails safe to CPU if GPU access was requested but isn't there. +if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then + echo "GPU detected - offloading layers to GPU" + NGL="${SURYA_INFERENCE_NGL:-999}" +else + echo "No GPU detected - running on CPU" + NGL=0 +fi + exec llama-server \ -m "$MODEL_PATH" \ --mmproj "$MMPROJ_PATH" \ - -ngl 0 \ + -ngl "$NGL" \ --host 0.0.0.0 \ --port "${PORT:-8000}" \ --parallel "${SURYA_INFERENCE_PARALLEL:-4}" \ From 007e5a86f133e1caf7a589b14c74f095e03dedb9 Mon Sep 17 00:00:00 2001 From: Ayan-josh-05 Date: Tue, 25 Aug 2026 16:47:36 +0530 Subject: [PATCH 6/9] Added .env.example file --- lending-poc/.env.example | 12 ++++++++++++ lending-poc/Database_setup.md | 4 ++-- lending-poc/field_mapping_poc/.env.example | 3 +++ lending-poc/frontend/.env.example | 2 ++ 4 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 lending-poc/.env.example create mode 100644 lending-poc/field_mapping_poc/.env.example create mode 100644 lending-poc/frontend/.env.example diff --git a/lending-poc/.env.example b/lending-poc/.env.example new file mode 100644 index 0000000..8c90e26 --- /dev/null +++ b/lending-poc/.env.example @@ -0,0 +1,12 @@ +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=lending_poc +POSTGRES_HOST_PORT=55439 +DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:55439/lending_poc +ENCRYPTION_KEY=changeme-generate-a-base64-fernet-key +DEBUG=true + +# Selects the surya-inference/ocr variant docker-compose.yml runs: "cpu" +# (default, always works) or "gpu" (requires an NVIDIA GPU + NVIDIA +# Container Toolkit / WSL GPU passthrough on the host). +COMPOSE_PROFILES=cpu diff --git a/lending-poc/Database_setup.md b/lending-poc/Database_setup.md index 35d443e..d8304a3 100644 --- a/lending-poc/Database_setup.md +++ b/lending-poc/Database_setup.md @@ -20,7 +20,7 @@ pip install -e ".[dev]" Create a `.env` file in the project root: ```bash -DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:55432/lending_poc +DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:55439/lending_poc ENCRYPTION_KEY=<32-byte base64 key> DEBUG=true ``` @@ -39,7 +39,7 @@ python3 -c "import base64, os; print(base64.b64encode(os.urandom(32)).decode())" docker compose up -d db ``` -This starts Postgres with pgvector on host port `55432` (mapped from container port `5432`), and waits until it reports healthy. +This starts Postgres with pgvector on host port `55439` (mapped from container port `5432`), and waits until it reports healthy. ## 4. Run database migrations diff --git a/lending-poc/field_mapping_poc/.env.example b/lending-poc/field_mapping_poc/.env.example new file mode 100644 index 0000000..12ae354 --- /dev/null +++ b/lending-poc/field_mapping_poc/.env.example @@ -0,0 +1,3 @@ +DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:55432/lending_poc +ENCRYPTION_KEY=changeme-generate-a-base64-fernet-key +DEBUG=true diff --git a/lending-poc/frontend/.env.example b/lending-poc/frontend/.env.example new file mode 100644 index 0000000..fe75629 --- /dev/null +++ b/lending-poc/frontend/.env.example @@ -0,0 +1,2 @@ +VITE_API_BASE_URL=http://localhost:8000 +VITE_TRANSLATION_API_BASE_URL=http://localhost:8000 From e948b5b77f1003a7fbfd9109704b37fbf1053c5e Mon Sep 17 00:00:00 2001 From: Ayan-josh-05 Date: Wed, 26 Aug 2026 13:19:40 +0530 Subject: [PATCH 7/9] fix: pin torchvision to CPU wheel in OCR Dockerfile torch was already pinned to the CPU-only index, but torchvision was left to resolve from default PyPI via surya-ocr's dependency, pulling in a CUDA-linked build. Mismatched torch/torchvision builds can break torchvision's compiled ops at runtime. Co-Authored-By: Claude Sonnet 5 --- lending-poc/document_processing/ocr/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lending-poc/document_processing/ocr/Dockerfile b/lending-poc/document_processing/ocr/Dockerfile index b4a2da8..0c068b3 100644 --- a/lending-poc/document_processing/ocr/Dockerfile +++ b/lending-poc/document_processing/ocr/Dockerfile @@ -15,7 +15,7 @@ WORKDIR /app # grants) and a host NVIDIA driver. ARG GPU=0 RUN if [ "$GPU" = "0" ]; then \ - pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu; \ + pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu; \ fi COPY requirements.txt . From bee85e01d4c60d8fe9d39e41ac9afae627ce2f05 Mon Sep 17 00:00:00 2001 From: Ayan-josh-05 Date: Wed, 26 Aug 2026 15:27:00 +0530 Subject: [PATCH 8/9] docs: add top-level README for running the full stack with Docker Consolidates setup instructions (env config, migrations, Ollama model pull, service ports, and GPU-profile usage) that were previously scattered across Database_setup.md and the OCR README, into one guide for a fresh clone. Co-Authored-By: Claude Sonnet 5 --- lending-poc/README.md | 140 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 lending-poc/README.md diff --git a/lending-poc/README.md b/lending-poc/README.md new file mode 100644 index 0000000..88e8ac8 --- /dev/null +++ b/lending-poc/README.md @@ -0,0 +1,140 @@ +# Lending POC + +Lending POC — FastAPI backend + PostgreSQL (pgvector), plus a document +processing pipeline (OCR, translation, field mapping) fronted by a gateway, +and a React frontend. This guide covers running the **entire stack in +Docker**. + +For running the `app` service natively against a containerized DB only +(e.g. for backend development with hot-reload outside Docker), see +[Database_setup.md](Database_setup.md). + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) and Docker Compose v2 + (`docker compose version`) +- Git +- **Optional, for GPU acceleration**: an NVIDIA GPU, the + [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html), + and (on Windows) WSL2 with GPU passthrough enabled + +## 1. Clone and configure environment + +```bash +git clone +cd lending-poc +cp .env.example .env +``` + +Generate a real `ENCRYPTION_KEY` — the app uses it to encrypt sensitive +database fields, and the placeholder value in `.env.example` is not a valid +key: + +```bash +python3 -c "import base64, os; print(base64.b64encode(os.urandom(32)).decode())" +``` + +Paste the result into `ENCRYPTION_KEY=` in `.env`. + +Leave `COMPOSE_PROFILES=cpu` as-is unless you have a working GPU setup — +see [Using a GPU](#using-a-gpu) below. + +## 2. Start the stack + +```bash +docker compose up --build -d +``` + +This builds and starts every service: `db`, `app`, `ollama`, `field_mapping`, +`translation`, `surya-inference` + `ocr`, `gateway`, and `frontend`. + +The first run takes a while — Ollama and Surya both download models on +first use. Watch progress with: + +```bash +docker compose logs -f +``` + +## 3. Run database migrations + +The `app` container doesn't run migrations automatically on startup: + +```bash +docker compose exec app alembic -c db/alembic.ini upgrade head +``` + +## 4. Pull the Ollama model + +Needed by the `translation` and `field_mapping` services: + +```bash +docker compose exec ollama ollama pull gemma4:e4b-it-qat +``` + +(Substitute whatever `OLLAMA_MODEL` is set to in `.env` if you changed it. +If you're running the GPU profile, use `ollama-gpu` instead of `ollama` in +the command above.) + +## 5. Verify it's running + +| Service | URL | Notes | +|---|---|---| +| Frontend | http://localhost:5173 | Main UI | +| Gateway | http://localhost:8080 | Fronts OCR / translation / field-mapping | +| App (backend API) | http://localhost:8000 | Docs at `/docs`; health at `/health` | +| Postgres | localhost:55439 | pgvector-enabled | +| OCR | http://localhost:8010 | Not normally called directly | +| Translation | http://localhost:8001 | Not normally called directly | +| Field mapping | http://localhost:8002 | Not normally called directly | +| Surya inference | http://localhost:8500 | OCR's inference backend | + +There are effectively two subsystems sharing this compose file: the +`app` + `db` lending backend, and a separate OCR/translation/field-mapping +pipeline fronted by `gateway`. The frontend talks to the gateway for +document processing and to the app for everything else. + +## Using a GPU + +`surya-inference`/`ocr` and `ollama` each come in a CPU and a GPU variant, +selected by `COMPOSE_PROFILES` in `.env`: + +- `COMPOSE_PROFILES=cpu` (default) — always works, no GPU required. +- `COMPOSE_PROFILES=gpu` — requires an NVIDIA GPU on the host plus the + NVIDIA Container Toolkit (and, on Windows, WSL2 GPU passthrough). + +To switch: + +```bash +# in .env +COMPOSE_PROFILES=gpu +``` + +```bash +docker compose up --build -d +``` + +Both GPU containers detect GPU access at startup and fall back to CPU +automatically if it isn't actually usable — but `docker compose up` will +fail to create the containers at all if the toolkit isn't installed, +since the GPU device reservation can't be satisfied. + +Ollama's own image auto-detects CUDA at runtime with no separate build, so +switching the profile is enough for it; `surya-inference`/`ocr` are built +from CUDA base images specifically for the `gpu` profile (see +[docker-compose.yml](docker-compose.yml) and +[document_processing/ocr/README.md](document_processing/ocr/README.md) +for details). + +## Stopping and cleanup + +```bash +docker compose down +``` + +Add `-v` to also delete the named volumes (`pgdata`, `ollama_models`, +`surya_models`) — this wipes the database and downloaded models, so only +do this if you want a clean slate: + +```bash +docker compose down -v +``` \ No newline at end of file From a4e03c0e2268d6ccc42788a61582af898dec4de8 Mon Sep 17 00:00:00 2001 From: Ayan-josh-05 Date: Wed, 26 Aug 2026 15:28:20 +0530 Subject: [PATCH 9/9] docs: document OLLAMA_MODEL/OLLAMA_HOST in .env.example Surfaces the Ollama model config used by translation and field_mapping, with a note on pulling the model into the container before first use. Co-Authored-By: Claude Sonnet 5 --- lending-poc/.env.example | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lending-poc/.env.example b/lending-poc/.env.example index 8c90e26..5419649 100644 --- a/lending-poc/.env.example +++ b/lending-poc/.env.example @@ -10,3 +10,9 @@ DEBUG=true # (default, always works) or "gpu" (requires an NVIDIA GPU + NVIDIA # Container Toolkit / WSL GPU passthrough on the host). COMPOSE_PROFILES=cpu + +# Model used by the translation and field_mapping services via Ollama. +# Must be pulled into the ollama container first: +# docker compose exec ollama ollama pull +OLLAMA_MODEL=gemma4:e4b-it-qat +# OLLAMA_HOST=http://ollama:11434