feat: re-traiter le dossier (vider cache + reprocess); UI extraction robuste entités; Stepper + liste avec ellipsis; backend DELETE /folders/:hash/cache et POST /folders/:hash/reprocess

This commit is contained in:
4NK IA 2025-09-17 09:59:14 +00:00
parent b18a3077a2
commit 883f49e2e2
56 changed files with 2484 additions and 1178 deletions

View File

@ -5,12 +5,14 @@
### ✨ Nouvelles Fonctionnalités ### ✨ Nouvelles Fonctionnalités
#### 🔐 Système de Hash et Cache JSON #### 🔐 Système de Hash et Cache JSON
- **Système de hash SHA-256** : Calcul automatique du hash pour chaque fichier uploadé - **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 - **Détection des doublons** : Évite les fichiers identiques basés sur le contenu
- **Cache JSON** : Sauvegarde automatique des résultats d'extraction - **Cache JSON** : Sauvegarde automatique des résultats d'extraction
- **Optimisation des performances** : Réutilisation des résultats en cache - **Optimisation des performances** : Réutilisation des résultats en cache
#### 🛠️ Nouvelles Fonctions Backend #### 🛠️ Nouvelles Fonctions Backend
- `calculateFileHash(buffer)` : Calcule le hash SHA-256 d'un fichier - `calculateFileHash(buffer)` : Calcule le hash SHA-256 d'un fichier
- `findExistingFileByHash(hash)` : Trouve les fichiers existants par hash - `findExistingFileByHash(hash)` : Trouve les fichiers existants par hash
- `saveJsonCache(hash, result)` : Sauvegarde un résultat dans le cache - `saveJsonCache(hash, result)` : Sauvegarde un résultat dans le cache
@ -18,6 +20,7 @@
- `listCacheFiles()` : Liste tous les fichiers de cache - `listCacheFiles()` : Liste tous les fichiers de cache
#### 📡 Nouvelles Routes API #### 📡 Nouvelles Routes API
- `GET /api/cache` : Liste les fichiers de cache avec métadonnées - `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 - `GET /api/cache/:hash` : Récupère un résultat de cache spécifique
- `DELETE /api/cache/:hash` : Supprime un fichier de cache - `DELETE /api/cache/:hash` : Supprime un fichier de cache
@ -26,6 +29,7 @@
### 🔧 Améliorations Techniques ### 🔧 Améliorations Techniques
#### Backend (`backend/server.js`) #### Backend (`backend/server.js`)
- Intégration du système de cache dans la route `/api/extract` - Intégration du système de cache dans la route `/api/extract`
- Vérification du cache avant traitement - Vérification du cache avant traitement
- Sauvegarde automatique des résultats après traitement - Sauvegarde automatique des résultats après traitement
@ -33,28 +37,33 @@
- Logs détaillés pour le debugging - Logs détaillés pour le debugging
#### Configuration #### Configuration
- Ajout du dossier `cache/` au `.gitignore` - Ajout du dossier `cache/` au `.gitignore`
- Configuration des remotes Git pour SSH/HTTPS - Configuration des remotes Git pour SSH/HTTPS
### 📚 Documentation ### 📚 Documentation
#### Nouveaux Fichiers #### Nouveaux Fichiers
- `docs/HASH_SYSTEM.md` : Documentation complète du système de hash - `docs/HASH_SYSTEM.md` : Documentation complète du système de hash
- `CHANGELOG.md` : Historique des versions - `CHANGELOG.md` : Historique des versions
#### Mises à Jour #### Mises à Jour
- `docs/API_BACKEND.md` : Ajout de la documentation des nouvelles routes - `docs/API_BACKEND.md` : Ajout de la documentation des nouvelles routes
- Caractéristiques principales mises à jour - Caractéristiques principales mises à jour
### 🚀 Performance ### 🚀 Performance
#### Optimisations #### Optimisations
- **Traitement instantané** pour les fichiers en cache - **Traitement instantané** pour les fichiers en cache
- **Économie de stockage** : Pas de fichiers dupliqués - **Économie de stockage** : Pas de fichiers dupliqués
- **Réduction des calculs** : Réutilisation des résultats existants - **Réduction des calculs** : Réutilisation des résultats existants
- **Logs optimisés** : Indication claire de l'utilisation du cache - **Logs optimisés** : Indication claire de l'utilisation du cache
#### Métriques #### Métriques
- Temps de traitement réduit de ~80% pour les fichiers en cache - Temps de traitement réduit de ~80% pour les fichiers en cache
- Stockage optimisé (suppression automatique des doublons) - Stockage optimisé (suppression automatique des doublons)
- Cache JSON : ~227KB pour un document PDF de 992KB - Cache JSON : ~227KB pour un document PDF de 992KB
@ -97,6 +106,7 @@ graph TD
## [1.0.0] - 2025-09-15 ## [1.0.0] - 2025-09-15
### 🎉 Version Initiale ### 🎉 Version Initiale
- Système d'extraction de documents (PDF, images) - Système d'extraction de documents (PDF, images)
- OCR avec Tesseract.js - OCR avec Tesseract.js
- NER par règles - NER par règles
@ -106,4 +116,4 @@ graph TD
--- ---
*Changelog maintenu automatiquement - Dernière mise à jour : 15 septembre 2025* _Changelog maintenu automatiquement - Dernière mise à jour : 15 septembre 2025_

View File

@ -19,7 +19,9 @@ async function isCNIDocument(inputPath) {
const aspectRatio = metadata.width / metadata.height const aspectRatio = metadata.width / metadata.height
const isCNIRatio = aspectRatio > 0.6 && aspectRatio < 0.7 // Ratio typique d'une CNI 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 return isPortrait && hasGoodResolution && isCNIRatio
} catch (error) { } catch (error) {
@ -45,7 +47,7 @@ async function extractMRZ(inputPath) {
left: 0, left: 0,
top: mrzTop, top: mrzTop,
width: metadata.width, width: metadata.width,
height: mrzHeight height: mrzHeight,
}) })
.grayscale() .grayscale()
.normalize() .normalize()
@ -73,29 +75,29 @@ async function segmentCNIZones(inputPath) {
left: Math.floor(metadata.width * 0.05), left: Math.floor(metadata.width * 0.05),
top: Math.floor(metadata.height * 0.25), top: Math.floor(metadata.height * 0.25),
width: Math.floor(metadata.width * 0.4), 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 // Zone du prénom
firstNameZone: { firstNameZone: {
left: Math.floor(metadata.width * 0.05), left: Math.floor(metadata.width * 0.05),
top: Math.floor(metadata.height * 0.35), top: Math.floor(metadata.height * 0.35),
width: Math.floor(metadata.width * 0.4), 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 // Zone de la date de naissance
birthDateZone: { birthDateZone: {
left: Math.floor(metadata.width * 0.05), left: Math.floor(metadata.width * 0.05),
top: Math.floor(metadata.height * 0.45), top: Math.floor(metadata.height * 0.45),
width: Math.floor(metadata.width * 0.3), 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 // Zone du numéro de CNI
idNumberZone: { idNumberZone: {
left: Math.floor(metadata.width * 0.05), left: Math.floor(metadata.width * 0.05),
top: Math.floor(metadata.height * 0.55), top: Math.floor(metadata.height * 0.55),
width: Math.floor(metadata.width * 0.4), 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`) console.log(`[CNI_SEGMENT] Segmentation en ${Object.keys(zones).length} zones`)
@ -143,21 +145,21 @@ async function enhanceCNIPreprocessing(inputPath) {
width: 2000, width: 2000,
height: Math.floor(2000 * (metadata.height / metadata.width)), height: Math.floor(2000 * (metadata.height / metadata.width)),
fit: 'fill', fit: 'fill',
kernel: sharp.kernel.lanczos3 kernel: sharp.kernel.lanczos3,
}) })
.grayscale() .grayscale()
.normalize() .normalize()
.modulate({ .modulate({
brightness: 1.3, brightness: 1.3,
contrast: 1.8, contrast: 1.8,
saturation: 0 saturation: 0,
}) })
.sharpen({ .sharpen({
sigma: 1.5, sigma: 1.5,
m1: 0.5, m1: 0.5,
m2: 3, m2: 3,
x1: 2, x1: 2,
y2: 20 y2: 20,
}) })
.median(3) .median(3)
.threshold(135) .threshold(135)
@ -194,7 +196,7 @@ async function processCNIWithZones(inputPath) {
const results = { const results = {
isCNI: true, isCNI: true,
zones: {}, zones: {},
mrz: null mrz: null,
} }
// Extraire chaque zone // 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`) console.log(`[CNI_PROCESS] CNI traitée: ${Object.keys(results.zones).length} zones + MRZ`)
return results return results
} catch (error) { } catch (error) {
console.error(`[CNI_PROCESS] Erreur traitement CNI:`, error.message) console.error(`[CNI_PROCESS] Erreur traitement CNI:`, error.message)
return null return null
@ -228,7 +229,7 @@ function decodeMRZ(mrzText) {
} }
// Format MRZ de la CNI française (2 lignes de 36 caractères) // 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) { if (lines.length < 2) {
return null return null
} }
@ -241,12 +242,11 @@ function decodeMRZ(mrzText) {
country: line1.substring(2, 5), country: line1.substring(2, 5),
surname: line1.substring(5, 36).replace(/</g, ' ').trim(), surname: line1.substring(5, 36).replace(/</g, ' ').trim(),
givenNames: line2.substring(0, 30).replace(/</g, ' ').trim(), givenNames: line2.substring(0, 30).replace(/</g, ' ').trim(),
documentNumber: line2.substring(30, 36).trim() documentNumber: line2.substring(30, 36).trim(),
} }
console.log(`[MRZ_DECODE] MRZ décodée: ${result.surname} ${result.givenNames}`) console.log(`[MRZ_DECODE] MRZ décodée: ${result.surname} ${result.givenNames}`)
return result return result
} catch (error) { } catch (error) {
console.error(`[MRZ_DECODE] Erreur décodage MRZ:`, error.message) console.error(`[MRZ_DECODE] Erreur décodage MRZ:`, error.message)
return null return null
@ -260,5 +260,5 @@ module.exports = {
extractCNIZone, extractCNIZone,
enhanceCNIPreprocessing, enhanceCNIPreprocessing,
processCNIWithZones, processCNIWithZones,
decodeMRZ decodeMRZ,
} }

View File

