Własny OCR do faktur i PDF-ów w n8n — FastAPI, Python i Docker

Kacper Sieradziński
Kacper4 min czytania
Streszczenie
  • Film: Własny serwis OCR dla PDF-ów — FastAPI + Python + D...
  • Co zbudujemy?
  • Dlaczego nie od razu OCR?
  • Zależności
Własny OCR do faktur i PDF-ów w n8n — FastAPI, Python i Docker

Masz dziesiątki albo setki faktur miesięcznie i chcesz automatycznie wyciągać z nich dane w n8n?

Możesz wysyłać każdy PDF do zewnętrznego API, ale wtedy płacisz za każde przetworzenie i przekazujesz dokumenty finansowe poza własną infrastrukturę.

Jest też prostsza opcja: własny serwis OCR postawiony na VPS-ie lub lokalnym serwerze. Przyjmuje PDF albo obraz, rozpoznaje tekst i zwraca wynik jako JSON. Możesz podpiąć go bezpośrednio do workflow w n8n.

Szkolenia dla developerów · 30 min

Przeszkól zespół developerski z AI, które przyspiesza kod

Claude Code, LangChain, OpenAI API, n8n — warsztat na Waszym repo. Zakres ustalimy na bezpłatnej rozmowie.

Kacper Sieradziński · founder Dokodu
4,9 · zwykle odpowiada w 2h

Wybierz dogodny termin bezpłatnej rozmowy (30 min).

Umów bezpłatną rozmowę

Film: Własny serwis OCR dla PDF-ów — FastAPI + Python + Docker

Poniżej znajdziesz kompletny tutorial. Możesz go przejść bez oglądania filmu.

Co zbudujemy?

Zbudujemy prosty serwis OCR, który:

  • przyjmuje plik PDF lub obraz,
  • sprawdza, czy PDF ma warstwę tekstową,
  • jeśli ma — wyciąga tekst bez OCR,
  • jeśli nie ma — renderuje strony do obrazów i uruchamia Tesseract,
  • zwraca wynik jako JSON,
  • działa w Dockerze,
  • nadaje się do podpięcia pod n8n.

Przykładowe wywołanie:

HTTP
1 2 3 POST /ocr Content-Type: multipart/form-data Body: file=faktura.pdf

Przykładowa odpowiedź:

JSON
1 2 3 4 5 { "pages": 2, "ocr_used": false, "text": "Faktura VAT nr 2024/12/001..." }

Dlaczego nie od razu OCR?

Nie każdy PDF jest skanem.

Część plików PDF ma normalną warstwę tekstową. Wtedy nie trzeba uruchamiać OCR. Wystarczy odczytać tekst bezpośrednio z dokumentu. To szybsze, tańsze obliczeniowo i zwykle dokładniejsze.

Dlatego serwis działa według prostej logiki:

  • PDF z tekstem → pobieramy tekst bezpośrednio,
  • PDF jako skan → renderujemy strony do obrazów i używamy Tesseract OCR,
  • obraz → od razu wysyłamy do Tesseract.

Przykład funkcji sprawdzającej, czy PDF ma warstwę tekstową:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 # ocr_utils.py import fitz # PyMuPDF def has_text_layer(pdf_bytes: bytes, threshold: int = 50) -> bool: """Sprawdza, czy PDF zawiera warstwę tekstową.""" doc = fitz.open(stream=pdf_bytes, filetype="pdf") for page in doc: text = page.get_text() if len(text.strip()) > threshold: return True return False

Jeśli tekstu jest więcej niż ustalony próg, traktujemy stronę jako tekstową. Jeśli nie — uruchamiamy OCR.

Zależności

W projekcie użyjemy:

fastapi
uvicorn[standard]
pymupdf
pytesseract
Pillow
python-multipart

Potrzebny jest też systemowy Tesseract OCR z językami polskim i angielskim:

Bash
1 apt-get install -y tesseract-ocr tesseract-ocr-pol tesseract-ocr-eng

Struktura projektu

Prosty układ plików może wyglądać tak:

ocr-service/
├── app.py
├── ocr_utils.py
├── requirements.txt
├── Dockerfile
└── docker-compose.yml

Krok 1: Logika OCR

