Docfile commited on
Commit
8a641ea
·
verified ·
1 Parent(s): 79b1a5a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +36 -282
app.py CHANGED
@@ -1,232 +1,4 @@
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('/')
@@ -236,47 +8,37 @@ def index():
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.")
@@ -285,22 +47,12 @@ def solve_image_route():
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"
@@ -309,20 +61,29 @@ def solve_image_route():
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
 
@@ -331,31 +92,24 @@ def solve_image_route():
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:
 
1
+ # ... (début du fichier app.py) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  # --- Routes Flask ---
4
  @app.route('/')
 
8
  @app.route('/solve', methods=['POST'])
9
  def solve_image_route():
10
  if client is None:
11
+ # Pour les erreurs retournées en dehors du générateur de stream,
12
+ # il est préférable de ne pas utiliser stream_with_context juste pour une erreur.
13
+ # Mais pour garder la cohérence avec la demande de SSE partout :
14
+ error_payload = {"error": "Le client Gemini n'est pas initialisé."}
15
+ return Response(f'data: {json.dumps(error_payload)}\n\n', mimetype='text/event-stream')
16
 
17
  if 'image' not in request.files:
18
+ error_payload = {"error": "Aucun fichier image fourni."}
19
+ return Response(f'data: {json.dumps(error_payload)}\n\n', mimetype='text/event-stream')
 
 
20
 
21
  file = request.files['image']
22
  if file.filename == '':
23
+ error_payload = {"error": "Aucun fichier sélectionné."}
24
+ return Response(f'data: {json.dumps(error_payload)}\n\n', mimetype='text/event-stream')
 
 
25
 
26
  try:
27
  image_data = file.read()
 
 
 
 
 
28
  send_to_telegram(image_data, "Image reçue pour résolution Gemini")
29
 
 
30
  img = Image.open(io.BytesIO(image_data))
 
31
  if img.format not in ['PNG', 'JPEG', 'WEBP', 'HEIC', 'HEIF']:
32
  print(f"Format d'image original {img.format} non optimal, conversion en PNG.")
33
  output_format = "PNG"
34
  else:
35
+ output_format = img.format.upper() # S'assurer que c'est en majuscules pour la mime_type
36
 
37
  buffered = io.BytesIO()
38
+ # Utiliser le format déterminé pour la sauvegarde
39
+ img.save(buffered, format=output_format if output_format != "JPEG" else "JPEG", quality=90 if output_format == "JPEG" else None) # Spécifier la qualité pour JPEG
40
  img_bytes_for_gemini = buffered.getvalue()
41
 
 
42
  prompt_parts = [
43
  types.Part.from_data(data=img_bytes_for_gemini, mime_type=f'image/{output_format.lower()}'),
44
  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.")
 
47
  def generate_stream():
48
  current_mode = 'starting'
49
  try:
 
 
 
 
50
  response_stream = client.generate_content(
51
  contents=prompt_parts,
52
  stream=True,
 
 
53
  )
54
 
55
  for chunk in response_stream:
 
 
 
 
56
  if current_mode != "answering":
57
  yield f'data: {json.dumps({"mode": "answering"})}\n\n'
58
  current_mode = "answering"
 
61
  for part in chunk.parts:
62
  if hasattr(part, 'text') and part.text:
63
  yield f'data: {json.dumps({"content": part.text})}\n\n'
64
+ elif hasattr(chunk, 'text') and chunk.text:
65
  yield f'data: {json.dumps({"content": chunk.text})}\n\n'
66
 
 
67
  except types.generation_types.BlockedPromptException as bpe:
68
  print(f"Blocked Prompt Exception: {bpe}")
69
+ error_message_detail = f"La requête a été bloquée en raison des filtres de sécurité: {str(bpe)}"
70
+ error_payload = {"error": error_message_detail}
71
+ yield f'data: {json.dumps(error_payload)}\n\n' # Ligne 318 modifiée
72
  except types.generation_types.StopCandidateException as sce:
73
  print(f"Stop Candidate Exception: {sce}")
74
+ error_message_detail = f"La génération s'est arrêtée prématurément: {str(sce)}"
75
+ error_payload = {"error": error_message_detail}
76
+ yield f'data: {json.dumps(error_payload)}\n\n' # Ligne 321 modifiée
77
  except Exception as e:
78
  print(f"Erreur pendant la génération Gemini: {e}")
79
+ error_message_detail = f"Une erreur est survenue avec Gemini: {str(e)}"
80
+ error_payload = {"error": error_message_detail}
81
+ yield f'data: {json.dumps(error_payload)}\n\n' # Ligne 325 modifiée
82
  finally:
83
+ # Peut-être envoyer un message de fin spécifique si aucun contenu n'a été généré
84
+ # ou si le mode est toujours 'starting' ou 'thinking'.
85
+ # Pour l'instant, on se contente de s'assurer que le stream se termine proprement.
86
+ # Un message de fin explicite pourrait être utile côté client.
87
  yield f'data: {json.dumps({"mode": "finished"})}\n\n'
88
 
89
 
 
92
  mimetype='text/event-stream',
93
  headers={
94
  'Cache-Control': 'no-cache',
95
+ 'X-Accel-Buffering': 'no',
96
  'Connection': 'keep-alive'
97
  }
98
  )
99
 
100
  except Exception as e:
101
  print(f"Erreur générale dans /solve: {e}")
102
+ error_message_detail = f"Une erreur inattendue est survenue sur le serveur: {str(e)}"
103
+ error_payload = {"error": error_message_detail}
104
+ # Retourner l'erreur en SSE pour que le client puisse l'afficher.
105
+ # Pas besoin de stream_with_context(iter([...])) pour un seul message.
106
+ return Response(f'data: {json.dumps(error_payload)}\n\n', mimetype='text/event-stream', status=500)
107
 
108
 
109
+ # ... (HTML_PAGE et le reste du fichier) ...
 
 
 
 
 
 
 
 
110
 
111
+ if __name__ == '__main__':
112
+ # ... (vérifications et app.run) ...
113
  if not GOOGLE_API_KEY:
114
  print("ERREUR CRITIQUE: GEMINI_API_KEY n'est pas défini. L'application ne peut pas démarrer correctement.")
115
  elif client is None: