Adjoumani commited on
Commit
dccba65
·
verified ·
1 Parent(s): 5946d9f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +809 -192
app.py CHANGED
@@ -1,4 +1,3 @@
1
- # Imports nécessaires
2
  import os
3
  import uuid
4
 
@@ -58,24 +57,20 @@ from dotenv import load_dotenv
58
 
59
  load_dotenv()
60
 
61
- # Chargement des variables d'environnement
62
- load_dotenv()
63
  SENDER_EMAIL = os.environ.get('SENDER_EMAIL')
64
  SENDER_PASSWORD = os.environ.get('SENDER_PASSWORD')
65
 
66
- # Configuration globale
67
  class Config:
 
 
 
 
68
  FASTTEXT_MODEL_PATH = "lid.176.bin"
 
 
69
 
70
- # Téléchargement du modèle FastText si nécessaire
71
- if not os.path.exists(Config.FASTTEXT_MODEL_PATH):
72
- import urllib.request
73
- urllib.request.urlretrieve(
74
- 'https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.bin',
75
- Config.FASTTEXT_MODEL_PATH
76
- )
77
 
78
- # Classes principales
79
  class PDFGenerator:
80
  @staticmethod
81
  def create_pdf(content: str, filename: str) -> str:
@@ -89,6 +84,7 @@ class PDFGenerator:
89
  fontSize=12,
90
  leading=14,
91
  )
 
92
  story = []
93
  title_style = ParagraphStyle(
94
  'CustomTitle',
@@ -96,23 +92,24 @@ class PDFGenerator:
96
  fontSize=16,
97
  spaceAfter=30,
98
  )
99
- story.append(Paragraph("Résumé", title_style))
100
  story.append(Paragraph(f"Date: {datetime.now().strftime('%d/%m/%Y %H:%M')}", custom_style))
101
  story.append(Spacer(1, 20))
 
102
  for line in content.split('\n'):
103
  if line.strip():
104
  if line.startswith('#'):
105
  story.append(Paragraph(line.strip('# '), styles['Heading2']))
106
  else:
107
  story.append(Paragraph(line, custom_style))
 
108
  doc.build(story)
109
  return filename
110
 
111
-
112
  class EmailSender:
113
  def __init__(self, sender_email: str, sender_password: str):
114
- self.sender_email = sender_email
115
- self.sender_password = sender_password
116
 
117
  def send_email(self, recipient_email: str, subject: str, body: str, pdf_path: str) -> bool:
118
  try:
@@ -121,10 +118,12 @@ class EmailSender:
121
  msg['To'] = recipient_email
122
  msg['Subject'] = subject
123
  msg.attach(MIMEText(body, 'plain'))
 
124
  with open(pdf_path, 'rb') as f:
125
  pdf_attachment = MIMEApplication(f.read(), _subtype='pdf')
126
  pdf_attachment.add_header('Content-Disposition', 'attachment', filename=os.path.basename(pdf_path))
127
  msg.attach(pdf_attachment)
 
128
  server = smtplib.SMTP('smtp.gmail.com', 587)
129
  server.starttls()
130
  server.login(self.sender_email, self.sender_password)
@@ -135,92 +134,248 @@ class EmailSender:
135
  st.error(f"Erreur d'envoi d'email: {str(e)}")
136
  return False
137
 
138
-
139
  class AudioProcessor:
140
  def __init__(self, model_name: str, prompt: str = None, chunk_length_ms: int = 300000):
141
  self.chunk_length_ms = chunk_length_ms
142
- self.llm = ChatGroq(model=model_name, temperature=0)
 
 
 
 
 
143
  self.custom_prompt = prompt
144
  self.language_detector = fasttext.load_model(Config.FASTTEXT_MODEL_PATH)
145
- self.text_splitter = RecursiveCharacterTextSplitter(chunk_size=4000, chunk_overlap=200)
 
 
 
 
 
 
 
 
 
146
 
147
  def check_language(self, text: str) -> str:
 
148
  prediction = self.language_detector.predict(text.replace('\n', ' '))
149
  return "OUI" if prediction[0][0] == '__label__fr' else "NON"
150
 
151
  def translate_to_french(self, text: str) -> str:
152
- messages = [
153
- SystemMessage(content="Traduisez ce texte en français :"),
154
- HumanMessage(content=text)
155
- ]
156
- result = self._make_api_call(messages)
157
- return result.generations[0][0].text
 
 
 
 
 
 
 
158
 
 
159
  @limits(calls=5000, period=60)
160
  def _make_api_call(self, messages):
161
  return self.llm.generate([messages])
162
 
163
  def chunk_audio(self, file_path: str) -> List[AudioSegment]:
164
- audio = AudioSegment.from_file(file_path)
165
- return [
166
- audio[i:i + self.chunk_length_ms]
167
- for i in range(0, len(audio), self.chunk_length_ms)
168
- ]
 
 
 
 
 
 
169
 
170
  def transcribe_chunk(self, audio_chunk: AudioSegment) -> str:
171
- with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as temp_file:
172
- audio_chunk.export(temp_file.name, format="mp3")
173
- with open(temp_file.name, "rb") as audio_file:
174
- response = self.groq_client.audio.transcriptions.create(
175
- file=audio_file,
176
- model="whisper-large-v3-turbo",
177
- language="fr"
178
- )
179
- os.unlink(temp_file.name)
180
- return response.text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
  def generate_summary(self, transcription: str) -> str:
183
  default_prompt = """
 
 
 
 
 
 
 
 
 
 
 
 
184
  # Résumé
185
- [résumé ici]
 
186
  # Points Clés
187
  • [point 1]
188
  • [point 2]
 
 
189
  # Actions Recommandées
190
  1. [action 1]
191
  2. [action 2]
 
 
192
  # Conclusion
193
- [conclusion ici]
194
  """
195
- prompt_template = self.custom_prompt or default_prompt
196
- chain = LLMChain(
197
- llm=self.llm,
198
- prompt=PromptTemplate(template=prompt_template, input_variables=["transcript"])
199
- )
200
- summary = chain.run(transcript=transcription)
201
- if self.check_language(summary) == "NON":
202
- summary = self.translate_to_french(summary)
203
- return summary
204
-
205
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  class VideoProcessor:
207
  def __init__(self):
208
  self.supported_formats = ['.mp4', '.avi', '.mov', '.mkv']
209
- self.cookie_file_path = "cookies.txt"
210
-
 
 
 
 
 
 
 
 
 
 
211
  def load_cookies(self):
 
212
  dataset = load_dataset("Adjoumani/YoutubeCookiesDataset")
213
  cookies = dataset["train"]["cookies"][0]
214
  with open(self.cookie_file_path, "w") as f:
215
  f.write(cookies)
 
216
 
 
217
  def extract_video_id(self, url: str) -> str:
218
- parsed_url = urlparse(url)
219
- if parsed_url.hostname in ['www.youtube.com', 'youtube.com']:
220
- return parse_qs(parsed_url.query)['v'][0]
221
- elif parsed_url.hostname == 'youtu.be':
222
- return parsed_url.path[1:]
223
- return None
 
 
 
224
 
225
  def get_youtube_transcription(self, video_id: str) -> Optional[str]:
226
  try:
@@ -230,73 +385,275 @@ class VideoProcessor:
230
  return None
231
 
232
  def download_youtube_audio(self, url: str) -> str:
233
- ydl_opts = {
234
- 'format': 'bestaudio/best',
235
- 'postprocessors': [{
236
- 'key': 'FFmpegExtractAudio',
237
- 'preferredcodec': 'mp3',
238
- 'preferredquality': '192',
239
- }],
240
- 'outtmpl': 'temp_audio.%(ext)s',
241
- 'cookiefile': self.cookie_file_path,
242
- }
243
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
244
- ydl.download([url])
245
- return 'temp_audio.mp3'
246
 