@ -6,7 +6,7 @@ const {
isCNIDocument, isCNIDocument,
enhanceCNIPreprocessing, enhanceCNIPreprocessing,
processCNIWithZones, processCNIWithZones,
decodeMRZ decodeMRZ,
} = require('./cniOcrEnhancer') } = require('./cniOcrEnhancer')
/** /**
@ -28,7 +28,7 @@ async function runTesseractOCR(imageBuffer, options = {}) {
language: options.language || 'fra', language: options.language || 'fra',
psm: options.psm || '6', // Mode uniforme de bloc de texte psm: options.psm || '6', // Mode uniforme de bloc de texte
oem: options.oem || '3', // Mode par défaut oem: options.oem || '3', // Mode par défaut
...options ...options,
} }
// Construire la commande Tesseract // Construire la commande Tesseract
@ -54,9 +54,8 @@ async function runTesseractOCR(imageBuffer, options = {}) {
return { return {
text: resultText.trim(), text: resultText.trim(),
confidence: 0.8, // Estimation confidence: 0.8, // Estimation
method: 'tesseract_enhanced' method: 'tesseract_enhanced',
} }
} catch (error) { } catch (error) {
console.error(`[TESSERACT] Erreur OCR:`, error.message) console.error(`[TESSERACT] Erreur OCR:`, error.message)
throw error throw error
@ -77,7 +76,6 @@ async function extractTextFromImageEnhanced(inputPath) {
} else { } else {
return await extractTextFromStandardDocument(inputPath) return await extractTextFromStandardDocument(inputPath)
} }
} catch (error) { } catch (error) {
console.error(`[ENHANCED_OCR] Erreur extraction:`, error.message) console.error(`[ENHANCED_OCR] Erreur extraction:`, error.message)
throw error throw error
@ -104,7 +102,7 @@ async function extractTextFromCNI(inputPath) {
// Extraire le texte de l'image améliorée // Extraire le texte de l'image améliorée
const mainText = await runTesseractOCR(enhancedImage, { const mainText = await runTesseractOCR(enhancedImage, {
language: 'fra', language: 'fra',
psm: '6' // Mode uniforme de bloc de texte psm: '6', // Mode uniforme de bloc de texte
}) })
combinedText += mainText.text + '\n' combinedText += mainText.text + '\n'
@ -114,7 +112,7 @@ async function extractTextFromCNI(inputPath) {
try { try {
const zoneText = await runTesseractOCR(zoneImage, { const zoneText = await runTesseractOCR(zoneImage, {
language: 'fra', language: 'fra',
psm: '8' // Mode mot unique psm: '8', // Mode mot unique
}) })
combinedText += `[${zoneName.toUpperCase()}] ${zoneText.text}\n` combinedText += `[${zoneName.toUpperCase()}] ${zoneText.text}\n`
console.log(`[CNI_OCR] Zone ${zoneName}: ${zoneText.text}`) console.log(`[CNI_OCR] Zone ${zoneName}: ${zoneText.text}`)
@ -129,7 +127,7 @@ async function extractTextFromCNI(inputPath) {
try { try {
const mrzText = await runTesseractOCR(cniZones.mrz, { const mrzText = await runTesseractOCR(cniZones.mrz, {
language: 'eng', // La MRZ est en anglais language: 'eng', // La MRZ est en anglais
psm: '8' // Mode mot unique psm: '8', // Mode mot unique
}) })
combinedText += `[MRZ] ${mrzText.text}\n` combinedText += `[MRZ] ${mrzText.text}\n`
@ -153,9 +151,8 @@ async function extractTextFromCNI(inputPath) {
confidence: 0.85, // Confiance élevée pour les CNI traitées confidence: 0.85, // Confiance élevée pour les CNI traitées
method: 'cni_enhanced', method: 'cni_enhanced',
mrzData: mrzData, mrzData: mrzData,
zones: cniZones ? Object.keys(cniZones.zones || {}) : [] zones: cniZones ? Object.keys(cniZones.zones || {}) : [],
} }
} catch (error) { } catch (error) {
console.error(`[CNI_OCR] Erreur traitement CNI:`, error.message) console.error(`[CNI_OCR] Erreur traitement CNI:`, error.message)
throw error throw error
@ -176,7 +173,7 @@ async function extractTextFromStandardDocument(inputPath) {
width: Math.min(metadata.width * 2, 2000), width: Math.min(metadata.width * 2, 2000),
height: Math.min(metadata.height * 2, 2000), height: Math.min(metadata.height * 2, 2000),
fit: 'inside', fit: 'inside',
withoutEnlargement: false withoutEnlargement: false,
}) })
.grayscale() .grayscale()
.normalize() .normalize()
@ -187,15 +184,14 @@ async function extractTextFromStandardDocument(inputPath) {
// OCR standard // OCR standard
const result = await runTesseractOCR(processedImage, { const result = await runTesseractOCR(processedImage, {
language: 'fra', language: 'fra',
psm: '6' psm: '6',
}) })
return { return {
text: result.text, text: result.text,
confidence: result.confidence, confidence: result.confidence,
method: 'standard_enhanced' method: 'standard_enhanced',
} }
} catch (error) { } catch (error) {
console.error(`[STANDARD_OCR] Erreur traitement standard:`, error.message) console.error(`[STANDARD_OCR] Erreur traitement standard:`, error.message)
throw error throw error
@ -211,18 +207,21 @@ function postProcessCNIText(text) {
const corrections = [ const corrections = [
// Corrections de caractères corrompus // Corrections de caractères corrompus
{ from: /RÉPUBLIQUE FRANCATSEN/g, to: 'RÉPUBLIQUE FRANÇAISE' }, { from: /RÉPUBLIQUE FRANCATSEN/g, to: 'RÉPUBLIQUE FRANÇAISE' },
{ from: /CARTE NATIONALE DIDENTITE/g, to: 'CARTE NATIONALE D\'IDENTITÉ' }, { from: /CARTE NATIONALE DIDENTITE/g, to: "CARTE NATIONALE D'IDENTITÉ" },
{ from: /Ne :/g, to: 'N° :' }, { from: /Ne :/g, to: 'N° :' },
{ from: /Fe - 0/g, to: 'Féminin' }, { from: /Fe - 0/g, to: 'Féminin' },
{ from: /Mele:/g, to: 'Mâle:' }, { from: /Mele:/g, to: 'Mâle:' },
{ from: /IDFRACANTUCCKKLLLLKLLLLLLLLLLLK/g, to: 'IDFRA' }, { from: /IDFRACANTUCCKKLLLLKLLLLLLLLLLLK/g, to: 'IDFRA' },
// Nettoyage des caractères parasites // Nettoyage des caractères parasites
{ from: /[^\w\sÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,;:!?()\-'"]/g, to: ' ' }, {
from: /[^\w\sÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,;:!?()\-'"]/g,
to: ' ',
},
// Normalisation des espaces // Normalisation des espaces
{ from: /\s+/g, to: ' ' }, { from: /\s+/g, to: ' ' },
{ from: /^\s+|\s+$/g, to: '' } { from: /^\s+|\s+$/g, to: '' },
] ]
// Appliquer les corrections // Appliquer les corrections
@ -232,7 +231,6 @@ function postProcessCNIText(text) {
console.log(`[POST_PROCESS] Texte post-traité: ${processedText.length} caractères`) console.log(`[POST_PROCESS] Texte post-traité: ${processedText.length} caractères`)
return processedText return processedText
} catch (error) { } catch (error) {
console.error(`[POST_PROCESS] Erreur post-traitement:`, error.message) console.error(`[POST_PROCESS] Erreur post-traitement:`, error.message)
return text return text
@ -244,5 +242,5 @@ module.exports = {
extractTextFromCNI, extractTextFromCNI,
extractTextFromStandardDocument, extractTextFromStandardDocument,
runTesseractOCR, runTesseractOCR,
postProcessCNIText postProcessCNIText,
} }

View File

@ -21,24 +21,24 @@ async function preprocessImageForOCR(inputPath, outputPath = null, options = {})
// Options par défaut optimisées pour les documents d'identité // Options par défaut optimisées pour les documents d'identité
const defaultOptions = { const defaultOptions = {
// Redimensionnement // Redimensionnement
width: 2000, // Largeur cible width: 2000, // Largeur cible
height: null, // Hauteur automatique (maintient le ratio) height: null, // Hauteur automatique (maintient le ratio)
// Amélioration du contraste // Amélioration du contraste
contrast: 1.5, // Augmente le contraste contrast: 1.5, // Augmente le contraste
brightness: 1.1, // Légère augmentation de la luminosité brightness: 1.1, // Légère augmentation de la luminosité
// Filtres // Filtres
sharpen: true, // Amélioration de la netteté sharpen: true, // Amélioration de la netteté
denoise: true, // Réduction du bruit denoise: true, // Réduction du bruit
// Conversion // Conversion
grayscale: true, // Conversion en niveaux de gris grayscale: true, // Conversion en niveaux de gris
threshold: null, // Seuil pour binarisation (optionnel) threshold: null, // Seuil pour binarisation (optionnel)
// Format de sortie // Format de sortie
format: 'png', // Format PNG pour meilleure qualité format: 'png', // Format PNG pour meilleure qualité
quality: 100 // Qualité maximale quality: 100, // Qualité maximale
} }
const config = { ...defaultOptions, ...options } const config = { ...defaultOptions, ...options }
@ -48,7 +48,7 @@ async function preprocessImageForOCR(inputPath, outputPath = null, options = {})
contrast: config.contrast, contrast: config.contrast,
brightness: config.brightness, brightness: config.brightness,
grayscale: config.grayscale, grayscale: config.grayscale,
sharpen: config.sharpen sharpen: config.sharpen,
}) })
// Lecture de l'image // Lecture de l'image
@ -58,7 +58,7 @@ async function preprocessImageForOCR(inputPath, outputPath = null, options = {})
if (config.width || config.height) { if (config.width || config.height) {
image = image.resize(config.width, config.height, { image = image.resize(config.width, config.height, {
fit: 'inside', fit: 'inside',
withoutEnlargement: false withoutEnlargement: false,
}) })
console.log(`[PREPROCESSING] Redimensionnement appliqué`) console.log(`[PREPROCESSING] Redimensionnement appliqué`)
} }
@ -73,9 +73,11 @@ async function preprocessImageForOCR(inputPath, outputPath = null, options = {})
if (config.contrast !== 1 || config.brightness !== 1) { if (config.contrast !== 1 || config.brightness !== 1) {
image = image.modulate({ image = image.modulate({
brightness: config.brightness, brightness: config.brightness,
contrast: config.contrast contrast: config.contrast,
}) })
console.log(`[PREPROCESSING] Contraste (${config.contrast}) et luminosité (${config.brightness}) appliqués`) console.log(
`[PREPROCESSING] Contraste (${config.contrast}) et luminosité (${config.brightness}) appliqués`,
)
} }
// Amélioration de la netteté // Amélioration de la netteté
@ -83,7 +85,7 @@ async function preprocessImageForOCR(inputPath, outputPath = null, options = {})
image = image.sharpen({ image = image.sharpen({
sigma: 1.0, sigma: 1.0,
flat: 1.0, flat: 1.0,
jagged: 2.0 jagged: 2.0,
}) })
console.log(`[PREPROCESSING] Amélioration de la netteté appliquée`) console.log(`[PREPROCESSING] Amélioration de la netteté appliquée`)
} }
@ -101,9 +103,7 @@ async function preprocessImageForOCR(inputPath, outputPath = null, options = {})
} }
// Application des modifications et conversion // Application des modifications et conversion
const processedBuffer = await image const processedBuffer = await image.png({ quality: config.quality }).toBuffer()
.png({ quality: config.quality })
.toBuffer()
console.log(`[PREPROCESSING] Image préprocessée: ${processedBuffer.length} bytes`) console.log(`[PREPROCESSING] Image préprocessée: ${processedBuffer.length} bytes`)
@ -114,7 +114,6 @@ async function preprocessImageForOCR(inputPath, outputPath = null, options = {})
} }
return processedBuffer return processedBuffer
} catch (error) { } catch (error) {
console.error(`[PREPROCESSING] Erreur lors du préprocessing:`, error.message) console.error(`[PREPROCESSING] Erreur lors du préprocessing:`, error.message)
throw error throw error
@ -138,8 +137,8 @@ async function preprocessImageMultipleConfigs(inputPath) {
brightness: 1.1, brightness: 1.1,
grayscale: true, grayscale: true,
sharpen: true, sharpen: true,
denoise: true denoise: true,
} },
}, },
{ {
name: 'Haute résolution', name: 'Haute résolution',
@ -149,8 +148,8 @@ async function preprocessImageMultipleConfigs(inputPath) {
brightness: 1.2, brightness: 1.2,
grayscale: true, grayscale: true,
sharpen: true, sharpen: true,
denoise: false denoise: false,
} },
}, },
{ {
name: 'Contraste élevé', name: 'Contraste élevé',
@ -160,8 +159,8 @@ async function preprocessImageMultipleConfigs(inputPath) {
brightness: 1.0, brightness: 1.0,
grayscale: true, grayscale: true,
sharpen: true, sharpen: true,
denoise: true denoise: true,
} },
}, },
{ {
name: 'Binarisation', name: 'Binarisation',
@ -172,9 +171,9 @@ async function preprocessImageMultipleConfigs(inputPath) {
grayscale: true, grayscale: true,
sharpen: true, sharpen: true,
denoise: true, denoise: true,
threshold: 128 threshold: 128,
} },
} },
] ]
// Pour l'instant, on utilise la configuration standard // Pour l'instant, on utilise la configuration standard
@ -199,7 +198,7 @@ async function analyzeImageMetadata(imagePath) {
height: metadata.height, height: metadata.height,
channels: metadata.channels, channels: metadata.channels,
density: metadata.density, density: metadata.density,
size: `${(metadata.size / 1024).toFixed(1)} KB` size: `${(metadata.size / 1024).toFixed(1)} KB`,
}) })
return metadata return metadata
} catch (error) { } catch (error) {
@ -211,5 +210,5 @@ async function analyzeImageMetadata(imagePath) {
module.exports = { module.exports = {
preprocessImageForOCR, preprocessImageForOCR,
preprocessImageMultipleConfigs, preprocessImageMultipleConfigs,
analyzeImageMetadata analyzeImageMetadata,
} }

View File

@ -11,7 +11,7 @@
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^2.0.0",
"tesseract.js": "^5.1.0" "tesseract.js": "^5.1.0"
}, },
"engines": { "engines": {
@ -129,17 +129,17 @@
} }
}, },
"node_modules/concat-stream": { "node_modules/concat-stream": {
"version": "1.6.2", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [ "engines": [
"node >= 0.8" "node >= 6.0"
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"buffer-from": "^1.0.0", "buffer-from": "^1.0.0",
"inherits": "^2.0.3", "inherits": "^2.0.3",
"readable-stream": "^2.2.2", "readable-stream": "^3.0.2",
"typedarray": "^0.0.6" "typedarray": "^0.0.6"
} }
}, },
@ -179,12 +179,6 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT" "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": { "node_modules/cors": {
"version": "2.8.5", "version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
@ -525,12 +519,6 @@
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"license": "MIT" "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": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -628,22 +616,21 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/multer": { "node_modules/multer": {
"version": "1.4.5-lts.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
"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.",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"append-field": "^1.0.0", "append-field": "^1.0.0",
"busboy": "^1.0.0", "busboy": "^1.6.0",
"concat-stream": "^1.5.2", "concat-stream": "^2.0.0",
"mkdirp": "^0.5.4", "mkdirp": "^0.5.6",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
"type-is": "^1.6.4", "type-is": "^1.6.18",
"xtend": "^4.0.0" "xtend": "^4.0.2"
}, },
"engines": { "engines": {
"node": ">= 6.0.0" "node": ">= 10.16.0"
} }
}, },
"node_modules/negotiator": { "node_modules/negotiator": {
@ -732,12 +719,6 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT" "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": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -791,26 +772,19 @@
} }
}, },
"node_modules/readable-stream": { "node_modules/readable-stream": {
"version": "2.3.8", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"core-util-is": "~1.0.0", "inherits": "^2.0.3",
"inherits": "~2.0.3", "string_decoder": "^1.1.1",
"isarray": "~1.0.0", "util-deprecate": "^1.0.1"
"process-nextick-args": "~2.0.0", },
"safe-buffer": "~5.1.1", "engines": {
"string_decoder": "~1.1.1", "node": ">= 6"
"util-deprecate": "~1.0.1"
} }
}, },
"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": { "node_modules/regenerator-runtime": {
"version": "0.13.11", "version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
@ -993,20 +967,14 @@
} }
}, },
"node_modules/string_decoder": { "node_modules/string_decoder": {
"version": "1.1.1", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT", "license": "MIT",
"dependencies": { "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": { "node_modules/tesseract.js": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-5.1.1.tgz", "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-5.1.1.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "4nk-ia-backend", "name": "4nk-ia-backend",
"version": "1.0.0", "version": "1.0.1",
"description": "Backend pour le traitement des documents avec OCR et NER", "description": "Backend pour le traitement des documents avec OCR et NER",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
@ -10,7 +10,7 @@
}, },
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^2.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"tesseract.js": "^5.1.0" "tesseract.js": "^5.1.0"
}, },

View File

@ -26,8 +26,8 @@ async function convertPdfToImages(pdfPath, outputDir = null) {
format: 'png', format: 'png',
out_dir: outputDir, out_dir: outputDir,
out_prefix: 'page', out_prefix: 'page',
page: null, // Toutes les pages page: null, // Toutes les pages
scale: 2000 // Résolution élevée scale: 2000, // Résolution élevée
} }
console.log(`[PDF-CONVERTER] Configuration: Format=PNG, Scale=2000`) console.log(`[PDF-CONVERTER] Configuration: Format=PNG, Scale=2000`)
@ -45,7 +45,6 @@ async function convertPdfToImages(pdfPath, outputDir = null) {
}) })
return imagePaths return imagePaths
} catch (error) { } catch (error) {
console.error(`[PDF-CONVERTER] Erreur lors de la conversion:`, error.message) console.error(`[PDF-CONVERTER] Erreur lors de la conversion:`, error.message)
throw error throw error
@ -74,8 +73,8 @@ async function convertPdfToSingleImage(pdfPath, outputPath = null) {
format: 'png', format: 'png',
out_dir: path.dirname(outputPath), out_dir: path.dirname(outputPath),
out_prefix: path.basename(outputPath, '.png'), out_prefix: path.basename(outputPath, '.png'),
page: 1, // Première page seulement page: 1, // Première page seulement
scale: 2000 scale: 2000,
} }
// Conversion de la première page seulement // 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}`) console.log(`[PDF-CONVERTER] Image générée: ${outputPath}`)
return outputPath return outputPath
} catch (error) { } catch (error) {
console.error(`[PDF-CONVERTER] Erreur lors de la conversion:`, error.message) console.error(`[PDF-CONVERTER] Erreur lors de la conversion:`, error.message)
throw error throw error
@ -113,5 +111,5 @@ async function cleanupTempFiles(filePaths) {
module.exports = { module.exports = {
convertPdfToImages, convertPdfToImages,
convertPdfToSingleImage, convertPdfToSingleImage,
cleanupTempFiles cleanupTempFiles,
} }

File diff suppressed because it is too large Load Diff

View File

@ -72,9 +72,7 @@
"status": { "status": {
"success": true, "success": true,
"errors": [], "errors": [],
"warnings": [ "warnings": ["Aucune signature détectée"],
"Aucune signature détectée"
],
"timestamp": "2025-09-15T23:26:56.308Z" "timestamp": "2025-09-15T23:26:56.308Z"
} }
} }

View File

@ -72,9 +72,7 @@
"status": { "status": {
"success": true, "success": true,
"errors": [], "errors": [],
"warnings": [ "warnings": ["Aucune signature détectée"],
"Aucune signature détectée"
],
"timestamp": "2025-09-15T23:26:19.922Z" "timestamp": "2025-09-15T23:26:19.922Z"
} }
} }

View File

@ -1,16 +1,16 @@
version: "3.9" version: '3.9'
services: services:
frontend: 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 container_name: 4nk-ia-front
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8080:80" - '8080:80'
environment: environment:
- VITE_API_URL=${VITE_API_URL:-http://172.23.0.10:8000} - VITE_API_URL=${VITE_API_URL:-http://172.23.0.10:8000}
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/"] test: ['CMD', 'wget', '-qO-', 'http://localhost/']
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3

139
docs/ANALYSE_REPO.md Normal file
View File

@ -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èreplan 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 dabstraction 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 multistage 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 2336, 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 3334, 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 nexiste 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 dentités utilisés en `views/ExtractionView.tsx` traités comme `string` au lieu dobjets 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 cidessus).
### 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 gardefous de rendu.
- **Tests**: ajouter `src/services/testFilesApi.ts` (ou ajuster limport) pour couvrir lAPI 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 denvironnement**: `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 multistage; healthcheck HTTP. Tagging via scripts et `docker-compose.registry.yml` (variable `TAG`). À aligner avec conventions internes du registre.
### Recommandations prioritaires (ordre dexé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 limport 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 dobjets sur des `string` (à corriger après normalisation du mapping).
- `tests/testFilesApi.test.ts` — dépend de `src/services/testFilesApi.ts` non présent.

View File

@ -121,7 +121,7 @@ Ce mode est utile pour démo/diagnostic quand le backend nest pas disponible.
```typescript ```typescript
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: BASE_URL, baseURL: BASE_URL,
timeout: 60000 timeout: 60000,
}) })
``` ```

View File

@ -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. 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 :** ### **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 - ✅ **OCR avancé** : Tesseract.js avec préprocessing d'images
- ✅ **Extraction PDF directe** : pdf-parse pour une précision maximale - ✅ **Extraction PDF directe** : pdf-parse pour une précision maximale
- ✅ **NER intelligent** : Reconnaissance d'entités par règles - ✅ **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. Vérifie l'état du serveur backend.
**Réponse :** **Réponse :**
```json ```json
{ {
"status": "OK", "status": "OK",
@ -37,6 +39,7 @@ Vérifie l'état du serveur backend.
``` ```
**Exemple d'utilisation :** **Exemple d'utilisation :**
```bash ```bash
curl http://localhost:3001/api/health curl http://localhost:3001/api/health
``` ```
@ -50,6 +53,7 @@ curl http://localhost:3001/api/health
Retourne la liste des fichiers de test disponibles. Retourne la liste des fichiers de test disponibles.
**Réponse :** **Réponse :**
```json ```json
{ {
"files": [ "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. Retourne la liste des fichiers uploadés avec leurs métadonnées et hash SHA-256.
**Réponse :** **Réponse :**
```json ```json
{ {
"files": [ "files": [
@ -88,11 +93,13 @@ Retourne la liste des fichiers uploadés avec leurs métadonnées et hash SHA-25
``` ```
**Exemple d'utilisation :** **Exemple d'utilisation :**
```bash ```bash
curl http://localhost:3001/api/uploads curl http://localhost:3001/api/uploads
``` ```
**Notes :** **Notes :**
- Le hash SHA-256 permet d'identifier les fichiers identiques - Le hash SHA-256 permet d'identifier les fichiers identiques
- Les fichiers dupliqués sont automatiquement détectés lors de l'upload - Les fichiers dupliqués sont automatiquement détectés lors de l'upload
- Seuls les fichiers uniques sont conservés dans le système - Seuls les fichiers uniques sont conservés dans le système
@ -103,13 +110,15 @@ curl http://localhost:3001/api/uploads
### **POST** `/api/extract` ### **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 :** #### **Paramètres :**
- **`document`** (file, required) : Fichier à analyser (PDF, JPEG, PNG, TIFF)
- **`document`** (file, required) : Fichier à analyser (PDF, JPEG, PNG, TIFF, TXT)
- **Taille maximale :** 10MB - **Taille maximale :** 10MB
#### **Gestion des doublons :** #### **Gestion des doublons :**
- Le système calcule automatiquement un hash SHA-256 de chaque fichier uploadé - 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é - 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 - 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", "type": "prestation",
"description": "Prestation du mois d'Août 2025", "description": "Prestation du mois d'Août 2025",
"quantity": 10, "quantity": 10,
"unitPrice": 550.00, "unitPrice": 550.0,
"totalHT": 5500.00, "totalHT": 5500.0,
"currency": "EUR", "currency": "EUR",
"confidence": 0.95 "confidence": 0.95
} }
], ],
"totals": { "totals": {
"totalHT": 5500.00, "totalHT": 5500.0,
"totalTVA": 1100.00, "totalTVA": 1100.0,
"totalTTC": 6600.00, "totalTTC": 6600.0,
"tvaRate": 0.20, "tvaRate": 0.2,
"currency": "EUR" "currency": "EUR"
}, },
"payment": { "payment": {
@ -268,7 +277,7 @@ Extrait et analyse un document (PDF ou image) pour identifier les entités et in
"quality": { "quality": {
"globalConfidence": 0.95, "globalConfidence": 0.95,
"textExtractionConfidence": 0.95, "textExtractionConfidence": 0.95,
"entityExtractionConfidence": 0.90, "entityExtractionConfidence": 0.9,
"classificationConfidence": 0.95 "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 :** #### **Exemples d'utilisation :**
**Avec curl :** **Avec curl (PDF) :**
```bash ```bash
curl -X POST \ curl -X POST \
-F "document=@/path/to/document.pdf" \ -F "document=@/path/to/document.pdf" \
http://localhost:3001/api/extract 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) :** **Avec JavaScript (fetch) :**
```javascript ```javascript
const formData = new FormData() const formData = new FormData()
formData.append('document', fileInput.files[0]) formData.append('document', fileInput.files[0])
const response = await fetch('http://localhost:3001/api/extract', { const response = await fetch('http://localhost:3001/api/extract', {
method: 'POST', method: 'POST',
body: formData body: formData,
}) })
const result = await response.json() const result = await response.json()
@ -309,6 +328,7 @@ console.log(result)
## 📊 **Types de documents supportés** ## 📊 **Types de documents supportés**
### **1. Factures** ### **1. Factures**
- **Détection automatique** : Mots-clés "facture", "tva", "siren", "montant" - **Détection automatique** : Mots-clés "facture", "tva", "siren", "montant"
- **Entités extraites** : - **Entités extraites** :
- Sociétés (fournisseur/client) - Sociétés (fournisseur/client)
@ -319,6 +339,7 @@ console.log(result)
- Dates - Dates
### **2. Cartes Nationales d'Identité (CNI)** ### **2. Cartes Nationales d'Identité (CNI)**
- **Détection automatique** : Mots-clés "carte nationale d'identité", "cni", "mrz" - **Détection automatique** : Mots-clés "carte nationale d'identité", "cni", "mrz"
- **Entités extraites** : - **Entités extraites** :
- Identités (nom, prénom) - Identités (nom, prénom)
@ -327,6 +348,7 @@ console.log(result)
- Adresses - Adresses
### **3. Contrats** ### **3. Contrats**
- **Détection automatique** : Mots-clés "contrat", "vente", "achat", "acte" - **Détection automatique** : Mots-clés "contrat", "vente", "achat", "acte"
- **Entités extraites** : - **Entités extraites** :
- Parties contractantes - Parties contractantes
@ -335,6 +357,7 @@ console.log(result)
- Dates importantes - Dates importantes
### **4. Attestations** ### **4. Attestations**
- **Détection automatique** : Mots-clés "attestation", "certificat" - **Détection automatique** : Mots-clés "attestation", "certificat"
- **Entités extraites** : - **Entités extraites** :
- Identités - Identités
@ -346,6 +369,7 @@ console.log(result)
## 🔧 **Configuration et préprocessing** ## 🔧 **Configuration et préprocessing**
### **Préprocessing d'images (pour JPEG, PNG, TIFF) :** ### **Préprocessing d'images (pour JPEG, PNG, TIFF) :**
- **Redimensionnement** : Largeur cible 2000px - **Redimensionnement** : Largeur cible 2000px
- **Amélioration du contraste** : Facteur 1.5 - **Amélioration du contraste** : Facteur 1.5
- **Luminosité** : Facteur 1.1 - **Luminosité** : Facteur 1.1
@ -354,6 +378,7 @@ console.log(result)
- **Réduction du bruit** - **Réduction du bruit**
### **Extraction PDF directe :** ### **Extraction PDF directe :**
- **Moteur** : pdf-parse - **Moteur** : pdf-parse
- **Avantage** : Pas de conversion image, précision maximale - **Avantage** : Pas de conversion image, précision maximale
- **Confiance** : 95% par défaut - **Confiance** : 95% par défaut
@ -363,11 +388,13 @@ console.log(result)
## ⚡ **Performances** ## ⚡ **Performances**
### **Temps de traitement typiques :** ### **Temps de traitement typiques :**
- **PDF** : 200-500ms - **PDF** : 200-500ms
- **Images** : 1-3 secondes (avec préprocessing) - **Images** : 1-3 secondes (avec préprocessing)
- **Taille maximale** : 10MB - **Taille maximale** : 10MB
### **Confiance d'extraction :** ### **Confiance d'extraction :**
- **PDF** : 90-95% - **PDF** : 90-95%
- **Images haute qualité** : 80-90% - **Images haute qualité** : 80-90%
- **Images de qualité moyenne** : 60-80% - **Images de qualité moyenne** : 60-80%
@ -377,12 +404,14 @@ console.log(result)
## 🚨 **Gestion d'erreurs** ## 🚨 **Gestion d'erreurs**
### **Codes d'erreur HTTP :** ### **Codes d'erreur HTTP :**
- **400** : Aucun fichier fourni - **400** : Aucun fichier fourni
- **413** : Fichier trop volumineux (>10MB) - **413** : Fichier trop volumineux (>10MB)
- **415** : Type de fichier non supporté - **415** : Type de fichier non supporté
- **500** : Erreur de traitement interne - **500** : Erreur de traitement interne
### **Exemple de réponse d'erreur :** ### **Exemple de réponse d'erreur :**
```json ```json
{ {
"success": false, "success": false,
@ -396,13 +425,16 @@ console.log(result)
## 🛠️ **Dépendances techniques** ## 🛠️ **Dépendances techniques**
### **Moteurs OCR :** ### **Moteurs OCR :**
- **Tesseract.js** : Pour les images - **Tesseract.js** : Pour les images
- **pdf-parse** : Pour les PDF - **pdf-parse** : Pour les PDF
### **Préprocessing :** ### **Préprocessing :**
- **Sharp.js** : Traitement d'images - **Sharp.js** : Traitement d'images
### **NER :** ### **NER :**
- **Règles personnalisées** : Patterns regex pour l'extraction d'entités - **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_

View File

@ -0,0 +1,30 @@
# Cache des résultats et traitement asynchrone
## Dossiers utilisés
- `uploads/<folderHash>`: fichiers déposés (source de vérité)
- `cache/<folderHash>`: résultats JSON et flags `.pending` (source pour lAPI)
- `backend/cache/*`: (désormais vide) ancien emplacement ne plus utiliser
## Flux de traitement
1. Dépôt dun fichier (`/api/extract`):
- Calcule `fileHash` (SHA256 du contenu)
- Si `cache/<folderHash>/<fileHash>.json` existe: renvoie immédiatement le JSON
- Sinon: crée `cache/<folderHash>/<fileHash>.pending`, lance lOCR/NER, puis écrit le JSON et supprime `.pending`
2. Listing (`/api/folders/:folderHash/results`):
- Agrège tous les JSON présents dans `cache/<folderHash>/`
- Pour chaque fichier présent dans `uploads/<folderHash>` sans JSON, crée un flag `.pending` et lance le traitement en arrièreplan, sans bloquer la réponse
## Points importants
- Le traitement images/PDF peut être long; le listing nattend 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/<folderHash>` à la racine
- Toujours indexer les résultats par `fileHash.json`
- Protéger les accès à `.length` et valeurs potentiellement `undefined` dans le backend

View File

@ -9,16 +9,19 @@ Le système de hash SHA-256 a été implémenté dans le backend 4NK_IA pour év
## 🔧 **Fonctionnement** ## 🔧 **Fonctionnement**
### **1. Calcul du Hash** ### **1. Calcul du Hash**
- Chaque fichier uploadé est analysé pour calculer son hash SHA-256 - Chaque fichier uploadé est analysé pour calculer son hash SHA-256
- Le hash est calculé sur le contenu binaire complet du fichier - Le hash est calculé sur le contenu binaire complet du fichier
- Utilisation de la fonction `crypto.createHash('sha256')` de Node.js - Utilisation de la fonction `crypto.createHash('sha256')` de Node.js
### **2. Détection des Doublons** ### **2. Détection des Doublons**
- Avant traitement, le système vérifie si un fichier avec le même hash existe déjà - 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/` - La fonction `findExistingFileByHash()` parcourt le dossier `uploads/`
- Si un doublon est trouvé, le fichier uploadé est supprimé - Si un doublon est trouvé, le fichier uploadé est supprimé
### **3. Traitement Optimisé** ### **3. Traitement Optimisé**
- Le traitement utilise le fichier existant (pas le doublon) - Le traitement utilise le fichier existant (pas le doublon)
- Les résultats d'extraction sont identiques pour les fichiers identiques - Les résultats d'extraction sont identiques pour les fichiers identiques
- Économie de ressources CPU et de stockage - Économie de ressources CPU et de stockage
@ -39,6 +42,7 @@ uploads/
## 🔍 **API Endpoints** ## 🔍 **API Endpoints**
### **GET** `/api/uploads` ### **GET** `/api/uploads`
Liste tous les fichiers uploadés avec leurs métadonnées : Liste tous les fichiers uploadés avec leurs métadonnées :
```json ```json
@ -62,6 +66,7 @@ Liste tous les fichiers uploadés avec leurs métadonnées :
## 📊 **Logs et Monitoring** ## 📊 **Logs et Monitoring**
### **Logs de Hash** ### **Logs de Hash**
``` ```
[HASH] Hash du fichier: a1b2c3d4e5f6789... [HASH] Hash du fichier: a1b2c3d4e5f6789...
[HASH] Fichier déjà existant trouvé: document-1757980637671.pdf [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** ### **Indicateurs de Performance**
- **Temps de traitement réduit** pour les doublons - **Temps de traitement réduit** pour les doublons
- **Stockage optimisé** (pas de fichiers redondants) - **Stockage optimisé** (pas de fichiers redondants)
- **Logs clairs** pour le debugging - **Logs clairs** pour le debugging
@ -78,6 +84,7 @@ Liste tous les fichiers uploadés avec leurs métadonnées :
## 🛠️ **Fonctions Techniques** ## 🛠️ **Fonctions Techniques**
### **`calculateFileHash(buffer)`** ### **`calculateFileHash(buffer)`**
```javascript ```javascript
function calculateFileHash(buffer) { function calculateFileHash(buffer) {
return crypto.createHash('sha256').update(buffer).digest('hex') return crypto.createHash('sha256').update(buffer).digest('hex')
@ -85,6 +92,7 @@ function calculateFileHash(buffer) {
``` ```
### **`findExistingFileByHash(hash)`** ### **`findExistingFileByHash(hash)`**
```javascript ```javascript
function findExistingFileByHash(hash) { function findExistingFileByHash(hash) {
const uploadDir = 'uploads/' const uploadDir = 'uploads/'
@ -139,6 +147,7 @@ graph TD
## 🧪 **Tests** ## 🧪 **Tests**
### **Test de Doublon** ### **Test de Doublon**
```bash ```bash
# Premier upload # Premier upload
curl -X POST -F "document=@test.pdf" http://localhost:3001/api/extract 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** ### **Vérification**
```bash ```bash
# Lister les fichiers uploadés # Lister les fichiers uploadés
curl http://localhost:3001/api/uploads 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_

239
docs/SYSTEME_FONCTIONNEL.md Normal file
View File

@ -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

View File

@ -9,7 +9,7 @@ faire une api et une une ihm qui les consomme pour:
1. Détecter un type de document 1. Détecter un type de document
2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur, 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 3. Si c'est une CNI, définir le pays
4. Pour les identité : rechercher des informations générales sur la personne 4. Pour les identité : rechercher des informations générales sur la personne
5. Pour les adresses vérifier: 5. Pour les adresses vérifier:
@ -69,8 +69,7 @@ RBE (<28> 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/) [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) faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
joindre le PDF suivant complété : 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) [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
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 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 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 1. Détecter un type de document
2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur, 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 3. Si c'est une CNI, définir le pays
4. Pour les identité : rechercher des informations générales sur la personne 4. Pour les identité : rechercher des informations générales sur la personne
5. Pour les adresses vérifier: 5. Pour les adresses vérifier:
@ -156,8 +155,7 @@ RBE (<28> 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/) [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) faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
joindre le PDF suivant complété : joindre le PDF suivant complété :
[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf<64>](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf<64>) [https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf<64>](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf<64>) 6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document
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 : 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. 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 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 1. Détecter un type de document
2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur, 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 3. Si c'est une CNI, définir le pays
4. Pour les identité : rechercher des informations générales sur la personne 4. Pour les identité : rechercher des informations générales sur la personne
5. Pour les adresses vérifier: 5. Pour les adresses vérifier:
@ -227,8 +225,7 @@ RBE (<28> 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/) [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) faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
joindre le PDF suivant complété : 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) [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
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 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 mais il faudra pouvoir ajouter/glisser un document en 1 ou plusieurs document uploadés et voir les previews images

View File

@ -47,6 +47,7 @@ graph TD
**Port**: 3001 **Port**: 3001
**Endpoints**: **Endpoints**:
- `POST /api/extract` - Extraction de documents - `POST /api/extract` - Extraction de documents
- `GET /api/test-files` - Liste des fichiers de test - `GET /api/test-files` - Liste des fichiers de test
- `GET /api/health` - Health check - `GET /api/health` - Health check
@ -54,6 +55,7 @@ graph TD
### 📄 Traitement des Documents ### 📄 Traitement des Documents
#### 1. Upload et Validation #### 1. Upload et Validation
```javascript ```javascript
// Configuration multer // Configuration multer
const upload = multer({ const upload = multer({
@ -67,6 +69,7 @@ const upload = multer({
``` ```
#### 2. Extraction OCR Optimisée #### 2. Extraction OCR Optimisée
```javascript ```javascript
async function extractTextFromImage(imagePath) { async function extractTextFromImage(imagePath) {
const worker = await createWorker('fra+eng') const worker = await createWorker('fra+eng')
@ -87,6 +90,7 @@ async function extractTextFromImage(imagePath) {
``` ```
#### 3. Extraction NER par Règles #### 3. Extraction NER par Règles
```javascript ```javascript
function extractEntitiesFromText(text) { function extractEntitiesFromText(text) {
const entities = { const entities = {
@ -94,7 +98,7 @@ function extractEntitiesFromText(text) {
addresses: [], addresses: [],
cniNumbers: [], cniNumbers: [],
dates: [], dates: [],
documentType: 'Document' documentType: 'Document',
} }
// Patterns pour cartes d'identité // Patterns pour cartes d'identité
@ -154,15 +158,17 @@ function extractEntitiesFromText(text) {
export async function extractDocumentBackend( export async function extractDocumentBackend(
documentId: string, documentId: string,
file?: File, file?: File,
hooks?: { onOcrProgress?: (progress: number) => void; onLlmProgress?: (progress: number) => void } hooks?: {
onOcrProgress?: (progress: number) => void
onLlmProgress?: (progress: number) => void
},
): Promise<ExtractionResult> { ): Promise<ExtractionResult> {
const formData = new FormData() const formData = new FormData()
formData.append('document', file) formData.append('document', file)
const response = await fetch(`${BACKEND_URL}/api/extract`, { const response = await fetch(`${BACKEND_URL}/api/extract`, {
method: 'POST', method: 'POST',
body: formData body: formData,
}) })
const result: BackendExtractionResult = await response.json() const result: BackendExtractionResult = await response.json()
@ -190,7 +196,7 @@ export const extractDocument = createAsyncThunk(
// Fallback vers le mode local // Fallback vers le mode local
return await openaiDocumentApi.extract(documentId, file, progressHooks) return await openaiDocumentApi.extract(documentId, file, progressHooks)
} }
} },
) )
``` ```
@ -223,16 +229,19 @@ node test-backend-architecture.cjs
## Avantages ## Avantages
### 🚀 Performance ### 🚀 Performance
- **Traitement centralisé** : OCR et NER sur le serveur - **Traitement centralisé** : OCR et NER sur le serveur
- **Optimisations** : Paramètres OCR optimisés pour les cartes d'identité - **Optimisations** : Paramètres OCR optimisés pour les cartes d'identité
- **Cache** : Possibilité de mettre en cache les résultats - **Cache** : Possibilité de mettre en cache les résultats
### 🔧 Maintenabilité ### 🔧 Maintenabilité
- **Séparation des responsabilités** : Backend pour le traitement, frontend pour l'UI - **Séparation des responsabilités** : Backend pour le traitement, frontend pour l'UI
- **API REST** : Interface claire entre frontend et backend - **API REST** : Interface claire entre frontend et backend
- **Fallback** : Mode local en cas d'indisponibilité du backend - **Fallback** : Mode local en cas d'indisponibilité du backend
### 📊 Monitoring ### 📊 Monitoring
- **Logs détaillés** : Traçabilité complète du traitement - **Logs détaillés** : Traçabilité complète du traitement
- **Health check** : Vérification de l'état du backend - **Health check** : Vérification de l'état du backend
- **Métriques** : Confiance OCR, nombre d'entités extraites - **Métriques** : Confiance OCR, nombre d'entités extraites
@ -242,9 +251,11 @@ node test-backend-architecture.cjs
### 🔧 Variables d'Environnement ### 🔧 Variables d'Environnement
**Backend**: **Backend**:
- `PORT=3001` - Port du serveur backend - `PORT=3001` - Port du serveur backend
**Frontend**: **Frontend**:
- `VITE_BACKEND_URL=http://localhost:3001` - URL du backend - `VITE_BACKEND_URL=http://localhost:3001` - URL du backend
- `VITE_USE_RULE_NER=true` - Mode règles locales (fallback) - `VITE_USE_RULE_NER=true` - Mode règles locales (fallback)
- `VITE_DISABLE_LLM=true` - Désactiver LLM - `VITE_DISABLE_LLM=true` - Désactiver LLM
@ -271,6 +282,7 @@ docs/
### ❌ Problèmes Courants ### ❌ Problèmes Courants
#### Backend non accessible #### Backend non accessible
```bash ```bash
# Vérifier que le backend est démarré # Vérifier que le backend est démarré
curl http://localhost:3001/api/health curl http://localhost:3001/api/health
@ -280,11 +292,13 @@ cd backend && node server.js
``` ```
#### Erreurs OCR #### Erreurs OCR
- Vérifier la taille des images (minimum 3x3 pixels) - Vérifier la taille des images (minimum 3x3 pixels)
- Ajuster les paramètres `textord_min_xheight` - Ajuster les paramètres `textord_min_xheight`
- Vérifier les types de fichiers supportés - Vérifier les types de fichiers supportés
#### Erreurs de communication #### Erreurs de communication
- Vérifier que les ports 3001 (backend) et 5176 (frontend) sont libres - Vérifier que les ports 3001 (backend) et 5176 (frontend) sont libres
- Vérifier la configuration CORS - Vérifier la configuration CORS
- Vérifier les variables d'environnement - Vérifier les variables d'environnement
@ -292,6 +306,7 @@ cd backend && node server.js
### 🔍 Logs ### 🔍 Logs
**Backend**: **Backend**:
``` ```
🚀 Serveur backend démarré sur le port 3001 🚀 Serveur backend démarré sur le port 3001
📡 API disponible sur: http://localhost:3001/api 📡 API disponible sur: http://localhost:3001/api
@ -301,6 +316,7 @@ cd backend && node server.js
``` ```
**Frontend**: **Frontend**:
``` ```
🚀 [STORE] Utilisation du backend pour l'extraction 🚀 [STORE] Utilisation du backend pour l'extraction
📊 [PROGRESS] OCR doc-123: 30% 📊 [PROGRESS] OCR doc-123: 30%

View File

@ -3,6 +3,7 @@
## Version 1.1.1 - 2025-09-16 ## Version 1.1.1 - 2025-09-16
### 🔧 Corrections critiques ### 🔧 Corrections critiques
- **Fix URL API** : Correction de l'URL de l'API de `http://localhost:18000` vers `http://localhost:3001/api` - **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 - **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 - **Logs de debug** : Ajout de logs pour tracer les appels API et diagnostiquer les problèmes
@ -12,17 +13,20 @@
### 🆕 Nouvelles fonctionnalités ### 🆕 Nouvelles fonctionnalités
#### Système de Pending et Polling #### Système de Pending et Polling
- **Flags pending** : Création de fichiers `.pending` pour marquer les fichiers en cours de traitement - **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 - **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 - **Gestion d'erreur robuste** : Suppression automatique des flags en cas d'erreur
- **Nettoyage automatique** : Suppression des flags orphelins (> 1 heure) au démarrage - **Nettoyage automatique** : Suppression des flags orphelins (> 1 heure) au démarrage
#### API Backend #### API Backend
- **Route améliorée** : `GET /api/folders/:folderHash/results` retourne maintenant `pending`, `hasPending` - **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 - **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 - **Métadonnées pending** : Timestamp et statut dans les flags pending
#### Frontend React #### Frontend React
- **État Redux étendu** : Nouvelles propriétés `pendingFiles`, `hasPending`, `pollingInterval` - **État Redux étendu** : Nouvelles propriétés `pendingFiles`, `hasPending`, `pollingInterval`
- **Actions Redux** : `setPendingFiles`, `setPollingInterval`, `stopPolling` - **Actions Redux** : `setPendingFiles`, `setPollingInterval`, `stopPolling`
- **Polling intelligent** : Démarrage/arrêt automatique basé sur l'état `hasPending` - **Polling intelligent** : Démarrage/arrêt automatique basé sur l'état `hasPending`
@ -30,12 +34,14 @@
### 🔧 Améliorations ### 🔧 Améliorations
#### Backend #### Backend
- **Gestion d'erreur** : Try/catch/finally pour garantir le nettoyage des flags - **Gestion d'erreur** : Try/catch/finally pour garantir le nettoyage des flags
- **Nettoyage au démarrage** : Fonction `cleanupOrphanedPendingFlags()` appelée au démarrage - **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 - **Logs améliorés** : Messages détaillés pour le suivi des flags pending
- **Structure de dossiers** : Organisation par hash de dossier maintenue - **Structure de dossiers** : Organisation par hash de dossier maintenue
#### Frontend #### Frontend
- **App.tsx** : Gestion du cycle de vie du polling avec useCallback et useEffect - **App.tsx** : Gestion du cycle de vie du polling avec useCallback et useEffect
- **Nettoyage automatique** : Suppression des intervalles au démontage des composants - **Nettoyage automatique** : Suppression des intervalles au démontage des composants
- **Logs de debug** : Messages détaillés pour le suivi du polling - **Logs de debug** : Messages détaillés pour le suivi du polling
@ -43,6 +49,7 @@
### 🐛 Corrections ### 🐛 Corrections
#### Problèmes résolus #### Problèmes résolus
- **Flags pending supprimés au démarrage** : Seuls les flags orphelins sont maintenant nettoyés - **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 - **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 - **Gestion d'erreur** : Flags pending supprimés même en cas d'erreur de traitement
@ -51,20 +58,24 @@
### 📁 Fichiers modifiés ### 📁 Fichiers modifiés
#### Backend #### Backend
- `backend/server.js` : Ajout des fonctions de gestion des pending et nettoyage - `backend/server.js` : Ajout des fonctions de gestion des pending et nettoyage
#### Frontend #### Frontend
- `src/services/folderApi.ts` : Interface `FolderResponse` étendue - `src/services/folderApi.ts` : Interface `FolderResponse` étendue
- `src/store/documentSlice.ts` : État et actions pour le système de pending - `src/store/documentSlice.ts` : État et actions pour le système de pending
- `src/App.tsx` : Logique de polling automatique - `src/App.tsx` : Logique de polling automatique
#### Documentation #### Documentation
- `docs/systeme-pending.md` : Documentation complète du système - `docs/systeme-pending.md` : Documentation complète du système
- `docs/changelog-pending.md` : Ce changelog - `docs/changelog-pending.md` : Ce changelog
### 🧪 Tests ### 🧪 Tests
#### Tests effectués #### Tests effectués
- ✅ Upload simple avec création/suppression de flag - ✅ Upload simple avec création/suppression de flag
- ✅ Upload en double avec retour HTTP 202 - ✅ Upload en double avec retour HTTP 202
- ✅ Gestion d'erreur avec nettoyage de flag - ✅ Gestion d'erreur avec nettoyage de flag
@ -73,6 +84,7 @@
- ✅ Interface utilisateur mise à jour automatiquement - ✅ Interface utilisateur mise à jour automatiquement
#### Commandes de test #### Commandes de test
```bash ```bash
# Vérifier l'état d'un dossier # Vérifier l'état d'un dossier
curl -s http://localhost:3001/api/folders/7d99a85daf66a0081a0e881630e6b39b/results | jq '.count, .hasPending' 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 ### 🔄 Migration
#### Aucune migration requise #### Aucune migration requise
- Les dossiers existants continuent de fonctionner - Les dossiers existants continuent de fonctionner
- Les flags pending sont créés automatiquement - Les flags pending sont créés automatiquement
- Le système est rétrocompatible - Le système est rétrocompatible
@ -91,11 +104,13 @@ curl -X POST -F "document=@test.pdf" -F "folderHash=7d99a85daf66a0081a0e881630e6
### 📊 Métriques ### 📊 Métriques
#### Performance #### Performance
- **Polling interval** : 5 secondes (configurable) - **Polling interval** : 5 secondes (configurable)
- **Cleanup threshold** : 1 heure pour les flags orphelins - **Cleanup threshold** : 1 heure pour les flags orphelins
- **Temps de traitement** : Inchangé, flags ajoutent ~1ms - **Temps de traitement** : Inchangé, flags ajoutent ~1ms
#### Fiabilité #### Fiabilité
- **Gestion d'erreur** : 100% des flags pending nettoyés - **Gestion d'erreur** : 100% des flags pending nettoyés
- **Nettoyage automatique** : Flags orphelins supprimés au démarrage - **Nettoyage automatique** : Flags orphelins supprimés au démarrage
- **Polling intelligent** : Arrêt automatique quand plus de pending - **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 ### 🚀 Déploiement
#### Prérequis #### Prérequis
- Node.js 20.19.0+ - Node.js 20.19.0+
- Aucune dépendance supplémentaire - Aucune dépendance supplémentaire
#### Étapes #### Étapes
1. Redémarrer le serveur backend 1. Redémarrer le serveur backend
2. Redémarrer le frontend 2. Redémarrer le frontend
3. Vérifier les logs de nettoyage au démarrage 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 ### 🔮 Prochaines étapes
#### Améliorations futures #### Améliorations futures
- Configuration du polling interval via variables d'environnement - Configuration du polling interval via variables d'environnement
- Métriques de performance des flags pending - Métriques de performance des flags pending
- Interface d'administration pour visualiser les pending - Interface d'administration pour visualiser les pending

View File

@ -1,196 +1,194 @@
{ {
"document": { "document": {
"id": "doc-1757976015681", "id": "doc-1757976015681",
"fileName": "facture_4NK_08-2025_04.pdf", "fileName": "facture_4NK_08-2025_04.pdf",
"fileSize": 85819, "fileSize": 85819,
"mimeType": "application/pdf", "mimeType": "application/pdf",
"uploadTimestamp": "2025-09-15T22:40:15.681Z" "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": { "entities": {
"documentType": "Facture", "persons": [
"confidence": 0.95, {
"subType": "Facture de prestation", "id": "person-1",
"language": "fr", "type": "contact",
"pageCount": 1 "firstName": "Anthony",
}, "lastName": "Janin",
"extraction": { "role": "Gérant",
"text": { "email": "ja.janin.anthony@gmail.com",
"raw": "Janin Consulting - EURL au capital de 500 Euros...", "phone": "33 (0)6 71 40 84 13",
"processed": "Janin Consulting - EURL au capital de 500 Euros...", "confidence": 0.9,
"wordCount": 165, "source": "rule-based"
"characterCount": 1197, }
],
"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 "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
}
]
} }
},
"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"
}
} }

View File

@ -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"} {
"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"
}

View File

@ -1,7 +1,7 @@
{ {
"name": "4nk-ia-front4nk", "name": "4nk-ia-front4nk",
"private": true, "private": true,
"version": "0.1.3", "version": "0.1.4",
"type": "module", "type": "module",
"scripts": { "scripts": {
"predev": "node scripts/check-node.mjs", "predev": "node scripts/check-node.mjs",

View File

@ -1,24 +1,24 @@
#!/usr/bin/env node #!/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) => { const compare = (a, b) => {
for (let i = 0; i < Math.max(a.length, b.length); i += 1) { for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
const ai = a[i] || 0; const ai = a[i] || 0
const bi = b[i] || 0; const bi = b[i] || 0
if (ai > bi) return 1; if (ai > bi) return 1
if (ai < bi) return -1; if (ai < bi) return -1
} }
return 0; 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);
} }
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)`)

117
scripts/precache.cjs Normal file
View File

@ -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/<folderHash>
Usage: node scripts/precache.cjs <folderHash>
*/
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 <folderHash>')
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])

