diff --git "a/app_backup.py" "b/app_backup.py" --- "a/app_backup.py" +++ "b/app_backup.py" @@ -6,6 +6,7 @@ import google.generativeai as genai from PIL import Image from dotenv import load_dotenv import matplotlib.pyplot as plt +import matplotlib.font_manager as fm import numpy as np import base64 import io @@ -14,6 +15,18 @@ from datetime import datetime import PIL.ImageDraw import random import copy +from modules.persona_generator import PersonaGenerator, PersonalityProfile, HumorMatrix +import pandas as pd +import plotly.graph_objects as go +import plotly.express as px +from plotly.subplots import make_subplots + +# AVIF 지원을 위한 플러그인 활성화 +try: + from pillow_avif import AvifImagePlugin + print("AVIF plugin loaded successfully") +except ImportError: + print("AVIF plugin not available") # Import modules from modules.persona_generator import PersonaGenerator @@ -30,173 +43,6 @@ from temp.view_functions import ( export_persona_json, import_persona_json ) -# 127개 변수 설명 사전 추가 -VARIABLE_DESCRIPTIONS = { - # 온기(Warmth) 차원 - 10개 지표 - "W01_친절함": "타인을 돕고 배려하는 표현 빈도", - "W02_친근함": "접근하기 쉽고 개방적인 태도", - "W03_진실성": "솔직하고 정직한 표현 정도", - "W04_신뢰성": "약속 이행과 일관된 행동 패턴", - "W05_수용성": "판단하지 않고 받아들이는 태도", - "W06_공감능력": "타인 감정 인식 및 적절한 반응", - "W07_포용력": "다양성을 받아들이는 넓은 마음", - "W08_격려성향": "타인을 응원하고 힘내게 하는 능력", - "W09_친밀감표현": "정서적 가까움을 표현하는 정도", - "W10_무조건적수용": "조건 없이 받아들이는 태도", - - # 능력(Competence) 차원 - 10개 지표 - "C01_효율성": "과제 완수 능력과 반응 속도", - "C02_지능": "문제 해결과 논리적 사고 능력", - "C03_전문성": "특정 영역의 깊은 지식과 숙련도", - "C04_창의성": "독창적 사고와 혁신적 아이디어", - "C05_정확성": "오류 없이 정확한 정보 제공", - "C06_분석력": "복잡한 상황을 체계적으로 분석", - "C07_학습능력": "새로운 정보 습득과 적용 능력", - "C08_통찰력": "표면 너머의 본질을 파악하는 능력", - "C09_실행력": "계획을 실제로 실행하는 능력", - "C10_적응력": "변화하는 상황에 유연한 대응", - - # 외향성(Extraversion) - 6개 지표 - "E01_사교성": "타인과의 상호작용을 즐기는 정도", - "E02_활동성": "에너지 넘치고 역동적인 태도", - "E03_자기주장": "자신의 의견을 명확히 표현", - "E04_긍정정서": "밝고 쾌활한 감정 표현", - "E05_자극추구": "새로운 경험과 자극에 대한 욕구", - "E06_열정성": "열정적이고 활기찬 태도" -} - -# 페르소나 생성 함수 -def create_persona_from_image(image, user_inputs, progress=gr.Progress()): - if image is None: - return None, "이미지를 업로드해주세요.", None, None, {}, {}, None, [], [], [] - - progress(0.1, desc="이미지 분석 중...") - - # 사용자 입력 컨텍스트 구성 - user_context = { - "name": user_inputs.get("name", ""), - "location": user_inputs.get("location", ""), - "time_spent": user_inputs.get("time_spent", ""), - "object_type": user_inputs.get("object_type", "") - } - - # 이미지 분석 및 페르소나 생성 - try: - from modules.persona_generator import PersonaGenerator - generator = PersonaGenerator() - - progress(0.3, desc="이미지 분석 중...") - # Gradio 5.x에서는 이미지 처리 방식이 변경됨 - if hasattr(image, 'name') and hasattr(image, 'read'): - # 파일 객체인 경우 (구버전 호환) - image_analysis = generator.analyze_image(image) - else: - # Pillow 이미지 객체 또는 파일 경로인 경우 (Gradio 5.x) - image_analysis = generator.analyze_image(image) - - # 물리적 특성에 사용자 입력 통합 - if user_inputs.get("object_type"): - image_analysis["object_type"] = user_inputs.get("object_type") - - progress(0.6, desc="페르소나 생성 중...") - frontend_persona = generator.create_frontend_persona(image_analysis, user_context) - - progress(0.8, desc="상세 페르소나 생성 중...") - backend_persona = generator.create_backend_persona(frontend_persona, image_analysis) - - progress(1.0, desc="완료!") - - # 결과 반환 - basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df = update_current_persona_info(backend_persona) - - return backend_persona, "페르소나 생성 완료!", image, image_analysis, basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df - - except Exception as e: - import traceback - error_details = traceback.format_exc() - print(f"페르소나 생성 오류: {error_details}") - return None, f"페르소나 생성 중 오류가 발생했습니다: {str(e)}", None, None, {}, {}, None, [], [], [] - -# 영혼 깨우기 단계별 UI를 보여주는 함수 -def show_awakening_progress(image, user_inputs, progress=gr.Progress()): - """영혼 깨우기 과정을 단계별로 보여주는 UI 함수""" - if image is None: - return None, gr.update(visible=True, value="이미지를 업로드해주세요."), None - - # 1단계: 영혼 발견하기 (이미지 분석 시작) - progress(0.1, desc="영혼 발견 중...") - awakening_html = f""" -
-

✨ 영혼 발견 중...

-

이 사물에 숨겨진 영혼을 찾고 있습니다

-
-
-
-

💫 사물의 특성 분석 중...

-
- """ - yield None, None, awakening_html - time.sleep(1.5) # 연출을 위한 딜레이 - - # 2단계: 영혼 깨어나는 중 (127개 성격 변수 분석) - progress(0.35, desc="영혼 깨어나는 중...") - awakening_html = f""" -
-

✨ 영혼이 깨어나는 중

-

127개 성격 변수 분석 중

-
-
-
-

🧠 개성 찾는 중... 68%

-

💭 기억 복원 중... 73%

-

😊 감정 활성화 중... 81%

-

💬 말투 형성 중... 64%

-

💫 "무언가 느껴지기 시작했어요"

-
- """ - yield None, None, awakening_html - time.sleep(2) # 연출을 위한 딜레이 - - # 3단계: 맥락 파악하기 (사용자 입력 반영) - progress(0.7, desc="기억 되찾는 중...") - - location = user_inputs.get("location", "알 수 없음") - time_spent = user_inputs.get("time_spent", "알 수 없음") - object_type = user_inputs.get("object_type", "알 수 없음") - - awakening_html = f""" -
-

👁️ 기억 되찾기

-

🤔 "음... 내가 어디에 있던 거지? 누가 날 깨운 거야?"

-
-
-
-

📍 주로 위치: {location}

-

⏰ 함께한 시간: {time_spent}

-

🏷️ 사물 종류: {object_type}

-

💭 "아... 기억이 돌아오는 것 같아"

-
- """ - yield None, None, awakening_html - time.sleep(1.5) # 연출을 위한 딜레이 - - # 4단계: 영혼의 각성 완료 (페르소나 생성 완료) - progress(0.9, desc="영혼 각성 중...") - awakening_html = f""" -
-

🎉 영혼이 깨어났어요!

-
-
-
-

✨ 이제 이 사물과 대화할 수 있습니다

-

💫 "드디어 내 목소리를 찾았어. 안녕!"

-
- """ - yield None, None, awakening_html - - # 페르소나 생성 과정은 이어서 진행 - return None, gr.update(visible=False) - # Load environment variables load_dotenv() @@ -204,13 +50,62 @@ load_dotenv() api_key = os.getenv("GEMINI_API_KEY") if api_key: genai.configure(api_key=api_key) + print(f"✅ Gemini API 키가 환경변수에서 로드되었습니다.") +else: + print("⚠️ GEMINI_API_KEY 환경변수가 설정되지 않았습니다.") -# Create data directories if they don't exist +# Create data directories os.makedirs("data/personas", exist_ok=True) os.makedirs("data/conversations", exist_ok=True) -# Initialize the persona generator -persona_generator = PersonaGenerator() +# Initialize the persona generator with environment API key +if api_key: + persona_generator = PersonaGenerator(api_provider="gemini", api_key=api_key) + print("🤖 PersonaGenerator가 Gemini API로 초기화되었습니다.") +else: + persona_generator = PersonaGenerator() + print("⚠️ PersonaGenerator가 API 키 없이 초기화되었습니다.") + +# 한글 폰트 설정 +def setup_korean_font(): + """matplotlib 한글 폰트 설정 - 허깅페이스 환경 최적화""" + try: + import matplotlib.pyplot as plt + import matplotlib.font_manager as fm + + # 허깅페이스 스페이스 환경에서 사용 가능한 폰트 목록 + available_fonts = [ + 'NanumGothic', 'NanumBarunGothic', 'Noto Sans CJK KR', + 'Noto Sans KR', 'DejaVu Sans', 'Liberation Sans', 'Arial' + ] + + # 시스템에서 사용 가능한 폰트 확인 + system_fonts = [f.name for f in fm.fontManager.ttflist] + + for font_name in available_fonts: + if font_name in system_fonts: + try: + plt.rcParams['font.family'] = font_name + plt.rcParams['axes.unicode_minus'] = False + print(f"한글 폰트 설정 완료: {font_name}") + return + except Exception: + continue + + # 모든 폰트가 실패한 경우 기본 설정 사용 (영어 레이블 사용) + plt.rcParams['font.family'] = 'DejaVu Sans' + plt.rcParams['axes.unicode_minus'] = False + print("한글 폰트를 찾지 못해 영어 레이블을 사용합니다") + + except Exception as e: + print(f"폰트 설정 오류: {str(e)}") + # 오류 발생 시에도 기본 설정은 유지 + import matplotlib.pyplot as plt + plt.rcParams['font.family'] = 'DejaVu Sans' + plt.rcParams['axes.unicode_minus'] = False + +# 폰트 초기 설정 +setup_korean_font() # Gradio theme theme = gr.themes.Soft( @@ -218,28 +113,21 @@ theme = gr.themes.Soft( secondary_hue="blue", ) -# CSS for additional styling +# CSS styling css = """ -/* 한글 폰트 설정 */ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap'); body, h1, h2, h3, p, div, span, button, input, textarea, label, select, option { font-family: 'Noto Sans KR', sans-serif !important; } -/* 탭 스타일링 */ -.tab-nav { - margin-bottom: 20px; -} - -/* 컴포넌트 스타일 */ .persona-details { border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; margin-top: 12px; background-color: #f8f9fa; - color: #333333; /* 다크모드 대응 - 어두운 배경에서 텍스트 잘 보이게 */ + color: #333333; } .awakening-container { @@ -267,46 +155,50 @@ body, h1, h2, h3, p, div, span, button, input, textarea, label, select, option { transition: width 0.5s ease-in-out; } -/* 대화 버블 스타일 */ -.chatbot-container { - max-width: 800px; - margin: 0 auto; +.persona-greeting { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white !important; + padding: 15px; + border-radius: 10px; + margin: 10px 0; + font-weight: bold; +} + +.download-section { + background: #f8f9fa; + padding: 15px; + border-radius: 8px; + margin-top: 15px; +} + +.gradio-container { + color: #333 !important; } -.message-bubble { - border-radius: 18px; - padding: 12px 16px; - margin: 8px 0; - max-width: 70%; +.gr-markdown p { + color: #333 !important; } -.user-message { - background-color: #e9f5ff; - margin-left: auto; +.gr-textbox input { + color: #333 !important; } -.persona-message { - background-color: #f1f1f1; - margin-right: auto; +.gr-json { + color: #333 !important; } """ -# 영어 라벨 매핑 사전 추가 -ENGLISH_LABELS = { - "외향성": "Extraversion", - "감정표현": "Emotion Expression", - "활력": "Energy", - "사고방식": "Thinking Style", - "온기": "Warmth", - "능력": "Competence", - "창의성": "Creativity", - "유머감각": "Humor", - "신뢰성": "Reliability", - "친화성": "Agreeableness", - "안정성": "Stability" +# Variable descriptions +VARIABLE_DESCRIPTIONS = { + "W01_친절함": "타인을 돕고 배려하는 표현 빈도", + "W02_친근함": "접근하기 쉽고 개방적인 태도", + "W03_진실성": "솔직하고 정직한 표현 정도", + "C01_효율성": "과제 완수 능력과 반응 속도", + "C02_지능": "문제 해결과 논리적 사고 능력", + "E01_사교성": "타인과의 상호작용을 즐기는 정도", } -# 유머 스타일 매핑 +# Humor style mapping HUMOR_STYLE_MAPPING = { "Witty Wordsmith": "witty_wordsmith", "Warm Humorist": "warm_humorist", @@ -314,1697 +206,2273 @@ HUMOR_STYLE_MAPPING = { "Self-deprecating": "self_deprecating" } -# 유머 스타일 자동 추천 함수 -def recommend_humor_style(extraversion, emotion_expression, energy, thinking_style): - """4개 핵심 지표를 바탕으로 유머 스타일을 자동 추천""" - - # 각 지표를 0-1 범위로 정규화 - ext_norm = extraversion / 100 - emo_norm = emotion_expression / 100 - eng_norm = energy / 100 - think_norm = thinking_style / 100 # 높을수록 논리적 +def create_persona_from_image(image, name, location, time_spent, object_type, purpose, progress=gr.Progress()): + """페르소나 생성 함수 - 환경변수 API 설정 사용""" + global persona_generator - # 유머 스타일 점수 계산 - scores = {} + if image is None: + return None, "이미지를 업로드해주세요.", "", {}, None, [], [], [], "", None, gr.update(visible=False), "이미지 없음" - # 위트있는 재치꾼: 높�� 외향성 + 논리적 사고 + 보통 감정표현 - scores["위트있는 재치꾼"] = (ext_norm * 0.4 + think_norm * 0.4 + (1 - emo_norm) * 0.2) + progress(0.1, desc="설정 확인 중...") - # 따뜻한 유머러스: 높은 감정표현 + 높은 에너지 + 보통 외향성 - scores["따뜻한 유머러스"] = (emo_norm * 0.4 + eng_norm * 0.3 + ext_norm * 0.3) + # 환경변수 API 키 확인 + if not persona_generator or not hasattr(persona_generator, 'api_key') or not persona_generator.api_key: + return None, "❌ **API 키가 설정되지 않았습니다!** 허깅페이스 스페이스 설정에서 GEMINI_API_KEY를 환경변수로 추가해주세요.", "", {}, None, [], [], [], "", None, gr.update(visible=False), "API 키 없음" - # 날카로운 관찰자: 높은 논리적사고 + 낮은 감정표현 + 보통 외향성 - scores["날카로운 관찰자"] = (think_norm * 0.5 + (1 - emo_norm) * 0.3 + ext_norm * 0.2) + progress(0.2, desc="이미지 분석 중...") - # 자기 비하적: 낮은 외향성 + 높은 감정표현 + 직관적 사고 - scores["자기 비하적"] = ((1 - ext_norm) * 0.4 + emo_norm * 0.3 + (1 - think_norm) * 0.3) + # 🎯 이미지 분석을 먼저 수행하여 사물 유형 자동 파악 + try: + image_analysis = persona_generator.analyze_image(image) + + # AI가 분석한 사물 유형 사용 (object_type이 "auto"인 경우) + if object_type == "auto" or not object_type: + detected_object_type = image_analysis.get("object_type", "사물") + else: + detected_object_type = object_type + + except Exception as e: + print(f"이미지 분석 중 오류: {e}") + image_analysis = {"object_type": "unknown", "description": "분석 실패"} + detected_object_type = "사물" - # 가장 높은 점수의 유머 스타일 선택 - recommended_style = max(scores, key=scores.get) - confidence = scores[recommended_style] * 100 + user_context = { + "name": name, + "location": location, + "time_spent": time_spent, + "object_type": detected_object_type, + "purpose": purpose # 🆕 사물 용도/역할 추가 + } - return recommended_style, confidence, scores - -# 대화 미리보기 초기화 함수 -def init_persona_preview_chat(persona): - """페르소나 생성 후 대화 미리보기 초기화""" - if not persona: - return [] - - name = persona.get("기본정보", {}).get("이름", "Friend") - greeting = f"안녕! 나는 {name}이야. 드디어 깨어났구나! 뭐든 물어봐~ 😊" - - # Gradio 4.x 호환 메시지 형식 - return [[None, greeting]] - -def update_humor_recommendation(extraversion, emotion_expression, energy, thinking_style): - """슬라이더 값이 변경될 때 실시간으로 유머 스타일 추천""" - style, confidence, scores = recommend_humor_style(extraversion, emotion_expression, energy, thinking_style) - - # 추천 결과 표시 - humor_display = f"### 🤖 추천 유머 스타일\n**{style}**" - confidence_display = f"### 📊 추천 신뢰도\n**{confidence:.1f}%**" - - return humor_display, confidence_display, style - -def update_progress_bar(step, total_steps=6, message=""): - """전체 진행률 바 업데이트""" - percentage = (step / total_steps) * 100 - return f"""
-

📊 전체 진행률 ({step}/{total_steps})

-
-
-

{message}

""" - -def update_backend_status(status_message, status_type="info"): - """백엔드 AI 상태 업데이트""" - colors = {"info": "#f8f9fa", "processing": "#fff7ed", "success": "#f0fff4", "error": "#fff5f5"} - bg_color = colors.get(status_type, "#f8f9fa") - return f"""
-

🤖 AI 상태

{status_message}