247
- def extract_audio_from_video(self, video_path: str) -> str:
248
- audio_path = f"{os.path.splitext(video_path)[0]}.mp3"
249
- with VideoFileClip(video_path) as video:
250
- video.audio.write_audiofile(audio_path)
251
- return audio_path
 
 
 
252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
  class DocumentProcessor:
255
  def __init__(self, model_name: str, prompt: str = None):
256
- self.llm = ChatGroq(model=model_name, temperature=0)
 
 
 
 
257
  self.custom_prompt = prompt
258
- self.language_detector = fasttext.load_model(Config.FASTTEXT_MODEL_PATH)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
  def process_protected_pdf(self, file_path: str, password: str = None) -> str:
261
- if password:
262
- with pikepdf.open(file_path, password=password) as pdf:
263
- unlocked_pdf_path = "unlocked_temp.pdf"
264
- pdf.save(unlocked_pdf_path)
 
 
 
 
 
 
 
 
 
 
 
265
  reader = PdfReader(unlocked_pdf_path)
266
- text = "\n".join(page.extract_text() for page in reader.pages)
 
 
 
 
267
  os.remove(unlocked_pdf_path)
268
- else:
269
- reader = PdfReader(file_path)
270
- text = "\n".join(page.extract_text() for page in reader.pages)
271
- return text
 
 
 
 
 
 
 
 
 
 
272
 
273
  def process_protected_office(self, file, file_type: str, password: str = None) -> str:
274
- if password:
275
- office_file = msoffcrypto.OfficeFile(file)
276
- office_file.load_key(password=password)
277
- decrypted = io.BytesIO()
278
- office_file.decrypt(decrypted)
279
- if file_type == 'docx':
280
- doc = Document(decrypted)
281
- return "\n".join([p.text for p in doc.paragraphs])
282
- elif file_type == 'pptx':
283
- ppt = Presentation(decrypted)
284
- return "\n".join([shape.text for slide in ppt.slides for shape in slide.shapes if hasattr(shape, "text")])
285
- else:
286
- if file_type == 'docx':
287
- doc = Document(file)
288
- return "\n".join([p.text for p in doc.paragraphs])
289
- elif file_type == 'pptx':
290
- ppt = Presentation(file)
291
- return "\n".join([shape.text for slide in ppt.slides for shape in slide.shapes if hasattr(shape, "text")])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
 
294
  def model_selection_sidebar():
 
295
  with st.sidebar:
296
  st.title("Configuration")
297
  model = st.selectbox(
298
  "Sélectionnez un modèle",
299
- ["mixtral-8x7b-32768", "llama-3.3-70b-versatile", "gemma2-9b-i", "llama3-70b-8192"]
 
 
 
 
 
300
  )
301
  prompt = st.text_area(
302
  "Instructions personnalisées pour le résumé",
@@ -304,31 +661,78 @@ def model_selection_sidebar():
304
  )
305
  return model, prompt
306
 
307
-
308
  def save_uploaded_file(uploaded_file) -> str:
 
309
  with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(uploaded_file.name)[1]) as tmp_file:
310
  tmp_file.write(uploaded_file.getvalue())
311
  return tmp_file.name
312
 
313
-
314
  def is_valid_email(email: str) -> bool:
 
315
  pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
316
  return bool(re.match(pattern, email))
317
 
318
-
319
  def enhance_main():
320
- st.title("🧠 MultiModal Genius - Résumé Intelligent de Contenus Multimédias")
321
- st.subheader("Transformez vidéos, audios, textes, pages webs et plus en résumés clairs grâce à l'IA")
322
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  if "audio_processor" not in st.session_state:
324
  model_name, custom_prompt = model_selection_sidebar()
325
  st.session_state.audio_processor = AudioProcessor(model_name, custom_prompt)
326
-
327
  if "auth_required" not in st.session_state:
328
  st.session_state.auth_required = False
329
-
 
330
  source_type = st.radio("Type de source", ["Audio/Vidéo", "Document", "Web"])
331
-
332
  try:
333
  if source_type == "Audio/Vidéo":
334
  process_audio_video()
@@ -340,10 +744,10 @@ def enhance_main():
340
  st.error(f"Une erreur est survenue: {str(e)}")
341
  st.error("Veuillez réessayer ou contacter le support.")
342
 
343
-
344
  def process_audio_video():
 
345
  source = st.radio("Choisissez votre source", ["Audio", "Vidéo locale", "YouTube"])
346
-
347
  if source == "Audio":
348
  handle_audio_input()
349
  elif source == "Vidéo locale":
@@ -351,16 +755,16 @@ def process_audio_video():
351
  else: # YouTube
352
  handle_youtube_input()
353
 
354
-
355
  def handle_audio_input():
 
356
  uploaded_file = st.file_uploader("Fichier audio", type=['mp3', 'wav', 'm4a', 'ogg'])
357
  audio_bytes = audio_recorder()
358
-
359
  if uploaded_file or audio_bytes:
360
  process_and_display_results(uploaded_file, audio_bytes)
361
 
362
-
363
  def handle_video_input():
 
364
  uploaded_video = st.file_uploader("Fichier vidéo", type=['mp4', 'avi', 'mov', 'mkv'])
365
  if uploaded_video:
366
  st.video(uploaded_video)
@@ -370,13 +774,14 @@ def handle_video_input():
370
  audio_path = video_processor.extract_audio_from_video(video_path)
371
  process_and_display_results(audio_path)
372
 
373
-
374
  def handle_youtube_input():
 
 
375
  youtube_url = st.text_input("URL YouTube")
376
  if youtube_url and st.button("Analyser"):
377
  video_processor = VideoProcessor()
378
  video_id = video_processor.extract_video_id(youtube_url)
379
-
380
  if video_id:
381
  st.video(youtube_url)
382
  with st.spinner("Traitement de la vidéo..."):
@@ -387,48 +792,62 @@ def handle_youtube_input():
387
  video_processor.load_cookies()
388
  audio_path = video_processor.download_youtube_audio(youtube_url)
389
  process_and_display_results(audio_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
 
392
  def process_and_display_results(file_path=None, audio_bytes=None, transcription=None):
393
- if transcription is None:
394
- if file_path:
395
- path = file_path if isinstance(file_path, str) else save_uploaded_file(file_path)
396
- elif audio_bytes:
397
- path = save_audio_bytes(audio_bytes)
398
- else:
399
- return
400
-
401
- chunks = st.session_state.audio_processor.chunk_audio(path)
402
- transcriptions = []
403
- with st.expander("Transcription", expanded=False):
404
- progress_bar = st.progress(0)
405
- for i, chunk in enumerate(chunks):
406
- transcription = st.session_state.audio_processor.transcribe_chunk(chunk)
407
- if transcription:
408
- transcriptions.append(transcription)
409
- progress_bar.progress((i + 1) / len(chunks))
410
- transcription = " ".join(transcriptions) if transcriptions else None
411
-
412
- if transcription:
413
- display_transcription_and_summary(transcription)
414
-
415
-
416
- def save_audio_bytes(audio_bytes: bytes) -> str:
417
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
418
- file_path = f"recording_{timestamp}.wav"
419
- with open(file_path, 'wb') as f:
420
- f.write(audio_bytes)
421
- return file_path
422
-
423
 
424
- def display_transcription_and_summary(transcription: str):
425
- st.subheader("Transcription")
426
- st.text_area("Texte transcrit:", value=transcription, height=200)
427
- st.subheader("Résumé et Analyse")
428
- summary = get_summary(transcription)
429
- st.markdown(summary)
430
- display_summary_and_downloads(summary)
 
431
 
 
 
 
 
 
 
 
 
 
 
 
 
432
 
433
  def get_summary(full_transcription):
434
  if full_transcription is not None:
@@ -439,24 +858,237 @@ def get_summary(full_transcription):
439
  separators=["\n\n", "\n", " ", ""]
440
  )
441
  chunks = text_splitter.split_text(full_transcription)
 
 