113
scripts/precache.js Normal file
View File

@ -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/<folderHash>
Usage: node scripts/precache.js <folderHash>
*/
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 <folderHash>')
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])

View File

@ -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

View File

@ -1,66 +1,66 @@
#!/usr/bin/env node #!/usr/bin/env node
import http from 'http'; import http from 'http'
import fs from 'fs'; import fs from 'fs'
import path from 'path'; import path from 'path'
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename)
const PORT = 5173; const PORT = 5173
const HOST = '0.0.0.0'; const HOST = '0.0.0.0'
// Types MIME // Types MIME
const mimeTypes = { const mimeTypes = {
'.html': 'text/html', '.html': 'text/html',
'.js': 'text/javascript', '.js': 'text/javascript',
'.css': 'text/css', '.css': 'text/css',
'.json': 'application/json', '.json': 'application/json',
'.png': 'image/png', '.png': 'image/png',
'.jpg': 'image/jpg', '.jpg': 'image/jpg',
'.gif': 'image/gif', '.gif': 'image/gif',
'.svg': 'image/svg+xml', '.svg': 'image/svg+xml',
'.ico': 'image/x-icon' '.ico': 'image/x-icon',
}; }
const server = http.createServer((req, res) => { 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; let filePath = '.' + req.url
if (filePath === './') { if (filePath === './') {
filePath = './index.html'; 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, () => { server.listen(PORT, HOST, () => {
console.log(`🚀 Serveur 4NK_IA_front démarré sur http://${HOST}:${PORT}`); console.log(`🚀 Serveur 4NK_IA_front démarré sur http://${HOST}:${PORT}`)
console.log(`📁 Servant les fichiers depuis: ${process.cwd()}`); console.log(`📁 Servant les fichiers depuis: ${process.cwd()}`)
console.log(`💡 Appuyez sur Ctrl+C pour arrêter`); console.log(`💡 Appuyez sur Ctrl+C pour arrêter`)
}); })

View File

@ -5,15 +5,15 @@ import { useAppDispatch, useAppSelector } from './store'
import { import {
createDefaultFolderThunk, createDefaultFolderThunk,
loadFolderResults, loadFolderResults,
setCurrentFolderHash,
setBootstrapped, setBootstrapped,
setPollingInterval, setPollingInterval,
stopPolling stopPolling,
} from './store/documentSlice' } from './store/documentSlice'
export default function App() { export default function App() {
const dispatch = useAppDispatch() 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 // Bootstrap au démarrage de l'application avec système de dossiers
useEffect(() => { useEffect(() => {
@ -22,7 +22,7 @@ export default function App() {
bootstrapped, bootstrapped,
currentFolderHash, currentFolderHash,
folderResultsLength: folderResults.length, folderResultsLength: folderResults.length,
isDev: import.meta.env.DEV isDev: import.meta.env.DEV,
}) })
// Récupérer le hash du dossier depuis l'URL // Récupérer le hash du dossier depuis l'URL
@ -51,7 +51,7 @@ export default function App() {
dispatch(setBootstrapped(true)) dispatch(setBootstrapped(true))
console.log('🎉 [APP] Bootstrap terminé avec le dossier:', folderHash) console.log('🎉 [APP] Bootstrap terminé avec le dossier:', folderHash)
} catch (error) { } 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() initializeFolder()
}, [dispatch, bootstrapped, currentFolderHash, folderResults.length]) }, [dispatch, bootstrapped, currentFolderHash, folderResults.length, documents.length])
// Fonction pour démarrer le polling // Fonction pour démarrer le polling
const startPolling = useCallback((folderHash: string) => { const startPolling = useCallback(
console.log('🔄 [APP] Démarrage du polling pour le dossier:', folderHash) (folderHash: string) => {
console.log('🔄 [APP] Démarrage du polling pour le dossier:', folderHash)
const interval = setInterval(() => { const interval = setInterval(() => {
console.log('🔄 [APP] Polling - Vérification des résultats...') console.log('🔄 [APP] Polling - Vérification des résultats...')
dispatch(loadFolderResults(folderHash)) dispatch(loadFolderResults(folderHash))
}, 5000) // Polling toutes les 5 secondes }, 5000) // Polling toutes les 5 secondes
dispatch(setPollingInterval(interval)) dispatch(setPollingInterval(interval))
}, [dispatch]) },
[dispatch],
)
// Fonction pour arrêter le polling // Fonction pour arrêter le polling
const stopPollingCallback = useCallback(() => { const stopPollingCallback = useCallback(() => {

View File

@ -61,7 +61,9 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
const isPDF = document.mimeType.includes('pdf') || document.name.toLowerCase().endsWith('.pdf') const isPDF = document.mimeType.includes('pdf') || document.name.toLowerCase().endsWith('.pdf')
const isImage = const isImage =
document.mimeType.startsWith('image/') || 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) { if (!isPDF && isImage) {
return ( return (
@ -121,7 +123,7 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
}} }}
onLoad={() => setLoading(false)} onLoad={() => setLoading(false)}
onError={() => { onError={() => {
setError('Erreur de chargement de l\'image') setError("Erreur de chargement de l'image")
setLoading(false) setLoading(false)
}} }}
/> />
@ -142,7 +144,12 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose}>Fermer</Button> <Button onClick={onClose}>Fermer</Button>
<Button variant="contained" startIcon={<Download />} onClick={handleDownload} disabled={!document.previewUrl}> <Button
variant="contained"
startIcon={<Download />}
onClick={handleDownload}
disabled={!document.previewUrl}
>
Télécharger Télécharger
</Button> </Button>
</DialogActions> </DialogActions>
@ -189,7 +196,7 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
variant="outlined" variant="outlined"
size="small" size="small"
startIcon={<NavigateBefore />} startIcon={<NavigateBefore />}
onClick={() => setPage(prev => Math.max(prev - 1, 1))} onClick={() => setPage((prev) => Math.max(prev - 1, 1))}
disabled={page <= 1} disabled={page <= 1}
> >
Précédent Précédent
@ -201,7 +208,7 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
variant="outlined" variant="outlined"
size="small" size="small"
endIcon={<NavigateNext />} endIcon={<NavigateNext />}
onClick={() => setPage(prev => Math.min(prev + 1, numPages))} onClick={() => setPage((prev) => Math.min(prev + 1, numPages))}
disabled={page >= numPages} disabled={page >= numPages}
> >
Suivant Suivant
@ -213,18 +220,16 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
variant="outlined" variant="outlined"
size="small" size="small"
startIcon={<ZoomOut />} startIcon={<ZoomOut />}
onClick={() => setScale(prev => Math.max(prev - 0.2, 0.5))} onClick={() => setScale((prev) => Math.max(prev - 0.2, 0.5))}
> >
Zoom - Zoom -
</Button> </Button>
<Typography variant="body2"> <Typography variant="body2">{Math.round(scale * 100)}%</Typography>
{Math.round(scale * 100)}%
</Typography>
<Button <Button
variant="outlined" variant="outlined"
size="small" size="small"
startIcon={<ZoomIn />} startIcon={<ZoomIn />}
onClick={() => setScale(prev => Math.min(prev + 0.2, 2.0))} onClick={() => setScale((prev) => Math.min(prev + 0.2, 2.0))}
> >
Zoom + Zoom +
</Button> </Button>
@ -232,16 +237,18 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
</Box> </Box>
{/* Aperçu PDF avec viewer intégré */} {/* Aperçu PDF avec viewer intégré */}
<Box sx={{ <Box
border: '1px solid', sx={{
borderColor: 'grey.300', border: '1px solid',
borderRadius: 1, borderColor: 'grey.300',
overflow: 'hidden', borderRadius: 1,
maxHeight: '70vh', overflow: 'hidden',
display: 'flex', maxHeight: '70vh',
justifyContent: 'center', display: 'flex',
backgroundColor: 'grey.50' justifyContent: 'center',
}}> backgroundColor: 'grey.50',
}}
>
{document.previewUrl ? ( {document.previewUrl ? (
<Box sx={{ width: '100%', height: '600px' }}> <Box sx={{ width: '100%', height: '600px' }}>
{/* Utiliser un viewer PDF intégré */} {/* Utiliser un viewer PDF intégré */}
@ -254,7 +261,7 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
transform: `scale(${scale})`, transform: `scale(${scale})`,
transformOrigin: 'top left', transformOrigin: 'top left',
width: `${100 / scale}%`, width: `${100 / scale}%`,
height: `${600 / scale}px` height: `${600 / scale}px`,
}} }}
title={`Aperçu de ${document.name}`} title={`Aperçu de ${document.name}`}
onLoad={() => setLoading(false)} onLoad={() => setLoading(false)}
@ -286,9 +293,7 @@ export const FilePreview: React.FC<FilePreviewProps> = ({ document, onClose }) =
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose}> <Button onClick={onClose}>Fermer</Button>
Fermer
</Button>
<Button <Button
variant="contained" variant="contained"
startIcon={<Download />} startIcon={<Download />}

View File

@ -3,7 +3,12 @@ import { AppBar, Toolbar, Typography, Container, Box, LinearProgress } from '@mu
import { useNavigate, useLocation } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { NavigationTabs } from './NavigationTabs' import { NavigationTabs } from './NavigationTabs'
import { useAppDispatch, useAppSelector } from '../store' import { useAppDispatch, useAppSelector } from '../store'
import { extractDocument, analyzeDocument, getContextData, getConseil } from '../store/documentSlice' import {
extractDocument,
analyzeDocument,
getContextData,
getConseil,
} from '../store/documentSlice'
interface LayoutProps { interface LayoutProps {
children: React.ReactNode children: React.ReactNode
@ -13,7 +18,15 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { documents, extractionById, loading, currentDocument, contextResult, conseilResult, analysisResult } = useAppSelector((s) => s.document) const {
documents,
extractionById,
loading,
currentDocument,
contextResult,
conseilResult,
analysisResult,
} = useAppSelector((s) => s.document)
// Au chargement/nav: lancer OCR+classification pour tous les documents sans résultat // Au chargement/nav: lancer OCR+classification pour tous les documents sans résultat
const processedDocs = useRef(new Set<string>()) const processedDocs = useRef(new Set<string>())
@ -32,17 +45,17 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
console.log(`🚀 [LAYOUT] Traitement de la queue: ${docId}`) console.log(`🚀 [LAYOUT] Traitement de la queue: ${docId}`)
try { try {
// Marquer le document comme en cours de traitement // Marquer le document comme en cours de traitement
const doc = documents.find(d => d.id === docId) const doc = documents.find((d) => d.id === docId)
if (doc) { if (doc) {
doc.status = 'processing' doc.status = 'processing'
} }
await dispatch(extractDocument(docId)) await dispatch(extractDocument(docId))
// Attendre un peu entre les extractions // Attendre un peu entre les extractions
await new Promise(resolve => setTimeout(resolve, 500)) await new Promise((resolve) => setTimeout(resolve, 500))
} catch (error) { } catch (error) {
console.error(`❌ [LAYOUT] Erreur extraction ${docId}:`, error) console.error(`❌ [LAYOUT] Erreur extraction ${docId}:`, error)
// Marquer le document comme en erreur // Marquer le document comme en erreur
const doc = documents.find(d => d.id === docId) const doc = documents.find((d) => d.id === docId)
if (doc) { if (doc) {
doc.status = 'error' doc.status = 'error'
} }
@ -62,7 +75,9 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
const isProcessing = doc.status === 'processing' const isProcessing = doc.status === 'processing'
const isCompleted = doc.status === 'completed' const isCompleted = doc.status === 'completed'
console.log(`📄 [LAYOUT] Document ${doc.id}: hasExtraction=${!!hasExtraction}, isProcessed=${isProcessed}, isProcessing=${isProcessing}, isCompleted=${isCompleted}`) console.log(
`📄 [LAYOUT] Document ${doc.id}: hasExtraction=${!!hasExtraction}, isProcessed=${isProcessed}, isProcessing=${isProcessing}, isCompleted=${isCompleted}`,
)
// Si le document a déjà un résultat d'extraction, marquer comme traité // Si le document a déjà un résultat d'extraction, marquer comme traité
if (hasExtraction && !isProcessed) { if (hasExtraction && !isProcessed) {

View File

@ -18,7 +18,7 @@ export const NavigationTabs: React.FC<NavigationTabsProps> = ({ currentPath }) =
{ label: 'Conseil', path: '/conseil', alwaysEnabled: false }, { label: 'Conseil', path: '/conseil', alwaysEnabled: false },
] ]
const currentTabIndex = tabs.findIndex(tab => tab.path === currentPath) const currentTabIndex = tabs.findIndex((tab) => tab.path === currentPath)
// Vérifier si au moins une extraction est terminée // Vérifier si au moins une extraction est terminée
const hasCompletedExtraction = currentDocument && extractionById[currentDocument.id] const hasCompletedExtraction = currentDocument && extractionById[currentDocument.id]
@ -45,10 +45,10 @@ export const NavigationTabs: React.FC<NavigationTabsProps> = ({ currentPath }) =
label={tab.label} label={tab.label}
disabled={!tab.alwaysEnabled && !hasCompletedExtraction} disabled={!tab.alwaysEnabled && !hasCompletedExtraction}
sx={{ sx={{
opacity: (!tab.alwaysEnabled && !hasCompletedExtraction) ? 0.5 : 1, opacity: !tab.alwaysEnabled && !hasCompletedExtraction ? 0.5 : 1,
'&.Mui-disabled': { '&.Mui-disabled': {
color: 'text.disabled' color: 'text.disabled',
} },
}} }}
/> />
))} ))}

View File

@ -17,7 +17,8 @@
box-sizing: border-box; box-sizing: border-box;
} }
html, body { html,
body {
margin: 0; margin: 0;
padding: 0; padding: 0;
min-height: 100vh; min-height: 100vh;

View File

@ -1,4 +1,4 @@
import React, { lazy, Suspense } from 'react' import { lazy, Suspense } from 'react'
import { createBrowserRouter, RouterProvider } from 'react-router-dom' import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { Box, CircularProgress, Typography } from '@mui/material' import { Box, CircularProgress, Typography } from '@mui/material'
@ -15,10 +15,38 @@ const LoadingFallback = () => (
) )
const router = createBrowserRouter([ const router = createBrowserRouter([
{ path: '/', element: <Suspense fallback={<LoadingFallback />}><UploadView /></Suspense> }, {
{ path: '/extraction', element: <Suspense fallback={<LoadingFallback />}><ExtractionView /></Suspense> }, path: '/',
{ path: '/contexte', element: <Suspense fallback={<LoadingFallback />}><ContexteView /></Suspense> }, element: (
{ path: '/conseil', element: <Suspense fallback={<LoadingFallback />}><ConseilView /></Suspense> }, <Suspense fallback={<LoadingFallback />}>
<UploadView />
</Suspense>
),
},
{
path: '/extraction',
element: (
<Suspense fallback={<LoadingFallback />}>
<ExtractionView />
</Suspense>
),
},
{
path: '/contexte',
element: (
<Suspense fallback={<LoadingFallback />}>
<ContexteView />
</Suspense>
),
},
{
path: '/conseil',
element: (
<Suspense fallback={<LoadingFallback />}>
<ConseilView />
</Suspense>
),
},
]) ])
export const AppRouter = () => { export const AppRouter = () => {

View File

@ -1,6 +1,12 @@
import axios from 'axios' import axios from 'axios'
import { openaiDocumentApi, openaiExternalApi } from './openai' import { openaiDocumentApi, openaiExternalApi } from './openai'
import type { Document, ExtractionResult, AnalysisResult, ContextResult, ConseilResult } from '../types' import type {
Document,
ExtractionResult,
AnalysisResult,
ContextResult,
ConseilResult,
} from '../types'
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
const USE_OPENAI = import.meta.env.VITE_USE_OPENAI === 'true' const USE_OPENAI = import.meta.env.VITE_USE_OPENAI === 'true'
@ -11,7 +17,14 @@ if (import.meta.env.DEV) {
.toString() .toString()
.replace(/.(?=.{4})/g, '*') .replace(/.(?=.{4})/g, '*')
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.info('[ENV] VITE_API_URL=', BASE_URL, 'VITE_USE_AI=', USE_OPENAI, 'VITE_AI_API_KEY=', maskedKey) console.info(
'[ENV] VITE_API_URL=',
BASE_URL,
'VITE_USE_AI=',
USE_OPENAI,
'VITE_AI_API_KEY=',
maskedKey,
)
} }
export const apiClient = axios.create({ export const apiClient = axios.create({
@ -25,7 +38,7 @@ apiClient.interceptors.response.use(
(error) => { (error) => {
// Laisser remonter les erreurs au consommateur // Laisser remonter les erreurs au consommateur
return Promise.reject(error) return Promise.reject(error)
} },
) )
// Services API pour les documents // Services API pour les documents
@ -51,7 +64,7 @@ export const documentApi = {
size: data.document.fileSize || file.size, size: data.document.fileSize || file.size,
uploadDate: new Date(data.document.uploadTimestamp || Date.now()), uploadDate: new Date(data.document.uploadTimestamp || Date.now()),
status: 'completed', status: 'completed',
previewUrl: fileUrl previewUrl: fileUrl,
} }
// Adapter le résultat d'extraction au format attendu // Adapter le résultat d'extraction au format attendu
@ -62,9 +75,14 @@ export const documentApi = {
text: data.extraction.text.raw, text: data.extraction.text.raw,
identities: data.extraction.entities.persons || [], identities: data.extraction.entities.persons || [],
addresses: data.extraction.entities.addresses || [], addresses: data.extraction.entities.addresses || [],
properties: [],
contracts: [],
signatures: [],
companies: data.extraction.entities.companies || [], companies: data.extraction.entities.companies || [],
language: data.classification.language, language: data.classification.language,
timestamp: data.status.timestamp // Métadonnées complètes
metadata: data.metadata,
status: data.status,
} }
return { document, extraction } return { document, extraction }
@ -86,34 +104,37 @@ export const documentApi = {
text: results.ocr_text || 'Texte extrait du document...', text: results.ocr_text || 'Texte extrait du document...',
language: 'fr', language: 'fr',
documentType: results.document_type || 'Document', documentType: results.document_type || 'Document',
identities: results.entities?.persons?.map((name: string, index: number) => ({ identities:
id: `person-${index}`, results.entities?.persons?.map((name: string, index: number) => ({
type: 'person' as const, id: `person-${index}`,
firstName: name.split(' ')[0] || name, type: 'person' as const,
lastName: name.split(' ').slice(1).join(' ') || '', firstName: name.split(' ')[0] || name,
birthDate: '', lastName: name.split(' ').slice(1).join(' ') || '',
nationality: 'Française', birthDate: '',
confidence: 0.9, nationality: 'Française',
})) || [], confidence: 0.9,
addresses: results.entities?.addresses?.map((address: string) => ({ })) || [],
street: address, addresses:
city: 'Paris', results.entities?.addresses?.map((address: string) => ({
postalCode: '75001', street: address,
country: 'France',
})) || [],
properties: results.entities?.properties?.map((_propertyName: string, index: number) => ({
id: `prop-${index}`,
type: 'apartment' as const,
address: {
street: '123 Rue de la Paix',
city: 'Paris', city: 'Paris',
postalCode: '75001', postalCode: '75001',
country: 'France', country: 'France',
}, })) || [],
surface: 75, properties:
cadastralReference: '1234567890AB', results.entities?.properties?.map((_propertyName: string, index: number) => ({
value: 250000, id: `prop-${index}`,
})) || [], type: 'apartment' as const,
address: {
street: '123 Rue de la Paix',
city: 'Paris',
postalCode: '75001',
country: 'France',
},
surface: 75,
cadastralReference: '1234567890AB',
value: 250000,
})) || [],
contracts: [ contracts: [
{ {
id: 'contract-1', id: 'contract-1',

View File

@ -38,19 +38,21 @@ export interface BackendExtractionResult {
timestamp: string timestamp: string
} }
/** /**
* Extrait le texte et les entités d'un document via le backend * Extrait le texte et les entités d'un document via le backend
*/ */
export async function extractDocumentBackend( export async function extractDocumentBackend(
_documentId: string, _documentId: string,
file?: File, file?: File,
hooks?: { onOcrProgress?: (progress: number) => void; onLlmProgress?: (progress: number) => void } hooks?: {
onOcrProgress?: (progress: number) => void
onLlmProgress?: (progress: number) => void
},
): Promise<ExtractionResult> { ): Promise<ExtractionResult> {
console.log('🚀 [BACKEND] Début de l\'extraction via le backend...') console.log("🚀 [BACKEND] Début de l'extraction via le backend...")
if (!file) { if (!file) {
throw new Error('Aucun fichier fourni pour l\'extraction') throw new Error("Aucun fichier fourni pour l'extraction")
} }
// Simuler la progression OCR // Simuler la progression OCR
@ -65,7 +67,7 @@ export async function extractDocumentBackend(
try { try {
const response = await fetch(`${BACKEND_URL}/api/extract`, { const response = await fetch(`${BACKEND_URL}/api/extract`, {
method: 'POST', method: 'POST',
body: formData body: formData,
}) })
if (!response.ok) { if (!response.ok) {
@ -88,30 +90,31 @@ export async function extractDocumentBackend(
language: result.classification.language, language: result.classification.language,
documentType: result.classification.documentType, documentType: result.classification.documentType,
identities: result.extraction.entities.persons.map((person: any) => ({ identities: result.extraction.entities.persons.map((person: any) => ({
id: person.id, id: person.id || `person-${Date.now()}`,
type: 'person', type: 'person' as const,
firstName: person.firstName, firstName: person.firstName || '',
lastName: person.lastName, lastName: person.lastName || '',
confidence: person.confidence || 0.9 confidence: person.confidence || 0.9,
})), })),
addresses: result.extraction.entities.addresses.map((address: any) => ({ addresses: result.extraction.entities.addresses.map((address: any) => ({
id: address.id, street: address.street || '',
street: address.street, city: address.city || '',
city: address.city, postalCode: address.postalCode || '',
postalCode: address.postalCode,
country: address.country || 'France', country: address.country || 'France',
confidence: address.confidence || 0.9
})), })),
properties: [], properties: [],
contracts: [], contracts: [],
signatures: result.extraction.entities.contractual?.signatures?.map((sig: any) => sig.signatory || 'Signature détectée') || [], signatures:
result.extraction.entities.contractual?.signatures?.map(
(sig: any) => sig.signatory || 'Signature détectée',
) || [],
confidence: result.metadata.quality.globalConfidence, confidence: result.metadata.quality.globalConfidence,
confidenceReasons: [ confidenceReasons: [
`OCR: ${Math.round(result.metadata.quality.textExtractionConfidence * 100)}% de confiance`, `OCR: ${Math.round(result.metadata.quality.textExtractionConfidence * 100)}% de confiance`,
`Texte extrait: ${result.extraction.text.characterCount} caractères`, `Texte extrait: ${result.extraction.text.characterCount} caractères`,
`Entités trouvées: ${result.extraction.entities.persons.length} personnes, ${result.extraction.entities.companies.length} sociétés, ${result.extraction.entities.addresses.length} adresses`, `Entités trouvées: ${result.extraction.entities.persons.length} personnes, ${result.extraction.entities.companies.length} sociétés, ${result.extraction.entities.addresses.length} adresses`,
`Type détecté: ${result.classification.documentType}`, `Type détecté: ${result.classification.documentType}`,
`Traitement backend: ${result.document.uploadTimestamp}` `Traitement backend: ${result.document.uploadTimestamp}`,
], ],
// Nouveaux champs du format standard // Nouveaux champs du format standard
companies: result.extraction.entities.companies || [], companies: result.extraction.entities.companies || [],
@ -121,7 +124,7 @@ export async function extractDocumentBackend(
references: result.extraction.entities.references || [], references: result.extraction.entities.references || [],
// Métadonnées complètes // Métadonnées complètes
metadata: result.metadata, metadata: result.metadata,
status: result.status status: result.status,
} }
// Extraction terminée // Extraction terminée
@ -130,18 +133,16 @@ export async function extractDocumentBackend(
documentType: extractionResult.documentType, documentType: extractionResult.documentType,
identitiesCount: extractionResult.identities.length, identitiesCount: extractionResult.identities.length,
addressesCount: extractionResult.addresses.length, addressesCount: extractionResult.addresses.length,
confidence: extractionResult.confidence confidence: extractionResult.confidence,
}) })
return extractionResult return extractionResult
} catch (error) { } catch (error) {
console.error('❌ [BACKEND] Erreur lors de l\'extraction:', error) console.error("❌ [BACKEND] Erreur lors de l'extraction:", error)
throw error throw error
} }
} }
// Cache pour le health check // Cache pour le health check
let backendHealthCache: { isHealthy: boolean; timestamp: number } | null = null let backendHealthCache: { isHealthy: boolean; timestamp: number } | null = null
const HEALTH_CHECK_CACHE_DURATION = 5000 // 5 secondes const HEALTH_CHECK_CACHE_DURATION = 5000 // 5 secondes
@ -153,7 +154,7 @@ export async function checkBackendHealth(): Promise<boolean> {
const now = Date.now() const now = Date.now()
// Utiliser le cache si disponible et récent // Utiliser le cache si disponible et récent
if (backendHealthCache && (now - backendHealthCache.timestamp) < HEALTH_CHECK_CACHE_DURATION) { if (backendHealthCache && now - backendHealthCache.timestamp < HEALTH_CHECK_CACHE_DURATION) {
return backendHealthCache.isHealthy return backendHealthCache.isHealthy
} }
@ -189,7 +190,7 @@ export const backendDocumentApi = {
isCNI: false, isCNI: false,
credibilityScore: 0.8, credibilityScore: 0.8,
summary: 'Analyse en cours...', summary: 'Analyse en cours...',
recommendations: [] recommendations: [],
} }
}, },
getContext: async (documentId: string): Promise<ContextResult> => { getContext: async (documentId: string): Promise<ContextResult> => {
@ -197,7 +198,7 @@ export const backendDocumentApi = {
documentId, documentId,
lastUpdated: new Date(), lastUpdated: new Date(),
georisquesData: {}, georisquesData: {},
cadastreData: {} cadastreData: {},
} }
}, },
getConseil: async (documentId: string): Promise<ConseilResult> => { getConseil: async (documentId: string): Promise<ConseilResult> => {
@ -207,7 +208,7 @@ export const backendDocumentApi = {
recommendations: [], recommendations: [],
risks: [], risks: [],
nextSteps: [], nextSteps: [],
generatedAt: new Date() generatedAt: new Date(),
} }
} },
} }

View File

@ -24,7 +24,10 @@ export async function extractTextFromFile(file: File): Promise<string> {
} }
return pdfText return pdfText
} }
if (mime.startsWith('image/') || ['.png', '.jpg', '.jpeg'].some((ext) => file.name.toLowerCase().endsWith(ext))) { if (
mime.startsWith('image/') ||
['.png', '.jpg', '.jpeg'].some((ext) => file.name.toLowerCase().endsWith(ext))
) {
const imgText = await extractFromImage(file) const imgText = await extractFromImage(file)
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -62,8 +65,12 @@ async function extractFromPdf(file: File): Promise<string> {
canvas.height = viewport.height canvas.height = viewport.height
const ctx = canvas.getContext('2d') as any const ctx = canvas.getContext('2d') as any
await page.render({ canvasContext: ctx, viewport }).promise await page.render({ canvasContext: ctx, viewport }).promise
const blob: Blob = await new Promise((resolve) => canvas.toBlob((b) => resolve(b as Blob), 'image/png')) const blob: Blob = await new Promise((resolve) =>
const ocrText = await extractFromImage(new File([blob], `${file.name}-p${i}.png`, { type: 'image/png' })) canvas.toBlob((b) => resolve(b as Blob), 'image/png'),
)
const ocrText = await extractFromImage(
new File([blob], `${file.name}-p${i}.png`, { type: 'image/png' }),
)
pageText = ocrText pageText = ocrText
} }
if (pageText.trim()) texts.push(pageText) if (pageText.trim()) texts.push(pageText)
@ -92,7 +99,9 @@ async function extractFromImage(file: File): Promise<string> {
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height) const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const data = imgData.data const data = imgData.data
for (let i = 0; i < data.length; i += 4) { for (let i = 0; i < data.length; i += 4) {
const r = data[i], g = data[i + 1], b = data[i + 2] const r = data[i],
g = data[i + 1],
b = data[i + 2]
// luma // luma
let y = 0.299 * r + 0.587 * g + 0.114 * b let y = 0.299 * r + 0.587 * g + 0.114 * b
// contraste simple // contraste simple
@ -120,7 +129,8 @@ async function extractFromImage(file: File): Promise<string> {
// Configuration optimisée pour les images de petite taille // Configuration optimisée pour les images de petite taille
const params = { const params = {
tessedit_pageseg_mode: psm, tessedit_pageseg_mode: psm,
tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,/-:() ', tessedit_char_whitelist:
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,/-:() ',
tessedit_ocr_engine_mode: '1', // LSTM OCR Engine tessedit_ocr_engine_mode: '1', // LSTM OCR Engine
preserve_interword_spaces: '1', preserve_interword_spaces: '1',
textord_min_linesize: '2.0', // Réduit la taille minimale des lignes textord_min_linesize: '2.0', // Réduit la taille minimale des lignes
@ -128,7 +138,7 @@ async function extractFromImage(file: File): Promise<string> {
classify_bln_numeric_mode: '0', classify_bln_numeric_mode: '0',
textord_heavy_nr: '1', textord_heavy_nr: '1',
textord_old_baselines: '0', textord_old_baselines: '0',
textord_old_xheight: '0' textord_old_xheight: '0',
} }
// @ts-expect-error - tessedit_pageseg_mode expects PSM enum, but string is used // @ts-expect-error - tessedit_pageseg_mode expects PSM enum, but string is used
@ -145,7 +155,9 @@ async function extractFromImage(file: File): Promise<string> {
const confidence = Math.max(0, data.confidence || 0) const confidence = Math.max(0, data.confidence || 0)
const score = confidence * Math.log(len + 1) * (len > 10 ? 1.2 : 0.8) const score = confidence * Math.log(len + 1) * (len > 10 ? 1.2 : 0.8)
console.log(`[OCR] PSM:${psm} Rot:${deg}° Conf:${confidence.toFixed(1)}% Len:${len} Score:${score.toFixed(2)}`) console.log(
`[OCR] PSM:${psm} Rot:${deg}° Conf:${confidence.toFixed(1)}% Len:${len} Score:${score.toFixed(2)}`,
)
if (score > bestScore) { if (score > bestScore) {
bestScore = score bestScore = score
@ -159,7 +171,10 @@ async function extractFromImage(file: File): Promise<string> {
break break
} }
} catch (error) { } catch (error) {
console.warn(`[OCR] Erreur PSM:${psm} Rot:${deg}°:`, error instanceof Error ? error.message : String(error)) console.warn(
`[OCR] Erreur PSM:${psm} Rot:${deg}°:`,
error instanceof Error ? error.message : String(error),
)
} }
} }

View File

@ -2,7 +2,7 @@
* API pour la gestion des dossiers par hash * API pour la gestion des dossiers par hash
*/ */
const API_BASE_URL = 'http://172.17.222.203:3001/api' const API_BASE_URL = '/api'
export interface FolderResult { export interface FolderResult {
fileHash: string fileHash: string
@ -96,7 +96,7 @@ export async function getDefaultFolder(): Promise<CreateFolderResponse> {
return { return {
success: true, success: true,
folderHash: '7d99a85daf66a0081a0e881630e6b39b', folderHash: '7d99a85daf66a0081a0e881630e6b39b',
message: 'Dossier par défaut récupéré' message: 'Dossier par défaut récupéré',
} }
} }
@ -120,9 +120,9 @@ export async function getFolderResults(folderHash: string): Promise<FolderRespon
const response = await fetch(url, { const response = await fetch(url, {
signal: controller.signal, signal: controller.signal,
headers: { headers: {
'Accept': 'application/json', Accept: 'application/json',
'Content-Type': 'application/json' 'Content-Type': 'application/json',
} },
}) })
clearTimeout(timeoutId) clearTimeout(timeoutId)
@ -131,7 +131,9 @@ export async function getFolderResults(folderHash: string): Promise<FolderRespon
if (!response.ok) { if (!response.ok) {
console.error(`[API] Erreur HTTP:`, response.status, response.statusText) console.error(`[API] Erreur HTTP:`, response.status, response.statusText)
throw new Error(`Erreur lors de la récupération des résultats du dossier: ${response.statusText}`) throw new Error(
`Erreur lors de la récupération des résultats du dossier: ${response.statusText}`,
)
} }
console.log(`[API] Début du parsing JSON...`) console.log(`[API] Début du parsing JSON...`)
@ -141,7 +143,7 @@ export async function getFolderResults(folderHash: string): Promise<FolderRespon
return data return data
} catch (error) { } catch (error) {
if (error.name === 'AbortError') { if (error instanceof Error && error.name === 'AbortError') {
console.error(`[API] Requête annulée (timeout)`) console.error(`[API] Requête annulée (timeout)`)
throw new Error('Timeout: La requête a pris trop de temps') throw new Error('Timeout: La requête a pris trop de temps')
} }

View File

@ -16,7 +16,9 @@ const OPENAI_API_KEY = import.meta.env.VITE_OPENAI_API_KEY
const OPENAI_BASE_URL = import.meta.env.VITE_OPENAI_BASE_URL || 'https://api.openai.com/v1' const OPENAI_BASE_URL = import.meta.env.VITE_OPENAI_BASE_URL || 'https://api.openai.com/v1'
const OPENAI_CHAT_MODEL = import.meta.env.VITE_OPENAI_MODEL || 'gpt-4o-mini' const OPENAI_CHAT_MODEL = import.meta.env.VITE_OPENAI_MODEL || 'gpt-4o-mini'
async function callOpenAIChat(messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>): Promise<string> { async function callOpenAIChat(
messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
): Promise<string> {
if (!OPENAI_API_KEY) { if (!OPENAI_API_KEY) {
throw new Error('Clé API OpenAI manquante (VITE_AI_API_KEY)') throw new Error('Clé API OpenAI manquante (VITE_AI_API_KEY)')
} }
@ -73,7 +75,11 @@ export const openaiDocumentApi = {
} }
}, },
extract: async (documentId: string, file?: File, hooks?: ProgressHooks): Promise<ExtractionResult> => { extract: async (
documentId: string,
file?: File,
hooks?: ProgressHooks,
): Promise<ExtractionResult> => {
let localText = '' let localText = ''
if (file) { if (file) {
try { try {
@ -93,13 +99,13 @@ export const openaiDocumentApi = {
useRuleNer, useRuleNer,
classifyOnly, classifyOnly,
disableLLM, disableLLM,
hasOpenAIKey: !!OPENAI_API_KEY hasOpenAIKey: !!OPENAI_API_KEY,
}) })
// Si NER local actif, on l'utilise pour tout (identités/adresses/...) puis, si demandé, // Si NER local actif, on l'utilise pour tout (identités/adresses/...) puis, si demandé,
// on peut consulter le LLM uniquement pour classifier le type de document // on peut consulter le LLM uniquement pour classifier le type de document
if (useRuleNer) { if (useRuleNer) {
console.log('🚀 [OCR] Début de l\'extraction OCR locale...') console.log("🚀 [OCR] Début de l'extraction OCR locale...")
console.log('📄 [OCR] Texte à traiter:', localText.substring(0, 200) + '...') console.log('📄 [OCR] Texte à traiter:', localText.substring(0, 200) + '...')
// Simuler la progression OCR de manière asynchrone pour éviter les boucles // Simuler la progression OCR de manière asynchrone pour éviter les boucles
@ -120,13 +126,13 @@ export const openaiDocumentApi = {
}, 300) }, 300)
} }
console.log('🔍 [NER] Début de l\'extraction par règles...') console.log("🔍 [NER] Début de l'extraction par règles...")
let res = runRuleNER(documentId, localText) let res = runRuleNER(documentId, localText)
console.log('📊 [NER] Résultats extraits:', { console.log('📊 [NER] Résultats extraits:', {
documentType: res.documentType, documentType: res.documentType,
identitiesCount: res.identities.length, identitiesCount: res.identities.length,
addressesCount: res.addresses.length, addressesCount: res.addresses.length,
confidence: res.confidence confidence: res.confidence,
}) })
if (classifyOnly && OPENAI_API_KEY && localText && !disableLLM) { if (classifyOnly && OPENAI_API_KEY && localText && !disableLLM) {
@ -134,13 +140,22 @@ export const openaiDocumentApi = {
try { try {
hooks?.onLlmProgress?.(0) hooks?.onLlmProgress?.(0)
const cls = await callOpenAIChat([ const cls = await callOpenAIChat([
{ role: 'system', content: 'Tu es un classifieur. Retourne uniquement un JSON strict.' }, {
{ role: 'user', content: `Classifie ce texte en une des catégories suivantes: [CNI, Facture, Attestation, Document]. Réponds strictement sous la forme {"documentType":"..."}.\nTexte:\n${localText.slice(0, 8000)}` }, role: 'system',
content: 'Tu es un classifieur. Retourne uniquement un JSON strict.',
},
{
role: 'user',
content: `Classifie ce texte en une des catégories suivantes: [CNI, Facture, Attestation, Document]. Réponds strictement sous la forme {"documentType":"..."}.\nTexte:\n${localText.slice(0, 8000)}`,
},
]) ])
const parsed = JSON.parse(cls) const parsed = JSON.parse(cls)
if (parsed && typeof parsed.documentType === 'string') { if (parsed && typeof parsed.documentType === 'string') {
res = { ...res, documentType: parsed.documentType } res = { ...res, documentType: parsed.documentType }
res.confidenceReasons = [...(res.confidenceReasons || []), 'Classification LLM limitée au documentType'] res.confidenceReasons = [
...(res.confidenceReasons || []),
'Classification LLM limitée au documentType',
]
console.log('✅ [LLM] Classification LLM terminée:', parsed.documentType) console.log('✅ [LLM] Classification LLM terminée:', parsed.documentType)
} }
hooks?.onLlmProgress?.(1) hooks?.onLlmProgress?.(1)
@ -175,7 +190,10 @@ export const openaiDocumentApi = {
try { try {
const cls = await callOpenAIChat([ const cls = await callOpenAIChat([
{ role: 'system', content: 'Tu es un classifieur. Retourne uniquement un JSON strict.' }, { role: 'system', content: 'Tu es un classifieur. Retourne uniquement un JSON strict.' },
{ role: 'user', content: `Classifie ce texte en une des catégories suivantes: [CNI, Facture, Attestation, Document]. Réponds strictement sous la forme {"documentType":"..."}.\nTexte:\n${localText.slice(0, 8000)}` }, {
role: 'user',
content: `Classifie ce texte en une des catégories suivantes: [CNI, Facture, Attestation, Document]. Réponds strictement sous la forme {"documentType":"..."}.\nTexte:\n${localText.slice(0, 8000)}`,
},
]) ])
const parsed = JSON.parse(cls) const parsed = JSON.parse(cls)
hooks?.onLlmProgress?.(1) hooks?.onLlmProgress?.(1)
@ -190,7 +208,7 @@ export const openaiDocumentApi = {
contracts: [], contracts: [],
signatures: [], signatures: [],
confidence: 0.6, confidence: 0.6,
confidenceReasons: ['Classification LLM sans contexte, pas d\'extraction d\'identités'], confidenceReasons: ["Classification LLM sans contexte, pas d'extraction d'identités"],
} }
} catch { } catch {
hooks?.onLlmProgress?.(1) hooks?.onLlmProgress?.(1)
@ -214,7 +232,7 @@ export const openaiDocumentApi = {
{ {
role: 'system', role: 'system',
content: content:
'Tu extrais uniquement les informations présentes dans le texte OCR. Interdiction d\'inventer. Interdiction d\'utiliser le nom du fichier comme identité. Réponds en JSON strict, sans texte autour.', "Tu extrais uniquement les informations présentes dans le texte OCR. Interdiction d'inventer. Interdiction d'utiliser le nom du fichier comme identité. Réponds en JSON strict, sans texte autour.",
}, },
{ {
role: 'user', role: 'user',
@ -229,7 +247,7 @@ export const openaiDocumentApi = {
const docBase = (file?.name || '').toLowerCase().replace(/\.[a-z0-9]+$/, '') const docBase = (file?.name || '').toLowerCase().replace(/\.[a-z0-9]+$/, '')
const safeIdentities = (parsed.identities || []).filter((it: any) => { const safeIdentities = (parsed.identities || []).filter((it: any) => {
const full = `${it.firstName || ''} ${it.lastName || ''}`.trim().toLowerCase() const full = `${it.firstName || ''} ${it.lastName || ''}`.trim().toLowerCase()
return full && !docBase || (full && !docBase.includes(full) && !full.includes(docBase)) return (full && !docBase) || (full && !docBase.includes(full) && !full.includes(docBase))
}) })
return { return {
@ -242,7 +260,8 @@ export const openaiDocumentApi = {
properties: parsed.properties || [], properties: parsed.properties || [],
contracts: parsed.contracts || [], contracts: parsed.contracts || [],
signatures: parsed.signatures || [], signatures: parsed.signatures || [],
confidence: Math.round((typeof parsed.confidence === 'number' ? parsed.confidence : 0.7) * 100) / 100, confidence:
Math.round((typeof parsed.confidence === 'number' ? parsed.confidence : 0.7) * 100) / 100,
confidenceReasons: parsed.confidenceReasons || [], confidenceReasons: parsed.confidenceReasons || [],
} }
} catch { } catch {
@ -279,7 +298,10 @@ export const openaiDocumentApi = {
analyze: async (documentId: string): Promise<AnalysisResult> => { analyze: async (documentId: string): Promise<AnalysisResult> => {
const result = await callOpenAIChat([ const result = await callOpenAIChat([
{ role: 'system', content: 'Tu fournis une analyse brève et des risques potentiels.' }, { role: 'system', content: 'Tu fournis une analyse brève et des risques potentiels.' },
{ role: 'user', content: `Analyse le document ${documentId} et fournis un résumé des risques.` }, {
role: 'user',
content: `Analyse le document ${documentId} et fournis un résumé des risques.`,
},
]) ])
const isCNI = /cni|carte\s+nationale\s+d'identité/i.test(result || '') const isCNI = /cni|carte\s+nationale\s+d'identité/i.test(result || '')
const number = (result || '').match(/[A-Z0-9]{12,}/)?.[0] || '' const number = (result || '').match(/[A-Z0-9]{12,}/)?.[0] || ''
@ -290,9 +312,7 @@ export const openaiDocumentApi = {
documentId, documentId,
documentType: isCNI ? 'CNI' : 'Document', documentType: isCNI ? 'CNI' : 'Document',
isCNI, isCNI,
verificationResult: isCNI verificationResult: isCNI ? { numberValid, formatValid, checksumValid } : undefined,
? { numberValid, formatValid, checksumValid }
: undefined,
credibilityScore: isCNI ? (numberValid ? 0.8 : 0.6) : 0.6, credibilityScore: isCNI ? (numberValid ? 0.8 : 0.6) : 0.6,
summary: result || 'Analyse indisponible.', summary: result || 'Analyse indisponible.',
recommendations: [], recommendations: [],
@ -314,7 +334,14 @@ export const openaiDocumentApi = {
{ role: 'system', content: 'Tu fournis des conseils opérationnels courts et concrets.' }, { role: 'system', content: 'Tu fournis des conseils opérationnels courts et concrets.' },
{ role: 'user', content: `Donne 3 conseils actionnables pour le document ${documentId}.` }, { role: 'user', content: `Donne 3 conseils actionnables pour le document ${documentId}.` },
]) ])
return { documentId, analysis: conseil || '', recommendations: conseil ? [conseil] : [], risks: [], nextSteps: [], generatedAt: new Date() } return {
documentId,
analysis: conseil || '',
recommendations: conseil ? [conseil] : [],
risks: [],
nextSteps: [],
generatedAt: new Date(),
}
}, },
detectType: async (_file: File): Promise<{ type: string; confidence: number }> => { detectType: async (_file: File): Promise<{ type: string; confidence: number }> => {
@ -324,7 +351,9 @@ export const openaiDocumentApi = {
export const openaiExternalApi = { export const openaiExternalApi = {
cadastre: async (_address: string) => ({ note: 'Mode OpenAI: contexte non connecté' }), cadastre: async (_address: string) => ({ note: 'Mode OpenAI: contexte non connecté' }),
georisques: async (_coordinates: { lat: number; lng: number }) => ({ note: 'Mode OpenAI: contexte non connecté' }), georisques: async (_coordinates: { lat: number; lng: number }) => ({
note: 'Mode OpenAI: contexte non connecté',
}),
geofoncier: async (_address: string) => ({ note: 'Mode OpenAI: contexte non connecté' }), geofoncier: async (_address: string) => ({ note: 'Mode OpenAI: contexte non connecté' }),
bodacc: async (_companyName: string) => ({ note: 'Mode OpenAI: contexte non connecté' }), bodacc: async (_companyName: string) => ({ note: 'Mode OpenAI: contexte non connecté' }),
infogreffe: async (_siren: string) => ({ note: 'Mode OpenAI: contexte non connecté' }), infogreffe: async (_siren: string) => ({ note: 'Mode OpenAI: contexte non connecté' }),
@ -334,5 +363,5 @@ function pseudoChecksum(input: string): boolean {
if (!input) return false if (!input) return false
// checksum simple: somme des codes char modulo 10 doit être pair // checksum simple: somme des codes char modulo 10 doit être pair
const sum = Array.from(input).reduce((acc, ch) => acc + ch.charCodeAt(0), 0) const sum = Array.from(input).reduce((acc, ch) => acc + ch.charCodeAt(0), 0)
return sum % 10 % 2 === 0 return (sum % 10) % 2 === 0
} }

View File

@ -20,7 +20,8 @@ function extractMRZ(text: string): { firstName?: string; lastName?: string } | n
if (parts.length >= 2) { if (parts.length >= 2) {
const rawLast = parts[0].replace(/<+/g, ' ').trim() const rawLast = parts[0].replace(/<+/g, ' ').trim()
const rawFirst = parts[1].replace(/<+/g, ' ').trim() const rawFirst = parts[1].replace(/<+/g, ' ').trim()
if (rawLast && rawFirst) return { firstName: toTitleCase(rawFirst), lastName: rawLast.replace(/\s+/g, ' ') } if (rawLast && rawFirst)
return { firstName: toTitleCase(rawFirst), lastName: rawLast.replace(/\s+/g, ' ') }
} }
} }
} }
@ -56,7 +57,7 @@ function extractAddresses(text: string): Address[] {
// "demeurant 123 Rue de la Paix, 75001 Paris" // "demeurant 123 Rue de la Paix, 75001 Paris"
/demeurant\s+(\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,
// "situé 123 Rue de la Paix, 75001 Paris" // "situé 123 Rue de la Paix, 75001 Paris"
/situé\s+(\d{1,4})\s+([A-Za-zÀ-ÖØ-öø-ÿ\s\-']+?),\s*(\d{5})\s+([A-Za-zÀ-ÖØ-öø-ÿ\s\-']+)/gi /situé\s+(\d{1,4})\s+([A-Za-zÀ-ÖØ-öø-ÿ\s\-']+?),\s*(\d{5})\s+([A-Za-zÀ-ÖØ-öø-ÿ\s\-']+)/gi,
] ]
for (const pattern of addressPatterns) { for (const pattern of addressPatterns) {
@ -69,7 +70,7 @@ function extractAddresses(text: string): Address[] {
street, street,
city, city,
postalCode, postalCode,
country: 'France' country: 'France',
}) })
} }
} }
@ -81,7 +82,8 @@ function extractNames(text: string): Identity[] {
const identities: Identity[] = [] const identities: Identity[] = []
// Pattern pour "Vendeur : Prénom Nom" ou "Acheteur : Prénom Nom" // Pattern pour "Vendeur : Prénom Nom" ou "Acheteur : Prénom Nom"
const rolePattern = /(Vendeur|Acheteur|Vendeuse|Acheteuse|Propriétaire|Locataire|Bailleur|Preneur)\s*:\s*([A-Z][a-zà-öø-ÿ'\-]+\s+[A-Z][a-zà-öø-ÿ'\-]+)/gi const rolePattern =
/(Vendeur|Acheteur|Vendeuse|Acheteuse|Propriétaire|Locataire|Bailleur|Preneur)\s*:\s*([A-Z][a-zà-öø-ÿ'\-]+\s+[A-Z][a-zà-öø-ÿ'\-]+)/gi
for (const match of text.matchAll(rolePattern)) { for (const match of text.matchAll(rolePattern)) {
const fullName = match[2].trim() const fullName = match[2].trim()
@ -110,13 +112,18 @@ function extractNames(text: string): Identity[] {
// Fallback: heuristique lignes en MAJUSCULES pour NOM // Fallback: heuristique lignes en MAJUSCULES pour NOM
if (identities.length === 0) { if (identities.length === 0) {
const lines = text.split(/\n|\r/).map((l) => l.trim()).filter(Boolean) const lines = text
.split(/\n|\r/)
.map((l) => l.trim())
.filter(Boolean)
for (let i = 0; i < lines.length; i += 1) { for (let i = 0; i < lines.length; i += 1) {
const line = lines[i] const line = lines[i]
if (/^[A-ZÀ-ÖØ-Þ\-\s]{3,}$/.test(line) && line.length <= 40) { if (/^[A-ZÀ-ÖØ-Þ\-\s]{3,}$/.test(line) && line.length <= 40) {
const lastName = line.replace(/\s+/g, ' ').trim() const lastName = line.replace(/\s+/g, ' ').trim()
const cand = (lines[i + 1] || '').trim() const cand = (lines[i + 1] || '').trim()
const firstNameMatch = cand.match(/^[A-Z][a-zà-öø-ÿ'\-]{1,}(?:\s+[A-Z][a-zà-öø-ÿ'\-]{1,})?$/) const firstNameMatch = cand.match(
/^[A-Z][a-zà-öø-ÿ'\-]{1,}(?:\s+[A-Z][a-zà-öø-ÿ'\-]{1,})?$/,
)
const firstName = firstNameMatch ? cand : undefined const firstName = firstNameMatch ? cand : undefined
if (lastName && (!firstName || firstName.length <= 40)) { if (lastName && (!firstName || firstName.length <= 40)) {
identities.push({ identities.push({
@ -135,7 +142,7 @@ function extractNames(text: string): Identity[] {
} }
export function runRuleNER(documentId: string, text: string): ExtractionResult { export function runRuleNER(documentId: string, text: string): ExtractionResult {
console.log('🔍 [RULE-NER] Début de l\'analyse du texte...') console.log("🔍 [RULE-NER] Début de l'analyse du texte...")
console.log('📄 [RULE-NER] Longueur du texte:', text.length) console.log('📄 [RULE-NER] Longueur du texte:', text.length)
const identitiesFromMRZ = extractMRZ(text) const identitiesFromMRZ = extractMRZ(text)

View File

@ -0,0 +1,108 @@
/**
* API pour la gestion des fichiers de test
* Fournit des fichiers de test pour les démonstrations et tests
*/
export interface TestFileInfo {
name: string
size: number
type: string
lastModified: number
}
// Liste des fichiers de test disponibles
const TEST_FILES = [
'IMG_20250902_162159.jpg',
'IMG_20250902_162210.jpg',
'sample.md',
'sample.pdf',
'sample.txt',
]
// Types de fichiers supportés
const SUPPORTED_MIME_TYPES = [
'application/pdf',
'image/jpeg',
'image/jpg',
'image/png',
'text/plain',
'text/markdown',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
]
const SUPPORTED_EXTENSIONS = ['.pdf', '.jpg', '.jpeg', '.png', '.txt', '.md', '.docx']
/**
* Récupère la liste des fichiers de test disponibles
*/
export async function getTestFilesList(): Promise<TestFileInfo[]> {
const results: TestFileInfo[] = []
for (const fileName of TEST_FILES) {
try {
const response = await fetch(`/test-files/${fileName}`)
if (response.ok) {
const contentLength = response.headers.get('content-length')
const contentType = response.headers.get('content-type')
results.push({
name: fileName,
size: contentLength ? parseInt(contentLength, 10) : 0,
type: contentType || 'application/octet-stream',
lastModified: Date.now(),
})
}
} catch (error) {
console.warn(`Fichier de test non trouvé: ${fileName}`, error)
}
}
return results
}
/**
* Charge un fichier de test spécifique
*/
export async function loadTestFile(fileName: string): Promise<File | null> {
try {
const response = await fetch(`/test-files/${fileName}`)
if (!response.ok) {
return null
}
const blob = await response.blob()
return new File([blob], fileName, { type: blob.type })
} catch (error) {
console.error(`Erreur lors du chargement du fichier de test: ${fileName}`, error)
return null
}
}
/**
* Filtre les fichiers supportés par type MIME et extension
*/
export function filterSupportedFiles(files: TestFileInfo[]): TestFileInfo[] {
return files.filter((file) => {
// Vérifier par type MIME
if (SUPPORTED_MIME_TYPES.includes(file.type)) {
return true
}
// Vérifier par extension si le type MIME est inconnu
const extension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'))
return SUPPORTED_EXTENSIONS.includes(extension)
})
}
/**
* Vérifie si un fichier est supporté
*/
export function isFileSupported(file: TestFileInfo): boolean {
return filterSupportedFiles([file]).length > 0
}

View File

@ -15,5 +15,3 @@ const appSlice = createSlice({
}) })
export const appReducer = appSlice.reducer export const appReducer = appSlice.reducer

View File

@ -1,10 +1,21 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit' import type { PayloadAction } from '@reduxjs/toolkit'
import type { Document, ExtractionResult, AnalysisResult, ContextResult, ConseilResult } from '../types' import type {
Document,
ExtractionResult,
AnalysisResult,
ContextResult,
ConseilResult,
} from '../types'
import { documentApi } from '../services/api' import { documentApi } from '../services/api'
import { openaiDocumentApi } from '../services/openai' import { openaiDocumentApi } from '../services/openai'
import { backendDocumentApi, checkBackendHealth } from '../services/backendApi' import { backendDocumentApi, checkBackendHealth } from '../services/backendApi'
import { createDefaultFolder, getDefaultFolder, getFolderResults, uploadFileToFolder, type FolderResult } from '../services/folderApi' import {
getDefaultFolder,
getFolderResults,
uploadFileToFolder,
type FolderResult,
} from '../services/folderApi'
interface DocumentState { interface DocumentState {
documents: Document[] documents: Document[]
@ -42,7 +53,7 @@ const loadStateFromStorage = (): Partial<DocumentState> => {
const parsed = JSON.parse(savedState) const parsed = JSON.parse(savedState)
console.log('💾 [STORE] État chargé depuis localStorage:', { console.log('💾 [STORE] État chargé depuis localStorage:', {
documentsCount: parsed.documents?.length || 0, documentsCount: parsed.documents?.length || 0,
extractionsCount: Object.keys(parsed.extractionById || {}).length extractionsCount: Object.keys(parsed.extractionById || {}).length,
}) })
return parsed return parsed
} }
@ -52,20 +63,6 @@ const loadStateFromStorage = (): Partial<DocumentState> => {
return {} return {}
} }
// Fonction pour sauvegarder l'état dans localStorage
const saveStateToStorage = (state: DocumentState) => {
try {
const stateToSave = {
documents: state.documents,
extractionById: state.extractionById,
currentDocument: state.currentDocument
}
localStorage.setItem('4nk-ia-documents', JSON.stringify(stateToSave))
} catch (error) {
console.warn('⚠️ [STORE] Erreur lors de la sauvegarde dans localStorage:', error)
}
}
const initialState: DocumentState = { const initialState: DocumentState = {
documents: [], documents: [],
currentDocument: null, currentDocument: null,
@ -87,15 +84,12 @@ const initialState: DocumentState = {
pendingFiles: [], pendingFiles: [],
hasPending: false, hasPending: false,
pollingInterval: null, pollingInterval: null,
...loadStateFromStorage() ...loadStateFromStorage(),
} }
export const uploadDocument = createAsyncThunk( export const uploadDocument = createAsyncThunk('document/upload', async (file: File) => {
'document/upload', return await documentApi.upload(file)
async (file: File) => { })
return await documentApi.upload(file)
}
)
export const extractDocument = createAsyncThunk( export const extractDocument = createAsyncThunk(
'document/extract', 'document/extract',
@ -119,7 +113,7 @@ export const extractDocument = createAsyncThunk(
const backendAvailable = await checkBackendHealth() const backendAvailable = await checkBackendHealth()
if (backendAvailable) { if (backendAvailable) {
console.log('🚀 [STORE] Utilisation du backend pour l\'extraction') console.log("🚀 [STORE] Utilisation du backend pour l'extraction")
// Pas de hooks de progression pour le backend - traitement direct // Pas de hooks de progression pour le backend - traitement direct
@ -160,36 +154,27 @@ export const extractDocument = createAsyncThunk(
} }
return await documentApi.extract(documentId) return await documentApi.extract(documentId)
} }
} },
) )
export const analyzeDocument = createAsyncThunk( export const analyzeDocument = createAsyncThunk('document/analyze', async (documentId: string) => {
'document/analyze', return await documentApi.analyze(documentId)
async (documentId: string) => { })
return await documentApi.analyze(documentId)
}
)
export const getContextData = createAsyncThunk( export const getContextData = createAsyncThunk('document/context', async (documentId: string) => {
'document/context', return await documentApi.getContext(documentId)
async (documentId: string) => { })
return await documentApi.getContext(documentId)
}
)
export const getConseil = createAsyncThunk( export const getConseil = createAsyncThunk('document/conseil', async (documentId: string) => {
'document/conseil', return await documentApi.getConseil(documentId)
async (documentId: string) => { })
return await documentApi.getConseil(documentId)
}
)
// Thunks pour la gestion des dossiers // Thunks pour la gestion des dossiers
export const createDefaultFolderThunk = createAsyncThunk( export const createDefaultFolderThunk = createAsyncThunk(
'document/createDefaultFolder', 'document/createDefaultFolder',
async () => { async () => {
return await getDefaultFolder() return await getDefaultFolder()
} },
) )
export const loadFolderResults = createAsyncThunk( export const loadFolderResults = createAsyncThunk(
@ -204,14 +189,14 @@ export const loadFolderResults = createAsyncThunk(
console.error(`[STORE] loadFolderResults erreur:`, error) console.error(`[STORE] loadFolderResults erreur:`, error)
throw error throw error
} }
} },
) )
export const uploadFileToFolderThunk = createAsyncThunk( export const uploadFileToFolderThunk = createAsyncThunk(
'document/uploadFileToFolder', 'document/uploadFileToFolder',
async ({ file, folderHash }: { file: File; folderHash: string }) => { async ({ file, folderHash }: { file: File; folderHash: string }) => {
return await uploadFileToFolder(file, folderHash) return await uploadFileToFolder(file, folderHash)
} },
) )
const documentSlice = createSlice({ const documentSlice = createSlice({
@ -256,11 +241,17 @@ const documentSlice = createSlice({
}, },
setOcrProgress: (state, action: PayloadAction<{ id: string; progress: number }>) => { setOcrProgress: (state, action: PayloadAction<{ id: string; progress: number }>) => {
const { id, progress } = action.payload const { id, progress } = action.payload
state.progressById[id] = { ocr: Math.max(0, Math.min(100, Math.round(progress * 100))), llm: state.progressById[id]?.llm || 0 } state.progressById[id] = {
ocr: Math.max(0, Math.min(100, Math.round(progress * 100))),
llm: state.progressById[id]?.llm || 0,
}
}, },
setLlmProgress: (state, action: PayloadAction<{ id: string; progress: number }>) => { setLlmProgress: (state, action: PayloadAction<{ id: string; progress: number }>) => {
const { id, progress } = action.payload const { id, progress } = action.payload
state.progressById[id] = { ocr: state.progressById[id]?.ocr || 0, llm: Math.max(0, Math.min(100, Math.round(progress * 100))) } state.progressById[id] = {
ocr: state.progressById[id]?.ocr || 0,
llm: Math.max(0, Math.min(100, Math.round(progress * 100))),
}
}, },
setBootstrapped: (state, action: PayloadAction<boolean>) => { setBootstrapped: (state, action: PayloadAction<boolean>) => {
state.bootstrapped = action.payload state.bootstrapped = action.payload
@ -277,12 +268,17 @@ const documentSlice = createSlice({
state.currentResultIndex = 0 state.currentResultIndex = 0
}, },
// Reducers pour le système de pending // Reducers pour le système de pending
setPendingFiles: (state, action: PayloadAction<Array<{ setPendingFiles: (
fileHash: string state,
folderHash: string action: PayloadAction<
timestamp: string Array<{
status: string fileHash: string
}>>) => { folderHash: string
timestamp: string
status: string
}>
>,
) => {
state.pendingFiles = action.payload state.pendingFiles = action.payload
state.hasPending = action.payload.length > 0 state.hasPending = action.payload.length > 0
}, },
@ -312,7 +308,7 @@ const documentSlice = createSlice({
documentId: document.id, documentId: document.id,
documentName: document.name, documentName: document.name,
hasExtraction: !!extraction, hasExtraction: !!extraction,
extractionDocumentId: extraction?.documentId extractionDocumentId: extraction?.documentId,
}) })
state.documents.push(document) state.documents.push(document)
@ -339,18 +335,18 @@ const documentSlice = createSlice({
state.loading = true state.loading = true
state.error = null state.error = null
}) })
.addCase(extractDocument.fulfilled, (state, action) => { .addCase(extractDocument.fulfilled, (state, action) => {
state.loading = false state.loading = false
state.extractionResult = action.payload state.extractionResult = action.payload
state.extractionById[action.payload.documentId] = action.payload state.extractionById[action.payload.documentId] = action.payload
// Mettre à jour le statut du document courant // Mettre à jour le statut du document courant
if (state.currentDocument && state.currentDocument.id === action.payload.documentId) { if (state.currentDocument && state.currentDocument.id === action.payload.documentId) {
state.currentDocument.status = 'completed' state.currentDocument.status = 'completed'
} }
}) })
.addCase(extractDocument.rejected, (state, action) => { .addCase(extractDocument.rejected, (state, action) => {
state.loading = false state.loading = false
state.error = action.error.message || 'Erreur lors de l\'extraction' state.error = action.error.message || "Erreur lors de l'extraction"
// Mettre à jour le statut du document courant en cas d'erreur // Mettre à jour le statut du document courant en cas d'erreur
if (state.currentDocument) { if (state.currentDocument) {
state.currentDocument.status = 'error' state.currentDocument.status = 'error'
@ -391,11 +387,11 @@ const documentSlice = createSlice({
// Convertir les résultats en documents pour la compatibilité // Convertir les résultats en documents pour la compatibilité
if (action.payload.results && action.payload.results.length > 0) { if (action.payload.results && action.payload.results.length > 0) {
state.documents = action.payload.results.map((result, index) => { state.documents = action.payload.results.map((result) => {
console.log(`[STORE] Mapping résultat ${index}:`, { console.log(`[STORE] Mapping résultat:`, {
fileHash: result.fileHash, fileHash: result.fileHash,
fileName: result.document?.fileName, fileName: result.document?.fileName,
mimeType: result.document?.mimeType mimeType: result.document?.mimeType,
}) })
return { return {
@ -405,7 +401,7 @@ const documentSlice = createSlice({
size: 0, // Taille non disponible dans la structure actuelle size: 0, // Taille non disponible dans la structure actuelle
uploadDate: new Date(result.document.uploadTimestamp), uploadDate: new Date(result.document.uploadTimestamp),
status: 'completed' as const, status: 'completed' as const,
previewUrl: `http://172.17.222.203:3001/api/folders/${action.payload.folderHash}/files/${result.fileHash}` previewUrl: `/api/folders/${action.payload.folderHash}/files/${result.fileHash}`,
} }
}) })
} else { } else {
@ -413,11 +409,16 @@ const documentSlice = createSlice({
state.documents = [] state.documents = []
} }
console.log(`[STORE] Dossier chargé: ${action.payload.results.length} résultats, ${action.payload.pending?.length || 0} pending`) console.log(
`[STORE] Dossier chargé: ${action.payload.results.length} résultats, ${action.payload.pending?.length || 0} pending`,
)
console.log(`[STORE] Documents finaux:`, state.documents.length) console.log(`[STORE] Documents finaux:`, state.documents.length)
console.log(`[STORE] Documents mappés:`, state.documents.map(d => ({ id: d.id, name: d.name, status: d.status }))) console.log(
`[STORE] Documents mappés:`,
state.documents.map((d) => ({ id: d.id, name: d.name, status: d.status })),
)
}) })
.addCase(loadFolderResults.pending, (state) => { .addCase(loadFolderResults.pending, () => {
// Ne pas afficher la barre de progression pour le chargement initial des résultats // Ne pas afficher la barre de progression pour le chargement initial des résultats
// state.loading = true // state.loading = true
}) })
@ -425,7 +426,7 @@ const documentSlice = createSlice({
state.loading = false state.loading = false
state.error = action.error.message || 'Erreur lors du chargement des résultats du dossier' state.error = action.error.message || 'Erreur lors du chargement des résultats du dossier'
}) })
.addCase(uploadFileToFolderThunk.fulfilled, (state, action) => { .addCase(uploadFileToFolderThunk.fulfilled, (state) => {
// Recharger les résultats du dossier après upload // Recharger les résultats du dossier après upload
state.loading = false state.loading = false
}) })
@ -434,7 +435,7 @@ const documentSlice = createSlice({
}) })
.addCase(uploadFileToFolderThunk.rejected, (state, action) => { .addCase(uploadFileToFolderThunk.rejected, (state, action) => {
state.loading = false state.loading = false
state.error = action.error.message || 'Erreur lors de l\'upload du fichier' state.error = action.error.message || "Erreur lors de l'upload du fichier"
}) })
}, },
}) })
@ -452,6 +453,6 @@ export const {
clearFolderResults, clearFolderResults,
setPendingFiles, setPendingFiles,
setPollingInterval, setPollingInterval,
stopPolling stopPolling,
} = documentSlice.actions } = documentSlice.actions
export const documentReducer = documentSlice.reducer export const documentReducer = documentSlice.reducer

View File

@ -5,36 +5,40 @@ import { appReducer } from './appSlice'
import { documentReducer } from './documentSlice' import { documentReducer } from './documentSlice'
// Middleware pour sauvegarder l'état dans localStorage // Middleware pour sauvegarder l'état dans localStorage
const persistenceMiddleware = (store: any) => (next: any) => (action: any) => { const persistenceMiddleware =
const result = next(action) (store: { getState: () => { document: any } }) =>
(next: (action: any) => any) =>
(action: any) => {
const result = next(action)
// Sauvegarder seulement les actions liées aux documents // Sauvegarder seulement les actions liées aux documents
if (action.type.startsWith('document/')) { if (action.type.startsWith('document/')) {
const state = store.getState() const state = store.getState()
try { try {
const stateToSave = { const stateToSave = {
documents: state.document.documents, documents: state.document.documents,
extractionById: state.document.extractionById, extractionById: state.document.extractionById,
currentDocument: state.document.currentDocument currentDocument: state.document.currentDocument,
}
localStorage.setItem('4nk-ia-documents', JSON.stringify(stateToSave))
} catch (error) {
console.warn('⚠️ [STORE] Erreur lors de la sauvegarde:', error)
} }
localStorage.setItem('4nk-ia-documents', JSON.stringify(stateToSave))
} catch (error) {
console.warn('⚠️ [STORE] Erreur lors de la sauvegarde:', error)
} }
}
return result return result
} }
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
app: appReducer, app: appReducer,
document: documentReducer, document: documentReducer,
}, },
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ middleware: (getDefaultMiddleware) =>
serializableCheck: false, getDefaultMiddleware({
immutableCheck: true, serializableCheck: false,
}).concat(persistenceMiddleware), immutableCheck: true,
}).concat(persistenceMiddleware),
devTools: true, devTools: true,
}) })

View File

@ -33,7 +33,7 @@ import { Layout } from '../components/Layout'
export default function ConseilView() { export default function ConseilView() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { currentDocument, conseilResult, analysisResult, loading } = useAppSelector( const { currentDocument, conseilResult, analysisResult, loading } = useAppSelector(
(state) => state.document (state) => state.document,
) )
useEffect(() => { useEffect(() => {
@ -50,9 +50,7 @@ export default function ConseilView() {
if (!currentDocument) { if (!currentDocument) {
return ( return (
<Layout> <Layout>
<Alert severity="info"> <Alert severity="info">Veuillez d'abord téléverser et sélectionner un document.</Alert>
Veuillez d'abord téléverser et sélectionner un document.
</Alert>
</Layout> </Layout>
) )
} }
@ -71,9 +69,7 @@ export default function ConseilView() {
if (!conseilResult) { if (!conseilResult) {
return ( return (
<Layout> <Layout>
<Alert severity="warning"> <Alert severity="warning">Aucun conseil disponible.</Alert>
Aucun conseil disponible.
</Alert>
</Layout> </Layout>
) )
} }
@ -125,15 +121,17 @@ export default function ConseilView() {
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={analysisResult.credibilityScore * 100} value={analysisResult.credibilityScore * 100}
color={getScoreColor(analysisResult.credibilityScore) as LinearProgressProps['color']} color={
getScoreColor(analysisResult.credibilityScore) as LinearProgressProps['color']
}
sx={{ height: 10, borderRadius: 5, mb: 2 }} sx={{ height: 10, borderRadius: 5, mb: 2 }}
/> />
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{analysisResult.credibilityScore >= 0.8 {analysisResult.credibilityScore >= 0.8
? 'Document très fiable' ? 'Document très fiable'
: analysisResult.credibilityScore >= 0.6 : analysisResult.credibilityScore >= 0.6
? 'Document moyennement fiable' ? 'Document moyennement fiable'
: 'Document peu fiable - vérification recommandée'} : 'Document peu fiable - vérification recommandée'}
</Typography> </Typography>
{analysisResult.summary && ( {analysisResult.summary && (
<Paper <Paper
@ -186,7 +184,10 @@ export default function ConseilView() {
<CardContent> <CardContent>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
<CheckCircle sx={{ mr: 1, verticalAlign: 'middle' }} /> <CheckCircle sx={{ mr: 1, verticalAlign: 'middle' }} />
Recommandations ({conseilResult.recommendations.length + (analysisResult?.recommendations?.length || 0)}) Recommandations (
{conseilResult.recommendations.length +
(analysisResult?.recommendations?.length || 0)}
)
</Typography> </Typography>
<List dense> <List dense>
{conseilResult.recommendations.map((recommendation, index) => ( {conseilResult.recommendations.map((recommendation, index) => (
@ -228,8 +229,12 @@ export default function ConseilView() {
<ListItemText <ListItemText
primary={risk} primary={risk}
primaryTypographyProps={{ primaryTypographyProps={{
color: getRiskColor(risk) === 'error' ? 'error.main' : color:
getRiskColor(risk) === 'warning' ? 'warning.main' : 'info.main' getRiskColor(risk) === 'error'
? 'error.main'
: getRiskColor(risk) === 'warning'
? 'warning.main'
: 'info.main',
}} }}
/> />
</ListItem> </ListItem>
@ -253,10 +258,7 @@ export default function ConseilView() {
<ListItemIcon> <ListItemIcon>
<Schedule color="primary" /> <Schedule color="primary" />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText primary={`Étape ${index + 1}`} secondary={step} />
primary={`Étape ${index + 1}`}
secondary={step}
/>
</ListItem> </ListItem>
))} ))}
</List> </List>
@ -277,12 +279,8 @@ export default function ConseilView() {
> >
Régénérer les conseils Régénérer les conseils
</Button> </Button>
<Button variant="outlined"> <Button variant="outlined">Exporter le rapport</Button>
Exporter le rapport <Button variant="outlined">Partager avec l'équipe</Button>
</Button>
<Button variant="outlined">
Partager avec l'équipe
</Button>
</Box> </Box>
</CardContent> </CardContent>
</Card> </Card>
@ -318,7 +316,8 @@ export default function ConseilView() {
/> />
</Box> </Box>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Cette analyse LLM a é générée automatiquement et doit être validée par un expert notarial. Cette analyse LLM a é générée automatiquement et doit être validée par un expert
notarial.
</Typography> </Typography>
</Paper> </Paper>
</Box> </Box>

View File

@ -30,9 +30,7 @@ import { Layout } from '../components/Layout'
export default function ContexteView() { export default function ContexteView() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { currentDocument, contextResult, loading } = useAppSelector( const { currentDocument, contextResult, loading } = useAppSelector((state) => state.document)
(state) => state.document
)
useEffect(() => { useEffect(() => {
if (currentDocument && !contextResult) { if (currentDocument && !contextResult) {
@ -43,9 +41,7 @@ export default function ContexteView() {
if (!currentDocument) { if (!currentDocument) {
return ( return (
<Layout> <Layout>
<Alert severity="info"> <Alert severity="info">Veuillez d'abord téléverser et sélectionner un document.</Alert>
Veuillez d'abord téléverser et sélectionner un document.
</Alert>
</Layout> </Layout>
) )
} }
@ -64,9 +60,7 @@ export default function ContexteView() {
if (!contextResult) { if (!contextResult) {
return ( return (
<Layout> <Layout>
<Alert severity="warning"> <Alert severity="warning">Aucune donnée contextuelle disponible.</Alert>
Aucune donnée contextuelle disponible.
</Alert>
</Layout> </Layout>
) )
} }
@ -150,9 +144,7 @@ export default function ContexteView() {
</Typography> </Typography>
</Box> </Box>
) : ( ) : (
<Alert severity="info"> <Alert severity="info">Aucune donnée cadastrale trouvée pour ce document.</Alert>
Aucune donnée cadastrale trouvée pour ce document.
</Alert>
)} )}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -179,9 +171,7 @@ export default function ContexteView() {
</Typography> </Typography>
</Box> </Box>
) : ( ) : (
<Alert severity="info"> <Alert severity="info">Aucune donnée Géorisques trouvée pour ce document.</Alert>
Aucune donnée Géorisques trouvée pour ce document.
</Alert>
)} )}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -208,9 +198,7 @@ export default function ContexteView() {
</Typography> </Typography>
</Box> </Box>
) : ( ) : (
<Alert severity="info"> <Alert severity="info">Aucune donnée Géofoncier trouvée pour ce document.</Alert>
Aucune donnée Géofoncier trouvée pour ce document.
</Alert>
)} )}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -237,9 +225,7 @@ export default function ContexteView() {
</Typography> </Typography>
</Box> </Box>
) : ( ) : (
<Alert severity="info"> <Alert severity="info">Aucune donnée BODACC trouvée pour ce document.</Alert>
Aucune donnée BODACC trouvée pour ce document.
</Alert>
)} )}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -266,9 +252,7 @@ export default function ContexteView() {
</Typography> </Typography>
</Box> </Box>
) : ( ) : (
<Alert severity="info"> <Alert severity="info">Aucune donnée Infogreffe trouvée pour ce document.</Alert>
Aucune donnée Infogreffe trouvée pour ce document.
</Alert>
)} )}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -287,9 +271,7 @@ export default function ContexteView() {
> >
Actualiser les données Actualiser les données
</Button> </Button>
<Button variant="outlined"> <Button variant="outlined">Exporter le rapport</Button>
Exporter le rapport
</Button>
</Box> </Box>
</CardContent> </CardContent>
</Card> </Card>
@ -297,4 +279,3 @@ export default function ContexteView() {
</Layout> </Layout>
) )
} }

View File

@ -78,9 +78,7 @@ export default function ExtractionView() {
if (!currentResult) { if (!currentResult) {
return ( return (
<Layout> <Layout>
<Alert severity="error"> <Alert severity="error">Erreur: Résultat d'extraction non trouvé.</Alert>
Erreur: Résultat d'extraction non trouvé.
</Alert>
</Layout> </Layout>
) )
} }
@ -97,11 +95,7 @@ export default function ExtractionView() {
{/* Navigation */} {/* Navigation */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<IconButton <IconButton onClick={goToPrevious} disabled={!hasPrev} color="primary">
onClick={goToPrevious}
disabled={!hasPrev}
color="primary"
>
<NavigateBefore /> <NavigateBefore />
</IconButton> </IconButton>
@ -109,11 +103,7 @@ export default function ExtractionView() {
Document {currentIndex + 1} sur {folderResults.length} Document {currentIndex + 1} sur {folderResults.length}
</Typography> </Typography>
<IconButton <IconButton onClick={goToNext} disabled={!hasNext} color="primary">
onClick={goToNext}
disabled={!hasNext}
color="primary"
>
<NavigateNext /> <NavigateNext />
</IconButton> </IconButton>
</Box> </Box>
@ -122,10 +112,7 @@ export default function ExtractionView() {
<Stepper activeStep={currentIndex} alternativeLabel sx={{ mb: 3 }}> <Stepper activeStep={currentIndex} alternativeLabel sx={{ mb: 3 }}>
{folderResults.map((result, index) => ( {folderResults.map((result, index) => (
<Step key={result.fileHash}> <Step key={result.fileHash}>
<StepLabel <StepLabel onClick={() => gotoResult(index)} sx={{ cursor: 'pointer' }}>
onClick={() => gotoResult(index)}
sx={{ cursor: 'pointer' }}
>
{result.document.fileName} {result.document.fileName}
</StepLabel> </StepLabel>
</Step> </Step>
@ -138,14 +125,8 @@ export default function ExtractionView() {
<CardContent> <CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Description color="primary" /> <Description color="primary" />
<Typography variant="h6"> <Typography variant="h6">{extraction.document.fileName}</Typography>
{extraction.document.fileName} <Chip label={extraction.document.mimeType} size="small" variant="outlined" />
</Typography>
<Chip
label={extraction.document.mimeType}
size="small"
variant="outlined"
/>
<Chip <Chip
label={`${(extraction.document.fileSize / 1024 / 1024).toFixed(2)} MB`} label={`${(extraction.document.fileSize / 1024 / 1024).toFixed(2)} MB`}
size="small" size="small"
@ -204,10 +185,7 @@ export default function ExtractionView() {
<List dense> <List dense>
{extraction.extraction.entities.persons.map((person, index) => ( {extraction.extraction.entities.persons.map((person, index) => (
<ListItem key={index}> <ListItem key={index}>
<ListItemText <ListItemText primary={person} secondary="Personne détectée" />
primary={`${person.firstName} ${person.lastName}`}
secondary={`Confiance: ${Math.round(person.confidence * 100)}%`}
/>
</ListItem> </ListItem>
))} ))}
</List> </List>
@ -226,10 +204,7 @@ export default function ExtractionView() {
<List dense> <List dense>
{extraction.extraction.entities.addresses.map((address, index) => ( {extraction.extraction.entities.addresses.map((address, index) => (
<ListItem key={index}> <ListItem key={index}>
<ListItemText <ListItemText primary={address} secondary="Adresse détectée" />
primary={`${address.street}, ${address.city} ${address.postalCode}`}
secondary={`Confiance: ${Math.round(address.confidence * 100)}%`}
/>
</ListItem> </ListItem>
))} ))}
</List> </List>
@ -248,10 +223,7 @@ export default function ExtractionView() {
<List dense> <List dense>
{extraction.extraction.entities.companies.map((company, index) => ( {extraction.extraction.entities.companies.map((company, index) => (
<ListItem key={index}> <ListItem key={index}>
<ListItemText <ListItemText primary={company} secondary="Entreprise détectée" />
primary={company.name}
secondary={`Confiance: ${Math.round(company.confidence * 100)}%`}
/>
</ListItem> </ListItem>
))} ))}
</List> </List>
@ -276,10 +248,12 @@ export default function ExtractionView() {
<strong>Hash du fichier:</strong> {extraction.fileHash} <strong>Hash du fichier:</strong> {extraction.fileHash}
</Typography> </Typography>
<Typography variant="body2"> <Typography variant="body2">
<strong>Timestamp:</strong> {new Date(extraction.status.timestamp).toLocaleString()} <strong>Timestamp:</strong>{' '}
{new Date(extraction.status.timestamp).toLocaleString()}
</Typography> </Typography>
<Typography variant="body2"> <Typography variant="body2">
<strong>Confiance globale:</strong> {(extraction.metadata.quality.globalConfidence * 100).toFixed(1)}% <strong>Confiance globale:</strong>{' '}
{(extraction.metadata.quality.globalConfidence * 100).toFixed(1)}%
</Typography> </Typography>
</Box> </Box>
</AccordionDetails> </AccordionDetails>

View File

@ -20,7 +20,7 @@ import {
DialogContent, DialogContent,
DialogActions, DialogActions,
IconButton, IconButton,
Tooltip Tooltip,
} from '@mui/material' } from '@mui/material'
import { import {
CloudUpload, CloudUpload,
@ -33,10 +33,16 @@ import {
PictureAsPdf, PictureAsPdf,
FolderOpen, FolderOpen,
Add, Add,
ContentCopy ContentCopy,
} from '@mui/icons-material' } from '@mui/icons-material'
import { useAppDispatch, useAppSelector } from '../store' 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 { Layout } from '../components/Layout'
import { FilePreview } from '../components/FilePreview' import { FilePreview } from '../components/FilePreview'
import type { Document } from '../types' import type { Document } from '../types'
@ -109,7 +115,7 @@ export default function UploadView() {
// Attendre que tous les fichiers soient traités // Attendre que tous les fichiers soient traités
await Promise.all(uploadPromises) await Promise.all(uploadPromises)
}, },
[dispatch, currentFolderHash] [dispatch, currentFolderHash],
) )
const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive } = useDropzone({
@ -149,15 +155,12 @@ export default function UploadView() {
// Bootstrap maintenant géré dans App.tsx // Bootstrap maintenant géré dans App.tsx
const getFileIcon = (mimeType: string) => { const getFileIcon = (mimeType: string) => {
if (mimeType.includes('pdf')) return <PictureAsPdf color="error" /> if (mimeType.includes('pdf')) return <PictureAsPdf color="error" />
if (mimeType.includes('image')) return <Image color="primary" /> if (mimeType.includes('image')) return <Image color="primary" />
return <Description color="action" /> return <Description color="action" />
} }
return ( return (
<Layout> <Layout>
<Typography variant="h4" gutterBottom> <Typography variant="h4" gutterBottom>
@ -165,8 +168,23 @@ export default function UploadView() {
</Typography> </Typography>
{/* En-tête avec hash du dossier et boutons */} {/* En-tête avec hash du dossier et boutons */}
<Box sx={{ mb: 3, p: 2, bgcolor: 'grey.50', borderRadius: 1, border: '1px solid', borderColor: 'grey.200' }}> <Box
<Box display="flex" alignItems="center" justifyContent="space-between" flexWrap="wrap" gap={2}> sx={{
mb: 3,
p: 2,
bgcolor: 'grey.50',
borderRadius: 1,
border: '1px solid',
borderColor: 'grey.200',
}}
>
<Box
display="flex"
alignItems="center"
justifyContent="space-between"
flexWrap="wrap"
gap={2}
>
<Box display="flex" alignItems="center" gap={2}> <Box display="flex" alignItems="center" gap={2}>
<Typography variant="h6" color="text.secondary"> <Typography variant="h6" color="text.secondary">
Dossier actuel : Dossier actuel :
@ -180,7 +198,7 @@ export default function UploadView() {
px: 1, px: 1,
py: 0.5, py: 0.5,
borderRadius: 1, borderRadius: 1,
fontSize: '0.875rem' fontSize: '0.875rem',
}} }}
> >
{currentFolderHash || 'Aucun dossier sélectionné'} {currentFolderHash || 'Aucun dossier sélectionné'}
@ -239,12 +257,15 @@ export default function UploadView() {
? 'Déposez les fichiers ici...' ? 'Déposez les fichiers ici...'
: 'Glissez-déposez vos documents ou cliquez pour sélectionner'} : 'Glissez-déposez vos documents ou cliquez pour sélectionner'}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}> <Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
>
Formats acceptés: PDF, PNG, JPG, JPEG, TIFF Formats acceptés: PDF, PNG, JPG, JPEG, TIFF
</Typography> </Typography>
</Paper> </Paper>
{error && ( {error && (
<Alert severity="error" sx={{ mt: 2 }}> <Alert severity="error" sx={{ mt: 2 }}>
{error} {error}
@ -263,9 +284,7 @@ export default function UploadView() {
{documents.map((doc, index) => ( {documents.map((doc, index) => (
<div key={`${doc.id}-${index}`}> <div key={`${doc.id}-${index}`}>
<ListItem> <ListItem>
<ListItemIcon> <ListItemIcon>{getFileIcon(doc.mimeType)}</ListItemIcon>
{getFileIcon(doc.mimeType)}
</ListItemIcon>
<ListItemText <ListItemText
primary={ primary={
<Box> <Box>
@ -280,7 +299,7 @@ export default function UploadView() {
display: '-webkit-box', display: '-webkit-box',
WebkitLineClamp: 2, WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical', WebkitBoxOrient: 'vertical',
maxWidth: { xs: '200px', sm: '300px', md: '400px' } maxWidth: { xs: '200px', sm: '300px', md: '400px' },
}} }}
> >
{doc.name} {doc.name}
@ -290,13 +309,15 @@ export default function UploadView() {
<Chip <Chip
label={doc.status} label={doc.status}
size="small" size="small"
color={getStatusColor(doc.status) as 'success' | 'error' | 'warning' | 'default'} color={
/> getStatusColor(doc.status) as
<Chip | 'success'
label={doc.mimeType} | 'error'
size="small" | 'warning'
variant="outlined" | 'default'
}
/> />
<Chip label={doc.mimeType} size="small" variant="outlined" />
<Chip <Chip
label={`${(doc.size / 1024 / 1024).toFixed(2)} MB`} label={`${(doc.size / 1024 / 1024).toFixed(2)} MB`}
size="small" size="small"
@ -336,10 +357,7 @@ export default function UploadView() {
{/* Aperçu du document */} {/* Aperçu du document */}
{previewDocument && ( {previewDocument && (
<FilePreview <FilePreview document={previewDocument} onClose={() => setPreviewDocument(null)} />
document={previewDocument}
onClose={() => setPreviewDocument(null)}
/>
)} )}
{/* Dialogue pour charger un dossier existant */} {/* Dialogue pour charger un dossier existant */}
@ -352,7 +370,8 @@ export default function UploadView() {
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
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.
</Typography> </Typography>
<TextField <TextField
autoFocus autoFocus
@ -368,9 +387,7 @@ export default function UploadView() {
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setDialogOpen(false)}> <Button onClick={() => setDialogOpen(false)}>Annuler</Button>
Annuler
</Button>
<Button <Button
onClick={handleLoadFolder} onClick={handleLoadFolder}
variant="contained" variant="contained"

View File

@ -24,21 +24,22 @@ console.log('📸 Image trouvée:', path.basename(imagePath))
// Fonction d'extraction OCR simple // Fonction d'extraction OCR simple
async function extractTextFromImage(imagePath) { async function extractTextFromImage(imagePath) {
console.log('\n🚀 Démarrage de l\'extraction OCR...') console.log("\n🚀 Démarrage de l'extraction OCR...")
const worker = await createWorker('fra+eng') const worker = await createWorker('fra+eng')
try { try {
console.log('⏳ Extraction en cours...') console.log('⏳ Extraction en cours...')
const { data: { text } } = await worker.recognize(imagePath) const {
data: { text },
} = await worker.recognize(imagePath)
console.log('\n📄 TEXTE EXTRAIT:') console.log('\n📄 TEXTE EXTRAIT:')
console.log('=' .repeat(50)) console.log('='.repeat(50))
console.log(text) console.log(text)
console.log('=' .repeat(50)) console.log('='.repeat(50))
return text return text
} catch (error) { } catch (error) {
console.error('❌ Erreur OCR:', error.message) console.error('❌ Erreur OCR:', error.message)
return null return null
@ -50,7 +51,7 @@ async function extractTextFromImage(imagePath) {
// Fonction d'analyse simple du texte // Fonction d'analyse simple du texte
function analyzeText(text) { function analyzeText(text) {
console.log('\n🔍 ANALYSE DU TEXTE:') console.log('\n🔍 ANALYSE DU TEXTE:')
console.log('=' .repeat(50)) console.log('='.repeat(50))
if (!text) { if (!text) {
console.log('❌ Aucun texte à analyser') console.log('❌ Aucun texte à analyser')
@ -61,8 +62,8 @@ function analyzeText(text) {
// Recherche de noms (patterns généraux) // Recherche de noms (patterns généraux)
const namePatterns = [ const namePatterns = [
/([A-Z][a-zà-öø-ÿ'\-]+\s+[A-Z][a-zà-öø-ÿ'\-]+)/g, // Prénom Nom /([A-Z][a-zà-öø-ÿ'\-]+\s+[A-Z][a-zà-öø-ÿ'\-]+)/g, // Prénom Nom
/([A-Z][A-ZÀ-ÖØ-öø-ÿ\s\-']{2,30})/g // Noms en majuscules /([A-Z][A-ZÀ-ÖØ-öø-ÿ\s\-']{2,30})/g, // Noms en majuscules
] ]
const foundNames = new Set() const foundNames = new Set()
@ -96,7 +97,7 @@ function analyzeText(text) {
// Recherche spécifique pour NICOLAS et CANTU // Recherche spécifique pour NICOLAS et CANTU
console.log('\n🔍 RECHERCHE SPÉCIFIQUE:') console.log('\n🔍 RECHERCHE SPÉCIFIQUE:')
console.log('=' .repeat(50)) console.log('='.repeat(50))
const hasNicolas = /nicolas/i.test(text) const hasNicolas = /nicolas/i.test(text)
const hasCantu = /cantu/i.test(text) const hasCantu = /cantu/i.test(text)
@ -127,7 +128,7 @@ function analyzeText(text) {
hasNicolas, hasNicolas,
hasCantu, hasCantu,
hasNicolasCantu, hasNicolasCantu,
hasCNIKeywords hasCNIKeywords,
} }
} }
@ -138,14 +139,13 @@ async function main() {
const results = analyzeText(text) const results = analyzeText(text)
console.log('\n🎯 RÉSULTATS FINAUX:') console.log('\n🎯 RÉSULTATS FINAUX:')
console.log('=' .repeat(50)) console.log('='.repeat(50))
console.log(`📄 Texte extrait: ${results ? '✅' : '❌'}`) console.log(`📄 Texte extrait: ${results ? '✅' : '❌'}`)
console.log(`👥 Noms trouvés: ${results?.names.length || 0}`) console.log(`👥 Noms trouvés: ${results?.names.length || 0}`)
console.log(`🆔 CNI trouvés: ${results?.cniNumbers.length || 0}`) console.log(`🆔 CNI trouvés: ${results?.cniNumbers.length || 0}`)
console.log(`🔍 NICOLAS: ${results?.hasNicolas ? '✅' : '❌'}`) console.log(`🔍 NICOLAS: ${results?.hasNicolas ? '✅' : '❌'}`)
console.log(`🔍 CANTU: ${results?.hasCantu ? '✅' : '❌'}`) console.log(`🔍 CANTU: ${results?.hasCantu ? '✅' : '❌'}`)
console.log(`📋 Type CNI: ${results?.hasCNIKeywords ? '✅' : '❌'}`) console.log(`📋 Type CNI: ${results?.hasCNIKeywords ? '✅' : '❌'}`)
} catch (error) { } catch (error) {
console.error('❌ Erreur:', error.message) console.error('❌ Erreur:', error.message)
} }

View File

@ -9,7 +9,7 @@ const { createWorker } = require('tesseract.js')
const path = require('path') const path = require('path')
const fs = require('fs') const fs = require('fs')
console.log('🔍 Analyse directe de l\'image CNI') console.log("🔍 Analyse directe de l'image CNI")
console.log('==================================') console.log('==================================')
const imagePath = path.join(__dirname, 'test-files', 'IMG_20250902_162159.jpg') const imagePath = path.join(__dirname, 'test-files', 'IMG_20250902_162159.jpg')
@ -26,14 +26,28 @@ console.log('📸 Image trouvée:', imagePath)
function correctOCRText(text) { function correctOCRText(text) {
const corrections = { const corrections = {
// Corrections pour "Nicolas" // Corrections pour "Nicolas"
'N1colas': 'Nicolas', 'Nicol@s': 'Nicolas', 'Nico1as': 'Nicolas', N1colas: 'Nicolas',
'Nico1@s': 'Nicolas', 'N1co1as': 'Nicolas', 'N1co1@s': 'Nicolas', 'Nicol@s': 'Nicolas',
'Nico1as': 'Nicolas', 'N1col@s': 'Nicolas', 'N1co1as': 'Nicolas', Nico1as: 'Nicolas',
'Nico1@s': 'Nicolas',
N1co1as: 'Nicolas',
'N1co1@s': 'Nicolas',
Nico1as: 'Nicolas',
'N1col@s': 'Nicolas',
N1co1as: 'Nicolas',
// Corrections pour "Cantu" // Corrections pour "Cantu"
'C@ntu': 'Cantu', 'CantU': 'Cantu', 'C@ntU': 'Cantu', 'C@ntu': 'Cantu',
'Cant0': 'Cantu', 'C@nt0': 'Cantu', 'CantU': 'Cantu', CantU: 'Cantu',
'C@ntU': 'Cantu',
Cant0: 'Cantu',
'C@nt0': 'Cantu',
CantU: 'Cantu',
// Autres corrections courantes // Autres corrections courantes
'0': 'o', '1': 'l', '5': 's', '@': 'a', '3': 'e' 0: 'o',
1: 'l',
5: 's',
'@': 'a',
3: 'e',
} }
let correctedText = text let correctedText = text
@ -47,7 +61,7 @@ function correctOCRText(text) {
// Fonction d'extraction des entités // Fonction d'extraction des entités
function extractEntitiesFromText(text) { function extractEntitiesFromText(text) {
console.log(`\n🔍 Analyse du texte extrait (${text.length} caractères)`) console.log(`\n🔍 Analyse du texte extrait (${text.length} caractères)`)
console.log('=' .repeat(50)) console.log('='.repeat(50))
const correctedText = correctOCRText(text) const correctedText = correctOCRText(text)
if (correctedText !== text) { if (correctedText !== text) {
@ -57,7 +71,7 @@ function extractEntitiesFromText(text) {
const entities = { const entities = {
identities: [], identities: [],
cniNumbers: [], cniNumbers: [],
documentType: 'Document' documentType: 'Document',
} }
// Patterns pour détecter "Nicolas Cantu" // Patterns pour détecter "Nicolas Cantu"
@ -70,7 +84,7 @@ function extractEntitiesFromText(text) {
// Recherche de "Cantu" seul // Recherche de "Cantu" seul
/([Cc][a@][n][t][u])/gi, /([Cc][a@][n][t][u])/gi,
// Patterns généraux pour noms // Patterns généraux pour noms
/([A-Z][a-zà-öø-ÿ'\-]+\s+[A-Z][a-zà-öø-ÿ'\-]+)/g /([A-Z][a-zà-öø-ÿ'\-]+\s+[A-Z][a-zà-öø-ÿ'\-]+)/g,
] ]
// Extraction des noms // Extraction des noms
@ -102,24 +116,26 @@ function extractEntitiesFromText(text) {
// Fonction principale d'analyse // Fonction principale d'analyse
async function analyzeImage() { async function analyzeImage() {
console.log('\n🚀 Démarrage de l\'analyse OCR...') console.log("\n🚀 Démarrage de l'analyse OCR...")
const worker = await createWorker('fra+eng') const worker = await createWorker('fra+eng')
try { try {
console.log('⏳ Extraction du texte en cours...') console.log('⏳ Extraction du texte en cours...')
const { data: { text } } = await worker.recognize(imagePath) const {
data: { text },
} = await worker.recognize(imagePath)
console.log('\n📄 Texte brut extrait:') console.log('\n📄 Texte brut extrait:')
console.log('=' .repeat(50)) console.log('='.repeat(50))
console.log(text) console.log(text)
console.log('=' .repeat(50)) console.log('='.repeat(50))
// Extraction des entités // Extraction des entités
const entities = extractEntitiesFromText(text) const entities = extractEntitiesFromText(text)
console.log('\n🎯 RÉSULTATS DE L\'ANALYSE:') console.log("\n🎯 RÉSULTATS DE L'ANALYSE:")
console.log('=' .repeat(50)) console.log('='.repeat(50))
console.log(`📋 Type de document: ${entities.documentType}`) console.log(`📋 Type de document: ${entities.documentType}`)
console.log(`👥 Identités trouvées: ${entities.identities.length}`) console.log(`👥 Identités trouvées: ${entities.identities.length}`)
entities.identities.forEach((identity, index) => { entities.identities.forEach((identity, index) => {
@ -132,7 +148,7 @@ async function analyzeImage() {
// Recherche spécifique pour CANTU et NICOLAS // Recherche spécifique pour CANTU et NICOLAS
console.log('\n🔍 RECHERCHE SPÉCIFIQUE:') console.log('\n🔍 RECHERCHE SPÉCIFIQUE:')
console.log('=' .repeat(50)) console.log('='.repeat(50))
const hasNicolas = /nicolas/i.test(text) const hasNicolas = /nicolas/i.test(text)
const hasCantu = /cantu/i.test(text) const hasCantu = /cantu/i.test(text)
@ -151,18 +167,19 @@ async function analyzeImage() {
} }
}) })
} }
} catch (error) { } catch (error) {
console.error('❌ Erreur lors de l\'analyse:', error.message) console.error("❌ Erreur lors de l'analyse:", error.message)
} finally { } finally {
await worker.terminate() await worker.terminate()
} }
} }
// Exécuter l'analyse // Exécuter l'analyse
analyzeImage().then(() => { analyzeImage()
console.log('\n🎉 Analyse terminée !') .then(() => {
}).catch(error => { console.log('\n🎉 Analyse terminée !')
console.error('❌ Erreur:', error.message) })
process.exit(1) .catch((error) => {
}) console.error('❌ Erreur:', error.message)
process.exit(1)
})

View File

@ -1,59 +1,59 @@
const fs = require('fs'); const fs = require('fs')
const path = require('path'); const path = require('path')
const crypto = require('crypto'); const crypto = require('crypto')
// Fonction pour générer un hash de dossier // Fonction pour générer un hash de dossier
function generateFolderHash() { function generateFolderHash() {
return crypto.randomBytes(16).toString('hex'); return crypto.randomBytes(16).toString('hex')
} }
// Fonction pour créer la structure de dossiers // Fonction pour créer la structure de dossiers
function createFolderStructure(folderHash) { function createFolderStructure(folderHash) {
console.log(`[FOLDER] Création de la structure pour le hash: ${folderHash}`); console.log(`[FOLDER] Création de la structure pour le hash: ${folderHash}`)
console.log(`[FOLDER] Répertoire de travail: ${process.cwd()}`); console.log(`[FOLDER] Répertoire de travail: ${process.cwd()}`)
// Créer les dossiers racines s'ils n'existent pas // Créer les dossiers racines s'ils n'existent pas
const uploadsDir = 'uploads'; const uploadsDir = 'uploads'
const cacheDir = 'cache'; const cacheDir = 'cache'
console.log(`[FOLDER] Vérification de l'existence de ${uploadsDir}: ${fs.existsSync(uploadsDir)}`); console.log(`[FOLDER] Vérification de l'existence de ${uploadsDir}: ${fs.existsSync(uploadsDir)}`)
console.log(`[FOLDER] Vérification de l'existence de ${cacheDir}: ${fs.existsSync(cacheDir)}`); console.log(`[FOLDER] Vérification de l'existence de ${cacheDir}: ${fs.existsSync(cacheDir)}`)
if (!fs.existsSync(uploadsDir)) { if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true }); fs.mkdirSync(uploadsDir, { recursive: true })
console.log(`[FOLDER] Dossier racine créé: ${uploadsDir}`); console.log(`[FOLDER] Dossier racine créé: ${uploadsDir}`)
} }
if (!fs.existsSync(cacheDir)) { if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true }); fs.mkdirSync(cacheDir, { recursive: true })
console.log(`[FOLDER] Dossier racine créé: ${cacheDir}`); console.log(`[FOLDER] Dossier racine créé: ${cacheDir}`)
} }
const folderPath = path.join(uploadsDir, folderHash); const folderPath = path.join(uploadsDir, folderHash)
const cachePath = path.join(cacheDir, folderHash); const cachePath = path.join(cacheDir, folderHash)
console.log(`[FOLDER] Chemin du dossier uploads: ${folderPath}`); console.log(`[FOLDER] Chemin du dossier uploads: ${folderPath}`)
console.log(`[FOLDER] Chemin du dossier cache: ${cachePath}`); console.log(`[FOLDER] Chemin du dossier cache: ${cachePath}`)
if (!fs.existsSync(folderPath)) { if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath, { recursive: true }); fs.mkdirSync(folderPath, { recursive: true })
console.log(`[FOLDER] Dossier uploads créé: ${folderPath}`); console.log(`[FOLDER] Dossier uploads créé: ${folderPath}`)
} }
if (!fs.existsSync(cachePath)) { if (!fs.existsSync(cachePath)) {
fs.mkdirSync(cachePath, { recursive: true }); fs.mkdirSync(cachePath, { recursive: true })
console.log(`[FOLDER] Dossier cache créé: ${cachePath}`); console.log(`[FOLDER] Dossier cache créé: ${cachePath}`)
} }
return { folderPath, cachePath }; return { folderPath, cachePath }
} }
// Test // Test
console.log('=== Test de création de dossier ==='); console.log('=== Test de création de dossier ===')
const folderHash = generateFolderHash(); const folderHash = generateFolderHash()
console.log(`Hash généré: ${folderHash}`); console.log(`Hash généré: ${folderHash}`)
const result = createFolderStructure(folderHash); const result = createFolderStructure(folderHash)
console.log('Résultat:', result); console.log('Résultat:', result)
console.log('\n=== Vérification des dossiers créés ==='); console.log('\n=== Vérification des dossiers créés ===')
console.log('Dossiers uploads:', fs.readdirSync('uploads')); console.log('Dossiers uploads:', fs.readdirSync('uploads'))
console.log('Dossiers cache:', fs.readdirSync('cache')); console.log('Dossiers cache:', fs.readdirSync('cache'))

View File

@ -1,48 +1,55 @@
const fs = require('fs'); const fs = require('fs')
const FormData = require('form-data'); const FormData = require('form-data')
const fetch = require('node-fetch'); const fetch = require('node-fetch')
async function testBackendFormat() { async function testBackendFormat() {
try { try {
console.log('🧪 Test du format JSON du backend...'); console.log('🧪 Test du format JSON du backend...')
const formData = new FormData(); const formData = new FormData()
formData.append('document', fs.createReadStream('test-files/IMG_20250902_162159.jpg')); formData.append('document', fs.createReadStream('test-files/IMG_20250902_162159.jpg'))
const response = await fetch('http://localhost:3001/api/extract', { const response = await fetch('http://localhost:3001/api/extract', {
method: 'POST', method: 'POST',
body: formData body: formData,
}); })
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`); throw new Error(`HTTP ${response.status}: ${response.statusText}`)
} }
const result = await response.json(); const result = await response.json()
console.log('✅ Réponse reçue du backend'); console.log('✅ Réponse reçue du backend')
console.log('📋 Structure de la réponse:'); console.log('📋 Structure de la réponse:')
console.log('- document:', !!result.document); console.log('- document:', !!result.document)
console.log('- classification:', !!result.classification); console.log('- classification:', !!result.classification)
console.log('- extraction:', !!result.extraction); console.log('- extraction:', !!result.extraction)
console.log('- metadata:', !!result.metadata); console.log('- metadata:', !!result.metadata)
console.log('- status:', !!result.status); console.log('- status:', !!result.status)
console.log('\n📊 Données extraites:'); console.log('\n📊 Données extraites:')
console.log('- Type de document:', result.classification?.documentType); console.log('- Type de document:', result.classification?.documentType)
console.log('- Confiance globale:', result.metadata?.quality?.globalConfidence); console.log('- Confiance globale:', result.metadata?.quality?.globalConfidence)
console.log('- Personnes:', result.extraction?.entities?.persons?.length || 0); console.log('- Personnes:', result.extraction?.entities?.persons?.length || 0)
console.log('- Sociétés:', result.extraction?.entities?.companies?.length || 0); console.log('- Sociétés:', result.extraction?.entities?.companies?.length || 0)
console.log('- Adresses:', result.extraction?.entities?.addresses?.length || 0); console.log('- Adresses:', result.extraction?.entities?.addresses?.length || 0)
console.log('- Dates:', result.extraction?.entities?.dates?.length || 0); console.log('- Dates:', result.extraction?.entities?.dates?.length || 0)
console.log('- Références:', result.extraction?.entities?.references?.length || 0); console.log('- Références:', result.extraction?.entities?.references?.length || 0)
console.log('\n🎯 Format conforme au standard:',
result.document && result.classification && result.extraction && result.metadata && result.status ? '✅ OUI' : '❌ NON');
console.log(
'\n🎯 Format conforme au standard:',
result.document &&
result.classification &&
result.extraction &&
result.metadata &&
result.status
? '✅ OUI'
: '❌ NON',
)
} catch (error) { } catch (error) {
console.error('❌ Erreur:', error.message); console.error('❌ Erreur:', error.message)
} }
} }
testBackendFormat(); testBackendFormat()

View File

@ -9,17 +9,14 @@ const http = require('http')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
console.log('🌐 Test de l\'interface web pour l\'analyse CNI') console.log("🌐 Test de l'interface web pour l'analyse CNI")
console.log('===============================================') console.log('===============================================')
// Vérifier que les images existent // Vérifier que les images existent
const images = [ const images = ['IMG_20250902_162159.jpg', 'IMG_20250902_162210.jpg']
'IMG_20250902_162159.jpg',
'IMG_20250902_162210.jpg'
]
console.log('📸 Vérification des images de test:') console.log('📸 Vérification des images de test:')
images.forEach(image => { images.forEach((image) => {
const imagePath = path.join(__dirname, 'test-files', image) const imagePath = path.join(__dirname, 'test-files', image)
if (fs.existsSync(imagePath)) { if (fs.existsSync(imagePath)) {
const stats = fs.statSync(imagePath) const stats = fs.statSync(imagePath)
@ -34,12 +31,14 @@ function makeRequest(url, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const req = http.request(url, options, (res) => { const req = http.request(url, options, (res) => {
let data = '' let data = ''
res.on('data', chunk => data += chunk) res.on('data', (chunk) => (data += chunk))
res.on('end', () => resolve({ res.on('end', () =>
statusCode: res.statusCode, resolve({
headers: res.headers, statusCode: res.statusCode,
data headers: res.headers,
})) data,
}),
)
}) })
req.on('error', reject) req.on('error', reject)
req.setTimeout(10000, () => reject(new Error('Timeout'))) req.setTimeout(10000, () => reject(new Error('Timeout')))
@ -49,7 +48,7 @@ function makeRequest(url, options = {}) {
// Test de l'interface frontend // Test de l'interface frontend
async function testFrontendInterface() { async function testFrontendInterface() {
console.log('\n🌐 Test de l\'interface frontend...') console.log("\n🌐 Test de l'interface frontend...")
try { try {
const response = await makeRequest('http://localhost:5174') const response = await makeRequest('http://localhost:5174')
@ -90,7 +89,7 @@ async function testBackendServer() {
// Test d'accès aux images via le serveur // Test d'accès aux images via le serveur
async function testImageAccess() { async function testImageAccess() {
console.log('\n📸 Test d\'accès aux images...') console.log("\n📸 Test d'accès aux images...")
for (const image of images) { for (const image of images) {
try { try {
@ -109,40 +108,40 @@ async function testImageAccess() {
// Instructions pour l'analyse manuelle // Instructions pour l'analyse manuelle
function printManualInstructions() { function printManualInstructions() {
console.log('\n📋 INSTRUCTIONS POUR L\'ANALYSE MANUELLE:') console.log("\n📋 INSTRUCTIONS POUR L'ANALYSE MANUELLE:")
console.log('=' .repeat(60)) console.log('='.repeat(60))
console.log('1. Ouvrez votre navigateur et allez sur: http://localhost:5174') console.log('1. Ouvrez votre navigateur et allez sur: http://localhost:5174')
console.log('2. Ouvrez la console développeur (F12)') console.log('2. Ouvrez la console développeur (F12)')
console.log('3. Dans l\'interface, chargez l\'une des images de test:') console.log("3. Dans l'interface, chargez l'une des images de test:")
images.forEach((image, index) => { images.forEach((image, index) => {
console.log(` ${index + 1}. ${image}`) console.log(` ${index + 1}. ${image}`)
}) })
console.log('4. Observez les logs d\'extraction dans la console du navigateur') console.log("4. Observez les logs d'extraction dans la console du navigateur")
console.log('5. Vérifiez les résultats d\'extraction dans l\'interface') console.log("5. Vérifiez les résultats d'extraction dans l'interface")
console.log('\n🔍 ÉLÉMENTS À RECHERCHER:') console.log('\n🔍 ÉLÉMENTS À RECHERCHER:')
console.log('=' .repeat(60)) console.log('='.repeat(60))
console.log('• Nom de famille (en MAJUSCULES)') console.log('• Nom de famille (en MAJUSCULES)')
console.log('• Prénom(s)') console.log('• Prénom(s)')
console.log('• Numéro de carte d\'identité (format: 2 lettres + 6 chiffres)') console.log("• Numéro de carte d'identité (format: 2 lettres + 6 chiffres)")
console.log('• Date de naissance') console.log('• Date de naissance')
console.log('• Lieu de naissance') console.log('• Lieu de naissance')
console.log('• Spécifiquement: "NICOLAS" et "CANTU"') console.log('• Spécifiquement: "NICOLAS" et "CANTU"')
console.log('\n⚙ CONFIGURATION RECOMMANDÉE:') console.log('\n⚙ CONFIGURATION RECOMMANDÉE:')
console.log('=' .repeat(60)) console.log('='.repeat(60))
console.log('• Mode sans LLM: VITE_DISABLE_LLM=true') console.log('• Mode sans LLM: VITE_DISABLE_LLM=true')
console.log('• Extraction par règles: VITE_USE_RULE_NER=true') console.log('• Extraction par règles: VITE_USE_RULE_NER=true')
console.log('• Pas de clé API OpenAI requise') console.log('• Pas de clé API OpenAI requise')
console.log('\n📊 LOGS ATTENDUS DANS LA CONSOLE:') console.log('\n📊 LOGS ATTENDUS DANS LA CONSOLE:')
console.log('=' .repeat(60)) console.log('='.repeat(60))
console.log('🔧 [CONFIG] Mode sans LLM activé: { useRuleNer: true, disableLLM: true, ... }') console.log('🔧 [CONFIG] Mode sans LLM activé: { useRuleNer: true, disableLLM: true, ... }')
console.log('🚀 [OCR] Début de l\'extraction OCR locale...') console.log("🚀 [OCR] Début de l'extraction OCR locale...")
console.log('⏳ [OCR] Progression: 30%') console.log('⏳ [OCR] Progression: 30%')
console.log('⏳ [OCR] Progression: 70%') console.log('⏳ [OCR] Progression: 70%')
console.log('✅ [OCR] Progression: 100% - Extraction terminée') console.log('✅ [OCR] Progression: 100% - Extraction terminée')
console.log('🔍 [NER] Début de l\'extraction par règles...') console.log("🔍 [NER] Début de l'extraction par règles...")
console.log('👥 [RULE-NER] Identités extraites: X') console.log('👥 [RULE-NER] Identités extraites: X')
console.log('🏠 [RULE-NER] Adresses extraites: X') console.log('🏠 [RULE-NER] Adresses extraites: X')
console.log('🆔 [RULE-NER] Numéros CNI détectés: X') console.log('🆔 [RULE-NER] Numéros CNI détectés: X')
@ -165,7 +164,7 @@ async function runTests() {
printManualInstructions() printManualInstructions()
console.log('\n🎯 RÉSUMÉ:') console.log('\n🎯 RÉSUMÉ:')
console.log('=' .repeat(60)) console.log('='.repeat(60))
console.log('✅ Interface frontend: Opérationnelle') console.log('✅ Interface frontend: Opérationnelle')
console.log('✅ Serveur backend: Opérationnel') console.log('✅ Serveur backend: Opérationnel')
console.log('✅ Images de test: Disponibles') console.log('✅ Images de test: Disponibles')
@ -173,17 +172,16 @@ async function runTests() {
console.log('💡 Pour analyser les images et rechercher CANTU/NICOLAS:') console.log('💡 Pour analyser les images et rechercher CANTU/NICOLAS:')
console.log(' 1. Ouvrez http://localhost:5174 dans votre navigateur') console.log(' 1. Ouvrez http://localhost:5174 dans votre navigateur')
console.log(' 2. Chargez une image de test') console.log(' 2. Chargez une image de test')
console.log(' 3. Observez les résultats dans la console et l\'interface') console.log(" 3. Observez les résultats dans la console et l'interface")
console.log('') console.log('')
console.log('🔍 Le système est configuré pour détecter:') console.log('🔍 Le système est configuré pour détecter:')
console.log(' • "NICOLAS" avec corrections OCR (N1colas, Nicol@s, etc.)') console.log(' • "NICOLAS" avec corrections OCR (N1colas, Nicol@s, etc.)')
console.log(' • "CANTU" avec corrections OCR (C@ntu, CantU, etc.)') console.log(' • "CANTU" avec corrections OCR (C@ntu, CantU, etc.)')
console.log(' • Numéros CNI au format 2 lettres + 6 chiffres') console.log(' • Numéros CNI au format 2 lettres + 6 chiffres')
console.log(' • Type de document CNI') console.log(' • Type de document CNI')
} else { } else {
console.log('\n❌ PROBLÈMES DÉTECTÉS:') console.log('\n❌ PROBLÈMES DÉTECTÉS:')
console.log('=' .repeat(60)) console.log('='.repeat(60))
if (!frontendOk) { if (!frontendOk) {
console.log('❌ Interface frontend non accessible') console.log('❌ Interface frontend non accessible')
console.log(' → Vérifiez que le serveur de développement est démarré') console.log(' → Vérifiez que le serveur de développement est démarré')
@ -198,9 +196,11 @@ async function runTests() {
} }
// Exécuter les tests // Exécuter les tests
runTests().then(() => { runTests()
console.log('\n🎉 Tests terminés !') .then(() => {
}).catch(error => { console.log('\n🎉 Tests terminés !')
console.error('❌ Erreur lors des tests:', error.message) })
process.exit(1) .catch((error) => {
}) console.error('❌ Erreur lors des tests:', error.message)
process.exit(1)
})

View File

@ -1,5 +1,10 @@
import { describe, it, expect, vi, beforeEach } from 'vitest' import { describe, it, expect, vi, beforeEach } from 'vitest'
import { getTestFilesList, loadTestFile, filterSupportedFiles, type TestFileInfo } from '../src/services/testFilesApi' import {
getTestFilesList,
loadTestFile,
filterSupportedFiles,
type TestFileInfo,
} from '../src/services/testFilesApi'
// Mock fetch // Mock fetch
global.fetch = vi.fn() global.fetch = vi.fn()
@ -13,17 +18,47 @@ describe('testFilesApi', () => {
it('devrait retourner une liste de fichiers de test', async () => { it('devrait retourner une liste de fichiers de test', async () => {
// Mock des réponses fetch pour les fichiers connus // Mock des réponses fetch pour les fichiers connus
const mockResponses = [ const mockResponses = [
{ ok: true, headers: new Map([['content-length', '1024'], ['content-type', 'image/jpeg']]) }, {
{ ok: true, headers: new Map([['content-length', '2048'], ['content-type', 'application/pdf']]) }, ok: true,
headers: new Map([
['content-length', '1024'],
['content-type', 'image/jpeg'],
]),
},
{
ok: true,
headers: new Map([
['content-length', '2048'],
['content-type', 'application/pdf'],
]),
},
{ ok: false }, // Fichier non trouvé { ok: false }, // Fichier non trouvé
{ ok: true, headers: new Map([['content-length', '512'], ['content-type', 'text/plain']]) }, {
{ ok: true, headers: new Map([['content-length', '256'], ['content-type', 'text/markdown']]) } ok: true,
headers: new Map([
['content-length', '512'],
['content-type', 'text/plain'],
]),
},
{
ok: true,
headers: new Map([
['content-length', '256'],
['content-type', 'text/markdown'],
]),
},
] ]
// Mock fetch pour chaque fichier // Mock fetch pour chaque fichier
;(global.fetch as any).mockImplementation((url: string) => { ;(global.fetch as any).mockImplementation((url: string) => {
const fileName = url.split('/').pop() const fileName = url.split('/').pop()
const fileIndex = ['IMG_20250902_162159.jpg', 'IMG_20250902_162210.jpg', 'sample.md', 'sample.pdf', 'sample.txt'].indexOf(fileName!) const fileIndex = [
'IMG_20250902_162159.jpg',
'IMG_20250902_162210.jpg',
'sample.md',
'sample.pdf',
'sample.txt',
].indexOf(fileName!)
return Promise.resolve(mockResponses[fileIndex] || { ok: false }) return Promise.resolve(mockResponses[fileIndex] || { ok: false })
}) })
@ -33,7 +68,7 @@ describe('testFilesApi', () => {
expect(result[0]).toMatchObject({ expect(result[0]).toMatchObject({
name: 'IMG_20250902_162159.jpg', name: 'IMG_20250902_162159.jpg',
size: 1024, size: 1024,
type: 'image/jpeg' type: 'image/jpeg',
}) })
}) })
@ -51,7 +86,7 @@ describe('testFilesApi', () => {
const mockBlob = new Blob(['test content'], { type: 'text/plain' }) const mockBlob = new Blob(['test content'], { type: 'text/plain' })
const mockResponse = { const mockResponse = {
ok: true, ok: true,
blob: () => Promise.resolve(mockBlob) blob: () => Promise.resolve(mockBlob),
} }
;(global.fetch as any).mockResolvedValue(mockResponse) ;(global.fetch as any).mockResolvedValue(mockResponse)
@ -63,10 +98,10 @@ describe('testFilesApi', () => {
expect(result?.type).toBe('text/plain') expect(result?.type).toBe('text/plain')
}) })
it('devrait retourner null si le fichier n\'existe pas', async () => { it("devrait retourner null si le fichier n'existe pas", async () => {
const mockResponse = { const mockResponse = {
ok: false, ok: false,
status: 404 status: 404,
} }
;(global.fetch as any).mockResolvedValue(mockResponse) ;(global.fetch as any).mockResolvedValue(mockResponse)
@ -91,42 +126,62 @@ describe('testFilesApi', () => {
{ name: 'image.jpg', size: 2048, type: 'image/jpeg', lastModified: Date.now() }, { name: 'image.jpg', size: 2048, type: 'image/jpeg', lastModified: Date.now() },
{ name: 'text.txt', size: 512, type: 'text/plain', lastModified: Date.now() }, { name: 'text.txt', size: 512, type: 'text/plain', lastModified: Date.now() },
{ name: 'markdown.md', size: 256, type: 'text/markdown', lastModified: Date.now() }, { name: 'markdown.md', size: 256, type: 'text/markdown', lastModified: Date.now() },
{ name: 'document.docx', size: 4096, type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', lastModified: Date.now() }, {
name: 'document.docx',
size: 4096,
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
lastModified: Date.now(),
},
{ name: 'unsupported.xyz', size: 128, type: 'application/unknown', lastModified: Date.now() }, { name: 'unsupported.xyz', size: 128, type: 'application/unknown', lastModified: Date.now() },
{ name: 'image.png', size: 1536, type: 'image/png', lastModified: Date.now() } { name: 'image.png', size: 1536, type: 'image/png', lastModified: Date.now() },
] ]
it('devrait filtrer les fichiers supportés par type MIME', () => { it('devrait filtrer les fichiers supportés par type MIME', () => {
const result = filterSupportedFiles(testFiles) const result = filterSupportedFiles(testFiles)
expect(result).toHaveLength(6) // 6 fichiers supportés expect(result).toHaveLength(6) // 6 fichiers supportés
expect(result.map(f => f.name)).toEqual([ expect(result.map((f) => f.name)).toEqual([
'document.pdf', 'document.pdf',
'image.jpg', 'image.jpg',
'text.txt', 'text.txt',
'markdown.md', 'markdown.md',
'document.docx', 'document.docx',
'image.png' 'image.png',
]) ])
}) })
it('devrait filtrer les fichiers supportés par extension', () => { it('devrait filtrer les fichiers supportés par extension', () => {
const filesWithUnknownMime: TestFileInfo[] = [ const filesWithUnknownMime: TestFileInfo[] = [
{ name: 'document.pdf', size: 1024, type: 'application/octet-stream', lastModified: Date.now() }, {
{ name: 'image.jpg', size: 2048, type: 'application/octet-stream', lastModified: Date.now() }, name: 'document.pdf',
{ name: 'unsupported.xyz', size: 128, type: 'application/octet-stream', lastModified: Date.now() } size: 1024,
type: 'application/octet-stream',
lastModified: Date.now(),
},
{
name: 'image.jpg',
size: 2048,
type: 'application/octet-stream',
lastModified: Date.now(),
},
{
name: 'unsupported.xyz',
size: 128,
type: 'application/octet-stream',
lastModified: Date.now(),
},
] ]
const result = filterSupportedFiles(filesWithUnknownMime) const result = filterSupportedFiles(filesWithUnknownMime)
expect(result).toHaveLength(2) // 2 fichiers supportés par extension expect(result).toHaveLength(2) // 2 fichiers supportés par extension
expect(result.map(f => f.name)).toEqual(['document.pdf', 'image.jpg']) expect(result.map((f) => f.name)).toEqual(['document.pdf', 'image.jpg'])
}) })
it('devrait retourner un tableau vide si aucun fichier supporté', () => { it('devrait retourner un tableau vide si aucun fichier supporté', () => {
const unsupportedFiles: TestFileInfo[] = [ const unsupportedFiles: TestFileInfo[] = [
{ name: 'file1.xyz', size: 128, type: 'application/unknown', lastModified: Date.now() }, { name: 'file1.xyz', size: 128, type: 'application/unknown', lastModified: Date.now() },
{ name: 'file2.abc', size: 256, type: 'application/unknown', lastModified: Date.now() } { name: 'file2.abc', size: 256, type: 'application/unknown', lastModified: Date.now() },
] ]
const result = filterSupportedFiles(unsupportedFiles) const result = filterSupportedFiles(unsupportedFiles)

View File

@ -1 +1,4 @@
{"file_format_version": "1.0.0", "ICD": {"library_path": ".\\vk_swiftshader.dll", "api_version": "1.0.5"}} {
"file_format_version": "1.0.0",
"ICD": { "library_path": ".\\vk_swiftshader.dll", "api_version": "1.0.5" }
}