lamekemal commited on
Commit
e8e2397
·
1 Parent(s): f32ba89
Files changed (1) hide show
  1. app.py +230 -212
app.py CHANGED
@@ -1,49 +1,57 @@
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
  """
4
- Extraction BRVM via Hugging Face Hub sur Space
5
- Avec téléchargement de PDF depuis un Hugging Face Dataset
6
- et upload des résultats JSON vers un autre Hugging Face Dataset
7
- Auteur : Gemini
 
 
 
 
 
 
8
  """
9
 
10
- import fitz
11
  import json
12
  from pathlib import Path
13
  from tqdm import tqdm
14
  import torch
15
  from transformers import pipeline, AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
16
- import requests
17
- import zipfile
18
- import io
19
  import os
20
  from huggingface_hub import hf_hub_download, HfApi
21
  from datetime import datetime
22
  import uuid
 
23
 
24
- # ---------- CONFIG ----------
25
- MODEL = "mistralai/Mistral-7B-Instruct-v0.3"
 
26
 
27
- HF_DATASET_REPO_ID_PDFS = "lamekemal/brvm-reports-pdfs"
 
 
28
  ZIP_FILENAME_IN_DATASET = "brvm_reports.zip"
29
 
30
- HF_DATASET_REPO_ID_JSON_OUTPUT = "lamekemal/brvm-reports-json"
31
-
32
- HF_TOKEN = os.getenv('HF_TOKEN')
 
 
33
 
34
- PDF_FOLDER = "brvm_reports"
35
- LOCAL_OUT_BASE_FOLDER = "local_json_outputs"
36
- # ----------------------------
37
 
38
- # Prompt optimisé pour le modèle Mistral Instruct
 
 
39
  PROMPT_TEMPLATE = """
40
- [INST] Tu es un expert en finance spécialisé dans la BRVM.
41
- À partir du texte ci-dessous issu d’un bulletin officiel de la cote BRVM,
42
- EXTRAIT UNIQUEMENT les données suivantes et RETOURNE UN JSON VALIDE au format strict.
43
- Ta réponse NE DOIT CONTENIR AUCUN AUTRE TEXTE QUE LE JSON.
44
- Commence ta réponse par [DEBUT_JSON] et termine la par [FIN_JSON].
45
 
46
- JSON Schema:
 
47
  {{
48
  "indicateurs": {{
49
  "brvm_10": {{ "niveau": float, "var_jour_pct": float, "var_annuelle_pct": float }},
@@ -69,230 +77,240 @@ JSON Schema:
69
  {{ "code": string, "emetteur": string, "coupon_pct": float, "echeance": string, "nominal": float, "cours": float }}
70
  ]
71
  }}
 
72
 
73
- Contraintes :
74
- - Si une donnée est absente, mets null.
75
- - Les nombres utilisent un point comme séparateur décimal.
76
 
77
- Texte du bulletin :
78
  {texte_pdf}[/INST]
79
  """
80
- # --- Initialisation du pipeline Hugging Face avec quantification ---
81
- extractor = None # Initialiser à None pour gérer les erreurs de chargement
82
 
