Spaces:
Sleeping
Sleeping
import gradio as gr | |
from openai import OpenAI | |
import openai | |
from pydantic import BaseModel, Field | |
import os | |
import requests | |
from PIL import Image | |
import tempfile | |
import io | |
import markdown | |
import base64 | |
import datetime | |
import json | |
import re | |
from dotenv import load_dotenv | |
load_dotenv() | |
# Clé OpenRouter | |
openrouter_api_key = os.getenv("OPENROUTER_API_KEY") | |
OPENROUTER_TEXT_MODEL = os.getenv("OPENROUTER_TEXT_MODEL", "mistralai/mistral-small-3.1-24b-instruct:free") | |
# Modèles OpenAI | |
OPENAI_TEXT_MODEL = "gpt-4o-mini" | |
OPENAI_IMAGE_MODEL = "dall-e-3" | |
# 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é") | |
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) | |
overall_comment: str = Field(default="") | |
# --- Fonctions Utilitaires --- | |
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"} | |
eye_color_mapping = {"": "","Bleu": "blue","Vert": "green","Marron": "brown","Gris": "gray"} | |
hair_style_mapping = {"": "","Court": "short","Long": "long","Bouclé": "curly","Rasé": "shaved","Chauve": "bald","Tresses": "braided","Queue de cheval": "ponytail","Coiffure afro": "afro","Dégradé": "fade"} | |
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"} | |
MAX_LOG_LINES = 150 | |
def update_log(event_description, session_log_state): | |
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):]) | |
updated_log = current_log + "\n" + new_log_entry if current_log else new_log_entry | |
return updated_log.strip() | |
def clean_json_response(raw_response): | |
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 start != -1 and end != -1 and end > start: | |
potential_json = raw_response[start:end+1] | |
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 | |
return raw_response.strip() | |
# --- 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." | |
client = active_api_client_holder.get("client") | |
if not client: | |
print("WARN: Client actif non trouvé, tentative de ré-initialisation.") | |
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é-init OpenAI: {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("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 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.").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.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""" | |
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 TOUTE ta réponse EXCLUSIVEMENT en utilisant le format JSON suivant (basé sur la classe Pydantic BiasAnalysisResponse): | |
{{ | |
"detected_biases": [ | |
{{ | |
"bias_type": "Type de biais identifié", | |
"explanation": "Explication contextuelle du risque.", | |
"advice": "Conseil spécifique d'atténuation." | |
}} | |
// ... autres biais détectés ... | |
], | |
"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'. | |
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, | |
max_tokens=2400, | |
response_format=response_format_config, | |
) | |
raw_response_content = completion.choices[0].message.content | |
# --- Parsing de la réponse --- | |
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: | |
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 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 ({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_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 --- | |
def display_bias_analysis(analysis_result): | |
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")) | |
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")) | |
for bias_info in biases: | |
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 via OpenAI ou OpenRouter.""" | |
inputs = args[:-1] | |
session_log_state = 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) = inputs | |
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}" | |
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." | |
# --- Construction du Prompt --- | |
gender_en = gender_mapping.get(gender, "person") | |
lens_aperture = "Kodak Portra 400" | |
lighting = "soft natural light" | |
photo_style_details = f"portrait {lighting}, shot on {lens_aperture}" | |
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) | |
final_prompt = f"{base_description}{details}" | |
log = update_log(f"Génération image (début via {api_source}): Prompt='{final_prompt[:100]}...'", log) | |
pil_image = None | |
try: | |
if api_source == "openai": | |
response = active_client.images.generate( | |
model=OPENAI_IMAGE_MODEL, prompt=final_prompt, size="1024x1024", n=1, | |
response_format="url", quality="standard" | |
) | |
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, 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_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): | |
log = session_log_state | |
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 | |
model_name = app_config["text_model"] | |
biases_text = "Aucune analyse de biais précédente." | |
if bias_analysis_dict: | |
try: | |
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""" | |
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: 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 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=800, | |
) | |
suggestions = response.choices[0].message.content.strip() | |
log = update_log(f"Raffinement (fin): Champ='{field_name}'. Suggestions: '{suggestions[:50]}...'", log) | |
return log, suggestions, 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): | |
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 = "<div style='flex: 0 0 320px; margin-left: 20px; text-align: center; align-self: flex-start;'>\n" | |
if not first_name or not last_name or not age: | |
summary += "<h2>Infos base manquantes</h2><p><i>Prénom, nom, âge requis (Étape 2).</i></p>"; image_html += "<p>Image non générée.</p>" | |
else: | |
if persona_image_pil and isinstance(persona_image_pil, Image.Image): | |
try: | |
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"<img src='{img_data_url}' alt='Persona {first_name}' style='max-width: 100%; height: auto; border: 1px solid #eee; border-radius: 5px; margin-top: 10px;'/>\n" | |
except Exception as e: img_err_msg = f"Erreur encodage image: {e}"; image_html += f"<p><i>{img_err_msg}</i></p>"; log = update_log(f"ERR Encodage Image Résumé: {e}", log) | |
else: image_html += "<p><i>Aucune image disponible.</i></p>" | |
summary += f"<div style='text-align: center;'><h1>{first_name} {last_name}, {age} ans ({gender})</h1></div>" | |
def add_section(title, fields): | |
content = "" | |
for label, value in fields.items(): | |
should_add = (label == "Revenus annuels (€)" and value is not None) or (label != "Revenus annuels (€)" and value) | |
if should_add: | |
if label == "Revenus annuels (€)" and isinstance(value, (int, float)): | |
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('<p>', '').replace('</p>', '').strip().replace("\n", "<br>") | |
content += f"<b>{label}:</b> {value_str_html}<br>\n" | |
return f"<h3 style='margin-top: 15px; margin-bottom: 5px; border-bottom: 1px solid #eee; padding-bottom: 2px;'>{title}</h3>\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 += "</div>" | |
final_html = f"<div style='display: flex; flex-wrap: wrap; align-items: flex-start; font-family: sans-serif; padding: 10px;'><div style='flex: 1; min-width: 350px; padding-right: 15px;'>{summary}</div>{image_html}</div>" | |
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; }" | |
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 --- | |
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="") | |
last_refinement_suggestion_state = gr.State(value=None) | |
# --- Affichage Statut Global --- | |
status_display = gr.Markdown(value="", elem_classes="status-message") | |
def update_status_display(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 --- | |
with gr.Tab("🔑 Configuration API", id=-1): | |
gr.Markdown("### Configuration des clés 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): | |
"""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 | |
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"✅ 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", 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 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: | |
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. 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(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 --- | |
with gr.Tab("🎯 Étape 1 : Objectif & analyse biais", id=0): | |
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", lines=4, scale=3) | |
with gr.Column(scale=1): | |
gr.Markdown("<small>Suggestions :</small>") | |
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":"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=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 --- | |
with gr.Tab("👤 Étape 2 : Image & infos de base", id=1): | |
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 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", size="sm") | |
with gr.Column(scale=1): | |
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("<small>💡 **Attention :** Les systèmes d'IA générative peuvent reproduire des stéréotypes. Clé OpenAI requise.</small>", 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): | |
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) | |
status_update_msg = ""; info_popup_msg = None | |
if error_message: | |
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(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 --- | |
with gr.Tab("📝 Étape 3 : Profil détaillé & raffinement", id=2): | |
gr.Markdown("### 3. Complétez les détails du persona") | |
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") | |
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=["", "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 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 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 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): | |
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: 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): | |
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 '{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) | |
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 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_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 --- | |
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é") | |
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 --- | |
with gr.Tab("📓 Journal de bord", id=4): | |
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 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é 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 | |
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) |