File size: 20,192 Bytes
c253059
da7ef42
ccfefd3
0f75c3f
10ad7a5
 
60b2cb1
c253059
ccfefd3
c23cd74
ccfefd3
10ad7a5
c23cd74
da7ef42
ccfefd3
 
 
 
 
10ad7a5
ccfefd3
10ad7a5
ccfefd3
10ad7a5
ccfefd3
10ad7a5
ccfefd3
10ad7a5
ccfefd3
de5608c
1d151d8
0f75c3f
 
 
 
 
 
 
 
10ad7a5
ccfefd3
 
 
 
 
0f75c3f
 
 
 
 
 
 
 
 
 
ccfefd3
c23cd74
0f75c3f
ccfefd3
 
 
c23cd74
 
ccfefd3
c23cd74
 
 
da7ef42
ccfefd3
 
 
 
10ad7a5
ccfefd3
10ad7a5
ccfefd3
 
 
 
 
 
da7ef42
ccfefd3
10ad7a5
ccfefd3
10ad7a5
ccfefd3
 
10ad7a5
ccfefd3
 
 
10ad7a5
 
ccfefd3
 
 
 
 
 
10ad7a5
 
dfc4deb
ccfefd3
10ad7a5
 
 
ccfefd3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dfc4deb
10ad7a5
ccfefd3
10ad7a5
 
 
ccfefd3
 
dfc4deb
ccfefd3
 
 
 
dfc4deb
10ad7a5
c23cd74
 
c96425b
ccfefd3
 
0f75c3f
a91af02
0f75c3f
ccfefd3
 
 
10ad7a5
60b2cb1
10ad7a5
ccfefd3
 
 
 
 
 
 
c23cd74
dfc4deb
 
ccfefd3
0f75c3f
 
 
10ad7a5
ccfefd3
10ad7a5
ccfefd3
 
10ad7a5
0f75c3f
 
10ad7a5
ccfefd3
10ad7a5
ccfefd3
 
 
 
 
0f75c3f
ccfefd3
dfc4deb
ccfefd3
 
 
dfc4deb
ccfefd3
 
 
10ad7a5
ccfefd3
10ad7a5
 
 
 
 
 
ccfefd3
 
 
10ad7a5
ccfefd3
 
 
 
 
dfc4deb
ccfefd3
 
0f75c3f
ccfefd3
 
10ad7a5
 
ccfefd3
 
 
 
 
 
 
 
 
 
10ad7a5
ccfefd3
 
dfc4deb
ccfefd3
 
 
 
 
dfc4deb
ccfefd3
dfc4deb
 
ccfefd3
 
 
dfc4deb
 
ccfefd3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dfc4deb
ccfefd3
 
 
 
 
dfc4deb
ccfefd3
60b2cb1
ccfefd3
 
 
 
 
60b2cb1
0f75c3f
 
 
 
 
 
 
 
 
 
 
 
ccfefd3
0f75c3f
ccfefd3
0f75c3f
ccfefd3
10ad7a5
0f75c3f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccfefd3
 
 
 
 
 
 
 
 
10ad7a5
c253059
ccfefd3
 
 
dfc4deb
c253059
ccfefd3
 
 
0f75c3f
 
 
 
 
 
 
 
ccfefd3
 
 
 
 
 
 
 
 
 
 
10ad7a5
 
 
c253059
ccfefd3
60b2cb1
0f75c3f
ccfefd3
 
 
 
 
0f75c3f
ccfefd3
 
 
 
 
 
 
 
60b2cb1
ccfefd3
c253059
ccfefd3
 
 
 
0f75c3f
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
import os
import json
import mimetypes
from flask import Flask, request, session, jsonify, redirect, url_for, flash, render_template
from dotenv import load_dotenv
import google.generativeai as genai
import requests
from werkzeug.utils import secure_filename
import markdown # Pour convertir la réponse en HTML

# --- Configuration Initiale ---
load_dotenv()

app = Flask(__name__)

# Clé secrète FORTEMENT recommandée pour les sessions
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'dev-secret-key-replace-in-prod')

# Configuration pour les uploads
UPLOAD_FOLDER = 'temp'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'} # Extensions autorisées
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 25 * 1024 * 1024  # Limite de taille (ex: 25MB)

# Créer le dossier temp s'il n'existe pas
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
print(f"Dossier d'upload configuré : {os.path.abspath(UPLOAD_FOLDER)}")

