Compare commits

..

No commits in common. "dev" and "staging" have entirely different histories.
dev ... staging

271 changed files with 4202 additions and 117938 deletions

View File

@ -1,18 +0,0 @@
node_modules
coverage
.git
.vscode
.idea
*.log
.env
.env.*
**/.DS_Store
**/*.test.*
**/*.spec.*
**/tests/**
**/test/**
dist
README.md
CHANGELOG.md
CODE_OF_CONDUCT.md
CONTRIBUTING.md

9
.env
View File

@ -1,13 +1,9 @@
# Configuration API Backend
VITE_API_URL=http://localhost:18000
VITE_API_URL=http://localhost:8000
# Configuration pour le développement
VITE_APP_NAME=4NK IA Lecoffre.io
VITE_APP_NAME=4NK IA Front Notarial
VITE_APP_VERSION=0.1.0
VITE_USE_RULE_NER=true
VITE_LLM_CLASSIFY_ONLY=true
VITE_BACKEND_URL=http://localhost:3001
# Configuration des services externes (optionnel)
VITE_CADASTRE_API_URL=https://apicarto.ign.fr/api/cadastre
@ -15,4 +11,3 @@ VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
VITE_GEOFONCIER_API_URL=https://api2.geofoncier.fr
VITE_BODACC_API_URL=https://bodacc-datadila.opendatasoft.com/api
VITE_INFOGREFFE_API_URL=https://entreprise.api.gouv.fr
OLLAMA_MIN_REVIEW_MS=100000

View File

@ -1,13 +1,9 @@
# Configuration API Backend
VITE_API_URL=http://localhost:18000
VITE_API_URL=http://localhost:8000
# Configuration pour le développement
VITE_APP_NAME=IA Lecoffre.io
VITE_APP_NAME=4NK IA Front Notarial
VITE_APP_VERSION=0.1.0
VITE_USE_RULE_NER=true
VITE_LLM_CLASSIFY_ONLY=true
VITE_BACKEND_URL=http://localhost:3001
# Configuration des services externes (optionnel)
VITE_CADASTRE_API_URL=https://apicarto.ign.fr/api/cadastre
@ -15,7 +11,3 @@ VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
VITE_GEOFONCIER_API_URL=https://api2.geofoncier.fr
VITE_BODACC_API_URL=https://bodacc-datadila.opendatasoft.com/api
VITE_INFOGREFFE_API_URL=https://entreprise.api.gouv.fr
VITE_OPENAI_API_KEY=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
VITE_USE_OPENAI=true
VITE_OPENAI_BASE_URL=https://api.openai.com/v1
VITE_OPENAI_MODEL=gpt-4o-mini

40
.gitignore vendored
View File

@ -6,8 +6,7 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.env
.git
node_modules
dist
dist-ssr
@ -23,40 +22,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Cursor IDE files
Cursor.exe
Cursor.VisualElementsManifest.xml
LICENSES.chromium.html
chrome_*.pak
d3dcompiler_*.dll
ffmpeg.dll
icudtl.dat
libEGL.dll
libGLESv2.dll
locales/
policies/
resources/
snapshot_blob.bin
test-document.*
tools/
unins000.*
v8_context_snapshot.bin
vk_swiftshader.*
vulkan-*.dll
_/
# Project specific
test-files/
uploads/
cache/
coverage/
resources.pak
log/
frontend.log
backend.log
backend/node_modules/
backend/cache/
backend/uploads/
backend/*.pid
*.pid

View File

@ -1,5 +0,0 @@
# Ignorer les guides et rapports générés
GUIDE_TEST_APERCU_CORRIGE.md
GUIDE_TEST_EXTRACTION_CORRIGEE.md
RAPPORT_ALIGNEMENT_BACKEND.md
RAPPORT_ANALYSE_DOCUMENT.md

1
.nvmrc
View File

@ -1 +0,0 @@
20.19.5

View File

@ -1,36 +0,0 @@
[Unit]
Description=4NK IA Backend Service
Documentation=https://git.4nkweb.com/4nk/4NK_IA_front
After=network.target
Wants=network.target
[Service]
Type=simple
User=debian
Group=debian
WorkingDirectory=/home/debian/4NK_IA_front
ExecStart=/usr/bin/node backend/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=4nk-ia-backend
# Variables d'environnement
Environment=NODE_ENV=production
Environment=PORT=3001
# Sécurité
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/home/debian/4NK_IA_front
# Limites de ressources
LimitNOFILE=65536
MemoryMax=1G
[Install]
WantedBy=multi-user.target

View File

@ -1,148 +1,74 @@
# 📋 Changelog - 4NK_IA Frontend
# Changelog
## [1.1.1] - 2025-09-18
## 0.1.0 - Version initiale complète
### 🔧 Améliorations Backend
### ✨ Fonctionnalités principales
#### Alignement des Entités
- **Scope des entités limité** : Seules 4 entités sont maintenant retournées par l'API
- `persons` : Identités des personnes
- `companies` : Entreprises et sociétés
- `addresses` : Adresses postales
- `contractual` : Clauses et signatures contractuelles
- **Suppression des entités non utilisées** : `dates`, `financial`, `references`, `metier` retirées de la réponse standard
- **Version API mise à jour** : `/api/health` renvoie maintenant la version `1.0.1`
- **Interface notariale complète** : Application front-end pour l'analyse de documents notariaux
- **Upload de documents** : Glisser-déposer avec prévisualisation (PDF, images)
- **Extraction de données** : OCR et identification d'objets standardisés
- **Analyse intelligente** : Score de vraisemblance et recommandations
- **Données contextuelles** : Intégration APIs externes (Cadastre, Géorisques, BODACC, Infogreffe)
- **Conseil IA** : Analyse LLM avec détection de risques
#### Interface Utilisateur
- **Onglets d'entités** : Interface organisée par type d'entité avec navigation par onglets
- **Gestion ciblée** : Édition et suppression des entités par catégorie
- **Enrichissement par entité** : Boutons d'enrichissement spécialisés par type
### 🏗️ Architecture technique
### 🐛 Corrections
- **Erreur React #31** : Correction des valeurs non-string passées aux composants React
- **TypeScript strict** : Résolution des erreurs de compilation pour build stricte
- **Déploiement** : Mise à jour automatique de l'interface sur `ai.4nkweb.com`
- **React 18 + TypeScript** : Framework moderne avec typage strict
- **Vite 7** : Build tool rapide et moderne
- **Material-UI v6** : Interface utilisateur professionnelle
- **Redux Toolkit** : Gestion d'état centralisée
- **React Router v6** : Navigation avec code splitting
- **Axios** : Client HTTP avec intercepteurs
### 📚 Documentation
- **Spécification UI** : Mise à jour de `docs/extraction_ui_spec.md` pour les 4 onglets
- **Tests** : Mise à jour des tests pour refléter les entités attendues
### 🛠️ Outillage et qualité
---
- **ESLint + Prettier** : Linting et formatage automatique
- **markdownlint** : Validation des fichiers Markdown
- **Vitest + Testing Library** : Tests unitaires et d'intégration
- **Coverage V8** : Rapport de couverture de code
## [1.1.0] - 2025-09-15
### 📚 Documentation et gouvernance
### ✨ Nouvelles Fonctionnalités
- **README complet** : Documentation technique détaillée
- **Fichiers open-source** : LICENSE (MIT), CONTRIBUTING.md, CODE_OF_CONDUCT.md
- **Structure docs/** : Documentation technique organisée
- **Tests/** : Squelette de tests avec exemples
#### 🔐 Système de Hash et Cache JSON
### 🔧 Gestion d'erreur et robustesse
- **Système de hash SHA-256** : Calcul automatique du hash pour chaque fichier uploadé
- **Détection des doublons** : Évite les fichiers identiques basés sur le contenu
- **Cache JSON** : Sauvegarde automatique des résultats d'extraction
- **Optimisation des performances** : Réutilisation des résultats en cache
- **Mode démonstration** : Fonctionnement complet sans backend
- **Gestion d'erreur gracieuse** : Fallback automatique pour tous les types d'erreurs
- **Intercepteurs Axios** : Gestion centralisée des erreurs API
- **Données de démonstration** : Exemples réalistes d'actes notariaux
#### 🛠️ Nouvelles Fonctions Backend
### 🎨 Interface utilisateur
- `calculateFileHash(buffer)` : Calcule le hash SHA-256 d'un fichier
- `findExistingFileByHash(hash)` : Trouve les fichiers existants par hash
- `saveJsonCache(hash, result)` : Sauvegarde un résultat dans le cache
- `getJsonCache(hash)` : Récupère un résultat depuis le cache
- `listCacheFiles()` : Liste tous les fichiers de cache
- **Design professionnel** : Interface claire avec fond blanc
- **Navigation intuitive** : Onglets et breadcrumbs
- **Responsive** : Adaptation mobile et desktop
- **Accessibilité** : Composants Material-UI accessibles
#### 📡 Nouvelles Routes API
### 🚀 Déploiement et CI
- `GET /api/cache` : Liste les fichiers de cache avec métadonnées
- `GET /api/cache/:hash` : Récupère un résultat de cache spécifique
- `DELETE /api/cache/:hash` : Supprime un fichier de cache
- `GET /api/uploads` : Liste les fichiers uploadés avec leurs hash
- **Scripts npm** : Build, test, lint, format
- **Variables d'environnement** : Configuration flexible des APIs
- **Git workflow** : Branches dev, staging, release
- **Versioning** : Tag v0.1.0 et CHANGELOG
### 🔧 Améliorations Techniques
### 🐛 Corrections et améliorations
#### Backend (`backend/server.js`)
- **Erreur d'hydratation HTML** : Structure HTML valide
- **Gestion d'erreur 404/405** : Fallback pour endpoints non supportés
- **ERR_CONNECTION_REFUSED** : Mode démo automatique
- **Console propre** : Suppression des erreurs visibles
- Intégration du système de cache dans la route `/api/extract`
- Vérification du cache avant traitement
- Sauvegarde automatique des résultats après traitement
- Gestion optimisée des fichiers dupliqués
- Logs détaillés pour le debugging
### 📦 Dépendances principales
#### Configuration
- Ajout du dossier `cache/` au `.gitignore`
- Configuration des remotes Git pour SSH/HTTPS
### 📚 Documentation
#### Nouveaux Fichiers
- `docs/HASH_SYSTEM.md` : Documentation complète du système de hash
- `CHANGELOG.md` : Historique des versions
#### Mises à Jour
- `docs/API_BACKEND.md` : Ajout de la documentation des nouvelles routes
- Caractéristiques principales mises à jour
### 🚀 Performance
#### Optimisations
- **Traitement instantané** pour les fichiers en cache
- **Économie de stockage** : Pas de fichiers dupliqués
- **Réduction des calculs** : Réutilisation des résultats existants
- **Logs optimisés** : Indication claire de l'utilisation du cache
#### Métriques
- Temps de traitement réduit de ~80% pour les fichiers en cache
- Stockage optimisé (suppression automatique des doublons)
- Cache JSON : ~227KB pour un document PDF de 992KB
### 🔍 Workflow d'Upload Optimisé
```mermaid
graph TD
A[Fichier Uploadé] --> B[Calcul Hash SHA-256]
B --> C{Cache Existe?}
C -->|Oui| D[Retour Résultat Cache]
C -->|Non| E{Fichier Existe?}
E -->|Oui| F[Utiliser Fichier Existant]
E -->|Non| G[Traitement Normal]
F --> H[Extraction & NER]
G --> H
H --> I[Sauvegarde Cache]
I --> J[Retour Résultat]
```
### 🧪 Tests Validés
- ✅ Upload de fichier unique
- ✅ Upload de fichier dupliqué (détection par hash)
- ✅ Utilisation du cache JSON
- ✅ Nouvelles routes API
- ✅ Gestion des erreurs
- ✅ Nettoyage automatique des fichiers temporaires
### 📊 Statistiques
- **Fichiers modifiés** : 4
- **Nouvelles lignes** : 443
- **Nouveaux fichiers** : 2
- **Routes API ajoutées** : 4
- **Fonctions ajoutées** : 5
---
## [1.0.0] - 2025-09-15
### 🎉 Version Initiale
- Système d'extraction de documents (PDF, images)
- OCR avec Tesseract.js
- NER par règles
- API backend complète
- Frontend React avec Material-UI
- Format JSON standardisé
---
_Changelog maintenu automatiquement - Dernière mise à jour : 15 septembre 2025_
- `react@^18.3.1` - Framework UI
- `typescript@^5.6.3` - Typage statique
- `vite@^7.1.5` - Build tool
- `@mui/material@^6.1.6` - Composants UI
- `@reduxjs/toolkit@^2.3.0` - Gestion d'état
- `react-router-dom@^6.28.0` - Routing
- `axios@^1.7.7` - Client HTTP
- `vitest@^2.1.8` - Framework de test

View File

@ -1,29 +0,0 @@
# syntax=docker/dockerfile:1.7-labs
# ---- Build stage ----
FROM node:20.19-alpine AS build
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --ignore-scripts
# Copy sources
COPY . .
# Ensure correct Node version for build via prebuild script
RUN npm run build
# ---- Runtime stage ----
FROM nginx:alpine AS runtime
# Copy SPA build
COPY --from=build /app/dist /usr/share/nginx/html
# Nginx config for SPA routing and caching
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK CMD wget -qO- http://localhost/ || exit 1
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,4 +1,4 @@
# Lecoffre.io
# 4NK IA Front Notarial
Application front-end pour l'analyse intelligente de documents notariaux avec IA.
@ -9,9 +9,6 @@ Application front-end pour l'analyse intelligente de documents notariaux avec IA
- **Upload multiple** : Glisser-déposer de documents (PDF, images)
- **Prévisualisation** : Affichage des documents uploadés
- **Types supportés** : PDF, PNG, JPG, JPEG
- **Système de dossiers** : Organisation par hash de dossier
- **Gestion des pending** : Suivi en temps réel des fichiers en cours de traitement
- **API robuste** : Backend sur port 3001 avec gestion d'erreurs
### 🔍 Extraction et analyse
@ -22,7 +19,9 @@ Application front-end pour l'analyse intelligente de documents notariaux avec IA
### 📊 Analyse intelligente
- **Score de vraisemblance** : Évaluation de la crédibilité du document
- **Recommandations** : Suggestions d'actions à effectuer
- **Synthèse** : Résumé automatique du document
### 🌐 Données contextuelles
@ -39,38 +38,26 @@ Application front-end pour l'analyse intelligente de documents notariaux avec IA
## 🚀 Technologies
- **Frontend** : React 19 + TypeScript
- **Frontend** : React 18 + TypeScript
- **Build** : Vite 7
- **UI** : Material-UI (MUI) v7
- **UI** : Material-UI (MUI) v6
- **State** : Redux Toolkit + React Redux
- **Routing** : React Router v7
- **Routing** : React Router v6
- **HTTP** : Axios
- **Tests** : Vitest + Testing Library
- **Linting** : ESLint + Prettier + markdownlint
- **Backend** : Node.js + Express
- **OCR** : Tesseract.js
- **IA** : OpenAI API
## 📦 Installation
### Prérequis
- Node.js >= 20.19.0 (LTS). Recommandé: 22.12+
- Node.js >= 22.12.0 (recommandé) ou >= 20.19.0
- npm >= 10.0.0
- nvm recommandé pour gérer les versions de Node
Avec nvm:
```bash
nvm use # utilise la version définie dans .nvmrc
# ou, si non installée
nvm install && nvm use
```
### Installation des dépendances
```bash
npm ci
npm install
```
### Configuration des environnements
@ -79,11 +66,6 @@ Créer un fichier `.env` :
```env
VITE_API_URL=http://localhost:8000
VITE_USE_OPENAI=false
VITE_OPENAI_API_KEY=
VITE_OPENAI_BASE_URL=https://api.openai.com/v1
VITE_OPENAI_MODEL=gpt-4o-mini
VITE_USE_RULE_NER=true
VITE_CADASTRE_API_URL=https://api.cadastre.gouv.fr
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
VITE_GEOFONCIER_API_URL=https://api.geofoncier.fr
@ -95,37 +77,21 @@ VITE_INFOGREFFE_API_URL=https://api.infogreffe.fr
```bash
# Développement
npm run start:all # Démarrer backend (PM2) + frontend (Vite) ensemble
npm run start:dev # Clean complet, backend restart, front Vite (séquentiel)
npm run dev # Serveur de développement (frontend seul)
npm run dev # Serveur de développement
npm run build # Build de production
npm run preview # Prévisualisation du build
# Backend (PM2)
npm run back:up # Démarrer le backend via PM2
npm run back:restart # Redémarrer ou démarrer le backend via PM2
# Qualité de code
npm run lint # Vérification ESLint
npm run lint:fix # Correction automatique ESLint
npm run format # Vérification Prettier
npm run format:fix # Formatage automatique
npm run mdlint # Vérification Markdown
# Tests
npm run test # Tests unitaires avec couverture
npm run test # Tests unitaires
npm run test:ui # Tests avec interface
# Docker
./scripts/docker-build.sh # Construire l'image locale
IMAGE_TAG=0.1.3 ./scripts/docker-build.sh # Construire avec tag explicite
IMAGE_TAG=0.1.3 ./scripts/docker-push.sh # Pousser vers git.4nkweb.com
```
### Nettoyage rapide (local)
```bash
# Purge et réinstallation
npm run clean:all
npm run test:coverage # Tests avec couverture
```
## 🏗️ Architecture
@ -140,6 +106,7 @@ src/
├── views/ # Vues de l'application
│ ├── UploadView.tsx # Upload de documents
│ ├── ExtractionView.tsx # Extraction de données
│ ├── AnalyseView.tsx # Analyse des documents
│ ├── ContexteView.tsx # Données contextuelles
│ └── ConseilView.tsx # Conseil IA
├── store/ # Gestion d'état Redux
@ -180,10 +147,14 @@ src/
- **Breadcrumbs** : Indication de la position actuelle
- **Actions contextuelles** : Boutons d'action selon la vue
## 🔧 Intégration backend
## 🔧 Mode démonstration
Toutes les fonctionnalités (upload, extraction, analyse, données contextuelles, conseil LLM) passent par le backend `4NK_IA_back`.
Les APIs externes (Cadastre, Géorisques, Géofoncier, BODACC, Infogreffe) sont appelées côté backend uniquement.
L'application fonctionne parfaitement en mode démonstration :
- **Données réalistes** : Exemples d'actes notariaux
- **Fonctionnalités complètes** : Toutes les vues opérationnelles
- **Gestion d'erreur** : Fallback automatique sans backend
- **Expérience utilisateur** : Interface identique au mode production
## 🧪 Tests
@ -217,19 +188,6 @@ npm run test:coverage # Rapport de couverture
npm run build
```
### Exécution via Docker
```bash
# Construire l'image
./scripts/docker-build.sh
# Lancer localement
docker run --rm -p 8080:80 git.4nkweb.com/4nk/4nk-ia-front:latest
# Pousser vers le registry (requiert authentification préalable)
IMAGE_TAG=0.1.3 ./scripts/docker-push.sh
```
### Variables d'environnement
Configurer les URLs des APIs externes selon l'environnement :
@ -275,5 +233,5 @@ Pour toute question ou problème :
---
**Version actuelle** : 0.1.3
**Dernière mise à jour** : Septembre 2025
**Version actuelle** : 0.1.0
**Dernière mise à jour** : Janvier 2024

View File

@ -1 +0,0 @@

View File

@ -1,264 +0,0 @@
const sharp = require('sharp')
const path = require('path')
const fs = require('fs')
/**
* Améliorateur OCR spécialisé pour les Cartes Nationales d'Identité françaises
* Utilise des techniques avancées de preprocessing et de segmentation
*/
// Fonction pour détecter si une image est probablement une CNI
async function isCNIDocument(inputPath) {
try {
const image = sharp(inputPath)
const metadata = await image.metadata()
// Vérifications pour une CNI française
const isPortrait = metadata.height > metadata.width
const hasGoodResolution = metadata.width > 800 && metadata.height > 500
const aspectRatio = metadata.width / metadata.height
const isCNIRatio = aspectRatio > 0.6 && aspectRatio < 0.7 // Ratio typique d'une CNI
console.log(
`[CNI_DETECT] ${path.basename(inputPath)} - Portrait: ${isPortrait}, Résolution: ${metadata.width}x${metadata.height}, Ratio: ${aspectRatio.toFixed(2)}`,
)
return isPortrait && hasGoodResolution && isCNIRatio
} catch (error) {
console.error(`[CNI_DETECT] Erreur détection CNI:`, error.message)
return false
}
}
// Fonction pour extraire la zone MRZ (Machine Readable Zone) d'une CNI
async function extractMRZ(inputPath) {
try {
const image = sharp(inputPath)
const metadata = await image.metadata()
// La MRZ est généralement en bas de la CNI
const mrzHeight = Math.floor(metadata.height * 0.15) // 15% de la hauteur
const mrzTop = metadata.height - mrzHeight
console.log(`[MRZ] Extraction de la zone MRZ: ${mrzTop},${mrzHeight}`)
const mrzImage = await image
.extract({
left: 0,
top: mrzTop,
width: metadata.width,
height: mrzHeight,
})
.grayscale()
.normalize()
.sharpen()
.threshold(128)
.png()
.toBuffer()
return mrzImage
} catch (error) {
console.error(`[MRZ] Erreur extraction MRZ:`, error.message)
return null
}
}
// Fonction pour segmenter une CNI en zones spécifiques
async function segmentCNIZones(inputPath) {
try {
const image = sharp(inputPath)
const metadata = await image.metadata()
const zones = {
// Zone du nom (généralement en haut à gauche)
nameZone: {
left: Math.floor(metadata.width * 0.05),
top: Math.floor(metadata.height * 0.25),
width: Math.floor(metadata.width * 0.4),
height: Math.floor(metadata.height * 0.15),
},
// Zone du prénom
firstNameZone: {
left: Math.floor(metadata.width * 0.05),
top: Math.floor(metadata.height * 0.35),
width: Math.floor(metadata.width * 0.4),
height: Math.floor(metadata.height * 0.15),
},
// Zone de la date de naissance
birthDateZone: {
left: Math.floor(metadata.width * 0.05),
top: Math.floor(metadata.height * 0.45),
width: Math.floor(metadata.width * 0.3),
height: Math.floor(metadata.height * 0.1),
},
// Zone du numéro de CNI
idNumberZone: {
left: Math.floor(metadata.width * 0.05),
top: Math.floor(metadata.height * 0.55),
width: Math.floor(metadata.width * 0.4),
height: Math.floor(metadata.height * 0.1),
},
}
console.log(`[CNI_SEGMENT] Segmentation en ${Object.keys(zones).length} zones`)
return zones
} catch (error) {
console.error(`[CNI_SEGMENT] Erreur segmentation:`, error.message)
return null
}
}
// Fonction pour extraire une zone spécifique d'une CNI
async function extractCNIZone(inputPath, zone, zoneName) {
try {
const image = sharp(inputPath)
const zoneImage = await image
.extract(zone)
.grayscale()
.normalize()
.sharpen({ sigma: 2, m1: 0.5, m2: 2, x1: 2, y2: 10 })
.modulate({ brightness: 1.2, contrast: 1.5 })
.threshold(140)
.png()
.toBuffer()
console.log(`[CNI_ZONE] Zone ${zoneName} extraite: ${zone.width}x${zone.height}`)
return zoneImage
} catch (error) {
console.error(`[CNI_ZONE] Erreur extraction zone ${zoneName}:`, error.message)
return null
}
}
// Fonction pour améliorer le preprocessing spécifiquement pour les CNI
async function enhanceCNIPreprocessing(inputPath) {
try {
console.log(`[CNI_ENHANCE] Amélioration CNI pour: ${path.basename(inputPath)}`)
const image = sharp(inputPath)
const metadata = await image.metadata()
// Configuration spécialisée pour les CNI
const enhancedImage = await image
.resize({
width: 2000,
height: Math.floor(2000 * (metadata.height / metadata.width)),
fit: 'fill',
kernel: sharp.kernel.lanczos3,
})
.grayscale()
.normalize()
.modulate({
brightness: 1.3,
contrast: 1.8,
saturation: 0,
})
.sharpen({
sigma: 1.5,
m1: 0.5,
m2: 3,
x1: 2,
y2: 20,
})
.median(3)
.threshold(135)
.png()
.toBuffer()
console.log(`[CNI_ENHANCE] Image améliorée: ${enhancedImage.length} bytes`)
return enhancedImage
} catch (error) {
console.error(`[CNI_ENHANCE] Erreur amélioration CNI:`, error.message)
return null
}
}
// Fonction pour traiter une CNI avec segmentation par zones
async function processCNIWithZones(inputPath) {
try {
console.log(`[CNI_PROCESS] Traitement CNI par zones: ${path.basename(inputPath)}`)
// Vérifier si c'est une CNI
const isCNI = await isCNIDocument(inputPath)
if (!isCNI) {
console.log(`[CNI_PROCESS] Document non reconnu comme CNI`)
return null
}
// Segmenter en zones
const zones = await segmentCNIZones(inputPath)
if (!zones) {
console.log(`[CNI_PROCESS] Échec de la segmentation`)
return null
}
const results = {
isCNI: true,
zones: {},
mrz: null,
}
// Extraire chaque zone
for (const [zoneName, zone] of Object.entries(zones)) {
const zoneImage = await extractCNIZone(inputPath, zone, zoneName)
if (zoneImage) {
results.zones[zoneName] = zoneImage
}
}
// Extraire la MRZ
const mrzImage = await extractMRZ(inputPath)
if (mrzImage) {
results.mrz = mrzImage
}
console.log(`[CNI_PROCESS] CNI traitée: ${Object.keys(results.zones).length} zones + MRZ`)
return results
} catch (error) {
console.error(`[CNI_PROCESS] Erreur traitement CNI:`, error.message)
return null
}
}
// Fonction pour décoder la MRZ (Machine Readable Zone)
function decodeMRZ(mrzText) {
try {
if (!mrzText || mrzText.length < 88) {
return null
}
// Format MRZ de la CNI française (2 lignes de 36 caractères)
const lines = mrzText.split('\n').filter((line) => line.trim().length > 0)
if (lines.length < 2) {
return null
}
const line1 = lines[0].trim()
const line2 = lines[1].trim()
const result = {
documentType: line1.substring(0, 2),
country: line1.substring(2, 5),
surname: line1.substring(5, 36).replace(/</g, ' ').trim(),
givenNames: line2.substring(0, 30).replace(/</g, ' ').trim(),
documentNumber: line2.substring(30, 36).trim(),
}
console.log(`[MRZ_DECODE] MRZ décodée: ${result.surname} ${result.givenNames}`)
return result
} catch (error) {
console.error(`[MRZ_DECODE] Erreur décodage MRZ:`, error.message)
return null
}
}
module.exports = {
isCNIDocument,
extractMRZ,
segmentCNIZones,
extractCNIZone,
enhanceCNIPreprocessing,
processCNIWithZones,
decodeMRZ,
}

View File

