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 = "성격 요약
" + "".join([f"- {item}
" for item in summary_items]) + "
"
# Personality traits
traits_html = ""
for trait, value in persona.get("성격특성", {}).items():
traits_html += f"""
"""
# 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"""
{summary_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"""
"""
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"""
"""
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"""
"""
# 4. 매력적 결함과 모순적 특성
flaws_html = ""
contradictions_html = ""
flaws = persona.get("매력적결함", [])
if flaws:
flaws_html = ""
for flaw in flaws:
flaws_html += f"- {flaw}
"
flaws_html += "
"
contradictions = persona.get("모순적특성", [])
if contradictions:
contradictions_html = ""
for contradiction in contradictions:
contradictions_html += f"- {contradiction}
"
contradictions_html += "
"
# 6. 프롬프트 템플릿 (있는 경우)
prompt_html = ""
if "프롬프트" in persona:
prompt_text = persona.get("프롬프트", "")
prompt_html = f"""
"""
# 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"""
성격 요약 (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)