JTh34 commited on
Commit
a9e787a
·
1 Parent(s): 4b3cbd6

🚀 Déploiement automatique RAG CHU 2025-06-30 22:48:22

Browse files
Files changed (1) hide show
  1. backend/src/vision_processor.py +142 -134
backend/src/vision_processor.py CHANGED
@@ -1,17 +1,14 @@
1
- """
2
- Processeur Vision Médical avec Claude - Version modulaire
3
- Module adapté pour l'architecture backend du projet RAG CHU
4
- """
5
  import base64
6
  import io
7
  import json
8
  import logging
9
  from pathlib import Path
10
  from typing import List, Dict, Tuple, Optional
11
- import fitz # PyMuPDF
12
  from PIL import Image
13
  import anthropic
14
  from langchain.docstore.document import Document as LangChainDocument
 
15
  from dataclasses import dataclass
16
  from docx import Document as DocxDocument
17
  import tempfile
@@ -19,32 +16,30 @@ from reportlab.pdfgen import canvas
19
  from reportlab.lib.pagesizes import letter
20
  from reportlab.lib.utils import simpleSplit
21
 
22
- from .config import settings
23
 
24
- # Configuration du logging
25
  logger = logging.getLogger(__name__)
26
 
27
  @dataclass
28
  class DocumentChunk:
29
- """Structure d'un chunk avec métadonnées enrichies"""
30
  content: str
31
  metadata: Dict
32
- bbox: Tuple[int, int, int, int] # Bounding box dans l'image
33
  confidence: float
34
 
35
  class VisualDocumentAnalyzer:
36
- """Analyseur de documents basé sur la vision Claude"""
37
-
38
  def __init__(self, anthropic_api_key: Optional[str] = None):
39
  api_key = anthropic_api_key or settings.anthropic_api_key
40
  if not api_key:
41
  raise ValueError("ClĂ© API Anthropic requise")
42
 
43
  self.client = anthropic.Anthropic(api_key=api_key)
44
- self.model = settings.anthropic_model
45
 
46
  def convert_doc_to_images(self, doc_path: str, dpi: int = 200) -> List[Image.Image]:
47
- """Convertit un document en images haute résolution"""
48
 
49
  # Si c'est un .docx, le convertir en PDF d'abord
50
  if doc_path.endswith('.docx'):
@@ -58,42 +53,36 @@ class VisualDocumentAnalyzer:
58
 
59
  for page_num in range(len(doc)):
60
  page = doc.load_page(page_num)
 
61
  # Matrice de transformation pour haute rĂ©solution
62
  mat = fitz.Matrix(dpi/72, dpi/72)
63
  pix = page.get_pixmap(matrix=mat)
64
 
65
- # Convertir en PIL Image
66
  img_data = pix.tobytes("png")
67
  img = Image.open(io.BytesIO(img_data))
68
- images.append(img)
69
 
70
  doc.close()
71
  return images
72
 
73
  def _docx_to_pdf(self, docx_path: str) -> str:
74
- """Convertit DOCX en PDF directement avec python-docx et reportlab"""
75
- logger.info("Conversion DOCX vers PDF avec python-docx")
76
  return self._extract_text_from_docx(docx_path)
77
 
78
  def _extract_text_from_docx(self, docx_path: str) -> str:
79
- """Extrait le texte directement du DOCX et le sauve comme PDF temporaire"""
80
-
81
-
82
  try:
83
- # Extraire le texte du DOCX avec plus de structure
84
  doc = DocxDocument(docx_path)
85
  text_content = []
86
 
87
  for paragraph in doc.paragraphs:
88
  text = paragraph.text.strip()
89
  if text:
90
- # Préserver la structure des titres (basé sur le style)
91
  if paragraph.style.name.startswith('Heading'):
92
  text_content.append(f"\n*** {text} ***\n")
93
  else:
94
  text_content.append(text)
95
-
96
- # Traiter aussi les tableaux
97
  for table in doc.tables:
98
  text_content.append("\n=== TABLEAU ===")
99
  for row in table.rows:
@@ -101,15 +90,11 @@ class VisualDocumentAnalyzer:
101
  if row_text:
