diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,45 +1,43 @@ -# --- Imports --- import gradio as gr from openai import OpenAI -import openai # Import top-level for error types +import openai from pydantic import BaseModel, Field import os import requests from PIL import Image import tempfile -import io # For BytesIO -import markdown # Required by gr.Markdown implicitly +import io +import markdown import base64 import datetime import json -import re # For cleaning JSON +import re from dotenv import load_dotenv -# --- Configuration Initiale --- -load_dotenv() # Charge les variables depuis un fichier .env s'il existe +load_dotenv() # Clé OpenRouter openrouter_api_key = os.getenv("OPENROUTER_API_KEY") -# 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 +OPENROUTER_TEXT_MODEL = os.getenv("OPENROUTER_TEXT_MODEL", "mistralai/mistral-small-3.1-24b-instruct:free") -# Modèles OpenAI (utilisés si clé fournie) -OPENAI_TEXT_MODEL = "gpt-4o-mini" # ou "gpt-4o" +# Modèles OpenAI +OPENAI_TEXT_MODEL = "gpt-4o-mini" OPENAI_IMAGE_MODEL = "dall-e-3" -# --- Modèles Pydantic (Inchangé) --- +# Modèle Image via OpenRouter +OPENROUTER_IMAGE_MODEL = "openai/dall-e-3" + +# --- Modèles Pydantic --- 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.") - advice: str = Field(..., description="Conseil spécifique pour atténuer ce biais.") + bias_type: str = Field(..., description="Type de biais identifié") + explanation: str = Field(..., description="Explication contextuelle") + advice: str = Field(..., description="Conseil d'atténuation") 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é.") + detected_biases: list[BiasInfo] = Field(default_factory=list) + overall_comment: str = Field(default="") # --- 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"} skin_color_mapping = {"": "","Clair": "light","Moyen": "medium","Foncé": "dark","Très foncé": "very dark"} @@ -48,106 +46,98 @@ hair_style_mapping = {"": "","Court": "short","Long": "long","Bouclé": "curly", hair_color_mapping = {"": "","Blond": "blonde","Brun": "brown","Noir": "black","Roux": "red","Gris": "gray","Blanc": "white"} clothing_style_mapping = {"": "","Décontracté": "casual","Professionnel": "professional","Sportif": "sporty"} accessories_mapping = {"": "","Lunettes": "glasses","Montre": "watch","Chapeau": "hat"} +gender_mapping = {"Homme": "man", "Femme": "woman", "Non-binaire": "non-binary person"} -# 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 "" log_lines = current_log.splitlines() - if len(log_lines) >= MAX_LOG_LINES: - current_log = "\n".join(log_lines[-(MAX_LOG_LINES-1):]) + 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() - -# 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.""" match = re.search(r"```json\s*({.*?})\s*```", raw_response, re.DOTALL | re.IGNORECASE) - if match: - return match.group(1) - start = raw_response.find('{') - end = raw_response.rfind('}') + if match: return match.group(1) + start = raw_response.find('{'); end = raw_response.rfind('}') if start != -1 and end != -1 and end > start: potential_json = raw_response[start:end+1] - try: - json.loads(potential_json) - return potential_json + try: json.loads(potential_json); return potential_json except json.JSONDecodeError: cleaned = re.sub(r",\s*([}\]])", r"\1", potential_json) - try: - json.loads(cleaned) - return cleaned - except json.JSONDecodeError: - pass + try: json.loads(cleaned); return cleaned + except json.JSONDecodeError: pass return raw_response.strip() -# --- Holder pour Client API Actif (hors gr.Blocks) --- -active_api_client_holder = { - "client": None, - "openai_key": None -} -# --- Fonctions Principales de l'Application --- +# --- Holder Client API --- +active_api_client_holder = {"client": None, "openai_key": None} +# --- Fonctions Principales --- def get_active_client(app_config): """Récupère le client stocké globalement.""" api_source = app_config.get("api_source") - if not api_source: - return None, "Source API non configurée." + if not api_source: return None, "Source API non configurée." client = active_api_client_holder.get("client") if not client: 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}" + active_api_client_holder["client"] = client; print("Client OpenAI ré-initialisé.") + except Exception as e: return None, f"Échec ré-init 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"Client API pour '{api_source}' non disponible." + client = OpenAI(base_url="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é-init OpenRouter: {e}" + else: return None, f"Impossible ré-init client pour '{api_source}'. Clé/config manquante." + if not client: return None, f"Client API pour '{api_source}' non disponible." return client, None def analyze_biases(app_config, objective_text, session_log_state): - """Analyse les biais dans l'objectif marketing.""" + """Analyse les biais dans l'objectif marketing en forçant un format JSON.""" log = session_log_state 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.").model_dump(), 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) - return BiasAnalysisResponse(overall_comment=f"Erreur: {error_msg}").dict(), log + return BiasAnalysisResponse(overall_comment=f"Erreur: {error_msg}").model_dump(), log - model_name = app_config["text_model"] + model_name = app_config.get("text_model") + api_source = app_config.get("api_source") + + # --- Génération du Schéma JSON --- + bias_schema = None + try: + bias_schema = BiasAnalysisResponse.model_json_schema() + + except Exception as schema_e: + log = update_log(f"ERREUR Génération schéma Pydantic: {schema_e}", log) + return BiasAnalysisResponse(overall_comment=f"Erreur interne génération schéma: {schema_e}").model_dump(), log + + # --- System Prompt --- system_prompt = f""" - 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}" + 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}" - 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é...). + 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). Pour chaque biais potentiel identifié : - - 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. + - 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. - Structurez votre réponse en utilisant le format JSON suivant (schéma Pydantic BiasAnalysisResponse): + Structure TOUTE ta réponse EXCLUSIVEMENT en utilisant le format JSON suivant (basé sur la classe Pydantic BiasAnalysisResponse): {{ "detected_biases": [ {{ @@ -155,46 +145,207 @@ def analyze_biases(app_config, objective_text, session_log_state): "explanation": "Explication contextuelle du risque.", "advice": "Conseil spécifique d'atténuation." }} + // ... autres biais détectés ... ], - "overall_comment": "Bref commentaire général. Indiquez si aucun biais majeur n'est détecté." + "overall_comment": "Bref commentaire général. Indique si aucun biais majeur n'est détecté." }} - 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'. + 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'. + Assure-toi que la sortie est un objet JSON unique et valide correspondant exactement à cette structure. Ne retourne AUCUN texte avant ou après le JSON. """ + response_content_str = "" try: + # --- Choix dynamique du response_format --- + response_format_config = None + + if api_source == "openai": + response_format_config = {"type": "json_object"} + log = update_log(f"INFO: Utilisation response_format=json_object pour OpenAI ({model_name})", log) + elif api_source == "openrouter" and bias_schema: + response_format_config = { + "type": "json_schema", + "json_schema": { + "name": "bias_analysis", + "strict": True, + "description": "Analyse des biais potentiels dans un objectif marketing.", + "schema": bias_schema + } + } + log = update_log(f"INFO: Utilisation response_format=json_schema pour OpenRouter ({model_name})", log) + else: + log = update_log(f"WARN: Aucun response_format spécifique appliqué pour {api_source}", log) + + # --- Appel API --- completion = active_client.chat.completions.create( model=model_name, messages=[{"role": "user", "content": system_prompt}], - temperature=0.4, max_tokens=800, - response_format={"type": "json_object"}, + temperature=0, + max_tokens=2400, + response_format=response_format_config, ) - response_content_str = completion.choices[0].message.content - cleaned_response_str = clean_json_response(response_content_str) + raw_response_content = completion.choices[0].message.content + + # --- Parsing de la réponse --- try: - parsed_response = BiasAnalysisResponse.parse_raw(cleaned_response_str) + parsed_response = BiasAnalysisResponse.model_validate_json(raw_response_content) 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 parsing réponse: {parse_error}").dict(), log + return parsed_response.model_dump(), log + except (json.JSONDecodeError, TypeError, ValueError) as direct_parse_error: + format_type = response_format_config.get('type', 'inconnu') if response_format_config else 'inconnu' + log = update_log(f"ERREUR Parsing direct réponse JSON (mode {format_type}): {direct_parse_error}. Contenu brut: {raw_response_content!r}", log) + cleaned_response_str = clean_json_response(str(raw_response_content)) + try: + parsed_response = BiasAnalysisResponse.model_validate_json(cleaned_response_str) + log = update_log(f"Analyse biais objectif (fin après clean): Biais trouvés - {len(parsed_response.detected_biases)}", log) + return parsed_response.model_dump(), log + except Exception as final_parse_error: + error_msg_detail = f"Erreur parsing JSON final: {final_parse_error}. Nettoyé: '{cleaned_response_str[:200]}...'" + print(error_msg_detail) + log = update_log(f"ERREUR Analyse biais parsing final (mode {format_type}): {final_parse_error}", log) + return BiasAnalysisResponse(overall_comment=f"Erreur technique parsing réponse JSON (mode {format_type}): {final_parse_error}").model_dump(), log + + # --- Gestion des erreurs API --- + except openai.BadRequestError as e: + error_type = type(e).__name__; error_details = repr(e) + user_error_msg = f"Erreur Requête API ({error_type}). Vérifiez param/modèle." + log_msg_prefix = f"ERREUR API Call ({api_source}, {model_name})" + + if "response_format" in str(e): + user_error_msg += f" Problème format réponse ({response_format_config.get('type', '?') if response_format_config else '?'})." + log_msg = f"{log_msg_prefix}: Problème format réponse. Détails: {error_details}" + elif "model" in str(e): + user_error_msg += " Modèle invalide ou non trouvé." + log_msg = f"{log_msg_prefix}: Modèle invalide. Détails: {error_details}" + else: + log_msg = f"{log_msg_prefix}: {str(e)}. Détails: {error_details}" + + print(log_msg); log = update_log(log_msg, log) + return BiasAnalysisResponse(overall_comment=user_error_msg).model_dump(), log + except openai.AuthenticationError as e: - 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 + error_msg = f"Erreur auth API ({api_source}). Vérifiez clé."; print(error_msg) + log = update_log(f"ERR API Auth ({api_source}): {error_msg}", log) + return BiasAnalysisResponse(overall_comment=error_msg).model_dump(), log + except openai.RateLimitError as e: - 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 + error_msg = f"Erreur API ({api_source}): Limite taux atteinte."; print(error_msg) + log = update_log(f"ERR API RateLimit ({api_source}): {error_msg}", log) + return BiasAnalysisResponse(overall_comment=error_msg).model_dump(), log + + except Exception as e: + error_type = type(e).__name__; error_details = repr(e) + user_error_msg = f"Erreur technique analyse ({error_type}). Vérifiez connexion/modèle." + log_msg = f"ERR Analyse biais API Call ({error_type} sur {api_source}, {model_name}): {str(e)}. Détails: {error_details}" + print(log_msg); log = update_log(log_msg, log) + return BiasAnalysisResponse(overall_comment=user_error_msg).model_dump(), log + """Analyse les biais dans l'objectif marketing en forçant un schéma JSON.""" + log = session_log_state + 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.").model_dump(), 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) + return BiasAnalysisResponse(overall_comment=f"Erreur: {error_msg}").model_dump(), log + + model_name = app_config["text_model"] + + # --- Génération et MODIFICATION du Schéma JSON --- + try: + bias_schema = BiasAnalysisResponse.model_json_schema() + + except Exception as schema_e: + log = update_log(f"ERREUR Génération schéma Pydantic: {schema_e}", log) + return BiasAnalysisResponse(overall_comment=f"Erreur interne génération schéma: {schema_e}").model_dump(), log + + # --- System Prompt --- + 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}" + + 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). + + 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. + + Structure ta réponse en utilisant le format JSON suivant (avec la classe Pydantic BiasAnalysisResponse): + {{ + "detected_biases": [ + {{ + "bias_type": "Type de biais identifié", + "explanation": "Explication contextuelle du risque.", + "advice": "Conseil spécifique d'atténuation." + }} + ], + "overall_comment": "Bref commentaire général. Indique 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'. + Ne retourne PAS de texte brut ou d'explications supplémentaires. Utilise uniquement le format JSON ci-dessus. + """ + response_content_str = "" + try: + # --- Appel API avec Structured Output --- + completion = active_client.chat.completions.create( + model=model_name, + messages=[{"role": "user", "content": system_prompt}], + temperature=0.2, + max_tokens=2400, + response_format={ + "type": "json_schema", + "json_schema": { + "name": "bias_analysis", + "strict": True, + "description": "Analyse des biais potentiels dans un objectif marketing.", + "schema": bias_schema + } + }, + ) + raw_response_content = completion.choices[0].message.content + + try: + parsed_response = BiasAnalysisResponse.model_validate_json(raw_response_content) + log = update_log(f"Analyse biais objectif (fin): Biais trouvés - {len(parsed_response.detected_biases)}", log) + return parsed_response.model_dump(), log + except (json.JSONDecodeError, TypeError, ValueError) as direct_parse_error: + log = update_log(f"ERREUR Parsing direct réponse JSON Schema: {direct_parse_error}. Contenu brut: {raw_response_content!r}", log) + cleaned_response_str = clean_json_response(str(raw_response_content)) + try: + parsed_response = BiasAnalysisResponse.model_validate_json(cleaned_response_str) + log = update_log(f"Analyse biais objectif (fin après clean): Biais trouvés - {len(parsed_response.detected_biases)}", log) + return parsed_response.model_dump(), log + except Exception as final_parse_error: + error_msg = f"Erreur parsing JSON final: {final_parse_error}. Nettoyé: '{cleaned_response_str[:200]}...'" + print(error_msg); log = update_log(f"ERREUR Analyse biais parsing final: {final_parse_error}", log) + return BiasAnalysisResponse(overall_comment=f"Erreur technique parsing réponse JSON Schema: {final_parse_error}").model_dump(), log + + except openai.BadRequestError as e: + error_type = type(e).__name__; error_details = repr(e) + user_error_msg = f"Erreur Requête API ({error_type}). Vérifiez les paramètres/schéma." + if "Invalid schema for response_format" in str(e): + user_error_msg += " Problème avec le format de réponse demandé." + log_msg = f"ERREUR API Call: Schéma JSON invalide selon l'API. Détails: {error_details}" + else: + log_msg = f"ERREUR API Call ({error_type}): {str(e)}. Détails: {error_details}" + print(log_msg); log = update_log(log_msg, log) + return BiasAnalysisResponse(overall_comment=user_error_msg).model_dump(), log + except openai.AuthenticationError as e: error_msg = f"Erreur auth API ({app_config.get('api_source', '?')}). Vérifiez clé."; print(error_msg); log = update_log(f"ERR API Auth: {error_msg}", log); return BiasAnalysisResponse(overall_comment=error_msg).model_dump(), log + except openai.RateLimitError as e: error_msg = f"Erreur API ({app_config.get('api_source', '?')}): Limite taux."; print(error_msg); log = update_log(f"ERR API RateLimit: {error_msg}", log); return BiasAnalysisResponse(overall_comment=error_msg).model_dump(), log except Exception as e: - 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 + error_type = type(e).__name__; error_details = repr(e) + user_error_msg = f"Erreur technique analyse ({error_type}). Vérifiez connexion/modèle." + log_msg = f"ERR Analyse biais API Call ({error_type}): {str(e)}. Détails: {error_details}" + print(log_msg); log = update_log(log_msg, log) + return BiasAnalysisResponse(overall_comment=user_error_msg).model_dump(), log -# --- display_bias_analysis (Unchanged from V2) --- +# --- display_bias_analysis --- 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", "") @@ -204,13 +355,14 @@ def display_bias_analysis(analysis_result): else: 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")) + highlighted_data.append((f"⚠️ {bias_info.get('bias_type', '?')}: ", "BIAS_TYPE")) + highlighted_data.append((f"{bias_info.get('explanation', '-')}\n", "EXPLANATION")) + highlighted_data.append((f"💡 Conseil: {bias_info.get('advice', '-')}\n", "ADVICE")) return highlighted_data +# --- generate_persona_image --- def generate_persona_image(app_config, *args): - """Génère l'image du persona.""" + """Génère l'image du persona via OpenAI ou OpenRouter.""" inputs = args[:-1] session_log_state = args[-1] log = session_log_state @@ -218,168 +370,194 @@ def generate_persona_image(app_config, *args): skin_color, eye_color, hair_style, hair_color, facial_expression, posture, clothing_style, accessories) = inputs - 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." + api_source = app_config.get("api_source") + image_gen_enabled = app_config.get("image_generation_enabled", False) + + if not image_gen_enabled: + log = update_log("Génération image: Désactivée (API non configurée ou non supportée).", log) + return None, log, "Génération d'image désactivée ou non supportée par la configuration API actuelle." + + active_client, client_error_msg = get_active_client(app_config) + if client_error_msg: + log = update_log(f"ERREUR Génération image (Client): {client_error_msg}", log) + return None, log, f"Erreur client API pour génération image: {client_error_msg}" - openai_client, error_msg = get_active_client(app_config) - if error_msg or app_config.get("api_source") != "openai": - 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." - 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]}.") - if hair_color_mapping.get(hair_color): prompt_parts.append(f"Hair color: {hair_color_mapping[hair_color]}.") - 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]}.") - 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) - + # --- Construction du Prompt --- + gender_en = gender_mapping.get(gender, "person") # Utiliser une valeur par défaut + lens_aperture = "shot on Kodak Portra 400" + lighting = "soft natural window light" + photo_style_details = f"portrait {lighting}, shot on {lens_aperture}, shallow depth of field" + + base_description = ( + f"{photo_style_details} of {first_name} {last_name}, " + f"a {age}-year-old {gender_en}. " + ) + + # Ajout des détails optionnels (moins d'emphase sur "skin texture") + details = "" + if skin_color_mapping.get(skin_color): details += f"Skin tone: {skin_color_mapping[skin_color]}. " + if eye_color_mapping.get(eye_color): details += f"Eye color: {eye_color_mapping[eye_color]}. " + if hair_style_mapping.get(hair_style): details += f"Hairstyle: {hair_style_mapping[hair_style]}. " + if hair_color_mapping.get(hair_color): details += f"Hair color: {hair_color_mapping[hair_color]}. " + if facial_expression_mapping.get(facial_expression): details += f"Facial expression: {facial_expression_mapping[facial_expression]}. " + if accessories_mapping.get(accessories): details += f"Wearing: {accessories_mapping[accessories]}. " + if clothing_style_mapping.get(clothing_style): details += f"Wearing {clothing_style_mapping[clothing_style]} clothing. " + # Le contexte peut être utile pour l'environnemental + if persona_description_en: details += f"Context: {persona_description_en}. " + + # Négatifs (garder ce qui est pertinent) + negatives = "Avoid: illustration, 3D render, plastic look, doll appearance, uncanny valley, deformed, blurry, bad anatomy, extra limbs, text, words, signature." + + final_prompt = f"{base_description}{details}{negatives}" + + log = update_log(f"Génération image (début via {api_source}): Prompt='{final_prompt[:100]}...'", log) + + pil_image = None try: - response = openai_client.images.generate( - 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() - pil_image = Image.open(io.BytesIO(img_response.content)) - log = update_log("Génération image (fin): Succès.", log) + if api_source == "openai": + response = active_client.images.generate( + model=OPENAI_IMAGE_MODEL, prompt=final_prompt, size="1024x1024", n=1, + response_format="url", quality="hd" + ) + image_url = response.data[0].url + img_response = requests.get(image_url) + img_response.raise_for_status() + pil_image = Image.open(io.BytesIO(img_response.content)) + + elif api_source == "openrouter": + # --- Appel via OpenRouter Chat Completions --- + response = active_client.chat.completions.create( + model=OPENROUTER_IMAGE_MODEL, + messages=[{"role": "user", "content": final_prompt}], + ) + + headers = { + "Authorization": f"Bearer {openrouter_api_key}", + "Content-Type": "application/json", + } + payload = { + "model": OPENROUTER_IMAGE_MODEL, + "messages": [{"role": "user", "content": final_prompt}], + "max_tokens": 150, + "n": 1, + } + api_url = "https://openrouter.ai/api/v1/chat/completions" + http_response = requests.post(api_url, headers=headers, json=payload) + http_response.raise_for_status() + response_data = http_response.json() + + image_base64_data = None + if response_data.get("choices"): + message = response_data["choices"][0].get("message", {}) + content = message.get("content") + if isinstance(content, list): + for part in content: + if part.get("type") == "image_url": + image_url_obj = part.get("image_url", {}) + image_base64_data = image_url_obj.get("url") + break + + if not image_base64_data: + log = update_log(f"ERREUR Image OpenRouter: Image non trouvée dans la réponse. Réponse: {str(response_data)[:500]}", log) + raise ValueError("Réponse OpenRouter ne contient pas d'URL d'image base64.") + + if not image_base64_data.startswith("data:image"): + raise ValueError(f"URL d'image invalide reçue: {image_base64_data[:100]}...") + image_base64_string = image_base64_data.split(',', 1)[1] + image_bytes = base64.b64decode(image_base64_string) + pil_image = Image.open(io.BytesIO(image_bytes)) + + else: + raise ValueError(f"Source API non supportée pour la génération d'image: {api_source}") + + log = update_log(f"Génération image (fin via {api_source}): Succès.", log) return pil_image, log, None - except openai.AuthenticationError as e: - 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 = "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: - 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 (openai.AuthenticationError, openai.RateLimitError, openai.BadRequestError, requests.exceptions.RequestException, ValueError, KeyError) as e: + error_type = type(e).__name__ + error_msg_detail = str(e) + if hasattr(e, 'response') and e.response is not None: + try: error_msg_detail += f" | Détail API: {e.response.text[:200]}" + except: pass + user_error_msg = f"Erreur génération image via {api_source} ({error_type})." + full_log_msg = f"ERREUR Génération image via {api_source} ({error_type}): {error_msg_detail}" + print(full_log_msg) + log = update_log(full_log_msg, log) + return None, log, user_error_msg except Exception as e: - 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 + error_type = type(e).__name__ + error_msg = f"Erreur inattendue génération image via {api_source} ({error_type}): {str(e)}" + print(error_msg); log = update_log(error_msg, log) + return None, log, f"Erreur inattendue ({error_type}) lors de la génération d'image." + +# --- refine_persona_details --- 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"Raffinement (début): Champ='{field_name}', Valeur initiale='{field_value[:50]}...'", log) + log = update_log(f"Raffinement (début): Champ='{field_name}', Valeur='{field_value[:50]}...'", log) active_client, error_msg = get_active_client(app_config) - if error_msg: - 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 + if error_msg: log = update_log(f"ERREUR Raffinement: {error_msg}", log); return log, f"ERREUR: {error_msg}", field_name model_name = app_config["text_model"] - biases_text = "Aucune analyse de biais précédente disponible." + biases_text = "Aucune analyse de biais précédente." 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.") - except Exception as e: - biases_text = f"Erreur lecture biais analysés: {e}" - log = update_log(f"ERREUR Lecture Biais Dict pour Raffinement: {e}", log) + detected = bias_analysis_dict.get("detected_biases", []) + biases_text = "\n".join([f"- {b.get('bias_type','?')}: {b.get('explanation','-')}" for b in detected]) if detected else bias_analysis_dict.get("overall_comment", "Aucun biais majeur détecté.") + except Exception as e: biases_text = f"Err lecture biais: {e}"; log = update_log(f"ERR Lecture Biais Dict: {e}", log) system_prompt = f""" - Vous êtes un assistant expert en marketing éthique, aidant à affiner le persona marketing pour '{first_name} {last_name}' ({age} ans). + Tu es un assistant IA 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 : 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 à : + 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 à : - 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 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. - """ + 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. + Ne fournis pas d'explications supplémentaires ou de texte brut. Utilise un format clair et concis.""" suggestions = "" try: response = active_client.chat.completions.create( - model=model_name, - messages=[{"role": "user", "content": system_prompt}], - temperature=0.6, max_tokens=150, + model=model_name, messages=[{"role": "user", "content": system_prompt}], + temperature=0.6, max_tokens=800, ) suggestions = response.choices[0].message.content.strip() 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 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}", 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 Raffinement '{field_name}': {str(e)}", log) - return log, f"ERREUR: {error_msg}", field_name + except openai.AuthenticationError as e: error_msg = f"Erreur auth API ({app_config.get('api_source', '?')}) raffinement."; print(error_msg); log = update_log(f"ERR 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', '?')}) (Refine): Limite taux."; print(error_msg); log = update_log(f"ERR API RateLimit (Refine): {error_msg}", log); return log, f"ERREUR: {error_msg}", field_name + except Exception as e: error_msg = f"Erreur raffinement '{field_name}': {str(e)}"; print(error_msg); log = update_log(f"ERR Raffinement '{field_name}': {str(e)}", log); return log, f"ERREUR: {error_msg}", field_name +# --- generate_summary --- 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 - (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, - product_related_activities, pain_points, product_goals, usage_scenarios, - brand_relationship, market_segment, commercial_objectives, - visual_codes, special_considerations, daily_life, references - ) = inputs - + session_log_state = args[-1]; inputs = args[:-1]; log = session_log_state + (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, product_related_activities, pain_points, product_goals, usage_scenarios, brand_relationship, market_segment, commercial_objectives, visual_codes, special_considerations, daily_life, references) = inputs log = update_log(f"Génération résumé: Pour '{first_name} {last_name}'.", log) - summary = "" - image_html = "
\n" - + summary = ""; 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" + summary += "

