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.

271 line
11KB

  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'dart:typed_data';
  5. import 'package:flutter/material.dart';import 'package:social_content_creator/routes/app_routes.dart';
  6. import 'package:social_content_creator/services/image_editing_service.dart'; // Contrat
  7. import 'package:social_content_creator/services/stable_diffusion_service.dart'; // Moteur 1
  8. import 'package:social_content_creator/services/gemini_service.dart'; // Moteur 2
  9. import 'package:image/image.dart' as img;
  10. // Énumération pour le choix du moteur d'IA
  11. enum ImageEngine { stableDiffusion, gemini }
  12. class AiEnhancementScreen extends StatefulWidget {
  13. final File image;
  14. final String prompt;
  15. const AiEnhancementScreen({
  16. super.key,
  17. required this.image,
  18. required this.prompt,
  19. });
  20. @override
  21. State<AiEnhancementScreen> createState() => _AiEnhancementScreenState();
  22. }
  23. enum GenerationState { idle, generating, done, error }
  24. class _AiEnhancementScreenState extends State<AiEnhancementScreen> {
  25. GenerationState _generationState = GenerationState.idle;
  26. final List<Uint8List> _generatedImagesData = [];
  27. StreamSubscription? _imageStreamSubscription;
  28. // --- NOUVEAUX ÉLÉMENTS ---
  29. // 1. Instancier les deux services dans une map
  30. final Map<ImageEngine, ImageEditingService> _services = {
  31. ImageEngine.stableDiffusion: StableDiffusionService(),
  32. ImageEngine.gemini: GeminiService(),
  33. };
  34. // 2. Garder en mémoire le moteur sélectionné (Stable Diffusion par défaut)
  35. ImageEngine _selectedEngine = ImageEngine.stableDiffusion;
  36. // --- FIN DES NOUVEAUX ÉLÉMENTS ---
  37. Future<void> _generateImageVariations() async {
  38. if (_generationState == GenerationState.generating) return;
  39. setState(() {
  40. _generatedImagesData.clear();
  41. _generationState = GenerationState.generating;
  42. });
  43. try {
  44. // Préparation de l'image (logique inchangée)
  45. final imageBytes = await widget.image.readAsBytes();
  46. final originalImage = img.decodeImage(imageBytes);
  47. if (originalImage == null) throw Exception("Impossible de décoder l'image.");
  48. final resizedImage = img.copyResize(originalImage, width: 1024);
  49. final resizedImageBytes = img.encodeJpg(resizedImage, quality: 90);
  50. final imageBase64 = base64Encode(resizedImageBytes);
  51. await _imageStreamSubscription?.cancel();
  52. // --- UTILISATION DU SERVICE SÉLECTIONNÉ ---
  53. // On récupère le bon service (Stable Diffusion ou Gemini) depuis la map
  54. final activeService = _services[_selectedEngine]!;
  55. _imageStreamSubscription = activeService.editImage(
  56. imageBase64,
  57. widget.prompt,
  58. resizedImage.width,
  59. resizedImage.height,
  60. // On demande 3 images à Stable Diffusion, Gemini gèrera ce paramètre
  61. numberOfImages: _selectedEngine == ImageEngine.stableDiffusion ? 3 : 1,
  62. ).listen(
  63. (receivedBase64Image) {
  64. setState(() => _generatedImagesData.add(base64Decode(receivedBase64Image)));
  65. },
  66. onError: (error) {
  67. if (!mounted) return;
  68. setState(() => _generationState = GenerationState.error);
  69. ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Erreur de génération : $error")));
  70. },
  71. onDone: () {
  72. if (!mounted) return;
  73. setState(() => _generationState = GenerationState.done);
  74. },
  75. );
  76. } catch (e) {
  77. if (!mounted) return;
  78. setState(() => _generationState = GenerationState.error);
  79. ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Erreur : ${e.toString()}")));
  80. }
  81. }
  82. void _navigateToPreview(int index) {
  83. if (index >= _generatedImagesData.length) return;
  84. final selectedImageData = _generatedImagesData[index];
  85. final imageBase64 = base64Encode(selectedImageData);
  86. Navigator.pushNamed(context, AppRoutes.imagePreview, arguments: imageBase64);
  87. }
  88. @override
  89. void dispose() {
  90. _imageStreamSubscription?.cancel();
  91. super.dispose();
  92. }
  93. @override
  94. Widget build(BuildContext context) {
  95. const double bottomButtonHeight = 90.0;
  96. return Scaffold(
  97. appBar: AppBar(title: const Text("3. Choisir une variation")),
  98. body: Stack(
  99. children: [
  100. SingleChildScrollView(
  101. padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, bottomButtonHeight),
  102. child: Column(
  103. crossAxisAlignment: CrossAxisAlignment.start,
  104. children: [
  105. Text("Image Originale", style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
  106. const SizedBox(height: 8),
  107. SizedBox(
  108. height: 300,
  109. width: double.infinity,
  110. child: ClipRRect(
  111. borderRadius: BorderRadius.circular(12.0),
  112. child: Image.file(widget.image, fit: BoxFit.cover),
  113. ),
  114. ),
  115. const SizedBox(height: 16),
  116. // --- AJOUT DU SÉLECTEUR DE MOTEUR D'IA ---
  117. Container(
  118. padding: const EdgeInsets.all(12),
  119. decoration: BoxDecoration(
  120. color: Theme.of(context).colorScheme.primary.withOpacity(0.05),
  121. borderRadius: BorderRadius.circular(12),
  122. border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.2))
  123. ),
  124. child: Column(
  125. crossAxisAlignment: CrossAxisAlignment.stretch,
  126. children: [
  127. Text(
  128. "Moteur d'IA pour la variation",
  129. style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
  130. textAlign: TextAlign.center,
  131. ),
  132. const SizedBox(height: 12),
  133. SegmentedButton<ImageEngine>(
  134. showSelectedIcon: false,
  135. segments: const <ButtonSegment<ImageEngine>>[
  136. ButtonSegment<ImageEngine>(
  137. value: ImageEngine.stableDiffusion,
  138. label: Text('Stable Diffusion'),
  139. icon: Icon(Icons.auto_awesome_outlined),
  140. ),
  141. ButtonSegment<ImageEngine>(
  142. value: ImageEngine.gemini,
  143. label: Text('Gemini'),
  144. icon: Icon(Icons.bubble_chart_outlined),
  145. ),
  146. ],
  147. selected: {_selectedEngine},
  148. onSelectionChanged: (Set<ImageEngine> newSelection) {
  149. // On ne change de moteur que si la génération n'est pas en cours
  150. if (_generationState != GenerationState.generating) {
  151. setState(() {
  152. _selectedEngine = newSelection.first;
  153. });
  154. }
  155. },
  156. ),
  157. const SizedBox(height: 16),
  158. // On garde le prompt de guidage dans le même encart
  159. ExpansionTile(
  160. title: const Text("Voir le prompt de guidage"),
  161. initiallyExpanded: false,
  162. tilePadding: EdgeInsets.zero,
  163. childrenPadding: const EdgeInsets.only(top: 8),
  164. children: [
  165. Text(widget.prompt, style: const TextStyle(fontStyle: FontStyle.italic)),
  166. ],
  167. ),
  168. ],
  169. ),
  170. ),
  171. // --- FIN DE L'AJOUT ---
  172. const SizedBox(height: 24),
  173. Text("Variations (cliquez pour choisir)", style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
  174. const SizedBox(height: 16),
  175. _buildGeneratedVariationsGrid(),
  176. ],
  177. ),
  178. ),
  179. Positioned(
  180. bottom: 0,
  181. left: 0,
  182. right: 0,
  183. child: Container(
  184. color: Theme.of(context).scaffoldBackgroundColor,
  185. padding: const EdgeInsets.all(16.0),
  186. child: FilledButton.icon(
  187. icon: _generationState == GenerationState.generating ? const SizedBox.shrink() : const Icon(Icons.auto_awesome),
  188. label: Text(_generationState == GenerationState.generating ? 'Génération en cours...' : 'Générer à nouveau'),
  189. onPressed: _generationState == GenerationState.generating ? null : _generateImageVariations,
  190. style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
  191. ),
  192. ),
  193. ),
  194. ],
  195. ),
  196. );
  197. }
  198. // Le reste du fichier est inchangé (_buildGeneratedVariationsGrid)
  199. Widget _buildGeneratedVariationsGrid() {
  200. // Si la génération est terminée et qu'il n'y a aucune image (cas d'erreur silencieuse)
  201. if (_generationState == GenerationState.done && _generatedImagesData.isEmpty) {
  202. return Container(
  203. height: 100,
  204. decoration: BoxDecoration(color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade200)),
  205. child: const Center(child: Text("La génération n'a produit aucune image.", textAlign: TextAlign.center)),
  206. );
  207. }
  208. if (_generationState == GenerationState.idle) {
  209. return Container(
  210. height: 100,
  211. decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade400, style: BorderStyle.solid)),
  212. child: const Center(child: Text("Cliquez sur 'Générer' pour commencer.")),
  213. );
  214. }
  215. // On affiche 3 slots, même si Gemini n'en remplit qu'un
  216. int displayCount = 3;
  217. if (_selectedEngine == ImageEngine.gemini && _generationState != GenerationState.generating) {
  218. displayCount = _generatedImagesData.length > 0 ? _generatedImagesData.length : 1;
  219. }
  220. return Row(
  221. mainAxisAlignment: MainAxisAlignment.start,
  222. children: List.generate(displayCount, (index) {
  223. bool hasImage = index < _generatedImagesData.length;
  224. return Expanded(
  225. child: Padding(
  226. padding: const EdgeInsets.symmetric(horizontal: 4.0),
  227. child: AspectRatio(
  228. aspectRatio: 1.0,
  229. child: GestureDetector(
  230. onTap: hasImage ? () => _navigateToPreview(index) : null,
  231. child: hasImage
  232. ? ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.memory(_generatedImagesData[index], fit: BoxFit.cover))
  233. : Container(
  234. decoration: BoxDecoration(color: Colors.grey.shade300, borderRadius: BorderRadius.circular(8)),
  235. child: _generationState == GenerationState.generating ? const Center(child: CircularProgressIndicator()) : const SizedBox(),
  236. ),
  237. ),
  238. ),
  239. ),
  240. );
  241. }),
  242. );
  243. }
  244. }