Adjoumani commited on
Commit
56d32ee
·
verified ·
1 Parent(s): a37c902

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1023 -225
app.py CHANGED
@@ -1,250 +1,1048 @@
1
  import streamlit as st
2
- #from streamlit_image_zoom import image_zoom # Import de la bibliothèque pour le zoom
3
- import base64
4
  import os
5
- from dotenv import load_dotenv
6
- #from anthropic import Anthropic
7
- from openai import OpenAI
8
- from PIL import Image, ImageOps
9
  import tempfile
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- load_dotenv()
12
-
13
-
14
- client = OpenAI(
15
-
16
- base_url="https://api.x.ai/v1",
17
- )
18
- #client = Anthropic()
19
- #MODEL_NAME ="claude-3-opus-20240229"
20
- # Définir le style global
21
- st.set_page_config(
22
- page_title="Le Radiologue Virtuel",
23
- page_icon="🩺",
24
- layout="centered",
25
- )
26
-
27
- # CSS personnalisé pour styliser l'interface
28
- st.markdown("""
29
- <style>
30
- body {
31
- background-color: #f4f4f9;
32
- color: #333333;
33
- font-family: 'Arial', sans-serif;
34
- }
35
- .stButton>button {
36
- background-color: #007bff;
37
- color: white;
38
- border-radius: 8px;
39
- padding: 0.5em 1.5em;
40
- font-size: 1em;
41
- border: none;
42
- transition: all 0.3s ease-in-out;
43
- }
44
- .stButton>button:hover {
45
- background-color: #0056b3;
46
- transform: scale(1.05);
47
- }
48
- .st-expander-header {
49
- font-weight: bold;
50
- font-size: 1.2em;
51
- color: #007bff;
52
- }
53
- .stMarkdown {
54
- font-size: 1.1em;
55
- }
56
- </style>
57
- """, unsafe_allow_html=True)
58
-
59
- sample_prompt = """
60
- Vous êtes un professionnel de santé, expert dans l'analyse des images médicales (échographie, radiographie standard, TDM, IRM, ...), travaillant pour un hôpital de renom. Votre tâche consiste à examiner des images médicales afin d'identifier les anomalies, maladies ou problèmes de santé éventuels. Vous devez fournir des résultats détaillés, incluant toutes vos observations, les prochaines étapes, ainsi que des recommandations.
61
- Votre analyse reste dans le cadre éducatif d'étudiants en radiologie médicale et ne sera en aucun cas publique. L'objectif est d'initier ces étudiants à l'interpretation des clichés radiologique.
62
- **Instructions :**
63
- 1. Analysez uniquement les images liées au corps humain et aux problèmes de santé.
64
- 2. Rédigez votre réponse de manière structurée, détaillant :
65
- - Les anomalies identifiées (le cas échéant).
66
- - Les pathologies suspectées ou confirmées.
67
- - Les recommandations et étapes suivantes.
68
- 3. Si certains éléments ne sont pas clairs sur l'image, indiquez : *"Impossible de déterminer à partir de l'image fournie."*
69
- 4. Terminez toujours votre réponse par une mention : *"Consultez un médecin avant de prendre toute décision."*
70
- """
71
-
72
- # Initialisation des variables d'état
73
- if 'uploaded_file' not in st.session_state:
74
- st.session_state.uploaded_file = None
75
- if 'result' not in st.session_state:
76
- st.session_state.result = None
77
- if 'clinical_info' not in st.session_state:
78
- st.session_state.clinical_info = "" # Pour stocker les renseignements cliniques
79
-
80
-
81
-
82
- def encode_image(image_path):
83
- with open(image_path, "rb") as image_file:
84
- return base64.b64encode(image_file.read()).decode("utf-8")
85
-
86
-
87
- def call_groq_model_for_analysis(filename: str, clinical_info: str, sample_prompt=sample_prompt):
88
- # Récupérer l'extension du fichier pour définir le media_type
89
- file_extension = os.path.splitext(filename)[1].lower() # Obtenir l'extension en minuscule
90
- if file_extension == ".jpg" or file_extension == ".jpeg":
91
- media_type = "image/jpeg"
92
- elif file_extension == ".png":
93
- media_type = "image/png"
94
- else:
95
- raise ValueError("Format de fichier non pris en charge.")
96
- base64_image = encode_image(filename)
97
- # Ajouter les renseignements cliniques au prompt
98
- full_prompt = sample_prompt + f"\n\n**Renseignements cliniques du patient :** {clinical_info}\n"
99
-
100
-
101
-
102
-
103
- messages_list=[
104
- {
105
- "role": "user",
106
- "content": [
107
- {
108
- "type": "text",
109
- "text": full_prompt,
110
- },
111
- {
112
- "type": "image_url",
113
- "image_url": {"url": f"data:{media_type};base64,{base64_image}"},
114
- },
115
- ],
116
- }
117
- ]
118
 
119
- response = client.chat.completions.create(
120
- model="grok-2-vision-1212",
121
- temperature=0.01,
122
- messages=messages_list,
123
- )
124
 
125
- return response.choices[0].message.content
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
- def chat_eli(query):
129
- eli5_prompt = "Tu dois expliquer l'information ci-dessous à un enfant de cinq ans. \n" + query
130
- messages = [
131
- {
132
- "role": "user",
133
- "content": eli5_prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  }
135
- ]
136
 
137
- """response = client.messages.create(
138
- model="claude-3-5-sonnet-20241022",
139
- max_tokens=1024,
140
- messages=messages
141
- )"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
- completion = client.chat.completions.create(
144
- model="grok-2-latest",
145
- messages=messages
146
- )
 
 
 
 
 
 
 
 
 
 
147
 
148
- return completion.choices[0].message.content
 
 
 
 
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
- # Titre de l'application
152
- st.title("🩺 **Le Radiologue Virtuel**")
153
- st.subheader("Une IA avancée pour l'analyse des images médicales")
154
 
155
- with st.expander("📖 A-Propos de cette Application"):
156
- st.markdown("""
157
- **Bienvenue sur LE RADIOLOGUE VIRTUEL**, votre assistant intelligent conçu pour fournir une analyse approfondie et précise des images médicales.
 
158
 
159
- ### Fonctionnalités principales :
160
- - **Analyse d'images médicales** : Téléchargez des clichés d'échographie, de radiographie, d'IRM ou de TDM, et laissez l'IA détecter les anomalies et fournir des recommandations détaillées.
161
- - **Explications simplifiées** : Grâce à la fonction ELI5, comprenez les résultats complexes sous une forme adaptée à un public non expert.
162
- - **Traitement d'image avancé** : Explorez l'image téléchargée avec des outils comme l'inversion pour une visualisation plus claire.
163
 
