From 883f49e2e26e29d5c20d28fede9422211f4540e3 Mon Sep 17 00:00:00 2001 From: 4NK IA Date: Wed, 17 Sep 2025 09:59:14 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20re-traiter=20le=20dossier=20(vider=20ca?= =?UTF-8?q?che=20+=20reprocess);=20UI=20extraction=20robuste=20entit=C3=A9?= =?UTF-8?q?s;=20Stepper=20+=20liste=20avec=20ellipsis;=20backend=20DELETE?= =?UTF-8?q?=20/folders/:hash/cache=20et=20POST=20/folders/:hash/reprocess?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 12 +- backend/cniOcrEnhancer.js | 32 +- backend/enhancedOcr.js | 36 +- backend/imagePreprocessing.js | 59 ++- backend/package-lock.json | 88 ++-- backend/package.json | 4 +- backend/pdfConverter.js | 12 +- backend/server.js | 650 ++++++++++++++++++++---------- backend_output.json | 4 +- current_backend_output.json | 4 +- docker-compose.registry.yml | 8 +- docs/ANALYSE_REPO.md | 139 +++++++ docs/API.md | 2 +- docs/API_BACKEND.md | 58 ++- docs/CACHE_ET_TRAITEMENT_ASYNC.md | 30 ++ docs/HASH_SYSTEM.md | 12 +- docs/SYSTEME_FONCTIONNEL.md | 239 +++++++++++ docs/TODO.md | 15 +- docs/architecture-backend.md | 26 +- docs/changelog-pending.md | 18 + docs/extact_model.json | 380 +++++++++-------- docs/systeme-pending.md | 2 +- document_analysis.json | 22 +- package.json | 2 +- scripts/check-node.mjs | 32 +- scripts/precache.cjs | 117 ++++++ scripts/precache.js | 113 ++++++ scripts/process-uploaded-files.sh | 75 ++++ scripts/simple-server.js | 106 ++--- src/App.tsx | 31 +- src/components/FilePreview.tsx | 55 +-- src/components/Layout.tsx | 27 +- src/components/NavigationTabs.tsx | 8 +- src/index.css | 5 +- src/router/index.tsx | 38 +- src/services/api.ts | 81 ++-- src/services/backendApi.ts | 55 +-- src/services/fileExtract.ts | 31 +- src/services/folderApi.ts | 16 +- src/services/openai.ts | 71 +++- src/services/ruleNer.ts | 21 +- src/services/testFilesApi.ts | 108 +++++ src/store/appSlice.ts | 2 - src/store/documentSlice.ts | 153 +++---- src/store/index.ts | 44 +- src/views/ConseilView.tsx | 49 ++- src/views/ContexteView.tsx | 37 +- src/views/ExtractionView.tsx | 54 +-- src/views/UploadView.tsx | 77 ++-- test-backend-direct.cjs | 24 +- test-cni-direct.cjs | 67 +-- test-folders.js | 76 ++-- test-format.js | 67 +-- test-web-interface.cjs | 70 ++-- tests/testFilesApi.test.ts | 93 ++++- vk_swiftshader_icd.json | 5 +- 56 files changed, 2484 insertions(+), 1178 deletions(-) create mode 100644 docs/ANALYSE_REPO.md create mode 100644 docs/CACHE_ET_TRAITEMENT_ASYNC.md create mode 100644 docs/SYSTEME_FONCTIONNEL.md create mode 100644 scripts/precache.cjs create mode 100644 scripts/precache.js create mode 100755 scripts/process-uploaded-files.sh create mode 100644 src/services/testFilesApi.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d3caca..464ef2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,14 @@ ### ✨ Nouvelles Fonctionnalités #### 🔐 Système de Hash et Cache JSON + - **Système de hash SHA-256** : Calcul automatique du hash pour chaque fichier uploadé - **Détection des doublons** : Évite les fichiers identiques basés sur le contenu - **Cache JSON** : Sauvegarde automatique des résultats d'extraction - **Optimisation des performances** : Réutilisation des résultats en cache #### 🛠️ Nouvelles Fonctions Backend + - `calculateFileHash(buffer)` : Calcule le hash SHA-256 d'un fichier - `findExistingFileByHash(hash)` : Trouve les fichiers existants par hash - `saveJsonCache(hash, result)` : Sauvegarde un résultat dans le cache @@ -18,6 +20,7 @@ - `listCacheFiles()` : Liste tous les fichiers de cache #### 📡 Nouvelles Routes API + - `GET /api/cache` : Liste les fichiers de cache avec métadonnées - `GET /api/cache/:hash` : Récupère un résultat de cache spécifique - `DELETE /api/cache/:hash` : Supprime un fichier de cache @@ -26,6 +29,7 @@ ### 🔧 Améliorations Techniques #### Backend (`backend/server.js`) + - Intégration du système de cache dans la route `/api/extract` - Vérification du cache avant traitement - Sauvegarde automatique des résultats après traitement @@ -33,28 +37,33 @@ - Logs détaillés pour le debugging #### Configuration + - Ajout du dossier `cache/` au `.gitignore` - Configuration des remotes Git pour SSH/HTTPS ### 📚 Documentation #### Nouveaux Fichiers + - `docs/HASH_SYSTEM.md` : Documentation complète du système de hash - `CHANGELOG.md` : Historique des versions #### Mises à Jour + - `docs/API_BACKEND.md` : Ajout de la documentation des nouvelles routes - Caractéristiques principales mises à jour ### 🚀 Performance #### Optimisations + - **Traitement instantané** pour les fichiers en cache - **Économie de stockage** : Pas de fichiers dupliqués - **Réduction des calculs** : Réutilisation des résultats existants - **Logs optimisés** : Indication claire de l'utilisation du cache #### Métriques + - Temps de traitement réduit de ~80% pour les fichiers en cache - Stockage optimisé (suppression automatique des doublons) - Cache JSON : ~227KB pour un document PDF de 992KB @@ -97,6 +106,7 @@ graph TD ## [1.0.0] - 2025-09-15 ### 🎉 Version Initiale + - Système d'extraction de documents (PDF, images) - OCR avec Tesseract.js - NER par règles @@ -106,4 +116,4 @@ graph TD --- -*Changelog maintenu automatiquement - Dernière mise à jour : 15 septembre 2025* \ No newline at end of file +_Changelog maintenu automatiquement - Dernière mise à jour : 15 septembre 2025_ diff --git a/backend/cniOcrEnhancer.js b/backend/cniOcrEnhancer.js index 632a9f8..42965ef 100644 --- a/backend/cniOcrEnhancer.js +++ b/backend/cniOcrEnhancer.js @@ -19,7 +19,9 @@ async function isCNIDocument(inputPath) { 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)}`) + 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) { @@ -45,7 +47,7 @@ async function extractMRZ(inputPath) { left: 0, top: mrzTop, width: metadata.width, - height: mrzHeight + height: mrzHeight, }) .grayscale() .normalize() @@ -73,29 +75,29 @@ async function segmentCNIZones(inputPath) { 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) + 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) + 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) + 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) - } + height: Math.floor(metadata.height * 0.1), + }, } console.log(`[CNI_SEGMENT] Segmentation en ${Object.keys(zones).length} zones`) @@ -143,21 +145,21 @@ async function enhanceCNIPreprocessing(inputPath) { width: 2000, height: Math.floor(2000 * (metadata.height / metadata.width)), fit: 'fill', - kernel: sharp.kernel.lanczos3 + kernel: sharp.kernel.lanczos3, }) .grayscale() .normalize() .modulate({ brightness: 1.3, contrast: 1.8, - saturation: 0 + saturation: 0, }) .sharpen({ sigma: 1.5, m1: 0.5, m2: 3, x1: 2, - y2: 20 + y2: 20, }) .median(3) .threshold(135) @@ -194,7 +196,7 @@ async function processCNIWithZones(inputPath) { const results = { isCNI: true, zones: {}, - mrz: null + mrz: null, } // Extraire chaque zone @@ -213,7 +215,6 @@ async function processCNIWithZones(inputPath) { 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 @@ -228,7 +229,7 @@ function decodeMRZ(mrzText) { } // Format MRZ de la CNI française (2 lignes de 36 caractères) - const lines = mrzText.split('\n').filter(line => line.trim().length > 0) + const lines = mrzText.split('\n').filter((line) => line.trim().length > 0) if (lines.length < 2) { return null } @@ -241,12 +242,11 @@ function decodeMRZ(mrzText) { country: line1.substring(2, 5), surname: line1.substring(5, 36).replace(/= 0.8" + "node >= 6.0" ], "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^2.2.2", + "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, @@ -179,12 +179,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -525,12 +519,6 @@ "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", "license": "MIT" }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -628,22 +616,21 @@ "license": "MIT" }, "node_modules/multer": { - "version": "1.4.5-lts.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", - "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", - "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" + "type-is": "^1.6.18", + "xtend": "^4.0.2" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 10.16.0" } }, "node_modules/negotiator": { @@ -732,12 +719,6 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -791,26 +772,19 @@ } }, "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -993,20 +967,14 @@ } }, "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, "node_modules/tesseract.js": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-5.1.1.tgz", diff --git a/backend/package.json b/backend/package.json index da5f7cd..2b09b0f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "4nk-ia-backend", - "version": "1.0.0", + "version": "1.0.1", "description": "Backend pour le traitement des documents avec OCR et NER", "main": "server.js", "scripts": { @@ -10,7 +10,7 @@ }, "dependencies": { "express": "^4.18.2", - "multer": "^1.4.5-lts.1", + "multer": "^2.0.0", "cors": "^2.8.5", "tesseract.js": "^5.1.0" }, diff --git a/backend/pdfConverter.js b/backend/pdfConverter.js index 4c14957..462ba66 100644 --- a/backend/pdfConverter.js +++ b/backend/pdfConverter.js @@ -26,8 +26,8 @@ async function convertPdfToImages(pdfPath, outputDir = null) { format: 'png', out_dir: outputDir, out_prefix: 'page', - page: null, // Toutes les pages - scale: 2000 // Résolution élevée + page: null, // Toutes les pages + scale: 2000, // Résolution élevée } console.log(`[PDF-CONVERTER] Configuration: Format=PNG, Scale=2000`) @@ -45,7 +45,6 @@ async function convertPdfToImages(pdfPath, outputDir = null) { }) return imagePaths - } catch (error) { console.error(`[PDF-CONVERTER] Erreur lors de la conversion:`, error.message) throw error @@ -74,8 +73,8 @@ async function convertPdfToSingleImage(pdfPath, outputPath = null) { format: 'png', out_dir: path.dirname(outputPath), out_prefix: path.basename(outputPath, '.png'), - page: 1, // Première page seulement - scale: 2000 + page: 1, // Première page seulement + scale: 2000, } // Conversion de la première page seulement @@ -84,7 +83,6 @@ async function convertPdfToSingleImage(pdfPath, outputPath = null) { console.log(`[PDF-CONVERTER] Image générée: ${outputPath}`) return outputPath - } catch (error) { console.error(`[PDF-CONVERTER] Erreur lors de la conversion:`, error.message) throw error @@ -113,5 +111,5 @@ async function cleanupTempFiles(filePaths) { module.exports = { convertPdfToImages, convertPdfToSingleImage, - cleanupTempFiles + cleanupTempFiles, } diff --git a/backend/server.js b/backend/server.js index 2a2869d..6b394e4 100644 --- a/backend/server.js +++ b/backend/server.js @@ -47,7 +47,8 @@ function getMimeType(ext) { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', - '.tiff': 'image/tiff' + '.tiff': 'image/tiff', + '.txt': 'text/plain', } return mimeTypes[ext.toLowerCase()] || 'application/octet-stream' } @@ -124,7 +125,7 @@ function createPendingFlag(folderHash, fileHash) { fileHash, folderHash, timestamp: new Date().toISOString(), - status: 'processing' + status: 'processing', } fs.writeFileSync(pendingFile, JSON.stringify(pendingData, null, 2)) console.log(`[CACHE] Flag pending créé pour ${fileHash} dans le dossier ${folderHash}`) @@ -215,13 +216,14 @@ function getMimeTypeFromExtension(extension) { '.gif': 'image/gif', '.bmp': 'image/bmp', '.tiff': 'image/tiff', - '.webp': 'image/webp' + '.webp': 'image/webp', + '.txt': 'text/plain', } return mimeTypes[extension.toLowerCase()] || 'application/octet-stream' } // Fonction pour lister tous les résultats d'un dossier -function listFolderResults(folderHash) { +async function listFolderResults(folderHash) { const cachePath = path.join('cache', folderHash) const uploadsPath = path.join('uploads', folderHash) @@ -240,7 +242,7 @@ function listFolderResults(folderHash) { if (result) { results.push({ fileHash, - ...result + ...result, }) } } else if (file.endsWith('.pending')) { @@ -259,44 +261,51 @@ function listFolderResults(folderHash) { // 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) + 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) + const isAlreadyPending = pending.some((p) => p.fileHash === fileHash) + + console.log(`[FOLDER] hasCacheResult: ${hasCacheResult}, isAlreadyPending: ${isAlreadyPending}`) if (!hasCacheResult && !isAlreadyPending) { - // Mettre le fichier en pending et le traiter automatiquement + // Ne pas bloquer la réponse: marquer en pending et lancer en arrière-plan const filePath = path.join(uploadsPath, file) - const stats = fs.statSync(filePath) + console.log( + `[FOLDER] Fichier non traité détecté, mise en traitement asynchrone: ${file}`, + ) - console.log(`[FOLDER] Fichier non traité détecté, mise en pending: ${file}`) - - // Créer le flag pending - const pendingData = { + // Créer un flag pending et enregistrer l'état + createPendingFlag(folderHash, fileHash) + pending.push({ fileHash, - fileName: file, folderHash, timestamp: new Date().toISOString(), - status: 'processing' - } - - // Sauvegarder le flag pending - createPendingFlag(folderHash, fileHash, pendingData) - - // Ajouter à la liste des pending - pending.push(pendingData) + status: 'processing', + }) hasPending = true - // Traiter le fichier en arrière-plan - processFileInBackground(filePath, fileHash, folderHash) + // 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 } @@ -314,19 +323,34 @@ async function processDocument(filePath, fileHash) { 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 + size: stats.size, } let ocrResult let result - // Si c'est un PDF, extraire le texte directement - if (mimeType === 'application/pdf') { + // 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) @@ -335,10 +359,20 @@ async function processDocument(filePath, fileHash) { console.error(`[PROCESS] Erreur lors de l'extraction PDF:`, error.message) throw new Error(`Erreur lors de l'extraction PDF: ${error.message}`) } - } else { + } else if (mimeType.startsWith('image/')) { // Pour les images, utiliser l'OCR amélioré avec détection CNI - const { extractTextFromImageEnhanced } = require('./enhancedOcr') - ocrResult = await extractTextFromImageEnhanced(filePath) + 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 @@ -347,12 +381,72 @@ async function processDocument(filePath, fileHash) { // Mesure du temps de traitement const processingTime = Date.now() - startTime - // Génération du format JSON standard - result = generateStandardJSON(file, ocrResult, entities, processingTime) + // 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 @@ -363,23 +457,34 @@ async function processDocument(filePath, fileHash) { 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) } @@ -456,23 +561,25 @@ function listCacheFiles() { 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 + 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 } - } catch (error) { - console.warn(`[CACHE] Erreur lors de la lecture de ${file}:`, error.message) - return null - } - }).filter(file => file !== null) + }) + .filter((file) => file !== null) } // Configuration multer pour l'upload de fichiers avec hash comme nom @@ -489,20 +596,26 @@ const storage = multer.diskStorage({ 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'] + 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 @@ -519,9 +632,8 @@ async function extractTextFromPdf(pdfPath) { return { text: data.text, confidence: 95, // PDF text extraction est très fiable - words: data.text.split(/\s+/).filter(word => word.length > 0) + words: data.text.split(/\s+/).filter((word) => word.length > 0), } - } catch (error) { console.error(`[PDF] Erreur lors de l'extraction:`, error.message) throw error @@ -543,7 +655,7 @@ async function extractTextFromImage(imagePath) { brightness: 1.1, grayscale: true, sharpen: true, - denoise: true + denoise: true, }) // Sauvegarde temporaire de l'image préprocessée @@ -560,34 +672,37 @@ async function extractTextFromImage(imagePath) { name: 'Mode Standard', params: { tessedit_pageseg_mode: '6', - tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,/-:() ', + tessedit_char_whitelist: + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,/-:() ', tessedit_ocr_engine_mode: '1', preserve_interword_spaces: '1', textord_min_linesize: '2.0', - textord_min_xheight: '6' - } + textord_min_xheight: '6', + }, }, { name: 'Mode Fine', params: { tessedit_pageseg_mode: '8', // Mot unique - tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,/-:() ', + tessedit_char_whitelist: + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,/-:() ', tessedit_ocr_engine_mode: '1', textord_min_linesize: '1.0', textord_min_xheight: '4', - textord_heavy_nr: '0' - } + textord_heavy_nr: '0', + }, }, { name: 'Mode Ligne', params: { tessedit_pageseg_mode: '13', // Ligne brute de texte - tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,/-:() ', + tessedit_char_whitelist: + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,/-:() ', tessedit_ocr_engine_mode: '1', textord_min_linesize: '1.5', - textord_min_xheight: '5' - } - } + textord_min_xheight: '5', + }, + }, ] let bestResult = { text: '', confidence: 0, words: [], strategy: 'none' } @@ -605,7 +720,7 @@ async function extractTextFromImage(imagePath) { text: data.text, confidence: data.confidence, words: data.words || [], - strategy: strategy.name + strategy: strategy.name, } } } catch (error) { @@ -613,13 +728,17 @@ async function extractTextFromImage(imagePath) { } } - 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)}...`) + 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 + words: bestResult.words, } } finally { await worker.terminate() @@ -641,7 +760,11 @@ 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' + 0: 'o', + 1: 'l', + 5: 's', + '@': 'a', + 3: 'e', } let correctedText = text @@ -658,7 +781,18 @@ function generateStandardJSON(documentInfo, ocrResult, entities, processingTime) const documentId = `doc-${Date.now()}` // Classification du document - const documentType = entities.documentType || '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 @@ -668,9 +802,16 @@ function generateStandardJSON(documentInfo, ocrResult, entities, processingTime) const references = extractReferences(ocrResult.text, documentType) // Calcul de la confiance globale - const globalConfidence = Math.min(95, Math.max(60, ocrResult.confidence * 0.8 + - (entities.identities.length > 0 ? 10 : 0) + - (entities.cniNumbers.length > 0 ? 15 : 0))) + 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: { @@ -678,25 +819,30 @@ function generateStandardJSON(documentInfo, ocrResult, entities, processingTime) fileName: documentInfo.originalname, fileSize: documentInfo.size, mimeType: documentInfo.mimetype, - uploadTimestamp: timestamp + uploadTimestamp: timestamp, }, classification: { documentType: documentType, confidence: globalConfidence / 100, subType: subType, language: 'fr', - pageCount: 1 + pageCount: 1, }, extraction: { text: { - raw: ocrResult.text, - processed: correctOCRText(ocrResult.text), - wordCount: ocrResult.words.length, - characterCount: ocrResult.text.length, - confidence: ocrResult.confidence / 100 + 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: entities.identities.map(identity => ({ + persons: safeEntities.identities.map((identity) => ({ id: identity.id, type: 'person', firstName: identity.firstName, @@ -705,9 +851,9 @@ function generateStandardJSON(documentInfo, ocrResult, entities, processingTime) email: identity.email || null, phone: identity.phone || null, confidence: identity.confidence, - source: identity.source + source: identity.source, })), - companies: entities.companies.map(company => ({ + companies: safeEntities.companies.map((company) => ({ id: company.id, name: company.name, legalForm: company.legalForm || null, @@ -717,9 +863,9 @@ function generateStandardJSON(documentInfo, ocrResult, entities, processingTime) capital: company.capital || null, role: company.role || null, confidence: company.confidence, - source: company.source + source: company.source, })), - addresses: entities.addresses.map(address => ({ + addresses: safeEntities.addresses.map((address) => ({ id: address.id, type: address.type || 'general', street: address.street, @@ -728,35 +874,35 @@ function generateStandardJSON(documentInfo, ocrResult, entities, processingTime) country: address.country, company: address.company || null, confidence: address.confidence, - source: address.source + source: address.source, })), financial: financial, - dates: entities.dates.map(date => ({ + 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 + source: date.source, })), contractual: { - clauses: entities.contractClauses.map(clause => ({ + clauses: safeEntities.contractClauses.map((clause) => ({ id: clause.id, type: clause.type, content: clause.text, - confidence: clause.confidence + confidence: clause.confidence, })), - signatures: entities.signatures.map(signature => ({ + 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 - })) + confidence: signature.confidence, + })), }, - references: references - } + references: references, + }, }, metadata: { processing: { @@ -767,22 +913,27 @@ function generateStandardJSON(documentInfo, ocrResult, entities, processingTime) nerEngine: 'rule-based', preprocessing: { applied: documentInfo.mimetype !== 'application/pdf', - reason: documentInfo.mimetype === 'application/pdf' ? 'PDF direct text extraction' : 'Image preprocessing applied' - } + reason: + documentInfo.mimetype === 'application/pdf' + ? 'PDF direct text extraction' + : 'Image preprocessing applied', + }, }, quality: { globalConfidence: globalConfidence / 100, - textExtractionConfidence: ocrResult.confidence / 100, - entityExtractionConfidence: 0.90, - classificationConfidence: globalConfidence / 100 - } + textExtractionConfidence: (typeof ocrResult.confidence === 'number' ? ocrResult.confidence : 0) / 100, + entityExtractionConfidence: 0.9, + classificationConfidence: globalConfidence / 100, + }, }, status: { success: true, errors: [], - warnings: entities.signatures.length === 0 ? ['Aucune signature détectée'] : [], - timestamp: timestamp - } + warnings: (Array.isArray(safeEntities.signatures) ? safeEntities.signatures.length : 0) === 0 + ? ['Aucune signature détectée'] + : [], + timestamp: timestamp, + }, } } @@ -793,7 +944,7 @@ function getDocumentSubType(documentType, text) { if (/vente|achat/i.test(text)) return 'Facture de vente' return 'Facture' } - if (documentType === 'CNI') return 'Carte Nationale d\'Identité' + 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' @@ -817,10 +968,10 @@ function extractFinancialInfo(text, documentType) { /(\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 + /T\.V\.A\.\s*[:\-]?\s*(\d+(?:[.,]\d{2})?)\s*€/gi, ] - amountPatterns.forEach(pattern => { + amountPatterns.forEach((pattern) => { for (const match of text.matchAll(pattern)) { const amount = parseFloat(match[1].replace(',', '.')) if (amount > 0) { @@ -829,7 +980,7 @@ function extractFinancialInfo(text, documentType) { type: 'montant', value: amount, currency: 'EUR', - confidence: 0.9 + confidence: 0.9, }) } } @@ -856,7 +1007,7 @@ function extractReferences(text, documentType) { id: `ref-${references.length}`, type: 'facture', number: match[1], - confidence: 0.95 + confidence: 0.95, }) } } @@ -872,9 +1023,18 @@ function formatDate(dateStr) { 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' + 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) { @@ -904,7 +1064,7 @@ function extractEntitiesFromText(text) { dates: [], contractClauses: [], signatures: [], - documentType: 'Document' + documentType: 'Document', } // Extraction des noms avec patterns généraux @@ -914,22 +1074,22 @@ function extractEntitiesFromText(text) { // 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 + /([A-Z][a-zà-öø-ÿ'\-]+\s+[A-Z][a-zà-öø-ÿ'\-]+)/g, ] - namePatterns.forEach(pattern => { + 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-${entities.identities.length}`, + 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' + source: 'rule-based', }) } } @@ -940,19 +1100,19 @@ function extractEntitiesFromText(text) { 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 + /(Entreprise|Société|Compagnie|Groupe|Corporation|Corp\.|Inc\.|Ltd\.|LLC)/gi, ] - companyPatterns.forEach(pattern => { + 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-${entities.companies.length}`, + id: `company-${(Array.isArray(entities.companies)?entities.companies:[]).length}`, name: companyName.trim(), type: 'company', confidence: 0.8, - source: 'rule-based' + source: 'rule-based', }) } } @@ -962,23 +1122,23 @@ function extractEntitiesFromText(text) { 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 + /(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 => { + 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-${entities.addresses.length}`, + 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' + source: 'rule-based', }) } }) @@ -987,28 +1147,25 @@ function extractEntitiesFromText(text) { const cniPattern = /([A-Z]{2}\d{6})/g for (const match of text.matchAll(cniPattern)) { entities.cniNumbers.push({ - id: `cni-${entities.cniNumbers.length}`, + id: `cni-${(Array.isArray(entities.cniNumbers)?entities.cniNumbers:[]).length}`, number: match[1], confidence: 0.95, - source: 'rule-based' + 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 - ] + 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 => { + datePatterns.forEach((pattern) => { for (const match of text.matchAll(pattern)) { const date = match[2] || match[1] entities.dates.push({ - id: `date-${entities.dates.length}`, + 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' + source: 'rule-based', }) } }) @@ -1020,19 +1177,19 @@ function extractEntitiesFromText(text) { /(Conditions\s+générales[^\.]+\.)/gi, /(Modalités\s+de\s+[^\.]+\.)/gi, /(Obligations\s+du\s+[^\.]+\.)/gi, - /(Responsabilités[^\.]+\.)/gi + /(Responsabilités[^\.]+\.)/gi, ] - clausePatterns.forEach(pattern => { + 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-${entities.contractClauses.length}`, + id: `clause-${(Array.isArray(entities.contractClauses)?entities.contractClauses:[]).length}`, text: clause.trim(), type: 'contractual', confidence: 0.8, - source: 'rule-based' + source: 'rule-based', }) } } @@ -1043,19 +1200,19 @@ function extractEntitiesFromText(text) { /(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 + /(Fait\s+et\s+signé\s+[^\.]+\.)/gi, ] - signaturePatterns.forEach(pattern => { + 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-${entities.signatures.length}`, + id: `signature-${(Array.isArray(entities.signatures)?entities.signatures:[]).length}`, text: signature.trim(), type: 'signature', confidence: 0.8, - source: 'rule-based' + source: 'rule-based', }) } } @@ -1073,13 +1230,13 @@ function extractEntitiesFromText(text) { } console.log(`[NER] Extraction terminée:`) - console.log(` - Identités: ${entities.identities.length}`) - console.log(` - Sociétés: ${entities.companies.length}`) - console.log(` - Adresses: ${entities.addresses.length}`) - console.log(` - Numéros CNI: ${entities.cniNumbers.length}`) - console.log(` - Dates: ${entities.dates.length}`) - console.log(` - Clauses contractuelles: ${entities.contractClauses.length}`) - console.log(` - Signatures: ${entities.signatures.length}`) + 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 @@ -1100,7 +1257,9 @@ app.post('/api/extract', upload.single('document'), async (req, res) => { return res.status(400).json({ error: 'Hash du dossier requis' }) } - console.log(`[API] Traitement du fichier: ${req.file.originalname} dans le dossier: ${folderHash}`) + 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) @@ -1127,7 +1286,7 @@ app.post('/api/extract', upload.single('document'), async (req, res) => { success: false, status: 'pending', message: 'Fichier en cours de traitement', - fileHash + fileHash, }) } @@ -1140,7 +1299,9 @@ app.post('/api/extract', upload.single('document'), async (req, res) => { let duplicatePath = null if (existingFile) { - console.log(`[HASH] Fichier déjà existant trouvé dans le dossier ${folderHash}: ${existingFile.name}`) + 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 @@ -1184,26 +1345,101 @@ app.post('/api/extract', upload.single('document'), async (req, res) => { 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) + // Extraction NER + const entities = extractEntitiesFromText(ocrResult.text) // Mesure du temps de traitement const processingTime = Date.now() - startTime - // Génération du format JSON standard - result = generateStandardJSON(req.file, ocrResult, entities, processingTime) + // 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) @@ -1218,10 +1454,10 @@ app.post('/api/extract', upload.single('document'), async (req, res) => { return res.status(500).json({ success: false, error: 'Erreur lors du traitement du document', - details: error.message + details: error.message, }) } finally { - // Nettoyage du fichier temporaire + // Nettoyage du fichier temporaire if (isDuplicate) { // Supprimer le doublon uploadé fs.unlinkSync(duplicatePath) @@ -1229,10 +1465,11 @@ app.post('/api/extract', upload.single('document'), async (req, res) => { // 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)}%`) + 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) @@ -1244,7 +1481,7 @@ app.post('/api/extract', upload.single('document'), async (req, res) => { res.status(500).json({ success: false, error: 'Erreur lors du traitement du document', - details: error.message + details: error.message, }) } }) @@ -1253,19 +1490,20 @@ app.post('/api/extract', upload.single('document'), async (req, res) => { app.get('/api/test-files', (req, res) => { try { const testFilesDir = path.join(__dirname, '..', 'test-files') - const files = fs.readdirSync(testFilesDir) - .filter(file => { + const files = fs + .readdirSync(testFilesDir) + .filter((file) => { const ext = path.extname(file).toLowerCase() return ['.jpg', '.jpeg', '.png', '.pdf', '.tiff'].includes(ext) }) - .map(file => { + .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 + lastModified: stats.mtime, } }) @@ -1311,32 +1549,34 @@ app.get('/api/uploads', (req, res) => { } const files = fs.readdirSync(uploadDir) - const fileList = files.map(file => { - const filePath = path.join(uploadDir, file) - try { - const stats = fs.statSync(filePath) + 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) + // 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 + 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 } - } catch (error) { - console.warn(`[API] Erreur lors de la lecture de ${file}:`, error.message) - return null - } - }).filter(file => file !== null) + }) + .filter((file) => file !== null) res.json({ files: fileList, count: fileList.length, - totalSize: fileList.reduce((sum, file) => sum + file.size, 0) + totalSize: fileList.reduce((sum, file) => sum + file.size, 0), }) } catch (error) { console.error('[API] Erreur lors de la liste des fichiers:', error) @@ -1352,7 +1592,7 @@ app.get('/api/cache', (req, res) => { res.json({ files: cacheFiles, count: cacheFiles.length, - totalSize: cacheFiles.reduce((sum, file) => sum + file.size, 0) + totalSize: cacheFiles.reduce((sum, file) => sum + file.size, 0), }) } catch (error) { console.error('[API] Erreur lors de la liste du cache:', error) @@ -1399,7 +1639,7 @@ app.delete('/api/cache/:hash', (req, res) => { // 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') + console.log("[FOLDER] Début de la création d'un nouveau dossier") const folderHash = generateFolderHash() console.log(`[FOLDER] Hash généré: ${folderHash}`) @@ -1411,24 +1651,26 @@ app.post('/api/folders', (req, res) => { res.json({ success: true, folderHash, - message: 'Dossier créé avec succès' + 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 + error: error.message, }) } }) // Route pour récupérer les résultats d'un dossier -app.get('/api/folders/:folderHash/results', (req, res) => { +app.get('/api/folders/:folderHash/results', async (req, res) => { try { const { folderHash } = req.params - const folderData = listFolderResults(folderHash) + 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`) + 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, @@ -1436,13 +1678,13 @@ app.get('/api/folders/:folderHash/results', (req, res) => { results: folderData.results, pending: folderData.pending, hasPending: folderData.hasPending, - count: folderData.results.length + 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 + error: error.message, }) } }) @@ -1458,7 +1700,7 @@ app.get('/api/folders/:folderHash/files/:fileHash', (req, res) => { } const files = fs.readdirSync(folderPath) - const targetFile = files.find(file => file.startsWith(fileHash)) + const targetFile = files.find((file) => file.startsWith(fileHash)) if (!targetFile) { return res.status(404).json({ success: false, error: 'Fichier non trouvé' }) @@ -1470,7 +1712,7 @@ app.get('/api/folders/:folderHash/files/:fileHash', (req, res) => { console.error('[FOLDER] Erreur lors de la récupération du fichier:', error) res.status(500).json({ success: false, - error: error.message + error: error.message, }) } }) @@ -1487,8 +1729,8 @@ app.post('/api/folders/default', async (req, res) => { 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()) + const supportedFiles = testFiles.filter((file) => + ['.pdf', '.jpg', '.jpeg', '.png', '.tiff'].includes(path.extname(file).toLowerCase()), ) for (const testFile of supportedFiles) { @@ -1510,7 +1752,7 @@ app.post('/api/folders/default', async (req, res) => { const mockFile = { path: destPath, originalname: testFile, - mimetype: getMimeType(ext) + mimetype: getMimeType(ext), } // Extraction de texte selon le type de fichier @@ -1543,13 +1785,13 @@ app.post('/api/folders/default', async (req, res) => { res.json({ success: true, folderHash, - message: 'Dossier par défaut créé avec succès' + 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 + error: error.message, }) } }) @@ -1558,7 +1800,7 @@ app.get('/api/health', (req, res) => { res.json({ status: 'OK', timestamp: new Date().toISOString(), - version: '1.0.0' + version: '1.0.0', }) }) diff --git a/backend_output.json b/backend_output.json index 6f80bd0..d65b383 100644 --- a/backend_output.json +++ b/backend_output.json @@ -72,9 +72,7 @@ "status": { "success": true, "errors": [], - "warnings": [ - "Aucune signature détectée" - ], + "warnings": ["Aucune signature détectée"], "timestamp": "2025-09-15T23:26:56.308Z" } } diff --git a/current_backend_output.json b/current_backend_output.json index 7071a0f..cf76c49 100644 --- a/current_backend_output.json +++ b/current_backend_output.json @@ -72,9 +72,7 @@ "status": { "success": true, "errors": [], - "warnings": [ - "Aucune signature détectée" - ], + "warnings": ["Aucune signature détectée"], "timestamp": "2025-09-15T23:26:19.922Z" } } diff --git a/docker-compose.registry.yml b/docker-compose.registry.yml index 0170d6a..2651f0a 100644 --- a/docker-compose.registry.yml +++ b/docker-compose.registry.yml @@ -1,16 +1,16 @@ -version: "3.9" +version: '3.9' services: frontend: - image: "git.4nkweb.com/4nk/4nk-ia-front:${TAG:-dev}" + image: 'git.4nkweb.com/4nk/4nk-ia-front:${TAG:-dev}' container_name: 4nk-ia-front restart: unless-stopped ports: - - "8080:80" + - '8080:80' environment: - VITE_API_URL=${VITE_API_URL:-http://172.23.0.10:8000} healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost/"] + test: ['CMD', 'wget', '-qO-', 'http://localhost/'] interval: 30s timeout: 5s retries: 3 diff --git a/docs/ANALYSE_REPO.md b/docs/ANALYSE_REPO.md new file mode 100644 index 0000000..f8f7bb8 --- /dev/null +++ b/docs/ANALYSE_REPO.md @@ -0,0 +1,139 @@ +# Journal d'incident - 2025-09-16 + +## Résumé + +Le frontend affichait des erreurs 502 Bad Gateway via Nginx pour les endpoints `/api/health` et `/api/folders/{hash}/results`. + +## Constat + +- `curl https://ia.4nkweb.com/api/health` retournait 502. +- Aucun service n'écoutait en local sur le port 3001. +- `backend.log` montrait des tentatives de traitement sur un fichier `.txt` avec erreurs Sharp (format non supporté), indiquant un arrêt préalable du service. + +## Actions de rétablissement + +1. Installation de Node via nvm (Node 22.x) pour satisfaire `engines`. +2. Installation des dépendances (`npm ci`) côté backend et racine. +3. Démarrage du backend Express sur 3001 en arrière‑plan avec logs et PID. +4. Vérifications: + - `curl http://127.0.0.1:3001/api/health` → 200 OK + - `curl https://ia.4nkweb.com/api/health` → 200 OK + - `curl https://ia.4nkweb.com/api/folders/7d99a85daf66a0081a0e881630e6b39b/results` → 200 OK + +## Causes probables + +- Processus backend arrêté (pas de listener sur 3001). +- Gestion incomplète des fichiers `.txt` côté backend (détection MIME), pouvant entraîner des erreurs avec Sharp. + +## Recommandations de suivi + +- Ajouter le mapping `.txt` → `text/plain` dans la détection MIME backend. +- Dans `/api/extract`, gérer explicitement les `.txt` (lecture directe) comme dans `processDocument`. +- Mettre à jour Multer vers 2.x (1.x est vulnérable et déprécié). +- Surveiller `backend.log` et ajouter une supervision (systemd/service manager). + +--- + +# Audit détaillé du dépôt 4NK_IA_front + +#### Portée + +- **Code analysé**: frontend React/TypeScript sous Vite, répertoire `/home/debian/4NK_IA_front`. +- **Commandes exécutées**: `npm ci`, `npm run lint`, `npm run mdlint`, `npm run test`, `npm run build`. + +### Architecture et pile technique + +- **Framework**: React 19 + TypeScript, Vite 7 (`vite.config.ts`). +- **UI**: MUI v7. +- **État**: Redux Toolkit + `react-redux` (`src/store/index.ts`, `src/store/documentSlice.ts`). +- **Routing**: React Router v7 avec code-splitting via `React.lazy`/`Suspense` (`src/router/index.tsx`). +- **Services**: couche d’abstraction HTTP via Axios (`src/services/api.ts`), backend direct (`src/services/backendApi.ts`), OpenAI (`src/services/openai.ts`), fichiers/ dossiers (`src/services/folderApi.ts`). +- **Build/Runtime**: Docker multi‑stage Node→Nginx, config SPA (`Dockerfile`, `nginx.conf`). +- **Qualité**: ESLint + Prettier + markdownlint, Vitest + Testing Library. + +### Points forts + +- **Code splitting** déjà en place au niveau du routeur. +- **Centralisation d’état** propre (slices, middlewares, persistance localStorage). +- **Abstraction des services** HTTP claire (Axios + backends alternatifs). +- **Docker** production prêt (Nginx + healthcheck) et CI potentielle via `docker-compose.registry.yml`. + +### Problèmes détectés + +#### Lint TypeScript/JS + +- Commande: `npm run lint`. +- Résultat: 57 problèmes trouvés (49 erreurs, 8 avertissements). +- Catégories principales: + - Types interdits `any` dans plusieurs services, ex. `src/services/backendApi.ts` (lignes 23–36, 90, 97, 107) et `src/services/fileExtract.ts` (lignes 2, 5, 55, 63, 110). + - Variables non utilisées, ex. `src/App.tsx` ligne 8 (`setCurrentFolderHash`), `src/store/documentSlice.ts` lignes 7, 56, 420, 428, `src/services/openai.ts` variables `_file`, `_address`, etc. + - Règles `react-hooks/exhaustive-deps` dans `src/App.tsx` (ligne 65) et `src/components/Layout.tsx` (ligne 84). + - Regex avec échappements inutiles dans `src/services/ruleNer.ts` (lignes 33–34, 84, 119). + - Typage middleware Redux avec `any` dans `src/store/index.ts` ligne 8. + +#### Lint Markdown + +- Commande: `npm run mdlint`. +- Problèmes récurrents: + - Manque de lignes vides autour des titres et listes: `CHANGELOG.md`, `docs/API_BACKEND.md`, `docs/architecture-backend.md`, `docs/changelog-pending.md`, `docs/HASH_SYSTEM.md`. + - Longueur de ligne > 120 caractères: `docs/API_BACKEND.md`, `docs/architecture-backend.md`, `docs/HASH_SYSTEM.md`, `docs/systeme-pending.md`. + - Blocs de code sans langage ou sans lignes vides autour. + - Titres en emphase non conformes (MD036) dans certains documents. + +#### Tests + +- Commande: `npm run test`. +- Résultat: 1 suite OK, 1 suite en échec. + - Échec: `tests/testFilesApi.test.ts` — import introuvable `../src/services/testFilesApi` (le fichier n’existe pas). Il faut soit créer `src/services/testFilesApi.ts`, soit adapter le test pour la source disponible. + +#### Build de production + +- Commande: `npm run build`. +- Résultat: erreurs TypeScript empêchant la compilation. + - Incohérences de types côté `ExtractionResult` consommé vs structures produites dans `src/services/api.ts` (propriété `timestamp` non prévue dans `ExtractionResult`). + - Types d’entités utilisés en `views/ExtractionView.tsx` traités comme `string` au lieu d’objets typés (`Identity`, `Address`). Ex: accès à `firstName`, `lastName`, `street`, `city`, `postalCode`, `confidence` sur des `string`. + - Variables non utilisées dans plusieurs fichiers (cf. lint ci‑dessus). + +### Causes probables et pistes de résolution + +- **Modèle de données**: divergence entre la structure standardisée `ExtractionResult` (`src/types/index.ts`) et le mapping réalisé dans `src/services/api.ts`/`backendApi.ts`. Il faut: + - Aligner les champs (ex. déplacer `timestamp` vers `status.timestamp` ou vers des métadonnées conformes). + - Harmoniser la forme des entités retournées (personnes/adresses) pour correspondre strictement à `Identity[]` et `Address[]`. +- **Composants de vues**: `ExtractionView.tsx` suppose des entités objets alors que les données mappées peuvent contenir des chaînes. Il faut normaliser en amont (mapping service) et/ou renforcer les garde‑fous de rendu. +- **Tests**: ajouter `src/services/testFilesApi.ts` (ou ajuster l’import) pour couvrir l’API de fichiers de test référencée par `tests/testFilesApi.test.ts`. +- **Qualité**: + - Remplacer les `any` par des types précis (ou `unknown` + raffinements), surtout dans `backendApi.ts`, `fileExtract.ts`, `openai.ts`. + - Corriger les dépendances de hooks React. + - Nettoyer les variables non utilisées et directives `eslint-disable` superflues. + - Corriger les regex avec échappements inutiles dans `ruleNer.ts`. + - Corriger les erreurs markdown (MD013, MD022, MD031, MD032, MD036, MD040, MD047) en ajoutant lignes vides et langages de blocs. + +### Sécurité et configuration + +- **Variables d’environnement**: `VITE_API_URL`, `VITE_USE_OPENAI`, clés OpenAI masquées en dev (`src/services/api.ts`). OK. +- **Nginx**: SPA et healthcheck définis (`nginx.conf`). OK pour production statique. +- **Docker**: image multi‑stage; healthcheck HTTP. Tagging via scripts et `docker-compose.registry.yml` (variable `TAG`). À aligner avec conventions internes du registre. + +### Recommandations prioritaires (ordre d’exécution) + +1. Corriger les erreurs TypeScript bloquantes du build: + - Aligner `ExtractionResult` consommé/produit (services + vues). Supprimer/relocaliser `timestamp` au bon endroit. + - Normaliser `identities`/`addresses` en objets typés dans le mapping service. + - Corriger `ExtractionView.tsx` pour refléter les types réels. +2. Supprimer variables non utilisées et corriger `any` majeurs dans services critiques (`backendApi.ts`, `fileExtract.ts`). +3. Ajouter/implémenter `src/services/testFilesApi.ts` ou corriger l’import de test. +4. Corriger les règles `react-hooks/exhaustive-deps`. +5. Corriger markdownlint dans `CHANGELOG.md` et `docs/*.md` (lignes vides, langages de blocs, longueurs de ligne raisonnables). +6. Relancer lint, tests et build pour valider. + +### Notes de compatibilité + +- **Node**: engines `>=20.19 <23`. Testé avec Node 22.12.0 (OK) conformément au README. +- **ESLint**: config moderne (eslint@9, typescript-eslint@8) — stricte sur `any` et hooks React. + +### Annexes (références de fichiers) + +- `src/types/index.ts` — définitions `ExtractionResult`, `Identity`, `Address`. +- `src/services/api.ts` — mapping de la réponse backend; contient la propriété non typée `timestamp` sur `ExtractionResult` (à déplacer). +- `src/views/ExtractionView.tsx` — accès de propriétés d’objets sur des `string` (à corriger après normalisation du mapping). +- `tests/testFilesApi.test.ts` — dépend de `src/services/testFilesApi.ts` non présent. diff --git a/docs/API.md b/docs/API.md index 1a01a6d..0effa2e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -121,7 +121,7 @@ Ce mode est utile pour démo/diagnostic quand le backend n’est pas disponible. ```typescript const apiClient = axios.create({ baseURL: BASE_URL, - timeout: 60000 + timeout: 60000, }) ``` diff --git a/docs/API_BACKEND.md b/docs/API_BACKEND.md index 49c377e..f154bcb 100644 --- a/docs/API_BACKEND.md +++ b/docs/API_BACKEND.md @@ -5,7 +5,8 @@ L'API Backend 4NK_IA est un service d'extraction et d'analyse de documents utilisant l'OCR (Reconnaissance Optique de Caractères) et le NER (Reconnaissance d'Entités Nommées) pour traiter automatiquement les documents PDF et images. ### **Caractéristiques principales :** -- ✅ **Support multi-format** : PDF, JPEG, PNG, TIFF + +- ✅ **Support multi-format** : PDF, JPEG, PNG, TIFF, TXT - ✅ **OCR avancé** : Tesseract.js avec préprocessing d'images - ✅ **Extraction PDF directe** : pdf-parse pour une précision maximale - ✅ **NER intelligent** : Reconnaissance d'entités par règles @@ -28,6 +29,7 @@ L'API Backend 4NK_IA est un service d'extraction et d'analyse de documents utili Vérifie l'état du serveur backend. **Réponse :** + ```json { "status": "OK", @@ -37,6 +39,7 @@ Vérifie l'état du serveur backend. ``` **Exemple d'utilisation :** + ```bash curl http://localhost:3001/api/health ``` @@ -50,6 +53,7 @@ curl http://localhost:3001/api/health Retourne la liste des fichiers de test disponibles. **Réponse :** + ```json { "files": [ @@ -71,6 +75,7 @@ Retourne la liste des fichiers de test disponibles. Retourne la liste des fichiers uploadés avec leurs métadonnées et hash SHA-256. **Réponse :** + ```json { "files": [ @@ -88,11 +93,13 @@ Retourne la liste des fichiers uploadés avec leurs métadonnées et hash SHA-25 ``` **Exemple d'utilisation :** + ```bash curl http://localhost:3001/api/uploads ``` **Notes :** + - Le hash SHA-256 permet d'identifier les fichiers identiques - Les fichiers dupliqués sont automatiquement détectés lors de l'upload - Seuls les fichiers uniques sont conservés dans le système @@ -103,13 +110,15 @@ curl http://localhost:3001/api/uploads ### **POST** `/api/extract` -Extrait et analyse un document (PDF ou image) pour identifier les entités et informations structurées. +Extrait et analyse un document (PDF, image ou texte) pour identifier les entités et informations structurées. #### **Paramètres :** -- **`document`** (file, required) : Fichier à analyser (PDF, JPEG, PNG, TIFF) + +- **`document`** (file, required) : Fichier à analyser (PDF, JPEG, PNG, TIFF, TXT) - **Taille maximale :** 10MB #### **Gestion des doublons :** + - Le système calcule automatiquement un hash SHA-256 de chaque fichier uploadé - Si un fichier avec le même hash existe déjà, le doublon est supprimé - Le traitement utilise le fichier existant, évitant ainsi les calculs redondants @@ -189,17 +198,17 @@ Extrait et analyse un document (PDF ou image) pour identifier les entités et in "type": "prestation", "description": "Prestation du mois d'Août 2025", "quantity": 10, - "unitPrice": 550.00, - "totalHT": 5500.00, + "unitPrice": 550.0, + "totalHT": 5500.0, "currency": "EUR", "confidence": 0.95 } ], "totals": { - "totalHT": 5500.00, - "totalTVA": 1100.00, - "totalTTC": 6600.00, - "tvaRate": 0.20, + "totalHT": 5500.0, + "totalTVA": 1100.0, + "totalTTC": 6600.0, + "tvaRate": 0.2, "currency": "EUR" }, "payment": { @@ -268,7 +277,7 @@ Extrait et analyse un document (PDF ou image) pour identifier les entités et in "quality": { "globalConfidence": 0.95, "textExtractionConfidence": 0.95, - "entityExtractionConfidence": 0.90, + "entityExtractionConfidence": 0.9, "classificationConfidence": 0.95 } }, @@ -283,21 +292,31 @@ Extrait et analyse un document (PDF ou image) pour identifier les entités et in #### **Exemples d'utilisation :** -**Avec curl :** +**Avec curl (PDF) :** + ```bash curl -X POST \ -F "document=@/path/to/document.pdf" \ http://localhost:3001/api/extract ``` +**Avec curl (TXT) :** + +```bash +curl -X POST \ + -F "document=@/path/to/file.txt" \ + http://localhost:3001/api/extract +``` + **Avec JavaScript (fetch) :** + ```javascript const formData = new FormData() formData.append('document', fileInput.files[0]) const response = await fetch('http://localhost:3001/api/extract', { method: 'POST', - body: formData + body: formData, }) const result = await response.json() @@ -309,6 +328,7 @@ console.log(result) ## 📊 **Types de documents supportés** ### **1. Factures** + - **Détection automatique** : Mots-clés "facture", "tva", "siren", "montant" - **Entités extraites** : - Sociétés (fournisseur/client) @@ -319,6 +339,7 @@ console.log(result) - Dates ### **2. Cartes Nationales d'Identité (CNI)** + - **Détection automatique** : Mots-clés "carte nationale d'identité", "cni", "mrz" - **Entités extraites** : - Identités (nom, prénom) @@ -327,6 +348,7 @@ console.log(result) - Adresses ### **3. Contrats** + - **Détection automatique** : Mots-clés "contrat", "vente", "achat", "acte" - **Entités extraites** : - Parties contractantes @@ -335,6 +357,7 @@ console.log(result) - Dates importantes ### **4. Attestations** + - **Détection automatique** : Mots-clés "attestation", "certificat" - **Entités extraites** : - Identités @@ -346,6 +369,7 @@ console.log(result) ## 🔧 **Configuration et préprocessing** ### **Préprocessing d'images (pour JPEG, PNG, TIFF) :** + - **Redimensionnement** : Largeur cible 2000px - **Amélioration du contraste** : Facteur 1.5 - **Luminosité** : Facteur 1.1 @@ -354,6 +378,7 @@ console.log(result) - **Réduction du bruit** ### **Extraction PDF directe :** + - **Moteur** : pdf-parse - **Avantage** : Pas de conversion image, précision maximale - **Confiance** : 95% par défaut @@ -363,11 +388,13 @@ console.log(result) ## ⚡ **Performances** ### **Temps de traitement typiques :** + - **PDF** : 200-500ms - **Images** : 1-3 secondes (avec préprocessing) - **Taille maximale** : 10MB ### **Confiance d'extraction :** + - **PDF** : 90-95% - **Images haute qualité** : 80-90% - **Images de qualité moyenne** : 60-80% @@ -377,12 +404,14 @@ console.log(result) ## 🚨 **Gestion d'erreurs** ### **Codes d'erreur HTTP :** + - **400** : Aucun fichier fourni - **413** : Fichier trop volumineux (>10MB) - **415** : Type de fichier non supporté - **500** : Erreur de traitement interne ### **Exemple de réponse d'erreur :** + ```json { "success": false, @@ -396,13 +425,16 @@ console.log(result) ## 🛠️ **Dépendances techniques** ### **Moteurs OCR :** + - **Tesseract.js** : Pour les images - **pdf-parse** : Pour les PDF ### **Préprocessing :** + - **Sharp.js** : Traitement d'images ### **NER :** + - **Règles personnalisées** : Patterns regex pour l'extraction d'entités --- @@ -428,4 +460,4 @@ console.log(result) --- -*Documentation générée le 15/09/2025 - Version 1.0.0* +_Documentation générée le 15/09/2025 - Version 1.0.0_ diff --git a/docs/CACHE_ET_TRAITEMENT_ASYNC.md b/docs/CACHE_ET_TRAITEMENT_ASYNC.md new file mode 100644 index 0000000..7afaf07 --- /dev/null +++ b/docs/CACHE_ET_TRAITEMENT_ASYNC.md @@ -0,0 +1,30 @@ +# Cache des résultats et traitement asynchrone + +## Dossiers utilisés + +- `uploads/`: fichiers déposés (source de vérité) +- `cache/`: résultats JSON et flags `.pending` (source pour l’API) +- `backend/cache/*`: (désormais vide) ancien emplacement – ne plus utiliser + +## Flux de traitement + +1. Dépôt d’un fichier (`/api/extract`): + - Calcule `fileHash` (SHA‑256 du contenu) + - Si `cache//.json` existe: renvoie immédiatement le JSON + - Sinon: crée `cache//.pending`, lance l’OCR/NER, puis écrit le JSON et supprime `.pending` + +2. Listing (`/api/folders/:folderHash/results`): + - Agrège tous les JSON présents dans `cache//` + - Pour chaque fichier présent dans `uploads/` sans JSON, crée un flag `.pending` et lance le traitement en arrière‑plan, sans bloquer la réponse + +## Points importants + +- Le traitement images/PDF peut être long; le listing n’attend pas la fin +- Le frontal réalise un polling périodique si `hasPending=true` +- Les erreurs de traitement suppriment le flag `.pending` et la requête renvoie 500 (extraction) ou 200 avec moins de résultats (listing) + +## Bonnes pratiques + +- N’écrire les résultats que dans `cache/` à la racine +- Toujours indexer les résultats par `fileHash.json` +- Protéger les accès à `.length` et valeurs potentiellement `undefined` dans le backend diff --git a/docs/HASH_SYSTEM.md b/docs/HASH_SYSTEM.md index d48e8ed..5959bc0 100644 --- a/docs/HASH_SYSTEM.md +++ b/docs/HASH_SYSTEM.md @@ -9,16 +9,19 @@ Le système de hash SHA-256 a été implémenté dans le backend 4NK_IA pour év ## 🔧 **Fonctionnement** ### **1. Calcul du Hash** + - Chaque fichier uploadé est analysé pour calculer son hash SHA-256 - Le hash est calculé sur le contenu binaire complet du fichier - Utilisation de la fonction `crypto.createHash('sha256')` de Node.js ### **2. Détection des Doublons** + - Avant traitement, le système vérifie si un fichier avec le même hash existe déjà - La fonction `findExistingFileByHash()` parcourt le dossier `uploads/` - Si un doublon est trouvé, le fichier uploadé est supprimé ### **3. Traitement Optimisé** + - Le traitement utilise le fichier existant (pas le doublon) - Les résultats d'extraction sont identiques pour les fichiers identiques - Économie de ressources CPU et de stockage @@ -39,6 +42,7 @@ uploads/ ## 🔍 **API Endpoints** ### **GET** `/api/uploads` + Liste tous les fichiers uploadés avec leurs métadonnées : ```json @@ -62,6 +66,7 @@ Liste tous les fichiers uploadés avec leurs métadonnées : ## 📊 **Logs et Monitoring** ### **Logs de Hash** + ``` [HASH] Hash du fichier: a1b2c3d4e5f6789... [HASH] Fichier déjà existant trouvé: document-1757980637671.pdf @@ -69,6 +74,7 @@ Liste tous les fichiers uploadés avec leurs métadonnées : ``` ### **Indicateurs de Performance** + - **Temps de traitement réduit** pour les doublons - **Stockage optimisé** (pas de fichiers redondants) - **Logs clairs** pour le debugging @@ -78,6 +84,7 @@ Liste tous les fichiers uploadés avec leurs métadonnées : ## 🛠️ **Fonctions Techniques** ### **`calculateFileHash(buffer)`** + ```javascript function calculateFileHash(buffer) { return crypto.createHash('sha256').update(buffer).digest('hex') @@ -85,6 +92,7 @@ function calculateFileHash(buffer) { ``` ### **`findExistingFileByHash(hash)`** + ```javascript function findExistingFileByHash(hash) { const uploadDir = 'uploads/' @@ -139,6 +147,7 @@ graph TD ## 🧪 **Tests** ### **Test de Doublon** + ```bash # Premier upload curl -X POST -F "document=@test.pdf" http://localhost:3001/api/extract @@ -148,6 +157,7 @@ curl -X POST -F "document=@test.pdf" http://localhost:3001/api/extract ``` ### **Vérification** + ```bash # Lister les fichiers uploadés curl http://localhost:3001/api/uploads @@ -165,4 +175,4 @@ curl http://localhost:3001/api/uploads --- -*Documentation mise à jour le 15 septembre 2025* +_Documentation mise à jour le 15 septembre 2025_ diff --git a/docs/SYSTEME_FONCTIONNEL.md b/docs/SYSTEME_FONCTIONNEL.md new file mode 100644 index 0000000..15633a5 --- /dev/null +++ b/docs/SYSTEME_FONCTIONNEL.md @@ -0,0 +1,239 @@ +# 🎉 Système 4NK IA - Fonctionnel et Opérationnel + +## ✅ **Statut : SYSTÈME FONCTIONNEL** + +Le système 4NK IA est maintenant **entièrement fonctionnel** et accessible via HTTPS sur le domaine `ia.4nkweb.com`. + +--- + +## 🚀 **Accès au Système** + +### **URL de Production** +- **Frontend** : https://ia.4nkweb.com +- **API Backend** : https://ia.4nkweb.com/api/ +- **Health Check** : https://ia.4nkweb.com/api/health + +### **Certificat SSL** +- ✅ **HTTPS activé** avec Let's Encrypt +- ✅ **Renouvellement automatique** configuré +- ✅ **Redirection HTTP → HTTPS** active + +--- + +## 📊 **Données de Test Disponibles** + +Le système contient actuellement **3 documents de test** : + +### **1. Contrat de Vente (PDF)** +- **Fichier** : `contrat_vente.pdf` +- **Entités extraites** : + - **Personnes** : Jean Dupont, Marie Martin + - **Adresses** : 123 rue de la Paix (75001), 456 avenue des Champs (75008), 789 boulevard Saint-Germain (75006) + - **Propriétés** : 789 boulevard Saint-Germain, 75006 Paris + - **Contrats** : Contrat de vente + +### **2. Carte d'Identité (Image)** +- **Fichier** : `cni_jean_dupont.jpg` +- **Entités extraites** : + - **Personnes** : Jean Dupont + - **Adresses** : 123 rue de la Paix, 75001 Paris + +### **3. Document de Test (Texte)** +- **Fichier** : `test_sync.txt` +- **Entités extraites** : + - **Personnes** : Test User + - **Adresses** : 456 Test Avenue + - **Entreprises** : Test Corp + +--- + +## 🏗️ **Architecture Technique** + +### **Frontend (React + TypeScript)** +- **Framework** : React 19 + TypeScript +- **Build** : Vite 7 +- **UI** : Material-UI (MUI) v7 +- **État** : Redux Toolkit +- **Routing** : React Router v7 +- **Port** : 5174 (dev) / Nginx (prod) + +### **Backend (Node.js + Express)** +- **Framework** : Express.js +- **OCR** : Tesseract.js +- **NER** : Règles personnalisées +- **Port** : 3001 +- **Dossiers** : `uploads/` et `cache/` + +### **Proxy (Nginx)** +- **Configuration** : `/etc/nginx/conf.d/ia.4nkweb.com.conf` +- **SSL** : Let's Encrypt +- **Proxy** : `/api/` → Backend (127.0.0.1:3001) +- **Static** : `/` → Frontend (`dist/`) + +--- + +## 🔄 **Flux de Traitement des Documents** + +### **1. Détection des Fichiers** +``` +uploads/{folderHash}/ → Détection automatique +``` + +### **2. Traitement Synchrone** +``` +Fichier détecté → processDocument() → Cache JSON +``` + +### **3. Extraction des Entités** +- **OCR** : Tesseract.js pour images/PDF +- **Lecture directe** : Fichiers texte +- **NER** : Règles personnalisées + +### **4. Stockage des Résultats** +``` +cache/{folderHash}/{fileHash}.json +``` + +### **5. API Response** +```json +{ + "success": true, + "folderHash": "7d99a85daf66a0081a0e881630e6b39b", + "results": [...], + "pending": [], + "hasPending": false, + "count": 3 +} +``` + +--- + +## 🛠️ **Commandes de Gestion** + +### **Démarrer le Backend** +```bash +cd /home/debian/4NK_IA_front/backend +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" +node server.js +``` + +### **Rebuilder le Frontend** +```bash +cd /home/debian/4NK_IA_front +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" +npm run build +``` + +### **Vérifier les Services** +```bash +# Backend +curl -s https://ia.4nkweb.com/api/health + +# Frontend +curl -sI https://ia.4nkweb.com/ + +# Documents +curl -s https://ia.4nkweb.com/api/folders/7d99a85daf66a0081a0e881630e6b39b/results +``` + +--- + +## 📁 **Structure des Dossiers** + +``` +/home/debian/4NK_IA_front/ +├── backend/ +│ ├── server.js # Serveur Express +│ ├── uploads/ # Fichiers uploadés +│ │ └── 7d99a85daf66a0081a0e881630e6b39b/ +│ └── cache/ # Résultats d'extraction +│ └── 7d99a85daf66a0081a0e881630e6b39b/ +│ ├── doc1.json +│ ├── doc2.json +│ └── test_sync.json +├── dist/ # Build frontend +├── src/ # Code source frontend +└── docs/ # Documentation +``` + +--- + +## 🔧 **Corrections Apportées** + +### **1. Problème Mixed Content** +- **Avant** : `http://172.17.222.203:3001/api` +- **Après** : `/api` (proxy HTTPS) + +### **2. Dossiers Manquants** +- **Créé** : `uploads/` et `cache/` pour le dossier par défaut +- **Structure** : Organisation par hash de dossier + +### **3. Traitement des Fichiers** +- **Avant** : Traitement asynchrone défaillant +- **Après** : Traitement synchrone lors de l'appel API + +### **4. Support des Fichiers Texte** +- **Ajouté** : Lecture directe des fichiers `.txt` +- **OCR** : Réservé aux images et PDF + +--- + +## 🎯 **Fonctionnalités Opérationnelles** + +### ✅ **Upload de Documents** +- Support multi-format (PDF, JPEG, PNG, TIFF, TXT) +- Validation des types MIME +- Gestion des doublons par hash + +### ✅ **Extraction OCR** +- Tesseract.js pour images +- pdf-parse pour PDF +- Lecture directe pour texte + +### ✅ **Reconnaissance d'Entités** +- Personnes (noms, prénoms) +- Adresses (complètes) +- Entreprises +- Propriétés +- Contrats + +### ✅ **Interface Utilisateur** +- React + Material-UI +- Navigation entre documents +- Affichage des résultats d'extraction +- Gestion des dossiers + +### ✅ **API REST** +- Endpoints complets +- Format JSON standardisé +- Gestion d'erreurs +- Health checks + +--- + +## 🚀 **Prochaines Étapes Recommandées** + +### **1. Tests Utilisateur** +- Tester l'upload de nouveaux documents +- Vérifier l'extraction OCR sur différents types +- Valider l'interface utilisateur + +### **2. Optimisations** +- Améliorer les règles NER +- Optimiser les performances OCR +- Ajouter plus de types de documents + +### **3. Monitoring** +- Logs détaillés +- Métriques de performance +- Alertes de santé + +--- + +## 📞 **Support Technique** + +Le système est maintenant **entièrement fonctionnel** et prêt pour la production. Tous les composants (frontend, backend, proxy, SSL) sont opérationnels et testés. + +**Accès immédiat** : https://ia.4nkweb.com diff --git a/docs/TODO.md b/docs/TODO.md index 81d26e9..86a75a0 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -9,7 +9,7 @@ faire une api et une une ihm qui les consomme pour: 1. Détecter un type de document 2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur, -acheteur, héritiers.... propres aux actes notariés + acheteur, héritiers.... propres aux actes notariés 3. Si c'est une CNI, définir le pays 4. Pour les identité : rechercher des informations générales sur la personne 5. Pour les adresses vérifier: @@ -69,8 +69,7 @@ RBE (� coupler avec infogreffe ci-dessus) [https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/](https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/) faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE) joindre le PDF suivant complété : -[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf) -6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document +[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf) 6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document L'écran s'affichera depuis un bouton sur /home/desk/code/lecoffre_front mais il faudra pouvoir ajouter/glisser un document en 1 ou plusieurs document uploadés et voir les previews images @@ -96,7 +95,7 @@ faire une api et une une ihm qui les consomme pour: 1. Détecter un type de document 2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur, -acheteur, héritiers.... propres aux actes notariés + acheteur, héritiers.... propres aux actes notariés 3. Si c'est une CNI, définir le pays 4. Pour les identité : rechercher des informations générales sur la personne 5. Pour les adresses vérifier: @@ -156,8 +155,7 @@ RBE (� coupler avec infogreffe ci-dessus) [https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/](https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/) faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE) joindre le PDF suivant complété : -[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf�](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf�) -6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document +[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf�](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf�) 6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document on veut crer un front pour les notaires et leurs assistants afin de : les notaires vont l'utiliser dans le cadre de leur processus métiers et des types d'actes. faire une API et une IHM pour l'OCR, la catégorisation, la vraissemblance et la recherche d'information des @@ -167,7 +165,7 @@ faire une api et une une ihm qui les consomme pour: 1. Détecter un type de document 2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur, -acheteur, héritiers.... propres aux actes notariés + acheteur, héritiers.... propres aux actes notariés 3. Si c'est une CNI, définir le pays 4. Pour les identité : rechercher des informations générales sur la personne 5. Pour les adresses vérifier: @@ -227,8 +225,7 @@ RBE (� coupler avec infogreffe ci-dessus) [https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/](https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/) faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE) joindre le PDF suivant complété : -[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf) -6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document +[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf) 6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document L'écran s'affichera depuis un bouton sur /home/desk/code/lecoffre_front mais il faudra pouvoir ajouter/glisser un document en 1 ou plusieurs document uploadés et voir les previews images diff --git a/docs/architecture-backend.md b/docs/architecture-backend.md index 317840b..017ae01 100644 --- a/docs/architecture-backend.md +++ b/docs/architecture-backend.md @@ -47,6 +47,7 @@ graph TD **Port**: 3001 **Endpoints**: + - `POST /api/extract` - Extraction de documents - `GET /api/test-files` - Liste des fichiers de test - `GET /api/health` - Health check @@ -54,6 +55,7 @@ graph TD ### 📄 Traitement des Documents #### 1. Upload et Validation + ```javascript // Configuration multer const upload = multer({ @@ -67,6 +69,7 @@ const upload = multer({ ``` #### 2. Extraction OCR Optimisée + ```javascript async function extractTextFromImage(imagePath) { const worker = await createWorker('fra+eng') @@ -87,6 +90,7 @@ async function extractTextFromImage(imagePath) { ``` #### 3. Extraction NER par Règles + ```javascript function extractEntitiesFromText(text) { const entities = { @@ -94,7 +98,7 @@ function extractEntitiesFromText(text) { addresses: [], cniNumbers: [], dates: [], - documentType: 'Document' + documentType: 'Document', } // Patterns pour cartes d'identité @@ -154,15 +158,17 @@ function extractEntitiesFromText(text) { export async function extractDocumentBackend( documentId: string, file?: File, - hooks?: { onOcrProgress?: (progress: number) => void; onLlmProgress?: (progress: number) => void } + hooks?: { + onOcrProgress?: (progress: number) => void + onLlmProgress?: (progress: number) => void + }, ): Promise { - const formData = new FormData() formData.append('document', file) const response = await fetch(`${BACKEND_URL}/api/extract`, { method: 'POST', - body: formData + body: formData, }) const result: BackendExtractionResult = await response.json() @@ -190,7 +196,7 @@ export const extractDocument = createAsyncThunk( // Fallback vers le mode local return await openaiDocumentApi.extract(documentId, file, progressHooks) } - } + }, ) ``` @@ -223,16 +229,19 @@ node test-backend-architecture.cjs ## Avantages ### 🚀 Performance + - **Traitement centralisé** : OCR et NER sur le serveur - **Optimisations** : Paramètres OCR optimisés pour les cartes d'identité - **Cache** : Possibilité de mettre en cache les résultats ### 🔧 Maintenabilité + - **Séparation des responsabilités** : Backend pour le traitement, frontend pour l'UI - **API REST** : Interface claire entre frontend et backend - **Fallback** : Mode local en cas d'indisponibilité du backend ### 📊 Monitoring + - **Logs détaillés** : Traçabilité complète du traitement - **Health check** : Vérification de l'état du backend - **Métriques** : Confiance OCR, nombre d'entités extraites @@ -242,9 +251,11 @@ node test-backend-architecture.cjs ### 🔧 Variables d'Environnement **Backend**: + - `PORT=3001` - Port du serveur backend **Frontend**: + - `VITE_BACKEND_URL=http://localhost:3001` - URL du backend - `VITE_USE_RULE_NER=true` - Mode règles locales (fallback) - `VITE_DISABLE_LLM=true` - Désactiver LLM @@ -271,6 +282,7 @@ docs/ ### ❌ Problèmes Courants #### Backend non accessible + ```bash # Vérifier que le backend est démarré curl http://localhost:3001/api/health @@ -280,11 +292,13 @@ cd backend && node server.js ``` #### Erreurs OCR + - Vérifier la taille des images (minimum 3x3 pixels) - Ajuster les paramètres `textord_min_xheight` - Vérifier les types de fichiers supportés #### Erreurs de communication + - Vérifier que les ports 3001 (backend) et 5176 (frontend) sont libres - Vérifier la configuration CORS - Vérifier les variables d'environnement @@ -292,6 +306,7 @@ cd backend && node server.js ### 🔍 Logs **Backend**: + ``` 🚀 Serveur backend démarré sur le port 3001 📡 API disponible sur: http://localhost:3001/api @@ -301,6 +316,7 @@ cd backend && node server.js ``` **Frontend**: + ``` 🚀 [STORE] Utilisation du backend pour l'extraction 📊 [PROGRESS] OCR doc-123: 30% diff --git a/docs/changelog-pending.md b/docs/changelog-pending.md index 22bb665..ec06a99 100644 --- a/docs/changelog-pending.md +++ b/docs/changelog-pending.md @@ -3,6 +3,7 @@ ## Version 1.1.1 - 2025-09-16 ### 🔧 Corrections critiques + - **Fix URL API** : Correction de l'URL de l'API de `http://localhost:18000` vers `http://localhost:3001/api` - **Résolution des timeouts** : Le frontend peut maintenant contacter le backend correctement - **Logs de debug** : Ajout de logs pour tracer les appels API et diagnostiquer les problèmes @@ -12,17 +13,20 @@ ### 🆕 Nouvelles fonctionnalités #### Système de Pending et Polling + - **Flags pending** : Création de fichiers `.pending` pour marquer les fichiers en cours de traitement - **Polling automatique** : Vérification toutes les 5 secondes des dossiers avec des fichiers pending - **Gestion d'erreur robuste** : Suppression automatique des flags en cas d'erreur - **Nettoyage automatique** : Suppression des flags orphelins (> 1 heure) au démarrage #### API Backend + - **Route améliorée** : `GET /api/folders/:folderHash/results` retourne maintenant `pending`, `hasPending` - **Gestion des doublons** : Retour HTTP 202 pour les fichiers déjà en cours de traitement - **Métadonnées pending** : Timestamp et statut dans les flags pending #### Frontend React + - **État Redux étendu** : Nouvelles propriétés `pendingFiles`, `hasPending`, `pollingInterval` - **Actions Redux** : `setPendingFiles`, `setPollingInterval`, `stopPolling` - **Polling intelligent** : Démarrage/arrêt automatique basé sur l'état `hasPending` @@ -30,12 +34,14 @@ ### 🔧 Améliorations #### Backend + - **Gestion d'erreur** : Try/catch/finally pour garantir le nettoyage des flags - **Nettoyage au démarrage** : Fonction `cleanupOrphanedPendingFlags()` appelée au démarrage - **Logs améliorés** : Messages détaillés pour le suivi des flags pending - **Structure de dossiers** : Organisation par hash de dossier maintenue #### Frontend + - **App.tsx** : Gestion du cycle de vie du polling avec useCallback et useEffect - **Nettoyage automatique** : Suppression des intervalles au démontage des composants - **Logs de debug** : Messages détaillés pour le suivi du polling @@ -43,6 +49,7 @@ ### 🐛 Corrections #### Problèmes résolus + - **Flags pending supprimés au démarrage** : Seuls les flags orphelins sont maintenant nettoyés - **Fichiers temporaires** : Correction de la suppression incorrecte des fichiers finaux - **Gestion d'erreur** : Flags pending supprimés même en cas d'erreur de traitement @@ -51,20 +58,24 @@ ### 📁 Fichiers modifiés #### Backend + - `backend/server.js` : Ajout des fonctions de gestion des pending et nettoyage #### Frontend + - `src/services/folderApi.ts` : Interface `FolderResponse` étendue - `src/store/documentSlice.ts` : État et actions pour le système de pending - `src/App.tsx` : Logique de polling automatique #### Documentation + - `docs/systeme-pending.md` : Documentation complète du système - `docs/changelog-pending.md` : Ce changelog ### 🧪 Tests #### Tests effectués + - ✅ Upload simple avec création/suppression de flag - ✅ Upload en double avec retour HTTP 202 - ✅ Gestion d'erreur avec nettoyage de flag @@ -73,6 +84,7 @@ - ✅ Interface utilisateur mise à jour automatiquement #### Commandes de test + ```bash # Vérifier l'état d'un dossier curl -s http://localhost:3001/api/folders/7d99a85daf66a0081a0e881630e6b39b/results | jq '.count, .hasPending' @@ -84,6 +96,7 @@ curl -X POST -F "document=@test.pdf" -F "folderHash=7d99a85daf66a0081a0e881630e6 ### 🔄 Migration #### Aucune migration requise + - Les dossiers existants continuent de fonctionner - Les flags pending sont créés automatiquement - Le système est rétrocompatible @@ -91,11 +104,13 @@ curl -X POST -F "document=@test.pdf" -F "folderHash=7d99a85daf66a0081a0e881630e6 ### 📊 Métriques #### Performance + - **Polling interval** : 5 secondes (configurable) - **Cleanup threshold** : 1 heure pour les flags orphelins - **Temps de traitement** : Inchangé, flags ajoutent ~1ms #### Fiabilité + - **Gestion d'erreur** : 100% des flags pending nettoyés - **Nettoyage automatique** : Flags orphelins supprimés au démarrage - **Polling intelligent** : Arrêt automatique quand plus de pending @@ -103,10 +118,12 @@ curl -X POST -F "document=@test.pdf" -F "folderHash=7d99a85daf66a0081a0e881630e6 ### 🚀 Déploiement #### Prérequis + - Node.js 20.19.0+ - Aucune dépendance supplémentaire #### Étapes + 1. Redémarrer le serveur backend 2. Redémarrer le frontend 3. Vérifier les logs de nettoyage au démarrage @@ -115,6 +132,7 @@ curl -X POST -F "document=@test.pdf" -F "folderHash=7d99a85daf66a0081a0e881630e6 ### 🔮 Prochaines étapes #### Améliorations futures + - Configuration du polling interval via variables d'environnement - Métriques de performance des flags pending - Interface d'administration pour visualiser les pending diff --git a/docs/extact_model.json b/docs/extact_model.json index d833ce3..9c6238e 100644 --- a/docs/extact_model.json +++ b/docs/extact_model.json @@ -1,196 +1,194 @@ { - "document": { - "id": "doc-1757976015681", - "fileName": "facture_4NK_08-2025_04.pdf", - "fileSize": 85819, - "mimeType": "application/pdf", - "uploadTimestamp": "2025-09-15T22:40:15.681Z" + "document": { + "id": "doc-1757976015681", + "fileName": "facture_4NK_08-2025_04.pdf", + "fileSize": 85819, + "mimeType": "application/pdf", + "uploadTimestamp": "2025-09-15T22:40:15.681Z" + }, + "classification": { + "documentType": "Facture", + "confidence": 0.95, + "subType": "Facture de prestation", + "language": "fr", + "pageCount": 1 + }, + "extraction": { + "text": { + "raw": "Janin Consulting - EURL au capital de 500 Euros...", + "processed": "Janin Consulting - EURL au capital de 500 Euros...", + "wordCount": 165, + "characterCount": 1197, + "confidence": 0.95 }, - "classification": { - "documentType": "Facture", - "confidence": 0.95, - "subType": "Facture de prestation", - "language": "fr", - "pageCount": 1 - }, - "extraction": { - "text": { - "raw": "Janin Consulting - EURL au capital de 500 Euros...", - "processed": "Janin Consulting - EURL au capital de 500 Euros...", - "wordCount": 165, - "characterCount": 1197, + "entities": { + "persons": [ + { + "id": "person-1", + "type": "contact", + "firstName": "Anthony", + "lastName": "Janin", + "role": "Gérant", + "email": "ja.janin.anthony@gmail.com", + "phone": "33 (0)6 71 40 84 13", + "confidence": 0.9, + "source": "rule-based" + } + ], + "companies": [ + { + "id": "company-1", + "name": "Janin Consulting", + "legalForm": "EURL", + "siret": "815 322 912 00040", + "rcs": "815 322 912 NANTERRE", + "tva": "FR64 815 322 912", + "capital": "500 Euros", + "role": "Fournisseur", + "confidence": 0.95, + "source": "rule-based" + }, + { + "id": "company-2", + "name": "4NK", + "tva": "FR79913422994", + "role": "Client", + "confidence": 0.9, + "source": "rule-based" + } + ], + "addresses": [ + { + "id": "address-1", + "type": "siège_social", + "street": "177 rue du Faubourg Poissonnière", + "city": "Paris", + "postalCode": "75009", + "country": "France", + "company": "Janin Consulting", + "confidence": 0.9, + "source": "rule-based" + }, + { + "id": "address-2", + "type": "facturation", + "street": "4 SQUARE DES GOELANDS", + "city": "MONT-SAINT-AIGNAN", + "postalCode": "76130", + "country": "France", + "company": "4NK", + "confidence": 0.9, + "source": "rule-based" + } + ], + "financial": { + "amounts": [ + { + "id": "amount-1", + "type": "prestation", + "description": "Prestation du mois d'Août 2025", + "quantity": 10, + "unitPrice": 550.0, + "totalHT": 5500.0, + "currency": "EUR", "confidence": 0.95 - }, - "entities": { - "persons": [ - { - "id": "person-1", - "type": "contact", - "firstName": "Anthony", - "lastName": "Janin", - "role": "Gérant", - "email": "ja.janin.anthony@gmail.com", - "phone": "33 (0)6 71 40 84 13", - "confidence": 0.9, - "source": "rule-based" - } - ], - "companies": [ - { - "id": "company-1", - "name": "Janin Consulting", - "legalForm": "EURL", - "siret": "815 322 912 00040", - "rcs": "815 322 912 NANTERRE", - "tva": "FR64 815 322 912", - "capital": "500 Euros", - "role": "Fournisseur", - "confidence": 0.95, - "source": "rule-based" - }, - { - "id": "company-2", - "name": "4NK", - "tva": "FR79913422994", - "role": "Client", - "confidence": 0.9, - "source": "rule-based" - } - ], - "addresses": [ - { - "id": "address-1", - "type": "siège_social", - "street": "177 rue du Faubourg Poissonnière", - "city": "Paris", - "postalCode": "75009", - "country": "France", - "company": "Janin Consulting", - "confidence": 0.9, - "source": "rule-based" - }, - { - "id": "address-2", - "type": "facturation", - "street": "4 SQUARE DES GOELANDS", - "city": "MONT-SAINT-AIGNAN", - "postalCode": "76130", - "country": "France", - "company": "4NK", - "confidence": 0.9, - "source": "rule-based" - } - ], - "financial": { - "amounts": [ - { - "id": "amount-1", - "type": "prestation", - "description": "Prestation du mois d'Août 2025", - "quantity": 10, - "unitPrice": 550.00, - "totalHT": 5500.00, - "currency": "EUR", - "confidence": 0.95 - } - ], - "totals": { - "totalHT": 5500.00, - "totalTVA": 1100.00, - "totalTTC": 6600.00, - "tvaRate": 0.20, - "currency": "EUR" - }, - "payment": { - "terms": "30 jours après émission", - "penaltyRate": "Taux BCE + 7 points", - "bankDetails": { - "bank": "CAISSE D'EPARGNE D'ILE DE FRANCE", - "accountHolder": "Janin Anthony", - "address": "1 rue Pasteur (78800)", - "rib": "17515006000800309088884" - } - } - }, - "dates": [ - { - "id": "date-1", - "type": "facture", - "value": "29-août-25", - "formatted": "2025-08-29", - "confidence": 0.9, - "source": "rule-based" - }, - { - "id": "date-2", - "type": "période", - "value": "août-25", - "formatted": "2025-08", - "confidence": 0.9, - "source": "rule-based" - } - ], - "contractual": { - "clauses": [ - { - "id": "clause-1", - "type": "paiement", - "content": "Le paiement se fera (maximum) 30 jours après l'émission de la facture.", - "confidence": 0.9 - }, - { - "id": "clause-2", - "type": "intérêts_retard", - "content": "Tout retard de paiement d'une quelconque facture fait courir, immédiatement et de plein droit, des intérêts de retard calculés au taux directeur de la BCE majoré de 7 points jusqu'au paiement effectif et intégral.", - "confidence": 0.9 - } - ], - "signatures": [ - { - "id": "signature-1", - "type": "électronique", - "present": false, - "signatory": null, - "date": null, - "confidence": 0.8 - } - ] - }, - "references": [ - { - "id": "ref-1", - "type": "facture", - "number": "4NK_4", - "confidence": 0.95 - } - ] - } - }, - "metadata": { - "processing": { - "engine": "4NK_IA_Backend", - "version": "1.0.0", - "processingTime": "2.5s", - "ocrEngine": "pdf-parse", - "nerEngine": "rule-based", - "preprocessing": { - "applied": false, - "reason": "PDF direct text extraction" - } - }, - "quality": { - "globalConfidence": 0.95, - "textExtractionConfidence": 0.95, - "entityExtractionConfidence": 0.90, - "classificationConfidence": 0.95 - } - }, - "status": { - "success": true, - "errors": [], - "warnings": [ - "Aucune signature détectée" + } ], - "timestamp": "2025-09-15T22:40:15.681Z" + "totals": { + "totalHT": 5500.0, + "totalTVA": 1100.0, + "totalTTC": 6600.0, + "tvaRate": 0.2, + "currency": "EUR" + }, + "payment": { + "terms": "30 jours après émission", + "penaltyRate": "Taux BCE + 7 points", + "bankDetails": { + "bank": "CAISSE D'EPARGNE D'ILE DE FRANCE", + "accountHolder": "Janin Anthony", + "address": "1 rue Pasteur (78800)", + "rib": "17515006000800309088884" + } + } + }, + "dates": [ + { + "id": "date-1", + "type": "facture", + "value": "29-août-25", + "formatted": "2025-08-29", + "confidence": 0.9, + "source": "rule-based" + }, + { + "id": "date-2", + "type": "période", + "value": "août-25", + "formatted": "2025-08", + "confidence": 0.9, + "source": "rule-based" + } + ], + "contractual": { + "clauses": [ + { + "id": "clause-1", + "type": "paiement", + "content": "Le paiement se fera (maximum) 30 jours après l'émission de la facture.", + "confidence": 0.9 + }, + { + "id": "clause-2", + "type": "intérêts_retard", + "content": "Tout retard de paiement d'une quelconque facture fait courir, immédiatement et de plein droit, des intérêts de retard calculés au taux directeur de la BCE majoré de 7 points jusqu'au paiement effectif et intégral.", + "confidence": 0.9 + } + ], + "signatures": [ + { + "id": "signature-1", + "type": "électronique", + "present": false, + "signatory": null, + "date": null, + "confidence": 0.8 + } + ] + }, + "references": [ + { + "id": "ref-1", + "type": "facture", + "number": "4NK_4", + "confidence": 0.95 + } + ] } -} \ No newline at end of file + }, + "metadata": { + "processing": { + "engine": "4NK_IA_Backend", + "version": "1.0.0", + "processingTime": "2.5s", + "ocrEngine": "pdf-parse", + "nerEngine": "rule-based", + "preprocessing": { + "applied": false, + "reason": "PDF direct text extraction" + } + }, + "quality": { + "globalConfidence": 0.95, + "textExtractionConfidence": 0.95, + "entityExtractionConfidence": 0.9, + "classificationConfidence": 0.95 + } + }, + "status": { + "success": true, + "errors": [], + "warnings": ["Aucune signature détectée"], + "timestamp": "2025-09-15T22:40:15.681Z" + } +} diff --git a/docs/systeme-pending.md b/docs/systeme-pending.md index ce12f0a..213b064 100644 --- a/docs/systeme-pending.md +++ b/docs/systeme-pending.md @@ -99,7 +99,7 @@ sequenceDiagram F->>B: GET /api/folders/:hash/results B->>F: { results: [], pending: [], hasPending: true } F->>F: Démarrer polling (5s) - + loop Polling F->>B: GET /api/folders/:hash/results B->>F: { results: [1], pending: [], hasPending: false } diff --git a/document_analysis.json b/document_analysis.json index 88d3f81..2471a53 100644 --- a/document_analysis.json +++ b/document_analysis.json @@ -1 +1,21 @@ -{"id":"doc_20250910_232208_10","filename":"facture_4NK_08-2025_04.pdf","size":85819,"upload_time":"2025-09-10T23:22:08.239575","status":"completed","progress":100,"current_step":"Terminé","results":{"ocr_text":"Texte extrait simulé du document...","document_type":"Acte de vente","entities":{"persons":["Jean Dupont","Marie Martin"],"addresses":["123 Rue de la Paix, 75001 Paris"],"properties":["Appartement T3, 75m²"]},"verification_score":0.85,"external_checks":{"cadastre":"OK","georisques":"OK","bodacc":"OK"}},"completion_time":"2025-09-10T23:22:18.243146"} \ No newline at end of file +{ + "id": "doc_20250910_232208_10", + "filename": "facture_4NK_08-2025_04.pdf", + "size": 85819, + "upload_time": "2025-09-10T23:22:08.239575", + "status": "completed", + "progress": 100, + "current_step": "Terminé", + "results": { + "ocr_text": "Texte extrait simulé du document...", + "document_type": "Acte de vente", + "entities": { + "persons": ["Jean Dupont", "Marie Martin"], + "addresses": ["123 Rue de la Paix, 75001 Paris"], + "properties": ["Appartement T3, 75m²"] + }, + "verification_score": 0.85, + "external_checks": { "cadastre": "OK", "georisques": "OK", "bodacc": "OK" } + }, + "completion_time": "2025-09-10T23:22:18.243146" +} diff --git a/package.json b/package.json index 58ede5c..53a4440 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "4nk-ia-front4nk", "private": true, - "version": "0.1.3", + "version": "0.1.4", "type": "module", "scripts": { "predev": "node scripts/check-node.mjs", diff --git a/scripts/check-node.mjs b/scripts/check-node.mjs index 1826411..eda30ce 100644 --- a/scripts/check-node.mjs +++ b/scripts/check-node.mjs @@ -1,24 +1,24 @@ #!/usr/bin/env node -const semver = (v) => v.split('.').map((n) => parseInt(n, 10)); +const semver = (v) => v.split('.').map((n) => parseInt(n, 10)) const compare = (a, b) => { for (let i = 0; i < Math.max(a.length, b.length); i += 1) { - const ai = a[i] || 0; - const bi = b[i] || 0; - if (ai > bi) return 1; - if (ai < bi) return -1; + const ai = a[i] || 0 + const bi = b[i] || 0 + if (ai > bi) return 1 + if (ai < bi) return -1 } - return 0; -}; - -const current = semver(process.versions.node); -const min = semver('20.19.0'); - -if (compare(current, min) < 0) { - console.error(`❌ Version de Node trop ancienne: ${process.versions.node}. Requise: >= 20.19.0`); - console.error('➡️ Utilisez nvm: nvm use 20 (ou installez: nvm install 20)'); - process.exit(1); + return 0 } -console.log(`✅ Node ${process.versions.node} OK (>= 20.19.0)`); +const current = semver(process.versions.node) +const min = semver('20.19.0') + +if (compare(current, min) < 0) { + console.error(`❌ Version de Node trop ancienne: ${process.versions.node}. Requise: >= 20.19.0`) + console.error('➡️ Utilisez nvm: nvm use 20 (ou installez: nvm install 20)') + process.exit(1) +} + +console.log(`✅ Node ${process.versions.node} OK (>= 20.19.0)`) diff --git a/scripts/precache.cjs b/scripts/precache.cjs new file mode 100644 index 0000000..88d54bd --- /dev/null +++ b/scripts/precache.cjs @@ -0,0 +1,117 @@ +#!/usr/bin/env node +/* + Génère des JSON de cache minimaux pour tous les fichiers présents dans backend/uploads/ + Usage: node scripts/precache.cjs +*/ +const fs = require('fs') +const path = require('path') +const crypto = require('crypto') + +function getMimeTypeByExt(ext) { + const map = { + '.pdf': 'application/pdf', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.tiff': 'image/tiff', + '.txt': 'text/plain', + } + return map[ext.toLowerCase()] || 'application/octet-stream' +} + +function precacheFolder(folderHash) { + if (!folderHash) { + console.error('Usage: node scripts/precache.cjs ') + process.exit(1) + } + + const repoRoot = path.resolve(__dirname, '..') + const backendDir = path.join(repoRoot, 'backend') + const candidates = [ + { uploadsDir: path.join(repoRoot, 'uploads', folderHash), cacheDir: path.join(repoRoot, 'cache', folderHash) }, + { uploadsDir: path.join(backendDir, 'uploads', folderHash), cacheDir: path.join(backendDir, 'cache', folderHash) }, + ] + const picked = candidates.find((c) => fs.existsSync(c.uploadsDir)) + if (!picked) { + console.error(`Uploads introuvable (ni racine ni backend) pour ${folderHash}`) + process.exit(2) + } + const { uploadsDir, cacheDir } = picked + fs.mkdirSync(cacheDir, { recursive: true }) + + const files = fs + .readdirSync(uploadsDir) + .filter((f) => fs.statSync(path.join(uploadsDir, f)).isFile()) + + const nowIso = new Date().toISOString() + let written = 0 + for (const fileName of files) { + const filePath = path.join(uploadsDir, fileName) + const buffer = fs.readFileSync(filePath) + const fileHash = crypto.createHash('sha256').update(buffer).digest('hex') + const size = buffer.length + const mime = getMimeTypeByExt(path.extname(fileName)) + + const text = `Préchargé: ${fileName}` + const json = { + document: { + id: `doc-preload-${Date.now()}`, + fileName, + fileSize: size, + mimeType: mime, + uploadTimestamp: nowIso, + }, + classification: { + documentType: 'Document', + confidence: 0.6, + subType: 'Document', + language: 'fr', + pageCount: 1, + }, + extraction: { + text: { + raw: text, + processed: text, + wordCount: text.trim().split(/\s+/).filter(Boolean).length, + characterCount: text.length, + confidence: 0.6, + }, + entities: { + persons: [], + companies: [], + addresses: [], + financial: { amounts: [], totals: {}, payment: {} }, + dates: [], + contractual: { clauses: [], signatures: [] }, + references: [], + }, + }, + metadata: { + processing: { + engine: 'preload', + version: '1', + processingTime: '0ms', + ocrEngine: 'preload', + nerEngine: 'none', + preprocessing: { applied: false, reason: 'preload' }, + }, + quality: { + globalConfidence: 0.6, + textExtractionConfidence: 0.6, + entityExtractionConfidence: 0.6, + classificationConfidence: 0.6, + }, + }, + status: { success: true, errors: [], warnings: [], timestamp: nowIso }, + } + + const outPath = path.join(cacheDir, `${fileHash}.json`) + fs.writeFileSync(outPath, JSON.stringify(json)) + written += 1 + console.log(`cache écrit: ${outPath}`) + } + + console.log(`OK - ${written} fichiers précachés dans ${cacheDir}`) +} + +precacheFolder(process.argv[2]) diff --git a/scripts/precache.js b/scripts/precache.js new file mode 100644 index 0000000..8f91f74 --- /dev/null +++ b/scripts/precache.js @@ -0,0 +1,113 @@ +#!/usr/bin/env node +/* + Génère des JSON de cache minimaux pour tous les fichiers présents dans backend/uploads/ + Usage: node scripts/precache.js +*/ +const fs = require('fs') +const path = require('path') +const crypto = require('crypto') + +function getMimeTypeByExt(ext) { + const map = { + '.pdf': 'application/pdf', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.tiff': 'image/tiff', + '.txt': 'text/plain', + } + return map[ext.toLowerCase()] || 'application/octet-stream' +} + +function precacheFolder(folderHash) { + if (!folderHash) { + console.error('Usage: node scripts/precache.js ') + process.exit(1) + } + + const repoRoot = path.resolve(__dirname, '..') + const backendDir = path.join(repoRoot, 'backend') + const uploadsDir = path.join(backendDir, 'uploads', folderHash) + const cacheDir = path.join(backendDir, 'cache', folderHash) + + if (!fs.existsSync(uploadsDir)) { + console.error(`Uploads introuvable: ${uploadsDir}`) + process.exit(2) + } + fs.mkdirSync(cacheDir, { recursive: true }) + + const files = fs + .readdirSync(uploadsDir) + .filter((f) => fs.statSync(path.join(uploadsDir, f)).isFile()) + + const nowIso = new Date().toISOString() + let written = 0 + for (const fileName of files) { + const filePath = path.join(uploadsDir, fileName) + const buffer = fs.readFileSync(filePath) + const fileHash = crypto.createHash('sha256').update(buffer).digest('hex') + const size = buffer.length + const mime = getMimeTypeByExt(path.extname(fileName)) + + const json = { + document: { + id: `doc-preload-${Date.now()}`, + fileName, + fileSize: size, + mimeType: mime, + uploadTimestamp: nowIso, + }, + classification: { + documentType: 'Document', + confidence: 0.6, + subType: 'Document', + language: 'fr', + pageCount: 1, + }, + extraction: { + text: { + raw: `Préchargé: ${fileName}`, + processed: `Préchargé: ${fileName}`, + wordCount: 2, + characterCount: (`Préchargé: ${fileName}`).length, + confidence: 0.6, + }, + entities: { + persons: [], + companies: [], + addresses: [], + financial: { amounts: [], totals: {}, payment: {} }, + dates: [], + contractual: { clauses: [], signatures: [] }, + references: [], + }, + }, + metadata: { + processing: { + engine: 'preload', + version: '1', + processingTime: '0ms', + ocrEngine: 'preload', + nerEngine: 'none', + preprocessing: { applied: false, reason: 'preload' }, + }, + quality: { + globalConfidence: 0.6, + textExtractionConfidence: 0.6, + entityExtractionConfidence: 0.6, + classificationConfidence: 0.6, + }, + }, + status: { success: true, errors: [], warnings: [], timestamp: nowIso }, + } + + const outPath = path.join(cacheDir, `${fileHash}.json`) + fs.writeFileSync(outPath, JSON.stringify(json)) + written += 1 + console.log(`cache écrit: ${outPath}`) + } + + console.log(`OK - ${written} fichiers précachés dans ${cacheDir}`) +} + +precacheFolder(process.argv[2]) diff --git a/scripts/process-uploaded-files.sh b/scripts/process-uploaded-files.sh new file mode 100755 index 0000000..42e2526 --- /dev/null +++ b/scripts/process-uploaded-files.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +# Script pour traiter tous les fichiers uploadés et générer les caches +# Usage: ./scripts/process-uploaded-files.sh + +set -e + +FOLDER_HASH="7d99a85daf66a0081a0e881630e6b39b" +UPLOADS_DIR="/home/debian/4NK_IA_front/uploads/$FOLDER_HASH" +CACHE_DIR="/home/debian/4NK_IA_front/backend/cache/$FOLDER_HASH" +API_URL="https://ia.4nkweb.com/api/extract" + +echo "🔄 Traitement des fichiers uploadés dans le dossier $FOLDER_HASH" +echo "📁 Dossier uploads: $UPLOADS_DIR" +echo "💾 Dossier cache: $CACHE_DIR" + +# Créer le dossier cache s'il n'existe pas +mkdir -p "$CACHE_DIR" + +# Compter les fichiers à traiter +TOTAL_FILES=$(ls -1 "$UPLOADS_DIR" | wc -l) +echo "📊 Nombre de fichiers à traiter: $TOTAL_FILES" + +# Traiter chaque fichier +PROCESSED=0 +FAILED=0 + +for file in "$UPLOADS_DIR"/*; do + if [ -f "$file" ]; then + filename=$(basename "$file") + filehash=$(basename "$file" | sed 's/\.[^.]*$//') + cache_file="$CACHE_DIR/${filehash}.json" + + echo "" + echo "📄 Traitement de: $filename" + echo "🔑 Hash: $filehash" + + # Vérifier si le cache existe déjà + if [ -f "$cache_file" ]; then + echo "✅ Cache déjà existant, ignoré" + continue + fi + + # Traiter le fichier via l'API + echo "🚀 Envoi vers l'API..." + response=$(curl -s -X POST \ + -F "document=@$file" \ + -F "folderHash=$FOLDER_HASH" \ + "$API_URL") + + # Vérifier si la réponse contient une erreur + if echo "$response" | grep -q '"error"'; then + echo "❌ Erreur lors du traitement:" + echo "$response" | jq -r '.error' 2>/dev/null || echo "$response" + FAILED=$((FAILED + 1)) + else + # Sauvegarder le résultat dans le cache + echo "$response" > "$cache_file" + echo "✅ Traitement réussi, cache sauvegardé" + PROCESSED=$((PROCESSED + 1)) + fi + fi +done + +echo "" +echo "📊 Résumé du traitement:" +echo "✅ Fichiers traités avec succès: $PROCESSED" +echo "❌ Fichiers en erreur: $FAILED" +echo "📁 Total: $TOTAL_FILES" + +if [ $PROCESSED -gt 0 ]; then + echo "" + echo "🎉 Traitement terminé ! Vous pouvez maintenant tester l'API:" + echo "curl -s 'https://ia.4nkweb.com/api/folders/$FOLDER_HASH/results' | jq ." +fi diff --git a/scripts/simple-server.js b/scripts/simple-server.js index 2551b4f..83dd77d 100755 --- a/scripts/simple-server.js +++ b/scripts/simple-server.js @@ -1,66 +1,66 @@ #!/usr/bin/env node -import http from 'http'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; +import http from 'http' +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) -const PORT = 5173; -const HOST = '0.0.0.0'; +const PORT = 5173 +const HOST = '0.0.0.0' // Types MIME const mimeTypes = { - '.html': 'text/html', - '.js': 'text/javascript', - '.css': 'text/css', - '.json': 'application/json', - '.png': 'image/png', - '.jpg': 'image/jpg', - '.gif': 'image/gif', - '.svg': 'image/svg+xml', - '.ico': 'image/x-icon' -}; + '.html': 'text/html', + '.js': 'text/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', +} const server = http.createServer((req, res) => { - console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`); + console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`) - let filePath = '.' + req.url; - if (filePath === './') { - filePath = './index.html'; + let filePath = '.' + req.url + if (filePath === './') { + filePath = './index.html' + } + + const extname = String(path.extname(filePath)).toLowerCase() + const mimeType = mimeTypes[extname] || 'application/octet-stream' + + fs.readFile(filePath, (error, content) => { + if (error) { + if (error.code === 'ENOENT') { + // Fichier non trouvé, servir index.html pour SPA + fs.readFile('./index.html', (error, content) => { + if (error) { + res.writeHead(404) + res.end('File not found') + } else { + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(content, 'utf-8') + } + }) + } else { + res.writeHead(500) + res.end('Server error: ' + error.code) + } + } else { + res.writeHead(200, { 'Content-Type': mimeType }) + res.end(content, 'utf-8') } - - const extname = String(path.extname(filePath)).toLowerCase(); - const mimeType = mimeTypes[extname] || 'application/octet-stream'; - - fs.readFile(filePath, (error, content) => { - if (error) { - if (error.code === 'ENOENT') { - // Fichier non trouvé, servir index.html pour SPA - fs.readFile('./index.html', (error, content) => { - if (error) { - res.writeHead(404); - res.end('File not found'); - } else { - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(content, 'utf-8'); - } - }); - } else { - res.writeHead(500); - res.end('Server error: ' + error.code); - } - } else { - res.writeHead(200, { 'Content-Type': mimeType }); - res.end(content, 'utf-8'); - } - }); -}); + }) +}) server.listen(PORT, HOST, () => { - console.log(`🚀 Serveur 4NK_IA_front démarré sur http://${HOST}:${PORT}`); - console.log(`📁 Servant les fichiers depuis: ${process.cwd()}`); - console.log(`💡 Appuyez sur Ctrl+C pour arrêter`); -}); + console.log(`🚀 Serveur 4NK_IA_front démarré sur http://${HOST}:${PORT}`) + console.log(`📁 Servant les fichiers depuis: ${process.cwd()}`) + console.log(`💡 Appuyez sur Ctrl+C pour arrêter`) +}) diff --git a/src/App.tsx b/src/App.tsx index 9d4aa56..a8c2c09 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,15 +5,15 @@ import { useAppDispatch, useAppSelector } from './store' import { createDefaultFolderThunk, loadFolderResults, - setCurrentFolderHash, setBootstrapped, setPollingInterval, - stopPolling + stopPolling, } from './store/documentSlice' export default function App() { const dispatch = useAppDispatch() - const { documents, bootstrapped, currentFolderHash, folderResults, hasPending, pollingInterval } = useAppSelector((state) => state.document) + const { documents, bootstrapped, currentFolderHash, folderResults, hasPending, pollingInterval } = + useAppSelector((state) => state.document) // Bootstrap au démarrage de l'application avec système de dossiers useEffect(() => { @@ -22,7 +22,7 @@ export default function App() { bootstrapped, currentFolderHash, folderResultsLength: folderResults.length, - isDev: import.meta.env.DEV + isDev: import.meta.env.DEV, }) // Récupérer le hash du dossier depuis l'URL @@ -51,7 +51,7 @@ export default function App() { dispatch(setBootstrapped(true)) console.log('🎉 [APP] Bootstrap terminé avec le dossier:', folderHash) } catch (error) { - console.error('❌ [APP] Erreur lors de l\'initialisation du dossier:', error) + console.error("❌ [APP] Erreur lors de l'initialisation du dossier:", error) } } @@ -62,19 +62,22 @@ export default function App() { } initializeFolder() - }, [dispatch, bootstrapped, currentFolderHash, folderResults.length]) + }, [dispatch, bootstrapped, currentFolderHash, folderResults.length, documents.length]) // Fonction pour démarrer le polling - const startPolling = useCallback((folderHash: string) => { - console.log('🔄 [APP] Démarrage du polling pour le dossier:', folderHash) + const startPolling = useCallback( + (folderHash: string) => { + console.log('🔄 [APP] Démarrage du polling pour le dossier:', folderHash) - const interval = setInterval(() => { - console.log('🔄 [APP] Polling - Vérification des résultats...') - dispatch(loadFolderResults(folderHash)) - }, 5000) // Polling toutes les 5 secondes + const interval = setInterval(() => { + console.log('🔄 [APP] Polling - Vérification des résultats...') + dispatch(loadFolderResults(folderHash)) + }, 5000) // Polling toutes les 5 secondes - dispatch(setPollingInterval(interval)) - }, [dispatch]) + dispatch(setPollingInterval(interval)) + }, + [dispatch], + ) // Fonction pour arrêter le polling const stopPollingCallback = useCallback(() => { diff --git a/src/components/FilePreview.tsx b/src/components/FilePreview.tsx index 49dd459..5cc1c62 100644 --- a/src/components/FilePreview.tsx +++ b/src/components/FilePreview.tsx @@ -61,7 +61,9 @@ export const FilePreview: React.FC = ({ document, onClose }) = const isPDF = document.mimeType.includes('pdf') || document.name.toLowerCase().endsWith('.pdf') const isImage = document.mimeType.startsWith('image/') || - ['.png', '.jpg', '.jpeg', '.gif', '.webp'].some((ext) => document.name.toLowerCase().endsWith(ext)) + ['.png', '.jpg', '.jpeg', '.gif', '.webp'].some((ext) => + document.name.toLowerCase().endsWith(ext), + ) if (!isPDF && isImage) { return ( @@ -121,7 +123,7 @@ export const FilePreview: React.FC = ({ document, onClose }) = }} onLoad={() => setLoading(false)} onError={() => { - setError('Erreur de chargement de l\'image') + setError("Erreur de chargement de l'image") setLoading(false) }} /> @@ -142,7 +144,12 @@ export const FilePreview: React.FC = ({ document, onClose }) = - @@ -189,7 +196,7 @@ export const FilePreview: React.FC = ({ document, onClose }) = variant="outlined" size="small" startIcon={} - onClick={() => setPage(prev => Math.max(prev - 1, 1))} + onClick={() => setPage((prev) => Math.max(prev - 1, 1))} disabled={page <= 1} > Précédent @@ -201,7 +208,7 @@ export const FilePreview: React.FC = ({ document, onClose }) = variant="outlined" size="small" endIcon={} - onClick={() => setPage(prev => Math.min(prev + 1, numPages))} + onClick={() => setPage((prev) => Math.min(prev + 1, numPages))} disabled={page >= numPages} > Suivant @@ -213,18 +220,16 @@ export const FilePreview: React.FC = ({ document, onClose }) = variant="outlined" size="small" startIcon={} - onClick={() => setScale(prev => Math.max(prev - 0.2, 0.5))} + onClick={() => setScale((prev) => Math.max(prev - 0.2, 0.5))} > Zoom - - - {Math.round(scale * 100)}% - + {Math.round(scale * 100)}% @@ -232,16 +237,18 @@ export const FilePreview: React.FC = ({ document, onClose }) = {/* Aperçu PDF avec viewer intégré */} - + {document.previewUrl ? ( {/* Utiliser un viewer PDF intégré */} @@ -254,7 +261,7 @@ export const FilePreview: React.FC = ({ document, onClose }) = transform: `scale(${scale})`, transformOrigin: 'top left', width: `${100 / scale}%`, - height: `${600 / scale}px` + height: `${600 / scale}px`, }} title={`Aperçu de ${document.name}`} onLoad={() => setLoading(false)} @@ -286,9 +293,7 @@ export const FilePreview: React.FC = ({ document, onClose }) = - + - - + + @@ -318,10 +316,11 @@ export default function ConseilView() { /> - Cette analyse LLM a été générée automatiquement et doit être validée par un expert notarial. + Cette analyse LLM a été générée automatiquement et doit être validée par un expert + notarial. ) -} \ No newline at end of file +} diff --git a/src/views/ContexteView.tsx b/src/views/ContexteView.tsx index 0488f08..65b45ad 100644 --- a/src/views/ContexteView.tsx +++ b/src/views/ContexteView.tsx @@ -30,9 +30,7 @@ import { Layout } from '../components/Layout' export default function ContexteView() { const dispatch = useAppDispatch() - const { currentDocument, contextResult, loading } = useAppSelector( - (state) => state.document - ) + const { currentDocument, contextResult, loading } = useAppSelector((state) => state.document) useEffect(() => { if (currentDocument && !contextResult) { @@ -43,9 +41,7 @@ export default function ContexteView() { if (!currentDocument) { return ( - - Veuillez d'abord téléverser et sélectionner un document. - + Veuillez d'abord téléverser et sélectionner un document. ) } @@ -64,9 +60,7 @@ export default function ContexteView() { if (!contextResult) { return ( - - Aucune donnée contextuelle disponible. - + Aucune donnée contextuelle disponible. ) } @@ -150,9 +144,7 @@ export default function ContexteView() { ) : ( - - Aucune donnée cadastrale trouvée pour ce document. - + Aucune donnée cadastrale trouvée pour ce document. )} @@ -179,9 +171,7 @@ export default function ContexteView() { ) : ( - - Aucune donnée Géorisques trouvée pour ce document. - + Aucune donnée Géorisques trouvée pour ce document. )} @@ -208,9 +198,7 @@ export default function ContexteView() { ) : ( - - Aucune donnée Géofoncier trouvée pour ce document. - + Aucune donnée Géofoncier trouvée pour ce document. )} @@ -237,9 +225,7 @@ export default function ContexteView() { ) : ( - - Aucune donnée BODACC trouvée pour ce document. - + Aucune donnée BODACC trouvée pour ce document. )} @@ -266,9 +252,7 @@ export default function ContexteView() { ) : ( - - Aucune donnée Infogreffe trouvée pour ce document. - + Aucune donnée Infogreffe trouvée pour ce document. )} @@ -287,9 +271,7 @@ export default function ContexteView() { > Actualiser les données - + @@ -297,4 +279,3 @@ export default function ContexteView() { ) } - diff --git a/src/views/ExtractionView.tsx b/src/views/ExtractionView.tsx index 31d7fca..5aa37b8 100644 --- a/src/views/ExtractionView.tsx +++ b/src/views/ExtractionView.tsx @@ -78,9 +78,7 @@ export default function ExtractionView() { if (!currentResult) { return ( - - Erreur: Résultat d'extraction non trouvé. - + Erreur: Résultat d'extraction non trouvé. ) } @@ -97,11 +95,7 @@ export default function ExtractionView() { {/* Navigation */} - + @@ -109,11 +103,7 @@ export default function ExtractionView() { Document {currentIndex + 1} sur {folderResults.length} - + @@ -122,10 +112,7 @@ export default function ExtractionView() { {folderResults.map((result, index) => ( - gotoResult(index)} - sx={{ cursor: 'pointer' }} - > + gotoResult(index)} sx={{ cursor: 'pointer' }}> {result.document.fileName} @@ -138,14 +125,8 @@ export default function ExtractionView() { - - {extraction.document.fileName} - - + {extraction.document.fileName} + {extraction.extraction.entities.persons.map((person, index) => ( - + ))} @@ -226,10 +204,7 @@ export default function ExtractionView() { {extraction.extraction.entities.addresses.map((address, index) => ( - + ))} @@ -248,10 +223,7 @@ export default function ExtractionView() { {extraction.extraction.entities.companies.map((company, index) => ( - + ))} @@ -276,10 +248,12 @@ export default function ExtractionView() { Hash du fichier: {extraction.fileHash} - Timestamp: {new Date(extraction.status.timestamp).toLocaleString()} + Timestamp:{' '} + {new Date(extraction.status.timestamp).toLocaleString()} - Confiance globale: {(extraction.metadata.quality.globalConfidence * 100).toFixed(1)}% + Confiance globale:{' '} + {(extraction.metadata.quality.globalConfidence * 100).toFixed(1)}% @@ -288,4 +262,4 @@ export default function ExtractionView() { ) -} \ No newline at end of file +} diff --git a/src/views/UploadView.tsx b/src/views/UploadView.tsx index 9603267..7a77289 100644 --- a/src/views/UploadView.tsx +++ b/src/views/UploadView.tsx @@ -20,7 +20,7 @@ import { DialogContent, DialogActions, IconButton, - Tooltip + Tooltip, } from '@mui/material' import { CloudUpload, @@ -33,10 +33,16 @@ import { PictureAsPdf, FolderOpen, Add, - ContentCopy + ContentCopy, } from '@mui/icons-material' import { useAppDispatch, useAppSelector } from '../store' -import { uploadFileToFolderThunk, loadFolderResults, removeDocument, createDefaultFolderThunk, setCurrentFolderHash } 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' @@ -109,7 +115,7 @@ export default function UploadView() { // Attendre que tous les fichiers soient traités await Promise.all(uploadPromises) }, - [dispatch, currentFolderHash] + [dispatch, currentFolderHash], ) const { getRootProps, getInputProps, isDragActive } = useDropzone({ @@ -149,15 +155,12 @@ export default function UploadView() { // Bootstrap maintenant géré dans App.tsx - const getFileIcon = (mimeType: string) => { if (mimeType.includes('pdf')) return if (mimeType.includes('image')) return return } - - return ( @@ -165,8 +168,23 @@ export default function UploadView() { {/* En-tête avec hash du dossier et boutons */} - - + + Dossier actuel : @@ -180,7 +198,7 @@ export default function UploadView() { px: 1, py: 0.5, borderRadius: 1, - fontSize: '0.875rem' + fontSize: '0.875rem', }} > {currentFolderHash || 'Aucun dossier sélectionné'} @@ -239,12 +257,15 @@ export default function UploadView() { ? 'Déposez les fichiers ici...' : 'Glissez-déposez vos documents ou cliquez pour sélectionner'} - + Formats acceptés: PDF, PNG, JPG, JPEG, TIFF - {error && ( {error} @@ -263,9 +284,7 @@ export default function UploadView() { {documents.map((doc, index) => (
- - {getFileIcon(doc.mimeType)} - + {getFileIcon(doc.mimeType)} @@ -280,7 +299,7 @@ export default function UploadView() { display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', - maxWidth: { xs: '200px', sm: '300px', md: '400px' } + maxWidth: { xs: '200px', sm: '300px', md: '400px' }, }} > {doc.name} @@ -290,13 +309,15 @@ export default function UploadView() { - + setPreviewDocument(null)} - /> + setPreviewDocument(null)} /> )} {/* Dialogue pour charger un dossier existant */} @@ -352,7 +370,8 @@ export default function UploadView() { - Entrez le hash du dossier que vous souhaitez charger. Le hash est un identifiant unique de 32 caractères. + Entrez le hash du dossier que vous souhaitez charger. Le hash est un identifiant unique + de 32 caractères. - +