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 :
- Charger document depuis
file_objects - Détecter nombre de pages (PDF.js ou lib Python)
- Créer entrées
document_pagespour chaque page - (Optionnel) Rendre pages en images PNG → upload R2 →
image_file_object_id - Enqueue
doc:ocrpour 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 :
- Charger page/document depuis
file_objects - Appeler
OcrProviderPort.ocr(file) - Persister texte dans
document_pages.textoudocument_artifacts(typeTEXT) - (Optionnel) Persister JSON OCR brut dans
document_artifacts(typeOCR_JSON) - Mettre à jour
status = 'OCR_DONE' - 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 :
- Charger texte OCR depuis
document_artifacts(typeTEXT) - Appeler LLM avec prompt structuré + schema Zod
- Valider réponse avec Zod
- Persister dans
document_artifacts(typeSTRUCTURED_JSON) - 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 : ExtractionController → ExtractionService → OcrService (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