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.
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.
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:
HTTP1 2 3POST /ocr Content-Type: multipart/form-data Body: file=faktura.pdf
Przykładowa odpowiedź:
JSON1 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ą:
Python1 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:
Bash1apt-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.
Python1 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 49import 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.
Python1 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 52from 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.
Dockerfile1 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 30FROM 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:
YAML1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20services: 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:
Bash1docker compose up -d
Sprawdź, czy działa:
Bash1curl http://localhost:8080/health
Oczekiwana odpowiedź:
JSON1{"ok":true}
Krok 6: Test z PDF-em
Możesz przetestować serwis prostym skryptem w Pythonie.
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15import 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ą:
JSON1 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:
Bash1 2 3git clone https://github.com/twoje-repo/pdf-reader.git cd pdf-reader docker compose up -d
Sprawdź status kontenera:
Bash1docker ps
Sprawdź endpoint:
Bash1curl 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:
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23from 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:
YAML1 2 3 4environment: - 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
| Metoda | Koszt | Prywatność | Szybkość |
|---|---|---|---|
| OpenAI Vision API | koszt za przetworzenie strony | dokumenty trafiają do zewnętrznego API | szybko |
| Zewnętrzny serwis OCR | zwykle abonament | zależnie od dostawcy | szybko |
| Własny serwis OCR | koszt VPS-a lub własnego serwera | dokumenty zostają u Ciebie | szybko |
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 →
