Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

251 linhas
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. final String _apiKey;
  7. // L'URL que vous utilisiez et qui fonctionnait
  8. final String _apiUrl =
  9. 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent';
  10. GeminiService({String? apiKey}) : _apiKey = apiKey ?? 'AIzaSyBQpeyl-Qi8QoCfwmJoxAumYYI5-nwit4Q'; // 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 String 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) {
  85. return '''You are a professional photo editor. Based on the provided image and instructions below,
  86. generate an edited version that enhances the photo while maintaining naturalness and authenticity.
  87. INSTRUCTIONS:
  88. $userPrompt
  89. EDITING GUIDELINES:
  90. - Preserve the original composition and subject
  91. - Maintain realistic skin textures (no plastic look)
  92. - Enhance lighting naturally with warm, soft tones
  93. - Improve colors while keeping them true-to-life
  94. - Add subtle details and clarity without over-processing
  95. - Keep the image aspect ratio and dimensions
  96. - Ensure the final result looks professional and Instagram-ready
  97. CONSTRAINTS:
  98. - Do NOT add watermarks or text
  99. - Do NOT change the main subject dramatically
  100. - Do NOT apply artificial filters or effects
  101. - Keep editing subtle and professional
  102. Generate the edited image following these guidelines.''';
  103. }
  104. // --- MÉTHODE D'EXTRACTION CORRIGÉE ---
  105. String _extractImageFromResponse(Map<String, dynamic> response) {
  106. try {
  107. final candidates = response['candidates'] as List<dynamic>?;
  108. if (candidates == null || candidates.isEmpty) {
  109. // Si il n'y a aucun candidat, c'est que le prompt a été bloqué AVANT la génération.
  110. final promptFeedback = response['promptFeedback'];
  111. if (promptFeedback != null && promptFeedback['blockReason'] != null) {
  112. throw Exception("Le prompt a été bloqué par Gemini pour la raison : ${promptFeedback['blockReason']}.");
  113. }
  114. throw Exception('Réponse invalide de Gemini : aucun "candidate" trouvé.');
  115. }
  116. final candidate = candidates.first;
  117. // 1. Vérifier la raison de la fin AVANT de chercher le contenu.
  118. final finishReason = candidate['finishReason'] as String?;
  119. if (finishReason != null && finishReason != 'STOP') {
  120. if (finishReason == 'SAFETY') {
  121. 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é.");
  122. }
  123. throw Exception("La génération s'est terminée prématurément. Raison : $finishReason");
  124. }
  125. // 2. Si tout va bien, on peut maintenant chercher le contenu.
  126. final content = candidate['content'] as Map<String, dynamic>?;
  127. if (content == null) {
  128. throw Exception('Pas de "content" dans la première candidate, malgré un "finishReason" correct. Réponse inattendue.');
  129. }
  130. final parts = content['parts'] as List<dynamic>?;
  131. if (parts == null || parts.isEmpty) {
  132. throw Exception('Pas de "parts" dans le content');
  133. }
  134. for (final part in parts) {
  135. final inlineData = part['inlineData'] as Map<String, dynamic>?;
  136. if (inlineData != null && inlineData.containsKey('data')) {
  137. return inlineData['data'] as String;
  138. }
  139. }
  140. throw Exception("Pas d'image (inlineData) dans les parts de la réponse.");
  141. } catch (e) {
  142. // Afficher la réponse brute aide toujours à déboguer
  143. print("--- Réponse brute de Gemini lors de l'erreur d'extraction ---");
  144. print(jsonEncode(response));
  145. print("------------------------------------------------------------");
  146. // Renvoie l'erreur spécifique que nous avons construite.
  147. throw Exception('Erreur d\'extraction de l\'image: $e');
  148. }
  149. }
  150. @override
  151. Future<String> generatePrompt(String base64Image) async {
  152. print("[GeminiService] 🚀 Lancement de l'analyse d'image pour le prompt...");
  153. final requestBody = {
  154. 'contents': [
  155. {
  156. 'parts': [
  157. {
  158. 'text':
  159. '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.'
  160. },
  161. {
  162. 'inlineData': {
  163. 'mimeType': 'image/jpeg',
  164. 'data': base64Image,
  165. }
  166. }
  167. ]
  168. }
  169. ]
  170. };
  171. try {
  172. final response = await http
  173. .post(
  174. Uri.parse('$_apiUrl?key=$_apiKey'),
  175. headers: {'Content-Type': 'application/json'},
  176. body: jsonEncode(requestBody),
  177. )
  178. .timeout(const Duration(minutes: 3));
  179. if (response.statusCode == 200) {
  180. final responseData = jsonDecode(response.body);
  181. return _extractTextFromResponse(responseData);
  182. } else {
  183. throw Exception('Erreur Gemini ${response.statusCode}: ${response.body}');
  184. }
  185. } catch (e) {
  186. throw Exception('Erreur génération prompt Gemini: $e');
  187. }
  188. }
  189. String _extractTextFromResponse(Map<String, dynamic> response) {
  190. try {
  191. final candidates = response['candidates'] as List<dynamic>?;
  192. if (candidates == null || candidates.isEmpty) {
  193. return '';
  194. }
  195. final content = candidates[0]['content'] as Map<String, dynamic>?;
  196. if (content == null) {
  197. return '';
  198. }
  199. final parts = content['parts'] as List<dynamic>?;
  200. if (parts == null || parts.isEmpty) {
  201. return '';
  202. }
  203. final buffer = StringBuffer();
  204. for (final part in parts) {
  205. final text = part['text'] as String?;
  206. if (text != null && text.isNotEmpty) {
  207. buffer.writeln(text.trim());
  208. }
  209. }
  210. return buffer.toString().trim();
  211. } catch (e) {
  212. print('Erreur extraction texte: $e');
  213. return '';
  214. }
  215. }
  216. }