4NK_IA_front/backend/server.js

1820 lines
61 KiB
JavaScript

#!/usr/bin/env node
/**
* Serveur backend pour le traitement des documents
* Gère l'OCR, l'extraction NER et renvoie du JSON au frontend
*/
const express = require('express')
const multer = require('multer')
const cors = require('cors')
const path = require('path')
const fs = require('fs')
const crypto = require('crypto')
const { createWorker } = require('tesseract.js')
const { preprocessImageForOCR, analyzeImageMetadata } = require('./imagePreprocessing')
const pdf = require('pdf-parse')
const app = express()
const PORT = process.env.PORT || 3001
// Middleware
app.use(cors())
app.use(express.json())
app.use(express.static('public'))
// Middleware de logging pour debug
app.use((req, res, next) => {
console.log(`[DEBUG] ${new Date().toISOString()} - ${req.method} ${req.url}`)
console.log(`[DEBUG] Headers:`, req.headers)
next()
})
// Fonction pour calculer le hash d'un fichier
function calculateFileHash(buffer) {
return crypto.createHash('sha256').update(buffer).digest('hex')
}
// Fonction pour générer un hash de dossier
function generateFolderHash() {
return crypto.randomBytes(16).toString('hex')
}
// Fonction pour obtenir le type MIME à partir de l'extension
function getMimeType(ext) {
const mimeTypes = {
'.pdf': 'application/pdf',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.tiff': 'image/tiff',
'.txt': 'text/plain',
}
return mimeTypes[ext.toLowerCase()] || 'application/octet-stream'
}
// Fonction pour créer la structure de dossiers
function createFolderStructure(folderHash) {
console.log(`[FOLDER] Création de la structure pour le hash: ${folderHash}`)
console.log(`[FOLDER] Répertoire de travail: ${process.cwd()}`)
// Créer les dossiers racines s'ils n'existent pas
const uploadsDir = 'uploads'
const cacheDir = 'cache'
console.log(`[FOLDER] Vérification de l'existence de ${uploadsDir}: ${fs.existsSync(uploadsDir)}`)
console.log(`[FOLDER] Vérification de l'existence de ${cacheDir}: ${fs.existsSync(cacheDir)}`)
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true })
console.log(`[FOLDER] Dossier racine créé: ${uploadsDir}`)
}
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true })
console.log(`[FOLDER] Dossier racine créé: ${cacheDir}`)
}
const folderPath = path.join(uploadsDir, folderHash)
const cachePath = path.join(cacheDir, folderHash)
console.log(`[FOLDER] Chemin du dossier uploads: ${folderPath}`)
console.log(`[FOLDER] Chemin du dossier cache: ${cachePath}`)
if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath, { recursive: true })
console.log(`[FOLDER] Dossier uploads créé: ${folderPath}`)
}
if (!fs.existsSync(cachePath)) {
fs.mkdirSync(cachePath, { recursive: true })
console.log(`[FOLDER] Dossier cache créé: ${cachePath}`)
}
return { folderPath, cachePath }
}
// Fonction pour sauvegarder le cache JSON dans un dossier spécifique
function saveJsonCacheInFolder(folderHash, fileHash, result) {
const { cachePath } = createFolderStructure(folderHash)
const cacheFile = path.join(cachePath, `${fileHash}.json`)
try {
fs.writeFileSync(cacheFile, JSON.stringify(result, null, 2))
console.log(`[CACHE] Résultat sauvegardé dans le dossier ${folderHash}: ${fileHash}`)
// Supprimer le flag pending si il existe
const pendingFile = path.join(cachePath, `${fileHash}.pending`)
if (fs.existsSync(pendingFile)) {
fs.unlinkSync(pendingFile)
console.log(`[CACHE] Flag pending supprimé pour ${fileHash}`)
}
return true
} catch (error) {
console.error(`[CACHE] Erreur lors de la sauvegarde dans le dossier ${folderHash}:`, error)
return false
}
}
// Fonction pour créer un flag pending
function createPendingFlag(folderHash, fileHash) {
const { cachePath } = createFolderStructure(folderHash)
const pendingFile = path.join(cachePath, `${fileHash}.pending`)
try {
const pendingData = {
fileHash,
folderHash,
timestamp: new Date().toISOString(),
status: 'processing',
}
fs.writeFileSync(pendingFile, JSON.stringify(pendingData, null, 2))
console.log(`[CACHE] Flag pending créé pour ${fileHash} dans le dossier ${folderHash}`)
return true
} catch (error) {
console.error(`[CACHE] Erreur lors de la création du flag pending:`, error)
return false
}
}
// Fonction pour vérifier si un fichier est en cours de traitement
function isFilePending(folderHash, fileHash) {
const cachePath = path.join('cache', folderHash)
const pendingFile = path.join(cachePath, `${fileHash}.pending`)
return fs.existsSync(pendingFile)
}
// Fonction pour nettoyer les flags pending orphelins (plus de 1 heure)
function cleanupOrphanedPendingFlags() {
console.log('[CLEANUP] Nettoyage des flags pending orphelins...')
const cacheDir = 'cache'
if (!fs.existsSync(cacheDir)) {
return
}
const folders = fs.readdirSync(cacheDir)
let cleanedCount = 0
for (const folder of folders) {
const folderPath = path.join(cacheDir, folder)
if (!fs.statSync(folderPath).isDirectory()) continue
const files = fs.readdirSync(folderPath)
for (const file of files) {
if (file.endsWith('.pending')) {
const pendingFile = path.join(folderPath, file)
try {
const stats = fs.statSync(pendingFile)
const age = Date.now() - stats.mtime.getTime()
const oneHour = 60 * 60 * 1000 // 1 heure en millisecondes
if (age > oneHour) {
fs.unlinkSync(pendingFile)
cleanedCount++
console.log(`[CLEANUP] Flag pending orphelin supprimé: ${file}`)
}
} catch (error) {
console.error(`[CLEANUP] Erreur lors du nettoyage de ${file}:`, error.message)
}
}
}
}
if (cleanedCount > 0) {
console.log(`[CLEANUP] ${cleanedCount} flags pending orphelins supprimés`)
} else {
console.log('[CLEANUP] Aucun flag pending orphelin trouvé')
}
}
// Fonction pour récupérer le cache JSON depuis un dossier spécifique
function getJsonCacheFromFolder(folderHash, fileHash) {
const cachePath = path.join('cache', folderHash)
const cacheFile = path.join(cachePath, `${fileHash}.json`)
if (fs.existsSync(cacheFile)) {
try {
const data = fs.readFileSync(cacheFile, 'utf8')
const result = JSON.parse(data)
console.log(`[CACHE] Résultat récupéré depuis le dossier ${folderHash}: ${fileHash}`)
return result
} catch (error) {
console.error(`[CACHE] Erreur lors de la lecture depuis le dossier ${folderHash}:`, error)
return null
}
}
return null
}
// Fonction pour obtenir le type MIME à partir de l'extension
function getMimeTypeFromExtension(extension) {
const mimeTypes = {
'.pdf': 'application/pdf',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.bmp': 'image/bmp',
'.tiff': 'image/tiff',
'.webp': 'image/webp',
'.txt': 'text/plain',
}
return mimeTypes[extension.toLowerCase()] || 'application/octet-stream'
}
// Fonction pour lister tous les résultats d'un dossier
async function listFolderResults(folderHash) {
const cachePath = path.join('cache', folderHash)
const uploadsPath = path.join('uploads', folderHash)
const results = []
const pending = []
let hasPending = false
// Traiter les fichiers en cache (avec résultats d'extraction)
if (fs.existsSync(cachePath)) {
const cacheFiles = fs.readdirSync(cachePath)
for (const file of cacheFiles) {
if (file.endsWith('.json')) {
const fileHash = path.basename(file, '.json')
const result = getJsonCacheFromFolder(folderHash, fileHash)
if (result) {
results.push({
fileHash,
...result,
})
}
} else if (file.endsWith('.pending')) {
const fileHash = path.basename(file, '.pending')
try {
const pendingData = JSON.parse(fs.readFileSync(path.join(cachePath, file), 'utf8'))
pending.push(pendingData)
hasPending = true
console.log(`[CACHE] Fichier en cours de traitement détecté: ${fileHash}`)
} catch (error) {
console.error(`[CACHE] Erreur lors de la lecture du flag pending ${file}:`, error)
}
}
}
}
// Traiter les fichiers en uploads (sans résultats d'extraction)
if (fs.existsSync(uploadsPath)) {
console.log(`[FOLDER] Dossier uploads trouvé: ${uploadsPath}`)
const uploadFiles = fs.readdirSync(uploadsPath)
console.log(`[FOLDER] Fichiers trouvés dans uploads: ${uploadFiles.length}`)
console.log(`[FOLDER] Liste des fichiers: ${uploadFiles.join(', ')}`)
for (const file of uploadFiles) {
console.log(`[FOLDER] Traitement du fichier: ${file}`)
// Extraire le hash du nom de fichier (format: hash.extension)
const fileHash = path.basename(file, path.extname(file))
console.log(`[FOLDER] Hash extrait: ${fileHash}`)
// Vérifier si ce fichier n'a pas déjà un résultat en cache
const hasCacheResult = results.some((result) => result.fileHash === fileHash)
// Vérifier si ce fichier n'est pas déjà en pending
const isAlreadyPending = pending.some((p) => p.fileHash === fileHash)
console.log(`[FOLDER] hasCacheResult: ${hasCacheResult}, isAlreadyPending: ${isAlreadyPending}`)
if (!hasCacheResult && !isAlreadyPending) {
// Ne pas bloquer la réponse: marquer en pending et lancer en arrière-plan
const filePath = path.join(uploadsPath, file)
console.log(
`[FOLDER] Fichier non traité détecté, mise en traitement asynchrone: ${file}`,
)
// Créer un flag pending et enregistrer l'état
createPendingFlag(folderHash, fileHash)
pending.push({
fileHash,
folderHash,
timestamp: new Date().toISOString(),
status: 'processing',
})
hasPending = true
// Lancer le traitement en arrière-plan (sans await)
processFileInBackground(filePath, fileHash, folderHash).catch((err) =>
console.error('[BACKGROUND] Erreur (non bloquante):', err?.message || err),
)
} else {
console.log(`[FOLDER] Fichier ${file} ignoré (déjà traité ou en cours)`)
}
}
} else {
console.log(`[FOLDER] Dossier uploads non trouvé: ${uploadsPath}`)
}
return { results, pending, hasPending }
}
// Fonction pour traiter un document (extraction de la logique de /api/extract)
async function processDocument(filePath, fileHash) {
const startTime = Date.now()
try {
console.log(`[PROCESS] Début du traitement: ${filePath}`)
// Obtenir les informations du fichier
const stats = fs.statSync(filePath)
const ext = path.extname(filePath)
const mimeType = getMimeTypeFromExtension(ext)
console.log(`[PROCESS] Fichier: ${path.basename(filePath)}`)
console.log(`[PROCESS] Extension: ${ext}`)
console.log(`[PROCESS] Type MIME: ${mimeType}`)
console.log(`[PROCESS] Taille: ${stats.size} bytes`)
// Créer un objet file similaire à celui de multer
const file = {
path: filePath,
originalname: path.basename(filePath),
mimetype: mimeType,
size: stats.size,
}
let ocrResult
let result
// Si c'est un fichier texte, lire directement le contenu
if (mimeType === 'text/plain') {
console.log(`[PROCESS] Lecture du fichier texte...`)
try {
const text = fs.readFileSync(filePath, 'utf8')
ocrResult = { text, confidence: 1.0 }
console.log(`[PROCESS] Texte lu: ${text.length} caractères`)
} catch (error) {
console.error(`[PROCESS] Erreur lors de la lecture du fichier texte:`, error.message)
throw new Error(`Erreur lors de la lecture du fichier texte: ${error.message}`)
}
} else if (mimeType === 'application/pdf') {
console.log(`[PROCESS] Extraction de texte depuis PDF...`)
try {
ocrResult = await extractTextFromPdf(filePath)
console.log(`[PROCESS] Texte extrait du PDF: ${ocrResult.text.length} caractères`)
} catch (error) {
console.error(`[PROCESS] Erreur lors de l'extraction PDF:`, error.message)
throw new Error(`Erreur lors de l'extraction PDF: ${error.message}`)
}
} else if (mimeType.startsWith('image/')) {
// Pour les images, utiliser l'OCR amélioré avec détection CNI
console.log(`[PROCESS] Traitement d'une image avec OCR...`)
try {
const { extractTextFromImageEnhanced } = require('./enhancedOcr')
ocrResult = await extractTextFromImageEnhanced(filePath)
console.log(`[PROCESS] OCR terminé: ${ocrResult.text.length} caractères`)
} catch (error) {
console.error(`[PROCESS] Erreur lors de l'OCR:`, error.message)
throw new Error(`Erreur lors de l'OCR: ${error.message}`)
}
} else {
console.error(`[PROCESS] Type de fichier non supporté: ${mimeType}`)
throw new Error(`Type de fichier non supporté: ${mimeType}`)
}
// Extraction NER
const entities = extractEntitiesFromText(ocrResult.text)
// Mesure du temps de traitement
const processingTime = Date.now() - startTime
// Génération du format JSON standard (avec repli sûr)
try {
result = generateStandardJSON(file, ocrResult, entities, processingTime)
} catch (genErr) {
console.error('[PROCESS] Erreur generateStandardJSON, application d\'un repli:', genErr)
const safeText = typeof ocrResult.text === 'string' ? ocrResult.text : ''
const safeMime = file.mimetype || getMimeType(path.extname(filePath))
const fallbackProcessing = {
engine: '4NK_IA_Backend',
version: '1.0.0',
processingTime: `${processingTime}ms`,
ocrEngine: safeMime === 'application/pdf' ? 'pdf-parse' : 'tesseract.js',
nerEngine: 'rule-based',
preprocessing: {
applied: safeMime !== 'application/pdf',
reason: safeMime === 'application/pdf' ? 'PDF direct text extraction' : 'Image preprocessing applied',
},
}
result = {
document: {
id: `doc-${Date.now()}`,
fileName: file.originalname || path.basename(filePath),
fileSize: file.size || (fs.existsSync(filePath) ? fs.statSync(filePath).size : 0),
mimeType: safeMime,
uploadTimestamp: new Date().toISOString(),
},
classification: {
documentType: 'Document',
confidence: 0.6,
subType: getDocumentSubType('Document', safeText),
language: 'fr',
pageCount: 1,
},
extraction: {
text: {
raw: safeText,
processed: correctOCRText(safeText),
wordCount: safeText.trim().split(/\s+/).filter(Boolean).length,
characterCount: safeText.length,
confidence: (typeof ocrResult.confidence === 'number' ? ocrResult.confidence : 0) / 100,
},
entities: {
persons: [],
companies: [],
addresses: [],
financial: extractFinancialInfo(safeText, 'Document'),
dates: [],
contractual: { clauses: [], signatures: [] },
references: extractReferences(safeText, 'Document'),
},
},
metadata: {
processing: fallbackProcessing,
quality: {
globalConfidence: 0.6,
textExtractionConfidence: (typeof ocrResult.confidence === 'number' ? ocrResult.confidence : 0) / 100,
entityExtractionConfidence: 0.6,
classificationConfidence: 0.6,
},
},
status: { success: true, errors: ['fallback: generateStandardJSON error'], warnings: [], timestamp: new Date().toISOString() },
}
}
console.log(`[PROCESS] Traitement terminé en ${processingTime}ms`)
return result
} catch (error) {
console.error(`[PROCESS] Erreur lors du traitement:`, error)
throw error
}
}
// Fonction pour traiter un fichier en arrière-plan
async function processFileInBackground(filePath, fileHash, folderHash) {
try {
console.log(`[BACKGROUND] Début du traitement en arrière-plan: ${filePath}`)
console.log(`[BACKGROUND] fileHash: ${fileHash}, folderHash: ${folderHash}`)
// Vérifier que le fichier existe
if (!fs.existsSync(filePath)) {
throw new Error(`Fichier non trouvé: ${filePath}`)
}
// Traiter le document
console.log(`[BACKGROUND] Appel de processDocument...`)
const result = await processDocument(filePath, fileHash)
console.log(`[BACKGROUND] processDocument terminé, résultat:`, result ? 'OK' : 'NULL')
// Sauvegarder le résultat dans le cache du dossier
console.log(`[BACKGROUND] Sauvegarde du résultat dans le cache...`)
const success = saveJsonCacheInFolder(folderHash, fileHash, result)
console.log(`[BACKGROUND] Sauvegarde: ${success ? 'OK' : 'ÉCHEC'}`)
if (success) {
// Supprimer le flag pending
console.log(`[BACKGROUND] Suppression du flag pending...`)
removePendingFlag(folderHash, fileHash)
console.log(`[BACKGROUND] Traitement terminé avec succès: ${fileHash}`)
} else {
console.error(`[BACKGROUND] Erreur lors de la sauvegarde du résultat: ${fileHash}`)
}
} catch (error) {
console.error(`[BACKGROUND] Erreur lors du traitement en arrière-plan:`, error)
console.error(`[BACKGROUND] Stack trace:`, error.stack)
// Supprimer le flag pending même en cas d'erreur
removePendingFlag(folderHash, fileHash)
}
}
// Fonction pour supprimer un flag pending
function removePendingFlag(folderHash, fileHash) {
try {
const cachePath = path.join('cache', folderHash)
const pendingFile = path.join(cachePath, `${fileHash}.pending`)
if (fs.existsSync(pendingFile)) {
fs.unlinkSync(pendingFile)
console.log(`[PENDING] Flag pending supprimé: ${fileHash}`)
}
} catch (error) {
console.error(`[PENDING] Erreur lors de la suppression du flag pending:`, error)
}
}
// Fonction pour vérifier si un fichier existe déjà par hash dans un dossier
function findExistingFileByHash(hash, folderHash) {
const folderPath = path.join('uploads', folderHash)
if (!fs.existsSync(folderPath)) return null
const files = fs.readdirSync(folderPath)
for (const file of files) {
// Vérifier si le nom de fichier commence par le hash
if (file.startsWith(hash)) {
const filePath = path.join(folderPath, file)
return { path: filePath, name: file }
}
}
return null
}
// Fonction pour sauvegarder le cache JSON
function saveJsonCache(hash, result) {
const cacheDir = 'cache/'
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true })
}
const cacheFile = path.join(cacheDir, `${hash}.json`)
try {
fs.writeFileSync(cacheFile, JSON.stringify(result, null, 2))
console.log(`[CACHE] Résultat sauvegardé: ${hash.substring(0, 16)}...`)
return true
} catch (error) {
console.error(`[CACHE] Erreur lors de la sauvegarde:`, error.message)
return false
}
}
// Fonction pour récupérer le cache JSON
function getJsonCache(hash) {
const cacheFile = path.join('cache/', `${hash}.json`)
try {
if (fs.existsSync(cacheFile)) {
const cachedData = fs.readFileSync(cacheFile, 'utf8')
const result = JSON.parse(cachedData)
console.log(`[CACHE] Résultat récupéré: ${hash.substring(0, 16)}...`)
return result
}
} catch (error) {
console.warn(`[CACHE] Erreur lors de la lecture du cache:`, error.message)
}
return null
}
// Fonction pour lister les fichiers de cache
function listCacheFiles() {
const cacheDir = 'cache/'
if (!fs.existsSync(cacheDir)) return []
const files = fs.readdirSync(cacheDir)
return files
.map((file) => {
const filePath = path.join(cacheDir, file)
try {
const stats = fs.statSync(filePath)
const hash = path.basename(file, '.json')
return {
hash: hash,
fileName: file,
size: stats.size,
createdDate: stats.birthtime,
modifiedDate: stats.mtime,
}
} catch (error) {
console.warn(`[CACHE] Erreur lors de la lecture de ${file}:`, error.message)
return null
}
})
.filter((file) => file !== null)
}
// Configuration multer pour l'upload de fichiers avec hash comme nom
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = 'uploads/'
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true })
}
cb(null, uploadDir)
},
filename: (req, file, cb) => {
// Utiliser un nom temporaire, le hash sera calculé après
const timestamp = Date.now()
const ext = path.extname(file.originalname)
cb(null, `temp-${timestamp}${ext}`)
},
})
const upload = multer({
storage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB max
fileFilter: (req, file, cb) => {
const allowedTypes = [
'image/jpeg',
'image/png',
'image/tiff',
'application/pdf',
'text/plain',
]
if (allowedTypes.includes(file.mimetype)) {
cb(null, true)
} else {
cb(new Error('Type de fichier non supporté'), false)
}
},
})
// Fonction d'extraction de texte depuis un PDF
async function extractTextFromPdf(pdfPath) {
console.log(`[PDF] Début de l'extraction de texte pour: ${path.basename(pdfPath)}`)
try {
const dataBuffer = fs.readFileSync(pdfPath)
const data = await pdf(dataBuffer)
console.log(`[PDF] Texte extrait: ${data.text.length} caractères`)
console.log(`[PDF] Nombre de pages: ${data.numpages}`)
return {
text: data.text,
confidence: 95, // PDF text extraction est très fiable
words: data.text.split(/\s+/).filter((word) => word.length > 0),
}
} catch (error) {
console.error(`[PDF] Erreur lors de l'extraction:`, error.message)
throw error
}
}
// Fonction d'extraction OCR optimisée avec préprocessing
async function extractTextFromImage(imagePath) {
console.log(`[OCR] Début de l'extraction pour: ${imagePath}`)
// Analyse des métadonnées de l'image
const metadata = await analyzeImageMetadata(imagePath)
// Préprocessing de l'image pour améliorer l'OCR
console.log(`[OCR] Préprocessing de l'image...`)
const preprocessedBuffer = await preprocessImageForOCR(imagePath, null, {
width: 2000,
contrast: 1.5,
brightness: 1.1,
grayscale: true,
sharpen: true,
denoise: true,
})
// Sauvegarde temporaire de l'image préprocessée
const tempPath = imagePath.replace(/\.[^/.]+$/, '_preprocessed.png')
await fs.promises.writeFile(tempPath, preprocessedBuffer)
console.log(`[OCR] Image préprocessée sauvegardée: ${tempPath}`)
const worker = await createWorker('fra+eng')
try {
// Stratégie multi-modes pour améliorer la détection
const strategies = [
{
name: 'Mode Standard',
params: {
tessedit_pageseg_mode: '6',
tessedit_char_whitelist:
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,/-:() ',
tessedit_ocr_engine_mode: '1',
preserve_interword_spaces: '1',
textord_min_linesize: '2.0',
textord_min_xheight: '6',
},
},
{
name: 'Mode Fine',
params: {
tessedit_pageseg_mode: '8', // Mot unique
tessedit_char_whitelist:
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,/-:() ',
tessedit_ocr_engine_mode: '1',
textord_min_linesize: '1.0',
textord_min_xheight: '4',
textord_heavy_nr: '0',
},
},
{
name: 'Mode Ligne',
params: {
tessedit_pageseg_mode: '13', // Ligne brute de texte
tessedit_char_whitelist:
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,/-:() ',
tessedit_ocr_engine_mode: '1',
textord_min_linesize: '1.5',
textord_min_xheight: '5',
},
},
]
let bestResult = { text: '', confidence: 0, words: [], strategy: 'none' }
for (const strategy of strategies) {
try {
console.log(`[OCR] Test de la stratégie: ${strategy.name}`)
await worker.setParameters(strategy.params)
const { data } = await worker.recognize(tempPath)
console.log(`[OCR] ${strategy.name} - Confiance: ${data.confidence}%`)
if (data.confidence > bestResult.confidence) {
bestResult = {
text: data.text,
confidence: data.confidence,
words: data.words || [],
strategy: strategy.name,
}
}
} catch (error) {
console.log(`[OCR] Erreur avec ${strategy.name}: ${error.message}`)
}
}
console.log(
`[OCR] Meilleur résultat (${bestResult.strategy}) - Confiance: ${bestResult.confidence}%`,
)
console.log(
`[OCR] Texte extrait (${bestResult.text.length} caractères): ${bestResult.text.substring(0, 200)}...`,
)
return {
text: bestResult.text,
confidence: bestResult.confidence,
words: bestResult.words,
}
} finally {
await worker.terminate()
// Nettoyage du fichier temporaire
try {
if (fs.existsSync(tempPath)) {
await fs.promises.unlink(tempPath)
console.log(`[OCR] Fichier temporaire supprimé: ${tempPath}`)
}
} catch (error) {
console.warn(`[OCR] Erreur lors de la suppression du fichier temporaire: ${error.message}`)
}
}
}
// Fonction de correction de texte pour améliorer la détection
function correctOCRText(text) {
// Corrections courantes pour les erreurs OCR
const corrections = {
// Corrections générales courantes seulement
0: 'o',
1: 'l',
5: 's',
'@': 'a',
3: 'e',
}
let correctedText = text
for (const [wrong, correct] of Object.entries(corrections)) {
correctedText = correctedText.replace(new RegExp(wrong, 'gi'), correct)
}
return correctedText
}
// Fonction pour générer le format JSON standard
function generateStandardJSON(documentInfo, ocrResult, entities, processingTime) {
const timestamp = new Date().toISOString()
const documentId = `doc-${Date.now()}`
// Classification du document
const safeEntities = {
identities: Array.isArray(entities?.identities) ? entities.identities : [],
companies: Array.isArray(entities?.companies) ? entities.companies : [],
addresses: Array.isArray(entities?.addresses) ? entities.addresses : [],
cniNumbers: Array.isArray(entities?.cniNumbers) ? entities.cniNumbers : [],
dates: Array.isArray(entities?.dates) ? entities.dates : [],
contractClauses: Array.isArray(entities?.contractClauses) ? entities.contractClauses : [],
signatures: Array.isArray(entities?.signatures) ? entities.signatures : [],
documentType: entities?.documentType || 'Document',
}
const { identities, cniNumbers } = safeEntities
const documentType = safeEntities.documentType
const subType = getDocumentSubType(documentType, ocrResult.text)
// Extraction des informations financières pour les factures
const financial = extractFinancialInfo(ocrResult.text, documentType)
// Extraction des références
const references = extractReferences(ocrResult.text, documentType)
// Calcul de la confiance globale
const baseConfidence = typeof ocrResult.confidence === 'number' ? ocrResult.confidence : 0
const globalConfidence = Math.min(
95,
Math.max(
60,
baseConfidence * 0.8 +
(identities.length > 0 ? 10 : 0) +
(cniNumbers.length > 0 ? 15 : 0),
),
)
return {
document: {
id: documentId,
fileName: documentInfo.originalname,
fileSize: documentInfo.size,
mimeType: documentInfo.mimetype,
uploadTimestamp: timestamp,
},
classification: {
documentType: documentType,
confidence: globalConfidence / 100,
subType: subType,
language: 'fr',
pageCount: 1,
},
extraction: {
text: {
raw: typeof ocrResult.text === 'string' ? ocrResult.text : '',
processed: correctOCRText(typeof ocrResult.text === 'string' ? ocrResult.text : ''),
wordCount: Array.isArray(ocrResult.words)
? ocrResult.words.length
: ((typeof ocrResult.text === 'string' ? ocrResult.text : '')
.trim()
.split(/\s+/)
.filter(Boolean).length),
characterCount: (typeof ocrResult.text === 'string' ? ocrResult.text : '').length,
confidence: (typeof ocrResult.confidence === 'number' ? ocrResult.confidence : 0) / 100,
},
entities: {
persons: safeEntities.identities.map((identity) => ({
id: identity.id,
type: 'person',
firstName: identity.firstName,
lastName: identity.lastName,
role: identity.role || null,
email: identity.email || null,
phone: identity.phone || null,
confidence: identity.confidence,
source: identity.source,
})),
companies: safeEntities.companies.map((company) => ({
id: company.id,
name: company.name,
legalForm: company.legalForm || null,
siret: company.siret || null,
rcs: company.rcs || null,
tva: company.tva || null,
capital: company.capital || null,
role: company.role || null,
confidence: company.confidence,
source: company.source,
})),
addresses: safeEntities.addresses.map((address) => ({
id: address.id,
type: address.type || 'general',
street: address.street,
city: address.city,
postalCode: address.postalCode,
country: address.country,
company: address.company || null,
confidence: address.confidence,
source: address.source,
})),
financial: financial,
dates: safeEntities.dates.map((date) => ({
id: date.id,
type: date.type || 'general',
value: date.date || date.value,
formatted: formatDate(date.date || date.value),
confidence: date.confidence,
source: date.source,
})),
contractual: {
clauses: safeEntities.contractClauses.map((clause) => ({
id: clause.id,
type: clause.type,
content: clause.text,
confidence: clause.confidence,
})),
signatures: safeEntities.signatures.map((signature) => ({
id: signature.id,
type: signature.type || 'électronique',
present: signature.present || false,
signatory: signature.signatory || null,
date: signature.date || null,
confidence: signature.confidence,
})),
},
references: references,
},
},
metadata: {
processing: {
engine: '4NK_IA_Backend',
version: '1.0.0',
processingTime: `${processingTime}ms`,
ocrEngine: documentInfo.mimetype === 'application/pdf' ? 'pdf-parse' : 'tesseract.js',
nerEngine: 'rule-based',
preprocessing: {
applied: documentInfo.mimetype !== 'application/pdf',
reason:
documentInfo.mimetype === 'application/pdf'
? 'PDF direct text extraction'
: 'Image preprocessing applied',
},
},
quality: {
globalConfidence: globalConfidence / 100,
textExtractionConfidence: (typeof ocrResult.confidence === 'number' ? ocrResult.confidence : 0) / 100,
entityExtractionConfidence: 0.9,
classificationConfidence: globalConfidence / 100,
},
},
status: {
success: true,
errors: [],
warnings: (Array.isArray(safeEntities.signatures) ? safeEntities.signatures.length : 0) === 0
? ['Aucune signature détectée']
: [],
timestamp: timestamp,
},
}
}
// Fonction pour déterminer le sous-type de document
function getDocumentSubType(documentType, text) {
if (documentType === 'Facture') {
if (/prestation|service/i.test(text)) return 'Facture de prestation'
if (/vente|achat/i.test(text)) return 'Facture de vente'
return 'Facture'
}
if (documentType === 'CNI') return "Carte Nationale d'Identité"
if (documentType === 'Contrat') {
if (/vente|achat/i.test(text)) return 'Contrat de vente'
if (/location|bail/i.test(text)) return 'Contrat de location'
return 'Contrat'
}
return documentType
}
// Fonction pour extraire les informations financières
function extractFinancialInfo(text, documentType) {
if (documentType !== 'Facture') {
return { amounts: [], totals: {}, payment: {} }
}
const amounts = []
const totals = {}
const payment = {}
// Extraction des montants
const amountPatterns = [
/(\d+(?:[.,]\d{2})?)\s*€/g,
/Total\s+H\.T\.\s*[:\-]?\s*(\d+(?:[.,]\d{2})?)\s*€/gi,
/Total\s+T\.T\.C\.\s*[:\-]?\s*(\d+(?:[.,]\d{2})?)\s*€/gi,
/T\.V\.A\.\s*[:\-]?\s*(\d+(?:[.,]\d{2})?)\s*€/gi,
]
amountPatterns.forEach((pattern) => {
for (const match of text.matchAll(pattern)) {
const amount = parseFloat(match[1].replace(',', '.'))
if (amount > 0) {
amounts.push({
id: `amount-${amounts.length}`,
type: 'montant',
value: amount,
currency: 'EUR',
confidence: 0.9,
})
}
}
})
// Extraction des conditions de paiement
const paymentPattern = /paiement\s+se\s+fera\s+\(maximum\)\s+(\d+)\s+jours/gi
const paymentMatch = paymentPattern.exec(text)
if (paymentMatch) {
payment.terms = `${paymentMatch[1]} jours après émission`
}
return { amounts, totals, payment }
}
// Fonction pour extraire les références
function extractReferences(text, documentType) {
const references = []
if (documentType === 'Facture') {
const facturePattern = /Facture\s+N°\s*[:\-]?\s*([A-Z0-9_-]+)/gi
for (const match of text.matchAll(facturePattern)) {
references.push({
id: `ref-${references.length}`,
type: 'facture',
number: match[1],
confidence: 0.95,
})
}
}
return references
}
// Fonction pour formater les dates
function formatDate(dateStr) {
if (!dateStr) return null
// Format DD-MM-YY vers YYYY-MM-DD
const match = dateStr.match(/(\d{2})-(\w+)-(\d{2})/)
if (match) {
const months = {
janvier: '01',
février: '02',
mars: '03',
avril: '04',
mai: '05',
juin: '06',
juillet: '07',
août: '08',
septembre: '09',
octobre: '10',
novembre: '11',
décembre: '12',
}
const month = months[match[2].toLowerCase()]
if (month) {
const year = '20' + match[3]
return `${year}-${month}-${match[1].padStart(2, '0')}`
}
}
return dateStr
}
// Fonction d'extraction NER par règles
function extractEntitiesFromText(text) {
console.log(`[NER] Début de l'extraction d'entités pour ${text.length} caractères`)
// Correction du texte OCR
const correctedText = correctOCRText(text)
if (correctedText !== text) {
console.log(`[NER] Texte corrigé: ${correctedText.substring(0, 100)}...`)
}
const entities = {
identities: [],
companies: [],
addresses: [],
cniNumbers: [],
dates: [],
contractClauses: [],
signatures: [],
documentType: 'Document',
}
// Extraction des noms avec patterns généraux
const namePatterns = [
// Patterns pour documents officiels
/(Vendeur|Acheteur|Vendeuse|Acheteuse|Propriétaire|Locataire|Bailleur|Preneur)\s*:\s*([A-Z][a-zà-öø-ÿ'\-]+\s+[A-Z][a-zà-öø-ÿ'\-]+)/gi,
// Lignes en MAJUSCULES (noms complets)
/^([A-Z][A-ZÀ-ÖØ-öø-ÿ\s\-']{2,30})$/gm,
// Noms avec prénom + nom
/([A-Z][a-zà-öø-ÿ'\-]+\s+[A-Z][a-zà-öø-ÿ'\-]+)/g,
]
namePatterns.forEach((pattern) => {
for (const match of correctedText.matchAll(pattern)) {
const fullName = match[2] || match[1] || match[0]
if (fullName && fullName.length > 3) {
const nameParts = fullName.trim().split(/\s+/)
if (nameParts.length >= 2) {
entities.identities.push({
id: `identity-${(Array.isArray(entities.identities)?entities.identities:[]).length}`,
type: 'person',
firstName: nameParts[0],
lastName: nameParts.slice(1).join(' '),
confidence: 0.9,
source: 'rule-based',
})
}
}
}
})
// Extraction des sociétés
const companyPatterns = [
/(S\.A\.R\.L\.|SAS|SASU|EURL|SNC|SCI|SARL|SA|SAS|SASU|EURL|SNC|SCI|S\.A\.|S\.A\.R\.L\.|S\.A\.S\.|S\.A\.S\.U\.|E\.U\.R\.L\.|S\.N\.C\.|S\.C\.I\.)/gi,
/([A-Z][A-Za-zÀ-ÖØ-öø-ÿ\s\-']{3,50})\s+(S\.A\.R\.L\.|SAS|SASU|EURL|SNC|SCI|SARL|SA)/gi,
/(Entreprise|Société|Compagnie|Groupe|Corporation|Corp\.|Inc\.|Ltd\.|LLC)/gi,
]
companyPatterns.forEach((pattern) => {
for (const match of text.matchAll(pattern)) {
const companyName = match[1] || match[0]
if (companyName && companyName.length > 3) {
entities.companies.push({
id: `company-${(Array.isArray(entities.companies)?entities.companies:[]).length}`,
name: companyName.trim(),
type: 'company',
confidence: 0.8,
source: 'rule-based',
})
}
}
})
// Extraction des adresses
const addressPatterns = [
/(\d{1,4})\s+([A-Za-zÀ-ÖØ-öø-ÿ\s\-']+?),\s*(\d{5})\s+([A-Za-zÀ-ÖØ-öø-ÿ\s\-']+)/gi,
/demeurant\s+(\d{1,4})\s+([A-Za-zÀ-ÖØ-öø-ÿ\s\-']+?),\s*(\d{5})\s+([A-Za-zÀ-ÖØ-öø-ÿ\s\-']+)/gi,
/(Adresse|Siège|Adresse de facturation)\s*:\s*(\d{1,4}\s+[A-Za-zÀ-ÖØ-öø-ÿ\s\-']+,\s*\d{5}\s+[A-Za-zÀ-ÖØ-öø-ÿ\s\-']+)/gi,
]
addressPatterns.forEach((pattern) => {
for (const match of text.matchAll(pattern)) {
const street = match[2] || match[1]
const city = match[4] || match[3]
const postalCode = match[3] || match[2]
entities.addresses.push({
id: `address-${(Array.isArray(entities.addresses)?entities.addresses:[]).length}`,
street: street ? `${street}`.trim() : '',
city: city ? city.trim() : '',
postalCode: postalCode ? postalCode.trim() : '',
country: 'France',
confidence: 0.9,
source: 'rule-based',
})
}
})
// Extraction des numéros de carte d'identité
const cniPattern = /([A-Z]{2}\d{6})/g
for (const match of text.matchAll(cniPattern)) {
entities.cniNumbers.push({
id: `cni-${(Array.isArray(entities.cniNumbers)?entities.cniNumbers:[]).length}`,
number: match[1],
confidence: 0.95,
source: 'rule-based',
})
}
// Extraction des dates
const datePatterns = [/(\d{2}\/\d{2}\/\d{4})/g, /(né|née)\s+le\s+(\d{2}\/\d{2}\/\d{4})/gi]
datePatterns.forEach((pattern) => {
for (const match of text.matchAll(pattern)) {
const date = match[2] || match[1]
entities.dates.push({
id: `date-${(Array.isArray(entities.dates)?entities.dates:[]).length}`,
date: date,
type: match[1]?.toLowerCase().includes('né') ? 'birth' : 'general',
confidence: 0.9,
source: 'rule-based',
})
}
})
// Extraction des clauses contractuelles
const clausePatterns = [
/(Article\s+\d+[:\-]?\s*[^\.]+\.)/gi,
/(Clause\s+\d+[:\-]?\s*[^\.]+\.)/gi,
/(Conditions\s+générales[^\.]+\.)/gi,
/(Modalités\s+de\s+[^\.]+\.)/gi,
/(Obligations\s+du\s+[^\.]+\.)/gi,
/(Responsabilités[^\.]+\.)/gi,
]
clausePatterns.forEach((pattern) => {
for (const match of text.matchAll(pattern)) {
const clause = match[1] || match[0]
if (clause && clause.length > 10) {
entities.contractClauses.push({
id: `clause-${(Array.isArray(entities.contractClauses)?entities.contractClauses:[]).length}`,
text: clause.trim(),
type: 'contractual',
confidence: 0.8,
source: 'rule-based',
})
}
}
})
// Extraction des signatures
const signaturePatterns = [
/(Signé\s+le\s+\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/gi,
/(Signature\s+de\s+[A-Z][a-zà-öø-ÿ'\-]+\s+[A-Z][a-zà-öø-ÿ'\-]+)/gi,
/(Par\s+[A-Z][a-zà-öø-ÿ'\-]+\s+[A-Z][a-zà-öø-ÿ'\-]+)/gi,
/(Fait\s+et\s+signé\s+[^\.]+\.)/gi,
]
signaturePatterns.forEach((pattern) => {
for (const match of text.matchAll(pattern)) {
const signature = match[1] || match[0]
if (signature && signature.length > 5) {
entities.signatures.push({
id: `signature-${(Array.isArray(entities.signatures)?entities.signatures:[]).length}`,
text: signature.trim(),
type: 'signature',
confidence: 0.8,
source: 'rule-based',
})
}
}
})
// Classification du type de document
if (/carte\s+nationale\s+d'identité|cni|mrz|identite/i.test(text)) {
entities.documentType = 'CNI'
} else if (/facture|tva|siren|montant|facturation/i.test(text)) {
entities.documentType = 'Facture'
} else if (/attestation|certificat/i.test(text)) {
entities.documentType = 'Attestation'
} else if (/contrat|vente|achat|acte/i.test(text)) {
entities.documentType = 'Contrat'
}
console.log(`[NER] Extraction terminée:`)
console.log(` - Identités: ${(Array.isArray(entities.identities)?entities.identities:[]).length}`)
console.log(` - Sociétés: ${(Array.isArray(entities.companies)?entities.companies:[]).length}`)
console.log(` - Adresses: ${(Array.isArray(entities.addresses)?entities.addresses:[]).length}`)
console.log(` - Numéros CNI: ${(Array.isArray(entities.cniNumbers)?entities.cniNumbers:[]).length}`)
console.log(` - Dates: ${(Array.isArray(entities.dates)?entities.dates:[]).length}`)
console.log(` - Clauses contractuelles: ${(Array.isArray(entities.contractClauses)?entities.contractClauses:[]).length}`)
console.log(` - Signatures: ${(Array.isArray(entities.signatures)?entities.signatures:[]).length}`)
console.log(` - Type: ${entities.documentType}`)
return entities
}
// Route pour l'extraction de documents
app.post('/api/extract', upload.single('document'), async (req, res) => {
const startTime = Date.now()
try {
if (!req.file) {
return res.status(400).json({ error: 'Aucun fichier fourni' })
}
// Récupérer le hash du dossier depuis les paramètres de requête
const folderHash = req.body.folderHash || req.query.folderHash
if (!folderHash) {
return res.status(400).json({ error: 'Hash du dossier requis' })
}
console.log(
`[API] Traitement du fichier: ${req.file.originalname} dans le dossier: ${folderHash}`,
)
// Calculer le hash du fichier uploadé
const fileBuffer = fs.readFileSync(req.file.path)
const fileHash = calculateFileHash(fileBuffer)
console.log(`[HASH] Hash du fichier: ${fileHash.substring(0, 16)}...`)
// Vérifier d'abord le cache JSON dans le dossier
const cachedResult = getJsonCacheFromFolder(folderHash, fileHash)
if (cachedResult) {
console.log(`[CACHE] Utilisation du résultat en cache du dossier ${folderHash}`)
// Supprimer le fichier temporaire
fs.unlinkSync(req.file.path)
// Retourner le résultat en cache
return res.json(cachedResult)
}
// Vérifier si le fichier est déjà en cours de traitement
if (isFilePending(folderHash, fileHash)) {
console.log(`[CACHE] Fichier déjà en cours de traitement: ${fileHash}`)
fs.unlinkSync(req.file.path)
return res.status(202).json({
success: false,
status: 'pending',
message: 'Fichier en cours de traitement',
fileHash,
})
}
// Créer un flag pending pour ce fichier
createPendingFlag(folderHash, fileHash)
// Vérifier si un fichier avec le même hash existe déjà dans le dossier
const existingFile = findExistingFileByHash(fileHash, folderHash)
let isDuplicate = false
let duplicatePath = null
if (existingFile) {
console.log(
`[HASH] Fichier déjà existant trouvé dans le dossier ${folderHash}: ${existingFile.name}`,
)
isDuplicate = true
// Sauvegarder le chemin du doublon pour suppression ultérieure
duplicatePath = req.file.path
// Utiliser le fichier existant pour le traitement
req.file.path = existingFile.path
req.file.originalname = existingFile.name
} else {
console.log(`[HASH] Nouveau fichier, renommage avec hash dans le dossier ${folderHash}`)
// Créer la structure du dossier si elle n'existe pas
const { folderPath } = createFolderStructure(folderHash)
// Renommer le fichier avec son hash + extension dans le dossier
const ext = path.extname(req.file.originalname)
const newFileName = `${fileHash}${ext}`
const newFilePath = path.join(folderPath, newFileName)
// Renommer le fichier
fs.renameSync(req.file.path, newFilePath)
// Mettre à jour les informations du fichier
req.file.path = newFilePath
req.file.filename = newFileName
console.log(`[HASH] Fichier renommé: ${newFileName}`)
}
let ocrResult
let result
try {
// Si c'est un PDF, extraire le texte directement
if (req.file.mimetype === 'application/pdf') {
console.log(`[API] Extraction de texte depuis PDF...`)
try {
ocrResult = await extractTextFromPdf(req.file.path)
console.log(`[API] Texte extrait du PDF: ${ocrResult.text.length} caractères`)
} catch (error) {
console.error(`[API] Erreur lors de l'extraction PDF:`, error.message)
throw new Error(`Erreur lors de l'extraction PDF: ${error.message}`)
}
} else if (req.file.mimetype === 'text/plain') {
// Lecture directe pour les fichiers texte
console.log(`[API] Lecture de texte depuis fichier .txt...`)
try {
const text = fs.readFileSync(req.file.path, 'utf8')
ocrResult = {
text,
confidence: 95,
words: text.split(/\s+/).filter((w) => w.length > 0),
}
console.log(`[API] Texte lu: ${text.length} caractères`)
} catch (error) {
console.error(`[API] Erreur lecture .txt:`, error.message)
throw new Error(`Erreur lors de la lecture du fichier texte: ${error.message}`)
}
} else {
// Pour les images, utiliser l'OCR amélioré avec détection CNI
const { extractTextFromImageEnhanced } = require('./enhancedOcr')
ocrResult = await extractTextFromImageEnhanced(req.file.path)
}
// Extraction NER
const entities = extractEntitiesFromText(ocrResult.text)
// Mesure du temps de traitement
const processingTime = Date.now() - startTime
// Génération du format JSON standard (avec repli sûr)
try {
result = generateStandardJSON(req.file, ocrResult, entities, processingTime)
} catch (genErr) {
console.error('[API] Erreur generateStandardJSON, application d\'un repli:', genErr)
const safeText = typeof ocrResult.text === 'string' ? ocrResult.text : ''
const safeMime = req.file.mimetype || getMimeType(path.extname(req.file.path))
const fallbackProcessing = {
engine: '4NK_IA_Backend',
version: '1.0.0',
processingTime: `${processingTime}ms`,
ocrEngine: safeMime === 'application/pdf' ? 'pdf-parse' : 'tesseract.js',
nerEngine: 'rule-based',
preprocessing: {
applied: safeMime !== 'application/pdf',
reason: safeMime === 'application/pdf' ? 'PDF direct text extraction' : 'Image preprocessing applied',
},
}
result = {
document: {
id: `doc-${Date.now()}`,
fileName: req.file.originalname || path.basename(req.file.path),
fileSize: req.file.size || (fs.existsSync(req.file.path) ? fs.statSync(req.file.path).size : 0),
mimeType: safeMime,
uploadTimestamp: new Date().toISOString(),
},
classification: {
documentType: 'Document',
confidence: 0.6,
subType: getDocumentSubType('Document', safeText),
language: 'fr',
pageCount: 1,
},
extraction: {
text: {
raw: safeText,
processed: correctOCRText(safeText),
wordCount: safeText.trim().split(/\s+/).filter(Boolean).length,
characterCount: safeText.length,
confidence: (typeof ocrResult.confidence === 'number' ? ocrResult.confidence : 0) / 100,
},
entities: {
persons: [],
companies: [],
addresses: [],
financial: extractFinancialInfo(safeText, 'Document'),
dates: [],
contractual: { clauses: [], signatures: [] },
references: extractReferences(safeText, 'Document'),
},
},
metadata: {
processing: fallbackProcessing,
quality: {
globalConfidence: 0.6,
textExtractionConfidence: (typeof ocrResult.confidence === 'number' ? ocrResult.confidence : 0) / 100,
entityExtractionConfidence: 0.6,
classificationConfidence: 0.6,
},
},
status: { success: true, errors: ['fallback: generateStandardJSON error'], warnings: [], timestamp: new Date().toISOString() },
}
}
// Sauvegarder le résultat dans le cache du dossier
saveJsonCacheInFolder(folderHash, fileHash, result)
console.log(`[API] Traitement terminé en ${Date.now() - startTime}ms`)
} catch (error) {
console.error(`[API] Erreur lors du traitement du fichier ${fileHash}:`, error)
// Supprimer le flag pending en cas d'erreur
const { cachePath } = createFolderStructure(folderHash)
const pendingFile = path.join(cachePath, `${fileHash}.pending`)
if (fs.existsSync(pendingFile)) {
fs.unlinkSync(pendingFile)
console.log(`[CACHE] Flag pending supprimé après erreur pour ${fileHash}`)
}
return res.status(500).json({
success: false,
error: 'Erreur lors du traitement du document',
details: error.message,
})
} finally {
// Nettoyage du fichier temporaire
if (isDuplicate) {
// Supprimer le doublon uploadé
fs.unlinkSync(duplicatePath)
}
// Note: Ne pas supprimer req.file.path car c'est le fichier final dans le dossier
}
console.log(
`[API] Traitement terminé avec succès - Confiance: ${Math.round(result.metadata.quality.globalConfidence * 100)}%`,
)
res.json(result)
} catch (error) {
console.error('[API] Erreur lors du traitement:', error)
// Nettoyage en cas d'erreur
if (req.file && fs.existsSync(req.file.path)) {
fs.unlinkSync(req.file.path)
}
res.status(500).json({
success: false,
error: 'Erreur lors du traitement du document',
details: error.message,
})
}
})
// Route pour lister les fichiers de test
app.get('/api/test-files', (req, res) => {
try {
const testFilesDir = path.join(__dirname, '..', 'test-files')
const files = fs
.readdirSync(testFilesDir)
.filter((file) => {
const ext = path.extname(file).toLowerCase()
return ['.jpg', '.jpeg', '.png', '.pdf', '.tiff'].includes(ext)
})
.map((file) => {
const filePath = path.join(testFilesDir, file)
const stats = fs.statSync(filePath)
return {
name: file,
size: stats.size,
type: path.extname(file).toLowerCase(),
lastModified: stats.mtime,
}
})
res.json({ success: true, files })
} catch (error) {
res.status(500).json({ success: false, error: error.message })
}
})
// Route pour servir un fichier de test individuel
app.get('/api/test-files/:filename', (req, res) => {
try {
const filename = req.params.filename
const testFilesDir = path.join(__dirname, '..', 'test-files')
const filePath = path.join(testFilesDir, filename)
// Vérifier que le fichier existe et est dans le bon répertoire
if (!fs.existsSync(filePath)) {
return res.status(404).json({ success: false, error: 'Fichier non trouvé' })
}
// Vérifier que le fichier est bien dans le répertoire test-files (sécurité)
const resolvedPath = path.resolve(filePath)
const resolvedTestDir = path.resolve(testFilesDir)
if (!resolvedPath.startsWith(resolvedTestDir)) {
return res.status(403).json({ success: false, error: 'Accès non autorisé' })
}
// Servir le fichier
res.sendFile(filePath)
} catch (error) {
res.status(500).json({ success: false, error: error.message })
}
})
// Route de santé
// Route pour lister les fichiers uploadés avec leurs hash
app.get('/api/uploads', (req, res) => {
try {
const uploadDir = 'uploads/'
if (!fs.existsSync(uploadDir)) {
return res.json({ files: [] })
}
const files = fs.readdirSync(uploadDir)
const fileList = files
.map((file) => {
const filePath = path.join(uploadDir, file)
try {
const stats = fs.statSync(filePath)
// Extraire le hash du nom de fichier (format: hash.extension)
const ext = path.extname(file)
const hash = path.basename(file, ext)
return {
name: file,
size: stats.size,
hash: hash,
uploadDate: stats.birthtime,
modifiedDate: stats.mtime,
}
} catch (error) {
console.warn(`[API] Erreur lors de la lecture de ${file}:`, error.message)
return null
}
})
.filter((file) => file !== null)
res.json({
files: fileList,
count: fileList.length,
totalSize: fileList.reduce((sum, file) => sum + file.size, 0),
})
} catch (error) {
console.error('[API] Erreur lors de la liste des fichiers:', error)
res.status(500).json({ error: 'Erreur lors de la récupération des fichiers' })
}
})
// Route pour lister les fichiers de cache JSON
app.get('/api/cache', (req, res) => {
try {
const cacheFiles = listCacheFiles()
res.json({
files: cacheFiles,
count: cacheFiles.length,
totalSize: cacheFiles.reduce((sum, file) => sum + file.size, 0),
})
} catch (error) {
console.error('[API] Erreur lors de la liste du cache:', error)
res.status(500).json({ error: 'Erreur lors de la récupération du cache' })
}
})
// Route pour récupérer un résultat de cache spécifique
app.get('/api/cache/:hash', (req, res) => {
try {
const { hash } = req.params
const cachedResult = getJsonCache(hash)
if (cachedResult) {
res.json(cachedResult)
} else {
res.status(404).json({ error: 'Résultat non trouvé dans le cache' })
}
} catch (error) {
console.error('[API] Erreur lors de la récupération du cache:', error)
res.status(500).json({ error: 'Erreur lors de la récupération du cache' })
}
})
// Route pour supprimer un fichier de cache
app.delete('/api/cache/:hash', (req, res) => {
try {
const { hash } = req.params
const cacheFile = path.join('cache/', `${hash}.json`)
if (fs.existsSync(cacheFile)) {
fs.unlinkSync(cacheFile)
console.log(`[CACHE] Fichier supprimé: ${hash.substring(0, 16)}...`)
res.json({ message: 'Fichier de cache supprimé avec succès' })
} else {
res.status(404).json({ error: 'Fichier de cache non trouvé' })
}
} catch (error) {
console.error('[API] Erreur lors de la suppression du cache:', error)
res.status(500).json({ error: 'Erreur lors de la suppression du cache' })
}
})
// Route pour créer un nouveau dossier
app.post('/api/folders', (req, res) => {
try {
console.log("[FOLDER] Début de la création d'un nouveau dossier")
const folderHash = generateFolderHash()
console.log(`[FOLDER] Hash généré: ${folderHash}`)
const result = createFolderStructure(folderHash)
console.log(`[FOLDER] Structure créée:`, result)
console.log(`[FOLDER] Nouveau dossier créé: ${folderHash}`)
res.json({
success: true,
folderHash,
message: 'Dossier créé avec succès',
})
} catch (error) {
console.error('[FOLDER] Erreur lors de la création du dossier:', error)
res.status(500).json({
success: false,
error: error.message,
})
}
})
// Route pour récupérer les résultats d'un dossier
app.get('/api/folders/:folderHash/results', async (req, res) => {
try {
const { folderHash } = req.params
const folderData = await listFolderResults(folderHash)
console.log(
`[FOLDER] Résultats récupérés pour le dossier ${folderHash}: ${folderData.results.length} fichiers, ${folderData.pending.length} en cours`,
)
res.json({
success: true,
folderHash,
results: folderData.results,
pending: folderData.pending,
hasPending: folderData.hasPending,
count: folderData.results.length,
})
} catch (error) {
console.error('[FOLDER] Erreur lors de la récupération des résultats:', error)
res.status(500).json({
success: false,
error: error.message,
})
}
})
// Route pour récupérer un fichier original depuis un dossier
app.get('/api/folders/:folderHash/files/:fileHash', (req, res) => {
try {
const { folderHash, fileHash } = req.params
const folderPath = path.join('uploads', folderHash)
if (!fs.existsSync(folderPath)) {
return res.status(404).json({ success: false, error: 'Dossier non trouvé' })
}
const files = fs.readdirSync(folderPath)
const targetFile = files.find((file) => file.startsWith(fileHash))
if (!targetFile) {
return res.status(404).json({ success: false, error: 'Fichier non trouvé' })
}
const filePath = path.join(folderPath, targetFile)
res.sendFile(path.resolve(filePath))
} catch (error) {
console.error('[FOLDER] Erreur lors de la récupération du fichier:', error)
res.status(500).json({
success: false,
error: error.message,
})
}
})
// Route pour créer le dossier par défaut avec les fichiers de test
app.post('/api/folders/default', async (req, res) => {
try {
const folderHash = generateFolderHash()
const { folderPath, cachePath } = createFolderStructure(folderHash)
console.log(`[FOLDER] Création du dossier par défaut: ${folderHash}`)
// Charger les fichiers de test dans le dossier
const testFilesDir = path.join(__dirname, '..', 'test-files')
if (fs.existsSync(testFilesDir)) {
const testFiles = fs.readdirSync(testFilesDir)
const supportedFiles = testFiles.filter((file) =>
['.pdf', '.jpg', '.jpeg', '.png', '.tiff'].includes(path.extname(file).toLowerCase()),
)
for (const testFile of supportedFiles) {
const sourcePath = path.join(testFilesDir, testFile)
const fileBuffer = fs.readFileSync(sourcePath)
const fileHash = calculateFileHash(fileBuffer)
const ext = path.extname(testFile)
const newFileName = `${fileHash}${ext}`
const destPath = path.join(folderPath, newFileName)
// Copier le fichier
fs.copyFileSync(sourcePath, destPath)
// Traiter le fichier et sauvegarder le résultat
try {
console.log(`[FOLDER] Traitement de ${testFile}...`)
// Simuler un objet req.file pour la logique existante
const mockFile = {
path: destPath,
originalname: testFile,
mimetype: getMimeType(ext),
}
// Extraction de texte selon le type de fichier
let ocrResult = null
if (ext.toLowerCase() === '.pdf') {
ocrResult = await extractTextFromPdf(destPath)
} else if (['.jpg', '.jpeg', '.png', '.tiff'].includes(ext.toLowerCase())) {
ocrResult = await extractTextFromImage(destPath)
}
if (ocrResult && ocrResult.text) {
// Extraction des entités
const entities = extractEntitiesFromText(ocrResult.text)
// Génération du résultat au format standard
const result = generateStandardJSON(mockFile, ocrResult, entities, 0)
// Sauvegarde dans le cache du dossier
saveJsonCacheInFolder(folderHash, fileHash, result)
console.log(`[FOLDER] Fichier de test traité: ${testFile} -> ${fileHash}`)
} else {
console.warn(`[FOLDER] Aucun texte extrait de ${testFile}`)
}
} catch (error) {
console.warn(`[FOLDER] Erreur lors du traitement de ${testFile}:`, error.message)
}
}
}
res.json({
success: true,
folderHash,
message: 'Dossier par défaut créé avec succès',
})
} catch (error) {
console.error('[FOLDER] Erreur lors de la création du dossier par défaut:', error)
res.status(500).json({
success: false,
error: error.message,
})
}
})
app.get('/api/health', (req, res) => {
res.json({
status: 'OK',
timestamp: new Date().toISOString(),
version: '1.0.0',
})
})
// Démarrage du serveur
app.listen(PORT, () => {
console.log(`🚀 Serveur backend démarré sur le port ${PORT}`)
console.log(`📡 API disponible sur: http://localhost:${PORT}/api`)
console.log(`🏥 Health check: http://localhost:${PORT}/api/health`)
console.log(`📁 Test files: http://localhost:${PORT}/api/test-files`)
console.log(`📂 Répertoire de travail: ${process.cwd()}`)
// Nettoyer les flags pending orphelins au démarrage
cleanupOrphanedPendingFlags()
})
module.exports = app