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
|
||||||
|
}
|
||||||
@ -336,8 +336,9 @@ async function processDocument(filePath, fileHash) {
|
|||||||
throw new Error(`Erreur lors de l'extraction PDF: ${error.message}`)
|
throw new Error(`Erreur lors de l'extraction PDF: ${error.message}`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Pour les images, utiliser l'OCR avec préprocessing
|
// Pour les images, utiliser l'OCR amélioré avec détection CNI
|
||||||
ocrResult = await extractTextFromImage(filePath)
|
const { extractTextFromImageEnhanced } = require('./enhancedOcr')
|
||||||
|
ocrResult = await extractTextFromImageEnhanced(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extraction NER
|
// Extraction NER
|
||||||
@ -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}`)
|
throw new Error(`Erreur lors de l'extraction PDF: ${error.message}`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Pour les images, utiliser l'OCR avec préprocessing
|
// Pour les images, utiliser l'OCR amélioré avec détection CNI
|
||||||
ocrResult = await extractTextFromImage(req.file.path)
|
const { extractTextFromImageEnhanced } = require('./enhancedOcr')
|
||||||
|
ocrResult = await extractTextFromImageEnhanced(req.file.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extraction NER
|
// 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
|
// Démarrage du serveur
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`🚀 Serveur backend démarré sur le port ${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",
|
"@mui/material": "^7.3.2",
|
||||||
"@reduxjs/toolkit": "^2.9.0",
|
"@reduxjs/toolkit": "^2.9.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
|
"canvas": "^3.2.0",
|
||||||
|
"jimp": "^1.6.0",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"pdf-poppler": "^0.2.1",
|
"pdf-poppler": "^0.2.1",
|
||||||
"pdf2pic": "^3.2.0",
|
"pdf2pic": "^3.2.0",
|
||||||
|
|||||||
@ -178,19 +178,3 @@ export async function uploadFileToFolder(file: File, folderHash: string): Promis
|
|||||||
|
|
||||||
return response.json()
|
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 { documentApi } from '../services/api'
|
||||||
import { openaiDocumentApi } from '../services/openai'
|
import { openaiDocumentApi } from '../services/openai'
|
||||||
import { backendDocumentApi, checkBackendHealth } from '../services/backendApi'
|
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 {
|
interface DocumentState {
|
||||||
documents: Document[]
|
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({
|
const documentSlice = createSlice({
|
||||||
name: 'document',
|
name: 'document',
|
||||||
initialState,
|
initialState,
|
||||||
@ -443,21 +436,6 @@ const documentSlice = createSlice({
|
|||||||
state.loading = false
|
state.loading = false
|
||||||
state.error = action.error.message || 'Erreur lors de l\'upload du fichier'
|
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,
|
PictureAsPdf,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Add,
|
Add,
|
||||||
ContentCopy,
|
ContentCopy
|
||||||
Delete
|
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { useAppDispatch, useAppSelector } from '../store'
|
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 { Layout } from '../components/Layout'
|
||||||
import { FilePreview } from '../components/FilePreview'
|
import { FilePreview } from '../components/FilePreview'
|
||||||
import type { Document } from '../types'
|
import type { Document } from '../types'
|
||||||
@ -85,22 +84,6 @@ export default function UploadView() {
|
|||||||
}
|
}
|
||||||
}, [currentFolderHash])
|
}, [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(
|
const onDrop = useCallback(
|
||||||
async (acceptedFiles: File[]) => {
|
async (acceptedFiles: File[]) => {
|
||||||
if (!currentFolderHash) {
|
if (!currentFolderHash) {
|
||||||
@ -229,17 +212,6 @@ export default function UploadView() {
|
|||||||
>
|
>
|
||||||
Charger dossier
|
Charger dossier
|
||||||
</Button>
|
</Button>
|
||||||
{currentFolderHash && (
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<Delete />}
|
|
||||||
onClick={handleClearCache}
|
|
||||||
size="small"
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
Vider le cache
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user