@ -1,148 +0,0 @@
/**
* Collecteur Adresses
* Géocodage via BAN (api-adresse.data.gouv.fr) et scaffold pour risques
*/
const fetch = require('node-fetch')
const USER_AGENT = '4NK-IA-Front/1.0 (Document Analysis Tool)'
const REQUEST_TIMEOUT_MS = 12000
// URLs des services publics
const GEORISQUE_BASE_URL = 'https://www.georisques.gouv.fr'
const CADASTRE_BASE_URL = 'https://cadastre.data.gouv.fr'
async function geocodeBAN(address) {
const q = [address.street, address.postalCode, address.city, address.country]
.filter(Boolean)
.join(' ')
const url = `https://api-adresse.data.gouv.fr/search/?q=${encodeURIComponent(q)}&limit=1`
const start = Date.now()
try {
const res = await fetch(url, {
headers: { 'User-Agent': USER_AGENT, 'Accept': 'application/json' },
timeout: REQUEST_TIMEOUT_MS,
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const json = await res.json()
const f = json.features && json.features[0]
if (!f) {
return { success: true, duration: Date.now() - start, score: 0, lat: null, lon: null, label: null }
}
return {
success: true,
duration: Date.now() - start,
score: f.properties && typeof f.properties.score === 'number' ? f.properties.score : null,
lat: f.geometry?.coordinates ? f.geometry.coordinates[1] : null,
lon: f.geometry?.coordinates ? f.geometry.coordinates[0] : null,
label: f.properties?.label || null,
}
} catch (e) {
return { success: false, duration: Date.now() - start, error: e.message }
}
}
async function collectGéoRisque(lat, lon) {
if (!lat || !lon) return { success: false, error: 'Coordonnées manquantes' }
const start = Date.now()
try {
// Recherche des risques majeurs par coordonnées
const url = `${GEORISQUE_BASE_URL}/api/risques?lat=${lat}&lon=${lon}&rayon=1000`
const res = await fetch(url, {
headers: { 'User-Agent': USER_AGENT, 'Accept': 'application/json' },
timeout: REQUEST_TIMEOUT_MS,
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
const risks = (data.risques || []).map(risk => ({
type: risk.type || 'Risque majeur',
level: risk.niveau || 'Non spécifié',
description: risk.description || '',
sourceUrl: `${GEORISQUE_BASE_URL}/risques/${risk.id || ''}`,
distance: risk.distance || null
}))
return {
success: true,
duration: Date.now() - start,
risks,
count: risks.length
}
} catch (e) {
return { success: false, duration: Date.now() - start, error: e.message, risks: [] }
}
}
async function collectCadastre(lat, lon) {
if (!lat || !lon) return { success: false, error: 'Coordonnées manquantes' }
const start = Date.now()
try {
// Recherche des parcelles cadastrales par coordonnées
const url = `${CADASTRE_BASE_URL}/api/parcelles?lat=${lat}&lon=${lon}&distance=100`
const res = await fetch(url, {
headers: { 'User-Agent': USER_AGENT, 'Accept': 'application/json' },
timeout: REQUEST_TIMEOUT_MS,
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
const parcelles = (data.parcelles || []).map(parcelle => ({
section: parcelle.section || '',
numero: parcelle.numero || '',
commune: parcelle.commune || '',
surface: parcelle.surface || null,
sourceUrl: `${CADASTRE_BASE_URL}/parcelles/${parcelle.id || ''}`
}))
return {
success: true,
duration: Date.now() - start,
parcelles,
count: parcelles.length
}
} catch (e) {
return { success: false, duration: Date.now() - start, error: e.message, parcelles: [] }
}
}
async function collectAddressData(address) {
// Étape 1: Géocodage BAN
const geocode = await geocodeBAN(address)
let risks = []
let cadastre = []
// Étape 2: Si géocodage réussi, collecter risques et cadastre
if (geocode.success && geocode.lat && geocode.lon) {
console.log(`[Address] Géocodage réussi: ${geocode.lat}, ${geocode.lon}`)
// Collecte parallèle des risques et du cadastre
const [risksResult, cadastreResult] = await Promise.all([
collectGéoRisque(geocode.lat, geocode.lon),
collectCadastre(geocode.lat, geocode.lon)
])
risks = risksResult.risks || []
cadastre = cadastreResult.parcelles || []
console.log(`[Address] Risques: ${risks.length}, Parcelles: ${cadastre.length}`)
}
return {
success: geocode.success,
geocode,
risks,
cadastre,
timestamp: new Date().toISOString(),
sources: ['ban', 'georisque', 'cadastre']
}
}
module.exports = {
collectAddressData,
}

View File

@ -1,235 +0,0 @@
/**
* Collecteur Bodacc - Gel des avoirs
* Scraping léger avec politesse pour vérifier les mentions de gel des avoirs
*/
const fetch = require('node-fetch');
const { JSDOM } = require('jsdom');
const BODACC_BASE_URL = 'https://www.bodacc.fr';
const USER_AGENT = '4NK-IA-Front/1.0 (Document Analysis Tool)';
const REQUEST_DELAY_MS = 1000; // 1 seconde entre les requêtes
const REQUEST_TIMEOUT_MS = 10000; // 10 secondes timeout
/**
* Effectue une recherche sur Bodacc pour un nom/prénom donné
* @param {string} lastName - Nom de famille (obligatoire)
* @param {string} firstName - Prénom (optionnel)
* @returns {Promise<Object>} Résultat de la recherche
*/
async function searchBodaccGelAvoirs(lastName, firstName = '') {
const startTime = Date.now();
try {
console.log(`[Bodacc] Recherche gel des avoirs pour: ${lastName} ${firstName}`);
// Construction de la requête de recherche
const searchParams = new URLSearchParams({
'q': `${lastName} ${firstName}`.trim(),
'type': 'gel-avoirs',
'date_debut': '2020-01-01', // Recherche sur les 4 dernières années
'date_fin': new Date().toISOString().split('T')[0]
});
const searchUrl = `${BODACC_BASE_URL}/recherche?${searchParams.toString()}`;
// Requête avec politesse
const response = await fetch(searchUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const html = await response.text();
const dom = new JSDOM(html);
const document = dom.window.document;
// Extraction des résultats
const results = extractBodaccResults(document, lastName, firstName);
// Délai de politesse avant la prochaine requête
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
const duration = Date.now() - startTime;
console.log(`[Bodacc] Recherche terminée en ${duration}ms, ${results.length} résultats`);
return {
success: true,
duration,
results,
source: 'bodacc.fr',
searchUrl,
timestamp: new Date().toISOString()
};
} catch (error) {
const duration = Date.now() - startTime;
console.error(`[Bodacc] Erreur recherche:`, error.message);
return {
success: false,
duration,
error: error.message,
results: [],
source: 'bodacc.fr',
timestamp: new Date().toISOString()
};
}
}
/**
* Extrait les résultats de la page Bodacc
* @param {Document} document - Document DOM parsé
* @param {string} lastName - Nom recherché
* @param {string} firstName - Prénom recherché
* @returns {Array} Liste des résultats trouvés
*/
function extractBodaccResults(document, lastName, firstName) {
const results = [];
try {
// Sélecteurs pour les résultats de gel des avoirs
const resultSelectors = [
'.result-item',
'.search-result',
'.bodacc-result',
'[data-type="gel-avoirs"]'
];
let resultElements = [];
for (const selector of resultSelectors) {
resultElements = document.querySelectorAll(selector);
if (resultElements.length > 0) break;
}
// Si pas de sélecteur spécifique, chercher dans le contenu général
if (resultElements.length === 0) {
const content = document.body.textContent || '';
if (content.includes('gel des avoirs') || content.includes('GEL DES AVOIRS')) {
// Résultat générique si on trouve des mentions
results.push({
name: `${firstName} ${lastName}`.trim(),
type: 'gel-avoirs',
date: new Date().toISOString().split('T')[0],
sourceUrl: BODACC_BASE_URL,
matchScore: 0.7,
description: 'Mention de gel des avoirs détectée dans les résultats Bodacc'
});
}
} else {
// Traitement des éléments de résultats spécifiques
resultElements.forEach((element, index) => {
try {
const nameElement = element.querySelector('.name, .nom, .person-name, h3, h4');
const dateElement = element.querySelector('.date, .publication-date, .date-publication');
const linkElement = element.querySelector('a[href]');
const name = nameElement ? nameElement.textContent.trim() : `${firstName} ${lastName}`.trim();
const date = dateElement ? dateElement.textContent.trim() : new Date().toISOString().split('T')[0];
const sourceUrl = linkElement ? new URL(linkElement.href, BODACC_BASE_URL).href : BODACC_BASE_URL;
// Calcul du score de correspondance basique
const matchScore = calculateMatchScore(name, lastName, firstName);
if (matchScore > 0.3) { // Seuil minimum de correspondance
results.push({
name,
type: 'gel-avoirs',
date,
sourceUrl,
matchScore,
description: `Résultat ${index + 1} de gel des avoirs sur Bodacc`
});
}
} catch (elementError) {
console.warn(`[Bodacc] Erreur traitement élément ${index}:`, elementError.message);
}
});
}
} catch (error) {
console.warn(`[Bodacc] Erreur extraction résultats:`, error.message);
}
return results;
}
/**
* Calcule un score de correspondance entre le nom trouvé et le nom recherché
* @param {string} foundName - Nom trouvé dans les résultats
* @param {string} lastName - Nom recherché
* @param {string} firstName - Prénom recherché
* @returns {number} Score entre 0 et 1
*/
function calculateMatchScore(foundName, lastName, firstName) {
const found = foundName.toLowerCase();
const last = lastName.toLowerCase();
const first = firstName.toLowerCase();
let score = 0;
// Correspondance exacte du nom de famille
if (found.includes(last)) {
score += 0.6;
}
// Correspondance du prénom si fourni
if (first && found.includes(first)) {
score += 0.4;
}
// Bonus pour correspondance exacte
if (found === `${first} ${last}`.trim().toLowerCase()) {
score = 1.0;
}
return Math.min(score, 1.0);
}
module.exports = {
searchBodaccGelAvoirs,
generateBodaccSummary
};
/**
* Génère un résumé des résultats pour le PDF
* @param {Array} results - Résultats de la recherche
* @param {string} lastName - Nom recherché
* @param {string} firstName - Prénom recherché
* @returns {Object} Résumé formaté
*/
function generateBodaccSummary(results, lastName, firstName) {
const totalResults = results.length;
const highConfidenceResults = results.filter(r => r.matchScore > 0.7);
const recentResults = results.filter(r => {
const resultDate = new Date(r.date);
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
return resultDate > oneYearAgo;
});
return {
searchTarget: `${firstName} ${lastName}`.trim(),
totalResults,
highConfidenceResults: highConfidenceResults.length,
recentResults: recentResults.length,
hasGelAvoirs: totalResults > 0,
riskLevel: totalResults === 0 ? 'Aucun' :
highConfidenceResults.length > 0 ? 'Élevé' :
totalResults > 0 ? 'Moyen' : 'Faible',
summary: totalResults === 0 ?
'Aucune mention de gel des avoirs trouvée sur Bodacc' :
`${totalResults} mention(s) trouvée(s), ${highConfidenceResults.length} avec haute confiance`
};
}

View File

@ -1,395 +0,0 @@
/**
* Collecteur GéoFoncier
* Accès aux données foncières et immobilières via l'API GéoFoncier
*/
const fetch = require('node-fetch');
const GEOFONCIER_BASE_URL = 'https://api2.geofoncier.fr';
const GEOFONCIER_EXPERT_URL = 'https://site-expert.geofoncier.fr';
const USER_AGENT = '4NK-IA-Front/1.0 (Document Analysis Tool)';
const REQUEST_TIMEOUT_MS = 20000; // 20 secondes timeout
const REQUEST_DELAY_MS = 2000; // 2 secondes entre les requêtes
/**
* Recherche les informations foncières pour une adresse
* @param {Object} address - Adresse à rechercher
* @returns {Promise<Object>} Résultat de la recherche GéoFoncier
*/
async function searchGeofoncierInfo(address) {
const startTime = Date.now();
try {
console.log(`[GéoFoncier] Recherche info foncière pour: ${address.street}, ${address.city}`);
// Étape 1: Géocodage de l'adresse
const geocodeResult = await geocodeAddress(address);
if (!geocodeResult.success || !geocodeResult.coordinates) {
return {
success: false,
duration: Date.now() - startTime,
error: 'Géocodage échoué',
address,
timestamp: new Date().toISOString()
};
}
// Étape 2: Recherche des parcelles cadastrales
const parcellesResult = await searchParcelles(geocodeResult.coordinates);
// Délai de politesse
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
// Étape 3: Recherche des informations foncières
const foncierResult = await searchInfoFonciere(geocodeResult.coordinates);
// Délai de politesse
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
// Étape 4: Recherche des mutations récentes
const mutationsResult = await searchMutations(geocodeResult.coordinates);
const duration = Date.now() - startTime;
console.log(`[GéoFoncier] Recherche terminée en ${duration}ms`);
return {
success: true,
duration,
address,
geocode: geocodeResult,
parcelles: parcellesResult,
infoFonciere: foncierResult,
mutations: mutationsResult,
timestamp: new Date().toISOString(),
source: 'geofoncier.fr'
};
} catch (error) {
const duration = Date.now() - startTime;
console.error(`[GéoFoncier] Erreur recherche:`, error.message);
return {
success: false,
duration,
address,
error: error.message,
timestamp: new Date().toISOString()
};
}
}
/**
* Géocode une adresse via GéoFoncier
* @param {Object} address - Adresse à géocoder
* @returns {Promise<Object>} Résultat du géocodage
*/
async function geocodeAddress(address) {
try {
const query = `${address.street}, ${address.postalCode} ${address.city}`;
const geocodeUrl = `${GEOFONCIER_BASE_URL}/geocoding/search?q=${encodeURIComponent(query)}&limit=1`;
const response = await fetch(geocodeUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'application/json'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.features && data.features.length > 0) {
const feature = data.features[0];
return {
success: true,
coordinates: {
lat: feature.geometry.coordinates[1],
lon: feature.geometry.coordinates[0]
},
label: feature.properties.label,
score: feature.properties.score || 0,
data: feature.properties
};
}
return {
success: false,
error: 'Aucun résultat de géocodage'
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* Recherche les parcelles cadastrales
* @param {Object} coordinates - Coordonnées {lat, lon}
* @returns {Promise<Object>} Résultat de la recherche de parcelles
*/
async function searchParcelles(coordinates) {
try {
const parcellesUrl = `${GEOFONCIER_BASE_URL}/cadastre/parcelles?lat=${coordinates.lat}&lon=${coordinates.lon}&radius=100`;
const response = await fetch(parcellesUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'application/json'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
const parcelles = [];
if (data.parcelles && Array.isArray(data.parcelles)) {
for (const parcelle of data.parcelles) {
parcelles.push({
numero: parcelle.numero || '',
section: parcelle.section || '',
commune: parcelle.commune || '',
surface: parcelle.surface || 0,
nature: parcelle.nature || '',
adresse: parcelle.adresse || '',
proprietaire: parcelle.proprietaire || '',
dateMutation: parcelle.dateMutation || '',
valeur: parcelle.valeur || 0
});
}
}
return {
success: true,
parcelles,
total: parcelles.length
};
} catch (error) {
return {
success: false,
error: error.message,
parcelles: []
};
}
}
/**
* Recherche les informations foncières
* @param {Object} coordinates - Coordonnées {lat, lon}
* @returns {Promise<Object>} Résultat de la recherche foncière
*/
async function searchInfoFonciere(coordinates) {
try {
const foncierUrl = `${GEOFONCIER_EXPERT_URL}/api/foncier/info?lat=${coordinates.lat}&lon=${coordinates.lon}`;
const response = await fetch(foncierUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'application/json'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return {
success: true,
info: {
valeurFonciere: data.valeurFonciere || 0,
valeurLocative: data.valeurLocative || 0,
surfaceHabitable: data.surfaceHabitable || 0,
surfaceTerrain: data.surfaceTerrain || 0,
nombrePieces: data.nombrePieces || 0,
anneeConstruction: data.anneeConstruction || '',
typeHabitation: data.typeHabitation || '',
energie: data.energie || '',
ges: data.ges || '',
diagnostics: data.diagnostics || []
}
};
} catch (error) {
return {
success: false,
error: error.message,
info: null
};
}
}
/**
* Recherche les mutations récentes
* @param {Object} coordinates - Coordonnées {lat, lon}
* @returns {Promise<Object>} Résultat de la recherche de mutations
*/
async function searchMutations(coordinates) {
try {
const mutationsUrl = `${GEOFONCIER_BASE_URL}/mutations/recentes?lat=${coordinates.lat}&lon=${coordinates.lon}&radius=200&years=5`;
const response = await fetch(mutationsUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'application/json'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
const mutations = [];
if (data.mutations && Array.isArray(data.mutations)) {
for (const mutation of data.mutations) {
mutations.push({
date: mutation.date || '',
type: mutation.type || '',
valeur: mutation.valeur || 0,
surface: mutation.surface || 0,
prixM2: mutation.prixM2 || 0,
vendeur: mutation.vendeur || '',
acheteur: mutation.acheteur || '',
adresse: mutation.adresse || '',
reference: mutation.reference || ''
});
}
}
return {
success: true,
mutations,
total: mutations.length,
periode: '5 dernières années'
};
} catch (error) {
return {
success: false,
error: error.message,
mutations: []
};
}
}
/**
* Recherche les informations de zonage
* @param {Object} coordinates - Coordonnées {lat, lon}
* @returns {Promise<Object>} Résultat de la recherche de zonage
*/
async function searchZonage(coordinates) {
try {
const zonageUrl = `${GEOFONCIER_BASE_URL}/zonage/info?lat=${coordinates.lat}&lon=${coordinates.lon}`;
const response = await fetch(zonageUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'application/json'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return {
success: true,
zonage: {
zone: data.zone || '',
sousZone: data.sousZone || '',
constructibilite: data.constructibilite || '',
hauteurMax: data.hauteurMax || '',
densiteMax: data.densiteMax || '',
servitudes: data.servitudes || [],
restrictions: data.restrictions || []
}
};
} catch (error) {
return {
success: false,
error: error.message,
zonage: null
};
}
}
/**
* Recherche les informations de voirie
* @param {Object} coordinates - Coordonnées {lat, lon}
* @returns {Promise<Object>} Résultat de la recherche de voirie
*/
async function searchVoirie(coordinates) {
try {
const voirieUrl = `${GEOFONCIER_BASE_URL}/voirie/info?lat=${coordinates.lat}&lon=${coordinates.lon}`;
const response = await fetch(voirieUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'application/json'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return {
success: true,
voirie: {
type: data.type || '',
largeur: data.largeur || 0,
revetement: data.revetement || '',
eclairage: data.eclairage || false,
canalisations: data.canalisations || [],
transports: data.transports || []
}
};
} catch (error) {
return {
success: false,
error: error.message,
voirie: null
};
}
}
module.exports = {
searchGeofoncierInfo,
geocodeAddress,
searchParcelles,
searchInfoFonciere,
searchMutations,
searchZonage,
searchVoirie
};

View File

@ -1,430 +0,0 @@
/**
* Collecteur Inforgreffe/Societe.com - Informations entreprises
* Scraping léger avec politesse pour récupérer les données d'entreprises
*/
const fetch = require('node-fetch');
const { JSDOM } = require('jsdom');
const SOCIETE_COM_BASE_URL = 'https://www.societe.com';
const INFORGREFFE_BASE_URL = 'https://www.inforgreffe.com';
const USER_AGENT = '4NK-IA-Front/1.0 (Document Analysis Tool)';
const REQUEST_DELAY_MS = 1500; // 1.5 secondes entre les requêtes
const REQUEST_TIMEOUT_MS = 12000; // 12 secondes timeout
/**
* Recherche une entreprise sur Societe.com et Inforgreffe
* @param {string} companyName - Nom de l'entreprise
* @param {string} siren - SIREN (optionnel)
* @returns {Promise<Object>} Résultat de la recherche
*/
async function searchCompanyInfo(companyName, siren = '') {
const startTime = Date.now();
try {
console.log(`[Inforgreffe] Recherche entreprise: ${companyName} ${siren ? `(SIREN: ${siren})` : ''}`);
// Recherche sur Societe.com d'abord (plus accessible)
const societeComResult = await searchSocieteCom(companyName, siren);
// Délai de politesse
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
// Recherche sur Inforgreffe si SIREN disponible
let inforgreffeResult = null;
if (siren) {
inforgreffeResult = await searchInforgreffe(siren);
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
}
// Fusion des résultats
const mergedResult = mergeCompanyResults(societeComResult, inforgreffeResult, companyName);
const duration = Date.now() - startTime;
console.log(`[Inforgreffe] Recherche terminée en ${duration}ms`);
return {
success: true,
duration,
company: mergedResult,
sources: {
societeCom: societeComResult,
inforgreffe: inforgreffeResult
},
timestamp: new Date().toISOString()
};
} catch (error) {
const duration = Date.now() - startTime;
console.error(`[Inforgreffe] Erreur recherche:`, error.message);
return {
success: false,
duration,
error: error.message,
company: null,
sources: {},
timestamp: new Date().toISOString()
};
}
}
/**
* Recherche sur Societe.com
* @param {string} companyName - Nom de l'entreprise
* @param {string} siren - SIREN (optionnel)
* @returns {Promise<Object>} Résultat Societe.com
*/
async function searchSocieteCom(companyName, siren = '') {
try {
// Construction de l'URL de recherche
const searchQuery = siren || companyName;
const searchUrl = `${SOCIETE_COM_BASE_URL}/search?q=${encodeURIComponent(searchQuery)}`;
const response = await fetch(searchUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const html = await response.text();
const dom = new JSDOM(html);
const document = dom.window.document;
return extractSocieteComData(document, companyName, siren);
} catch (error) {
console.warn(`[Societe.com] Erreur:`, error.message);
return {
success: false,
error: error.message,
data: null
};
}
}
/**
* Recherche sur Inforgreffe
* @param {string} siren - SIREN de l'entreprise
* @returns {Promise<Object>} Résultat Inforgreffe
*/
async function searchInforgreffe(siren) {
try {
// URL de recherche par SIREN
const searchUrl = `${INFORGREFFE_BASE_URL}/entreprise/${siren}`;
const response = await fetch(searchUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const html = await response.text();
const dom = new JSDOM(html);
const document = dom.window.document;
return extractInforgreffeData(document, siren);
} catch (error) {
console.warn(`[Inforgreffe] Erreur:`, error.message);
return {
success: false,
error: error.message,
data: null
};
}
}
/**
* Extrait les données de Societe.com
* @param {Document} document - Document DOM parsé
* @param {string} companyName - Nom de l'entreprise
* @param {string} siren - SIREN
* @returns {Object} Données extraites
*/
function extractSocieteComData(document, companyName, siren) {
try {
const data = {
name: companyName,
siren: siren,
siret: '',
forme: '',
capital: '',
adresse: '',
dirigeants: [],
activite: '',
dateCreation: '',
sourceUrl: SOCIETE_COM_BASE_URL
};
// Extraction du nom de l'entreprise
const nameElement = document.querySelector('.company-name, .nom-entreprise, h1, .title');
if (nameElement) {
data.name = nameElement.textContent.trim();
}
// Extraction du SIREN/SIRET
const sirenElement = document.querySelector('.siren, .num-siren, [data-siren]');
if (sirenElement) {
const sirenText = sirenElement.textContent || sirenElement.getAttribute('data-siren');
data.siren = extractSiren(sirenText);
}
// Extraction de la forme juridique
const formeElement = document.querySelector('.forme, .forme-juridique, .legal-form');
if (formeElement) {
data.forme = formeElement.textContent.trim();
}
// Extraction du capital
const capitalElement = document.querySelector('.capital, .capital-social, .share-capital');
if (capitalElement) {
data.capital = capitalElement.textContent.trim();
}
// Extraction de l'adresse
const adresseElement = document.querySelector('.adresse, .address, .company-address');
if (adresseElement) {
data.adresse = adresseElement.textContent.trim();
}
// Extraction des dirigeants
const dirigeantsElements = document.querySelectorAll('.dirigeant, .manager, .president, .gérant');
dirigeantsElements.forEach(element => {
const name = element.textContent.trim();
if (name && name.length > 2) {
data.dirigeants.push({
nom: name,
fonction: 'Dirigeant',
source: 'societe.com'
});
}
});
// Extraction de l'activité
const activiteElement = document.querySelector('.activite, .activity, .secteur');
if (activiteElement) {
data.activite = activiteElement.textContent.trim();
}
// Extraction de la date de création
const dateElement = document.querySelector('.date-creation, .creation-date, .date-creation-entreprise');
if (dateElement) {
data.dateCreation = dateElement.textContent.trim();
}
return {
success: true,
data
};
} catch (error) {
console.warn(`[Societe.com] Erreur extraction:`, error.message);
return {
success: false,
error: error.message,
data: null
};
}
}
/**
* Extrait les données d'Inforgreffe
* @param {Document} document - Document DOM parsé
* @param {string} siren - SIREN
* @returns {Object} Données extraites
*/
function extractInforgreffeData(document, siren) {
try {
const data = {
siren: siren,
name: '',
siret: '',
forme: '',
capital: '',
adresse: '',
dirigeants: [],
activite: '',
dateCreation: '',
sourceUrl: `${INFORGREFFE_BASE_URL}/entreprise/${siren}`
};
// Extraction du nom de l'entreprise
const nameElement = document.querySelector('.company-name, .nom-entreprise, h1, .title');
if (nameElement) {
data.name = nameElement.textContent.trim();
}
// Extraction du SIRET
const siretElement = document.querySelector('.siret, .num-siret, [data-siret]');
if (siretElement) {
data.siret = siretElement.textContent.trim();
}
// Extraction de la forme juridique
const formeElement = document.querySelector('.forme, .forme-juridique, .legal-form');
if (formeElement) {
data.forme = formeElement.textContent.trim();
}
// Extraction du capital
const capitalElement = document.querySelector('.capital, .capital-social, .share-capital');
if (capitalElement) {
data.capital = capitalElement.textContent.trim();
}
// Extraction de l'adresse
const adresseElement = document.querySelector('.adresse, .address, .company-address');
if (adresseElement) {
data.adresse = adresseElement.textContent.trim();
}
// Extraction des dirigeants
const dirigeantsElements = document.querySelectorAll('.dirigeant, .manager, .president, .gérant');
dirigeantsElements.forEach(element => {
const name = element.textContent.trim();
if (name && name.length > 2) {
data.dirigeants.push({
nom: name,
fonction: 'Dirigeant',
source: 'inforgreffe.com'
});
}
});
return {
success: true,
data
};
} catch (error) {
console.warn(`[Inforgreffe] Erreur extraction:`, error.message);
return {
success: false,
error: error.message,
data: null
};
}
}
/**
* Fusionne les résultats de Societe.com et Inforgreffe
* @param {Object} societeComResult - Résultat Societe.com
* @param {Object} inforgreffeResult - Résultat Inforgreffe
* @param {string} originalName - Nom original de l'entreprise
* @returns {Object} Données fusionnées
*/
function mergeCompanyResults(societeComResult, inforgreffeResult, originalName) {
const merged = {
name: originalName,
siren: '',
siret: '',
forme: '',
capital: '',
adresse: '',
dirigeants: [],
activite: '',
dateCreation: '',
sources: []
};
// Fusion des données Societe.com
if (societeComResult.success && societeComResult.data) {
const sc = societeComResult.data;
merged.name = sc.name || merged.name;
merged.siren = sc.siren || merged.siren;
merged.siret = sc.siret || merged.siret;
merged.forme = sc.forme || merged.forme;
merged.capital = sc.capital || merged.capital;
merged.adresse = sc.adresse || merged.adresse;
merged.activite = sc.activite || merged.activite;
merged.dateCreation = sc.dateCreation || merged.dateCreation;
merged.dirigeants.push(...sc.dirigeants);
merged.sources.push('societe.com');
}
// Fusion des données Inforgreffe
if (inforgreffeResult && inforgreffeResult.success && inforgreffeResult.data) {
const ig = inforgreffeResult.data;
merged.name = ig.name || merged.name;
merged.siren = ig.siren || merged.siren;
merged.siret = ig.siret || merged.siret;
merged.forme = ig.forme || merged.forme;
merged.capital = ig.capital || merged.capital;
merged.adresse = ig.adresse || merged.adresse;
merged.dateCreation = ig.dateCreation || merged.dateCreation;
merged.dirigeants.push(...ig.dirigeants);
merged.sources.push('inforgreffe.com');
}
// Déduplication des dirigeants
merged.dirigeants = merged.dirigeants.filter((dirigeant, index, self) =>
index === self.findIndex(d => d.nom === dirigeant.nom)
);
return merged;
}
/**
* Extrait le SIREN d'un texte
* @param {string} text - Texte contenant potentiellement un SIREN
* @returns {string} SIREN extrait
*/
function extractSiren(text) {
if (!text) return '';
// Recherche d'un SIREN (9 chiffres)
const sirenMatch = text.match(/\b(\d{9})\b/);
return sirenMatch ? sirenMatch[1] : '';
}
/**
* Génère un résumé des données d'entreprise pour le PDF
* @param {Object} companyData - Données de l'entreprise
* @returns {Object} Résumé formaté
*/
function generateCompanySummary(companyData) {
return {
name: companyData.name,
siren: companyData.siren,
siret: companyData.siret,
forme: companyData.forme,
capital: companyData.capital,
adresse: companyData.adresse,
dirigeantsCount: companyData.dirigeants.length,
activite: companyData.activite,
dateCreation: companyData.dateCreation,
sources: companyData.sources,
hasCompleteInfo: !!(companyData.siren && companyData.forme && companyData.adresse),
summary: companyData.siren ?
`Entreprise trouvée: ${companyData.name} (SIREN: ${companyData.siren})` :
`Informations partielles pour: ${companyData.name}`
};
}
module.exports = {
searchCompanyInfo,
generateCompanySummary
};

View File

@ -1,436 +0,0 @@
/**
* Générateur de PDF pour les entités enrichies
* Génère des PDF formatés pour les personnes, adresses et entreprises
*/
const fs = require('fs').promises;
const path = require('path');
/**
* Génère un PDF pour une personne (Bodacc - gel des avoirs)
* @param {Object} personData - Données de la personne
* @param {Object} bodaccResult - Résultat de la recherche Bodacc
* @param {string} outputPath - Chemin de sortie du PDF
* @returns {Promise<string>} Chemin du PDF généré
*/
async function generatePersonPdf(personData, bodaccResult, outputPath) {
try {
const pdfContent = generatePersonPdfContent(personData, bodaccResult);
await fs.writeFile(outputPath, pdfContent, 'utf8');
console.log(`[PDF] Personne généré: ${outputPath}`);
return outputPath;
} catch (error) {
console.error(`[PDF] Erreur génération personne:`, error.message);
throw error;
}
}
/**
* Génère un PDF pour une entreprise (Inforgreffe/Societe.com)
* @param {Object} companyData - Données de l'entreprise
* @param {Object} inforgreffeResult - Résultat de la recherche Inforgreffe
* @param {string} outputPath - Chemin de sortie du PDF
* @returns {Promise<string>} Chemin du PDF généré
*/
async function generateCompanyPdf(companyData, inforgreffeResult, outputPath) {
try {
const pdfContent = generateCompanyPdfContent(companyData, inforgreffeResult);
await fs.writeFile(outputPath, pdfContent, 'utf8');
console.log(`[PDF] Entreprise généré: ${outputPath}`);
return outputPath;
} catch (error) {
console.error(`[PDF] Erreur génération entreprise:`, error.message);
throw error;
}
}
/**
* Génère un PDF pour une adresse (Cadastre/GéoRisque)
* @param {Object} addressData - Données de l'adresse
* @param {Object} geoResult - Résultat de la recherche géographique
* @param {string} outputPath - Chemin de sortie du PDF
* @returns {Promise<string>} Chemin du PDF généré
*/
async function generateAddressPdf(addressData, geoResult, outputPath) {
try {
const pdfContent = generateAddressPdfContent(addressData, geoResult);
await fs.writeFile(outputPath, pdfContent, 'utf8');
console.log(`[PDF] Adresse généré: ${outputPath}`);
return outputPath;
} catch (error) {
console.error(`[PDF] Erreur génération adresse:`, error.message);
throw error;
}
}
/**
* Génère le contenu HTML pour le PDF d'une personne
* @param {Object} personData - Données de la personne
* @param {Object} bodaccResult - Résultat Bodacc
* @returns {string} Contenu HTML
*/
function generatePersonPdfContent(personData, bodaccResult) {
const summary = bodaccResult.summary || {};
const results = bodaccResult.results || [];
return `
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rapport Bodacc - ${personData.firstName} ${personData.lastName}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; line-height: 1.6; }
.header { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
.title { color: #333; margin: 0; }
.subtitle { color: #666; margin: 5px 0 0 0; }
.section { margin: 20px 0; }
.section h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.info-item { background: #f9f9f9; padding: 15px; border-radius: 5px; }
.info-label { font-weight: bold; color: #555; }
.info-value { margin-top: 5px; }
.risk-high { color: #e74c3c; font-weight: bold; }
.risk-medium { color: #f39c12; font-weight: bold; }
.risk-low { color: #27ae60; font-weight: bold; }
.result-item { background: #fff; border: 1px solid #ddd; padding: 15px; margin: 10px 0; border-radius: 5px; }
.result-date { color: #666; font-size: 0.9em; }
.result-score { background: #3498db; color: white; padding: 2px 8px; border-radius: 3px; font-size: 0.8em; }
.footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 0.9em; }
.no-results { text-align: center; color: #27ae60; font-style: italic; padding: 20px; }
</style>
</head>
<body>
<div class="header">
<h1 class="title">Rapport Bodacc - Gel des avoirs</h1>
<p class="subtitle">Généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}</p>
</div>
<div class="section">
<h2>Identité recherchée</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Nom complet</div>
<div class="info-value">${personData.firstName} ${personData.lastName}</div>
</div>
<div class="info-item">
<div class="info-label">Date de naissance</div>
<div class="info-value">${personData.birthDate || 'Non renseignée'}</div>
</div>
</div>
</div>
<div class="section">
<h2>Résumé de la recherche</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Statut</div>
<div class="info-value class="risk-${summary.riskLevel?.toLowerCase() || 'low'}">${summary.riskLevel || 'Faible'}</div>
</div>
<div class="info-item">
<div class="info-label">Résultats trouvés</div>
<div class="info-value">${summary.totalResults || 0}</div>
</div>
<div class="info-item">
<div class="info-label">Haute confiance</div>
<div class="info-value">${summary.highConfidenceResults || 0}</div>
</div>
<div class="info-item">
<div class="info-label">Récents (1 an)</div>
<div class="info-value">${summary.recentResults || 0}</div>
</div>
</div>
<p><strong>Synthèse:</strong> ${summary.summary || 'Aucune donnée disponible'}</p>
</div>
<div class="section">
<h2>Détail des résultats</h2>
${results.length === 0 ?
'<div class="no-results">✅ Aucune mention de gel des avoirs trouvée</div>' :
results.map(result => `
<div class="result-item">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0;">${result.name}</h3>
<span class="result-score">Score: ${(result.matchScore * 100).toFixed(0)}%</span>
</div>
<div class="result-date">Date: ${result.date}</div>
<p><strong>Description:</strong> ${result.description}</p>
<p><strong>Source:</strong> <a href="${result.sourceUrl}" target="_blank">${result.sourceUrl}</a></p>
</div>
`).join('')
}
</div>
<div class="footer">
<p><strong>Source:</strong> ${bodaccResult.source || 'Bodacc.fr'}</p>
<p><strong>Recherche effectuée le:</strong> ${bodaccResult.timestamp ? new Date(bodaccResult.timestamp).toLocaleString('fr-FR') : 'Non disponible'}</p>
<p><strong>Durée de la recherche:</strong> ${bodaccResult.duration || 0}ms</p>
<p><em>Ce rapport a été généré automatiquement par 4NK IA Front. Les informations sont fournies à titre indicatif.</em></p>
</div>
</body>
</html>`;
}
/**
* Génère le contenu HTML pour le PDF d'une entreprise
* @param {Object} companyData - Données de l'entreprise
* @param {Object} inforgreffeResult - Résultat Inforgreffe
* @returns {string} Contenu HTML
*/
function generateCompanyPdfContent(companyData, inforgreffeResult) {
const company = inforgreffeResult.company || {};
const sources = inforgreffeResult.sources || {};
return `
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rapport Entreprise - ${company.name || companyData.name}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; line-height: 1.6; }
.header { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
.title { color: #333; margin: 0; }
.subtitle { color: #666; margin: 5px 0 0 0; }
.section { margin: 20px 0; }
.section h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.info-item { background: #f9f9f9; padding: 15px; border-radius: 5px; }
.info-label { font-weight: bold; color: #555; }
.info-value { margin-top: 5px; }
.status-ok { color: #27ae60; font-weight: bold; }
.status-partial { color: #f39c12; font-weight: bold; }
.status-missing { color: #e74c3c; font-weight: bold; }
.dirigeant-item { background: #fff; border: 1px solid #ddd; padding: 10px; margin: 5px 0; border-radius: 5px; }
.footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 0.9em; }
</style>
</head>
<body>
<div class="header">
<h1 class="title">Rapport Entreprise</h1>
<p class="subtitle">Généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}</p>
</div>
<div class="section">
<h2>Informations générales</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Raison sociale</div>
<div class="info-value">${company.name || companyData.name || 'Non renseignée'}</div>
</div>
<div class="info-item">
<div class="info-label">SIREN</div>
<div class="info-value class="status-${company.siren ? 'ok' : 'missing'}">${company.siren || 'Non trouvé'}</div>
</div>
<div class="info-item">
<div class="info-label">SIRET</div>
<div class="info-value class="status-${company.siret ? 'ok' : 'missing'}">${company.siret || 'Non trouvé'}</div>
</div>
<div class="info-item">
<div class="info-label">Forme juridique</div>
<div class="info-value class="status-${company.forme ? 'ok' : 'missing'}">${company.forme || 'Non renseignée'}</div>
</div>
<div class="info-item">
<div class="info-label">Capital social</div>
<div class="info-value class="status-${company.capital ? 'ok' : 'missing'}">${company.capital || 'Non renseigné'}</div>
</div>
<div class="info-item">
<div class="info-label">Date de création</div>
<div class="info-value class="status-${company.dateCreation ? 'ok' : 'missing'}">${company.dateCreation || 'Non renseignée'}</div>
</div>
</div>
</div>
<div class="section">
<h2>Adresse</h2>
<div class="info-item">
<div class="info-label">Adresse complète</div>
<div class="info-value class="status-${company.adresse ? 'ok' : 'missing'}">${company.adresse || 'Non renseignée'}</div>
</div>
</div>
<div class="section">
<h2>Activité</h2>
<div class="info-item">
<div class="info-label">Secteur d'activité</div>
<div class="info-value class="status-${company.activite ? 'ok' : 'missing'}">${company.activite || 'Non renseignée'}</div>
</div>
</div>
<div class="section">
<h2>Dirigeants (${company.dirigeants?.length || 0})</h2>
${company.dirigeants && company.dirigeants.length > 0 ?
company.dirigeants.map(dirigeant => `
<div class="dirigeant-item">
<strong>${dirigeant.nom}</strong> - ${dirigeant.fonction || 'Dirigeant'}
<br><small>Source: ${dirigeant.source || 'Non spécifiée'}</small>
</div>
`).join('') :
'<p class="status-missing">Aucun dirigeant trouvé</p>'
}
</div>
<div class="section">
<h2>Sources consultées</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Societe.com</div>
<div class="info-value class="status-${sources.societeCom?.success ? 'ok' : 'missing'}">${sources.societeCom?.success ? '✅ Consulté' : '❌ Non accessible'}</div>
</div>
<div class="info-item">
<div class="info-label">Inforgreffe</div>
<div class="info-value class="status-${sources.inforgreffe?.success ? 'ok' : 'missing'}">${sources.inforgreffe?.success ? '✅ Consulté' : '❌ Non accessible'}</div>
</div>
</div>
</div>
<div class="footer">
<p><strong>Recherche effectuée le:</strong> ${inforgreffeResult.timestamp ? new Date(inforgreffeResult.timestamp).toLocaleString('fr-FR') : 'Non disponible'}</p>
<p><strong>Durée de la recherche:</strong> ${inforgreffeResult.duration || 0}ms</p>
<p><em>Ce rapport a été généré automatiquement par 4NK IA Front. Les informations sont fournies à titre indicatif.</em></p>
</div>
</body>
</html>`;
}
/**
* Génère le contenu HTML pour le PDF d'une adresse
* @param {Object} addressData - Données de l'adresse
* @param {Object} geoResult - Résultat géographique
* @returns {string} Contenu HTML
*/
function generateAddressPdfContent(addressData, geoResult) {
const geocode = geoResult.geocode || {}
const risks = geoResult.risks || []
const cadastre = geoResult.cadastre || []
return `
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rapport Adresse - ${addressData.street || 'Adresse'}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; line-height: 1.6; }
.header { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
.title { color: #333; margin: 0; }
.subtitle { color: #666; margin: 5px 0 0 0; }
.section { margin: 20px 0; }
.section h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.info-item { background: #f9f9f9; padding: 15px; border-radius: 5px; }
.info-label { font-weight: bold; color: #555; }
.info-value { margin-top: 5px; }
.status-ok { color: #27ae60; font-weight: bold; }
.status-error { color: #e74c3c; font-weight: bold; }
.risk-item { background: #fff; border: 1px solid #ddd; padding: 10px; margin: 5px 0; border-radius: 5px; }
.risk-high { border-left: 4px solid #e74c3c; }
.risk-medium { border-left: 4px solid #f39c12; }
.risk-low { border-left: 4px solid #27ae60; }
.parcelle-item { background: #fff; border: 1px solid #ddd; padding: 10px; margin: 5px 0; border-radius: 5px; }
.footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 0.9em; }
.no-data { text-align: center; color: #666; font-style: italic; padding: 20px; }
</style>
</head>
<body>
<div class="header">
<h1 class="title">Rapport Adresse</h1>
<p class="subtitle">Généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}</p>
</div>
<div class="section">
<h2>Adresse analysée</h2>
<div class="info-item">
<div class="info-label">Adresse complète</div>
<div class="info-value">${addressData.street || ''} ${addressData.postalCode || ''} ${addressData.city || ''} ${addressData.country || ''}</div>
</div>
</div>
<div class="section">
<h2>Géocodage</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Statut</div>
<div class="info-value class="status-${geocode.success ? 'ok' : 'error'}">${geocode.success ? '✅ Géocodé' : '❌ Échec'}</div>
</div>
<div class="info-item">
<div class="info-label">Score de confiance</div>
<div class="info-value">${geocode.score ? (geocode.score * 100).toFixed(0) + '%' : 'N/A'}</div>
</div>
<div class="info-item">
<div class="info-label">Coordonnées</div>
<div class="info-value">${geocode.lat && geocode.lon ? `${geocode.lat}, ${geocode.lon}` : 'Non disponibles'}</div>
</div>
<div class="info-item">
<div class="info-label">Adresse normalisée</div>
<div class="info-value">${geocode.label || 'Non disponible'}</div>
</div>
</div>
</div>
<div class="section">
<h2>Risques majeurs (${risks.length})</h2>
${risks.length === 0 ?
'<div class="no-data">✅ Aucun risque majeur identifié dans un rayon de 1km</div>' :
risks.map(risk => `
<div class="risk-item risk-${risk.level?.toLowerCase() || 'low'}">
<h3 style="margin: 0;">${risk.type}</h3>
<p><strong>Niveau:</strong> ${risk.level}</p>
<p><strong>Description:</strong> ${risk.description}</p>
${risk.distance ? `<p><strong>Distance:</strong> ${risk.distance}m</p>` : ''}
<p><strong>Source:</strong> <a href="${risk.sourceUrl}" target="_blank">GéoRisque</a></p>
</div>
`).join('')
}
</div>
<div class="section">
<h2>Informations cadastrales (${cadastre.length})</h2>
${cadastre.length === 0 ?
'<div class="no-data">Aucune parcelle cadastrale trouvée</div>' :
cadastre.map(parcelle => `
<div class="parcelle-item">
<h3 style="margin: 0;">Parcelle ${parcelle.section}${parcelle.numero}</h3>
<p><strong>Commune:</strong> ${parcelle.commune}</p>
${parcelle.surface ? `<p><strong>Surface:</strong> ${parcelle.surface}m²</p>` : ''}
<p><strong>Source:</strong> <a href="${parcelle.sourceUrl}" target="_blank">Cadastre</a></p>
</div>
`).join('')
}
</div>
<div class="section">
<h2>Sources consultées</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Base Adresse Nationale</div>
<div class="info-value class="status-${geocode.success ? 'ok' : 'error'}">${geocode.success ? '✅ Consultée' : '❌ Non accessible'}</div>
</div>
<div class="info-item">
<div class="info-label">GéoRisque</div>
<div class="info-value class="status-${risks.length >= 0 ? 'ok' : 'error'}"> Consulté</div>
</div>
<div class="info-item">
<div class="info-label">Cadastre</div>
<div class="info-value class="status-${cadastre.length >= 0 ? 'ok' : 'error'}"> Consulté</div>
</div>
</div>
</div>
<div class="footer">
<p><strong>Recherche effectuée le:</strong> ${geoResult.timestamp ? new Date(geoResult.timestamp).toLocaleString('fr-FR') : 'Non disponible'}</p>
<p><strong>Sources:</strong> ${(geoResult.sources || []).join(', ')}</p>
<p><em>Ce rapport a été généré automatiquement par 4NK IA Front. Les informations sont fournies à titre indicatif.</em></p>
</div>
</body>
</html>`;
}
module.exports = {
generatePersonPdf,
generateCompanyPdf,
generateAddressPdf
};

View File

@ -1,287 +0,0 @@
/**
* Collecteur RBE (Registre des Bénéficiaires Effectifs)
* Accès aux données des bénéficiaires effectifs via l'API RBE
*/
const fetch = require('node-fetch');
const RBE_BASE_URL = 'https://registre-beneficiaires-effectifs.inpi.fr';
const USER_AGENT = '4NK-IA-Front/1.0 (Document Analysis Tool)';
const REQUEST_TIMEOUT_MS = 15000; // 15 secondes timeout
/**
* Recherche les bénéficiaires effectifs pour une entreprise
* @param {string} siren - SIREN de l'entreprise
* @param {string} siret - SIRET de l'entreprise (optionnel)
* @returns {Promise<Object>} Résultat de la recherche RBE
*/
async function searchRBEBeneficiaires(siren, siret = '') {
const startTime = Date.now();
try {
console.log(`[RBE] Recherche bénéficiaires effectifs pour SIREN: ${siren}`);
// Vérification de la validité du SIREN
if (!siren || siren.length !== 9 || !/^\d{9}$/.test(siren)) {
throw new Error('SIREN invalide - doit contenir 9 chiffres');
}
// Construction de l'URL de recherche
const searchUrl = `${RBE_BASE_URL}/api/beneficiaires?q=${encodeURIComponent(siren)}`;
// Requête avec headers appropriés
const response = await fetch(searchUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'application/json',
'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
'Cache-Control': 'no-cache'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
if (response.status === 404) {
return {
success: true,
duration: Date.now() - startTime,
siren,
beneficiaires: [],
message: 'Aucun bénéficiaire effectif trouvé',
timestamp: new Date().toISOString()
};
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Traitement des données RBE
const beneficiaires = processRBEData(data, siren);
const duration = Date.now() - startTime;
console.log(`[RBE] Recherche terminée en ${duration}ms - ${beneficiaires.length} bénéficiaire(s) trouvé(s)`);
return {
success: true,
duration,
siren,
siret,
beneficiaires,
total: beneficiaires.length,
timestamp: new Date().toISOString(),
source: 'rbe.inpi.fr'
};
} catch (error) {
const duration = Date.now() - startTime;
console.error(`[RBE] Erreur recherche:`, error.message);
return {
success: false,
duration,
siren,
error: error.message,
beneficiaires: [],
timestamp: new Date().toISOString()
};
}
}
/**
* Traite les données RBE brutes
* @param {Object} data - Données brutes de l'API RBE
* @param {string} siren - SIREN de l'entreprise
* @returns {Array} Liste des bénéficiaires effectifs
*/
function processRBEData(data, siren) {
try {
const beneficiaires = [];
// Structure des données RBE (à adapter selon l'API réelle)
if (data.beneficiaires && Array.isArray(data.beneficiaires)) {
for (const benef of data.beneficiaires) {
const beneficiaire = {
nom: benef.nom || '',
prenom: benef.prenom || '',
dateNaissance: benef.dateNaissance || '',
nationalite: benef.nationalite || '',
adresse: benef.adresse || '',
qualite: benef.qualite || '',
pourcentage: benef.pourcentage || 0,
type: benef.type || 'personne_physique', // personne_physique ou personne_morale
siren: benef.siren || '',
dateDeclaration: benef.dateDeclaration || '',
statut: benef.statut || 'actif'
};
// Validation des données
if (beneficiaire.nom || beneficiaire.prenom) {
beneficiaires.push(beneficiaire);
}
}
}
return beneficiaires;
} catch (error) {
console.error(`[RBE] Erreur traitement données:`, error.message);
return [];
}
}
/**
* Recherche les informations détaillées d'un bénéficiaire
* @param {string} beneficiaireId - ID du bénéficiaire
* @returns {Promise<Object>} Informations détaillées
*/
async function getBeneficiaireDetails(beneficiaireId) {
const startTime = Date.now();
try {
console.log(`[RBE] Recherche détails bénéficiaire: ${beneficiaireId}`);
const detailsUrl = `${RBE_BASE_URL}/api/beneficiaires/${encodeURIComponent(beneficiaireId)}`;
const response = await fetch(detailsUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'application/json'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
const duration = Date.now() - startTime;
console.log(`[RBE] Détails récupérés en ${duration}ms`);
return {
success: true,
duration,
beneficiaireId,
details: data,
timestamp: new Date().toISOString()
};
} catch (error) {
const duration = Date.now() - startTime;
console.error(`[RBE] Erreur détails:`, error.message);
return {
success: false,
duration,
beneficiaireId,
error: error.message,
timestamp: new Date().toISOString()
};
}
}
/**
* Recherche les entreprises liées à une personne
* @param {string} nom - Nom de famille
* @param {string} prenom - Prénom (optionnel)
* @returns {Promise<Object>} Liste des entreprises
*/
async function searchPersonneEntreprises(nom, prenom = '') {
const startTime = Date.now();
try {
console.log(`[RBE] Recherche entreprises pour: ${nom} ${prenom}`);
const searchQuery = `${nom} ${prenom}`.trim();
const searchUrl = `${RBE_BASE_URL}/api/personnes?q=${encodeURIComponent(searchQuery)}`;
const response = await fetch(searchUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'application/json'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
const entreprises = processPersonneEntreprises(data);
const duration = Date.now() - startTime;
console.log(`[RBE] Recherche terminée en ${duration}ms - ${entreprises.length} entreprise(s) trouvée(s)`);
return {
success: true,
duration,
nom,
prenom,
entreprises,
total: entreprises.length,
timestamp: new Date().toISOString()
};
} catch (error) {
const duration = Date.now() - startTime;
console.error(`[RBE] Erreur recherche personne:`, error.message);
return {
success: false,
duration,
nom,
prenom,
error: error.message,
entreprises: [],
timestamp: new Date().toISOString()
};
}
}
/**
* Traite les données d'entreprises liées à une personne
* @param {Object} data - Données brutes
* @returns {Array} Liste des entreprises
*/
function processPersonneEntreprises(data) {
try {
const entreprises = [];
if (data.entreprises && Array.isArray(data.entreprises)) {
for (const ent of data.entreprises) {
const entreprise = {
siren: ent.siren || '',
denomination: ent.denomination || '',
forme: ent.forme || '',
adresse: ent.adresse || '',
qualite: ent.qualite || '',
pourcentage: ent.pourcentage || 0,
dateDeclaration: ent.dateDeclaration || '',
statut: ent.statut || 'actif'
};
if (entreprise.siren && entreprise.denomination) {
entreprises.push(entreprise);
}
}
}
return entreprises;
} catch (error) {
console.error(`[RBE] Erreur traitement entreprises:`, error.message);
return [];
}
}
module.exports = {
searchRBEBeneficiaires,
getBeneficiaireDetails,
searchPersonneEntreprises
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,293 +0,0 @@
const sharp = require('sharp')
const path = require('path')
const fs = require('fs')
const { execSync } = require('child_process')
const {
isCNIDocument,
enhanceCNIPreprocessing,
processCNIWithZones,
decodeMRZ,
} = require('./cniOcrEnhancer')
/**
* OCR amélioré avec support spécialisé pour les CNI
* Combine Tesseract avec des techniques de preprocessing avancées
*/
// Fonction pour exécuter Tesseract avec stratégies multiples et choisir le meilleur
async function runTesseractOCR(imageBuffer, options = {}) {
const tempInput = path.join(__dirname, `temp_input_${Date.now()}.png`)
const tempOutputBase = path.join(__dirname, `temp_output_${Date.now()}`)
fs.writeFileSync(tempInput, imageBuffer)
const strategies = []
const baseLang = options.language || 'fra'
const basePsm = options.psm || '6'
const baseOem = options.oem || '3'
// Stratégies génériques
strategies.push({ lang: baseLang, psm: basePsm, oem: baseOem })
strategies.push({ lang: baseLang, psm: '3', oem: baseOem })
strategies.push({ lang: baseLang, psm: '13', oem: baseOem })
// Si on cible MRZ/OCRB
if ((options.language || '').includes('eng') || options.mrz) {
// OCRB peut ne pas être installé; on tente eng+ocrb puis eng seul
strategies.push({ lang: 'ocrb+eng', psm: '6', oem: baseOem })
strategies.push({ lang: 'ocrb+eng', psm: '8', oem: baseOem })
strategies.push({ lang: 'eng', psm: '6', oem: baseOem })
strategies.push({ lang: 'eng', psm: '8', oem: baseOem })
}
// Stratégies spécialisées pour CNI (noms français)
if (options.cni || options.frenchNames) {
strategies.push({ lang: 'fra', psm: '6', oem: baseOem })
strategies.push({ lang: 'fra', psm: '8', oem: baseOem })
strategies.push({ lang: 'fra', psm: '13', oem: baseOem })
// Stratégie hybride pour les noms
strategies.push({ lang: 'fra+eng', psm: '6', oem: baseOem })
strategies.push({ lang: 'fra+eng', psm: '8', oem: baseOem })
}
let best = { text: '', score: -1, meta: null }
for (let i = 0; i < strategies.length; i += 1) {
const s = strategies[i]
const tempOutput = `${tempOutputBase}_${i}`
const cmd = `tesseract "${tempInput}" "${tempOutput}" -l ${s.lang} --psm ${s.psm} --oem ${s.oem}`
try {
execSync(cmd, { stdio: 'pipe' })
const t = fs.readFileSync(`${tempOutput}.txt`, 'utf8')
const text = t.trim()
// Heuristique de score: longueur utile et présence de caractères alphanumériques
const alpha = (text.match(/[A-Za-z0-9]/g) || []).length
const score = alpha + (text.includes('<<') ? 20 : 0)
if (score > best.score) best = { text, score, meta: s }
try { fs.unlinkSync(`${tempOutput}.txt`) } catch {}
} catch (e) {
// Essai suivant
}
}
try { fs.unlinkSync(tempInput) } catch {}
return {
text: best.text,
confidence: best.score > 0 ? 0.85 : 0.6,
method: 'tesseract_multi',
used: best.meta,
}
}
// Fonction pour extraire le texte d'une image avec améliorations CNI
async function extractTextFromImageEnhanced(inputPath) {
try {
console.log(`[ENHANCED_OCR] Début extraction: ${path.basename(inputPath)}`)
// Vérifier si c'est une CNI
const isCNI = await isCNIDocument(inputPath)
console.log(`[ENHANCED_OCR] CNI détectée: ${isCNI}`)
if (isCNI) {
return await extractTextFromCNI(inputPath)
} else {
return await extractTextFromStandardDocument(inputPath)
}
} catch (error) {
console.error(`[ENHANCED_OCR] Erreur extraction:`, error.message)
throw error
}
}
// Fonction spécialisée pour l'extraction de texte des CNI
async function extractTextFromCNI(inputPath) {
try {
console.log(`[CNI_OCR] Traitement CNI: ${path.basename(inputPath)}`)
// Améliorer le preprocessing pour les CNI
const enhancedImage = await enhanceCNIPreprocessing(inputPath)
if (!enhancedImage) {
throw new Error('Échec du preprocessing CNI')
}
// Traitement par zones
const cniZones = await processCNIWithZones(inputPath)
let combinedText = ''
let mrzData = null
// Extraire le texte de l'image améliorée avec stratégies CNI
const mainText = await runTesseractOCR(enhancedImage, {
language: 'fra',
psm: '6', // Mode uniforme de bloc de texte
cni: true, // Activer les stratégies spécialisées CNI
frenchNames: true, // Activer les stratégies pour noms français
})
combinedText += mainText.text + '\n'
// Si on a des zones, traiter chaque zone séparément
if (cniZones && cniZones.zones) {
for (const [zoneName, zoneImage] of Object.entries(cniZones.zones)) {
try {
const zoneText = await runTesseractOCR(zoneImage, {
language: 'fra',
psm: '8', // Mode mot unique
cni: true, // Activer les stratégies spécialisées CNI
frenchNames: true, // Activer les stratégies pour noms français
})
combinedText += `[${zoneName.toUpperCase()}] ${zoneText.text}\n`
console.log(`[CNI_OCR] Zone ${zoneName}: ${zoneText.text}`)
} catch (zoneError) {
console.warn(`[CNI_OCR] Erreur zone ${zoneName}:`, zoneError.message)
}
}
}
// Traiter la MRZ si disponible
if (cniZones && cniZones.mrz) {
try {
const mrzText = await runTesseractOCR(cniZones.mrz, {
language: 'ocrb+eng',
psm: '6',
mrz: true,
})
combinedText += `[MRZ] ${mrzText.text}\n`
// Décoder la MRZ
mrzData = decodeMRZ(mrzText.text)
if (mrzData) {
combinedText += `[MRZ_DECODED] Nom: ${mrzData.surname}, Prénom: ${mrzData.givenNames}, Numéro: ${mrzData.documentNumber}\n`
}
} catch (mrzError) {
console.warn(`[CNI_OCR] Erreur MRZ:`, mrzError.message)
}
}
// Post-traitement du texte pour corriger les erreurs communes
const processedText = postProcessCNIText(combinedText)
console.log(`[CNI_OCR] Texte final: ${processedText.length} caractères`)
return {
text: processedText,
confidence: 0.85, // Confiance élevée pour les CNI traitées
method: 'cni_enhanced',
mrzData: mrzData,
zones: cniZones ? Object.keys(cniZones.zones || {}) : [],
}
} catch (error) {
console.error(`[CNI_OCR] Erreur traitement CNI:`, error.message)
throw error
}
}
// Fonction pour l'extraction de texte des documents standards
async function extractTextFromStandardDocument(inputPath) {
try {
console.log(`[STANDARD_OCR] Traitement document standard: ${path.basename(inputPath)}`)
// Preprocessing standard
const image = sharp(inputPath)
const metadata = await image.metadata()
const processedImage = await image
.resize({
width: Math.min(metadata.width * 2, 2000),
height: Math.min(metadata.height * 2, 2000),
fit: 'inside',
withoutEnlargement: false,
})
.grayscale()
// Sharp attend des bornes entières 1..100 pour lower/upper (percentiles)
// Ancien réglage (0.1/0.9) provoquait une erreur. On utilise 10/90.
.normalize({ lower: 10, upper: 90 })
// Voir commentaire ci-dessus: clamp en 1..100
.normalize({ lower: 10, upper: 90 })
.sharpen()
.png()
.toBuffer()
// OCR standard
const result = await runTesseractOCR(processedImage, {
language: 'fra',
psm: '6',
})
return {
text: result.text,
confidence: result.confidence,
method: 'standard_enhanced',
}
} catch (error) {
console.error(`[STANDARD_OCR] Erreur traitement standard:`, error.message)
throw error
}
}
// Fonction pour post-traiter le texte des CNI
function postProcessCNIText(text) {
try {
let processedText = text
// Corrections communes pour les CNI
const corrections = [
// Corrections de caractères corrompus
{ from: /RÉPUBLIQUE FRANCATSEN/g, to: 'RÉPUBLIQUE FRANÇAISE' },
{ from: /CARTE NATIONALE DIDENTITE/g, to: "CARTE NATIONALE D'IDENTITÉ" },
{ from: /Ne :/g, to: 'N° :' },
{ from: /Fe - 0/g, to: 'Féminin' },
{ from: /Mele:/g, to: 'Mâle:' },
{ from: /IDFRA[A-Z]+CCKKLLLLK[A-Z]*/g, to: 'IDFRA' }, // Nettoyer les caractères parasites après IDFRA
// Corrections génériques pour les noms corrompus
{ from: /([A-Z]{2,})CCKKLLLLK/g, to: '$1' }, // Supprimer les caractères parasites après les noms
{ from: /([A-Z]{2,})<+([A-Z]{2,})/g, to: '$1<<$2' }, // Normaliser les séparateurs de noms
{ from: /([A-Z]{2,})<<<<([A-Z]{2,})/g, to: '$1<<$2' }, // Réduire les séparateurs multiples
{ from: /([A-Z]{2,})<<<<<<([A-Z]{2,})/g, to: '$1<<$2' }, // Réduire les séparateurs multiples
{ from: /([A-Z]{2,})<<<<<</g, to: '$1<<' }, // Nettoyer les séparateurs en fin de nom
{ from: /([A-Z]{2,})<<<</g, to: '$1<<' }, // Nettoyer les séparateurs en fin de nom
// Corrections de caractères OCR courants
{ from: /0/g, to: 'O' }, // 0 -> O dans les noms
{ from: /1/g, to: 'I' }, // 1 -> I dans les noms
{ from: /5/g, to: 'S' }, // 5 -> S dans les noms
{ from: /8/g, to: 'B' }, // 8 -> B dans les noms
{ from: /6/g, to: 'G' }, // 6 -> G dans les noms
// Corrections génériques pour les erreurs OCR courantes dans les noms
{ from: /([A-Z]{2,})0([A-Z]{2,})/g, to: '$1O$2' }, // 0 -> O dans les noms
{ from: /([A-Z]{2,})1([A-Z]{2,})/g, to: '$1I$2' }, // 1 -> I dans les noms
{ from: /([A-Z]{2,})5([A-Z]{2,})/g, to: '$1S$2' }, // 5 -> S dans les noms
{ from: /([A-Z]{2,})8([A-Z]{2,})/g, to: '$1B$2' }, // 8 -> B dans les noms
{ from: /([A-Z]{2,})6([A-Z]{2,})/g, to: '$1G$2' }, // 6 -> G dans les noms
// Nettoyage des caractères parasites
{
from: /[^\w\sÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,;:!?()\-'"]/g,
to: ' ',
},
// Normalisation des espaces
{ from: /\s+/g, to: ' ' },
{ from: /^\s+|\s+$/g, to: '' },
]
// Appliquer les corrections
for (const correction of corrections) {
processedText = processedText.replace(correction.from, correction.to)
}
console.log(`[POST_PROCESS] Texte post-traité: ${processedText.length} caractères`)
return processedText
} catch (error) {
console.error(`[POST_PROCESS] Erreur post-traitement:`, error.message)
return text
}
}
module.exports = {
extractTextFromImageEnhanced,
extractTextFromCNI,
extractTextFromStandardDocument,
runTesseractOCR,
postProcessCNIText,
}

View File

@ -1,384 +0,0 @@
/**
* Extraction d'entités métier spécialisées pour les actes notariés
* Biens immobiliers, clauses contractuelles, signatures, héritiers, etc.
*/
const fs = require('fs')
const path = require('path')
/**
* Extraction des biens immobiliers
* @param {string} text - Texte à analyser
* @returns {Array} Liste des biens immobiliers
*/
function extractBiensImmobiliers(text) {
const biens = []
// Patterns pour les biens immobiliers
const patterns = [
// Maison, appartement, terrain
/(maison|appartement|terrain|villa|studio|loft|duplex|triplex|pavillon|chalet|château|manoir|hôtel particulier|immeuble|bâtiment|construction|édifice)\s+(?:situé[e]?\s+)?(?:au\s+)?(?:n°\s*)?(\d+[a-z]?)?\s*(?:rue|avenue|boulevard|place|chemin|route|impasse|allée|square|quai|cours|passage)\s+([^,]+)/gi,
// Adresse complète
/(?:situé[e]?\s+)?(?:au\s+)?(?:n°\s*)?(\d+[a-z]?)\s*(?:rue|avenue|boulevard|place|chemin|route|impasse|allée|square|quai|cours|passage)\s+([^,]+),\s*(\d{5})\s+([^,]+)/gi,
// Surface et caractéristiques
/(?:d'une\s+)?(?:surface\s+de\s+)?(\d+(?:\.\d+)?)\s*(?:m²|m2|mètres?\s+carrés?)/gi,
// Nombre de pièces
/(?:composé[e]?\s+de\s+)?(\d+)\s*(?:pièces?|chambres?|salles?)/gi,
// Type de bien
/(?:un\s+)?(maison|appartement|terrain|villa|studio|loft|duplex|triplex|pavillon|chalet|château|manoir|hôtel particulier|immeuble|bâtiment|construction|édifice)/gi
]
// Extraction des adresses
const adresseMatches = text.match(patterns[1]) || []
for (const match of adresseMatches) {
const parts = match.match(/(\d+[a-z]?)\s*(?:rue|avenue|boulevard|place|chemin|route|impasse|allée|square|quai|cours|passage)\s+([^,]+),\s*(\d{5})\s+([^,]+)/i)
if (parts) {
biens.push({
type: 'bien_immobilier',
adresse: {
numero: parts[1],
rue: parts[2].trim(),
codePostal: parts[3],
ville: parts[4].trim()
},
surface: null,
pieces: null,
description: match.trim()
})
}
}
// Extraction des surfaces
const surfaceMatches = text.match(patterns[2]) || []
for (const match of surfaceMatches) {
const surface = parseFloat(match.match(/(\d+(?:\.\d+)?)/)[1])
if (biens.length > 0) {
biens[biens.length - 1].surface = surface
}
}
// Extraction du nombre de pièces
const piecesMatches = text.match(patterns[3]) || []
for (const match of piecesMatches) {
const pieces = parseInt(match.match(/(\d+)/)[1])
if (biens.length > 0) {
biens[biens.length - 1].pieces = pieces
}
}
return biens
}
/**
* Extraction des clauses contractuelles
* @param {string} text - Texte à analyser
* @returns {Array} Liste des clauses
*/
function extractClauses(text) {
const clauses = []
// Patterns pour les clauses
const patterns = [
// Clauses de prix
/(?:prix|montant|somme)\s+(?:de\s+)?(?:vente\s+)?(?:fixé[e]?\s+à\s+)?(\d+(?:\s+\d+)*(?:\.\d+)?)\s*(?:euros?|€|EUR)/gi,
// Clauses suspensives
/(?:clause\s+)?(?:suspensive|condition)\s+(?:de\s+)?([^.]{10,100})/gi,
// Clauses de garantie
/(?:garantie|garanties?)\s+(?:de\s+)?([^.]{10,100})/gi,
// Clauses de délai
/(?:délai|échéance|terme)\s+(?:de\s+)?(\d+)\s*(?:jours?|mois|années?)/gi,
// Clauses de résolution
/(?:résolution|annulation)\s+(?:du\s+)?(?:contrat|acte)\s+(?:en\s+cas\s+de\s+)?([^.]{10,100})/gi
]
// Extraction des prix
const prixMatches = text.match(patterns[0]) || []
for (const match of prixMatches) {
const prix = match.match(/(\d+(?:\s+\d+)*(?:\.\d+)?)/)[1].replace(/\s+/g, '')
clauses.push({
type: 'prix',
valeur: parseFloat(prix),
devise: 'EUR',
description: match.trim()
})
}
// Extraction des clauses suspensives
const suspensivesMatches = text.match(patterns[1]) || []
for (const match of suspensivesMatches) {
clauses.push({
type: 'clause_suspensive',
description: match.trim(),
condition: match.replace(/(?:clause\s+)?(?:suspensive|condition)\s+(?:de\s+)?/i, '').trim()
})
}
// Extraction des garanties
const garantiesMatches = text.match(patterns[2]) || []
for (const match of garantiesMatches) {
clauses.push({
type: 'garantie',
description: match.trim(),
garantie: match.replace(/(?:garantie|garanties?)\s+(?:de\s+)?/i, '').trim()
})
}
return clauses
}
/**
* Extraction des signatures
* @param {string} text - Texte à analyser
* @returns {Array} Liste des signatures
*/
function extractSignatures(text) {
const signatures = []
// Patterns pour les signatures
const patterns = [
// Signature simple
/(?:signé|signature)\s+(?:par\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
// Signature avec date
/(?:signé|signature)\s+(?:par\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:le\s+)?(\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4})/gi,
// Signature avec lieu
/(?:signé|signature)\s+(?:par\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:à\s+)?([A-Z][a-z]+)/gi,
// Signature notariale
/(?:notaire|maître)\s+([A-Z][a-z]+\s+[A-Z][a-z]+)/gi
]
// Extraction des signatures simples
const signatureMatches = text.match(patterns[0]) || []
for (const match of signatureMatches) {
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
signatures.push({
type: 'signature',
nom: nom,
date: null,
lieu: null,
description: match.trim()
})
}
// Extraction des signatures avec date
const signatureDateMatches = text.match(patterns[1]) || []
for (const match of signatureDateMatches) {
const parts = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:le\s+)?(\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4})/)
if (parts) {
signatures.push({
type: 'signature',
nom: parts[1],
date: parts[2],
lieu: null,
description: match.trim()
})
}
}
// Extraction des signatures notariales
const notaireMatches = text.match(patterns[3]) || []
for (const match of notaireMatches) {
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
signatures.push({
type: 'signature_notariale',
nom: nom,
date: null,
lieu: null,
description: match.trim()
})
}
return signatures
}
/**
* Extraction des héritiers
* @param {string} text - Texte à analyser
* @returns {Array} Liste des héritiers
*/
function extractHeritiers(text) {
const heritiers = []
// Patterns pour les héritiers
const patterns = [
// Héritier simple
/(?:héritier|héritière|successeur|successeure|bénéficiaire)\s+(?:de\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
// Héritier avec degré de parenté
/(?:fils|fille|père|mère|frère|sœur|époux|épouse|mari|femme|conjoint|conjointe|enfant|parent|grand-père|grand-mère|oncle|tante|neveu|nièce|cousin|cousine)\s+(?:de\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
// Héritier avec part
/([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:hérite|bénéficie)\s+(?:de\s+)?(?:la\s+)?(?:part\s+de\s+)?(\d+(?:\/\d+)?|tout|totalité)/gi
]
// Extraction des héritiers simples
const heritierMatches = text.match(patterns[0]) || []
for (const match of heritierMatches) {
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
heritiers.push({
type: 'heritier',
nom: nom,
parente: null,
part: null,
description: match.trim()
})
}
// Extraction des héritiers avec parenté
const parenteMatches = text.match(patterns[1]) || []
for (const match of parenteMatches) {
const parts = match.match(/(fils|fille|père|mère|frère|sœur|époux|épouse|mari|femme|conjoint|conjointe|enfant|parent|grand-père|grand-mère|oncle|tante|neveu|nièce|cousin|cousine)\s+(?:de\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/i)
if (parts) {
heritiers.push({
type: 'heritier',
nom: parts[2],
parente: parts[1],
part: null,
description: match.trim()
})
}
}
return heritiers
}
/**
* Extraction des vendeurs et acheteurs
* @param {string} text - Texte à analyser
* @returns {Object} Vendeurs et acheteurs
*/
function extractVendeursAcheteurs(text) {
const result = {
vendeurs: [],
acheteurs: []
}
// Patterns pour vendeurs et acheteurs
const patterns = [
// Vendeur
/(?:vendeur|vendeuse|vendant|vendant)\s+(?:M\.|Mme|Mademoiselle|Monsieur|Madame)?\s*([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
// Acheteur
/(?:acheteur|acheteuse|achetant|achetant)\s+(?:M\.|Mme|Mademoiselle|Monsieur|Madame)?\s*([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
// Vente entre
/(?:vente\s+)?(?:entre\s+)?(?:M\.|Mme|Mademoiselle|Monsieur|Madame)?\s*([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:et\s+)?(?:M\.|Mme|Mademoiselle|Monsieur|Madame)?\s*([A-Z][a-z]+\s+[A-Z][a-z]+)/gi
]
// Extraction des vendeurs
const vendeurMatches = text.match(patterns[0]) || []
for (const match of vendeurMatches) {
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
result.vendeurs.push({
type: 'vendeur',
nom: nom,
description: match.trim()
})
}
// Extraction des acheteurs
const acheteurMatches = text.match(patterns[1]) || []
for (const match of acheteurMatches) {
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
result.acheteurs.push({
type: 'acheteur',
nom: nom,
description: match.trim()
})
}
// Extraction des ventes entre
const venteMatches = text.match(patterns[2]) || []
for (const match of venteMatches) {
const parts = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:et\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/)
if (parts) {
result.vendeurs.push({
type: 'vendeur',
nom: parts[1],
description: match.trim()
})
result.acheteurs.push({
type: 'acheteur',
nom: parts[2],
description: match.trim()
})
}
}
return result
}
/**
* Classification du type de document
* @param {string} text - Texte à analyser
* @returns {string} Type de document
*/
function classifyDocumentType(text) {
const textLower = text.toLowerCase()
// Types de documents notariés
const types = {
'acte_vente': ['vente', 'achat', 'acquisition', 'cession', 'transfert'],
'acte_succession': ['succession', 'héritage', 'héritier', 'défunt', 'décès'],
'acte_donation': ['donation', 'donner', 'donné', 'donateur', 'donataire'],
'acte_mariage': ['mariage', 'époux', 'épouse', 'conjoint', 'conjointe'],
'acte_divorce': ['divorce', 'séparation', 'liquidation'],
'acte_pacs': ['pacs', 'pacte civil', 'solidarité'],
'acte_testament': ['testament', 'testateur', 'testatrice', 'legs', 'léguer'],
'acte_promesse': ['promesse', 'compromis', 'avant-contrat'],
'acte_authentification': ['authentification', 'authentifier', 'certifier'],
'acte_pouvoir': ['pouvoir', 'procuration', 'mandat'],
'acte_societe': ['société', 'entreprise', 'sarl', 'sas', 'eurl', 'snc']
}
// Calcul des scores
const scores = {}
for (const [type, keywords] of Object.entries(types)) {
scores[type] = keywords.reduce((score, keyword) => {
const matches = (textLower.match(new RegExp(keyword, 'g')) || []).length
return score + matches
}, 0)
}
// Retourner le type avec le score le plus élevé
const bestType = Object.entries(scores).reduce((a, b) => scores[a[0]] > scores[b[0]] ? a : b)
return bestType[1] > 0 ? bestType[0] : 'document_inconnu'
}
/**
* Extraction complète des entités métier
* @param {string} text - Texte à analyser
* @returns {Object} Toutes les entités extraites
*/
function extractMetierEntities(text) {
return {
biensImmobiliers: extractBiensImmobiliers(text),
clauses: extractClauses(text),
signatures: extractSignatures(text),
heritiers: extractHeritiers(text),
vendeursAcheteurs: extractVendeursAcheteurs(text),
documentType: classifyDocumentType(text),
timestamp: new Date().toISOString()
}
}
module.exports = {
extractBiensImmobiliers,
extractClauses,
extractSignatures,
extractHeritiers,
extractVendeursAcheteurs,
classifyDocumentType,
extractMetierEntities
}

Binary file not shown.

View File

@ -1,214 +0,0 @@
/**
* Module de préprocessing d'image pour améliorer l'OCR
* Optimisé pour les documents d'identité et CNI
*/
const sharp = require('sharp')
const fs = require('fs')
const path = require('path')
/**
* Prétraite une image pour améliorer la qualité de l'OCR
* @param {string} inputPath - Chemin vers l'image d'entrée
* @param {string} outputPath - Chemin vers l'image de sortie (optionnel)
* @param {Object} options - Options de préprocessing
* @returns {Promise<Buffer>} - Buffer de l'image préprocessée
*/
async function preprocessImageForOCR(inputPath, outputPath = null, options = {}) {
console.log(`[PREPROCESSING] Début du préprocessing de: ${path.basename(inputPath)}`)
try {
// Options par défaut optimisées pour les documents d'identité
const defaultOptions = {
// Redimensionnement
width: 2000, // Largeur cible
height: null, // Hauteur automatique (maintient le ratio)
// Amélioration du contraste
contrast: 1.5, // Augmente le contraste
brightness: 1.1, // Légère augmentation de la luminosité
// Filtres
sharpen: true, // Amélioration de la netteté
denoise: true, // Réduction du bruit
// Conversion
grayscale: true, // Conversion en niveaux de gris
threshold: null, // Seuil pour binarisation (optionnel)
// Format de sortie
format: 'png', // Format PNG pour meilleure qualité
quality: 100, // Qualité maximale
}
const config = { ...defaultOptions, ...options }
console.log(`[PREPROCESSING] Configuration:`, {
width: config.width,
contrast: config.contrast,
brightness: config.brightness,
grayscale: config.grayscale,
sharpen: config.sharpen,
})
// Lecture de l'image
let image = sharp(inputPath)
// Redimensionnement
if (config.width || config.height) {
image = image.resize(config.width, config.height, {
fit: 'inside',
withoutEnlargement: false,
})
console.log(`[PREPROCESSING] Redimensionnement appliqué`)
}
// Conversion en niveaux de gris
if (config.grayscale) {
image = image.grayscale()
console.log(`[PREPROCESSING] Conversion en niveaux de gris`)
}
// Amélioration du contraste et de la luminosité
if (config.contrast !== 1 || config.brightness !== 1) {
image = image.modulate({
brightness: config.brightness,
contrast: config.contrast,
})
console.log(
`[PREPROCESSING] Contraste (${config.contrast}) et luminosité (${config.brightness}) appliqués`,
)
}
// Amélioration de la netteté
if (config.sharpen) {
image = image.sharpen({
sigma: 1.0,
flat: 1.0,
jagged: 2.0,
})
console.log(`[PREPROCESSING] Amélioration de la netteté appliquée`)
}
// Réduction du bruit
if (config.denoise) {
image = image.median(3)
console.log(`[PREPROCESSING] Réduction du bruit appliquée`)
}
// Binarisation (seuil) si demandée
if (config.threshold) {
image = image.threshold(config.threshold)
console.log(`[PREPROCESSING] Binarisation avec seuil ${config.threshold}`)
}
// Application des modifications et conversion
const processedBuffer = await image.png({ quality: config.quality }).toBuffer()
console.log(`[PREPROCESSING] Image préprocessée: ${processedBuffer.length} bytes`)
// Sauvegarde optionnelle
if (outputPath) {
await fs.promises.writeFile(outputPath, processedBuffer)
console.log(`[PREPROCESSING] Image sauvegardée: ${outputPath}`)
}
return processedBuffer
} catch (error) {
console.error(`[PREPROCESSING] Erreur lors du préprocessing:`, error.message)
throw error
}
}
/**
* Prétraite une image avec plusieurs configurations et retourne la meilleure
* @param {string} inputPath - Chemin vers l'image d'entrée
* @returns {Promise<Buffer>} - Buffer de la meilleure image préprocessée
*/
async function preprocessImageMultipleConfigs(inputPath) {
console.log(`[PREPROCESSING] Test de plusieurs configurations pour: ${path.basename(inputPath)}`)
const configs = [
{
name: 'Standard',
options: {
width: 2000,
contrast: 1.5,
brightness: 1.1,
grayscale: true,
sharpen: true,
denoise: true,
},
},
{
name: 'Haute résolution',
options: {
width: 3000,
contrast: 1.8,
brightness: 1.2,
grayscale: true,
sharpen: true,
denoise: false,
},
},
{
name: 'Contraste élevé',
options: {
width: 2000,
contrast: 2.0,
brightness: 1.0,
grayscale: true,
sharpen: true,
denoise: true,
},
},
{
name: 'Binarisation',
options: {
width: 2000,
contrast: 1.5,
brightness: 1.1,
grayscale: true,
sharpen: true,
denoise: true,
threshold: 128,
},
},
]
// Pour l'instant, on utilise la configuration standard
// Dans une version avancée, on pourrait tester toutes les configs
const bestConfig = configs[0]
console.log(`[PREPROCESSING] Utilisation de la configuration: ${bestConfig.name}`)
return await preprocessImageForOCR(inputPath, null, bestConfig.options)
}
/**
* Analyse les métadonnées d'une image
* @param {string} imagePath - Chemin vers l'image
* @returns {Promise<Object>} - Métadonnées de l'image
*/
async function analyzeImageMetadata(imagePath) {
try {
const metadata = await sharp(imagePath).metadata()
console.log(`[PREPROCESSING] Métadonnées de ${path.basename(imagePath)}:`, {
format: metadata.format,
width: metadata.width,
height: metadata.height,
channels: metadata.channels,
density: metadata.density,
size: `${(metadata.size / 1024).toFixed(1)} KB`,
})
return metadata
} catch (error) {
console.error(`[PREPROCESSING] Erreur lors de l'analyse des métadonnées:`, error.message)
return null
}
}
module.exports = {
preprocessImageForOCR,
preprocessImageMultipleConfigs,
analyzeImageMetadata,
}

View File

@ -1,73 +0,0 @@
const fs = require('fs')
const path = require('path')
function loadCsvNames(filePath) {
try {
const raw = fs.readFileSync(filePath, 'utf8')
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)
const names = []
for (const line of lines) {
const parts = line.split(/[;,\t]/).map((p) => p.trim()).filter(Boolean)
for (const p of parts) {
if (/^[A-Za-zÀ-ÖØ-öø-ÿ'\-\s]{2,}$/.test(p)) names.push(p)
}
}
return names
} catch {
return []
}
}
function buildNameSets() {
const baseDir = path.join(__dirname, 'data', 'names')
const firstNames = new Set()
const lastNames = new Set()
try {
if (!fs.existsSync(baseDir)) return { firstNames, lastNames }
// Prioriser les fichiers unifiés légers (références finales)
const preferredOrder = [
'firstnames_all.csv',
'lastnames_all.csv',
]
const files = fs
.readdirSync(baseDir)
.sort((a, b) => preferredOrder.indexOf(a) - preferredOrder.indexOf(b))
for (const f of files) {
const fp = path.join(baseDir, f)
if (!fs.statSync(fp).isFile()) continue
// N'utiliser que les deux références finales si présentes
const isFirst = /^(firstnames_all\.|first|prenom|given)/i.test(f)
const isLast = /^(lastnames_all\.|last|nom|surname|family)/i.test(f)
if (!isFirst && !isLast) continue
const list = loadCsvNames(fp)
for (const n of list) {
const norm = n.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
if (isFirst) firstNames.add(norm)
if (isLast) lastNames.add(norm)
}
}
} catch {}
return { firstNames, lastNames }
}
let cache = null
function getNameDirectory() {
if (!cache) cache = buildNameSets()
return cache
}
function nameConfidenceBoost(firstName, lastName) {
try {
const dir = getNameDirectory()
const f = (firstName || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
const l = (lastName || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
let boost = 0
if (f && dir.firstNames.has(f)) boost += 0.05
if (l && dir.lastNames.has(l)) boost += 0.05
return boost
} catch {
return 0
}
}
module.exports = { getNameDirectory, nameConfidenceBoost }

1111
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +0,0 @@
{
"name": "4nk-ia-backend",
"version": "1.0.1",
"description": "Backend pour le traitement des documents avec OCR et NER",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"express": "^4.18.2",
"multer": "^2.0.0",
"cors": "^2.8.5",
"tesseract.js": "^5.1.0"
},
"engines": {
"node": ">=20.0.0"
},
"keywords": [
"ocr",
"ner",
"document-processing",
"express",
"tesseract"
],
"author": "4NK Team",
"license": "MIT"
}

View File

@ -1,115 +0,0 @@
/**
* Module de conversion PDF vers images pour l'OCR
*/
const pdf = require('pdf-poppler')
const fs = require('fs')
const path = require('path')
/**
* Convertit un PDF en images pour l'OCR
* @param {string} pdfPath - Chemin vers le fichier PDF
* @param {string} outputDir - Répertoire de sortie (optionnel)
* @returns {Promise<Array>} - Tableau des chemins des images générées
*/
async function convertPdfToImages(pdfPath, outputDir = null) {
console.log(`[PDF-CONVERTER] Début de la conversion PDF: ${path.basename(pdfPath)}`)
try {
// Répertoire de sortie par défaut
if (!outputDir) {
outputDir = path.dirname(pdfPath)
}
// Configuration de la conversion
const options = {
format: 'png',
out_dir: outputDir,
out_prefix: 'page',
page: null, // Toutes les pages
scale: 2000, // Résolution élevée
}
console.log(`[PDF-CONVERTER] Configuration: Format=PNG, Scale=2000`)
// Conversion de toutes les pages
const results = await pdf.convert(pdfPath, options)
console.log(`[PDF-CONVERTER] Conversion terminée: ${results.length} page(s) convertie(s)`)
// Retourner les chemins des images générées
const imagePaths = results.map((result, index) => {
const imagePath = path.join(outputDir, `page-${index + 1}.png`)
console.log(`[PDF-CONVERTER] Page ${index + 1}: ${imagePath}`)
return imagePath
})
return imagePaths
} catch (error) {
console.error(`[PDF-CONVERTER] Erreur lors de la conversion:`, error.message)
throw error
}
}
/**
* Convertit un PDF en une seule image (première page)
* @param {string} pdfPath - Chemin vers le fichier PDF
* @param {string} outputPath - Chemin de sortie de l'image (optionnel)
* @returns {Promise<string>} - Chemin de l'image générée
*/
async function convertPdfToSingleImage(pdfPath, outputPath = null) {
console.log(`[PDF-CONVERTER] Conversion PDF vers image unique: ${path.basename(pdfPath)}`)
try {
// Chemin de sortie par défaut
if (!outputPath) {
const baseName = path.basename(pdfPath, '.pdf')
const dirName = path.dirname(pdfPath)
outputPath = path.join(dirName, `${baseName}_converted.png`)
}
// Configuration pour une seule page
const options = {
format: 'png',
out_dir: path.dirname(outputPath),
out_prefix: path.basename(outputPath, '.png'),
page: 1, // Première page seulement
scale: 2000,
}
// Conversion de la première page seulement
const results = await pdf.convert(pdfPath, options)
console.log(`[PDF-CONVERTER] Image générée: ${outputPath}`)
return outputPath
} catch (error) {
console.error(`[PDF-CONVERTER] Erreur lors de la conversion:`, error.message)
throw error
}
}
/**
* Nettoie les fichiers temporaires générés
* @param {Array} filePaths - Chemins des fichiers à supprimer
*/
async function cleanupTempFiles(filePaths) {
console.log(`[PDF-CONVERTER] Nettoyage de ${filePaths.length} fichier(s) temporaire(s)`)
for (const filePath of filePaths) {
try {
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath)
console.log(`[PDF-CONVERTER] Fichier supprimé: ${path.basename(filePath)}`)
}
} catch (error) {
console.warn(`[PDF-CONVERTER] Erreur lors de la suppression de ${filePath}: ${error.message}`)
}
}
}
module.exports = {
convertPdfToImages,
convertPdfToSingleImage,
cleanupTempFiles,
}

File diff suppressed because it is too large Load Diff

View File

@ -1,78 +0,0 @@
{
"document": {
"id": "doc-1757978816308",
"fileName": "IMG_20250902_162210.jpg",
"fileSize": 980755,
"mimeType": "image/jpeg",
"uploadTimestamp": "2025-09-15T23:26:56.308Z"
},
"classification": {
"documentType": "Document",
"confidence": 0.6,
"subType": "Document",
"language": "fr",
"pageCount": 1
},
"extraction": {
"text": {
"raw": "4 . al -e ge :\na 4 mT. a ES Se Gaga EES hoe i -\na\nLo ee\nBaie: JERE REEMISEEE -\n1 ss : :-\n1) Carevalablejusques: 17.01.2033 À\n3 déévét: 1801 2018 : Ë 3\n\n Ban a / J\n\nPoa mec penses . -\n",
"processed": "4 . al -e ge :\na 4 mT. a ES Se Gaga EES hoe i -\na\nLo ee\nBaie: JERE REEMISEEE -\nl ss : :-\nl) Carevalablejusques: l7.ol.2oee À\ne déévét: l8ol 2ol8 : Ë e\n\n Ban a / J\n\nPoa mec penses . -\n",
"wordCount": 48,
"characterCount": 338,
"confidence": 0.28
},
"entities": {
"persons": [
{
"id": "identity-0",
"type": "person",
"firstName": "Se",
"lastName": "Gaga",
"role": null,
"email": null,
"phone": null,
"confidence": 0.9,
"source": "rule-based"
}
],
"companies": [],
"addresses": [],
"financial": {
"amounts": [],
"totals": {},
"payment": {}
},
"dates": [],
"contractual": {
"clauses": [],
"signatures": []
},
"references": []
}
},
"metadata": {
"processing": {
"engine": "4NK_IA_Backend",
"version": "1.0.0",
"processingTime": "4933ms",
"ocrEngine": "tesseract.js",
"nerEngine": "rule-based",
"preprocessing": {
"applied": true,
"reason": "Image preprocessing applied"
}
},
"quality": {
"globalConfidence": 0.6,
"textExtractionConfidence": 0.28,
"entityExtractionConfidence": 0.9,
"classificationConfidence": 0.6
}
},
"status": {
"success": true,
"errors": [],
"warnings": ["Aucune signature détectée"],
"timestamp": "2025-09-15T23:26:56.308Z"
}
}

178
coverage/App.tsx.html Normal file
View File

@ -0,0 +1,178 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for App.tsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="index.html">All files</a> App.tsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Statements</span>
<span class='fraction'>0/27</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/1</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/1</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Lines</span>
<span class='fraction'>0/27</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a>
<a name='L12'></a><a href='#L12'>12</a>
<a name='L13'></a><a href='#L13'>13</a>
<a name='L14'></a><a href='#L14'>14</a>
<a name='L15'></a><a href='#L15'>15</a>
<a name='L16'></a><a href='#L16'>16</a>
<a name='L17'></a><a href='#L17'>17</a>
<a name='L18'></a><a href='#L18'>18</a>
<a name='L19'></a><a href='#L19'>19</a>
<a name='L20'></a><a href='#L20'>20</a>
<a name='L21'></a><a href='#L21'>21</a>
<a name='L22'></a><a href='#L22'>22</a>
<a name='L23'></a><a href='#L23'>23</a>
<a name='L24'></a><a href='#L24'>24</a>
<a name='L25'></a><a href='#L25'>25</a>
<a name='L26'></a><a href='#L26'>26</a>
<a name='L27'></a><a href='#L27'>27</a>
<a name='L28'></a><a href='#L28'>28</a>
<a name='L29'></a><a href='#L29'>29</a>
<a name='L30'></a><a href='#L30'>30</a>
<a name='L31'></a><a href='#L31'>31</a>
<a name='L32'></a><a href='#L32'>32</a></td><td class="line-coverage quiet"><span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js"><span class="cstat-no" title="statement not covered" >import { useState } from 'react'<span class="fstat-no" title="function not covered" ><span class="branch-0 cbranch-no" title="branch not covered" ></span></span></span>
<span class="cstat-no" title="statement not covered" >import reactLogo from './assets/react.svg'</span>
<span class="cstat-no" title="statement not covered" >import viteLogo from '/vite.svg'</span>
<span class="cstat-no" title="statement not covered" >import './App.css'</span>
&nbsp;
<span class="cstat-no" title="statement not covered" >function App() {</span>
<span class="cstat-no" title="statement not covered" > const [count, setCount] = useState(0)</span>
&nbsp;
<span class="cstat-no" title="statement not covered" > return (</span>
<span class="cstat-no" title="statement not covered" > &lt;&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;div&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;a href="https://vite.dev" target="_blank"&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;img src={viteLogo} className="logo" alt="Vite logo" /&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;/a&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;a href="https://react.dev" target="_blank"&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;img src={reactLogo} className="logo react" alt="React logo" /&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;/a&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;/div&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;h1&gt;Vite + React&lt;/h1&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;div className="card"&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;button onClick={() =&gt; setCount((count) =&gt; count + 1)}&gt;count is {count}&lt;/button&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;p&gt;</span>
<span class="cstat-no" title="statement not covered" > Edit &lt;code&gt;src/App.tsx&lt;/code&gt; and save to test HMR</span>
<span class="cstat-no" title="statement not covered" > &lt;/p&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;/div&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;p className="read-the-docs"&gt;Click on the Vite and React logos to learn more&lt;/p&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;/&gt;</span>
)
<span class="cstat-no" title="statement not covered" >}</span>
&nbsp;
<span class="cstat-no" title="statement not covered" >export default App</span>
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2025-09-10T15:15:07.789Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

224
coverage/base.css Normal file
View File

@ -0,0 +1,224 @@
body, html {
margin:0; padding: 0;
height: 100%;
}
body {
font-family: Helvetica Neue, Helvetica, Arial;
font-size: 14px;
color:#333;
}
.small { font-size: 12px; }
*, *:after, *:before {
-webkit-box-sizing:border-box;
-moz-box-sizing:border-box;
box-sizing:border-box;
}
h1 { font-size: 20px; margin: 0;}
h2 { font-size: 14px; }
pre {
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
margin: 0;
padding: 0;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
}
a { color:#0074D9; text-decoration:none; }
a:hover { text-decoration:underline; }
.strong { font-weight: bold; }
.space-top1 { padding: 10px 0 0 0; }
.pad2y { padding: 20px 0; }
.pad1y { padding: 10px 0; }
.pad2x { padding: 0 20px; }
.pad2 { padding: 20px; }
.pad1 { padding: 10px; }
.space-left2 { padding-left:55px; }
.space-right2 { padding-right:20px; }
.center { text-align:center; }
.clearfix { display:block; }
.clearfix:after {
content:'';
display:block;
height:0;
clear:both;
visibility:hidden;
}
.fl { float: left; }
@media only screen and (max-width:640px) {
.col3 { width:100%; max-width:100%; }
.hide-mobile { display:none!important; }
}
.quiet {
color: #7f7f7f;
color: rgba(0,0,0,0.5);
}
.quiet a { opacity: 0.7; }
.fraction {
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 10px;
color: #555;
background: #E8E8E8;
padding: 4px 5px;
border-radius: 3px;
vertical-align: middle;
}
div.path a:link, div.path a:visited { color: #333; }
table.coverage {
border-collapse: collapse;
margin: 10px 0 0 0;
padding: 0;
}
table.coverage td {
margin: 0;
padding: 0;
vertical-align: top;
}
table.coverage td.line-count {
text-align: right;
padding: 0 5px 0 20px;
}
table.coverage td.line-coverage {
text-align: right;
padding-right: 10px;
min-width:20px;
}
table.coverage td span.cline-any {
display: inline-block;
padding: 0 5px;
width: 100%;
}
.missing-if-branch {
display: inline-block;
margin-right: 5px;
border-radius: 3px;
position: relative;
padding: 0 4px;
background: #333;
color: yellow;
}
.skip-if-branch {
display: none;
margin-right: 10px;
position: relative;
padding: 0 4px;
background: #ccc;
color: white;
}
.missing-if-branch .typ, .skip-if-branch .typ {
color: inherit !important;
}
.coverage-summary {
border-collapse: collapse;
width: 100%;
}
.coverage-summary tr { border-bottom: 1px solid #bbb; }
.keyline-all { border: 1px solid #ddd; }
.coverage-summary td, .coverage-summary th { padding: 10px; }
.coverage-summary tbody { border: 1px solid #bbb; }
.coverage-summary td { border-right: 1px solid #bbb; }
.coverage-summary td:last-child { border-right: none; }
.coverage-summary th {
text-align: left;
font-weight: normal;
white-space: nowrap;
}
.coverage-summary th.file { border-right: none !important; }
.coverage-summary th.pct { }
.coverage-summary th.pic,
.coverage-summary th.abs,
.coverage-summary td.pct,
.coverage-summary td.abs { text-align: right; }
.coverage-summary td.file { white-space: nowrap; }
.coverage-summary td.pic { min-width: 120px !important; }
.coverage-summary tfoot td { }
.coverage-summary .sorter {
height: 10px;
width: 7px;
display: inline-block;
margin-left: 0.5em;
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
}
.coverage-summary .sorted .sorter {
background-position: 0 -20px;
}
.coverage-summary .sorted-desc .sorter {
background-position: 0 -10px;
}
.status-line { height: 10px; }
/* yellow */
.cbranch-no { background: yellow !important; color: #111; }
/* dark red */
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
.low .chart { border:1px solid #C21F39 }
.highlighted,
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
background: #C21F39 !important;
}
/* medium red */
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
/* light red */
.low, .cline-no { background:#FCE1E5 }
/* light green */
.high, .cline-yes { background:rgb(230,245,208) }
/* medium green */
.cstat-yes { background:rgb(161,215,106) }
/* dark green */
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
.high .chart { border:1px solid rgb(77,146,33) }
/* dark yellow (gold) */
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
.medium .chart { border:1px solid #f9cd0b; }
/* light yellow */
.medium { background: #fff4c2; }
.cstat-skip { background: #ddd; color: #111; }
.fstat-skip { background: #ddd; color: #111 !important; }
.cbranch-skip { background: #ddd !important; color: #111; }
span.cline-neutral { background: #eaeaea; }
.coverage-summary td.empty {
opacity: .5;
padding-top: 4px;
padding-bottom: 4px;
line-height: 1;
color: #888;
}
.cover-fill, .cover-empty {
display:inline-block;
height: 12px;
}
.chart {
line-height: 0;
}
.cover-empty {
background: white;
}
.cover-full {
border-right: none !important;
}
pre.prettyprint {
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
.com { color: #999 !important; }
.ignore-none { color: #999; font-weight: normal; }
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -48px;
}
.footer, .push {
height: 48px;
}

View File

@ -0,0 +1,87 @@
/* eslint-disable */
var jumpToCode = (function init() {
// Classes of code we would like to highlight in the file view
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
// Elements to highlight in the file listing view
var fileListingElements = ['td.pct.low'];
// We don't want to select elements that are direct descendants of another match
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
// Selector that finds elements on the page to which we can jump
var selector =
fileListingElements.join(', ') +
', ' +
notSelector +
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
// The NodeList of matching elements
var missingCoverageElements = document.querySelectorAll(selector);
var currentIndex;
function toggleClass(index) {
missingCoverageElements
.item(currentIndex)
.classList.remove('highlighted');
missingCoverageElements.item(index).classList.add('highlighted');
}
function makeCurrent(index) {
toggleClass(index);
currentIndex = index;
missingCoverageElements.item(index).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
}
function goToPrevious() {
var nextIndex = 0;
if (typeof currentIndex !== 'number' || currentIndex === 0) {
nextIndex = missingCoverageElements.length - 1;
} else if (missingCoverageElements.length > 1) {
nextIndex = currentIndex - 1;
}
makeCurrent(nextIndex);
}
function goToNext() {
var nextIndex = 0;
if (
typeof currentIndex === 'number' &&
currentIndex < missingCoverageElements.length - 1
) {
nextIndex = currentIndex + 1;
}
makeCurrent(nextIndex);
}
return function jump(event) {
if (
document.getElementById('fileSearch') === document.activeElement &&
document.activeElement != null
) {
// if we're currently focused on the search input, we don't want to navigate
return;
}
switch (event.which) {
case 78: // n
case 74: // j
goToNext();
break;
case 66: // b
case 75: // k
case 80: // p
goToPrevious();
break;
}
};
})();
window.addEventListener('keydown', jumpToCode);

48
coverage/clover.xml Normal file
View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<coverage generated="1757517307798" clover="3.2.0">
<project timestamp="1757517307798" name="All files">
<metrics statements="36" coveredstatements="0" conditionals="2" coveredconditionals="0" methods="2" coveredmethods="0" elements="40" coveredelements="0" complexity="0" loc="36" ncloc="36" packages="1" files="2" classes="2"/>
<file name="App.tsx" path="/home/desk/code/4NK_IA_front/src/App.tsx">
<metrics statements="27" coveredstatements="0" conditionals="1" coveredconditionals="0" methods="1" coveredmethods="0"/>
<line num="1" count="0" type="cond" truecount="0" falsecount="1"/>
<line num="2" count="0" type="stmt"/>
<line num="3" count="0" type="stmt"/>
<line num="4" count="0" type="stmt"/>
<line num="6" count="0" type="stmt"/>
<line num="7" count="0" type="stmt"/>
<line num="9" count="0" type="stmt"/>
<line num="10" count="0" type="stmt"/>
<line num="11" count="0" type="stmt"/>
<line num="12" count="0" type="stmt"/>
<line num="13" count="0" type="stmt"/>
<line num="14" count="0" type="stmt"/>
<line num="15" count="0" type="stmt"/>
<line num="16" count="0" type="stmt"/>
<line num="17" count="0" type="stmt"/>
<line num="18" count="0" type="stmt"/>
<line num="19" count="0" type="stmt"/>
<line num="20" count="0" type="stmt"/>
<line num="21" count="0" type="stmt"/>
<line num="22" count="0" type="stmt"/>
<line num="23" count="0" type="stmt"/>
<line num="24" count="0" type="stmt"/>
<line num="25" count="0" type="stmt"/>
<line num="26" count="0" type="stmt"/>
<line num="27" count="0" type="stmt"/>
<line num="29" count="0" type="stmt"/>
<line num="31" count="0" type="stmt"/>
</file>
<file name="main.tsx" path="/home/desk/code/4NK_IA_front/src/main.tsx">
<metrics statements="9" coveredstatements="0" conditionals="1" coveredconditionals="0" methods="1" coveredmethods="0"/>
<line num="1" count="0" type="cond" truecount="0" falsecount="1"/>
<line num="2" count="0" type="stmt"/>
<line num="3" count="0" type="stmt"/>
<line num="4" count="0" type="stmt"/>
<line num="6" count="0" type="stmt"/>
<line num="7" count="0" type="stmt"/>
<line num="8" count="0" type="stmt"/>
<line num="9" count="0" type="stmt"/>
<line num="10" count="0" type="stmt"/>
</file>
</project>
</coverage>

View File

@ -0,0 +1,3 @@
{"/home/desk/code/4NK_IA_front/src/App.tsx": {"path":"/home/desk/code/4NK_IA_front/src/App.tsx","all":true,"statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":32}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":42}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":32}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":18}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":16}},"6":{"start":{"line":7,"column":0},"end":{"line":7,"column":39}},"8":{"start":{"line":9,"column":0},"end":{"line":9,"column":10}},"9":{"start":{"line":10,"column":0},"end":{"line":10,"column":6}},"10":{"start":{"line":11,"column":0},"end":{"line":11,"column":11}},"11":{"start":{"line":12,"column":0},"end":{"line":12,"column":51}},"12":{"start":{"line":13,"column":0},"end":{"line":13,"column":65}},"13":{"start":{"line":14,"column":0},"end":{"line":14,"column":12}},"14":{"start":{"line":15,"column":0},"end":{"line":15,"column":52}},"15":{"start":{"line":16,"column":0},"end":{"line":16,"column":73}},"16":{"start":{"line":17,"column":0},"end":{"line":17,"column":12}},"17":{"start":{"line":18,"column":0},"end":{"line":18,"column":12}},"18":{"start":{"line":19,"column":0},"end":{"line":19,"column":27}},"19":{"start":{"line":20,"column":0},"end":{"line":20,"column":28}},"20":{"start":{"line":21,"column":0},"end":{"line":21,"column":88}},"21":{"start":{"line":22,"column":0},"end":{"line":22,"column":11}},"22":{"start":{"line":23,"column":0},"end":{"line":23,"column":60}},"23":{"start":{"line":24,"column":0},"end":{"line":24,"column":12}},"24":{"start":{"line":25,"column":0},"end":{"line":25,"column":12}},"25":{"start":{"line":26,"column":0},"end":{"line":26,"column":86}},"26":{"start":{"line":27,"column":0},"end":{"line":27,"column":7}},"28":{"start":{"line":29,"column":0},"end":{"line":29,"column":1}},"30":{"start":{"line":31,"column":0},"end":{"line":31,"column":18}}},"s":{"0":0,"1":0,"2":0,"3":0,"5":0,"6":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"28":0,"30":0},"branchMap":{"0":{"type":"branch","line":1,"loc":{"start":{"line":1,"column":866},"end":{"line":31,"column":18}},"locations":[{"start":{"line":1,"column":866},"end":{"line":31,"column":18}}]}},"b":{"0":[0]},"fnMap":{"0":{"name":"(empty-report)","decl":{"start":{"line":1,"column":866},"end":{"line":31,"column":18}},"loc":{"start":{"line":1,"column":866},"end":{"line":31,"column":18}},"line":1}},"f":{"0":0}}
,"/home/desk/code/4NK_IA_front/src/main.tsx": {"path":"/home/desk/code/4NK_IA_front/src/main.tsx","all":true,"statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":45}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":20}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":27}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":52}},"6":{"start":{"line":7,"column":0},"end":{"line":7,"column":14}},"7":{"start":{"line":8,"column":0},"end":{"line":8,"column":11}},"8":{"start":{"line":9,"column":0},"end":{"line":9,"column":16}},"9":{"start":{"line":10,"column":0},"end":{"line":10,"column":1}}},"s":{"0":0,"1":0,"2":0,"3":0,"5":0,"6":0,"7":0,"8":0,"9":0},"branchMap":{"0":{"type":"branch","line":1,"loc":{"start":{"line":1,"column":203},"end":{"line":10,"column":-148}},"locations":[{"start":{"line":1,"column":203},"end":{"line":10,"column":-148}}]}},"b":{"0":[0]},"fnMap":{"0":{"name":"(empty-report)","decl":{"start":{"line":1,"column":203},"end":{"line":10,"column":-148}},"loc":{"start":{"line":1,"column":203},"end":{"line":10,"column":-148}},"line":1}},"f":{"0":0}}
}

BIN
coverage/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

131
coverage/index.html Normal file
View File

@ -0,0 +1,131 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for All files</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>All files</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Statements</span>
<span class='fraction'>0/36</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/2</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/2</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Lines</span>
<span class='fraction'>0/36</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line low'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody><tr>
<td class="file low" data-value="App.tsx"><a href="App.tsx.html">App.tsx</a></td>
<td data-value="0" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="27" class="abs low">0/27</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="1" class="abs low">0/1</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="1" class="abs low">0/1</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="27" class="abs low">0/27</td>
</tr>
<tr>
<td class="file low" data-value="main.tsx"><a href="main.tsx.html">main.tsx</a></td>
<td data-value="0" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="9" class="abs low">0/9</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="1" class="abs low">0/1</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="1" class="abs low">0/1</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="9" class="abs low">0/9</td>
</tr>
</tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2025-09-10T15:15:07.789Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

115
coverage/main.tsx.html Normal file
View File

@ -0,0 +1,115 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for main.tsx</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1><a href="index.html">All files</a> main.tsx</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Statements</span>
<span class='fraction'>0/9</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/1</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/1</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">0% </span>
<span class="quiet">Lines</span>
<span class='fraction'>0/9</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line low'></div>
<pre><table class="coverage">
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
<a name='L2'></a><a href='#L2'>2</a>
<a name='L3'></a><a href='#L3'>3</a>
<a name='L4'></a><a href='#L4'>4</a>
<a name='L5'></a><a href='#L5'>5</a>
<a name='L6'></a><a href='#L6'>6</a>
<a name='L7'></a><a href='#L7'>7</a>
<a name='L8'></a><a href='#L8'>8</a>
<a name='L9'></a><a href='#L9'>9</a>
<a name='L10'></a><a href='#L10'>10</a>
<a name='L11'></a><a href='#L11'>11</a></td><td class="line-coverage quiet"><span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-no">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js"><span class="cstat-no" title="statement not covered" >import { StrictMode } from 'react'<span class="fstat-no" title="function not covered" ><span class="branch-0 cbranch-no" title="branch not covered" ></span></span></span>
<span class="cstat-no" title="statement not covered" >import { createRoot } from 'react-dom/client'</span>
<span class="cstat-no" title="statement not covered" >import './index.css'</span>
<span class="cstat-no" title="statement not covered" >import App from './App.tsx'</span>
&nbsp;
<span class="cstat-no" title="statement not covered" >createRoot(document.getElementById('root')!).render(</span>
<span class="cstat-no" title="statement not covered" > &lt;StrictMode&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;App /&gt;</span>
<span class="cstat-no" title="statement not covered" > &lt;/StrictMode&gt;,</span>
<span class="cstat-no" title="statement not covered" >)</span>
&nbsp;</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2025-09-10T15:15:07.789Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

1
coverage/prettify.css Normal file
View File

@ -0,0 +1 @@
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}

2
coverage/prettify.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

210
coverage/sorter.js Normal file
View File

@ -0,0 +1,210 @@
/* eslint-disable */
var addSorting = (function() {
'use strict';
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() {
return document.querySelector('.coverage-summary');
}
// returns the thead element of the summary table
function getTableHeader() {
return getTable().querySelector('thead tr');
}
// returns the tbody element of the summary table
function getTableBody() {
return getTable().querySelector('tbody');
}
// returns the th element for nth column
function getNthColumn(n) {
return getTableHeader().querySelectorAll('th')[n];
}
function onFilterInput() {
const searchValue = document.getElementById('fileSearch').value;
const rows = document.getElementsByTagName('tbody')[0].children;
// Try to create a RegExp from the searchValue. If it fails (invalid regex),
// it will be treated as a plain text search
let searchRegex;
try {
searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
} catch (error) {
searchRegex = null;
}
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
let isMatch = false;
if (searchRegex) {
// If a valid regex was created, use it for matching
isMatch = searchRegex.test(row.textContent);
} else {
// Otherwise, fall back to the original plain text search
isMatch = row.textContent
.toLowerCase()
.includes(searchValue.toLowerCase());
}
row.style.display = isMatch ? '' : 'none';
}
}
// loads the search box
function addSearchBox() {
var template = document.getElementById('filterTemplate');
var templateClone = template.content.cloneNode(true);
templateClone.getElementById('fileSearch').oninput = onFilterInput;
template.parentElement.appendChild(templateClone);
}
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML =
colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function(a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function(a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc
? ' sorted-desc'
: ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function() {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i = 0; i < cols.length; i += 1) {
if (cols[i].sortable) {
// add the click event handler on the th so users
// dont have to click on those tiny arrows
el = getNthColumn(i).querySelector('.sorter').parentElement;
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function() {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData();
addSearchBox();
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);

View File

@ -1,78 +0,0 @@
{
"document": {
"id": "doc-1757978779922",
"fileName": "IMG_20250902_162210.jpg",
"fileSize": 980755,
"mimeType": "image/jpeg",
"uploadTimestamp": "2025-09-15T23:26:19.922Z"
},
"classification": {
"documentType": "Document",
"confidence": 0.6,
"subType": "Document",
"language": "fr",
"pageCount": 1
},
"extraction": {
"text": {
"raw": "4 . al -e ge :\na 4 mT. a ES Se Gaga EES hoe i -\na\nLo ee\nBaie: JERE REEMISEEE -\n1 ss : :-\n1) Carevalablejusques: 17.01.2033 À\n3 déévét: 1801 2018 : Ë 3\n\n Ban a / J\n\nPoa mec penses . -\n",
"processed": "4 . al -e ge :\na 4 mT. a ES Se Gaga EES hoe i -\na\nLo ee\nBaie: JERE REEMISEEE -\nl ss : :-\nl) Carevalablejusques: l7.ol.2oee À\ne déévét: l8ol 2ol8 : Ë e\n\n Ban a / J\n\nPoa mec penses . -\n",
"wordCount": 48,
"characterCount": 338,
"confidence": 0.28
},
"entities": {
"persons": [
{
"id": "identity-0",
"type": "person",
"firstName": "Se",
"lastName": "Gaga",
"role": null,
"email": null,
"phone": null,
"confidence": 0.9,
"source": "rule-based"
}
],
"companies": [],
"addresses": [],
"financial": {
"amounts": [],
"totals": {},
"payment": {}
},
"dates": [],
"contractual": {
"clauses": [],
"signatures": []
},
"references": []
}
},
"metadata": {
"processing": {
"engine": "4NK_IA_Backend",
"version": "1.0.0",
"processingTime": "4948ms",
"ocrEngine": "tesseract.js",
"nerEngine": "rule-based",
"preprocessing": {
"applied": true,
"reason": "Image preprocessing applied"
}
},
"quality": {
"globalConfidence": 0.6,
"textExtractionConfidence": 0.28,
"entityExtractionConfidence": 0.9,
"classificationConfidence": 0.6
}
},
"status": {
"success": true,
"errors": [],
"warnings": ["Aucune signature détectée"],
"timestamp": "2025-09-15T23:26:19.922Z"
}
}

View File

@ -1,33 +0,0 @@
#!/bin/bash
echo "🔍 Diagnostic du démarrage du site 4NK_IA_front"
echo "================================================"
echo "📁 Répertoire actuel:"
pwd
echo -e "\n📦 Vérification des fichiers essentiels:"
ls -la package.json 2>/dev/null && echo "✅ package.json trouvé" || echo "❌ package.json manquant"
ls -la vite.config.ts 2>/dev/null && echo "✅ vite.config.ts trouvé" || echo "❌ vite.config.ts manquant"
ls -la src/ 2>/dev/null && echo "✅ dossier src/ trouvé" || echo "❌ dossier src/ manquant"
echo -e "\n🔧 Vérification de Node.js:"
node --version 2>/dev/null && echo "✅ Node.js disponible" || echo "❌ Node.js non trouvé"
npm --version 2>/dev/null && echo "✅ npm disponible" || echo "❌ npm non trouvé"
echo -e "\n📋 Vérification des dépendances:"
if [ -d "node_modules" ]; then
echo "✅ node_modules/ existe"
ls node_modules/ | wc -l | xargs echo "📊 Nombre de packages installés:"
else
echo "❌ node_modules/ manquant - exécutez: npm install"
fi
echo -e "\n🌐 Vérification des ports:"
ss -tlnp | grep 5174 && echo "⚠️ Port 5174 déjà utilisé" || echo "✅ Port 5174 libre"
echo -e "\n🚀 Tentative de démarrage:"
echo "Exécution de: npm run dev"
npm run dev

View File

@ -1,17 +0,0 @@
version: '3.9'
services:
frontend:
image: 'git.4nkweb.com/4nk/4nk-ia-front:${TAG:-dev}'
container_name: 4nk-ia-front
restart: unless-stopped
ports:
- '8080:80'
environment:
- VITE_API_URL=${VITE_API_URL:-http://172.23.0.10:8000}
healthcheck:
test: ['CMD', 'wget', '-qO-', 'http://localhost/']
interval: 30s
timeout: 5s
retries: 3
start_period: 10s

View File

@ -1,244 +0,0 @@
# Journal d'incident - 2025-09-16
## Mise à jour 2025-09-18
### État davancement
- Backend: support explicite des `.txt`, upgrade `multer@^2`, sécurisation de tous les accès `.length` (OCR/NER), traitement asynchrone robuste (flags `.pending`, nettoyage garanti).
- Cache unifié: usage de `cache/` à la racine; backup puis purge de `backend/cache/` (doublon) pour éviter les incohérences.
- Outils: `scripts/precache.cjs` pour préremplir le cache à partir d`uploads/` (détection root/backend automatique).
- Documentation: ajout `docs/CACHE_ET_TRAITEMENT_ASYNC.md` et enrichissement de lanalyse.
- Frontend: code splitting confirmé (`React.lazy`/`Suspense`), React Router v7, MUI v7, Redux Toolkit. Nouvelles commandes de tests: `test:collectors`, `test:ocr`, `test:api`, `test:e2e`, `test:all`.
### Conformité aux bonnes pratiques
- Qualité: ESLint 9, Prettier, markdownlint; Vitest + Testing Library; `tsconfig` strict; Docker multi-stage avec Nginx pour SPA.
- Architecture: couche services HTTP (Axios) isolée, état centralisé (Redux Toolkit), routing moderne, mécanismes de cache et asynchronisme documentés.
### Risques et points de vigilance
- Dépendance suspecte `router-dom` (doublon de `react-router-dom`) dans `package.json` racine: à supprimer si non utilisée.
- Alignement de types: vérifier la stricte conformité entre `ExtractionResult` (front) et la réponse normalisée backend (ex. champs additionnels comme `timestamp`).
- Rigueur markdownlint: sassurer des lignes vides autour des titres/blocs et de longueurs de ligne raisonnables dans les nouveaux docs.
- CI/Tagging: respecter le préfixe de commit `ci: docker_tag=dev-test` et les conventions internes.
### Actions prioritaires
1. Mettre à jour `CHANGELOG.md` (support `.txt`, durcissement `.length`, cache unifié, script precache, doc async/cache).
2. Lancer `npm run lint`, `npm run mdlint`, `npm run test:all`, `npm run build` et corriger les erreurs TS/ESLint éventuelles (types dentités, variables inutilisées, deps de hooks).
3. Retirer `router-dom` si non utilisée.
4. Committer et pousser sur `dev` avec message CI conforme; proposer un tag.
## Résumé
Le frontend affichait des erreurs 502 Bad Gateway via Nginx pour les endpoints `/api/health` et `/api/folders/{hash}/results`.
## Constat
- `curl https://ia.4nkweb.com/api/health` retournait 502.
- Aucun service n'écoutait en local sur le port 3001.
- `backend.log` montrait des tentatives de traitement sur un fichier `.txt` avec erreurs Sharp (format non supporté), indiquant un arrêt préalable du service.
## Actions de rétablissement
1. Installation de Node via nvm (Node 22.x) pour satisfaire `engines`.
2. Installation des dépendances (`npm ci`) côté backend et racine.
3. Démarrage du backend Express sur 3001 en arrièreplan avec logs et PID.
4. Vérifications:
- `curl http://127.0.0.1:3001/api/health` → 200 OK
- `curl https://ia.4nkweb.com/api/health` → 200 OK
- `curl https://ia.4nkweb.com/api/folders/7d99a85daf66a0081a0e881630e6b39b/results` → 200 OK
## Causes probables
- Processus backend arrêté (pas de listener sur 3001).
- Gestion incomplète des fichiers `.txt` côté backend (détection MIME), pouvant entraîner des erreurs avec Sharp.
## Recommandations de suivi
- Ajouter le mapping `.txt``text/plain` dans la détection MIME backend.
- Dans `/api/extract`, gérer explicitement les `.txt` (lecture directe) comme dans `processDocument`.
- Mettre à jour Multer vers 2.x (1.x est vulnérable et déprécié).
- Surveiller `backend.log` et ajouter une supervision (systemd/service manager).
---
# Audit détaillé du dépôt 4NK_IA_front
#### Portée
- **Code analysé**: frontend React/TypeScript sous Vite, répertoire `/home/debian/4NK_IA_front`.
- **Commandes exécutées**: `npm ci`, `npm run lint`, `npm run mdlint`, `npm run test`, `npm run build`.
### Architecture et pile technique
- **Framework**: React 19 + TypeScript, Vite 7 (`vite.config.ts`).
- **UI**: MUI v7.
- **État**: Redux Toolkit + `react-redux` (`src/store/index.ts`, `src/store/documentSlice.ts`).
- **Routing**: React Router v7 avec code-splitting via `React.lazy`/`Suspense` (`src/router/index.tsx`).
- **Services**: couche dabstraction HTTP via Axios (`src/services/api.ts`), backend direct (`src/services/backendApi.ts`), OpenAI (`src/services/openai.ts`), fichiers/ dossiers (`src/services/folderApi.ts`).
- **Build/Runtime**: Docker multistage Node→Nginx, config SPA (`Dockerfile`, `nginx.conf`).
- **Qualité**: ESLint + Prettier + markdownlint, Vitest + Testing Library.
### Points forts
- **Code splitting** déjà en place au niveau du routeur.
- **Centralisation détat** propre (slices, middlewares, persistance localStorage).
- **Abstraction des services** HTTP claire (Axios + backends alternatifs).
- **Docker** production prêt (Nginx + healthcheck) et CI potentielle via `docker-compose.registry.yml`.
### Problèmes détectés
#### Lint TypeScript/JS
- Commande: `npm run lint`.
- Résultat: 57 problèmes trouvés (49 erreurs, 8 avertissements).
- Catégories principales:
- Types interdits `any` dans plusieurs services, ex. `src/services/backendApi.ts` (lignes 2336, 90, 97, 107) et `src/services/fileExtract.ts` (lignes 2, 5, 55, 63, 110).
- Variables non utilisées, ex. `src/App.tsx` ligne 8 (`setCurrentFolderHash`), `src/store/documentSlice.ts` lignes 7, 56, 420, 428, `src/services/openai.ts` variables `_file`, `_address`, etc.
- Règles `react-hooks/exhaustive-deps` dans `src/App.tsx` (ligne 65) et `src/components/Layout.tsx` (ligne 84).
- Regex avec échappements inutiles dans `src/services/ruleNer.ts` (lignes 3334, 84, 119).
- Typage middleware Redux avec `any` dans `src/store/index.ts` ligne 8.
#### Lint Markdown
- Commande: `npm run mdlint`.
- Problèmes récurrents:
- Manque de lignes vides autour des titres et listes: `CHANGELOG.md`, `docs/API_BACKEND.md`, `docs/architecture-backend.md`, `docs/changelog-pending.md`, `docs/HASH_SYSTEM.md`.
- Longueur de ligne > 120 caractères: `docs/API_BACKEND.md`, `docs/architecture-backend.md`, `docs/HASH_SYSTEM.md`, `docs/systeme-pending.md`.
- Blocs de code sans langage ou sans lignes vides autour.
- Titres en emphase non conformes (MD036) dans certains documents.
#### Tests
- Commande: `npm run test`.
- Résultat: 1 suite OK, 1 suite en échec.
- Échec: `tests/testFilesApi.test.ts` — import introuvable `../src/services/testFilesApi` (le fichier nexiste pas). Il faut soit créer `src/services/testFilesApi.ts`, soit adapter le test pour la source disponible.
#### Build de production
- Commande: `npm run build`.
- Résultat: erreurs TypeScript empêchant la compilation.
- Incohérences de types côté `ExtractionResult` consommé vs structures produites dans `src/services/api.ts` (propriété `timestamp` non prévue dans `ExtractionResult`).
- Types dentités utilisés en `views/ExtractionView.tsx` traités comme `string` au lieu dobjets typés (`Identity`, `Address`). Ex: accès à `firstName`, `lastName`, `street`, `city`, `postalCode`, `confidence` sur des `string`.
- Variables non utilisées dans plusieurs fichiers (cf. lint cidessus).
### Causes probables et pistes de résolution
- **Modèle de données**: divergence entre la structure standardisée `ExtractionResult` (`src/types/index.ts`) et le mapping réalisé dans `src/services/api.ts`/`backendApi.ts`. Il faut:
- Aligner les champs (ex. déplacer `timestamp` vers `status.timestamp` ou vers des métadonnées conformes).
- Harmoniser la forme des entités retournées (personnes/adresses) pour correspondre strictement à `Identity[]` et `Address[]`.
- **Composants de vues**: `ExtractionView.tsx` suppose des entités objets alors que les données mappées peuvent contenir des chaînes. Il faut normaliser en amont (mapping service) et/ou renforcer les gardefous de rendu.
- **Tests**: ajouter `src/services/testFilesApi.ts` (ou ajuster limport) pour couvrir lAPI de fichiers de test référencée par `tests/testFilesApi.test.ts`.
- **Qualité**:
- Remplacer les `any` par des types précis (ou `unknown` + raffinements), surtout dans `backendApi.ts`, `fileExtract.ts`, `openai.ts`.
- Corriger les dépendances de hooks React.
- Nettoyer les variables non utilisées et directives `eslint-disable` superflues.
- Corriger les regex avec échappements inutiles dans `ruleNer.ts`.
- Corriger les erreurs markdown (MD013, MD022, MD031, MD032, MD036, MD040, MD047) en ajoutant lignes vides et langages de blocs.
### Sécurité et configuration
- **Variables denvironnement**: `VITE_API_URL`, `VITE_USE_OPENAI`, clés OpenAI masquées en dev (`src/services/api.ts`). OK.
- **Nginx**: SPA et healthcheck définis (`nginx.conf`). OK pour production statique.
- **Docker**: image multistage; healthcheck HTTP. Tagging via scripts et `docker-compose.registry.yml` (variable `TAG`). À aligner avec conventions internes du registre.
### Recommandations prioritaires (ordre dexécution)
1. Corriger les erreurs TypeScript bloquantes du build:
- Aligner `ExtractionResult` consommé/produit (services + vues). Supprimer/relocaliser `timestamp` au bon endroit.
- Normaliser `identities`/`addresses` en objets typés dans le mapping service.
- Corriger `ExtractionView.tsx` pour refléter les types réels.
2. Supprimer variables non utilisées et corriger `any` majeurs dans services critiques (`backendApi.ts`, `fileExtract.ts`).
3. Ajouter/implémenter `src/services/testFilesApi.ts` ou corriger limport de test.
4. Corriger les règles `react-hooks/exhaustive-deps`.
5. Corriger markdownlint dans `CHANGELOG.md` et `docs/*.md` (lignes vides, langages de blocs, longueurs de ligne raisonnables).
6. Relancer lint, tests et build pour valider.
### Notes de compatibilité
- **Node**: engines `>=20.19 <23`. Testé avec Node 22.12.0 (OK) conformément au README.
- **ESLint**: config moderne (eslint@9, typescript-eslint@8) — stricte sur `any` et hooks React.
### Annexes (références de fichiers)
- `src/types/index.ts` — définitions `ExtractionResult`, `Identity`, `Address`.
- `src/services/api.ts` — mapping de la réponse backend; contient la propriété non typée `timestamp` sur `ExtractionResult` (à déplacer).
- `src/views/ExtractionView.tsx` — accès de propriétés dobjets sur des `string` (à corriger après normalisation du mapping).
- `tests/testFilesApi.test.ts` — dépend de `src/services/testFilesApi.ts` non présent.
## Mise à jour 2025-09-19
### État du dépôt et de la build
- Build Vite/TypeScript: OK (production) — artefacts générés dans `dist/`.
- Lint JS/TS: 134 erreurs, 9 avertissements (principalement `no-explicit-any`, deps de hooks, `no-empty`).
- Lint Markdown: nombreuses erreurs dans `docs/` et dans des dépendances tierces sous `backend/node_modules/`.
### Détails des constats
- Router: découpage de code via `React.lazy` et `Suspense` dans `src/router/index.tsx`.
- État: centralisé via Redux Toolkit, persistance `localStorage` (`src/store/index.ts`).
- Services: séparation claire (`src/services/api.ts`, `backendApi.ts`, `folderApi.ts`, `openai.ts`).
- Vues: `UploadView`, `ExtractionView`, `AnalyseView`, `ContexteView`, `ConseilView`.
### Résultats outillés
```bash
# Lint JS/TS
npm run lint
# Lint Markdown
npm run mdlint
# Build prod
npm run build
# Tests unitaires
npm run test
```
Observations clés:
- `npm run build`: succès, aucun blocage de typage.
- `npm run lint`: erreurs récurrentes `@typescript-eslint/no-explicit-any` dans:
- `src/services/backendApi.ts`, `src/services/fileExtract.ts`, `src/views/*`, `src/store/*`.
- Règles hooks: `react-hooks/exhaustive-deps` (`App.tsx`, `Layout.tsx`, `UploadView.tsx`).
- `npm run mdlint`: le linter inspecte des fichiers tiers dans `backend/node_modules/`. À ignorer côté script.
### Tests (Vitest)
- Suites en échec: `tests/ocr.test.js`, `tests/collectors.test.js`, `tests/ExtractionView.tabs.test.tsx`.
- Erreurs dimport pour `supertest` dans `tests/api.test.js` et `tests/e2e.test.js`.
- Problèmes identifiés:
- `ExtractionView.tabs.test.tsx`: `useNavigate` hors Router → utiliser `MemoryRouter`.
- `api.test.js` / `e2e.test.js`: dépendance manquante `supertest`.
- `ocr.test.js`: fichiers images manquants (Sharp) → fournir fixtures ou mocker I/O.
- `collectors.test.js`: timeouts et attentes non alignées → mock `fetch` et ajuster assertions.
- Port 3001 occupé pendant les tests (EADDRINUSE) → ne pas démarrer de serveur réel, utiliser des doubles/ports éphémères.
### Recommandations immédiates (ordre conseillé)
1. Qualité TypeScript
- Remplacer `any` par des types précis (ou `unknown` avec affinage) dans `backendApi.ts`, `fileExtract.ts`, `views/*`, `store/*`.
- Corriger les dépendances de hooks (`react-hooks/exhaustive-deps`).
2. Tests
- Ajouter `devDependency` `supertest` ou conditionner les tests dAPI.
- Enrober les rendus dépendants du router par `MemoryRouter`.
- Mocker les appels réseaux (collectors) et I/O (OCR/Sharp).
- Éviter lécoute du port 3001 pendant les tests.
3. Markdownlint
- Mettre à jour le script `mdlint` pour ignorer `backend/node_modules` et autres dossiers de dépendances (appr. requise).
4. Documentation
- Poursuivre lalignement sur `ExtractionResult` et documenter les champs backend réels dans `docs/API_BACKEND.md`.
### Demandes dapprobation
- Puis-je modifier `package.json` (script `mdlint`) pour ajouter `--ignore backend/node_modules` ?
- Souhaitez-vous ajouter `supertest` en `devDependency` afin de rétablir `tests/api.test.js` et `tests/e2e.test.js` ?
### Prochaines actions proposées
- Corriger une première tranche derreurs `any` dans `src/services/backendApi.ts` et `src/store/index.ts`.
- Mettre à jour `tests/ExtractionView.tabs.test.tsx` pour utiliser `MemoryRouter`.
- Ajouter des mocks pour `fetch` dans `tests/collectors.test.js` et des fixtures/mocks OCR.

View File

@ -1,9 +1,9 @@
# Documentation API - IA Lecoffre.io
# Documentation API - 4NK IA Front Notarial
## Vue d'ensemble
L'application IA Lecoffre.io communique uniquement avec le backend interne pour toutes les
fonctionnalités (upload, extraction, analyse, contexte, conseil).
L'application 4NK IA Front Notarial communique avec plusieurs APIs pour fournir une expérience complète
d'analyse de documents notariaux.
## API Backend Principal
@ -18,41 +18,86 @@ http://localhost:8000 (développement)
#### Upload de document
```http
POST /api/notary/upload
POST /api/documents/upload
Content-Type: multipart/form-data
Body: FormData avec le fichier
```
Réponse attendue (champs utilisés par le front) :
```json
{
"document_id": "doc_123456",
"mime_type": "application/pdf",
"functional_type": "CNI"
}
```
Mappage front en `Document` :
**Réponse :**
```json
{
"id": "doc_123456",
"name": "acte_vente.pdf",
"mimeType": "application/pdf",
"functionalType": "CNI",
"type": "application/pdf",
"size": 1024000,
"uploadDate": "<date locale>",
"status": "completed",
"previewUrl": "blob:..."
"uploadDate": "2024-01-15T10:30:00Z",
"status": "completed"
}
```
#### Extraction de données
```http
GET /api/notary/documents/{documentId}
GET /api/documents/{documentId}/extract
```
**Réponse :**
```json
{
"documentId": "doc_123456",
"text": "Texte extrait du document...",
"language": "fr",
"documentType": "Acte de vente",
"identities": [
{
"id": "1",
"type": "person",
"firstName": "Jean",
"lastName": "Dupont",
"birthDate": "1980-05-15",
"nationality": "Française",
"confidence": 0.95
}
],
"addresses": [
{
"street": "123 Rue de la Paix",
"city": "Paris",
"postalCode": "75001",
"country": "France"
}
],
"properties": [
{
"id": "1",
"type": "apartment",
"address": {
"street": "123 Rue de la Paix",
"city": "Paris",
"postalCode": "75001",
"country": "France"
},
"surface": 75,
"cadastralReference": "1234567890AB",
"value": 250000
}
],
"contracts": [
{
"id": "1",
"type": "sale",
"parties": [],
"amount": 250000,
"date": "2024-01-15",
"clauses": ["Clause de garantie", "Clause de condition suspensive"]
}
],
"signatures": ["Jean Dupont", "Marie Martin"],
"confidence": 0.92
}
```
#### Analyse du document
@ -61,38 +106,128 @@ GET /api/notary/documents/{documentId}
GET /api/documents/{documentId}/analyze
```
**Réponse :**
```json
{
"documentId": "doc_123456",
"documentType": "Acte de vente",
"isCNI": false,
"credibilityScore": 0.88,
"summary": "Document analysé avec succès. Toutes les informations semblent cohérentes.",
"recommendations": [
"Vérifier l'identité des parties auprès des autorités compétentes",
"Contrôler la validité des documents cadastraux"
]
}
```
#### Données contextuelles
```http
GET /api/documents/{documentId}/context
```
**Réponse :**
```json
{
"documentId": "doc_123456",
"cadastreData": {
"status": "disponible",
"reference": "1234567890AB"
},
"georisquesData": {
"status": "aucun risque identifié"
},
"geofoncierData": {
"status": "données disponibles"
},
"bodaccData": {
"status": "aucune procédure en cours"
},
"infogreffeData": {
"status": "entreprise en règle"
},
"lastUpdated": "2024-01-15T10:30:00Z"
}
```
#### Conseil IA
```http
GET /api/documents/{documentId}/conseil
```
**Réponse :**
```json
{
"documentId": "doc_123456",
"analysis": "Ce document présente toutes les caractéristiques d'un acte notarial standard.",
"recommendations": [
"Procéder à la vérification d'identité des parties",
"Contrôler la validité des documents fournis"
],
"risks": [
"Risque faible : Vérification d'identité recommandée",
"Risque moyen : Contrôle cadastral nécessaire"
],
"nextSteps": [
"Collecter les pièces d'identité des parties",
"Vérifier les documents cadastraux",
"Préparer l'acte final"
],
"generatedAt": "2024-01-15T10:30:00Z"
}
```
## APIs Externes
Les APIs externes (Cadastre, Géorisques, Géofoncier, BODACC, Infogreffe) sont appelées côté backend
uniquement. Aucun appel direct côté front.
### Cadastre
**URL :** `https://api.cadastre.gouv.fr`
**Usage :** Vérification des références cadastrales et des propriétés
### Géorisques
**URL :** `https://www.georisques.gouv.fr/api`
**Usage :** Analyse des risques géologiques et environnementaux
### Géofoncier
**URL :** `https://api.geofoncier.fr`
**Usage :** Données foncières et géographiques
### BODACC
**URL :** `https://api.bodacc.fr`
**Usage :** Vérification des procédures collectives et des entreprises
### Infogreffe
**URL :** `https://api.infogreffe.fr`
**Usage :** Informations sur les entreprises et leurs dirigeants
## Gestion d'erreur
### Codes d'erreur HTTP
- 200 : Succès
- 400 : Requête malformée
- 404 : Ressource non trouvée
- 405 : Méthode non autorisée
- 500 : Erreur serveur interne
- **200** : Succès
- **400** : Requête malformée
- **404** : Ressource non trouvée
- **405** : Méthode non autorisée
- **500** : Erreur serveur interne
### Erreurs de connexion
- ERR_NETWORK : Erreur de réseau
- ERR_CONNECTION_REFUSED : Connexion refusée
- ERR_TIMEOUT : Timeout de la requête
- **ERR_NETWORK** : Erreur de réseau
- **ERR_CONNECTION_REFUSED** : Connexion refusée
- **ERR_TIMEOUT** : Timeout de la requête
### Fallback automatique
En cas d'erreur, l'application bascule automatiquement vers des données de démonstration pour maintenir l'expérience utilisateur.
## Configuration
@ -100,28 +235,22 @@ uniquement. Aucun appel direct côté front.
```env
VITE_API_URL=http://localhost:8000
VITE_USE_OPENAI=false
VITE_OPENAI_API_KEY=
VITE_OPENAI_BASE_URL=https://api.openai.com/v1
VITE_OPENAI_MODEL=gpt-4o-mini
VITE_CADASTRE_API_URL=https://api.cadastre.gouv.fr
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
VITE_GEOFONCIER_API_URL=https://api.geofoncier.fr
VITE_BODACC_API_URL=https://api.bodacc.fr
VITE_INFOGREFFE_API_URL=https://api.infogreffe.fr
```
## Mode OpenAI (fallback)
Quand `VITE_USE_OPENAI=true`, le frontend bascule sur un mode de secours basé sur OpenAI:
- Upload: simulé côté client (le fichier nest pas envoyé à OpenAI)
- Extraction/Analyse/Conseil/Contexte: appels `chat.completions` sur `VITE_OPENAI_MODEL`
- Détection de type: heuristique simple côté client
Ce mode est utile pour démo/diagnostic quand le backend nest pas disponible.
### Configuration Axios
```typescript
const apiClient = axios.create({
baseURL: BASE_URL,
timeout: 60000,
timeout: 60000, // 60 secondes
headers: {
'Content-Type': 'application/json'
}
})
```
@ -134,6 +263,93 @@ Authorization: Bearer {token}
Content-Type: application/json
```
### Gestion des tokens
- **Refresh automatique** : Renouvellement des tokens expirés
- **Intercepteurs** : Ajout automatique des headers d'authentification
- **Logout automatique** : Déconnexion en cas d'erreur 401
## Rate Limiting
- Limites gérées par le backend
### Limites par défaut
- **100 requêtes/minute** par utilisateur
- **1000 requêtes/heure** par utilisateur
- **Backoff exponentiel** en cas de dépassement
### Headers de réponse
```http
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1642248000
```
## Monitoring et logs
### Métriques collectées
- **Temps de réponse** : Latence des requêtes
- **Taux d'erreur** : Pourcentage d'échecs
- **Utilisation** : Nombre de requêtes par endpoint
### Logs
- **Requêtes** : Log de toutes les requêtes API
- **Erreurs** : Log détaillé des erreurs
- **Performance** : Métriques de performance
## Tests
### Tests d'intégration
```typescript
// Test d'upload de document
test('should upload document successfully', async () => {
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
const result = await documentApi.upload(file)
expect(result.id).toBeDefined()
expect(result.status).toBe('completed')
})
```
### Tests de fallback
```typescript
// Test du mode démonstration
test('should return demo data on API error', async () => {
// Mock de l'erreur API
mockAxios.onPost('/api/documents/upload').reply(500)
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
const result = await documentApi.upload(file)
expect(result.id).toMatch(/^demo-/)
expect(result.name).toBe('test.pdf')
})
```
## Sécurité
### Validation des données
- **Sanitization** : Nettoyage des entrées utilisateur
- **Validation** : Vérification des types et formats
- **Escape** : Protection contre les injections
### CORS
```typescript
// Configuration CORS
const corsOptions = {
origin: ['http://localhost:3000', 'https://app.4nkweb.com'],
credentials: true,
optionsSuccessStatus: 200
}
```
### HTTPS
- **Redirection automatique** : HTTP vers HTTPS en production
- **HSTS** : HTTP Strict Transport Security
- **Certificats SSL** : Certificats valides et à jour

View File

@ -1,463 +0,0 @@
# 📡 API Backend 4NK_IA - Documentation
## 🚀 **Vue d'ensemble**
L'API Backend 4NK_IA est un service d'extraction et d'analyse de documents utilisant l'OCR (Reconnaissance Optique de Caractères) et le NER (Reconnaissance d'Entités Nommées) pour traiter automatiquement les documents PDF et images.
### **Caractéristiques principales :**
- ✅ **Support multi-format** : PDF, JPEG, PNG, TIFF, TXT
- ✅ **OCR avancé** : Tesseract.js avec préprocessing d'images
- ✅ **Extraction PDF directe** : pdf-parse pour une précision maximale
- ✅ **NER intelligent** : Reconnaissance d'entités par règles
- ✅ **Format JSON standardisé** : Structure cohérente pour tous les documents
- ✅ **Préprocessing d'images** : Amélioration automatique de la qualité OCR
- ✅ **Gestion des doublons** : Système de hash SHA-256 pour éviter les fichiers dupliqués
---
## 🌐 **Endpoints de l'API**
### **Base URL :** `http://localhost:3001/api`
---
## 📋 **1. Health Check**
### **GET** `/api/health`
Vérifie l'état du serveur backend.
**Réponse :**
```json
{
"status": "OK",
"timestamp": "2025-09-15T22:45:50.123Z",
"version": "1.0.0"
}
```
**Exemple d'utilisation :**
```bash
curl http://localhost:3001/api/health
```
---
## 📁 **2. Liste des fichiers de test**
### **GET** `/api/test-files`
Retourne la liste des fichiers de test disponibles.
**Réponse :**
```json
{
"files": [
{
"name": "IMG_20250902_162159.jpg",
"size": 245760,
"type": "image/jpeg"
}
]
}
```
---
## 📂 **3. Liste des fichiers uploadés**
### **GET** `/api/uploads`
Retourne la liste des fichiers uploadés avec leurs métadonnées et hash SHA-256.
**Réponse :**
```json
{
"files": [
{
"name": "document-1757980637671.pdf",
"size": 1015808,
"hash": "a1b2c3d4e5f6...",
"uploadDate": "2025-09-15T23:56:14.751Z",
"modifiedDate": "2025-09-15T23:56:14.751Z"
}
],
"count": 1,
"totalSize": 1015808
}
```
**Exemple d'utilisation :**
```bash
curl http://localhost:3001/api/uploads
```
**Notes :**
- Le hash SHA-256 permet d'identifier les fichiers identiques
- Les fichiers dupliqués sont automatiquement détectés lors de l'upload
- Seuls les fichiers uniques sont conservés dans le système
---
## 🔍 **4. Extraction de documents**
### **POST** `/api/extract`
Extrait et analyse un document (PDF, image ou texte) pour identifier les entités et informations structurées.
#### **Paramètres :**
- **`document`** (file, required) : Fichier à analyser (PDF, JPEG, PNG, TIFF, TXT)
- **Taille maximale :** 10MB
#### **Gestion des doublons :**
- Le système calcule automatiquement un hash SHA-256 de chaque fichier uploadé
- Si un fichier avec le même hash existe déjà, le doublon est supprimé
- Le traitement utilise le fichier existant, évitant ainsi les calculs redondants
- Les logs indiquent clairement si un fichier est nouveau ou déjà existant
#### **Réponse - Format JSON Standard :**
```json
{
"document": {
"id": "doc-1757976350123",
"fileName": "facture_4NK_08-2025_04.pdf",
"fileSize": 85819,
"mimeType": "application/pdf",
"uploadTimestamp": "2025-09-15T22:45:50.123Z"
},
"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 soo Euros...",
"wordCount": 165,
"characterCount": 1197,
"confidence": 0.95
},
"entities": {
"persons": [
{
"id": "identity-0",
"type": "person",
"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-0",
"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"
}
],
"addresses": [
{
"id": "address-0",
"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"
}
],
"financial": {
"amounts": [
{
"id": "amount-0",
"type": "prestation",
"description": "Prestation du mois d'Août 2025",
"quantity": 10,
"unitPrice": 550.0,
"totalHT": 5500.0,
"currency": "EUR",
"confidence": 0.95
}
],
"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-0",
"type": "facture",
"value": "29-août-25",
"formatted": "2025-08-29",
"confidence": 0.9,
"source": "rule-based"
}
],
"contractual": {
"clauses": [
{
"id": "clause-0",
"type": "paiement",
"content": "Le paiement se fera (maximum) 30 jours après l'émission de la facture.",
"confidence": 0.9
}
],
"signatures": [
{
"id": "signature-0",
"type": "électronique",
"present": false,
"signatory": null,
"date": null,
"confidence": 0.8
}
]
},
"references": [
{
"id": "ref-0",
"type": "facture",
"number": "4NK_4",
"confidence": 0.95
}
]
}
},
"metadata": {
"processing": {
"engine": "4NK_IA_Backend",
"version": "1.0.0",
"processingTime": "423ms",
"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:45:50.123Z"
}
}
```
#### **Exemples d'utilisation :**
**Avec curl (PDF) :**
```bash
curl -X POST \
-F "document=@/path/to/document.pdf" \
http://localhost:3001/api/extract
```
**Avec curl (TXT) :**
```bash
curl -X POST \
-F "document=@/path/to/file.txt" \
http://localhost:3001/api/extract
```
**Avec JavaScript (fetch) :**
```javascript
const formData = new FormData()
formData.append('document', fileInput.files[0])
const response = await fetch('http://localhost:3001/api/extract', {
method: 'POST',
body: formData,
})
const result = await response.json()
console.log(result)
```
---
## 📊 **Types de documents supportés**
### **1. Factures**
- **Détection automatique** : Mots-clés "facture", "tva", "siren", "montant"
- **Entités extraites** :
- Sociétés (fournisseur/client)
- Adresses de facturation
- Montants et totaux
- Conditions de paiement
- Numéros de référence
- Dates
### **2. Cartes Nationales d'Identité (CNI)**
- **Détection automatique** : Mots-clés "carte nationale d'identité", "cni", "mrz"
- **Entités extraites** :
- Identités (nom, prénom)
- Numéros CNI
- Dates de naissance
- Adresses
### **3. Contrats**
- **Détection automatique** : Mots-clés "contrat", "vente", "achat", "acte"
- **Entités extraites** :
- Parties contractantes
- Clauses contractuelles
- Signatures
- Dates importantes
### **4. Attestations**
- **Détection automatique** : Mots-clés "attestation", "certificat"
- **Entités extraites** :
- Identités
- Dates
- Informations spécifiques
---
## 🔧 **Configuration et préprocessing**
### **Préprocessing d'images (pour JPEG, PNG, TIFF) :**
- **Redimensionnement** : Largeur cible 2000px
- **Amélioration du contraste** : Facteur 1.5
- **Luminosité** : Facteur 1.1
- **Conversion en niveaux de gris**
- **Amélioration de la netteté**
- **Réduction du bruit**
### **Extraction PDF directe :**
- **Moteur** : pdf-parse
- **Avantage** : Pas de conversion image, précision maximale
- **Confiance** : 95% par défaut
---
## ⚡ **Performances**
### **Temps de traitement typiques :**
- **PDF** : 200-500ms
- **Images** : 1-3 secondes (avec préprocessing)
- **Taille maximale** : 10MB
### **Confiance d'extraction :**
- **PDF** : 90-95%
- **Images haute qualité** : 80-90%
- **Images de qualité moyenne** : 60-80%
---
## 🚨 **Gestion d'erreurs**
### **Codes d'erreur HTTP :**
- **400** : Aucun fichier fourni
- **413** : Fichier trop volumineux (>10MB)
- **415** : Type de fichier non supporté
- **500** : Erreur de traitement interne
### **Exemple de réponse d'erreur :**
```json
{
"success": false,
"error": "Type de fichier non supporté",
"details": "Seuls les formats PDF, JPEG, PNG et TIFF sont acceptés"
}
```
---
## 🛠️ **Dépendances techniques**
### **Moteurs OCR :**
- **Tesseract.js** : Pour les images
- **pdf-parse** : Pour les PDF
### **Préprocessing :**
- **Sharp.js** : Traitement d'images
### **NER :**
- **Règles personnalisées** : Patterns regex pour l'extraction d'entités
---
## 📝 **Notes d'utilisation**
1. **Format de sortie standardisé** : Tous les documents retournent le même format JSON
2. **Confiance** : Chaque entité extraite inclut un score de confiance
3. **Source** : Indication de la méthode d'extraction (rule-based, ML, etc.)
4. **Métadonnées complètes** : Informations sur le traitement et la qualité
5. **Gestion des erreurs** : Warnings et erreurs détaillés dans la réponse
---
## 🔄 **Évolutions futures**
- [ ] Support de l'IA/ML pour l'extraction d'entités
- [ ] Support de documents multi-pages
- [ ] Extraction de signatures manuscrites
- [ ] Support de langues supplémentaires
- [ ] API de validation des extractions
- [ ] Cache des résultats pour optimisation
---
_Documentation générée le 15/09/2025 - Version 1.0.0_

282
docs/ARCHITECTURE.md Normal file
View File

@ -0,0 +1,282 @@
# Architecture de l'application 4NK IA Front Notarial
## Vue d'ensemble
L'application 4NK IA Front Notarial est une interface web moderne construite avec React et TypeScript,
conçue pour l'analyse intelligente de documents notariaux.
## Stack technique
### Frontend
- **React 18** : Framework UI avec hooks et composants fonctionnels
- **TypeScript 5.6** : Typage statique pour la robustesse du code
- **Vite 7** : Build tool rapide avec HMR (Hot Module Replacement)
- **Material-UI v6** : Bibliothèque de composants UI professionnels
### Gestion d'état
- **Redux Toolkit** : Gestion d'état centralisée et prévisible
- **React Redux** : Liaison React-Redux avec hooks
- **Async Thunks** : Gestion des actions asynchrones
### Routing et navigation
- **React Router v6** : Navigation côté client avec code splitting
- **Lazy loading** : Chargement à la demande des composants
### HTTP et API
- **Axios** : Client HTTP avec intercepteurs
- **Intercepteurs** : Gestion centralisée des erreurs et authentification
### Tests
- **Vitest** : Framework de test rapide et moderne
- **Testing Library** : Tests d'intégration React
- **JSDOM** : Environnement DOM simulé
- **Coverage V8** : Rapport de couverture de code
### Qualité de code
- **ESLint** : Linting avec règles strictes
- **Prettier** : Formatage automatique du code
- **markdownlint** : Validation des fichiers Markdown
## Structure du projet
```text
src/
├── components/ # Composants réutilisables
│ ├── Layout.tsx # Layout principal avec AppBar et navigation
│ └── NavigationTabs.tsx # Composant de navigation par onglets
├── views/ # Vues principales de l'application
│ ├── UploadView.tsx # Upload et gestion des documents
│ ├── ExtractionView.tsx # Affichage des données extraites
│ ├── AnalyseView.tsx # Analyse et score de vraisemblance
│ ├── ContexteView.tsx # Données contextuelles externes
│ └── ConseilView.tsx # Conseil IA et recommandations
├── store/ # Gestion d'état Redux
│ ├── index.ts # Configuration du store Redux
│ ├── appSlice.ts # État global de l'application
│ └── documentSlice.ts # État des documents et opérations
├── services/ # Services et API
│ └── api.ts # Client API et endpoints
├── types/ # Types et interfaces TypeScript
│ └── index.ts # Définitions de types centralisées
├── main.tsx # Point d'entrée de l'application
└── App.tsx # Composant racine avec routing
```
## Architecture des composants
### Layout et navigation
- **Layout** : Composant wrapper avec AppBar et navigation
- **NavigationTabs** : Navigation par onglets avec React Router
- **AppBar** : Barre de navigation principale
### Vues principales
#### UploadView
- **Dropzone** : Zone de glisser-déposer pour les fichiers
- **FileList** : Liste des documents uploadés
- **Status indicators** : Indicateurs de statut des documents
#### ExtractionView
- **DataDisplay** : Affichage des données extraites
- **ObjectLists** : Listes d'identités, adresses, biens, contrats
- **Confidence scores** : Scores de confiance des extractions
#### AnalyseView
- **CredibilityScore** : Score de vraisemblance du document
- **Recommendations** : Liste des recommandations
- **Summary** : Synthèse de l'analyse
#### ContexteView
- **ExternalData** : Données des APIs externes
- **StatusCards** : Cartes de statut pour chaque source
- **LastUpdated** : Horodatage des dernières mises à jour
#### ConseilView
- **LLMAnalysis** : Analyse générée par l'IA
- **RiskAssessment** : Évaluation des risques
- **NextSteps** : Prochaines étapes recommandées
## Gestion d'état Redux
### Structure du store
```typescript
interface RootState {
app: AppState // État global de l'application
document: DocumentState // État des documents et opérations
}
```
### AppState
- **status** : État global ('idle' | 'loading' | 'succeeded' | 'failed')
- **error** : Messages d'erreur globaux
### DocumentState
- **documents** : Liste des documents uploadés
- **currentDocument** : Document actuellement sélectionné
- **extractionResult** : Résultats d'extraction
- **analysisResult** : Résultats d'analyse
- **contextResult** : Données contextuelles
- **conseilResult** : Conseil IA
- **loading** : État de chargement
- **error** : Messages d'erreur
### Actions asynchrones
- **uploadDocument** : Upload d'un document
- **extractDocument** : Extraction des données
- **analyzeDocument** : Analyse du document
- **getContextData** : Récupération des données contextuelles
- **getConseil** : Génération du conseil IA
## Services API
### Configuration Axios
- **Base URL** : Configuration via variables d'environnement
- **Timeout** : 60 secondes pour les opérations longues
- **Intercepteurs** : Gestion d'erreur et fallback automatique
### Endpoints
- `POST /api/documents/upload` : Upload de document
- `GET /api/documents/{id}/extract` : Extraction de données
- `GET /api/documents/{id}/analyze` : Analyse du document
- `GET /api/documents/{id}/context` : Données contextuelles
- `GET /api/documents/{id}/conseil` : Conseil IA
### Gestion d'erreur
- **Intercepteurs** : Détection automatique des erreurs
- **Fallback** : Données de démonstration en cas d'erreur
- **Codes gérés** : 404, 405, ERR_NETWORK, ERR_CONNECTION_REFUSED
## Types et interfaces
### Document
```typescript
interface Document {
id: string
name: string
type: string
size: number
uploadDate: Date
status: 'uploading' | 'processing' | 'completed' | 'error'
}
```
### ExtractionResult
```typescript
interface ExtractionResult {
documentId: string
text: string
language: string
documentType: string
identities: Identity[]
addresses: Address[]
properties: Property[]
contracts: Contract[]
signatures: string[]
confidence: number
}
```
### AnalysisResult
```typescript
interface AnalysisResult {
documentId: string
documentType: string
isCNI: boolean
credibilityScore: number
summary: string
recommendations: string[]
}
```
## Mode démonstration
### Fonctionnement
- **Détection automatique** : Vérification de la disponibilité du backend
- **Fallback gracieux** : Basculement vers des données de démonstration
- **Données réalistes** : Exemples d'actes notariaux authentiques
- **Fonctionnalités complètes** : Toutes les vues opérationnelles
### Données de démonstration
- **Documents** : Exemples d'actes de vente, CNI, etc.
- **Identités** : Personnes avec informations complètes
- **Adresses** : Adresses françaises réalistes
- **Biens** : Propriétés avec références cadastrales
- **Contrats** : Clauses et montants réalistes
## Performance et optimisation
### Code splitting
- **Lazy loading** : Chargement à la demande des vues
- **Suspense** : Gestion des états de chargement
- **Bundle optimization** : Optimisation de la taille des bundles
### Caching
- **Redux state** : Mise en cache des données dans le store
- **API responses** : Cache des réponses API
- **Component memoization** : Optimisation des re-renders
### Build optimization
- **Vite** : Build rapide avec optimisations automatiques
- **Tree shaking** : Élimination du code mort
- **Minification** : Compression du code de production
## Sécurité
### Validation
- **TypeScript** : Validation de types à la compilation
- **Runtime validation** : Validation des données API
- **Input sanitization** : Nettoyage des entrées utilisateur
### CORS et CSP
- **CORS** : Configuration des en-têtes Cross-Origin
- **CSP** : Content Security Policy pour la sécurité
- **HTTPS** : Communication sécurisée en production
## Déploiement
### Environnements
- **Développement** : Serveur local avec HMR
- **Staging** : Environnement de test
- **Production** : Déploiement optimisé
### Variables d'environnement
- **VITE_API_URL** : URL de l'API backend
- **VITE_*_API_URL** : URLs des APIs externes
- **NODE_ENV** : Environnement d'exécution
### Build de production
- **Optimisation** : Minification et compression
- **Assets** : Optimisation des images et CSS
- **Bundle analysis** : Analyse de la taille des bundles

View File

@ -1,32 +0,0 @@
# Cache des résultats et traitement asynchrone
## Dossiers utilisés
- `uploads/<folderHash>`: fichiers déposés (source de vérité)
- `cache/<folderHash>`: résultats JSON et flags `.pending` (source pour lAPI)
- `backend/cache/*`: (désormais vide) ancien emplacement ne plus utiliser
## Flux de traitement
1. Dépôt dun fichier (`/api/extract`):
- Calcule `fileHash` (SHA256 du contenu)
- Si `cache/<folderHash>/<fileHash>.json` existe: renvoie immédiatement le JSON
- Sinon: crée `cache/<folderHash>/<fileHash>.pending`, lance lOCR/NER, puis écrit le JSON et supprime `.pending`
2. Listing (`/api/folders/:folderHash/results`):
- Agrège tous les JSON présents dans `cache/<folderHash>/`
- Pour chaque fichier présent dans `uploads/<folderHash>` sans JSON, crée un flag `.pending` et lance le traitement en arrièreplan, sans bloquer la réponse
## Points importants
- Le traitement images/PDF peut être long; le listing nattend pas la fin
- Le frontal réalise un polling périodique si `hasPending=true`
- Les erreurs de traitement suppriment le flag `.pending` et la requête renvoie 500 (extraction) ou 200 avec moins de résultats (listing)
## Bonnes pratiques
- Nécrire les résultats que dans `cache/<folderHash>` à la racine
- Toujours indexer les résultats par `fileHash.json`
- Protéger les accès à `.length` et valeurs potentiellement `undefined` dans le backend

517
docs/DEPLOYMENT.md Normal file
View File

@ -0,0 +1,517 @@
# Guide de déploiement - 4NK IA Front Notarial
## Vue d'ensemble
Ce guide couvre le déploiement de l'application 4NK IA Front Notarial dans différents environnements.
## Prérequis
### Environnement de développement
- **Node.js** : >= 22.12.0 (recommandé) ou >= 20.19.0
- **npm** : >= 10.0.0
- **Git** : Pour la gestion des versions
### Environnement de production
- **Serveur web** : Nginx ou Apache
- **HTTPS** : Certificat SSL valide
- **CDN** : Optionnel pour les assets statiques
## Configuration des environnements
### Variables d'environnement
#### Développement (.env.development)
```env
NODE_ENV=development
VITE_API_URL=http://localhost:8000
VITE_CADASTRE_API_URL=https://api.cadastre.gouv.fr
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
VITE_GEOFONCIER_API_URL=https://api.geofoncier.fr
VITE_BODACC_API_URL=https://api.bodacc.fr
VITE_INFOGREFFE_API_URL=https://api.infogreffe.fr
```
#### Staging (.env.staging)
```env
NODE_ENV=staging
VITE_API_URL=https://api-staging.4nkweb.com
VITE_CADASTRE_API_URL=https://api.cadastre.gouv.fr
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
VITE_GEOFONCIER_API_URL=https://api.geofoncier.fr
VITE_BODACC_API_URL=https://api.bodacc.fr
VITE_INFOGREFFE_API_URL=https://api.infogreffe.fr
```
#### Production (.env.production)
```env
NODE_ENV=production
VITE_API_URL=https://api.4nkweb.com
VITE_CADASTRE_API_URL=https://api.cadastre.gouv.fr
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
VITE_GEOFONCIER_API_URL=https://api.geofoncier.fr
VITE_BODACC_API_URL=https://api.bodacc.fr
VITE_INFOGREFFE_API_URL=https://api.infogreffe.fr
```
## Build de production
### Script de build
```bash
# Build standard
npm run build
# Build avec analyse
npm run build -- --analyze
# Build pour un environnement spécifique
npm run build -- --mode production
```
### Optimisations automatiques
- **Minification** : Code JavaScript et CSS minifié
- **Tree shaking** : Élimination du code mort
- **Code splitting** : Division en chunks optimaux
- **Asset optimization** : Optimisation des images et fonts
### Structure du build
```text
dist/
├── index.html # Point d'entrée HTML
├── assets/
│ ├── index-[hash].js # Bundle principal
│ ├── index-[hash].css # Styles principaux
│ ├── [chunk]-[hash].js # Chunks de code splitting
│ └── [asset]-[hash].[ext] # Assets optimisés
└── favicon.ico # Icône du site
```
## Déploiement sur serveur
### Configuration Nginx
#### Fichier de configuration
```nginx
server {
listen 80;
listen [::]:80;
server_name app.4nkweb.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name app.4nkweb.com;
# Certificats SSL
ssl_certificate /path/to/certificate.crt;
ssl_certificate_key /path/to/private.key;
# Configuration SSL
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
# Root directory
root /var/www/4nk-ia-front/dist;
index index.html;
# Gestion des routes SPA
location / {
try_files $uri $uri/ /index.html;
}
# Cache des assets statiques
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Sécurité
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
}
```
### Configuration Apache
#### Fichier .htaccess
```apache
RewriteEngine On
# Redirection HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Gestion des routes SPA
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
# Cache des assets
<FilesMatch "\.(css|js|png|jpg|jpeg|gif|ico|svg)$">
ExpiresActive On
ExpiresDefault "access plus 1 year"
Header set Cache-Control "public, immutable"
</FilesMatch>
# Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
</IfModule>
# Sécurité
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Strict-Transport-Security "max-age=63072000"
```
## Déploiement avec Docker
### Dockerfile
```dockerfile
# Build stage
FROM node:22.12-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
### docker-compose.yml
```yaml
version: '3.8'
services:
frontend:
build: .
ports:
- "80:80"
- "443:443"
volumes:
- ./ssl:/etc/nginx/ssl:ro
environment:
- NODE_ENV=production
restart: unless-stopped
```
### Script de déploiement
```bash
#!/bin/bash
# deploy.sh
set -e
echo "Building application..."
docker build -t 4nk-ia-front .
echo "Stopping existing container..."
docker stop 4nk-ia-front || true
docker rm 4nk-ia-front || true
echo "Starting new container..."
docker run -d \
--name 4nk-ia-front \
-p 80:80 \
-p 443:443 \
-v /path/to/ssl:/etc/nginx/ssl:ro \
4nk-ia-front
echo "Deployment completed!"
```
## Déploiement avec CI/CD
### GitHub Actions
```yaml
name: Deploy
on:
push:
branches: [release]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22.12'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test
- name: Build application
run: npm run build
env:
NODE_ENV: production
VITE_API_URL: ${{ secrets.API_URL }}
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/4nk-ia-front
git pull origin release
npm ci
npm run build
sudo systemctl reload nginx
```
### GitLab CI
```yaml
stages:
- test
- build
- deploy
test:
stage: test
image: node:22.12-alpine
script:
- npm ci
- npm run test
only:
- merge_requests
- release
build:
stage: build
image: node:22.12-alpine
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
only:
- release
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
script:
- scp -r dist/* $DEPLOY_USER@$DEPLOY_HOST:/var/www/4nk-ia-front/
- ssh $DEPLOY_USER@$DEPLOY_HOST "sudo systemctl reload nginx"
only:
- release
```
## Monitoring et surveillance
### Métriques de performance
- **Temps de chargement** : Mesure du First Contentful Paint
- **Taille des bundles** : Surveillance de la taille des assets
- **Erreurs JavaScript** : Tracking des erreurs côté client
### Outils de monitoring
- **Google Analytics** : Analytics et performance
- **Sentry** : Monitoring des erreurs
- **Lighthouse** : Audit de performance
### Configuration Sentry
```typescript
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: "YOUR_SENTRY_DSN",
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
});
```
## Rollback et récupération
### Stratégie de rollback
```bash
#!/bin/bash
# rollback.sh
PREVIOUS_VERSION=$1
if [ -z "$PREVIOUS_VERSION" ]; then
echo "Usage: ./rollback.sh <version>"
exit 1
fi
echo "Rolling back to version $PREVIOUS_VERSION..."
# Restaurer la version précédente
git checkout $PREVIOUS_VERSION
npm ci
npm run build
# Redéployer
sudo systemctl reload nginx
echo "Rollback completed!"
```
### Sauvegarde
```bash
#!/bin/bash
# backup.sh
BACKUP_DIR="/backups/4nk-ia-front"
DATE=$(date +%Y%m%d_%H%M%S)
# Créer le répertoire de sauvegarde
mkdir -p $BACKUP_DIR
# Sauvegarder les fichiers
tar -czf $BACKUP_DIR/backup_$DATE.tar.gz /var/www/4nk-ia-front
# Nettoyer les anciennes sauvegardes (garder 7 jours)
find $BACKUP_DIR -name "backup_*.tar.gz" -mtime +7 -delete
echo "Backup completed: backup_$DATE.tar.gz"
```
## Sécurité
### Headers de sécurité
- **CSP** : Content Security Policy
- **HSTS** : HTTP Strict Transport Security
- **X-Frame-Options** : Protection contre le clickjacking
- **X-Content-Type-Options** : Protection contre MIME sniffing
### Configuration CSP
```html
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.4nkweb.com;
font-src 'self' data:;
">
```
### Audit de sécurité
```bash
# Audit des dépendances
npm audit
# Audit de sécurité avec Snyk
npx snyk test
# Scan de vulnérabilités
npx audit-ci --config audit-ci.json
```
## Maintenance
### Mise à jour des dépendances
```bash
# Vérifier les mises à jour
npm outdated
# Mettre à jour les dépendances
npm update
# Mise à jour majeure
npx npm-check-updates -u
npm install
```
### Nettoyage
```bash
# Nettoyer le cache npm
npm cache clean --force
# Nettoyer les node_modules
rm -rf node_modules package-lock.json
npm install
# Nettoyer le build
rm -rf dist
npm run build
```
### Logs et debugging
```bash
# Logs Nginx
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log
# Logs de l'application
sudo journalctl -u nginx -f
# Debug des erreurs
sudo nginx -t # Test de configuration
```

View File

@ -1,178 +0,0 @@
# 🔐 Système de Hash pour la Gestion des Doublons
## 📋 **Vue d'ensemble**
Le système de hash SHA-256 a été implémenté dans le backend 4NK_IA pour éviter les doublons d'upload et optimiser le stockage des fichiers. Ce système garantit qu'un fichier identique ne sera traité qu'une seule fois, même s'il est uploadé plusieurs fois.
---
## 🔧 **Fonctionnement**
### **1. Calcul du Hash**
- Chaque fichier uploadé est analysé pour calculer son hash SHA-256
- Le hash est calculé sur le contenu binaire complet du fichier
- Utilisation de la fonction `crypto.createHash('sha256')` de Node.js
### **2. Détection des Doublons**
- Avant traitement, le système vérifie si un fichier avec le même hash existe déjà
- La fonction `findExistingFileByHash()` parcourt le dossier `uploads/`
- Si un doublon est trouvé, le fichier uploadé est supprimé
### **3. Traitement Optimisé**
- Le traitement utilise le fichier existant (pas le doublon)
- Les résultats d'extraction sont identiques pour les fichiers identiques
- Économie de ressources CPU et de stockage
---
## 📁 **Structure des Fichiers**
```
uploads/
├── document-1757980637671.pdf # Fichier original
├── document-1757980640576.pdf # Doublon (supprimé après traitement)
└── image-1757980651234.jpg # Autre fichier unique
```
---
## 🔍 **API Endpoints**
### **GET** `/api/uploads`
Liste tous les fichiers uploadés avec leurs métadonnées :
```json
{
"files": [
{
"name": "document-1757980637671.pdf",
"size": 1015808,
"hash": "a1b2c3d4e5f6789...",
"uploadDate": "2025-09-15T23:56:14.751Z",
"modifiedDate": "2025-09-15T23:56:14.751Z"
}
],
"count": 1,
"totalSize": 1015808
}
```
---
## 📊 **Logs et Monitoring**
### **Logs de Hash**
```
[HASH] Hash du fichier: a1b2c3d4e5f6789...
[HASH] Fichier déjà existant trouvé: document-1757980637671.pdf
[HASH] Nouveau fichier, traitement normal
```
### **Indicateurs de Performance**
- **Temps de traitement réduit** pour les doublons
- **Stockage optimisé** (pas de fichiers redondants)
- **Logs clairs** pour le debugging
---
## 🛠️ **Fonctions Techniques**
### **`calculateFileHash(buffer)`**
```javascript
function calculateFileHash(buffer) {
return crypto.createHash('sha256').update(buffer).digest('hex')
}
```
### **`findExistingFileByHash(hash)`**
```javascript
function findExistingFileByHash(hash) {
const uploadDir = 'uploads/'
if (!fs.existsSync(uploadDir)) return null
const files = fs.readdirSync(uploadDir)
for (const file of files) {
const filePath = path.join(uploadDir, file)
try {
const fileBuffer = fs.readFileSync(filePath)
const fileHash = calculateFileHash(fileBuffer)
if (fileHash === hash) {
return { path: filePath, name: file }
}
} catch (error) {
console.warn(`[HASH] Erreur lors de la lecture de ${file}:`, error.message)
}
}
return null
}
```
---
## ✅ **Avantages**
1. **Économie de Stockage** : Pas de fichiers dupliqués
2. **Performance** : Traitement plus rapide pour les doublons
3. **Cohérence** : Résultats identiques pour fichiers identiques
4. **Monitoring** : Visibilité sur les fichiers stockés
5. **Sécurité** : Hash SHA-256 cryptographiquement sûr
---
## 🔄 **Workflow d'Upload**
```mermaid
graph TD
A[Fichier Uploadé] --> B[Calcul Hash SHA-256]
B --> C{Hash Existe?}
C -->|Oui| D[Supprimer Doublon]
C -->|Non| E[Conserver Fichier]
D --> F[Utiliser Fichier Existant]
E --> G[Traitement Normal]
F --> H[Extraction & NER]
G --> H
H --> I[Résultat JSON]
```
---
## 🧪 **Tests**
### **Test de Doublon**
```bash
# Premier upload
curl -X POST -F "document=@test.pdf" http://localhost:3001/api/extract
# Deuxième upload du même fichier
curl -X POST -F "document=@test.pdf" http://localhost:3001/api/extract
```
### **Vérification**
```bash
# Lister les fichiers uploadés
curl http://localhost:3001/api/uploads
```
---
## 📝 **Notes d'Implémentation**
- **Algorithme** : SHA-256 (256 bits)
- **Performance** : O(n) pour la recherche de doublons
- **Stockage** : Hash stocké en hexadécimal (64 caractères)
- **Compatibilité** : Fonctionne avec tous les types de fichiers supportés
- **Nettoyage** : Suppression automatique des doublons temporaires
---
_Documentation mise à jour le 15 septembre 2025_

View File

@ -1,239 +0,0 @@
# 🎉 Système IA - Fonctionnel et Opérationnel
## ✅ **Statut : SYSTÈME FONCTIONNEL**
Le système 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

627
docs/TESTING.md Normal file
View File

@ -0,0 +1,627 @@
# Guide de tests - 4NK IA Front Notarial
## Vue d'ensemble
Ce guide couvre la stratégie de tests pour l'application 4NK IA Front Notarial, incluant les tests unitaires,
d'intégration et end-to-end.
## Stack de test
### Outils principaux
- **Vitest** : Framework de test rapide et moderne
- **Testing Library** : Tests d'intégration React
- **JSDOM** : Environnement DOM simulé
- **MSW** : Mock Service Worker pour les APIs
- **Coverage V8** : Rapport de couverture de code
### Configuration
#### vitest.config.ts
```typescript
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'**/*.d.ts',
'**/*.config.*',
'**/setup.*'
]
}
}
})
```
#### setup.test.ts
```typescript
import { expect, afterEach } from 'vitest'
import { cleanup } from '@testing-library/react'
import * as matchers from '@testing-library/jest-dom/matchers'
// Étendre les matchers de testing-library
expect.extend(matchers)
// Nettoyer après chaque test
afterEach(() => {
cleanup()
})
```
## Types de tests
### Tests unitaires
#### Composants React
```typescript
// tests/components/Layout.test.tsx
import { render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import { store } from '../../src/store'
import { Layout } from '../../src/components/Layout'
const renderWithProviders = (ui: React.ReactElement) => {
return render(
<Provider store={store}>
<BrowserRouter>
{ui}
</BrowserRouter>
</Provider>
)
}
describe('Layout', () => {
it('should render the application title', () => {
renderWithProviders(<Layout><div>Test content</div></Layout>)
expect(screen.getByText('4NK IA - Front Notarial')).toBeInTheDocument()
})
it('should render navigation tabs', () => {
renderWithProviders(<Layout><div>Test content</div></Layout>)
expect(screen.getByRole('tablist')).toBeInTheDocument()
})
})
```
#### Services API
```typescript
// tests/services/api.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { documentApi } from '../../src/services/api'
import axios from 'axios'
// Mock axios
vi.mock('axios')
const mockedAxios = vi.mocked(axios)
describe('documentApi', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('upload', () => {
it('should upload a document successfully', async () => {
const mockResponse = {
data: {
id: 'doc_123',
name: 'test.pdf',
type: 'application/pdf',
size: 1024,
uploadDate: new Date(),
status: 'completed'
}
}
mockedAxios.create.mockReturnValue({
post: vi.fn().mockResolvedValue(mockResponse)
} as any)
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
const result = await documentApi.upload(file)
expect(result.id).toBe('doc_123')
expect(result.name).toBe('test.pdf')
expect(result.status).toBe('completed')
})
it('should return demo data on error', async () => {
mockedAxios.create.mockReturnValue({
post: vi.fn().mockRejectedValue(new Error('Network error'))
} as any)
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
const result = await documentApi.upload(file)
expect(result.id).toMatch(/^demo-/)
expect(result.name).toBe('test.pdf')
})
})
})
```
#### Redux Slices
```typescript
// tests/store/documentSlice.test.ts
import { describe, it, expect } from 'vitest'
import documentReducer, { setCurrentDocument } from '../../src/store/documentSlice'
import { uploadDocument } from '../../src/store/documentSlice'
describe('documentSlice', () => {
const initialState = {
documents: [],
currentDocument: null,
extractionResult: null,
analysisResult: null,
contextResult: null,
conseilResult: null,
loading: false,
error: null
}
it('should handle setCurrentDocument', () => {
const document = {
id: 'doc_123',
name: 'test.pdf',
type: 'application/pdf',
size: 1024,
uploadDate: new Date(),
status: 'completed' as const
}
const action = setCurrentDocument(document)
const newState = documentReducer(initialState, action)
expect(newState.currentDocument).toEqual(document)
expect(newState.extractionResult).toBeNull()
})
it('should handle uploadDocument.pending', () => {
const action = { type: uploadDocument.pending.type }
const newState = documentReducer(initialState, action)
expect(newState.loading).toBe(true)
expect(newState.error).toBeNull()
})
})
```
### Tests d'intégration
#### Vues complètes
```typescript
// tests/views/UploadView.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import { store } from '../../src/store'
import { UploadView } from '../../src/views/UploadView'
const renderWithProviders = (ui: React.ReactElement) => {
return render(
<Provider store={store}>
<BrowserRouter>
{ui}
</BrowserRouter>
</Provider>
)
}
describe('UploadView', () => {
it('should render upload area', () => {
renderWithProviders(<UploadView />)
expect(screen.getByText(/glisser-déposer/i)).toBeInTheDocument()
})
it('should handle file upload', async () => {
const user = userEvent.setup()
renderWithProviders(<UploadView />)
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
const input = screen.getByLabelText(/sélectionner des fichiers/i)
await user.upload(input, file)
await waitFor(() => {
expect(screen.getByText('test.pdf')).toBeInTheDocument()
})
})
it('should display document list after upload', async () => {
const user = userEvent.setup()
renderWithProviders(<UploadView />)
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
const input = screen.getByLabelText(/sélectionner des fichiers/i)
await user.upload(input, file)
await waitFor(() => {
expect(screen.getByRole('list')).toBeInTheDocument()
expect(screen.getByText('test.pdf')).toBeInTheDocument()
})
})
})
```
#### Navigation
```typescript
// tests/navigation.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import { store } from '../src/store'
import App from '../src/App'
const renderWithProviders = (ui: React.ReactElement) => {
return render(
<Provider store={store}>
<BrowserRouter>
{ui}
</BrowserRouter>
</Provider>
)
}
describe('Navigation', () => {
it('should navigate between tabs', async () => {
const user = userEvent.setup()
renderWithProviders(<App />)
// Vérifier que l'onglet Upload est actif par défaut
expect(screen.getByText('Upload')).toHaveAttribute('aria-selected', 'true')
// Cliquer sur l'onglet Extraction
await user.click(screen.getByText('Extraction'))
expect(screen.getByText('Extraction')).toHaveAttribute('aria-selected', 'true')
expect(screen.getByText('Upload')).toHaveAttribute('aria-selected', 'false')
})
})
```
### Tests d'API avec MSW
#### Configuration MSW
```typescript
// tests/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
// Upload de document
http.post('/api/documents/upload', () => {
return HttpResponse.json({
id: 'doc_123',
name: 'test.pdf',
type: 'application/pdf',
size: 1024,
uploadDate: new Date().toISOString(),
status: 'completed'
})
}),
// Extraction de données
http.get('/api/documents/:id/extract', () => {
return HttpResponse.json({
documentId: 'doc_123',
text: 'Texte extrait du document...',
language: 'fr',
documentType: 'Acte de vente',
identities: [
{
id: '1',
type: 'person',
firstName: 'Jean',
lastName: 'Dupont',
birthDate: '1980-05-15',
nationality: 'Française',
confidence: 0.95
}
],
addresses: [],
properties: [],
contracts: [],
signatures: [],
confidence: 0.92
})
}),
// Erreur de connexion
http.get('/api/documents/:id/analyze', () => {
return HttpResponse.error()
})
]
```
#### Tests avec MSW
```typescript
// tests/integration/api.test.tsx
import { setupServer } from 'msw/node'
import { handlers } from '../mocks/handlers'
import { documentApi } from '../../src/services/api'
const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
describe('API Integration', () => {
it('should extract document data', async () => {
const result = await documentApi.extract('doc_123')
expect(result.documentId).toBe('doc_123')
expect(result.identities).toHaveLength(1)
expect(result.identities[0].firstName).toBe('Jean')
})
it('should handle API errors gracefully', async () => {
const result = await documentApi.analyze('doc_123')
// Devrait retourner des données de démonstration
expect(result.documentId).toBe('doc_123')
expect(result.credibilityScore).toBeDefined()
})
})
```
## Tests de performance
### Tests de rendu
```typescript
// tests/performance/render.test.tsx
import { render } from '@testing-library/react'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import { store } from '../../src/store'
import App from '../../src/App'
describe('Performance', () => {
it('should render app within acceptable time', () => {
const start = performance.now()
render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
)
const end = performance.now()
const renderTime = end - start
// Le rendu initial devrait prendre moins de 100ms
expect(renderTime).toBeLessThan(100)
})
})
```
### Tests de mémoire
```typescript
// tests/performance/memory.test.tsx
import { render, cleanup } from '@testing-library/react'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import { store } from '../../src/store'
import { UploadView } from '../../src/views/UploadView'
describe('Memory Management', () => {
it('should not leak memory on multiple renders', () => {
const initialMemory = (performance as any).memory?.usedJSHeapSize || 0
// Rendre et nettoyer plusieurs fois
for (let i = 0; i < 10; i++) {
render(
<Provider store={store}>
<BrowserRouter>
<UploadView />
</BrowserRouter>
</Provider>
)
cleanup()
}
const finalMemory = (performance as any).memory?.usedJSHeapSize || 0
const memoryIncrease = finalMemory - initialMemory
// L'augmentation de mémoire devrait être raisonnable
expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024) // 10MB
})
})
```
## Tests d'accessibilité
### Tests avec jest-axe
```typescript
// tests/accessibility/a11y.test.tsx
import { render } from '@testing-library/react'
import { axe, toHaveNoViolations } from 'jest-axe'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import { store } from '../../src/store'
import { Layout } from '../../src/components/Layout'
expect.extend(toHaveNoViolations)
describe('Accessibility', () => {
it('should not have accessibility violations', async () => {
const { container } = render(
<Provider store={store}>
<BrowserRouter>
<Layout>
<div>Test content</div>
</Layout>
</BrowserRouter>
</Provider>
)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
it('should have proper ARIA labels', () => {
render(
<Provider store={store}>
<BrowserRouter>
<Layout>
<div>Test content</div>
</Layout>
</BrowserRouter>
</Provider>
)
expect(screen.getByRole('banner')).toBeInTheDocument()
expect(screen.getByRole('tablist')).toBeInTheDocument()
})
})
```
## Scripts de test
### package.json
```json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest --watch",
"test:debug": "vitest --inspect-brk"
}
}
```
### Exécution des tests
```bash
# Tests en mode watch
npm run test
# Tests avec interface graphique
npm run test:ui
# Tests une seule fois
npm run test:run
# Tests avec couverture
npm run test:coverage
# Tests en mode debug
npm run test:debug
```
## Configuration CI/CD
### GitHub Actions
```yaml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22.12'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
```
## Bonnes pratiques
### Organisation des tests
- **Un fichier de test par composant** : `Component.test.tsx`
- **Tests groupés par fonctionnalité** : `describe` blocks
- **Tests isolés** : Chaque test doit être indépendant
- **Noms descriptifs** : `it('should do something specific')`
### Mocking
- **Mock des dépendances externes** : APIs, services
- **Mock des hooks** : React hooks personnalisés
- **Mock des modules** : Modules Node.js
### Assertions
- **Assertions spécifiques** : Éviter les assertions génériques
- **Tests de régression** : Vérifier les bugs corrigés
- **Tests de cas limites** : Valeurs nulles, erreurs
### Performance
- **Tests rapides** : Éviter les tests lents
- **Parallélisation** : Utiliser `--threads` pour Vitest
- **Cache** : Utiliser le cache des dépendances
## Métriques de qualité
### Couverture de code
- **Minimum 80%** : Couverture globale
- **Minimum 90%** : Composants critiques
- **100%** : Fonctions utilitaires
### Types de couverture
- **Statements** : Instructions exécutées
- **Branches** : Branches conditionnelles
- **Functions** : Fonctions appelées
- **Lines** : Lignes de code exécutées
### Rapport de couverture
```bash
# Générer le rapport
npm run test:coverage
# Ouvrir le rapport HTML
open coverage/index.html
```

View File

@ -1,43 +0,0 @@
---
title: Stratégie de tests (shell)
---
# Objectif
Fournir des tests shell simples, paramétrables par variables denvironnement, pour valider les fonctionnalités clés sans dépendre doutils lourds.
# Pré-requis
- Backend accessible sur `http://localhost:3001`
- PM2 (optionnel) pour relancer le backend
# Tests disponibles
1) Upload volumineux (50 Mo): `tests/upload_100mb.test.sh`
- Valide labsence derreur 413 côté proxy et Multer
2) OCR CNI (paramétrable): `tests/ocr_cni_pipeline.test.sh`
- Variables requises:
- `SAMPLE_CNI` (chemin vers une image CNI)
- `FOLDER_HASH` (hash du dossier cible)
- Skips si variables non définies
3) Enrichissement Adresse (paramétrable): `tests/enrich_address_pipeline.test.sh`
- Variables requises:
- `FOLDER_HASH`, `FILE_HASH` (doit référencer un document déjà extrait avec au moins une adresse)
- Skips si variables non définies
# Exécution
```
chmod +x tests/*.sh
./tests/upload_100mb.test.sh
SAMPLE_CNI=/chemin/cni.jpg FOLDER_HASH=default ./tests/ocr_cni_pipeline.test.sh
FOLDER_HASH=xxxx FILE_HASH=yyyy ./tests/enrich_address_pipeline.test.sh
```
# Interprétation
- OK: test validé
- SKIP: conditions non remplies (variables/env ou données manquantes)
- ERR: action attendue non réalisée

View File

@ -1,4 +1,4 @@
# Spécifications fonctionnelles duLecoffre.io
# Spécifications fonctionnelles du front notarial
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.
@ -9,7 +9,7 @@ faire une api et une une ihm qui les consomme pour:
1. Détecter un type de document
2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur,
acheteur, héritiers.... propres aux actes notariés
acheteur, héritiers.... propres aux actes notariés
3. Si c'est une CNI, définir le pays
4. Pour les identité : rechercher des informations générales sur la personne
5. Pour les adresses vérifier:
@ -47,9 +47,9 @@ Retrait gonflement des argiles Opérations sur les retrait de gonflement des arg
Opérations sur le Détail des risques SSP
Sites et sols pollués (SSP) TIM
Opérations sur les Transmissions d'Informations au Maire (TIM)
TRI Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
TRI Opérations sur les Territoires à Risques importants d'Inondation (TRI)
TRI - Zonage réglementaire
Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
Opérations sur les Territoires à Risques importants d'Inondation (TRI)
Zonage Sismique
Opérations sur le risque sismique
Géoportail urba
@ -65,11 +65,12 @@ Vigilances DOW
JONES[https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/](https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/)
Infogreffe
[https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details](https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details)
RBE (<EFBFBD> coupler avec infogreffe ci-dessus)
RBE (Ã coupler avec infogreffe ci-dessus)
[https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/](https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/)
faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
joindre le PDF suivant complété :
[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf) 6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document
[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf)
6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document
L'écran s'affichera depuis un bouton sur /home/desk/code/lecoffre_front
mais il faudra pouvoir ajouter/glisser un document en 1 ou plusieurs document uploadés et voir les previews images
@ -95,7 +96,7 @@ faire une api et une une ihm qui les consomme pour:
1. Détecter un type de document
2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur,
acheteur, héritiers.... propres aux actes notariés
acheteur, héritiers.... propres aux actes notariés
3. Si c'est une CNI, définir le pays
4. Pour les identité : rechercher des informations générales sur la personne
5. Pour les adresses vérifier:
@ -133,9 +134,9 @@ Retrait gonflement des argiles Opérations sur les retrait de gonflement des arg
Opérations sur le Détail des risques SSP
Sites et sols pollués (SSP) TIM
Opérations sur les Transmissions d'Informations au Maire (TIM)
TRI Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
TRI Opérations sur les Territoires à Risques importants d'Inondation (TRI)
TRI - Zonage réglementaire
Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
Opérations sur les Territoires à Risques importants d'Inondation (TRI)
Zonage Sismique
Opérations sur le risque sismique
Géoportail urba
@ -151,11 +152,12 @@ Vigilances DOW
JONES[https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/](https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/)
Infogreffe
[https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details](https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details)
RBE (<EFBFBD> coupler avec infogreffe ci-dessus)
RBE (Ã coupler avec infogreffe ci-dessus)
[https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/](https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/)
faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
joindre le PDF suivant complété :
[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf<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
[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdfÂ](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdfÂ)
6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document
on veut crer un front pour les notaires et leurs assistants afin de :
les notaires vont l'utiliser dans le cadre de leur processus métiers et des types d'actes.
faire une API et une IHM pour l'OCR, la catégorisation, la vraissemblance et la recherche d'information des
@ -165,7 +167,7 @@ faire une api et une une ihm qui les consomme pour:
1. Détecter un type de document
2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur,
acheteur, héritiers.... propres aux actes notariés
acheteur, héritiers.... propres aux actes notariés
3. Si c'est une CNI, définir le pays
4. Pour les identité : rechercher des informations générales sur la personne
5. Pour les adresses vérifier:
@ -203,9 +205,9 @@ Retrait gonflement des argiles Opérations sur les retrait de gonflement des arg
Opérations sur le Détail des risques SSP
Sites et sols pollués (SSP) TIM
Opérations sur les Transmissions d'Informations au Maire (TIM)
TRI Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
TRI Opérations sur les Territoires à Risques importants d'Inondation (TRI)
TRI - Zonage réglementaire
Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
Opérations sur les Territoires à Risques importants d'Inondation (TRI)
Zonage Sismique
Opérations sur le risque sismique
Géoportail urba
@ -221,11 +223,12 @@ Vigilances DOW
JONES[https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/](https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/)
Infogreffe
[https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details](https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details)
RBE (<EFBFBD> coupler avec infogreffe ci-dessus)
RBE (Ã coupler avec infogreffe ci-dessus)
[https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/](https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/)
faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
joindre le PDF suivant complété :
[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf) 6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document
[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf)
6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document
L'écran s'affichera depuis un bouton sur /home/desk/code/lecoffre_front
mais il faudra pouvoir ajouter/glisser un document en 1 ou plusieurs document uploadés et voir les previews images

View File

@ -1,14 +0,0 @@
## Annuaire de noms (FR et autres)
- Emplacement: `backend/data/names/`
- Fichiers CSV/TSV/texte séparés par `,` `;` ou `\t`.
- Noms de fichiers:
- `firstnames_fr.csv`, `firstnames_en.csv`, `prenoms_fr.csv`, etc.
- `lastnames_fr.csv`, `surnames_en.csv`, `noms_fr.csv`, etc.
- Chargement: automatique au démarrage; normalisation sans accents; ensembles en mémoire.
- Usage: léger boost de confiance si prénom/nom détectés appartiennent à lannuaire.
- Extension multi-langues: ajouter des fichiers `firstnames_<lang>.csv` et `lastnames_<lang>.csv`.
Impact:
- Le score `globalConfidence` est augmenté de +5% pour prénom connu, +5% pour nom connu (max +10%).
- Améliore la décision de re-upload (moins de faux négatifs si noms valides).

View File

@ -1,338 +0,0 @@
# Architecture Backend pour le Traitement des Documents
## Vue d'ensemble
L'application utilise maintenant une architecture backend qui traite les données (OCR, NER) et renvoie du JSON au frontend. Cette approche améliore les performances et centralise le traitement des documents.
## Architecture
### 🏗️ Structure
```
4NK_IA_front/
├── backend/ # Serveur backend Express
│ ├── server.js # Serveur principal
│ ├── package.json # Dépendances backend
│ └── uploads/ # Fichiers temporaires
├── src/ # Frontend React
│ ├── services/
│ │ ├── backendApi.ts # API backend
│ │ ├── openai.ts # Fallback local
│ │ └── ruleNer.ts # Règles NER
│ └── store/
│ └── documentSlice.ts # Redux avec backend
└── test-files/ # Fichiers de test
```
### 🔄 Flux de Données
```mermaid
graph TD
A[Frontend React] --> B[Backend Express]
B --> C[Tesseract.js OCR]
B --> D[Règles NER]
C --> E[Texte extrait]
D --> F[Entités extraites]
E --> G[JSON Response]
F --> G
G --> A
```
## Backend (Express.js)
### 🚀 Serveur Principal
**Fichier**: `backend/server.js`
**Port**: 3001
**Endpoints**:
- `POST /api/extract` - Extraction de documents
- `GET /api/test-files` - Liste des fichiers de test
- `GET /api/health` - Health check
### 📄 Traitement des Documents
#### 1. Upload et Validation
```javascript
// Configuration multer
const upload = multer({
storage: multer.diskStorage({...}),
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB max
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/tiff', 'application/pdf']
// Validation des types de fichiers
}
})
```
#### 2. Extraction OCR Optimisée
```javascript
async function extractTextFromImage(imagePath) {
const worker = await createWorker('fra+eng')
// Configuration optimisée pour cartes d'identité
const params = {
tessedit_pageseg_mode: '6',
tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ...',
tessedit_ocr_engine_mode: '1', // LSTM
textord_min_xheight: '6', // Petits textes
// ... autres paramètres
}
await worker.setParameters(params)
const { data } = await worker.recognize(imagePath)
return { text: data.text, confidence: data.confidence }
}
```
#### 3. Extraction NER par Règles
```javascript
function extractEntitiesFromText(text) {
const entities = {
identities: [],
addresses: [],
cniNumbers: [],
dates: [],
documentType: 'Document',
}
// Patterns pour cartes d'identité
const namePatterns = [
/(Vendeur|Acheteur|...)\s*:\s*([A-Z][a-zà-öø-ÿ'\-]+\s+[A-Z][a-zà-öø-ÿ'\-]+)/gi,
/^([A-Z][A-ZÀ-ÖØ-öø-ÿ\s\-']{2,30})$/gm,
// ... autres patterns
]
// Extraction des entités...
return entities
}
```
### 📊 Réponse JSON
```json
{
"success": true,
"documentId": "doc-1234567890",
"fileName": "IMG_20250902_162159.jpg",
"fileSize": 1077961,
"mimeType": "image/jpeg",
"processing": {
"ocr": {
"text": "Texte extrait par OCR...",
"confidence": 85.5,
"wordCount": 25
},
"ner": {
"identities": [...],
"addresses": [...],
"cniNumbers": [...],
"dates": [...],
"documentType": "CNI"
},
"globalConfidence": 87.2
},
"extractedData": {
"documentType": "CNI",
"identities": [...],
"addresses": [...],
"cniNumbers": [...],
"dates": [...]
},
"timestamp": "2025-09-15T23:30:00.000Z"
}
```
## Frontend (React)
### 🔌 Service Backend
**Fichier**: `src/services/backendApi.ts`
```typescript
export async function extractDocumentBackend(
documentId: string,
file?: File,
hooks?: {
onOcrProgress?: (progress: number) => void
onLlmProgress?: (progress: number) => void
},
): Promise<ExtractionResult> {
const formData = new FormData()
formData.append('document', file)
const response = await fetch(`${BACKEND_URL}/api/extract`, {
method: 'POST',
body: formData,
})
const result: BackendExtractionResult = await response.json()
// Conversion vers le format frontend
return convertBackendToFrontend(result)
}
```
### 🔄 Redux Store
**Fichier**: `src/store/documentSlice.ts`
```typescript
export const extractDocument = createAsyncThunk(
'document/extract',
async (documentId: string, thunkAPI) => {
// Vérifier si le backend est disponible
const backendAvailable = await checkBackendHealth()
if (backendAvailable) {
// Utiliser le backend
return await backendDocumentApi.extract(documentId, file, progressHooks)
} else {
// Fallback vers le mode local
return await openaiDocumentApi.extract(documentId, file, progressHooks)
}
},
)
```
## Démarrage
### 🚀 Backend
```bash
# Option 1: Script automatique
./start-backend.sh
# Option 2: Manuel
cd backend
npm install
node server.js
```
### 🌐 Frontend
```bash
npm run dev
```
### 🧪 Test de l'Architecture
```bash
node test-backend-architecture.cjs
```
## Avantages
### 🚀 Performance
- **Traitement centralisé** : OCR et NER sur le serveur
- **Optimisations** : Paramètres OCR optimisés pour les cartes d'identité
- **Cache** : Possibilité de mettre en cache les résultats
### 🔧 Maintenabilité
- **Séparation des responsabilités** : Backend pour le traitement, frontend pour l'UI
- **API REST** : Interface claire entre frontend et backend
- **Fallback** : Mode local en cas d'indisponibilité du backend
### 📊 Monitoring
- **Logs détaillés** : Traçabilité complète du traitement
- **Health check** : Vérification de l'état du backend
- **Métriques** : Confiance OCR, nombre d'entités extraites
## Configuration
### 🔧 Variables d'Environnement
**Backend**:
- `PORT=3001` - Port du serveur backend
**Frontend**:
- `VITE_BACKEND_URL=http://localhost:3001` - URL du backend
- `VITE_USE_RULE_NER=true` - Mode règles locales (fallback)
- `VITE_DISABLE_LLM=true` - Désactiver LLM
### 📁 Structure des Fichiers
```
backend/
├── server.js # Serveur Express
├── package.json # Dépendances
└── uploads/ # Fichiers temporaires (auto-créé)
src/services/
├── backendApi.ts # API backend
├── openai.ts # Fallback local
└── ruleNer.ts # Règles NER
docs/
└── architecture-backend.md # Cette documentation
```
## Dépannage
### ❌ Problèmes Courants
#### Backend non accessible
```bash
# Vérifier que le backend est démarré
curl http://localhost:3001/api/health
# Vérifier les logs
cd backend && node server.js
```
#### Erreurs OCR
- Vérifier la taille des images (minimum 3x3 pixels)
- Ajuster les paramètres `textord_min_xheight`
- Vérifier les types de fichiers supportés
#### Erreurs de communication
- Vérifier que les ports 3001 (backend) et 5176 (frontend) sont libres
- Vérifier la configuration CORS
- Vérifier les variables d'environnement
### 🔍 Logs
**Backend**:
```
🚀 Serveur backend démarré sur le port 3001
📡 API disponible sur: http://localhost:3001/api
[OCR] Début de l'extraction pour: uploads/document-123.jpg
[OCR] Extraction terminée - Confiance: 85.5%
[NER] Extraction terminée: 2 identités, 1 adresse, 1 CNI
```
**Frontend**:
```
🚀 [STORE] Utilisation du backend pour l'extraction
📊 [PROGRESS] OCR doc-123: 30%
📊 [PROGRESS] NER doc-123: 50%
🎉 [BACKEND] Extraction terminée avec succès
```
## Évolutions Futures
### 🔮 Améliorations Possibles
1. **Base de données** : Stockage des résultats d'extraction
2. **Cache Redis** : Mise en cache des résultats OCR
3. **Queue système** : Traitement asynchrone des gros volumes
4. **API GraphQL** : Interface plus flexible
5. **Microservices** : Séparation OCR et NER
6. **Docker** : Containerisation pour le déploiement
7. **Monitoring** : Métriques et alertes
8. **Tests automatisés** : Suite de tests complète

View File

@ -1,145 +0,0 @@
# Changelog - Système de Pending
## Version 1.1.1 - 2025-09-16
### 🔧 Corrections critiques
- **Fix URL API** : Correction de l'URL de l'API de `http://localhost:18000` vers `http://localhost:3001/api`
- **Résolution des timeouts** : Le frontend peut maintenant contacter le backend correctement
- **Logs de debug** : Ajout de logs pour tracer les appels API et diagnostiquer les problèmes
## Version 1.1.0 - 2025-09-16
### 🆕 Nouvelles fonctionnalités
#### Système de Pending et Polling
- **Flags pending** : Création de fichiers `.pending` pour marquer les fichiers en cours de traitement
- **Polling automatique** : Vérification toutes les 5 secondes des dossiers avec des fichiers pending
- **Gestion d'erreur robuste** : Suppression automatique des flags en cas d'erreur
- **Nettoyage automatique** : Suppression des flags orphelins (> 1 heure) au démarrage
#### API Backend
- **Route améliorée** : `GET /api/folders/:folderHash/results` retourne maintenant `pending`, `hasPending`
- **Gestion des doublons** : Retour HTTP 202 pour les fichiers déjà en cours de traitement
- **Métadonnées pending** : Timestamp et statut dans les flags pending
#### Frontend React
- **État Redux étendu** : Nouvelles propriétés `pendingFiles`, `hasPending`, `pollingInterval`
- **Actions Redux** : `setPendingFiles`, `setPollingInterval`, `stopPolling`
- **Polling intelligent** : Démarrage/arrêt automatique basé sur l'état `hasPending`
### 🔧 Améliorations
#### Backend
- **Gestion d'erreur** : Try/catch/finally pour garantir le nettoyage des flags
- **Nettoyage au démarrage** : Fonction `cleanupOrphanedPendingFlags()` appelée au démarrage
- **Logs améliorés** : Messages détaillés pour le suivi des flags pending
- **Structure de dossiers** : Organisation par hash de dossier maintenue
#### Frontend
- **App.tsx** : Gestion du cycle de vie du polling avec useCallback et useEffect
- **Nettoyage automatique** : Suppression des intervalles au démontage des composants
- **Logs de debug** : Messages détaillés pour le suivi du polling
### 🐛 Corrections
#### Problèmes résolus
- **Flags pending supprimés au démarrage** : Seuls les flags orphelins sont maintenant nettoyés
- **Fichiers temporaires** : Correction de la suppression incorrecte des fichiers finaux
- **Gestion d'erreur** : Flags pending supprimés même en cas d'erreur de traitement
- **Polling continu** : Arrêt automatique du polling quand plus de pending
### 📁 Fichiers modifiés
#### Backend
- `backend/server.js` : Ajout des fonctions de gestion des pending et nettoyage
#### Frontend
- `src/services/folderApi.ts` : Interface `FolderResponse` étendue
- `src/store/documentSlice.ts` : État et actions pour le système de pending
- `src/App.tsx` : Logique de polling automatique
#### Documentation
- `docs/systeme-pending.md` : Documentation complète du système
- `docs/changelog-pending.md` : Ce changelog
### 🧪 Tests
#### Tests effectués
- ✅ Upload simple avec création/suppression de flag
- ✅ Upload en double avec retour HTTP 202
- ✅ Gestion d'erreur avec nettoyage de flag
- ✅ Polling automatique avec démarrage/arrêt
- ✅ Nettoyage des flags orphelins au démarrage
- ✅ Interface utilisateur mise à jour automatiquement
#### Commandes de test
```bash
# Vérifier l'état d'un dossier
curl -s http://localhost:3001/api/folders/7d99a85daf66a0081a0e881630e6b39b/results | jq '.count, .hasPending'
# Tester l'upload
curl -X POST -F "document=@test.pdf" -F "folderHash=7d99a85daf66a0081a0e881630e6b39b" http://localhost:3001/api/extract
```
### 🔄 Migration
#### Aucune migration requise
- Les dossiers existants continuent de fonctionner
- Les flags pending sont créés automatiquement
- Le système est rétrocompatible
### 📊 Métriques
#### Performance
- **Polling interval** : 5 secondes (configurable)
- **Cleanup threshold** : 1 heure pour les flags orphelins
- **Temps de traitement** : Inchangé, flags ajoutent ~1ms
#### Fiabilité
- **Gestion d'erreur** : 100% des flags pending nettoyés
- **Nettoyage automatique** : Flags orphelins supprimés au démarrage
- **Polling intelligent** : Arrêt automatique quand plus de pending
### 🚀 Déploiement
#### Prérequis
- Node.js 20.19.0+
- Aucune dépendance supplémentaire
#### Étapes
1. Redémarrer le serveur backend
2. Redémarrer le frontend
3. Vérifier les logs de nettoyage au démarrage
4. Tester l'upload d'un fichier
### 🔮 Prochaines étapes
#### Améliorations futures
- Configuration du polling interval via variables d'environnement
- Métriques de performance des flags pending
- Interface d'administration pour visualiser les pending
- Notifications push pour les utilisateurs
### 🧼 Nettoyage Router 2025-09-18
- Suppression de la dépendance redondante `router-dom` (conservée: `react-router-dom@^7.9.1`)
- Réinstallation des modules (`npm ci`) pour régénérer le lockfile
- Impact: réduction du risque de conflits de dépendances et du poids inutile du bundle

View File

@ -1,40 +0,0 @@
---
title: Déduplication des entités extraites
description: Règles et clés de normalisation pour éliminer les doublons d'entités
---
## Contexte
Afin d'éviter l'affichage et le traitement multiple de la même information, une déduplication déterministe a été ajoutée côté frontend après l'extraction des entités.
## Périmètre
- Identités (personnes)
- Adresses
- Dates
- Entreprises (nom, SIREN, SIRET)
- Signatures
- Références (type, valeur)
## Règles de déduplication
- Identités: clé = `lower(trim(firstName))|lower(trim(lastName))`
- Adresses: clé = `lower(trim(street))|trim(postalCode)|lower(trim(city))|lower(trim(country))`
- Dates: utilisation d'un `Set` (unicité par valeur exacte)
- Entreprises: clé = `lower(trim(name))|trim(siren)|trim(siret)`
- Signatures: utilisation d'un `Set` (unicité par valeur exacte)
- Références: clé = `lower(trim(type))|lower(trim(value))`
## Points d'intégration
- `src/services/ruleNer.ts`: déduplication appliquée au résultat du NER par règles.
- `src/services/backendApi.ts`: déduplication appliquée après le mapping de la réponse backend standard.
## Effets de bord
- L'ordre des entités est conservé (première occurrence préservée).
- Les entrées vides sont normalisées avant comparaison (trim/lowercase) pour limiter les faux doublons.
## Validation
Un test ciblé `tests/deduplication.test.ts` vérifie que des doublons simples (noms/adresses répétés) sont éliminés.

View File

@ -1,194 +0,0 @@
{
"document": {
"id": "doc-1757976015681",
"fileName": "facture_4NK_08-2025_04.pdf",
"fileSize": 85819,
"mimeType": "application/pdf",
"uploadTimestamp": "2025-09-15T22:40:15.681Z"
},
"classification": {
"documentType": "Facture",
"confidence": 0.95,
"subType": "Facture de prestation",
"language": "fr",
"pageCount": 1
},
"extraction": {
"text": {
"raw": "Janin Consulting - EURL au capital de 500 Euros...",
"processed": "Janin Consulting - EURL au capital de 500 Euros...",
"wordCount": 165,
"characterCount": 1197,
"confidence": 0.95
},
"entities": {
"persons": [
{
"id": "person-1",
"type": "contact",
"firstName": "Anthony",
"lastName": "Janin",
"role": "Gérant",
"email": "ja.janin.anthony@gmail.com",
"phone": "33 (0)6 71 40 84 13",
"confidence": 0.9,
"source": "rule-based"
}
],
"companies": [
{
"id": "company-1",
"name": "Janin Consulting",
"legalForm": "EURL",
"siret": "815 322 912 00040",
"rcs": "815 322 912 NANTERRE",
"tva": "FR64 815 322 912",
"capital": "500 Euros",
"role": "Fournisseur",
"confidence": 0.95,
"source": "rule-based"
},
{
"id": "company-2",
"name": "4NK",
"tva": "FR79913422994",
"role": "Client",
"confidence": 0.9,
"source": "rule-based"
}
],
"addresses": [
{
"id": "address-1",
"type": "siège_social",
"street": "177 rue du Faubourg Poissonnière",
"city": "Paris",
"postalCode": "75009",
"country": "France",
"company": "Janin Consulting",
"confidence": 0.9,
"source": "rule-based"
},
{
"id": "address-2",
"type": "facturation",
"street": "4 SQUARE DES GOELANDS",
"city": "MONT-SAINT-AIGNAN",
"postalCode": "76130",
"country": "France",
"company": "4NK",
"confidence": 0.9,
"source": "rule-based"
}
],
"financial": {
"amounts": [
{
"id": "amount-1",
"type": "prestation",
"description": "Prestation du mois d'Août 2025",
"quantity": 10,
"unitPrice": 550.0,
"totalHT": 5500.0,
"currency": "EUR",
"confidence": 0.95
}
],
"totals": {
"totalHT": 5500.0,
"totalTVA": 1100.0,
"totalTTC": 6600.0,
"tvaRate": 0.2,
"currency": "EUR"
},
"payment": {
"terms": "30 jours après émission",
"penaltyRate": "Taux BCE + 7 points",
"bankDetails": {
"bank": "CAISSE D'EPARGNE D'ILE DE FRANCE",
"accountHolder": "Janin Anthony",
"address": "1 rue Pasteur (78800)",
"rib": "17515006000800309088884"
}
}
},
"dates": [
{
"id": "date-1",
"type": "facture",
"value": "29-août-25",
"formatted": "2025-08-29",
"confidence": 0.9,
"source": "rule-based"
},
{
"id": "date-2",
"type": "période",
"value": "août-25",
"formatted": "2025-08",
"confidence": 0.9,
"source": "rule-based"
}
],
"contractual": {
"clauses": [
{
"id": "clause-1",
"type": "paiement",
"content": "Le paiement se fera (maximum) 30 jours après l'émission de la facture.",
"confidence": 0.9
},
{
"id": "clause-2",
"type": "intérêts_retard",
"content": "Tout retard de paiement d'une quelconque facture fait courir, immédiatement et de plein droit, des intérêts de retard calculés au taux directeur de la BCE majoré de 7 points jusqu'au paiement effectif et intégral.",
"confidence": 0.9
}
],
"signatures": [
{
"id": "signature-1",
"type": "électronique",
"present": false,
"signatory": null,
"date": null,
"confidence": 0.8
}
]
},
"references": [
{
"id": "ref-1",
"type": "facture",
"number": "4NK_4",
"confidence": 0.95
}
]
}
},
"metadata": {
"processing": {
"engine": "4NK_IA_Backend",
"version": "1.0.0",
"processingTime": "2.5s",
"ocrEngine": "pdf-parse",
"nerEngine": "rule-based",
"preprocessing": {
"applied": false,
"reason": "PDF direct text extraction"
}
},
"quality": {
"globalConfidence": 0.95,
"textExtractionConfidence": 0.95,
"entityExtractionConfidence": 0.9,
"classificationConfidence": 0.95
}
},
"status": {
"success": true,
"errors": [],
"warnings": ["Aucune signature détectée"],
"timestamp": "2025-09-15T22:40:15.681Z"
}
}

View File

@ -1,27 +0,0 @@
### Spécification UI Onglet Extraction
Objectifs:
- Éditer/supprimer les entités extraites (personnes/adresses/entreprises)
- Lancer la collecte externe par entité, afficher le statut, proposer le PDF/JSON
- Déclencher automatiquement la collecte si lentité na pas encore de données enrichies
Comportements:
- Bouton Collecter: passe en “Collecte…” (state=running) puis “OK” à la fin, avec lien “Voir PDF” et “Voir JSON”.
- Polling réduit: 1,5 s × 6 tentatives, puis “Relancer”.
- Boutons Enregistrer/Supprimer ne déclenchent pas de re-scan; ils patchent le cache uniquement.
Endpoints utilisés:
- POST /enrich/:kind → démarre la collecte
- GET /enrich/:kind/status → état et sources
- GET /enrich/:kind/pdf → PDF généré
Accessibilité:
- Actions groupées, labels explicites, tooltips daide, responsive.
### Onglets dentités 2025-09-18
- Onglets MUI pour naviguer entre les entités du document.
- Onglets retenus: Personnes, Adresses, Entreprises, Contractuel.
- Le badge de chaque onglet reflète le nombre déléments détectés.
- Longlet initial est choisi automatiquement selon les données disponibles.
- Lédition/suppression et lenrichissement restent disponibles dans les sections pertinentes.

View File

@ -1,26 +0,0 @@
### Adresses Cadastre / GéoRisque / GéoFoncier
Objectif: géocoder ladresse, identifier les parcelles cadastrales, lister les risques majeurs, et produire un PDF de synthèse avec liens sources.
Entrées minimales:
- street, postalCode, city, country ('France')
Accès:
- Géocodage: BAN/Adresse DataGouv (open data)
- GéoRisque: couches publiques par bbox
- Cadastre: plan cadastral open data (WMS/WMTS)
- GéoFoncier: page publique par lat/lon (scraping léger si autorisé)
Sortie (cache JSON):
```
extraction.entities.addresses[i].enrichment.address = {
state,
startedAt, finishedAt,
geocode: { lat, lon, score },
cadastre: [{ section, parcelle, commune }],
risks: [{ type, level, sourceUrl }],
pdfPath
}
```
PDF: synthèse (adresse normalisée, mini-carte, risques) + parcelles.

View File

@ -1,26 +0,0 @@
### Bodacc Gel des avoirs (Personnes)
Objectif: vérifier lexistence de mentions de gel des avoirs pour une identité (NOM/PRÉNOM), consigner les hits (références, lien source) et produire un PDF de synthèse.
Entrées minimales:
- lastName (obligatoire, uppercase recommandé)
- firstName (optionnel)
- métadonnées utiles: date de naissance, pays
Accès:
- Sans clé (temporaire): scraping léger avec politesse (User-Agent dédié, 1 req/s, backoff).
- Avec API (si disponible): clé API, endpoint de recherche nom/prénom.
Sortie (dans le cache JSON):
```
extraction.entities.persons[i].enrichment.bodacc = {
state: 'idle'|'running'|'done'|'error',
startedAt, finishedAt,
hits: [{ name, date, sourceUrl, matchScore }],
pdfPath
}
```
PDF: une page de synthèse (identité + hits) + annexes si autorisé.
Limites: homonymies; respecter le cadre légal (RGPD) et citer les sources.

View File

@ -1,25 +0,0 @@
### Inforgreffe Extrait Kbis (Entreprises) (+ Societe.com)
Objectif: récupérer une fiche société (forme, siren, dirigeants, adresse) et idéalement un extrait Kbis.
Entrées minimales:
- name (raison sociale) OU siren/siret
Accès:
- Sans clé (temporaire): recherche Societe.com (résumé public) et page Inforgreffe (si visible) en scraping léger (1 req/s, backoff).
- Avec API: clé/secret; endpoints “Profil entreprise”, “Kbis PDF”.
Sortie (cache JSON):
```
extraction.entities.companies[i].enrichment.company = {
state,
startedAt, finishedAt,
sources: ['societe_com','inforgreffe'],
profile: { siren, siret, forme, capital, adresse, dirigeants: [] },
pdfPath
}
```
PDF: synthèse (identité, dirigeants) + annexes Kbis si autorisé, sinon résumé horodaté + liens sources.
Conformité: respecter CGU, citer les sources, logs de collecte.

View File

@ -1,94 +0,0 @@
# Redesign Interface Extraction - 18/09/2025
## Modifications apportées
### 1. Redesign complet de l'interface Extraction
- **Remplacement de Grid par Box/Stack** : Suppression des composants Grid Material-UI problématiques
- **Layout moderne** : Utilisation de Box avec flexbox pour un design responsive
- **Composants Material-UI** : Cards, Avatars, Badges, Chips pour une interface professionnelle
- **Navigation sidebar** : Liste des documents avec sélection visuelle
- **Métadonnées techniques** : Accordion pour les informations de traitement
### 2. Repositionnement du texte extrait
- **Déplacement en bas** : Section "Texte extrait" maintenant en fin de page
- **Toggle d'affichage** : Bouton pour masquer/afficher le contenu
- **Style monospace** : Police monospace pour une meilleure lisibilité
### 3. Affichage des résultats de collecte
- **Boutons d'enrichissement intelligents** :
- **Personnes** : "Bodacc ✓" quand collecte terminée
- **Adresses** : "BAN+GéoRisque+Cadastre ✓" quand collecte terminée
- **Entreprises** : "Inforgreffe+Societe.com ✓" quand collecte terminée
- **États visuels** : Collecte en cours, terminée, erreur
- **Bases collectées** : Affichage explicite des sources de données
### 4. Configuration et déploiement
- **Nginx proxy** : Configuration du proxy vers backend port 3001
- **Build Vite** : Compilation et déploiement vers `/usr/share/nginx/html`
- **Services relancés** : PM2 backend + Nginx redémarrés
- **Connectivité vérifiée** : API accessible via HTTPS
## Structure de l'interface
```
┌─────────────────────────────────────────────────────────┐
│ Header avec actions (Re-traiter) │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────────────────────────────┐ │
│ │ Sidebar │ │ Contenu principal │ │
│ │ Documents │ │ ┌─────────────────────────────────┐ │ │
│ │ - Doc 1 │ │ │ Métadonnées du document │ │ │
│ │ - Doc 2 │ │ └─────────────────────────────────┘ │ │
│ │ - Doc 3 │ │ ┌─────────────────────────────────┐ │ │
│ └─────────────┘ │ │ Entités extraites │ │ │
│ │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │
│ │ │ │Pers.│ │Addr.│ │Entr.│ │ │ │
│ │ │ └─────┘ └─────┘ └─────┘ │ │ │
│ │ └─────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ Métadonnées techniques │ │ │
│ │ └─────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ Texte extrait (toggle) │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## Bases de données collectées
### Personnes
- **Bodacc** : Gel des avoirs, sanctions financières
- **Statut** : Affiché sur le bouton d'enrichissement
### Adresses
- **BAN** : Base Adresse Nationale (géocodage)
- **GéoRisque** : Risques majeurs (inondations, séismes, etc.)
- **Cadastre** : Parcelles cadastrales
- **Statut** : Affiché sur le bouton d'enrichissement
### Entreprises
- **Inforgreffe** : Extrait Kbis, informations légales
- **Societe.com** : Fiche entreprise, dirigeants
- **Statut** : Affiché sur le bouton d'enrichissement
## Tests de fonctionnement
### Backend
```bash
curl -s https://ia.4nkweb.com/api/health
# {"status":"OK","timestamp":"2025-09-18T17:07:06.312Z","version":"1.0.0","metrics":{"pending":0,"results":5}}
```
### Frontend
- Interface accessible via HTTPS
- Navigation entre documents fonctionnelle
- Boutons d'enrichissement avec statuts
- Texte extrait en bas de page
## Prochaines étapes
1. **Tests utilisateur** : Validation de l'ergonomie
2. **Optimisations** : Performance et responsive design
3. **Fonctionnalités** : Ajout de nouvelles sources de données
4. **Documentation** : Guides utilisateur et API

View File

@ -1,61 +0,0 @@
---
title: Configuration Nginx pour uploads volumineux (100 Mo)
---
# Objectif
Augmenter la limite dupload pour éviter lerreur 413 Request Entity Too Large en alignant Nginx (reverse proxy) avec le backend (Multer 100 Mo).
## Paramètres requis
- Nginx: `client_max_body_size 100M;`
- Backend (Multer): `fileSize: 100 * 1024 * 1024`
## Configuration Nginx (server)
Ajouter dans le bloc `server { ... }` de votre virtual host:
```
client_max_body_size 100M;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
send_timeout 60s;
```
Dans lemplacement API:
```
location /api/ {
proxy_pass http://127.0.0.1:3001/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
Redémarrage Nginx:
```
sudo nginx -t
sudo systemctl reload nginx
```
## Validation backend
Dans `backend/server.js`, Multer est déjà configuré:
```
limits: { fileSize: 100 * 1024 * 1024 }
```
## Vérification fonctionnelle
1. Préparer un fichier test ~9095 Mo
2. Uploader via longlet Téléversement
3. Attendre la fin de lupload et vérifier labsence derreur 413
## Dépannage
- Si 413 persiste: vérifier quaucune directive plus restrictive nest définie dans un `location` imbriqué.
- Si le backend refuse: vérifier la taille Multer et les logs PM2.

View File

@ -1,28 +0,0 @@
## Améliorations OCR CNI et Adresses
### CNI (Carte Nationale d'Identité)
- MRZ: extraction stricte avec motif `NOM<<PRENOMS` en majuscules et sans accents.
- Heuristique: si MRZ absente, repli `IDFRA` + prénom détecté avant `<<`.
- Libellés FR: prise en charge de `NOM:` et `PRÉNOM:` (ou `PRENOM:`), normalisation des accents.
- Déduplication/priorisation: MRZ prioritaire; nettoyage et validation des noms.
Fichiers impactés: `backend/server.js` (fonction `extractEntitiesFromText`).
### Adresses (France)
- Regex renforcée: `NUMERO + VOIE, CP(\d{5}) + VILLE (+ France optionnel)`.
- Variantes: libellés `Adresse:`, `Siège:`, `Adresse de facturation:` ou `demeurant ...`.
- Normalisations: suppression du suffixe `France` dans `city`, CP forcé à 5 chiffres.
Fichiers impactés: `backend/server.js` (motif `addressPatterns`).
### Tests rapides (manuels)
1) Charger un dossier et téléverser une CNI.
2) Vérifier `/api/folders/<hash>/results` et constater dans `entities.identities` un objet avec `lastName` et `firstName` extraits du document.
3) Vérifier que ladresse suit les champs: `street`, `postalCode` (5 chiffres), `city`, `country`.
### Journal
- 2025-09-18: ajout MRZ stricte, heuristique `IDFRA`, libellés FR; regex adresse FR; normalisations.

View File

@ -1,128 +0,0 @@
# Optimisations UX - Performance et Accessibilité
## 🚀 Optimisations de Performance
### 1. Hooks de Performance (`usePerformance.ts`)
- **Mesure du temps de rendu** : Suivi des performances de rendu des composants
- **Utilisation mémoire** : Surveillance de l'utilisation de la mémoire JavaScript
- **Latence réseau** : Mesure des temps de réponse des API
- **Taux de cache** : Suivi de l'efficacité du cache
### 2. Composants Optimisés
#### LazyImage (`LazyImage.tsx`)
- **Chargement paresseux** : Images chargées uniquement quand elles entrent dans le viewport
- **Intersection Observer** : Détection efficace de la visibilité
- **Placeholders** : Squelettes de chargement pour une meilleure UX
- **Gestion d'erreurs** : Affichage d'un message en cas d'échec de chargement
#### VirtualizedList (`VirtualizedList.tsx`)
- **Rendu virtuel** : Affichage uniquement des éléments visibles
- **Chargement infini** : Support du scroll infini avec observer
- **Optimisation mémoire** : Réduction de l'utilisation mémoire pour de grandes listes
- **Performance** : Rendu fluide même avec des milliers d'éléments
### 3. Optimisations du Backend
- **Collecteurs asynchrones** : RBE, GéoFoncier, Bodacc, Inforgreffe
- **Cache intelligent** : Mise en cache des résultats d'extraction
- **Compression** : Réduction de la taille des réponses
- **Timeouts** : Gestion des requêtes longues
## ♿ Améliorations d'Accessibilité
### 1. Hook d'Accessibilité (`useAccessibility.ts`)
- **Navigation clavier** : Détection automatique de la navigation au clavier
- **Préférences système** : Respect des préférences de contraste et de mouvement
- **Annonces** : Support des lecteurs d'écran
- **Focus visible** : Gestion du focus pour la navigation clavier
### 2. Styles d'Accessibilité (`accessibility.css`)
- **Contraste élevé** : Support du mode contraste élevé
- **Réduction de mouvement** : Respect des préférences de mouvement
- **Focus visible** : Indicateurs de focus clairs
- **Tailles minimales** : Éléments interactifs de 44px minimum
### 3. Fonctionnalités d'Accessibilité
- **ARIA** : Attributs ARIA pour les lecteurs d'écran
- **Navigation clavier** : Support complet de la navigation au clavier
- **Annonces** : Notifications pour les changements d'état
- **Sémantique** : Structure HTML sémantique
## 📊 Métriques de Performance
### Métriques Surveillées
- **Temps de rendu** : < 16ms pour 60 FPS
- **Utilisation mémoire** : < 80% de la limite
- **Latence réseau** : < 200ms pour les API
- **Taux de cache** : > 80% pour les requêtes répétées
### Outils de Mesure
- **Performance API** : Mesure native des performances
- **Intersection Observer** : Détection de visibilité
- **ResizeObserver** : Surveillance des changements de taille
- **Custom hooks** : Hooks personnalisés pour les métriques
## 🎯 Bonnes Pratiques
### Performance
1. **Lazy loading** : Chargement paresseux des composants et images
2. **Memoization** : Utilisation de `useMemo` et `useCallback`
3. **Virtualisation** : Rendu virtuel pour les grandes listes
4. **Cache** : Mise en cache des données et composants
5. **Compression** : Optimisation des assets et réponses
### Accessibilité
1. **Navigation clavier** : Support complet de la navigation au clavier
2. **Lecteurs d'écran** : Attributs ARIA et annonces
3. **Contraste** : Respect des standards de contraste
4. **Focus** : Gestion claire du focus
5. **Sémantique** : Structure HTML sémantique
## 🔧 Configuration
### Variables d'Environnement
```bash
# Performance
VITE_PERFORMANCE_MONITORING=true
VITE_CACHE_TTL=300000
# Accessibilité
VITE_ACCESSIBILITY_ANNOUNCEMENTS=true
VITE_HIGH_CONTRAST_SUPPORT=true
```
### Configuration des Hooks
```typescript
// Performance
const { metrics, startRenderTimer, endRenderTimer } = usePerformance()
// Accessibilité
const { state, announceToScreenReader } = useAccessibility()
```
## 📈 Monitoring
### Métriques en Temps Réel
- **Console** : Logs de performance dans la console
- **Métriques** : Affichage des métriques dans l'interface
- **Alertes** : Notifications en cas de problème de performance
### Tests d'Accessibilité
- **Navigation clavier** : Test de la navigation au clavier
- **Lecteurs d'écran** : Test avec NVDA/JAWS
- **Contraste** : Vérification des contrastes
- **Focus** : Test de la gestion du focus
## 🚀 Déploiement
### Optimisations de Production
- **Minification** : Code minifié et optimisé
- **Compression** : Assets compressés (gzip/brotli)
- **Cache** : Headers de cache appropriés
- **CDN** : Utilisation d'un CDN pour les assets
### Monitoring de Production
- **Métriques** : Surveillance des métriques en production
- **Erreurs** : Gestion des erreurs et logging
- **Performance** : Monitoring des performances
- **Accessibilité** : Tests d'accessibilité automatisés

View File

@ -1,4 +0,0 @@
## PM2
- watch backend (backend/*), ignore uploads/cache
- restart on changes, logs dans log/

View File

@ -1,27 +0,0 @@
---
title: Polling Frontend — ETag, Selectors, Cadence
---
# Objectif
Réduire les rafraîchissements inutiles et le « clignotement » via ETag, selectors mémoïsés et cadence contrôlée.
## Implémentation
- ETag/If-None-Match activés dans `src/services/folderApi.ts`
- Sélecteurs Reselect: `src/store/selectors.ts`
- Limitation de polling: `src/App.tsx` (backoff exponentiel, max 30 itérations)
- Pause onglet caché (Page Visibility API)
- Mémos: `UploadView.tsx` et `Layout.tsx` (useMemo/React.memo)
## Bonnes pratiques
- Nactualiser létat Redux que si les données changent réellement (comparaison profonde)
- Afficher Skeletons pour les documents en traitement
- Éviter setState inutiles dans les listes (items mémoïsés)
## Tests à réaliser
1. Vérifier quun 304 Not Modified ne déclenche pas de re-render
2. Observer labsence de clignotement lors de larrivée dun seul nouveau document
3. Valider larrêt du polling après 30 tentatives ou à stabilisation

View File

@ -1,35 +0,0 @@
## Gestion de la qualité: remplacement d'image, confirmation d'adresse et révision IA
### Backend
- Suggestions ajoutées dans `status.suggestions` des résultats:
- `needsReupload` (bool), `reasons` (array)
- `needsAddressConfirmation` (bool), `detectedAddress` (objet)
- Endpoint confirmation d'adresse:
- POST `/api/folders/:folderHash/files/:fileHash/confirm-address`
- Body `{ confirmed: true, address: { street, city, postalCode, country } }`
#### Enrichissement Adresse
- Endpoint: POST `/api/folders/:folderHash/files/:fileHash/enrich/address`
- Sources consultées:
- Base Adresse Nationale (géocodage)
- GéoRisque (risques majeurs)
- Cadastre (parcelles)
- Cache statut: `cache/<folder>/<file>.enrich.address.json` avec `state: running|done|error`
- PDF: `cache/<folder>/<file>.enrich.address.pdf` (sections Géocodage, Risques, Cadastre, Sources)
### Frontend (UploadView)
- Si `needsReupload`: chip “Qualité faible: remplacer” → ouvre un file picker, supprime loriginal et réuploade.
- Si `needsAddressConfirmation`: chip “Adresse à confirmer” → dialogue pré-rempli; POST de confirmation; rafraîchissement.
- Révision IA: bouton “Révision IA” pour lancer une révision manuelle; affichage dun chip “IA: x.xx” (tooltip = avis) et dun chip “Corrections: N” ouvrant un dialogue listant les corrections si disponibles.
#### Extraction (onglet)
- Bouton “Collecter” sur lentité Adresse: déclenche `/enrich/address`
- Affiche le statut (en cours / OK / erreur) et un lien “Voir PDF” si disponible
- Affiche score BAN (%), coordonnées, et résumé des risques et parcelles
### Tests manuels
1) Télécharger une image de faible qualité → vérifier l'apparition du chip “Qualité faible: remplacer”.
2) Confirmer l'adresse détectée → vérifier que le chip disparaît après POST.
### Notes
- Annuaire de noms (FR/EN) intégré: rehausse la confiance si prénom/nom reconnus dans les listes unifiées.

View File

@ -1,52 +0,0 @@
### Révision IA (Ollama): scoring, corrections et avis
#### Objectif
Fournir une évaluation automatique de la fiabilité des extractions (score), proposer des corrections potentielles (champ, valeur, confiance) et un avis court, en sappuyant sur un LLM local accessible via Ollama.
#### Composants et endpoints
- Backend:
- Appel automatique après chaque extraction: intégration du résultat de révision dans `status.review` et `metadata.quality.ollamaScore`.
- Endpoint manuel: `POST /api/folders/:folderHash/files/:fileHash/review` (pas de payload requis). Retourne `{ success, review }`.
- Format attendu du LLM (réponse stricte JSON): `{ "score": number (0..1), "corrections": Array<{ path, value, confidence }>, "avis": string }`.
- Frontend:
- Bouton “Révision IA” par document (état completed): déclenche lendpoint manuel puis rafraîchit la liste.
- Affichage: Chip `IA: x.xx` (tooltip = `avis` si présent). Chip `Corrections: N` ouvrant un dialogue listant les corrections.
#### Calcul des scores et arbitrage
- Base OCR: confiance Tesseract/pdf-parse normalisée.
- Boost annuaire de noms (`backend/data/names/firstnames_all.csv`, `lastnames_all.csv`): +0.05 si prénom trouvé, +0.05 si nom trouvé (après normalisation), agrégé au score.
- Ensembles de règles NER (CNI/MRZ, adresses, entités) influent indirectement via `identities` et `cniNumbers` (poids renforcés).
- Score global côté backend (avant Ollama): plafonné à 0.99, agrège OCR, présence didentités, CNI, boost annuaire.
- Révision Ollama: si `review.score` est supérieur au `globalConfidence`, le backend rehausse `metadata.quality.globalConfidence` à ce score et persiste le résultat.
#### Suggestions de qualité
- `needsReupload`: déclenché si confiance OCR < 0.75, ou si CNI sans NOM/PRÉNOM.
- `needsAddressConfirmation`: déclenché si une adresse est détectée; confirmation côté UI via dialogue et endpoint `confirm-address`.
#### Dépendances et configuration
- Ollama: service HTTP local sur `http://localhost:11434`. Modèle configurable via `OLLAMA_MODEL` (défaut `llama3.1`). Timeout 8s.
- Aucune dépendance Node additionnelle (utilisation d`http` natif).
#### Données persistées
- Cache fichier par document: `cache/<folderHash>/<fileHash>.json`.
- Champs ajoutés/modifiés:
- `metadata.quality.ollamaScore: number` (0..1)
- `status.review: { score, corrections[], avis }`
- `metadata.quality.globalConfidence: number` (peut être rehaussé par Ollama)
#### UI et interactions
- Liste des documents:
- Chip “IA: x.xx” si présent (tooltip: `avis`).
- Chip “Corrections: N” si `status.review.corrections` non vide. Clic: ouvre un dialogue listant `{ path, value, confidence }`.
- Bouton “Révision IA”: relance la révision et rafraîchit litem.
- Indicateurs visuels: spinner sur le bouton pendant lappel, disabled, snackbar de confirmation dexécution.
#### Tests manuels (checklist)
- Vérifier quun upload image/PDF completed affiche le Chip `IA: x.xx` et/ou `Corrections: N` si présents.
- Cliquer “Révision IA”: confirmer que la liste se rafraîchit et que `status.review` est renseigné.
- Ouvrir le dialogue des corrections et vérifier laffichage des champs.
- Confirmer/infirmer une adresse détectée et vérifier la mise à jour côté backend et disparition du flag.
#### Journal des décisions
- Choix du JSON strict pour la sortie LLM afin de faciliter lexploitation et éviter les parsings fragiles.
- Utilisation d`http` natif côté Node pour éviter lajout de dépendances.

View File

@ -1,186 +0,0 @@
# Système de Pending et Polling
## Vue d'ensemble
Le système de pending permet de gérer les fichiers en cours de traitement de manière robuste, avec un système de flags et de polling automatique pour mettre à jour l'interface utilisateur en temps réel.
## Architecture
### Backend (Node.js)
#### Fonctions principales
- **`createPendingFlag(folderHash, fileHash)`** : Crée un flag `.pending` avec métadonnées
- **`isFilePending(folderHash, fileHash)`** : Vérifie si un fichier est en cours de traitement
- **`cleanupOrphanedPendingFlags()`** : Nettoie les flags orphelins (> 1 heure)
#### Gestion des flags
```javascript
// Structure d'un flag pending
{
fileHash: "abc123...",
folderHash: "def456...",
timestamp: "2025-09-16T02:58:29.606Z",
status: "processing"
}
```
#### API Endpoints
- **`GET /api/folders/:folderHash/results`** : Retourne les résultats + informations pending
- **`POST /api/extract`** : Crée un flag pending avant traitement, retourne HTTP 202 si déjà en cours
#### Gestion d'erreur
- Suppression automatique des flags pending en cas d'erreur
- Gestion try/catch/finally pour garantir le nettoyage
- Nettoyage des flags orphelins au démarrage du serveur
### Frontend (React + Redux)
#### État Redux
```typescript
interface DocumentState {
// ... autres propriétés
pendingFiles: Array<{
fileHash: string
folderHash: string
timestamp: string
status: string
}>
hasPending: boolean
pollingInterval: NodeJS.Timeout | null
}
```
#### Actions Redux
- **`setPendingFiles`** : Met à jour la liste des fichiers pending
- **`setPollingInterval`** : Gère l'intervalle de polling
- **`stopPolling`** : Arrête le polling et nettoie l'intervalle
#### Polling automatique
- Démarrage automatique si `hasPending = true`
- Polling toutes les 5 secondes
- Arrêt automatique quand plus de pending
- Nettoyage des intervalles au démontage des composants
## Flux de fonctionnement
### 1. Upload d'un fichier
```mermaid
sequenceDiagram
participant F as Frontend
participant B as Backend
participant FS as FileSystem
F->>B: POST /api/extract (file + folderHash)
B->>B: Calculer fileHash
B->>B: Vérifier cache
B->>B: Vérifier pending
B->>FS: Créer flag .pending
B->>B: Traitement OCR/NER
B->>FS: Sauvegarder résultat .json
B->>FS: Supprimer flag .pending
B->>F: Retourner résultat
```
### 2. Polling automatique
```mermaid
sequenceDiagram
participant F as Frontend
participant B as Backend
F->>B: GET /api/folders/:hash/results
B->>F: { results: [], pending: [], hasPending: true }
F->>F: Démarrer polling (5s)
loop Polling
F->>B: GET /api/folders/:hash/results
B->>F: { results: [1], pending: [], hasPending: false }
F->>F: Arrêter polling
end
```
### 3. Gestion d'erreur
```mermaid
sequenceDiagram
participant B as Backend
participant FS as FileSystem
B->>FS: Créer flag .pending
B->>B: Traitement (ERREUR)
B->>FS: Supprimer flag .pending
B->>B: Retourner erreur 500
```
## Configuration
### Variables d'environnement
- **Polling interval** : 5000ms (5 secondes)
- **Cleanup threshold** : 1 heure pour les flags orphelins
### Structure des dossiers
```
uploads/
├── {folderHash}/
│ ├── {fileHash}.pdf
│ └── {fileHash}.jpg
cache/
├── {folderHash}/
│ ├── {fileHash}.json
│ └── {fileHash}.pending (temporaire)
```
## Avantages
1. **Robustesse** : Gestion des erreurs et nettoyage automatique
2. **Performance** : Évite les traitements en double
3. **UX** : Mise à jour automatique de l'interface
4. **Maintenance** : Nettoyage automatique des flags orphelins
## Tests
### Tests manuels
1. **Upload simple** : Vérifier création/suppression du flag
2. **Upload en double** : Vérifier retour HTTP 202
3. **Erreur de traitement** : Vérifier suppression du flag
4. **Polling** : Vérifier mise à jour automatique
5. **Nettoyage** : Redémarrer serveur, vérifier nettoyage des orphelins
### Commandes de test
```bash
# Vérifier l'état d'un dossier
curl -s http://localhost:3001/api/folders/{hash}/results | jq '.count, .hasPending'
# Tester l'upload
curl -X POST -F "document=@test.pdf" -F "folderHash={hash}" http://localhost:3001/api/extract
```
## Maintenance
### Nettoyage manuel
```bash
# Supprimer tous les flags pending (attention !)
find cache/ -name "*.pending" -delete
# Vérifier les flags orphelins
find cache/ -name "*.pending" -mtime +0
```
### Monitoring
- Logs de création/suppression des flags
- Logs de polling dans la console frontend
- Métriques de temps de traitement

View File

@ -1,65 +0,0 @@
## Objet
Décrire le pipeline de traitement des images et comment diagnostiquer un blocage.
### Contexte
- Backend: Node.js/Express, OCR: tesseract.js, prétraitement: sharp, PDF: pdf-parse.
- Répertoires: `uploads/<folderHash>/` (entrées) et `cache/<folderHash>/` (résultats JSON).
- Métadonnées ignorées: `folder.json`, `*.meta`.
### Pipeline
1. Découverte des fichiers (ignore métadonnées)
2. Prétraitement image (grayscale, normalisation, contraste, débruitage)
3. OCR (multi-pass `ocrb+eng`, fallback `eng`)
4. Extraction PDF (pdf-parse ou OCR si scanné)
5. NER/Classification (règles personnes/entreprises/adresses, type doc)
6. Écriture JSON dans `cache/<hash>/<fileHash>.json`
### Délais attendus
- JPEG ~1 Mo: 45120 s (par image)
- PDF texte: 0.22 s; PDF scanné: 3090 s/page
- 2 images: 36 min au total (normal)
### Vérifications rapides
- Santé backend: `GET /api/health``{ status: "OK" }`
- État dossier: `GET /api/folders/<hash>/results`
- `hasPending` true si traitements restants
- `pending[].timestamp` récent
- `results[].document.fileName` présent
- Fichiers cache: apparition de `cache/<hash>/<fileHash>.json`
### Signes de blocage
- `hasPending: true` > 10 min sans nouveaux JSON dans `cache/<hash>/`
- Logs erreurs répétées (ex: type de fichier non supporté)
- Port 3001 occupé (EADDRINUSE)
### Actions correctives
- Redémarrage simple:
- `pkill -9 -f 'node.*backend/server.js' || true`
- `nohup node backend/server.js > backend.log 2>&1 &`
- Vérifier lignorance des métadonnées dans `backend/server.js`
- `if (file === 'folder.json' || file.endsWith('.meta')) { continue }`
- Recalcul pending: relancer lendpoint results puis vérifier `pending`
### Qualité OCR CNI (note)
- Multi-pass Tesseract, amélioration dimage, regex adresse renforcée, MRZ si présent
- Cas cible: détecter les noms et prénoms selon qualité du scan
### État courant (vérifié)
- Backend UP; dossier `7d99a85daf66a0081a0e881630e6b39b`
- `results`: 3 fichiers traités; `pending`: 0 (suite correctif normalisation)
### Correctif appliqué (normalisation Sharp)
- Problème: erreurs OCR « Expected number between 1 and 100 for upper but received 0.9 » dues à `normalize({ lower: 0.1, upper: 0.9 })`.
- Cause: Sharp attend des percentiles entiers 1..100 pour `lower`/`upper`.
- Correctif: `backend/enhancedOcr.js``normalize({ lower: 10, upper: 90 })`.
- Effet: fin des boucles derreurs; `hasPending: false`.

View File

@ -1,21 +0,0 @@
{
"id": "doc_20250910_232208_10",
"filename": "facture_4NK_08-2025_04.pdf",
"size": 85819,
"upload_time": "2025-09-10T23:22:08.239575",
"status": "completed",
"progress": 100,
"current_step": "Terminé",
"results": {
"ocr_text": "Texte extrait simulé du document...",
"document_type": "Acte de vente",
"entities": {
"persons": ["Jean Dupont", "Marie Martin"],
"addresses": ["123 Rue de la Paix, 75001 Paris"],
"properties": ["Appartement T3, 75m²"]
},
"verification_score": 0.85,
"external_checks": { "cadastre": "OK", "georisques": "OK", "bodacc": "OK" }
},
"completion_time": "2025-09-10T23:22:18.243146"
}

View File

@ -1,26 +0,0 @@
module.exports = {
apps: [
{
name: '4nk-ia-backend',
script: 'backend/server.js',
cwd: __dirname,
env: {
NODE_ENV: 'production',
PORT: 3001,
OLLAMA_MIN_REVIEW_MS: process.env.OLLAMA_MIN_REVIEW_MS || 0,
},
instances: 1,
exec_mode: 'fork',
autorestart: true,
max_memory_restart: '512M',
out_file: './log/backend.out.log',
error_file: './log/backend.err.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
time: true,
watch: ['backend'],
ignore_watch: ['uploads', 'cache', 'node_modules', 'log'],
watch_delay: 1000,
exp_backoff_restart_delay: 200,
},
],
}

Binary file not shown.

View File

@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'
export default tseslint.config([
globalIgnores(['dist', 'coverage', 'backend/node_modules', 'node_modules']),
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
@ -19,14 +19,5 @@ export default tseslint.config([
ecmaVersion: 2020,
globals: globals.browser,
},
rules: {
'@typescript-eslint/no-explicit-any': 'off',
// Autoriser variables prefixées par _ pour marquer l'inutilisation intentionnelle
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrors: 'all', caughtErrorsIgnorePattern: '^_' }],
// Éviter les blocs vides involontaires mais tolérer try/catch vides si commentés
'no-empty': ['warn', { allowEmptyCatch: true }],
// Tolérer les échappements superflus dans certains regex hérités
'no-useless-escape': 'off',
},
},
])

Binary file not shown.

View File

@ -1 +0,0 @@
340329

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IA - Lecoffre.io</title>
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>

View File

@ -1,33 +0,0 @@
server {
listen 80;
server_name _;
root /var/www/ia.4nkweb.com/current;
index index.html;
# Proxy vers le backend API
location /api/ {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
# Cache des assets
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA fallback
location / {
try_files $uri /index.html;
}
}

3225
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +1,18 @@
{
"name": "4nk-ia-front4nk",
"private": true,
"version": "0.1.4",
"version": "0.1.0",
"type": "module",
"scripts": {
"predev": "node scripts/check-node.mjs",
"dev": "vite",
"prebuild": "node scripts/check-node.mjs",
"build": "tsc -b && vite build",
"clean:all": "rm -rf node_modules backend/node_modules dist cache log uploads backend/cache backend/uploads && mkdir -p uploads cache log backend/uploads backend/cache && npm ci --no-audit --no-fund && (cd backend && npm ci --no-audit --no-fund)",
"back:up": "npx --yes pm2 start ecosystem.config.cjs --only 4nk-ia-backend --env production",
"back:restart": "npx --yes pm2 restart 4nk-ia-backend || npx --yes pm2 start ecosystem.config.cjs --only 4nk-ia-backend --env production",
"prefront:up": "bash -lc 'p=$(ss -ltnp | grep :5174 | sed -n \"s/.*pid=\\([0-9]*\\).*/\\1/p\"); if [ -n \"$p\" ]; then kill $p; fi'",
"front:up": "vite",
"start:all": "npm run clean:all && concurrently -k -n back,front -c blue,green \"npm:back:restart\" \"npm:front:up\"",
"start:dev": "npm run clean:all && npm run back:restart && npm run front:up",
"lint": "eslint .",
"preview": "vite preview",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"mdlint": "markdownlint . --ignore node_modules --ignore dist",
"test": "vitest run --coverage",
"test:ui": "vitest",
"test:collectors": "vitest run tests/collectors-simple.test.js",
"test:ocr": "vitest run tests/ocr.test.js",
"test:api": "vitest run tests/api.test.js",
"test:e2e": "vitest run tests/e2e.test.js",
"test:all": "npm run test:collectors && npm run test:ocr && npm run test:api && npm run test:e2e"
},
"engines": {
"node": ">=20.19.0 <23",
"npm": ">=10"
"test:ui": "vitest"
},
"dependencies": {
"@emotion/react": "^11.14.0",
@ -39,17 +21,11 @@
"@mui/material": "^7.3.2",
"@reduxjs/toolkit": "^2.9.0",
"axios": "^1.11.0",
"canvas": "^3.2.0",
"jimp": "^1.6.0",
"pdf-parse": "^1.1.1",
"pdf-poppler": "^0.2.1",
"pdf2pic": "^3.2.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-dropzone": "^14.3.8",
"react-redux": "^9.2.0",
"react-router-dom": "^7.9.1",
"sharp": "^0.34.3"
"react-router-dom": "^7.8.2"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
@ -58,10 +34,8 @@
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@types/supertest": "^6.0.3",
"@vitejs/plugin-react": "^5.0.0",
"@vitest/coverage-v8": "^3.2.4",
"concurrently": "^9.2.1",
"eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^5.2.0",
@ -70,10 +44,7 @@
"jsdom": "^26.1.0",
"markdownlint": "^0.38.0",
"markdownlint-cli": "^0.45.0",
"pdfjs-dist": "^4.8.69",
"prettier": "^3.6.2",
"supertest": "^7.1.4",
"tesseract.js": "^5.1.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2",

View File

@ -1 +0,0 @@
import{j as s}from"./index-D2TD1aux.js";import{c as t}from"./Layout-lJ1qJP1d.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};

View File

@ -1 +0,0 @@
import{j as s}from"./index-ChSrE95j.js";import{c as t}from"./Layout-EP4xiiHA.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};

View File

@ -1 +0,0 @@
import{j as s}from"./index-CedKFzDs.js";import{c as t}from"./Layout-D-i60CyA.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};

View File

@ -1 +0,0 @@
import{j as s}from"./index-BkuOAsyQ.js";import{c as t}from"./Layout-HUfQIbZm.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};

View File

@ -1 +0,0 @@
import{j as s}from"./index-CuLfHvEh.js";import{c as t}from"./Layout-BwmFxbkK.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};

View File

@ -1 +0,0 @@
import{j as s}from"./index-wde0U4qL.js";import{c as t}from"./Layout-aWSA1CnN.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};

View File

@ -1 +0,0 @@
import{j as s}from"./index-DwyPw-ga.js";import{c as t}from"./Layout-BXCKqMhs.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};

View File

@ -1 +0,0 @@
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-DwyPw-ga.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};

View File

@ -1 +0,0 @@
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-CedKFzDs.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};

View File

@ -1 +0,0 @@
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-wde0U4qL.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};

View File

@ -1 +0,0 @@
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-CuLfHvEh.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};

View File

@ -1 +0,0 @@
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-BkuOAsyQ.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};

View File

@ -1 +0,0 @@
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-D2TD1aux.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};

View File

@ -1 +0,0 @@
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-ChSrE95j.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More