# --- Configuration de l'API Gemini ---
MODEL_FLASH = 'gemini-2.0-flash' # Default model
MODEL_PRO = 'gemini-2.5-pro-exp-03-25' # Advanced model
SYSTEM_INSTRUCTION = "Tu es un assistant intelligent et amical nommé Mariam. Tu assistes les utilisateurs au mieux de tes capacités. Tu as été créé par Aenir."
SAFETY_SETTINGS = [
    {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
    {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
    {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
    {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
]
GEMINI_CONFIGURED = False
try:
    gemini_api_key = os.getenv("GOOGLE_API_KEY")
    if not gemini_api_key:
        print("ERREUR: Clé API GOOGLE_API_KEY manquante dans le fichier .env")
    else:
        genai.configure(api_key=gemini_api_key)
        # Just configure, don't create model instance yet
        # Check if we can list models as a basic configuration test
        models_list = [m.name for m in genai.list_models()]
        if f'models/{MODEL_FLASH}' in models_list and f'models/{MODEL_PRO}' in models_list:
             print(f"Configuration Gemini effectuée. Modèles requis ({MODEL_FLASH}, {MODEL_PRO}) disponibles.")
             print(f"System instruction: {SYSTEM_INSTRUCTION}")
             GEMINI_CONFIGURED = True
        else:
            print(f"ERREUR: Les modèles requis ({MODEL_FLASH}, {MODEL_PRO}) ne sont pas tous disponibles via l'API.")
            print(f"Modèles trouvés: {models_list}")

except Exception as e:
    print(f"ERREUR Critique lors de la configuration initiale de Gemini : {e}")
    print("L'application fonctionnera sans les fonctionnalités IA.")

# --- Fonctions Utilitaires ---

def allowed_file(filename):
    """Vérifie si l'extension du fichier est autorisée."""
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def perform_web_search(query):
    """Effectue une recherche web via l'API Serper."""
    serper_api_key = os.getenv("SERPER_API_KEY")
    if not serper_api_key:
        print("AVERTISSEMENT: Clé API SERPER_API_KEY manquante. Recherche web désactivée.")
        return None

    search_url = "https://google.serper.dev/search"
    headers = {
        'X-API-KEY': serper_api_key,
        'Content-Type': 'application/json'
    }
    payload = json.dumps({"q": query, "gl": "fr", "hl": "fr"}) # Ajout localisation FR

    try:
        print(f"Recherche Serper pour: '{query}'")
        response = requests.post(search_url, headers=headers, data=payload, timeout=10)
        response.raise_for_status() # Lève une exception pour les erreurs HTTP (4xx, 5xx)
        data = response.json()
        print("Résultats de recherche Serper obtenus.")
        # print(json.dumps(data, indent=2)) # Décommenter pour voir les résultats bruts
        return data
    except requests.exceptions.Timeout:
        print("Erreur lors de la recherche web : Timeout")
        return None
    except requests.exceptions.RequestException as e:
        print(f"Erreur lors de la recherche web : {e}")
        # Essayer de lire le corps de la réponse d'erreur si possible
        try:
            error_details = e.response.json()
            print(f"Détails de l'erreur Serper: {error_details}")
        except:
            pass # Ignorer si le corps n'est pas JSON ou n'existe pas
        return None
    except json.JSONDecodeError as e:
        print(f"Erreur lors du décodage de la réponse JSON de Serper : {e}")
        print(f"Réponse reçue (texte brut) : {response.text}")
        return None

def format_search_results(data):
    """Met en forme les résultats de recherche (format Markdown)."""
    if not data:
        return "Aucun résultat de recherche web trouvé pertinent."

    results = []

    # Réponse directe (Answer Box)
    if data.get('answerBox'):
        ab = data['answerBox']
        title = ab.get('title', '')
        snippet = ab.get('snippet') or ab.get('answer', '')
        if snippet:
            results.append(f"**Réponse rapide : {title}**\n{snippet}\n")

    # Knowledge Graph
    if data.get('knowledgeGraph'):
        kg = data['knowledgeGraph']
        title = kg.get('title', '')
        type = kg.get('type', '')
        description = kg.get('description', '')
        if title and description:
             results.append(f"**{title} ({type})**\n{description}\n")
        if kg.get('attributes'):
             for attr, value in kg['attributes'].items():
                  results.append(f"- {attr}: {value}")


    # Résultats organiques
    if data.get('organic'):
        results.append("**Pages web pertinentes :**")
        for i, item in enumerate(data['organic'][:3], 1): # Top 3
            title = item.get('title', 'Sans titre')
            link = item.get('link', '#')
            snippet = item.get('snippet', 'Pas de description.')
            results.append(f"{i}. **[{title}]({link})**\n   {snippet}\n")

    # People Also Ask
    if data.get('peopleAlsoAsk'):
        results.append("**Questions liées :**")
        for i, item in enumerate(data['peopleAlsoAsk'][:2], 1): # Top 2
             results.append(f"- {item.get('question', '')}")


    if not results:
        return "Aucun résultat structuré trouvé dans la recherche web."

    return "\n".join(results)

def prepare_gemini_history(chat_history):
    """Convertit l'historique stocké en session au format attendu par Gemini API."""
    gemini_history = []
    for message in chat_history:
        role = 'user' if message['role'] == 'user' else 'model'
        # Utiliser le 'raw_text' stocké pour Gemini
        text_part = message.get('raw_text', '') # Fallback au cas où
        parts = [text_part]
        # NOTE: La gestion des fichiers des tours PRÉCÉDENTS n'est pas gérée ici.
        # L'API generate_content se concentre généralement sur le fichier du tour ACTUEL.
        # Si une référence de fichier passée était nécessaire, il faudrait la stocker
        # et la ré-attacher ici (potentiellement plus complexe).
        gemini_history.append({'role': role, 'parts': parts})
    return gemini_history

# --- Routes Flask ---

@app.route('/')
def root():
    """Sert la page HTML principale."""
    return render_template('index.html')

@app.route('/api/history', methods=['GET'])
def get_history():
    """Fournit l'historique de chat stocké en session au format JSON."""
    if 'chat_history' not in session:
        session['chat_history'] = []

    # Préparer l'historique pour l'affichage (contient déjà le HTML pour l'assistant)
    display_history = [
        {'role': msg.get('role', 'unknown'), 'text': msg.get('text', '')}
        for msg in session.get('chat_history', [])
    ]
    print(f"API: Récupération de l'historique ({len(display_history)} messages)")
    return jsonify({'success': True, 'history': display_history})

@app.route('/api/chat', methods=['POST'])
def chat_api():
    """Gère les nouvelles requêtes de chat via AJAX."""
    if not GEMINI_CONFIGURED:
        print("API ERREUR: Tentative d'appel à /api/chat sans configuration Gemini valide.")
        return jsonify({'success': False, 'error': "Le service IA n'est pas configuré correctement."}), 503 # Service Unavailable

    # Récupération des données du formulaire
    prompt = request.form.get('prompt', '').strip()
    use_web_search_str = request.form.get('web_search', 'false') # 'true' ou 'false'
    use_web_search = use_web_search_str.lower() == 'true'
    file = request.files.get('file')
    use_advanced_str = request.form.get('advanced_reasoning', 'false') # Get the new flag
    use_advanced = use_advanced_str.lower() == 'true'

    # Validation simple
    if not prompt and not file:
        return jsonify({'success': False, 'error': 'Veuillez fournir un message ou un fichier.'}), 400

    print(f"\n--- Nouvelle requête /api/chat ---")
    print(f"Prompt reçu: '{prompt[:50]}...'")
    print(f"Recherche Web activée: {use_web_search}")
    print(f"Raisonnement avancé demandé: {use_advanced}")
    print(f"Fichier reçu: {file.filename if file else 'Aucun'}")

    # Initialiser l'historique de session si nécessaire
    if 'chat_history' not in session:
        session['chat_history'] = []

    uploaded_gemini_file = None # L'objet fichier retourné par genai.upload_file
    uploaded_filename = None    # Juste le nom du fichier pour référence/affichage
    filepath_to_delete = None   # Chemin du fichier local à supprimer après traitement

    # --- Gestion de l'upload de fichier ---
    if file and file.filename != '':
        if allowed_file(file.filename):
            try:
                filename = secure_filename(file.filename)
                filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
                file.save(filepath)
                filepath_to_delete = filepath # Marquer pour suppression
                uploaded_filename = filename
                print(f"Fichier '{filename}' sauvegardé temporairement dans '{filepath}'")

                # Détecter le MimeType pour Gemini
                mime_type = mimetypes.guess_type(filepath)[0]
                if not mime_type:
                    mime_type = 'application/octet-stream' # Fallback
                    print(f"AVERTISSEMENT: Impossible de deviner le MimeType pour '{filename}', utilisation de '{mime_type}'.")

                # Uploader vers Google AI (peut prendre du temps)
                print(f"Upload du fichier vers Google AI (MimeType: {mime_type})...")
                # Note: L'API upload_file est générique et ne dépend pas du modèle Flash/Pro
                uploaded_gemini_file = genai.upload_file(path=filepath, mime_type=mime_type)
                print(f"Fichier '{uploaded_gemini_file.name}' uploadé avec succès vers Google AI.")

            except Exception as e:
                print(f"ERREUR Critique lors du traitement/upload du fichier '{filename}': {e}")
                # Supprimer le fichier local même en cas d'erreur d'upload Gemini
                if filepath_to_delete and os.path.exists(filepath_to_delete):
                    try:
                        os.remove(filepath_to_delete)
                        print(f"Fichier temporaire '{filepath_to_delete}' supprimé après erreur.")
                    except OSError as del_e:
                         print(f"Erreur lors de la suppression du fichier temporaire après erreur: {del_e}")
                # Renvoyer une erreur claire au client
                return jsonify({'success': False, 'error': f"Erreur lors du traitement du fichier: {e}"}), 500
        else:
            print(f"ERREUR: Type de fichier non autorisé: {file.filename}")
            return jsonify({'success': False, 'error': f"Type de fichier non autorisé. Extensions permises: {', '.join(ALLOWED_EXTENSIONS)}"}), 400

    # --- Préparation du message utilisateur pour l'historique et Gemini ---
    # Texte brut pour Gemini (et pour l'historique interne)
    raw_user_text = prompt
    # Texte pour l'affichage dans l'interface (peut inclure le nom de fichier)
    display_user_text = f"[{uploaded_filename}] {prompt}" if uploaded_filename and prompt else (prompt or f"[{uploaded_filename}]")

    # Ajout à l'historique de session
    user_history_entry = {
        'role': 'user',
        'text': display_user_text, # Pour get_history et potentiellement debug
        'raw_text': raw_user_text,   # Pour l'envoi à Gemini via prepare_gemini_history
        # On ne stocke PAS l'objet 'uploaded_gemini_file' dans la session
    }
    session['chat_history'].append(user_history_entry)
    session.modified = True # Indiquer que la session a été modifiée

    # --- Préparation des 'parts' pour l'appel Gemini ACTUEL ---
    current_gemini_parts = []
    if uploaded_gemini_file:
        current_gemini_parts.append(uploaded_gemini_file) # L'objet fichier uploadé

    final_prompt_for_gemini = raw_user_text # Commencer avec le texte brut

    # --- Recherche Web (si activée et si un prompt textuel existe) ---
    if use_web_search and raw_user_text:
        print("Activation de la recherche web...")
        search_data = perform_web_search(raw_user_text)
        if search_data:
            formatted_results = format_search_results(search_data)
            # Construire un prompt enrichi pour Gemini
            final_prompt_for_gemini = f"""Voici la question originale de l'utilisateur:
"{raw_user_text}"

J'ai effectué une recherche web et voici les informations pertinentes trouvées:
--- DEBUT RESULTATS WEB ---
{formatted_results}
--- FIN RESULTATS WEB ---

En te basant sur ces informations ET sur ta connaissance générale, fournis une réponse complète et bien structurée à la question originale de l'utilisateur."""
            print("Prompt enrichi avec les résultats de recherche web.")
        else:
            print("Aucun résultat de recherche web pertinent trouvé ou erreur, utilisation du prompt original.")
            # final_prompt_for_gemini reste raw_user_text

    # Ajouter le texte (potentiellement enrichi) aux parts pour Gemini
    current_gemini_parts.append(final_prompt_for_gemini)

    # --- Appel à l'API Gemini ---
    try:
        # Préparer l'historique des messages PRÉCÉDENTS
        gemini_history = prepare_gemini_history(session['chat_history'][:-1]) # Exclut le message actuel
        print(f"Préparation de l'appel Gemini avec {len(gemini_history)} messages d'historique.")
        # Construire le contenu complet pour l'appel
        contents_for_gemini = gemini_history + [{'role': 'user', 'parts': current_gemini_parts}]

        # Choisir le nom du modèle à utiliser
        selected_model_name = MODEL_PRO if use_advanced else MODEL_FLASH
        print(f"Utilisation du modèle Gemini: {selected_model_name}")

        # Créer l'instance du modèle spécifique pour cette requête
        # Réutiliser les paramètres globaux (safety, system instruction)
        active_model = genai.GenerativeModel(
            model_name=selected_model_name,
            safety_settings=SAFETY_SETTINGS, # defined globally
            system_instruction=SYSTEM_INSTRUCTION # defined globally
        )

        # Appel API
        print(f"Envoi de la requête à {selected_model_name}...")
        # Utilisation de generate_content en mode non-streamé
        response = active_model.generate_content(contents_for_gemini)
        # print(response) # Décommenter pour voir la réponse brute de l'API

        # Extraire le texte de la réponse (gestion d'erreur potentielle ici si la réponse est bloquée etc.)
        # Gérer le cas où la réponse est bloquée par les safety settings
        try:
             response_text_raw = response.text
        except ValueError:
            # Si response.text échoue, la réponse a probablement été bloquée.
            print("ERREUR: La réponse de Gemini a été bloquée (probablement par les safety settings).")
            print(f"Détails du blocage : {response.prompt_feedback}")
            # Vous pouvez décider quoi renvoyer au client ici.
            # Soit une erreur spécifique, soit un message générique.
            response_text_raw = "Désolé, ma réponse a été bloquée car elle pourrait enfreindre les règles de sécurité."
            # Convertir ce message d'erreur en HTML aussi pour la cohérence
            response_html = markdown.markdown(response_text_raw)

        else:
            # Si response.text réussit, continuer normalement
            print(f"Réponse reçue de Gemini (brute, début): '{response_text_raw[:100]}...'")
            # Convertir la réponse Markdown en HTML pour l'affichage
            response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
            print("Réponse convertie en HTML.")


        # Ajouter la réponse de l'assistant à l'historique de session
        assistant_history_entry = {
            'role': 'assistant',
            'text': response_html,      # HTML pour l'affichage via get_history
            'raw_text': response_text_raw # Texte brut pour les futurs appels Gemini
        }
        session['chat_history'].append(assistant_history_entry)
        session.modified = True

        # Renvoyer la réponse HTML au frontend
        print("Envoi de la réponse HTML au client.")
        return jsonify({'success': True, 'message': response_html})

    except Exception as e:
        print(f"ERREUR Critique lors de l'appel à Gemini ou du traitement de la réponse : {e}")
        # En cas d'erreur, retirer le dernier message utilisateur de l'historique
        # pour éviter les boucles d'erreur si le message lui-même pose problème.
        # Vérifier si l'historique n'est pas vide avant de pop
        if session.get('chat_history'):
            session['chat_history'].pop()
            session.modified = True
            print("Le dernier message utilisateur a été retiré de l'historique suite à l'erreur.")
        else:
            print("L'historique était déjà vide lors de l'erreur.")

        # Renvoyer une erreur générique mais informative
        return jsonify({'success': False, 'error': f"Une erreur interne est survenue lors de la génération de la réponse. Détails: {e}"}), 500

    finally:
        # --- Nettoyage du fichier temporaire ---
        if filepath_to_delete and os.path.exists(filepath_to_delete):
            try:
                os.remove(filepath_to_delete)
                print(f"Fichier temporaire '{filepath_to_delete}' supprimé avec succès.")
            except OSError as e:
                print(f"ERREUR lors de la suppression du fichier temporaire '{filepath_to_delete}': {e}")


@app.route('/clear', methods=['POST'])
def clear_chat():
    """Efface l'historique de chat dans la session."""
    session.pop('chat_history', None)
    # session.pop('web_search', None) # On ne stocke pas ça en session
    print("API: Historique de chat effacé via /clear.")

    # Adapter la réponse selon si c'est une requête AJAX (fetch) ou une soumission classique
    # Vérification si la requête vient probablement de fetch (simple)
    is_ajax = 'XMLHttpRequest' == request.headers.get('X-Requested-With') or \
              'application/json' in request.headers.get('Accept', '') # Plus robuste

    if is_ajax:
         return jsonify({'success': True, 'message': 'Historique effacé.'})
    else:
        # Comportement pour une soumission de formulaire classique (si jamais utilisé)
        flash("Conversation effacée.", "info")
        return redirect(url_for('root')) # Redirige vers la racine


# --- Démarrage de l'application ---
if __name__ == '__main__':
    print("Démarrage du serveur Flask...")
    # Utiliser host='0.0.0.0' pour rendre accessible sur le réseau local
    # debug=True est pratique pour le développement, mais à désactiver en production !
    # Changer le port si nécessaire (ex: 5000, 5001, 8080)
    # Utiliser un port différent si le port 5000 est déjà pris
    port = int(os.environ.get('PORT', 5001))
    app.run(debug=True, host='0.0.0.0', port=port)