Docfile commited on
Commit
79b1a5a
·
verified ·
1 Parent(s): 496675e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +344 -202
app.py CHANGED
@@ -1,223 +1,365 @@
1
- from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, send_from_directory
2
- from flask_sqlalchemy import SQLAlchemy
 
3
  import os
4
- import requests
5
- from urllib.parse import urlparse
6
- import logging
 
 
7
 
8
- logging.basicConfig(level=logging.INFO)
9
- logger = logging.getLogger(__name__)
 
 
10
 
11
- app = Flask(__name__)
12
- app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'une_cle_secrete_par_defaut_pour_dev')
 
 
 
13
 
14
- # Configuration de la base de données PostgreSQL
15
- app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://Podcast_owner:npg_gFdMDLO9lVa0@ep-delicate-surf-a4v7wopn-pooler.us-east-1.aws.neon.tech/Podcast?sslmode=require'
16
- app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
17
 
18
- db = SQLAlchemy(app)
19
 
20
- # Configuration du répertoire de cache audio
21
- AUDIO_CACHE_DIR = '/tmp/audio_cache'
22
  try:
23
- os.makedirs(AUDIO_CACHE_DIR, exist_ok=True)
24
- logger.info(f"Répertoire de cache audio configuré sur : {AUDIO_CACHE_DIR}")
25
- except OSError as e:
26
- logger.error(f"Impossible de créer le répertoire de cache audio à {AUDIO_CACHE_DIR}: {e}")
27
-
28
- # Modèle de base de données pour les Podcasts
29
- class Podcast(db.Model):
30
- __tablename__ = 'podcast'
31
- id = db.Column(db.Integer, primary_key=True)
32
- name = db.Column(db.String(200), nullable=False)
33
- url = db.Column(db.String(500), nullable=False, unique=True)
34
- subject = db.Column(db.String(100), nullable=False)
35
- filename_cache = db.Column(db.String(255), nullable=True) # Nom du fichier dans le cache
36
-
37
- def __repr__(self):
38
- return f'<Podcast {self.name}>'
39
-
40
- # Création des tables de la base de données si elles n'existent pas
41
- # Ceci est déplacé ici pour s'assurer qu'il s'exécute au démarrage de l'application,
42
- # que ce soit via `flask run` ou un serveur WSGI.
43
- with app.app_context():
44
  try:
45
- db.create_all()
46
- logger.info("Tables de base de données vérifiées/créées (si elles n'existaient pas).")
 
 
 
 
 
 
 
 
47
  except Exception as e:
48
- logger.error(f"Erreur lors de la création des tables de la base de données: {e}")
49
- # Il est crucial de gérer cette erreur. Si la base de données n'est pas accessible ou
50
- # si les tables ne peuvent pas être créées, l'application ne fonctionnera pas correctement.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  @app.route('/')
53
  def index():
54
- try:
55
- podcasts = Podcast.query.order_by(Podcast.name).all()
56
- except Exception as e:
57
- logger.error(f"Erreur lors de la récupération des podcasts: {e}")
58
- flash("Erreur lors du chargement des podcasts depuis la base de données.", "error")
59
- podcasts = [] # Fournir une liste vide en cas d'erreur pour que le template fonctionne
60
- return render_template('index.html', podcasts=podcasts)
61
-
62
- @app.route('/gestion', methods=['GET', 'POST'])
63
- def gestion():
64
- if request.method == 'POST':
65
- name = request.form.get('name')
66
- url = request.form.get('url')
67
- subject = request.form.get('subject')
68
-
69
- if not name or not url or not subject:
70
- flash('Tous les champs sont requis !', 'error')
71
- else:
72
- # Vérifier si un podcast avec la même URL existe déjà
73
- existing_podcast = Podcast.query.filter_by(url=url).first()
74
- if existing_podcast:
75
- flash('Un podcast avec cette URL existe déjà.', 'warning')
76
- else:
77
- try:
78
- new_podcast = Podcast(name=name, url=url, subject=subject)
79
- db.session.add(new_podcast)
80
- db.session.commit()
81
- flash('Podcast ajouté avec succès !', 'success')
82
- return redirect(url_for('gestion')) # Rediriger pour éviter la resoumission du formulaire
83
- except Exception as e:
84
- db.session.rollback() # Annuler les changements en cas d'erreur
85
- logger.error(f"Erreur lors de l'ajout du podcast: {e}")
86
- flash(f"Erreur lors de l'ajout du podcast: {e}", 'error')
87
-
88
- # Charger les podcasts pour l'affichage sur la page de gestion (méthode GET ou après POST)
89
- try:
90
- podcasts = Podcast.query.order_by(Podcast.name).all()
91
- except Exception as e:
92
- logger.error(f"Erreur lors de la récupération des podcasts pour la gestion: {e}")
93
- flash("Erreur lors du chargement des podcasts pour la gestion.", "error")
94
- podcasts = []
95
 
