Aller au contenu principal

Document Processing / OCR PDF - Aaperture

Module A : Pipeline complet d'ingestion, OCR, indexation et extraction structurée pour documents PDF.

Contexte (photographes)

Importer contrats PDF, questionnaires scannés, pièces administratives, docs clients → OCR + indexation + extraction structurée optionnelle.

Bounded Context (DDD)

DocumentProcessing

  • Agrégats : Document, DocumentArtifact
  • Domain services : DocumentIngestionService, DocumentPipelineService
  • Ports :
    • OcrProviderPort (impl Node ou Python)
    • StoragePort (R2)
    • SearchIndexPort (optionnel)

Schéma DB

documents

CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL,
owner_user_id UUID NOT NULL,
source TEXT NOT NULL, -- UPLOAD|EMAIL|PORTAL
file_object_id UUID NOT NULL REFERENCES file_objects(id),
mime_type TEXT NOT NULL,
page_count INT,
status TEXT NOT NULL DEFAULT 'UPLOADED', -- UPLOADED|PROCESSING|OCR_DONE|EXTRACTED|FAILED
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_documents_org ON documents(org_id);
CREATE INDEX idx_documents_status ON documents(status);
CREATE INDEX idx_documents_owner ON documents(owner_user_id);

document_pages (si OCR page par page)

CREATE TABLE document_pages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
page_index INT NOT NULL,
image_file_object_id UUID REFERENCES file_objects(id),
status TEXT NOT NULL DEFAULT 'PENDING', -- PENDING|OCR_DONE|FAILED
text TEXT, -- Texte OCR (ou via document_artifacts)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(document_id, page_index)
);

CREATE INDEX idx_document_pages_doc ON document_pages(document_id);
CREATE INDEX idx_document_pages_status ON document_pages(status);

document_artifacts

CREATE TABLE document_artifacts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
type TEXT NOT NULL, -- TEXT|OCR_JSON|STRUCTURED_JSON|EMBEDDINGS
file_object_id UUID REFERENCES file_objects(id),
content JSONB, -- ou TEXT pour type TEXT
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_document_artifacts_doc ON document_artifacts(document_id);
CREATE INDEX idx_document_artifacts_type ON document_artifacts(type);

Jobs

doc:ingest (input: documentId)

Responsabilités :

  • Détecter nombre de pages
  • (Optionnel) Rendre images des pages
  • Créer document_pages
  • Enqueue doc:ocr (par page ou par doc)

Payload :

{
v: 1,
orgId: "uuid",
userId: "uuid",
entityId: "uuid", // document.id
}

Processus :

  1. Charger document depuis file_objects
  2. Détecter nombre de pages (PDF.js ou lib Python)
  3. Créer entrées document_pages pour chaque page
  4. (Optionnel) Rendre pages en images PNG → upload R2 → image_file_object_id
  5. Enqueue doc:ocr pour chaque page (ou document entier si OCR global)

doc:ocr (input: documentPageId ou documentId)

Responsabilités :

  • Exécuter OCR via OcrProviderPort
  • Persister texte/artefacts
  • Mettre à jour statuts

Payload :

{
v: 1,
orgId: "uuid",
userId: "uuid",
entityId: "uuid", // document_page.id ou document.id
pageIndex?: number, // si page spécifique
}

Processus :

  1. Charger page/document depuis file_objects
  2. Appeler OcrProviderPort.ocr(file)
  3. Persister texte dans document_pages.text ou document_artifacts (type TEXT)
  4. (Optionnel) Persister JSON OCR brut dans document_artifacts (type OCR_JSON)
  5. Mettre à jour status = 'OCR_DONE'
  6. Si toutes les pages sont OCR → mettre documents.status = 'OCR_DONE'

doc:extract_structured (optionnel)

Responsabilités :

  • Extraction JSON (LLM + Zod schema)
  • Stocker STRUCTURED_JSON

Payload :

{
v: 1,
orgId: "uuid",
userId: "uuid",
entityId: "uuid", // document.id
schema: "CONTRACT" | "QUESTIONNAIRE" | "INVOICE" | "CUSTOM", // ou Zod schema custom
}

Processus :

  1. Charger texte OCR depuis document_artifacts (type TEXT)
  2. Appeler LLM avec prompt structuré + schema Zod
  3. Valider réponse avec Zod
  4. Persister dans document_artifacts (type STRUCTURED_JSON)
  5. Mettre à jour documents.status = 'EXTRACTED'

Endpoints (OpenAPI + DTO Zod)

POST /documents

Init upload → presigned URL.

Request :

{
filename: string;
mimeType: string;
source: "UPLOAD" | "EMAIL" | "PORTAL";
}

Response :

{
documentId: string;
uploadUrl: string; // Presigned R2 URL
expiresAt: string;
}

POST /documents/:id/complete-upload

Marque UPLOADED + enqueue doc:ingest.

Response :

{
documentId: string;
status: "UPLOADED";
jobId: string; // job doc:ingest
}

GET /documents/:id

Statut + résumé artefacts.

Response :

