// lib/services/gemini_service.dart import 'dart:async';import 'dart:convert'; import 'package:http/http.dart' as http; import 'image_editing_service.dart'; class GeminiService implements ImageEditingService { final String _apiKey; // L'URL que vous utilisiez et qui fonctionnait final String _apiUrl = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent'; GeminiService({String? apiKey}) : _apiKey = apiKey ?? 'AIzaSyBQpeyl-Qi8QoCfwmJoxAumYYI5-nwit4Q'; // Remplacez par votre clé /// EditImage qui retourne un Stream, comme attendu par l'interface. @override Stream editImage(String base64Image, String prompt, int width, int height, {int numberOfImages = 1}) { // 1. On crée un StreamController final controller = StreamController(); // 2. On lance la génération en arrière-plan _generateAndStreamImage(controller, base64Image, prompt, width, height); // 3. On retourne le stream immédiatement return controller.stream; } /// Méthode privée qui contient la logique de génération correcte. Future _generateAndStreamImage(StreamController controller, String base64Image, String prompt, int width, int height) async { print("[GeminiService] 🚀 Lancement de la génération d'image..."); final editPrompt = _buildEditPrompt(prompt, width, height); final requestBody = { 'contents': [ { 'parts': [ {'text': editPrompt}, { 'inlineData': { 'mimeType': 'image/jpeg', 'data': base64Image, } } ] } ], 'generationConfig': { // La documentation la plus récente suggère d'utiliser 'tool_config' pour ce genre de tâche // mais nous allons garder la version qui marchait pour vous. // Si une erreur survient, ce sera le premier endroit à vérifier. // 👇👇👇 CETTE LIGNE EST LA CLÉ DU SUCCÈS 👇👇👇 'responseModalities': ['IMAGE', 'TEXT'], } }; try { // --- CORRECTION DE L'URL --- // L'action ':generateContent' est déjà dans la variable _apiUrl. final response = await http .post( Uri.parse('$_apiUrl?key=$_apiKey'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(requestBody), ) .timeout(const Duration(minutes: 5)); if (response.statusCode == 200) { final responseData = jsonDecode(response.body); final String singleImage = _extractImageFromResponse(responseData); print("[GeminiService] ✅ Image reçue. Envoi dans le stream..."); controller.add(singleImage); } else { final errorBody = jsonDecode(response.body); final errorMessage = errorBody['error']?['message'] ?? response.body; throw Exception('Erreur Gemini ${response.statusCode}: $errorMessage'); } } catch (e, s) { print("[GeminiService] ❌ Erreur lors de la génération: $e"); controller.addError(e, s); } finally { print("[GeminiService] Stream fermé."); controller.close(); } } // --- TOUTES LES MÉTHODES HELPER SONT MAINTENANT À L'INTÉRIEUR DE LA CLASSE --- String _buildEditPrompt(String userPrompt, int width, int height) { return '''You are a professional photo editor. Based on the provided image and instructions below, generate an edited version that enhances the photo while maintaining naturalness and authenticity. INSTRUCTIONS: $userPrompt EDITING GUIDELINES: - Preserve the original composition and subject - Maintain realistic skin textures (no plastic look) - Enhance lighting naturally with warm, soft tones - Improve colors while keeping them true-to-life - Add subtle details and clarity without over-processing - Keep the image aspect ratio and dimensions - Ensure the final result looks professional and Instagram-ready CONSTRAINTS: - Do NOT add watermarks or text - Do NOT change the main subject dramatically - Do NOT apply artificial filters or effects - Keep editing subtle and professional Generate the edited image following these guidelines.'''; } // --- MÉTHODE D'EXTRACTION CORRIGÉE --- String _extractImageFromResponse(Map response) { try { final candidates = response['candidates'] as List?; if (candidates == null || candidates.isEmpty) { // Si il n'y a aucun candidat, c'est que le prompt a été bloqué AVANT la génération. final promptFeedback = response['promptFeedback']; if (promptFeedback != null && promptFeedback['blockReason'] != null) { throw Exception("Le prompt a été bloqué par Gemini pour la raison : ${promptFeedback['blockReason']}."); } throw Exception('Réponse invalide de Gemini : aucun "candidate" trouvé.'); } final candidate = candidates.first; // 1. Vérifier la raison de la fin AVANT de chercher le contenu. final finishReason = candidate['finishReason'] as String?; if (finishReason != null && finishReason != 'STOP') { if (finishReason == 'SAFETY') { throw Exception("La génération a été stoppée par Gemini pour des raisons de sécurité (SAFETY). L'image ou le prompt a été jugé inapproprié."); } throw Exception("La génération s'est terminée prématurément. Raison : $finishReason"); } // 2. Si tout va bien, on peut maintenant chercher le contenu. final content = candidate['content'] as Map?; if (content == null) { throw Exception('Pas de "content" dans la première candidate, malgré un "finishReason" correct. Réponse inattendue.'); } final parts = content['parts'] as List?; if (parts == null || parts.isEmpty) { throw Exception('Pas de "parts" dans le content'); } for (final part in parts) { final inlineData = part['inlineData'] as Map?; if (inlineData != null && inlineData.containsKey('data')) { return inlineData['data'] as String; } } throw Exception("Pas d'image (inlineData) dans les parts de la réponse."); } catch (e) { // Afficher la réponse brute aide toujours à déboguer print("--- Réponse brute de Gemini lors de l'erreur d'extraction ---"); print(jsonEncode(response)); print("------------------------------------------------------------"); // Renvoie l'erreur spécifique que nous avons construite. throw Exception('Erreur d\'extraction de l\'image: $e'); } } @override Future generatePrompt(String base64Image) async { print("[GeminiService] 🚀 Lancement de l'analyse d'image pour le prompt..."); final requestBody = { 'contents': [ { 'parts': [ { 'text': 'Analyze this image and suggest specific professional photo enhancements and a good instagram publication text. Focus on: lighting, colors, composition, and overall aesthetic. Be concise and actionable.' }, { 'inlineData': { 'mimeType': 'image/jpeg', 'data': base64Image, } } ] } ] }; try { final response = await http .post( Uri.parse('$_apiUrl?key=$_apiKey'), headers: {'Content-Type': 'application/json'}, body: jsonEncode(requestBody), ) .timeout(const Duration(minutes: 3)); if (response.statusCode == 200) { final responseData = jsonDecode(response.body); return _extractTextFromResponse(responseData); } else { throw Exception('Erreur Gemini ${response.statusCode}: ${response.body}'); } } catch (e) { throw Exception('Erreur génération prompt Gemini: $e'); } } String _extractTextFromResponse(Map response) { try { final candidates = response['candidates'] as List?; if (candidates == null || candidates.isEmpty) { return ''; } final content = candidates[0]['content'] as Map?; if (content == null) { return ''; } final parts = content['parts'] as List?; if (parts == null || parts.isEmpty) { return ''; } final buffer = StringBuffer(); for (final part in parts) { final text = part['text'] as String?; if (text != null && text.isNotEmpty) { buffer.writeln(text.trim()); } } return buffer.toString().trim(); } catch (e) { print('Erreur extraction texte: $e'); return ''; } } }