// lib/services/stable_diffusion_service.dart import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:image/image.dart' as img; import 'image_editing_service.dart'; // --- Définitions des filtres (isolés pour être utilisés avec compute) --- // Look-up table pour les couleurs typedef ColorLut = ({List r, List g, List b}); // Filtre 1: Applique un filtre chaud via une LUT Uint8List _applyWarmthFilter((Uint8List, ColorLut) params) { final imageBytes = params.$1; final lut = params.$2; final image = img.decodeImage(imageBytes); if (image == null) return imageBytes; for (final pixel in image) { pixel.r = lut.r[pixel.r.toInt()]; pixel.g = lut.g[pixel.g.toInt()]; pixel.b = lut.b[pixel.b.toInt()]; } return Uint8List.fromList(img.encodeJpg(image, quality: 95)); } // Filtre 2: Augmente le contraste et la saturation Uint8List _applyContrastSaturationFilter(Uint8List imageBytes) { final image = img.decodeImage(imageBytes); if (image == null) return imageBytes; img.adjustColor(image, contrast: 1.2, saturation: 1.15); return Uint8List.fromList(img.encodeJpg(image, quality: 95)); } // Calcule la LUT pour le filtre chaud ColorLut _computeWarmthLut() { final rLut = List.generate(256, (i) => (i * 1.15).clamp(0, 255).toInt()); final gLut = List.generate(256, (i) => (i * 1.05).clamp(0, 255).toInt()); final bLut = List.generate(256, (i) => (i * 0.90).clamp(0, 255).toInt()); return (r: rLut, g: gLut, b: bLut); } class StableDiffusionService implements ImageEditingService { final String _apiUrl = 'http://192.168.20.200:7860/sdapi/v1/img2img'; @override Future generatePrompt(String base64Image) async { throw UnimplementedError('Stable Diffusion ne peut pas générer de prompt.'); } @override Stream editImage(String base64Image, String prompt, int width, int height, {int numberOfImages = 3}) { final controller = StreamController(); _generateVariations(controller, base64Image, prompt, width, height); return controller.stream; } /// NOUVELLE LOGIQUE DE GÉNÉRATION, PLUS EFFICACE Future _generateVariations(StreamController controller, String base64Image, String prompt, int width, int height) async { try { // 1. On fait UN SEUL appel à Stable Diffusion pour une variation de base. final response = await _createImageRequest( base64Image: base64Image, prompt: "$prompt, high quality, sharp focus", // Prompt générique de qualité width: width, height: height, denoisingStrength: 0.30, // Assez pour une variation notable cfgScale: 7.0, ); if (response.statusCode != 200) { throw Exception('Erreur Stable Diffusion ${response.statusCode}: ${response.body}'); } final responseData = jsonDecode(response.body); final generatedImageBase64 = (responseData['images'] as List).first as String; // La variation de base est notre première image. controller.add(generatedImageBase64); // 2. On prépare les filtres à appliquer sur cette variation de base. final generatedImageBytes = base64Decode(generatedImageBase64); final warmthLut = _computeWarmthLut(); // 3. On lance les deux filtres en parallèle pour plus de rapidité. final futureWarm = compute(_applyWarmthFilter, (generatedImageBytes, warmthLut)); final futureContrast = compute(_applyContrastSaturationFilter, generatedImageBytes); // 4. On attend les résultats des filtres et on les ajoute au stream. final results = await Future.wait([futureWarm, futureContrast]); for (final filteredBytes in results) { controller.add(base64Encode(filteredBytes)); } } catch (e, stackTrace) { controller.addError(e, stackTrace); } finally { controller.close(); // On ferme le stream quand tout est fini. } } Future _createImageRequest({ required String base64Image, required String prompt, required int width, required int height, required double denoisingStrength, required double cfgScale, }) { final requestBody = { 'init_images': [base64Image], 'prompt': prompt, 'seed': -1, 'negative_prompt': 'blurry, low quality, artifacts, distorted, oversaturated, plastic skin, over-sharpen, artificial, harsh shadows, filters, watermark, cold lighting, blue tones, washed out, unnatural, sepia, text, letters', 'steps': 25, // 25 est souvent suffisant pour img2img 'cfg_scale': cfgScale, 'denoising_strength': denoisingStrength, 'sampler_name': 'DPM++ 2M Karras', 'width': width, 'height': height, 'restore_faces': false, }; return http.post( Uri.parse(_apiUrl), headers: {'Content-Type': 'application/json'}, body: jsonEncode(requestBody), ).timeout(const Duration(minutes: 5)); } }