diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,4 +1,4 @@ -# --- Imports and Initial Config (mostly unchanged) --- +# --- Imports --- import gradio as gr from openai import OpenAI import openai # Import top-level for error types @@ -8,7 +8,6 @@ import requests from PIL import Image import tempfile import io # For BytesIO -# import numpy as np # Not strictly needed if using PIL import markdown # Required by gr.Markdown implicitly import base64 import datetime @@ -19,16 +18,16 @@ from dotenv import load_dotenv # --- Configuration Initiale --- load_dotenv() # Charge les variables depuis un fichier .env s'il existe -# Clé OpenRouter (obligatoire pour le fonctionnement de base) +# Clé OpenRouter openrouter_api_key = os.getenv("OPENROUTER_API_KEY") -OPENROUTER_TEXT_MODEL = "google/gemini-pro-1.5" # Modèle OpenRouter par défaut -# OPENROUTER_TEXT_MODEL = "google/gemini-flash-1.5" # Alternative plus rapide +# Modèle OpenRouter par défaut (Utilise un modèle gratuit si disponible) +OPENROUTER_TEXT_MODEL = os.getenv("OPENROUTER_TEXT_MODEL", "google/gemini-2.5-pro-exp-03-25:free") # Fallback si non défini # Modèles OpenAI (utilisés si clé fournie) OPENAI_TEXT_MODEL = "gpt-4o-mini" # ou "gpt-4o" OPENAI_IMAGE_MODEL = "dall-e-3" -# --- Pydantic Models (Unchanged) --- +# --- Modèles Pydantic (Inchangé) --- class BiasInfo(BaseModel): bias_type: str = Field(..., description="Type de biais identifié (ex: Stéréotype de genre, Biais de confirmation)") explanation: str = Field(..., description="Explication de pourquoi cela pourrait être un biais dans ce contexte.") @@ -38,8 +37,8 @@ class BiasAnalysisResponse(BaseModel): detected_biases: list[BiasInfo] = Field(default_factory=list, description="Liste des biais potentiels détectés.") overall_comment: str = Field(default="", description="Commentaire général ou indication si aucun biais majeur n'est détecté.") -# --- Fonctions Utilitaires (Unchanged, except maybe clean_json if needed later) --- -# ... (update_log, clean_json_response, mappings remain the same) ... +# --- Fonctions Utilitaires --- + # Dictionnaires de correspondance (Inchangés) posture_mapping = {"": "","Debout": "standing up","Assis": "sitting","Allongé": "lying down","Accroupi": "crouching","En mouvement": "moving","Reposé": "resting"} facial_expression_mapping = {"": "","Souriant": "smiling","Sérieux": "serious","Triste": "sad","En colère": "angry","Surpris": "surprised","Pensif": "thoughtful"} @@ -50,31 +49,25 @@ hair_color_mapping = {"": "","Blond": "blonde","Brun": "brown","Noir": "black"," clothing_style_mapping = {"": "","Décontracté": "casual","Professionnel": "professional","Sportif": "sporty"} accessories_mapping = {"": "","Lunettes": "glasses","Montre": "watch","Chapeau": "hat"} -# Fonction de mise à jour du journal (Limitée et formatée) +# Fonction de mise à jour du journal MAX_LOG_LINES = 150 def update_log(event_description, session_log_state): """Ajoute une entrée au log et le retourne, en limitant sa taille.""" timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") new_log_entry = f"[{timestamp}] {event_description}" current_log = session_log_state if session_log_state else "" - - # Limiter la taille du log log_lines = current_log.splitlines() - # Garde les N dernières lignes if len(log_lines) >= MAX_LOG_LINES: current_log = "\n".join(log_lines[-(MAX_LOG_LINES-1):]) - updated_log = current_log + "\n" + new_log_entry if current_log else new_log_entry - return updated_log.strip() # Retourne le log mis à jour + return updated_log.strip() +# Fonction de nettoyage JSON def clean_json_response(raw_response): """Tente d'extraire un bloc JSON valide d'une réponse LLM potentiellement bruitée.""" - # Recherche d'un bloc JSON marqué par ```json ... ``` match = re.search(r"```json\s*({.*?})\s*```", raw_response, re.DOTALL | re.IGNORECASE) if match: return match.group(1) - # Recherche d'un objet JSON commençant par { et finissant par } - # More robust search: find the first '{' and the last '}' start = raw_response.find('{') end = raw_response.rfind('}') if start != -1 and end != -1 and end > start: @@ -83,105 +76,78 @@ def clean_json_response(raw_response): json.loads(potential_json) return potential_json except json.JSONDecodeError: - # Attempt to fix common issues like trailing commas (simple case) cleaned = re.sub(r",\s*([}\]])", r"\1", potential_json) try: - json.loads(cleaned) - return cleaned + json.loads(cleaned) + return cleaned except json.JSONDecodeError: - pass # Give up on this match - - # If nothing works, return the raw response hoping it's already JSON or error handled elsewhere + pass return raw_response.strip() - -# --- Holder for Active API Clients (Keep outside gr.Blocks) --- -# This avoids storing complex objects in gr.State, which causes the TypeError +# --- Holder pour Client API Actif (hors gr.Blocks) --- active_api_client_holder = { "client": None, - "openai_key": None # Store the validated key here temporarily if needed + "openai_key": None } -# --- Fonctions Principales de l'Application (Mises à jour) --- +# --- Fonctions Principales de l'Application --- def get_active_client(app_config): - """Retrieves the globally stored client based on app_config.""" + """Récupère le client stocké globalement.""" api_source = app_config.get("api_source") if not api_source: - return None, "API source not configured." - + return None, "Source API non configurée." client = active_api_client_holder.get("client") - - # Check if the stored client matches the configured source - if client: - if api_source == "openai" and not isinstance(client, OpenAI): - # Check if it's the OpenRouter client mistakenly stored - # This check might need refinement based on how you differentiate clients - pass # Assume it's correct for now, relies on configure_api_clients logic - elif api_source == "openrouter" and not client.base_url.startswith("https://openrouter.ai"): - pass # Assume it's correct - if not client: - # Attempt to re-initialize if missing (e.g., after script reload) - print("WARN: Active client not found in holder, attempting re-initialization based on config.") - if api_source == "openai" and active_api_client_holder.get("openai_key"): - try: - client = OpenAI(api_key=active_api_client_holder["openai_key"]) - active_api_client_holder["client"] = client - print("Re-initialized OpenAI client.") - except Exception as e: - return None, f"Failed to re-initialize OpenAI client: {e}" - elif api_source == "openrouter" and openrouter_api_key: - try: - client = OpenAI( - base_url="https://openrouter.ai/api/v1", - api_key=openrouter_api_key, - ) - active_api_client_holder["client"] = client - print("Re-initialized OpenRouter client.") - except Exception as e: - return None, f"Failed to re-initialize OpenRouter client: {e}" - else: - return None, f"Cannot re-initialize client for source '{api_source}'. Missing key or config." - - + print("WARN: Client actif non trouvé, tentative de ré-initialisation.") + # Tentative de ré-initialisation (utile si script rechargé) + if api_source == "openai" and active_api_client_holder.get("openai_key"): + try: + client = OpenAI(api_key=active_api_client_holder["openai_key"]) + active_api_client_holder["client"] = client + print("Client OpenAI ré-initialisé.") + except Exception as e: return None, f"Échec ré-initialisation client OpenAI: {e}" + elif api_source == "openrouter" and openrouter_api_key: + try: + client = OpenAI(base_url="[https://openrouter.ai/api/v1](https://openrouter.ai/api/v1)", api_key=openrouter_api_key) + active_api_client_holder["client"] = client + print("Client OpenRouter ré-initialisé.") + except Exception as e: return None, f"Échec ré-initialisation client OpenRouter: {e}" + else: + return None, f"Impossible de ré-initialiser le client pour '{api_source}'. Clé ou config manquante." if not client: - return None, f"API client for '{api_source}' is not available or failed to initialize." - - return client, None # Return client and no error message + return None, f"Client API pour '{api_source}' non disponible." + return client, None -def analyze_biases_v2(app_config, objective_text, session_log_state): - """Analyse les biais dans l'objectif marketing (utilise le client API actif).""" +def analyze_biases(app_config, objective_text, session_log_state): + """Analyse les biais dans l'objectif marketing.""" log = session_log_state - log = update_log(f"Analyse Biais Objectif (début): '{objective_text[:50]}...'", log) - + log = update_log(f"Analyse biais objectif (début): '{objective_text[:50]}...'", log) if not objective_text: - return BiasAnalysisResponse(overall_comment="Veuillez fournir un objectif marketing.").dict(), update_log("Analyse Biais: Objectif vide.", log) + return BiasAnalysisResponse(overall_comment="Veuillez fournir un objectif marketing.").dict(), update_log("Analyse biais: Objectif vide.", log) active_client, error_msg = get_active_client(app_config) if error_msg: - log = update_log(f"ERREUR Analyse Biais: {error_msg}", log) + log = update_log(f"ERREUR Analyse biais: {error_msg}", log) return BiasAnalysisResponse(overall_comment=f"Erreur: {error_msg}").dict(), log model_name = app_config["text_model"] - - # --- System Prompt (Unchanged) --- system_prompt = f""" - Tu es un expert en marketing éthique et en psychologie cognitive, spécialisé dans la création de personas. - Analyse l'objectif marketing suivant : "{objective_text}" + Vous êtes un expert en marketing éthique et en psychologie cognitive, spécialisé dans la création de personas. + Analysez l'objectif marketing suivant : "{objective_text}" - Identifie les BIAIS COGNITIFS POTENTIELS ou RISQUES DE STÉRÉOTYPES pertinents pour la création de personas. Concentre-toi sur : - 1. **Stéréotypes / Généralisations Hâtives :** Suppose-t-on des traits basés sur le genre, l'âge, l'ethnie, le statut socio-économique sans justification ? (Ex: 'tous les jeunes urbains sont écolos') - 2. **Biais de Confirmation / Affinité :** L'objectif semble-t-il chercher à valider une idée préconçue ou refléter trop les opinions du concepteur ? (Ex: 'prouver que notre produit est parfait pour CE type de personne') - 3. **Simplification Excessive / Manque de Nuance :** Le groupe cible est-il décrit de manière trop monolithique, ignorant la diversité interne ? (Ex: 'les seniors actifs' sans différencier leurs motivations ou capacités) - 4. **Autres biais pertinents** (Ex: Oubli de fréquence de base, Biais de normalité si applicable). + Identifiez les BIAIS COGNITIFS POTENTIELS ou RISQUES DE STÉRÉOTYPES pertinents pour la création de personas. Concentrez-vous sur : + 1. **Stéréotypes / Généralisations hâtives :** Suppose-t-on des traits basés sur des groupes (genre, âge, ethnie, statut socio-économique...) sans justification ? (Ex: 'tous les jeunes urbains sont écolos') + 2. **Biais de confirmation / d'affinité :** L'objectif semble-t-il chercher à valider une idée préconçue ou refléter trop les opinions du concepteur ? (Ex: 'prouver que notre produit est parfait pour CE type de personne') + 3. **Simplification excessive / Manque de nuance :** Le groupe cible est-il décrit de manière trop monolithique, ignorant la diversité interne ? (Ex: 'les seniors actifs' sans différencier leurs motivations ou capacités) + 4. **Autres biais pertinents** (Ex: Oubli de fréquence de base, Biais de normalité...). Pour chaque biais potentiel identifié : - - Nomme le type de biais (ex: Stéréotype d'âge). - - Explique brièvement POURQUOI c'est un risque DANS CE CONTEXTE de création de persona. - - Propose un CONSEIL PRÉCIS pour nuancer l'objectif ou être vigilant lors de la création. + - Nommez le type de biais (ex: Stéréotype d'âge). + - Expliquez brièvement POURQUOI c'est un risque DANS CE CONTEXTE de création de persona. + - Proposez un CONSEIL PRÉCIS pour nuancer l'objectif ou être vigilant lors de la création. - Structure ta réponse en utilisant le format JSON suivant (avec la classe Pydantic BiasAnalysisResponse): + Structurez votre réponse en utilisant le format JSON suivant (schéma Pydantic BiasAnalysisResponse): {{ "detected_biases": [ {{ @@ -190,111 +156,80 @@ def analyze_biases_v2(app_config, objective_text, session_log_state): "advice": "Conseil spécifique d'atténuation." }} ], - "overall_comment": "Bref commentaire général. Indique si aucun biais majeur n'est détecté." + "overall_comment": "Bref commentaire général. Indiquez si aucun biais majeur n'est détecté." }} - Réponds en français. S'il n'y a pas de biais clair, retourne une liste 'detected_biases' vide et indique-le dans 'overall_comment'. + Répondez en français. S'il n'y a pas de biais clair, retournez une liste 'detected_biases' vide et indiquez-le dans 'overall_comment'. """ - - response_content_str = "" # Init for the bloc except + response_content_str = "" try: completion = active_client.chat.completions.create( model=model_name, - messages=[ - {"role": "user", "content": system_prompt} - ], - temperature=0.4, - max_tokens=800, + messages=[{"role": "user", "content": system_prompt}], + temperature=0.4, max_tokens=800, response_format={"type": "json_object"}, ) - response_content_str = completion.choices[0].message.content cleaned_response_str = clean_json_response(response_content_str) - - # Try parsing the cleaned JSON response try: parsed_response = BiasAnalysisResponse.parse_raw(cleaned_response_str) - log = update_log(f"Analyse Biais Objectif (fin): Biais trouvés - {len(parsed_response.detected_biases)}", log) + log = update_log(f"Analyse biais objectif (fin): Biais trouvés - {len(parsed_response.detected_biases)}", log) return parsed_response.dict(), log except Exception as parse_error: error_msg = f"Erreur parsing JSON après nettoyage: {parse_error}. Réponse nettoyée: '{cleaned_response_str[:200]}...'" print(error_msg) - log = update_log(f"ERREUR Analyse Biais Parsing: {parse_error}", log) - return BiasAnalysisResponse(overall_comment=f"Erreur technique lors du parsing de la réponse: {parse_error}").dict(), log - - + log = update_log(f"ERREUR Analyse biais parsing: {parse_error}", log) + return BiasAnalysisResponse(overall_comment=f"Erreur technique parsing réponse: {parse_error}").dict(), log except openai.AuthenticationError as e: - error_msg = f"Erreur d'authentification API ({app_config.get('api_source', 'Inconnu')}). Vérifiez votre clé." - print(error_msg) - log = update_log(f"ERREUR API Auth: {error_msg}", log) + error_msg = f"Erreur authentification API ({app_config.get('api_source', 'Inconnu')}). Vérifiez clé." + print(error_msg); log = update_log(f"ERREUR API Auth: {error_msg}", log) return BiasAnalysisResponse(overall_comment=error_msg).dict(), log except openai.RateLimitError as e: - error_msg = f"Erreur API ({app_config.get('api_source', 'Inconnu')}): Limite de taux atteinte. Réessayez plus tard." - print(error_msg) - log = update_log(f"ERREUR API RateLimit: {error_msg}", log) + error_msg = f"Erreur API ({app_config.get('api_source', 'Inconnu')}): Limite de taux atteinte." + print(error_msg); log = update_log(f"ERREUR API RateLimit: {error_msg}", log) return BiasAnalysisResponse(overall_comment=error_msg).dict(), log except Exception as e: - error_msg = f"Erreur pendant l'analyse des biais: {str(e)}. Réponse brute: '{response_content_str[:200]}...'" - print(error_msg) - log = update_log(f"ERREUR Analyse Biais API Call: {str(e)}", log) - # Try to return a compatible error structure - return BiasAnalysisResponse(overall_comment=f"Erreur technique lors de l'analyse: {str(e)}").dict(), log - -# --- display_bias_analysis_v2 (Unchanged) --- -def display_bias_analysis_v2(analysis_result): - """Formate l'analyse des biais pour l'affichage avec HighlightedText.""" - # Prend directement le dict retourné par analyze_biases_v2 - if not analysis_result: - return [("Aucune analyse effectuée.", None)] # Retourne format HighlightedText - + error_msg = f"Erreur pendant analyse biais: {str(e)}. Réponse brute: '{response_content_str[:200]}...'" + print(error_msg); log = update_log(f"ERREUR Analyse biais API Call: {str(e)}", log) + return BiasAnalysisResponse(overall_comment=f"Erreur technique analyse: {str(e)}").dict(), log + +# --- display_bias_analysis (Unchanged from V2) --- +def display_bias_analysis(analysis_result): + """Formate l'analyse des biais pour l'affichage.""" + if not analysis_result: return [("Aucune analyse effectuée.", None)] biases = analysis_result.get("detected_biases", []) overall_comment = analysis_result.get("overall_comment", "") - highlighted_data = [] - if "Erreur" in overall_comment: - highlighted_data.append((overall_comment, "ERROR")) # Étiquette spécifique pour erreurs - elif not biases: - highlighted_data.append((overall_comment or "Aucun biais majeur détecté.", "INFO")) + if "Erreur" in overall_comment: highlighted_data.append((overall_comment, "ERROR")) + elif not biases: highlighted_data.append((overall_comment or "Aucun biais majeur détecté.", "INFO")) else: - if overall_comment: - highlighted_data.append((overall_comment + "\n\n", "COMMENT")) + if overall_comment: highlighted_data.append((overall_comment + "\n\n", "COMMENT")) for bias_info in biases: highlighted_data.append((f"⚠️ {bias_info.get('bias_type', 'Type inconnu')}: ", "BIAS_TYPE")) highlighted_data.append((f"{bias_info.get('explanation', 'Pas d’explication.')}\n", "EXPLANATION")) highlighted_data.append((f"💡 Conseil: {bias_info.get('advice', 'Pas de conseil.')}\n", "ADVICE")) - - # Retourne les données formatées pour HighlightedText return highlighted_data - -def generate_persona_image_v2(app_config, *args): - """Génère l'image du persona en utilisant OpenAI si activé, sinon retourne None.""" - # Les 13 premiers args sont les inputs de l'image, le dernier est session_log_state +def generate_persona_image(app_config, *args): + """Génère l'image du persona.""" inputs = args[:-1] session_log_state = args[-1] log = session_log_state - (first_name, last_name, age, gender, persona_description_en, # Renommé pour clarté + (first_name, last_name, age, gender, persona_description_en, skin_color, eye_color, hair_style, hair_color, facial_expression, posture, clothing_style, accessories) = inputs - # Vérifier si la génération d'image est activée (nécessite clé OpenAI valide) if not app_config.get("image_generation_enabled", False): - log = update_log("Génération Image: Désactivée (Clé API OpenAI non fournie/valide).", log) - return None, log, "Génération d'image désactivée. Veuillez fournir une clé API OpenAI valide dans l'onglet Configuration." + log = update_log("Génération image: Désactivée (clé API OpenAI non fournie/valide).", log) + return None, log, "Génération d'image désactivée. Veuillez fournir une clé API OpenAI valide." - # Get the active OpenAI client (Image generation always uses OpenAI in this app) - # We assume configure_api_clients stored the *correct* client if image_gen is enabled openai_client, error_msg = get_active_client(app_config) if error_msg or app_config.get("api_source") != "openai": - final_error = f"Erreur interne ou mauvaise config pour Génération Image: {error_msg or 'Client non OpenAI actif'}" - log = update_log(f"ERREUR Génération Image: {final_error}", log) - return None, log, final_error - - # Vérifier les champs obligatoires + final_error = f"Erreur interne/config pour génération image: {error_msg or 'Client non OpenAI actif'}" + log = update_log(f"ERREUR Génération image: {final_error}", log); return None, log, final_error if not first_name or not last_name or not age or not gender: - return None, log, "Veuillez remplir Prénom, Nom, Âge et Genre pour générer l'image." + return None, log, "Veuillez remplir prénom, nom, âge et genre pour générer l'image." - # --- Build Prompt (Unchanged) --- - prompt_parts = [f"one person only, close-up portrait photo of {first_name} {last_name}, a {gender} aged {age}."] # Préciser "photo", "portrait" + prompt_parts = [f"one person only, close-up portrait photo of {first_name} {last_name}, a {gender} aged {age}."] if skin_color_mapping.get(skin_color): prompt_parts.append(f"Skin tone: {skin_color_mapping[skin_color]}.") if eye_color_mapping.get(eye_color): prompt_parts.append(f"Eye color: {eye_color_mapping[eye_color]}.") if hair_style_mapping.get(hair_style): prompt_parts.append(f"Hairstyle: {hair_style_mapping[hair_style]}.") @@ -302,145 +237,111 @@ def generate_persona_image_v2(app_config, *args): if facial_expression_mapping.get(facial_expression): prompt_parts.append(f"Facial expression: {facial_expression_mapping[facial_expression]}.") if posture_mapping.get(posture): prompt_parts.append(f"Posture: {posture_mapping[posture]}.") if clothing_style_mapping.get(clothing_style): prompt_parts.append(f"Clothing style: {clothing_style_mapping[clothing_style]}.") - if accessories_mapping.get(accessories): prompt_parts.append(f"Wearing: {accessories_mapping[accessories]}.") # "Wearing" est souvent mieux pour les accessoires + if accessories_mapping.get(accessories): prompt_parts.append(f"Wearing: {accessories_mapping[accessories]}.") if persona_description_en: prompt_parts.append(f"Background or context: {persona_description_en}.") prompt_parts.append("Realistic photo style, high detail, natural lighting.") final_prompt = " ".join(prompt_parts) - - log = update_log(f"Génération Image (début): Prompt='{final_prompt[:100]}...'", log) + log = update_log(f"Génération image (début): Prompt='{final_prompt[:100]}...'", log) try: response = openai_client.images.generate( - model=OPENAI_IMAGE_MODEL, - prompt=final_prompt, - size="1024x1024", - n=1, - response_format="url", # Ou "b64_json" - quality="standard", # ou "hd" - style="natural" # ou "vivid" + model=OPENAI_IMAGE_MODEL, prompt=final_prompt, size="1024x1024", n=1, + response_format="url", quality="standard", style="natural" ) - image_url = response.data[0].url img_response = requests.get(image_url) - img_response.raise_for_status() # Vérifie les erreurs HTTP + img_response.raise_for_status() pil_image = Image.open(io.BytesIO(img_response.content)) - - log = update_log("Génération Image (fin): Succès.", log) - return pil_image, log, None # Image, Log, No error message - - # --- Error Handling (Unchanged) --- + log = update_log("Génération image (fin): Succès.", log) + return pil_image, log, None except openai.AuthenticationError as e: - error_msg = f"Erreur d'authentification API OpenAI. Vérifiez votre clé." - print(error_msg) - log = update_log(f"ERREUR API Auth (Image): {error_msg}", log) - return None, log, error_msg # Retourne None pour l'image, log, et message d'erreur + error_msg = "Erreur authentification API OpenAI. Vérifiez clé." + print(error_msg); log = update_log(f"ERREUR API Auth (Image): {error_msg}", log) + return None, log, error_msg except openai.RateLimitError as e: - error_msg = f"Erreur API OpenAI (Image): Limite de taux atteinte. Réessayez plus tard." - print(error_msg) - log = update_log(f"ERREUR API RateLimit (Image): {error_msg}", log) + error_msg = "Erreur API OpenAI (Image): Limite de taux atteinte." + print(error_msg); log = update_log(f"ERREUR API RateLimit (Image): {error_msg}", log) return None, log, error_msg - except openai.BadRequestError as e: # Erreur fréquente si le prompt est refusé - error_msg = f"Erreur API OpenAI (Image): Requête invalide (prompt refusé ?). Détails: {e}" - print(error_msg) - log = update_log(f"ERREUR API BadRequest (Image): {error_msg}", log) + except openai.BadRequestError as e: + error_msg = f"Erreur API OpenAI (Image): Requête invalide (prompt refusé?). Détails: {e}" + print(error_msg); log = update_log(f"ERREUR API BadRequest (Image): {error_msg}", log) return None, log, error_msg except Exception as e: - error_msg = f"Erreur lors de la génération de l'image: {str(e)}" - print(error_msg) - log = update_log(f"ERREUR Génération Image: {str(e)}", log) + error_msg = f"Erreur lors génération image: {str(e)}" + print(error_msg); log = update_log(f"ERREUR Génération image: {str(e)}", log) return None, log, error_msg - -def refine_persona_details_v2(app_config, first_name, last_name, age, field_name, field_value, bias_analysis_dict, marketing_objectives, session_log_state): - """Affine les détails du persona (utilise le client API actif).""" - # Note: bias_analysis_json_str is now bias_analysis_dict +def refine_persona_details(app_config, first_name, last_name, age, field_name, field_value, bias_analysis_dict, marketing_objectives, session_log_state): + """Affine les détails du persona via le système d'IA.""" log = session_log_state - log = update_log(f"Refinement (début): Champ='{field_name}', Valeur initiale='{field_value[:50]}...'", log) - - # Get active client + log = update_log(f"Raffinement (début): Champ='{field_name}', Valeur initiale='{field_value[:50]}...'", log) active_client, error_msg = get_active_client(app_config) if error_msg: - log = update_log(f"ERREUR Refinement: {error_msg}", log) - return log, f"ERREUR: {error_msg}" # Return log and error message + log = update_log(f"ERREUR Raffinement: {error_msg}", log) + return log, f"ERREUR: {error_msg}", field_name # Return error and field name for display context model_name = app_config["text_model"] - - # Process bias analysis results (now expects a dict) - biases_text = "Aucune analyse de biais précédente disponible ou chargée." + biases_text = "Aucune analyse de biais précédente disponible." if bias_analysis_dict: try: detected_biases = bias_analysis_dict.get("detected_biases", []) if detected_biases: biases_text = "\n".join([f"- {b.get('bias_type','N/A')}: {b.get('explanation','N/A')}" for b in detected_biases]) else: - biases_text = bias_analysis_dict.get("overall_comment", "Aucun biais majeur détecté lors de l'analyse initiale.") # Use overall comment if no biases + biases_text = bias_analysis_dict.get("overall_comment", "Aucun biais majeur détecté lors de l'analyse initiale.") except Exception as e: - biases_text = f"Erreur lors de la lecture des biais analysés (dict): {e}" - log = update_log(f"ERREUR Lecture Biais Dict pour Refinement: {e}", log) - + biases_text = f"Erreur lecture biais analysés: {e}" + log = update_log(f"ERREUR Lecture Biais Dict pour Raffinement: {e}", log) - # --- System Prompt (Unchanged, uses biases_text) --- system_prompt = f""" - Tu es un assistant IA expert en marketing éthique, aidant à affiner le persona marketing pour '{first_name} {last_name}' ({age} ans). + Vous êtes un assistant expert en marketing éthique, aidant à affiner le persona marketing pour '{first_name} {last_name}' ({age} ans). L'objectif marketing initial était : "{marketing_objectives}" L'analyse initiale de cet objectif a soulevé les points suivants : {biases_text} - Tâche: Concentre-toi UNIQUEMENT sur le champ '{field_name}' dont la valeur actuelle est '{field_value}'. - Propose 1 à 2 suggestions CONCISES et ACTIONNABLES pour améliorer, nuancer ou enrichir cette valeur. - Tes suggestions doivent viser à : + Tâche : Concentrez-vous UNIQUEMENT sur le champ '{field_name}' dont la valeur actuelle est '{field_value}'. + Proposez 1 à 2 suggestions CONCISES et ACTIONNABLES pour améliorer, nuancer ou enrichir cette valeur. + Vos suggestions doivent viser à : - Rendre le persona plus réaliste et moins cliché. - ATTÉNUER spécifiquement les biais potentiels listés ci-dessus s'ils sont pertinents pour ce champ. - Rester cohérent avec l'objectif marketing général. - Éviter les généralisations excessives. - Si la valeur actuelle semble bonne ou si tu manques de contexte pour faire une suggestion pertinente, indique-le simplement (ex: "La valeur actuelle semble appropriée." ou "Difficile de suggérer sans plus de contexte."). - Réponds en français. Ne fournis QUE les suggestions ou le commentaire d'approbation/manque de contexte. Ne répète pas la question. + Si la valeur actuelle semble bonne ou si vous manquez de contexte pour suggérer, indiquez-le simplement (ex: "La valeur actuelle semble appropriée."). + Répondez en français. Ne fournissez QUE les suggestions ou le commentaire d'approbation/manque de contexte. Ne répétez pas la question. Ne vous excusez pas. """ - suggestions = "" # Init for the bloc except + suggestions = "" try: response = active_client.chat.completions.create( model=model_name, messages=[{"role": "user", "content": system_prompt}], - temperature=0.6, # Un peu plus de créativité pour les suggestions - max_tokens=150, + temperature=0.6, max_tokens=150, ) suggestions = response.choices[0].message.content.strip() - - log = update_log(f"Refinement (fin): Champ='{field_name}'. Suggestions: '{suggestions[:50]}...'", log) - # Return updated log and suggestions (or None if error) - return log, suggestions - - # --- Error Handling (Unchanged) --- + log = update_log(f"Raffinement (fin): Champ='{field_name}'. Suggestions: '{suggestions[:50]}...'", log) + # Return log, suggestion, and field name for display context + return log, suggestions, field_name except openai.AuthenticationError as e: - error_msg = f"Erreur d'authentification API ({app_config.get('api_source', 'Inconnu')}) pendant raffinement. Vérifiez votre clé." - print(error_msg) - log = update_log(f"ERREUR API Auth (Refine): {error_msg}", log) - return log, f"ERREUR: {error_msg}" # Return error message for display + error_msg = f"Erreur authentification API ({app_config.get('api_source', 'Inconnu')}) pendant raffinement." + print(error_msg); log = update_log(f"ERREUR API Auth (Refine): {error_msg}", log) + return log, f"ERREUR: {error_msg}", field_name except openai.RateLimitError as e: error_msg = f"Erreur API ({app_config.get('api_source', 'Inconnu')}) (Refine): Limite de taux atteinte." - print(error_msg) - log = update_log(f"ERREUR API RateLimit (Refine): {error_msg}", log) - return log, f"ERREUR: {error_msg}" + print(error_msg); log = update_log(f"ERREUR API RateLimit (Refine): {error_msg}", log) + return log, f"ERREUR: {error_msg}", field_name except Exception as e: error_msg = f"Erreur lors du raffinement pour '{field_name}': {str(e)}" - print(error_msg) - log = update_log(f"ERREUR Refinement '{field_name}': {str(e)}", log) - return log, f"ERREUR: {error_msg}" - -# --- generate_summary_v2 (Unchanged, already handles PIL image correctly) --- -def generate_summary_v2(*args): - """Génère le résumé HTML du persona (gestion image PIL).""" - # Le dernier arg est session_log_state, l'avant-dernier est persona_image (PIL ou None) - inputs = args[:-2] # Tous les champs textuels/numériques/dropdowns - persona_image_pil = args[-2] # Peut être None ou un objet PIL Image + print(error_msg); log = update_log(f"ERREUR Raffinement '{field_name}': {str(e)}", log) + return log, f"ERREUR: {error_msg}", field_name + +def generate_summary(persona_image_pil, *args): + """Génère le résumé HTML du persona.""" + # Args structure: first_name, ..., references, session_log_state session_log_state = args[-1] + inputs = args[:-1] log = session_log_state - - # Extrait tous les champs (assurez-vous que l'ordre correspond à all_persona_inputs_for_summary) - (first_name, last_name, age, gender, persona_description_en, # Utiliser la version EN pour le contexte image - skin_color, eye_color, hair_style, hair_color, facial_expression, - posture, clothing_style, accessories, + (first_name, last_name, age, gender, persona_description_en, + skin_color, eye_color, hair_style, hair_color, facial_expression, posture, clothing_style, accessories, marital_status, education_level, profession, income, personality_traits, values_beliefs, motivations, hobbies_interests, main_responsibilities, daily_activities, technology_relationship, @@ -449,32 +350,27 @@ def generate_summary_v2(*args): visual_codes, special_considerations, daily_life, references ) = inputs - log = update_log(f"Génération Résumé: Pour '{first_name} {last_name}'.", log) - + log = update_log(f"Génération résumé: Pour '{first_name} {last_name}'.", log) summary = "" - image_html = "
\n" # Div pour l'image + image_html = "
\n" if not first_name or not last_name or not age: summary += "

