import os import json import time import gradio as gr import google.generativeai as genai from PIL import Image from dotenv import load_dotenv import matplotlib.pyplot as plt import numpy as np import base64 import io import uuid from datetime import datetime import PIL.ImageDraw import random import copy # Import modules from modules.persona_generator import PersonaGenerator from modules.data_manager import save_persona, load_persona, list_personas, toggle_frontend_backend_view # Import local modules from temp.frontend_view import create_frontend_view_html from temp.backend_view import create_backend_view_html from temp.view_functions import ( plot_humor_matrix, generate_personality_chart, save_current_persona, refine_persona, get_personas_list, load_selected_persona, update_current_persona_info, get_personality_variables_df, get_attractive_flaws_df, get_contradictions_df, 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="이미지 분석 중...") 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="이미지를 업로드해주세요.") # 1단계: 영혼 발견하기 (이미지 분석 시작) progress(0.1, desc="영혼 발견 중...") awakening_html = f"""

✨ 영혼 발견 중...

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

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

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

✨ 영혼이 깨어나는 중

127개 성격 변수 분석 중

🧠 개성 찾는 중... 68%

💭 기억 복원 중... 73%

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

💬 말투 형성 중... 64%

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

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

🎉 영혼이 깨어났어요!

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

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

""" yield awakening_html # 페르소나 생성 과정은 이어서 진행 return None, gr.update(visible=False) # Load environment variables load_dotenv() # Configure Gemini API api_key = os.getenv("GEMINI_API_KEY") if api_key: genai.configure(api_key=api_key) # Create data directories if they don't exist os.makedirs("data/personas", exist_ok=True) os.makedirs("data/conversations", exist_ok=True) # Initialize the persona generator persona_generator = PersonaGenerator() # Gradio theme theme = gr.themes.Soft( primary_hue="indigo", secondary_hue="blue", ) # CSS for additional 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; /* 다크모드 대응 - 어두운 배경에서 텍스트 잘 보이게 */ } .awakening-container { border: 1px solid #e0e0e0; border-radius: 12px; padding: 20px; background-color: #f9f9ff; margin: 15px 0; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.05); } .awakening-progress { height: 8px; background-color: #e8e8e8; border-radius: 4px; margin: 20px 0; overflow: hidden; } .awakening-progress-bar { height: 100%; background: linear-gradient(90deg, #6366f1, #a855f7); border-radius: 4px; transition: width 0.5s ease-in-out; } /* 대화 버블 스타일 */ .chatbot-container { max-width: 800px; margin: 0 auto; } .message-bubble { border-radius: 18px; padding: 12px 16px; margin: 8px 0; max-width: 70%; } .user-message { background-color: #e9f5ff; margin-left: auto; } .persona-message { background-color: #f1f1f1; margin-right: auto; } """ # 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_열정성": "열정적이고 활기찬 태도" } # 영혼 깨우기 단계별 UI를 보여주는 함수 def show_awakening_progress(image, user_inputs, progress=gr.Progress()): """영혼 깨우기 과정을 단계별로 보여주는 UI 함수""" if image is None: return None, gr.update(visible=True, value="이미지를 업로드해주세요.") # 1단계: 영혼 발견하기 (이미지 분석 시작) progress(0.1, desc="영혼 발견 중...") awakening_html = f"""

✨ 영혼 발견 중...

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

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

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

✨ 영혼이 깨어나는 중

127개 성격 변수 분석 중

🧠 개성 찾는 중... 68%

💭 기억 복원 중... 73%

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

💬 말투 형성 중... 64%

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

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

🎉 영혼이 깨어났어요!

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

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