Infos base manquantes

Prénom, nom, âge requis (Étape 2).

"; image_html += "

Image non générée.

" else: if persona_image_pil and isinstance(persona_image_pil, Image.Image): try: - buffered = io.BytesIO() - 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') - 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}" - # Style pour l'image : largeur max 100%, hauteur auto pour responsivité dans la colonne + buffered = io.BytesIO(); img_to_save = persona_image_pil.copy() + if img_to_save.mode == 'RGBA' or 'transparency' in img_to_save.info: img_to_save = img_to_save.convert('RGB') + img_to_save.save(buffered, format="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" - except Exception as e: - img_err_msg = f"Erreur encodage image: {e}" - image_html += f"

{img_err_msg}

\n" - log = update_log(f"ERREUR Encodage Image Résumé: {e}", log) - else: - image_html += "

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

\n" - - summary += f"

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

\n" - + except Exception as e: img_err_msg = f"Erreur encodage image: {e}"; image_html += f"

{img_err_msg}

"; log = update_log(f"ERR Encodage Image Résumé: {e}", log) + else: image_html += "

Aucune image disponible.

" + summary += f"

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

" def add_section(title, fields): content = "" for label, value in fields.items(): @@ -391,278 +569,174 @@ def generate_summary(persona_image_pil, *args): 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: return f"

