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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +245 -48
app.py CHANGED
@@ -1,4 +1,221 @@
1
- # ... (début du fichier app.py) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  # --- Routes Flask ---
4
  @app.route('/')
@@ -8,45 +225,40 @@ def index():
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.")
45
  ]
46
 
47
  def generate_stream():
48
  current_mode = 'starting'
49
  try:
 
50
  response_stream = client.generate_content(
51
  contents=prompt_parts,
52
  stream=True,
@@ -66,54 +278,39 @@ def solve_image_route():
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
-
90
  return Response(
91
  stream_with_context(generate_stream()),
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:
116
  print("ERREUR CRITIQUE: Le client Gemini n'a pas pu être initialisé. Vérifiez votre clé API et la connectivité.")
117
  else:
118
- print("Prêt à démarrer Flask.")
119
  app.run(debug=True, host='0.0.0.0', 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 json
8
+
9
+ # --- Configuration ---
10
+ GOOGLE_API_KEY = os.environ.get("GEMINI_API_KEY")
11
+
12
+ if not GOOGLE_API_KEY:
13
+ raise ValueError("La variable d'environnement GEMINI_API_KEY n'est pas définie.")
14
+
15
+ app = Flask(__name__)
16
+
17
+ # --- Initialisation client Gemini ---
18
+ def initialize_gemini_client():
19
+ try:
20
+ return genai.GenerativeModel(
21
+ model_name="gemini-1.5-flash-latest", # Modèle par défaut
22
+ api_key=GOOGLE_API_KEY,
23
+ generation_config=types.GenerationConfig(
24
+ temperature=0.7,
25
+ ),
26
+ )
27
+ except Exception as e:
28
+ print(f"Erreur lors de l'initialisation du client GenAI : {e}")
29
+ return None
30
+
31
+ client = initialize_gemini_client()
32
+
33
+ # --- Code HTML/CSS/JS pour le Frontend ---
34
+ HTML_PAGE = """
35
+ <!DOCTYPE html>
36
+ <html lang="fr">
37
+ <head>
38
+ <meta charset="UTF-8">
39
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
40
+ <title>Gemini Image Solver</title>
41
+ <style>
42
+ 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; }
43
+ .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; }
44
+ h1 { color: #1a73e8; text-align: center; margin-bottom: 25px; }
45
+ input[type="file"] { display: block; margin-bottom: 15px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; width: calc(100% - 22px); }
46
+ button { background-color: #1a73e8; color: white; padding: 12px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.3s; }
47
+ button:hover { background-color: #1558b0; }
48
+ button:disabled { background-color: #ccc; cursor: not-allowed; }
49
+ #response-container { margin-top: 25px; }
50
+ #status { margin-bottom: 10px; font-style: italic; color: #555; }
51
+ #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; }
52
+ .copy-button { background-color: #34a853; margin-top: 10px; }
53
+ .copy-button:hover { background-color: #2a8442; }
54
+ .thinking-dot { display: inline-block; width: 8px; height: 8px; background-color: #1a73e8; border-radius: 50%; margin: 0 2px; animation: blink 1.4s infinite both; }
55
+ .thinking-dot:nth-child(2) { animation-delay: .2s; }
56
+ .thinking-dot:nth-child(3) { animation-delay: .4s; }
57
+ @keyframes blink { 0%, 80%, 100% { opacity: 0; } 40% { opacity: 1; } }
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <div class="container">
62
+ <h1>Résoudre une image avec Gemini</h1>
63
+ <input type="file" id="imageUpload" accept="image/*">
64
+ <button id="solveButton">Envoyer et Résoudre</button>
65
+
66
+ <div id="response-container">
67
+ <div id="status">Prêt à recevoir une image.</div>
68
+ <h2>Réponse de Gemini:</h2>
69
+ <div id="response-area"></div>
70
+ <button id="copyButton" class="copy-button" style="display:none;">Copier la Réponse</button>
71
+ </div>
72
+ </div>
73
+
74
+ <script>
75
+ const imageUpload = document.getElementById('imageUpload');
76
+ const solveButton = document.getElementById('solveButton');
77
+ const responseArea = document.getElementById('response-area');
78
+ const statusDiv = document.getElementById('status');
79
+ const copyButton = document.getElementById('copyButton');
80
+ let fullResponse = '';
81
+
82
+ solveButton.addEventListener('click', async () => {
83
+ const file = imageUpload.files[0];
84
+ if (!file) {
85
+ statusDiv.textContent = 'Veuillez sélectionner une image.';
86
+ return;
87
+ }
88
+
89
+ solveButton.disabled = true;
90
+ responseArea.textContent = '';
91
+ fullResponse = '';
92
+ copyButton.style.display = 'none';
93
+ statusDiv.innerHTML = 'Envoi et traitement en cours <span class="thinking-dot"></span><span class="thinking-dot"></span><span class="thinking-dot"></span>';
94
+
95
+ const formData = new FormData();
96
+ formData.append('image', file);
97
+
98
+ try {
99
+ const response = await fetch('/solve', {
100
+ method: 'POST',
101
+ body: formData
102
+ });
103
+
104
+ if (!response.ok) {
105
+ const errorData = await response.json();
106
+ throw new Error(errorData.error || `Erreur serveur: ${response.status}`);
107
+ }
108
+
109
+ const reader = response.body.getReader();
110
+ const decoder = new TextDecoder();
111
+ let buffer = '';
112
+
113
+ statusDiv.textContent = 'Réception de la réponse...';
114
+
115
+ while (true) {
116
+ const { value, done } = await reader.read();
117
+ if (done) break;
118
+
119
+ buffer += decoder.decode(value, { stream: true });
120
+
121
+ // Process Server-Sent Events
122
+ let eventEndIndex;
123
+ while ((eventEndIndex = buffer.indexOf('\\n\\n')) !== -1) {
124
+ const eventString = buffer.substring(0, eventEndIndex);
125
+ buffer = buffer.substring(eventEndIndex + 2); // Length of '\n\n'
126
+
127
+ if (eventString.startsWith('data: ')) {
128
+ try {
129
+ const jsonData = JSON.parse(eventString.substring(6)); // Length of 'data: '
130
+ if (jsonData.error) {
131
+ responseArea.textContent += `ERREUR: ${jsonData.error}\\n`;
132
+ statusDiv.textContent = 'Erreur lors de la génération.';
133
+ console.error("SSE Error:", jsonData.error);
134
+ break;
135
+ }
136
+ if (jsonData.mode === 'thinking') {
137
+ statusDiv.innerHTML = 'Gemini réfléchit <span class="thinking-dot"></span><span class="thinking-dot"></span><span class="thinking-dot"></span>';
138
+ } else if (jsonData.mode === 'answering') {
139
+ statusDiv.textContent = 'Gemini répond...';
140
+ }
141
+ if (jsonData.content) {
142
+ responseArea.textContent += jsonData.content;
143
+ fullResponse += jsonData.content;
144
+ }
145
+ } catch (e) {
146
+ console.error("Error parsing SSE JSON:", e, "Data:", eventString);
147
+ }
148
+ }
149
+ }
150
+ }
151
+ statusDiv.textContent = 'Terminé.';
152
+ if(fullResponse) {
153
+ copyButton.style.display = 'block';
154
+ }
155
+
156
+ } catch (error) {
157
+ console.error('Erreur:', error);
158
+ responseArea.textContent = `Erreur: ${error.message}`;
159
+ statusDiv.textContent = 'Une erreur est survenue.';
160
+ } finally {
161
+ solveButton.disabled = false;
162
+ }
163
+ });
164
+
165
+ copyButton.addEventListener('click', () => {
166
+ if (navigator.clipboard && fullResponse) {
167
+ navigator.clipboard.writeText(fullResponse)
168
+ .then(() => {
169
+ const originalText = copyButton.textContent;
170
+ copyButton.textContent = 'Copié !';
171
+ setTimeout(() => { copyButton.textContent = originalText; }, 2000);
172
+ })
173
+ .catch(err => {
174
+ console.error('Erreur de copie: ', err);
175
+ statusDiv.textContent = 'Erreur lors de la copie.';
176
+ });
177
+ } else {
178
+ // Fallback pour les navigateurs plus anciens
179
+ try {
180
+ const textArea = document.createElement("textarea");
181
+ textArea.value = fullResponse;
182
+ document.body.appendChild(textArea);
183
+ textArea.focus();
184
+ textArea.select();
185
+ document.execCommand('copy');
186
+ document.body.removeChild(textArea);
187
+ const originalText = copyButton.textContent;
188
+ copyButton.textContent = 'Copié !';
189
+ setTimeout(() => { copyButton.textContent = originalText; }, 2000);
190
+ } catch (err) {
191
+ console.error('Fallback copy error:', err);
192
+ statusDiv.textContent = "La copie a échoué. Veuillez copier manuellement.";
193
+ }
194
+ }
195
+ });
196
+ </script>
197
+ </body>
198
+ </html>
199
+ """
200
+
201
+ # --- Fonctions d'utilitaires pour le traitement d'images ---
202
+ def process_image(image_bytes):
203
+ """Traite une image pour l'envoyer à Gemini"""
204
+ try:
205
+ img = Image.open(io.BytesIO(image_bytes))
206
+ # Assurez-vous que le format est supporté par Gemini
207
+ if img.format not in ['PNG', 'JPEG', 'WEBP', 'HEIC', 'HEIF']:
208
+ print(f"Format d'image original {img.format} non optimal, conversion en PNG.")
209
+ output_format = "PNG"
210
+ else:
211
+ output_format = img.format
212
+
213
+ buffered = io.BytesIO()
214
+ img.save(buffered, format=output_format)
215
+ return buffered.getvalue(), output_format.lower()
216
+ except Exception as e:
217
+ print(f"Erreur lors du traitement de l'image: {e}")
218
+ raise
219
 
220
  # --- Routes Flask ---
221
  @app.route('/')
 
225
  @app.route('/solve', methods=['POST'])
226
  def solve_image_route():
227
  if client is None:
228
+ return Response(
229
+ stream_with_context(iter([f'data: {json.dumps({"error": "Le client Gemini n\'est pas initialisé."})}\n\n'])),
230
+ mimetype='text/event-stream'
231
+ )
 
232
 
233
  if 'image' not in request.files:
234
+ return Response(
235
+ stream_with_context(iter([f'data: {json.dumps({"error": "Aucun fichier image fourni."})}\n\n'])),
236
+ mimetype='text/event-stream'
237
+ )
238
 
239
  file = request.files['image']
240
  if file.filename == '':
241
+ return Response(
242
+ stream_with_context(iter([f'data: {json.dumps({"error": "Aucun fichier sélectionné."})}\n\n'])),
243
+ mimetype='text/event-stream'
244
+ )
245
 
246
  try:
247
  image_data = file.read()
 
 
 
 
 
 
 
 
248
 
249
+ # Préparer l'image pour Gemini
250
+ processed_image, output_format = process_image(image_data)
 
 
251
 
252
+ # Le prompt pour Gemini
253
  prompt_parts = [
254
+ types.Part.from_data(data=processed_image, mime_type=f'image/{output_format}'),
255
  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.")
256
  ]
257
 
258
  def generate_stream():
259
  current_mode = 'starting'
260
  try:
261
+ # Utilisation de generate_content avec stream=True
262
  response_stream = client.generate_content(
263
  contents=prompt_parts,
264
  stream=True,
 
278
 
279
  except types.generation_types.BlockedPromptException as bpe:
280
  print(f"Blocked Prompt Exception: {bpe}")
281
+ yield f'data: {json.dumps({"error": f"La requête a été bloquée en raison des filtres de sécurité: {bpe}"})}\n\n'
 
 
282
  except types.generation_types.StopCandidateException as sce:
283
  print(f"Stop Candidate Exception: {sce}")
284
+ yield f'data: {json.dumps({"error": f"La génération s'est arrêtée prématurément: {sce}"})}\n\n'
 
 
285
  except Exception as e:
286
  print(f"Erreur pendant la génération Gemini: {e}")
287
+ yield f'data: {json.dumps({"error": f"Une erreur est survenue avec Gemini: {str(e)}"})}\n\n'
 
 
288
  finally:
 
 
 
 
289
  yield f'data: {json.dumps({"mode": "finished"})}\n\n'
290
 
 
291
  return Response(
292
  stream_with_context(generate_stream()),
293
  mimetype='text/event-stream',
294
  headers={
295
  'Cache-Control': 'no-cache',
296
+ 'X-Accel-Buffering': 'no', # Important pour Nginx
297
  'Connection': 'keep-alive'
298
  }
299
  )
300
 
301
  except Exception as e:
302
  print(f"Erreur générale dans /solve: {e}")
303
+ return Response(
304
+ stream_with_context(iter([f'data: {json.dumps({"error": f"Une erreur inattendue est survenue sur le serveur: {str(e)}"})}\n\n'])),
305
+ mimetype='text/event-stream'
306
+ )
 
 
 
 
307
 
308
  if __name__ == '__main__':
309
+ # Vérification finale avant de lancer
310
  if not GOOGLE_API_KEY:
311
  print("ERREUR CRITIQUE: GEMINI_API_KEY n'est pas défini. L'application ne peut pas démarrer correctement.")
312
  elif client is None:
313
  print("ERREUR CRITIQUE: Le client Gemini n'a pas pu être initialisé. Vérifiez votre clé API et la connectivité.")
314
  else:
315
+ print("Serveur démarré. Accédez à http://localhost:5000 pour utiliser l'application.")
316
  app.run(debug=True, host='0.0.0.0', port=5000)