102
  text_content.append(row_text)
103
  text_content.append("=== FIN TABLEAU ===\n")
104
-
105
- # Créer un PDF temporaire avec le texte
106
  with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
107
  pdf_path = tmp.name
108
 
109
  c = canvas.Canvas(pdf_path, pagesize=letter)
110
  width, height = letter
111
-
112
- # Configuration de police
113
  c.setFont("Helvetica", 10)
114
  y_position = height - 50
115
  margin = 50
@@ -119,7 +104,6 @@ class VisualDocumentAnalyzer:
119
  if not paragraph.strip():
120
  continue
121
 
122
- # Gérer les titres
123
  if paragraph.startswith("***") and paragraph.endswith("***"):
124
  if y_position < 100:
125
  c.showPage()
@@ -130,7 +114,6 @@ class VisualDocumentAnalyzer:
130
  c.setFont("Helvetica", 10)
131
  continue
132
 
133
- # Gérer les tableaux
134
  if paragraph.startswith("==="):
135
  if y_position < 50:
136
  c.showPage()
@@ -141,7 +124,6 @@ class VisualDocumentAnalyzer:
141
  c.setFont("Helvetica", 10)
142
  continue
143
 
144
- # Découper le texte automatiquement
145
  lines = simpleSplit(paragraph, "Helvetica", 10, max_width)
146
 
147
  for line in lines:
@@ -152,16 +134,13 @@ class VisualDocumentAnalyzer:
152
  c.drawString(margin, y_position, line)
153
  y_position -= 12
154
 
155
- # Espace entre paragraphes
156
  y_position -= 8
157
 
158
  c.save()
159
- logger.info("PDF créé à partir du texte DOCX avec structure préservée")
160
  return pdf_path
161
 
162
  except Exception as e:
163
  logger.error(f"Erreur extraction DOCX: {e}")
164
- # En dernier recours, créer un PDF minimal
165
  try:
166
  with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
167
  pdf_path = tmp.name
@@ -170,83 +149,58 @@ class VisualDocumentAnalyzer:
170
  c.drawString(50, 750, f"Document: {Path(docx_path).name}")
171
  c.drawString(50, 730, "Erreur lors de l'extraction du contenu DOCX")
172
  c.save()
173
- logger.warning("PDF minimal créé en fallback")
174
  return pdf_path
175
  except:
176
- # Ultime fallback : retourner le fichier original
177
  return docx_path
178
 
179
  async def analyze_page_structure(self, image: Image.Image, page_num: int) -> Dict:
180
- """Analyse la structure d'une page avec Claude"""
181
-
182
- # Convertir l'image en base64
183
  buffered = io.BytesIO()
184
  image.save(buffered, format="PNG")
185
  img_b64 = base64.b64encode(buffered.getvalue()).decode()
186
-
187
- # Prompt spécialisé pour documents médicaux
188
  analysis_prompt = """
189
- Analyse cette page de recommandations médicales et identifie précisément:
190
 
191
- 1. **STRUCTURE HIERARCHIQUE**:
192
- - Titre principal et sous-titres
193
- - Sections (A, B, C, D, etc.)
194
- - Numérotation et listes
195
 
196
- 2. **ELEMENTS STRUCTURELS**:
197
- - Tableaux (posologies, critères, alternatives)
198
- - Encadrés/points forts
199
- - Listes Ă  puces
200
- - Paragraphes de texte continu
201
 
202
- 3. **CONTENU MEDICAL**:
203
- - Noms de médicaments et posologies
204
- - Critères cliniques (gravité, stabilité)
205
- - Cas cliniques spécifiques
206
- - Durées de traitement
207
 
208
- 4. **ZONES DE TEXTE** avec coordinates approximatives (x, y, largeur, hauteur en %)
209
-
210
- Retourne un JSON structuré avec:
211
  ```json
212
  {
 
213
  "page_type": "guidelines|dosage_table|criteria_list",
214
- "main_sections": [
215
  {
216
- "title": "titre section",
217
  "type": "section|table|criteria|dosage|case_study",
218
- "bbox_percent": [x, y, width, height],
219
- "content_preview": "aperçu du contenu...",
220
- "medical_entities": ["amoxicilline", "PAC grave", etc.],
221
- "confidence": 0.0-1.0
222
- }
223
- ],
224
- "tables": [
225
- {
226
- "title": "nom du tableau",
227
- "type": "dosage|criteria|alternatives",
228
- "bbox_percent": [x, y, width, height],
229
- "columns": ["colonne1", "colonne2"],
230
- "medical_focus": "antibiotiques|critères cliniques|durées"
231
  }
232
  ],
233
  "key_medical_info": {
234
  "medications": ["liste des mĂ©dicaments"],
235
  "dosages": ["posologies identifiĂ©es"],
236
  "clinical_criteria": ["critères cliniques"],
237
- "patient_types": ["PAC grave", "sans comorbidité", etc.]
238
  }
239
  }
240
  ```
241
-
242
- Sois très précis sur les bounding boxes et identifie tous les éléments médicaux importants.
 
243
  """