442
  if len(chunks) > 1:
443
  summary = st.session_state.audio_processor.summarize_long_transcription(full_transcription)
444
  else:
445
  summary = st.session_state.audio_processor.generate_summary(full_transcription)
446
  else:
447
  st.error("La transcription a échoué")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  return None
449
- return summary
450
 
451
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  def display_summary_and_downloads(summary: str):
 
 
 
 
453
  timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
 
 
454
  pdf_filename = f"resume_{timestamp}.pdf"
455
  pdf_path = PDFGenerator.create_pdf(summary, pdf_filename)
456
-
 
457
  docx_filename = f"resume_{timestamp}.docx"
458
  docx_path = generate_docx(summary, docx_filename)
459
-
 
460
  col1, col2 = st.columns(2)
461
  with col1:
462
  with open(pdf_path, "rb") as pdf_file:
@@ -466,6 +1098,7 @@ def display_summary_and_downloads(summary: str):
466
  file_name=pdf_filename,
467
  mime="application/pdf"
468
  )
 
469
  with col2:
470
  with open(docx_path, "rb") as docx_file:
471
  st.download_button(
@@ -474,9 +1107,11 @@ def display_summary_and_downloads(summary: str):
474
  file_name=docx_filename,
475
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
476
  )
477
-
 
478
  st.markdown("### 📧 Recevoir le résumé par email")
479
  recipient_email = st.text_input("Entrez votre adresse email:")
 
480
  if st.button("Envoyer par email"):
481
  if not is_valid_email(recipient_email):
482
  st.error("Veuillez entrer une adresse email valide.")
@@ -493,30 +1128,14 @@ def display_summary_and_downloads(summary: str):
493
  else:
494
  st.error("Échec de l'envoi de l'email.")
495
 
 
 
 
 
 
 
 
496
 
497
- def generate_docx(content: str, filename: str):
498
- doc = Document()
499
- doc.add_heading('Résumé', 0)
500
- doc.add_paragraph(f"Date: {datetime.now().strftime('%d/%m/%Y %H:%M')}")
501
- for line in content.split('\n'):
502
- if line.strip():
503
- if line.startswith('#'):
504
- doc.add_heading(line.strip('# '), level=1)
505
- else:
506
- doc.add_paragraph(line)
507
- doc.save(filename)
508
- return filename
509
-
510
- def cleanup_temporary_files():
511
- temp_files = ['temp_audio.mp3', 'temp_video.mp4']
512
- for temp_file in temp_files:
513
- if os.path.exists(temp_file):
514
- try:
515
- os.remove(temp_file)
516
- except Exception:
517
- pass
518
-
519
-
520
  if __name__ == "__main__":
521
  try:
522
  enhance_main()
@@ -524,6 +1143,4 @@ if __name__ == "__main__":
524
  st.error(f"Une erreur inattendue est survenue: {str(e)}")
525
  st.error("Veuillez réessayer ou contacter le support technique.")
526
  finally:
527
- cleanup_temporary_files()
528
-
529
-
 
 
1
  import os
2
  import uuid
3
 
 
57
 
58
  load_dotenv()
59
 
 
 
60
  SENDER_EMAIL = os.environ.get('SENDER_EMAIL')
61
  SENDER_PASSWORD = os.environ.get('SENDER_PASSWORD')
62
 
 
63
  class Config:
64
+ """Centralisation de la configuration"""
65
+ #GROQ_API_KEY = ""
66
+ #SENDER_EMAIL = ""
67
+ #SENDER_PASSWORD = ""
68
  FASTTEXT_MODEL_PATH = "lid.176.bin"
69
+ import urllib.request
70
+ urllib.request.urlretrieve('https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.bin', 'lid.176.bin')
71
 
 
 
 
 
 
 
 
72
 
73
+ # Classes PDFGenerator et EmailSender restent inchangées...
74
  class PDFGenerator:
75
  @staticmethod
76
  def create_pdf(content: str, filename: str) -> str:
 
84
  fontSize=12,
85
  leading=14,
86
  )
87
+
88
  story = []
89
  title_style = ParagraphStyle(
90
  'CustomTitle',
 
92
  fontSize=16,
93
  spaceAfter=30,
94
  )
95
+ story.append(Paragraph("Résumé Audio", title_style))
96
  story.append(Paragraph(f"Date: {datetime.now().strftime('%d/%m/%Y %H:%M')}", custom_style))
97
  story.append(Spacer(1, 20))
98
+
99
  for line in content.split('\n'):
100
  if line.strip():
101
  if line.startswith('#'):
102
  story.append(Paragraph(line.strip('# '), styles['Heading2']))
103
  else:
104
  story.append(Paragraph(line, custom_style))
105
+
106
  doc.build(story)
107
  return filename
108
 
 
109
  class EmailSender:
110
  def __init__(self, sender_email: str, sender_password: str):
111
+ self.sender_email = SENDER_EMAIL # or Config.SENDER_EMAIL
112
+ self.sender_password = SENDER_PASSWORD # or Config.SENDER_PASSWORD
113
 
114
  def send_email(self, recipient_email: str, subject: str, body: str, pdf_path: str) -> bool:
115
  try:
 
118
  msg['To'] = recipient_email
119
  msg['Subject'] = subject
120
  msg.attach(MIMEText(body, 'plain'))
121
+
122
  with open(pdf_path, 'rb') as f:
123
  pdf_attachment = MIMEApplication(f.read(), _subtype='pdf')
124
  pdf_attachment.add_header('Content-Disposition', 'attachment', filename=os.path.basename(pdf_path))
125
  msg.attach(pdf_attachment)
126
+
127
  server = smtplib.SMTP('smtp.gmail.com', 587)
128
  server.starttls()
129
  server.login(self.sender_email, self.sender_password)
 
134
  st.error(f"Erreur d'envoi d'email: {str(e)}")
135
  return False
136
 
 
137
  class AudioProcessor:
138
  def __init__(self, model_name: str, prompt: str = None, chunk_length_ms: int = 300000):
139
  self.chunk_length_ms = chunk_length_ms
140
+ self.groq_client = Groq() #api_key=Config.GROQ_API_KEY
141
+ self.llm = ChatGroq(
142
+ model=model_name,
143
+ temperature=0,
144
+ #api_key=Config.GROQ_API_KEY
145
+ )
146
  self.custom_prompt = prompt
147
  self.language_detector = fasttext.load_model(Config.FASTTEXT_MODEL_PATH)
148
+ self.text_splitter = RecursiveCharacterTextSplitter(
149
+ chunk_size=4000,
150
+ chunk_overlap=200
151
+ )
152
+ #self.custom_prompt = prompt
153
+ # Définition des limites de taux : 5000 tokens par minute
154
+ self.CALLS_PER_MINUTE = 5000
155
+ self.PERIOD = 60 # 60 secondes = 1 minute
156
+ # Add language detection model
157
+ #self.language_detector = fasttext.load_model('lid.176.bin')
158
 
159
  def check_language(self, text: str) -> str:
160
+ """Vérifie si le texte est en français"""
161
  prediction = self.language_detector.predict(text.replace('\n', ' '))
162
  return "OUI" if prediction[0][0] == '__label__fr' else "NON"
163
 
164
  def translate_to_french(self, text: str) -> str:
165
+ """Traduit le texte en français si nécessaire"""
166
+ try:
167
+ messages = [
168
+ SystemMessage(content="Vous êtes un traducteur professionnel agréé en Français. Traduisez le texte suivant en français en conservant le format et la structure:"),
169
+ HumanMessage(content=text)
170
+ ]
171
+ result = self._make_api_call(messages)
172
+ return result.generations[0][0].text
173
+ except Exception as e:
174
+ if "rate_limit_exceeded" in str(e):
175
+ time.sleep(60)
176
+ return self.translate_to_french(text)
177
+ raise e
178
 
179
+ @sleep_and_retry
180
  @limits(calls=5000, period=60)