{title}

\n{content}\n" - return "" - - summary += add_section("Infos socio-démographiques", { - "État civil": marital_status, "Niveau d'éducation": education_level, - "Profession": profession, "Revenus annuels (€)": income - }) - summary += add_section("Psychographie", { - "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", { - "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", { - "Responsabilités principales": main_responsibilities, - "Activités journalières": daily_activities, - "Une journée type / Citation": daily_life - }) - summary += add_section("Marketing & considérations spéciales", { - "Relation avec la marque": brand_relationship, - "Segment de marché": market_segment, - "Objectifs commerciaux (SMART)": commercial_objectives, - "Codes visuels / Marques préférées": visual_codes, - "Considérations spéciales (accessibilité, culture...)": special_considerations, - "Références / Sources de données": references - }) - image_html += "
\n" - final_html = "
\n" - final_html += f"
\n{summary}
\n" - final_html += image_html - final_html += "
" + return f"

{title}

\n{content}\n" if content else "" + summary += add_section("Infos socio-démographiques", {"État civil": marital_status, "Niveau d'éducation": education_level, "Profession": profession, "Revenus annuels (€)": income}) + summary += add_section("Psychographie", {"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", {"Relation technologie": technology_relationship, "Tâches liées": product_related_activities, "Points de douleur": pain_points, "Objectifs produit": product_goals, "Scénarios d'utilisation": usage_scenarios}) + summary += add_section("Contexte pro/quotidien", {"Responsabilités principales": main_responsibilities, "Activités journalières": daily_activities, "Journée type/Citation": daily_life}) + summary += add_section("Marketing & considérations", {"Relation marque": brand_relationship, "Segment marché": market_segment, "Objectifs commerciaux": commercial_objectives, "Codes visuels": visual_codes, "Considérations spéciales": special_considerations, "Références/Sources": references}) + image_html += "
" + final_html = f"
{summary}
{image_html}
" return final_html, log # --- Interface Gradio --- +css = ".suggestion-box {border: 1px solid #e0e0e0; border-radius: 5px; padding: 10px; margin: 10px 0; background-color: #f9f9f9;} .suggestion-box h4 { margin-top: 0; margin-bottom: 5px; }" -# 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; } -""" - -with gr.Blocks(theme=gr.themes.Glass(), css=css) as demo: - # --- Titre et Description (Corrigés) --- +with gr.Blocks(theme=gr.themes.Default(), css=css) as demo: 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.") - # --- États Globaux (Corrigés) --- - app_config_state = gr.State(value={ - "api_source": None, "text_model": None, "image_generation_enabled": False, - "openai_key_provided": False, "openrouter_key_provided": bool(openrouter_api_key) - }) + # --- États Globaux --- + app_config_state = gr.State(value={"api_source": None, "text_model": None, "image_generation_enabled": False, "openai_key_provided": False, "openrouter_key_provided": bool(openrouter_api_key)}) bias_analysis_result_state = gr.State(value={}) persona_image_pil_state = gr.State(value=None) session_log_state = gr.State(value="") 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) + last_refinement_suggestion_state = gr.State(value=None) - # --- Affichage Global Statut/Erreur (Corrigé) --- + # --- Affichage Statut Global --- status_display = gr.Markdown(value="", elem_classes="status-message") def update_status_display(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) + if new_message and any(k in new_message for k in ["ERREUR", "WARN", "Configuration"]): 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 (Corrigé) --- + # --- Onglet 0 : Configuration API --- with gr.Tab("🔑 Configuration API", id=-1): 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 (`OPENROUTER_API_KEY`).") - else: - gr.Markdown("❌ **Clé API OpenRouter (`OPENROUTER_API_KEY`) non trouvée.** OpenRouter ne fonctionnera pas sans clé (ou utilisez une clé OpenAI).") - - openai_api_key_input = gr.Textbox( - 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." - ) - configure_api_button = gr.Button("Appliquer la configuration API") + gr.Markdown("Cet outil utilise un système d'IA. Choisissez votre fournisseur. En l'absence de saisie d'une clé API, un mode par défaut sera utilisé.") + gr.Markdown("**Note :** Si vous avez une clé OpenAI valide, elle sera utilisée pour la génération d'images et de texte. Sinon, OpenRouter sera utilisé pour le texte uniquement (images désactivées).") + if openrouter_api_key: gr.Markdown("✅ Clé API **OpenRouter** trouvée.") + else: gr.Markdown("❌ **Clé API OpenRouter non trouvée.** Mode OpenRouter indisponible sans clé.") + openai_api_key_input = gr.Textbox(label="Clé API OpenAI (optionnelle)", type="password", placeholder="Entrez clé OpenAI pour DALL-E 3 / GPT", info="Si valide: utilisée pour images ET texte. Sinon: OpenRouter (si clé dispo) pour texte.") + configure_api_button = gr.Button("Appliquer la configuration") api_status_display = gr.Markdown("Statut API : Non configuré.") 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() - active_api_client_holder["client"] = None; active_api_client_holder["openai_key"] = None + """Configure le client API et met à jour l'état.""" + openai_key_provided = bool(openai_key); openrouter_key_available = current_config["openrouter_key_provided"] + 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 if openai_key_provided: try: - temp_client = OpenAI(api_key=openai_key) - temp_client.models.list() # Test auth/connexion + temp_client = OpenAI(api_key=openai_key); temp_client.models.list() # Test 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**."; 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 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 + status_msg = f"✅ Config **OpenAI** active (Texte: `{text_model}`, Images: {OPENAI_IMAGE_MODEL} direct)."; config["openai_key_provided"] = True + current_log = update_log("Config: Client OpenAI OK.", current_log) + except openai.AuthenticationError: status_msg = "⚠️ Clé OpenAI **invalide**."; current_log = update_log("ERR Config OpenAI: Clé invalide.", current_log); config["openai_key_provided"] = False; openai_key_provided = False + except Exception as e: status_msg = f"⚠️ Clé OpenAI fournie mais erreur: {str(e)}."; current_log = update_log(f"ERR Config OpenAI: {e}", current_log); config["openai_key_provided"] = False; openai_key_provided = False elif openrouter_key_available: try: - 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) + temp_client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=openrouter_api_key) + client_to_store = temp_client; api_source = "openrouter"; text_model = OPENROUTER_TEXT_MODEL + image_enabled = False + status_msg = f"✅ Config **OpenRouter** active (Texte: `{text_model}`)."; config["openai_key_provided"] = False + current_log = update_log("Config: Client OpenRouter OK (Images désactivées).", current_log) except Exception as e: - 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 + status_msg = f"❌ Erreur init OpenRouter: {e}."; current_log = update_log(f"ERR Config OpenRouter: {e}", current_log); 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." + status_msg = "❌ Aucune clé API valide disponible/configurée." client_to_store = None; api_source = None; text_model = None; image_enabled = False; config["openai_key_provided"] = False 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) + log_msg = f"Config API. Source: {api_source or 'Aucune'}, Images: {'Actif' if image_enabled else 'Inactif'}." + if "OK." not in current_log.splitlines()[-1]: current_log = update_log(log_msg, current_log) return config, status_msg, current_log - 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] - ) + configure_api_button.click(configure_api_clients, [openai_api_key_input, app_config_state, session_log_state], [app_config_state, api_status_display, session_log_state]) - # --- Onglet 1 : Objectif & Analyse Biais (Corrigé) --- + # --- Onglet 1 : Objectif & Analyse Biais --- 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. Un système d'IA analysera votre objectif pour identifier des biais cognitifs potentiels.") + gr.Markdown("### 1. Définissez l'objectif marketing") + gr.Markdown("Pourquoi créez-vous ce persona ? Le système d'IA analysera l'objectif pour identifier des biais potentiels.") with gr.Row(): - objective_input = gr.Textbox(label="Objectif marketing pour ce persona", lines=4, scale=3) + objective_input = gr.Textbox(label="Objectif marketing", lines=4, scale=3) with gr.Column(scale=1): 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") + suggestion_button1 = gr.Button("Ex 1 : Service éco urbain", size="sm") + suggestion_button2 = gr.Button("Ex 2 : App fitness seniors", size="sm") analyze_button = gr.Button("🔍 Analyser l'objectif (biais)") - gr.Markdown("---") - 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": "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, - 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') - - 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é)." + gr.Markdown("---"); 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":"coral", "EXPLANATION":"lightgray", "ADVICE":"green", "INFO":"blue", "COMMENT":"orange", "ERROR":"red"}) + gr.Markdown("---"); gr.Markdown("### 🤔 Réflexion") + user_reflection_on_biases = gr.Textbox(label="Comment utiliser cette analyse ?", lines=2, placeholder="Ex: Attention au stéréotype X...") + log_reflection_button = gr.Button("📝 Enregistrer réflexion", size='sm') + 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)." + suggestion2_text = "Développer une application mobile de fitness personnalisée pour les seniors actifs (+65 ans) cherchant à maintenir une vie saine et sociale." suggestion_button1.click(lambda: suggestion1_text, outputs=objective_input) suggestion_button2.click(lambda: suggestion2_text, outputs=objective_input) - + + analyze_button.click( - fn=analyze_biases, # Renommé - inputs=[app_config_state, objective_input, session_log_state], - outputs=[bias_analysis_result_state, session_log_state] - ).then( - fn=display_bias_analysis, # Renommé - inputs=bias_analysis_result_state, - outputs=bias_analysis_output_highlighted - ).then( - 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] - ) - def log_user_reflection(reflection_text, log_state): - if reflection_text: return update_log(f"Réflexion utilisateur (Étape 1): '{reflection_text}'", log_state) - return log_state + fn=lambda: gr.update(interactive=False), + inputs=None, + outputs=[analyze_button] + ).then( + fn=analyze_biases, + inputs=[app_config_state, objective_input, session_log_state], + outputs=[bias_analysis_result_state, session_log_state] + ).then( + fn=display_bias_analysis, + inputs=bias_analysis_result_state, + outputs=bias_analysis_output_highlighted + ).then( + fn=lambda r, l: update_status_display(r.get("overall_comment", "") if "Erreur" in r.get("overall_comment", "") else "", l), + inputs=[bias_analysis_result_state, session_log_state], + outputs=[status_display, session_log_state] + ).then( + fn=lambda: gr.update(interactive=True), + inputs=None, + outputs=[analyze_button] + ) + def log_user_reflection(r, l): return update_log(f"Réflexion (1): '{r}'", l) if r else l log_reflection_button.click(log_user_reflection, [user_reflection_on_biases, session_log_state], [session_log_state]) - # --- Onglet 2 : Image & Infos Base (Corrigé) --- + # --- Onglet 2 : Image & Infos Base --- with gr.Tab("👤 Étape 2 : Image & infos de base", id=1): - gr.Markdown("### 2. Créez l'identité visuelle et les informations de base") + gr.Markdown("### 2. Identité visuelle et informations de base") with gr.Row(): 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, anglais)", lines=1, - info="Ex: 'reading a book in a cafe', 'working on laptop', 'hiking'" - ) + persona_description_en_input = gr.Textbox(label="Contexte image (optionnel, anglais)", lines=1, info="Ex: 'reading book', 'working on laptop'") 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 (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="") - 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.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 (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="") ; accessories_input = gr.Dropdown(label="Accessoires", choices=list(accessories_mapping.keys()), value="") + reset_visuals_button = gr.Button("Réinitialiser détails", size="sm") with gr.Column(scale=1): - # Image display - persona_image_output = gr.Image( - label="Image du persona", type="pil", - interactive=False - ) + 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") + gr.Markdown("💡 **Attention :** Les systèmes d'IA générative peuvent reproduire des stéréotypes. Clé OpenAI requise.", 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) - def handle_image_generation(*args): - """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é + pil_image, updated_log, error_message = generate_persona_image(app_config, *persona_inputs, log_state) status_update_msg = ""; info_popup_msg = None if error_message: - 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 any(k in error_message for k in ["Veuillez remplir", "désactivée"]): info_popup_msg = error_message + else: status_update_msg = error_message if info_popup_msg: gr.Info(info_popup_msg) return pil_image, updated_log, status_update_msg - 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=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] - ) - app_config_state.change( - fn=lambda config: gr.update(interactive=config.get("image_generation_enabled", False)), - inputs=app_config_state, outputs=generate_image_button - ) + generate_image_button.click(handle_image_generation, generate_image_inputs, generate_image_outputs).then(lambda img: img, persona_image_pil_state, persona_image_output).then(update_status_display, [status_message_state, session_log_state], [status_display, session_log_state]) + app_config_state.change(lambda cfg: gr.update(interactive=cfg.get("image_generation_enabled", False)), app_config_state, generate_image_button) - # --- Onglet 3 : Profil Détaillé & Raffinement (Corrigé) --- + # --- Onglet 3 : Profil Détaillé & Raffinement --- 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 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") - + gr.Markdown("Utilisez '💡' pour obtenir des suggestions du système d'IA afin de nuancer ce champ.") + refinement_suggestion_display = gr.Markdown("*Cliquez sur '💡' à côté d'un champ pour une suggestion.*", elem_classes="suggestion-box") with gr.Row(): with gr.Column(): gr.Markdown("#### Infos socio-démographiques") @@ -670,220 +744,103 @@ with gr.Blocks(theme=gr.themes.Glass(), css=css) as demo: 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) - 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("💡", 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("💡", 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("💡", 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("💡", scale=1, size='sm') + with gr.Row(equal_height=False): personality_traits_input = gr.Textbox(label="Traits personnalité", lines=2, scale=4); refine_personality_traits_button = gr.Button("💡", scale=1, size='sm') + with gr.Row(equal_height=False): values_beliefs_input = gr.Textbox(label="Valeurs, croyances", lines=2, scale=4); refine_values_beliefs_button = gr.Button("💡", scale=1, size='sm') + with gr.Row(equal_height=False): motivations_input = gr.Textbox(label="Motivations", lines=2, scale=4); refine_motivations_button = gr.Button("💡", scale=1, size='sm') + with gr.Row(equal_height=False): hobbies_interests_input = gr.Textbox(label="Loisirs, intérêts", lines=2, scale=4); refine_hobbies_interests_button = gr.Button("💡", scale=1, size='sm') with gr.Column(): - 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, 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 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("💡", scale=1, size='sm') - with gr.Row(equal_height=False): - 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("💡", scale=1, size='sm') - + gr.Markdown("#### Relation produit/service") + with gr.Row(equal_height=False): technology_relationship_input = gr.Textbox(label="Relation technologie", lines=2, scale=4, info="Ex: adopte vite, prudent..."); 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 liées 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", lines=2, scale=4); refine_pain_points_button = gr.Button("💡", scale=1, size='sm') + with gr.Row(equal_height=False): product_goals_input = gr.Textbox(label="Objectifs avec 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", lines=2, scale=4); refine_usage_scenarios_button = gr.Button("💡", scale=1, size='sm') with gr.Accordion("Autres informations (optionnel)", open=False): - with gr.Row(): - with gr.Column(): - gr.Markdown("#### Contexte professionnel/vie quotidienne") - with gr.Row(equal_height=False): - 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", 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="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") - 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("💡", 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("💡", scale=1, size='sm') - with gr.Row(equal_height=False): - 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("💡", scale=1, size='sm') - with gr.Row(equal_height=False): - 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", lines=2, scale=4) - refine_references_button = gr.Button("💡", scale=1, size='sm') - - # Handler pour l'affichage des suggestions + with gr.Row(): + with gr.Column(): + gr.Markdown("#### Contexte pro/quotidien") + with gr.Row(equal_height=False): main_responsibilities_input = gr.Textbox(label="Responsabilités", 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", 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="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") + with gr.Row(equal_height=False): brand_relationship_input = gr.Textbox(label="Relation marque", lines=2, scale=4); refine_brand_relationship_button = gr.Button("💡", scale=1, size='sm') + with gr.Row(equal_height=False): market_segment_input = gr.Textbox(label="Segment marché", lines=2, scale=4); refine_market_segment_button = gr.Button("💡", scale=1, size='sm') + with gr.Row(equal_height=False): commercial_objectives_input = gr.Textbox(label="Objectifs commerciaux", 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", lines=2, scale=4); 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", 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", lines=2, scale=4); refine_references_button = gr.Button("💡", scale=1, size='sm') + 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.*" - - # Fonction intermédiaire pour gérer l'appel et le stockage de la suggestion + if "ERREUR:" not in suggestion_text: return f"#### Suggestion pour '{field_name}' :\n\n{suggestion_text}" + else: return "*Erreur lors du dernier raffinement (voir statut/log).*" + return "*Cliquez sur '💡' pour une suggestion.*" def handle_refinement_request(app_config, fname, lname, age_val, field_name_display, field_val, bias_state_dict, objectives, log_state): - """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 - + updated_log, result_text, field_name_ctx = refine_persona_details(app_config, fname, lname, age_val, field_name_display, field_val, bias_state_dict, objectives, log_state) + status_update_msg = ""; suggestion_details = 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: - gr.Warning(f"Pas de suggestion reçue pour '{field_name_display}'.") - + if "ERREUR:" in result_text: status_update_msg = result_text; gr.Warning(f"Erreur raffinement '{field_name_display}'. Voir log.") + else: suggestion_details = (field_name_display, result_text) + else: gr.Warning(f"Pas de suggestion pour '{field_name_display}'.") return updated_log, status_update_msg, suggestion_details + def create_refine_handler(f_name, i_comp): return lambda app_c, fn, ln, age, f_val, bias_s, obj, log_s: handle_refinement_request(app_c, fn, ln, age, f_name, f_val, bias_s, obj, log_s) - # Lambda générique - def create_refine_handler(field_name_display, input_component): - 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) - - common_inputs_refine = [app_config_state, first_name_input, last_name_input, age_input] - state_inputs_refine = [bias_analysis_result_state, objective_input, session_log_state] - # Les sorties de handle_refinement_request + common_ref_inputs = [app_config_state, first_name_input, last_name_input, age_input] + state_ref_inputs = [bias_analysis_result_state, objective_input, session_log_state] refine_handler_outputs = [session_log_state, status_message_state, last_refinement_suggestion_state] - refine_buttons_map = { - refine_personality_traits_button: ("Traits de personnalité", personality_traits_input), - refine_values_beliefs_button: ("Valeurs et croyances", values_beliefs_input), - refine_motivations_button: ("Motivations", motivations_input), - refine_hobbies_interests_button: ("Loisirs et intérêts", hobbies_interests_input), - refine_technology_relationship_button: ("Relation avec la technologie", technology_relationship_input), - refine_product_related_activities_button: ("Tâches liées au produit", product_related_activities_input), - refine_pain_points_button: ("Points de douleur", pain_points_input), - refine_product_goals_button: ("Objectifs produit", product_goals_input), - 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_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), - refine_visual_codes_button: ("Codes visuels", visual_codes_input), - refine_special_considerations_button: ("Considérations spéciales", special_considerations_input), - refine_references_button: ("Références", references_input), + refine_personality_traits_button: ("Traits personnalité", personality_traits_input), refine_values_beliefs_button: ("Valeurs, croyances", values_beliefs_input), + refine_motivations_button: ("Motivations", motivations_input), refine_hobbies_interests_button: ("Loisirs, intérêts", hobbies_interests_input), + refine_technology_relationship_button: ("Relation technologie", technology_relationship_input), refine_product_related_activities_button: ("Tâches liées", product_related_activities_input), + refine_pain_points_button: ("Points de douleur", pain_points_input), refine_product_goals_button: ("Objectifs produit", product_goals_input), + refine_usage_scenarios_button: ("Scénarios utilisation", usage_scenarios_input), refine_main_responsibilities_button: ("Responsabilités", 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_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), refine_visual_codes_button: ("Codes visuels/Marques", visual_codes_input), + refine_special_considerations_button: ("Considérations spéciales", special_considerations_input), refine_references_button: ("Références/Sources", references_input), } - for btn, (label, input_comp) in refine_buttons_map.items(): btn.click( - fn=create_refine_handler(label, input_comp), - inputs=common_inputs_refine + [input_comp] + state_inputs_refine, - 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] - ) + fn=create_refine_handler(label, input_comp), inputs=common_ref_inputs + [input_comp] + state_ref_inputs, outputs=refine_handler_outputs + ).then(update_status_display, [status_message_state, session_log_state], [status_display, session_log_state] + ).then(display_refinement_suggestion, [last_refinement_suggestion_state], [refinement_suggestion_display]) - # --- Onglet 4 : Résumé Persona (Corrigé) --- + # --- Onglet 4 : Résumé Persona --- 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") - summary_content = gr.Markdown(elem_classes="persona-summary", value="Cliquez sur 'Générer' pour voir le résumé.") - - # 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, - 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, - session_log_state # Log state last - ] - summary_button.click( - fn=generate_summary, # Renommé - inputs=all_persona_inputs_for_summary, - outputs=[summary_content, session_log_state] - ) + summary_button = gr.Button("Générer le résumé") + summary_content = gr.Markdown(elem_classes="persona-summary", value="Cliquez sur 'Générer'...") + all_summary_inputs = [persona_image_pil_state, 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, 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, session_log_state] + summary_button.click(generate_summary, all_summary_inputs, [summary_content, session_log_state]) - # --- Onglet 5 : Journal de Bord (Corrigé) --- + # --- 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, 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) - 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") - - session_log_state.change(fn=lambda log_data: log_data, inputs=session_log_state, outputs=log_display_final) - - def prepare_log_for_download(log_data): - """Prépare le fichier log temporaire pour le téléchargement.""" - if not log_data: return gr.update(visible=False) + gr.Markdown("### Suivi du processus") + gr.Markdown("Historique des actions, réflexions et erreurs.") + log_display_final = gr.Textbox(label="Historique session", lines=20, interactive=False, max_lines=MAX_LOG_LINES) + download_log_button = gr.DownloadButton(label="Télécharger journal", visible=False) + export_log_button_final = gr.Button("Préparer export journal") + session_log_state.change(fn=lambda log: log, inputs=session_log_state, outputs=log_display_final) + def prep_log_dl(log): + if not log: return gr.update(visible=False) try: - 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: {temp_filepath}") - return gr.update(value=temp_filepath, visible=True) # Met à jour DownloadButton - except Exception as e: - 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) - -# --- Lancement de l'Application (Corrigé) --- -if not openrouter_api_key: - 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 + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt', encoding='utf-8') as tf: tf.write(log); fp = tf.name + print(f"Log prêt: {fp}"); return gr.update(value=fp, visible=True) + except Exception as e: print(f"Err création log DL: {e}"); return gr.update(visible=False) + export_log_button_final.click(prep_log_dl, session_log_state, download_log_button) + +# --- Lancement App --- +if not openrouter_api_key: print("\n"+"="*60+"\nWARN: Clé OpenRouter manquante. Fonctionnement limité à OpenAI si clé fournie.\n"+"="*60+"\n") initial_api_status = "Statut API : Non configuré." if openrouter_api_key: - print("Clé OpenRouter trouvée, tentative de configuration initiale...") + print("Clé OR trouvée, config initiale...") 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é + app_config_state.value = initial_config; session_log_state.value = initial_log + print(initial_api_status); api_status_display.value = initial_api_status + except Exception as init_e: print(f"ERR config initiale OR: {init_e}"); initial_api_status = f"❌ Err config initiale OR: {init_e}"; api_status_display.value = initial_api_status + demo.queue().launch(debug=False, share=False, pwa=True) \ No newline at end of file