Spaces:
Sleeping
Sleeping
import gradio as gr | |
from openai import OpenAI | |
from pydantic import BaseModel, Field | |
import os | |
import requests | |
from PIL import Image | |
import tempfile | |
import numpy as np | |
import markdown | |
import base64 | |
import datetime | |
import json | |
from dotenv import load_dotenv | |
# --- Configuration --- | |
load_dotenv() # Charge les variables depuis un fichier .env s'il existe | |
api_key = os.getenv("OPENROUTER_API_KEY") | |
if not api_key: | |
print("ERREUR: Clé API OpenRouter non trouvée. Définissez OPENROUTER_API_KEY dans l'environnement ou le fichier .env.") | |
# Mettre ici une gestion d'erreur plus propre si besoin | |
# Initialisation du client OpenAI pour pointer vers OpenRouter | |
client = OpenAI( | |
base_url="https://openrouter.ai/api/v1", | |
api_key=api_key, | |
) | |
MODEL_NAME="google/gemini-2.5-pro-exp-03-25:free" | |
# --- Modèles Pydantic pour la structuration --- | |
class BiasInfo(BaseModel): | |
bias_type: str = Field(..., description="Type de biais identifié (ex: Stéréotype de genre, Biais de confirmation)") | |
explanation: str = Field(..., description="Explication de pourquoi cela pourrait être un biais dans ce contexte.") | |
advice: str = Field(..., description="Conseil spécifique pour atténuer ce biais.") | |
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 --- | |
# Dictionnaires de correspondance (conservés de V1) | |
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"} | |
# Fonction de mise à jour du journal | |
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.value if session_log_state.value else "" | |
# Limiter la taille du log si nécessaire pour éviter des problèmes de performance | |
log_lines = current_log.splitlines() | |
if len(log_lines) > 100: # Garde les 100 dernières lignes par exemple | |
current_log = "\n".join(log_lines[-100:]) | |
updated_log = current_log + new_log_entry + "\n" | |
session_log_state.value = updated_log | |
return updated_log | |
# --- Fonctions Principales de l'Application --- | |
def analyze_biases_v2(objective_text, session_log_state): | |
"""Analyse les biais dans l'objectif marketing (V2 avec prompt ciblé).""" | |
log_event_start = f"Analyse Biais Objectif (début): '{objective_text[:50]}...'" | |
update_log(log_event_start, session_log_state) | |
# Prompt système V2 (catégories + exemples) | |
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'. | |
""" | |
try: | |
completion = client.chat.completions.create( | |
model=MODEL_NAME, | |
messages=[ | |
{"role": "user", "content": system_prompt} # Intégration de l'objectif dans le prompt système peut être plus robuste pour certains modèles | |
], | |
temperature=0.4, | |
max_tokens=800, | |
response_format={"type": "json_object"}, # Demander explicitement du JSON si le modèle le supporte bien | |
) | |
response_content_str = completion.choices[0].message.content | |
# Essayer de parser la réponse JSON | |
parsed_response = BiasAnalysisResponse.parse_raw(response_content_str) | |
log_event_end = f"Analyse Biais Objectif (fin): Biais trouvés - {len(parsed_response.detected_biases)}" | |
update_log(log_event_end, session_log_state) | |
return parsed_response.dict(), update_log("", session_log_state) # Retourne dict et log mis à jour | |
except Exception as e: | |
error_msg = f"Erreur pendant l'analyse des biais: {str(e)}. Réponse brute: {response_content_str if 'response_content_str' in locals() else 'N/A'}" | |
print(error_msg) | |
update_log(f"ERREUR Analyse Biais: {str(e)}", session_log_state) | |
# Retourner une structure d'erreur compatible | |
error_response = BiasAnalysisResponse(overall_comment=f"Erreur technique lors de l'analyse: {str(e)}").dict() | |
return error_response, update_log("", session_log_state) | |
def display_bias_analysis_v2(analysis_result): | |
"""Formate l'analyse des biais pour l'affichage avec HighlightedText.""" | |
if not analysis_result or "error" in analysis_result: | |
return [("Erreur ou pas de résultat.", None)], analysis_result # Retourne un format compatible HighlightedText | |
biases = analysis_result.get("detected_biases", []) | |
overall_comment = analysis_result.get("overall_comment", "") | |
highlighted_data = [] | |
if not biases: | |
highlighted_data.append((overall_comment or "Aucun biais majeur détecté.", "INFO")) | |
else: | |
if overall_comment: | |
highlighted_data.append((overall_comment + "\n", "COMMENT")) | |
for bias_info in biases: | |
highlighted_data.append((f"⚠️ {bias_info['bias_type']}: ", "BIAS_TYPE")) | |
highlighted_data.append((f"{bias_info['explanation']} ", "EXPLANATION")) | |
highlighted_data.append((f"💡 Conseil: {bias_info['advice']}\n", "ADVICE")) | |
# Retourne les données formatées pour HighlightedText et le résultat brut pour usage ultérieur | |
return highlighted_data, analysis_result | |
def generate_persona_image_v2(*args): | |
"""Génère l'image du persona (V2 avec logging).""" | |
# Les 13 premiers args sont les inputs de l'image, le dernier est session_log_state | |
inputs = args[:-1] | |
session_log_state = args[-1] | |
(first_name, last_name, age, gender, persona_description, | |
skin_color, eye_color, hair_style, hair_color, facial_expression, | |
posture, clothing_style, accessories) = inputs | |
if not first_name or not last_name or not age or not gender: | |
gr.Info("Veuillez remplir tous les champs pour générer l'image du persona.") | |
return None, update_log("Génération Image: Champs manquants.", session_log_state) | |
prompt = f"one person only. {first_name} {last_name}, {gender}, {age} years old. Realistic photo style." | |
# Mapping et ajout des détails | |
details = "" | |
if skin_color: details += f" Skin tone: {skin_color_mapping.get(skin_color, skin_color)}." | |
if eye_color: details += f" Eye color: {eye_color_mapping.get(eye_color, eye_color)}." | |
if hair_style: details += f" Hairstyle: {hair_style_mapping.get(hair_style, hair_style)}." | |
if hair_color: details += f" Hair color: {hair_color_mapping.get(hair_color, hair_color)}." | |
if facial_expression: details += f" Facial expression: {facial_expression_mapping.get(facial_expression, facial_expression)}." | |
if posture: details += f" Posture: {posture_mapping.get(posture, posture)}." | |
if clothing_style: details += f" Clothing style: {clothing_style_mapping.get(clothing_style, clothing_style)}." | |
if accessories: details += f" Accessories: {accessories_mapping.get(accessories, accessories)}." | |
if persona_description: details += f" Background context or activity: {persona_description}." | |
final_prompt = prompt + details | |
log_event_start = f"Génération Image (début): Prompt='{final_prompt[:100]}...'" | |
update_log(log_event_start, session_log_state) | |
try: | |
response = client.images.generate( | |
model="dall-e-3", # OpenRouter pourrait ne pas supporter DALL-E 3 directement, vérifier la compatibilité ou utiliser un modèle image dispo via OpenRouter | |
# Alternative possible via OpenRouter : "stabilityai/stable-diffusion-xl-1024-v1-0" (nécessite ajustement) | |
# Pour l'instant on laisse DALL-E 3 en supposant une clé OpenAI valide aussi, ou l'utilisateur devra adapter | |
prompt=final_prompt, | |
size="1024x1024", | |
n=1, | |
) | |
image_url = response.data[0].url | |
response_image = requests.get(image_url) | |
response_image.raise_for_status() # Vérifie les erreurs HTTP | |
# Utiliser BytesIO pour éviter fichier temporaire si possible avec Gradio Image | |
# img_bytes = response_image.content | |
# pil_image = Image.open(BytesIO(img_bytes)) | |
# return pil_image, update_log("Génération Image (fin): Succès.", session_log_state) | |
# Méthode avec fichier temporaire (comme V1) si BytesIO pose problème | |
temp_image = tempfile.NamedTemporaryFile(delete=False, suffix='.png') | |
with open(temp_image.name, 'wb') as f: | |
f.write(response_image.content) | |
image_path = temp_image.name | |
update_log(f"Génération Image (fin): Succès. Image @ {image_path}", session_log_state) | |
return image_path, update_log("", session_log_state) # Retourne chemin et log | |
except Exception as e: | |
error_msg = f"Erreur lors de la génération de l'image: {str(e)}" | |
print(error_msg) | |
update_log(f"ERREUR Génération Image: {str(e)}", session_log_state) | |
gr.Error(error_msg) | |
return None, update_log("", session_log_state) | |
def refine_persona_details_v2(first_name, last_name, age, field_name, field_value, bias_analysis_json_str, marketing_objectives, session_log_state): | |
"""Affine les détails du persona (V2 avec lien aux biais et logging).""" | |
log_event_start = f"Refinement (début): Champ='{field_name}', Valeur initiale='{field_value[:50]}...'" | |
update_log(log_event_start, session_log_state) | |
# Essayer de récupérer les biais détectés précédemment | |
biases_text = "Aucune analyse de biais précédente disponible." | |
if bias_analysis_json_str: | |
try: | |
bias_analysis_data = json.loads(bias_analysis_json_str) # Charger depuis l'état caché | |
detected_biases = bias_analysis_data.get("detected_biases", []) | |
if detected_biases: | |
biases_text = "\n".join([f"- {b['bias_type']}: {b['explanation']}" for b in detected_biases]) | |
else: | |
biases_text = "Aucun biais majeur détecté lors de l'analyse initiale." | |
except Exception as e: | |
biases_text = f"Erreur lors de la récupération des biais analysés: {e}" | |
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 biais potentiels 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 pour améliorer ou nuancer cette valeur. | |
Tes suggestions doivent viser à rendre le persona plus réaliste, moins stéréotypé, et/ou à ATTÉNUER les biais potentiels listés ci-dessus, tout en restant cohérent avec l'objectif marketing général. | |
Si la valeur actuelle semble bonne ou si tu manques de contexte, indique-le simplement. | |
Réponds en français. Ne fournis que les suggestions. | |
""" | |
try: | |
response = client.chat.completions.create( | |
model=MODEL_NAME, | |
messages=[{"role": "user", "content": system_prompt}], | |
temperature=0.5, # Un peu plus de créativité pour les suggestions | |
max_tokens=150, | |
) | |
suggestions = response.choices[0].message.content.strip() | |
log_event_end = f"Refinement (fin): Champ='{field_name}'. Suggestions: '{suggestions[:50]}...'" | |
update_log(log_event_end, session_log_state) | |
# Affiche les suggestions dans une InfoBox | |
gr.Info(f"Suggestions pour '{field_name}':\n{suggestions}") | |
return update_log("", session_log_state) # Retourne juste le log mis à jour pour l'output invisible | |
except Exception as e: | |
error_msg = f"Erreur lors du raffinement pour '{field_name}': {str(e)}" | |
print(error_msg) | |
update_log(f"ERREUR Refinement '{field_name}': {str(e)}", session_log_state) | |
gr.Error(error_msg) | |
return update_log("", session_log_state) | |
def generate_summary_v2(*args): | |
"""Génère le résumé du persona (V2 avec logging et meilleure gestion image).""" | |
# Les 28 premiers args sont les inputs persona, le 29ème est persona_image_path, le 30ème est session_log_state | |
inputs = args[:-1] | |
session_log_state = args[-1] | |
(first_name, last_name, age, gender, persona_description, | |
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, # Ajout usage_scenarios | |
brand_relationship, market_segment, commercial_objectives, # Ajout autres champs V1 | |
visual_codes, special_considerations, daily_life, references, # Ajout autres champs V1 | |
persona_image_path # Récupère le chemin de l'image | |
) = inputs | |
log_event = f"Génération Résumé: Pour '{first_name} {last_name}'." | |
update_log(log_event, session_log_state) | |
summary = "" | |
image_html = "<div style='flex: 0 0 320px; margin-left: 20px; text-align: center;'>\n" # Div pour l'image | |
if not first_name or not last_name or not age: | |
summary += "**Veuillez fournir au moins le prénom, le nom et l'âge (Étape 2).**\n" | |
image_html += "<p>Image non générée.</p>\n" | |
else: | |
# Essayer de charger et encoder l'image depuis le chemin stocké | |
if persona_image_path and os.path.exists(persona_image_path): | |
try: | |
with open(persona_image_path, "rb") as img_file: | |
img_bytes = img_file.read() | |
img_base64 = base64.b64encode(img_bytes).decode() | |
img_data_url = f"data:image/png;base64,{img_base64}" | |
image_html += f"<img src='{img_data_url}' alt='Persona {first_name}' style='max-width: 300px; height: auto; border: 1px solid #eee; border-radius: 5px;'/>\n" | |
except Exception as e: | |
image_html += f"<p>Erreur chargement image: {e}</p>\n" | |
update_log(f"ERREUR Chargement Image Résumé: {e}", session_log_state) | |
else: | |
image_html += "<p>Aucune image générée.</p>\n" | |
# Section Informations Personnelles (Titre centré) | |
summary += f"<div style='text-align: center;'><h1>{first_name} {last_name}, {age} ans ({gender})</h1></div>\n" | |
if persona_description: summary += f"<p><i>{persona_description}</i></p>\n" | |
# Assemblage des autres sections (avec vérification si champ rempli) | |
def add_section(title, fields): | |
content = "" | |
for label, value in fields.items(): | |
if value: # N'ajoute que si la valeur existe | |
# Formatage spécial pour les revenus | |
if label == "Revenus annuels (€)" and isinstance(value, (int, float)): | |
value_str = f"{value:,.0f} €".replace(",", " ") # Format numérique | |
else: | |
value_str = str(value) | |
content += f"**{label}**: {value_str}<br>\n" | |
if content: | |
return f"<h3>{title}</h3>\n{content}<br>\n" | |
return "" | |
summary += add_section("Infos Socio-Démographiques", { | |
"État civil": marital_status, "Niveau d'éducation": education_level, | |
"Profession": profession, "Revenus annuels (€)": income | |
}) | |
summary += add_section("Psychographie", { | |
"Traits de personnalité": personality_traits, "Valeurs et croyances": values_beliefs, | |
"Motivations intrinsèques": motivations, "Hobbies et intérêts": hobbies_interests | |
}) | |
summary += add_section("Relation au Produit/Service", { | |
"Relation avec la technologie": technology_relationship, | |
"Tâches liées au produit": product_related_activities, | |
"Points de douleur (Pain points)": pain_points, | |
"Objectifs d’utilisation du produit": product_goals, | |
"Scénarios d’utilisation": usage_scenarios | |
}) | |
summary += add_section("Contexte Professionnel/Vie Quotidienne", { | |
"Responsabilités principales": main_responsibilities, | |
"Activités journalières": daily_activities, | |
"Une journée dans la vie": daily_life | |
}) | |
summary += add_section("Marketing & Considérations Spéciales", { | |
"Relation avec la marque": brand_relationship, | |
"Segment de marché": market_segment, | |
"Objectifs commerciaux (SMART)": commercial_objectives, | |
"Graphiques et codes visuels": visual_codes, | |
"Considérations spéciales (accessibilité)": special_considerations, | |
"Références (sources de données)": references | |
}) | |
image_html += "</div>\n" # Ferme div image | |
# Assemblage final avec flexbox | |
final_html = "<div style='display: flex; flex-wrap: wrap; align-items: flex-start;'>\n" | |
final_html += f"<div style='flex: 1; min-width: 300px;'>\n{summary}</div>\n" # Colonne texte | |
final_html += image_html # Colonne image | |
final_html += "</div>" | |
return final_html, update_log("", session_log_state) # Retourne HTML et log | |
# --- Interface Gradio V2 --- | |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="orange")) as demo: | |
gr.Markdown("# PersonaGenAI V2 : Assistant de Création de Persona") | |
gr.Markdown("Outil d'aide à la création de personas marketing, intégrant l'IA générative pour stimuler la créativité et la réflexivité face aux biais cognitifs et algorithmiques.") | |
# --- État Global --- | |
# Pour stocker le résultat de l'analyse de biais pour l'utiliser dans l'étape 3 | |
# Utiliser du JSON comme chaîne de caractères pour passer des données structurées | |
bias_analysis_result_state = gr.State(value=None) | |
# Pour stocker le chemin de l'image générée | |
persona_image_path_state = gr.State(value=None) | |
# Pour stocker le log de session | |
session_log_state = gr.State(value="") | |
with gr.Tabs() as tabs: | |
# --- Onglet 0 : Configuration API (Optionnel mais bonne pratique) --- | |
with gr.Tab("🔑 Configuration API", id=-1): | |
gr.Markdown("### Configuration OpenRouter API") | |
gr.Markdown(f"Utilise le modèle : `{MODEL_NAME}` via OpenRouter. Utiliser : google/gemini-2.5-pro-exp-03-25:free") | |
# Possibilité d'ajouter un input pour changer la clé si besoin, mais pas essentiel pour la démo AIMS | |
gr.HTML(f"<small>Clé utilisée (tronquée): {api_key[:10]}...{api_key[-4:] if len(api_key)>14 else ''}</small>") | |
# --- Onglet 1 : Objectif & Analyse Biais --- | |
with gr.Tab("🎯 Étape 1: Objectif & Analyse Biais", id=0): | |
gr.Markdown("### 1. Définissez votre objectif marketing") | |
gr.Markdown("Décrivez pourquoi vous créez ce persona. L'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): | |
# Exemples pour guider l'utilisateur | |
gr.Markdown("<small>Suggestions d'objectifs :</small>") | |
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("---") | |
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"} | |
) | |
gr.Markdown("---") | |
gr.Markdown("### 🤔 Réflexion") | |
user_reflection_on_biases = gr.Textbox( | |
label="Comment comptez-vous utiliser cette analyse pour la suite ?", | |
lines=2, | |
placeholder="Ex: Je vais veiller à ne pas tomber dans le stéréotype X, je vais chercher des données pour nuancer Y..." | |
) | |
log_reflection_button = gr.Button("📝 Enregistrer la réflexion", size='sm') | |
# 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." | |
suggestion_button1.click(lambda: suggestion1_text, outputs=objective_input) | |
suggestion_button2.click(lambda: suggestion2_text, outputs=objective_input) | |
analyze_button.click( | |
fn=analyze_biases_v2, | |
inputs=[objective_input, session_log_state], | |
outputs=[bias_analysis_result_state, session_log_state] # Stocke le résultat JSON brut dans l'état | |
).then( | |
fn=display_bias_analysis_v2, | |
inputs=bias_analysis_result_state, # Utilise le résultat stocké | |
outputs=[bias_analysis_output_highlighted, bias_analysis_result_state] # Affiche formaté + remet dans l'état (pour màj) | |
).then( | |
fn=lambda log: update_log("", log), # Force la MAJ du log display via l'état | |
inputs=session_log_state, | |
outputs=session_log_state # Ne change rien mais déclenche l'update du textbox log | |
) | |
def log_user_reflection(reflection_text, log_state): | |
update_log(f"Réflexion Utilisateur (Étape 1): '{reflection_text}'", log_state) | |
return update_log("", log_state) | |
log_reflection_button.click( | |
fn=log_user_reflection, | |
inputs=[user_reflection_on_biases, session_log_state], | |
outputs=[session_log_state] | |
) | |
# --- Onglet 2 : Image & Infos Base --- | |
with gr.Tab("👤 Étape 2: Image & Infos Base", id=1): | |
gr.Markdown("### 2. Créez l'identité visuelle et les informations de base") | |
with gr.Row(): | |
with gr.Column(scale=1.4): # Colonne de gauche pour les inputs | |
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") # Ajout Non-binaire | |
persona_description_input = gr.Textbox(label="Contexte/Activité pour l'image (optionnel, en anglais)", lines=1) | |
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="") | |
with gr.Row(): | |
facial_expression_input = gr.Dropdown(label="Expression", choices=list(facial_expression_mapping.keys()), value="") | |
posture_input = gr.Dropdown(label="Posture", choices=list(posture_mapping.keys()), value="") | |
with gr.Row(): | |
clothing_style_input = gr.Dropdown(label="Style Vêtements", choices=list(clothing_style_mapping.keys()), value="") | |
accessories_input = gr.Dropdown(label="Accessoires", choices=list(accessories_mapping.keys()), value="") | |
reset_visuals_button = gr.Button("Réinitialiser Détails Visuels", size="sm") | |
with gr.Column(scale=1): # Colonne de droite pour l'image et le bouton | |
persona_image_output = gr.Image(label="Image du Persona", type="filepath", height=400) # filepath pour stocker le chemin | |
generate_image_button = gr.Button("🖼️ Générer / Mettre à jour l'Image") | |
gr.Markdown("<small>💡 **Attention :** Les IA génératrices d'images peuvent reproduire des stéréotypes. Utilisez les détails visuels pour créer une représentation nuancée et inclusive.</small>", 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.click(lambda: [""] * len(visual_inputs), outputs=visual_inputs) | |
generate_image_button.click( | |
fn=generate_persona_image_v2, | |
inputs=[ | |
first_name_input, last_name_input, age_input, gender_input, persona_description_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 # Passer l'état du log | |
], | |
outputs=[persona_image_path_state, session_log_state] # Stocker le chemin de l'image dans l'état | |
).then( | |
lambda path_state: path_state.value, # Récupérer la valeur du chemin depuis l'état | |
inputs=persona_image_path_state, | |
outputs=persona_image_output # Mettre à jour l'affichage de l'image | |
) | |
# --- Onglet 3 : Profil Détaillé & Raffinement IA --- | |
with gr.Tab("📝 Étape 3: Profil Détaillé & Raffinement IA", 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.") | |
# Organiser en sections pour plus de clarté | |
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=["", "Études secondaires", "Baccalauréat", "Licence", "Master", "Doctorat", "Autre"]) | |
profession_input = gr.Textbox(label="Profession") | |
income_input = gr.Number(label="Revenus annuels (€)", minimum=0, step=1000) | |
gr.Markdown("#### Psychographie") | |
with gr.Row(equal_height=False): | |
personality_traits_input = gr.Textbox(label="Traits de personnalité", lines=2, scale=4) | |
refine_personality_traits_button = gr.Button("💡 Affiner", 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') | |
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') | |
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') | |
with gr.Column(): | |
gr.Markdown("#### Relation au Produit/Service") | |
with gr.Row(equal_height=False): | |
technology_relationship_input = gr.Textbox(label="Relation avec la technologie (ex: early adopter, prudent...)", lines=2, scale=4) | |
refine_technology_relationship_button = gr.Button("💡 Affiner", 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') | |
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') | |
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') | |
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') | |
# Ajouter les autres champs de V1 ici si nécessaire (Responsabilités, Journée type, Marketing...) pour un persona complet | |
with gr.Accordion("Autres Informations (Optionnel)", open=False): | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("#### Contexte Professionnel/Vie Quotidienne") | |
with gr.Row(equal_height=False): | |
main_responsibilities_input = gr.Textbox(label="Responsabilités principales (pro/perso)", lines=2, scale=4) | |
refine_main_responsibilities_button = gr.Button("💡 Affiner", 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') | |
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') | |
with gr.Column(): | |
gr.Markdown("#### Marketing & Considérations Spéciales") | |
with gr.Row(equal_height=False): | |
brand_relationship_input = gr.Textbox(label="Relation avec la marque", lines=2, scale=4) | |
refine_brand_relationship_button = gr.Button("💡 Affiner", 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') | |
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') | |
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') | |
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') | |
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') | |
# Fonction lambda générique pour appeler refine_persona_details_v2 | |
def create_refine_lambda(field_name_display, input_component): | |
return lambda fname, lname, age_val, field_val, bias_state, objectives, log_state: \ | |
refine_persona_details_v2(fname, lname, age_val, field_name_display, field_val, json.dumps(bias_state) if bias_state else None, objectives, log_state) | |
# Lier chaque bouton "Affiner" | |
common_inputs = [first_name_input, last_name_input, age_input] | |
state_inputs = [bias_analysis_result_state, objective_input, session_log_state] | |
common_outputs = [session_log_state] # Sortie invisible pour mettre à jour le log | |
refine_personality_traits_button.click(create_refine_lambda("Traits de personnalité", personality_traits_input), inputs=common_inputs + [personality_traits_input] + state_inputs, outputs=common_outputs) | |
refine_values_beliefs_button.click(create_refine_lambda("Valeurs et croyances", values_beliefs_input), inputs=common_inputs + [values_beliefs_input] + state_inputs, outputs=common_outputs) | |
refine_motivations_button.click(create_refine_lambda("Motivations", motivations_input), inputs=common_inputs + [motivations_input] + state_inputs, outputs=common_outputs) | |
refine_hobbies_interests_button.click(create_refine_lambda("Loisirs et intérêts", hobbies_interests_input), inputs=common_inputs + [hobbies_interests_input] + state_inputs, outputs=common_outputs) | |
refine_technology_relationship_button.click(create_refine_lambda("Relation avec la technologie", technology_relationship_input), inputs=common_inputs + [technology_relationship_input] + state_inputs, outputs=common_outputs) | |
refine_product_related_activities_button.click(create_refine_lambda("Tâches liées au produit", product_related_activities_input), inputs=common_inputs + [product_related_activities_input] + state_inputs, outputs=common_outputs) | |
refine_pain_points_button.click(create_refine_lambda("Points de douleur", pain_points_input), inputs=common_inputs + [pain_points_input] + state_inputs, outputs=common_outputs) | |
refine_product_goals_button.click(create_refine_lambda("Objectifs produit", product_goals_input), inputs=common_inputs + [product_goals_input] + state_inputs, outputs=common_outputs) | |
refine_usage_scenarios_button.click(create_refine_lambda("Scénarios d'utilisation", usage_scenarios_input), inputs=common_inputs + [usage_scenarios_input] + state_inputs, outputs=common_outputs) | |
# Ajouter les clics pour les autres boutons "Affiner" si ajoutés... | |
refine_main_responsibilities_button.click(create_refine_lambda("Responsabilités principales", main_responsibilities_input), inputs=common_inputs + [main_responsibilities_input] + state_inputs, outputs=common_outputs) | |
refine_daily_activities_button.click(create_refine_lambda("Activités journalières", daily_activities_input), inputs=common_inputs + [daily_activities_input] + state_inputs, outputs=common_outputs) | |
refine_daily_life_button.click(create_refine_lambda("Journée type/Citation", daily_life_input), inputs=common_inputs + [daily_life_input] + state_inputs, outputs=common_outputs) | |
refine_brand_relationship_button.click(create_refine_lambda("Relation marque", brand_relationship_input), inputs=common_inputs + [brand_relationship_input] + state_inputs, outputs=common_outputs) | |
refine_market_segment_button.click(create_refine_lambda("Segment marché", market_segment_input), inputs=common_inputs + [market_segment_input] + state_inputs, outputs=common_outputs) | |
refine_commercial_objectives_button.click(create_refine_lambda("Objectifs commerciaux", commercial_objectives_input), inputs=common_inputs + [commercial_objectives_input] + state_inputs, outputs=common_outputs) | |
refine_visual_codes_button.click(create_refine_lambda("Codes visuels", visual_codes_input), inputs=common_inputs + [visual_codes_input] + state_inputs, outputs=common_outputs) | |
refine_special_considerations_button.click(create_refine_lambda("Considérations spéciales", special_considerations_input), inputs=common_inputs + [special_considerations_input] + state_inputs, outputs=common_outputs) | |
refine_references_button.click(create_refine_lambda("Références", references_input), inputs=common_inputs + [references_input] + state_inputs, outputs=common_outputs) | |
# --- Onglet 4 : Résumé du Persona --- | |
with gr.Tab("📄 Étape 4: Résumé du Persona", id=3): | |
gr.Markdown("### 4. Visualisez le persona complet") | |
summary_button = gr.Button("Générer le Résumé du Persona") | |
summary_content = gr.Markdown(elem_classes="persona-summary") # Ajouter une classe pour CSS potentiel | |
# Collecter tous les inputs nécessaires pour le résumé | |
all_persona_inputs = [ | |
first_name_input, last_name_input, age_input, gender_input, persona_description_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, | |
persona_image_path_state, # Passer l'état contenant le chemin de l'image | |
session_log_state | |
] | |
summary_button.click( | |
fn=generate_summary_v2, | |
inputs=all_persona_inputs, | |
outputs=[summary_content, session_log_state] # Met à jour le contenu et le log | |
) | |
# --- 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 de votre session pour faciliter l'analyse et la traçabilité.") | |
log_display_final = gr.Textbox(label="Historique de la session", lines=20, interactive=False) | |
export_log_button_final = gr.Button("Exporter le Journal en .txt") | |
log_file_output = gr.File(label="Télécharger le Journal", file_count="single", visible=False) # Caché initialement | |
# Mettre à jour l'affichage du log quand l'état change | |
session_log_state.change( | |
fn=lambda log_data: log_data, | |
inputs=session_log_state, | |
outputs=log_display_final | |
) | |
# Fonction pour préparer et retourner le fichier log | |
def export_log_file(log_data): | |
if not log_data: | |
return gr.update(visible=False) | |
# Créer un fichier texte temporaire | |
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"Log exporté vers : {temp_filepath}") # Pour debug | |
# Mettre à jour le composant File pour proposer le téléchargement | |
return gr.update(value=temp_filepath, visible=True) | |
export_log_button_final.click( | |
fn=export_log_file, | |
inputs=session_log_state, | |
outputs=log_file_output | |
) | |
# Lancer l'application | |
demo.queue().launch(debug=True, share=False) # Share=False par défaut pour sécurité avec clé API |