|
- // 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 {
-
- GeminiService({String? apiKey}) : _apiKey = apiKey ?? 'AIzaSyBQpeyl-Qi8QoCfwmJoxAumYYI5-nwit4Q';
- 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'; // Remplacez par votre clé
-
- /// EditImage qui retourne un Stream, comme attendu par l'interface.
- @override
- Stream<String> editImage(String base64Image,
- String prompt,
- int width,
- int height,
- {int numberOfImages = 1}) {
- // 1. On crée un StreamController
- final controller = StreamController<String>();
-
- // 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<void> _generateAndStreamImage(StreamController<String> 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 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) => '''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<String, dynamic> response) {
- try {
- final candidates = response['candidates'] as List<dynamic>?;
- 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<String, dynamic>?;
- 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<dynamic>?;
- if (parts == null || parts.isEmpty) {
- throw Exception('Pas de "parts" dans le content');
- }
-
- for (final part in parts) {
- final inlineData = part['inlineData'] as Map<String, dynamic>?;
- 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<String> 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<String, dynamic> response) {
- try {
- final candidates = response['candidates'] as List<dynamic>?;
- if (candidates == null || candidates.isEmpty) {
- return '';
- }
- final content = candidates[0]['content'] as Map<String, dynamic>?;
- if (content == null) {
- return '';
- }
- final parts = content['parts'] as List<dynamic>?;
- 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 '';
- }
- }
- }
|