lamekemal commited on
Commit
ad83998
·
1 Parent(s): 9866a34
Files changed (3) hide show
  1. app.py +50 -298
  2. requirements.txt +1 -0
  3. script_brvm.py +148 -0
app.py CHANGED
@@ -1,323 +1,75 @@
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 }},
58
- "brvm_composite": {{ "niveau": float, "var_jour_pct": float, "var_annuelle_pct": float }},
59
- "capitalisation_actions_fcfa": float,
60
- "capitalisation_obligations_fcfa": float,
61
- "volume_echange": float,
62
- "valeur_transigee_fcfa": float
63
- }},
64
- "plus_fortes_hausses": [
65
- {{ "symbol": string, "nom": string, "cours": float, "var_jour_pct": float, "var_annuelle_pct": float }}
66
- ],
67
- "plus_fortes_baisses": [
68
- {{ "symbol": string, "nom": string, "cours": float, "var_jour_pct": float, "var_annuelle_pct": float }}
69
- ],
70
- "actions": [
71
- {{ "symbol": string, "nom": string, "cours_jour": float, "var_jour_pct": float, "volume": float, "valeur_fcfa": float, "dernier_dividende": float|null, "date_dividende": string|null }}
72
- ],
73
- "dividendes": [
74
- {{ "symbol": string, "nom": string, "montant_fcfa": float, "date_paiement": string }}
75
- ],
76
- "obligations": [
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 et tente de la réparer.
181
- """
182
- try:
183
- # 1. Isoler le texte généré par le modèle
184
- generated_text = raw_output.split("[/INST]")[-1].strip()
185
-
186
- # 2. Trouver les accolades de début et de fin
187
- start_index = generated_text.find('{')
188
- end_index = generated_text.rfind('}')
189
-
190
- if start_index != -1 and end_index != -1 and end_index > start_index:
191
- json_str = generated_text[start_index : end_index + 1]
192
-
193
- # 3. Tenter de réparer les guillemets simples sur les clés
194
- # Attention : ceci est une solution de contournement, le modèle devrait générer du JSON valide.
195
- # Un regex simple pour remplacer les clés non-entre-guillemets
196
- # json_str = re.sub(r'([\{\s,]+)(\w+)(:)', r'\1"\2"\3', json_str)
197
- #
198
- # Pour la démonstration, on se contente de la tentative de parsing
199
- return json.loads(json_str)
200
- else:
201
- raise ValueError("Accolades JSON non trouvées dans la sortie.")
202
-
203
- except json.JSONDecodeError as e:
204
- print(f"Erreur de décodage JSON : {e}")
205
- # Retourner la sortie brute pour l'analyse
206
- return {"error": "JSONDecodeError", "details": str(e), "raw_output": raw_output}
207
- except Exception as e:
208
- print(f"Erreur inattendue lors du parsing : {e}")
209
- return {"error": "ParsingFailed", "details": str(e), "raw_output": raw_output}
210
-
211
- def process_single_pdf(pdf_path, pipeline):
212
- """
213
- Traite un seul fichier PDF : extraction de texte, appel du modèle et parsing du JSON.
214
- """
215
- print(f"--- Traitement de : {pdf_path.name} ---")
216
-
217
- # 1. Extraire le texte
218
- text = extract_text_from_pdf(pdf_path)
219
- if not text.strip():
220
- return {"error": "PDF vide ou illisible", "source_file": pdf_path.name}
221
 
222
- # 2. Préparer le prompt
223
- prompt = PROMPT_TEMPLATE.format(texte_pdf=text[:30000]) # Tronquer pour être sûr de ne pas dépasser la limite de contexte
 
224
 
225
- # 3. Appeler le modèle
226
- try:
227
- response = pipeline(
228
- prompt,
229
- max_new_tokens=MAX_NEW_TOKENS,
230
- temperature=0.2,
231
- do_sample=False, # Pour des résultats déterministes
232
- return_full_text=False, # Ne retourne que le texte généré
233
- pad_token_id=pipeline.tokenizer.eos_token_id # Évite un avertissement
234
  )
235
- raw_output = response[0]['generated_text']
236
-
237
- # 4. Parser le JSON
238
- data = parse_json_from_model_output(f"[INST]{prompt}[/INST]{raw_output}") # Reconstituer pour le parser
239
- data['source_file'] = pdf_path.name # Ajouter la source pour la traçabilité
240
- return data
241
-
242
- except Exception as e:
243
- print(f"❌ Erreur lors de l'appel du pipeline pour '{pdf_path.name}': {e}")
244
- return {"error": "PipelineExecutionError", "details": str(e), "source_file": pdf_path.name}
245
-
246
 
247
- def upload_results_to_hf(local_folder, repo_id, hf_token):
248
- """
249
- Uploade le contenu d'un dossier vers un dataset sur le Hub Hugging Face.
250
- """
251
- if not hf_token:
252
- print("⚠️ Avertissement : HF_TOKEN non configuré. L'upload est ignoré.")
253
- print(f"Les résultats sont disponibles localement dans : {local_folder}")
254
- return
255
-
256
- try:
257
- api = HfApi(token=hf_token)
258
- repo_url = api.create_repo(repo_id, repo_type="dataset", exist_ok=True).repo_url
259
-
260
- commit_message = f"Rapport d'extraction BRVM du {datetime.now().strftime('%Y-%m-%d %H:%M')}"
261
-
262
- print(f"Début de l'upload de '{local_folder}' vers '{repo_id}'...")
263
- api.upload_folder(
264
- folder_path=str(local_folder),
265
- repo_id=repo_id,
266
- repo_type="dataset",
267
- commit_message=commit_message
268
- )
269
- print(f"✅ Upload terminé avec succès ! Consultez les résultats sur : {repo_url}")
270
- except Exception as e:
271
- print(f"❌ Erreur lors de l'upload vers Hugging Face : {e}")
272
- print("Veuillez vérifier votre HF_TOKEN et les permissions d'écriture sur le dépôt.")
273
 
 
 
274
 
275
- def main():
276
- """
277
- Fonction principale orchestrant le processus complet.
278
- """
279
- # Initialisation du modèle en premier pour échouer rapidement si nécessaire
280
- extractor_pipeline = initialize_model_pipeline()
281
- if not extractor_pipeline:
282
- return # Arrêt si le modèle ne peut pas être chargé
283
 
284
- # Téléchargement et extraction des PDFs
285
- pdf_files = download_and_extract_pdfs(
286
- HF_DATASET_PDFS_REPO_ID,
287
- ZIP_FILENAME_IN_DATASET,
288
- LOCAL_PDF_FOLDER,
289
- LOCAL_CACHE_DIR
290
- )
291
- if not pdf_files:
292
- print("Aucun PDF à traiter. Arrêt du script.")
293
- return
294
 
295
- # Création d'un dossier de sortie unique pour cette exécution
296
- run_id = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + "_" + uuid.uuid4().hex[:8]
297
- local_output_dir = LOCAL_JSON_OUTPUT_BASE_FOLDER / run_id
298
- local_output_dir.mkdir(parents=True, exist_ok=True)
299
- print(f"Les résultats JSON seront sauvegardés dans : {local_output_dir}")
300
 
301
- all_results = []
302
- for pdf_path in tqdm(pdf_files, desc="Traitement des PDFs"):
303
- result = process_single_pdf(pdf_path, extractor_pipeline)
304
- all_results.append(result)
305
 
306
- # Sauvegarder le résultat individuel
307
- output_json_path = local_output_dir / f"{pdf_path.stem}.json"
308
- with open(output_json_path, "w", encoding="utf-8") as f:
309
- json.dump(result, f, ensure_ascii=False, indent=2)
310
 
311
- # Sauvegarder le fichier agrégé
312
- aggregate_file_path = local_output_dir / "_aggregate_results.json"
313
- with open(aggregate_file_path, "w", encoding="utf-8") as f:
314
- json.dump(all_results, f, ensure_ascii=False, indent=2)
315
 
316
- print(f"Traitement terminé. {len(pdf_files)} fichiers traités.")
317
-
318
- # Upload des résultats
319
- upload_results_to_hf(local_output_dir, HF_DATASET_JSON_REPO_ID, HF_TOKEN)
320
-
321
 
322
- if __name__ == "__main__":
323
- main()
 
1
+ # app.py
 
 
 
2
 
3
+ import gradio as gr
4
+ import threading
 
 
 
 
 
 
 
 
 
5
  import json
6
  from pathlib import Path
 
 
 
 
 
7
  from datetime import datetime
8
  import uuid
9
+ import os
10
 
11
+ from script_brvm import (
12
+ initialize_model_pipeline,
13
+ download_and_extract_pdfs,
14
+ process_single_pdf,
15
+ upload_results_to_hf_single
16
+ )
17
 
18
+ # ---------- CONFIGURATION ----------
19
  HF_DATASET_PDFS_REPO_ID = "lamekemal/brvm-reports-pdfs"
 
20
  ZIP_FILENAME_IN_DATASET = "brvm_reports.zip"
 
 
 
21
  LOCAL_PDF_FOLDER = Path("brvm_reports_extracted")
 
22
  LOCAL_CACHE_DIR = Path("./hf_cache")
23
+ HF_TOKEN = os.getenv("HF_TOKEN")
24
+ HF_DATASET_JSON_REPO_ID = "lamekemal/brvm-reports-json"
25
+ LOCAL_JSON_OUTPUT_BASE_FOLDER = Path("brvm_json_outputs")
26
 
27
+ extractor_pipeline = None
28
+ processed_files = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ def load_model():
31
+ global extractor_pipeline
32
+ extractor_pipeline = initialize_model_pipeline()
33
 
34
+ def start_background_processing(status_box):
35
+ def background_task():
36
+ pdf_files = download_and_extract_pdfs(
37
+ HF_DATASET_PDFS_REPO_ID,
38
+ ZIP_FILENAME_IN_DATASET,
39
+ LOCAL_PDF_FOLDER,
40
+ LOCAL_CACHE_DIR
 
 
41
  )
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ run_id = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + "_" + uuid.uuid4().hex[:8]
44
+ local_output_dir = LOCAL_JSON_OUTPUT_BASE_FOLDER / run_id
45
+ local_output_dir.mkdir(parents=True, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ for pdf_path in pdf_files:
48
+ result = process_single_pdf(pdf_path, extractor_pipeline)
49
 
50
+ output_json_path = local_output_dir / f"{pdf_path.stem}.json"
51
+ with open(output_json_path, "w", encoding="utf-8") as f:
52
+ json.dump(result, f, ensure_ascii=False, indent=2)
 
 
 
 
 
53
 
54
+ upload_results_to_hf_single(result, HF_DATASET_JSON_REPO_ID, HF_TOKEN)
 
 
 
 
 
 
 
 
 
55
 
56
+ processed_files.append(pdf_path.name)
57
+ status_box.update(value="\n".join(processed_files))
 
 
 
58
 
59
+ thread = threading.Thread(target=background_task)
60
+ thread.start()
 
 
61
 
62
+ def launch_processing(status_box):
63
+ start_background_processing(status_box)
64
+ return " Traitement lancé."
 
65
 
66
+ with gr.Blocks() as demo:
67
+ gr.Markdown("# 📊 Extraction BRVM automatisée")
68
+ gr.Markdown("Le modèle est chargé au démarrage. Cliquez sur le bouton pour lancer le traitement des bulletins.")
 
69
 
70
+ status_box = gr.Textbox(label="Fichiers traités", lines=20)
71
+ launch_button = gr.Button("🚀 Lancer le traitement")
72
+ launch_button.click(launch_processing, inputs=[status_box], outputs=[status_box])
 
 
73
 
74
+ load_model()
75
+ demo.launch()
requirements.txt CHANGED
@@ -7,3 +7,4 @@ huggingface_hub
7
  bitsandbytes
8
  sentencepiece # NOUVELLE DÉPENDANCE
9
  accelerate
 
 
7
  bitsandbytes
8
  sentencepiece # NOUVELLE DÉPENDANCE
9
  accelerate
10
+ gradio
script_brvm.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # script_brvm.py
2
+
3
+ import fitz # PyMuPDF
4
+ import json
5
+ from pathlib import Path
6
+ import torch
7
+ from transformers import pipeline, AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
8
+ import os
9
+ from huggingface_hub import hf_hub_download, HfApi
10
+ from datetime import datetime
11
+ import zipfile
12
+
13
+ # ---------- CONFIGURATION ----------
14
+ MODEL_ID = "mistralai/Mistral-7B-Instruct-v0.3"
15
+ PROMPT_TEMPLATE = """
16
+ [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.
17
+
18
+ **Texte du bulletin à analyser :**
19
+ {texte_pdf}[/INST]
20
+ """
21
+
22
+ MAX_NEW_TOKENS = 8192
23
+
24
+ # ---------- MODÈLE ----------
25
+ def initialize_model_pipeline():
26
+ try:
27
+ bnb_config = BitsAndBytesConfig(
28
+ load_in_4bit=True,
29
+ bnb_4bit_quant_type="nf4",
30
+ bnb_4bit_compute_dtype=torch.bfloat16,
31
+ bnb_4bit_use_double_quant=True,
32
+ )
33
+
34
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
35
+ model = AutoModelForCausalLM.from_pretrained(
36
+ MODEL_ID,
37
+ quantization_config=bnb_config,
38
+ device_map="auto",
39
+ torch_dtype=torch.bfloat16,
40
+ trust_remote_code=True
41
+ )
42
+
43
+ if tokenizer.pad_token is None:
44
+ tokenizer.pad_token = tokenizer.eos_token
45
+
46
+ extractor_pipeline = pipeline(
47
+ "text-generation",
48
+ model=model,
49
+ tokenizer=tokenizer,
50
+ )
51
+ return extractor_pipeline
52
+ except Exception as e:
53
+ print(f"Erreur chargement modèle : {e}")
54
+ return None
55
+
56
+ # ---------- PDF ----------
57
+ def download_and_extract_pdfs(repo_id, zip_filename, target_folder, cache_folder):
58
+ try:
59
+ local_zip_path = hf_hub_download(
60
+ repo_id=repo_id,
61
+ filename=zip_filename,
62
+ repo_type="dataset",
63
+ cache_dir=cache_folder
64
+ )
65
+
66
+ target_folder.mkdir(parents=True, exist_ok=True)
67
+ extracted_files = []
68
+
69
+ with zipfile.ZipFile(local_zip_path, 'r') as z:
70
+ for member in z.infolist():
71
+ if member.is_dir() or member.filename.startswith('/') or '..' in member.filename:
72
+ continue
73
+ if member.filename.lower().endswith('.pdf'):
74
+ target_path = target_folder / Path(member.filename).name
75
+ with z.open(member) as source, open(target_path, "wb") as target:
76
+ target.write(source.read())
77
+ extracted_files.append(target_path)
78
+
79
+ return extracted_files
80
+ except Exception as e:
81
+ print(f"Erreur extraction PDF : {e}")
82
+ return []
83
+
84
+ def extract_text_from_pdf(pdf_path):
85
+ try:
86
+ with fitz.open(pdf_path) as doc:
87
+ return "\n".join(page.get_text() for page in doc)
88
+ except Exception as e:
89
+ print(f"Erreur lecture PDF : {e}")
90
+ return ""
91
+
92
+ # ---------- TRAITEMENT ----------
93
+ def parse_json_from_model_output(raw_output):
94
+ try:
95
+ generated_text = raw_output.split("[/INST]")[-1].strip()
96
+ start_index = generated_text.find('{')
97
+ end_index = generated_text.rfind('}')
98
+ if start_index != -1 and end_index != -1 and end_index > start_index:
99
+ json_str = generated_text[start_index : end_index + 1]
100
+ return json.loads(json_str)
101
+ else:
102
+ raise ValueError("Accolades JSON non trouvées.")
103
+ except Exception as e:
104
+ return {"error": "ParsingFailed", "details": str(e), "raw_output": raw_output}
105
+
106
+ def process_single_pdf(pdf_path, pipeline):
107
+ text = extract_text_from_pdf(pdf_path)
108
+ if not text.strip():
109
+ return {"error": "PDF vide", "source_file": pdf_path.name}
110
+
111
+ prompt = PROMPT_TEMPLATE.format(texte_pdf=text[:30000])
112
+ try:
113
+ response = pipeline(
114
+ prompt,
115
+ max_new_tokens=MAX_NEW_TOKENS,
116
+ temperature=0.2,
117
+ do_sample=False,
118
+ return_full_text=False,
119
+ pad_token_id=pipeline.tokenizer.eos_token_id
120
+ )
121
+ raw_output = response[0]['generated_text']
122
+ data = parse_json_from_model_output(f"[INST]{prompt}[/INST]{raw_output}")
123
+ data['source_file'] = pdf_path.name
124
+ return data
125
+ except Exception as e:
126
+ return {"error": "PipelineError", "details": str(e), "source_file": pdf_path.name}
127
+
128
+ # ---------- UPLOAD ----------
129
+ def upload_results_to_hf_single(result, repo_id, hf_token):
130
+ if not hf_token:
131
+ print("HF_TOKEN manquant.")
132
+ return
133
+ try:
134
+ api = HfApi(token=hf_token)
135
+ temp_path = Path("temp_result.json")
136
+ with open(temp_path, "w", encoding="utf-8") as f:
137
+ json.dump(result, f, ensure_ascii=False, indent=2)
138
+
139
+ api.upload_file(
140
+ path_or_fileobj=str(temp_path),
141
+ repo_id=repo_id,
142
+ repo_type="dataset",
143
+ path_in_repo=f"{result['source_file']}.json",
144
+ commit_message=f"Upload {result['source_file']}"
145
+ )
146
+ temp_path.unlink()
147
+ except Exception as e:
148
+ print(f"Erreur upload : {e}")