Informations de base manquantes

\n" summary += "

Veuillez fournir au moins le prénom, le nom et l'âge (Étape 2).

\n" image_html += "

Image non générée.

\n" else: - # Intégrer l'image si elle existe (objet PIL) if persona_image_pil and isinstance(persona_image_pil, Image.Image): try: - # Convertir l'image PIL en base64 pour l'intégrer directement buffered = io.BytesIO() - # Sauvegarder en PNG (ou JPEG si préféré) dans le buffer mémoire - # Handle potential RGBA issues for JPEG - img_to_save = persona_image_pil + img_to_save = persona_image_pil.copy() # Work on a copy if img_to_save.mode == 'RGBA' or 'transparency' in img_to_save.info: - img_to_save = img_to_save.convert('RGB') # Convert to RGB if it has alpha - - img_to_save.save(buffered, format="JPEG") # Use JPEG for smaller size usually + img_to_save = img_to_save.convert('RGB') + img_to_save.save(buffered, format="JPEG", quality=85) # Use JPEG, quality 85 img_bytes = buffered.getvalue() img_base64 = base64.b64encode(img_bytes).decode() img_data_url = f"data:image/jpeg;base64,{img_base64}" - image_html += f"Persona {first_name}\n" + # Style pour l'image : largeur max 100%, hauteur auto pour responsivité dans la colonne + image_html += f"Persona {first_name}\n" except Exception as e: img_err_msg = f"Erreur encodage image: {e}" image_html += f"