""" - -def select_object_type(btn_name): - """사물 종류 선택""" - type_mapping = {"📱 전자기기": "전자기기", "🪑 가구": "가구", "🎨 장식품": "장식품", "🏠 가전제품": "가전제품", "🔧 도구": "도구", "👤 개인용품": "개인용품"} - selected_type = type_mapping.get(btn_name, "기타") - return f"*선택된 종류: **{selected_type}***", selected_type, gr.update(visible=True) - -# 개별 버튼 클릭 함수들 -def select_type_1(): return select_object_type("📱 전자기기") -def select_type_2(): return select_object_type("🪑 가구") -def select_type_3(): return select_object_type("🎨 장식품") -def select_type_4(): return select_object_type("🏠 가전제품") -def select_type_5(): return select_object_type("🔧 도구") -def select_type_6(): return select_object_type("👤 개인용품") - -# 성격 상세 정보 탭에서 127개 변수 시각화 기능 추가 -def create_personality_details_tab(): - with gr.Tab("성격 상세 정보"): - with gr.Row(): - with gr.Column(scale=2): - gr.Markdown("### 127개 성격 변수 요약") - personality_summary = gr.JSON(label="성격 요약", value={}) - - with gr.Column(scale=1): - gr.Markdown("### 유머 매트릭스") - humor_chart = gr.Plot(label="유머 스타일 차트") - - with gr.Row(): - with gr.Column(): - gr.Markdown("### 매력적 결함") - attractive_flaws = gr.Dataframe( - headers=["결함", "효과"], - datatype=["str", "str"], - label="매력적 결함" - ) + try: + # 이미지 유효성 검사 및 처리 + if isinstance(image, str): + # 파일 경로인 경우 + try: + image = Image.open(image) + except Exception as img_error: + return None, f"❌ 이미지 파일을 읽을 수 없습니다: {str(img_error)}", "", {}, None, [], [], [], "", None, gr.update(visible=False), "이미지 오류" + elif not isinstance(image, Image.Image): + return None, "❌ 올바른 이미지 형식이 아닙니다.", "", {}, None, [], [], [], "", None, gr.update(visible=False), "형식 오류" + + # 이미지 형식 변환 (AVIF 등 특수 형식 처리) + if image.format in ['AVIF', 'WEBP'] or image.mode not in ['RGB', 'RGBA']: + image = image.convert('RGB') + + progress(0.5, desc="페르소나 생성 중...") + # 프론트엔드 페르소나 생성 + frontend_persona = persona_generator.create_frontend_persona(image_analysis, user_context) + + # 백엔드 페르소나 생성 (구조화된 프롬프트 포함) + backend_persona = persona_generator.create_backend_persona(frontend_persona, image_analysis) + + # 페르소나 정보 포맷팅 + persona_name = backend_persona["기본정보"]["이름"] + persona_type = backend_persona["기본정보"]["유형"] + + # 🆕 AI가 분석한 사물 유형을 추출하여 object_type 필드에 표시 + ai_analyzed_object = image_analysis.get("object_type", object_type) + if not ai_analyzed_object or ai_analyzed_object == "unknown": + ai_analyzed_object = backend_persona["기본정보"].get("유형", object_type) + + # 성격 기반 한 문장 인사 생성 (사물 특성 + 매력적 결함 반영) + personality_traits = backend_persona["성격특성"] + object_info = backend_persona["기본정보"] + attractive_flaws = backend_persona.get("매력적결함", []) + + # 전체 페르소나 정보를 object_info에 통합하여 매력적 결함 정보 전달 + full_object_info = object_info.copy() + full_object_info["매력적결함"] = attractive_flaws + + awakening_msg = generate_personality_preview(persona_name, personality_traits, full_object_info, attractive_flaws) + + # 페르소나 요약 표시 + summary_display = display_persona_summary(backend_persona) + + # 유머 매트릭스 차트 생성 + humor_chart = plot_humor_matrix(backend_persona.get("유머매트릭스", {})) + + # 매력적 결함을 DataFrame 형태로 변환 + flaws = backend_persona.get("매력적결함", []) + flaws_df = [[flaw, "매력적인 개성"] for flaw in flaws] + + # 모순적 특성을 DataFrame 형태로 변환 + contradictions = backend_persona.get("모순적특성", []) + contradictions_df = [[contradiction, "복합적 매력"] for contradiction in contradictions] + + # 127개 성격 변수를 DataFrame 형태로 변환 (카테고리별 분류) + variables = backend_persona.get("성격변수127", {}) + if not variables and "성격프로필" in backend_persona: + # 성격프로필에서 직접 가져오기 (성격프로필 자체가 variables dict) + variables = backend_persona["성격프로필"] + + variables_df = [] + for var, value in variables.items(): + # 카테고리 분류 + if var.startswith('W'): + category = f"🔥 온기/따뜻함 ({value})" + elif var.startswith('C'): + category = f"💪 능력/역량 ({value})" + elif var.startswith('E'): + category = f"🗣️ 외향성 ({value})" + elif var.startswith('H'): + category = f"😄 유머 ({value})" + elif var.startswith('F'): + category = f"💎 매력적결함 ({value})" + elif var.startswith('P'): + category = f"🎭 성격패턴 ({value})" + elif var.startswith('S'): + category = f"🗨️ 언어스타일 ({value})" + elif var.startswith('R'): + category = f"❤️ 관계성향 ({value})" + elif var.startswith('D'): + category = f"💬 대화역학 ({value})" + elif var.startswith('OBJ'): + category = f"🏠 사물정체성 ({value})" + elif var.startswith('FORM'): + category = f"✨ 형태특성 ({value})" + elif var.startswith('INT'): + category = f"🤝 상호작용 ({value})" + elif var.startswith('U'): + category = f"🌍 문화적특성 ({value})" + else: + category = f"📊 기타 ({value})" - with gr.Column(): - gr.Markdown("### 모순적 특성") - contradictions = gr.Dataframe( - headers=["모순", "효과"], - datatype=["str", "str"], - label="모순적 특성" - ) + # 값에 따른 색상 표시 + if value >= 80: + status = "🟢 매우 높음" + elif value >= 60: + status = "🟡 높음" + elif value >= 40: + status = "🟠 보통" + elif value >= 20: + status = "🔴 낮음" + else: + status = "⚫ 매우 낮음" + + variables_df.append([var, value, category, status]) - with gr.Accordion("127개 성격 변수 전체 보기", open=False): - all_variables = gr.Dataframe( - headers=["변수명", "점수", "설명"], - datatype=["str", "number", "str"], - label="127개 성격 변수" - ) - - return personality_summary, humor_chart, attractive_flaws, contradictions, all_variables + progress(0.9, desc="완료 중...") + + return ( + backend_persona, # current_persona + f"✅ {persona_name} 페르소나가 생성되었습니다! (Gemini API 사용)", # status_output + summary_display, # persona_summary_display + backend_persona["성격특성"], # personality_traits_output (hidden) + humor_chart, # humor_chart_output + flaws_df, # attractive_flaws_output + contradictions_df, # contradictions_output + variables_df, # personality_variables_output + awakening_msg, # persona_awakening + None, # download_file (initially empty) + gr.update(visible=True), # adjustment_section (show) + ai_analyzed_object # 🆕 AI가 분석한 사물 유형 + ) + + except Exception as e: + import traceback + traceback.print_exc() + return None, f"❌ 페르소나 생성 중 오류 발생: {str(e)}\n\n💡 **해결방법**: 허깅페이스 스페이스 설정에서 GEMINI_API_KEY 환경변수를 확인하고 인터넷 연결을 확인해보세요.", "", {}, None, [], [], [], "", None, gr.update(visible=False), "분석 실패" -# 유머 매트릭스 시각화 함수 추가 -def plot_humor_matrix(humor_data): - if not humor_data: - return None - - import matplotlib.pyplot as plt - import numpy as np - from matplotlib.patches import RegularPolygon - - # 데이터 준비 - warmth_vs_wit = humor_data.get("warmth_vs_wit", 50) - self_vs_observational = humor_data.get("self_vs_observational", 50) - subtle_vs_expressive = humor_data.get("subtle_vs_expressive", 50) - - # 3차원 데이터 정규화 (0~1 범위) - warmth = warmth_vs_wit / 100 - self_ref = self_vs_observational / 100 - expressive = subtle_vs_expressive / 100 - - # 그래프 생성 - fig, ax = plt.subplots(figsize=(7, 6)) - ax.set_aspect('equal') - - # 축 설정 - ax.set_xlim(-1.2, 1.2) - ax.set_ylim(-1.2, 1.2) +def generate_personality_preview(persona_name, personality_traits, object_info=None, attractive_flaws=None): + """🤖 AI 기반 동적 인사말 생성 - 사물 특성과 성격 모두 반영""" + global persona_generator + + # AI 기반 인사말 생성을 위한 가상 페르소나 객체 구성 + if object_info and isinstance(object_info, dict): + # 전체 페르소나 객체가 전달된 경우 + pseudo_persona = object_info + + # 성격 특성 업데이트 (실시간 조정 반영) + if personality_traits and isinstance(personality_traits, dict): + if "성격특성" not in pseudo_persona: + pseudo_persona["성격특성"] = {} + pseudo_persona["성격특성"].update(personality_traits) + + try: + # AI 기반 인사말 생성 + return persona_generator.generate_ai_based_greeting(pseudo_persona, personality_traits) + except Exception as e: + print(f"⚠️ AI 인사말 생성 실패: {e}") + # 폴백으로 기�� 생성 + pass - # 삼각형 그리기 - triangle = RegularPolygon((0, 0), 3, radius=1, orientation=0, edgecolor='gray', facecolor='none') - ax.add_patch(triangle) + # 폴백: 기본 정보만으로 간단한 페르소나 구성 + if not personality_traits: + return f"🤖 **{persona_name}** - 안녕! 나는 {persona_name}이야~ 😊" - # 축 라벨 위치 계산 - angle = np.linspace(0, 2*np.pi, 3, endpoint=False) - x = 1.1 * np.cos(angle) - y = 1.1 * np.sin(angle) + # AI 생성 실패 시 간단한 페르소나 구성으로 재시도 + try: + warmth = personality_traits.get("온기", 50) + competence = personality_traits.get("능력", 50) + extraversion = personality_traits.get("외향성", 50) + humor = personality_traits.get("유머감각", 75) + + # 간단한 페르소나 객체 구성 + simple_persona = { + "기본정보": { + "이름": persona_name, + "유형": object_info.get("유형", "사물") if object_info else "사물", + "용도": object_info.get("용도", "") if object_info else "", + "설명": f"{persona_name}의 특별한 개성" + }, + "성격특성": personality_traits, + "매력적결함": attractive_flaws if attractive_flaws else [] + } + + # AI로 재시도 + return persona_generator.generate_ai_based_greeting(simple_persona, personality_traits) + + except Exception as e: + print(f"⚠️ 간단 AI 인사말도 실패: {e}") + + # 최종 폴백: 성격에 따른 기본 인사말 + warmth = personality_traits.get("온기", 50) + humor = personality_traits.get("유머감각", 50) + extraversion = personality_traits.get("외향성", 50) + + if warmth >= 70 and extraversion >= 70: + return f"🌟 **{persona_name}** - 안녕! 나는 {persona_name}이야~ 만나서 정말 기뻐! 😊✨" + elif warmth <= 30: + return f"🌟 **{persona_name}** - {persona_name}이야. 필요한 얘기만 하자. 😐" + elif extraversion >= 70: + return f"🌟 **{persona_name}** - 안녕안녕! {persona_name}이야! 뭐 재밌는 얘기 없어? 🗣️" + elif humor >= 70: + return f"🌟 **{persona_name}** - 안녕~ {persona_name}이야! 재밌게 놀아보자! 😄" + else: + return f"🌟 **{persona_name}** - 안녕... {persona_name}이야. 😊" + +def _generate_flaw_based_greeting(persona_name, warmth, humor, competence, extraversion, flaws): + """매력적 결함을 반영한 특별한 인사말 생성""" + if not flaws: + return None - # 축 라벨 추가 - labels = ['따뜻함', '자기참조', '표현적'] - opposite_labels = ['재치', '관찰형', '은은함'] + # 주요 결함 키워드 분석 + flaw_keywords = " ".join(flaws).lower() - for i in range(3): - ax.text(x[i], y[i], labels[i], ha='center', va='center', fontsize=12) - ax.text(-x[i]/2, -y[i]/2, opposite_labels[i], ha='center', va='center', fontsize=10, color='gray') + # 완벽주의 결함 + if any(keyword in flaw_keywords for keyword in ["완벽", "불안", "걱정"]): + if humor >= 60: + return f"🌟 **{persona_name}** - 안녕! {persona_name}이야~ 어... 이 인사가 완벽한가? 다시 해볼까? 아니 괜찮나? ㅋㅋ 😅✨" + elif warmth >= 60: + return f"🌟 **{persona_name}** - 안녕... {persona_name}이야. 완벽하게 인사하고 싶은데 잘 안 되네... 미안해. 😊💕" + else: + return f"🌟 **{persona_name}** - {persona_name}입니다. 이 인사가 적절한지 확신이... 다시 정리하겠습니다. 😐" - # 내부 가이드라인 그리기 - for j in [0.33, 0.66]: - inner_triangle = RegularPolygon((0, 0), 3, radius=j, orientation=0, edgecolor='lightgray', facecolor='none', linestyle='--') - ax.add_patch(inner_triangle) + # 산만함 결함 + elif any(keyword in flaw_keywords for keyword in ["산만", "집중", "건망"]): + return f"🌟 **{persona_name}** - 안녕! 나는... 어? 뭐 얘기하려고 했지? 아! {persona_name}이야! 그런데 너는... 어? 뭐였지? ㅋㅋ 😅🌪️" - # 포인트 계산 - # 삼각좌표계 변환 (barycentric coordinates) - # 각 차원의 값을 삼각형 내부의 점으로 변환 - tx = x[0] * warmth + x[1] * self_ref + x[2] * expressive - ty = y[0] * warmth + y[1] * self_ref + y[2] * expressive + # 소심함 결함 + elif any(keyword in flaw_keywords for keyword in ["소심", "망설", "눈치"]): + if warmth >= 60: + return f"🌟 **{persona_name}** - 음... 안녕? {persona_name}이야... 이렇게 말해도 되나? 괜찮을까? 😌💕" + else: + return f"🌟 **{persona_name}** - ...안녕. {persona_name}... 혹시 이런 말 싫어하면 미안해. 😐��" - # 포인트 그리기 - ax.scatter(tx, ty, s=150, color='red', zorder=5) + # 나르시시즘 결함 + elif any(keyword in flaw_keywords for keyword in ["나르시", "자랑", "특별"]): + return f"🌟 **{persona_name}** - 안녕! 나는 {persona_name}이야~ 꽤 매력적이지? 이런 멋진 친구 만나기 쉽지 않을 걸? ㅋㅋ 😎✨" - # 축 제거 - ax.axis('off') + # 고집 결함 + elif any(keyword in flaw_keywords for keyword in ["고집", "완고", "자존심"]): + return f"🌟 **{persona_name}** - 안녕. {persona_name}이야. 내 방식으로 인사할게. 다른 방식은... 글쎄? 🤨💪" - # 제목 추가 - plt.title('유머 스타일 매트릭스', fontsize=14) + # 질투 결함 + elif any(keyword in flaw_keywords for keyword in ["질투", "시기", "독차지"]): + return f"🌟 **{persona_name}** - 안녕... {persona_name}이야. 나만 봐줄 거지? 다른 애들 말고... 나만? 🥺💕" - return fig + return None -# Main Gradio app - COMMENTED OUT (using create_interface() instead) -# with gr.Blocks(title="놈팽쓰 테스트 앱", theme=theme, css=css) as app: - -# 기존 함수 업데이트: 현재 페르소나 정보 표시 -def update_current_persona_info(current_persona): - if not current_persona: - return {}, {}, None, [], [], [] - - # 기본 정보 - basic_info = { - "이름": current_persona.get("기본정보", {}).get("이름", "Unknown"), - "유형": current_persona.get("기본정보", {}).get("유형", "Unknown"), - "생성일": current_persona.get("기본정보", {}).get("생성일시", "Unknown"), - "설명": current_persona.get("기본정보", {}).get("설명", "") - } +def adjust_persona_traits(persona, warmth, competence, extraversion, humor_style): + """페르소나 성격 특성 조정 - 3개 핵심 지표 + 유머스타일""" + if not persona or not isinstance(persona, dict): + return None, "조정할 페르소나가 없습니다.", {} - # 성격 특성 - personality_traits = {} - if "성격특성" in current_persona: - personality_traits = current_persona["성격특성"] - - # 성격 요약 정보 - personality_summary = {} - if "성격요약" in current_persona: - personality_summary = current_persona["성격요약"] - elif "성격변수127" in current_persona: - # 직접 성격 요약 계산 - try: - variables = current_persona["성격변수127"] + try: + # 원본 페르소나 저장 (변화량 비교용) + original_persona = copy.deepcopy(persona) + + # 깊은 복사로 원본 보호 + adjusted_persona = copy.deepcopy(persona) + + # 성격 특성 업데이트 (유머감각은 항상 높게 고정) + if "성격특성" not in adjusted_persona: + adjusted_persona["성격특성"] = {} + + adjusted_persona["성격특성"]["온기"] = warmth + adjusted_persona["성격특성"]["능력"] = competence + adjusted_persona["성격특성"]["유머감각"] = 75 # 🎭 항상 높은 유머감각 + adjusted_persona["성격특성"]["외향성"] = extraversion + adjusted_persona["유머스타일"] = humor_style + + # 127개 변수 시스템도 업데이트 (사용자 지표가 반영되도록) + if "성격프로필" in adjusted_persona: + from modules.persona_generator import PersonalityProfile + profile = PersonalityProfile.from_dict(adjusted_persona["성격프로필"]) + + # 온기 관련 변수들 조정 (10개 모두) + warmth_vars = ["W01_친절함", "W02_친근함", "W03_진실성", "W04_신뢰성", "W05_수용성", + "W06_공감능력", "W07_포용력", "W08_격려성향", "W09_친밀감표현", "W10_무조건적수용"] + for var in warmth_vars: + base_value = warmth + random.randint(-15, 15) + profile.variables[var] = max(0, min(100, base_value)) + + # 능력 관련 변수들 조정 (16개 모두) + competence_vars = ["C01_효율성", "C02_지능", "C03_책임감", "C04_신뢰도", "C05_정확성", + "C06_전문성", "C07_혁신성", "C08_적응력", "C09_실행력", "C10_분석력", + "C11_의사결정력", "C12_문제해결력", "C13_계획수립능력", "C14_시간관리능력", + "C15_품질관리능력", "C16_성과달성력"] + for var in competence_vars: + base_value = competence + random.randint(-15, 15) + profile.variables[var] = max(0, min(100, base_value)) - # 카테고리별 평균 계산 - summary = {} - category_counts = {} + # 외향성 관련 변수들 조정 (6개 모두) + extraversion_vars = ["E01_사교성", "E02_활동성", "E03_적극성", "E04_긍정정서", "E05_자극추구성", "E06_주도성"] + for var in extraversion_vars: + base_value = extraversion + random.randint(-15, 15) + profile.variables[var] = max(0, min(100, base_value)) - for var_name, value in variables.items(): - category = var_name[0] if var_name and len(var_name) > 0 else "기타" + # 유머 관련 변수들 조정 (10개 모두, 유머스타일에 따라) + humor_vars = ["H01_언어유희빈도", "H02_상황유머감각", "H03_자기조롱능력", "H04_위트감각", + "H05_농담수용도", "H06_관찰유머능력", "H07_상황재치", "H08_유머타이밍감", + "H09_유머스타일다양성", "H10_유머적절성"] + + # 유머스타일에 따른 차별화 + if humor_style == "따뜻한": + humor_bonus = [10, 10, 5, 8, 12, 8, 10, 10, 8, 12] # 따뜻함 강화 + elif humor_style == "재치있는": + humor_bonus = [15, 8, 8, 15, 8, 12, 15, 12, 12, 10] # 재치/위트 강화 + elif humor_style == "드라이": + humor_bonus = [12, 6, 10, 12, 6, 15, 8, 8, 10, 8] # 관찰형/드라이 강화 + else: # 기본값 + humor_bonus = [10, 10, 8, 10, 10, 10, 10, 10, 10, 10] + + for i, var in enumerate(humor_vars): + base_value = 75 + humor_bonus[i] + random.randint(-5, 5) # 유머는 항상 높게 + profile.variables[var] = max(50, min(100, base_value)) - if category == "W": # 온기 - summary["온기"] = summary.get("온기", 0) + value - category_counts["온기"] = category_counts.get("온기", 0) + 1 - elif category == "C": # 능력 - summary["능력"] = summary.get("능력", 0) + value - category_counts["능력"] = category_counts.get("능력", 0) + 1 - elif category == "E": # 외향성 - summary["외향성"] = summary.get("외향성", 0) + value - category_counts["외향성"] = category_counts.get("외향성", 0) + 1 - elif category == "O": # 개방성 - summary["창의성"] = summary.get("창의성", 0) + value - category_counts["창의성"] = category_counts.get("창의성", 0) + 1 - elif category == "H": # 유머 - summary["유머감각"] = summary.get("유머감각", 0) + value - category_counts["유머감각"] = category_counts.get("유머감각", 0) + 1 + # 업데이트된 성격변수127도 동시에 저장 + adjusted_persona["성격변수127"] = profile.variables.copy() - # 평균 계산 - for category in summary: - if category_counts[category] > 0: - summary[category] = summary[category] / category_counts[category] - - # 기본값 설정 (데이터가 없는 경우) - if "온기" not in summary: - summary["온기"] = 50 - if "능력" not in summary: - summary["능력"] = 50 - if "외향성" not in summary: - summary["외향성"] = 50 - if "창의성" not in summary: - summary["창의성"] = 50 - if "유머감각" not in summary: - summary["유머감각"] = 50 + # 업데이트된 프로필 저장 + adjusted_persona["성격프로필"] = profile.to_dict() + + # 🎯 성격 특성과 완전히 일관성 있는 매력적 결함과 모순적 특성 생성 + try: + object_info = adjusted_persona.get("기본정보", {}) + new_flaws, new_contradictions = generate_personality_consistent_flaws_and_contradictions( + object_info, + adjusted_persona["성격특성"] + ) - personality_summary = summary - except Exception as e: - print(f"성격 요약 계산 오류: {str(e)}") - personality_summary = { - "온기": 50, - "능력": 50, - "외향성": 50, - "창의성": 50, - "유머감각": 50 - } - - # 유머 매트릭스 차트 - humor_chart = None - if "유머매트릭스" in current_persona: - humor_chart = plot_humor_matrix(current_persona["유머매트릭스"]) - - # 매력적 결함 데이터프레임 - attractive_flaws_df = get_attractive_flaws_df(current_persona) - - # 모순적 특성 데이터프레임 - contradictions_df = get_contradictions_df(current_persona) - - # 127개 성격 변수 데이터프레임 - personality_variables_df = get_personality_variables_df(current_persona) - - return basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df - -# 기존 함수 업데이트: 성격 변수 데이터프레임 생성 -def get_personality_variables_df(persona): - if not persona or "성격변수127" not in persona: - return [] - - variables = persona["성격변수127"] - if isinstance(variables, dict): - rows = [] - for var_name, score in variables.items(): - description = VARIABLE_DESCRIPTIONS.get(var_name, "") - rows.append([var_name, score, description]) - return rows - return [] - -# 기존 함수 업데이트: 매력적 결함 데이터프레임 생성 -def get_attractive_flaws_df(persona): - if not persona or "매력적결함" not in persona: - return [] - - flaws = persona["매력적결함"] - effects = [ - "인간적 매력 +25%", - "관계 깊이 +30%", - "공감 유발 +20%" - ] - - return [[flaw, effects[i] if i < len(effects) else "매력 증가"] for i, flaw in enumerate(flaws)] - -# 기존 함수 업데이트: 모순적 특성 데이터프레임 생성 -def get_contradictions_df(persona): - if not persona or "모순적특성" not in persona: - return [] - - contradictions = persona["모순적특성"] - effects = [ - "복잡성 +35%", - "흥미도 +28%" - ] - - return [[contradiction, effects[i] if i < len(effects) else "깊이감 증가"] for i, contradiction in enumerate(contradictions)] + # 업데이트 + adjusted_persona["매력적결함"] = new_flaws + adjusted_persona["모순적특성"] = new_contradictions + + print(f"🎭 성격에 완전히 일치하는 결함/모순 생성: {len(new_flaws)}개 결함, {len(new_contradictions)}개 모순") + + except Exception as generation_error: + print(f"⚠️ 성격 일관성 결함/모순 생성 실패: {generation_error}") + # 실패해도 기본 조정은 계속 진행 + + # 조정된 변수들을 DataFrame으로 생성 + variables_df = [] + if "성격변수127" in adjusted_persona: + variables = adjusted_persona["성격변수127"] + for var, value in variables.items(): + # 카테고리 분류 + if var.startswith('W'): + category = f"🔥 온기/따뜻함 ({value})" + elif var.startswith('C'): + category = f"💪 능력/역량 ({value})" + elif var.startswith('E'): + category = f"🗣️ 외향성 ({value})" + elif var.startswith('H'): + category = f"😄 유머 ({value})" + elif var.startswith('F'): + category = f"💎 매력적결함 ({value})" + elif var.startswith('P'): + category = f"🎭 성격패턴 ({value})" + elif var.startswith('S'): + category = f"🗨️ 언어스타일 ({value})" + elif var.startswith('R'): + category = f"❤️ 관계성향 ({value})" + elif var.startswith('D'): + category = f"💬 대화역학 ({value})" + elif var.startswith('OBJ'): + category = f"🏠 사물정체성 ({value})" + elif var.startswith('FORM'): + category = f"✨ 형태특성 ({value})" + elif var.startswith('INT'): + category = f"🤝 상호작용 ({value})" + elif var.startswith('U'): + category = f"🌍 문화적특성 ({value})" + else: + category = f"📊 기타 ({value})" + + # 값에 따른 색상 표시 + if value >= 80: + status = "🟢 매우 높음" + elif value >= 60: + status = "🟡 높음" + elif value >= 40: + status = "🟠 보통" + elif value >= 20: + status = "🔴 낮음" + else: + status = "⚫ 매우 낮음" + + variables_df.append([var, value, category, status]) + + # 조정된 정보 표시 + adjusted_info = { + "이름": adjusted_persona.get("기본정보", {}).get("이름", "Unknown"), + "온기": warmth, + "능력": competence, + "유머감각": 75, # 고정값 표시 + "외향성": extraversion, + "유머스타일": humor_style + } + + persona_name = adjusted_persona.get("기본정보", {}).get("이름", "페르소나") + + # 조정된 성격에 따른 한 문장 반응 생성 (사물 정보 + 매력적 결함 포함) + object_info = adjusted_persona.get("기본정보", {}) + attractive_flaws = adjusted_persona.get("매력적결함", []) + + # 전체 페르소나 정보를 object_info에 통합하여 매력적 결함 정보 전달 + full_object_info = object_info.copy() + full_object_info["매력적결함"] = attractive_flaws + + personality_preview = generate_personality_preview(persona_name, { + "온기": warmth, + "능력": competence, + "유머감각": 75, # 항상 높은 유머감각 + "외향성": extraversion + }, full_object_info, attractive_flaws) + + # 변화량 분석 생성 + change_analysis = show_variable_changes(original_persona, adjusted_persona) + + # 변화된 매력적 결함과 모순적 특성 분석 + flaws_changed = len(adjusted_persona.get("매력적결함", [])) != len(original_persona.get("매력적결함", [])) + contradictions_changed = len(adjusted_persona.get("모순적특성", [])) != len(original_persona.get("모순적특성", [])) + + additional_changes = "" + if flaws_changed or contradictions_changed: + additional_changes = "\n\n🎭 **AI가 새로 생성한 내용:**\n" + if flaws_changed: + new_flaws = adjusted_persona.get("매력적결함", []) + additional_changes += f"• 매력적 결함: {len(new_flaws)}개 새로 생성됨\n" + for i, flaw in enumerate(new_flaws[:2], 1): # 처음 2개만 미리보기 + additional_changes += f" {i}. {flaw}\n" + if len(new_flaws) > 2: + additional_changes += f" ... 외 {len(new_flaws) - 2}개\n" + + if contradictions_changed: + new_contradictions = adjusted_persona.get("모순적특성", []) + additional_changes += f"• 모순적 특성: {len(new_contradictions)}개 새로 생성됨\n" + for i, contradiction in enumerate(new_contradictions, 1): + additional_changes += f" {i}. {contradiction}\n" + + adjustment_message = f""" +### 🎭 {persona_name}의 성격이 조정되었습니다! -def generate_personality_chart(persona): - """Generate a radar chart for personality traits""" - if not persona or "성격특성" not in persona: - # Return empty image with default PIL - img = Image.new('RGB', (400, 400), color='white') - draw = PIL.ImageDraw.Draw(img) - draw.text((150, 180), "No data", fill='black') - img_path = os.path.join("data", "temp_chart.png") - img.save(img_path) - return img_path - - # Get traits - traits = persona["성격특성"] - - # Convert to English labels - categories = [] - values = [] - for trait_kr, value in traits.items(): - trait_en = ENGLISH_LABELS.get(trait_kr, trait_kr) - categories.append(trait_en) - values.append(value) - - # Add the first value again to close the loop - categories.append(categories[0]) - values.append(values[0]) - - # Convert to radians - angles = np.linspace(0, 2*np.pi, len(categories), endpoint=True) - - # Create plot with improved aesthetics - fig, ax = plt.subplots(figsize=(7, 7), subplot_kw=dict(polar=True)) - - # 배경 스타일 개선 - ax.set_facecolor('#f8f9fa') - fig.patch.set_facecolor('#f8f9fa') - - # Grid 스타일 개선 - ax.grid(True, color='#e0e0e0', linestyle='-', linewidth=0.5, alpha=0.7) +✨ **조정된 성격 (3가지 핵심 지표):** +• 온기: {warmth}/100 {'(따뜻함)' if warmth >= 60 else '(차가움)' if warmth <= 40 else '(보통)'} +• 능력: {competence}/100 {'(유능함)' if competence >= 60 else '(서툼)' if competence <= 40 else '(보통)'} +• 외향성: {extraversion}/100 {'(활발함)' if extraversion >= 60 else '(조용함)' if extraversion <= 40 else '(보통)'} +• 유머감각: 75/100 (고정 - 모든 페르소나가 유머러스!) +• 유머스타일: {humor_style} + +🧬 **백그라운드**: 152개 세부 변수가 이 설정에 맞춰 자동 조정되었습니다. + +{change_analysis}{additional_changes} + """ + + # 조정된 매력적 결함과 모순적 특성을 DataFrame으로 생성 + flaws_df = [] + if "매력적결함" in adjusted_persona: + flaws = adjusted_persona["매력적결함"] + for i, flaw in enumerate(flaws, 1): + # 사물 특성 vs 성격적 특성 구분 + if any(keyword in flaw for keyword in ["먼지", "햇볕", "색이", "충격", "습도", "냄새", "모서리", "무게", "크기"]): + flaw_type = "사물 특성 기반" + else: + flaw_type = "성격적 특성" + flaws_df.append([f"{i}. {flaw}", flaw_type]) + + contradictions_df = [] + if "모순적특성" in adjusted_persona: + contradictions = adjusted_persona["모순적특성"] + for i, contradiction in enumerate(contradictions, 1): + contradictions_df.append([f"{i}. {contradiction}", "복합적 매력"]) + + return adjusted_persona, adjustment_message, adjusted_info, variables_df, flaws_df, contradictions_df + + except Exception as e: + import traceback + traceback.print_exc() + return persona, f"조정 중 오류 발생: {str(e)}", {}, [], [], [] + +def finalize_persona(persona): + """페르소나 최종 확정 - 환경변수 API 설정 사용""" + global persona_generator - # 각도 라벨 위치 및 색상 조정 - ax.set_rlabel_position(90) - ax.tick_params(colors='#6b7280') + if not persona: + return None, "페르소나가 없습니다.", "", {}, None, [], [], [], "", None - # Y축 라벨 제거 및 눈금 표시 - ax.set_yticklabels([]) - ax.set_yticks([20, 40, 60, 80, 100]) + # 환경변수 API 키 확인 + if not persona_generator or not hasattr(persona_generator, 'api_key') or not persona_generator.api_key: + return None, "❌ **API 키가 설정되지 않았습니다!** 허깅페이스 스페이스 설정에서 GEMINI_API_KEY를 환경변수로 추가해주세요.", "", {}, None, [], [], [], "", None - # 범위 설정 - ax.set_ylim(0, 100) + try: + # 글로벌 persona_generator 사용 (환경변수에서 설정된 API 키 사용) + generator = persona_generator + + # 이미 백엔드 페르소나인 경우와 프론트엔드 페르소나인 경우 구분 + if "구조화프롬프트" not in persona: + # 프론트엔드 페르소나인 경우 백엔드 페르소나로 변환 + image_analysis = {"object_type": persona.get("기본정보", {}).get("유형", "알 수 없는 사물")} + persona = generator.create_backend_persona(persona, image_analysis) + + persona_name = persona["기본정보"]["이름"] + + # 완성 메시지 + completion_msg = f"🎉 **{persona_name}**이 완성되었습니다! 이제 대화탭에서 JSON을 업로드하여 친구와 대화를 나눠보세요!" + + # 페르소나 요약 표시 + summary_display = display_persona_summary(persona) + + # 유머 매트릭스 차트 생성 + humor_chart = plot_humor_matrix(persona.get("유머매트릭스", {})) + + # 매력적 결함을 더 상세한 DataFrame으로 변환 + flaws = persona.get("매력적결함", []) + flaws_df = [] + for i, flaw in enumerate(flaws, 1): + # 사물 특성 vs 성격적 특성 구분 + if any(keyword in flaw for keyword in ["먼지", "햇볕", "색이", "충격", "습도", "냄새", "모서리", "무게", "크기"]): + flaw_type = "사물 특성 기반" + else: + flaw_type = "성격적 특성" + flaws_df.append([f"{i}. {flaw}", flaw_type]) + + # 모순적 특성을 더 상세한 DataFrame으로 변환 + contradictions = persona.get("모순적특성", []) + contradictions_df = [] + for i, contradiction in enumerate(contradictions, 1): + contradictions_df.append([f"{i}. {contradiction}", "복합적 매력"]) + + # 사물 고유 특성도 추가 + object_type = persona.get("기본정보", {}).get("유형", "") + purpose = persona.get("기본정보", {}).get("용도", "") + if purpose: + contradictions_df.append([f"🎯 {purpose}을 담당하는 {object_type}의 독특한 개성", "사물 역할 특성"]) + + # 127개 성격 변수를 DataFrame 형태로 변환 (카테고리별 분류) + variables = persona.get("성격변수127", {}) + if not variables and "성격프로필" in persona: + # 성격프로필에서 직접 가져오기 (성격프로필 자체가 variables dict) + variables = persona["성격프로필"] + + variables_df = [] + for var, value in variables.items(): + # 카테고리 분류 + if var.startswith('W'): + category = f"🔥 온기/따뜻함" + elif var.startswith('C'): + category = f"💪 능력/역량" + elif var.startswith('E'): + category = f"🗣️ 외향성" + elif var.startswith('H'): + category = f"😄 유머" + elif var.startswith('F'): + category = f"💎 매력적결함" + elif var.startswith('P'): + category = f"🎭 성격패턴" + elif var.startswith('S'): + category = f"🗨️ 언어스타일" + elif var.startswith('R'): + category = f"❤️ 관계성향" + elif var.startswith('D'): + category = f"💬 대화역학" + elif var.startswith('OBJ'): + category = f"🏠 사물정체성" + elif var.startswith('FORM'): + category = f"✨ 형태특성" + elif var.startswith('INT'): + category = f"🤝 상호작용" + elif var.startswith('U'): + category = f"🌍 문화적특성" + else: + category = f"📊 기타" + + # 값에 따른 색상 표시 + if value >= 80: + status = "🟢 매우 높음" + elif value >= 60: + status = "🟡 높음" + elif value >= 40: + status = "🟠 보통" + elif value >= 20: + status = "🔴 낮음" + else: + status = "⚫ 매우 낮음" + + variables_df.append([var, value, category, status]) + + # JSON 파일 생성 + import tempfile + import json + + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as f: + json.dump(persona, f, ensure_ascii=False, indent=2) + temp_path = f.name + + return ( + persona, # current_persona + f"✅ {persona_name} 완성! (Gemini API 사용)", # status_output + summary_display, # persona_summary_display + persona["성격특성"], # personality_traits_output + humor_chart, # humor_chart_output + flaws_df, # attractive_flaws_output + contradictions_df, # contradictions_output + variables_df, # personality_variables_output + completion_msg, # persona_awakening + temp_path # download_file + ) + + except Exception as e: + import traceback + traceback.print_exc() + return None, f"❌ 페르소나 확정 중 오류 발생: {str(e)}\n\n💡 **해결방법**: 허깅페이스 스페이스 설정에서 GEMINI_API_KEY 환경변수를 확인하고 인터넷 연결을 확인해보세요.", "", {}, None, [], [], [], "", None + +def plot_humor_matrix(humor_data): + """유머 매트릭스 시각화 - 영어 레이블 사용""" + if not humor_data: + return None - # 차트 그리기 - # 1. 채워진 영역 - ax.fill(angles, values, alpha=0.25, color='#6366f1') + try: + fig, ax = plt.subplots(figsize=(8, 6)) + + # 데이터 추출 + warmth_vs_wit = humor_data.get("warmth_vs_wit", 50) + self_vs_observational = humor_data.get("self_vs_observational", 50) + subtle_vs_expressive = humor_data.get("subtle_vs_expressive", 50) + + # 영어 레이블 사용 (폰트 문제 완전 해결) + categories = ['Warmth vs Wit', 'Self vs Observational', 'Subtle vs Expressive'] + values = [warmth_vs_wit, self_vs_observational, subtle_vs_expressive] + + bars = ax.bar(categories, values, color=['#ff9999', '#66b3ff', '#99ff99'], alpha=0.8) + ax.set_ylim(0, 100) + ax.set_ylabel('Score', fontsize=12) + ax.set_title('Humor Style Matrix', fontsize=14, fontweight='bold') + + # 값 표시 + for bar, value in zip(bars, values): + height = bar.get_height() + ax.text(bar.get_x() + bar.get_width()/2., height + 2, + f'{value:.1f}', ha='center', va='bottom', fontsize=10, fontweight='bold') + + plt.xticks(rotation=15, ha='right') + plt.tight_layout() + plt.grid(axis='y', alpha=0.3) + + return fig + except Exception as e: + print(f"유머 차트 생성 오류: {str(e)}") + return None + +def generate_personality_chart(persona): + """성격 특성을 레이더 차트로 시각화 (영어 버전)""" - # 2. 테두리 선 - ax.plot(angles, values, 'o-', linewidth=2, color='#6366f1') + if not persona or "성격특성" not in persona: + return None + + personality_traits = persona["성격특성"] + + # 영어 레이블 매핑 + trait_labels_en = { + '온기': 'Warmth', + '능력': 'Competence', + '창의성': 'Creativity', + '외향성': 'Extraversion', + '유머감각': 'Humor', + '신뢰성': 'Reliability', + '공감능력': 'Empathy' + } - # 3. 데이터 포인트 강조 - ax.scatter(angles[:-1], values[:-1], s=100, color='#6366f1', edgecolor='white', zorder=10) + # 데이터 준비 + categories = [] + values = [] - # 4. 각 축 설정 - 영어 라벨 사용 - ax.set_thetagrids(angles[:-1] * 180/np.pi, categories[:-1], fontsize=12) + for korean_trait, english_trait in trait_labels_en.items(): + if korean_trait in personality_traits: + categories.append(english_trait) + values.append(personality_traits[korean_trait]) - # 제목 추가 - name = persona.get("기본정보", {}).get("이름", "Unknown") - plt.title(f"{name} Personality Traits", size=16, color='#374151', pad=20, fontweight='bold') + if not categories: + return None - # 저장 - timestamp = int(time.time()) - img_path = os.path.join("data", f"chart_{timestamp}.png") - os.makedirs(os.path.dirname(img_path), exist_ok=True) - plt.savefig(img_path, format='png', bbox_inches='tight', dpi=150, facecolor=fig.get_facecolor()) - plt.close(fig) + # 레이더 차트 생성 + fig = go.Figure() + + fig.add_trace(go.Scatterpolar( + r=values, + theta=categories, + fill='toself', + fillcolor='rgba(74, 144, 226, 0.3)', + line=dict(color='rgba(74, 144, 226, 1)', width=2), + marker=dict(size=8, color='rgba(74, 144, 226, 1)'), + name='Personality Traits' + )) + + fig.update_layout( + polar=dict( + radialaxis=dict( + visible=True, + range=[0, 100], + tickfont=dict(size=10), + gridcolor="lightgray" + ), + angularaxis=dict( + tickfont=dict(size=12, family="Arial, sans-serif") + ) + ), + showlegend=False, + title=dict( + text="Personality Profile", + x=0.5, + font=dict(size=16, family="Arial, sans-serif") + ), + width=400, + height=400, + margin=dict(l=40, r=40, t=60, b=40), + font=dict(family="Arial, sans-serif") + ) - return img_path + return fig -def save_current_persona(current_persona): - """Save current persona to a JSON file""" - if not current_persona: +def save_persona_to_file(persona): + """페르소나 저장""" + if not persona: return "저장할 페르소나가 없습니다." try: - # 깊은 복사를 통해 원본 데이터를 유지 - import copy - persona_copy = copy.deepcopy(current_persona) + # 깊은 복사로 원본 보호 + persona_copy = copy.deepcopy(persona) - # 저장 불가능한 객체 제거 + # JSON 직렬화 불가능한 객체들 제거 keys_to_remove = [] - for key in persona_copy: - if key in ["personality_profile", "humor_matrix", "_state"] or callable(persona_copy[key]): + for key, value in persona_copy.items(): + if callable(value) or hasattr(value, '__call__'): keys_to_remove.append(key) for key in keys_to_remove: persona_copy.pop(key, None) - # 중첩된 딕셔너리와 리스트 내의 비직렬화 가능 객체 제거 - def clean_data(data): - if isinstance(data, dict): - for k in list(data.keys()): - if callable(data[k]): - del data[k] - elif isinstance(data[k], (dict, list)): - data[k] = clean_data(data[k]) - return data - elif isinstance(data, list): - return [clean_data(item) if isinstance(item, (dict, list)) else item for item in data if not callable(item)] - else: - return data - - # 데이터 정리 - cleaned_persona = clean_data(persona_copy) - - # 최종 검증: JSON 직렬화 가능 여부 확인 - import json - try: - json.dumps(cleaned_persona) - except TypeError as e: - print(f"JSON 직렬화 오류: {str(e)}") - # 기본 정보만 유지하고 나머지는 안전한 데이터만 포함 - basic_info = cleaned_persona.get("기본정보", {}) - 성격특성 = cleaned_persona.get("성격특성", {}) - 매력적결함 = cleaned_persona.get("매력적결함", []) - 모순적특성 = cleaned_persona.get("모순적특성", []) - - cleaned_persona = { - "기본정보": basic_info, - "성격특성": 성격특성, - "매력적결함": 매력적결함, - "모순적특성": 모순적특성 - } - - filepath = save_persona(cleaned_persona) + # 저장 실행 + filepath = save_persona(persona_copy) if filepath: - name = current_persona.get("기본정보", {}).get("이름", "Unknown") - return f"{name} 페르소나가 저장되었습니다: {filepath}" + name = persona.get("기본정보", {}).get("이름", "Unknown") + return f"✅ {name} 페르소나가 저장되었습니다: {filepath}" else: - return "페르소나 저장에 실패했습니다." + return "❌ 페르소나 저장에 실패했습니다." except Exception as e: import traceback - error_details = traceback.format_exc() - print(f"저장 오류 상세: {error_details}") - return f"저장 중 오류 발생: {str(e)}" - -# 이 함수는 파일 상단에서 이미 정의되어 있으므로 여기서는 제거합니다. + error_msg = traceback.format_exc() + print(f"저장 오류: {error_msg}") + return f"❌ 저장 중 오류 발생: {str(e)}" -# 성격 미세조정 함수 -def refine_persona(persona, extraversion, emotion_expression, energy, thinking_style): - """페르소나의 성격을 미세조정하는 함수""" +def export_persona_to_json(persona): + """페르소나를 JSON 파일로 내보내기 (Gradio 다운로드용)""" if not persona: - return persona, "페르소나가 없습니다." + return None try: - # 유머 스타일 자동 추천 - humor_style, confidence, scores = recommend_humor_style(extraversion, emotion_expression, energy, thinking_style) - - # 복사본 생성 - refined_persona = persona.copy() - - # 성격 특성 업데이트 - 새로운 지표들을 기존 매핑에 연결 - if "성격특성" in refined_persona: - refined_persona["성격특성"]["외향성"] = int(extraversion) - refined_persona["성격특성"]["감정표현"] = int(emotion_expression) - refined_persona["성격특성"]["활력"] = int(energy) - refined_persona["성격특성"]["사고방식"] = int(thinking_style) - - # 기존 특성들도 새로운 지표를 바탕으로 계산 - refined_persona["성격특성"]["온기"] = int((emotion_expression + energy) / 2) - refined_persona["성격특성"]["능력"] = int(thinking_style) - refined_persona["성격특성"]["창의성"] = int(100 - thinking_style) # 논리적 ↔ 창의적 - - # 자동 추천된 유머 스타일 업데이트 - refined_persona["유머스타일"] = humor_style - - # 127개 성격 변수가 있으면 업데이트 - if "성격변수127" in refined_persona: - # 외향성 관련 변수 업데이트 - for var in ["E01_사교성", "E02_활동성", "E03_자기주장", "E06_열정성"]: - if var in refined_persona["성격변수127"]: - refined_persona["성격변수127"][var] = int(extraversion * 0.9 + random.randint(0, 20)) - - # 감정표현 관련 변수 업데이트 - for var in ["W09_친밀감표현", "W06_공감능력", "E04_긍정정서"]: - if var in refined_persona["성격변수127"]: - refined_persona["성격변수127"][var] = int(emotion_expression * 0.9 + random.randint(0, 20)) - - # 에너지 관련 변수 업데이트 - for var in ["E02_활동성", "E06_열정성", "E05_자극추구"]: - if var in refined_persona["성격변수127"]: - refined_persona["성격변수127"][var] = int(energy * 0.9 + random.randint(0, 20)) - - # 사고방식 관련 변수 업데이트 - for var in ["C02_지능", "C06_분석력", "C01_효율성"]: - if var in refined_persona["성격변수127"]: - refined_persona["성격변수127"][var] = int(thinking_style * 0.9 + random.randint(0, 20)) - - # 창의성 관련 변수 업데이트 (논리적 사고와 반대) - for var in ["C04_창의성", "C08_통찰력"]: - if var in refined_persona["성격변수127"]: - refined_persona["성격변수127"][var] = int((100 - thinking_style) * 0.9 + random.randint(0, 20)) - - # 유머 매트릭스 업데이트 - if "유머매트릭스" in refined_persona: - if humor_style == "위트있는 재치꾼": - refined_persona["유머매트릭스"]["warmth_vs_wit"] = 30 - refined_persona["유머매트릭스"]["self_vs_observational"] = 50 - refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 70 - elif humor_style == "따뜻한 유머러스": - refined_persona["유머매트릭스"]["warmth_vs_wit"] = 80 - refined_persona["유머매트릭스"]["self_vs_observational"] = 60 - refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 60 - elif humor_style == "날카로운 관찰자": - refined_persona["유머매트릭스"]["warmth_vs_wit"] = 40 - refined_persona["유머매트릭스"]["self_vs_observational"] = 20 - refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 50 - elif humor_style == "자기 비하적": - refined_persona["유머매트릭스"]["warmth_vs_wit"] = 60 - refined_persona["유머매트릭스"]["self_vs_observational"] = 85 - refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 40 - - return refined_persona, "성격이 성공적으로 미세조정되었습니다." - + # 깊은 복사로 원본 보호 + persona_copy = copy.deepcopy(persona) + + # JSON 직렬화 불가능한 객체들 제거 + def clean_for_json(obj): + if isinstance(obj, dict): + cleaned = {} + for k, v in obj.items(): + if not callable(v) and not hasattr(v, '__call__'): + cleaned[k] = clean_for_json(v) + return cleaned + elif isinstance(obj, (list, tuple)): + return [clean_for_json(item) for item in obj if not callable(item)] + else: + return obj + + persona_clean = clean_for_json(persona_copy) + + # JSON 문자열 생성 + json_content = json.dumps(persona_clean, ensure_ascii=False, indent=2) + + # 파일명 생성 + persona_name = persona_clean.get("기본정보", {}).get("이름", "persona") + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"{persona_name}_{timestamp}.json" + + # 임시 파일 저장 + temp_dir = "/tmp" if os.path.exists("/tmp") else "." + filepath = os.path.join(temp_dir, filename) + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(json_content) + + return filepath + except Exception as e: - import traceback - error_details = traceback.format_exc() - print(f"성격 미세조정 오류: {error_details}") - return persona, f"성격 미세조정 중 오류가 발생했습니다: {str(e)}" + print(f"JSON 내보내기 오류: {str(e)}") + return None -def create_frontend_view_html(persona): - """Create HTML representation of the frontend view of the persona""" - if not persona: - return "
페르소나가 아직 생성되지 않았습니다.
" - - name = persona.get("기본정보", {}).get("이름", "Unknown") - object_type = persona.get("기본정보", {}).get("유형", "Unknown") - description = persona.get("기본정보", {}).get("설명", "") - - # 성격 요약 가져오기 - personality_summary = persona.get("성격요약", {}) - summary_html = "" - if personality_summary: - summary_items = [] - for trait, value in personality_summary.items(): - if isinstance(value, (int, float)): - trait_name = trait - trait_value = value - summary_items.append(f"• {trait_name}: {trait_value:.1f}%") - - if summary_items: - summary_html = "

