Explorar el Código

initial commit with error on post_creation_service.dart on parsing json

master
Yann Pugliese hace 3 semanas
padre
commit
15b0cc6bfb
Se han modificado 30 ficheros con 937 adiciones y 702 borrados
  1. +2
    -6
      lib/core/theme/app_theme.dart
  2. +13
    -16
      lib/data/models/content_post.dart
  3. +15
    -20
      lib/data/models/user_profile.dart
  4. +5
    -5
      lib/data/services/api_service.dart
  5. +13
    -13
      lib/data/services/auth_service.dart
  6. +8
    -9
      lib/data/services/image_service.dart
  7. +19
    -0
      lib/domain/app_filter.dart
  8. +70
    -0
      lib/domain/catalogs/filter_catalog.dart
  9. +38
    -49
      lib/main.dart
  10. +195
    -183
      lib/presentation/screens/ai_enhancement/ai_enhancement_screen.dart
  11. +1
    -3
      lib/presentation/screens/export/export_screen.dart
  12. +7
    -9
      lib/presentation/screens/home/home_screen.dart
  13. +5
    -7
      lib/presentation/screens/home/login_screen.dart
  14. +4
    -4
      lib/presentation/screens/image_preview/image_preview_screen.dart
  15. +33
    -21
      lib/presentation/screens/media_picker/media_picker_screen.dart
  16. +11
    -15
      lib/presentation/screens/post_preview/post_preview_screen.dart
  17. +13
    -14
      lib/presentation/screens/post_refinement/post_refinement_screen.dart
  18. +11
    -22
      lib/presentation/screens/profile_setup/profile_setup_screen.dart
  19. +35
    -38
      lib/presentation/screens/text_generation/text_generation_screen.dart
  20. +16
    -24
      lib/presentation/widgets/creation_flow_layout.dart
  21. +47
    -55
      lib/repositories/ai_repository.dart
  22. +9
    -11
      lib/services/gemini_service.dart
  23. +47
    -21
      lib/services/image_analysis_service.dart
  24. +62
    -25
      lib/services/ollama_service.dart
  25. +53
    -17
      lib/services/post_creation_service.dart
  26. +105
    -100
      lib/services/stable_diffusion_service.dart
  27. +2
    -2
      lib/services/text_improvment_service.dart
  28. +84
    -12
      pubspec.lock
  29. +13
    -0
      pubspec.yaml
  30. +1
    -1
      test/widget_test.dart

+ 2
- 6
lib/core/theme/app_theme.dart Ver fichero

@@ -3,8 +3,7 @@ import 'package:google_fonts/google_fonts.dart';
import 'colors.dart';

class AppTheme {
static ThemeData get lightTheme {
return ThemeData(
static ThemeData get lightTheme => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.primary,
@@ -86,10 +85,8 @@ class AppTheme {
),
),
);
}

static ThemeData get darkTheme {
return ThemeData(
static ThemeData get darkTheme => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.primary,
@@ -97,5 +94,4 @@ class AppTheme {
),
scaffoldBackgroundColor: const Color(0xFF121212),
);
}
}

+ 13
- 16
lib/data/models/content_post.dart Ver fichero

@@ -25,27 +25,26 @@ enum SocialPlatform {

/// Modèle pour un post de contenu (Dart 3.10)
final class ContentPost {
/// Modèle pour un post de contenu (Dart 3.10)final class ContentPost {
final File? originalMedia;
final File? enhancedMedia;
final MediaType mediaType;
final String? generatedText;
final bool includeEmojis;
final bool includeCommercialInfo;
final Set<SocialPlatform> selectedPlatforms;
final DateTime createdAt;

// CORRECTION 1 : Le mot-clé 'const' est retiré du constructeur.
ContentPost({
this.originalMedia,
required this.mediaType, this.originalMedia,
this.enhancedMedia,
required this.mediaType,
this.generatedText,
this.includeEmojis = true,
this.includeCommercialInfo = false,
this.selectedPlatforms = const {},
DateTime? createdAt,
}) : createdAt = createdAt ?? DateTime.now(); // Cette ligne est maintenant valide.
}) : createdAt = createdAt ?? DateTime.now();
/// Modèle pour un post de contenu (Dart 3.10)final class ContentPost {
final File? originalMedia;
final File? enhancedMedia;
final MediaType mediaType;
final String? generatedText;
final bool includeEmojis;
final bool includeCommercialInfo;
final Set<SocialPlatform> selectedPlatforms;
final DateTime createdAt; // Cette ligne est maintenant valide.

/// Utiliser records pour retourner plusieurs valeurs
(File?, String?) getMediaAndText() => (enhancedMedia ?? originalMedia, generatedText);
@@ -60,8 +59,7 @@ final class ContentPost {
Set<SocialPlatform>? selectedPlatforms,
// Note : On n'ajoute pas 'createdAt' dans les paramètres ici,
// car on veut généralement conserver la date de création originale lors d'une copie.
}) {
return ContentPost(
}) => ContentPost(
originalMedia: originalMedia ?? this.originalMedia,
enhancedMedia: enhancedMedia ?? this.enhancedMedia,
mediaType: mediaType ?? this.mediaType,
@@ -70,9 +68,8 @@ final class ContentPost {
includeCommercialInfo: includeCommercialInfo ?? this.includeCommercialInfo,
selectedPlatforms: selectedPlatforms ?? this.selectedPlatforms,
// CORRECTION 2 : On passe explicitement la date de création de l'objet actuel ('this').
createdAt: this.createdAt,
createdAt: createdAt,
);
}

@override
String toString() =>

+ 15
- 20
lib/data/models/user_profile.dart Ver fichero

@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';