{
id: string;
status: "UPLOADED" | "PROCESSING" | "OCR_DONE" | "EXTRACTED" | "FAILED";
pageCount: number;
artifacts: Array<{
type: "TEXT" | "OCR_JSON" | "STRUCTURED_JSON";
createdAt: string;
}>;
}

GET /documents/:id/text

Texte OCR (concaténation de toutes les pages).

Response :

{
text: string;
pages: Array<{
pageIndex: number;
text: string;
}>;
}

POST /documents/:id/extract (optionnel)

Lance extraction structurée.

Request :

{
schema: "CONTRACT" | "QUESTIONNAIRE" | "INVOICE" | "CUSTOM";
customSchema?: ZodSchema; // si CUSTOM
}

Response :

{
jobId: string; // job doc:extract_structured
}

Frontend (React + TanStack Query)

⚠️ État actuel (2026-01-24)

Page Documents globale supprimée : La page /documents et l'UI associée ont été supprimées car les documents doivent être scopés par un contexte (session, contact, etc.). Les documents ne doivent pas exister sans contexte.

OCR/Extraction fonctionnel : L'extraction OCR reste disponible via l'endpoint /duplicates/extract utilisé pour la détection de doublons. Voir ExtractionService et ExtractionController.

Workers documentaires disponibles : Les workers doc:ingest, doc:ocr, et doc:extract_structured sont implémentés et fonctionnels, mais ne sont pas actuellement utilisés dans l'UI. Ils peuvent être réutilisés lorsque l'intégration des documents dans les sessions/contacts sera implémentée.

Hooks (créés mais non utilisés actuellement)

Note : Ces hooks ont été créés pour la page Documents globale qui a été supprimée. Ils restent disponibles pour une utilisation future avec contexte.

useCreateDocumentUpload()

const { mutate: createUpload } = useCreateDocumentUpload();

createUpload({
filename: "contrat.pdf",
mimeType: "application/pdf",
source: "UPLOAD"
}, {
onSuccess: ({ documentId, uploadUrl }) => {
// Upload vers uploadUrl, puis appeler completeUpload
}
});

useCompleteDocumentUpload(documentId)

const { mutate: completeUpload } = useCompleteDocumentUpload(documentId);

completeUpload(undefined, {
onSuccess: ({ jobId }) => {
// Polling status du document
}
});

useDocument(documentId) (polling si PROCESSING)

const { data: document } = useDocument(documentId, {
refetchInterval: (query) => {
return query.state.data?.status === "PROCESSING" ? 2000 : false;
}
});

useDocumentText(documentId)

const { data: text } = useDocumentText(documentId);

useExtractDocument(documentId) (mutation)

const { mutate: extract } = useExtractDocument(documentId);

extract({
schema: "CONTRACT"
});

Extraction OCR pour doublons (actuellement utilisé)

L'extraction OCR est utilisée via l'endpoint /duplicates/extract :

// Frontend: frontend/src/client/duplicates/useExtraction.ts
const { mutate: extractFile } = useExtractFile();

extractFile({ file }, {
onSuccess: (data) => {
// data.extracted_data contient les données structurées
// data.raw_text contient le texte OCR
}
});

Backend : ExtractionControllerExtractionServiceOcrService (Tesseract.js) + LlmService (OpenAI)

UI (composants créés mais non utilisés)

Note : Ces composants ont été créés pour la page Documents globale qui a été supprimée. Ils restent disponibles pour une utilisation future avec contexte.

Liste Documents + filtres

  • Liste avec statut, nombre de pages, date
  • Filtres : statut, source, date
  • Actions : upload, supprimer

Détail Document

  • Statut pipeline : Badge avec progression (UPLOADED → PROCESSING → OCR_DONE → EXTRACTED)
  • Preview : Affichage PDF (via viewer)
  • Texte OCR : Affichage texte avec recherche
  • Extraction structurée : Affichage JSON structuré avec possibilité d'éditer

Prochaines étapes

Pour intégrer les documents dans le contexte session/contact :

  1. Ajouter des champs de contexte : Ajouter session_id et contact_id (optionnels) à la table documents
  2. Créer des endpoints scopés : POST /sessions/:id/documents, POST /contacts/:id/documents
  3. Réutiliser les workers : Les workers doc:ingest, doc:ocr, doc:extract_structured sont déjà fonctionnels
  4. Intégrer dans l'UI : Ajouter des sections documents dans les pages session/contact

Tests

Unit

  • Zod payloads validation
  • Transitions statuts (UPLOADED → PROCESSING → OCR_DONE)

Integration

  • doc:ingestdoc:ocr (pipeline complet)
  • Gestion erreurs (PDF invalide, OCR échoué)

E2E

  • Upload → OCR → texte dispo
  • Extraction structurée → JSON valide

Edge Cases

Idempotence

  • doc:ingest : Vérifier si document_pages existent déjà
  • doc:ocr : Vérifier si texte OCR existe déjà

Retries

  • OCR peut échouer (timeout, API externe down)
  • Retry avec backoff exponentiel (5 tentatives max)

Performance

  • OCR peut être lent (10-30s par page)
  • Traiter pages en parallèle (si OCR provider supporte)
  • Limiter nombre de pages simultanées par organisation

Références