{img_err_msg}

\n" @@ -482,44 +378,23 @@ def generate_summary_v2(*args): else: image_html += "

Aucune image générée ou disponible.

\n" - # Section Informations Personnelles (Titre centré) summary += f"

{first_name} {last_name}, {age} ans ({gender})

\n" - # summary += f"

{persona_description_en}

\n" # Commenté - # Assemblage des autres sections (avec vérification si champ rempli) def add_section(title, fields): content = "" for label, value in fields.items(): - # N'ajoute que si la valeur existe (n'est pas None, False, 0, ou chaîne vide) - # Exception for income == 0 which might be valid - should_add = False - if label == "Revenus annuels (€)": - # Add if value is not None (0 is a valid income) - should_add = value is not None - elif value: # Standard check for other fields - should_add = True - + should_add = (label == "Revenus annuels (€)" and value is not None) or (label != "Revenus annuels (€)" and value) if should_add: - # Formatage spécial pour les revenus if label == "Revenus annuels (€)" and isinstance(value, (int, float)): - # Format numérique avec séparateur de milliers (espace) - try: - # Use non-breaking space for thousands separator in HTML - value_str = f"{int(value):,} €".replace(",", " ") - except ValueError: # Gère le cas où income serait une chaîne ou autre chose - value_str = str(value) + " €" - else: - value_str = str(value) - # Remplace les sauts de ligne par
pour l'affichage HTML, escape HTML chars + try: value_str = f"{int(value):,} €".replace(",", " ") + except ValueError: value_str = str(value) + " €" + else: value_str = str(value) value_str_html = markdown.markdown(value_str).replace('

