How does the anonymization logic work in detail?
Explique moi la logique d'anonymisation de ce repo. En detail
Excellent! Maintenant j'ai une vue complète. Créons un document de synthèse détaillé:
##🔐 EXPLICATION DÉTAILLÉE DE LA LOGIQUE D'ANONYMISATION - AnonDocs
###1. Architecture Générale
Le système fonctionne sur 5 couches principales:
Routes HTTP → Contrôleurs → Service Anonymisation → Service LLM → Réponse
###2. Flux d'Anonymisation - Détail Complet
####ÉTAPE 1 : Réception du Contenu
L'API accepte 3 formats:
- Texte brut : via
POST /api/anonymize - Documents (DOCX, PDF, TXT) : via
POST /api/document(upload de fichier) - Streaming : via
POST /api/stream/*(SSE - Server-Sent Events)
Validation basique:
// src/controllers/anonymize.controller.ts
if (!text || typeof text !== 'string') {
// Erreur: texte obligatoire et doit être string
}
if (text.trim().length === 0) {
// Erreur: texte ne peut pas être vide
}
####ÉTAPE 2 : Découpage en Chunks (Chunking)
Le texte est divisé en morceaux gérables pour le LLM.
Service: src/services/chunking.service.ts
Logique:
chunkText(text: string): string[] {
// Si texte < chunk_size → retourner texte entier
if (text.length <= this.chunkSize) {
return [text];
}
// Sinon, diviser avec:
// - Taille: config.CHUNK_SIZE (défaut: 1500 caractères)
// - Chevauchement: config.CHUNK_OVERLAP (défaut: 200 caractères)
// - Limite intelligente: casse aux limites de phrases (. ! ?)
}
Exemple:
Texte original (5000 caractères)
↓
Chunk 1 (1500 chars) [0-1500]
Chunk 2 (1500 chars) [1300-2800] ← 200 chars d'overlap
Chunk 3 (1500 chars) [2600-4100]
Chunk 4 (reste)
Avantage: Les chunks qui se chevauchent garantissent que les PII à la limite d'un chunk ne seront pas manquées.
####ÉTAPE 3 : Anonymisation de chaque Chunk
Service: src/services/llm.service.ts → méthode anonymizeChunk()
Processus:
-
System Prompt : Instructions au LLM pour identifier et remplacer les PII
-
Sélection du Provider LLM (défaut: configurable)
- OpenAI (ou compatible: LocalAI, LM Studio)
- Anthropic Claude
- Ollama (LLMs locaux comme Mistral, Llama, etc.)
-
Envoi au LLM avec contexte:
const systemPrompt = `
You are an expert document anonymization assistant. Your task is to:
1. Identify and remove all Personally Identifiable Information (PII)
2. Replace PII with generic placeholders: [NAME], [ADDRESS], [EMAIL], [PHONE], [DATE], [ORGANIZATION]
3. Maintain structure and readability
4. Return JSON with: anonymizedText, piiDetected, replacements
PII includes:
- Personal names
- Physical addresses
- Email addresses
- Phone numbers
- Dates of birth / identifying dates
- Organization names (if identifying individuals)
- ID numbers (SSN, passport, driver's license, etc.)
- Financial information (credit card, bank account numbers)
`;
- Réponse attendue du LLM (format JSON):
{
"anonymizedText": "John Smith called... → [NAME] called...",
"piiDetected": {
"names": ["John Smith"],
"addresses": [],
"emails": ["john@example.com"],
"phoneNumbers": ["+33 6 12 34 56 78"],
"dates": [],
"organizations": [],
"other": []
},
"replacements": [
{"original": "John Smith", "anonymized": "[NAME]"},
{"original": "john@example.com", "anonymized": "[EMAIL]"},
{"original": "+33 6 12 34 56 78", "anonymized": "[PHONE]"}
]
}
- Extraction et validation:
// Extraction JSON from response (handle markdown formatting)
const jsonMatch = content.match(/\{[\s\S]*\}/);
const result = JSON.parse(jsonMatch[0]) as AnonymizationResult;
####ÉTAPE 4 : Traitement des Chunks
Deux modes disponibles (configurable):
Mode Séquentiel (par défaut):
if (config.chunking.enableParallel === false) {
for (let i = 0; i < textChunks.length; i++) {
const result = await llmService.anonymizeChunk(chunk);
// Attendre avant le chunk suivant
progressEmitter.emit('progress', {
type: 'chunk_completed',
progress: Math.round(((i + 1) / totalChunks) * 90),
currentChunk: i + 1,
totalChunks: totalChunks
});
}
}
Mode Parallèle (plus rapide):
if (config.chunking.enableParallel === true) {
results = await Promise.all(
textChunks.map(chunk => llmService.anonymizeChunk(chunk))
);
}
####ÉTAPE 5 : Agrégation des Résultats
Service: src/services/anonymization.service.ts
// Fusionner tous les chunks traités
for (const result of results) {
anonymizedChunks.push(result.anonymizedText);
// Agréger les PII détectés
allPiiDetected.names.push(...result.piiDetected.names);
allPiiDetected.emails.push(...result.piiDetected.emails);
// ... etc pour tous les types
// Lister toutes les remplacements
allReplacements.push(...result.replacements);
}
// Joindre les chunks avec une séparation
const anonymizedText = anonymizedChunks.join('\n\n');
####ÉTAPE 6 : Gestion Spécifique des DOCX
Service: src/services/docx-formatter.service.ts
Point crucial : Préserver la mise en forme (polices, couleurs, styles, images, etc.)
Processus:
- Lecture du DOCX (c'est un ZIP contenant du XML):
const data = fs.readFileSync(inputPath);
const zip = await JSZip.loadAsync(data);
const documentXml = await zip.file('word/document.xml')?.async('string');
- Extraction du texte (pour anonymisation):
// Parser le XML DOCX
const parsedXml = await parseXml(documentXml);
// Extraire les noeuds de texte <w:t>
private extractTextNodes(obj: any): TextNode[] {
// Traverser récursivement l'arborescence XML
if ('w:t' in obj) { // Nœud texte trouvé
textNodes.push({
text: textContent,
startPos, endPos,
xmlPath: [chemin vers ce nœud]
});
}
// Continuer récursivement...
}
- Anonymisation du texte (comme étape 1-5):
const result = await anonymizationService.anonymizeText(text, provider);
// Retourne: result.replacements = [
// {"original": "John", "anonymized": "[NAME]"},
// ...
// ]
- Application des remplacements au XML (sans casser la structure):
private applyReplacementsToXml(obj: any, replacements: PiiReplacement[]): void {
if ('w:t' in obj) {
// Obtenir le texte du nœud
const textContent = obj['w:t'];
// Appliquer TOUS les remplacements
for (const replacement of replacements) {
const regex = new RegExp(escapeRegex(replacement.original), 'g');
obj['w:t'] = obj['w:t'].replace(regex, replacement.anonymized);
}
}
// Récursivement pour tous les nœuds
if (Array.isArray(obj)) {
obj.forEach(item => this.applyReplacementsToXml(item, replacements));
} else {
Object.keys(obj).forEach(key => {
this.applyReplacementsToXml(obj[key], replacements);
});
}
}
- Reconstruction du DOCX:
// Reconvertir le XML modifié
const newDocumentXml = builder.buildObject(parsedXml);
// Réintégrer dans le ZIP
zip.file('word/document.xml', newDocumentXml);
// Générer le nouveau DOCX
const buffer = await zip.generateAsync({ type: 'nodebuffer' });
fs.writeFileSync(outputPath, buffer);
###3. Types de PII Détectés
Le LLM est instruit à détecter:
┌─────────────────────────────────────────┐
│ PII CATEGORIES SUPPORTÉES │
├─────────────────────────────────────────┤
│ • Names (noms personnels) │
│ • Addresses (adresses physiques) │
│ • Emails (adresses email) │
│ • Phone Numbers (numéros de téléphone) │
│ • Dates (dates de naissance, etc.) │
│ • Organizations (entreprises identifi.) │
│ • ID Numbers (SSN, passeport, etc.) │
│ • Financial Info (CB, comptes bancaires)│
│ • Other (autres PII détectés) │
└─────────────────────────────────────────┘
###4. Flux Complet - Exemple Concret
INPUT:
"John Smith (john@example.com, +33612345678) works at Acme Corp"
↓ [Pas de chunking, < 1500 chars]
LLM SYSTEM PROMPT:
"Identifiez et remplacez tous les PII par des placeholders"
↓ [Invocation LLM]
LLM RESPONSE (JSON):
{
"anonymizedText": "[NAME] ([EMAIL], [PHONE]) works at [ORGANIZATION]",
"piiDetected": {
"names": ["John Smith"],
"emails": ["john@example.com"],
"phoneNumbers": ["+33612345678"],
"organizations": ["Acme Corp"]
},
"replacements": [
{"original": "John Smith", "anonymized": "[NAME]"},
{"original": "john@example.com", "anonymized": "[EMAIL]"},
{"original": "+33612345678", "anonymized": "[PHONE]"},
{"original": "Acme Corp", "anonymized": "[ORGANIZATION]"}
]
}
↓ [Aggregation des résultats]
OUTPUT FINAL:
{
"anonymizedText": "[NAME] ([EMAIL], [PHONE]) works at [ORGANIZATION]",
"piiDetected": {...},
"replacements": {...},
"chunksProcessed": 1,
"wordsPerMinute": 3500,
"processingTimeMs": 1234
}
###5. Mécanismes de Sécurité
| Mécanisme | Détail |
|---|---|
| Pas de persistance | Aucune donnée stockée en DB |
| Traitement local | Contrôle total sur où s'exécute le LLM |
| Confidentialité des données | Pour Ollama: 100% local, zéro réseau externe |
| Validations d'entrée | Vérification du texte, du provider, du MIME type |
| Gestion des fichiers | Suppression immédiate après traitement |
| Path traversal protection | Validation stricte des noms de fichiers |
###6. Points Clés de l'Architecture
✅ Modularité : Chaque service a une responsabilité unique
✅ Flexibilité : Support multiple LLM providers
✅ Scalabilité : Chunking + traitement parallèle optionnel
✅ Intégrité des documents : Préservation de la mise en forme DOCX
✅ Traçabilité : Logging détaillé + métriques (WPM, temps)
✅ Stateless : Pas de session, API RESTful pure
###7. Configuration Clé
// src/config/index.ts
{
chunking: {
chunkSize: 1500, // Taille de chunk en caractères
chunkOverlap: 200, // Chevauchement pour continuité
enableParallel: false // Parallélisation des chunks
},
llm: {
defaultProvider: 'ollama', // openai | anthropic | ollama
ollama: {
baseUrl: 'http://localhost:11434',
model: 'mistral',
temperature: 0 // 0 = déterministe (recommandé)
}
}
}
C'est un système robuste, modulaire et pensé pour la confidentialité! 🔒
END OF POST