lamekemal commited on
Commit
f32ba89
·
1 Parent(s): d63235e
Files changed (1) hide show
  1. app.py +93 -59
app.py CHANGED
@@ -11,41 +11,39 @@ import fitz
11
  import json
12
  from pathlib import Path
13
  from tqdm import tqdm
14
- from transformers import pipeline
 
15
  import requests
16
  import zipfile
17
  import io
18
  import os
19
- from huggingface_hub import hf_hub_download, HfApi # Importation pour télécharger et uploader
20
  from datetime import datetime
21
- import uuid # Pour générer un identifiant unique
22
 
23
  # ---------- CONFIG ----------
24
- MODEL = "mistralai/Mistral-7B-Instruct-v0.3" # Modèle Hugging Face à utiliser
25
 
26
- # Hugging Face Dataset d'où télécharger le fichier ZIP des PDF
27
- HF_DATASET_REPO_ID_PDFS = "lamekemal/brvm-reports-pdfs" # L'ID de votre dépôt de dataset pour les PDF
28
- ZIP_FILENAME_IN_DATASET = "brvm_reports.zip" # Le nom du fichier ZIP à l'intérieur de ce dataset
29
 
30
- # Hugging Face Dataset où uploader les résultats JSON
31
  HF_DATASET_REPO_ID_JSON_OUTPUT = "lamekemal/brvm-reports-json"
32
 
33
- # Vous devez ajouter votre jeton Hugging Face (avec rôle "Write" pour l'upload)
34
- # comme un secret dans les paramètres de votre Hugging Face Space.
35
- # Le nom du secret DOIT être HF_TOKEN.
36
- # os.getenv('HF_TOKEN') récupérera automatiquement cette valeur.
37
  HF_TOKEN = os.getenv('HF_TOKEN')
38
 
39
- # Dossiers locaux pour le traitement
40
- PDF_FOLDER = "brvm_reports" # Dossier où les PDF seront extraits et traités
41
- LOCAL_OUT_BASE_FOLDER = "local_json_outputs" # Dossier de base local pour les sorties JSON temporaires
42
  # ----------------------------
43
 
 
44
  PROMPT_TEMPLATE = """
45
- Tu es un expert en finance spécialisé dans la BRVM.
46
  À partir du texte ci-dessous issu d’un bulletin officiel de la cote BRVM,
47
- extrait uniquement les données suivantes et retourne un JSON valide au format strict :
 
 
48
 
 
49
  {{
50
  "indicateurs": {{
51
  "brvm_10": {{ "niveau": float, "var_jour_pct": float, "var_annuelle_pct": float }},
@@ -73,27 +71,49 @@ extrait uniquement les données suivantes et retourne un JSON valide au format s
73
  }}
74
 
75
  Contraintes :
76
- - Ne mets aucun texte hors du JSON
77
- - Si une donnée est absente, mets null
78
- - Les nombres utilisent un point comme séparateur décimal
79
 
80
  Texte du bulletin :
81
- {texte_pdf}
82
  """
 
 
83
 
84
- # --- Initialisation du pipeline Hugging Face ---
85
  try:
86
- print("Chargement du modèle Hugging Face...")
87
- # Le pipeline utilisera automatiquement le jeton HF_TOKEN si disponible en tant que secret/variable d'environnement
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  extractor = pipeline(
89
  "text-generation",
90
- model=MODEL,
 
91
  device=0 # Utilise le GPU si disponible (0 pour le premier GPU, -1 pour CPU)
92
  )
93
- print("Modèle chargé avec succès.")
94
  except Exception as e:
95
  print(f"Erreur lors du chargement du modèle Hugging Face : {e}")
96
- extractor = None
97
 
98
  # --- Fonctions d'extraction et de traitement ---
99
  def extract_text(pdf_path):
@@ -109,32 +129,63 @@ def extract_text(pdf_path):
109
  def call_huggingface_model(text):
110
  """Appel du modèle Hugging Face pour extraire le JSON à partir du texte."""
111
  if extractor is None:
 
112
  return {"error": "Modèle non chargé."}
113
 
114
  prompt = PROMPT_TEMPLATE.format(texte_pdf=text)
115
 
116
  try:
117
- # Ajuster max_new_tokens si les réponses JSON sont tronquées
118
- response = extractor(prompt, max_new_tokens=2048)
 
 
 
 
 
 
119
 
120
- # Le résultat est souvent une liste de dictionnaires, on prend le premier
121
  output_text = response[0]['generated_text']
122
 
123
- # Il se peut que le modèle répète le prompt, on doit donc extraire la partie JSON
124
- json_start = output_text.find('{')
125
- json_end = output_text.rfind('}') + 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
  if json_start != -1 and json_end != -1:
128
- json_text = output_text[json_start:json_end]
129
- return json.loads(json_text)
 
 
 
 
 
 
 
 
130
  else:
131
- return {"error": "JSON non trouvé dans la réponse", "raw": output_text}
132
 
133
  except json.JSONDecodeError:
134
- print(f"Erreur JSONDecodeError: {output_text}") # Afficher le texte brut qui a causé l'erreur JSON
135
- return {"error": "JSON invalide", "raw": output_text}
136
  except Exception as e:
137
- return {"error": str(e)}
138
 
139
  def download_and_extract_zip_from_hf_dataset(dataset_repo_id, zip_filename, target_folder):