/// Enum pour les tons de message (Dart 3.10 with enhanced members)
enum MessageTone {
@@ -25,9 +24,6 @@ enum TextStyleEnum {

/// Modèle de profil utilisateur (Dart 3.10)
final class UserProfile {
final String profession;
final MessageTone tone;
final TextStyleEnum textStyle;

const UserProfile({
required this.profession,
@@ -35,18 +31,7 @@ final class UserProfile {
required this.textStyle,
});

/// Records pour extraction facile des données
(String, MessageTone, TextStyleEnum) toRecord() =>
(profession, tone, textStyle);

Map<String, dynamic> toJson() => <String, dynamic>{
'profession': profession,
'tone': tone.name,
'textStyle': textStyle.name,
};

factory UserProfile.fromJson(Map<String, dynamic> json) {
return UserProfile(
factory UserProfile.fromJson(Map<String, dynamic> json) => UserProfile(
profession: json['profession'] as String? ?? '',
tone: MessageTone.values.firstWhere(
(e) => e.name == json['tone'],
@@ -57,19 +42,29 @@ final class UserProfile {
orElse: () => TextStyleEnum.classic,
),
);
}
final String profession;
final MessageTone tone;
final TextStyleEnum textStyle;

/// Records pour extraction facile des données
(String, MessageTone, TextStyleEnum) toRecord() =>
(profession, tone, textStyle);

Map<String, dynamic> toJson() => <String, dynamic>{
'profession': profession,
'tone': tone.name,
'textStyle': textStyle.name,
};

UserProfile copyWith({
String? profession,
MessageTone? tone,
TextStyleEnum? textStyle,
}) {
return UserProfile(
}) => UserProfile(
profession: profession ?? this.profession,
tone: tone ?? this.tone,
textStyle: textStyle ?? this.textStyle,
);
}

@override
String toString() =>

+ 5
- 5
lib/data/services/api_service.dart Ver fichero

@@ -6,11 +6,6 @@ import '../models/content_post.dart';

/// Service API pour l'amélioration IA et la génération de texte
class ApiService {
late final Dio _dio;

static const String baseUrl = 'https://api.your-ai-provider.com';
static const String aiEnhancementEndpoint = '/api/v1/enhance-image';
static const String textGenerationEndpoint = '/api/v1/generate-text';

ApiService() {
_dio = Dio(
@@ -31,6 +26,11 @@ class ApiService {
),
);
}
late final Dio _dio;

static const String baseUrl = 'https://api.your-ai-provider.com';
static const String aiEnhancementEndpoint = '/api/v1/enhance-image';
static const String textGenerationEndpoint = '/api/v1/generate-text';

/// Améliorer un média via l'API IA
Future<EnhancementResult> enhanceMedia(

+ 13
- 13
lib/data/services/auth_service.dart Ver fichero

@@ -4,11 +4,11 @@ import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';

// Un modèle simple pour stocker les informations de l'utilisateur connecté
class UserProfile {

UserProfile({required this.name, required this.email, this.pictureUrl});
final String name;
final String email;
final String? pictureUrl;

UserProfile({required this.name, required this.email, this.pictureUrl});
}

class AuthService {
@@ -22,20 +22,20 @@ class AuthService {
/// Tente de se connecter avec Facebook
Future<bool> loginWithFacebook() async {
try {
print("[AuthService] Tentative de connexion avec Facebook...");
print('[AuthService] Tentative de connexion avec Facebook...');

// Lance la popup de connexion Facebook
final LoginResult result = await FacebookAuth.instance.login(
final result = await FacebookAuth.instance.login(
permissions: ['public_profile', 'email'], // Demande les permissions
);

// Vérifie si la connexion a réussi
if (result.status == LoginStatus.success) {
print("[AuthService] Connexion Facebook réussie.");
print('[AuthService] Connexion Facebook réussie.');

// Récupère les données de l'utilisateur depuis l'API Graph de Facebook
final userData = await FacebookAuth.instance.getUserData(
fields: "name,email,picture.width(200)", // Champs demandés
fields: 'name,email,picture.width(200)', // Champs demandés
);

// Crée notre objet UserProfile
@@ -45,15 +45,15 @@ class AuthService {
pictureUrl: userData['picture']?['data']?['url'],
);

print("[AuthService] Utilisateur connecté : ${_userProfile!.name}");
print('[AuthService] Utilisateur connecté : ${_userProfile!.name}');
return true;
} else {
print("[AuthService] Échec de la connexion : ${result.status}");
print("[AuthService] Message : ${result.message}");
print('[AuthService] Échec de la connexion : ${result.status}');
print('[AuthService] Message : ${result.message}');
return false;
}
} catch (e, s) {
print("[AuthService] ❌ Erreur lors de la connexion Facebook: $e");
print('[AuthService] ❌ Erreur lors de la connexion Facebook: $e');
print(s);
return false;
}
@@ -62,12 +62,12 @@ class AuthService {
/// Déconnecte l'utilisateur
Future<void> logout() async {
try {
print("[AuthService] Déconnexion...");
print('[AuthService] Déconnexion...');
await FacebookAuth.instance.logOut();
_userProfile = null;
print("[AuthService] Utilisateur déconnecté.");
print('[AuthService] Utilisateur déconnecté.');
} catch (e) {
print("[AuthService] ❌ Erreur lors de la déconnexion: $e");
print('[AuthService] ❌ Erreur lors de la déconnexion: $e');
}
}
}

+ 8
- 9
lib/data/services/image_service.dart Ver fichero

@@ -1,8 +1,9 @@
import 'dart:io';

import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';

/// Service pour gérer l'accès aux images et vidéos
class ImageService {
@@ -11,7 +12,7 @@ class ImageService {
/// Sélectionner une image depuis la galerie
Future<File?> pickImageFromGallery() async {
try {
final XFile? image = await _picker.pickImage(
final image = await _picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1920,
maxHeight: 1920,
@@ -27,7 +28,7 @@ class ImageService {
/// Capturer une image avec la caméra
Future<File?> captureImageFromCamera() async {
try {
final XFile? photo = await _picker.pickImage(
final photo = await _picker.pickImage(
source: ImageSource.camera,
maxWidth: 1920,
maxHeight: 1920,
@@ -43,7 +44,7 @@ class ImageService {
/// Sélectionner une vidéo depuis la galerie
Future<File?> pickVideoFromGallery() async {
try {
final XFile? video = await _picker.pickVideo(
final video = await _picker.pickVideo(
source: ImageSource.gallery,
maxDuration: const Duration(minutes: 2),
);
@@ -57,7 +58,7 @@ class ImageService {
/// Capturer une vidéo avec la caméra
Future<File?> captureVideoFromCamera() async {
try {
final XFile? video = await _picker.pickVideo(
final video = await _picker.pickVideo(
source: ImageSource.camera,
maxDuration: const Duration(minutes: 2),
);
@@ -80,15 +81,13 @@ class ImageService {
}

/// Obtenir la taille d'un fichier en MB
double getFileSizeInMB(File file) {
return file.lengthSync() / (1024 * 1024);
}
double getFileSizeInMB(File file) => file.lengthSync() / (1024 * 1024);
}

/// Exception personnalisée pour ImageService
class ImagePickerException implements Exception {
final String message;
ImagePickerException(this.message);
final String message;

@override
String toString() => 'ImagePickerException: $message';

+ 19
- 0
lib/domain/app_filter.dart Ver fichero

@@ -0,0 +1,19 @@
// lib/domain/models/app_filter.dart

// Un identifiant unique pour chaque type de filtre
enum FilterType {
lutFile,
}
class ImageFilter { // Chemin vers le fichier LUT (SEULEMENT pour les filtres LUT)

const ImageFilter({
required this.id,
required this.type,
required this.description,
this.lutAssetPath,
});
final String id; // Nom affiché à l'utilisateur (ex: "Éclat Chaud")
final FilterType type; // L'identifiant du type de filtre
final String description; // La description pour aider Llava à choisir
final String? lutAssetPath;
}

+ 70
- 0
lib/domain/catalogs/filter_catalog.dart Ver fichero

@@ -0,0 +1,70 @@
// lib/domain/catalogs/filter_catalog.dart
import '../app_filter.dart';

/// Le catalogue central de TOUS les filtres disponibles dans l'application.
final List<ImageFilter> availableFilters = [

// --- Vos Filtres LUT (.cube) avec des descriptions améliorées ---
ImageFilter(
id: 'Contraste Chaud',
type: FilterType.lutFile,
lutAssetPath: 'assets/luts/01_Contrast_Warm.cube',
description: "Combine un contraste élevé avec une dominante de couleurs chaudes (oranges, jaunes). Idéal pour les paysages d'automne ou les scènes au coucher du soleil.",
),
ImageFilter(
id: 'Boost de Saturation',
type: FilterType.lutFile,
lutAssetPath: 'assets/luts/02_Saturation_Boost.cube',
description: "Intensifie toutes les couleurs de l'image pour un rendu très vif et éclatant. Parfait pour les scènes de nature, la nourriture ou les festivals.",
),
ImageFilter(
id: 'Teinte Orangée',
type: FilterType.lutFile,
lutAssetPath: 'assets/luts/03_Orange_Tint.cube',
description: "Applique une légère teinte orange sur toute l'image, créant une atmosphère chaude et unifiée, typique de l'heure dorée (golden hour).",
),
ImageFilter(
id: 'Vintage',
type: FilterType.lutFile,
lutAssetPath: 'assets/luts/04_Vintage.cube',
description: 'Donne un aspect rétro avec des couleurs légèrement désaturées, des noirs délavés et une dominante magenta dans les ombres. Idéal pour un look nostalgique.',
),
ImageFilter(
id: 'Bleu Froid',
type: FilterType.lutFile,
lutAssetPath: 'assets/luts/05_Cool_Blue.cube',
description: "Refroidit l'image en ajoutant une dominante de bleu dans les tons moyens et les ombres. Parfait pour les paysages d'hiver, les scènes urbaines ou une ambiance mélancolique.",
),
ImageFilter(
id: 'Boost des Ombres',
type: FilterType.lutFile,
lutAssetPath: 'assets/luts/06_Shadow_Boost.cube',
description: "Éclaircit les zones sombres de l'image pour révéler plus de détails, sans affecter les hautes lumières. Utile pour les photos à contre-jour ou très contrastées.",
),
ImageFilter(
id: 'Lumineux',
type: FilterType.lutFile,
lutAssetPath: 'assets/luts/07_Bright.cube',
description: "Augmente la luminosité globale de l'image pour un rendu clair et aéré. Idéal pour les photos sombres ou pour créer une esthétique minimaliste et propre.",
),
ImageFilter(
id: 'Sépia',
type: FilterType.lutFile,
lutAssetPath: 'assets/luts/08_Sepia.cube',
description: 'Applique une teinte monochrome marron classique pour un look ancien et historique. Parfait pour un style intemporel et élégant.',
),
// Vous aviez 8 filtres, il en manque peut-être 2. Vous pouvez les ajouter ici.
// Exemple:
// ImageFilter(
// id: "Noir & Blanc Dramatique",
// type: FilterType.lutFile,
// lutAssetPath: "assets/luts/09_dramatic_bw.cube",
// description: "Convertit en noir et blanc avec un contraste très élevé pour un effet puissant et graphique.",
// ),
// ImageFilter(
// id: "Teinte Verte Cinéma",
// type: FilterType.lutFile,
// lutAssetPath: "assets/luts/10_matrix_green.cube",
// description: "Donne une dominante verte aux ombres et aux tons froids, rappelant certains films de science-fiction.",
// ),
];

+ 38
- 49
lib/main.dart Ver fichero

@@ -1,21 +1,18 @@
// lib/main.dart
import 'dart:io';
import 'package:flutter/material.dart';

// --- IMPORTS MANQUANTS AJOUTÉS ---
import 'package:social_content_creator/repositories/ai_repository.dart';
// Les autres imports d'écrans sont corrects
import 'package:social_content_creator/presentation/screens/ai_enhancement/ai_enhancement_screen.dart';
import 'package:social_content_creator/presentation/screens/export/export_screen.dart';
import 'package:social_content_creator/presentation/screens/home/login_screen.dart';
import 'package:social_content_creator/presentation/screens/home/home_screen.dart';
import 'package:social_content_creator/presentation/screens/image_preview/image_preview_screen.dart';
import 'package:social_content_creator/presentation/screens/media_picker/media_picker_screen.dart';
import 'package:social_content_creator/presentation/screens/post_preview/post_preview_screen.dart';
import 'package:social_content_creator/presentation/screens/post_refinement/post_refinement_screen.dart';
import 'package:social_content_creator/presentation/screens/profile_setup/profile_setup_screen.dart';
import 'package:social_content_creator/presentation/screens/text_generation/text_generation_screen.dart';
import 'package:social_content_creator/routes/app_routes.dart';
import 'presentation/screens/ai_enhancement/ai_enhancement_screen.dart';
import 'presentation/screens/export/export_screen.dart';
import 'presentation/screens/home/home_screen.dart';
import 'presentation/screens/home/login_screen.dart';
import 'presentation/screens/image_preview/image_preview_screen.dart';
import 'presentation/screens/media_picker/media_picker_screen.dart';
import 'presentation/screens/post_preview/post_preview_screen.dart';
import 'presentation/screens/post_refinement/post_refinement_screen.dart';
import 'presentation/screens/profile_setup/profile_setup_screen.dart';
import 'presentation/screens/text_generation/text_generation_screen.dart';
import 'repositories/ai_repository.dart';
import 'routes/app_routes.dart';

void main() {
runApp(const MyApp());
@@ -27,7 +24,8 @@ class MyApp extends StatelessWidget {

@override
Widget build(BuildContext context) {
final Color seedColor = Colors.blue;
// ... (votre code de thème inchangé)
const Color seedColor = Colors.blue;
return MaterialApp(
title: 'Social Content Creator',
theme: ThemeData(
@@ -47,10 +45,9 @@ class MyApp extends StatelessWidget {
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
initialRoute: _devSkipLogin ? AppRoutes.home : AppRoutes.login,
// --- GESTIONNAIRE DE ROUTES ENTIÈREMENT CORRIGÉ ET COMPLÉTÉ ---
onGenerateRoute: (settings) {
switch (settings.name) {
// --- Routes simples sans arguments ---
// --- Routes simples inchangées ---
case '/':
case AppRoutes.login:
return MaterialPageRoute(builder: (_) => const LoginScreen());
@@ -63,34 +60,29 @@ class MyApp extends StatelessWidget {
case AppRoutes.export:
return MaterialPageRoute(builder: (_) => const ExportScreen());

// --- Routes avec arguments ---
// --- ROUTE AiEnhancement CORRIGÉE ---
case AppRoutes.aiEnhancement:
if (settings.arguments is Map<String, dynamic>) {
final args = settings.arguments as Map<String, dynamic>;
if (args.containsKey('image') &&
args['image'] is File &&
args.containsKey('prompt') &&
args['prompt'] is String) {
return MaterialPageRoute(
builder: (_) => AiEnhancementScreen(
image: args['image']!,
prompt: args['prompt']!,
),
);
}
if (settings.arguments is AiEnhancementScreenArguments) {
final args = settings.arguments! as AiEnhancementScreenArguments;
return MaterialPageRoute(
builder: (_) => AiEnhancementScreen(arguments: args),
);
}
return _errorRoute("Arguments invalides pour AiEnhancementScreen.");
return _errorRoute('Arguments (AiEnhancementScreenArguments) invalides pour AiEnhancementScreen.');

// --- Routes avec arguments (inchangées) ---
case AppRoutes.imagePreview:
// ... (votre code inchangé)
if (settings.arguments is String) {
final imageBase64 = settings.arguments as String;
final imageBase64 = settings.arguments! as String;
return MaterialPageRoute(
builder: (_) => ImagePreviewScreen(imageBase64: imageBase64),
);
}
return _errorRoute("Argument (String) invalide pour ImagePreviewScreen");
return _errorRoute('Argument (String) invalide pour ImagePreviewScreen');

case AppRoutes.textGeneration:
// ... (votre code inchangé)
final args = settings.arguments;
if (args is Map<String, dynamic>) {
final imageBase64 = args['imageBase64'] as String?;
@@ -107,47 +99,44 @@ class MyApp extends StatelessWidget {
);
}
}
// L'erreur était ici : il manquait le cas de fallback
return _errorRoute("Arguments invalides pour TextGenerationScreen.");
return _errorRoute('Arguments invalides pour TextGenerationScreen.');

// --- ROUTE MANQUANTE AJOUTÉE ---
case AppRoutes.postRefinement:
// ... (votre code inchangé)
if (settings.arguments is PostRefinementScreenArguments) {
final args = settings.arguments as PostRefinementScreenArguments;
final args = settings.arguments! as PostRefinementScreenArguments;
return MaterialPageRoute(
builder: (_) => PostRefinementScreen(arguments: args),
);
}
return _errorRoute("Arguments (PostRefinementScreenArguments) invalides pour PostRefinementScreen.");
return _errorRoute('Arguments (PostRefinementScreenArguments) invalides pour PostRefinementScreen.');

// --- ROUTE MANQUANTE AJOUTÉE ---
case AppRoutes.postPreview:
// ... (votre code inchangé)
if (settings.arguments is PostPreviewArguments) {
final args = settings.arguments as PostPreviewArguments;
final args = settings.arguments! as PostPreviewArguments;
return MaterialPageRoute(
builder: (_) => PostPreviewScreen(arguments: args),
);
}
return _errorRoute("Arguments (PostPreviewArguments) invalides pour PostPreviewScreen.");
return _errorRoute('Arguments (PostPreviewArguments) invalides pour PostPreviewScreen.');

default:
return _errorRoute("Route non trouvée: ${settings.name}");
return _errorRoute('Route non trouvée: ${settings.name}');
}
},
);
}

// Méthode helper pour afficher une page d'erreur propre
MaterialPageRoute _errorRoute(String message) {
return MaterialPageRoute(
// ... (votre méthode _errorRoute inchangée)
MaterialPageRoute _errorRoute(String message) => MaterialPageRoute(
builder: (_) => Scaffold(
appBar: AppBar(title: const Text('Erreur de Navigation')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(16),
child: Text(message, textAlign: TextAlign.center),
)),
),
);
}
}

+ 195
- 183
lib/presentation/screens/ai_enhancement/ai_enhancement_screen.dart Ver fichero

@@ -1,87 +1,119 @@
// lib/presentation/screens/ai_enhancement/ai_enhancement_screen.dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';import 'package:social_content_creator/routes/app_routes.dart';
import 'package:social_content_creator/services/image_editing_service.dart'; // Contrat
import 'package:social_content_creator/services/stable_diffusion_service.dart'; // Moteur 1
import 'package:social_content_creator/services/gemini_service.dart'; // Moteur 2
import 'package:image/image.dart' as img;

// Énumération pour le choix du moteur d'IA
enum ImageEngine { stableDiffusion, gemini }
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;

class AiEnhancementScreen extends StatefulWidget {
final File image;
final String prompt;
import '../../../domain/app_filter.dart';
import '../../../domain/catalogs/filter_catalog.dart';
import '../../../repositories/ai_repository.dart';
// --- AJOUT NÉCESSAIRE POUR LA NAVIGATION ---
import '../../../routes/app_routes.dart';

const AiEnhancementScreen({
super.key,
// --- CLASSE D'ARGUMENTS (INCHANGÉE) ---
class AiEnhancementScreenArguments {
AiEnhancementScreenArguments({
required this.image,
required this.prompt,
required this.initialPrompt,
required this.suggestedFilterIds,
required this.aiRepository,
});
final File image;
final String initialPrompt;
final List<String> suggestedFilterIds;
final AiRepository aiRepository;
}

// --- ÉNUMÉRATIONS (INCHANGÉES) ---
enum GenerationState { idle, generating, done, error }
enum ImageGenerationEngine { stableDiffusion, gemini }

class AiEnhancementScreen extends StatefulWidget {
const AiEnhancementScreen({required this.arguments, super.key});
final AiEnhancementScreenArguments arguments;

@override
State<AiEnhancementScreen> createState() => _AiEnhancementScreenState();
}

enum GenerationState { idle, generating, done, error }

class _AiEnhancementScreenState extends State<AiEnhancementScreen> {
// --- GESTION D'ÉTAT ---
GenerationState _generationState = GenerationState.idle;
ImageGenerationEngine? _selectedEngine;
final List<Uint8List> _generatedImagesData = [];
StreamSubscription? _imageStreamSubscription;

// --- NOUVEAUX ÉLÉMENTS ---
// 1. Instancier les deux services dans une map
final Map<ImageEngine, ImageEditingService> _services = {
ImageEngine.stableDiffusion: StableDiffusionService(),
ImageEngine.gemini: GeminiService(),
};
// --- NOUVEL ÉTAT POUR GÉRER LA VISIBILITÉ DU PROMPT ---
bool _isPromptVisible = false;

// 2. Garder en mémoire le moteur sélectionné (Stable Diffusion par défaut)
ImageEngine _selectedEngine = ImageEngine.stableDiffusion;
// --- FIN DES NOUVEAUX ÉLÉMENTS ---
// --- DONNÉES DE L'ÉCRAN ---
late final TextEditingController _promptController;
late final List<ImageFilter> _preselectedFilters;

Future<void> _generateImageVariations() async {
@override
void initState() {
super.initState();
_promptController = TextEditingController(text: widget.arguments.initialPrompt);
_preselectedFilters = availableFilters
.where((filter) => widget.arguments.suggestedFilterIds.contains(filter.id))
.toList();
}

@override
void dispose() {
_imageStreamSubscription?.cancel();
_promptController.dispose();
super.dispose();
}

/// Lance la génération d'images avec le moteur spécifié.
Future<void> _startGeneration(ImageGenerationEngine engine) async {
if (_generationState == GenerationState.generating) return;

setState(() {
_generatedImagesData.clear();
_generationState = GenerationState.generating;
_selectedEngine = engine;
});

try {
// Préparation de l'image (logique inchangée)
final imageBytes = await widget.image.readAsBytes();
final originalImage = img.decodeImage(imageBytes);
if (originalImage == null) throw Exception("Impossible de décoder l'image.");
final imageBytes = await widget.arguments.image.readAsBytes();
final imageBase64 = base64Encode(imageBytes);


final resizedImage = img.copyResize(originalImage, width: 1024);
final resizedImageBytes = img.encodeJpg(resizedImage, quality: 90);
final imageBase64 = base64Encode(resizedImageBytes);
// --- DÉBUT DE LA CORRECTION ---

await _imageStreamSubscription?.cancel();
// 1. Décoder l'image pour obtenir ses dimensions
final originalImage = img.decodeImage(imageBytes);
if (originalImage == null) {
throw Exception("Impossible de lire les dimensions de l'image.");
}

// --- UTILISATION DU SERVICE SÉLECTIONNÉ ---
// On récupère le bon service (Stable Diffusion ou Gemini) depuis la map
final activeService = _services[_selectedEngine]!;
// 2. Récupérer la largeur et la hauteur
final int originalWidth = originalImage.width;
final int originalHeight = originalImage.height;

_imageStreamSubscription = activeService.editImage(
// --- FIN DE LA CORRECTION ---
await _imageStreamSubscription?.cancel();
_imageStreamSubscription = widget.arguments.aiRepository.editImage(
imageBase64,
widget.prompt,
resizedImage.width,
resizedImage.height,
// On demande 3 images à Stable Diffusion, Gemini gèrera ce paramètre
numberOfImages: _selectedEngine == ImageEngine.stableDiffusion ? 3 : 1,
_promptController.text,
originalWidth,
originalHeight,
filtersToApply: _preselectedFilters,
).listen(
(receivedBase64Image) {
if (!mounted) return;
setState(() => _generatedImagesData.add(base64Decode(receivedBase64Image)));
},
onError: (error) {
if (!mounted) return;
setState(() => _generationState = GenerationState.error);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Erreur de génération : $error")));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erreur de génération : $error')));
},
onDone: () {
if (!mounted) return;
@@ -92,10 +124,11 @@ class _AiEnhancementScreenState extends State<AiEnhancementScreen> {
} catch (e) {
if (!mounted) return;
setState(() => _generationState = GenerationState.error);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Erreur : ${e.toString()}")));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erreur : ${e.toString()}')));
}
}

// --- NOUVELLE MÉTHODE POUR NAVIGUER VERS L'APERÇU ---
void _navigateToPreview(int index) {
if (index >= _generatedImagesData.length) return;
final selectedImageData = _generatedImagesData[index];
@@ -103,168 +136,147 @@ class _AiEnhancementScreenState extends State<AiEnhancementScreen> {
Navigator.pushNamed(context, AppRoutes.imagePreview, arguments: imageBase64);
}

@override
void dispose() {
_imageStreamSubscription?.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
const double bottomButtonHeight = 90.0;

return Scaffold(
appBar: AppBar(title: const Text("3. Choisir une variation")),
body: Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, bottomButtonHeight),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Image Originale", style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
SizedBox(
height: 300,
width: double.infinity,
child: ClipRRect(
borderRadius: BorderRadius.circular(12.0),
child: Image.file(widget.image, fit: BoxFit.cover),
),
),
const SizedBox(height: 16),
appBar: AppBar(title: const Text('2. Amélioration IA')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildEngineChoiceSection(),
const SizedBox(height: 24),
// --- SECTION DU PROMPT MISE À JOUR ---
_buildCollapsiblePromptSection(),
const SizedBox(height: 24),
Text('Résultats de la génération', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
// --- GRILLE DES RÉSULTATS MISE À JOUR ---
_buildGeneratedVariationsGrid(),
],
),
),
);
}

// --- AJOUT DU SÉLECTEUR DE MOTEUR D'IA ---
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.2))
Widget _buildEngineChoiceSection() {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Image.file(
widget.arguments.image,
width: 100,
height: 100,
fit: BoxFit.cover,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Choisir le moteur IA', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
FilledButton.icon(
onPressed: _generationState == GenerationState.generating ? null : () => _startGeneration(ImageGenerationEngine.stableDiffusion),
icon: const Icon(Icons.auto_awesome_outlined),
label: const Text('Stable Diffusion'),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Moteur d'IA pour la variation",
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
SegmentedButton<ImageEngine>(
showSelectedIcon: false,
segments: const <ButtonSegment<ImageEngine>>[
ButtonSegment<ImageEngine>(
value: ImageEngine.stableDiffusion,
label: Text('Stable Diffusion'),
icon: Icon(Icons.auto_awesome_outlined),
),
ButtonSegment<ImageEngine>(
value: ImageEngine.gemini,
label: Text('Gemini'),
icon: Icon(Icons.bubble_chart_outlined),
),
],
selected: {_selectedEngine},
onSelectionChanged: (Set<ImageEngine> newSelection) {
// On ne change de moteur que si la génération n'est pas en cours
if (_generationState != GenerationState.generating) {
setState(() {
_selectedEngine = newSelection.first;
});
}
},
),
const SizedBox(height: 16),
// On garde le prompt de guidage dans le même encart
ExpansionTile(
title: const Text("Voir le prompt de guidage"),
initiallyExpanded: false,
tilePadding: EdgeInsets.zero,
childrenPadding: const EdgeInsets.only(top: 8),
children: [
Text(widget.prompt, style: const TextStyle(fontStyle: FontStyle.italic)),
],
),
],
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: _generationState == GenerationState.generating ? null : () => _startGeneration(ImageGenerationEngine.gemini),
icon: const Icon(Icons.bubble_chart_outlined),
label: const Text('Gemini'),
),
),
// --- FIN DE L'AJOUT ---

const SizedBox(height: 24),
Text("Variations (cliquez pour choisir)", style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
_buildGeneratedVariationsGrid(),
],
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
color: Theme.of(context).scaffoldBackgroundColor,
padding: const EdgeInsets.all(16.0),
child: FilledButton.icon(
icon: _generationState == GenerationState.generating ? const SizedBox.shrink() : const Icon(Icons.auto_awesome),
label: Text(_generationState == GenerationState.generating ? 'Génération en cours...' : 'Générer à nouveau'),
onPressed: _generationState == GenerationState.generating ? null : _generateImageVariations,
style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
],
),
),
),
],
],
),
),
);
}

// Le reste du fichier est inchangé (_buildGeneratedVariationsGrid)
/// --- NOUVELLE VERSION DE LA SECTION PROMPT ---
/// Utilise un ExpansionTile pour être masquable/affichable.
Widget _buildCollapsiblePromptSection() {
return ExpansionTile(
title: Text(
'Prompt de Guidage',
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
subtitle: Text(
_isPromptVisible ? 'Modifiez le prompt pour affiner le résultat' : 'Afficher pour modifier',
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey.shade600),
),
onExpansionChanged: (isExpanded) {
setState(() => _isPromptVisible = isExpanded);
},
children: [
Padding(
padding: const EdgeInsets.only(top: 8.0, bottom: 16.0),
child: TextField(
controller: _promptController,
decoration: const InputDecoration(
hintText: 'Décrivez l\'image que vous souhaitez obtenir...',
border: OutlineInputBorder(),
),
maxLines: 4,
minLines: 2,
),
),
],
);
}

/// --- NOUVELLE VERSION DE LA GRILLE DES RÉSULTATS ---
/// Utilise un GestureDetector pour rendre chaque image cliquable.
Widget _buildGeneratedVariationsGrid() {
// Si la génération est terminée et qu'il n'y a aucune image (cas d'erreur silencieuse)
if (_generationState == GenerationState.done && _generatedImagesData.isEmpty) {
if (_generationState == GenerationState.idle) {
return Container(
height: 100,
decoration: BoxDecoration(color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade200)),
child: const Center(child: Text("La génération n'a produit aucune image.", textAlign: TextAlign.center)),
decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(8)),
child: const Center(child: Text("Choisissez un moteur pour commencer.")),
);
}

if (_generationState == GenerationState.idle) {
if (_generationState == GenerationState.generating) {
return const Padding(
padding: EdgeInsets.all(32.0),
child: Center(child: CircularProgressIndicator()),
);
}
if (_generationState == GenerationState.done && _generatedImagesData.isEmpty) {
return Container(
height: 100,
decoration: BoxDecoration(color: Colors.grey.shade200, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade400, style: BorderStyle.solid)),
child: const Center(child: Text("Cliquez sur 'Générer' pour commencer.")),
decoration: BoxDecoration(color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade200)),
child: const Center(child: Text("La génération n'a produit aucune image.", textAlign: TextAlign.center)),
);
}

// On affiche 3 slots, même si Gemini n'en remplit qu'un
int displayCount = 3;
if (_selectedEngine == ImageEngine.gemini && _generationState != GenerationState.generating) {
displayCount = _generatedImagesData.length > 0 ? _generatedImagesData.length : 1;
}

return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: List.generate(displayCount, (index) {
bool hasImage = index < _generatedImagesData.length;
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: AspectRatio(
aspectRatio: 1.0,
child: GestureDetector(
onTap: hasImage ? () => _navigateToPreview(index) : null,
child: hasImage
? ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.memory(_generatedImagesData[index], fit: BoxFit.cover))
: Container(
decoration: BoxDecoration(color: Colors.grey.shade300, borderRadius: BorderRadius.circular(8)),
child: _generationState == GenerationState.generating ? const Center(child: CircularProgressIndicator()) : const SizedBox(),
),
),
),
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: _generatedImagesData.length,
itemBuilder: (context, index) {
// ON ENTOURE L'IMAGE D'UN GESTUREDETECTOR
return GestureDetector(
onTap: () => _navigateToPreview(index),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.memory(_generatedImagesData[index], fit: BoxFit.cover),
),
);
}),
},
);
}
}

+ 1
- 3
lib/presentation/screens/export/export_screen.dart Ver fichero

@@ -4,8 +4,7 @@ final class ExportScreen extends StatelessWidget {
const ExportScreen({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Partager')),
body: Center(
child: Column(
@@ -25,5 +24,4 @@ final class ExportScreen extends StatelessWidget {
),
),
);
}
}

+ 7
- 9
lib/presentation/screens/home/home_screen.dart Ver fichero

@@ -1,26 +1,25 @@
import 'package:flutter/material.dart';
import 'package:social_content_creator/routes/app_routes.dart';
import '../../../routes/app_routes.dart';

class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,

title: const Text("Accueil"),
title: const Text('Accueil'),
// Optionnel : Ajoutez un bouton de déconnexion
actions: [
IconButton(
icon: const Icon(Icons.logout),
tooltip: "Déconnexion",
tooltip: 'Déconnexion',
onPressed: () {
// Navigue vers l'écran de login et supprime toutes les routes précédentes
Navigator.of(context).pushNamedAndRemoveUntil(
AppRoutes.login,
(Route<dynamic> route) => false
(route) => false
);
},
)
@@ -31,13 +30,13 @@ class HomeScreen extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Vous êtes connecté !",
'Vous êtes connecté !',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 40),
ElevatedButton.icon(
icon: const Icon(Icons.add_photo_alternate_outlined),
label: const Text("Commencer une nouvelle publication"),
label: const Text('Commencer une nouvelle publication'),
onPressed: () {
// Lance le parcours de création de post existant
Navigator.of(context).pushNamed(AppRoutes.mediaPicker);
@@ -50,5 +49,4 @@ class HomeScreen extends StatelessWidget {
),
),
);
}
}

+ 5
- 7
lib/presentation/screens/home/login_screen.dart Ver fichero

@@ -17,7 +17,7 @@ class _LoginScreenState extends State<LoginScreen> {
_isLoading = true;
});

final bool success = await _authService.loginWithFacebook();
final success = await _authService.loginWithFacebook();

setState(() {
_isLoading = false;
@@ -30,21 +30,20 @@ class _LoginScreenState extends State<LoginScreen> {
} else if (mounted) {
// Afficher un message d'erreur
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("La connexion a échoué. Veuillez réessayer.")),
const SnackBar(content: Text('La connexion a échoué. Veuillez réessayer.')),
);
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Connexion")),
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Connexion')),
body: Center(
child: _isLoading
? const CircularProgressIndicator()
: ElevatedButton.icon(
icon: const Icon(Icons.facebook),
label: const Text("Se connecter avec Facebook"),
label: const Text('Se connecter avec Facebook'),
onPressed: _handleFacebookLogin,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1877F2), // Couleur de Facebook
@@ -54,5 +53,4 @@ class _LoginScreenState extends State<LoginScreen> {
),
),
);
}
}

+ 4
- 4
lib/presentation/screens/image_preview/image_preview_screen.dart Ver fichero

@@ -12,15 +12,15 @@ import '../../../routes/app_routes.dart';
// Cet écran n'a plus besoin de recevoir un service, car l'écran suivant
// instanciera lui-même le AiRepository dont il a besoin.
final class ImagePreviewScreen extends StatelessWidget {
final String imageBase64;

const ImagePreviewScreen({super.key, required this.imageBase64});
const ImagePreviewScreen({required this.imageBase64, super.key});
final String imageBase64;

@override
Widget build(BuildContext context) {
// Le AiRepository est instancié ici, uniquement si on en a besoin pour la navigation.
// Dans ce cas, on le passe à l'écran suivant.
final AiRepository aiRepository = AiRepository();
final aiRepository = AiRepository();

try {
final imageBytes = base64Decode(imageBase64);
@@ -37,7 +37,7 @@ final class ImagePreviewScreen extends StatelessWidget {
child: InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 4.0,
maxScale: 4,
child: Image.memory(
imageBytes,
fit: BoxFit.contain,

+ 33
- 21
lib/presentation/screens/media_picker/media_picker_screen.dart Ver fichero

@@ -1,11 +1,15 @@
// lib/presentation/screens/media_picker/media_picker_screen.dart

import 'dart:convert'; // <<< NÉCESSAIRE POUR BASE64
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:social_content_creator/routes/app_routes.dart';
import 'package:social_content_creator/services/image_analysis_service.dart'; // <<< IMPORT CORRECT

// --- IMPORTS NÉCESSAIRES ---
import '../../../routes/app_routes.dart';
import '../../../repositories/ai_repository.dart';
import '../../../services/image_analysis_service.dart';
import '../ai_enhancement/ai_enhancement_screen.dart';

class MediaPickerScreen extends StatefulWidget {
const MediaPickerScreen({super.key});
@@ -17,12 +21,17 @@ class MediaPickerScreen extends StatefulWidget {
class _MediaPickerScreenState extends State<MediaPickerScreen> {
final _picker = ImagePicker();
XFile? _selectedMedia;

// On instancie la classe CONCRÈTE
final ImageAnalysisService _analysisService = OllamaImageAnalysisService();
bool _isAnalyzing = false;

// --- DÉCLARATION CORRECTE DES DÉPENDANCES ---
// On instancie les dépendances. AiRepository n'a pas besoin d'arguments.
final AiRepository _aiRepository = AiRepository();
// --- FIN DE LA CORRECTION ---

/// Déclenche la sélection d'image depuis la source choisie (galerie ou caméra).
Future<void> _pickImage(ImageSource source) async {
if (_isAnalyzing) return;

try {
final file = await _picker.pickImage(source: source, imageQuality: 85, maxWidth: 1280);
if (file != null) {
@@ -37,7 +46,7 @@ class _MediaPickerScreenState extends State<MediaPickerScreen> {
}
}

/// Fonction qui analyse l'image et navigue vers l'écran suivant.
/// Fonction qui analyse l'image et navigue vers l'écran d'amélioration.
Future<void> _analyzeAndNavigate() async {
if (_selectedMedia == null) return;

@@ -45,26 +54,28 @@ class _MediaPickerScreenState extends State<MediaPickerScreen> {

try {
final imageFile = File(_selectedMedia!.path);

// --- CORRECTION APPLIQUÉE ICI ---
// 1. Convertir l'image en base64, comme attendu par le service.
final imageBytes = await imageFile.readAsBytes();
final String imageBase64 = base64Encode(imageBytes);
final imageBase64 = base64Encode(imageBytes);

// On utilise la méthode d'analyse directement depuis le repository.
final ImageAnalysisResult analysisResult = await _aiRepository.analyzeImage(imageBase64);

// 2. Appeler la bonne méthode (`analyzeImage`) avec le bon argument (`base64Image`).
final String imagePrompt = await _analysisService.analyzeImage(imageBase64);
// --- FIN DE LA CORRECTION ---
final String prompt = analysisResult.prompt;
final List<String> filterIds = analysisResult.filterIds;

if (!mounted) return;

// 3. Navigation avec l'image et le prompt
final screenArguments = AiEnhancementScreenArguments(
image: imageFile,
initialPrompt: prompt,
suggestedFilterIds: filterIds,
aiRepository: _aiRepository,
);

Navigator.pushNamed(
context,
AppRoutes.aiEnhancement,
arguments: <String, dynamic>{
'image': imageFile,
'prompt': imagePrompt,
},
arguments: screenArguments,
);

} catch (e) {
@@ -81,6 +92,7 @@ class _MediaPickerScreenState extends State<MediaPickerScreen> {

@override
Widget build(BuildContext context) {
// Le code du Widget build reste inchangé
return Scaffold(
appBar: AppBar(title: const Text('1. Choisir une Image')),
body: Stack(
@@ -92,7 +104,7 @@ class _MediaPickerScreenState extends State<MediaPickerScreen> {
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -100,7 +112,7 @@ class _MediaPickerScreenState extends State<MediaPickerScreen> {
const Icon(Icons.camera_enhance, size: 80, color: Colors.grey),
const SizedBox(height: 24),
Text(
"Choisissez une image pour commencer",
'Choisissez une image pour commencer',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),

+ 11
- 15
lib/presentation/screens/post_preview/post_preview_screen.dart Ver fichero

@@ -8,35 +8,32 @@ import '../../widgets/creation_flow_layout.dart';
// --- ACTION 1 : CRÉER LA CLASSE D'ARGUMENTS MANQUANTE ---
// Cette classe encapsule toutes les données nécessaires pour cet écran.
class PostPreviewArguments {
final String imageBase64;
final String text;
final AiRepository aiRepository;

PostPreviewArguments({
required this.imageBase64,
required this.text,
required this.aiRepository,
});
final String imageBase64;
final String text;
final AiRepository aiRepository;
}

// --- ACTION 2 : ADAPTER L'ÉCRAN POUR UTILISER LA CLASSE D'ARGUMENTS ---
final class PostPreviewScreen extends StatelessWidget {
// L'écran attend maintenant un seul objet 'arguments'
final PostPreviewArguments arguments;

const PostPreviewScreen({
super.key,
required this.arguments, // Le constructeur est simplifié
const PostPreviewScreen({required this.arguments, // Le constructeur est simplifié, super.key,, super.key,
});
// L'écran attend maintenant un seul objet 'arguments'
final PostPreviewArguments arguments;

@override
Widget build(BuildContext context) {
return CreationFlowLayout(
Widget build(BuildContext context) => CreationFlowLayout(
// Adaptez ce chiffre au nombre total d'étapes de votre flux.
currentStep: 6,
title: "Aperçu & Publication",
title: 'Aperçu & Publication',
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Widget de carte simulant un post de réseau social
@@ -59,7 +56,7 @@ final class PostPreviewScreen extends StatelessWidget {
),
// Le texte final du post
Padding(
padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(16),
child: Text(
// On accède aux données via l'objet 'arguments'
arguments.text,
@@ -69,7 +66,7 @@ final class PostPreviewScreen extends StatelessWidget {
// Barre d'actions (inchangée)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
@@ -127,5 +124,4 @@ final class PostPreviewScreen extends StatelessWidget {
),
),
);
}
}

+ 13
- 14
lib/presentation/screens/post_refinement/post_refinement_screen.dart Ver fichero

@@ -3,7 +3,7 @@
import 'package:flutter/material.dart';
// --- CORRECTION 1 : IMPORTER LE REPOSITORY ---
import '../../../repositories/ai_repository.dart';
import 'package:social_content_creator/routes/app_routes.dart';
import '../../../routes/app_routes.dart';

import '../../widgets/creation_flow_layout.dart';
import '../post_preview/post_preview_screen.dart';
@@ -11,25 +11,24 @@ import '../post_preview/post_preview_screen.dart';

// --- CORRECTION 2 : DÉFINIR UNE CLASSE D'ARGUMENTS PROPRE ---
class PostRefinementScreenArguments {
final String initialText;
final String imageBase64;
final AiRepository aiRepository;

PostRefinementScreenArguments({
required this.initialText,
required this.imageBase64,
required this.aiRepository,
});
final String initialText;
final String imageBase64;
final AiRepository aiRepository;
}

class PostRefinementScreen extends StatefulWidget {
// Le constructeur attend maintenant la classe d'arguments.
final PostRefinementScreenArguments arguments;

const PostRefinementScreen({
super.key,
required this.arguments,
required this.arguments, super.key,
});
// Le constructeur attend maintenant la classe d'arguments.
final PostRefinementScreenArguments arguments;

@override
State<PostRefinementScreen> createState() => _PostRefinementScreenState();
@@ -109,7 +108,7 @@ class _PostRefinementScreenState extends State<PostRefinementScreen> {
// Le widget build est INCHANGÉ dans sa structure.
return CreationFlowLayout( // Ajout du layout de flux
currentStep: 5, // C'est la 6ème étape
title: "5. Affinage du texte",
title: '5. Affinage du texte',
child: Scaffold(
appBar: AppBar(
title: const Text('Affiner le post'),
@@ -122,7 +121,7 @@ class _PostRefinementScreenState extends State<PostRefinementScreen> {
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -139,7 +138,7 @@ class _PostRefinementScreenState extends State<PostRefinementScreen> {
const Row(children: [
Expanded(child: Divider()),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text("Améliorer avec l'IA"),
),
Expanded(child: Divider()),
@@ -147,9 +146,9 @@ class _PostRefinementScreenState extends State<PostRefinementScreen> {
const SizedBox(height: 16),
TextField(
controller: _promptController,
decoration: InputDecoration(
hintText: "Ex: ajoute plus de détails, rends-le plus fun...",
border: const OutlineInputBorder(),
decoration: const InputDecoration(
hintText: 'Ex: ajoute plus de détails, rends-le plus fun...',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),

+ 11
- 22
lib/presentation/screens/profile_setup/profile_setup_screen.dart Ver fichero

@@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import '../../../core/theme/colors.dart';
import '../../../data/models/user_profile.dart';
// L'import des routes n'est plus nécessaire ici pour la navigation, mais on le garde pour la propreté.
import 'package:social_content_creator/routes/app_routes.dart';


final class ProfileSetupScreen extends StatefulWidget {
@@ -44,8 +42,7 @@ class _ProfileSetupScreenState extends State<ProfileSetupScreen> {
}

@override
Widget build(BuildContext context) {
return Scaffold(
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Créer votre profil')),
body: SafeArea(
child: Column(
@@ -107,22 +104,20 @@ class _ProfileSetupScreenState extends State<ProfileSetupScreen> {
),
),
);
}
}

// ... Le widget _ProfileCard reste inchangé ...
class _ProfileCard extends StatefulWidget {
final UserProfile profile;
final Function(UserProfile) onUpdate;
final VoidCallback? onRemove;
final int index;

const _ProfileCard({
required this.profile,
required this.onUpdate,
this.onRemove,
required this.index,
required this.index, this.onRemove,
});
final UserProfile profile;
final Function(UserProfile) onUpdate;
final VoidCallback? onRemove;
final int index;

@override
State<_ProfileCard> createState() => _ProfileCardState();
@@ -152,8 +147,7 @@ class _ProfileCardState extends State<_ProfileCard> {
}

@override
Widget build(BuildContext context) {
return Card(
Widget build(BuildContext context) => Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16),
@@ -178,29 +172,24 @@ class _ProfileCardState extends State<_ProfileCard> {
Text('Ton:', style: Theme.of(context).textTheme.bodySmall),
Wrap(
spacing: 8,
children: MessageTone.values.map((t) {
return FilterChip(
children: MessageTone.values.map((t) => FilterChip(
label: Text(t.displayName),
selected: widget.profile.tone == t,
onSelected: (_) => widget.onUpdate(widget.profile.copyWith(tone: t)),
);
}).toList(),
)).toList(),
),
const SizedBox(height: 16),
Text('Style:', style: Theme.of(context).textTheme.bodySmall),
Wrap(
spacing: 8,
children: TextStyleEnum.values.map((s) {
return FilterChip(
children: TextStyleEnum.values.map((s) => FilterChip(
label: Text(s.displayName),
selected: widget.profile.textStyle == s,
onSelected: (_) => widget.onUpdate(widget.profile.copyWith(textStyle: s)),
);
}).toList(),
)).toList(),
),
],
),
),
);
}
}

+ 35
- 38
lib/presentation/screens/text_generation/text_generation_screen.dart Ver fichero

@@ -1,35 +1,32 @@
// lib/presentation/screens/text_generation/text_generation_screen.dart

import 'dart:convert';
import 'package:flutter/material.dart';// --- CORRECTION 1 : IMPORTER LE REPOSITORY ---
import 'package:flutter/material.dart';
import '../../../repositories/ai_repository.dart';
import '../../../routes/app_routes.dart';
import '../../widgets/creation_flow_layout.dart';
import '../post_refinement/post_refinement_screen.dart'; // Import pour la classe d'arguments
import '../post_refinement/post_refinement_screen.dart';

// --- CORRECTION 2 : DÉFINIR UNE CLASSE D'ARGUMENTS PROPRE ---
class TextGenerationScreenArguments {
final String imageBase64;
final AiRepository aiRepository;

TextGenerationScreenArguments({
required this.imageBase64,
required this.aiRepository,
});
final String imageBase64;
final AiRepository aiRepository;
}

final class TextGenerationScreen extends StatefulWidget {
// Le constructeur attend maintenant la classe d'arguments.
final TextGenerationScreenArguments arguments;

const TextGenerationScreen({super.key, required this.arguments});
const TextGenerationScreen({required this.arguments, super.key});
final TextGenerationScreenArguments arguments;

@override
State<TextGenerationScreen> createState() => _TextGenerationScreenState();
}

class _TextGenerationScreenState extends State<TextGenerationScreen> {
// Les variables d'état et contrôleurs restent inchangés
bool _loading = false;
List<String> _generatedIdeas = [];
final List<String> _logs = [];
@@ -40,7 +37,7 @@ class _TextGenerationScreenState extends State<TextGenerationScreen> {
@override
void initState() {
super.initState();
_addLog("🖼️ Image reçue avec succès.");
_addLog('🖼️ Image reçue avec succès.');
}

void _addLog(String log) {
@@ -56,13 +53,12 @@ class _TextGenerationScreenState extends State<TextGenerationScreen> {
super.dispose();
}

// --- CORRECTION 3 : UTILISER LE REPOSITORY POUR LA GÉNÉRATION ---
// --- MÉTHODE CORRIGÉE ET SIMPLIFIÉE ---
Future<void> _handleGenerateIdeas() async {
if (_loading) {
_addLog("⏳ Annulation : Génération déjà en cours.");
_addLog('⏳ Annulation : Génération déjà en cours.');
return;
}

if (!mounted) return;

setState(() {
@@ -77,10 +73,10 @@ class _TextGenerationScreenState extends State<TextGenerationScreen> {
final profession = _professionController.text;
final tone = _toneController.text;

_addLog("⚙️ Paramètres : Métier='${profession}', Ton='${tone}'.");
_addLog("🚀 Appel du AiRepository...");
_addLog("⚙️ Paramètres : Métier='$profession', Ton='$tone'.");
_addLog('🚀 Appel du AiRepository vers Ollama...');

// On appelle la méthode du Repository, qui délègue au bon service.
// 1. On récupère directement la liste de chaînes de caractères.
final ideas = await widget.arguments.aiRepository.generatePostIdeas(
base64Image: widget.arguments.imageBase64,
profession: profession,
@@ -88,21 +84,25 @@ class _TextGenerationScreenState extends State<TextGenerationScreen> {
);

if (!mounted) return;
_addLog("✅ Succès ! Réponse reçue d'Ollama.");

// 2. La logique de découpage ('split') est supprimée car inutile.

_addLog("✅ Succès ! Réponse reçue.");
if (ideas.isEmpty) {
_addLog("⚠️ Avertissement : Le service a retourné une liste vide.");
_addLog('⚠️ Avertissement : Le service a retourné une liste vide.');
} else {
_addLog("📦 ${ideas.length} idée(s) reçue(s).");
_addLog('📦 ${ideas.length} idée(s) reçue(s).');
}

// 3. On met à jour l'état directement avec la liste reçue.
setState(() {
_generatedIdeas = ideas;
});
_addLog("✨ Interface mise à jour.");
_addLog('✨ Interface mise à jour avec la liste des idées.');

} catch (e) {
if (!mounted) return;
_addLog("❌ ERREUR : ${e.toString()}");
_addLog('❌ ERREUR : ${e.toString()}');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Erreur: ${e.toString()}'),
backgroundColor: Colors.red));
@@ -110,11 +110,10 @@ class _TextGenerationScreenState extends State<TextGenerationScreen> {
if (mounted) {
setState(() => _loading = false);
}
_addLog("⏹️ Fin du processus.");
_addLog('⏹️ Fin du processus.');
}
}

// --- CORRECTION 4 : NAVIGATION PROPRE VERS L'ÉCRAN D'AFFINAGE ---
void _navigateToRefinementScreen(String selectedIdea) {
Navigator.pushNamed(
context,
@@ -122,21 +121,21 @@ class _TextGenerationScreenState extends State<TextGenerationScreen> {
arguments: PostRefinementScreenArguments(
initialText: selectedIdea,
imageBase64: widget.arguments.imageBase64,
aiRepository: widget.arguments.aiRepository, // On passe le Repository
aiRepository: widget.arguments.aiRepository,
),
);
}

@override
Widget build(BuildContext context) {
// La structure du build reste la même, mais je corrige l'index de l'étape.
// Le reste du fichier build est identique et correct.
return CreationFlowLayout(
currentStep: 4, // C'est la 5ème étape (index 4)
currentStep: 4,
title: '4. Génération de Texte',
child: Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -144,7 +143,7 @@ class _TextGenerationScreenState extends State<TextGenerationScreen> {
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200),
child: ClipRRect(
borderRadius: BorderRadius.circular(12.0),
borderRadius: BorderRadius.circular(12),
child: Image.memory(
base64Decode(widget.arguments.imageBase64),
width: double.infinity,
@@ -187,7 +186,7 @@ class _TextGenerationScreenState extends State<TextGenerationScreen> {
const SizedBox(height: 24),
if (_generatedIdeas.isNotEmpty) ...[
const Divider(height: 32),
Text("Choisissez une idée à affiner",
Text('Choisissez une idée à affiner',
style: Theme.of(context)
.textTheme
.titleMedium
@@ -203,7 +202,7 @@ class _TextGenerationScreenState extends State<TextGenerationScreen> {
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
title: Text(idea,
maxLines: 3,
maxLines: 5, // On peut garder plus de lignes
overflow: TextOverflow.ellipsis),
leading:
CircleAvatar(child: Text('${index + 1}')),
@@ -212,11 +211,10 @@ class _TextGenerationScreenState extends State<TextGenerationScreen> {
},
),
],
const SizedBox(height: 150), // Espace pour la console de logs
const SizedBox(height: 150),
],
),
),
// La console de logs reste inchangée
Positioned(
bottom: 0,
left: 0,
@@ -228,7 +226,7 @@ class _TextGenerationScreenState extends State<TextGenerationScreen> {
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).scaffoldBackgroundColor.withOpacity(0.0),
Theme.of(context).scaffoldBackgroundColor.withOpacity(0),
Theme.of(context).scaffoldBackgroundColor.withOpacity(0.6),
Theme.of(context).scaffoldBackgroundColor,
],
@@ -242,17 +240,16 @@ class _TextGenerationScreenState extends State<TextGenerationScreen> {
itemBuilder: (context, index) {
final log = _logs[index];
return Text(log, style: TextStyle(
// Logique de couleur conditionnelle pour la lisibilité
color: log.contains('❌')
? Theme.of(context).colorScheme.error // Rouge pour les erreurs
? Theme.of(context).colorScheme.error
: (log.contains('✅') || log.contains('📦') || log.contains('✨'))
? Colors.green[600] // Vert pour les succès
? Colors.green[600]
: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.8), // Couleur par défaut
.withOpacity(0.8),
fontSize: 12
),
),
);

},

+ 16
- 24
lib/presentation/widgets/creation_flow_layout.dart Ver fichero

@@ -3,27 +3,25 @@
import 'package:flutter/material.dart';

class CreationProgressIndicator extends StatelessWidget {
final int currentStep;
final int totalSteps;

const CreationProgressIndicator({ super.key,
required this.currentStep,
const CreationProgressIndicator({ required this.currentStep, super.key,
this.totalSteps = 3,
});
final int currentStep;
final int totalSteps;

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
// --- MODIFICATION 1 : On centre la Row principale ---
child: Center(
child: Row(
// On s'assure que la Row ne prend que la place nécessaire
mainAxisSize: MainAxisSize.min,
children: List.generate(totalSteps, (index) {
bool isActive = index == currentStep;
bool isCompleted = index < currentStep;
bool isLast = index == totalSteps - 1;
final isActive = index == currentStep;
final isCompleted = index < currentStep;
final isLast = index == totalSteps - 1;

// --- MODIFICATION 2 : Chaque segment a maintenant une largeur fixe ---
return Row(
@@ -36,7 +34,7 @@ class CreationProgressIndicator extends StatelessWidget {
shape: BoxShape.circle,
color: isActive || isCompleted
? Theme.of(context).primaryColor
: Theme.of(context).colorScheme.surfaceVariant,
: Theme.of(context).colorScheme.surfaceContainerHighest,
),
child: Center(
child: isCompleted
@@ -58,10 +56,10 @@ class CreationProgressIndicator extends StatelessWidget {
// On donne une largeur fixe à la ligne de connexion
width: 60,
height: 2,
margin: const EdgeInsets.symmetric(horizontal: 8.0),
margin: const EdgeInsets.symmetric(horizontal: 8),
color: isCompleted
? Theme.of(context).primaryColor
: Theme.of(context).colorScheme.surfaceVariant,
: Theme.of(context).colorScheme.surfaceContainerHighest,
),
],
);
@@ -69,26 +67,21 @@ class CreationProgressIndicator extends StatelessWidget {
),
),
);
}
}


// Le reste du fichier CreationFlowLayout reste inchangé
class CreationFlowLayout extends StatelessWidget {
final int currentStep;
final String title;
final Widget child;

const CreationFlowLayout({
super.key,
required this.currentStep,
required this.title,
required this.child,
required this.currentStep, required this.title, required this.child, super.key,
});
final int currentStep;
final String title;
final Widget child;

@override
Widget build(BuildContext context) {
return Scaffold(
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text(title),
elevation: 0,
@@ -104,5 +97,4 @@ class CreationFlowLayout extends StatelessWidget {
],
),
);
}
}

+ 47
- 55
lib/repositories/ai_repository.dart Ver fichero

@@ -1,77 +1,72 @@
// lib/repositories/ai_repository.dart

// 1. IMPORTER LES INTERFACES ET LES IMPLÉMENTATIONS NÉCESSAIRES
// Le fichier ollama_service.dart contient nos nouvelles classes découpées.
import '../services/ollama_service.dart';
import '../services/stable_diffusion_service.dart';
import 'dart:convert'; // <-- IMPORT NÉCESSAIRE POUR jsonDecode
import '../domain/app_filter.dart';
import '../services/gemini_service.dart';
import '../services/image_editing_service.dart';

// Énumération pour choisir le modèle de génération d'image.
enum ImageGenerationModel { stableDiffusion, gemini }
import '../services/stable_diffusion_service.dart';
import '../services/ollama_service.dart'; // <-- VOTRE FICHIER DE SERVICE PRINCIPAL

/// Le AiRepository est le point d'entrée centralisé pour toutes les opérations d'IA.
/// L'interface utilisateur ne parlera qu'à ce Repository.
class AiRepository {
// --- NOS SERVICES SPÉCIALISÉS ---

// Pour l'analyse initiale de l'image ("prompt 1").
// On utilise l'implémentation Ollama, mais on ne dépend que de l'interface.
final ImageAnalysisService _imageAnalyzer = OllamaImageAnalysisService();

// Pour générer les idées de posts.
final PostCreationService _postCreator = OllamaPostCreationService();

// Pour améliorer le texte.
final TextImprovementService _textImprover = OllamaTextImprovementService();
final StableDiffusionService stableDiffusionService = StableDiffusionService();
final GeminiService geminiService = GeminiService();

// --- CORRECTION MAJEURE ICI ---
/// Étape 1: Analyse l'image, parse le JSON et retourne un objet structuré.
Future<ImageAnalysisResult> analyzeImage(String base64Image) async {
print("[AiRepository] Délégation de l'analyse à l'ImageAnalyzer...");

// 1. On récupère la chaîne JSON brute depuis le service.
final String jsonString = await _imageAnalyzer.analyzeImage(base64Image);

// Une collection de services pour la GÉNÉRATION D'IMAGES.
final Map<ImageGenerationModel, ImageEditingService> _imageGenerators;
print("[AiRepository] Parsing de la réponse JSON : $jsonString");

// Le constructeur initialise la collection de générateurs d'images.
AiRepository()
: _imageGenerators = {
ImageGenerationModel.stableDiffusion: StableDiffusionService(),
ImageGenerationModel.gemini: GeminiService(),
};
// 2. On parse cette chaîne JSON pour en faire un objet Map.
final Map<String, dynamic> jsonMap = jsonDecode(jsonString);

// --- LES MÉTHODES PUBLIQUES UTILISÉES PAR L'INTERFACE UTILISATEUR ---
// 3. On extrait les valeurs du Map. Le nom de la clé 'description' vient du prompt que vous avez écrit.
final String prompt = jsonMap['description'] as String? ?? 'No description found.';
final List<String> filterIds = (jsonMap['filters'] as List<dynamic>? ?? []).cast<String>();

/// Étape: `MediaPickerScreen` -> `AiEnhancementScreen`
/// Analyse l'image pour obtenir une description textuelle (le "prompt 1").
/// C'est la tâche qui est lancée "en amont".
Future<String> analyzeImageForPrompt(String base64Image) {
print("[AiRepository] Délégation de l'analyse de l'image à l'ImageAnalyzer...");
return _imageAnalyzer.analyzeImage(base64Image);
// 4. On retourne le Record (l'objet structuré) que l'UI attend.
return (prompt: prompt, filterIds: filterIds);
}
// --- FIN DE LA CORRECTION ---

/// Étape: `AiEnhancementScreen`
/// Génère de nouvelles versions d'une image en utilisant un modèle spécifique.
Stream<String> generateImageVariations({
required ImageGenerationModel model,
required String base64Image,
required String prompt,
required int width,
required int height,
int numberOfImages = 1,
}) {
print("[AiRepository] Délégation de la génération d'images au modèle : $model");
final generator = _imageGenerators[model];
if (generator == null) {
// Retourne un stream avec une erreur si le modèle n'est pas configuré.
return Stream.error(Exception("Le modèle de génération d'image '$model' n'est pas disponible."));
}
return generator.editImage(base64Image, prompt, width, height, numberOfImages: numberOfImages);
// ... (dans la classe AiRepository)


Stream<String> editImage(
String base64Image,
String prompt,
int width,
int height, {
List<ImageFilter> filtersToApply = const [],
}) {
// --- CORRECTION ---
// Puisque stableDiffusionService.editImage retourne déjà le bon type (Stream<String>),
// on le retourne directement.
return stableDiffusionService.editImage(
base64Image,
prompt,
width,
height,
);
// --- FIN DE LA CORRECTION ---
}

/// Étape: `TextGenerationScreen`
/// Génère des idées de posts basées sur l'image et un profil utilisateur.

// ... (le reste de la classe est inchangé)


Future<List<String>> generatePostIdeas({
required String base64Image,
required String profession,
required String tone,
}) {
print("[AiRepository] Délégation de la création de posts au PostCreator...");
return _postCreator.generatePostIdeas(
base64Image: base64Image,
profession: profession,
@@ -79,13 +74,10 @@ class AiRepository {
);
}

/// Étape: `PostRefinementScreen`
/// Améliore un texte existant selon une instruction.
Future<String> improvePostText({
required String originalText,
required String userInstruction,
}) {
print("[AiRepository] Délégation de l'amélioration du texte au TextImprover...");
return _textImprover.improveText(
originalText: originalText,
userInstruction: userInstruction,

+ 9
- 11
lib/services/gemini_service.dart Ver fichero

@@ -4,13 +4,13 @@ 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';

GeminiService({String? apiKey}) : _apiKey = apiKey ?? 'AIzaSyBQpeyl-Qi8QoCfwmJoxAumYYI5-nwit4Q'; // Remplacez par votre clé
'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
@@ -78,9 +78,9 @@ class GeminiService implements ImageEditingService {

if (response.statusCode == 200) {
final responseData = jsonDecode(response.body);
final String singleImage = _extractImageFromResponse(responseData);
final singleImage = _extractImageFromResponse(responseData);

print("[GeminiService] ✅ Image reçue. Envoi dans le stream...");
print('[GeminiService] ✅ Image reçue. Envoi dans le stream...');
controller.add(singleImage);
} else {
final errorBody = jsonDecode(response.body);
@@ -88,18 +88,17 @@ class GeminiService implements ImageEditingService {
throw Exception('Erreur Gemini ${response.statusCode}: $errorMessage');
}
} catch (e, s) {
print("[GeminiService] ❌ Erreur lors de la génération: $e");
print('[GeminiService] ❌ Erreur lors de la génération: $e');
controller.addError(e, s);
} finally {
print("[GeminiService] Stream fermé.");
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) {
return '''You are a professional photo editor. Based on the provided image and instructions below,
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:
@@ -121,7 +120,6 @@ CONSTRAINTS:
- Keep editing subtle and professional

Generate the edited image following these guidelines.''';
}

// --- MÉTHODE D'EXTRACTION CORRIGÉE ---
String _extractImageFromResponse(Map<String, dynamic> response) {
@@ -170,7 +168,7 @@ Generate the edited image following these guidelines.''';
// 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("------------------------------------------------------------");
print('------------------------------------------------------------');
// Renvoie l'erreur spécifique que nous avons construite.
throw Exception('Erreur d\'extraction de l\'image: $e');
}

+ 47
- 21
lib/services/image_analysis_service.dart Ver fichero

@@ -1,25 +1,49 @@
import 'dart:convert';

import 'package:http/http.dart' as http;

/// Définit un contrat pour tout service capable d'analyser une image
/// et de la décrire sous forme de texte.
// --- AJOUTS NÉCESSAIRES ---
import '../domain/catalogs/filter_catalog.dart'; // 1. Importer le catalogue de filtres// --- MISE À JOUR DU CONTRAT (SIGNATURE DE LA MÉTHODE) ---

/// L'objet retourné par l'analyse de l'IA
typedef ImageAnalysisResult = ({String prompt, List<String> filterIds});

/// Définit un contrat pour tout service capable d'analyser une image.
abstract class ImageAnalysisService {
/// Prend une image en base64 et retourne une description textuelle (prompt).
Future<String> analyzeImage(String base64Image);
/// Prend une image en base64 et retourne un prompt ET une liste d'ID de filtres.
Future<ImageAnalysisResult> analyzeImage(String base64Image); // 2. Mettre à jour la signature
}

// --- MISE À JOUR DE L'IMPLÉMENTATION ---

/// L'implémentation Ollama de ce service.
class OllamaImageAnalysisService implements ImageAnalysisService {
final String _apiUrl = 'http://192.168.20.200:11434/api/generate';
final String _visionModel = 'llava:7b';

@override
Future<String> analyzeImage(String base64Image) async {
print(
"[OllamaImageAnalysisService] 🚀 Lancement de l'analyse de l'image...");
Future<ImageAnalysisResult> analyzeImage(String base64Image) async {
print("[OllamaImageAnalysisService] 🚀 Lancement de l'analyse et de la suggestion de filtres...");

// 3. Préparer la liste des filtres pour l'IA
final filterDescriptions = availableFilters.map((filter) {
return "- ID: \"${filter.id}\"\n Description: ${filter.description}";
}).join('\n\n');

// 4. Mettre à jour le prompt système pour demander un JSON
final requestPrompt = '''
As a professional photographe and instagram, describe this imange and give ideas to improve quality to publish on social network.
You are an expert image analyst and social media content director.
Analyze the provided image and perform two tasks:
1. Generate a detailed, high-quality, descriptive prompt in English for an image generation model like Stable Diffusion. The prompt must focus on subject, style, lighting, and composition.
2. Choose the 3 BEST filters to enhance this image from the list below.

Available filters:
$filterDescriptions

You MUST reply ONLY with a valid JSON object in the following format. Do not include any other text, markdown, or explanations.
{
"prompt": "your detailed image prompt here...",
"filters": ["ID_OF_BEST_FILTER_1", "ID_OF_SECOND_BEST_FILTER_2", "ID_OF_THIRD_BEST_FILTER_3"]
}
''';

final requestBody = {
@@ -27,6 +51,8 @@ As a professional photographe and instagram, describe this imange and give ideas
'prompt': requestPrompt,
'images': [base64Image],
'stream': false,
// 5. Demander explicitement un retour en JSON
'format': 'json',
};

try {
@@ -37,21 +63,21 @@ As a professional photographe and instagram, describe this imange and give ideas
).timeout(const Duration(minutes: 2));

if (response.statusCode == 200) {
final body = jsonDecode(response.body);
final generatedPrompt = (body['response'] as String? ?? '')
.trim()
.replaceAll('\n', ' ');
print(
"[OllamaImageAnalysisService] ✅ Analyse terminée : $generatedPrompt");
return generatedPrompt;
// 6. Parser la réponse JSON
final responseBodyString = jsonDecode(response.body)['response'] as String;
final responseJson = jsonDecode(responseBodyString) as Map<String, dynamic>;

final prompt = responseJson['prompt'] as String? ?? 'No prompt generated.';
final filterIds = (responseJson['filters'] as List<dynamic>? ?? []).cast<String>();

print('[OllamaImageAnalysisService] ✅ Analyse terminée. Prompt: $prompt, Filtres: $filterIds');
return (prompt: prompt, filterIds: filterIds);
} else {
throw Exception(
'Erreur Ollama (analyzeImage) ${response.statusCode}: ${response
.body}');
throw Exception('Erreur Ollama (analyzeImage) ${response.statusCode}: ${response.body}');
}
} catch (e) {
print("[OllamaImageAnalysisService] ❌ Exception : ${e.toString()}");
print('[OllamaImageAnalysisService] ❌ Exception : ${e.toString()}');
rethrow;
}
}
}
}

+ 62
- 25
lib/services/ollama_service.dart Ver fichero

@@ -2,15 +2,15 @@

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../domain/catalogs/filter_catalog.dart'; // <-- 1. IMPORTER LE CATALOGUE DE FILTRES

// =========================================================================
// SERVICE 1: ANALYSE D'IMAGE (POUR LE PROMPT INITIAL)
// SERVICE 1: ANALYSE D'IMAGE (POUR PROMPT ET SUGGESTIONS DE FILTRES)
// =========================================================================

/// Définit un contrat pour tout service capable d'analyser une image
/// et de la décrire sous forme de texte.
typedef ImageAnalysisResult = ({String prompt, List<String> filterIds});
/// Définit un contrat pour tout service capable d'analyser une image.
abstract class ImageAnalysisService {
/// Prend une image en base64 et retourne une description textuelle (prompt).
/// Prend une image en base64 et retourne une chaîne JSON contenant la description et les filtres suggérés.
Future<String> analyzeImage(String base64Image);
}

@@ -19,17 +19,38 @@ class OllamaImageAnalysisService implements ImageAnalysisService {
final String _apiUrl = 'http://192.168.20.200:11434/api/generate';
final String _visionModel = 'llava:7b';

// --- MÉTHODE MISE À JOUR ---
@override
Future<String> analyzeImage(String base64Image) async {
print("[OllamaImageAnalysisService] 🚀 Lancement de l'analyse de l'image...");
print("[OllamaImageAnalysisService] 🚀 Lancement de l'analyse pour description ET suggestions de filtres...");

// 2. Préparer un texte décrivant nos filtres pour que Llava puisse choisir
final filtersForPrompt = availableFilters
.map((f) => 'Filtre "${f.id}": ${f.description}')
.join('\n');

// 3. NOUVEAU PROMPT qui demande une sortie JSON avec description et filtres
final requestPrompt = '''
Décris cette image en une phrase courte et factuelle, comme un prompt pour une IA.
Concentre-toi sur le sujet principal, son action et l'environnement.
Sois direct, concis et ne mentionne pas le mot 'image' ou 'photo'.
SYSTEM: Tu es un retoucheur photo expert. Ta mission est double :
1. Décris l'image fournie en une seule phrase courte et factuelle. et donne les axes d'amélioration pour l'IA comme un prompt pour des posts sur Instagram.
2. Analyse l'image et suggère les 2 filtres les plus pertinents depuis la liste ci-dessous.

LISTE DES FILTRES DISPONIBLES :
$filtersForPrompt

Tu dois répondre dans un format JSON strict, sans AUCUN autre texte avant ou après.
Le format doit être :
{
"description": "Ta description de l'image ici ainsi que les axes d'améliorations.",
"filters": ["ID du Filtre 1", "ID du Filtre 2"]
}

USER: Voici l'image.
''';

final requestBody = {
'model': _visionModel,
'format': 'json', // 4. FORCER LA SORTIE EN JSON (très important !)
'prompt': requestPrompt,
'images': [base64Image],
'stream': false,
@@ -44,14 +65,22 @@ Sois direct, concis et ne mentionne pas le mot 'image' ou 'photo'.

if (response.statusCode == 200) {
final body = jsonDecode(response.body);
final generatedPrompt = (body['response'] as String? ?? '').trim().replaceAll('\n', ' ');
print("[OllamaImageAnalysisService] ✅ Analyse terminée : $generatedPrompt");
return generatedPrompt;
// 5. La réponse de l'IA est une chaîne de caractères qui contient notre JSON.
final jsonString = (body['response'] as String? ?? '').trim();
print('[OllamaImageAnalysisService] ✅ Réponse JSON brute reçue : $jsonString');

// On valide que le JSON n'est pas vide avant de le retourner
if (jsonString.isEmpty) {
throw Exception("La réponse d'Ollama est vide.");
}

// On retourne la chaîne JSON complète pour que l'appelant puisse la parser.
return jsonString;
} else {
throw Exception('Erreur Ollama (analyzeImage) ${response.statusCode}: ${response.body}');
}
} catch (e) {
print("[OllamaImageAnalysisService] ❌ Exception : ${e.toString()}");
print('[OllamaImageAnalysisService] ❌ Exception : ${e.toString()}');
rethrow;
}
}
@@ -59,7 +88,7 @@ Sois direct, concis et ne mentionne pas le mot 'image' ou 'photo'.


// =========================================================================
// SERVICE 2: CRÉATION DE POSTS SOCIAUX
// SERVICE 2: CRÉATION DE POSTS SOCIAUX (INCHANGÉ)
// =========================================================================

/// Définit un contrat pour tout service capable de générer des idées de posts.
@@ -82,15 +111,23 @@ class OllamaPostCreationService implements PostCreationService {
required String profession,
required String tone,
}) async {
// ... (VOTRE CODE INCHANGÉ ICI) ...
final requestPrompt = """
You are a social media expert.
Act as a "$profession".
You are a social media expert helping a client.
Client is a professional of "$profession".
Analyze the image.
Generate 3 short and engaging social media post ideas in french with a "$tone" tone.
Génère 3 propositions de texte distinctes pour une publication Instagram, chacune avec un angle marketing différent avec le ton "$tone" :

1. **Angle Éducatif/Informatif :** Rédige un texte qui apporte de la valeur, une astuce, ou une information clé en lien avec l'image. Utilise des emojis pertinents pour structurer le texte. Termine par une question ouverte pour encourager les commentaires. Inclus 3-5 hashtags de niche.

2. **Angle Inspirationnel/Storytelling :** Rédige un texte court qui raconte une histoire ou partage une réflexion personnelle inspirée par l'image. L'objectif est de créer une connexion émotionnelle. Termine par un appel à l'action simple (ex: "Double-tape si tu es d'accord !"). Inclus 3 hashtags plus larges et aspirationnels.

3. **Angle Promotionnel/Commercial :** Rédige un texte qui met en avant un produit, un service ou une offre spéciale lié à l'image. Le ton doit être engageant et persuasif, pas trop vendeur. Met en évidence le bénéfice principal pour le client. Termine par un appel à l'action clair (ex: "Clique sur le lien en bio pour en savoir plus !"). Inclus 3 hashtags liés au produit ou au service.

Your output MUST be a valid JSON array of strings.

Example:
["Idée de post 1...", "Idée de post 2...", "Idée de post 3..."]
["Texte de la proposition 1...", "Texte de la proposition 2...", "Texte de la proposition 3..."]
""";

final requestBody = {
@@ -101,7 +138,7 @@ class OllamaPostCreationService implements PostCreationService {
};

try {
print("[OllamaPostCreationService] 🚀 Appel pour générer des idées...");
print('[OllamaPostCreationService] 🚀 Appel pour générer des idées...');
final response = await http.post(
Uri.parse(_apiUrl),
headers: {'Content-Type': 'application/json'},
@@ -118,14 +155,14 @@ class OllamaPostCreationService implements PostCreationService {
final ideasList = jsonDecode(jsonString) as List;
return ideasList.map((idea) => idea.toString()).toList();
} catch (e) {
print("[OllamaPostCreationService] ❌ Erreur de parsing JSON. Réponse : $jsonString");
print('[OllamaPostCreationService] ❌ Erreur de parsing JSON. Réponse : $jsonString');
return [jsonString]; // Retourne la réponse brute comme une seule idée
}
} else {
throw Exception('Erreur Ollama (generatePostIdeas) ${response.statusCode}: ${response.body}');
}
} catch (e) {
print("[OllamaPostCreationService] ❌ Exception : ${e.toString()}");
print('[OllamaPostCreationService] ❌ Exception : ${e.toString()}');
rethrow;
}
}
@@ -133,7 +170,7 @@ class OllamaPostCreationService implements PostCreationService {


// =========================================================================
// SERVICE 3: AMÉLIORATION DE TEXTE
// SERVICE 3: AMÉLIORATION DE TEXTE (INCHANGÉ)
// =========================================================================

/// Définit un contrat pour tout service capable d'améliorer un texte.
@@ -146,6 +183,7 @@ abstract class TextImprovementService {

/// L'implémentation Ollama de ce service.
class OllamaTextImprovementService implements TextImprovementService {
// ... (VOTRE CODE INCHANGÉ ICI) ...
final String _apiUrl = 'http://192.168.20.200:11434/api/generate';
final String _textModel = 'gpt-oss:20b';

@@ -154,7 +192,7 @@ class OllamaTextImprovementService implements TextImprovementService {
required String originalText,
required String userInstruction,
}) async {
print("[OllamaTextImprovementService] 🚀 Appel pour améliorer le texte...");
print('[OllamaTextImprovementService] 🚀 Appel pour améliorer le texte...');
final requestPrompt = """
You are a social media writing assistant.
A user wants to improve the following text:
@@ -188,9 +226,8 @@ class OllamaTextImprovementService implements TextImprovementService {
throw Exception('Erreur Ollama (improveText) ${response.statusCode}: ${response.body}');
}
} catch (e) {
print("[OllamaTextImprovementService] ❌ Exception : ${e.toString()}");
print('[OllamaTextImprovementService] ❌ Exception : ${e.toString()}');
rethrow;
}
}
}


+ 53
- 17
lib/services/post_creation_service.dart Ver fichero

@@ -15,32 +15,40 @@ class OllamaPostCreationService implements PostCreationService {
final String _apiUrl = 'http://192.168.20.200:11434/api/generate';
final String _visionModel = 'llava:7b';

// --- DÉBUT DE L'UNIQUE ET CORRECTE MÉTHODE ---
@override
@override
Future<List<String>> generatePostIdeas({
required String base64Image,
required String profession,
required String tone,
}) async {
final requestPrompt = """
You are a social media expert.
Act as a "$profession".
Analyze the image.
Generate 3 short and engaging social media post ideas in french with a "$tone" tone.
Your output MUST be a valid JSON array of strings.
final requestPrompt = '''
You are a social media expert.
Act as a "$profession".
Analyze the image.
Generate 3 short and engaging social media post ideas in french with a "$tone" tone.
IMPORTANT:
Your output MUST be a valid JSON array of objects, where each object has a "text" key.
- DO NOT add any markdown like ```json or ```.
- DO NOT add any text before or after the JSON array.
- Ensure all strings inside the JSON are properly escaped (especially newlines \n).

Example:
["", "", ""]
""";
Example of a perfect response:
[{"text": "idée 1"}, {"text": "idée 2"}, {"text": "idée 3"}]
''';

final requestBody = {
'model': _visionModel,
'prompt': requestPrompt,
'images': [base64Image],
'stream': false,
'format': 'json',
};

try {
print("[OllamaPostCreationService] 🚀 Appel pour générer des idées...");
print('[OllamaPostCreationService] 🚀 Appel pour générer des idées...');
final response = await http.post(
Uri.parse(_apiUrl),
headers: {'Content-Type': 'application/json'},
@@ -49,22 +57,50 @@ class OllamaPostCreationService implements PostCreationService {

if (response.statusCode == 200) {
final responseData = jsonDecode(response.body);
final jsonString = (responseData['response'] as String? ?? '').trim();
final rawResponse = (responseData['response'] as String? ?? '').trim();

if (rawResponse.isEmpty) return ["Le modèle n'a retourné aucune réponse."];

if (jsonString.isEmpty) return [];
// La ligne "cleanedResponse" inutile a été supprimée ici.

try {
final ideasList = jsonDecode(jsonString) as List;
return ideasList.map((idea) => idea.toString()).toList();
final startIndex = rawResponse.indexOf('[');
final endIndex = rawResponse.lastIndexOf(']');

if (startIndex != -1 && endIndex > startIndex) {
final jsonArrayString = rawResponse.substring(startIndex, endIndex + 1);

// On tente le parsing
final jsonList = jsonDecode(jsonArrayString) as List;

// Logique de parsing pour une liste d'objets
return jsonList.map((item) {
if (item is Map<String, dynamic> && item.containsKey('text')) {
return item['text'].toString();
}
return 'Format de l\'idée inattendu';
})
.where((text) => text != 'Format de l\'idée inattendu')
.toList();

} else {
// Le modèle a répondu mais sans tableau JSON
throw const FormatException("Aucun tableau JSON '[]' trouvé dans la réponse.");
}
} catch (e) {
print("[OllamaPostCreationService] ❌ Erreur de parsing JSON. Réponse : $jsonString");
return [jsonString]; // Retourne la réponse brute comme une seule idée
// C'EST ICI QUE VOTRE ERREUR SE PRODUIT
print('[OllamaPostCreationService] ❌ Erreur de parsing JSON.');
// AJOUT : Logguer l'erreur spécifique pour savoir POURQUOI le JSON est invalide
print('[OllamaPostCreationService] ❌ Erreur spécifique : ${e.toString()}');
print('[OllamaPostCreationService] ❌ Réponse brute : "$rawResponse"');
return ["Le format de la réponse de l'IA est inattendu."];
}

} else {
throw Exception('Erreur Ollama (generatePostIdeas) ${response.statusCode}: ${response.body}');
}
} catch (e) {
print("[OllamaPostCreationService] ❌ Exception : ${e.toString()}");
print('[OllamaPostCreationService] ❌ Exception : ${e.toString()}');
rethrow;
}
}

+ 105
- 100
lib/services/stable_diffusion_service.dart Ver fichero

@@ -1,129 +1,116 @@
// lib/services/stable_diffusion_service.dart

import 'dart:async';
import 'dart:convert';
import 'dart:async';
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<int> r, List<int> g, List<int> b});
class StableDiffusionService implements ImageEditingService {
final String _apiUrl = 'http://192.168.20.200:7860/sdapi/v1/img2img';

// 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;
@override
Future<String> generatePrompt(String base64Image) async {
throw UnimplementedError('Stable Diffusion ne peut pas générer de prompt.');
}

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()];
@override
Stream<String> editImage(
String base64Image,
String prompt,
int width,
int height, {
int numberOfImages = 3,
}) {
return Stream.fromFuture(() async {

print("[StableDiffusionService] Redimensionnement de l'image d'entrée...");

// 1. Décoder l'image d'entrée.
final originalImageBytes = base64Decode(base64Image);
final originalImage = img.decodeImage(originalImageBytes);

if (originalImage == null) {
throw Exception("Impossible de décoder l'image d'entrée pour le redimensionnement.");
}
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;
// 2. Redimensionner l'image proprement aux dimensions cibles (SANS la rogner).
final resizedImage = img.copyResize(
originalImage,
width: width,
height: height,
interpolation: img.Interpolation.average, // Algorithme de qualité
);

img.adjustColor(image, contrast: 1.2, saturation: 1.15);
// 3. Ré-encoder l'image redimensionnée pour l'envoyer à l'API.
final resizedBase64Image = base64Encode(img.encodeJpg(resizedImage));

return Uint8List.fromList(img.encodeJpg(image, quality: 95));
}
// --- FIN DE LA CORRECTION ---

// 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);
}
print("[StableDiffusionService] Préparation de 3 requêtes parallèles...");

final basePrompt = prompt;
final warmPrompt = "$prompt, warm golden hour lighting, soft golden glow, rich warm tones, amber light";

class StableDiffusionService implements ImageEditingService {
final String _apiUrl = 'http://192.168.20.200:7860/sdapi/v1/img2img';
// On utilise l'image redimensionnée (`resizedBase64Image`) pour toutes les requêtes.
final requests = [
_createImageRequest(resizedBase64Image, basePrompt, width, height),
_createImageRequest(resizedBase64Image, warmPrompt, width, height),
_createImageRequest(resizedBase64Image, warmPrompt, width, height),
];

@override
Future<String> generatePrompt(String base64Image) async {
throw UnimplementedError('Stable Diffusion ne peut pas générer de prompt.');
}
try {
final responses = await Future.wait(requests);

@override
Stream<String> editImage(String base64Image, String prompt, int width, int height, {int numberOfImages = 3}) {
final controller = StreamController<String>();
_generateVariations(controller, base64Image, prompt, width, height);
return controller.stream;
final imagesBase64 = responses.map((response) {
final responseData = jsonDecode(response.body);
return (responseData['images'] as List).first as String;
}).toList();

if (imagesBase64.length < 3) {
throw Exception('Stable Diffusion n\'a pas retourné 3 images.');
}

/// NOUVELLE LOGIQUE DE GÉNÉRATION, PLUS EFFICACE
Future<void> _generateVariations(StreamController<String> 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.
}
print("[StableDiffusionService] Application du filtre de chaleur...");

final warmedImages = await Future.wait([
_applyWarmthFilter(base64Decode(imagesBase64[1])),
_applyWarmthFilter(base64Decode(imagesBase64[2])),
]);

print("[StableDiffusionService] Filtre appliqué. Retour des 3 images.");

return [
imagesBase64[0],
base64Encode(warmedImages[0]),
base64Encode(warmedImages[1]),
];

} catch (e) {
print("Erreur lors des requêtes parallèles à Stable Diffusion: $e");
rethrow;
}
}())
.expand((images) => images);
}

Future<http.Response> _createImageRequest({
required String base64Image,
required String prompt,
required int width,
required int height,
required double denoisingStrength,
required double cfgScale,
}) {
// ... (le reste de la classe, _createImageRequest etc., est inchangé)

/// Méthode privée pour créer une requête HTTP POST pour une seule image.
Future<http.Response> _createImageRequest(String base64Image, String prompt, int width, int height) {
// Le corps de la requête est maintenant simple, sans batch_size
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,
'prompt': prompt, // un seul prompt (string)
'seed': -1, // un seul seed (integer)
'negative_prompt': 'blurry, low quality, artifacts, distorted, oversaturated, plastic skin, over-sharpen, artificial, harsh shadows, filters, watermark, cold lighting, blue tones, washed out, unnatural',
'steps': 30, // Un peu moins de steps car les requêtes sont en parallèle
'cfg_scale': 6.8,
'denoising_strength': 0.25,
'sampler_name': 'DPM++ 2M Karras',
'scheduler': 'karras',
'width': width,
'height': height,
'restore_faces': false,
@@ -133,6 +120,24 @@ class StableDiffusionService implements ImageEditingService {
Uri.parse(_apiUrl),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(requestBody),
).timeout(const Duration(minutes: 5));
).timeout(const Duration(minutes: 10));
}

/// Fonction de post-traitement pour ajouter un filtre chaud à une image.
Future<Uint8List> _applyWarmthFilter(Uint8List imageBytes) async {
return await compute(_processingWarmthFilter, imageBytes);
}
}

// Fonction globale pour le 'compute'
Uint8List _processingWarmthFilter(Uint8List imageBytes) {
final image = img.decodeImage(imageBytes);
if (image == null) return imageBytes;

for (final pixel in image) {
pixel.r = (pixel.r * 1.15).clamp(0, 255).toInt();
pixel.g = (pixel.g * 1.05).clamp(0, 255).toInt();
pixel.b = (pixel.b * 0.90).clamp(0, 255).toInt();
}
return Uint8List.fromList(img.encodeJpg(image, quality: 95));
}

+ 2
- 2
lib/services/text_improvment_service.dart Ver fichero

@@ -19,7 +19,7 @@ class OllamaTextImprovementService implements TextImprovementService {
required String originalText,
required String userInstruction,
}) async {
print("[OllamaTextImprovementService] 🚀 Appel pour améliorer le texte...");
print('[OllamaTextImprovementService] 🚀 Appel pour améliorer le texte...');
final requestPrompt = """
You are a social media writing assistant.
A user wants to improve the following text:
@@ -53,7 +53,7 @@ class OllamaTextImprovementService implements TextImprovementService {
throw Exception('Erreur Ollama (improveText) ${response.statusCode}: ${response.body}');
}
} catch (e) {
print("[OllamaTextImprovementService] ❌ Exception : ${e.toString()}");
print('[OllamaTextImprovementService] ❌ Exception : ${e.toString()}');
rethrow;
}
}

+ 84
- 12
pubspec.lock Ver fichero

@@ -53,10 +53,10 @@ packages:
dependency: transitive
description:
name: build
sha256: dfb67ccc9a78c642193e0c2d94cb9e48c2c818b3178a86097d644acdcde6a8d9
sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "3.1.0"
build_config:
dependency: transitive
description:
@@ -69,18 +69,34 @@ packages:
dependency: transitive
description:
name: build_daemon
sha256: "409002f1adeea601018715d613115cfaf0e31f512cb80ae4534c79867ae2363d"
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
url: "https://pub.dev"
source: hosted
version: "4.1.1"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "3.0.3"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: a9461b8e586bf018dd4afd2e13b49b08c6a844a4b226c8d1d10f3a723cdd78c3
sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30
url: "https://pub.dev"
source: hosted
version: "2.7.1"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b"
url: "https://pub.dev"
source: hosted
version: "2.10.1"
version: "9.3.1"
built_collection:
dependency: transitive
description:
@@ -217,6 +233,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
exif:
dependency: transitive
description:
name: exif
sha256: a7980fdb3b7ffcd0b035e5b8a5e1eef7cadfe90ea6a4e85ebb62f87b96c7a172
url: "https://pub.dev"
source: hosted
version: "3.3.0"
facebook_auth_desktop:
dependency: transitive
description:
@@ -269,10 +293,10 @@ packages:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.6.2"
version: "2.7.0"
file_selector_windows:
dependency: transitive
description:
@@ -326,6 +350,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.1"
flutter_gpu_filters_interface:
dependency: transitive
description:
name: flutter_gpu_filters_interface
sha256: "5729d6f3c5c6034c698100ceab48ef745c7ed1c88d4250861f0fcf7bca347d0e"
url: "https://pub.dev"
source: hosted
version: "0.0.18"
flutter_image_filters:
dependency: "direct main"
description:
name: flutter_image_filters
sha256: "7f7d3da08104737daeff98093cacaa3ade9a4b3d29cb93b0665119ed5cf4c02f"
url: "https://pub.dev"
source: hosted
version: "0.0.32"
flutter_lints:
dependency: "direct dev"
description:
@@ -400,6 +440,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
@@ -428,10 +476,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
version: "1.6.0"
http_multi_server:
dependency: transitive
description:
@@ -460,10 +508,10 @@ packages:
dependency: "direct main"
description:
name: image_picker
sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041"
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.2.1"
image_picker_android:
dependency: transitive
description:
@@ -869,6 +917,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
source_span:
dependency: transitive
description:
@@ -877,6 +933,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqflite:
dependency: transitive
description:
@@ -981,6 +1045,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.6"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
typed_data:
dependency: transitive
description:

+ 13
- 0
pubspec.yaml Ver fichero

@@ -44,6 +44,7 @@ dependencies:
intl: ^0.20.0
uuid: ^4.4.0
flutter_facebook_auth: ^7.1.0
flutter_image_filters: ^0.0.32

dev_dependencies:
flutter_test:
@@ -52,12 +53,24 @@ dev_dependencies:
flutter_lints: ^4.0.0
build_runner: ^2.4.0


flutter:
uses-material-design: true

assets:
- assets/images/
- assets/icons/
- assets/luts/01_Contrast_Warm.cube
- assets/luts/02_Saturation_Boost.cube
- assets/luts/03_Orange_Tint.cube
- assets/luts/04_Vintage.cube
- assets/luts/05_Cool_Blue.cube
- assets/luts/06_Shadow_Boost.cube
- assets/luts/07_Bright.cube
- assets/luts/08_Sepia.cube
- assets/luts/09_Pastel_Contrast.cube
- assets/luts/10_Magenta_Soft.cube


fonts:
- family: Inter

+ 1
- 1
test/widget_test.dart Ver fichero

@@ -11,7 +11,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:social_content_creator/main.dart';

void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
testWidgets('Counter increments smoke test', (tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());


Cargando…
Cancelar
Guardar