Najpierw tworzymy plik ocr_utils.py. To tutaj obsługujemy PDF-y, obrazy i decyzję, czy OCR jest potrzebny.

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import fitz import pytesseract from PIL import Image import io def extract_text(file_bytes: bytes, filename: str) -> dict: ext = filename.rsplit(".", 1)[-1].lower() if ext == "pdf": return _process_pdf(file_bytes) img = Image.open(io.BytesIO(file_bytes)) text = pytesseract.image_to_string(img, lang="pol+eng") return { "pages": 1, "ocr_used": True, "text": text } def _process_pdf(pdf_bytes: bytes) -> dict: doc = fitz.open(stream=pdf_bytes, filetype="pdf") all_text = [] ocr_used = False for page in doc: text = page.get_text().strip() if len(text) > 50: all_text.append(text) continue ocr_used = True pix = page.get_pixmap(dpi=300) img_bytes = pix.tobytes("png") img = Image.open(io.BytesIO(img_bytes)) ocr_text = pytesseract.image_to_string(img, lang="pol+eng") all_text.append(ocr_text) return { "pages": len(doc), "ocr_used": ocr_used, "text": "\n\n".join(all_text) }

Najważniejszy element: używamy PyMuPDF zarówno do odczytu tekstu, jak i do renderowania stron PDF do obrazów. Dzięki temu nie potrzebujemy pdf2image ani Popplera.

Krok 2: API w FastAPI