', '').replace('

', '').strip().replace("\n", "
") content += f"{label}: {value_str_html}
\n" - if content: - # Ajoute un peu d'espace avant la section - return f"

{title}

\n{content}\n" + if content: return f"

{title}

\n{content}\n" return "" - # Construire le résumé par sections - summary += add_section("Infos Socio-Démographiques", { + summary += add_section("Infos socio-démographiques", { "État civil": marital_status, "Niveau d'éducation": education_level, "Profession": profession, "Revenus annuels (€)": income }) @@ -527,19 +402,19 @@ def generate_summary_v2(*args): "Traits de personnalité": personality_traits, "Valeurs et croyances": values_beliefs, "Motivations intrinsèques": motivations, "Hobbies et intérêts": hobbies_interests }) - summary += add_section("Relation au Produit/Service", { + summary += add_section("Relation au produit/service", { "Relation avec la technologie": technology_relationship, "Tâches liées au produit/service": product_related_activities, "Points de douleur (Pain points)": pain_points, "Objectifs d’utilisation du produit/service": product_goals, "Scénarios d’utilisation typiques": usage_scenarios }) - summary += add_section("Contexte Professionnel/Vie Quotidienne", { + summary += add_section("Contexte professionnel/vie quotidienne", { "Responsabilités principales": main_responsibilities, "Activités journalières": daily_activities, - "Une journée type / Citation": daily_life # Renommé pour correspondre au label + "Une journée type / Citation": daily_life }) - summary += add_section("Marketing & Considérations Spéciales", { + summary += add_section("Marketing & considérations spéciales", { "Relation avec la marque": brand_relationship, "Segment de marché": market_segment, "Objectifs commerciaux (SMART)": commercial_objectives, @@ -547,486 +422,365 @@ def generate_summary_v2(*args): "Considérations spéciales (accessibilité, culture...)": special_considerations, "Références / Sources de données": references }) - - image_html += "
\n" # Ferme div image - - # Assemblage final avec flexbox + image_html += "
\n" final_html = "
\n" - final_html += f"
\n{summary}
\n" # Colonne texte - final_html += image_html # Colonne image + final_html += f"
\n{summary}
\n" + final_html += image_html final_html += "
" - - # Return the generated HTML and updated log return final_html, log +# --- Interface Gradio --- + +# CSS personnalisé pour les suggestions +css = """ +.suggestion-box { + border: 1px solid #e0e0e0; + border-radius: 5px; + padding: 10px; + margin-top: 10px; + margin-bottom: 10px; + background-color: #f9f9f9; /* Fond légèrement grisé */ +} +.suggestion-box h4 { margin-top: 0; margin-bottom: 5px; } +""" -# --- Interface Gradio V2 (Mise à jour avec BYOK et suggestions) --- - -with gr.Blocks(theme=gr.themes.Glass()) as demo: - gr.Markdown("# PersonaGenAI V2 : Assistant de Création de Persona Marketing") - gr.Markdown("Outil d'aide à la création de personas, intégrant l'IA générative (OpenRouter ou OpenAI) pour stimuler la créativité et la réflexivité face aux biais.") +with gr.Blocks(theme=gr.themes.Glass(), css=css) as demo: + # --- Titre et Description (Corrigés) --- + gr.Markdown("# PersonaGenAI : Assistant de création de persona marketing") + gr.Markdown("Outil d'aide à la création de personas, intégrant un système d'IA générative (OpenRouter ou OpenAI) pour stimuler la créativité et la réflexivité face aux biais.") - # --- État Global Partagé --- - # Stocke la configuration active (flags, modèle, mais PAS le client objet) + # --- États Globaux (Corrigés) --- app_config_state = gr.State(value={ - # "client": None, # REMOVED - DO NOT STORE CLIENT OBJECT IN STATE - "api_source": None, # 'openai' or 'openrouter' - "text_model": None, - "image_generation_enabled": False, - "openai_key_provided": False, # Flag if key was entered - "openrouter_key_provided": bool(openrouter_api_key) + "api_source": None, "text_model": None, "image_generation_enabled": False, + "openai_key_provided": False, "openrouter_key_provided": bool(openrouter_api_key) }) - # Stocke le résultat de l'analyse de biais (dict) bias_analysis_result_state = gr.State(value={}) - # Stocke l'image générée (objet PIL ou None) - Keep this, gr.Image handles PIL persona_image_pil_state = gr.State(value=None) - # Stocke le log de session (chaîne de caractères) session_log_state = gr.State(value="") - # Pour afficher les messages d'erreur/status globaux status_message_state = gr.State(value="") + # État pour stocker la dernière suggestion d'affinement + last_refinement_suggestion_state = gr.State(value=None) # Sera (field_name, suggestion_text) - # --- Affichage Global du Statut/Erreur --- - status_display = gr.Markdown(value="", elem_classes="status-message") # Pour afficher les messages d'erreur/info - - # Fonction pour mettre à jour le message de statut + # --- Affichage Global Statut/Erreur (Corrigé) --- + status_display = gr.Markdown(value="", elem_classes="status-message") def update_status_display(new_message, current_log): - # Met aussi à jour le log si un message est affiché - if new_message: - # Avoid logging redundant "success" messages or empty updates - if "ERREUR" in new_message or "WARN" in new_message or ("Configuration" in new_message and "active" in new_message) : - current_log = update_log(f"STATUS: {new_message}", current_log) + """Met à jour le statut et le log si pertinent.""" + if new_message and ("ERREUR" in new_message or "WARN" in new_message or ("Configuration" in new_message and "active" in new_message)): + current_log = update_log(f"STATUS: {new_message}", current_log) return new_message, current_log # --- Onglets --- with gr.Tabs() as tabs: - # --- Onglet 0 : Configuration API (BYOK) --- + # --- Onglet 0 : Configuration API (Corrigé) --- with gr.Tab("🔑 Configuration API", id=-1): - gr.Markdown("### Configuration des Clés API") - gr.Markdown("Cette application utilise une IA pour analyser et générer du contenu. Choisissez votre fournisseur d'API.") - - # Statut de la clé OpenRouter (obligatoire pour le mode de base) + gr.Markdown("### Configuration des clés API") + gr.Markdown("Ce outil utilise un système d'IA pour analyser et générer du contenu. Choisissez votre fournisseur d'API.") if openrouter_api_key: - gr.Markdown("✅ Clé API **OpenRouter** trouvée dans l'environnement (`OPENROUTER_API_KEY`).") + gr.Markdown("✅ Clé API **OpenRouter** trouvée (`OPENROUTER_API_KEY`).") else: - gr.Markdown("❌ **Clé API OpenRouter (`OPENROUTER_API_KEY`) non trouvée.** Le mode OpenRouter ne fonctionnera pas. Veuillez la définir dans vos variables d'environnement ou un fichier `.env` (ou utiliser une clé OpenAI).") + gr.Markdown("❌ **Clé API OpenRouter (`OPENROUTER_API_KEY`) non trouvée.** OpenRouter ne fonctionnera pas sans clé (ou utilisez une clé OpenAI).") - # Champ pour la clé OpenAI (optionnelle) openai_api_key_input = gr.Textbox( - label="Clé API OpenAI (Optionnelle)", - type="password", - placeholder="Entrez votre clé OpenAI ici pour activer DALL-E 3 et utiliser OpenAI pour le texte", - info="Si fournie et valide, cette clé sera utilisée pour la génération d'images (DALL-E 3) ET pour l'analyse/raffinement de texte (GPT). Sinon, OpenRouter (si clé dispo) sera utilisé pour le texte et la génération d'images sera désactivée." + label="Clé API OpenAI (optionnelle)", type="password", + placeholder="Entrez votre clé OpenAI ici pour DALL-E 3 et GPT", + info="Si fournie et valide : utilisée pour les images (DALL-E 3) ET le texte (GPT). Sinon : OpenRouter (si clé dispo) pour le texte, pas d'images." ) - # Bouton pour appliquer la config (initialise les clients) - configure_api_button = gr.Button("Appliquer la Configuration API") - # Affichage du statut de la configuration active - api_status_display = gr.Markdown("Statut API: Non configuré.") + configure_api_button = gr.Button("Appliquer la configuration API") + api_status_display = gr.Markdown("Statut API : Non configuré.") - # Fonction de configuration des clients API (modifiée) def configure_api_clients(openai_key, current_config, current_log): + """Valide les clés, configure le client actif et met à jour l'état de configuration.""" openai_key_provided = bool(openai_key) openrouter_key_available = current_config["openrouter_key_provided"] - status_msg = "" - config = current_config.copy() # Copie pour modification - - # Clear previous client and stored key from holder - active_api_client_holder["client"] = None - active_api_client_holder["openai_key"] = None + status_msg = ""; config = current_config.copy() + active_api_client_holder["client"] = None; active_api_client_holder["openai_key"] = None + api_source = None; text_model = None; image_enabled = False; client_to_store = None - api_source = None - text_model = None - image_enabled = False - client_to_store = None # The client object we will put in the global holder - - # Priorité à OpenAI si clé fournie if openai_key_provided: try: temp_client = OpenAI(api_key=openai_key) - # Simple test call (optional but good) - temp_client.models.list(limit=1) # Less expensive test - - # If test succeeds: - client_to_store = temp_client - active_api_client_holder["openai_key"] = openai_key # Store key if needed for re-init - api_source = "openai" - text_model = OPENAI_TEXT_MODEL - image_enabled = True - status_msg = f"✅ Configuration **OpenAI** active (Modèle texte: `{text_model}`, Images: DALL-E 3 activé)." - config["openai_key_provided"] = True + temp_client.models.list() # Test auth/connexion + client_to_store = temp_client; active_api_client_holder["openai_key"] = openai_key + api_source = "openai"; text_model = OPENAI_TEXT_MODEL; image_enabled = True + status_msg = f"✅ Configuration **OpenAI** active (Modèle texte: `{text_model}`, Images: DALL-E 3 activé)."; config["openai_key_provided"] = True current_log = update_log("Configuration: Client OpenAI initialisé et testé.", current_log) - except openai.AuthenticationError: - status_msg = "⚠️ Clé API OpenAI fournie mais **invalide**. Vérifiez la clé." - log_msg = f"ERREUR API Config OpenAI: Clé invalide." - current_log = update_log(log_msg, current_log) - print(log_msg) - config["openai_key_provided"] = False - openai_key_provided = False # Force fallback check + status_msg = "⚠️ Clé API OpenAI fournie mais **invalide**."; log_msg = f"ERREUR API Config OpenAI: Clé invalide." + current_log = update_log(log_msg, current_log); print(log_msg); config["openai_key_provided"] = False; openai_key_provided = False except Exception as e: - status_msg = f"⚠️ Clé OpenAI fournie mais erreur de connexion/test: {str(e)}. Vérifiez la clé et la connectivité." - log_msg = f"ERREUR API Config OpenAI: {e}" - current_log = update_log(log_msg, current_log) - print(log_msg) - config["openai_key_provided"] = False - openai_key_provided = False # Force fallback check - - # Fallback vers OpenRouter si clé OpenAI non fournie ou invalide, ET si clé OpenRouter existe - # Use 'elif' to avoid configuring OpenRouter if OpenAI was successful + status_msg = f"⚠️ Clé OpenAI fournie mais erreur connexion/test: {str(e)}."; log_msg = f"ERREUR API Config OpenAI: {e}" + current_log = update_log(log_msg, current_log); print(log_msg); config["openai_key_provided"] = False; openai_key_provided = False elif openrouter_key_available: try: - temp_client = OpenAI( - base_url="https://openrouter.ai/api/v1", - api_key=openrouter_api_key, - ) - # Simple test (e.g., list models - adapt if needed for OpenRouter structure) - # temp_client.models.list(limit=1) # Might need adjustment based on OpenRouter API/latency - - client_to_store = temp_client - api_source = "openrouter" - text_model = OPENROUTER_TEXT_MODEL - image_enabled = False # Image désactivée avec OpenRouter - status_msg = f"✅ Configuration **OpenRouter** active (Modèle texte: `{text_model}`, Images: Désactivé)." - config["openai_key_provided"] = False # Ensure this is false + temp_client = OpenAI(base_url="[https://openrouter.ai/api/v1](https://openrouter.ai/api/v1)", api_key=openrouter_api_key) + # Optionnel : Test pour OpenRouter (peut être lent ou échouer sans bloquer) + # try: temp_client.models.list(); current_log = update_log("Config: Client OpenRouter testé.", current_log) + # except Exception as test_e: current_log = update_log(f"WARN: Test OpenRouter échoué: {test_e}", current_log) + client_to_store = temp_client; api_source = "openrouter"; text_model = OPENROUTER_TEXT_MODEL; image_enabled = False + status_msg = f"✅ Configuration **OpenRouter** active (Modèle texte: `{text_model}`, Images: Désactivé)."; config["openai_key_provided"] = False current_log = update_log("Configuration: Client OpenRouter initialisé.", current_log) - except Exception as e: - status_msg = f"❌ Erreur lors de l'initialisation d'OpenRouter (clé: {openrouter_api_key is not None}): {e}." - log_msg = f"ERREUR API Config OpenRouter: {e}" - current_log = update_log(log_msg, current_log) - print(log_msg) - client_to_store = None # Ensure no client is stored - api_source = None - text_model = None - image_enabled = False - config["openai_key_provided"] = False - - else: # No valid OpenAI key provided AND no OpenRouter key available - if not openai_key_provided and not openrouter_key_available: - status_msg = "❌ Aucune clé API valide (ni OpenAI ni OpenRouter) n'est disponible/configurée. L'application ne peut pas fonctionner." - elif not openrouter_key_available: # OpenAI key was provided but failed, and no OpenRouter fallback - status_msg += " Et aucune clé OpenRouter n'est disponible comme alternative." # Append to previous OpenAI error msg - else: # Should not happen given the logic, but as a safeguard - status_msg = "❌ Impossible de configurer un client API." - - client_to_store = None # Ensure no client is stored - api_source = None - text_model = None - image_enabled = False - config["openai_key_provided"] = False - - - # Store the successfully created client (or None) in the global holder - active_api_client_holder["client"] = client_to_store - - # Mettre à jour l'état global (config flags only) - # config["client"] = client_to_store # DO NOT STORE CLIENT IN STATE - config["api_source"] = api_source - config["text_model"] = text_model - config["image_generation_enabled"] = image_enabled - - log_msg = f"Configuration API appliquée. Source Active: {api_source or 'Aucune'}, Images: {'Actif' if image_enabled else 'Inactif'}." - # Avoid double logging if already logged above - if "Configuration:" not in log_msg: - current_log = update_log(log_msg, current_log) + status_msg = f"❌ Erreur initialisation OpenRouter (clé: {'Oui' if openrouter_api_key else 'Non'}): {e}."; log_msg = f"ERREUR API Config OpenRouter: {e}" + current_log = update_log(log_msg, current_log); print(log_msg); client_to_store = None; api_source = None; text_model = None; image_enabled = False; config["openai_key_provided"] = False + else: + if not openai_key_provided and not openrouter_key_available: status_msg = "❌ Aucune clé API valide (OpenAI ou OpenRouter) disponible/configurée." + elif not openrouter_key_available: + if "OpenAI fournie mais" not in status_msg: status_msg = "❌ Clé OpenAI invalide ou erreur." + status_msg += " Pas d'alternative OpenRouter disponible." + else: status_msg = "❌ Impossible de configurer un client API." + client_to_store = None; api_source = None; text_model = None; image_enabled = False; config["openai_key_provided"] = False - # Return the new config state dict, the status message, and the updated log - # The interactivity update is handled by the .change() on app_config_state + active_api_client_holder["client"] = client_to_store + config["api_source"] = api_source; config["text_model"] = text_model; config["image_generation_enabled"] = image_enabled + log_msg = f"Config API appliquée. Source: {api_source or 'Aucune'}, Images: {'Actif' if image_enabled else 'Inactif'}." + if not any(phrase in current_log.splitlines()[-1] for phrase in ["Client OpenAI initialisé", "Client OpenRouter initialisé"]) : current_log = update_log(log_msg, current_log) return config, status_msg, current_log - - # Link the configuration button - # Outputs: update app_config_state, api_status_display markdown, session_log_state configure_api_button.click( fn=configure_api_clients, inputs=[openai_api_key_input, app_config_state, session_log_state], outputs=[app_config_state, api_status_display, session_log_state] ) - # --- Onglet 1 : Objectif & Analyse Biais --- - with gr.Tab("🎯 Étape 1: Objectif & Analyse Biais", id=0): + # --- Onglet 1 : Objectif & Analyse Biais (Corrigé) --- + with gr.Tab("🎯 Étape 1 : Objectif & analyse biais", id=0): gr.Markdown("### 1. Définissez votre objectif marketing") - gr.Markdown("Décrivez pourquoi vous créez ce persona. L'IA analysera votre objectif pour identifier des biais cognitifs potentiels.") + gr.Markdown("Décrivez pourquoi vous créez ce persona. Un système d'IA analysera votre objectif pour identifier des biais cognitifs potentiels.") with gr.Row(): objective_input = gr.Textbox(label="Objectif marketing pour ce persona", lines=4, scale=3) with gr.Column(scale=1): - gr.Markdown("Suggestions d'objectifs :") - suggestion_button1 = gr.Button("Exemple 1 : Service Écologique Urbain", size="sm") - suggestion_button2 = gr.Button("Exemple 2 : App Fitness Seniors", size="sm") - analyze_button = gr.Button("🔍 Analyser l'Objectif pour Biais") + gr.Markdown("Suggestions :") + suggestion_button1 = gr.Button("Exemple 1 : Service écologique urbain", size="sm") + suggestion_button2 = gr.Button("Exemple 2 : App fitness seniors", size="sm") + analyze_button = gr.Button("🔍 Analyser l'objectif (biais)") gr.Markdown("---") - gr.Markdown("### Analyse des Biais Potentiels") + gr.Markdown("### Analyse des biais potentiels") bias_analysis_output_highlighted = gr.HighlightedText( - label="Biais détectés et Conseils", - show_legend=True, - color_map={"BIAS_TYPE": "red", "EXPLANATION": "gray", "ADVICE": "green", "INFO": "blue", "COMMENT": "orange", "ERROR": "darkred"} # Ajout ERROR + label="Biais détectés et conseils", show_legend=True, + color_map={"BIAS_TYPE": "red", "EXPLANATION": "lightgray", "ADVICE": "green", "INFO": "blue", "COMMENT": "orange", "ERROR": "darkred"} ) gr.Markdown("---") gr.Markdown("### 🤔 Réflexion") user_reflection_on_biases = gr.Textbox( - label="Comment comptez-vous utiliser cette analyse pour la suite ?", - lines=2, + label="Comment comptez-vous utiliser cette analyse pour la suite ?", lines=2, placeholder="Ex: Je vais veiller à ne pas tomber dans le stéréotype X, je vais chercher des données pour nuancer Y..." ) log_reflection_button = gr.Button("📝 Enregistrer la réflexion", size='sm') - # Logique de l'onglet 1 - suggestion1_text = "Je souhaite créer un persona pour promouvoir un nouveau service de livraison écologique destiné aux jeunes professionnels urbains soucieux de l'environnement (25-35 ans). Il doit incarner ces valeurs et besoins identifiés lors de notre étude préalable." - suggestion2_text = "Développer une application mobile de fitness personnalisée pour les seniors actifs (+65 ans) cherchant à maintenir une vie saine et sociale. Le persona doit refléter leurs besoins (facilité, convivialité) et préférences." - + suggestion1_text = "Créer un persona pour promouvoir un nouveau service de livraison écologique destiné aux jeunes professionnels urbains soucieux de l'environnement (25-35 ans). Il doit incarner ces valeurs et besoins." + suggestion2_text = "Développer une application mobile de fitness personnalisée pour les seniors actifs (+65 ans) cherchant à maintenir une vie saine et sociale. Le persona doit refléter leurs besoins (facilité, convivialité)." suggestion_button1.click(lambda: suggestion1_text, outputs=objective_input) suggestion_button2.click(lambda: suggestion2_text, outputs=objective_input) - # Action du bouton Analyser analyze_button.click( - fn=analyze_biases_v2, + fn=analyze_biases, # Renommé inputs=[app_config_state, objective_input, session_log_state], - outputs=[bias_analysis_result_state, session_log_state] # Stores the result dict + updates log + outputs=[bias_analysis_result_state, session_log_state] ).then( - fn=display_bias_analysis_v2, - inputs=bias_analysis_result_state, # Uses the stored result (dict) - outputs=bias_analysis_output_highlighted # Displays formatted output + fn=display_bias_analysis, # Renommé + inputs=bias_analysis_result_state, + outputs=bias_analysis_output_highlighted ).then( - # Updates the global status display only if the analysis returned an error message fn=lambda result, log: update_status_display(result.get("overall_comment", "") if "Erreur" in result.get("overall_comment", "") else "", log), inputs=[bias_analysis_result_state, session_log_state], outputs=[status_display, session_log_state] ) - - # Action du bouton Enregistrer Réflexion def log_user_reflection(reflection_text, log_state): - if reflection_text: # Only log if there's text - log = update_log(f"Réflexion Utilisateur (Étape 1): '{reflection_text}'", log_state) - return log - return log_state # Return unchanged log if input is empty - log_reflection_button.click( - fn=log_user_reflection, - inputs=[user_reflection_on_biases, session_log_state], - outputs=[session_log_state] - ) + if reflection_text: return update_log(f"Réflexion utilisateur (Étape 1): '{reflection_text}'", log_state) + return log_state + log_reflection_button.click(log_user_reflection, [user_reflection_on_biases, session_log_state], [session_log_state]) - - # --- Onglet 2 : Image & Infos Base --- - with gr.Tab("👤 Étape 2: Image & Infos Base", id=1): + # --- Onglet 2 : Image & Infos Base (Corrigé) --- + with gr.Tab("👤 Étape 2 : Image & infos de base", id=1): gr.Markdown("### 2. Créez l'identité visuelle et les informations de base") with gr.Row(): - # FIX: Changed scale to integer - with gr.Column(scale=1): # Colonne de gauche pour les inputs (adjust scale integer as needed) + with gr.Column(scale=1): first_name_input = gr.Textbox(label="Prénom") last_name_input = gr.Textbox(label="Nom") age_input = gr.Slider(label="Âge", minimum=18, maximum=100, step=1, value=30) gender_input = gr.Radio(label="Genre", choices=["Homme", "Femme", "Non-binaire"], value="Homme") - persona_description_en_input = gr.Textbox(label="Contexte/Activité pour l'image (optionnel, en anglais)", lines=1, info="Ex: 'reading a book in a cozy cafe', 'working on a laptop in a modern office', 'hiking on a sunny day'") - - with gr.Accordion("🎨 Détails Visuels (Optionnel)", open=False): + persona_description_en_input = gr.Textbox( + label="Contexte/activité pour l'image (optionnel, anglais)", lines=1, + info="Ex: 'reading a book in a cafe', 'working on laptop', 'hiking'" + ) + with gr.Accordion("🎨 Détails visuels (optionnel)", open=False): with gr.Row(): skin_color_input = gr.Dropdown(label="Teint", choices=list(skin_color_mapping.keys()), value="") eye_color_input = gr.Dropdown(label="Yeux", choices=list(eye_color_mapping.keys()), value="") with gr.Row(): hair_style_input = gr.Dropdown(label="Coiffure", choices=list(hair_style_mapping.keys()), value="") - hair_color_input = gr.Dropdown(label="Cheveux", choices=list(hair_color_mapping.keys()), value="") + hair_color_input = gr.Dropdown(label="Cheveux (couleur)", choices=list(hair_color_mapping.keys()), value="") with gr.Row(): facial_expression_input = gr.Dropdown(label="Expression", choices=list(facial_expression_mapping.keys()), value="") posture_input = gr.Dropdown(label="Posture", choices=list(posture_mapping.keys()), value="") with gr.Row(): - clothing_style_input = gr.Dropdown(label="Style Vêtements", choices=list(clothing_style_mapping.keys()), value="") + clothing_style_input = gr.Dropdown(label="Style vêtements", choices=list(clothing_style_mapping.keys()), value="") accessories_input = gr.Dropdown(label="Accessoires", choices=list(accessories_mapping.keys()), value="") - reset_visuals_button = gr.Button("Réinitialiser Détails Visuels", size="sm") - - with gr.Column(scale=1): # Colonne de droite pour l'image et le bouton - # Use type="pil" to handle image in memory - persona_image_output = gr.Image(label="Image du Persona", type="pil", height=400, interactive=False) # Non éditable par l'utilisateur - # This button's interactivity is controlled by app_config_state.change() - generate_image_button = gr.Button("🖼️ Générer / Mettre à jour l'Image", interactive=False) # Start disabled - gr.Markdown("💡 **Attention :** Les IA génératrices d'images peuvent reproduire des stéréotypes. Utilisez les détails visuels avec discernement. La génération d'image nécessite une clé API OpenAI valide (voir onglet Configuration).", elem_classes="warning") - - # Logique de l'onglet 2 - visual_inputs = [ - skin_color_input, eye_color_input, hair_style_input, hair_color_input, - facial_expression_input, posture_input, clothing_style_input, accessories_input - ] + reset_visuals_button = gr.Button("Réinitialiser détails visuels", size="sm") + with gr.Column(scale=1): + # Image display + persona_image_output = gr.Image( + label="Image du persona", type="pil", + interactive=False + ) + generate_image_button = gr.Button("🖼️ Générer / Mettre à jour l'image", interactive=False) + gr.Markdown("💡 **Attention :** Les systèmes d'IA génératrice d'images peuvent reproduire des stéréotypes. La génération d'image nécessite une clé API OpenAI valide.", elem_classes="warning") + + visual_inputs = [skin_color_input, eye_color_input, hair_style_input, hair_color_input, facial_expression_input, posture_input, clothing_style_input, accessories_input] reset_visuals_button.click(lambda: [""] * len(visual_inputs), outputs=visual_inputs) - # Action du bouton Générer Image - # Defines an intermediate function to handle multiple outputs and messages def handle_image_generation(*args): - # The first input is app_config_state, the last is log_state - # The others are the persona fields - app_config = args[0] - log_state = args[-1] - persona_inputs = args[1:-1] # first_name, last_name, etc. - - # Call the generation function (which now gets client via get_active_client) - pil_image, updated_log, error_message = generate_persona_image_v2(app_config, *persona_inputs, log_state) - - # Prepare component updates - status_update_msg = "" # Message for the global status display - info_popup_msg = None # Message for gr.Info popup - + """Gère l'appel et les messages pour la génération d'image.""" + app_config = args[0]; log_state = args[-1]; persona_inputs = args[1:-1] + pil_image, updated_log, error_message = generate_persona_image(app_config, *persona_inputs, log_state) # Renommé + status_update_msg = ""; info_popup_msg = None if error_message: - if "Veuillez remplir" in error_message or "Génération d'image désactivée" in error_message: - info_popup_msg = error_message # Use popup for user guidance - else: - status_update_msg = error_message # Use global status for API/internal errors - - # Show popup if needed - if info_popup_msg: - gr.Info(info_popup_msg) - - # Return the PIL image (or None), the updated log, and the status message string + if "Veuillez remplir" in error_message or "désactivée" in error_message: info_popup_msg = error_message + else: status_update_msg = error_message # Erreurs API/internes dans le statut + if info_popup_msg: gr.Info(info_popup_msg) return pil_image, updated_log, status_update_msg - # Connect the button click + generate_image_inputs = [app_config_state, first_name_input, last_name_input, age_input, gender_input, persona_description_en_input] + visual_inputs + [session_log_state] + generate_image_outputs = [persona_image_pil_state, session_log_state, status_message_state] generate_image_button.click( - fn=handle_image_generation, - inputs=[app_config_state] + [ # Pass config state first - first_name_input, last_name_input, age_input, gender_input, persona_description_en_input, - skin_color_input, eye_color_input, hair_style_input, hair_color_input, - facial_expression_input, posture_input, clothing_style_input, accessories_input, - session_log_state # Pass log state last - ], - outputs=[ - persona_image_pil_state, # Update the PIL image state - session_log_state, # Update the log state - status_message_state # Update the status message state - ] - ).then( # Chain to update the image display from the state - fn=lambda img_state: img_state, - inputs=persona_image_pil_state, - outputs=persona_image_output - ).then( # Chain to update the global status display from the state - fn=update_status_display, # Use the existing update function - inputs=[status_message_state, session_log_state], - outputs=[status_display, session_log_state] + fn=handle_image_generation, inputs=generate_image_inputs, outputs=generate_image_outputs + ).then( + fn=lambda img_state: img_state, inputs=persona_image_pil_state, outputs=persona_image_output + ).then( + fn=update_status_display, inputs=[status_message_state, session_log_state], outputs=[status_display, session_log_state] ) - - # Update button interactivity when API config changes app_config_state.change( fn=lambda config: gr.update(interactive=config.get("image_generation_enabled", False)), - inputs=app_config_state, - outputs=generate_image_button + inputs=app_config_state, outputs=generate_image_button ) - - # --- Onglet 3 : Profil Détaillé & Raffinement IA --- - with gr.Tab("📝 Étape 3: Profil Détaillé & Raffinement IA", id=2): + # --- Onglet 3 : Profil Détaillé & Raffinement (Corrigé) --- + with gr.Tab("📝 Étape 3 : Profil détaillé & raffinement", id=2): gr.Markdown("### 3. Complétez les détails du persona") - gr.Markdown("Remplissez les champs suivants. Utilisez le bouton '💡 Affiner' pour obtenir des suggestions de l'IA visant à améliorer le champ spécifique, en tenant compte de votre objectif initial et des biais potentiels identifiés.") + gr.Markdown("Remplissez les champs suivants. Utilisez le bouton '💡 Affiner' pour obtenir des suggestions du système d'IA, en tenant compte de votre objectif et des biais identifiés.") + + # Zone pour afficher les suggestions d'affinement + refinement_suggestion_display = gr.Markdown(value="*Aucune suggestion pour le moment.*", elem_classes="suggestion-box") - # Organize into sections with gr.Row(): with gr.Column(): - gr.Markdown("#### Infos Socio-Démographiques") + gr.Markdown("#### Infos socio-démographiques") marital_status_input = gr.Dropdown(label="État civil", choices=["", "Célibataire", "En couple", "Marié(e)", "Divorcé(e)", "Veuf(ve)"]) - education_level_input = gr.Dropdown(label="Niveau d'éducation", choices=["", "Études secondaires", "Baccalauréat", "Licence", "Master", "Doctorat", "Autre"]) + education_level_input = gr.Dropdown(label="Niveau d'éducation", choices=["", "Secondaire", "Bac", "Licence", "Master", "Doctorat", "Autre"]) profession_input = gr.Textbox(label="Profession") - income_input = gr.Number(label="Revenus annuels (€)", minimum=0, step=1000) # Allow 0 + income_input = gr.Number(label="Revenus annuels (€)", minimum=0, step=1000) gr.Markdown("#### Psychographie") with gr.Row(equal_height=False): personality_traits_input = gr.Textbox(label="Traits de personnalité", lines=2, scale=4) - refine_personality_traits_button = gr.Button("💡 Affiner", scale=1, size='sm') + refine_personality_traits_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): values_beliefs_input = gr.Textbox(label="Valeurs et croyances", lines=2, scale=4) - refine_values_beliefs_button = gr.Button("💡 Affiner", scale=1, size='sm') + refine_values_beliefs_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): motivations_input = gr.Textbox(label="Motivations (objectifs, désirs)", lines=2, scale=4) - refine_motivations_button = gr.Button("💡 Affiner", scale=1, size='sm') + refine_motivations_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): hobbies_interests_input = gr.Textbox(label="Loisirs et intérêts", lines=2, scale=4) - refine_hobbies_interests_button = gr.Button("💡 Affiner", scale=1, size='sm') - + refine_hobbies_interests_button = gr.Button("💡", scale=1, size='sm') with gr.Column(): - gr.Markdown("#### Relation au Produit/Service") + gr.Markdown("#### Relation au produit/service") with gr.Row(equal_height=False): - technology_relationship_input = gr.Textbox(label="Relation avec la technologie", lines=2, scale=4, info="Ex: early adopter, prudent, technophobe, pragmatique...") - refine_technology_relationship_button = gr.Button("💡 Affiner", scale=1, size='sm') + technology_relationship_input = gr.Textbox(label="Relation avec la technologie", lines=2, scale=4, info="Ex: early adopter, prudent, pragmatique...") + refine_technology_relationship_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): - product_related_activities_input = gr.Textbox(label="Tâches/activités liées à votre produit/service", lines=2, scale=4) - refine_product_related_activities_button = gr.Button("💡 Affiner", scale=1, size='sm') + product_related_activities_input = gr.Textbox(label="Tâches/activités liées au produit/service", lines=2, scale=4) + refine_product_related_activities_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): pain_points_input = gr.Textbox(label="Points de douleur (frustrations, problèmes)", lines=2, scale=4) - refine_pain_points_button = gr.Button("💡 Affiner", scale=1, size='sm') + refine_pain_points_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): - product_goals_input = gr.Textbox(label="Objectifs en utilisant votre produit/service", lines=2, scale=4) - refine_product_goals_button = gr.Button("💡 Affiner", scale=1, size='sm') + product_goals_input = gr.Textbox(label="Objectifs avec le produit/service", lines=2, scale=4) + refine_product_goals_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): usage_scenarios_input = gr.Textbox(label="Scénarios d'utilisation typiques", lines=2, scale=4) - refine_usage_scenarios_button = gr.Button("💡 Affiner", scale=1, size='sm') + refine_usage_scenarios_button = gr.Button("💡", scale=1, size='sm') - # Other optional fields - with gr.Accordion("Autres Informations (Optionnel)", open=False): + with gr.Accordion("Autres informations (optionnel)", open=False): with gr.Row(): with gr.Column(): - gr.Markdown("#### Contexte Professionnel/Vie Quotidienne") + gr.Markdown("#### Contexte professionnel/vie quotidienne") with gr.Row(equal_height=False): - main_responsibilities_input = gr.Textbox(label="Responsabilités principales (pro/perso)", lines=2, scale=4) - refine_main_responsibilities_button = gr.Button("💡 Affiner", scale=1, size='sm') + main_responsibilities_input = gr.Textbox(label="Responsabilités principales", lines=2, scale=4) + refine_main_responsibilities_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): - daily_activities_input = gr.Textbox(label="Activités journalières typiques", lines=2, scale=4) - refine_daily_activities_button = gr.Button("💡 Affiner", scale=1, size='sm') + daily_activities_input = gr.Textbox(label="Activités journalières", lines=2, scale=4) + refine_daily_activities_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): - daily_life_input = gr.Textbox(label="Une journée type / Citation marquante", lines=2, scale=4) - refine_daily_life_button = gr.Button("💡 Affiner", scale=1, size='sm') - + daily_life_input = gr.Textbox(label="Journée type / Citation", lines=2, scale=4) + refine_daily_life_button = gr.Button("💡", scale=1, size='sm') with gr.Column(): - gr.Markdown("#### Marketing & Considérations Spéciales") + gr.Markdown("#### Marketing & considérations spéciales") with gr.Row(equal_height=False): brand_relationship_input = gr.Textbox(label="Relation avec la marque", lines=2, scale=4) - refine_brand_relationship_button = gr.Button("💡 Affiner", scale=1, size='sm') + refine_brand_relationship_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): market_segment_input = gr.Textbox(label="Segment de marché", lines=2, scale=4) - refine_market_segment_button = gr.Button("💡 Affiner", scale=1, size='sm') + refine_market_segment_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): - commercial_objectives_input = gr.Textbox(label="Objectifs commerciaux liés (SMART)", lines=2, scale=4) - refine_commercial_objectives_button = gr.Button("💡 Affiner", scale=1, size='sm') + commercial_objectives_input = gr.Textbox(label="Objectifs commerciaux liés", lines=2, scale=4) + refine_commercial_objectives_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): visual_codes_input = gr.Textbox(label="Codes visuels / Marques préférées", lines=2, scale=4) - refine_visual_codes_button = gr.Button("💡 Affiner", scale=1, size='sm') + refine_visual_codes_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): - special_considerations_input = gr.Textbox(label="Considérations spéciales (accessibilité, culturelles...)", lines=2, scale=4) - refine_special_considerations_button = gr.Button("💡 Affiner", scale=1, size='sm') + special_considerations_input = gr.Textbox(label="Considérations spéciales", lines=2, scale=4) + refine_special_considerations_button = gr.Button("💡", scale=1, size='sm') with gr.Row(equal_height=False): - references_input = gr.Textbox(label="Références / Sources de données", lines=2, scale=4) - refine_references_button = gr.Button("💡 Affiner", scale=1, size='sm') - + references_input = gr.Textbox(label="Références / Sources", lines=2, scale=4) + refine_references_button = gr.Button("💡", scale=1, size='sm') + + # Handler pour l'affichage des suggestions + def display_refinement_suggestion(suggestion_state): + """Met en forme et affiche la dernière suggestion.""" + if suggestion_state: + field_name, suggestion_text = suggestion_state + if "ERREUR:" not in suggestion_text: + return f"#### Suggestion pour '{field_name}' :\n\n{suggestion_text}" + else: + # Si c'est une erreur, elle est déjà dans le statut, on n'affiche rien ici + return "*Une erreur est survenue lors de la dernière tentative de raffinement (voir statut/log).*" + return "*Cliquez sur '💡' à côté d'un champ pour obtenir une suggestion.*" - # Intermediate function to handle refinement requests and display results + # Fonction intermédiaire pour gérer l'appel et le stockage de la suggestion def handle_refinement_request(app_config, fname, lname, age_val, field_name_display, field_val, bias_state_dict, objectives, log_state): - # Call the refinement function - updated_log, result = refine_persona_details_v2(app_config, fname, lname, age_val, field_name_display, field_val, bias_state_dict, objectives, log_state) - - status_update_msg = "" # For global status - # Display the result (suggestions or error) - if result: - if "ERREUR:" in result: - # Show API/internal errors in global status and as a warning popup - status_update_msg = result - gr.Warning(f"Erreur lors du raffinement pour '{field_name_display}'. Vérifiez le log et le statut.") - else: - # Show suggestions in an Info popup - gr.Info(f"Suggestions pour '{field_name_display}':\n{result}") - # No need to update global status for successful suggestions + """Appelle l'API et retourne log, message statut, et détails suggestion.""" + updated_log, result_text, field_name_ctx = refine_persona_details( # Renommé + app_config, fname, lname, age_val, field_name_display, field_val, bias_state_dict, objectives, log_state + ) + status_update_msg = "" + suggestion_details = None # Sera (field_name, result_text) ou None + + if result_text: + if "ERREUR:" in result_text: + status_update_msg = result_text + gr.Warning(f"Erreur raffinement pour '{field_name_display}'. Voir statut/log.") + # Ne pas stocker l'erreur comme suggestion affichable + else: + # Stocker les détails pour affichage via l'état dédié + suggestion_details = (field_name_display, result_text) + # Pas besoin de statut global pour une suggestion réussie else: - # Case where result is None or empty gr.Warning(f"Pas de suggestion reçue pour '{field_name_display}'.") - return updated_log, status_update_msg + return updated_log, status_update_msg, suggestion_details - # Generic lambda function to call the refinement handler + # Lambda générique def create_refine_handler(field_name_display, input_component): - # The lambda takes the inputs required by handle_refinement_request return lambda app_conf, fname, lname, age_val, field_val, bias_state, objectives, log_state: \ handle_refinement_request(app_conf, fname, lname, age_val, field_name_display, field_val, bias_state, objectives, log_state) - # Link each "Refine" button common_inputs_refine = [app_config_state, first_name_input, last_name_input, age_input] - # Pass the bias_analysis_result_state (which holds the dict) state_inputs_refine = [bias_analysis_result_state, objective_input, session_log_state] - # Outputs update the log and potentially the global status message state - common_outputs_refine = [session_log_state, status_message_state] + # Les sorties de handle_refinement_request + refine_handler_outputs = [session_log_state, status_message_state, last_refinement_suggestion_state] - # Map buttons to their corresponding input component and label refine_buttons_map = { refine_personality_traits_button: ("Traits de personnalité", personality_traits_input), refine_values_beliefs_button: ("Valeurs et croyances", values_beliefs_input), @@ -1039,7 +793,7 @@ with gr.Blocks(theme=gr.themes.Glass()) as demo: refine_usage_scenarios_button: ("Scénarios d'utilisation", usage_scenarios_input), refine_main_responsibilities_button: ("Responsabilités principales", main_responsibilities_input), refine_daily_activities_button: ("Activités journalières", daily_activities_input), - refine_daily_life_button: ("Journée type/Citation", daily_life_input), + refine_daily_life_button: ("Journée type / Citation", daily_life_input), refine_brand_relationship_button: ("Relation marque", brand_relationship_input), refine_market_segment_button: ("Segment marché", market_segment_input), refine_commercial_objectives_button: ("Objectifs commerciaux", commercial_objectives_input), @@ -1052,136 +806,84 @@ with gr.Blocks(theme=gr.themes.Glass()) as demo: btn.click( fn=create_refine_handler(label, input_comp), inputs=common_inputs_refine + [input_comp] + state_inputs_refine, - outputs=common_outputs_refine - ).then( # Chain to update the global status display from the state - fn=update_status_display, - inputs=[status_message_state, session_log_state], - outputs=[status_display, session_log_state] + outputs=refine_handler_outputs # Met à jour log, statut, et état suggestion + ).then( # Chaîne pour mettre à jour l'affichage du statut + fn=update_status_display, + inputs=[status_message_state, session_log_state], + outputs=[status_display, session_log_state] + ).then( # Chaîne pour mettre à jour l'affichage de la suggestion + fn=display_refinement_suggestion, + inputs=[last_refinement_suggestion_state], + outputs=[refinement_suggestion_display] ) - - # --- Onglet 4 : Résumé du Persona --- - with gr.Tab("📄 Étape 4: Résumé du Persona", id=3): + # --- Onglet 4 : Résumé Persona (Corrigé) --- + with gr.Tab("📄 Étape 4 : Résumé du persona", id=3): gr.Markdown("### 4. Visualisez le persona complet") - summary_button = gr.Button("Générer le Résumé du Persona") - # Use Markdown to display the HTML summary + summary_button = gr.Button("Générer le résumé du persona") summary_content = gr.Markdown(elem_classes="persona-summary", value="Cliquez sur 'Générer' pour voir le résumé.") - # Collect all inputs for the summary IN THE CORRECT ORDER for generate_summary_v2 - all_persona_inputs_for_summary = [ + # Inputs pour generate_summary (l'image vient en premier maintenant) + all_persona_inputs_for_summary = [persona_image_pil_state] + [ # Image PIL state first first_name_input, last_name_input, age_input, gender_input, persona_description_en_input, - skin_color_input, eye_color_input, hair_style_input, hair_color_input, - facial_expression_input, posture_input, clothing_style_input, accessories_input, + skin_color_input, eye_color_input, hair_style_input, hair_color_input, facial_expression_input, posture_input, clothing_style_input, accessories_input, marital_status_input, education_level_input, profession_input, income_input, personality_traits_input, values_beliefs_input, motivations_input, hobbies_interests_input, main_responsibilities_input, daily_activities_input, technology_relationship_input, product_related_activities_input, pain_points_input, product_goals_input, usage_scenarios_input, brand_relationship_input, market_segment_input, commercial_objectives_input, visual_codes_input, special_considerations_input, daily_life_input, references_input, - # Add necessary states last - persona_image_pil_state, # Pass the state containing the PIL image - session_log_state + session_log_state # Log state last ] - summary_button.click( - fn=generate_summary_v2, + fn=generate_summary, # Renommé inputs=all_persona_inputs_for_summary, - outputs=[summary_content, session_log_state] # Updates content and log + outputs=[summary_content, session_log_state] ) - # --- Onglet 5 : Journal de Bord --- - with gr.Tab("📓 Journal de Bord", id=4): - gr.Markdown("### Suivi du Processus de Création") - gr.Markdown("Ce journal enregistre les étapes clés, les réflexions et les erreurs de votre session.") + # --- Onglet 5 : Journal de Bord (Corrigé) --- + with gr.Tab("📓 Journal de bord", id=4): + gr.Markdown("### Suivi du processus de création") + gr.Markdown("Ce journal enregistre les étapes clés, réflexions et erreurs de la session.") log_display_final = gr.Textbox(label="Historique de la session", lines=20, interactive=False, max_lines=MAX_LOG_LINES) - # Use gr.DownloadButton for better UX - download_log_button = gr.DownloadButton(label="Télécharger le Journal", visible=False) # Hidden initially - - # Update log display when state changes - session_log_state.change( - fn=lambda log_data: log_data, - inputs=session_log_state, - outputs=log_display_final, - # Add queue=False to make UI update instantly for log - # queue=False # Might cause issues if log updates very rapidly? Test. - ) + download_log_button = gr.DownloadButton(label="Télécharger le journal", visible=False) + export_log_button_final = gr.Button("Préparer l'export du journal") - # Update global status display when its state changes - # This might be redundant if status updates always accompany log updates, but safe to keep - # status_message_state.change( - # fn=update_status_display, - # inputs=[status_message_state, session_log_state], - # outputs=[status_display, session_log_state] - # ) + session_log_state.change(fn=lambda log_data: log_data, inputs=session_log_state, outputs=log_display_final) - # Function to prepare the log file for the DownloadButton - # Keep this outside the click if possible, or ensure it's fast def prepare_log_for_download(log_data): - if not log_data: - return gr.update(visible=False) # Keep button hidden if no log - + """Prépare le fichier log temporaire pour le téléchargement.""" + if not log_data: return gr.update(visible=False) try: - # Create a temporary text file Gradio can serve with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt', encoding='utf-8') as temp_file: temp_file.write(log_data) temp_filepath = temp_file.name - print(f"Fichier log prêt pour téléchargement : {temp_filepath}") - # Return the path for the DownloadButton and make it visible - # Gradio is supposed to handle cleanup. - return gr.update(value=temp_filepath, visible=True) + print(f"Fichier log prêt: {temp_filepath}") + return gr.update(value=temp_filepath, visible=True) # Met à jour DownloadButton except Exception as e: - print(f"Erreur création fichier log pour téléchargement: {e}") - # Update status display with error? - # update_status_display(f"Erreur export log: {e}", log_data) # Careful with state updates here - return gr.update(visible=False) # Keep button hidden on error - - - # Instead of a separate export button, trigger preparation when log changes? Or use DownloadButton directly? - # Let's use the DownloadButton's direct file generation capability if possible. - - # Simpler approach: Directly provide the generating function to DownloadButton - def generate_log_content(log_data): - if not log_data: - return None # Or raise an error? Gradio might handle None better. - # Return the content directly, Gradio handles file creation - filename = f"personagenai_log_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" - # Must return a file path or bytes/BytesIO - log_bytes = log_data.encode('utf-8') - temp_file = io.BytesIO(log_bytes) - temp_file.name = filename # Suggest a filename - # Returning BytesIO might not work directly with gr.DownloadButton value generation - # Let's stick to the NamedTemporaryFile approach, triggered by a separate button - - # Re-add the export button to trigger the file prep - export_log_button_final = gr.Button("Préparer l'Export du Journal") - - export_log_button_final.click( - fn=prepare_log_for_download, - inputs=session_log_state, - outputs=download_log_button # Update the DownloadButton (makes visible, sets path) - ) + print(f"Erreur création fichier log: {e}") + return gr.update(visible=False) + export_log_button_final.click(prepare_log_for_download, session_log_state, download_log_button) -# --- Launch App --- -# Initial check for OpenRouter key +# --- Lancement de l'Application (Corrigé) --- if not openrouter_api_key: - print("\n" + "="*60) - print("AVERTISSEMENT : Clé API OpenRouter (`OPENROUTER_API_KEY`) non trouvée.") - print("Le fonctionnement dépendra de la fourniture d'une clé OpenAI valide.") - print("="*60 + "\n") - # Initialize app_config_state accordingly in the Gradio definition? - # The initial state already reflects this with "openrouter_key_provided": False - -# Set initial API status message based on initial config possibilities -initial_api_status = "Statut API: Non configuré." + print("\n" + "="*60 + "\nAVERTISSEMENT : Clé API OpenRouter non trouvée.\nLe fonctionnement dépendra d'une clé OpenAI valide.\n" + "="*60 + "\n") + +# Configuration initiale si OpenRouter est disponible +initial_api_status = "Statut API : Non configuré." if openrouter_api_key: - # Attempt to configure OpenRouter by default if key exists print("Clé OpenRouter trouvée, tentative de configuration initiale...") - initial_config, initial_api_status, initial_log = configure_api_clients(None, app_config_state.value, "") - app_config_state.value = initial_config # Update initial state value - session_log_state.value = initial_log - print(initial_api_status) # Print status to console - # We need to update the Markdown display default value too - api_status_display.value = initial_api_status # Set initial value for the Markdown component - -demo.queue().launch(debug=False, share=False) # debug=True helpful for development \ No newline at end of file + try: + initial_config, initial_api_status, initial_log = configure_api_clients(None, app_config_state.value, "") + app_config_state.value = initial_config + session_log_state.value = initial_log + print(initial_api_status) + api_status_display.value = initial_api_status # Met à jour la valeur initiale du composant Markdown + except Exception as init_e: + print(f"Erreur lors de la configuration initiale d'OpenRouter: {init_e}") + initial_api_status = f"❌ Erreur configuration initiale OpenRouter: {init_e}" + api_status_display.value = initial_api_status # Affiche l'erreur dans l'UI + +# Lancement avec PWA activé +demo.queue().launch(debug=False, share=False, pwa=True) \ No newline at end of file