181
  def _make_api_call(self, messages):
182
  return self.llm.generate([messages])
183
 
184
  def chunk_audio(self, file_path: str) -> List[AudioSegment]:
185
+ try:
186
+ audio = AudioSegment.from_file(file_path)
187
+ if len(audio) < self.chunk_length_ms:
188
+ return [audio]
189
+ return [
190
+ audio[i:i + self.chunk_length_ms]
191
+ for i in range(0, len(audio), self.chunk_length_ms)
192
+ ]
193
+ except Exception as e:
194
+ st.error(f"Error processing audio file: {str(e)}")
195
+ return []
196
 
197
  def transcribe_chunk(self, audio_chunk: AudioSegment) -> str:
198
+ try:
199
+ with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as temp_file:
200
+ audio_chunk.export(temp_file.name, format="mp3")
201
+ with open(temp_file.name, "rb") as audio_file:
202
+ try:
203
+ response = self.groq_client.audio.transcriptions.create(
204
+ file=audio_file,
205
+ model="whisper-large-v3-turbo",
206
+ language="fr"
207
+ )
208
+ except Exception as e:
209
+ if "rate_limit_exceeded" in str(e):
210
+ st.warning("Limite de taux atteinte pendant la transcription. Attente avant nouvelle tentative...")
211
+ time.sleep(60)
212
+ return self.transcribe_chunk(audio_chunk)
213
+ raise e
214
+ os.unlink(temp_file.name)
215
+ return response.text
216
+ except Exception as e:
217
+ st.error(f"Transcription error: {str(e)}")
218
+ return ""
219
+
220
+
221
+ # Dans la classe AudioProcessor, ajoutez cette méthode :
222
+ def split_text(self, text: str, max_tokens: int = 4000) -> List[str]:
223
+ text_splitter = RecursiveCharacterTextSplitter(
224
+ chunk_size=max_tokens * 4, # Estimation approximative tokens -> caractères
225
+ chunk_overlap=200,
226
+ length_function=len,
227
+ separators=["\n\n", "\n", " ", ""]
228
+ )
229
+ return text_splitter.split_text(text)
230
 
231
  def generate_summary(self, transcription: str) -> str:
232
  default_prompt = """
233
+ Vous êtes un assistant expert spécialisé dans le résumé et l'analyse d'enregistrements audio en langue française.
234
+ Voici la transcription à analyser:
235
+
236
+ {transcript}
237
+
238
+ Veuillez fournir:
239
+ 1. Un résumé concis (3-4 phrases)
240
+ 2. Les points clés (maximum 5 points)
241
+ 3. Les actions recommandées (si pertinent)
242
+ 4. Une conclusion brève
243
+
244
+ Format souhaité:
245
  # Résumé
246
+ [votre résumé]
247
+
248
  # Points Clés
249
  • [point 1]
250
  • [point 2]
251
+ ...
252
+
253
  # Actions Recommandées
254
  1. [action 1]
255
  2. [action 2]
256
+ ...
257
+
258
  # Conclusion
259
+ [votre conclusion]
260
  """
261
+
262
+ prompt_template = self.custom_prompt if self.custom_prompt else default_prompt
263
+
264
+ try:
265
+ chain = LLMChain(
266
+ llm=self.llm,
267
+ prompt=PromptTemplate(
268
+ template=prompt_template,
269
+ input_variables=["transcript"]
270
+ )
271
+ )
272
+
273
+ summary = chain.run(transcript=transcription)
274
+
275
+ # Vérification de la langue
276
+ if self.check_language(summary) == "NON":
277
+ st.warning("Résumé généré dans une autre langue. Traduction en cours...")
278
+ summary = self.translate_to_french(summary)
279
+
280
+ return summary
281
+ except Exception as e:
282
+ if "rate_limit_exceeded" in str(e):
283
+ st.warning("Limite de taux atteinte. Attente avant nouvelle tentative...")
284
+ time.sleep(60) # Attendre 1 minute
285
+ return self.generate_summary(transcription)
286
+ raise e
287
+
288
+ # Méthodes existantes inchangées...
289
+
290
+
291
+
292
+ def summarize_long_transcription(self, transcription: str) -> str:
293
+ chunks = self.split_text(transcription, max_tokens=4000)
294
+ partial_summaries = []
295
+
296
+ for i, chunk in enumerate(chunks):
297
+ st.write(f"Traitement du segment {i + 1}/{len(chunks)}...")
298
+ try:
299
+ messages = [
300
+ SystemMessage(content="Vous êtes un assistant expert en résumé de texte en français."),
301
+ HumanMessage(content=f"Résumez ce texte en français : {chunk}")
302
+ ]
303
+ result = self._make_api_call(messages)
304
+ partial_summary = result.generations[0][0].text
305
+
306
+ # Vérification de la langue pour chaque segment
307
+ if self.check_language(partial_summary) == "NON":
308
+ partial_summary = self.translate_to_french(partial_summary)
309
+
310
+ partial_summaries.append(partial_summary)
311
+ except Exception as e:
312
+ if "rate_limit_exceeded" in str(e):
313
+ st.warning(f"Limite de taux atteinte au segment {i+1}. Attente avant nouvelle tentative...")
314
+ time.sleep(60)
315
+ i -= 1
316
+ continue
317
+ raise e
318
+
319
+ try:
320
+ final_prompt = f"""Combinez ces résumés partiels en un résumé global cohérent en langue française :
321
+
322
+ {' '.join(partial_summaries)}
323
+ """
324
+ messages = [
325
+ SystemMessage(content="Vous êtes un assistant expert en résumé de texte en français."),
326
+ HumanMessage(content=final_prompt)
327
+ ]
328
+ final_result = self._make_api_call(messages)
329
+ final_summary = final_result.generations[0][0].text
330
+
331
+ # Vérification finale de la langue
332
+ if self.check_language(final_summary) == "NON":
333
+ st.warning("Résumé final dans une autre langue. Traduction en cours...")
334
+ final_summary = self.translate_to_french(final_summary)
335
+
336
+ return final_summary
337
+
338
+ except Exception as e:
339
+ if "rate_limit_exceeded" in str(e):
340
+ st.warning("Limite de taux atteinte lors de la génération du résumé final. Attente avant nouvelle tentative...")
341
+ time.sleep(60)
342
+ return self.summarize_long_transcription(transcription)
343
+ raise e
344
+
345
  class VideoProcessor:
346
  def __init__(self):
347
  self.supported_formats = ['.mp4', '.avi', '.mov', '.mkv']
348
+ self.cookie_file_path = "cookies.txt" # Chemin du fichier cookies
349
+ self.ydl_opts = {
350
+ 'format': 'bestaudio/best',
351
+ 'postprocessors': [{
352
+ 'key': 'FFmpegExtractAudio',
353
+ 'preferredcodec': 'mp3',
354
+ 'preferredquality': '192',
355
+ }],
356
+ 'outtmpl': 'temp_audio.%(ext)s'
357
+ }
358
+
359
+
360
  def load_cookies(self):
361
+ """Charge les cookies depuis Hugging Face et les enregistre localement."""
362
  dataset = load_dataset("Adjoumani/YoutubeCookiesDataset")
363
  cookies = dataset["train"]["cookies"][0]
364
  with open(self.cookie_file_path, "w") as f:
365
  f.write(cookies)
366
+ print(f"Cookies enregistrés dans {self.cookie_file_path}")
367
 
368
+
369
  def extract_video_id(self, url: str) -> str:
370
+ try:
371
+ parsed_url = urlparse(url)
372
+ if parsed_url.hostname in ['www.youtube.com', 'youtube.com']:
373
+ return parse_qs(parsed_url.query)['v'][0]
374
+ elif parsed_url.hostname == 'youtu.be':
375
+ return parsed_url.path[1:]
376
+ return None
377
+ except Exception:
378
+ return None
379
 
380
  def get_youtube_transcription(self, video_id: str) -> Optional[str]:
381
  try:
 
385
  return None
386
 
387
  def download_youtube_audio(self, url: str) -> str:
388
+ try:
389
+ # Ajoutez le fichier cookies dans les options
390
+ ydl_opts = self.ydl_opts.copy()
391
+ ydl_opts['cookiefile'] = self.cookie_file_path
 
 
 
 
 
 
 
 
 
392
 
393
+ # Téléchargement
394
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
395
+ ydl.download([url])
396
+
397
+ # Vérifier si le fichier audio existe
398
+ audio_path = 'temp_audio.mp3'
399
+ if not os.path.exists(audio_path):
400
+ raise FileNotFoundError(f"Le fichier {audio_path} n'a pas été généré.")
401
 
402
+ return audio_path
403
+ except Exception as e:
404
+ raise RuntimeError(f"Erreur lors du téléchargement : {str(e)}")
405
+
406
+ def extract_audio_from_video(self, video_path: str) -> str:
407
+ try:
408
+ audio_path = f"{os.path.splitext(video_path)[0]}.mp3"
409
+ with VideoFileClip(video_path) as video:
410
+ video.audio.write_audiofile(audio_path)
411
+ return audio_path
412
+ except Exception as e:
413
+ st.error(f"Erreur lors de l'extraction audio: {str(e)}")
414
+ raise
415
 
416
  class DocumentProcessor:
417
  def __init__(self, model_name: str, prompt: str = None):
418
+ self.llm = ChatGroq(
419
+ model=model_name,
420
+ temperature=0,
421
+ #api_key=Config.GROQ_API_KEY
422
+ )
423
  self.custom_prompt = prompt
424
+ #self.text_splitter = RecursiveCharacterTextSplitter(
425
+ # chunk_size=4000,
426
+ # chunk_overlap=200
427
+ #)
428
+ self.language_detector = fasttext.load_model('lid.176.bin')
429
+
430
+ def split_text(self, text: str, max_tokens: int = 4000) -> List[str]:
431
+ text_splitter = RecursiveCharacterTextSplitter(
432
+ chunk_size=max_tokens * 4, # Estimation approximative tokens -> caractères
433
+ chunk_overlap=200,
434
+ length_function=len,
435
+ separators=["\n\n", "\n", " ", ""]
436
+ )
437
+ return text_splitter.split_text(text)
438
+
439
+ def check_language(self, text: str) -> str:
440
+ """Vérifie si le texte est en français"""
441
+ prediction = self.language_detector.predict(text.replace('\n', ' '))
442
+ return "OUI" if prediction[0][0] == '__label__fr' else "NON"
443
+
444
+ def translate_to_french(self, text: str) -> str:
445
+ """Traduit le texte en français si nécessaire"""
446
+ try:
447
+ messages = [
448
+ SystemMessage(content="Vous êtes un traducteur professionnel agrée en Français. Traduisez le texte suivant en français en conservant le format et la structure:"),
449
+ HumanMessage(content=text)
450
+ ]
451
+ result = self._make_api_call(messages)
452
+ return result.generations[0][0].text
453
+ except Exception as e:
454
+ if "rate_limit_exceeded" in str(e):
455
+ time.sleep(60)
456
+ return self.translate_to_french(text)
457
+ raise e
458
+
459
+ # Méthodes existantes de DocumentProcessor inchangées...
460
+ @sleep_and_retry
461
+ @limits(calls=5000, period=60)
462
+ def _make_api_call(self, messages):
463
+ return self.llm.generate([messages])
464
+
465
+
466
 
467
  def process_protected_pdf(self, file_path: str, password: str = None) -> str:
468
+ """
469
+ Traite un PDF, avec ou sans mot de passe, et extrait le texte.
470
+
471
+ :param file_path: Chemin vers le fichier PDF.
472
+ :param password: Mot de passe du fichier PDF (si nécessaire).
473
+ :return: Texte extrait du PDF.
474
+ """
475
+ try:
476
+ # Si un mot de passe est fourni, tenter de déverrouiller le PDF
477
+ if password:
478
+ with pikepdf.open(file_path, password=password) as pdf:
479
+ unlocked_pdf_path = "unlocked_temp.pdf"
480
+ pdf.save(unlocked_pdf_path)
481
+
482
+ # Utiliser le fichier temporaire déverrouillé
483
  reader = PdfReader(unlocked_pdf_path)
484
+ text = ""
485
+ for page in reader.pages:
486
+ text += page.extract_text()
487
+
488
+ # Supprimer le fichier temporaire
489
  os.remove(unlocked_pdf_path)
490
+
491
+ else:
492
+ # Si aucun mot de passe, traiter directement le PDF
493
+ reader = PdfReader(file_path)
494
+ text = ""
495
+ for page in reader.pages:
496
+ text += page.extract_text()
497
+
498
+ return text
499
+
500
+ except pikepdf.PasswordError:
501
+ raise ValueError("Mot de passe PDF incorrect")
502
+ except Exception as e:
503
+ raise RuntimeError(f"Erreur lors du traitement du PDF : {e}")
504
 
505
  def process_protected_office(self, file, file_type: str, password: str = None) -> str:
506
+ """
507
+ Traite un fichier Office (protégé ou non) et extrait le texte.
508
+
509
+ :param file: Le fichier Office à traiter.
510
+ :param password: Mot de passe du fichier (si nécessaire, sinon None).
511
+ :param file_type: Type du fichier ('docx' ou 'pptx').
512
+ :return: Texte extrait du fichier.
513
+ """
514
+ try:
515
+ if password:
516
+ # Cas un mot de passe est fourni, tenter de déverrouiller le fichier
517
+ office_file = msoffcrypto.OfficeFile(file)
518
+ office_file.load_key(password=password)
519
+
520
+ decrypted = io.BytesIO()
521
+ office_file.decrypt(decrypted)
522
+
523
+ if file_type == 'docx':
524
+ doc = docx.Document(decrypted)
525
+ return "\n".join([p.text for p in doc.paragraphs])
526
+ elif file_type == 'pptx':
527
+ ppt = pptx.Presentation(decrypted)
528
+ return "\n".join([shape.text for slide in ppt.slides
529
+ for shape in slide.shapes if hasattr(shape, "text")])
530
+ else:
531
+ # Cas où aucun mot de passe n'est fourni, traiter directement le fichier
532
+ if file_type == 'docx':
533
+ doc = docx.Document(file) # Charger le fichier sans décryptage
534
+ return "\n".join([p.text for p in doc.paragraphs])
535
+ elif file_type == 'pptx':
536
+ ppt = pptx.Presentation(file)
537
+ return "\n".join([shape.text for slide in ppt.slides
538
+ for shape in slide.shapes if hasattr(shape, "text")])
539
+
540
+ raise ValueError("Type de fichier non supporté. Utilisez 'docx' ou 'pptx'.")
541
+
542
+ except msoffcrypto.exceptions.InvalidKeyError:
543
+ raise ValueError("Mot de passe incorrect ou fichier non valide.")
544
+ except Exception as e:
545
+ raise RuntimeError(f"Erreur lors du traitement du fichier Office : {e}")
546
+
547
+
548
+
549
+ def scrape_web_content(self, url: str, auth: Dict[str, str] = None) -> str:
550
+ try:
551
+ if auth:
552
+ session = requests.Session()
553
+ session.auth = HTTPBasicAuth(auth['username'], auth['password'])
554
+ response = session.get(url, timeout=30)
555
+ else:
556
+ response = requests.get(url, timeout=30)
557
+
558
+ response.raise_for_status()
559
+ downloaded = trafilatura.extract(response.text)
560
+
561
+ if not downloaded:
562
+ raise ValueError("Impossible d'extraire le contenu de cette page")
563
+ return downloaded
564
+
565
+ except requests.exceptions.HTTPError as e:
566
+ if e.response.status_code == 401:
567
+ raise ValueError("Authentification requise pour accéder à cette page")
568
+ elif e.response.status_code == 404:
569
+ raise ValueError("Page introuvable")
570
+ else:
571
+ raise ValueError(f"Erreur HTTP: {e.response.status_code}")
572
+ except requests.exceptions.RequestException:
573
+ raise ValueError("URL invalide ou inaccessible")
574
+
575
+ def summarize_text(self, transcription: str) -> str:
576
+ chunks = self.split_text(transcription, max_tokens=4000)
577
+ partial_summaries = []
578
+
579
+ for i, chunk in enumerate(chunks):
580
+ st.write(f"Traitement du segment {i + 1}/{len(chunks)}...")
581
+ try:
582
+ messages = [
583
+ SystemMessage(content="Vous êtes un assistant expert en résumé de texte en français."),
584
+ HumanMessage(content=f"Résumez ce texte en français : {chunk}")
585
+ ]
586
+ result = self._make_api_call(messages)
587
+ partial_summary = result.generations[0][0].text
588
+
589
+ # Vérification de la langue pour chaque segment
590
+ if self.check_language(partial_summary) == "NON":
591
+ partial_summary = self.translate_to_french(partial_summary)
592
+
593
+ partial_summaries.append(partial_summary)
594
+ except Exception as e:
595
+ if "rate_limit_exceeded" in str(e):
596
+ st.warning(f"Limite de taux atteinte au segment {i+1}. Attente avant nouvelle tentative...")
597
+ time.sleep(60)
598
+ i -= 1
599
+ continue
600
+ raise e
601
+
602
+ try:
603
+ final_prompt = f"""Combinez ces résumés partiels en un résumé global cohérent en langue française :
604
+
605
+ {' '.join(partial_summaries)}
606
+ """
607
+ messages = [
608
+ SystemMessage(content="Vous êtes un assistant expert en résumé de texte en français."),
609
+ HumanMessage(content=final_prompt)
610
+ ]
611
+ final_result = self._make_api_call(messages)
612
+ final_summary = final_result.generations[0][0].text
613
+
614
+ # Vérification finale de la langue
615
+ if self.check_language(final_summary) == "NON":
616
+ st.warning("Résumé final dans une autre langue. Traduction en cours...")
617
+ final_summary = self.translate_to_french(final_summary)
618
+
619
+ return final_summary
620
+
621
+ except Exception as e:
622
+ if "rate_limit_exceeded" in str(e):
623
+ st.warning("Limite de taux atteinte lors de la génération du résumé final. Attente avant nouvelle tentative...")
624
+ time.sleep(60)
625
+ return self.summarize_long_transcription(transcription)
626
+ raise e
627
+
628
+
629
+ def generate_docx(content: str, filename: str):
630
+ doc = Document()
631
+ doc.add_heading('Résumé Audio', 0)
632
+ doc.add_paragraph(f"Date: {datetime.now().strftime('%d/%m/%Y %H:%M')}")
633
+
634
+ for line in content.split('\n'):
635
+ if line.strip():
636
+ if line.startswith('#'):
637
+ doc.add_heading(line.strip('# '), level=1)
638
+ else:
639
+ doc.add_paragraph(line)
640
+
641
+ doc.save(filename)
642
+ return filename
643
 
644
 
645
  def model_selection_sidebar():
646
+ """Configuration du modèle dans la barre latérale"""
647
  with st.sidebar:
648
  st.title("Configuration")
649
  model = st.selectbox(
650
  "Sélectionnez un modèle",
651
+ [
652
+ "mixtral-8x7b-32768",
653
+ "llama-3.3-70b-versatile",
654
+ "gemma2-9b-i",
655
+ "llama3-70b-8192"
656
+ ]
657
  )
658
  prompt = st.text_area(
659
  "Instructions personnalisées pour le résumé",
 
661
  )
662
  return model, prompt
663
 
 
664
  def save_uploaded_file(uploaded_file) -> str:
665
+ """Sauvegarde un fichier uploadé et retourne son chemin"""
666
  with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(uploaded_file.name)[1]) as tmp_file:
667
  tmp_file.write(uploaded_file.getvalue())
668
  return tmp_file.name
669
 
 
670
  def is_valid_email(email: str) -> bool:
671
+ """Valide le format d'une adresse email"""
672
  pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
673
  return bool(re.match(pattern, email))
674
 
 
675
  def enhance_main():
676
+
677
+
678
+ # Titre de l'application
679
+ st.title("🧠 **MultiModal Genius - Résumé Intelligent de Contenus Multimédias**")
680
+ st.subheader("Transformez vidéos, audios, textes, pages webs et plus en résumés clairs et percutants grâce à la puissance de l'IA")
681
+
682
+ with st.expander("Notice d'utilisation 📜"):
683
+ st.markdown("""
684
+ ## **Bienvenue dans l'application MultiModal Genius !** 🎉
685
+ Cette application exploite la puissance de l'IA pour résumer des contenus multimédias variés, tels que des **documents**, **vidéos YouTube**, **audios**, **pages web**, et bien plus encore ! 🧠✨
686
+
687
+ ### **Comment utiliser l'application ?**
688
+ 1. **Documents** 📄 :
689
+ - **Formats supportés** : `.pdf`, `.docx`, `.pptx`, `.txt`
690
+ - Chargez un document via le bouton **"Télécharger un fichier"**.
691
+ - ⚠️ **Remarque** : Les documents contenant plus de **10 pages** peuvent entraîner des résultats imprécis en raison des limitations des modèles d'IA.
692
+
693
+ 2. **Vidéos YouTube** 📹 :
694
+ - Collez simplement l'URL de la vidéo.
695
+ - La vidéo est automatiquement découpée en segments pour une analyse et un résumé précis.
696
+ - **Durée du traitement** : Plus la vidéo est longue, plus le traitement peut prendre du temps.
697
+
698
+ 3. **Audios** 🎵 :
699
+ - Téléchargez un fichier audio au format `.mp3`.
700
+ - L'audio sera transcrit par blocs (chunks) avant d'être résumé.
701
+ - ⚠️ **Remarque** : Les fichiers audio de grande taille peuvent rallonger le processus.
702
+
703
+ 4. **Pages Web** 🌐 :
704
+ - Fournissez l'URL de la page.
705
+ - Le contenu textuel sera extrait, découpé en blocs, puis résumé.
706
+
707
+ ### **Pourquoi le résumé peut être long ?**
708
+ - **Traitement volumineux** : Les contenus trop longs ou complexes nécessitent un découpage en plusieurs blocs (chunks). Ces blocs sont analysés et traduits avant d'être rassemblés pour un résumé final.
709
+ - **Limites des modèles IA** : Certains contenus trop volumineux peuvent provoquer des hallucinations du modèle (résultats incohérents ou incorrects).
710
+
711
+ ### **Fonctionnalités à venir 🚀**
712
+ - **Description d'images** 🖼️ : Transformez vos images en descriptions riches et détaillées.
713
+ - **Extraction de données** 📊 : Convertissez vos contenus en **format JSON** structuré.
714
+ - **Amélioration des résumés longs** : Réduction des hallucinations grâce à des optimisations.
715
+ - Et bien plus encore ! 🎯
716
+
717
+ ### **Astuce pour une meilleure expérience**
718
+ - **Préférez des contenus courts ou moyennement volumineux** pour des résultats optimaux.
719
+ - En cas de traitement long, un indicateur de progression vous tiendra informé. ⏳
720
+
721
+ ### **Nous sommes là pour vous aider !**
722
+ Si vous rencontrez un problème ou avez une suggestion pour améliorer l'application, n'hésitez pas à nous contacter. 🙌
723
+ """)
724
+
725
+
726
  if "audio_processor" not in st.session_state:
727
  model_name, custom_prompt = model_selection_sidebar()
728
  st.session_state.audio_processor = AudioProcessor(model_name, custom_prompt)
729
+
730
  if "auth_required" not in st.session_state:
731
  st.session_state.auth_required = False