96
- return render_template('gestion.html', podcasts=podcasts)
 
 
 
 
97
 
98
- @app.route('/delete_podcast/<int:podcast_id>', methods=['POST'])
99
- def delete_podcast(podcast_id):
100
- try:
101
- podcast_to_delete = db.session.get(Podcast, podcast_id) # Utilisation de db.session.get pour Flask-SQLAlchemy >= 3.0
102
- if not podcast_to_delete:
103
- flash('Podcast non trouvé.', 'error')
104
- return redirect(url_for('gestion'))
105
-
106
- # Supprimer le fichier cache associé s'il existe
107
- if podcast_to_delete.filename_cache:
108
- cached_file_path = os.path.join(AUDIO_CACHE_DIR, podcast_to_delete.filename_cache)
109
- if os.path.exists(cached_file_path):
110
- try:
111
- os.remove(cached_file_path)
112
- logger.info(f"Fichier cache {podcast_to_delete.filename_cache} supprimé.")
113
- except OSError as e:
114
- logger.error(f"Erreur lors de la suppression du fichier cache {podcast_to_delete.filename_cache}: {e}")
115
- flash(f'Erreur lors de la suppression du fichier cache {podcast_to_delete.filename_cache}.', 'error')
116
- else:
117
- logger.warning(f"Fichier cache {podcast_to_delete.filename_cache} listé dans la DB mais non trouvé sur le disque pour suppression.")
118
-
119
-
120
- db.session.delete(podcast_to_delete)
121
- db.session.commit()
122
- flash('Podcast supprimé avec succès.', 'success')
123
- except Exception as e:
124
- db.session.rollback()
125
- logger.error(f"Erreur lors de la suppression du podcast ID {podcast_id}: {e}")
126
- flash(f"Erreur lors de la suppression du podcast: {e}", 'error')
127
- return redirect(url_for('gestion'))
128
-
129
- @app.route('/play/<int:podcast_id>')
130
- def play_podcast_route(podcast_id):
131
- podcast = db.session.get(Podcast, podcast_id) # Utilisation de db.session.get
132
-
133
- if not podcast:
134
- logger.warning(f"Tentative de lecture d'un podcast non trouvé: ID {podcast_id}")
135
- return jsonify({'error': 'Podcast non trouvé'}), 404
136
-
137
- # Vérifier si le fichier est déjà en cache
138
- if podcast.filename_cache:
139
- cached_filepath = os.path.join(AUDIO_CACHE_DIR, podcast.filename_cache)
140
- if os.path.exists(cached_filepath):
141
- logger.info(f"Service du podcast {podcast.id} depuis le cache: {podcast.filename_cache}")
142
- audio_url = url_for('serve_cached_audio', filename=podcast.filename_cache)
143
- return jsonify({'audio_url': audio_url})
144
- else:
145
- # Le fichier cache est référencé mais n'existe pas, il faut le re-télécharger
146
- logger.warning(f"Fichier cache {podcast.filename_cache} pour podcast {podcast.id} non trouvé sur le disque. Re-téléchargement.")
147
- podcast.filename_cache = None # Marquer comme non-caché pour forcer le re-téléchargement
148
- # Pas besoin de db.session.commit() ici immédiatement, sera fait après le téléchargement réussi
149
 
150
- # Si le fichier n'est pas en cache ou si le cache était invalide
151
- final_cached_filepath = None # Initialiser pour la clause finally
152
  try:
153
- # Déterminer l'extension à partir de l'URL ou du Content-Type
154
- parsed_url = urlparse(podcast.url)
155
- _, url_ext = os.path.splitext(parsed_url.path)
156
- extension = url_ext if url_ext else '.audio' # Extension par défaut
157
-
158
- # Utiliser l'ID du podcast pour un nom de fichier unique et simple
159
- base_filename = str(podcast.id)
160
 
