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:
parent
b18a3077a2
commit
883f49e2e2
12
CHANGELOG.md
12
CHANGELOG.md
@ -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_
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
88
backend/package-lock.json
generated
88
backend/package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
139
docs/ANALYSE_REPO.md
Normal 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ère‑plan avec logs et PID.
|
||||||
|
4. Vérifications:
|
||||||
|
- `curl http://127.0.0.1:3001/api/health` → 200 OK
|
||||||
|
- `curl https://ia.4nkweb.com/api/health` → 200 OK
|
||||||
|
- `curl https://ia.4nkweb.com/api/folders/7d99a85daf66a0081a0e881630e6b39b/results` → 200 OK
|
||||||
|
|
||||||
|
## Causes probables
|
||||||
|
|
||||||
|
- Processus backend arrêté (pas de listener sur 3001).
|
||||||
|
- Gestion incomplète des fichiers `.txt` côté backend (détection MIME), pouvant entraîner des erreurs avec Sharp.
|
||||||
|
|
||||||
|
## Recommandations de suivi
|
||||||
|
|
||||||
|
- Ajouter le mapping `.txt` → `text/plain` dans la détection MIME backend.
|
||||||
|
- Dans `/api/extract`, gérer explicitement les `.txt` (lecture directe) comme dans `processDocument`.
|
||||||
|
- Mettre à jour Multer vers 2.x (1.x est vulnérable et déprécié).
|
||||||
|
- Surveiller `backend.log` et ajouter une supervision (systemd/service manager).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Audit détaillé du dépôt 4NK_IA_front
|
||||||
|
|
||||||
|
#### Portée
|
||||||
|
|
||||||
|
- **Code analysé**: frontend React/TypeScript sous Vite, répertoire `/home/debian/4NK_IA_front`.
|
||||||
|
- **Commandes exécutées**: `npm ci`, `npm run lint`, `npm run mdlint`, `npm run test`, `npm run build`.
|
||||||
|
|
||||||
|
### Architecture et pile technique
|
||||||
|
|
||||||
|
- **Framework**: React 19 + TypeScript, Vite 7 (`vite.config.ts`).
|
||||||
|
- **UI**: MUI v7.
|
||||||
|
- **État**: Redux Toolkit + `react-redux` (`src/store/index.ts`, `src/store/documentSlice.ts`).
|
||||||
|
- **Routing**: React Router v7 avec code-splitting via `React.lazy`/`Suspense` (`src/router/index.tsx`).
|
||||||
|
- **Services**: couche d’abstraction HTTP via Axios (`src/services/api.ts`), backend direct (`src/services/backendApi.ts`), OpenAI (`src/services/openai.ts`), fichiers/ dossiers (`src/services/folderApi.ts`).
|
||||||
|
- **Build/Runtime**: Docker multi‑stage Node→Nginx, config SPA (`Dockerfile`, `nginx.conf`).
|
||||||
|
- **Qualité**: ESLint + Prettier + markdownlint, Vitest + Testing Library.
|
||||||
|
|
||||||
|
### Points forts
|
||||||
|
|
||||||
|
- **Code splitting** déjà en place au niveau du routeur.
|
||||||
|
- **Centralisation d’état** propre (slices, middlewares, persistance localStorage).
|
||||||
|
- **Abstraction des services** HTTP claire (Axios + backends alternatifs).
|
||||||
|
- **Docker** production prêt (Nginx + healthcheck) et CI potentielle via `docker-compose.registry.yml`.
|
||||||
|
|
||||||
|
### Problèmes détectés
|
||||||
|
|
||||||
|
#### Lint TypeScript/JS
|
||||||
|
|
||||||
|
- Commande: `npm run lint`.
|
||||||
|
- Résultat: 57 problèmes trouvés (49 erreurs, 8 avertissements).
|
||||||
|
- Catégories principales:
|
||||||
|
- Types interdits `any` dans plusieurs services, ex. `src/services/backendApi.ts` (lignes 23–36, 90, 97, 107) et `src/services/fileExtract.ts` (lignes 2, 5, 55, 63, 110).
|
||||||
|
- Variables non utilisées, ex. `src/App.tsx` ligne 8 (`setCurrentFolderHash`), `src/store/documentSlice.ts` lignes 7, 56, 420, 428, `src/services/openai.ts` variables `_file`, `_address`, etc.
|
||||||
|
- Règles `react-hooks/exhaustive-deps` dans `src/App.tsx` (ligne 65) et `src/components/Layout.tsx` (ligne 84).
|
||||||
|
- Regex avec échappements inutiles dans `src/services/ruleNer.ts` (lignes 33–34, 84, 119).
|
||||||
|
- Typage middleware Redux avec `any` dans `src/store/index.ts` ligne 8.
|
||||||
|
|
||||||
|
#### Lint Markdown
|
||||||
|
|
||||||
|
- Commande: `npm run mdlint`.
|
||||||
|
- Problèmes récurrents:
|
||||||
|
- Manque de lignes vides autour des titres et listes: `CHANGELOG.md`, `docs/API_BACKEND.md`, `docs/architecture-backend.md`, `docs/changelog-pending.md`, `docs/HASH_SYSTEM.md`.
|
||||||
|
- Longueur de ligne > 120 caractères: `docs/API_BACKEND.md`, `docs/architecture-backend.md`, `docs/HASH_SYSTEM.md`, `docs/systeme-pending.md`.
|
||||||
|
- Blocs de code sans langage ou sans lignes vides autour.
|
||||||
|
- Titres en emphase non conformes (MD036) dans certains documents.
|
||||||
|
|
||||||
|
#### Tests
|
||||||
|
|
||||||
|
- Commande: `npm run test`.
|
||||||
|
- Résultat: 1 suite OK, 1 suite en échec.
|
||||||
|
- Échec: `tests/testFilesApi.test.ts` — import introuvable `../src/services/testFilesApi` (le fichier n’existe pas). Il faut soit créer `src/services/testFilesApi.ts`, soit adapter le test pour la source disponible.
|
||||||
|
|
||||||
|
#### Build de production
|
||||||
|
|
||||||
|
- Commande: `npm run build`.
|
||||||
|
- Résultat: erreurs TypeScript empêchant la compilation.
|
||||||
|
- Incohérences de types côté `ExtractionResult` consommé vs structures produites dans `src/services/api.ts` (propriété `timestamp` non prévue dans `ExtractionResult`).
|
||||||
|
- Types d’entités utilisés en `views/ExtractionView.tsx` traités comme `string` au lieu d’objets typés (`Identity`, `Address`). Ex: accès à `firstName`, `lastName`, `street`, `city`, `postalCode`, `confidence` sur des `string`.
|
||||||
|
- Variables non utilisées dans plusieurs fichiers (cf. lint ci‑dessus).
|
||||||
|
|
||||||
|
### Causes probables et pistes de résolution
|
||||||
|
|
||||||
|
- **Modèle de données**: divergence entre la structure standardisée `ExtractionResult` (`src/types/index.ts`) et le mapping réalisé dans `src/services/api.ts`/`backendApi.ts`. Il faut:
|
||||||
|
- Aligner les champs (ex. déplacer `timestamp` vers `status.timestamp` ou vers des métadonnées conformes).
|
||||||
|
- Harmoniser la forme des entités retournées (personnes/adresses) pour correspondre strictement à `Identity[]` et `Address[]`.
|
||||||
|
- **Composants de vues**: `ExtractionView.tsx` suppose des entités objets alors que les données mappées peuvent contenir des chaînes. Il faut normaliser en amont (mapping service) et/ou renforcer les garde‑fous de rendu.
|
||||||
|
- **Tests**: ajouter `src/services/testFilesApi.ts` (ou ajuster l’import) pour couvrir l’API de fichiers de test référencée par `tests/testFilesApi.test.ts`.
|
||||||
|
- **Qualité**:
|
||||||
|
- Remplacer les `any` par des types précis (ou `unknown` + raffinements), surtout dans `backendApi.ts`, `fileExtract.ts`, `openai.ts`.
|
||||||
|
- Corriger les dépendances de hooks React.
|
||||||
|
- Nettoyer les variables non utilisées et directives `eslint-disable` superflues.
|
||||||
|
- Corriger les regex avec échappements inutiles dans `ruleNer.ts`.
|
||||||
|
- Corriger les erreurs markdown (MD013, MD022, MD031, MD032, MD036, MD040, MD047) en ajoutant lignes vides et langages de blocs.
|
||||||
|
|
||||||
|
### Sécurité et configuration
|
||||||
|
|
||||||
|
- **Variables d’environnement**: `VITE_API_URL`, `VITE_USE_OPENAI`, clés OpenAI masquées en dev (`src/services/api.ts`). OK.
|
||||||
|
- **Nginx**: SPA et healthcheck définis (`nginx.conf`). OK pour production statique.
|
||||||
|
- **Docker**: image multi‑stage; healthcheck HTTP. Tagging via scripts et `docker-compose.registry.yml` (variable `TAG`). À aligner avec conventions internes du registre.
|
||||||
|
|
||||||
|
### Recommandations prioritaires (ordre d’exécution)
|
||||||
|
|
||||||
|
1. Corriger les erreurs TypeScript bloquantes du build:
|
||||||
|
- Aligner `ExtractionResult` consommé/produit (services + vues). Supprimer/relocaliser `timestamp` au bon endroit.
|
||||||
|
- Normaliser `identities`/`addresses` en objets typés dans le mapping service.
|
||||||
|
- Corriger `ExtractionView.tsx` pour refléter les types réels.
|
||||||
|
2. Supprimer variables non utilisées et corriger `any` majeurs dans services critiques (`backendApi.ts`, `fileExtract.ts`).
|
||||||
|
3. Ajouter/implémenter `src/services/testFilesApi.ts` ou corriger l’import de test.
|
||||||
|
4. Corriger les règles `react-hooks/exhaustive-deps`.
|
||||||
|
5. Corriger markdownlint dans `CHANGELOG.md` et `docs/*.md` (lignes vides, langages de blocs, longueurs de ligne raisonnables).
|
||||||
|
6. Relancer lint, tests et build pour valider.
|
||||||
|
|
||||||
|
### Notes de compatibilité
|
||||||
|
|
||||||
|
- **Node**: engines `>=20.19 <23`. Testé avec Node 22.12.0 (OK) conformément au README.
|
||||||
|
- **ESLint**: config moderne (eslint@9, typescript-eslint@8) — stricte sur `any` et hooks React.
|
||||||
|
|
||||||
|
### Annexes (références de fichiers)
|
||||||
|
|
||||||
|
- `src/types/index.ts` — définitions `ExtractionResult`, `Identity`, `Address`.
|
||||||
|
- `src/services/api.ts` — mapping de la réponse backend; contient la propriété non typée `timestamp` sur `ExtractionResult` (à déplacer).
|
||||||
|
- `src/views/ExtractionView.tsx` — accès de propriétés d’objets sur des `string` (à corriger après normalisation du mapping).
|
||||||
|
- `tests/testFilesApi.test.ts` — dépend de `src/services/testFilesApi.ts` non présent.
|
||||||
@ -121,7 +121,7 @@ Ce mode est utile pour démo/diagnostic quand le backend n’est pas disponible.
|
|||||||
```typescript
|
```typescript
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: BASE_URL,
|
baseURL: BASE_URL,
|
||||||
timeout: 60000
|
timeout: 60000,
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -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_
|
||||||
|
|||||||
30
docs/CACHE_ET_TRAITEMENT_ASYNC.md
Normal file
30
docs/CACHE_ET_TRAITEMENT_ASYNC.md
Normal 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 l’API)
|
||||||
|
- `backend/cache/*`: (désormais vide) ancien emplacement – ne plus utiliser
|
||||||
|
|
||||||
|
## Flux de traitement
|
||||||
|
|
||||||
|
1. Dépôt d’un fichier (`/api/extract`):
|
||||||
|
- Calcule `fileHash` (SHA‑256 du contenu)
|
||||||
|
- Si `cache/<folderHash>/<fileHash>.json` existe: renvoie immédiatement le JSON
|
||||||
|
- Sinon: crée `cache/<folderHash>/<fileHash>.pending`, lance l’OCR/NER, puis écrit le JSON et supprime `.pending`
|
||||||
|
|
||||||
|
2. Listing (`/api/folders/:folderHash/results`):
|
||||||
|
- Agrège tous les JSON présents dans `cache/<folderHash>/`
|
||||||
|
- Pour chaque fichier présent dans `uploads/<folderHash>` sans JSON, crée un flag `.pending` et lance le traitement en arrière‑plan, sans bloquer la réponse
|
||||||
|
|
||||||
|
## Points importants
|
||||||
|
|
||||||
|
- Le traitement images/PDF peut être long; le listing n’attend pas la fin
|
||||||
|
- Le frontal réalise un polling périodique si `hasPending=true`
|
||||||
|
- Les erreurs de traitement suppriment le flag `.pending` et la requête renvoie 500 (extraction) ou 200 avec moins de résultats (listing)
|
||||||
|
|
||||||
|
## Bonnes pratiques
|
||||||
|
|
||||||
|
- N’écrire les résultats que dans `cache/<folderHash>` à la racine
|
||||||
|
- Toujours indexer les résultats par `fileHash.json`
|
||||||
|
- Protéger les accès à `.length` et valeurs potentiellement `undefined` dans le backend
|
||||||
@ -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
239
docs/SYSTEME_FONCTIONNEL.md
Normal 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
|
||||||
15
docs/TODO.md
15
docs/TODO.md
@ -9,7 +9,7 @@ faire une api et une une ihm qui les consomme pour:
|
|||||||
|
|
||||||
1. Détecter un type de document
|
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
|
||||||
|
|||||||
@ -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%
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
117
scripts/precache.cjs
Normal 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
113
scripts/precache.js
Normal 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])
|
||||||
75
scripts/process-uploaded-files.sh
Executable file
75
scripts/process-uploaded-files.sh
Executable 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
|
||||||
@ -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`)
|
||||||
});
|
})
|
||||||
|
|||||||
31
src/App.tsx
31
src/App.tsx
@ -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(() => {
|
||||||
|
|||||||
@ -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 />}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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',
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 = () => {
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
108
src/services/testFilesApi.ts
Normal file
108
src/services/testFilesApi.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -15,5 +15,3 @@ const appSlice = createSlice({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const appReducer = appSlice.reducer
|
export const appReducer = appSlice.reducer
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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 été générée automatiquement et doit être validée par un expert notarial.
|
Cette analyse LLM a été générée automatiquement et doit être validée par un expert
|
||||||
|
notarial.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
})
|
||||||
|
|||||||
@ -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'))
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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)
|
||||||
|
})
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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" }
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user