Teraz tworzymy app.py. Serwis ma dwa endpointy:

  • GET /health — sprawdzenie, czy aplikacja działa,
  • POST /ocr — przesłanie pliku i zwrócenie tekstu.
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 from fastapi import FastAPI, UploadFile, File, HTTPException from fastapi.middleware.cors import CORSMiddleware from ocr_utils import extract_text import os app = FastAPI(title="OCR Service") app.add_middleware( CORSMiddleware, allow_origins=["*"], # w produkcji ogranicz do adresu swojego n8n allow_methods=["*"], allow_headers=["*"], ) ALLOWED_EXTENSIONS = {"pdf", "png", "jpg", "jpeg", "tiff"} @app.get("/health") def health(): return {"ok": True} @app.post("/ocr") async def ocr_endpoint(file: UploadFile = File(...)): if not file: raise HTTPException(status_code=400, detail="Brak pliku") ext = file.filename.rsplit(".", 1)[-1].lower() if ext not in ALLOWED_EXTENSIONS: raise HTTPException( status_code=400, detail=f"Nieobsługiwany format. Dozwolone: {', '.join(ALLOWED_EXTENSIONS)}" ) contents = await file.read() return extract_text(contents, file.filename) if __name__ == "__main__": import uvicorn port = int(os.getenv("PORT", 8080)) workers = int(os.getenv("WORKERS", 2)) uvicorn.run( "app:app", host="0.0.0.0", port=port, workers=workers )

Krok 3: requirements.txt

Dodaj plik requirements.txt:

fastapi
uvicorn[standard]
pymupdf
pytesseract
Pillow
python-multipart

Krok 4: Dockerfile

Teraz zamykamy aplikację w Dockerze.

Dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 FROM python:3.13-slim RUN apt-get update && apt-get install -y \ tesseract-ocr \ tesseract-ocr-pol \ tesseract-ocr-eng \ libgl1 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* RUN useradd -m appuser WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN chown -R appuser:appuser /app USER appuser ENV PORT=8080 ENV WORKERS=2 ENV TIMEOUT=120 HEALTHCHECK --interval=30s --timeout=10s \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" CMD uvicorn app:app --host 0.0.0.0 --port $PORT --workers $WORKERS

Ważne: aplikacja działa jako użytkownik bez uprawnień root. To prosty, ale istotny element bezpieczeństwa.

Krok 5: Docker Compose

Dodaj plik docker-compose.yml:

YAML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 services: ocr-service: build: . ports: - "8080:8080" environment: - PORT=8080 - WORKERS=2 restart: unless-stopped healthcheck: test: [ "CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" ] interval: 30s timeout: 10s retries: 3

Uruchom serwis:

Bash
1 docker compose up -d

Sprawdź, czy działa:

Bash
1 curl http://localhost:8080/health

Oczekiwana odpowiedź:

JSON
1 {"ok":true}

Krok 6: Test z PDF-em

Możesz przetestować serwis prostym skryptem w Pythonie.

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import requests with open("faktura.pdf", "rb") as f: response = requests.post( "http://localhost:8080/ocr", files={ "file": ("faktura.pdf", f, "application/pdf") } ) result = response.json() print(f"Strony: {result['pages']}") print(f"OCR użyty: {result['ocr_used']}") print(f"Tekst: {result['text'][:200]}...")

Przykładowy wynik dla PDF-a z warstwą tekstową:

JSON
1 2 3 4 5 { "pages": 1, "ocr_used": false, "text": "Faktura VAT 2024/12/001\nNabywca: Firma XYZ..." }

Deploy na VPS

Po lokalnym teście możesz przenieść serwis na VPS, np. Hostinger, Hetzner, DigitalOcean albo inny serwer z Dockerem.

Na serwerze:

Bash
1 2 3 git clone https://github.com/twoje-repo/pdf-reader.git cd pdf-reader docker compose up -d

Sprawdź status kontenera:

Bash
1 docker ps

Sprawdź endpoint:

Bash
1 curl http://twoj-ip:8080/health

Co warto dodać w produkcji?

Przed użyciem serwisu na prawdziwych dokumentach firmowych dodaj kilka zabezpieczeń:

  • ogranicz CORS do adresu swojego n8n,
  • dodaj API key w nagłówku,
  • ustaw limit rozmiaru pliku,
  • nie loguj treści dokumentów,
  • nie wystawiaj serwisu publicznie bez autoryzacji,
  • najlepiej trzymaj go w prywatnej sieci razem z n8n.

Minimalna autoryzacja przez API key może wyglądać tak:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from fastapi import Header API_KEY = os.getenv("API_KEY") @app.post("/ocr") async def ocr_endpoint( file: UploadFile = File(...), x_api_key: str = Header(default=None) ): if API_KEY and x_api_key != API_KEY: raise HTTPException(status_code=401, detail="Unauthorized") ext = file.filename.rsplit(".", 1)[-1].lower() if ext not in ALLOWED_EXTENSIONS: raise HTTPException( status_code=400, detail=f"Nieobsługiwany format. Dozwolone: {', '.join(ALLOWED_EXTENSIONS)}" ) contents = await file.read() return extract_text(contents, file.filename)

W Docker Compose dodaj wtedy:

YAML
1 2 3 4 environment: - PORT=8080 - WORKERS=2 - API_KEY=twoj-tajny-klucz

Integracja z n8n

W n8n dodaj węzeł HTTP Request. Ustawienia:

  • Method: POST
  • URL: http://twoj-serwer:8080/ocr
  • Body Content Type: Form Data
  • Body Parameter: file
  • Type: n8n Binary File
  • Input Data Field Name: nazwa pola z plikiem z poprzedniego node'a

Jeśli dodałeś API key, ustaw też nagłówek:

x-api-key: twoj-tajny-klucz

Przykładowy workflow:

Trigger: nowy plik w Google Drive
    ↓
Download File
    ↓
HTTP Request: POST /ocr
    ↓
JSON z tekstem dokumentu
    ↓
Extract Data: kod, regex albo model językowy
    ↓
Zapis do bazy, arkusza albo CRM

Co można zrobić z tekstem po OCR?

Po wyciągnięciu tekstu możesz w n8n automatycznie:

  • odczytać numer faktury,
  • wyciągnąć NIP sprzedawcy i nabywcy,
  • pobrać datę wystawienia i termin płatności,
  • odczytać kwotę netto, VAT i brutto,
  • porównać dane z zamówieniem,
  • zapisać wynik do Google Sheets, Airtable, Notion, CRM albo bazy danych,
  • przekazać tekst do modelu AI tylko wtedy, gdy naprawdę jest to potrzebne.

To ważne, bo nie każdy etap musi być obsługiwany przez AI. Proste dane często da się wyciągnąć szybciej i taniej zwykłym kodem albo wyrażeniami regularnymi.

Porównanie kosztów

MetodaKosztPrywatnośćSzybkość
OpenAI Vision APIkoszt za przetworzenie stronydokumenty trafiają do zewnętrznego APIszybko
Zewnętrzny serwis OCRzwykle abonamentzależnie od dostawcyszybko
Własny serwis OCRkoszt VPS-a lub własnego serweradokumenty zostają u Ciebieszybko

Przy małej liczbie dokumentów zewnętrzne API może być wygodne. Przy większej skali albo dokumentach wrażliwych własny OCR daje większą kontrolę nad kosztami, bezpieczeństwem i przepływem danych.

Kiedy to rozwiązanie ma sens?

Własny serwis OCR sprawdzi się szczególnie wtedy, gdy:

  • przetwarzasz faktury, umowy, zamówienia albo skany dokumentów,
  • chcesz ograniczyć wysyłanie danych poza firmę,
  • masz powtarzalny proces w n8n,
  • chcesz zmniejszyć liczbę płatnych wywołań do zewnętrznych API,
  • potrzebujesz prostego endpointu, który może działać w tle.

To nie musi być duży system. W wielu przypadkach wystarczy mały serwis, który robi jedną rzecz dobrze: przyjmuje dokument i zwraca tekst.

Dalej w tym klastrze

Chcesz zautomatyzować przetwarzanie dokumentów w swojej firmie? Porozmawiajmy →

Tagi

#AI#automatyzacja#Python#n8n#FastAPI#OCR