244
 
245
  try:
246
  import httpx
247
  import asyncio
248
 
249
- # Utiliser httpx pour un appel asynchrone Ă  Anthropic
250
  headers = {
251
  "Content-Type": "application/json",
252
  "x-api-key": self.client.api_key,
@@ -255,7 +209,7 @@ class VisualDocumentAnalyzer:
255
 
256
  data = {
257
  "model": self.model,
258
- "max_tokens": 2000,
259
  "messages": [
260
  {
261
  "role": "user",
@@ -284,7 +238,6 @@ class VisualDocumentAnalyzer:
284
  headers=headers
285
  )
286
 
287
- # Parser la réponse JSON
288
  if response.status_code == 200:
289
  result = response.json()
290
  response_text = result["content"][0]["text"]
@@ -308,12 +261,12 @@ class VisualDocumentAnalyzer:
308
  return self._fallback_analysis(page_num)
309
 
310
  def _fallback_analysis(self, page_num: int) -> Dict:
311
- """Analyse de fallback en cas d'échec"""
312
  return {
 
313
  "page_type": "unknown",
314
  "page_number": page_num,
315
- "main_sections": [],
316
- "tables": [],
317
  "key_medical_info": {
318
  "medications": [],
319
  "dosages": [],
@@ -323,16 +276,26 @@ class VisualDocumentAnalyzer:
323
  }
324
 
325
  class IntelligentMedicalProcessor:
326
- """Processeur intelligent pour documents médicaux"""
327
-
328
  def __init__(self, anthropic_api_key: Optional[str] = None):
329
  self.analyzer = VisualDocumentAnalyzer(anthropic_api_key)
330
 
331
- async def process_medical_document(self, doc_path: str, progress_callback=None) -> List[LangChainDocument]:
332
- """Traite un document médical et retourne des chunks enrichis"""
333
- logger.info(f"Traitement du document: {doc_path}")
 
 
 
 
 
 
 
 
 
 
334
 
335
- # Convertir en images
 
336
  if progress_callback:
337
  await progress_callback(f"Conversion du document en images...", "vision")
338
 
@@ -341,65 +304,110 @@ class IntelligentMedicalProcessor:
341
  if progress_callback:
342
  await progress_callback(f"Document converti: {len(images)} pages Ă  analyser", "vision")
343
 
344
- # Analyser chaque page avec streaming temps réel
345
- import asyncio
346
- analysis_results = []
347
 
348
- async def analyze_single_page(i, image):
349
  if progress_callback:
350
- await progress_callback(f"Analyse de la page {i+1}/{len(images)} avec Anthropic Claude...", "vision")
351
 
352
  analysis = await self.analyzer.analyze_page_structure(image, i)
 
 
 
 
 
 
353
 
354
  if progress_callback:
355
- sections_found = len(analysis.get('main_sections', []))
356
- tables_found = len(analysis.get('tables', []))
357
  await progress_callback(
358
- f"Page {i+1} analysée: {sections_found} sections, {tables_found} tableaux",
359
- "success",
360
- {
361
- "page": i+1,
362
- "sections": sections_found,
363
- "tables": tables_found,
364
- "page_type": analysis.get('page_type', 'unknown')
365
- }
366
  )
367
- return analysis
368
 
369
- # Traiter les pages une par une pour le streaming
370
- for i, image in enumerate(images):
371
- analysis = await analyze_single_page(i, image)
372
- analysis_results.append(analysis)
373
 
374
- # Créer les documents LangChain
375
  if progress_callback:
376
- total_sections = sum(len(analysis.get('main_sections', [])) for analysis in analysis_results)
377
- await progress_callback(f"Création de {total_sections} chunks structurés...", "chunking")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
 
379
  documents = []
380
- for i, analysis in enumerate(analysis_results):
381
- # Créer un document par section principale
382
- for section in analysis.get('main_sections', []):
383
- metadata = {
384
- 'source': doc_path,
385
- 'page': i,
386
- 'section_title': section.get('title', ''),
387
- 'section_type': section.get('type', 'unknown'),
388
- 'medical_entities': section.get('medical_entities', []),
389
- 'confidence': section.get('confidence', 0.0),
390
- 'bbox': section.get('bbox_percent', []),
391
- 'key_medical_info': analysis.get('key_medical_info', {})
392
- }
393
-
394
- document = LangChainDocument(
395
- page_content=section.get('content_preview', ''),
396
- metadata=metadata
397
- )
398
- documents.append(document)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
- logger.info(f"Document traité: {len(documents)} sections extraites")
401
  return documents
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
 
403
  def create_medical_processor(anthropic_api_key: Optional[str] = None) -> IntelligentMedicalProcessor:
404
- """Factory function pour créer un processeur médical"""
405
- return IntelligentMedicalProcessor(anthropic_api_key)
 
 
 
 
 
1
  import base64
2
  import io
3
  import json
4
  import logging
5
  from pathlib import Path
6
  from typing import List, Dict, Tuple, Optional
7
+ import fitz
8
  from PIL import Image
9
  import anthropic
10
  from langchain.docstore.document import Document as LangChainDocument
11
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
12
  from dataclasses import dataclass
13
  from docx import Document as DocxDocument
14
  import tempfile
 
16
  from reportlab.lib.pagesizes import letter
17
  from reportlab.lib.utils import simpleSplit
18
 
19
+ from config import settings
20
 
 
21
  logger = logging.getLogger(__name__)
22
 
23
  @dataclass
24
  class DocumentChunk:
25
+ """Structure d'un chunk de document avec métadonnées enrichies"""
26
  content: str
27
  metadata: Dict
28
+ bbox: Tuple[int, int, int, int]
29
  confidence: float
30
 
31
  class VisualDocumentAnalyzer:
32
+ """Analyseur de documents basé sur Claude Vision pour l'extraction de texte médical"""
 
33
  def __init__(self, anthropic_api_key: Optional[str] = None):
34
  api_key = anthropic_api_key or settings.anthropic_api_key
35
  if not api_key:
36
  raise ValueError("ClĂ© API Anthropic requise")
37
 
38
  self.client = anthropic.Anthropic(api_key=api_key)
39
+ self.model = "claude-3-haiku-20240307"
40
 
41
  def convert_doc_to_images(self, doc_path: str, dpi: int = 200) -> List[Image.Image]:
42
+ """Convertit un document (PDF/DOCX) en images haute résolution"""
43
 
44
  # Si c'est un .docx, le convertir en PDF d'abord
45
  if doc_path.endswith('.docx'):
 
53
 
54
  for page_num in range(len(doc)):
55
  page = doc.load_page(page_num)
56
+
57
  # Matrice de transformation pour haute rĂ©solution
58
  mat = fitz.Matrix(dpi/72, dpi/72)
59
  pix = page.get_pixmap(matrix=mat)
60
 
61
+ # Convertir en PIL
62
  img_data = pix.tobytes("png")
63
  img = Image.open(io.BytesIO(img_data))
64
+ images.append(img)
65
 
66
  doc.close()
67
  return images
68
 
69
  def _docx_to_pdf(self, docx_path: str) -> str:
70
+ """Convertit un fichier DOCX en PDF temporaire"""
 
71
  return self._extract_text_from_docx(docx_path)
72
 
73
  def _extract_text_from_docx(self, docx_path: str) -> str:
74
+ """Extrait le texte d'un DOCX et génère un PDF temporaire"""
 
 
75
  try:
 
76
  doc = DocxDocument(docx_path)
77
  text_content = []
78
 
79
  for paragraph in doc.paragraphs:
80
  text = paragraph.text.strip()
81
  if text:
 
82
  if paragraph.style.name.startswith('Heading'):
83
  text_content.append(f"\n*** {text} ***\n")
84
  else:
85
  text_content.append(text)
 
 
86
  for table in doc.tables:
87
  text_content.append("\n=== TABLEAU ===")
88
  for row in table.rows:
 
90
  if row_text:
91
  text_content.append(row_text)
92
  text_content.append("=== FIN TABLEAU ===\n")
 
 
93
  with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
94
  pdf_path = tmp.name
95
 
96
  c = canvas.Canvas(pdf_path, pagesize=letter)
97
  width, height = letter
 
 
98
  c.setFont("Helvetica", 10)
99
  y_position = height - 50
100
  margin = 50
 
104
  if not paragraph.strip():
105
  continue
106
 
 
107
  if paragraph.startswith("***") and paragraph.endswith("***"):
108
  if y_position < 100:
109
  c.showPage()
 
114
  c.setFont("Helvetica", 10)
115
  continue
116
 
 
117
  if paragraph.startswith("==="):
118
  if y_position < 50:
119
  c.showPage()
 
124
  c.setFont("Helvetica", 10)
125
  continue
126
 
 
127
  lines = simpleSplit(paragraph, "Helvetica", 10, max_width)
128
 
129
  for line in lines:
 
134
  c.drawString(margin, y_position, line)
135
  y_position -= 12
136
 
 
137
  y_position -= 8
138
 
139
  c.save()
 
140
  return pdf_path
141
 
142
  except Exception as e:
143
  logger.error(f"Erreur extraction DOCX: {e}")
 
144
  try:
145
  with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
146
  pdf_path = tmp.name
 
149
  c.drawString(50, 750, f"Document: {Path(docx_path).name}")
150
  c.drawString(50, 730, "Erreur lors de l'extraction du contenu DOCX")
151
  c.save()
 
152
  return pdf_path
153
  except:
 
154
  return docx_path
155
 
156
  async def analyze_page_structure(self, image: Image.Image, page_num: int) -> Dict:
157
+ """Analyse une page avec Claude Vision et extrait le texte complet"""
 
 
158
  buffered = io.BytesIO()
159
  image.save(buffered, format="PNG")
160
  img_b64 = base64.b64encode(buffered.getvalue()).decode()
 
 
161
  analysis_prompt = """
162
+ Analyse cette page de document médical et EXTRAIT TOUT LE TEXTE VISIBLE.
163
 
164
+ Je veux deux choses :
 
 
 
165
 
166
+ 1. **TEXTE COMPLET** : Reproduis fidèlement TOUT le texte visible sur l'image,
167
+ en préservant la structure (titres, paragraphes, listes, tableaux).
 
 
 
168
 
169
+ 2. **STRUCTURE** : Identifie les sections logiques pour le découpage.
 
 
 
 
170
 
171
+ Retourne un JSON structuré :
 
 
172
  ```json
173
  {
174
+ "full_text": "TOUT LE TEXTE DE LA PAGE ICI, FORMATÉ AVEC DES RETOURS À LA LIGNE",
175
  "page_type": "guidelines|dosage_table|criteria_list",
176
+ "sections": [
177
  {
178
+ "title": "titre de la section",
179
  "type": "section|table|criteria|dosage|case_study",
180
+ "text_content": "TEXTE COMPLET DE CETTE SECTION",
181
+ "start_char": 0,
182
+ "end_char": 150,
183
+ "medical_entities": ["amoxicilline", "PAC grave"],
184
+ "confidence": 0.9
 
 
 
 
 
 
 
 
185
  }
186
  ],
187
  "key_medical_info": {
188
  "medications": ["liste des mĂ©dicaments"],
189
  "dosages": ["posologies identifiĂ©es"],
190
  "clinical_criteria": ["critères cliniques"],
191
+ "patient_types": ["PAC grave", "sans comorbidité"]
192
  }
193
  }
194
  ```
195
+
196
+ IMPORTANT : Le champ "full_text" doit contenir TOUT le texte de la page,
197
+ pas seulement un aperçu. Les sections doivent référencer des parties de ce texte complet.
198
  """
199
 
200
  try:
201
  import httpx
202
  import asyncio
203
 
 
204
  headers = {
205
  "Content-Type": "application/json",
206
  "x-api-key": self.client.api_key,
 
209
 
210
  data = {
211
  "model": self.model,
212
+ "max_tokens": 4000, # Augmenté pour plus de texte
213
  "messages": [
214
  {
215
  "role": "user",
 
238
  headers=headers
239
  )
240
 
 
241
  if response.status_code == 200:
242
  result = response.json()
243
  response_text = result["content"][0]["text"]
 
261
  return self._fallback_analysis(page_num)
262
 
263
  def _fallback_analysis(self, page_num: int) -> Dict:
264
+ """Retourne une analyse de fallback en cas d'échec de l'API Claude"""
265
  return {
266
+ "full_text": "",
267
  "page_type": "unknown",
268
  "page_number": page_num,
269
+ "sections": [],
 
270
  "key_medical_info": {
271
  "medications": [],
272
  "dosages": [],
 
276
  }
277
 
278
  class IntelligentMedicalProcessor:
279
+ """Processeur intelligent pour documents médicaux avec chunking adaptatif"""
 
280
  def __init__(self, anthropic_api_key: Optional[str] = None):
281
  self.analyzer = VisualDocumentAnalyzer(anthropic_api_key)
282
 
283
+ self.text_splitter = RecursiveCharacterTextSplitter(
284
+ chunk_size=800,
285
+ chunk_overlap=100,
286
+ separators=[
287
+ "\n\n*** ",
288
+ "\n\n",
289
+ "\n=== ",
290
+ "\n• ",
291
+ "\n- ",
292
+ ". ",
293
+ " "
294
+ ]
295
+ )
296
 
297
+ async def process_medical_document(self, doc_path: str, progress_callback=None) -> List[LangChainDocument]:
298
+ """Traite un document médical et retourne des chunks LangChain enrichis"""
299
  if progress_callback:
300
  await progress_callback(f"Conversion du document en images...", "vision")
301
 
 
304
  if progress_callback:
305
  await progress_callback(f"Document converti: {len(images)} pages Ă  analyser", "vision")
306
 
307
+ all_text_content = []
308
+ page_analyses = []
 
309
 
310
+ for i, image in enumerate(images):
311
  if progress_callback:
312
+ await progress_callback(f"Extraction texte complet page {i+1}/{len(images)}...", "vision")
313
 
314
  analysis = await self.analyzer.analyze_page_structure(image, i)
315
+ page_analyses.append(analysis)
316
+
317
+ full_text = analysis.get('full_text', '')
318
+ if full_text.strip():
319
+ page_header = f"\n\n=== PAGE {i+1} ===\n"
320
+ all_text_content.append(page_header + full_text)
321
 
322
  if progress_callback:
323
+ text_length = len(full_text)
324
+ sections_found = len(analysis.get('sections', []))
325
  await progress_callback(
326
+ f"Page {i+1}: {text_length} caractères, {sections_found} sections",
327
+ "success"
 
 
 
 
 
 
328
  )
 
329
 
330
+ complete_document_text = "\n".join(all_text_content)
 
 
 
331
 
 
332
  if progress_callback:
333
+ await progress_callback(f"Texte total extrait: {len(complete_document_text)} caractères", "success")
334
+ await progress_callback("Découpage intelligent en chunks...", "chunking")
335
+
336
+ # Debug: Afficher le contenu total extrait
337
+ logger.info("📄 EXTRACTION DOCUMENTAIRE TERMINÉE:")
338
+ logger.info("="*100)
339
+ logger.info(f" 📊 Nombre de pages analysées: {len(images)}")
340
+ logger.info(f" 📏 Texte total extrait: {len(complete_document_text)} caractères")
341
+ logger.info(f" 📋 Aperçu du texte (500 premiers chars): {complete_document_text[:500]}{'...' if len(complete_document_text) > 500 else ''}")
342
+ logger.info("="*100)
343
+
344
+ text_chunks = self.text_splitter.split_text(complete_document_text)
345
+
346
+ logger.info("✂️ DÉCOUPAGE EN CHUNKS:")
347
+ logger.info("="*100)
348
+ logger.info(f" 🔢 Nombre de chunks créés: {len(text_chunks)}")
349
+ logger.info(f" 📏 Taille chunks: {settings.chunk_size} caractères")
350
+ logger.info(f" 🔄 Chevauchement: {settings.chunk_overlap} caractères")
351
+ logger.info("="*100)
352
 
353
  documents = []
354
+ for chunk_idx, chunk_text in enumerate(text_chunks):
355
+ page_num = self._find_page_for_chunk(chunk_text, page_analyses)
356
+ medical_entities = self._extract_medical_entities_from_chunk(chunk_text, page_analyses)
357
+
358
+ # Debug: Afficher chaque chunk créé
359
+ logger.info(f"đź§© CHUNK VISION {chunk_idx+1}/{len(text_chunks)}:")
360
+ logger.info(f" đź“„ Page source: {page_num}")
361
+ logger.info(f" 📏 Taille: {len(chunk_text)} caractères")
362
+ logger.info(f" 🏥 Entités médicales: {medical_entities}")
363
+ logger.info(f" đź“‹ Contenu (200 premiers chars): {chunk_text[:200]}{'...' if len(chunk_text) > 200 else ''}")
364
+ logger.info(" " + "-"*80)
365
+
366
+ metadata = {
367
+ 'source': doc_path,
368
+ 'chunk_id': chunk_idx,
369
+ 'page': page_num,
370
+ 'chunk_size': len(chunk_text),
371
+ 'medical_entities': medical_entities,
372
+ 'document_type': 'medical_guidelines',
373
+ 'extraction_method': 'claude_vision_ocr',
374
+ 'total_chunks': len(text_chunks)
375
+ }
376
+
377
+ document = LangChainDocument(
378
+ page_content=chunk_text,
379
+ metadata=metadata
380
+ )
381
+ documents.append(document)
382
+
383
+ logger.info(f"✅ CRÉATION CHUNKS VISION TERMINÉE: {len(documents)} documents LangChain créés")
384
+ logger.info("="*100)
385
+
386
+ if progress_callback:
387
+ await progress_callback(f"✅ {len(documents)} chunks créés", "success")
388
 
 
389
  return documents
390
+
391
+ def _find_page_for_chunk(self, chunk_text: str, page_analyses: List[Dict]) -> int:
392
+ """Trouve la page source d'un chunk de texte"""
393
+ for analysis in page_analyses:
394
+ if analysis.get('full_text', '') in chunk_text or chunk_text in analysis.get('full_text', ''):
395
+ return analysis.get('page_number', 0)
396
+ return 0
397
+
398
+ def _extract_medical_entities_from_chunk(self, chunk_text: str, page_analyses: List[Dict]) -> List[str]:
399
+ """Extrait les entités médicales pertinentes pour un chunk donné"""
400
+ entities = set()
401
+
402
+ for analysis in page_analyses:
403
+ medical_info = analysis.get('key_medical_info', {})
404
+ for entity_type in ['medications', 'dosages', 'clinical_criteria', 'patient_types']:
405
+ for entity in medical_info.get(entity_type, []):
406
+ if entity.lower() in chunk_text.lower():
407
+ entities.add(entity)
408
+
409
+ return list(entities)
410
 
411
  def create_medical_processor(anthropic_api_key: Optional[str] = None) -> IntelligentMedicalProcessor:
412
+ """Factory pour créer un processeur médical intelligent"""
413
+ return IntelligentMedicalProcessor(anthropic_api_key)