Spaces:
Running
Running
File size: 10,420 Bytes
8071ff2 f388c93 6b10944 451d8eb 8071ff2 451d8eb 8071ff2 451d8eb 12d4886 f388c93 8071ff2 451d8eb 12d4886 e7761b5 8071ff2 451d8eb 8071ff2 451d8eb 8071ff2 e7761b5 f388c93 8071ff2 2ef19ee 8071ff2 f388c93 8071ff2 f388c93 949f8bc 451d8eb 8071ff2 451d8eb 8071ff2 451d8eb 8071ff2 451d8eb 8071ff2 451d8eb 8071ff2 451d8eb 8071ff2 451d8eb 8071ff2 451d8eb 8071ff2 451d8eb 8071ff2 451d8eb 8071ff2 451d8eb f388c93 8071ff2 451d8eb 8071ff2 451d8eb 8071ff2 451d8eb 8071ff2 451d8eb 8071ff2 f388c93 451d8eb 8071ff2 e7761b5 2ef19ee 93f4a81 8071ff2 2ef19ee 8071ff2 451d8eb 8071ff2 2ef19ee 451d8eb 8071ff2 2ef19ee f388c93 8071ff2 |
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 |
from flask import Flask, render_template, request, jsonify, Response, stream_with_context, abort
from google import genai
from google.genai import types
import os
from PIL import Image
import io
import base64
import json
import logging
from werkzeug.utils import secure_filename
import mimetypes
# Configuration du logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
# Configuration
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16 MB max de taille d'image
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
# Récupération de la clé API depuis les variables d'environnement
GOOGLE_API_KEY = os.environ.get("GEMINI_API_KEY")
if not GOOGLE_API_KEY:
logger.error("La clé API Gemini n'est pas configurée. Définissez la variable d'environnement GEMINI_API_KEY.")
# Initialisation du client Gemini
try:
client = genai.Client(api_key=GOOGLE_API_KEY)
logger.info("Client Gemini initialisé avec succès")
except Exception as e:
logger.error(f"Erreur lors de l'initialisation du client Gemini: {e}")
client = None
def allowed_file(filename):
"""Vérifie si le fichier a une extension autorisée"""
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def process_image(file_data):
"""Traite l'image téléchargée et retourne la chaîne base64"""
try:
img = Image.open(io.BytesIO(file_data))
# Redimensionnement si l'image est trop grande (facultatif)
max_size = 1600 # pixels
if max(img.size) > max_size:
ratio = max_size / max(img.size)
new_size = (int(img.size[0] * ratio), int(img.size[1] * ratio))
img = img.resize(new_size, Image.LANCZOS)
logger.info(f"Image redimensionnée à {new_size}")
buffered = io.BytesIO()
img.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode()
return img_str
except Exception as e:
logger.error(f"Erreur lors du traitement de l'image: {e}")
raise ValueError(f"Impossible de traiter l'image: {str(e)}")
def generate_ai_response(model_id, img_str, prompt, thinking_budget=None):
"""Fonction générique pour la génération de contenu avec les modèles Gemini"""
if client is None:
raise ValueError("Le client Gemini n'est pas initialisé")
contents = [
{'inline_data': {'mime_type': 'image/png', 'data': img_str}},
prompt
]
config_args = {}
if thinking_budget:
config_args["thinking_config"] = types.ThinkingConfig(thinking_budget=thinking_budget)
config = types.GenerateContentConfig(**config_args)
try:
return client.models.generate_content_stream(
model=model_id,
contents=contents,
config=config
)
except Exception as e:
logger.error(f"Erreur lors de la génération de contenu avec {model_id}: {e}")
raise ValueError(f"Erreur lors de la génération: {str(e)}")
def stream_response(response_generator):
"""Fonction qui gère le streaming des réponses"""
mode = 'starting'
try:
for chunk in response_generator:
# Vérification de validité du chunk
if not hasattr(chunk, 'candidates') or not chunk.candidates:
continue
for part in chunk.candidates[0].content.parts:
# Gestion du mode de pensée
if hasattr(part, 'thought') and part.thought:
if mode != "thinking":
yield f'data: {json.dumps({"mode": "thinking"})}\n\n'
mode = "thinking"
# Mode de réponse
else:
if mode != "answering":
yield f'data: {json.dumps({"mode": "answering"})}\n\n'
mode = "answering"
# Envoi du contenu s'il existe
if hasattr(part, 'text') and part.text:
yield f'data: {json.dumps({"content": part.text})}\n\n'
except Exception as e:
logger.error(f"Erreur pendant le streaming: {e}")
yield f'data: {json.dumps({"error": str(e)})}\n\n'
@app.route('/')
def index():
"""Page d'accueil principale"""
try:
return render_template('index.html')
except Exception as e:
logger.error(f"Erreur lors du rendu de index.html: {e}")
return "Une erreur est survenue. Veuillez réessayer plus tard.", 500
@app.route('/free')
def maintenance():
"""Page de maintenance"""
try:
return render_template('maj.html')
except Exception as e:
logger.error(f"Erreur lors du rendu de maj.html: {e}")
return "Page en maintenance. Veuillez revenir plus tard.", 503
@app.route('/health')
def health_check():
"""Endpoint de vérification de santé pour monitoring"""
status = {
"status": "ok",
"gemini_client": client is not None
}
return jsonify(status)
@app.route('/solve', methods=['POST'])
def solve():
"""Endpoint utilisant le modèle Pro avec capacités de réflexion étendues"""
if not client:
return jsonify({"error": "Service non disponible - client Gemini non initialisé"}), 503
try:
# Vérification de l'image
if 'image' not in request.files:
return jsonify({"error": "Aucune image n'a été envoyée"}), 400
file = request.files['image']
if file.filename == '':
return jsonify({"error": "Aucun fichier sélectionné"}), 400
if not allowed_file(file.filename):
return jsonify({"error": "Format de fichier non autorisé"}), 400
# Détection du type de contenu
file_data = file.read()
content_type = mimetypes.guess_type(file.filename)[0]
if not content_type or not content_type.startswith('image/'):
return jsonify({"error": "Le fichier envoyé n'est pas une image valide"}), 400
# Traitement de l'image
try:
img_str = process_image(file_data)
except ValueError as e:
return jsonify({"error": str(e)}), 400
# Génération de la réponse
try:
response_generator = generate_ai_response(
model_id="gemini-2.5-pro-exp-03-25",
img_str=img_str,
prompt="Résous ça en français with rendering latex",
thinking_budget=8000
)
return Response(
stream_with_context(stream_response(response_generator)),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no'
}
)
except ValueError as e:
return jsonify({"error": str(e)}), 500
except Exception as e:
logger.error(f"Erreur dans /solve: {e}")
return jsonify({"error": "Une erreur inconnue est survenue"}), 500
@app.route('/solved', methods=['POST'])
def solved():
"""Endpoint utilisant le modèle Flash pour des réponses plus rapides"""
if not client:
return jsonify({"error": "Service non disponible - client Gemini non initialisé"}), 503
try:
# Vérification de l'image
if 'image' not in request.files:
return jsonify({"error": "Aucune image n'a été envoyée"}), 400
file = request.files['image']
if file.filename == '':
return jsonify({"error": "Aucun fichier sélectionné"}), 400
if not allowed_file(file.filename):
return jsonify({"error": "Format de fichier non autorisé"}), 400
# Détection du type de contenu
file_data = file.read()
content_type = mimetypes.guess_type(file.filename)[0]
if not content_type or not content_type.startswith('image/'):
return jsonify({"error": "Le fichier envoyé n'est pas une image valide"}), 400
# Traitement de l'image
try:
img_str = process_image(file_data)
except ValueError as e:
return jsonify({"error": str(e)}), 400
# Génération de la réponse
try:
response_generator = generate_ai_response(
model_id="gemini-2.5-flash-preview-04-17",
img_str=img_str,
prompt="Résous ça en français with rendering latex"
)
return Response(
stream_with_context(stream_response(response_generator)),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no'
}
)
except ValueError as e:
return jsonify({"error": str(e)}), 500
except Exception as e:
logger.error(f"Erreur dans /solved: {e}")
return jsonify({"error": "Une erreur inconnue est survenue"}), 500
@app.errorhandler(413)
def request_entity_too_large(error):
"""Gestion de l'erreur de fichier trop volumineux"""
return jsonify({"error": f"Le fichier est trop volumineux. Taille maximale: {MAX_CONTENT_LENGTH/1024/1024} MB"}), 413
@app.errorhandler(404)
def page_not_found(error):
"""Gestion de l'erreur 404"""
return jsonify({"error": "Page non trouvée"}), 404
@app.errorhandler(500)
def internal_server_error(error):
"""Gestion de l'erreur 500"""
logger.error(f"Erreur 500: {error}")
return jsonify({"error": "Erreur interne du serveur"}), 500
if __name__ == '__main__':
# Vérification des dépendances et configuration avant le démarrage
if not client:
logger.warning("L'application démarre sans client Gemini initialisé. Certaines fonctionnalités seront indisponibles.")
# Configuration pour le développement
debug_mode = os.environ.get("FLASK_DEBUG", "False").lower() == "true"
port = int(os.environ.get("PORT", 5000))
app.run(debug=debug_mode, host='0.0.0.0', port=port) |