vic3610 commited on
Commit
f99c886
·
verified ·
1 Parent(s): d2fdb39

Create analyze_bob_hf.py

Browse files
Files changed (1) hide show
  1. analyze_bob_hf.py +537 -0
analyze_bob_hf.py ADDED
@@ -0,0 +1,537 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script d'analyse des transcriptions avec Hugging Face Transformers
4
+ Analyse les fichiers txt dans output/transcriptions et génère un résumé structuré
5
+ """
6
+
7
+ import os
8
+ from pathlib import Path
9
+ import torch
10
+ from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
11
+ from datetime import datetime
12
+ import re
13
+
14
+ # Bootstrap environnement portable
15
+ try:
16
+ from portable_env import setup_portable_env
17
+ setup_portable_env()
18
+ except Exception:
19
+ pass
20
+
21
+ # Configuration (via env, avec fallback local)
22
+ BASE_DIR = Path(os.environ.get("BOB_BASE_DIR", Path(__file__).parent.parent))
23
+ TRANSCRIPTIONS_DIR = Path(os.environ.get("BOB_TRANSCRIPTIONS_DIR", BASE_DIR / "output" / "transcriptions"))
24
+ OUTPUT_FILE = Path(os.environ.get("BOB_OUTPUT_FILE", BASE_DIR / "output" / "resume_bob.txt"))
25
+ HF_MODEL = os.environ.get("HF_MODEL", "meta-llama/Llama-3.2-1B-Instruct") # par défaut
26
+
27
+ def load_hf_model():
28
+ """Charge un modèle Hugging Face"""
29
+ try:
30
+ print(f"Chargement du modèle Hugging Face: {HF_MODEL}")
31
+
32
+ # Utiliser pipeline pour plus de simplicité
33
+ generator = pipeline(
34
+ "text-generation",
35
+ model=HF_MODEL,
36
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
37
+ device_map="auto" if torch.cuda.is_available() else "cpu",
38
+ token=os.environ.get("HF_TOKEN") # Pour les modèles privés
39
+ )
40
+
41
+ print(f"Modèle {HF_MODEL} chargé avec succès")
42
+ return generator
43
+ except Exception as e:
44
+ print(f"Erreur lors du chargement du modèle Hugging Face: {e}")
45
+ print("Assurez-vous que le modèle est disponible et que vous avez les permissions nécessaires")
46
+ return None
47
+
48
+ def create_analysis_prompt():
49
+ """Crée le prompt d'analyse"""
50
+ return """RÔLE: Expert en classification de contenu journalistique RTL.
51
+
52
+ TÂCHE: Extraire 3 informations précises de cette transcription radio :
53
+
54
+ 1. AUTEUR : Nom complet du journaliste/présentateur
55
+ - Chercher "les précisions pour RTL de [NOM]" ou signature en fin
56
+ - Si absent : "Inconnu"
57
+
58
+ 2. QUALIFICATION du format (TRÈS IMPORTANT) :
59
+ - P = PAPIER seul : Lecture continue par le journaliste, pas d'interviews
60
+ • Phrases à la 3e personne uniquement
61
+ • Aucune citation directe de témoins
62
+ • Style narratif/descriptif pur
63
+
64
+ - P+S = PAPIER + SON : Reportage avec interviews/témoignages
65
+ • Présence de citations directes ("Je...", "Nous...")
66
+ • Témoignages de personnes citées par leur prénom
67
+ • Alternance narratif + paroles rapportées
68
+ • Phrases comme "explique Alexandre", "témoigne Lucas"
69
+
70
+ - QR = QUESTIONS-RÉPONSES : Interview/débat en direct
71
+ • Format conversationnel
72
+ • Questions-réponses explicites
73
+ • Dialogue en temps réel
74
+
75
+ 3. TITRE : Sujet principal en 4-6 mots, MAJUSCULES, style presse
76
+
77
+ INDICES DE DÉTECTION P+S :
78
+ - Citations à la 1ère personne : "J'ai été hospitalisé", "Nous avons commencé"
79
+ - Prénoms + témoignages : "Alexandre explique", "Lucas raconte"
80
+ - Discours rapporté : "Il dit que", "Ils nous ont dit"
81
+ - Changement de ton narratif
82
+
83
+ FORMAT OBLIGATOIRE :
84
+ AUTEUR|QUALIFICATION|TITRE"""
85
+
86
+ def detect_format_indicators(text):
87
+ """Détecte automatiquement les indicateurs de format P/P+S/QR/MT"""
88
+ indicators = {
89
+ 'p_plus_s': 0, # Papier + Son
90
+ 'qr': 0, # Questions-Réponses
91
+ 'mt': 0, # Micro-Trottoir
92
+ 'p_only': 0 # Papier seul
93
+ }
94
+
95
+ text_lower = text.lower()
96
+
97
+ # Indicateurs Micro-Trottoir (MT)
98
+ mt_patterns = [
99
+ r'\bmoi je (?:trouve|pense|crois|dis)',
100
+ r'\bje trouve (?:que|ça|dommage)',
101
+ r'\bje pense que',
102
+ r'\bpour moi',
103
+ r'\bà mon avis',
104
+ r'\bfranchement',
105
+ r'en arrivant',
106
+ r'je viens (?:de|d\')',
107
+ r'aujourd\'hui',
108
+ r'c\'est dommage',
109
+ r'malheureusement',
110
+ r'donc je (?:voulais|pense)',
111
+ r'quand même',
112
+ r'un petit peu',
113
+ r'vraiment dommage',
114
+ ]
115
+
116
+ # Indicateurs P+S (Papier + Son)
117
+ p_plus_s_patterns = [
118
+ r'\bje\s+(?:suis|ai|me|pense|crois|vais|veux|dois)',
119
+ r'\bnous\s+(?:avons|sommes|étions|allons|devons)',
120
+ r'\bj\'(?:ai|étais|avais|irai|aurais)',
121
+ r'\bmon\s+(?:père|fils|mari|frère)',
122
+ r'\bma\s+(?:mère|fille|femme|sœur)',
123
+ r'\b(?:explique|témoigne|raconte|confie|précise|ajoute|poursuit)\s+\w+',
124
+ r'\b\w+\s+(?:explique|témoigne|raconte|confie|précise|ajoute|poursuit)',
125
+ r'selon\s+\w+',
126
+ r'(?:il|elle|ils|elles)\s+(?:dit|disent|explique|expliquent|affirme|assure)\s+que',
127
+ r'pour\s+\w+\s*,',
128
+ r'comme\s+(?:le\s+)?(?:dit|explique|précise)\s+\w+',
129
+ r'voilà ce à quoi',
130
+ r'c\'est qu?\'?à? partir',
131
+ r'certains d\'entre (?:nous|eux)',
132
+ r'parmi les\s+\d+',
133
+ r'\b[A-Z][a-z]+\s+qui\s+(?:est|a|était)',
134
+ r'comme\s+[A-Z][a-z]+',
135
+ r'fièvre\s+et\s+\w+',
136
+ r'hospitalisé',
137
+ r'symptômes',
138
+ r'malade',
139
+ ]
140
+
141
+ # Indicateurs QR (Questions-Réponses)
142
+ qr_patterns = [
143
+ r'\?.*[A-Z]', # Question suivie de réponse
144
+ r'question\s*:',
145
+ r'réponse\s*:',
146
+ r'vous\s+(?:pensez|croyez|dites)',
147
+ r'que\s+pensez-vous',
148
+ r'interview',
149
+ r'débat',
150
+ ]
151
+
152
+ # Compter les patterns
153
+ for pattern in mt_patterns:
154
+ indicators['mt'] += len(re.findall(pattern, text_lower, re.IGNORECASE))
155
+
156
+ for pattern in p_plus_s_patterns:
157
+ indicators['p_plus_s'] += len(re.findall(pattern, text_lower, re.IGNORECASE))
158
+
159
+ for pattern in qr_patterns:
160
+ indicators['qr'] += len(re.findall(pattern, text_lower, re.IGNORECASE))
161
+
162
+ # Si ni MT ni P+S ni QR détecté fortement, c'est probablement P seul
163
+ if indicators['mt'] < 3 and indicators['p_plus_s'] < 3 and indicators['qr'] < 2:
164
+ indicators['p_only'] = 5
165
+
166
+ return indicators
167
+
168
+ def analyze_transcription(generator, transcription_text, filename):
169
+ """Analyse une transcription avec Hugging Face"""
170
+ try:
171
+ # Analyse automatique des patterns
172
+ format_indicators = detect_format_indicators(transcription_text)
173
+
174
+ # Créer un hint pour l'IA basé sur l'analyse automatique
175
+ max_score = max(format_indicators.values())
176
+ likely_format = [k for k, v in format_indicators.items() if v == max_score][0]
177
+
178
+ hint_map = {
179
+ 'p_plus_s': "ATTENTION: Nombreux témoignages détectés → OBLIGATOIREMENT P+S",
180
+ 'qr': "ATTENTION: Format questions-réponses détecté → OBLIGATOIREMENT QR",
181
+ 'p_only': "ATTENTION: Aucun témoignage/interview → OBLIGATOIREMENT P"
182
+ }
183
+
184
+ # Si détection très claire (score élevé), forcer le format
185
+ force_format = ""
186
+ if format_indicators['p_plus_s'] >= 5:
187
+ force_format = "\nFORMAT IMPOSÉ: Utilise OBLIGATOIREMENT 'P+S' pour la qualification."
188
+ elif format_indicators['qr'] >= 3:
189
+ force_format = "\nFORMAT IMPOSÉ: Utilise OBLIGATOIREMENT 'QR' pour la qualification."
190
+ elif format_indicators['p_plus_s'] <= 1 and format_indicators['qr'] <= 1:
191
+ force_format = "\nFORMAT IMPOSÉ: Utilise OBLIGATOIREMENT 'P' pour la qualification."
192
+
193
+ format_hint = hint_map.get(likely_format, "")
194
+
195
+ prompt = create_analysis_prompt()
196
+ enhanced_prompt = f"{prompt}\n\n{format_hint}{force_format}"
197
+
198
+ # Format pour Llama
199
+ full_prompt = f"<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n\n{enhanced_prompt}\n\nTRANSCRIPTION:\n{transcription_text}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n"
200
+
201
+ print(f"Analyse de: {filename}")
202
+ print(f" Indices détectés: MT={format_indicators['mt']}, P+S={format_indicators['p_plus_s']}, QR={format_indicators['qr']}, P={format_indicators['p_only']}")
203
+
204
+ # Génération avec le modèle
205
+ response = generator(
206
+ full_prompt,
207
+ max_new_tokens=150,
208
+ temperature=0.2,
209
+ top_k=20,
210
+ top_p=0.8,
211
+ repetition_penalty=1.15,
212
+ do_sample=True,
213
+ pad_token_id=generator.tokenizer.eos_token_id
214
+ )
215
+
216
+ result = response[0]['generated_text'].replace(full_prompt, '').strip()
217
+
218
+ # Parser le résultat
219
+ result = result.replace('\n', ' ').strip()
220
+
221
+ if "|" in result:
222
+ # Prendre la première ligne qui contient des |
223
+ lines = result.split('\n')
224
+ for line in lines:
225
+ if "|" in line and line.count("|") >= 2:
226
+ parts = line.split("|")
227
+ if len(parts) >= 3:
228
+ auteur = parts[0].strip()
229
+ qualification = parts[1].strip()
230
+ titre = parts[2].strip()
231
+
232
+ # Nettoyer et formater
233
+ if auteur.lower() in ["inconnu", "non mentionné", "auteur", ""]:
234
+ auteur = "Inconnu"
235
+
236
+ # Valider la qualification
237
+ if qualification.upper() not in ["P", "P+S", "SON", "MT", "QR"]:
238
+ qualification = "P" # Défaut
239
+
240
+ return {
241
+ "success": True,
242
+ "auteur": auteur,
243
+ "qualification": qualification.upper(),
244
+ "titre": titre.upper(),
245
+ "filename": filename
246
+ }
247
+
248
+ # Si pas de format avec |, essayer de parser différemment
249
+ if " - " in result:
250
+ parts = result.split(" - ")
251
+ if len(parts) >= 3:
252
+ auteur = parts[0].strip()
253
+ qualification = parts[1].strip()
254
+ titre = " - ".join(parts[2:]).strip()
255
+
256
+ if auteur.lower() in ["inconnu", "non mentionné", "auteur", ""]:
257
+ auteur = "Inconnu"
258
+
259
+ # Valider la qualification
260
+ if qualification.upper() not in ["P", "P+S", "QR"]:
261
+ qualification = "P"
262
+
263
+ return {
264
+ "success": True,
265
+ "auteur": auteur,
266
+ "qualification": qualification.upper(),
267
+ "titre": titre.upper(),
268
+ "filename": filename
269
+ }
270
+
271
+ # Si le format n'est pas correct
272
+ return {
273
+ "success": False,
274
+ "error": f"Format de réponse incorrect: {result}",
275
+ "filename": filename,
276
+ "raw_response": result
277
+ }
278
+
279
+ except Exception as e:
280
+ return {
281
+ "success": False,
282
+ "error": str(e),
283
+ "filename": filename
284
+ }
285
+
286
+ def read_transcription_file(file_path):
287
+ """Lit le contenu d'un fichier de transcription et extrait les métadonnées"""
288
+ try:
289
+ with open(file_path, 'r', encoding='utf-8') as f:
290
+ content = f.read()
291
+
292
+ # Extraire les métadonnées
293
+ metadata = {}
294
+ lines = content.split('\n')
295
+
296
+ for line in lines[:10]: # Chercher dans les 10 premières lignes
297
+ if line.startswith('Fichier source:'):
298
+ metadata['filename'] = line.replace('Fichier source:', '').strip()
299
+ elif line.startswith('Durée de traitement:'):
300
+ metadata['processing_time'] = line.replace('Durée de traitement:', '').strip()
301
+
302
+ # Extraire seulement le texte de transcription (après les métadonnées)
303
+ if "--------------------------------------------------" in content:
304
+ parts = content.split("--------------------------------------------------")
305
+ if len(parts) > 1:
306
+ text_content = parts[1].strip()
307
+ return text_content, metadata
308
+
309
+ return content.strip(), metadata
310
+
311
+ except Exception as e:
312
+ print(f"Erreur lors de la lecture de {file_path}: {e}")
313
+ return None, {}
314
+
315
+ def apply_duration_correction(result, duration_seconds, format_indicators=None):
316
+ """Applique une correction probabiliste basée sur la durée et les patterns détectés"""
317
+ if not duration_seconds:
318
+ return result
319
+
320
+ original_qualification = result.get("qualification", "")
321
+ corrected = False
322
+
323
+ # Priorité 1: Détection Micro-Trottoir basée sur patterns + durée
324
+ if format_indicators and format_indicators.get('mt', 0) >= 8 and duration_seconds < 60:
325
+ if original_qualification in ["P", "P+S", "SON"]:
326
+ result["qualification"] = "MT"
327
+ corrected = True
328
+ print(f" → Correction MT détecté: {original_qualification} → MT (patterns={format_indicators['mt']})")
329
+
330
+ # Priorité 2: Logique probabiliste selon durée (si pas MT)
331
+ elif not corrected:
332
+ if duration_seconds < 30:
333
+ # < 30s = quasi-certainement un SON
334
+ if original_qualification in ["P", "P+S"]:
335
+ result["qualification"] = "SON"
336
+ corrected = True
337
+ print(f" → Correction durée < 30s: {original_qualification} → SON")
338
+
339
+ elif 30 <= duration_seconds <= 40:
340
+ # 30-40s = probablement un SON, mais peut être P
341
+ if original_qualification == "P+S":
342
+ result["qualification"] = "SON"
343
+ corrected = True
344
+ print(f" → Correction durée 30-40s: P+S → SON")
345
+
346
+ return result
347
+
348
+ def extract_author_from_filename(filename):
349
+ """Extrait le nom du journaliste depuis le nom du fichier"""
350
+ try:
351
+ # Nettoyer le nom du fichier
352
+ clean_name = filename.replace('_transcription.txt', '').replace('.mp3', '').replace('.MP3', '')
353
+
354
+ # Patterns courants pour extraire un nom (prénom + nom)
355
+ words = clean_name.split()
356
+
357
+ # Chercher une séquence de 2 mots qui commencent par une majuscule
358
+ for i in range(len(words) - 1):
359
+ word1 = words[i].strip('()[]{}.,;:!?-_')
360
+ word2 = words[i + 1].strip('()[]{}.,;:!?-_')
361
+
362
+ # Vérifier si les deux mots ressemblent à un prénom + nom
363
+ if (len(word1) >= 2 and len(word2) >= 2 and
364
+ word1[0].isupper() and word2[0].isupper() and
365
+ word1.isalpha() and word2.isalpha()):
366
+ return f"{word1} {word2}"
367
+
368
+ # Si pas trouvé, chercher le premier mot qui commence par une majuscule
369
+ for word in words:
370
+ clean_word = word.strip('()[]{}.,;:!?-_')
371
+ if len(clean_word) >= 2 and clean_word[0].isupper() and clean_word.isalpha():
372
+ # Essayer de trouver le mot suivant
373
+ word_index = words.index(word)
374
+ if word_index + 1 < len(words):
375
+ next_word = words[word_index + 1].strip('()[]{}.,;:!?-_')
376
+ if len(next_word) >= 2 and next_word[0].isupper() and next_word.isalpha():
377
+ return f"{clean_word} {next_word}"
378
+ return clean_word
379
+
380
+ return "Inconnu"
381
+
382
+ except Exception as e:
383
+ print(f"Erreur extraction auteur de {filename}: {e}")
384
+ return "Inconnu"
385
+
386
+ def get_audio_duration(audio_filename, input_dir):
387
+ """Calcule la durée d'un fichier audio en secondes totales"""
388
+ try:
389
+ from pydub import AudioSegment
390
+ audio_path = None
391
+ audio_extensions = ['.mp3', '.wav', '.m4a', '.flac', '.ogg', '.mp4', '.avi', '.mov']
392
+ # Rechercher le fichier audio correspondant
393
+ for ext in audio_extensions:
394
+ potential_path = input_dir / audio_filename.replace('_transcription.txt', ext)
395
+ if potential_path.exists():
396
+ audio_path = potential_path
397
+ break
398
+ base_name = audio_filename.replace('_transcription.txt', '')
399
+ potential_path = input_dir / f"{base_name}{ext}"
400
+ if potential_path.exists():
401
+ audio_path = potential_path
402
+ break
403
+ if audio_path:
404
+ audio = AudioSegment.from_file(str(audio_path))
405
+ duration_seconds = len(audio) / 1000 # pydub retourne en millisecondes
406
+ minutes = int(duration_seconds // 60)
407
+ seconds = int(duration_seconds % 60)
408
+ return minutes * 100 + seconds
409
+ return None
410
+ except Exception as e:
411
+ print(f"Erreur calcul durée pour {audio_filename}: {e}")
412
+ return None
413
+
414
+ def get_transcription_files(transcriptions_dir):
415
+ """Récupère tous les fichiers de transcription"""
416
+ if not transcriptions_dir.exists():
417
+ print(f"Le dossier {transcriptions_dir} n'existe pas")
418
+ return []
419
+ txt_files = list(transcriptions_dir.glob("*_transcription.txt"))
420
+ return sorted(txt_files)
421
+
422
+ def main():
423
+ """Fonction principale"""
424
+ print("=" * 60)
425
+ print("ANALYSE DES BOB AVEC HUGGING FACE")
426
+ print("=" * 60)
427
+
428
+ # Vérification des dossiers
429
+ script_dir = Path(__file__).parent.absolute()
430
+ transcriptions_dir = Path(os.environ.get("BOB_TRANSCRIPTIONS_DIR", script_dir.parent / "output" / "transcriptions"))
431
+ output_file = Path(os.environ.get("BOB_OUTPUT_FILE", script_dir.parent / "output" / "resume_bob.txt"))
432
+ input_dir = Path(os.environ.get("BOB_INPUT_DIR", script_dir.parent / "input"))
433
+
434
+ # Utiliser la fonction factorisée avec print comme log
435
+ analyze_files_hf(
436
+ transcriptions_dir=transcriptions_dir,
437
+ input_dir=input_dir,
438
+ output_file=output_file,
439
+ log_fn=print,
440
+ progress_fn=None,
441
+ cancel_fn=None,
442
+ )
443
+
444
+ if __name__ == "__main__":
445
+ main()
446
+
447
+ # --- API factorisée pour le GUI ---
448
+ def analyze_files_hf(transcriptions_dir: Path, input_dir: Path, output_file: Path, log_fn=print, progress_fn=None, cancel_fn=None):
449
+ """Analyse tous les fichiers de transcription avec Hugging Face"""
450
+ log = log_fn or (lambda *a, **k: None)
451
+
452
+ log("Dossier transcriptions: {}".format(transcriptions_dir))
453
+ log("Fichier de sortie: {}".format(output_file))
454
+ log("")
455
+
456
+ transcription_files = get_transcription_files(transcriptions_dir)
457
+ if not transcription_files:
458
+ log("Aucun fichier de transcription trouvé")
459
+ log("Assurez-vous d'avoir exécuté le script de transcription d'abord")
460
+ return {"success": False, "count": 0}
461
+
462
+ log(f"Trouvé {len(transcription_files)} fichier(s) de transcription:")
463
+ for i, file in enumerate(transcription_files, 1):
464
+ log(f" {i}. {file.name}")
465
+ log("")
466
+
467
+ # Initialisation du modèle Hugging Face
468
+ generator = load_hf_model()
469
+ if not generator:
470
+ return {"success": False, "error": "Modèle Hugging Face indisponible"}
471
+
472
+ results = []
473
+ success_count = 0
474
+ total = len(transcription_files)
475
+
476
+ for i, file_path in enumerate(transcription_files, 1):
477
+ if cancel_fn and cancel_fn():
478
+ log("⏹️ Analyse annulée")
479
+ break
480
+
481
+ log(f"[{i}/{total}] ")
482
+
483
+ transcription_text, metadata = read_transcription_file(file_path)
484
+ if not transcription_text:
485
+ log(f"✗ Impossible de lire {file_path.name}")
486
+ if progress_fn:
487
+ progress_fn(i, total)
488
+ continue
489
+
490
+ duration = get_audio_duration(file_path.name, input_dir)
491
+ author_from_filename = extract_author_from_filename(file_path.name)
492
+
493
+ result = analyze_transcription(generator, transcription_text, file_path.name)
494
+
495
+ if result["success"]:
496
+ format_indicators = detect_format_indicators(transcription_text)
497
+ result = apply_duration_correction(result, duration, format_indicators)
498
+ if result["auteur"].lower() in ["inconnu", "non mentionné", "auteur", ""]:
499
+ result["auteur"] = author_from_filename
500
+ result["duree"] = duration if duration else "000"
501
+ result["filename_source"] = metadata.get("filename", file_path.name)
502
+ log(f"✓ {result['auteur']} - {result['qualification']} - {result['titre']} - {result['duree']}")
503
+ results.append(result)
504
+ success_count += 1
505
+ else:
506
+ log(f"✗ Erreur: {result['error']}")
507
+ if "raw_response" in result:
508
+ log(f" Réponse brute: {result['raw_response']}")
509
+
510
+ if progress_fn:
511
+ progress_fn(i, total)
512
+ log("")
513
+
514
+ if results:
515
+ output_file.parent.mkdir(parents=True, exist_ok=True)
516
+ with open(output_file, 'w', encoding='utf-8') as f:
517
+ f.write(f"# RÉSUMÉ DES BOB - {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}\n")
518
+ f.write(f"# Format: Auteur | Qualification | Titre | Durée\n")
519
+ f.write("# Qualification: P=papier, P+S=papier+son, QR=question-réponse\n")
520
+ f.write("# Durée: format MMss (ex: 1min04 = 104)\n")
521
+ f.write("# " + "="*70 + "\n\n")
522
+ for r in results:
523
+ line = f"{r['auteur']} | {r['qualification']} | {r['titre']} | {r['duree']}"
524
+ f.write(line + "\n")
525
+
526
+ log("=" * 60)
527
+ log("RÉSUMÉ GÉNÉRÉ")
528
+ log("=" * 60)
529
+ log(f"Fichiers analysés: {total}")
530
+ log(f"Analyses réussies: {success_count}")
531
+ log(f"Analyses échouées: {total - success_count}")
532
+ log(f"Fichier de résumé: {output_file}")
533
+
534
+ return {"success": True, "count": total, "ok": success_count, "results": results}
535
+ else:
536
+ log("Aucune analyse réussie, pas de fichier de résumé généré")
537
+ return {"success": False, "count": total, "ok": 0}