732
+
733
+ # Interface principale
734
  source_type = st.radio("Type de source", ["Audio/Vidéo", "Document", "Web"])
735
+
736
  try:
737
  if source_type == "Audio/Vidéo":
738
  process_audio_video()
 
744
  st.error(f"Une erreur est survenue: {str(e)}")
745
  st.error("Veuillez réessayer ou contacter le support.")
746
 
 
747
  def process_audio_video():
748
+ """Traitement des sources audio et vidéo"""
749
  source = st.radio("Choisissez votre source", ["Audio", "Vidéo locale", "YouTube"])
750
+
751
  if source == "Audio":
752
  handle_audio_input()
753
  elif source == "Vidéo locale":
 
755
  else: # YouTube
756
  handle_youtube_input()
757
 
 
758
  def handle_audio_input():
759
+ """Gestion des entrées audio"""
760
  uploaded_file = st.file_uploader("Fichier audio", type=['mp3', 'wav', 'm4a', 'ogg'])
761
  audio_bytes = audio_recorder()
762
+
763
  if uploaded_file or audio_bytes:
764
  process_and_display_results(uploaded_file, audio_bytes)
765
 
 
766
  def handle_video_input():
767
+ """Gestion des entrées vidéo"""
768
  uploaded_video = st.file_uploader("Fichier vidéo", type=['mp4', 'avi', 'mov', 'mkv'])
769
  if uploaded_video:
770
  st.video(uploaded_video)
 
774
  audio_path = video_processor.extract_audio_from_video(video_path)
775
  process_and_display_results(audio_path)
776
 
 
777
  def handle_youtube_input():
778
+ """Gestion des entrées YouTube"""
779
+
780
  youtube_url = st.text_input("URL YouTube")
781
  if youtube_url and st.button("Analyser"):
782
  video_processor = VideoProcessor()
783
  video_id = video_processor.extract_video_id(youtube_url)
784
+
785
  if video_id:
786
  st.video(youtube_url)
787
  with st.spinner("Traitement de la vidéo..."):
 
792
  video_processor.load_cookies()
793
  audio_path = video_processor.download_youtube_audio(youtube_url)
794
  process_and_display_results(audio_path)
795
+
796
+ #if youtube_url and st.button("Analyser"):
797
+ # if not re.match(r'^https?://(?:www\.)?youtube\.com/watch\?v=[\w-]+|^https?://youtu\.be/[\w-]+', youtube_url):
798
+ # st.error("URL YouTube invalide")
799
+ # else:
800
+ # video_processor = VideoProcessor()
801
+ # video_id = video_processor.extract_video_id(youtube_url)
802
+ # if video_id:
803
+ # st.video(youtube_url)
804
+
805
+ # with st.spinner("Récupération du contenu de la vidéo..."):
806
+ # Essayer d'abord d'obtenir la transcription
807
+ # transcription = video_processor.get_youtube_transcription(video_id)
808
+
809
+ # if transcription:
810
+ # st.success("Transcription YouTube trouvée!")
811
+ # process_and_display_results(None, None, transcription)
812
+ # else:
813
+ # st.info("Pas de transcription disponible. Extraction de l'audio...")
814
+ # video_processor.load_cookies()
815
+ # audio_path = video_processor.download_youtube_audio(youtube_url)
816
+ # process_and_display_results(audio_path)
817
 
818
 
819
  def process_and_display_results(file_path=None, audio_bytes=None, transcription=None):
820
+ """Traitement et affichage des résultats"""
821
+ try:
822
+ if transcription is None:
823
+ transcription = get_transcription(file_path, audio_bytes)
824
+
825
+ if transcription:
826
+ display_transcription_and_summary(transcription)
827
+ finally:
828
+ cleanup_temporary_files()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
829
 
830
+ def get_transcription(file_path=None, audio_bytes=None) -> str:
831
+ """Obtention de la transcription"""
832
+ if file_path:
833
+ path = file_path if isinstance(file_path, str) else save_uploaded_file(file_path)
834
+ elif audio_bytes:
835
+ path = save_audio_bytes(audio_bytes)
836
+ else:
837
+ return None
838
 
839
+ chunks = st.session_state.audio_processor.chunk_audio(path)
840
+ transcriptions = []
841
+
842
+ with st.expander("Transcription", expanded=False):
843
+ progress_bar = st.progress(0)
844
+ for i, chunk in enumerate(chunks):
845
+ transcription = st.session_state.audio_processor.transcribe_chunk(chunk)
846
+ if transcription:
847
+ transcriptions.append(transcription)
848
+ progress_bar.progress((i + 1) / len(chunks))
849
+
850
+ return " ".join(transcriptions) if transcriptions else None
851
 
852
  def get_summary(full_transcription):
853
  if full_transcription is not None:
 
858
  separators=["\n\n", "\n", " ", ""]
859
  )
860
  chunks = text_splitter.split_text(full_transcription)
861
+
862
+ # Résumé basé sur le nombre de morceaux
863
  if len(chunks) > 1:
864
  summary = st.session_state.audio_processor.summarize_long_transcription(full_transcription)
865
  else:
866
  summary = st.session_state.audio_processor.generate_summary(full_transcription)
867
  else:
868
  st.error("La transcription a échoué")
869
+ return None # Retourne None si la transcription est invalide
870
+
871
+ return summary # Retourne le résumé
872
+
873
+ def generate_and_download_documents(summary: str):
874
+ """Génération et téléchargement des documents"""
875
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
876
+
877
+ # Génération PDF
878
+ pdf_filename = f"resume_{timestamp}.pdf"
879
+ pdf_path = PDFGenerator.create_pdf(summary, pdf_filename)
880
+
881
+ # Génération DOCX
882
+ docx_filename = f"resume_{timestamp}.docx"
883
+ docx_path = generate_docx(summary, docx_filename)
884
+
885
+ # Boutons de téléchargement avec des clés uniques
886
+ col1, col2 = st.columns(2)
887
+ with col1:
888
+ with open(pdf_path, "rb") as pdf_file:
889
+ st.download_button(
890
+ "📥 Télécharger PDF",
891
+ pdf_file,
892
+ file_name=pdf_filename,
893
+ mime="application/pdf",
894
+ key=f"download_pdf_{uuid.uuid4()}" # Utilisation d'un UUID
895
+ )
896
+
897
+ with col2:
898
+ with open(docx_path, "rb") as docx_file:
899
+ st.download_button(
900
+ "📥 Télécharger DOCX",
901
+ docx_file,
902
+ file_name=docx_filename,
903
+ mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
904
+ key=f"download_docx_{uuid.uuid4()}" # Utilisation d'un UUID
905
+ )
906
+
907
+ return pdf_path
908
+
909
+
910
+ def display_transcription_and_summary(transcription: str):
911
+ """Affichage de la transcription et du résumé"""
912
+ st.subheader("Transcription")
913
+ st.text_area("Texte transcrit:", value=transcription, height=200)
914
+
915
+ st.subheader("Résumé et Analyse")
916
+ summary = get_summary(transcription)
917
+ st.markdown(summary)
918
+
919
+ # Génération et téléchargement des documents
920
+ #generate_and_download_documents(summary)
921
+ display_summary_and_downloads(summary)
922
+ # Option d'envoi par email
923
+ #handle_email_sending(summary)
924
+
925
+
926
+
927
+
928
+ def handle_email_sending(summary: str):
929
+ """Gestion de l'envoi par email"""
930
+ st.subheader("📧 Recevoir le résumé par email")
931
+ recipient_email = st.text_input("Entrez votre adresse email:")
932
+
933
+ if st.button("Envoyer par email"):
934
+ if not is_valid_email(recipient_email):
935
+ st.error("Veuillez entrer une adresse email valide.")
936
+ return
937
+
938
+ with st.spinner("Envoi de l'email en cours..."):
939
+ pdf_path = generate_and_download_documents(summary)
940
+ email_sender = EmailSender(SENDER_EMAIL, SENDER_PASSWORD)
941
+
942
+ if email_sender.send_email(
943
+ recipient_email,
944
+ "Résumé de votre contenu audio/vidéo",
945
+ "Veuillez trouver ci-joint le résumé de votre contenu.",
946
+ pdf_path
947
+ ):
948
+ st.success("Email envoyé avec succès!")
949
+ else:
950
+ st.error("Échec de l'envoi de l'email.")
951
+
952
+
953
+
954
+ def cleanup_temporary_files():
955
+ """Nettoyage des fichiers temporaires"""
956
+ temp_files = ['temp_audio.mp3', 'temp_video.mp4']
957
+ for temp_file in temp_files:
958
+ if os.path.exists(temp_file):
959
+ try:
960
+ os.remove(temp_file)
961
+ except Exception:
962
+ pass
963
+
964
+ def process_document():
965
+ """Traitement des documents"""
966
+ file = st.file_uploader("Chargez votre document", type=['pdf', 'docx', 'pptx', 'txt'])
967
+ password = st.text_input("Mot de passe (si protégé)", type="password")
968
+
969
+ if file:
970
+ try:
971
+ doc_processor = DocumentProcessor(
972
+ st.session_state.audio_processor.llm.model_name,
973
+ st.session_state.audio_processor.custom_prompt
974
+ )
975
+ text = process_document_with_password(file, password, doc_processor)
976
+ if text:
977
+ summary = doc_processor.summarize_text(text)
978
+ st.markdown("### 📝 Résumé et Analyse")
979
+ st.markdown(summary)
980
+ display_summary_and_downloads(summary)
981
+ except ValueError as e:
982
+ st.error(str(e))
983
+
984
+ def process_document_with_password(file, password: str, doc_processor: DocumentProcessor) -> Optional[str]:
985
+ """Traitement des documents protégés par mot de passe"""
986
+ file_extension = os.path.splitext(file.name)[1].lower()
987
+
988
+ try:
989
+ if file_extension == '.pdf':
990
+ return doc_processor.process_protected_pdf(file, password)
991
+ elif file_extension in ['.docx', '.pptx']:
992
+ return doc_processor.process_protected_office(file, file_extension[1:], password)
993
+ elif file_extension == '.txt':
994
+ return file.read().decode('utf-8')
995
+ else:
996
+ st.error("Format de fichier non supporté")
997
+ return None
998
+ except ValueError as e:
999
+ st.error(str(e))
1000
  return None
 
