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"

\n"
+ # Style pour l'image : largeur max 100%, hauteur auto pour responsivité dans la colonne
+ image_html += f"

\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