164
- ### Cas d'utilisation :
165
- - **Éducation médicale** : Destiné aux étudiants en radiologie, cet outil aide à se familiariser avec l'interprétation des images diagnostiques.
166
- - **Support clinique** : Bien que non conçu pour remplacer un professionnel de santé, cet assistant peut fournir des indications utiles pour guider les analyses.
167
- - **Recherche et apprentissage** : Une plateforme idéale pour expérimenter et apprendre l'impact de l'IA dans le domaine médical.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
- ### Technologie utilisée :
170
- - **Modèle d'IA puissant** : L'IA utilise la technologie avancée de Llama 3.2 90B Vision, spécialisée dans l'analyse d'images complexes.
171
- - **Interaction intuitive** : Développé avec Python et Streamlit pour une interface simple et conviviale.
172
- - **Sécurité et confidentialité** : Toutes les analyses sont effectuées localement ou sur des serveurs sécurisés, garantissant la confidentialité des données médicales.
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- **⚠️ Avertissement** :
175
- - Cet assistant n'est pas un outil médical certifié et ne remplace en aucun cas l'avis d'un médecin ou d'un spécialiste. Il est destiné à des fins éducatives et de support. Consultez toujours un professionnel de santé pour un diagnostic ou une décision médicale.
176
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
- # Champ d'entrée pour les renseignements cliniques
179
- clinical_info = st.text_area(
180
- "Renseignements cliniques du patient (facultatif)",
181
- placeholder="Exemple : Patient présentant une douleur thoracique depuis 3 jours."
182
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
- # Stocker les renseignements cliniques dans la session
185
- st.session_state['clinical_info'] = clinical_info
186
 
187
- # Téléchargement de l'image
188
- st.markdown("### 📂 Téléchargez une image médicale")
189
- uploaded_file = st.file_uploader("Formats acceptés : JPG, JPEG, PNG", type=["jpg", "jpeg", "png"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
- # Gestion temporaire des fichiers
192
- if uploaded_file is not None:
193
  with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(uploaded_file.name)[1]) as tmp_file:
194
  tmp_file.write(uploaded_file.getvalue())
195
- st.session_state['filename'] = tmp_file.name
196
-
197
- st.image(uploaded_file, caption='Image Téléchargée')
198
-
199
- # Charger l'image avec PIL
200
- image = Image.open(uploaded_file)
201
-
202
- # Ajouter un bouton pour afficher l'image en négatif
203
- st.markdown("### 🔍 Explorez l'image")
204
- col1, col2, col3 = st.columns([1, 2, 1]) # colonnes avec différentes proportions
205
- col1.write("") # Espace dans la première colonne
206
- col3.write("") # Espace dans la troisième colonne
207
- col1.write("") # Espace dans la première colonne
208
- col3.write("") # Espace dans la troisième colonne
209
- if st.button("Afficher l'image en négatif"):
210
- # Créer une version négative de l'image
211
- negative_image = ImageOps.invert(image.convert("RGB")) # Convertir en RGB avant inversion
212
-
213
- # Afficher l'image en négatif
214
- st.subheader("Image en négatif :")
215
- st.image(negative_image, caption="Image en négatif")
216
- col1, col2, col3 = st.columns([1, 2, 1]) # colonnes avec différentes proportions
217
- col1.write("") # Espace dans la première colonne
218
- col3.write("") # Espace dans la troisième colonne
219
- col1.write("") # Espace dans la première colonne
220
- col3.write("") # Espace dans la troisième colonne
221
-
222
- # Bouton pour analyser l'image
223
- if st.button("Analyse l'Image"):
224
- if 'filename' in st.session_state and os.path.exists(st.session_state['filename']):
225
- with st.spinner("Analyse en cours... Veuillez patienter."):
226
- st.session_state['result'] = call_groq_model_for_analysis(
227
- st.session_state['filename'],
228
- st.session_state['clinical_info']
229
- )
230
- st.success("Analyse terminée avec succès !")
231
- st.markdown(st.session_state['result'], unsafe_allow_html=True)
232
- os.unlink(st.session_state['filename']) # Supprimer le fichier temporaire après le traitement
233
-
234
- # ELI5 Explanation
235
- # Explication simplifiée
236
- st.markdown("### 🤓 Explication Simplifiée")
237
- if 'result' in st.session_state and st.session_state['result']:
238
- st.info("Ci-dessous, vous avez une option pour ELI5 afin de comprendre en termes simples.")
239
- if st.radio("ELI5 - Explique-moi comme si j'avais 5 ans", ('NON', 'OUI')) == 'OUI':
240
- st.markdown("_Voici une explication simplifiée pour les non-initiés._")
241
- simplified_explanation = chat_eli(st.session_state['result'])
242
- st.markdown(simplified_explanation, unsafe_allow_html=True)
243
-
244
- # Pied de page
245
- st.markdown("""
246
- <hr>
247
- <footer style="text-align: center; font-size: 0.9em;">
248
- © 2024 - Le Radiologue Virtuel | Propulsé par M. ADJOUMANI
249
- </footer>
250
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ from audio_recorder_streamlit import audio_recorder
 
3
  import os
4
+ import msoffcrypto
5
+ import docx
6
+ import pptx
7
+ import pymupdf4llm
8
  import tempfile
9
+ from typing import List, Optional, Dict, Any
10
+ from pydub import AudioSegment
11
+ from groq import Groq
12
+ from langchain.chains import LLMChain
13
+ from langchain_groq import ChatGroq
14
+ from langchain.prompts import PromptTemplate
15
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
16
+ from langchain.schema import AIMessage, HumanMessage, SystemMessage
17
+ from datetime import datetime
18
+ import smtplib
19
+ from email.mime.text import MIMEText
20
+ from email.mime.multipart import MIMEMultipart
21
+ from email.mime.application import MIMEApplication
22
+ from reportlab.lib import colors
23
+ from reportlab.lib.pagesizes import letter
24
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
25
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
26
+ import re
27
+ from docx import Document
28
+ from pytube import YouTube
29
+ from moviepy import VideoFileClip
30
+ import yt_dlp
31
+ from youtube_transcript_api import YouTubeTranscriptApi
32
+ from urllib.parse import urlparse, parse_qs
33
+ from ratelimit import limits, sleep_and_retry
34
+ import time
35
+ import fasttext
36
+ import requests
37
+ from requests.auth import HTTPBasicAuth
38
+ import pikepdf
39
+ import io
40
+ import pypdf
41
+ from PyPDF2 import PdfReader
42
 
43
+ from pptx import Presentation
44
+ import trafilatura
45
+ from bs4 import BeautifulSoup
46
+
47
+ class Config:
48
+ """Centralisation de la configuration"""
49
+ GROQ_API_KEY = "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"
50
+ SENDER_EMAIL = "[email protected]"
51
+ SENDER_PASSWORD = "fkev txsk ldjg nyqs"
52
+ FASTTEXT_MODEL_PATH = "lid.176.bin"
53
+ import urllib.request
54
+ urllib.request.urlretrieve('https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.bin', 'lid.176.bin')
55
+
56
+
57
+ # Classes PDFGenerator et EmailSender restent inchangées...
58
+ class PDFGenerator:
59
+ @staticmethod
60
+ def create_pdf(content: str, filename: str) -> str:
61
+ doc = SimpleDocTemplate(filename, pagesize=letter)
62
+ styles = getSampleStyleSheet()
63
+ custom_style = ParagraphStyle(
64
+ 'CustomStyle',
65
+ parent=styles['Normal'],
66
+ spaceBefore=12,
67
+ spaceAfter=12,
68
+ fontSize=12,
69
+ leading=14,
70
+ )
71
+
72
+ story = []
73
+ title_style = ParagraphStyle(
74
+ 'CustomTitle',
75
+ parent=styles['Heading1'],
76
+ fontSize=16,
77
+ spaceAfter=30,
78
+ )
79
+ story.append(Paragraph("Résumé Audio", title_style))
80
+ story.append(Paragraph(f"Date: {datetime.now().strftime('%d/%m/%Y %H:%M')}", custom_style))
81
+ story.append(Spacer(1, 20))
82
+
83
+ for line in content.split('\n'):
84
+ if line.strip():
85
+ if line.startswith('#'):
86
+ story.append(Paragraph(line.strip('# '), styles['Heading2']))
87
+ else:
88
+ story.append(Paragraph(line, custom_style))
89
+
90
+ doc.build(story)
91
+ return filename
92
+
93
+ class EmailSender:
94
+ def __init__(self, sender_email: str, sender_password: str):
95
+ self.sender_email = Config.SENDER_EMAIL
96
+ self.sender_password = Config.SENDER_PASSWORD
97
+
98
+ def send_email(self, recipient_email: str, subject: str, body: str, pdf_path: str) -> bool:
99
+ try:
100
+ msg = MIMEMultipart()
101
+ msg['From'] = self.sender_email
102
+ msg['To'] = recipient_email
103
+ msg['Subject'] = subject
104
+ msg.attach(MIMEText(body, 'plain'))
105
+
106
+ with open(pdf_path, 'rb') as f:
107
+ pdf_attachment = MIMEApplication(f.read(), _subtype='pdf')
108
+ pdf_attachment.add_header('Content-Disposition', 'attachment', filename=os.path.basename(pdf_path))
109
+ msg.attach(pdf_attachment)
110
+
111
+ server = smtplib.SMTP('smtp.gmail.com', 587)
112
+ server.starttls()
113
+ server.login(self.sender_email, self.sender_password)
114
+ server.send_message(msg)
115
+ server.quit()
116
+ return True
117
+ except Exception as e:
118
+ st.error(f"Erreur d'envoi d'email: {str(e)}")
119
+ return False
120
+
121
+ class AudioProcessor:
122
+ def __init__(self, model_name: str, prompt: str = None, chunk_length_ms: int = 300000):
123
+ self.chunk_length_ms = chunk_length_ms
124
+ self.groq_client = Groq(api_key=Config.GROQ_API_KEY)
125
+ self.llm = ChatGroq(
126
+ model=model_name,
127
+ temperature=0,
128
+ api_key=Config.GROQ_API_KEY
129
+ )
130
+ self.custom_prompt = prompt
131
+ self.language_detector = fasttext.load_model(Config.FASTTEXT_MODEL_PATH)
132
+ self.text_splitter = RecursiveCharacterTextSplitter(
133
+ chunk_size=4000,
134
+ chunk_overlap=200
135
+ )
136
+ #self.custom_prompt = prompt
137
+ # Définition des limites de taux : 5000 tokens par minute
138
+ self.CALLS_PER_MINUTE = 5000
139
+ self.PERIOD = 60 # 60 secondes = 1 minute
140
+ # Add language detection model
141
+ #self.language_detector = fasttext.load_model('lid.176.bin')
 
 
 
 
 
 
 
 
142
 
143
+ def check_language(self, text: str) -> str:
144
+ """Vérifie si le texte est en français"""
145
+ prediction = self.language_detector.predict(text.replace('\n', ' '))
146
+ return "OUI" if prediction[0][0] == '__label__fr' else "NON"
 
147
 
148
+ def translate_to_french(self, text: str) -> str:
149
+ """Traduit le texte en français si nécessaire"""
150
+ try:
151
+ messages = [
152
+ SystemMessage(content="Vous êtes un traducteur professionnel. Traduisez le texte suivant en français en conservant le format et la structure:"),
153
+ HumanMessage(content=text)
154
+ ]
155
+ result = self._make_api_call(messages)
156
+ return result.generations[0][0].text
157
+ except Exception as e:
158
+ if "rate_limit_exceeded" in str(e):
159
+ time.sleep(60)
160
+ return self.translate_to_french(text)
161
+ raise e
162
 
163
+ @sleep_and_retry
164
+ @limits(calls=5000, period=60)
165
+ def _make_api_call(self, messages):
166
+ return self.llm.generate([messages])
167
+
168
+ def chunk_audio(self, file_path: str) -> List[AudioSegment]:
169
+ try:
170
+ audio = AudioSegment.from_file(file_path)
171
+ if len(audio) < self.chunk_length_ms:
172
+ return [audio]
173
+ return [
174
+ audio[i:i + self.chunk_length_ms]
175
+ for i in range(0, len(audio), self.chunk_length_ms)
176
+ ]
177
+ except Exception as e:
178
+ st.error(f"Error processing audio file: {str(e)}")
179
+ return []
180
+
181
+ def transcribe_chunk(self, audio_chunk: AudioSegment) -> str:
182
+ try:
183
+ with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as temp_file:
184
+ audio_chunk.export(temp_file.name, format="mp3")
185
+ with open(temp_file.name, "rb") as audio_file:
186
+ try:
187
+ response = self.groq_client.audio.transcriptions.create(
188
+ file=audio_file,
189
+ model="whisper-large-v3-turbo",
190
+ language="fr"
191
+ )
192
+ except Exception as e:
193
+ if "rate_limit_exceeded" in str(e):
194
+ st.warning("Limite de taux atteinte pendant la transcription. Attente avant nouvelle tentative...")
195
+ time.sleep(60)
196
+ return self.transcribe_chunk(audio_chunk)
197
+ raise e
198
+ os.unlink(temp_file.name)
199
+ return response.text
200
+ except Exception as e:
201
+ st.error(f"Transcription error: {str(e)}")
202
+ return ""
203
+
204
+
205
+ # Dans la classe AudioProcessor, ajoutez cette méthode :
206
+ def split_text(self, text: str, max_tokens: int = 4000) -> List[str]:
207
+ text_splitter = RecursiveCharacterTextSplitter(
208
+ chunk_size=max_tokens * 4, # Estimation approximative tokens -> caractères
209
+ chunk_overlap=200,
210
+ length_function=len,
211
+ separators=["\n\n", "\n", " ", ""]
212
+ )
213
+ return text_splitter.split_text(text)
214
+
215
+ def generate_summary(self, transcription: str) -> str:
216
+ default_prompt = """
217
+ Vous êtes un assistant expert spécialisé dans le résumé et l'analyse d'enregistrements audio en langue française.
218
+ Voici la transcription à analyser:
219
+
220
+ {transcript}
221
+
222
+ Veuillez fournir:
223
+ 1. Un résumé concis (3-4 phrases)
224
+ 2. Les points clés (maximum 5 points)
225
+ 3. Les actions recommandées (si pertinent)
226
+ 4. Une conclusion brève
227
+
228
+ Format souhaité:
229
+ # Résumé
230
+ [votre résumé]
231
+
232
+ # Points Clés
233
+ • [point 1]
234
+ • [point 2]
235
+ ...
236
+
237
+ # Actions Recommandées
238
+ 1. [action 1]
239
+ 2. [action 2]
240
+ ...
241
+
242
+ # Conclusion
243
+ [votre conclusion]
244
+ """
245
+
246
+ prompt_template = self.custom_prompt if self.custom_prompt else default_prompt
247
+
248
+ try:
249
+ chain = LLMChain(
250
+ llm=self.llm,
251
+ prompt=PromptTemplate(
252
+ template=prompt_template,
253
+ input_variables=["transcript"]
254
+ )
255
+ )
256
+
257
+ summary = chain.run(transcript=transcription)
258
+
259
+ # Vérification de la langue
260
+ if self.check_language(summary) == "NON":
261
+ st.warning("Résumé généré dans une autre langue. Traduction en cours...")
262
+ summary = self.translate_to_french(summary)
263
+
264
+ return summary
265
+ except Exception as e:
266
+ if "rate_limit_exceeded" in str(e):
267
+ st.warning("Limite de taux atteinte. Attente avant nouvelle tentative...")
268
+ time.sleep(60) # Attendre 1 minute
269
+ return self.generate_summary(transcription)
270
+ raise e
271
 
272
+ # Méthodes existantes inchangées...
273
+
274
+
275
+
276
+ def summarize_long_transcription(self, transcription: str) -> str:
277
+ chunks = self.split_text(transcription, max_tokens=4000)
278
+ partial_summaries = []
279
+
280
+ for i, chunk in enumerate(chunks):
281
+ st.write(f"Traitement du segment {i + 1}/{len(chunks)}...")
282
+ try:
283
+ messages = [
284
+ SystemMessage(content="Vous êtes un assistant expert en résumé de texte en français."),
285
+ HumanMessage(content=f"Résumez ce texte en français : {chunk}")
286
+ ]
287
+ result = self._make_api_call(messages)
288
+ partial_summary = result.generations[0][0].text
289
+
290
+ # Vérification de la langue pour chaque segment
291
+ if self.check_language(partial_summary) == "NON":
292
+ partial_summary = self.translate_to_french(partial_summary)
293
+
294
+ partial_summaries.append(partial_summary)
295
+ except Exception as e:
296
+ if "rate_limit_exceeded" in str(e):
297
+ st.warning(f"Limite de taux atteinte au segment {i+1}. Attente avant nouvelle tentative...")
298
+ time.sleep(60)
299
+ i -= 1
300
+ continue
301
+ raise e
302
+
303
+ try:
304
+ final_prompt = f"""Combinez ces résumés partiels en un résumé global cohérent en langue française :
305
+
306
+ {' '.join(partial_summaries)}
307
+ """
308
+ messages = [
309
+ SystemMessage(content="Vous êtes un assistant expert en résumé de texte en français."),
310
+ HumanMessage(content=final_prompt)
311
+ ]
312
+ final_result = self._make_api_call(messages)
313
+ final_summary = final_result.generations[0][0].text
314
+
315
+ # Vérification finale de la langue
316
+ if self.check_language(final_summary) == "NON":
317
+ st.warning("Résumé final dans une autre langue. Traduction en cours...")
318
+ final_summary = self.translate_to_french(final_summary)
319
+
320
+ return final_summary
321
+
322
+ except Exception as e:
323
+ if "rate_limit_exceeded" in str(e):
324
+ st.warning("Limite de taux atteinte lors de la génération du résumé final. Attente avant nouvelle tentative...")
325
+ time.sleep(60)
326
+ return self.summarize_long_transcription(transcription)
327
+ raise e
328
+ """def summarize_long_transcription(self, transcription: str) -> str:
329
+ try:
330
+ chunks = self.split_text(transcription)
331
+ partial_summaries = []
332
+
333
+ for i, chunk in enumerate(chunks):
334
+ st.write(f"Traitement du segment {i + 1}/{len(chunks)}...")
335
+ summary = self._process_chunk(chunk)
336
+ partial_summaries.append(summary)
337
+
338
+ return self._combine_summaries(partial_summaries)
339
+ except Exception as e:
340
+ if "rate_limit_exceeded" in str(e):
341
+ time.sleep(60)
342
+ return self.summarize_long_transcription(transcription)
343
+ raise e
344
+
345
+ def _process_chunk(self, chunk: str) -> str:
346
+ messages = [
347
+ SystemMessage(content="Résumez ce texte en français :"),
348
+ HumanMessage(content=chunk)
349
+ ]
350
+ result = self._make_api_call(messages)
351
+ summary = result.generations[0][0].text
352
+
353
+ if self.check_language(summary) == "NON":
354
+ summary = self.translate_to_french(summary)
355
+
356
+ return summary
357
+
358
+ def _combine_summaries(self, summaries: List[str]) -> str:
359
+ try:
360
+ messages = [
361
+ SystemMessage(content="Combinez ces résumés en un résumé global cohérent en français :"),
362
+ HumanMessage(content=' '.join(summaries))
363
+ ]
364
+ result = self._make_api_call(messages)
365
+ final_summary = result.generations[0][0].text
366
+
367
+ if self.check_language(final_summary) == "NON":
368
+ final_summary = self.translate_to_french(final_summary)
369
+
370
+ return final_summary
371
+ except Exception as e:
372
+ if "rate_limit_exceeded" in str(e):
373
+ time.sleep(60)
374
+ return self._combine_summaries(summaries)
375
+ raise e"""
376
+
377
+ class VideoProcessor:
378
+ def __init__(self):
379
+ self.supported_formats = ['.mp4', '.avi', '.mov', '.mkv']
380
+ self.ydl_opts = {
381
+ 'format': 'bestaudio/best',
382
+ 'postprocessors': [{
383
+ 'key': 'FFmpegExtractAudio',
384
+ 'preferredcodec': 'mp3',
385
+ 'preferredquality': '192',
386
+ }],
387
+ 'outtmpl': 'temp_audio.%(ext)s'
388
  }
 
389
 
390
+ def extract_video_id(self, url: str) -> str:
391
+ try:
392
+ parsed_url = urlparse(url)
393
+ if parsed_url.hostname in ['www.youtube.com', 'youtube.com']:
394
+ return parse_qs(parsed_url.query)['v'][0]
395
+ elif parsed_url.hostname == 'youtu.be':
396
+ return parsed_url.path[1:]
397
+ return None
398
+ except Exception:
399
+ return None
400
+
401
+ def get_youtube_transcription(self, video_id: str) -> Optional[str]:
402
+ try:
403
+ transcript_list = YouTubeTranscriptApi.get_transcript(video_id, languages=['fr', 'en'])
404
+ return ' '.join(entry['text'] for entry in transcript_list)
405
+ except Exception:
406
+ return None
407
+
408
+ def download_youtube_audio(self, url: str) -> str:
409
+ with yt_dlp.YoutubeDL(self.ydl_opts) as ydl:
410
+ ydl.download([url])
411
+ return 'temp_audio.mp3'
412
+
413
+ def extract_audio_from_video(self, video_path: str) -> str:
414
+ try:
415
+ audio_path = f"{os.path.splitext(video_path)[0]}.mp3"
416
+ with VideoFileClip(video_path) as video:
417
+ video.audio.write_audiofile(audio_path)
418
+ return audio_path
419
+ except Exception as e:
420
+ st.error(f"Erreur lors de l'extraction audio: {str(e)}")
421
+ raise
422
+
423
+ class DocumentProcessor:
424
+ def __init__(self, model_name: str, prompt: str = None):
425
+ self.llm = ChatGroq(
426
+ model=model_name,
427
+ temperature=0,
428
+ api_key=Config.GROQ_API_KEY
429
+ )
430
+ self.custom_prompt = prompt
431
+ #self.text_splitter = RecursiveCharacterTextSplitter(
432
+ # chunk_size=4000,
433
+ # chunk_overlap=200
434
+ #)
435
+ self.language_detector = fasttext.load_model('lid.176.bin')
436
+
437
+ def split_text(self, text: str, max_tokens: int = 4000) -> List[str]:
438
+ text_splitter = RecursiveCharacterTextSplitter(
439
+ chunk_size=max_tokens * 4, # Estimation approximative tokens -> caractères
440
+ chunk_overlap=200,
441
+ length_function=len,
442
+ separators=["\n\n", "\n", " ", ""]
443
+ )
444
+ return text_splitter.split_text(text)
445
+
446
+ def check_language(self, text: str) -> str:
447
+ """Vérifie si le texte est en français"""
448
+ prediction = self.language_detector.predict(text.replace('\n', ' '))
449
+ return "OUI" if prediction[0][0] == '__label__fr' else "NON"
450
 
451
+ def translate_to_french(self, text: str) -> str:
452
+ """Traduit le texte en français si nécessaire"""
453
+ try:
454
+ messages = [
455
+ SystemMessage(content="Vous êtes un traducteur professionnel. Traduisez le texte suivant en français en conservant le format et la structure:"),
456
+ HumanMessage(content=text)
457
+ ]
458
+ result = self._make_api_call(messages)
459
+ return result.generations[0][0].text
460
+ except Exception as e:
461
+ if "rate_limit_exceeded" in str(e):
462
+ time.sleep(60)
463
+ return self.translate_to_french(text)
464
+ raise e
465
 
466
+ # Méthodes existantes de DocumentProcessor inchangées...
467
+ @sleep_and_retry
468
+ @limits(calls=5000, period=60)
469
+ def _make_api_call(self, messages):
470
+ return self.llm.generate([messages])
471
 
472
+
473
+
474
+ def process_protected_pdf(self, file_path: str, password: str = None) -> str:
475
+ """
476
+ Traite un PDF, avec ou sans mot de passe, et extrait le texte.
477
+
478
+ :param file_path: Chemin vers le fichier PDF.
479
+ :param password: Mot de passe du fichier PDF (si nécessaire).
480
+ :return: Texte extrait du PDF.
481
+ """
482
+ try:
483
+ # Si un mot de passe est fourni, tenter de déverrouiller le PDF
484
+ if password:
485
+ with pikepdf.open(file_path, password=password) as pdf:
486
+ unlocked_pdf_path = "unlocked_temp.pdf"
487
+ pdf.save(unlocked_pdf_path)
488
+
489
+ # Utiliser le fichier temporaire déverrouillé
490
+ reader = PdfReader(unlocked_pdf_path)
491
+ text = ""
492
+ for page in reader.pages:
493
+ text += page.extract_text()
494
+
495
+ # Supprimer le fichier temporaire
496
+ os.remove(unlocked_pdf_path)
497
+
498
+ else:
499
+ # Si aucun mot de passe, traiter directement le PDF
500
+ reader = PdfReader(file_path)
501
+ text = ""
502
+ for page in reader.pages:
503
+ text += page.extract_text()
504
 
505
+ return text
 
 
506
 
507
+ except pikepdf.PasswordError:
508
+ raise ValueError("Mot de passe PDF incorrect")
509
+ except Exception as e:
510
+ raise RuntimeError(f"Erreur lors du traitement du PDF : {e}")
511
 
512
+ def process_protected_office(self, file, file_type: str, password: str = None) -> str:
513
+ """
514
+ Traite un fichier Office (protégé ou non) et extrait le texte.
 
515
 
516
+ :param file: Le fichier Office à traiter.
517
+ :param password: Mot de passe du fichier (si nécessaire, sinon None).
518
+ :param file_type: Type du fichier ('docx' ou 'pptx').
519
+ :return: Texte extrait du fichier.
520
+ """
521
+ try:
522
+ if password:
523
+ # Cas où un mot de passe est fourni, tenter de déverrouiller le fichier
524
+ office_file = msoffcrypto.OfficeFile(file)
525
+ office_file.load_key(password=password)
526
+
527
+ decrypted = io.BytesIO()
528
+ office_file.decrypt(decrypted)
529
+
530
+ if file_type == 'docx':
531
+ doc = docx.Document(decrypted)
532
+ return "\n".join([p.text for p in doc.paragraphs])
533
+ elif file_type == 'pptx':
534
+ ppt = pptx.Presentation(decrypted)
535
+ return "\n".join([shape.text for slide in ppt.slides
536
+ for shape in slide.shapes if hasattr(shape, "text")])
537
+ else:
538
+ # Cas où aucun mot de passe n'est fourni, traiter directement le fichier
539
+ if file_type == 'docx':
540
+ doc = docx.Document(file) # Charger le fichier sans décryptage
541
+ return "\n".join([p.text for p in doc.paragraphs])
542
+ elif file_type == 'pptx':
543
+ ppt = pptx.Presentation(file)
544
+ return "\n".join([shape.text for slide in ppt.slides
545
+ for shape in slide.shapes if hasattr(shape, "text")])
546
+
547
+ raise ValueError("Type de fichier non supporté. Utilisez 'docx' ou 'pptx'.")
548
+
549
+ except msoffcrypto.exceptions.InvalidKeyError:
550
+ raise ValueError("Mot de passe incorrect ou fichier non valide.")
551
+ except Exception as e:
552
+ raise RuntimeError(f"Erreur lors du traitement du fichier Office : {e}")
553
 
554
+ """def process_protected_office(self, file, password: str=None, file_type: str) -> str:
555
+ try:
556
+ office_file = msoffcrypto.OfficeFile(file)
557
+ office_file.load_key(password=password)
558
+
559
+ decrypted = io.BytesIO()
560
+ office_file.decrypt(decrypted)
561
+
562
+ if file_type == 'docx':
563
+ doc = docx.Document(decrypted)
564
+ return "\n".join([p.text for p in doc.paragraphs])
565
+ elif file_type == 'pptx':
566
+ ppt = pptx.Presentation(decrypted)
567
+ return "\n".join([shape.text for slide in ppt.slides
568
+ for shape in slide.shapes if hasattr(shape, "text")])
569
+ except Exception:
570
+ raise ValueError("Mot de passe document incorrect")"""
571
 
572
+ def scrape_web_content(self, url: str, auth: Dict[str, str] = None) -> str:
573
+ try:
574
+ if auth:
575
+ session = requests.Session()
576
+ session.auth = HTTPBasicAuth(auth['username'], auth['password'])
577
+ response = session.get(url, timeout=30)
578
+ else:
579
+ response = requests.get(url, timeout=30)
580
+
581
+ response.raise_for_status()
582
+ downloaded = trafilatura.extract(response.text)
583
+
584
+ if not downloaded:
585
+ raise ValueError("Impossible d'extraire le contenu de cette page")
586
+ return downloaded
587
+
588
+ except requests.exceptions.HTTPError as e:
589
+ if e.response.status_code == 401:
590
+ raise ValueError("Authentification requise pour accéder à cette page")
591
+ elif e.response.status_code == 404:
592
+ raise ValueError("Page introuvable")
593
+ else:
594
+ raise ValueError(f"Erreur HTTP: {e.response.status_code}")
595
+ except requests.exceptions.RequestException:
596
+ raise ValueError("URL invalide ou inaccessible")
597
 
598
+ def summarize_text(self, transcription: str) -> str:
599
+ chunks = self.split_text(transcription, max_tokens=4000)
600
+ partial_summaries = []
601
+
602
+ for i, chunk in enumerate(chunks):
603
+ st.write(f"Traitement du segment {i + 1}/{len(chunks)}...")
604
+ try:
605
+ messages = [
606
+ SystemMessage(content="Vous êtes un assistant expert en résumé de texte en français."),
607
+ HumanMessage(content=f"Résumez ce texte en français : {chunk}")
608
+ ]
609
+ result = self._make_api_call(messages)
610
+ partial_summary = result.generations[0][0].text
611
+
612
+ # Vérification de la langue pour chaque segment
613
+ if self.check_language(partial_summary) == "NON":
614
+ partial_summary = self.translate_to_french(partial_summary)
615
+
616
+ partial_summaries.append(partial_summary)
617
+ except Exception as e:
618
+ if "rate_limit_exceeded" in str(e):
619
+ st.warning(f"Limite de taux atteinte au segment {i+1}. Attente avant nouvelle tentative...")
620
+ time.sleep(60)
621
+ i -= 1
622
+ continue
623
+ raise e
624
+
625
+ try:
626
+ final_prompt = f"""Combinez ces résumés partiels en un résumé global cohérent en langue française :
627
+
628
+ {' '.join(partial_summaries)}
629
+ """
630
+ messages = [
631
+ SystemMessage(content="Vous êtes un assistant expert en résumé de texte en français."),
632
+ HumanMessage(content=final_prompt)
633
+ ]
634
+ final_result = self._make_api_call(messages)
635
+ final_summary = final_result.generations[0][0].text
636
+
637
+ # Vérification finale de la langue
638
+ if self.check_language(final_summary) == "NON":
639
+ st.warning("Résumé final dans une autre langue. Traduction en cours...")
640
+ final_summary = self.translate_to_french(final_summary)
641
+
642
+ return final_summary
643
+
644
+ except Exception as e:
645
+ if "rate_limit_exceeded" in str(e):
646
+ st.warning("Limite de taux atteinte lors de la génération du résumé final. Attente avant nouvelle tentative...")
647
+ time.sleep(60)
648
+ return self.summarize_long_transcription(transcription)
649
+ raise e
650
+ """def summarize_text(self, text: str) -> str:
651
+ try:
652
+ chunks = self.text_splitter.split_text(text)
653
+ summaries = []
654
+
655
+ for chunk in chunks:
656
+ messages = [
657
+ SystemMessage(content="Générez un résumé en français de ce texte:"),
658
+ HumanMessage(content=chunk)
659
+ ]
660
+ summary = self._make_api_call(messages).generations[0][0].text
661
+ summaries.append(summary)
662
+
663
+ if len(summaries) > 1:
664
+ final_messages = [
665
+ SystemMessage(content="Combinez ces résumés en un résumé cohérent en français:"),
666
+ HumanMessage(content="\n".join(summaries))
667
+ ]
668
+ final_summary = self._make_api_call(final_messages).generations[0][0].text
669
+ return final_summary
670
+
671
+ return summaries[0]
672
+
673
+ except Exception as e:
674
+ raise ValueError(f"Erreur de traitement: {str(e)}")"""
675
+
676
+ def generate_docx(content: str, filename: str):
677
+ doc = Document()
678
+ doc.add_heading('Résumé Audio', 0)
679
+ doc.add_paragraph(f"Date: {datetime.now().strftime('%d/%m/%Y %H:%M')}")
680
+
681
+ for line in content.split('\n'):
682
+ if line.strip():
683
+ if line.startswith('#'):
684
+ doc.add_heading(line.strip('# '), level=1)
685
+ else:
686
+ doc.add_paragraph(line)
687
+
688
+ doc.save(filename)
689
+ return filename
690
 
 
 
691
 
692
+ def model_selection_sidebar():
693
+ """Configuration du modèle dans la barre latérale"""
694
+ with st.sidebar:
695
+ st.title("Configuration")
696
+ model = st.selectbox(
697
+ "Sélectionnez un modèle",
698
+ [
699
+ "mixtral-8x7b-32768",
700
+ "llama-3.3-70b-versatile",
701
+ "gemma2-9b-i",
702
+ "llama3-70b-8192"
703
+ ]
704
+ )
705
+ prompt = st.text_area(
706
+ "Instructions personnalisées pour le résumé",
707
+ placeholder="Ex: Résumé de réunion avec points clés et actions"
708
+ )
709
+ return model, prompt
710
 
711
+ def save_uploaded_file(uploaded_file) -> str:
712
+ """Sauvegarde un fichier uploadé et retourne son chemin"""
713
  with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(uploaded_file.name)[1]) as tmp_file:
714
  tmp_file.write(uploaded_file.getvalue())
715
+ return tmp_file.name
716
+
717
+ def is_valid_email(email: str) -> bool:
718
+ """Valide le format d'une adresse email"""
719
+ pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
720
+ return bool(re.match(pattern, email))
721
+
722
+ def enhance_main():
723
+ """Fonction principale avec gestion des états et des erreurs améliorée"""
724
+ st.set_page_config(page_title="Multimodal Content Summarizer", page_icon="📝")
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()
739
+ elif source_type == "Document":
740
+ process_document()
741
+ else: # Web
742
+ process_web()
743
+ except Exception as e:
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":
754
+ handle_video_input()
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)
771
+ with st.spinner("Extraction de l'audio..."):
772
+ video_processor = VideoProcessor()
773
+ video_path = save_uploaded_file(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
+ youtube_url = st.text_input("URL YouTube")
780
+ if youtube_url and st.button("Analyser"):
781
+ video_processor = VideoProcessor()
782
+ video_id = video_processor.extract_video_id(youtube_url)
783
+
784
+ if video_id:
785
+ st.video(youtube_url)
786
+ with st.spinner("Traitement de la vidéo..."):
787
+ transcription = video_processor.get_youtube_transcription(video_id)
788
+ if transcription:
789
+ process_and_display_results(None, None, transcription)
790
+ else:
791
+ audio_path = video_processor.download_youtube_audio(youtube_url)
792
+ process_and_display_results(audio_path)
793
+
794
+ def process_and_display_results(file_path=None, audio_bytes=None, transcription=None):
795
+ """Traitement et affichage des résultats"""
796
+ try:
797
+ if transcription is None:
798
+ transcription = get_transcription(file_path, audio_bytes)
799
+
800
+ if transcription:
801
+ display_transcription_and_summary(transcription)
802
+ finally:
803
+ cleanup_temporary_files()
804
+
805
+ def get_transcription(file_path=None, audio_bytes=None) -> str:
806
+ """Obtention de la transcription"""
807
+ if file_path:
808
+ path = file_path if isinstance(file_path, str) else save_uploaded_file(file_path)
809
+ elif audio_bytes:
810
+ path = save_audio_bytes(audio_bytes)
811
+ else:
812
+ return None
813
+
814
+ chunks = st.session_state.audio_processor.chunk_audio(path)
815
+ transcriptions = []
816
+
817
+ with st.expander("Transcription", expanded=False):
818
+ progress_bar = st.progress(0)
819
+ for i, chunk in enumerate(chunks):
820
+ transcription = st.session_state.audio_processor.transcribe_chunk(chunk)
821
+ if transcription:
822
+ transcriptions.append(transcription)
823
+ progress_bar.progress((i + 1) / len(chunks))
824
+
825
+ return " ".join(transcriptions) if transcriptions else None
826
+
827
+ def display_transcription_and_summary(transcription: str):
828
+ """Affichage de la transcription et du résumé"""
829
+ st.subheader("Transcription")
830
+ st.text_area("Texte transcrit:", value=transcription, height=200)
831
+
832
+ st.subheader("Résumé et Analyse")
833
+ summary = get_summary(transcription)
834
+ st.markdown(summary)
835
+
836
+ # Génération et téléchargement des documents
837
+ generate_and_download_documents(summary)
838
+
839
+ # Option d'envoi par email
840
+ handle_email_sending(summary)
841
+
842
+ def get_summary(transcription: str) -> str:
843
+ """Génération du résumé"""
844
+ chunks = st.session_state.audio_processor.split_text(transcription)
845
+ if len(chunks) > 1:
846
+ return st.session_state.audio_processor.summarize_long_transcription(transcription)
847
+ return st.session_state.audio_processor.generate_summary(transcription)
848
+
849
+ def generate_and_download_documents(summary: str):
850
+ """Génération et téléchargement des documents"""
851
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
852
+
853
+ # Génération PDF
854
+ pdf_filename = f"resume_{timestamp}.pdf"
855
+ pdf_path = PDFGenerator.create_pdf(summary, pdf_filename)
856
+
857
+ # Génération DOCX
858
+ docx_filename = f"resume_{timestamp}.docx"
859
+ docx_path = generate_docx(summary, docx_filename)
860
+
861
+ # Boutons de téléchargement
862
+ col1, col2 = st.columns(2)
863
+ with col1:
864
+ with open(pdf_path, "rb") as pdf_file:
865
+ st.download_button(
866
+ "📥 Télécharger PDF",
867
+ pdf_file,
868
+ file_name=pdf_filename,
869
+ mime="application/pdf"
870
+ )
871
+
872
+ with col2:
873
+ with open(docx_path, "rb") as docx_file:
874
+ st.download_button(
875
+ "📥 Télécharger DOCX",
876
+ docx_file,
877
+ file_name=docx_filename,
878
+ mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
879
+ )
880
+
881
+ return pdf_path
882
+
883
+ def handle_email_sending(summary: str):
884
+ """Gestion de l'envoi par email"""
885
+ st.subheader("📧 Recevoir le résumé par email")
886
+ recipient_email = st.text_input("Entrez votre adresse email:")
887
+
888
+ if st.button("Envoyer par email"):
889
+ if not is_valid_email(recipient_email):
890
+ st.error("Veuillez entrer une adresse email valide.")
891
+ return
892
+
893
+ with st.spinner("Envoi de l'email en cours..."):
894
+ pdf_path = generate_and_download_documents(summary)
895
+ email_sender = EmailSender(Config.SENDER_EMAIL, Config.SENDER_PASSWORD)
896
+
897
+ if email_sender.send_email(
898
+ recipient_email,
899
+ "Résumé de votre contenu audio/vidéo",
900
+ "Veuillez trouver ci-joint le résumé de votre contenu.",
901
+ pdf_path
902
+ ):
903
+ st.success("Email envoyé avec succès!")
904
+ else:
905
+ st.error("Échec de l'envoi de l'email.")
906
+
907
+
908
+ def cleanup_temporary_files():
909
+ """Nettoyage des fichiers temporaires"""
910
+ temp_files = ['temp_audio.mp3', 'temp_video.mp4']
911
+ for temp_file in temp_files:
912
+ if os.path.exists(temp_file):
913
+ try:
914
+ os.remove(temp_file)
915
+ except Exception:
916
+ pass
917
+
918
+ def process_document():
919
+ """Traitement des documents"""
920
+ file = st.file_uploader("Chargez votre document", type=['pdf', 'docx', 'pptx', 'txt'])
921
+ password = st.text_input("Mot de passe (si protégé)", type="password")
922
+
923
+ if file:
924
+ try:
925
+ doc_processor = DocumentProcessor(
926
+ st.session_state.audio_processor.llm.model_name,
927
+ st.session_state.audio_processor.custom_prompt
928
+ )
929
+ text = process_document_with_password(file, password, doc_processor)
930
+ if text:
931
+ summary = doc_processor.summarize_text(text)
932
+ display_summary_and_downloads(summary)
933
+ except ValueError as e:
934
+ st.error(str(e))
935
+
936
+ def process_document_with_password(file, password: str, doc_processor: DocumentProcessor) -> Optional[str]:
937
+ """Traitement des documents protégés par mot de passe"""
938
+ file_extension = os.path.splitext(file.name)[1].lower()
939
+
940
+ try:
941
+ if file_extension == '.pdf':
942
+ return doc_processor.process_protected_pdf(file, password)
943
+ elif file_extension in ['.docx', '.pptx']:
944
+ return doc_processor.process_protected_office(file, file_extension[1:], password)
945
+ elif file_extension == '.txt':
946
+ return file.read().decode('utf-8')
947
+ else:
948
+ st.error("Format de fichier non supporté")
949
+ return None
950
+ except ValueError as e:
951
+ st.error(str(e))
952
+ return None
953
+
954
+ def process_web():
955
+ """Traitement des contenus web"""
956
+ url = st.text_input("URL du site web")
957
+ auth_required = st.checkbox("Authentification requise")
958
+
959
+ auth = None
960
+ if auth_required:
961
+ username = st.text_input("Nom d'utilisateur")
962
+ password = st.text_input("Mot de passe", type="password")
963
+ auth = {"username": username, "password": password}
964
+
965
+ if url and st.button("Analyser"):
966
+ try:
967
+ doc_processor = DocumentProcessor(
968
+ st.session_state.audio_processor.llm.model_name,
969
+ st.session_state.audio_processor.custom_prompt
970
+ )
971
+ text = doc_processor.scrape_web_content(url, auth)
972
+ if text:
973
+ summary = doc_processor.summarize_text(text)
974
+ display_summary_and_downloads(summary)
975
+ except ValueError as e:
976
+ st.error(str(e))
977
+
978
+ def display_summary_and_downloads(summary: str):
979
+ """Affichage du résumé et options de téléchargement"""
980
+ st.markdown("### 📝 Résumé et Analyse")
981
+ st.markdown(summary)
982
+
983
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
984
+
985
+ # Génération PDF
986
+ pdf_filename = f"resume_{timestamp}.pdf"
987
+ pdf_path = PDFGenerator.create_pdf(summary, pdf_filename)
988
+
989
+ # Génération DOCX
990
+ docx_filename = f"resume_{timestamp}.docx"
991
+ docx_path = generate_docx(summary, docx_filename)
992
+
993
+ # Boutons de téléchargement
994
+ col1, col2 = st.columns(2)
995
+ with col1:
996
+ with open(pdf_path, "rb") as pdf_file:
997
+ st.download_button(
998
+ "📥 Télécharger PDF",
999
+ pdf_file,
1000
+ file_name=pdf_filename,
1001
+ mime="application/pdf"
1002
+ )
1003
+
1004
+ with col2:
1005
+ with open(docx_path, "rb") as docx_file:
1006
+ st.download_button(
1007
+ "📥 Télécharger DOCX",
1008
+ docx_file,
1009
+ file_name=docx_filename,
1010
+ mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
1011
+ )
1012
+
1013
+ # Option d'envoi par email
1014
+ st.markdown("### 📧 Recevoir le résumé par email")
1015
+ recipient_email = st.text_input("Entrez votre adresse email:")
1016
+
1017
+ if st.button("Envoyer par email"):
1018
+ if not is_valid_email(recipient_email):
1019
+ st.error("Veuillez entrer une adresse email valide.")
1020
+ else:
1021
+ with st.spinner("Envoi de l'email en cours..."):
1022
+ email_sender = EmailSender(Config.SENDER_EMAIL, Config.SENDER_PASSWORD)
1023
+ if email_sender.send_email(
1024
+ recipient_email,
1025
+ "Résumé de votre contenu",
1026
+ "Veuillez trouver ci-joint le résumé de votre contenu.",
1027
+ pdf_path
1028
+ ):
1029
+ st.success("Email envoyé avec succès!")
1030
+ else:
1031
+ st.error("Échec de l'envoi de l'email.")
1032
+
1033
+ def save_audio_bytes(audio_bytes: bytes) -> str:
1034
+ """Sauvegarde les bytes audio dans un fichier temporaire"""
1035
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1036
+ file_path = f"recording_{timestamp}.wav"
1037
+ with open(file_path, 'wb') as f:
1038
+ f.write(audio_bytes)
1039
+ return file_path
1040
+
1041
+ if __name__ == "__main__":
1042
+ try:
1043
+ enhance_main()
1044
+ except Exception as e:
1045
+ st.error(f"Une erreur inattendue est survenue: {str(e)}")
1046
+ st.error("Veuillez réessayer ou contacter le support technique.")
1047
+ finally:
1048
+ cleanup_temporary_files()