140
  """
@@ -144,34 +195,28 @@ def download_and_extract_zip_from_hf_dataset(dataset_repo_id, zip_filename, targ
144
  print(f"Tentative de téléchargement du fichier '{zip_filename}' "
145
  f"depuis le dataset Hugging Face : {dataset_repo_id}")
146
  try:
147
- # Télécharger le fichier ZIP depuis le dataset Hugging Face Hub
148
  local_zip_path = hf_hub_download(
149
  repo_id=dataset_repo_id,
150
  filename=zip_filename,
151
  repo_type="dataset",
152
- cache_dir="./hf_cache" # Vous pouvez spécifier un répertoire de cache
153
  )
154
  print(f"Fichier ZIP téléchargé localement : {local_zip_path}")
155
 
156
- # Créer le dossier cible s'il n'existe pas
157
  Path(target_folder).mkdir(parents=True, exist_ok=True)
158
 
159
  extracted_files = []
160
  with zipfile.ZipFile(local_zip_path, 'r') as z:
161
  for file_info in z.infolist():
162
- # Construire le chemin complet du fichier extrait
163
- # Éviter les chemins absolus ou les traversées de répertoire pour la sécurité
164
  if file_info.filename.startswith('/') or '..' in file_info.filename:
165
  print(f"Skipping potentially unsafe path: {file_info.filename}")
166
  continue
167
 
168
  extracted_path = Path(target_folder) / file_info.filename
169
 
170
- # Créer les sous-dossiers si nécessaire
171
  if file_info.is_dir():
172
  extracted_path.mkdir(parents=True, exist_ok=True)
173
  elif file_info.filename.lower().endswith('.pdf'):
174
- # Extraire uniquement les fichiers PDF
175
  print(f"Extraction de : {file_info.filename} vers {extracted_path}")
176
  try:
177
  with extracted_path.open("wb") as outfile:
@@ -189,13 +234,11 @@ def download_and_extract_zip_from_hf_dataset(dataset_repo_id, zip_filename, targ
189
  print(f"Une erreur est survenue lors du téléchargement ou de l'extraction depuis Hugging Face Dataset : {e}")
190
  return []
191
  finally:
192
- # Laisser hf_hub_download gérer son propre cache.
193
  pass
194
 
195
  # --- Fonction principale ---
196
  def main():
197
  """Fonction principale pour exécuter l'ensemble du processus d'extraction."""
198
- # 1. Télécharger et extraire les PDF depuis le Hugging Face Dataset
199
  downloaded_pdfs = download_and_extract_zip_from_hf_dataset(
200
  HF_DATASET_REPO_ID_PDFS, ZIP_FILENAME_IN_DATASET, PDF_FOLDER
201
  )
@@ -204,8 +247,6 @@ def main():
204
  print("Aucun fichier PDF n'a pu être téléchargé ou extrait. Arrêt du processus.")
205
  return
206
 
207
- # 2. Préparer le dossier de sortie LOCAL unique pour les JSON
208
- # Créer un nom de dossier unique basé sur la date/heure et un UUID
209
  unique_folder_name = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + "_" + str(uuid.uuid4())[:8]
210
  local_out_dir = Path(LOCAL_OUT_BASE_FOLDER) / unique_folder_name
211
  local_out_dir.mkdir(parents=True, exist_ok=True)
@@ -223,31 +264,25 @@ def main():
223
  data = call_huggingface_model(text)
224
  aggregate.append(data)
225
 
226
- # Sauvegarde par fichier dans le dossier local unique
227
  out_path = local_out_dir / f"{pdf_file.stem}.json"
228
  with open(out_path, "w", encoding="utf-8") as f:
229
  json.dump(data, f, ensure_ascii=False, indent=2)
230
 
231
- # Sauvegarde du fichier global aggrégé dans le dossier local unique
232
  with open(local_out_dir / "brvm_aggregate.json", "w", encoding="utf-8") as f:
233
  json.dump(aggregate, f, ensure_ascii=False, indent=2)
234
 
235
  print(f"[OK] Extraction terminée - {len(downloaded_pdfs)} fichiers traités dans {local_out_dir}.")
236
 
237
- # 3. Uploader les résultats vers le Hugging Face Dataset dédié
238
  if HF_TOKEN:
239
  try:
240
  api = HfApi(token=HF_TOKEN)
241
  print(f"Début de l'upload des résultats vers {HF_DATASET_REPO_ID_JSON_OUTPUT}/{unique_folder_name}...")
242
 
243
- # Utilisation de upload_large_folder pour les gros volumes de fichiers
244
- api.upload_folder( # Note: upload_folder est recommandé même pour les "large folders" pour les datasets
245
- # C'est le message d'avertissement qui est trompeur.
246
- # L'API gère LFS en interne pour les gros fichiers.
247
  folder_path=str(local_out_dir),
248
  repo_id=HF_DATASET_REPO_ID_JSON_OUTPUT,
249
  repo_type="dataset",
250
- path_in_repo=unique_folder_name, # Le dossier à créer dans le dataset
251
  commit_message=f"Extraction BRVM du {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
252
  )
253
  print(f"[OK] Upload terminé vers https://huggingface.co/datasets/{HF_DATASET_REPO_ID_JSON_OUTPUT}/tree/main/{unique_folder_name}")
@@ -260,6 +295,5 @@ def main():
260
  print(f"Les fichiers sont disponibles localement dans le Space à l'adresse : {local_out_dir}")
261
 
262
 
263
- # Point d'entrée du script
264
  if __name__ == "__main__":
265
  main()
 
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 }},
 
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):
 
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
  """
 
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:
 
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
  )
 
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)
 
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}")
 
295
  print(f"Les fichiers sont disponibles localement dans le Space à l'adresse : {local_out_dir}")
296
 
297
 
 
298
  if __name__ == "__main__":
299
  main()