83
- try:
84
- print("Chargement du modèle Hugging Face avec quantification 4-bit...")
85
-
86
- # Configuration de la quantification 4-bit
87
- bnb_config = BitsAndBytesConfig(
88
- load_in_4bit=True,
89
- bnb_4bit_quant_type="nf4", # NormalFloat4 quantization
90
- bnb_4bit_compute_dtype=torch.bfloat16, # Type de données pour les calculs
91
- bnb_4bit_use_double_quant=True, # Double quantification pour plus de précision
92
- )
93
-
94
- # Charger le tokenizer et le modèle avec la configuration de quantification
95
- tokenizer = AutoTokenizer.from_pretrained(MODEL)
96
- model = AutoModelForCausalLM.from_pretrained(
97
- MODEL,
98
- quantization_config=bnb_config,
99
- device_map="auto", # Permet de charger le modèle sur le(s) GPU disponible(s)
100
- torch_dtype=torch.bfloat16 # Spécifier le dtype pour le chargement initial
101
- )
102
-
103
- # Créer le pipeline avec le modèle et le tokenizer chargés
104
- if tokenizer.pad_token is None:
105
- tokenizer.pad_token = tokenizer.eos_token # Ou un autre token approprié si nécessaire
106
-
107
- extractor = pipeline(
108
- "text-generation",
109
- model=model,
110
- tokenizer=tokenizer,
111
- device=0 # Utilise le GPU si disponible (0 pour le premier GPU, -1 pour CPU)
112
- )
113
- print("Modèle quantifié chargé avec succès.")
114
- except Exception as e:
115
- print(f"Erreur lors du chargement du modèle Hugging Face : {e}")
116
- # Ne pas initialiser extractor si erreur
117
-
118
- # --- Fonctions d'extraction et de traitement ---
119
- def extract_text(pdf_path):
120
- """Extraction du texte brut à partir d'un PDF avec PyMuPDF."""
121
- try:
122
- doc = fitz.open(pdf_path)
123
- text = "\n".join(page.get_text() for page in doc)
124
- return text
125
- except Exception as e:
126
- print(f"[Erreur PDF] {pdf_path} : {e}")
127
- return ""
128
 
129
- def call_huggingface_model(text):
130
- """Appel du modèle Hugging Face pour extraire le JSON à partir du texte."""
131
- if extractor is None:
132
- print("[Erreur] Le pipeline d'extraction n'a pas été initialisé. Impossible d'appeler le modèle.")
133
- return {"error": "Modèle non chargé."}
134
-
135
- prompt = PROMPT_TEMPLATE.format(texte_pdf=text)
136
-
137
  try:
138
- # Augmenter considérablement max_new_tokens pour éviter la troncature
139
- # Augmenté de 4096 à 8192
140
- response = extractor(
141
- prompt,
142
- max_new_tokens=8192,
143
- do_sample=False,
144
- num_return_sequences=1
145
- )
146
-
147
- output_text = response[0]['generated_text']
148
 
149
- json_start_marker = "[DEBUT_JSON]"
150
- json_end_marker = "[FIN_JSON]"
 
 
 
 
 
 
151
 
152
- # La réponse du modèle Mistral peut inclure le prompt, donc on cherche le début du JSON APRÈS la fin du prompt ou le début du marqueur
153
- # et la fin du JSON AVANT la fin du marqueur.
154
-
155
- # Trouver la fin du prompt pour commencer la recherche du JSON
156
- prompt_end_index = output_text.rfind("[/INST]") # C'est le marqueur de fin d'instruction
157
 
158
- # Si le marqueur [/INST] est trouvé, commencer la recherche du JSON après.
159
- if prompt_end_index != -1:
160
- search_start_index = prompt_end_index + len("[/INST]")
161
- text_to_parse = output_text[search_start_index:]
162
- else:
163
- # Si le marqueur [/INST] n'est pas trouvé (moins probable avec un modèle instruct),
164
- # on prend toute la chaîne et on se base sur les marqueurs [DEBUT_JSON]/[FIN_JSON]
165
- text_to_parse = output_text
166
-
167
- json_start = text_to_parse.find(json_start_marker)
168
- json_end = text_to_parse.rfind(json_end_marker)
169
-
170
- if json_start != -1 and json_end != -1:
171
- json_text_with_markers = text_to_parse[json_start + len(json_start_marker):json_end]
172
-
173
- actual_json_start = json_text_with_markers.find('{')
174
- actual_json_end = json_text_with_markers.rfind('}') + 1
175
-
176
- if actual_json_start != -1 and actual_json_end != -1:
177
- json_content = json_text_with_markers[actual_json_start:actual_json_end]
178
- return json.loads(json_content)
179
- else:
180
- return {"error": "JSON non trouvé entre les accolades après les marqueurs", "raw": output_text}
181
- else:
182
- return {"error": "Marqueurs JSON non trouvés", "raw": output_text}
183
-
184
- except json.JSONDecodeError:
185
- print(f"Erreur JSONDecodeError: Le texte brut était : {output_text[:500]}...")
186
- return {"error": "JSON invalide ou mal formé", "raw": output_text}
187
  except Exception as e:
188
- return {"error": str(e), "raw": output_text}
 
189
 
190
- def download_and_extract_zip_from_hf_dataset(dataset_repo_id, zip_filename, target_folder):
191
  """
192
- Télécharge un fichier ZIP depuis un Hugging Face Dataset et extrait les PDF
193
- dans un dossier cible. Crée le dossier cible s'il n'existe pas.
194
  """
195
- print(f"Tentative de téléchargement du fichier '{zip_filename}' "
196
- f"depuis le dataset Hugging Face : {dataset_repo_id}")
197
  try:
198
  local_zip_path = hf_hub_download(
199
- repo_id=dataset_repo_id,
200
  filename=zip_filename,
201
  repo_type="dataset",
202
- cache_dir="./hf_cache"
203
  )
204
- print(f"Fichier ZIP téléchargé localement : {local_zip_path}")
205
-
206
- Path(target_folder).mkdir(parents=True, exist_ok=True)
207
 
208
  extracted_files = []
209
  with zipfile.ZipFile(local_zip_path, 'r') as z:
210
- for file_info in z.infolist():
211
- if file_info.filename.startswith('/') or '..' in file_info.filename:
212
- print(f"Skipping potentially unsafe path: {file_info.filename}")
213
  continue
214
-
215
- extracted_path = Path(target_folder) / file_info.filename
216
 
217
- if file_info.is_dir():
218
- extracted_path.mkdir(parents=True, exist_ok=True)
219
- elif file_info.filename.lower().endswith('.pdf'):
220
- print(f"Extraction de : {file_info.filename} vers {extracted_path}")
221
- try:
222
- with extracted_path.open("wb") as outfile:
223
- outfile.write(z.read(file_info.filename))
224
- extracted_files.append(extracted_path)
225
- except Exception as e:
226
- print(f"Erreur lors de l'extraction de {file_info.filename}: {e}")
227
- else:
228
- print(f"Ignoré (non PDF) : {file_info.filename}")
229
-
230
- print(f"Extraction terminée. {len(extracted_files)} fichiers PDF extraits.")
231
- return extracted_files
232
 
 
 
233
  except Exception as e:
234
- print(f"Une erreur est survenue lors du téléchargement ou de l'extraction depuis Hugging Face Dataset : {e}")
235
  return []
236
- finally:
237
- pass
238
 
239
- # --- Fonction principale ---
240
- def main():
241
- """Fonction principale pour exécuter l'ensemble du processus d'extraction."""
242
- downloaded_pdfs = download_and_extract_zip_from_hf_dataset(
243
- HF_DATASET_REPO_ID_PDFS, ZIP_FILENAME_IN_DATASET, PDF_FOLDER
244
- )
245
-
246
- if not downloaded_pdfs:
247
- print("Aucun fichier PDF n'a pu être téléchargé ou extrait. Arrêt du processus.")
248
- return
249
 
250
- unique_folder_name = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + "_" + str(uuid.uuid4())[:8]
251
- local_out_dir = Path(LOCAL_OUT_BASE_FOLDER) / unique_folder_name
252
- local_out_dir.mkdir(parents=True, exist_ok=True)
 
 
 
 
 
 
253
 
254
- aggregate = []
255
-
256
- print(f"Début du traitement de {len(downloaded_pdfs)} fichiers PDF extraits...")
257
 
258
- for pdf_file in tqdm(downloaded_pdfs, desc="Traitement PDFs"):
259
- text = extract_text(pdf_file)
260
- if not text.strip():
261
- print(f"Le fichier {pdf_file.name} est vide ou l'extraction de texte a échoué, on le passe.")
262
- continue
263
 
264
- data = call_huggingface_model(text)
265
- aggregate.append(data)
 
 
 
 
 
 
266
 
