Przejdź do głównej zawartości

Jak zbudować backend OCR

Ten przewodnik jest dla zespołu, który pisze usługę OCR. Sam kontrakt HTTP opisuje strona Kontrakt API — tutaj chodzi o to, jak go spełnić.

Budujesz usługę zleceń: przyjmujesz plik, natychmiast zwracasz identyfikator, przetwarzasz w tle, a wynik wydajesz przez odpytanie. Sancho nie zna Twojej technologii — liczy się tylko zgodność z dwoma endpointami i schematem JSON.

  1. Użytkownik wgrywa PDF lub zdjęcie w Sancho. Plik trafia do S3, a w bazie powstaje wpis o statusie pending.

  2. Sancho wysyła plik na Twój endpoint przyjęcia (POST). Odpowiadasz od razu identyfikatorem zlecenia — nie czekasz na zakończenie OCR.

  3. Twój worker przetwarza dokument w tle.

  4. Sancho odpytuje endpoint statusu co OCR_POLL_INTERVAL_MS (domyślnie 3 s). Dopóki zwracasz pending/processing/queued, odpytywanie trwa.

  5. Gdy zwrócisz ready wraz z data, Sancho waliduje wynik schematem, zapisuje go i uruchamia dalsze kroki (podpięcie dokumentu, uzupełnienie formularza).

  6. error, przekroczenie limitu odpytań albo niezgodność ze schematem kończą się oznaczeniem dokumentu jako błędny. Użytkownik może wgrać plik ponownie — Sancho nie ponawia zlecenia samodzielnie.

Etap Limit
Odpowiedź na POST (przyjęcie) 60 s, ale celuj w kilka sekund
Odpowiedź na GET (status) 15 s
Całe rozpoznanie OCR_POLL_INTERVAL_MS × OCR_POLL_MAX_ATTEMPTS, domyślnie 3 minuty

Domyślne 3000 ms × 60 to dokładnie te 3 minuty. Możesz rozłożyć budżet inaczej (np. 5000 ms × 36), ale nie da się go wydłużyć. Dokumenty wymagające dłuższej obróbki trzeba przetworzyć wstępnie, zanim trafią do Sancho.

Pojedyncza nieudana próba odpytania nie przerywa przetwarzania — Sancho ponawia ją w ramach pozostałego budżetu. Chwilowa awaria sieci nie zepsuje więc całego zlecenia.

Minimalna usługa w FastAPI. Trzyma zlecenia w pamięci i nie ma uwierzytelniania — to szkielet pokazujący kształt kontraktu, nie kod produkcyjny.

import uuid
from fastapi import BackgroundTasks, FastAPI, Form, HTTPException, UploadFile
app = FastAPI()
jobs: dict[str, dict] = {}
def recognize(job_id: str, content: bytes, document_type: str) -> None:
try:
# Tu wchodzi Docling / Twój pipeline. Wynik musi być zgodny ze schematem.
jobs[job_id] = {"status": "ready", "data": parse(content, document_type)}
except Exception as error:
jobs[job_id] = {"status": "error", "message": str(error)}
@app.post("/ocr/documents")
async def submit(background: BackgroundTasks, file: UploadFile, documentType: str = Form(...)) -> dict:
if documentType not in {"purchase_order", "goods_received_source"}:
raise HTTPException(status_code=400, detail="unsupported documentType")
job_id = str(uuid.uuid4())
jobs[job_id] = {"status": "pending"}
background.add_task(recognize, job_id, await file.read(), documentType)
return {"id": job_id, "status": "pending"}
@app.get("/ocr/documents/{job_id}")
async def status(job_id: str) -> dict:
job = jobs.get(job_id)
if job is None:
raise HTTPException(status_code=404, detail="unknown job")
return {"id": job_id, **job}

Do produkcji dołóż trwałe przechowywanie zleceń (restart usługi nie może gubić wyników), kolejkę zamiast BackgroundTasks oraz weryfikację nagłówka Authorization, jeśli ustawisz OCR_CUSTOM_API_KEY.

Pełne definicje: purchase-order.schema.json i goods-received-source.schema.json. Poniżej rzeczy, które najczęściej sprawiają kłopot.

