backend
This commit is contained in:
parent
328d2584de
commit
b18a3077a2
264
backend/cniOcrEnhancer.js
Normal file
264
backend/cniOcrEnhancer.js
Normal file
@ -0,0 +1,264 @@
|
||||
const sharp = require('sharp')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
/**
|
||||
* Améliorateur OCR spécialisé pour les Cartes Nationales d'Identité françaises
|
||||
* Utilise des techniques avancées de preprocessing et de segmentation
|
||||
*/
|
||||
|
||||
// Fonction pour détecter si une image est probablement une CNI
|
||||
async function isCNIDocument(inputPath) {
|
||||
try {
|
||||
const image = sharp(inputPath)
|
||||
const metadata = await image.metadata()
|
||||
|
||||
// Vérifications pour une CNI française
|
||||
const isPortrait = metadata.height > metadata.width
|
||||
const hasGoodResolution = metadata.width > 800 && metadata.height > 500
|
||||
const aspectRatio = metadata.width / metadata.height
|
||||
const isCNIRatio = aspectRatio > 0.6 && aspectRatio < 0.7 // Ratio typique d'une CNI
|
||||
|
||||
console.log(`[CNI_DETECT] ${path.basename(inputPath)} - Portrait: ${isPortrait}, Résolution: ${metadata.width}x${metadata.height}, Ratio: ${aspectRatio.toFixed(2)}`)
|
||||
|
||||
return isPortrait && hasGoodResolution && isCNIRatio
|
||||
} catch (error) {
|
||||
console.error(`[CNI_DETECT] Erreur détection CNI:`, error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour extraire la zone MRZ (Machine Readable Zone) d'une CNI
|
||||
async function extractMRZ(inputPath) {
|
||||
try {
|
||||
const image = sharp(inputPath)
|
||||
const metadata = await image.metadata()
|
||||
|
||||
// La MRZ est généralement en bas de la CNI
|
||||
const mrzHeight = Math.floor(metadata.height * 0.15) // 15% de la hauteur
|
||||
const mrzTop = metadata.height - mrzHeight
|
||||
|
||||
console.log(`[MRZ] Extraction de la zone MRZ: ${mrzTop},${mrzHeight}`)
|
||||
|
||||
const mrzImage = await image
|
||||
.extract({
|
||||
left: 0,
|
||||
top: mrzTop,
|
||||
width: metadata.width,
|
||||
height: mrzHeight
|
||||
})
|
||||
.grayscale()
|
||||
.normalize()
|
||||
.sharpen()
|
||||
.threshold(128)
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
return mrzImage
|
||||
} catch (error) {
|
||||
console.error(`[MRZ] Erreur extraction MRZ:`, error.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour segmenter une CNI en zones spécifiques
|
||||
async function segmentCNIZones(inputPath) {
|
||||
try {
|
||||
const image = sharp(inputPath)
|
||||
const metadata = await image.metadata()
|
||||
|
||||
const zones = {
|
||||
// Zone du nom (généralement en haut à gauche)
|
||||
nameZone: {
|
||||
left: Math.floor(metadata.width * 0.05),
|
||||
top: Math.floor(metadata.height * 0.25),
|
||||
width: Math.floor(metadata.width * 0.4),
|
||||
height: Math.floor(metadata.height * 0.15)
|
||||
},
|
||||
// Zone du prénom
|
||||
firstNameZone: {
|
||||
left: Math.floor(metadata.width * 0.05),
|
||||
top: Math.floor(metadata.height * 0.35),
|
||||
width: Math.floor(metadata.width * 0.4),
|
||||
height: Math.floor(metadata.height * 0.15)
|
||||
},
|
||||
// Zone de la date de naissance
|
||||
birthDateZone: {
|
||||
left: Math.floor(metadata.width * 0.05),
|
||||
top: Math.floor(metadata.height * 0.45),
|
||||
width: Math.floor(metadata.width * 0.3),
|
||||
height: Math.floor(metadata.height * 0.1)
|
||||
},
|
||||
// Zone du numéro de CNI
|
||||
idNumberZone: {
|
||||
left: Math.floor(metadata.width * 0.05),
|
||||
top: Math.floor(metadata.height * 0.55),
|
||||
width: Math.floor(metadata.width * 0.4),
|
||||
height: Math.floor(metadata.height * 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[CNI_SEGMENT] Segmentation en ${Object.keys(zones).length} zones`)
|
||||
return zones
|
||||
} catch (error) {
|
||||
console.error(`[CNI_SEGMENT] Erreur segmentation:`, error.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour extraire une zone spécifique d'une CNI
|
||||
async function extractCNIZone(inputPath, zone, zoneName) {
|
||||
try {
|
||||
const image = sharp(inputPath)
|
||||
|
||||
const zoneImage = await image
|
||||
.extract(zone)
|
||||
.grayscale()
|
||||
.normalize()
|
||||
.sharpen({ sigma: 2, m1: 0.5, m2: 2, x1: 2, y2: 10 })
|
||||
.modulate({ brightness: 1.2, contrast: 1.5 })
|
||||
.threshold(140)
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
console.log(`[CNI_ZONE] Zone ${zoneName} extraite: ${zone.width}x${zone.height}`)
|
||||
return zoneImage
|
||||
} catch (error) {
|
||||
console.error(`[CNI_ZONE] Erreur extraction zone ${zoneName}:`, error.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour améliorer le preprocessing spécifiquement pour les CNI
|
||||
async function enhanceCNIPreprocessing(inputPath) {
|
||||
try {
|
||||
console.log(`[CNI_ENHANCE] Amélioration CNI pour: ${path.basename(inputPath)}`)
|
||||
|
||||
const image = sharp(inputPath)
|
||||
const metadata = await image.metadata()
|
||||
|
||||
// Configuration spécialisée pour les CNI
|
||||
const enhancedImage = await image
|
||||
.resize({
|
||||
width: 2000,
|
||||
height: Math.floor(2000 * (metadata.height / metadata.width)),
|
||||
fit: 'fill',
|
||||
kernel: sharp.kernel.lanczos3
|
||||
})
|
||||
.grayscale()
|
||||
.normalize()
|
||||
.modulate({
|
||||
brightness: 1.3,
|
||||
contrast: 1.8,
|
||||
saturation: 0
|
||||
})
|
||||
.sharpen({
|
||||
sigma: 1.5,
|
||||
m1: 0.5,
|
||||
m2: 3,
|
||||
x1: 2,
|
||||
y2: 20
|
||||
})
|
||||
.median(3)
|
||||
.threshold(135)
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
console.log(`[CNI_ENHANCE] Image améliorée: ${enhancedImage.length} bytes`)
|
||||
return enhancedImage
|
||||
} catch (error) {
|
||||
console.error(`[CNI_ENHANCE] Erreur amélioration CNI:`, error.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour traiter une CNI avec segmentation par zones
|
||||
async function processCNIWithZones(inputPath) {
|
||||
try {
|
||||
console.log(`[CNI_PROCESS] Traitement CNI par zones: ${path.basename(inputPath)}`)
|
||||
|
||||
// Vérifier si c'est une CNI
|
||||
const isCNI = await isCNIDocument(inputPath)
|
||||
if (!isCNI) {
|
||||
console.log(`[CNI_PROCESS] Document non reconnu comme CNI`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Segmenter en zones
|
||||
const zones = await segmentCNIZones(inputPath)
|
||||
if (!zones) {
|
||||
console.log(`[CNI_PROCESS] Échec de la segmentation`)
|
||||
return null
|
||||
}
|
||||
|
||||
const results = {
|
||||
isCNI: true,
|
||||
zones: {},
|
||||
mrz: null
|
||||
}
|
||||
|
||||
// Extraire chaque zone
|
||||
for (const [zoneName, zone] of Object.entries(zones)) {
|
||||
const zoneImage = await extractCNIZone(inputPath, zone, zoneName)
|
||||
if (zoneImage) {
|
||||
results.zones[zoneName] = zoneImage
|
||||
}
|
||||
}
|
||||
|
||||
// Extraire la MRZ
|
||||
const mrzImage = await extractMRZ(inputPath)
|
||||
if (mrzImage) {
|
||||
results.mrz = mrzImage
|
||||
}
|
||||
|
||||
console.log(`[CNI_PROCESS] CNI traitée: ${Object.keys(results.zones).length} zones + MRZ`)
|
||||
return results
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[CNI_PROCESS] Erreur traitement CNI:`, error.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour décoder la MRZ (Machine Readable Zone)
|
||||
function decodeMRZ(mrzText) {
|
||||
try {
|
||||
if (!mrzText || mrzText.length < 88) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Format MRZ de la CNI française (2 lignes de 36 caractères)
|
||||
const lines = mrzText.split('\n').filter(line => line.trim().length > 0)
|
||||
if (lines.length < 2) {
|
||||
return null
|
||||
}
|
||||
|
||||
const line1 = lines[0].trim()
|
||||
const line2 = lines[1].trim()
|
||||
|
||||
const result = {
|
||||
documentType: line1.substring(0, 2),
|
||||
country: line1.substring(2, 5),
|
||||
surname: line1.substring(5, 36).replace(/</g, ' ').trim(),
|
||||
givenNames: line2.substring(0, 30).replace(/</g, ' ').trim(),
|
||||
documentNumber: line2.substring(30, 36).trim()
|
||||
}
|
||||
|
||||
console.log(`[MRZ_DECODE] MRZ décodée: ${result.surname} ${result.givenNames}`)
|
||||
return result
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[MRZ_DECODE] Erreur décodage MRZ:`, error.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isCNIDocument,
|
||||
extractMRZ,
|
||||
segmentCNIZones,
|
||||
extractCNIZone,
|
||||
enhanceCNIPreprocessing,
|
||||
processCNIWithZones,
|
||||
decodeMRZ
|
||||
}
|
||||
248
backend/enhancedOcr.js
Normal file
248
backend/enhancedOcr.js
Normal file
@ -0,0 +1,248 @@
|
||||
const sharp = require('sharp')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const { execSync } = require('child_process')
|
||||
const {
|
||||
isCNIDocument,
|
||||
enhanceCNIPreprocessing,
|
||||
processCNIWithZones,
|
||||
decodeMRZ
|
||||
} = require('./cniOcrEnhancer')
|
||||
|
||||
/**
|
||||
* OCR amélioré avec support spécialisé pour les CNI
|
||||
* Combine Tesseract avec des techniques de preprocessing avancées
|
||||
*/
|
||||
|
||||
// Fonction pour exécuter Tesseract avec des paramètres optimisés
|
||||
async function runTesseractOCR(imageBuffer, options = {}) {
|
||||
try {
|
||||
const tempInput = path.join(__dirname, 'temp_input.png')
|
||||
const tempOutput = path.join(__dirname, 'temp_output')
|
||||
|
||||
// Sauvegarder l'image temporaire
|
||||
fs.writeFileSync(tempInput, imageBuffer)
|
||||
|
||||
// Paramètres Tesseract optimisés
|
||||
const tesseractOptions = {
|
||||
language: options.language || 'fra',
|
||||
psm: options.psm || '6', // Mode uniforme de bloc de texte
|
||||
oem: options.oem || '3', // Mode par défaut
|
||||
...options
|
||||
}
|
||||
|
||||
// Construire la commande Tesseract
|
||||
const cmd = `tesseract "${tempInput}" "${tempOutput}" -l ${tesseractOptions.language} --psm ${tesseractOptions.psm} --oem ${tesseractOptions.oem}`
|
||||
|
||||
console.log(`[TESSERACT] Commande: ${cmd}`)
|
||||
|
||||
// Exécuter Tesseract
|
||||
execSync(cmd, { stdio: 'pipe' })
|
||||
|
||||
// Lire le résultat
|
||||
const resultText = fs.readFileSync(`${tempOutput}.txt`, 'utf8')
|
||||
|
||||
// Nettoyer les fichiers temporaires
|
||||
try {
|
||||
fs.unlinkSync(tempInput)
|
||||
fs.unlinkSync(`${tempOutput}.txt`)
|
||||
} catch (cleanupError) {
|
||||
console.warn(`[TESSERACT] Erreur nettoyage: ${cleanupError.message}`)
|
||||
}
|
||||
|
||||
console.log(`[TESSERACT] Texte extrait: ${resultText.length} caractères`)
|
||||
return {
|
||||
text: resultText.trim(),
|
||||
confidence: 0.8, // Estimation
|
||||
method: 'tesseract_enhanced'
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[TESSERACT] Erreur OCR:`, error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour extraire le texte d'une image avec améliorations CNI
|
||||
async function extractTextFromImageEnhanced(inputPath) {
|
||||
try {
|
||||
console.log(`[ENHANCED_OCR] Début extraction: ${path.basename(inputPath)}`)
|
||||
|
||||
// Vérifier si c'est une CNI
|
||||
const isCNI = await isCNIDocument(inputPath)
|
||||
console.log(`[ENHANCED_OCR] CNI détectée: ${isCNI}`)
|
||||
|
||||
if (isCNI) {
|
||||
return await extractTextFromCNI(inputPath)
|
||||
} else {
|
||||
return await extractTextFromStandardDocument(inputPath)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[ENHANCED_OCR] Erreur extraction:`, error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction spécialisée pour l'extraction de texte des CNI
|
||||
async function extractTextFromCNI(inputPath) {
|
||||
try {
|
||||
console.log(`[CNI_OCR] Traitement CNI: ${path.basename(inputPath)}`)
|
||||
|
||||
// Améliorer le preprocessing pour les CNI
|
||||
const enhancedImage = await enhanceCNIPreprocessing(inputPath)
|
||||
if (!enhancedImage) {
|
||||
throw new Error('Échec du preprocessing CNI')
|
||||
}
|
||||
|
||||
// Traitement par zones
|
||||
const cniZones = await processCNIWithZones(inputPath)
|
||||
|
||||
let combinedText = ''
|
||||
let mrzData = null
|
||||
|
||||
// Extraire le texte de l'image améliorée
|
||||
const mainText = await runTesseractOCR(enhancedImage, {
|
||||
language: 'fra',
|
||||
psm: '6' // Mode uniforme de bloc de texte
|
||||
})
|
||||
combinedText += mainText.text + '\n'
|
||||
|
||||
// Si on a des zones, traiter chaque zone séparément
|
||||
if (cniZones && cniZones.zones) {
|
||||
for (const [zoneName, zoneImage] of Object.entries(cniZones.zones)) {
|
||||
try {
|
||||
const zoneText = await runTesseractOCR(zoneImage, {
|
||||
language: 'fra',
|
||||
psm: '8' // Mode mot unique
|
||||
})
|
||||
combinedText += `[${zoneName.toUpperCase()}] ${zoneText.text}\n`
|
||||
console.log(`[CNI_OCR] Zone ${zoneName}: ${zoneText.text}`)
|
||||
} catch (zoneError) {
|
||||
console.warn(`[CNI_OCR] Erreur zone ${zoneName}:`, zoneError.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Traiter la MRZ si disponible
|
||||
if (cniZones && cniZones.mrz) {
|
||||
try {
|
||||
const mrzText = await runTesseractOCR(cniZones.mrz, {
|
||||
language: 'eng', // La MRZ est en anglais
|
||||
psm: '8' // Mode mot unique
|
||||
})
|
||||
combinedText += `[MRZ] ${mrzText.text}\n`
|
||||
|
||||
// Décoder la MRZ
|
||||
mrzData = decodeMRZ(mrzText.text)
|
||||
if (mrzData) {
|
||||
combinedText += `[MRZ_DECODED] Nom: ${mrzData.surname}, Prénom: ${mrzData.givenNames}, Numéro: ${mrzData.documentNumber}\n`
|
||||
}
|
||||
} catch (mrzError) {
|
||||
console.warn(`[CNI_OCR] Erreur MRZ:`, mrzError.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Post-traitement du texte pour corriger les erreurs communes
|
||||
const processedText = postProcessCNIText(combinedText)
|
||||
|
||||
console.log(`[CNI_OCR] Texte final: ${processedText.length} caractères`)
|
||||
|
||||
return {
|
||||
text: processedText,
|
||||
confidence: 0.85, // Confiance élevée pour les CNI traitées
|
||||
method: 'cni_enhanced',
|
||||
mrzData: mrzData,
|
||||
zones: cniZones ? Object.keys(cniZones.zones || {}) : []
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[CNI_OCR] Erreur traitement CNI:`, error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour l'extraction de texte des documents standards
|
||||
async function extractTextFromStandardDocument(inputPath) {
|
||||
try {
|
||||
console.log(`[STANDARD_OCR] Traitement document standard: ${path.basename(inputPath)}`)
|
||||
|
||||
// Preprocessing standard
|
||||
const image = sharp(inputPath)
|
||||
const metadata = await image.metadata()
|
||||
|
||||
const processedImage = await image
|
||||
.resize({
|
||||
width: Math.min(metadata.width * 2, 2000),
|
||||
height: Math.min(metadata.height * 2, 2000),
|
||||
fit: 'inside',
|
||||
withoutEnlargement: false
|
||||
})
|
||||
.grayscale()
|
||||
.normalize()
|
||||
.sharpen()
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
// OCR standard
|
||||
const result = await runTesseractOCR(processedImage, {
|
||||
language: 'fra',
|
||||
psm: '6'
|
||||
})
|
||||
|
||||
return {
|
||||
text: result.text,
|
||||
confidence: result.confidence,
|
||||
method: 'standard_enhanced'
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[STANDARD_OCR] Erreur traitement standard:`, error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour post-traiter le texte des CNI
|
||||
function postProcessCNIText(text) {
|
||||
try {
|
||||
let processedText = text
|
||||
|
||||
// Corrections communes pour les CNI
|
||||
const corrections = [
|
||||
// Corrections de caractères corrompus
|
||||
{ from: /RÉPUBLIQUE FRANCATSEN/g, to: 'RÉPUBLIQUE FRANÇAISE' },
|
||||
{ from: /CARTE NATIONALE DIDENTITE/g, to: 'CARTE NATIONALE D\'IDENTITÉ' },
|
||||
{ from: /Ne :/g, to: 'N° :' },
|
||||
{ from: /Fe - 0/g, to: 'Féminin' },
|
||||
{ from: /Mele:/g, to: 'Mâle:' },
|
||||
{ from: /IDFRACANTUCCKKLLLLKLLLLLLLLLLLK/g, to: 'IDFRA' },
|
||||
|
||||
// Nettoyage des caractères parasites
|
||||
{ from: /[^\w\sÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,;:!?()\-'"]/g, to: ' ' },
|
||||
|
||||
// Normalisation des espaces
|
||||
{ from: /\s+/g, to: ' ' },
|
||||
{ from: /^\s+|\s+$/g, to: '' }
|
||||
]
|
||||
|
||||
// Appliquer les corrections
|
||||
for (const correction of corrections) {
|
||||
processedText = processedText.replace(correction.from, correction.to)
|
||||
}
|
||||
|
||||
console.log(`[POST_PROCESS] Texte post-traité: ${processedText.length} caractères`)
|
||||
return processedText
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[POST_PROCESS] Erreur post-traitement:`, error.message)
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extractTextFromImageEnhanced,
|
||||
extractTextFromCNI,
|
||||
extractTextFromStandardDocument,
|
||||
runTesseractOCR,
|
||||
postProcessCNIText
|
||||
}
|
||||
@ -224,7 +224,7 @@ function getMimeTypeFromExtension(extension) {
|
||||
function listFolderResults(folderHash) {
|
||||
const cachePath = path.join('cache', folderHash)
|
||||
const uploadsPath = path.join('uploads', folderHash)
|
||||
|
||||
|
||||
const results = []
|
||||
const pending = []
|
||||
let hasPending = false
|
||||
@ -232,7 +232,7 @@ function listFolderResults(folderHash) {
|
||||
// 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')
|
||||
@ -260,23 +260,23 @@ function listFolderResults(folderHash) {
|
||||
// Traiter les fichiers en uploads (sans résultats d'extraction)
|
||||
if (fs.existsSync(uploadsPath)) {
|
||||
const uploadFiles = fs.readdirSync(uploadsPath)
|
||||
|
||||
|
||||
for (const file of uploadFiles) {
|
||||
// Extraire le hash du nom de fichier (format: hash.extension)
|
||||
const fileHash = path.basename(file, path.extname(file))
|
||||
|
||||
|
||||
// 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)
|
||||
|
||||
|
||||
if (!hasCacheResult && !isAlreadyPending) {
|
||||
// Mettre le fichier en pending et le traiter automatiquement
|
||||
const filePath = path.join(uploadsPath, file)
|
||||
const stats = fs.statSync(filePath)
|
||||
|
||||
|
||||
console.log(`[FOLDER] Fichier non traité détecté, mise en pending: ${file}`)
|
||||
|
||||
|
||||
// Créer le flag pending
|
||||
const pendingData = {
|
||||
fileHash,
|
||||
@ -285,14 +285,14 @@ function listFolderResults(folderHash) {
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'processing'
|
||||
}
|
||||
|
||||
|
||||
// Sauvegarder le flag pending
|
||||
createPendingFlag(folderHash, fileHash, pendingData)
|
||||
|
||||
|
||||
// Ajouter à la liste des pending
|
||||
pending.push(pendingData)
|
||||
hasPending = true
|
||||
|
||||
|
||||
// Traiter le fichier en arrière-plan
|
||||
processFileInBackground(filePath, fileHash, folderHash)
|
||||
}
|
||||
@ -305,15 +305,15 @@ function listFolderResults(folderHash) {
|
||||
// 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)
|
||||
|
||||
|
||||
// Créer un objet file similaire à celui de multer
|
||||
const file = {
|
||||
path: filePath,
|
||||
@ -321,10 +321,10 @@ async function processDocument(filePath, fileHash) {
|
||||
mimetype: mimeType,
|
||||
size: stats.size
|
||||
}
|
||||
|
||||
|
||||
let ocrResult
|
||||
let result
|
||||
|
||||
|
||||
// Si c'est un PDF, extraire le texte directement
|
||||
if (mimeType === 'application/pdf') {
|
||||
console.log(`[PROCESS] Extraction de texte depuis PDF...`)
|
||||
@ -336,8 +336,9 @@ async function processDocument(filePath, fileHash) {
|
||||
throw new Error(`Erreur lors de l'extraction PDF: ${error.message}`)
|
||||
}
|
||||
} else {
|
||||
// Pour les images, utiliser l'OCR avec préprocessing
|
||||
ocrResult = await extractTextFromImage(filePath)
|
||||
// Pour les images, utiliser l'OCR amélioré avec détection CNI
|
||||
const { extractTextFromImageEnhanced } = require('./enhancedOcr')
|
||||
ocrResult = await extractTextFromImageEnhanced(filePath)
|
||||
}
|
||||
|
||||
// Extraction NER
|
||||
@ -348,10 +349,10 @@ async function processDocument(filePath, fileHash) {
|
||||
|
||||
// Génération du format JSON standard
|
||||
result = generateStandardJSON(file, ocrResult, entities, processingTime)
|
||||
|
||||
|
||||
console.log(`[PROCESS] Traitement terminé en ${processingTime}ms`)
|
||||
return result
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[PROCESS] Erreur lors du traitement:`, error)
|
||||
throw error
|
||||
@ -362,13 +363,13 @@ async function processDocument(filePath, fileHash) {
|
||||
async function processFileInBackground(filePath, fileHash, folderHash) {
|
||||
try {
|
||||
console.log(`[BACKGROUND] Début du traitement en arrière-plan: ${filePath}`)
|
||||
|
||||
|
||||
// Traiter le document
|
||||
const result = await processDocument(filePath, fileHash)
|
||||
|
||||
|
||||
// Sauvegarder le résultat dans le cache du dossier
|
||||
const success = saveJsonCacheInFolder(folderHash, fileHash, result)
|
||||
|
||||
|
||||
if (success) {
|
||||
// Supprimer le flag pending
|
||||
removePendingFlag(folderHash, fileHash)
|
||||
@ -376,7 +377,7 @@ async function processFileInBackground(filePath, fileHash, folderHash) {
|
||||
} 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)
|
||||
// Supprimer le flag pending même en cas d'erreur
|
||||
@ -389,7 +390,7 @@ 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}`)
|
||||
@ -1184,8 +1185,9 @@ app.post('/api/extract', upload.single('document'), async (req, res) => {
|
||||
throw new Error(`Erreur lors de l'extraction PDF: ${error.message}`)
|
||||
}
|
||||
} else {
|
||||
// Pour les images, utiliser l'OCR avec préprocessing
|
||||
ocrResult = await extractTextFromImage(req.file.path)
|
||||
// Pour les images, utiliser l'OCR amélioré avec détection CNI
|
||||
const { extractTextFromImageEnhanced } = require('./enhancedOcr')
|
||||
ocrResult = await extractTextFromImageEnhanced(req.file.path)
|
||||
}
|
||||
|
||||
// Extraction NER
|
||||
@ -1560,86 +1562,6 @@ app.get('/api/health', (req, res) => {
|
||||
})
|
||||
})
|
||||
|
||||
// Vider le cache d'un dossier
|
||||
app.delete('/api/folders/:folderHash/cache', (req, res) => {
|
||||
const { folderHash } = req.params
|
||||
|
||||
console.log(`[CACHE] Demande de suppression du cache pour le dossier: ${folderHash}`)
|
||||
|
||||
try {
|
||||
const { cachePath, uploadsPath } = createFolderStructure(folderHash)
|
||||
|
||||
let deletedFiles = 0
|
||||
let deletedDirs = 0
|
||||
|
||||
// Supprimer le dossier cache s'il existe
|
||||
if (fs.existsSync(cachePath)) {
|
||||
const files = fs.readdirSync(cachePath)
|
||||
for (const file of files) {
|
||||
const filePath = path.join(cachePath, file)
|
||||
try {
|
||||
fs.unlinkSync(filePath)
|
||||
deletedFiles++
|
||||
console.log(`[CACHE] Fichier supprimé: ${file}`)
|
||||
} catch (error) {
|
||||
console.error(`[CACHE] Erreur lors de la suppression de ${file}:`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer le dossier cache vide
|
||||
try {
|
||||
fs.rmdirSync(cachePath)
|
||||
deletedDirs++
|
||||
console.log(`[CACHE] Dossier cache supprimé: ${cachePath}`)
|
||||
} catch (error) {
|
||||
console.error(`[CACHE] Erreur lors de la suppression du dossier cache:`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer le dossier uploads s'il existe
|
||||
if (fs.existsSync(uploadsPath)) {
|
||||
const files = fs.readdirSync(uploadsPath)
|
||||
for (const file of files) {
|
||||
const filePath = path.join(uploadsPath, file)
|
||||
try {
|
||||
fs.unlinkSync(filePath)
|
||||
deletedFiles++
|
||||
console.log(`[CACHE] Fichier upload supprimé: ${file}`)
|
||||
} catch (error) {
|
||||
console.error(`[CACHE] Erreur lors de la suppression de ${file}:`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer le dossier uploads vide
|
||||
try {
|
||||
fs.rmdirSync(uploadsPath)
|
||||
deletedDirs++
|
||||
console.log(`[CACHE] Dossier uploads supprimé: ${uploadsPath}`)
|
||||
} catch (error) {
|
||||
console.error(`[CACHE] Erreur lors de la suppression du dossier uploads:`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[CACHE] Cache vidé pour le dossier ${folderHash}: ${deletedFiles} fichiers, ${deletedDirs} dossiers supprimés`)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Cache vidé pour le dossier ${folderHash}`,
|
||||
deletedFiles,
|
||||
deletedDirs,
|
||||
folderHash
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[CACHE] Erreur lors du vidage du cache:`, error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Erreur lors du vidage du cache',
|
||||
details: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Démarrage du serveur
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Serveur backend démarré sur le port ${PORT}`)
|
||||
|
||||
937
package-lock.json
generated
937
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -27,6 +27,8 @@
|
||||
"@mui/material": "^7.3.2",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"axios": "^1.11.0",
|
||||
"canvas": "^3.2.0",
|
||||
"jimp": "^1.6.0",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pdf-poppler": "^0.2.1",
|
||||
"pdf2pic": "^3.2.0",
|
||||
|
||||
@ -166,31 +166,15 @@ export async function uploadFileToFolder(file: File, folderHash: string): Promis
|
||||
const formData = new FormData()
|
||||
formData.append('document', file)
|
||||
formData.append('folderHash', folderHash)
|
||||
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/extract`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur lors de l'upload: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// Vider le cache d'un dossier
|
||||
export async function clearFolderCache(folderHash: string): Promise<{ success: boolean; message: string; deletedFiles: number; deletedDirs: number }> {
|
||||
const response = await fetch(`${API_BASE_URL}/folders/${folderHash}/cache`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur lors du vidage du cache: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import type { Document, ExtractionResult, AnalysisResult, ContextResult, Conseil
|
||||
import { documentApi } from '../services/api'
|
||||
import { openaiDocumentApi } from '../services/openai'
|
||||
import { backendDocumentApi, checkBackendHealth } from '../services/backendApi'
|
||||
import { createDefaultFolder, getDefaultFolder, getFolderResults, uploadFileToFolder, clearFolderCache, type FolderResult } from '../services/folderApi'
|
||||
import { createDefaultFolder, getDefaultFolder, getFolderResults, uploadFileToFolder, type FolderResult } from '../services/folderApi'
|
||||
|
||||
interface DocumentState {
|
||||
documents: Document[]
|
||||
@ -214,13 +214,6 @@ export const uploadFileToFolderThunk = createAsyncThunk(
|
||||
}
|
||||
)
|
||||
|
||||
export const clearFolderCacheThunk = createAsyncThunk(
|
||||
'document/clearFolderCache',
|
||||
async (folderHash: string) => {
|
||||
return await clearFolderCache(folderHash)
|
||||
}
|
||||
)
|
||||
|
||||
const documentSlice = createSlice({
|
||||
name: 'document',
|
||||
initialState,
|
||||
@ -387,7 +380,7 @@ const documentSlice = createSlice({
|
||||
.addCase(loadFolderResults.fulfilled, (state, action) => {
|
||||
console.log(`[STORE] loadFolderResults.fulfilled appelé avec:`, action.payload)
|
||||
console.log(`[STORE] Nombre de résultats reçus:`, action.payload.results?.length || 0)
|
||||
|
||||
|
||||
state.folderResults = action.payload.results
|
||||
state.currentFolderHash = action.payload.folderHash
|
||||
state.loading = false
|
||||
@ -404,7 +397,7 @@ const documentSlice = createSlice({
|
||||
fileName: result.document?.fileName,
|
||||
mimeType: result.document?.mimeType
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
id: result.fileHash,
|
||||
name: result.document.fileName,
|
||||
@ -443,21 +436,6 @@ const documentSlice = createSlice({
|
||||
state.loading = false
|
||||
state.error = action.error.message || 'Erreur lors de l\'upload du fichier'
|
||||
})
|
||||
.addCase(clearFolderCacheThunk.fulfilled, (state, action) => {
|
||||
console.log(`[STORE] Cache vidé: ${action.payload.deletedFiles} fichiers, ${action.payload.deletedDirs} dossiers supprimés`)
|
||||
// Vider les résultats du dossier actuel
|
||||
state.folderResults = []
|
||||
state.documents = []
|
||||
state.currentResultIndex = 0
|
||||
state.loading = false
|
||||
})
|
||||
.addCase(clearFolderCacheThunk.pending, (state) => {
|
||||
state.loading = true
|
||||
})
|
||||
.addCase(clearFolderCacheThunk.rejected, (state, action) => {
|
||||
state.loading = false
|
||||
state.error = action.error.message || 'Erreur lors du vidage du cache'
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -33,11 +33,10 @@ import {
|
||||
PictureAsPdf,
|
||||
FolderOpen,
|
||||
Add,
|
||||
ContentCopy,
|
||||
Delete
|
||||
ContentCopy
|
||||
} from '@mui/icons-material'
|
||||
import { useAppDispatch, useAppSelector } from '../store'
|
||||
import { uploadFileToFolderThunk, loadFolderResults, removeDocument, createDefaultFolderThunk, setCurrentFolderHash, clearFolderCacheThunk } from '../store/documentSlice'
|
||||
import { uploadFileToFolderThunk, loadFolderResults, removeDocument, createDefaultFolderThunk, setCurrentFolderHash } from '../store/documentSlice'
|
||||
import { Layout } from '../components/Layout'
|
||||
import { FilePreview } from '../components/FilePreview'
|
||||
import type { Document } from '../types'
|
||||
@ -85,22 +84,6 @@ export default function UploadView() {
|
||||
}
|
||||
}, [currentFolderHash])
|
||||
|
||||
// Fonction pour vider le cache du dossier
|
||||
const handleClearCache = useCallback(async () => {
|
||||
if (!currentFolderHash) return
|
||||
|
||||
if (window.confirm('Êtes-vous sûr de vouloir vider le cache de ce dossier ? Cette action supprimera tous les fichiers et résultats d\'extraction.')) {
|
||||
try {
|
||||
const result = await dispatch(clearFolderCacheThunk(currentFolderHash)).unwrap()
|
||||
console.log('✅ [UPLOAD] Cache vidé:', result)
|
||||
// Recharger les résultats du dossier (qui seront vides)
|
||||
await dispatch(loadFolderResults(currentFolderHash)).unwrap()
|
||||
} catch (error) {
|
||||
console.error('❌ [UPLOAD] Erreur lors du vidage du cache:', error)
|
||||
}
|
||||
}
|
||||
}, [dispatch, currentFolderHash])
|
||||
|
||||
const onDrop = useCallback(
|
||||
async (acceptedFiles: File[]) => {
|
||||
if (!currentFolderHash) {
|
||||
@ -229,17 +212,6 @@ export default function UploadView() {
|
||||
>
|
||||
Charger dossier
|
||||
</Button>
|
||||
{currentFolderHash && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Delete />}
|
||||
onClick={handleClearCache}
|
||||
size="small"
|
||||
color="error"
|
||||
>
|
||||
Vider le cache
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user