""" yield awakening_html # 페르소나 생성 과정은 이어서 진행 return None, gr.update(visible=False) # 성격 상세 정보 탭에서 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="매력적 결함" ) with gr.Column(): gr.Markdown("### 모순적 특성") contradictions = gr.Dataframe( headers=["모순", "효과"], datatype=["str", "str"], label="모순적 특성" ) 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 # 유머 매트릭스 시각화 함수 추가 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) # 삼각형 그리기 triangle = RegularPolygon((0, 0), 3, radius=1, orientation=0, edgecolor='gray', facecolor='none') ax.add_patch(triangle) # 축 라벨 위치 계산 angle = np.linspace(0, 2*np.pi, 3, endpoint=False) x = 1.1 * np.cos(angle) y = 1.1 * np.sin(angle) # 축 라벨 추가 labels = ['따뜻함', '자기참조', '표현적'] opposite_labels = ['재치', '관찰형', '은은함'] 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') # 내부 가이드라인 그리기 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) # 포인트 계산 # 삼각좌표계 변환 (barycentric coordinates) # 각 차원의 값을 삼각형 내부의 점으로 변환 tx = x[0] * warmth + x[1] * self_ref + x[2] * expressive ty = y[0] * warmth + y[1] * self_ref + y[2] * expressive # 포인트 그리기 ax.scatter(tx, ty, s=150, color='red', zorder=5) # 축 제거 ax.axis('off') # 제목 추가 plt.title('유머 스타일 매트릭스', fontsize=14) return fig # Main Gradio app with gr.Blocks(title="놈팽쓰 테스트 앱", theme=theme, css=css) as app: # Global state current_persona = gr.State(None) conversation_history = gr.State([]) analysis_result_state = gr.State(None) personas_data = gr.State([]) current_view = gr.State("frontend") # View 상태 추가 gr.Markdown( """ # 🎭 놈팽쓰(MemoryTag) 테스트 앱 사물에 영혼을 불어넣어 대화할 수 있는 페르소나 생성 테스트 앱입니다. ## 사용 방법 1. **영혼 깨우기** 탭에서 이미지를 업로드하거나 이름을 입력하여 사물의 영혼을 깨웁니다. 2. **대화하기** 탭에서 생성된 페르소나와 대화합니다. 3. **페르소나 관리** 탭에서 저장된 페르소나를 관리합니다. """ ) with gr.Tabs() as tabs: # Tab 1: Soul Awakening with gr.Tab("영혼 깨우기"): with gr.Row(): with gr.Column(scale=1): gr.Markdown("### 🎭 사물 영혼 깨우기") # Image upload input_image = gr.Image(type="filepath", label="사물 이미지 업로드") # 사용자 입력 (위치, 함께한 시간, 사물명) with gr.Group(): gr.Markdown("### 사물 정보 입력") user_input_name = gr.Textbox(label="사물 이름", placeholder="(선택) 이름을 지정하세요") user_input_location = gr.Textbox(label="위치", placeholder="이 사물은 어디에 있나요?") user_input_time = gr.Textbox(label="함께한 시간", placeholder="얼마나 함께했나요?") user_input_type = gr.Textbox(label="사물 종류", placeholder="무슨 종류의 사물인가요?") # Create button create_button = gr.Button("영혼 깨우기", variant="primary") # Error message error_message = gr.Markdown("", visible=False) with gr.Column(scale=1): # 영혼 깨우기 진행 과정 awakening_progress_html = gr.HTML("사물의 영혼을 깨워주세요.") # 프론트/백 뷰 토글 버튼 with gr.Group(visible=False) as view_toggle_group: gr.Markdown("### 페르소나 정보 보기") with gr.Row(): frontend_button = gr.Button("프론트엔드 뷰", variant="primary") backend_button = gr.Button("백엔드 뷰", variant="secondary") # 페르소나 뷰 persona_view = gr.HTML("페르소나가 생성되면 여기에 표시됩니다.") # 성격 차트 personality_chart = gr.Image(label="성격 차트", visible=False) # 영혼 깨우기 후 버튼 행 with gr.Row(visible=False) as post_awakening_buttons: chat_start_button = gr.Button("이 친구와 대화하기", variant="primary") save_persona_button = gr.Button("이 친구 저장하기") refine_button = gr.Button("성격 미세조정") # 저장 결과 메시지 save_result_message = gr.Markdown("", visible=False) # 성격 미세조정 패널 with gr.Group(visible=False) as refine_panel: gr.Markdown("### 💫 친구 성향 미세조정") with gr.Row(): with gr.Column(): warmth_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="🌟 온기", info="차분함 ↔ 따뜻함") competence_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="💪 능력", info="직관적 ↔ 논리적") creativity_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="🎨 창의성", info="실용적 ↔ 창의적") with gr.Column(): extraversion_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="🗣️ 외향성", info="내향적 ↔ 외향적") humor_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="😄 유머감각", info="진지함 ↔ 유머러스") trust_slider = gr.Slider(minimum=0, maximum=100, value=50, step=1, label="🤝 신뢰성", info="유연함 ↔ 신뢰감") with gr.Row(): gr.Markdown("### 😄 유머 스타일 선택") humor_style = gr.Radio( ["위트있는 재치꾼", "따뜻한 유머러스", "날카로운 관찰자", "자기 비하적"], label="유머 스타일", value="따뜻한 유머러스" ) apply_refine_button = gr.Button("이 성향으로 확정", variant="primary") # Tab 2: Chat with gr.Tab("대화하기"): with gr.Row(): with gr.Column(scale=2): # 대화 인터페이스 chatbot = gr.Chatbot(label="대화", height=600) with gr.Row(): chat_input = gr.Textbox(placeholder="사물과 대화해보세요...", show_label=False) chat_button = gr.Button("전송", variant="primary") with gr.Column(scale=1): # 현재 페르소나 요약 gr.Markdown("### 현재 페르소나") current_persona_info = gr.JSON(label="기본 정보") current_persona_traits = gr.JSON(label="성격 특성") gr.Markdown("### 소통 스타일") current_humor_style = gr.Markdown() # 유머 매트릭스 차트 추가 humor_chart = gr.Plot(label="유머 스타일 차트", visible=True) gr.Markdown("### 매력적 결함") current_flaws_df = gr.Dataframe( headers=["결함", "효과"], datatype=["str", "str"], label="매력적 결함" ) gr.Markdown("### 모순적 특성") current_contradictions_df = gr.Dataframe( headers=["모순", "효과"], datatype=["str", "str"], label="모순적 특성" ) with gr.Accordion("127개 성격 변수", open=False): current_all_variables_df = gr.Dataframe( headers=["변수명", "점수", "설명"], datatype=["str", "number", "str"], label="성격 변수" ) # Tab 3: Persona Management with gr.Tab("페르소나 관리"): with gr.Row(): refresh_btn = gr.Button("페르소나 목록 새로고침") personas_df = gr.Dataframe( headers=["이름", "유형", "생성일시", "파일명"], datatype=["str", "str", "str", "str"], label="저장된 페르소나 목록", row_count=10 ) with gr.Row(): load_btn = gr.Button("선택한 페르소나 불러오기") load_result = gr.Markdown("") with gr.Row(): with gr.Column(): selected_persona_frontend = gr.HTML("페르소나를 선택해주세요.") with gr.Column(): selected_persona_chart = gr.Image( label="성격 차트" ) with gr.Accordion("백엔드 상세 정보", open=False): selected_persona_backend = gr.HTML("페르소나를 선택해주세요.") # Event handlers # Soul Awakening create_button.click( fn=show_awakening_progress, inputs=[input_image, gr.State({ "name": lambda: user_input_name.value, "location": lambda: user_input_location.value, "time_spent": lambda: user_input_time.value, "object_type": lambda: user_input_type.value })], outputs=[awakening_progress_html, error_message] ).then( fn=create_persona_from_image, inputs=[input_image, gr.State({ "name": lambda: user_input_name.value, "location": lambda: user_input_location.value, "time_spent": lambda: user_input_time.value, "object_type": lambda: user_input_type.value })], outputs=[current_persona, error_message, input_image, analysis_result_state, current_persona_info, current_persona_traits, humor_chart, current_flaws_df, current_contradictions_df, current_all_variables_df] ).then( fn=create_frontend_view_html, inputs=[current_persona], outputs=[persona_view] ).then( fn=generate_personality_chart, inputs=[current_persona], outputs=[personality_chart] ).then( fn=lambda: (gr.update(visible=True), gr.update(visible=True), gr.update(visible=True)), outputs=[post_awakening_buttons, view_toggle_group, personality_chart] ) # 프론트/백 뷰 토글 이벤트 frontend_button.click( fn=lambda p: ("frontend", create_frontend_view_html(p), ""), inputs=[current_persona], outputs=[current_view, persona_view, error_message] ) backend_button.click( fn=lambda p: ("backend", create_backend_view_html(p), ""), inputs=[current_persona], outputs=[current_view, persona_view, error_message] ) # 성격 미세조정 패널 표시 refine_button.click( fn=lambda: gr.update(visible=True), outputs=[refine_panel] ) # 성격 미세조정 적용 apply_refine_button.click( fn=lambda p, w, c, cr, e, h, t, hs: refine_persona(p, w, c, cr, e, h, t, hs), inputs=[current_persona, warmth_slider, competence_slider, creativity_slider, extraversion_slider, humor_slider, trust_slider, humor_style], outputs=[current_persona, error_message] ).then( fn=create_frontend_view_html, inputs=[current_persona], outputs=[persona_view] ).then( fn=generate_personality_chart, inputs=[current_persona], outputs=[personality_chart] ).then( fn=lambda: (gr.update(visible=False), gr.update(visible=True)), outputs=[refine_panel, post_awakening_buttons] ) # 대화 탭으로 이동 chat_start_button.click( fn=lambda: gr.Tabs(selected=1), outputs=[tabs] ) # Persona Management refresh_btn.click( fn=get_personas_list, outputs=[personas_df, personas_data] ) load_btn.click( fn=load_selected_persona, inputs=[personas_df, personas_data], outputs=[current_persona, load_result, selected_persona_frontend, selected_persona_backend, selected_persona_chart] ).then( fn=lambda x: x, inputs=[selected_persona_frontend], outputs=[current_persona_info] ) # Initial load of personas list app.load( fn=get_personas_list, outputs=[personas_df, personas_data] ) # 저장 버튼 이벤트 핸들러 추가 save_persona_button.click( fn=save_current_persona, inputs=[current_persona], outputs=[save_result_message] ).then( fn=lambda: gr.update(visible=True), outputs=[save_result_message] ) # 기존 함수 업데이트: 현재 페르소나 정보 표시 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("설명", "") } # 성격 특성 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"] # 카테고리별 평균 계산 summary = {} category_counts = {} for var_name, value in variables.items(): category = var_name[0] if var_name and len(var_name) > 0 else "기타" 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 # 평균 계산 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 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)] 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), "데이터 없음", fill='black') img_path = os.path.join("data", "temp_chart.png") img.save(img_path) return img_path # Get traits traits = persona["성격특성"] # Create radar chart categories = list(traits.keys()) values = list(traits.values()) # 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) # 한글 폰트 설정 - 기본적으로 사용 가능한 폰트를 먼저 시도 # Matplotlib에서 지원하는 한글 폰트 목록 korean_fonts = ['NanumGothic', 'NanumGothicCoding', 'NanumMyeongjo', 'Malgun Gothic', 'Gulim', 'Batang', 'Arial Unicode MS', 'DejaVu Sans'] # 폰트 설정 plt.rcParams['font.family'] = 'sans-serif' # 기본 폰트 패밀리 # 여러 폰트를 시도 font_found = False for font in korean_fonts: try: plt.rcParams['font.sans-serif'] = [font] + plt.rcParams.get('font.sans-serif', []) plt.text(0, 0, '테스트', fontfamily=font) font_found = True print(f"성공적으로 한글 폰트를 설정했습니다: {font}") break except: continue if not font_found: print("한글 지원 폰트를 찾을 수 없습니다. 영문으로 표시합니다.") # 영어 라벨 매핑 english_labels = { "온기": "Warmth", "능력": "Ability", "신뢰성": "Trust", "친화성": "Friendly", "창의성": "Creative", "유머감각": "Humor", "외향성": "Extraversion" } categories = [english_labels.get(cat, cat) for cat in categories] # 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) # 각도 라벨 위치 및 색상 조정 ax.set_rlabel_position(90) ax.tick_params(colors='#6b7280') # Y축 라벨 제거 및 눈금 표시 ax.set_yticklabels([]) ax.set_yticks([20, 40, 60, 80, 100]) # 범위 설정 ax.set_ylim(0, 100) # 차트 그리기 # 1. 채워진 영역 ax.fill(angles, values, alpha=0.25, color='#6366f1') # 2. 테두리 선 ax.plot(angles, values, 'o-', linewidth=2, color='#6366f1') # 3. 데이터 포인트 강조 ax.scatter(angles[:-1], values[:-1], s=100, color='#6366f1', edgecolor='white', zorder=10) # 4. 각 축 설정 ax.set_thetagrids(angles[:-1] * 180/np.pi, categories[:-1], fontsize=12) # 제목 추가 name = persona.get("기본정보", {}).get("이름", "Unknown") plt.title(f"{name} 성격 특성", size=16, color='#374151', pad=20, fontweight='bold') # 저장 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) return img_path def save_current_persona(current_persona): """Save current persona to a JSON file""" if not current_persona: return "저장할 페르소나가 없습니다." try: # 깊은 복사를 통해 원본 데이터를 유지 import copy persona_copy = copy.deepcopy(current_persona) # 저장 불가능한 객체 제거 keys_to_remove = [] for key in persona_copy: if key in ["personality_profile", "humor_matrix", "_state"] or callable(persona_copy[key]): 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) if filepath: name = current_persona.get("기본정보", {}).get("이름", "Unknown") return f"{name} 페르소나가 저장되었습니다: {filepath}" else: return "페르소나 저장에 실패했습니다." except Exception as e: import traceback error_details = traceback.format_exc() print(f"저장 오류 상세: {error_details}") return f"저장 중 오류 발생: {str(e)}" # 이 함수는 파일 상단에서 이미 정의되어 있으므로 여기서는 제거합니다. # 성격 미세조정 함수 def refine_persona(persona, warmth, competence, creativity, extraversion, humor, trust, humor_style): """페르소나의 성격을 미세조정하는 함수""" if not persona: return persona, "페르소나가 없습니다." try: # 복사본 생성 refined_persona = persona.copy() # 성격 특성 업데이트 if "성격특성" in refined_persona: refined_persona["성격특성"]["온기"] = int(warmth) refined_persona["성격특성"]["능력"] = int(competence) refined_persona["성격특성"]["창의성"] = int(creativity) refined_persona["성격특성"]["외향성"] = int(extraversion) refined_persona["성격특성"]["유머감각"] = int(humor) refined_persona["성격특성"]["신뢰성"] = int(trust) # 유머 스타일 업데이트 refined_persona["유머스타일"] = humor_style # 127개 성격 변수가 있으면 업데이트 if "성격변수127" in refined_persona: # 온기 관련 변수 업데이트 for var in ["W01_친절함", "W02_친근함", "W06_공감능력", "W07_포용력"]: if var in refined_persona["성격변수127"]: refined_persona["성격변수127"][var] = int(warmth * 0.9 + random.randint(0, 20)) # 능력 관련 변수 업데이트 for var in ["C01_효율성", "C02_지능", "C05_정확성", "C09_실행력"]: if var in refined_persona["성격변수127"]: refined_persona["성격변수127"][var] = int(competence * 0.9 + random.randint(0, 20)) # 창의성 관련 변수 업데이트 for var in ["C04_창의성", "C08_통찰력"]: if var in refined_persona["성격변수127"]: refined_persona["성격변수127"][var] = int(creativity * 0.9 + random.randint(0, 20)) # 외향성 관련 변수 업데이트 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)) # 유머 관련 변수 업데이트 if "H01_유머감각" in refined_persona["성격변수127"]: refined_persona["성격변수127"]["H01_유머감각"] = int(humor * 0.9 + random.randint(0, 20)) # 신뢰성 관련 변수 업데이트 if "W04_신뢰성" in refined_persona["성격변수127"]: refined_persona["성격변수127"]["W04_신뢰성"] = int(trust * 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, "성격이 성공적으로 미세조정되었습니다." except Exception as e: import traceback error_details = traceback.format_exc() print(f"성격 미세조정 오류: {error_details}") return persona, f"성격 미세조정 중 오류가 발생했습니다: {str(e)}" 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}
  • " # 소통 방식 communication_style = persona.get("소통방식", "") # 유머 스타일 humor_style = persona.get("유머스타일", "") # 전체 HTML 스타일과 내용 html = f"""

    {name}

    {object_type} - {description}

    {summary_html}

    성격 특성

    {traits_html}

    소통 스타일

    {communication_style}

    유머 스타일

    {humor_style}

    매력적 결함

    """ return html def create_backend_view_html(persona): """Create HTML representation of the backend view of the persona""" if not persona: return "
    페르소나가 아직 생성되지 않았습니다.
    " name = persona.get("기본정보", {}).get("이름", "Unknown") # 백엔드 기본 정보 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) } 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 위트
    위트
    따뜻함
    자기참조 vs 관찰형
    관찰형
    자기참조
    미묘함 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}
    """ # 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}
    {prompt_html}

    전체 백엔드 데이터

    {json_preview}
    """ return html def get_personas_list(): """Get list of personas for the dataframe""" personas = list_personas() # Convert to dataframe format df_data = [] for i, persona in enumerate(personas): df_data.append([ persona["name"], persona["type"], persona["created_at"], persona["filename"] ]) return df_data, personas 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 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"] # Load persona persona = load_persona(filepath) if not persona: return None, "페르소나 로딩에 실패했습니다.", None, None, None # 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) # Generate personality chart chart_image_path = generate_personality_chart(frontend_view) return persona, f"{persona['기본정보']['이름']}을(를) 로드했습니다.", frontend_html, backend_html, chart_image_path except Exception as e: return None, f"페르소나 로딩 중 오류 발생: {str(e)}", None, None, None # 페르소나와 대화하는 함수 추가 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, "" if not persona: chat_history.append((user_message, "페르소나가 로드되지 않았습니다. 먼저 페르소나를 생성하거나 불러오세요.")) return chat_history, "" try: # 페르소나 생성기에서 대화 기능 호출 conversation_history = [(msg[0], msg[1]) for msg in chat_history] # 페르소나 생성기에서 대화 함수 호출 response = persona_generator.chat_with_persona(persona, user_message, conversation_history) # 대화 기록에 추가 chat_history.append((user_message, response)) # 현재 시간에 대화 저장 (구현 여부에 따라 주석 처리) # save_conversation({ # "persona_id": persona.get("id", "unknown"), # "persona_name": persona.get("name", "Unknown Persona"), # "timestamp": datetime.now().isoformat(), # "user_message": user_message, # "persona_response": response # }) 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 __name__ == "__main__": app.launch(server_name="0.0.0.0", share=False)