Trzy różne numery na dokumencie źródłowym

Section titled “Trzy różne numery na dokumencie źródłowym”
Pole Co to jest Przykład
documentNumber Numer samego dokumentu (WZ, faktury, dowodu dostawy) WZ/0612/24/10/MG
order Wewnętrzny numer zamówienia / zlecenia wystawcy (ZS, ZO, ZAD, „nasz nr dokumentu”) ZS/1420/2026
externalOrder Numer zamówienia po stronie nabywcy (Customer PO) ZAM_W/2025/508

Pomylenie ich powoduje, że Sancho nie połączy dostawy z właściwym zamówieniem. Jeśli nie masz pewności, które jest które, zostaw pole puste — brak wartości jest lepszy niż wartość w złym polu.

Wartości muszą być liczbami JSON, nie napisami. Polskie zapisy trzeba znormalizować:

Na dokumencie W JSON
1 234,56 1234.56
12,000 (ilość) 12
1.234,56 1234.56

Uwaga na separator tysięcy: spacja zwykła, spacja nierozdzielająca i kropka występują wymiennie.

Format YYYY-MM-DD. 12.06.20262026-06-12. Jeśli nie umiesz jednoznacznie odczytać daty, zwróć null.

Najlepiej same cyfry (5261040828). Sancho i tak usuwa spacje i myślniki, ale nie rozdziela prefiksu kraju — PL5261040828 zostanie zapisane w całości.

items to jedna pozycja na wiersz tabeli. Pola, których nie odczytałeś, po prostu pomiń — pominięta wartość jest traktowana jak null, a pominięta tablica jak []. Nie wstawiaj pustych napisów ani zer zastępczych: 0 w quantity znaczy „ilość zero”, a nie „nie wiem”.

W schemacie goods-received-source pozycje usługowe transportu / wysyłki nie są pozycjami towarowymi — pomiń je w items, a ich wartość netto zwróć w polu deliveryCostNet dokumentu.

  1. Wyślij dokument i zapamiętaj identyfikator:

    Terminal window
    curl -sS -X POST https://ocr.firma.local/ocr/documents \
    -F "file=@zamowienie.pdf" \
    -F "documentType=purchase_order"

    Oczekiwane: kod 2xx i {"id": "...", "status": "pending"} w czasie poniżej kilku sekund.

  2. Odpytaj o wynik:

    Terminal window
    curl -sS https://ocr.firma.local/ocr/documents/<id>

    Oczekiwane: najpierw {"status": "processing"}, potem {"status": "ready", "data": {...}}.

  3. Sprawdź wynik schematem:

    Terminal window
    curl -sS https://ocr.firma.local/ocr/documents/<id> | jq .data > wynik.json
    curl -sS https://sancho-wms.pl/schemas/purchase-order.schema.json > schemat.json
    check-jsonschema --schemafile schemat.json wynik.json
  4. Sprawdź nieznane zlecenie — GET /ocr/documents/nie-istnieje musi zwrócić 404, a nie 200 ze statusem pending.

  5. Sprawdź dokument nieczytelny — Twoja usługa musi zwrócić {"status": "error", "message": "..."}, a nie wisieć w processing w nieskończoność.

  • data jako napis z JSON-em zamiast obiektu. Sancho nie parsuje napisu drugi raz.
  • Liczby jako napisy ("1 234,56"). Walidacja schematu odrzuci taki wynik i dokument zostanie oznaczony błędem.
  • status: "ready" bez pola data. Sancho potraktuje to jako pusty wynik i odrzuci na walidacji.
  • Blokująca odpowiedź na POST — trzymanie połączenia do końca OCR. Limit to 60 sekund, a i tak marnuje budżet odpytywania.
  • 404 dla zlecenia, które jeszcze się przetwarza. Zwracaj status, nie błąd — inaczej marnujesz budżet na ponawianie.
  • Zgubienie wyniku po restarcie usługi. Zlecenie w toku, którego stan zniknął, kończy się błędem po stronie Sancho.
  • Wartości zastępcze0, "brak", "-" w polach, których nie odczytano. Pomijaj takie pola.