1001
 
1002
 
1003
+
1004
+
1005
+ def is_text_content(url):
1006
+ try:
1007
+ # Utiliser Selenium ou Playwright pour le rendu JavaScript
1008
+ response = requests.get(url)
1009
+ return ('text' in response.headers.get('content-type', '').lower()
1010
+ or 'html' in response.headers.get('content-type', '').lower()
1011
+ or 'application/json' in response.headers.get('content-type', '').lower())
1012
+ except:
1013
+ return False
1014
+
1015
+ def is_valid_content_url(url):
1016
+ """Vérifie si l'URL est valide pour l'extraction de contenu"""
1017
+ parsed = urlparse(url)
1018
+
1019
+ excluded_domains = [
1020
+ 'youtube.com', 'vimeo.com', 'dailymotion.com',
1021
+ 'imgur.com', 'flickr.com', 'instagram.com',
1022
+ 'facebook.com', 'fb.com', 'twitter.com', 'x.com',
1023
+ 'tiktok.com', 'linkedin.com', 'pinterest.com',
1024
+ 'snapchat.com', 'reddit.com', 'tumblr.com',
1025
+ 'whatsapp.com', 'telegram.org', 'discord.com'
1026
+ ]
1027
+
1028
+ excluded_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mp3', '.pdf']
1029
+
1030
+ domain = parsed.netloc.lower()
1031
+ path = parsed.path.lower()
1032
+
1033
+ return not (
1034
+ any(exc in domain for exc in excluded_domains) or
1035
+ any(path.endswith(ext) for ext in excluded_extensions)
1036
+ )
1037
+
1038
+ def process_web():
1039
+ """Traitement des contenus web"""
1040
+ url = st.text_input("URL du site web")
1041
+ auth_required = st.checkbox("Authentification requise")
1042
+
1043
+ auth = None
1044
+ if auth_required:
1045
+ username = st.text_input("Nom d'utilisateur")
1046
+ password = st.text_input("Mot de passe", type="password")
1047
+ auth = {"username": username, "password": password}
1048
+
1049
+ if url and st.button("Analyser"):
1050
+ if not url.startswith(('http://', 'https://')):
1051
+ st.error("L'URL doit commencer par 'http://' ou 'https://'")
1052
+ return
1053
+
1054
+ if not is_valid_content_url(url):
1055
+ st.error(f"Cette URL ({url}) ne peut pas être traitée (vidéo, image ou autre contenu non supporté)")
1056
+ return
1057
+
1058
+ if not is_text_content(url):
1059
+ st.error(f"Cette URL ({url}) ne contient pas de contenu textuel analysable")
1060
+ return
1061
+
1062
+ try:
1063
+ doc_processor = DocumentProcessor(
1064
+ st.session_state.audio_processor.llm.model_name,
1065
+ st.session_state.audio_processor.custom_prompt
1066
+ )
1067
+ text = doc_processor.scrape_web_content(url, auth)
1068
+ if text:
1069
+ summary = doc_processor.summarize_text(text)
1070
+ st.markdown("### 📝 Résumé et Analyse")
1071
+ st.markdown(summary)
1072
+ display_summary_and_downloads(summary)
1073
+ except ValueError as e:
1074
+ st.error(str(e))
1075
+
1076
  def display_summary_and_downloads(summary: str):
1077
+ """Affichage du résumé et options de téléchargement"""
1078
+ #st.markdown("### 📝 Résumé et Analyse")
1079
+ #st.markdown(summary)
1080
+
1081
  timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
1082
+
1083
+ # Génération PDF
1084
  pdf_filename = f"resume_{timestamp}.pdf"
1085
  pdf_path = PDFGenerator.create_pdf(summary, pdf_filename)
1086
+
1087
+ # Génération DOCX
1088
  docx_filename = f"resume_{timestamp}.docx"
1089
  docx_path = generate_docx(summary, docx_filename)
1090
+
1091
+ # Boutons de téléchargement
1092
  col1, col2 = st.columns(2)
1093
  with col1:
1094
  with open(pdf_path, "rb") as pdf_file:
 
1098
  file_name=pdf_filename,
1099
  mime="application/pdf"
1100
  )
1101
+
1102
  with col2:
1103
  with open(docx_path, "rb") as docx_file:
1104
  st.download_button(
 
1107
  file_name=docx_filename,
1108
  mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
1109
  )
1110
+
1111
+ # Option d'envoi par email
1112
  st.markdown("### 📧 Recevoir le résumé par email")
1113
  recipient_email = st.text_input("Entrez votre adresse email:")
1114
+
1115
  if st.button("Envoyer par email"):
1116
  if not is_valid_email(recipient_email):
1117
  st.error("Veuillez entrer une adresse email valide.")
 
1128
  else:
1129
  st.error("Échec de l'envoi de l'email.")
1130
 
1131
+ def save_audio_bytes(audio_bytes: bytes) -> str:
1132
+ """Sauvegarde les bytes audio dans un fichier temporaire"""
1133
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1134
+ file_path = f"recording_{timestamp}.wav"
1135
+ with open(file_path, 'wb') as f:
1136
+ f.write(audio_bytes)
1137
+ return file_path
1138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1139
  if __name__ == "__main__":
1140
  try:
1141
  enhance_main()
 
1143
  st.error(f"Une erreur inattendue est survenue: {str(e)}")
1144
  st.error("Veuillez réessayer ou contacter le support technique.")
1145
  finally:
1146
+ cleanup_temporary_files()