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

Create bob_gui_hf.py

Browse files
Files changed (1) hide show
  1. bob_gui_hf.py +450 -0
bob_gui_hf.py ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ BOB Processor - Interface graphique (Hugging Face version)
4
+ Application GUI pour traiter automatiquement les fichiers audio BOB
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ import threading
10
+ import time
11
+ from pathlib import Path
12
+ from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
13
+ QWidget, QPushButton, QLabel, QProgressBar, QTextEdit,
14
+ QFileDialog, QMessageBox, QFrame, QGridLayout, QCheckBox,
15
+ QComboBox, QGroupBox)
16
+ from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer
17
+ from PyQt5.QtGui import QFont, QPixmap, QIcon
18
+
19
+ # Imports des modules de traitement
20
+ # Bootstrap environnement portable avant toute importation lourde
21
+ try:
22
+ from portable_env import setup_portable_env
23
+ setup_portable_env()
24
+ except Exception as _e:
25
+ # En cas d'échec du bootstrap, continuer mais loguer plus tard
26
+ pass
27
+
28
+ from transcribe_audio import transcribe_file, load_whisper_model, get_audio_files
29
+ from analyze_bob_hf import analyze_files_hf # Version Hugging Face
30
+
31
+ # Masquer la console Windows si présente
32
+ def _hide_windows_console():
33
+ if os.name == 'nt':
34
+ try:
35
+ import ctypes
36
+ hwnd = ctypes.windll.kernel32.GetConsoleWindow()
37
+ if hwnd:
38
+ ctypes.windll.user32.ShowWindow(hwnd, 0)
39
+ except Exception:
40
+ pass
41
+
42
+ class WorkerThread(QThread):
43
+ """Thread pour le traitement en arrière-plan"""
44
+ progress = pyqtSignal(int)
45
+ log = pyqtSignal(str)
46
+ finished = pyqtSignal(str)
47
+
48
+ def __init__(self, input_dir, output_dir, whisper_model, hf_model_name: str, fast_mode: bool):
49
+ super().__init__()
50
+ self.input_dir = Path(input_dir)
51
+ self.output_dir = Path(output_dir)
52
+ self.whisper_model = whisper_model
53
+ self.hf_model_name = hf_model_name
54
+ self.fast_mode = fast_mode
55
+ self.is_cancelled = False
56
+
57
+ def run(self):
58
+ try:
59
+ # Créer les dossiers de sortie
60
+ transcriptions_dir = self.output_dir / "transcriptions"
61
+ transcriptions_dir.mkdir(parents=True, exist_ok=True)
62
+
63
+ self.log.emit("🚀 Début du traitement BOB...")
64
+ self.log.emit(f"📁 Dossier d'entrée: {self.input_dir}")
65
+ self.log.emit(f"📁 Dossier de sortie: {self.output_dir}")
66
+
67
+ # Étape 1: Récupérer les fichiers audio
68
+ audio_files = get_audio_files(self.input_dir)
69
+ if not audio_files:
70
+ self.log.emit("❌ Aucun fichier audio trouvé!")
71
+ self.finished.emit("Aucun fichier audio trouvé dans le dossier sélectionné.")
72
+ return
73
+
74
+ total_files = len(audio_files)
75
+ self.log.emit(f"📊 {total_files} fichier(s) audio trouvé(s)")
76
+
77
+ # Étape 2: Chargement du modèle Whisper
78
+ self.log.emit(f"🧠 Chargement du modèle Whisper ({self.whisper_model})...")
79
+ model = load_whisper_model(self.whisper_model)
80
+ self.log.emit("✅ Modèle Whisper chargé avec succès!")
81
+
82
+ # Étape 3: Transcription des fichiers
83
+ self.log.emit("🎵 Début de la transcription...")
84
+ for i, audio_file in enumerate(audio_files):
85
+ if self.is_cancelled:
86
+ break
87
+
88
+ self.log.emit(f"📝 [{i+1}/{total_files}] Transcription: {audio_file.name}")
89
+
90
+ # Transcription
91
+ success = transcribe_file(model, audio_file, transcriptions_dir)
92
+
93
+ if success:
94
+ self.log.emit(f"✅ Transcrit: {audio_file.name}")
95
+ else:
96
+ self.log.emit(f"❌ Erreur: {audio_file.name}")
97
+
98
+ # Mise à jour du progrès (70% pour transcription)
99
+ progress = int(((i + 1) / total_files) * 70)
100
+ self.progress.emit(progress)
101
+
102
+ if self.is_cancelled:
103
+ self.finished.emit("Traitement annulé par l'utilisateur.")
104
+ return
105
+
106
+ # Étape 4: Analyse avec Hugging Face
107
+ self.log.emit("🤖 Début de l'analyse avec Hugging Face...")
108
+ self.log.emit(f"🤖 Modèle HF sélectionné: {self.hf_model_name} | Mode rapide: {'ON' if self.fast_mode else 'OFF'}")
109
+
110
+ # Rediriger temporairement les prints vers notre log
111
+ import io
112
+ import contextlib
113
+
114
+ # Capturer les sorties
115
+ captured_output = io.StringIO()
116
+ with contextlib.redirect_stdout(captured_output):
117
+ try:
118
+ # Configurer les variables d'environnement
119
+ os.environ["HF_MODEL"] = self.hf_model_name
120
+
121
+ # Exécuter l'analyse avec progression
122
+ def prog(cur, total):
123
+ base = 70
124
+ span = 30
125
+ val = base + int((cur / total) * span)
126
+ self.progress.emit(min(99, val))
127
+ def cancelled():
128
+ return self.is_cancelled
129
+ def logger(*args, **kwargs):
130
+ msg = " ".join(str(a) for a in args)
131
+ print(msg)
132
+
133
+ result = analyze_files_hf(
134
+ transcriptions_dir=transcriptions_dir,
135
+ input_dir=Path(self.input_dir),
136
+ output_file=Path(self.output_dir) / "resume_bob.txt",
137
+ log_fn=logger,
138
+ progress_fn=prog,
139
+ cancel_fn=cancelled,
140
+ )
141
+
142
+ except Exception as e:
143
+ self.log.emit(f"❌ Erreur lors de l'analyse: {e}")
144
+
145
+ # Récupérer et afficher les logs d'analyse
146
+ analysis_output = captured_output.getvalue()
147
+ for line in analysis_output.split('\n'):
148
+ if line.strip():
149
+ self.log.emit(line)
150
+
151
+ self.progress.emit(100)
152
+ self.log.emit("🎉 Traitement terminé avec succès!")
153
+
154
+ # Vérifier le fichier de résultat
155
+ result_file = self.output_dir / "resume_bob.txt"
156
+ if result_file.exists():
157
+ self.finished.emit(f"Traitement terminé! Résultats sauvegardés dans:\n{result_file}")
158
+ else:
159
+ self.finished.emit("Traitement terminé mais fichier de résultat introuvable.")
160
+
161
+ except Exception as e:
162
+ self.log.emit(f"❌ Erreur critique: {e}")
163
+ self.finished.emit(f"Erreur lors du traitement: {e}")
164
+
165
+ def cancel(self):
166
+ self.is_cancelled = True
167
+
168
+ class BOBProcessorGUI(QMainWindow):
169
+ def __init__(self):
170
+ super().__init__()
171
+ self.worker_thread = None
172
+ self.input_dir = None
173
+ self.output_dir = None
174
+ # Chrono
175
+ self.elapsed_seconds = 0
176
+ self.timer = QTimer(self)
177
+ self.timer.timeout.connect(self._tick_elapsed)
178
+ self.init_ui()
179
+
180
+ def init_ui(self):
181
+ self.setWindowTitle("BOB Processor - Hugging Face Version")
182
+ self.setGeometry(100, 100, 800, 600)
183
+
184
+ # Widget central
185
+ central_widget = QWidget()
186
+ self.setCentralWidget(central_widget)
187
+
188
+ # Layout principal
189
+ layout = QVBoxLayout(central_widget)
190
+
191
+ # Titre
192
+ title = QLabel("🎵 BOB 🤖 (Hugging Face)")
193
+ title.setAlignment(Qt.AlignCenter)
194
+ title.setFont(QFont("Arial", 18, QFont.Bold))
195
+ title.setStyleSheet("color: #2c3e50; margin: 10px;")
196
+ layout.addWidget(title)
197
+
198
+ subtitle = QLabel("Transcription automatique et analyse intelligente des sujets")
199
+ subtitle.setAlignment(Qt.AlignCenter)
200
+ subtitle.setFont(QFont("Arial", 10))
201
+ subtitle.setStyleSheet("color: #7f8c8d; margin-bottom: 20px;")
202
+ layout.addWidget(subtitle)
203
+
204
+ # Séparateur
205
+ line = QFrame()
206
+ line.setFrameShape(QFrame.HLine)
207
+ line.setFrameShadow(QFrame.Sunken)
208
+ layout.addWidget(line)
209
+
210
+ # Configuration
211
+ config_group = QGroupBox("⚙️ Configuration")
212
+ config_group.setFont(QFont("Arial", 12, QFont.Bold))
213
+ config_layout = QGridLayout(config_group)
214
+
215
+ # Sélection des dossiers
216
+ self.input_label = QLabel("📁 Dossier d'entrée (MP3): Aucun dossier sélectionné")
217
+ self.input_label.setFont(QFont("Arial", 11))
218
+ self.input_label.setStyleSheet("padding: 8px; background-color: #ecf0f1; border-radius: 3px; font-size: 11pt;")
219
+ config_layout.addWidget(self.input_label, 0, 0, 1, 2)
220
+
221
+ self.input_btn = QPushButton("Choisir dossier MP3")
222
+ self.input_btn.setFont(QFont("Arial", 11, QFont.Bold))
223
+ self.input_btn.clicked.connect(self.select_input_dir)
224
+ self.input_btn.setStyleSheet("padding: 10px; background-color: #3498db; color: white; border-radius: 3px; font-size: 11pt; font-weight: bold;")
225
+ config_layout.addWidget(self.input_btn, 0, 2)
226
+
227
+ self.output_label = QLabel("📁 Dossier de sortie: Aucun dossier sélectionné")
228
+ self.output_label.setFont(QFont("Arial", 11))
229
+ self.output_label.setStyleSheet("padding: 8px; background-color: #ecf0f1; border-radius: 3px; font-size: 11pt;")
230
+ config_layout.addWidget(self.output_label, 1, 0, 1, 2)
231
+
232
+ self.output_btn = QPushButton("Choisir dossier de sortie")
233
+ self.output_btn.setFont(QFont("Arial", 11, QFont.Bold))
234
+ self.output_btn.clicked.connect(self.select_output_dir)
235
+ self.output_btn.setStyleSheet("padding: 10px; background-color: #3498db; color: white; border-radius: 3px; font-size: 11pt; font-weight: bold;")
236
+ config_layout.addWidget(self.output_btn, 1, 2)
237
+
238
+ # Options
239
+ model_label = QLabel("🧠 Modèle Whisper:")
240
+ model_label.setFont(QFont("Arial", 11, QFont.Bold))
241
+ config_layout.addWidget(model_label, 2, 0)
242
+ self.model_combo = QComboBox()
243
+ self.model_combo.setFont(QFont("Arial", 11))
244
+ self.model_combo.addItems([
245
+ "medium (recommandé)",
246
+ "small (plus rapide mais moins précis)",
247
+ "large (plus précis mais plus lent)",
248
+ ])
249
+ self.model_combo.setCurrentText("medium (recommandé)")
250
+ self.model_combo.setStyleSheet("font-size: 11pt; padding: 5px;")
251
+ config_layout.addWidget(self.model_combo, 2, 1)
252
+
253
+ # Sélecteur de modèle HF pour l'analyse
254
+ hf_label = QLabel("🤖 Modèle HF (analyse):")
255
+ hf_label.setFont(QFont("Arial", 11, QFont.Bold))
256
+ config_layout.addWidget(hf_label, 3, 0)
257
+ self.hf_combo = QComboBox()
258
+ self.hf_combo.setFont(QFont("Arial", 11))
259
+ self.hf_combo.addItems([
260
+ "meta-llama/Llama-3.2-1B-Instruct (meilleur compromis)",
261
+ "microsoft/Phi-3-mini-4k-instruct (rapide)",
262
+ "mistralai/Mistral-7B-Instruct-v0.3 (meilleure qualité)",
263
+ ])
264
+ self.hf_combo.setCurrentText("meta-llama/Llama-3.2-1B-Instruct (meilleur compromis)")
265
+ self.hf_combo.setStyleSheet("font-size: 11pt; padding: 5px;")
266
+ config_layout.addWidget(self.hf_combo, 3, 1)
267
+
268
+ # Aide: format des noms de fichiers MP3
269
+ hint = QLabel("ℹ️ Les fichiers MP3 doivent contenir le nom du journaliste pour extraire l’auteur (ex: ‘Marie Dupont.mp3’).")
270
+ hint.setWordWrap(True)
271
+ hint.setFont(QFont("Arial", 9))
272
+ hint.setStyleSheet("color: #8c0000; padding-top: 6px;")
273
+ config_layout.addWidget(hint, 4, 0, 1, 3)
274
+
275
+ layout.addWidget(config_group)
276
+
277
+ # Contrôles
278
+ controls_layout = QHBoxLayout()
279
+
280
+ self.start_btn = QPushButton("▶️ DÉMARRER LE TRAITEMENT")
281
+ self.start_btn.setFont(QFont("Arial", 12, QFont.Bold))
282
+ self.start_btn.clicked.connect(self.start_processing)
283
+ self.start_btn.setEnabled(False)
284
+ self.start_btn.setStyleSheet("padding: 15px; background-color: #27ae60; color: white; font-weight: bold; border-radius: 5px; font-size: 12pt;")
285
+
286
+ self.cancel_btn = QPushButton("⏹️ ANNULER")
287
+ self.cancel_btn.setFont(QFont("Arial", 12, QFont.Bold))
288
+ self.cancel_btn.clicked.connect(self.cancel_processing)
289
+ self.cancel_btn.setEnabled(False)
290
+ self.cancel_btn.setStyleSheet("padding: 15px; background-color: #e74c3c; color: white; font-weight: bold; border-radius: 5px; font-size: 12pt;")
291
+
292
+ controls_layout.addWidget(self.start_btn)
293
+ controls_layout.addWidget(self.cancel_btn)
294
+ layout.addLayout(controls_layout)
295
+
296
+ # Barre de progression
297
+ self.progress_bar = QProgressBar()
298
+ self.progress_bar.setStyleSheet("QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; } QProgressBar::chunk { background-color: #3498db; }")
299
+ layout.addWidget(self.progress_bar)
300
+
301
+ # Chrono
302
+ self.elapsed_label = QLabel("⏱️ Temps écoulé: 00:00")
303
+ self.elapsed_label.setAlignment(Qt.AlignCenter)
304
+ self.elapsed_label.setFont(QFont("Arial", 10, QFont.Bold))
305
+ self.elapsed_label.setStyleSheet("color: #2c3e50; margin-bottom: 10px;")
306
+ layout.addWidget(self.elapsed_label)
307
+
308
+ # Zone de log
309
+ log_group = QGroupBox("📋 Journal d'exécution")
310
+ log_group.setFont(QFont("Arial", 12, QFont.Bold))
311
+ log_layout = QVBoxLayout(log_group)
312
+
313
+ self.log_text = QTextEdit()
314
+ self.log_text.setReadOnly(True)
315
+ self.log_text.setFont(QFont("Courier New", 10))
316
+ self.log_text.setStyleSheet("background-color: #2c3e50; color: #ecf0f1; font-family: 'Courier New'; font-size: 10pt;")
317
+ self.log_text.append("📝 Prêt à traiter vos fichiers BOB...")
318
+ log_layout.addWidget(self.log_text)
319
+
320
+ layout.addWidget(log_group)
321
+
322
+ # Vérifier les conditions initiales
323
+ self.check_start_conditions()
324
+
325
+ def select_input_dir(self):
326
+ dir_path = QFileDialog.getExistingDirectory(self, "Sélectionner le dossier contenant les fichiers MP3")
327
+ if dir_path:
328
+ self.input_dir = dir_path
329
+ # Tronquer le chemin s'il est trop long pour l'affichage
330
+ display_path = dir_path if len(dir_path) < 60 else "..." + dir_path[-57:]
331
+ self.input_label.setText(f"📁 Dossier d'entrée: {display_path}")
332
+ self.check_start_conditions()
333
+
334
+ def select_output_dir(self):
335
+ dir_path = QFileDialog.getExistingDirectory(self, "Sélectionner le dossier de sortie")
336
+ if dir_path:
337
+ self.output_dir = dir_path
338
+ # Tronquer le chemin s'il est trop long pour l'affichage
339
+ display_path = dir_path if len(dir_path) < 60 else "..." + dir_path[-57:]
340
+ self.output_label.setText(f"📁 Dossier de sortie: {display_path}")
341
+ self.check_start_conditions()
342
+
343
+ def check_start_conditions(self):
344
+ """Vérifie si toutes les conditions sont remplies pour démarrer"""
345
+ can_start = bool(self.input_dir and self.output_dir)
346
+ self.start_btn.setEnabled(can_start)
347
+
348
+ def start_processing(self):
349
+ """Démarre le traitement"""
350
+ if not self.input_dir or not self.output_dir:
351
+ QMessageBox.warning(self, "Erreur", "Veuillez sélectionner les dossiers d'entrée et de sortie.")
352
+ return
353
+
354
+ # Configuration du thread de traitement
355
+ model_text = self.model_combo.currentText()
356
+ whisper_model = model_text.split()[0] # Prendre le premier mot (small/medium/large)
357
+
358
+ # HF selection -> nom de modèle Hugging Face
359
+ hf_choice = self.hf_combo.currentText()
360
+ if "Llama-3.2-1B" in hf_choice:
361
+ hf_model_name = "meta-llama/Llama-3.2-1B-Instruct"
362
+ fast_mode = True
363
+ elif "Phi-3" in hf_choice:
364
+ hf_model_name = "microsoft/Phi-3-mini-4k-instruct"
365
+ fast_mode = True
366
+ else:
367
+ hf_model_name = "mistralai/Mistral-7B-Instruct-v0.3"
368
+ fast_mode = False
369
+
370
+ # Configuration des variables d'environnement
371
+ os.environ["BOB_INPUT_DIR"] = str(self.input_dir)
372
+ os.environ["BOB_TRANSCRIPTIONS_DIR"] = str(Path(self.output_dir) / "transcriptions")
373
+ os.environ["BOB_OUTPUT_FILE"] = str(Path(self.output_dir) / "resume_bob.txt")
374
+ os.environ["WHISPER_MODEL"] = whisper_model
375
+ os.environ["HF_MODEL"] = hf_model_name
376
+
377
+ self.worker_thread = WorkerThread(self.input_dir, self.output_dir, whisper_model, hf_model_name, fast_mode)
378
+ self.worker_thread.progress.connect(self.update_progress)
379
+ self.worker_thread.log.connect(self.add_log)
380
+ self.worker_thread.finished.connect(self.processing_finished)
381
+
382
+ # Interface en mode traitement
383
+ self.start_btn.setEnabled(False)
384
+ self.cancel_btn.setEnabled(True)
385
+ self.progress_bar.setValue(0)
386
+ self.log_text.clear()
387
+
388
+ # Démarrer le traitement
389
+ # Chrono: démarrer
390
+ self.elapsed_seconds = 0
391
+ self._update_elapsed_label()
392
+ self.timer.start(1000)
393
+
394
+ self.worker_thread.start()
395
+
396
+ def cancel_processing(self):
397
+ """Annule le traitement en cours"""
398
+ if self.worker_thread and self.worker_thread.isRunning():
399
+ self.worker_thread.cancel()
400
+ self.add_log("⏹️ Annulation en cours...")
401
+
402
+ def update_progress(self, value):
403
+ """Met à jour la barre de progression"""
404
+ self.progress_bar.setValue(value)
405
+
406
+ def add_log(self, message):
407
+ """Ajoute un message au journal"""
408
+ timestamp = time.strftime("%H:%M:%S")
409
+ self.log_text.append(f"[{timestamp}] {message}")
410
+
411
+ # Auto-scroll vers le bas
412
+ scrollbar = self.log_text.verticalScrollBar()
413
+ scrollbar.setValue(scrollbar.maximum())
414
+
415
+ def processing_finished(self, message):
416
+ """Traitement terminé"""
417
+ self.start_btn.setEnabled(True)
418
+ self.cancel_btn.setEnabled(False)
419
+ # Arrêter le chrono
420
+ if self.timer.isActive():
421
+ self.timer.stop()
422
+
423
+ # Afficher le message de fin
424
+ if "Erreur" in message or "annulé" in message:
425
+ QMessageBox.warning(self, "Traitement terminé", message)
426
+ else:
427
+ QMessageBox.information(self, "Succès!", message)
428
+
429
+ # Chrono helpers
430
+ def _tick_elapsed(self):
431
+ self.elapsed_seconds += 1
432
+ self._update_elapsed_label()
433
+
434
+ def _update_elapsed_label(self):
435
+ m = self.elapsed_seconds // 60
436
+ s = self.elapsed_seconds % 60
437
+ self.elapsed_label.setText(f"⏱️ Temps écoulé: {m:02d}:{s:02d}")
438
+
439
+ def main():
440
+ _hide_windows_console()
441
+ app = QApplication(sys.argv)
442
+ app.setStyle('Fusion')
443
+
444
+ window = BOBProcessorGUI()
445
+ window.show()
446
+
447
+ sys.exit(app.exec_())
448
+
449
+ if __name__ == "__main__":
450
+ main()