Adjoumani commited on
Commit
8821c14
·
verified ·
1 Parent(s): d0c2a08

Update app.py

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