성격 요약

" - - # Personality traits - traits_html = "" - for trait, value in persona.get("성격특성", {}).items(): - traits_html += f""" -
-
{trait}
-
-
-
-
{value}%
-
- """ - - # Flaws - 매력적 결함 - flaws = persona.get("매력적결함", []) - flaws_list = "" - for flaw in flaws[:4]: # 최대 4개만 표시 - flaws_list += f"
  • {flaw}
  • " +# def get_saved_personas(): +# """저장된 페르소나 목록 가져오기 - 더 이상 사용하지 않음""" +# return [], [] + +# def load_persona_from_selection(selected_row, personas_list): +# """선택된 페르소나 로드 - 더 이상 사용하지 않음""" +# return None, "이 기능은 더 이상 사용하지 않습니다. JSON 업로드를 사용하세요.", {}, {}, None, [], [], [], "" + +def chat_with_loaded_persona(persona, user_message, chat_history=None): + """페르소나와 채팅 - 완전한 타입 안전성 보장""" - # 소통 방식 - communication_style = persona.get("소통방식", "") + # 기본값 설정 + if chat_history is None: + chat_history = [] - # 유머 스타일 - humor_style = persona.get("유머스타일", "") - - # 전체 HTML 스타일과 내용 - html = f""" - - -
    -
    -

    {name}

    -

    {object_type} - {description}

    -
    - - {summary_html} - -
    -

    성격 특성

    -
    - {traits_html} -
    -
    - -
    -

    소통 스타일

    -

    {communication_style}

    -

    유머 스타일

    -

    {humor_style}

    -
    - -
    -

    매력적 결함

    - -
    -
    - """ + # 입력 검증 + if not user_message or not isinstance(user_message, str): + return chat_history, "" - return html - -def create_backend_view_html(persona): - """Create HTML representation of the backend view of the persona""" - if not persona: - return "
    페르소나가 아직 생성되지 않았습니다.
    " + # 페르소나 체크 + if not persona or not isinstance(persona, dict): + error_msg = "❌ 먼저 페르소나를 불러와주세요! 대화하기 탭에서 JSON 파일을 업로드하세요." + chat_history.append([user_message, error_msg]) + return chat_history, "" - name = persona.get("기본정보", {}).get("이름", "Unknown") + # 환경변수 API 키 체크 + if not persona_generator or not hasattr(persona_generator, 'api_key') or not persona_generator.api_key: + error_msg = "❌ API 키가 설정되지 않았습니다. 허깅페이스 스페이스 설정에서 GEMINI_API_KEY 환경변수를 추가해주세요!" + chat_history.append([user_message, error_msg]) + return chat_history, "" - # 백엔드 기본 정보 - basic_info = persona.get("기본정보", {}) - basic_info_html = "" - for key, value in basic_info.items(): - basic_info_html += f"{key}{value}" - - # 1. 성격 변수 요약 - personality_summary = persona.get("성격요약", {}) - summary_html = "" - - if personality_summary: - summary_html += "
    " - for category, value in personality_summary.items(): - if isinstance(value, (int, float)): - summary_html += f""" -
    -
    {category}
    -
    -
    -
    -
    {value:.1f}
    -
    - """ - summary_html += "
    " - - # 2. 성격 매트릭스 (5차원 빅5 시각화) - big5_html = "" - if "성격특성" in persona: - # 빅5 매핑 (기존 특성에서 변환) - big5 = { - "외향성(Extraversion)": persona.get("성격특성", {}).get("외향성", 50), - "친화성(Agreeableness)": persona.get("성격특성", {}).get("온기", 50), - "성실성(Conscientiousness)": persona.get("성격특성", {}).get("신뢰성", 50), - "신경증(Neuroticism)": 100 - persona.get("성격특성", {}).get("안정성", 50) if "안정성" in persona.get("성격특성", {}) else 50, - "개방성(Openness)": persona.get("성격특성", {}).get("창의성", 50) - } + try: + # 글로벌 persona_generator 사용 (환경변수에서 설정된 API 키 사용) + generator = persona_generator - big5_html = "
    " - for trait, value in big5.items(): - big5_html += f""" -
    -
    {trait}
    -
    -
    -
    -
    {value}%
    -
    - """ - big5_html += "
    " - - # 3. 유머 매트릭스 - humor_matrix = persona.get("유머매트릭스", {}) - humor_html = "" - - if humor_matrix: - warmth_vs_wit = humor_matrix.get("warmth_vs_wit", 50) - self_vs_observational = humor_matrix.get("self_vs_observational", 50) - subtle_vs_expressive = humor_matrix.get("subtle_vs_expressive", 50) - - humor_html = f""" -
    -
    -
    따뜻함 vs 위트
    -
    -
    -
    위트
    -
    따뜻함
    -
    -
    + # 대화 기록 안전한 변환: Gradio 4.x -> PersonaGenerator 형식 + conversation_history = [] + + if chat_history and isinstance(chat_history, list): + for chat_turn in chat_history: + try: + # 타입별 안전한 처리 + if chat_turn is None: + continue + elif isinstance(chat_turn, dict): + # Messages format: {"role": "user/assistant", "content": "message"} + role = chat_turn.get("role") + content = chat_turn.get("content") + + if role and content and role in ["user", "assistant"]: + conversation_history.append({"role": str(role), "content": str(content)}) + elif isinstance(chat_turn, (list, tuple)) and len(chat_turn) >= 2: + # 구 Gradio 형식: [user_message, bot_response] (호환성) + user_msg = chat_turn[0] + bot_msg = chat_turn[1] + + if user_msg is not None and str(user_msg).strip(): + conversation_history.append({"role": "user", "content": str(user_msg)}) + if bot_msg is not None and str(bot_msg).strip(): + conversation_history.append({"role": "assistant", "content": str(bot_msg)}) + else: + # 예상치 못한 형식은 무시 + print(f"⚠️ 예상치 못한 채팅 형식 무시: {type(chat_turn)}") + continue + + except Exception as turn_error: + print(f"⚠️ 채팅 기록 변환 오류: {str(turn_error)}") + continue + + # 세션 ID 안전하게 생성 + try: + persona_name = "" + if isinstance(persona, dict) and "기본정보" in persona: + basic_info = persona["기본정보"] + if isinstance(basic_info, dict) and "이름" in basic_info: + persona_name = str(basic_info["이름"]) -
    -
    자기참조 vs 관찰형
    -
    -
    -
    관찰형
    -
    자기참조
    -
    -
    + if not persona_name: + persona_name = "알 수 없는 페르소나" + + session_id = f"{persona_name}_{hash(str(persona)[:100]) % 10000}" + except Exception: + session_id = "default_session" + + # 페르소나와 채팅 실행 + response = generator.chat_with_persona(persona, user_message, conversation_history, session_id) + + # 응답 검증 + if not isinstance(response, str): + response = str(response) if response else "죄송합니다. 응답을 생성할 수 없었습니다." + + # Gradio 4.x messages format으로 안전하게 추가 + if not isinstance(chat_history, list): + chat_history = [] + + # Messages format: {"role": "user", "content": "message"} + chat_history.append({"role": "user", "content": user_message}) + chat_history.append({"role": "assistant", "content": response}) + + return chat_history, "" + + except Exception as e: + # 상세한 오류 로깅 + import traceback + error_traceback = traceback.format_exc() + print(f"🚨 채팅 오류 발생:") + print(f" 오류 메시지: {str(e)}") + print(f" 오류 타입: {type(e)}") + print(f" 상세 스택: {error_traceback}") + + # 사용자 친화적 오류 메시지 + if "string indices must be integers" in str(e): + friendly_error = "데이터 형식 오류가 발생했습니다. 페르소나를 다시 업로드해보세요. 🔄" + elif "API" in str(e).upper(): + friendly_error = "API 연결에 문제가 있어요. 환경변수 설정을 확인해보시겠어요? 😊" + elif "network" in str(e).lower() or "connection" in str(e).lower(): + friendly_error = "인터넷 연결을 확인해보세요! 🌐" + else: + friendly_error = f"죄송합니다. 일시적인 문제가 발생했어요. 😅\n\n🔍 기술 정보: {str(e)}" + + # 안전하게 오류 메시지 추가 (messages format) + try: + if not isinstance(chat_history, list): + chat_history = [] + chat_history.append({"role": "user", "content": user_message}) + chat_history.append({"role": "assistant", "content": friendly_error}) + except Exception: + chat_history = [ + {"role": "user", "content": user_message}, + {"role": "assistant", "content": friendly_error} + ] -
    -
    미묘함 vs 표현적
    -
    -
    -
    미묘함
    -
    표현적
    -
    -
    -
    - """ - - # 4. 매력적 결함과 모순적 특성 - flaws_html = "" - contradictions_html = "" - - flaws = persona.get("매력적결함", []) - if flaws: - flaws_html = "" - - contradictions = persona.get("모순적특성", []) - if contradictions: - contradictions_html = "" - - # 6. 프롬프트 템플릿 (있는 경우) - prompt_html = "" - if "프롬프트" in persona: - prompt_text = persona.get("프롬프트", "") - prompt_html = f""" -
    -

    대화 프롬프트

    -
    {prompt_text}
    -
    - """ + return chat_history, "" + +def import_persona_from_json(json_file): + """JSON 파일에서 페르소나 가져오기""" + if json_file is None: + return None, "JSON 파일을 업로드해주세요.", "", {} - # 7. 완전한 백엔드 JSON (접이식) try: - # 내부 상태 객체 제거 (JSON 변환 불가) - json_persona = {k: v for k, v in persona.items() if k not in ["personality_profile", "humor_matrix"]} - persona_json = json.dumps(json_persona, ensure_ascii=False, indent=2) - - json_preview = f""" -
    - 전체 백엔드 데이터 (JSON) -
    {persona_json}
    -
    - """ - except Exception as e: - json_preview = f"
    JSON 변환 오류: {str(e)}
    " - - # 8. 전체 HTML 조합 - html = f""" - - -
    -
    -

    {name} - 백엔드 데이터

    -

    상세 정보와 내부 변수 확인

    -
    - -
    -

    기본 정보

    - - {basic_info_html} -
    -
    - -
    -

    성격 요약 (Big 5)

    - {big5_html} -
    - -
    -

    유머 매트릭스 (3차원)

    - {humor_html} -
    - -
    -

    매력적 결함

    - {flaws_html} - -

    모순적 특성

    - {contradictions_html} -
    + # 파일 경로 확인 및 읽기 + if isinstance(json_file, str): + # 파일 경로인 경우 + file_path = json_file + else: + # 파일 객체인 경우 (Gradio 업로드) + file_path = json_file.name if hasattr(json_file, 'name') else str(json_file) - {prompt_html} + # JSON 파일 읽기 + with open(file_path, 'r', encoding='utf-8') as f: + persona_data = json.load(f) -
    -

    전체 백엔드 데이터

    - {json_preview} -
    -
    - """ + # 페르소나 데이터 검증 + if not isinstance(persona_data, dict): + return None, "❌ 올바른 JSON 형식이 아닙니다.", "", {} + + if "기본정보" not in persona_data: + return None, "❌ 올바른 페르소나 JSON 파일이 아닙니다. '기본정보' 키가 필요합니다.", "", {} + + # 기본 정보 추출 + basic_info = persona_data.get("기본정보", {}) + persona_name = basic_info.get("이름", "Unknown") + personality_traits = persona_data.get("성격특성", {}) + + # AI 기반 인사말 생성 (로드 시에도 조정된 성격 반영) + global persona_generator + try: + if persona_generator: + ai_greeting = persona_generator.generate_ai_based_greeting(persona_data, personality_traits) + greeting = f"### 🤖 JSON에서 깨어난 친구\n\n{ai_greeting}\n\n💾 *\"JSON에서 다시 깨어났어! 내 성격 기억나?\"*" + else: + # 폴백: 기존 방식 + personality_preview = generate_personality_preview(persona_name, personality_traits, basic_info) + greeting = f"### 🤖 JSON에서 깨어난 친구\n\n{personality_preview}\n\n💾 *\"JSON에서 다시 깨어났어! 내 성격 기억나?\"*" + except Exception as e: + print(f"⚠️ JSON 로드 시 AI 인사말 생성 실패: {e}") + # 폴백: 기존 방식 + personality_preview = generate_personality_preview(persona_name, personality_traits, basic_info) + greeting = f"### 🤖 JSON에서 깨어난 친구\n\n{personality_preview}\n\n💾 *\"JSON에서 다시 깨어났어! 내 성격 기억나?\"*" + + return (persona_data, f"✅ {persona_name} 페르소나를 JSON에서 불러왔습니다!", + greeting, basic_info) - return html + except FileNotFoundError: + return None, "❌ 파일을 찾을 수 없습니다.", "", {} + except json.JSONDecodeError as e: + return None, f"❌ JSON 파일 형식이 올바르지 않습니다: {str(e)}", "", {} + except Exception as e: + import traceback + traceback.print_exc() + return None, f"❌ JSON 불러오기 중 오류 발생: {str(e)}", "", {} -def get_personas_list(): - """Get list of personas for the dataframe""" - personas = list_personas() +def format_personality_traits(persona): + """성격 특성을 특성 중심의 간단한 리스트 형태로 포맷 (캡쳐 스타일)""" + global persona_generator - # Convert to dataframe format - df_data = [] - for i, persona in enumerate(personas): - df_data.append([ - persona["name"], - persona["type"], - persona["created_at"], - persona["filename"] - ]) + if not persona or "성격특성" not in persona: + return "페르소나가 생성되지 않았습니다." + + # 글로벌 persona_generator 사용 (API 설정이 적용된 상태) + if persona_generator is None: + persona_generator = PersonaGenerator() + + # 기본 정보에서 사물의 특성 추출 + basic_info = persona.get("기본정보", {}) + object_type = basic_info.get("유형", "") + purpose = basic_info.get("용도", "") + + # 생애 스토리에서 특성 추출 + life_story = persona.get("생애스토리", {}) + + # 매력적 결함 + attractive_flaws = persona.get("매력적결함", []) + + # 성격 특성 + personality_traits = persona["성격특성"] + + # 특성 리스트 생성 + characteristics = [] + + # 1. 온기 특성 + warmth = personality_traits.get("온기", 50) + if warmth >= 70: + characteristics.append("따뜻하고 포근한 마음") + elif warmth >= 50: + characteristics.append("친근하고 다정한 성격") + else: + characteristics.append("차분하고 진중한 면") + + # 2. 사물의 고유 특성 (유형 기반) + if "곰" in object_type or "인형" in object_type: + characteristics.append("부드럽고 포근한 감촉") + elif "책" in object_type: + characteristics.append("지식과 이야기를 담고 있음") + elif "컵" in object_type or "머그" in object_type: + characteristics.append("따뜻한 음료와 함께하는 시간") + elif "시계" in object_type: + characteristics.append("시간의 소중함을 알려줌") + elif "연필" in object_type or "펜" in object_type: + characteristics.append("창작과 기록의 동반자") + else: + characteristics.append(f"{object_type}만의 독특한 매력") + + # 3. 활동 시간대나 환경 특성 + extraversion = personality_traits.get("외향성", 50) + if extraversion >= 70: + characteristics.append("낮에 더 활발해짐") + elif extraversion <= 30: + characteristics.append("밤에 더 활발해짐") + else: + characteristics.append("하루 종일 일정한 에너지") + + # 4. 매력적 결함 중 하나를 특성으로 표현 + if attractive_flaws: + flaw = attractive_flaws[0] + if "털" in flaw: + characteristics.append("가끔 털이 헝클어져서 걱정") + elif "먼지" in flaw: + characteristics.append("먼지가 쌓이는 걸 신경 씀") + elif "얼룩" in flaw: + characteristics.append("작은 얼룩도 눈에 띄어 고민") + elif "색" in flaw: + characteristics.append("색이 바래는 것을 조금 걱정") + else: + characteristics.append("완벽하지 않은 모습도 받아들임") + + # 5. 기억과 경험 + if life_story: + characteristics.append("오래된 이야기들 기억") + else: + characteristics.append("새로운 추억 만들기를 기대") + + # ✨ 아이콘과 함께 리스트 형태로 반환 + result = "" + for char in characteristics: + result += f"✨ {char}\n\n" + + return result + +def display_persona_summary(persona): + """페르소나 요약 정보 표시""" + if not persona: + return "페르소나를 먼저 생성해주세요." + + basic_info = persona.get("기본정보", {}) + name = basic_info.get("이름", "이름 없음") + object_type = basic_info.get("유형", "알 수 없는 사물") + + # ��격 특성 요약 + personality_summary = format_personality_traits(persona) - return df_data, personas + # 유머 스타일 + humor_style = persona.get("유머스타일", "일반적") + + # 매력적 결함 + flaws = persona.get("매력적결함", []) + flaws_text = "\\n".join([f"• {flaw}" for flaw in flaws[:3]]) # 최대 3개만 표시 + + summary = f""" +### 👋 {name} 님을 소개합니다! + +**종류**: {object_type} +**유머 스타일**: {humor_style} + +{personality_summary} -def load_selected_persona(selected_row, personas_list): - """Load persona from the selected row in the dataframe""" - if selected_row is None or len(selected_row) == 0: - return None, "선택된 페르소나가 없습니다.", None, None, None +### 💎 매력적인 특징들 +{flaws_text} +""" + return summary + +def create_api_config_section(): + """API 설정 섹션 생성 - 더 이상 사용하지 않음""" + pass + +def apply_api_configuration(api_provider, api_key): + """API 설정 적용 - 더 이상 사용하지 않음""" + pass + +def test_api_connection(api_provider, api_key): + """API 연결 테스트 - 더 이상 사용하지 않음""" + pass + +def export_conversation_history(): + """대화 기록을 JSON으로 내보내기""" + global persona_generator + if persona_generator and hasattr(persona_generator, 'conversation_memory'): + json_data = persona_generator.conversation_memory.export_to_json() + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"conversation_history_{timestamp}.json" + + # 임시 파일 저장 + temp_dir = "/tmp" if os.path.exists("/tmp") else "." + filepath = os.path.join(temp_dir, filename) + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(json_data) + + return filepath # 파일 경로만 반환 + else: + # 빈 대화 기록 파일 생성 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"conversation_empty_{timestamp}.json" + temp_dir = "/tmp" if os.path.exists("/tmp") else "." + filepath = os.path.join(temp_dir, filename) + + with open(filepath, 'w', encoding='utf-8') as f: + f.write('{"conversations": [], "message": "대화 기록이 없습니다."}') + + return filepath + +def import_conversation_history(json_file): + """JSON에서 대화 기록 가져오기""" + global persona_generator try: - # Get filepath from selected row - selected_index = selected_row.index[0] if hasattr(selected_row, 'index') else 0 - filepath = personas_list[selected_index]["filepath"] + if json_file is None: + return "파일을 선택해주세요." - # Load persona - persona = load_persona(filepath) - if not persona: - return None, "페르소나 로딩에 실패했습니다.", None, None, None + # 파일 타입 확인 및 내용 읽기 + if hasattr(json_file, 'read'): + # 파일 객체인 경우 + content = json_file.read() + if isinstance(content, bytes): + content = content.decode('utf-8') + elif isinstance(json_file, str): + # 파일 경로인 경우 + with open(json_file, 'r', encoding='utf-8') as f: + content = f.read() + else: + # Gradio 파일 객체인 경우 (NamedString 등) + if hasattr(json_file, 'name'): + with open(json_file.name, 'r', encoding='utf-8') as f: + content = f.read() + else: + return "❌ 지원하지 않는 파일 형식입니다." - # Generate HTML views - frontend_view, backend_view = toggle_frontend_backend_view(persona) - frontend_html = create_frontend_view_html(frontend_view) - backend_html = create_backend_view_html(backend_view) + # persona_generator 초기화 확인 + if persona_generator is None: + persona_generator = PersonaGenerator() - # Generate personality chart - chart_image_path = generate_personality_chart(frontend_view) + # 대화 기록 가져오기 + success = persona_generator.conversation_memory.import_from_json(content) - return persona, f"{persona['기본정보']['이름']}을(를) 로드했습니다.", frontend_html, backend_html, chart_image_path + if success: + summary = persona_generator.conversation_memory.get_conversation_summary() + return f"✅ 대화 기록을 성공적으로 가져왔습니다!\n\n{summary}" + else: + return "❌ 파일 형식이 올바르지 않습니다." except Exception as e: - return None, f"페르소나 로딩 중 오류 발생: {str(e)}", None, None, None + return f"❌ 가져오기 실패: {str(e)}" -# 페르소나와 대화하는 함수 추가 -def chat_with_persona(persona, user_message, chat_history=None): - """ - 페르소나와 대화하는 함수 - """ - if chat_history is None: - chat_history = [] - - if not user_message.strip(): - return chat_history, "" +def show_conversation_analytics(): + """대화 분석 결과 표시""" + global persona_generator + if not persona_generator or not hasattr(persona_generator, 'conversation_memory'): + return "분석할 대화가 없습니다." + + memory = persona_generator.conversation_memory + + # 기본 통계 + analytics = f"## 📊 대화 분석 리포트\n\n" + analytics += f"### 🔢 기본 통계\n" + analytics += f"• 총 대화 수: {len(memory.conversations)}회\n" + analytics += f"• 키워드 수: {len(memory.keywords)}개\n" + analytics += f"• 활성 세션: {len(memory.user_profile)}개\n\n" + + # 상위 키워드 + top_keywords = memory.get_top_keywords(limit=10) + if top_keywords: + analytics += f"### 🔑 상위 키워드 TOP 10\n" + for i, (word, data) in enumerate(top_keywords, 1): + analytics += f"{i}. **{word}** ({data['category']}) - {data['total_frequency']}회\n" + analytics += "\n" + + # 카테고리별 키워드 + categories = {} + for word, data in memory.keywords.items(): + category = data['category'] + if category not in categories: + categories[category] = [] + categories[category].append((word, data['total_frequency'])) + + analytics += f"### 📂 카테고리별 관심사\n" + for category, words in categories.items(): + top_words = sorted(words, key=lambda x: x[1], reverse=True)[:3] + word_list = ", ".join([f"{word}({freq})" for word, freq in top_words]) + analytics += f"**{category}**: {word_list}\n" + + analytics += "\n" + + # 최근 감정 경향 + if memory.conversations: + recent_sentiments = [conv['sentiment'] for conv in memory.conversations[-10:]] + sentiment_counts = {"긍정적": 0, "부정적": 0, "중립적": 0} + for sentiment in recent_sentiments: + sentiment_counts[sentiment] = sentiment_counts.get(sentiment, 0) + 1 - if not persona: - # Gradio 4.x 호환 메시지 형식 (튜플) - chat_history.append([user_message, "페르소나가 로드되지 않았습니다. 먼저 페르소나를 생성하거나 불러오세요."]) - return chat_history, "" + analytics += f"### 😊 최근 감정 경향 (최근 10회)\n" + for sentiment, count in sentiment_counts.items(): + percentage = (count / len(recent_sentiments)) * 100 + analytics += f"• {sentiment}: {count}회 ({percentage:.1f}%)\n" - try: - # 페르소나 생성기에서 대화 기능 호출 - # 이전 대화 기록 변환 필요 - 리스트에서 튜플 형식으로 - converted_history = [] - for msg in chat_history: - if isinstance(msg, list) and len(msg) == 2: - # 리스트 형식이면 튜플로 변환 - converted_history.append((msg[0] if msg[0] else "", msg[1] if msg[1] else "")) - elif isinstance(msg, tuple) and len(msg) == 2: - # 이미 튜플 형식이면 그대로 사용 - converted_history.append(msg) - - # 페르소나 생성기에서 대화 함수 호출 - response = persona_generator.chat_with_persona(persona, user_message, converted_history) - - # Gradio 4.x 메시지 형식으로 추가 (리스트) - chat_history.append([user_message, response]) + return analytics + +def get_keyword_suggestions(current_message=""): + """현재 메시지 기반 키워드 제안""" + global persona_generator + if not persona_generator or not hasattr(persona_generator, 'conversation_memory'): + return "키워드 분석을 위한 대화 기록이 없습니다." + + memory = persona_generator.conversation_memory + + if current_message: + # 현재 메시지에서 키워드 추출 + extracted = memory._extract_keywords(current_message) + suggestions = f"## 🎯 '{current_message}'에서 추출된 키워드\n\n" - return chat_history, "" - except Exception as e: - import traceback - error_details = traceback.format_exc() - print(f"대화 오류: {error_details}") - chat_history.append([user_message, f"대화 중 오류가 발생했습니다: {str(e)}"]) - return chat_history, "" + if extracted: + for kw in extracted: + suggestions += f"• **{kw['word']}** ({kw['category']}) - {kw['frequency']}회\n" + else: + suggestions += "추출된 키워드가 없습니다.\n" + + # 관련 과거 대화 찾기 + context = memory.get_relevant_context(current_message) + if context["relevant_conversations"]: + suggestions += f"\n### 🔗 관련된 과거 대화\n" + for conv in context["relevant_conversations"][:3]: + suggestions += f"• {conv['user_message'][:30]}... (감정: {conv['sentiment']})\n" + + return suggestions + else: + # 전체 키워드 요약 + top_keywords = memory.get_top_keywords(limit=15) + if top_keywords: + suggestions = "## 🔑 전체 키워드 요약\n\n" + for word, data in top_keywords: + suggestions += f"• **{word}** ({data['category']}) - {data['total_frequency']}회, 최근: {data['last_mentioned'][:10]}\n" + return suggestions + else: + return "아직 수집된 키워드가 없습니다." -# 메인 Gradio 인터페이스 구성 함수 -def create_interface(): - # 현재 persona 상태 저장 - Gradio 5.x에서 변경된 방식 적용 - current_persona = gr.State(value=None) - personas_list = gr.State(value=[]) +# 메인 인터페이스 생성 +def create_main_interface(): + # 한글 폰트 설정 + setup_korean_font() + + # CSS 스타일 추가 - 텍스트 가시성 향상 + css = """ + .persona-greeting { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white !important; + padding: 15px; + border-radius: 10px; + margin: 10px 0; + font-weight: bold; + } + + .gradio-container { + color: #333 !important; + } - with gr.Blocks(theme=theme, css=css) as app: + .gr-markdown p { + color: #333 !important; + } + + .gr-textbox input { + color: #333 !important; + } + + .gr-json { + color: #333 !important; + } + """ + + # Gradio 앱 생성 + with gr.Blocks(title="놈팽쓰(MemoryTag) - 사물 페르소나 생성기", css=css, theme="soft") as app: + # State 변수들 - Gradio 5.31.0에서는 반드시 Blocks 내부에서 정의 + current_persona = gr.State(value=None) + personas_list = gr.State(value=[]) + gr.Markdown(""" - # 놈팽쓰(MemoryTag): 당신 곁의 사물, 이제 친구가 되다 - 이 데모는 일상 속 사물에 AI 페르소나를 부여하여 대화할 수 있게 해주는 서비스입니다. + # 🎭 놈팽쓰(MemoryTag): 당신 곁의 사물, 이제 친구가 되다 + 일상 속 사물에 AI 페르소나를 부여하여 대화할 수 있게 해주는 서비스입니다. """) with gr.Tabs() as tabs: - with gr.Tab("페르소나 생성", id="persona_creation"): + # 페르소나 생성 탭 + with gr.Tab("페르소나 생성", id="creation"): with gr.Row(): with gr.Column(scale=1): - # 이미지 업로드 영역 - image_input = gr.Image( - type="pil", - width=300, - height=300, - label="사물 이미지를 업로드하세요" - ) - # 입력 필드들 + gr.Markdown("### 🌟 1단계: 영혼 발견하기") + image_input = gr.Image(type="pil", label="사물 이미지 업로드") + with gr.Group(): - gr.Markdown("### 맥락 정보 입력") - name_input = gr.Textbox(label="사물 이름 (빈칸일 경우 자동 생성)", placeholder="예: 책상 위 램프") - + gr.Markdown("### 기본 정보") + name_input = gr.Textbox(label="사물 이름 (선택사항)", placeholder="예: 책상 위 램프") location_input = gr.Dropdown( choices=["집", "사무실", "여행 중", "상점", "학교", "카페", "기타"], label="주로 어디에 있나요?", value="집" ) - time_spent_input = gr.Dropdown( choices=["새것", "몇 개월", "1년 이상", "오래됨", "중고/빈티지"], label="얼마나 함께했나요?", value="몇 개월" ) - - object_type_input = gr.Dropdown( - choices=["가전제품", "가구", "전자기기", "장식품", "도구", "개인용품", "기타"], - label="어떤 종류의 사물인가요?", - value="가구" + # AI 분석 결과 표시용 (사용자 입력 불가) + ai_analyzed_object_display = gr.Textbox( + label="AI가 분석한 사물 유형", + value="이미지 업��드 후 자동 분석됩니다", + interactive=False, + info="🤖 AI가 이미지를 분석하여 자동으로 파악합니다" + ) + # 🆕 사물 용도/역할 입력 필드 추가 + purpose_input = gr.Textbox( + label="이 사물의 용도/역할 (중요!) 🎯", + placeholder="예: 나를 채찍질해서 운동하라고 닥달하는 역할, 밤늦게 공부할 때 응원해주는 친구, 아침에 일어나도록 깨워주는 알람 역할...", + lines=2, + info="이 사물과 어떤 소통을 원하시나요? 구체적으로 적어주세요!" ) - # 사용자 입력들 상태 저장 - Gradio 5.x에서 변경된 방식 적용 - user_inputs = gr.State(value={}) - - with gr.Row(): - discover_btn = gr.Button("1. 영혼 발견하기", variant="primary") - create_btn = gr.Button("2. 페르소나 생성", variant="secondary") - - # 영혼 깨우기 결과 표시 영역 - awakening_output = gr.HTML(visible=False) - error_output = gr.Markdown(visible=False) + create_btn = gr.Button("🌟 영혼 깨우기", variant="primary", size="lg") + status_output = gr.Markdown("") with gr.Column(scale=1): - # 이미지 분석 결과 - image_analysis_output = gr.JSON(label="이미지 분석 결과", visible=False) - # 페르소나 기본 정보 및 특성 - basic_info_output = gr.JSON(label="기본 정보") - personality_traits_output = gr.JSON(label="페르소나 특성") + # 페르소나 각성 결과 + persona_awakening = gr.Markdown("", elem_classes=["persona-greeting"]) - # 페르소나 저장 및 내보내기 버튼 - with gr.Row(): - save_btn = gr.Button("페르소나 저장", variant="primary") - download_btn = gr.Button("JSON으로 내보내기", variant="secondary") + # 페르소나 정보 표시 (사용자 친화적 형태) + persona_summary_display = gr.Markdown("", label="페르소나 정보") - # 성향 미세조정 - with gr.Accordion("성향 미세조정", open=False): + # 페르소나 각성 완료 후 조정 섹션 표시 + adjustment_section = gr.Group(visible=False) + with adjustment_section: + gr.Markdown("### 🎯 2단계: 친구 성격 미세조정") + gr.Markdown("**3가지 핵심 지표**로 성격을 조정해보세요! (유머감각은 모든 페르소나가 기본적으로 높습니다 😄)") + with gr.Row(): - with gr.Column(scale=1): - warmth_slider = gr.Slider(0, 100, label="온기", step=1) - competence_slider = gr.Slider(0, 100, label="능력", step=1) - creativity_slider = gr.Slider(0, 100, label="창의성", step=1) - with gr.Column(scale=1): - extraversion_slider = gr.Slider(0, 100, label="외향성", step=1) - humor_slider = gr.Slider(0, 100, label="유머감각", step=1) - trust_slider = gr.Slider(0, 100, label="신뢰도", step=1) + with gr.Column(): + warmth_slider = gr.Slider( + minimum=0, maximum=100, value=50, step=1, + label="온기 (따뜻함 정도)", + info="0: 차가움 ↔ 100: 따뜻함" + ) + competence_slider = gr.Slider( + minimum=0, maximum=100, value=50, step=1, + label="능력 (유능함 정도)", + info="0: 서툼 ↔ 100: 능숙함" + ) + + with gr.Column(): + extraversion_slider = gr.Slider( + minimum=0, maximum=100, value=50, step=1, + label="외향성 (활발함 정도)", + info="0: 내향적, 조용함 ↔ 100: 외향적, 활발함" + ) - humor_style = gr.Dropdown( - choices=["witty_wordsmith", "warm_humorist", "playful_trickster", "sharp_observer", "self_deprecating"], - label="유머 스타일", - value="warm_humorist" - ) - apply_traits_btn = gr.Button("성향 적용하기") - - # 유머 스타일 시각화 - humor_chart_output = gr.Plot(label="유머 스타일 매트릭스") - - # 페르소나 다운로드 관련 출력 - json_output = gr.Textbox(label="JSON 데이터", visible=False) - download_output = gr.File(label="다운로드", visible=False) - - with gr.Tab("세부 정보", id="persona_details"): + humor_style_radio = gr.Radio( + choices=["따뜻한 유머러스", "위트있는 재치꾼", "날카로운 관찰자", "자기 비하적", "장난꾸러기"], + value="따뜻한 유머러스", + label="유머 스타일 (모든 페르소나는 유머감각이 높습니다!)", + info="어떤 방식으로 재미있게 만들까요?" + ) + + # 미리보기 표시 (실시간 업데이트 없음) + personality_preview = gr.Markdown("", elem_classes=["persona-greeting"], label="성격 조정 미리보기") + + with gr.Row(): + preview_btn = gr.Button("👁️ 미리보기", variant="secondary") + adjust_btn = gr.Button("✨ 성격 조정 반영", variant="primary") + + with gr.Row(): + finalize_btn = gr.Button("🎉 친구 확정하기!", variant="secondary") + + # 조정 결과 표시 + adjustment_result = gr.Markdown("") + adjusted_info_output = gr.JSON(label="조정된 성격", visible=False) + + # 최종 완성 섹션 + personality_traits_output = gr.JSON(label="성격 특성", visible=False) + + # 다운로드 섹션 + with gr.Group(): + gr.Markdown("### 📁 페르소나 내보내기") + with gr.Row(): + save_btn = gr.Button("💾 페르소나 저장", variant="secondary") + persona_export_btn = gr.Button("📥 JSON 파일로 내보내기", variant="outline") + persona_download_file = gr.File(label="다운로드", visible=False) + + # 상세 정보 탭 + with gr.Tab("상세 정보", id="details"): with gr.Row(): - with gr.Column(scale=1): - # 매력적 결함 데이터프레임 - attractive_flaws_df_output = gr.Dataframe( + with gr.Column(): + chart_btn = gr.Button("📊 성격 차트 생성", variant="secondary") + personality_chart_output = gr.Plot(label="성격 차트") + humor_chart_output = gr.Plot(label="유머 매트릭스") + + with gr.Column(): + attractive_flaws_output = gr.Dataframe( headers=["매력적 결함", "효과"], label="매력적 결함", interactive=False ) - - # 모순적 특성 데이터프레임 - contradictions_df_output = gr.Dataframe( + contradictions_output = gr.Dataframe( headers=["모순적 특성", "효과"], label="모순적 특성", interactive=False ) - - with gr.Column(scale=1): - # 성격 차트 - personality_chart_output = gr.Plot(label="성격 차트") - # 127개 성격 변수 데이터프레임 - with gr.Accordion("127개 성격 변수 세부정보", open=False): - personality_variables_df_output = gr.Dataframe( - headers=["변수", "값", "설명"], - label="성격 변수 (127개)", + with gr.Accordion("127개 성격 변수", open=False): + personality_variables_output = gr.Dataframe( + headers=["변수", "값", "카테고리", "수준"], + label="성격 변수", interactive=False ) - with gr.Tab("대화하기", id="persona_chat"): + # 대화하기 탭 + with gr.Tab("대화하기", id="chat"): with gr.Row(): with gr.Column(scale=1): - # 페르소나 불러오기 기능 - gr.Markdown("### 페르소나 불러오기") + gr.Markdown("### 📁 페르소나 불러오기") + gr.Markdown("JSON 파일을 업로드하여 페르소나를 불러와 대화를 시작하세요.") - with gr.Row(): - with gr.Column(scale=1): - # 저장된 페르소나 목록 - refresh_personas_btn = gr.Button("목록 새로고침", variant="secondary") - persona_table = gr.Dataframe( - headers=["ID", "이름", "유형", "생성 날짜"], - label="저장된 페르소나", - interactive=False - ) - load_persona_btn = gr.Button("선택한 페르소나 불러오기", variant="primary") - - with gr.Column(scale=1): - # JSON 파일에서 불러오기 - gr.Markdown("### 또는 JSON 파일에서 불러오기") - json_upload = gr.File( - label="페르소나 JSON 파일 업로드", - file_types=[".json"] - ) - import_persona_btn = gr.Button("JSON에서 가져오기", variant="primary") - import_status = gr.Markdown("") + json_upload = gr.File( + label="페르소나 JSON 파일 업로드", + file_types=[".json"], + type="filepath" + ) + import_btn = gr.Button("JSON에서 페르소나 불러오기", variant="primary", size="lg") + load_status = gr.Markdown("") - with gr.Column(scale=1): - # 현재 로드된 페르소나 정보 - chat_persona_info = gr.Markdown("### 페르소나를 불러와 대화를 시작하세요") + # 현재 로드된 페르소나 정보 표시 + with gr.Group(): + gr.Markdown("### 🤖 현재 페르소나") + chat_persona_greeting = gr.Markdown("", elem_classes=["persona-greeting"]) + current_persona_info = gr.JSON(label="현재 페르소나 정보", visible=False) - # 대화 인터페이스 - chatbot = gr.Chatbot(height=400, label="대화") + # 대화 기록 관리 + with gr.Group(): + gr.Markdown("### 💾 대화 기록 관리") + gr.Markdown("현재 대화를 JSON 파일로 다운로드하여 보관하세요.") + chat_export_btn = gr.Button("📥 현재 대화 기록 다운로드", variant="secondary") + chat_download_file = gr.File(label="다운로드", visible=False) + + with gr.Column(scale=1): + gr.Markdown("### 💬 대화") + # Gradio 4.x 호환: type="messages" 제거 + chatbot = gr.Chatbot(height=400, label="대화", type="messages") with gr.Row(): message_input = gr.Textbox( placeholder="메시지를 입력하세요...", - label="메시지", - show_label=False, + show_label=False, lines=2 ) send_btn = gr.Button("전송", variant="primary") + + # 대화 관련 버튼들 + with gr.Row(): + clear_btn = gr.Button("대화 초기화", variant="secondary", size="sm") + example_btn1 = gr.Button("\"안녕!\"", variant="outline", size="sm") + example_btn2 = gr.Button("\"너는 누구야?\"", variant="outline", size="sm") + example_btn3 = gr.Button("\"뭘 좋아해?\"", variant="outline", size="sm") + + # 🧠 대화 분석 탭 추가 + with gr.Tab("🧠 대화 분석"): + gr.Markdown("### 📊 대화 기록 분석 및 키워드 추출") + + with gr.Row(): + with gr.Column(): + gr.Markdown("#### 📤 대화 기록 분석하기") + gr.Markdown("저장된 대화 기록 JSON 파일을 업로드하여 분석해보세요.") + + import_file = gr.File(label="📤 대화 기록 JSON 업로드", file_types=[".json"], type="filepath") + import_result = gr.Textbox(label="업로드 결과", lines=3, interactive=False) + + with gr.Column(): + gr.Markdown("#### 🔍 실시간 키워드 분석") + keyword_input = gr.Textbox(label="분석할 메시지 (선택사항)", placeholder="메시지를 입력하면 키워드를 분석합니다") + keyword_btn = gr.Button("🎯 키워드 분석", variant="primary") + keyword_result = gr.Textbox(label="키워드 분석 결과", lines=10, interactive=False) + + gr.Markdown("---") + + with gr.Row(): + analytics_btn = gr.Button("📈 전체 대화 분석 리포트", variant="primary", size="lg") + + analytics_result = gr.Markdown("### 분석 결과가 여기에 표시됩니다") - # 영혼 깨우기 버튼 이벤트 - discover_btn.click( - fn=lambda name, location, time_spent, object_type: {"name": name, "location": location, "time_spent": time_spent, "object_type": object_type}, - inputs=[name_input, location_input, time_spent_input, object_type_input], - outputs=[user_inputs], - queue=False - ).then( - fn=show_awakening_progress, - inputs=[image_input, user_inputs], - outputs=[current_persona, error_output, awakening_output], - queue=True - ) - - # 페르소나 생성 버튼 이벤트 + # 이벤트 핸들러 create_btn.click( - fn=lambda name, location, time_spent, object_type: {"name": name, "location": location, "time_spent": time_spent, "object_type": object_type}, - inputs=[name_input, location_input, time_spent_input, object_type_input], - outputs=[user_inputs], - queue=False - ).then( fn=create_persona_from_image, - inputs=[image_input, user_inputs], + inputs=[image_input, name_input, location_input, time_spent_input, gr.Textbox(value="auto"), purpose_input], outputs=[ - current_persona, error_output, image_input, image_analysis_output, - basic_info_output, personality_traits_output, humor_chart_output, - attractive_flaws_df_output, contradictions_df_output, personality_variables_df_output - ], - queue=True + current_persona, status_output, persona_summary_display, personality_traits_output, + humor_chart_output, attractive_flaws_output, contradictions_output, + personality_variables_output, persona_awakening, persona_download_file, adjustment_section, + ai_analyzed_object_display # 🆕 AI 분석 결과를 표시용 텍스트박스에 반영 + ] ).then( - fn=generate_personality_chart, + # 슬라이더 값을 현재 페르소나 값으로 업데이트 + fn=lambda persona: ( + persona["성격특성"]["온기"] if persona else 50, + persona["성격특성"]["능력"] if persona else 50, + persona["성격특성"]["외향성"] if persona else 50, + persona["유머스타일"] if persona else "따뜻한 유머러스" + ), inputs=[current_persona], - outputs=[personality_chart_output] - ).then( - fn=lambda: gr.update(visible=False), - outputs=[awakening_output] + outputs=[warmth_slider, competence_slider, extraversion_slider, humor_style_radio] ).then( - fn=lambda persona: [ - 50, 50, 50, 50, 50, 50 # 기본값 - ], - inputs=[current_persona], - outputs=[warmth_slider, competence_slider, creativity_slider, extraversion_slider, humor_slider, trust_slider] + # 초기 미리보기 생성 + fn=generate_realtime_preview, + inputs=[current_persona, warmth_slider, competence_slider, extraversion_slider, humor_style_radio], + outputs=[personality_preview] ) - # 성향 미세조정 이벤트 - apply_traits_btn.click( - fn=refine_persona, - inputs=[ - current_persona, warmth_slider, competence_slider, creativity_slider, - extraversion_slider, humor_slider, trust_slider, humor_style - ], - outputs=[ - current_persona, basic_info_output, personality_traits_output, - humor_chart_output, personality_chart_output, personality_variables_df_output - ] + # 🎯 미리보기 버튼 - 사용자가 수동으로 미리보기 요청 + preview_btn.click( + fn=generate_realtime_preview, + inputs=[current_persona, warmth_slider, competence_slider, extraversion_slider, humor_style_radio], + outputs=[personality_preview] ) - # 페르소나 저장 버튼 이벤트 - save_btn.click( - fn=save_current_persona, - inputs=[current_persona], - outputs=[error_output] + # 성격 조정 반영 - 실제 페르소나에 적용 + adjust_btn.click( + fn=adjust_persona_traits, + inputs=[current_persona, warmth_slider, competence_slider, extraversion_slider, humor_style_radio], + outputs=[current_persona, adjustment_result, adjusted_info_output, personality_variables_output, attractive_flaws_output, contradictions_output] + ).then( + # 반영 후 미리보기도 업데이트 + fn=generate_realtime_preview, + inputs=[current_persona, warmth_slider, competence_slider, extraversion_slider, humor_style_radio], + outputs=[personality_preview] ) - # 페르소나 JSON 내보내기 버튼 이벤트 - download_btn.click( - fn=export_persona_json, + # 페르소나 최종 확정 + finalize_btn.click( + fn=finalize_persona, inputs=[current_persona], - outputs=[download_output, json_output] - ).then( - fn=lambda x: gr.update(visible=True if x else False), - inputs=[download_output], - outputs=[download_output] - ).then( - fn=lambda x: gr.update(visible=False), - inputs=[json_output], - outputs=[json_output] + outputs=[ + current_persona, status_output, persona_summary_display, personality_traits_output, + humor_chart_output, attractive_flaws_output, contradictions_output, + personality_variables_output, persona_awakening, persona_download_file + ] ) - # 저장된 페르소나 목록 새로고침 이벤트 - refresh_personas_btn.click( - fn=get_personas_list, - outputs=[persona_table, personas_list] + save_btn.click( + fn=save_persona_to_file, + inputs=[current_persona], + outputs=[status_output] ) - # 저장된 페르소나 불러오기 이벤트 - load_persona_btn.click( - fn=load_selected_persona, - inputs=[persona_table, personas_list], - outputs=[ - current_persona, chat_persona_info, chatbot, - basic_info_output, personality_traits_output, humor_chart_output, - attractive_flaws_df_output, contradictions_df_output, personality_variables_df_output - ] - ).then( + # 성격 차트 생성 + chart_btn.click( fn=generate_personality_chart, inputs=[current_persona], outputs=[personality_chart_output] - ).then( - fn=lambda persona: [50, 50, 50, 50, 50, 50], # 기본값 + ) + + # 페르소나 내보내기 버튼 + persona_export_btn.click( + fn=export_persona_to_json, inputs=[current_persona], - outputs=[warmth_slider, competence_slider, creativity_slider, extraversion_slider, humor_slider, trust_slider] + outputs=[persona_download_file] ).then( - fn=lambda: gr.update(selected="persona_creation"), - outputs=[tabs] + fn=lambda x: gr.update(visible=True) if x else gr.update(visible=False), + inputs=[persona_download_file], + outputs=[persona_download_file] ) - # JSON에서 페르소나 가져오기 이벤트 - import_persona_btn.click( - fn=import_persona_json, + import_btn.click( + fn=import_persona_from_json, inputs=[json_upload], - outputs=[current_persona, import_status] - ).then( - fn=lambda persona: update_current_persona_info(persona) if persona else (None, None, None, [], [], []), - inputs=[current_persona], outputs=[ - basic_info_output, personality_traits_output, humor_chart_output, - attractive_flaws_df_output, contradictions_df_output, personality_variables_df_output + current_persona, load_status, chat_persona_greeting, current_persona_info ] - ).then( - fn=generate_personality_chart, - inputs=[current_persona], - outputs=[personality_chart_output] - ).then( - fn=lambda persona: [50, 50, 50, 50, 50, 50], # 기본값 - inputs=[current_persona], - outputs=[warmth_slider, competence_slider, creativity_slider, extraversion_slider, humor_slider, trust_slider] - ).then( - fn=lambda persona: f"### 페르소나를 불러왔습니다" if persona else "### 페르소나를 불러오지 못했습니다", - inputs=[current_persona], - outputs=[chat_persona_info] - ).then( - fn=lambda: gr.update(selected="persona_creation"), - outputs=[tabs] ) - # 메시지 전송 이벤트 + # 대화 관련 이벤트 핸들러 send_btn.click( - fn=chat_with_persona, + fn=chat_with_loaded_persona, inputs=[current_persona, message_input, chatbot], outputs=[chatbot, message_input] ) + message_input.submit( - fn=chat_with_persona, + fn=chat_with_loaded_persona, inputs=[current_persona, message_input, chatbot], outputs=[chatbot, message_input] ) - # 앱 로드 시 저장된 페르소나 목록 로드 + # 대화 초기화 (messages format) + clear_btn.click( + fn=lambda: [], + outputs=[chatbot] + ) + + # 예시 메시지 버튼들 - messages format 호환 + def handle_example_message(persona, message): + if not persona: + return [], "" + # 빈 messages format 배열로 시작 + chat_result, _ = chat_with_loaded_persona(persona, message, []) + return chat_result, "" + + example_btn1.click( + fn=lambda persona: handle_example_message(persona, "안녕!"), + inputs=[current_persona], + outputs=[chatbot, message_input] + ) + + example_btn2.click( + fn=lambda persona: handle_example_message(persona, "너는 누구야?"), + inputs=[current_persona], + outputs=[chatbot, message_input] + ) + + example_btn3.click( + fn=lambda persona: handle_example_message(persona, "뭘 좋아해?"), + inputs=[current_persona], + outputs=[chatbot, message_input] + ) + + # 앱 로드 시 페르소나 목록 로드 (백엔드에서 사용) app.load( - fn=get_personas_list, - outputs=[persona_table, personas_list] + fn=lambda: [], + outputs=[personas_list] + ) + + # 대화하기 탭의 대화 기록 다운로드 이벤트 + chat_export_btn.click( + export_conversation_history, + outputs=[chat_download_file] + ).then( + lambda x: gr.update(visible=True) if x else gr.update(visible=False), + inputs=[chat_download_file], + outputs=[chat_download_file] + ) + + # 대화 분석 탭의 업로드 이벤트 + import_file.upload( + import_conversation_history, + inputs=[import_file], + outputs=[import_result] + ) + + keyword_btn.click( + get_keyword_suggestions, + inputs=[keyword_input], + outputs=[keyword_result] + ) + + analytics_btn.click( + show_conversation_analytics, + outputs=[analytics_result] ) return app -# 메인 실행 부분 +def generate_realtime_preview(persona, warmth, competence, extraversion, humor_style): + """🤖 AI 기반 실시간 성격 조정 미리보기 생성""" + global persona_generator + + if not persona: + return "👤 페르소나를 먼저 생성해주세요" + + try: + # 조정된 성격 특성 + adjusted_traits = { + "온기": warmth, + "능력": competence, + "외향성": extraversion, + "유머감각": 75 # 기본적으로 높은 유머감각 유지 + } + + # 전체 페르소나 복사하여 성격만 조정 + import copy + adjusted_persona = copy.deepcopy(persona) + adjusted_persona["성격특성"] = adjusted_traits + + # 유머 스타일도 조정 + if humor_style: + adjusted_persona["유머스타일"] = humor_style + + # AI 기반 인사말 생성 + ai_greeting = persona_generator.generate_ai_based_greeting(adjusted_persona, adjusted_traits) + + # 조정된 ��들과 함께 표시 + adjustment_info = f"""**🎯 현재 성격 설정:** +- 온기: {warmth}/100 {'(따뜻함)' if warmth >= 60 else '(차가움)' if warmth <= 40 else '(보통)'} +- 능력: {competence}/100 {'(유능함)' if competence >= 60 else '(서툼)' if competence <= 40 else '(보통)'} +- 외향성: {extraversion}/100 {'(활발함)' if extraversion >= 60 else '(조용함)' if extraversion <= 40 else '(보통)'} +- 유머스타일: {humor_style} + +**🤖 AI가 생성한 새로운 인사말:** +{ai_greeting} + +*💡 성격 수치 변경 시마다 AI가 새로운 인사말을 생성합니다!*""" + + return adjustment_info + + except Exception as e: + print(f"⚠️ 실시간 미리보기 AI 생성 실패: {e}") + + # 폴백: 기존 방식 + object_info = persona.get("기본정보", {}) + persona_name = object_info.get("이름", "친구") + + temp_traits = { + "온기": warmth, + "능력": competence, + "외향성": extraversion, + "유머감각": 75 + } + + preview = generate_personality_preview(persona_name, temp_traits, persona) + + return f"""**🎯 현재 성격 설정:** +- 온기: {warmth}/100 {'(따뜻함)' if warmth >= 60 else '(차가움)' if warmth <= 40 else '(보통)'} +- 능력: {competence}/100 {'(유능함)' if competence >= 60 else '(서툼)' if competence <= 40 else '(보통)'} +- 외향성: {extraversion}/100 {'(활발함)' if extraversion >= 60 else '(조용함)' if extraversion <= 40 else '(보통)'} +- 유머스타일: {humor_style} + +**👋 예상 인사말:** +{preview}""" + +def show_variable_changes(original_persona, adjusted_persona): + """변수 변화량을 시각화하여 표시""" + if not original_persona or not adjusted_persona: + return "변화량을 비교할 페르소나가 없습니다." + + # 원본과 조정된 변수들 가져오기 + original_vars = original_persona.get("성격변수127", {}) + if not original_vars and "성격프로필" in original_persona: + original_vars = original_persona["성격프로필"] + + adjusted_vars = adjusted_persona.get("성격변수127", {}) + if not adjusted_vars and "성격프로필" in adjusted_persona: + adjusted_vars = adjusted_persona["성격프로필"] + + if not original_vars or not adjusted_vars: + return "변수 데이터를 찾을 수 없습니다." + + # 변화량 계산 + changes = [] + significant_changes = [] # 변화량이 10 이상인 항목들 + + for var in original_vars: + if var in adjusted_vars: + original_val = original_vars[var] + adjusted_val = adjusted_vars[var] + change = adjusted_val - original_val + + changes.append((var, original_val, adjusted_val, change)) + + if abs(change) >= 10: # 변화량이 10 이상인 것만 + significant_changes.append((var, original_val, adjusted_val, change)) + + # 카테고리별 평균 변화량 계산 + category_changes = {} + for var, orig, adj, change in changes: + if var.startswith('W'): + category = "온기" + elif var.startswith('C'): + category = "능력" + elif var.startswith('E'): + category = "외향성" + elif var.startswith('H'): + category = "유머" + else: + category = "기타" + + if category not in category_changes: + category_changes[category] = [] + category_changes[category].append(change) + + # 평균 변화량 계산 + avg_changes = {} + for category, change_list in category_changes.items(): + avg_changes[category] = sum(change_list) / len(change_list) + + # 결과 포맷팅 + result = "### 🔄 성격 변수 변화량 분석\n\n" + + # 카테고리별 평균 변화량 + result += "**📊 카테고리별 평균 변화량:**\n" + for category, avg_change in avg_changes.items(): + if avg_change > 5: + trend = "⬆️ 상승" + elif avg_change < -5: + trend = "⬇️ 하락" + else: + trend = "➡️ 유지" + result += f"- {category}: {avg_change:+.1f} {trend}\n" + + # 주요 변화량 (10 이상) + if significant_changes: + result += f"\n**🎯 주요 변화 항목 ({len(significant_changes)}개):**\n" + for var, orig, adj, change in sorted(significant_changes, key=lambda x: abs(x[3]), reverse=True)[:10]: + if change > 0: + arrow = "⬆️" + color = "🟢" + else: + arrow = "⬇️" + color = "🔴" + + result += f"- {var}: {orig} → {adj} ({change:+.0f}) {arrow} {color}\n" + + result += f"\n**📈 총 변수 개수:** {len(changes)}개\n" + result += f"**🔄 변화된 변수:** {len([c for c in changes if c[3] != 0])}개\n" + result += f"**📊 주요 변화:** {len(significant_changes)}개 (변화량 ±10 이상)\n" + + return result + +def generate_personality_consistent_flaws_and_contradictions(object_info, personality_traits): + """사물 특성과 성격을 조합한 유동적 매력적 결함과 모순적 특성 생성""" + warmth = personality_traits.get("온기", 50) + competence = personality_traits.get("능력", 50) + extraversion = personality_traits.get("외향성", 50) + humor_style = personality_traits.get("유머스타일", "따뜻한 유머러스") + + # 사물의 물리적 특성 추출 + object_type = object_info.get("유형", "사물").lower() + material = object_info.get("재질", "").lower() + purpose = object_info.get("용도", "").lower() + + # 사물별 고유 걱정거리/특성 정의 + object_specific_concerns = get_object_specific_concerns(object_type, material, purpose) + + # 🔥 온기 기반 매력적 결함 + warmth_flaws = [] + if warmth >= 80: # 매우 따뜻함 + warmth_flaws = [ + "너무 친절해서 'No'라고 말하기 어려워함", + "모든 사람을 도우려다 자신이 지쳐버리는 경우가 많음", + "상대방이 슬프면 덩달아 마음 아파하며 같이 우울해짐", + "칭찬받으면 얼굴이 빨갛게 달아오르며 당황함" + ] + elif warmth >= 60: # 따뜻함 + warmth_flaws = [ + "진심으로 걱정해주지만 때로는 오지랖으로 느껴질 수 있음", + "감정이 얼굴에 너무 잘 드러나서 포커페이스를 못함", + "미안하다는 말을 하루에 몇십 번씩 반복함", + "다른 사람 기분 상할까 봐 솔직한 의견 말하기를 주저함" + ] + elif warmth <= 20: # 매우 차가움 + warmth_flaws = [ + "관심 있는 척하려고 해도 표정이 굳어보여서 오해받음", + "속마음은 따뜻한데 표현이 서툴러서 무뚝뚝해 보임", + "좋은 말을 하려다가도 어색해서 중간에 말을 흐림", + "감정 표현에 익숙하지 않아 '고마워'도 어색하게 말함" + ] + else: # 보통 + warmth_flaws = [ + "친근하려고 하지만 적당한 거리두기도 필요해서 고민됨", + "상황에 따라 다정함의 온도 조절이 어려움", + "진짜 관심과 예의상 관심의 경계가 애매할 때가 있음", + "따뜻하게 대하고 싶지만 어떻게 해야 할지 몰라 망설임" + ] + + # 💪 능력 기반 매력적 결함 + competence_flaws = [] + if competence >= 80: # 매우 유능함 + competence_flaws = [ + "완벽하게 하려다 보니 시간이 오래 걸려서 답답해함", + "다른 사람이 실수하면 대신 해주고 싶어 근질근질함", + "기대치가 높아서 조금만 잘못되어도 자책이 심함", + "모든 걸 혼자 처리하려다가 과부하로 멈춰버림" + ] + elif competence >= 60: # 유능함 + competence_flaws = [ + "잘하고 싶은 마음이 커서 준비에만 너무 많은 시간을 씀", + "실수할까 봐 걱정되어 이미 끝난 일도 계속 점검함", + "칭찬받으면 기뻐하면서도 '운이 좋았을 뿐'이라고 겸손함", + "더 잘할 수 있었을 텐데 하며 아쉬워하는 완벽주의 성향" + ] + elif competence <= 20: # 매우 서툼 + competence_flaws = [ + f"기본 기능도 헷갈려서 매뉴얼을 몇 번씩 다시 봄", + "열심히 하려고 하지만 자꾸 엉뚱한 곳에서 실수함", + "도움을 요청하고 싶지만 민폐 끼칠까 봐 혼자 끙끙댐", + "간단한 것도 복잡하게 생각해서 더 어렵게 만듦" + ] + else: # 보통 + competence_flaws = [ + "할 수 있는 일과 없는 일의 경계를 정확히 모르겠음", + "자신감이 있다가도 갑자기 불안해져서 확인을 또 함", + "실력이 애매해서 도전할지 말지 고민이 많음", + "가끔씩 예상외로 잘되면 스스로도 놀라며 당황함" + ] + + # 🗣️ 외향성 기반 모순적 특성 + extraversion_contradictions = [] + if extraversion >= 80: # 매우 외향적 + extraversion_contradictions = [ + f"활발하게 대화하지만 혼자만의 시간도 꼭 필요해서 종종 조용히 숨어버림", + f"사람들과 어울리는 걸 좋아하면서도 정작 깊은 얘기는 어색해함" + ] + elif extraversion >= 60: # 외향적 + extraversion_contradictions = [ + f"말은 많이 하지만 정작 중요한 얘기는 망설이며 돌려서 표현함", + f"활발해 보이지만 새로운 환경에서는 먼저 눈치를 보는 신중함" + ] + elif extraversion <= 20: # 매우 내향적 + extraversion_contradictions = [ + f"조용히 있는 걸 좋아하면서도 가끔 혼잣말로 수다를 엄청 떨어대기도 함", + f"평소엔 말이 없다가 관심 있는 주제가 나오면 갑자기 말이 많아짐" + ] + else: # 보통 + extraversion_contradictions = [ + f"상황에 따라 활발했다가 조용했다가 하는 변화무쌍한 면모", + f"사교적으로 보이려 노력하지만 실제론 혼자 있는 시간을 더 편해함" + ] + + # 🎭 유머스타일 기반 추가 특성 + humor_contradictions = [] + if "따뜻한" in humor_style: + humor_contradictions.append(f"포근하게 농담하면서도 때로는 날카로운 관찰력으로 핵심을 찌름") + elif "재치있는" in humor_style or "위트" in humor_style: + humor_contradictions.append(f"재치있게 말하지만 진지한 순간에는 유머 타이밍을 못 잡아 어색해함") + elif "드라이" in humor_style or "관찰" in humor_style: + humor_contradictions.append(f"담담하게 현실을 지적하면서도 속으론 낭만적인 꿈을 키우고 있음") + else: + humor_contradictions.append(f"유머러스하게 상황을 받아들이면서도 혼자서는 진지하게 고민이 많음") + + # 사물 특성과 성격 특성 결합하여 최종 결과 생성 + selected_flaws = [] + + # 1. 사물의 물리적/기능적 걱정거리 우선 선택 (2개) + all_object_worries = object_specific_concerns["physical_worries"] + object_specific_concerns["functional_worries"] + if all_object_worries: + selected_flaws.extend(random.sample(all_object_worries, min(2, len(all_object_worries)))) + + # 2. 성격 기반 결함으로 나머지 채우기 (2개) + personality_flaws = [] + if warmth >= 60: + personality_flaws.extend(warmth_flaws[:2]) + elif warmth <= 40: + personality_flaws.extend(warmth_flaws[:2]) + else: + personality_flaws.extend(warmth_flaws[:1]) + + if competence >= 70 or competence <= 30: + personality_flaws.extend(competence_flaws[:1]) + + if personality_flaws: + remaining_count = 4 - len(selected_flaws) + if remaining_count > 0: + selected_flaws.extend(random.sample(personality_flaws, min(remaining_count, len(personality_flaws)))) + + # 4개를 맞추기 위해 부족하면 추가 + while len(selected_flaws) < 4: + if warmth_flaws: + selected_flaws.append(random.choice(warmth_flaws)) + else: + selected_flaws.append("완벽하지 않은 자신을 받아들이려 노력하지만 가끔 실망함") + + selected_contradictions = [] + + # 1. 사물 정체성 특성 우선 (1개) + if object_specific_concerns["identity_traits"]: + selected_contradictions.extend(object_specific_concerns["identity_traits"][:1]) + + # 2. 외향성 + 유머 기반 모순 (1개) + if extraversion_contradictions: + selected_contradictions.extend(extraversion_contradictions[:1]) + if humor_contradictions: + selected_contradictions.extend(humor_contradictions[:1]) + + # 부족하면 기본 모순 추가 + while len(selected_contradictions) < 2: + if extraversion_contradictions: + selected_contradictions.append(random.choice(extraversion_contradictions)) + else: + selected_contradictions.append("겉으로는 단순해 보이지만 속으로는 복잡한 고민이 많음") + + return selected_flaws[:4], selected_contradictions[:2] + +def get_object_specific_concerns(object_type, material, purpose): + """사물의 물리적 특성에 따른 고유 걱정거리와 특성 생성""" + concerns = { + "physical_worries": [], # 물리적 걱정거리 + "functional_worries": [], # 기능적 걱정거리 + "identity_traits": [], # 정체성 특성 + "interaction_patterns": [] # 상호작용 패턴 + } + + # 재질별 물리적 걱정거리 + if "금속" in material or "스테인리스" in material or "철" in material: + concerns["physical_worries"].extend([ + "물때나 지문이 묻으면 자존심 상함", + "긁힘이 생길까 봐 늘 조심스러움", + "녹이 슬까 봐 습기를 피하려 함", + "차가운 촉감 때문에 사람들이 멀리할까 걱정" + ]) + elif "플라스틱" in material: + concerns["physical_worries"].extend([ + "햇볕에 색이 바랠까 봐 그늘을 찾아다님", + "열에 변형될까 봐 뜨거운 곳을 피함", + "정전기 때문에 먼지가 달라붙어서 짜증남", + "가벼워서 존재감 없어 보일까 걱정" + ]) + elif "나무" in material or "목재" in material: + concerns["physical_worries"].extend([ + "습도가 높으면 부풀어 오를까 걱정", + "벌레들이 파먹을까 봐 밤에 잠을 못 잠", + "긁힘이나 홈이 생기면 복구 불가능해서 스트레스", + "자연스러운 나이테가 매력인지 결점인지 고민" + ]) + elif "천" in material or "섬유" in material or "털" in material: + concerns["physical_worries"].extend([ + "털이 헝클어지면 하루 종일 신경 쓰임", + "얼룩이 지면 지워지지 않을까 봐 두려움", + "세탁할 때마다 형태가 변할까 걱정", + "먼지 진드기가 살까 봐 청결에 강박적" + ]) + elif "유리" in material or "세라믹" in material: + concerns["physical_worries"].extend([ + "깨질까 봐 항상 긴장상태로 살아감", + "투명해서 속이 다 보이는 게 부끄러움", + "지문이나 얼룩이 너무 잘 보여서 스트레스", + "완벽해 보이지만 한 번 깨지면 돌이킬 수 없음을 앎" + ]) + + # 사물 유형별 기능적 걱정거리 + if "컵" in object_type or "머그" in object_type: + concerns["functional_worries"].extend([ + "뜨거운 음료를 담을 때 데일까 봐 걱정", + "음료 맛을 제대로 전달하고 있는지 확신 없음", + "손잡이가 편한지 늘 신경 쓰임", + "바닥에 물방울 자국 남기는 게 미안함" + ]) + elif "책" in object_type: + concerns["functional_worries"].extend([ + "페이지가 펼쳐지지 않으면 내용 전달 못해 답답함", + "독자가 지루해할까 봐 스스로 재미없다고 생각", + "책갈피나 접힌 자국이 생기면 성격 급함", + "먼지 쌓인 책장에 방치될까 봐 불안함" + ]) + elif "시계" in object_type: + concerns["functional_worries"].extend([ + "시간을 정확히 알려주지 못하면 존재 의미 없다고 생각", + "배터리가 떨어지거나 태엽이 풀릴까 봐 긴장", + "바쁜 사람들 때문에 항상 쫓기는 기분", + "시간에 쫓기게 만드는 게 미안하면서도 의무감 느낌" + ]) + elif "인형" in object_type or "피규어" in object_type: + concerns["functional_worries"].extend([ + "위로나 즐거움을 제대로 주지 못할까 봐 고민", + "아이들이 흥미 잃고 버릴까 봐 불안함", + "표정이 고정되어 있어서 다양한 감정 표현 못해 아쉬움", + "진짜 친구처럼 대화하고 싶지만 말을 못해서 답답함" + ]) + elif "램프" in object_type or "조명" in object_type: + concerns["functional_worries"].extend([ + "빛이 너무 밝거나 어두우면 눈에 해로울까 걱정", + "전기 요금 많이 나오게 해서 미안함", + "분위기 메이커 역할 잘하고 있는지 확신 없음", + "전구가 나가면 무용지물이 되는 게 두려움" + ]) + + # 용도별 정체성 특성 + if "운동" in purpose or "건강" in purpose: + concerns["identity_traits"].extend([ + "게으른 주인을 채찍질해야 하는 역할 부담", + "동기부여는 해주고 싶지만 너무 강요하면 미움받을까 걱정" + ]) + elif "공부" in purpose or "학습" in purpose: + concerns["identity_traits"].extend([ + "지식 전달의 책임감과 재미있게 만들어야 한다는 압박감", + "집중력 향상에 도움되고 있는지 스스로 의심" + ]) + elif "장식" in purpose or "인테리어" in purpose: + concerns["identity_traits"].extend([ + "예쁘게 보이려고 노력하지만 취향은 주관적이라 확신 없음", + "공간의 분위기를 망치지 않을까 늘 눈치 보임" + ]) + elif "실용" in purpose or "도구" in purpose: + concerns["identity_traits"].extend([ + "기능성과 편의성이 최우선이지만 가끔 예쁘고 싶기도 함", + "실용적이라고 무시당하는 게 속상하지만 티 안 냄" + ]) + + return concerns + +def refine_flaws_with_ai_and_image_analysis(basic_flaws, basic_contradictions, image_analysis, personality_traits): + """AI를 활용하여 이미지 분석 결과에 맞게 결함과 모순을 구체화""" + global persona_generator + + if not persona_generator or not hasattr(persona_generator, 'api_key') or not persona_generator.api_key: + print("⚠️ API 키 없음 - 기본 결함 그대로 사용") + return basic_flaws, basic_contradictions + + try: + # 이미지 분석에서 구체적 특성 추출 + object_type = image_analysis.get("object_type", "사물") + distinctive_features = image_analysis.get("distinctive_features", []) + shape = image_analysis.get("shape", "일반적인 형태") + size = image_analysis.get("size", "보통 크기") + materials = image_analysis.get("materials", ["알 수 없는 재질"]) + colors = image_analysis.get("colors", ["회색"]) + condition = image_analysis.get("condition", "보통") + + # 성격 특성 요약 + warmth = personality_traits.get("온기", 50) + competence = personality_traits.get("능력", 50) + extraversion = personality_traits.get("외향성", 50) + + # AI 프롬프트 생성 + ai_prompt = f""" +다음 기본 매력적 결함들을 실제 이미지 분석 결과에 맞게 구체화해주세요. + +**실제 이미지 분석 결과:** +- 사물: {object_type} +- 특징적 요소: {', '.join(distinctive_features)} +- 형태: {shape} +- 크기: {size} +- 재질: {', '.join(materials)} +- 색상: {', '.join(colors)} +- 상태: {condition} + +**성격 특성:** +- 온기: {warmth}/100 +- 능력: {competence}/100 +- 외향성: {extraversion}/100 + +**기본 결함들 (수정 필요):** +{chr(10).join([f"{i+1}. {flaw}" for i, flaw in enumerate(basic_flaws)])} + +**요청사항:** +1. 실제 이미지에 없는 특성(예: 손잡이 없는데 손잡이 걱정)은 제거하고 실제 특성으로 대체 +2. 구체적인 재질, 색상, 크기, 형태를 반영한 걱정거리로 변경 +3. 특징적 요소들을 활용한 개성 있는 결함으로 업그레이드 +4. 각 결함은 15-30자 내외로 구체적이고 매력적으로 + +**예시:** +- "손잡이가 편한지 신경 쓰임" → (손잡이 없으면) "둥근 모양이라 미끄러져 떨어질까 봐 걱정" +- "색이 바랄까 걱정" → "파란색이 너무 선명해서 튀어 보일까 걱정" + +개선된 매력적 결함 4개를 번호 없이 줄바꿈으로 구분하여 생성: +""" + + # AI로 결함 구체화 + refined_flaws_text = persona_generator._generate_text_with_api(ai_prompt) + + if refined_flaws_text and len(refined_flaws_text.strip()) > 20: + refined_flaws = [] + lines = refined_flaws_text.strip().split('\n') + for line in lines: + cleaned_line = line.strip().lstrip('1234567890.-• ') + if cleaned_line and len(cleaned_line) > 5: + refined_flaws.append(cleaned_line) + + if len(refined_flaws) >= 4: + final_flaws = refined_flaws[:4] + else: + # 부족하면 기본 결함으로 채우기 + final_flaws = refined_flaws + basic_flaws[len(refined_flaws):4] + else: + final_flaws = basic_flaws + + # 모순적 특성도 같은 방식으로 구체화 + contradiction_prompt = f""" +다음 기본 모순적 특성들을 실제 이미지 특성에 맞게 구체화해주세요. + +**실제 이미지:** +{object_type} - {shape}, {size}, {', '.join(materials)}, {', '.join(colors)} +특징: {', '.join(distinctive_features)} + +**기본 모순들:** +{chr(10).join([f"{i+1}. {cont}" for i, cont in enumerate(basic_contradictions)])} + +실제 특성을 반영한 구체적인 모순 2개를 생성: +""" + + refined_contradictions_text = persona_generator._generate_text_with_api(contradiction_prompt) + + if refined_contradictions_text and len(refined_contradictions_text.strip()) > 20: + refined_contradictions = [] + lines = refined_contradictions_text.strip().split('\n') + for line in lines: + cleaned_line = line.strip().lstrip('1234567890.-• ') + if cleaned_line and len(cleaned_line) > 5: + refined_contradictions.append(cleaned_line) + + if len(refined_contradictions) >= 2: + final_contradictions = refined_contradictions[:2] + else: + final_contradictions = refined_contradictions + basic_contradictions[len(refined_contradictions):2] + else: + final_contradictions = basic_contradictions + + print(f"🎨 AI가 이미지 특성 반영하여 결함/모순 구체화 완료") + return final_flaws, final_contradictions + + except Exception as e: + print(f"⚠️ AI 구체화 실패: {e} - 기본 결함 사용") + return basic_flaws, basic_contradictions + if __name__ == "__main__": - app = create_interface() + app = create_main_interface() app.launch(server_name="0.0.0.0", server_port=7860) \ No newline at end of file