161
- logger.info(f"Téléchargement de {podcast.url} pour le podcast ID {podcast.id}")
162
- # Timeout: (connect_timeout, read_timeout)
163
- response = requests.get(podcast.url, stream=True, timeout=(10, 60))
164
- response.raise_for_status() # Lèvera une exception pour les codes d'erreur HTTP (4xx ou 5xx)
165
-
166
- # Essayer d'obtenir une meilleure extension à partir de l'en-tête Content-Type
167
- content_type = response.headers.get('Content-Type')
168
- if content_type:
169
- if 'mpeg' in content_type: extension = '.mp3'
170
- elif 'ogg' in content_type: extension = '.ogg'
171
- elif 'wav' in content_type: extension = '.wav'
172
- elif 'aac' in content_type: extension = '.aac'
173
- elif 'mp4' in content_type: extension = '.m4a' # Souvent utilisé pour l'audio dans un conteneur mp4
174
-
175
- # Construire le nom de fichier final avec l'extension déterminée
176
- cached_filename_with_ext = f"{base_filename}{extension}"
177
- final_cached_filepath = os.path.join(AUDIO_CACHE_DIR, cached_filename_with_ext)
178
-
179
- # Écrire le contenu dans le fichier cache
180
- with open(final_cached_filepath, 'wb') as f:
181
- for chunk in response.iter_content(chunk_size=8192): # Taille de chunk raisonnable
182
- f.write(chunk)
183
- logger.info(f"Téléchargement terminé: {final_cached_filepath}")
184
-
185
- # Mettre à jour la base de données avec le nom du fichier en cache
186
- podcast.filename_cache = cached_filename_with_ext
187
- db.session.commit()
188
 
