You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

249 satır
8.7KB

  1. // lib/services/gemini_service.dart
  2. import 'dart:async';import 'dart:convert';
  3. import 'package:http/http.dart' as http;
  4. import 'image_editing_service.dart';
  5. class GeminiService implements ImageEditingService {
  6. GeminiService({String? apiKey}) : _apiKey = apiKey ?? 'AIzaSyBQpeyl-Qi8QoCfwmJoxAumYYI5-nwit4Q';
  7. final String _apiKey;
  8. // L'URL que vous utilisiez et qui fonctionnait
  9. final String _apiUrl =
  10. 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent'; // Remplacez par votre clé
  11. /// EditImage qui retourne un Stream, comme attendu par l'interface.
  12. @override
  13. Stream<String> editImage(String base64Image,
  14. String prompt,
  15. int width,
  16. int height,
  17. {int numberOfImages = 1}) {
  18. // 1. On crée un StreamController
  19. final controller = StreamController<String>();
  20. // 2. On lance la génération en arrière-plan
  21. _generateAndStreamImage(controller, base64Image, prompt, width, height);
  22. // 3. On retourne le stream immédiatement
  23. return controller.stream;
  24. }
  25. /// Méthode privée qui contient la logique de génération correcte.
  26. Future<void> _generateAndStreamImage(StreamController<String> controller,
  27. String base64Image,
  28. String prompt,
  29. int width,
  30. int height) async {
  31. print("[GeminiService] 🚀 Lancement de la génération d'image...");
  32. final editPrompt = _buildEditPrompt(prompt, width, height);
  33. final requestBody = {
  34. 'contents': [
  35. {
  36. 'parts': [
  37. {'text': editPrompt},
  38. {
  39. 'inlineData': {
  40. 'mimeType': 'image/jpeg',
  41. 'data': base64Image,
  42. }
  43. }
  44. ]
  45. }
  46. ],
  47. 'generationConfig': {
  48. // La documentation la plus récente suggère d'utiliser 'tool_config' pour ce genre de tâche
  49. // mais nous allons garder la version qui marchait pour vous.
  50. // Si une erreur survient, ce sera le premier endroit à vérifier.
  51. // 👇👇👇 CETTE LIGNE EST LA CLÉ DU SUCCÈS 👇👇👇
  52. 'responseModalities': ['IMAGE', 'TEXT'],
  53. }
  54. };
  55. try {
  56. // --- CORRECTION DE L'URL ---
  57. // L'action ':generateContent' est déjà dans la variable _apiUrl.
  58. final response = await http
  59. .post(
  60. Uri.parse('$_apiUrl?key=$_apiKey'),
  61. headers: {'Content-Type': 'application/json'},
  62. body: jsonEncode(requestBody),
  63. )
  64. .timeout(const Duration(minutes: 5));
  65. if (response.statusCode == 200) {
  66. final responseData = jsonDecode(response.body);
  67. final singleImage = _extractImageFromResponse(responseData);
  68. print('[GeminiService] ✅ Image reçue. Envoi dans le stream...');
  69. controller.add(singleImage);
  70. } else {
  71. final errorBody = jsonDecode(response.body);
  72. final errorMessage = errorBody['error']?['message'] ?? response.body;
  73. throw Exception('Erreur Gemini ${response.statusCode}: $errorMessage');
  74. }
  75. } catch (e, s) {
  76. print('[GeminiService] ❌ Erreur lors de la génération: $e');
  77. controller.addError(e, s);
  78. } finally {
  79. print('[GeminiService] Stream fermé.');
  80. controller.close();
  81. }
  82. }
  83. // --- TOUTES LES MÉTHODES HELPER SONT MAINTENANT À L'INTÉRIEUR DE LA CLASSE ---
  84. String _buildEditPrompt(String userPrompt, int width, int height) => '''You are a professional photo editor. Based on the provided image and instructions below,
  85. generate an edited version that enhances the photo while maintaining naturalness and authenticity.
  86. INSTRUCTIONS:
  87. $userPrompt
  88. EDITING GUIDELINES:
  89. - Preserve the original composition and subject
  90. - Maintain realistic skin textures (no plastic look)
  91. - Enhance lighting naturally with warm, soft tones
  92. - Improve colors while keeping them true-to-life
  93. - Add subtle details and clarity without over-processing
  94. - Keep the image aspect ratio and dimensions
  95. - Ensure the final result looks professional and Instagram-ready
  96. CONSTRAINTS:
  97. - Do NOT add watermarks or text
  98. - Do NOT change the main subject dramatically
  99. - Do NOT apply artificial filters or effects
  100. - Keep editing subtle and professional
  101. Generate the edited image following these guidelines.''';
  102. // --- MÉTHODE D'EXTRACTION CORRIGÉE ---
  103. String _extractImageFromResponse(Map<String, dynamic> response) {
  104. try {
  105. final candidates = response['candidates'] as List<dynamic>?;
  106. if (candidates == null || candidates.isEmpty) {
  107. // Si il n'y a aucun candidat, c'est que le prompt a été bloqué AVANT la génération.
  108. final promptFeedback = response['promptFeedback'];
  109. if (promptFeedback != null && promptFeedback['blockReason'] != null) {
  110. throw Exception("Le prompt a été bloqué par Gemini pour la raison : ${promptFeedback['blockReason']}.");
  111. }
  112. throw Exception('Réponse invalide de Gemini : aucun "candidate" trouvé.');
  113. }
  114. final candidate = candidates.first;
  115. // 1. Vérifier la raison de la fin AVANT de chercher le contenu.
  116. final finishReason = candidate['finishReason'] as String?;
  117. if (finishReason != null && finishReason != 'STOP') {
  118. if (finishReason == 'SAFETY') {
  119. 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é.");
  120. }
  121. throw Exception("La génération s'est terminée prématurément. Raison : $finishReason");
  122. }
  123. // 2. Si tout va bien, on peut maintenant chercher le contenu.
  124. final content = candidate['content'] as Map<String, dynamic>?;
  125. if (content == null) {
  126. throw Exception('Pas de "content" dans la première candidate, malgré un "finishReason" correct. Réponse inattendue.');
  127. }
  128. final parts = content['parts'] as List<dynamic>?;
  129. if (parts == null || parts.isEmpty) {
  130. throw Exception('Pas de "parts" dans le content');
  131. }
  132. for (final part in parts) {
  133. final inlineData = part['inlineData'] as Map<String, dynamic>?;
  134. if (inlineData != null && inlineData.containsKey('data')) {
  135. return inlineData['data'] as String;
  136. }
  137. }
  138. throw Exception("Pas d'image (inlineData) dans les parts de la réponse.");
  139. } catch (e) {
  140. // Afficher la réponse brute aide toujours à déboguer
  141. print("--- Réponse brute de Gemini lors de l'erreur d'extraction ---");
  142. print(jsonEncode(response));
  143. print('------------------------------------------------------------');
  144. // Renvoie l'erreur spécifique que nous avons construite.
  145. throw Exception('Erreur d\'extraction de l\'image: $e');
  146. }
  147. }
  148. @override
  149. Future<String> generatePrompt(String base64Image) async {
  150. print("[GeminiService] 🚀 Lancement de l'analyse d'image pour le prompt...");
  151. final requestBody = {
  152. 'contents': [
  153. {
  154. 'parts': [
  155. {
  156. 'text':
  157. '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.'
  158. },
  159. {
  160. 'inlineData': {
  161. 'mimeType': 'image/jpeg',
  162. 'data': base64Image,
  163. }
  164. }
  165. ]
  166. }
  167. ]
  168. };
  169. try {
  170. final response = await http
  171. .post(
  172. Uri.parse('$_apiUrl?key=$_apiKey'),
  173. headers: {'Content-Type': 'application/json'},
  174. body: jsonEncode(requestBody),
  175. )
  176. .timeout(const Duration(minutes: 3));
  177. if (response.statusCode == 200) {
  178. final responseData = jsonDecode(response.body);
  179. return _extractTextFromResponse(responseData);
  180. } else {
  181. throw Exception('Erreur Gemini ${response.statusCode}: ${response.body}');
  182. }
  183. } catch (e) {
  184. throw Exception('Erreur génération prompt Gemini: $e');
  185. }
  186. }
  187. String _extractTextFromResponse(Map<String, dynamic> response) {
  188. try {
  189. final candidates = response['candidates'] as List<dynamic>?;
  190. if (candidates == null || candidates.isEmpty) {
  191. return '';
  192. }
  193. final content = candidates[0]['content'] as Map<String, dynamic>?;
  194. if (content == null) {
  195. return '';
  196. }
  197. final parts = content['parts'] as List<dynamic>?;
  198. if (parts == null || parts.isEmpty) {
  199. return '';
  200. }
  201. final buffer = StringBuffer();
  202. for (final part in parts) {
  203. final text = part['text'] as String?;
  204. if (text != null && text.isNotEmpty) {
  205. buffer.writeln(text.trim());
  206. }
  207. }
  208. return buffer.toString().trim();
  209. } catch (e) {
  210. print('Erreur extraction texte: $e');
  211. return '';
  212. }
  213. }
  214. }