267
- out_path = local_out_dir / f"{pdf_file.stem}.json"
268
- with open(out_path, "w", encoding="utf-8") as f:
269
- json.dump(data, f, ensure_ascii=False, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
- with open(local_out_dir / "brvm_aggregate.json", "w", encoding="utf-8") as f:
272
- json.dump(aggregate, f, ensure_ascii=False, indent=2)
 
273
 
274
- print(f"[OK] Extraction terminée - {len(downloaded_pdfs)} fichiers traités dans {local_out_dir}.")
275
 
276
- if HF_TOKEN:
277
- try:
278
- api = HfApi(token=HF_TOKEN)
279
- print(f"Début de l'upload des résultats vers {HF_DATASET_REPO_ID_JSON_OUTPUT}/{unique_folder_name}...")
280
-
281
- api.upload_folder(
282
- folder_path=str(local_out_dir),
283
- repo_id=HF_DATASET_REPO_ID_JSON_OUTPUT,
284
- repo_type="dataset",
285
- path_in_repo=unique_folder_name,
286
- commit_message=f"Extraction BRVM du {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
287
- )
288
- print(f"[OK] Upload terminé vers https://huggingface.co/datasets/{HF_DATASET_REPO_ID_JSON_OUTPUT}/tree/main/{unique_folder_name}")
289
- except Exception as e:
290
- print(f"[ERREUR] Échec de l'upload des résultats vers Hugging Face Dataset : {e}")
291
- print("Veuillez vérifier que le HF_TOKEN est correctement configuré avec les permissions d'écriture.")
292
- print(f"Et que le dépôt '{HF_DATASET_REPO_ID_JSON_OUTPUT}' existe et que vous y avez accès.")
293
- else:
294
- print("[AVERTISSEMENT] HF_TOKEN non configuré. Les résultats ne seront PAS uploadés vers le Dataset Hugging Face.")
295
- print(f"Les fichiers sont disponibles localement dans le Space à l'adresse : {local_out_dir}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
 
298
  if __name__ == "__main__":
 
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
  """
4
+ Extraction de données de bulletins BRVM via Hugging Face.
5
+
6
+ Ce script optimisé :
7
+ 1. Télécharge un fichier ZIP de rapports PDF depuis un Hugging Face Dataset.
8
+ 2. Extrait le texte de chaque PDF.
9
+ 3. Utilise un modèle Mistral quantifié pour extraire les données structurées en JSON.
10
+ 4. Gère les erreurs de manière robuste (parsing, extraction).
11
+ 5. Uploade les résultats JSON vers un autre Hugging Face Dataset.
12
+
13
+ Auteur: Gemini (avec optimisations)
14
  """
15
 
16
+ import fitz # PyMuPDF
17
  import json
18
  from pathlib import Path
19
  from tqdm import tqdm
20
  import torch
21
  from transformers import pipeline, AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
 
 
 
22
  import os
23
  from huggingface_hub import hf_hub_download, HfApi
24
  from datetime import datetime
25
  import uuid
26
+ import zipfile
27
 
28
+ # ---------- CONFIGURATION CENTRALE ----------
29
+ # --- Modèle et Quantification ---
30
+ MODEL_ID = "mistralai/Mistral-7B-Instruct-v0.3"
31
 
32
+ # --- Dépôts Hugging Face ---
33
+ HF_DATASET_PDFS_REPO_ID = "lamekemal/brvm-reports-pdfs"
34
+ HF_DATASET_JSON_REPO_ID = "lamekemal/brvm-reports-json"
35
  ZIP_FILENAME_IN_DATASET = "brvm_reports.zip"
36
 
37
+ # --- Token et Dossiers Locaux ---
38
+ HF_TOKEN = os.getenv('HF_TOKEN') # Le token est récupéré des variables d'environnement (secrets)
39
+ LOCAL_PDF_FOLDER = Path("brvm_reports_extracted")
40
+ LOCAL_JSON_OUTPUT_BASE_FOLDER = Path("brvm_json_outputs")
41
+ LOCAL_CACHE_DIR = Path("./hf_cache")
42
 
43
+ # --- Paramètres du Pipeline ---
44
+ # Assez grand pour contenir le JSON complet, même pour des rapports denses.
45
+ MAX_NEW_TOKENS = 8192
46
 
47
+ # --- Prompt Optimisé ---
48
+ # Plus direct, plus concis, et sans marqueurs custom.
49
+ # Le modèle est instruit de ne retourner QUE le JSON, ce qui simplifie le parsing.
50
  PROMPT_TEMPLATE = """
51
+ [INST]Tu es un expert en analyse de données financières de la BRVM. Extrais les informations du texte suivant et retourne-les sous la forme d'un objet JSON unique et valide. Ta réponse doit commencer par `{` et se terminer par `}`. N'inclus aucun texte, explication ou formatage en dehors de l'objet JSON.
 
 
 
 
52
 
53
+ **JSON Schema attendu :**
54
+ ```json
55
  {{
56
  "indicateurs": {{
57
  "brvm_10": {{ "niveau": float, "var_jour_pct": float, "var_annuelle_pct": float }},
 
77
  {{ "code": string, "emetteur": string, "coupon_pct": float, "echeance": string, "nominal": float, "cours": float }}
78
  ]
79
  }}
80
+ ```
81
 
82
+ **Contraintes :**
83
+ - Si une donnée est manquante, utilise la valeur `null`.
84
+ - Utilise un point `.` comme séparateur décimal.
85
 
86
+ **Texte du bulletin à analyser :**
87
  {texte_pdf}[/INST]
88
  """
 
 
89
 
90
+ # ---------- FONCTIONS ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
+ def initialize_model_pipeline():
93
+ """
94
+ Charge le modèle et le tokenizer avec une quantification 4-bit et crée le pipeline.
95
+ Retourne le pipeline ou None en cas d'erreur.
96
+ """
 
 
 
97
  try:
98
+ print(f"Chargement du modèle '{MODEL_ID}' avec quantification 4-bit...")
99
+
100
+ # Configuration de la quantification pour réduire l'empreinte mémoire
101
+ bnb_config = BitsAndBytesConfig(
102
+ load_in_4bit=True,
103
+ bnb_4bit_quant_type="nf4", # Type de quantification (NormalFloat4) - bon équilibre performance/précision
104
+ bnb_4bit_compute_dtype=torch.bfloat16, # Type pour les calculs, bfloat16 est rapide sur les GPU récents
105
+ bnb_4bit_use_double_quant=True, # Améliore la précision avec une surcoût mémoire minime
106
+ )
 
107
 
108
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
109
+ model = AutoModelForCausalLM.from_pretrained(
110
+ MODEL_ID,
111
+ quantization_config=bnb_config,
112
+ device_map="auto", # Répartit automatiquement le modèle sur les ressources disponibles (GPU/CPU)
113
+ torch_dtype=torch.bfloat16,
114
+ trust_remote_code=True # Nécessaire pour certains modèles
115
+ )
116
 
117
+ # Assurer que le pad_token est défini pour éviter les avertissements
118
+ if tokenizer.pad_token is None:
119
+ tokenizer.pad_token = tokenizer.eos_token
 
 
120
 
121
+ extractor_pipeline = pipeline(
122
+ "text-generation",
123
+ model=model,
124
+ tokenizer=tokenizer,
125
+ )
126
+ print("✅ Modèle et pipeline chargés avec succès.")
127
+ return extractor_pipeline
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  except Exception as e:
129
+ print(f" Erreur critique lors du chargement du modèle : {e}")
130
+ return None
131
 
132
+ def download_and_extract_pdfs(repo_id, zip_filename, target_folder, cache_folder):
133
  """
134
+ Télécharge un ZIP depuis un dataset HF et en extrait les fichiers PDF.
 
135
  """
136
+ print(f"Téléchargement de '{zip_filename}' depuis le dataset '{repo_id}'...")
 
137
  try:
138
  local_zip_path = hf_hub_download(
139
+ repo_id=repo_id,
140
  filename=zip_filename,
141
  repo_type="dataset",
142
+ cache_dir=cache_folder
143
  )
144
+ print(f"Fichier ZIP téléchargé dans : {local_zip_path}")
145
+
146
+ target_folder.mkdir(parents=True, exist_ok=True)
147
 
148
  extracted_files = []
149
  with zipfile.ZipFile(local_zip_path, 'r') as z:
150
+ for member in z.infolist():
151
+ # Sécurité : Ignorer les chemins absolus ou contenant '..'
152
+ if member.is_dir() or member.filename.startswith('/') or '..' in member.filename:
153
  continue
 
 
154
 
155
+ if member.filename.lower().endswith('.pdf'):
156
+ target_path = target_folder / Path(member.filename).name
157
+ with z.open(member) as source, open(target_path, "wb") as target:
158
+ target.write(source.read())
159
+ extracted_files.append(target_path)
 
 
 
 
 
 
 
 
 
 
160
 
161
+ print(f"✅ {len(extracted_files)} fichiers PDF extraits dans '{target_folder}'.")
162
+ return extracted_files
163
  except Exception as e:
164
+ print(f" Erreur lors du téléchargement ou de l'extraction : {e}")
165
  return []
 
 
166
 
167
+ def extract_text_from_pdf(pdf_path):
168
+ """
169
+ Extrait le contenu textuel brut d'un fichier PDF.
170
+ """
171
+ try:
172
+ with fitz.open(pdf_path) as doc:
173
+ return "\n".join(page.get_text() for page in doc)
174
+ except Exception as e:
175
+ print(f"⚠️ Avertissement : Impossible de lire le PDF '{pdf_path.name}'. Erreur : {e}")
176
+ return ""
177
 
178
+ def parse_json_from_model_output(raw_output):
179
+ """
180
+ Extrait une chaîne JSON de la sortie brute du modèle.
181
+ Stratégie robuste : trouve la première '{' et la dernière '}'.
182
+ """
183
+ try:
184
+ # La réponse du modèle inclut le prompt, on ne garde que le texte généré
185
+ # Le marqueur [/INST] sépare le prompt de la réponse.
186
+ generated_text = raw_output.split("[/INST]")[-1]
187
 
188
+ start_index = generated_text.find('{')
189
+ end_index = generated_text.rfind('}')
 
190
 
191
+ if start_index != -1 and end_index != -1 and end_index > start_index:
192
+ json_str = generated_text[start_index : end_index + 1]
193
+ return json.loads(json_str)
194
+ else:
195
+ raise ValueError("Accolades JSON non trouvées dans la sortie.")
196
 
197
+ except json.JSONDecodeError as e:
198
+ print(f"Erreur de décodage JSON : {e}")
199
+ # En cas d'erreur, on retourne un objet d'erreur avec la sortie brute pour le débogage.
200
+ return {"error": "JSONDecodeError", "details": str(e), "raw_output": raw_output}
201
+ except Exception as e:
202
+ print(f"Erreur inattendue lors du parsing : {e}")
203
+ return {"error": "ParsingFailed", "details": str(e), "raw_output": raw_output}
204
+
205
 
206
+ def process_single_pdf(pdf_path, pipeline):
207
+ """
208
+ Traite un seul fichier PDF : extraction de texte, appel du modèle et parsing du JSON.
209
+ """
210
+ print(f"--- Traitement de : {pdf_path.name} ---")
211
+
212
+ # 1. Extraire le texte
213
+ text = extract_text_from_pdf(pdf_path)
214
+ if not text.strip():
215
+ return {"error": "PDF vide ou illisible", "source_file": pdf_path.name}
216
+
217
+ # 2. Préparer le prompt
218
+ prompt = PROMPT_TEMPLATE.format(texte_pdf=text[:30000]) # Tronquer pour être sûr de ne pas dépasser la limite de contexte
219
+
220
+ # 3. Appeler le modèle
221
+ try:
222
+ response = pipeline(
223
+ prompt,
224
+ max_new_tokens=MAX_NEW_TOKENS,
225
+ do_sample=False, # Pour des résultats déterministes
226
+ return_full_text=False, # Ne retourne que le texte généré
227
+ pad_token_id=pipeline.tokenizer.eos_token_id # Évite un avertissement
228
+ )
229
+ raw_output = response[0]['generated_text']
230
+
231
+ # 4. Parser le JSON
232
+ data = parse_json_from_model_output(f"[INST]{prompt}[/INST]{raw_output}") # Reconstituer pour le parser
233
+ data['source_file'] = pdf_path.name # Ajouter la source pour la traçabilité
234
+ return data
235
 
236
+ except Exception as e:
237
+ print(f"❌ Erreur lors de l'appel du pipeline pour '{pdf_path.name}': {e}")
238
+ return {"error": "PipelineExecutionError", "details": str(e), "source_file": pdf_path.name}
239
 
 
240
 
241
+ def upload_results_to_hf(local_folder, repo_id, hf_token):
242
+ """
243
+ Uploade le contenu d'un dossier vers un dataset sur le Hub Hugging Face.
244
+ """
245
+ if not hf_token:
246
+ print("⚠️ Avertissement : HF_TOKEN non configuré. L'upload est ignoré.")
247
+ print(f"Les résultats sont disponibles localement dans : {local_folder}")
248
+ return
249
+
250
+ try:
251
+ api = HfApi(token=hf_token)
252
+ repo_url = api.create_repo(repo_id, repo_type="dataset", exist_ok=True).repo_url
253
+
254
+ commit_message = f"Rapport d'extraction BRVM du {datetime.now().strftime('%Y-%m-%d %H:%M')}"
255
+
256
+ print(f"Début de l'upload de '{local_folder}' vers '{repo_id}'...")
257
+ api.upload_folder(
258
+ folder_path=str(local_folder),
259
+ repo_id=repo_id,
260
+ repo_type="dataset",
261
+ commit_message=commit_message
262
+ )
263
+ print(f"✅ Upload terminé avec succès ! Consultez les résultats sur : {repo_url}")
264
+ except Exception as e:
265
+ print(f"❌ Erreur lors de l'upload vers Hugging Face : {e}")
266
+ print("Veuillez vérifier votre HF_TOKEN et les permissions d'écriture sur le dépôt.")
267
+
268
+
269
+ def main():
270
+ """
271
+ Fonction principale orchestrant le processus complet.
272
+ """
273
+ # Initialisation du modèle en premier pour échouer rapidement si nécessaire
274
+ extractor_pipeline = initialize_model_pipeline()
275
+ if not extractor_pipeline:
276
+ return # Arrêt si le modèle ne peut pas être chargé
277
+
278
+ # Téléchargement et extraction des PDFs
279
+ pdf_files = download_and_extract_pdfs(
280
+ HF_DATASET_PDFS_REPO_ID,
281
+ ZIP_FILENAME_IN_DATASET,
282
+ LOCAL_PDF_FOLDER,
283
+ LOCAL_CACHE_DIR
284
+ )
285
+ if not pdf_files:
286
+ print("Aucun PDF à traiter. Arrêt du script.")
287
+ return
288
+
289
+ # Création d'un dossier de sortie unique pour cette exécution
290
+ run_id = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + "_" + uuid.uuid4().hex[:8]
291
+ local_output_dir = LOCAL_JSON_OUTPUT_BASE_FOLDER / run_id
292
+ local_output_dir.mkdir(parents=True, exist_ok=True)
293
+ print(f"Les résultats JSON seront sauvegardés dans : {local_output_dir}")
294
+
295
+ all_results = []
296
+ for pdf_path in tqdm(pdf_files, desc="Traitement des PDFs"):
297
+ result = process_single_pdf(pdf_path, extractor_pipeline)
298
+ all_results.append(result)
299
+
300
+ # Sauvegarder le résultat individuel
301
+ output_json_path = local_output_dir / f"{pdf_path.stem}.json"
302
+ with open(output_json_path, "w", encoding="utf-8") as f:
303
+ json.dump(result, f, ensure_ascii=False, indent=2)
304
+
305
+ # Sauvegarder le fichier agrégé
306
+ aggregate_file_path = local_output_dir / "_aggregate_results.json"
307
+ with open(aggregate_file_path, "w", encoding="utf-8") as f:
308
+ json.dump(all_results, f, ensure_ascii=False, indent=2)
309
+
310
+ print(f"Traitement terminé. {len(pdf_files)} fichiers traités.")
311
+
312
+ # Upload des résultats
313
+ upload_results_to_hf(local_output_dir, HF_DATASET_JSON_REPO_ID, HF_TOKEN)
314
 
315
 
316
  if __name__ == "__main__":