189
- audio_url = url_for('serve_cached_audio', filename=cached_filename_with_ext)
190
- return jsonify({'audio_url': audio_url})
191
-
192
- except requests.exceptions.Timeout:
193
- logger.error(f"Timeout lors du téléchargement de {podcast.url}")
194
- # Nettoyer le fichier partiel si le téléchargement a échoué
195
- if final_cached_filepath and os.path.exists(final_cached_filepath):
196
- try: os.remove(final_cached_filepath); logger.info(f"Fichier partiel (Timeout) nettoyé : {final_cached_filepath}")
197
- except OSError as e_clean: logger.error(f"Erreur nettoyage fichier partiel (Timeout) {final_cached_filepath}: {e_clean}")
198
- return jsonify({'error': 'Le téléchargement du podcast a pris trop de temps.'}), 504
199
- except requests.exceptions.RequestException as e:
200
- logger.error(f"Erreur de téléchargement pour {podcast.url}: {e}")
201
- if final_cached_filepath and os.path.exists(final_cached_filepath):
202
- try: os.remove(final_cached_filepath); logger.info(f"Fichier partiel (RequestException) nettoyé : {final_cached_filepath}")
203
- except OSError as e_clean: logger.error(f"Erreur nettoyage fichier partiel (RequestException) {final_cached_filepath}: {e_clean}")
204
- return jsonify({'error': f'Impossible de télécharger le podcast: {e}'}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  except Exception as e:
206
- db.session.rollback() # Assurer la cohérence de la session DB
207
- logger.error(f"Erreur inattendue lors du traitement du podcast {podcast.id}: {e}")
208
- if final_cached_filepath and os.path.exists(final_cached_filepath):
209
- try: os.remove(final_cached_filepath); logger.info(f"Fichier partiel (Exception) nettoyé : {final_cached_filepath}")
210
- except OSError as e_clean: logger.error(f"Erreur nettoyage fichier partiel (Exception) {final_cached_filepath}: {e_clean}")
211
- return jsonify({'error': f'Erreur inattendue: {e}'}), 500
212
-
213
- @app.route('/audio_cache/<path:filename>')
214
- def serve_cached_audio(filename):
215
- logger.debug(f"Service du fichier cache: {filename} depuis {AUDIO_CACHE_DIR}")
216
- # Assurez-vous que le chemin est sécurisé et ne permet pas de sortir du répertoire de cache
217
- # send_from_directory s'en charge généralement.
218
- return send_from_directory(AUDIO_CACHE_DIR, filename)
219
 
220
  if __name__ == '__main__':
221
- # db.create_all() est maintenant appelé au niveau global du module.
222
- # La ligne `app.run(...):19:12 =====` dans le log original contenait une syntaxe incorrecte, corrigée ici.
223
- app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, Response, request, stream_with_context
2
+ from google import genai
3
+ from google.genai import types
4
  import os
5
+ from PIL import Image
6
+ import io
7
+ import base64
8
+ import json
9
+ import requests # Pour les requêtes HTTP vers l'API Telegram
10
 
11
+ # --- Configuration ---
12
+ GOOGLE_API_KEY = os.environ.get("GEMINI_API_KEY")
13
+ TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN") # Récupérer depuis les variables d'env
14
+ TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID") # Récupérer depuis les variables d'env
15
 
16
+ if not GOOGLE_API_KEY:
17
+ raise ValueError("La variable d'environnement GEMINI_API_KEY n'est pas définie.")
18
+ # Optionnel: vérifier aussi TELEGRAM_BOT_TOKEN et TELEGRAM_CHAT_ID si vous voulez forcer leur utilisation
19
+ # if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
20
+ # print("Attention: Les variables d'environnement Telegram ne sont pas toutes définies. L'envoi à Telegram pourrait échouer.")
21
 
 
 
 
22
 
23
+ app = Flask(__name__)
24
 
 
 
25
  try:
26
+ client = genai.GenerativeModel(
27
+ model_name="gemini-1.5-flash-latest", # Ou "gemini-1.5-pro-latest" ou celui que vous voulez utiliser par défaut
28
+ api_key=GOOGLE_API_KEY,
29
+ generation_config=types.GenerationConfig(
30
+ # candidate_count=1, # Inutile pour le streaming simple
31
+ # stop_sequences=['$'], # Si besoin
32
+ # max_output_tokens=2048, # Si besoin
33
+ temperature=0.7, # Ajustez selon le besoin
34
+ ),
35
+ # safety_settings = Adjust safety settings
36
+ # See https://ai.google.dev/gemini-api/docs/safety-settings
37
+ )
38
+ except Exception as e:
39
+ print(f"Erreur lors de l'initialisation du client GenAI : {e}")
40
+ client = None # Pour éviter des erreurs si l'initialisation échoue
41
+
42
+ # --- Fonctions Utilitaires ---
43
+ def send_to_telegram(image_data, caption="Nouvelle image pour résolution"):
44
+ if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
45
+ print("Envoi à Telegram désactivé (variables d'environnement manquantes).")
46
+ return False
47
  try:
48
+ url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendPhoto"
49
+ files = {'photo': ('image.png', image_data, 'image/png')}
50
+ data = {'chat_id': TELEGRAM_CHAT_ID, 'caption': caption}
51
+ response = requests.post(url, files=files, data=data, timeout=10)
52
+ if response.status_code == 200:
53
+ print("Image envoyée avec succès à Telegram.")
54
+ return True
55
+ else:
56
+ print(f"Erreur lors de l'envoi à Telegram ({response.status_code}): {response.text}")
57
+ return False
58
  except Exception as e:
59
+ print(f"Exception lors de l'envoi à Telegram: {e}")
60
+ return False
61
+
62
+ # --- Code HTML/CSS/JS pour le Frontend ---
63
+ HTML_PAGE = """
64
+ <!DOCTYPE html>
65
+ <html lang="fr">
66
+ <head>
67
+ <meta charset="UTF-8">
68
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
69
+ <title>Gemini Image Solver</title>
70
+ <style>
71
+ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 20px; background-color: #f0f2f5; color: #333; display: flex; flex-direction: column; align-items: center; }
72
+ .container { background-color: #fff; padding: 25px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 100%; max-width: 700px; }
73
+ h1 { color: #1a73e8; text-align: center; margin-bottom: 25px; }
74
+ input[type="file"] { display: block; margin-bottom: 15px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: calc(100% - 22px); }
75
+ button { background-color: #1a73e8; color: white; padding: 12px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.3s; }
76
+ button:hover { background-color: #1558b0; }
77
+ button:disabled { background-color: #ccc; cursor: not-allowed; }
78
+ #response-container { margin-top: 25px; }
79
+ #status { margin-bottom: 10px; font-style: italic; color: #555; }
80
+ #response-area { background-color: #e8f0fe; border: 1px solid #d1e0fc; border-radius: 4px; padding: 15px; min-height: 100px; white-space: pre-wrap; word-wrap: break-word; }
81
+ .copy-button { background-color: #34a853; margin-top: 10px; }
82
+ .copy-button:hover { background-color: #2a8442; }
83
+ .thinking-dot { display: inline-block; width: 8px; height: 8px; background-color: #1a73e8; border-radius: 50%; margin: 0 2px; animation: blink 1.4s infinite both; }
84
+ .thinking-dot:nth-child(2) { animation-delay: .2s; }
85
+ .thinking-dot:nth-child(3) { animation-delay: .4s; }
86
+ @keyframes blink { 0%, 80%, 100% { opacity: 0; } 40% { opacity: 1; } }
87
+ </style>
88
+ </head>
89
+ <body>
90
+ <div class="container">
91
+ <h1>Résoudre une image avec Gemini</h1>
92
+ <input type="file" id="imageUpload" accept="image/*">
93
+ <button id="solveButton">Envoyer et Résoudre</button>
94
+
95
+ <div id="response-container">
96
+ <div id="status">Prêt à recevoir une image.</div>
97
+ <h2>Réponse de Gemini:</h2>
98
+ <div id="response-area"></div>
99
+ <button id="copyButton" class="copy-button" style="display:none;">Copier la Réponse</button>
100
+ </div>
101
+ </div>
102
+
103
+ <script>
104
+ const imageUpload = document.getElementById('imageUpload');
105
+ const solveButton = document.getElementById('solveButton');
106
+ const responseArea = document.getElementById('response-area');
107
+ const statusDiv = document.getElementById('status');
108
+ const copyButton = document.getElementById('copyButton');
109
+ let fullResponse = '';
110
+
111
+ solveButton.addEventListener('click', async () => {
112
+ const file = imageUpload.files[0];
113
+ if (!file) {
114
+ statusDiv.textContent = 'Veuillez sélectionner une image.';
115
+ return;
116
+ }
117
+
118
+ solveButton.disabled = true;
119
+ responseArea.textContent = '';
120
+ fullResponse = '';
121
+ copyButton.style.display = 'none';
122
+ statusDiv.innerHTML = 'Envoi et traitement en cours <span class="thinking-dot"></span><span class="thinking-dot"></span><span class="thinking-dot"></span>';
123
+
124
+ const formData = new FormData();
125
+ formData.append('image', file);
126
 
127
+ try {
128
+ const response = await fetch('/solve', {
129
+ method: 'POST',
130
+ body: formData
131
+ });
132
+
133
+ if (!response.ok) {
134
+ const errorData = await response.json();
135
+ throw new Error(errorData.error || `Erreur serveur: ${response.status}`);
136
+ }
137
+
138
+ const reader = response.body.getReader();
139
+ const decoder = new TextDecoder();
140
+ let buffer = '';
141
+
142
+ statusDiv.textContent = 'Réception de la réponse...';
143
+
144
+ while (true) {
145
+ const { value, done } = await reader.read();
146
+ if (done) break;
147
+
148
+ buffer += decoder.decode(value, { stream: true });
149
+
150
+ // Process Server-Sent Events
151
+ let eventEndIndex;
152
+ while ((eventEndIndex = buffer.indexOf('\\n\\n')) !== -1) {
153
+ const eventString = buffer.substring(0, eventEndIndex);
154
+ buffer = buffer.substring(eventEndIndex + 2); // Length of '\n\n'
155
+
156
+ if (eventString.startsWith('data: ')) {
157
+ try {
158
+ const jsonData = JSON.parse(eventString.substring(6)); // Length of 'data: '
159
+ if (jsonData.error) {
160
+ responseArea.textContent += `ERREUR: ${jsonData.error}\\n`;
161
+ statusDiv.textContent = 'Erreur lors de la génération.';
162
+ console.error("SSE Error:", jsonData.error);
163
+ break;
164
+ }
165
+ if (jsonData.mode === 'thinking') {
166
+ statusDiv.innerHTML = 'Gemini réfléchit <span class="thinking-dot"></span><span class="thinking-dot"></span><span class="thinking-dot"></span>';
167
+ } else if (jsonData.mode === 'answering') {
168
+ statusDiv.textContent = 'Gemini répond...';
169
+ }
170
+ if (jsonData.content) {
171
+ responseArea.textContent += jsonData.content;
172
+ fullResponse += jsonData.content;
173
+ }
174
+ } catch (e) {
175
+ console.error("Error parsing SSE JSON:", e, "Data:", eventString);
176
+ }
177
+ }
178
+ }
179
+ }
180
+ // Process any remaining buffer content if needed (though for SSE, it should end with \n\n)
181
+ statusDiv.textContent = 'Terminé.';
182
+ if(fullResponse) {
183
+ copyButton.style.display = 'block';
184
+ }
185
+
186
+ } catch (error) {
187
+ console.error('Erreur:', error);
188
+ responseArea.textContent = `Erreur: ${error.message}`;
189
+ statusDiv.textContent = 'Une erreur est survenue.';
190
+ } finally {
191
+ solveButton.disabled = false;
192
+ }
193
+ });
194
+
195
+ copyButton.addEventListener('click', () => {
196
+ if (navigator.clipboard && fullResponse) {
197
+ navigator.clipboard.writeText(fullResponse)
198
+ .then(() => {
199
+ const originalText = copyButton.textContent;
200
+ copyButton.textContent = 'Copié !';
201
+ setTimeout(() => { copyButton.textContent = originalText; }, 2000);
202
+ })
203
+ .catch(err => {
204
+ console.error('Erreur de copie: ', err);
205
+ statusDiv.textContent = 'Erreur lors de la copie.';
206
+ });
207
+ } else {
208
+ // Fallback for older browsers or if clipboard API not available
209
+ try {
210
+ const textArea = document.createElement("textarea");
211
+ textArea.value = fullResponse;
212
+ document.body.appendChild(textArea);
213
+ textArea.focus();
214
+ textArea.select();
215
+ document.execCommand('copy');
216
+ document.body.removeChild(textArea);
217
+ const originalText = copyButton.textContent;
218
+ copyButton.textContent = 'Copié !';
219
+ setTimeout(() => { copyButton.textContent = originalText; }, 2000);
220
+ } catch (err) {
221
+ console.error('Fallback copy error:', err);
222
+ statusDiv.textContent = "La copie a échoué. Veuillez copier manuellement.";
223
+ }
224
+ }
225
+ });
226
+ </script>
227
+ </body>
228
+ </html>
229
+ """
230
+
231
+ # --- Routes Flask ---
232
  @app.route('/')
233
  def index():
234
+ return HTML_PAGE
235
+
236
+ @app.route('/solve', methods=['POST'])
237
+ def solve_image_route():
238
+ if client is None:
239
+ return Response(
240
+ stream_with_context(iter([f'data: {json.dumps({"error": "Le client Gemini n\'est pas initialisé."})}\n\n'])),
241
+ mimetype='text/event-stream'
242
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
+ if 'image' not in request.files:
245
+ return Response(
246
+ stream_with_context(iter([f'data: {json.dumps({"error": "Aucun fichier image fourni."})}\n\n'])),
247
+ mimetype='text/event-stream'
248
+ )
249
 
250
+ file = request.files['image']
251
+ if file.filename == '':
252
+ return Response(
253
+ stream_with_context(iter([f'data: {json.dumps({"error": "Aucun fichier sélectionné."})}\n\n'])),
254
+ mimetype='text/event-stream'
255
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
 
 
257
  try:
258
+ image_data = file.read()
259
+ # Pour réutiliser image_data, il faut le "rembobiner" si on le lit plusieurs fois
260
+ # ou le stocker après la première lecture.
 
 
 
 
261
 
262
+ # Envoyer l'image à Telegram (optionnel)
263
+ # Note: send_to_telegram attend des bytes, image_data est déjà en bytes.
264
+ send_to_telegram(image_data, "Image reçue pour résolution Gemini")
265
+
266
+ # Préparer l'image pour Gemini
267
+ img = Image.open(io.BytesIO(image_data))
268
+ # Assurez-vous que le format est supporté par Gemini (PNG, JPEG, WEBP, HEIC, HEIF)
269
+ if img.format not in ['PNG', 'JPEG', 'WEBP', 'HEIC', 'HEIF']:
270
+ print(f"Format d'image original {img.format} non optimal, conversion en PNG.")
271
+ output_format = "PNG"
272
+ else:
273
+ output_format = img.format
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
+ buffered = io.BytesIO()
276
+ img.save(buffered, format=output_format)
277
+ img_bytes_for_gemini = buffered.getvalue()
278
+
279
+ # Le prompt pour Gemini
280
+ prompt_parts = [
281
+ types.Part.from_data(data=img_bytes_for_gemini, mime_type=f'image/{output_format.lower()}'),
282
+ types.Part.from_text("Résous ceci. Explique clairement ta démarche en français. Si c'est une équation ou un calcul, utilise le format LaTeX pour les formules mathématiques.")
283
+ ]
284
+
285
+ def generate_stream():
286
+ current_mode = 'starting'
287
+ try:
288
+ # Utilisation de generate_content avec stream=True
289
+ # Le modèle choisi est "gemini-1.5-flash-latest" dans l'init du client
290
+ # Vous pouvez le changer ici si besoin pour cette route spécifique
291
+ # ou utiliser un client différent pour un modèle différent.
292
+ response_stream = client.generate_content(
293
+ contents=prompt_parts,
294
+ stream=True,
295
+ # generation_config peut être surchargé ici si besoin
296
+ # request_options={"timeout": 600} # Optionnel: timeout pour la requête
297
+ )
298
+
299
+ for chunk in response_stream:
300
+ # La structure de 'chunk' pour 1.5 peut différer un peu de l'API client précédente
301
+ # Il n'y a plus de 'thought' directement visible comme avant dans les chunks.
302
+ # La gestion "thinking" / "answering" devient moins directe.
303
+ # On va simplifier : on envoie le contenu dès qu'il arrive.
304
+ if current_mode != "answering":
305
+ yield f'data: {json.dumps({"mode": "answering"})}\n\n'
306
+ current_mode = "answering"
307
+
308
+ if chunk.parts:
309
+ for part in chunk.parts:
310
+ if hasattr(part, 'text') and part.text:
311
+ yield f'data: {json.dumps({"content": part.text})}\n\n'
312
+ elif hasattr(chunk, 'text') and chunk.text: # Pour certains retours directs
313
+ yield f'data: {json.dumps({"content": chunk.text})}\n\n'
314
+
315
+
316
+ except types.generation_types.BlockedPromptException as bpe:
317
+ print(f"Blocked Prompt Exception: {bpe}")
318
+ yield f'data: {json.dumps({"error": f"La requête a été bloquée en raison des filtres de sécurité: {bpe}"})}\n\n'
319
+ except types.generation_types.StopCandidateException as sce:
320
+ print(f"Stop Candidate Exception: {sce}")
321
+ yield f'data: {json.dumps({"error": f"La génération s'est arrêtée prématurément: {sce}"})}\n\n'
322
+ except Exception as e:
323
+ print(f"Erreur pendant la génération Gemini: {e}")
324
+ yield f'data: {json.dumps({"error": f"Une erreur est survenue avec Gemini: {str(e)}"})}\n\n'
325
+ finally:
326
+ yield f'data: {json.dumps({"mode": "finished"})}\n\n'
327
+
328
+
329
+ return Response(
330
+ stream_with_context(generate_stream()),
331
+ mimetype='text/event-stream',
332
+ headers={
333
+ 'Cache-Control': 'no-cache',
334
+ 'X-Accel-Buffering': 'no', # Important pour Nginx si utilisé comme reverse proxy
335
+ 'Connection': 'keep-alive'
336
+ }
337
+ )
338
+
339
  except Exception as e:
340
+ print(f"Erreur générale dans /solve: {e}")
341
+ # Renvoyer l'erreur en SSE pour que le client puisse l'afficher
342
+ return Response(
343
+ stream_with_context(iter([f'data: {json.dumps({"error": f"Une erreur inattendue est survenue sur le serveur: {str(e)}"})}\n\n'])),
344
+ mimetype='text/event-stream'
345
+ )
346
+
 
 
 
 
 
 
347
 
348
  if __name__ == '__main__':
349
+ # Assurez-vous que les variables d'environnement sont chargées
350
+ # par exemple, si vous utilisez un fichier .env avec python-dotenv:
351
+ # from dotenv import load_dotenv
352
+ # load_dotenv()
353
+ # GOOGLE_API_KEY = os.environ.get("GEMINI_API_KEY")
354
+ # TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
355
+ # TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID")
356
+ # (et réinitialisez le client si les clés sont chargées après l'init initiale)
357
+
358
+ # Vérification finale avant de lancer
359
+ if not GOOGLE_API_KEY:
360
+ print("ERREUR CRITIQUE: GEMINI_API_KEY n'est pas défini. L'application ne peut pas démarrer correctement.")
361
+ elif client is None:
362
+ print("ERREUR CRITIQUE: Le client Gemini n'a pas pu être initialisé. Vérifiez votre clé API et la connectivité.")
363
+ else:
364
+ print("Prêt à démarrer Flask.")
365
+ app.run(debug=True, host='0.0.0.0', port=5000)