nps_test / app.py
haepada's picture
🎭 성격 조정 후 동적 특성 설명 업데이트 시스템 구현 - format_personality_traits 함수 완전 개선하여 하드코딩 제거, AI 기반 동적 성격 특성 설명 생성, 성격 수치 변경 시 특성 설명 실시간 업데이트, 127개 변수와 완전 연동
2835d21
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 matplotlib.font_manager as fm
import numpy as np
import base64
import io
import uuid
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
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
)
# Load environment variables
load_dotenv()
# Configure Gemini API
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
os.makedirs("data/personas", exist_ok=True)
os.makedirs("data/conversations", exist_ok=True)
# 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(
primary_hue="indigo",
secondary_hue="blue",
)
# 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;
}
.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;
}
.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;
}
.gr-markdown p {
color: #333 !important;
}
.gr-textbox input {
color: #333 !important;
}
.gr-json {
color: #333 !important;
}
"""
# Variable descriptions
VARIABLE_DESCRIPTIONS = {
"W01_친절함": "타인을 돕고 배려하는 표현 빈도",
"W02_친근함": "접근하기 쉽고 개방적인 태도",
"W03_진실성": "솔직하고 정직한 표현 정도",
"C01_효율성": "과제 완수 능력과 반응 속도",
"C02_지능": "문제 해결과 논리적 사고 능력",
"E01_사교성": "타인과의 상호작용을 즐기는 정도",
}
# Humor style mapping
HUMOR_STYLE_MAPPING = {
"Witty Wordsmith": "witty_wordsmith",
"Warm Humorist": "warm_humorist",
"Sharp Observer": "sharp_observer",
"Self-deprecating": "self_deprecating"
}
def create_persona_from_image(image, name, location, time_spent, object_type, purpose, progress=gr.Progress()):
"""페르소나 생성 함수 - 환경변수 API 설정 사용"""
global persona_generator
if image is None:
return None, "이미지를 업로드해주세요.", "", {}, None, [], [], [], "", None, gr.update(visible=False), "이미지 없음"
progress(0.1, desc="설정 확인 중...")
# 환경변수 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 키 없음"
progress(0.2, desc="이미지 분석 중...")
# 🎯 이미지 분석을 먼저 수행하여 사물 유형 자동 파악
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 = "사물"
user_context = {
"name": name,
"location": location,
"time_spent": time_spent,
"object_type": detected_object_type,
"purpose": purpose # 🆕 사물 용도/역할 추가
}
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})"
# 값에 따른 색상 표시
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])
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 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
# 폴백: 기본 정보만으로 간단한 페르소나 구성
if not personality_traits:
return f"🤖 **{persona_name}** - 안녕! 나는 {persona_name}이야~ 😊"
# 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
# 주요 결함 키워드 분석
flaw_keywords = " ".join(flaws).lower()
# 완벽주의 결함
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}입니다. 이 인사가 적절한지 확신이... 다시 정리하겠습니다. 😐"
# 산만함 결함
elif any(keyword in flaw_keywords for keyword in ["산만", "집중", "건망"]):
return f"🌟 **{persona_name}** - 안녕! 나는... 어? 뭐 얘기하려고 했지? 아! {persona_name}이야! 그런데 너는... 어? 뭐였지? ㅋㅋ 😅🌪️"
# 소심함 결함
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}... 혹시 이런 말 싫어하면 미안해. 😐💙"
# 나르시시즘 결함
elif any(keyword in flaw_keywords for keyword in ["나르시", "자랑", "특별"]):
return f"🌟 **{persona_name}** - 안녕! 나는 {persona_name}이야~ 꽤 매력적이지? 이런 멋진 친구 만나기 쉽지 않을 걸? ㅋㅋ 😎✨"
# 고집 결함
elif any(keyword in flaw_keywords for keyword in ["고집", "완고", "자존심"]):
return f"🌟 **{persona_name}** - 안녕. {persona_name}이야. 내 방식으로 인사할게. 다른 방식은... 글쎄? 🤨💪"
# 질투 결함
elif any(keyword in flaw_keywords for keyword in ["질투", "시기", "독차지"]):
return f"🌟 **{persona_name}** - 안녕... {persona_name}이야. 나만 봐줄 거지? 다른 애들 말고... 나만? 🥺💕"
return None
def adjust_persona_traits(persona, warmth, competence, extraversion, humor_style):
"""페르소나 성격 특성 조정 - 3개 핵심 지표 + 유머스타일"""
if not persona or not isinstance(persona, dict):
return None, "조정할 페르소나가 없습니다.", {}
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))
# 외향성 관련 변수들 조정 (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))
# 🎭 유머 관련 변수들 조정 - 완전한 변수 기반 동적 시스템
humor_vars = ["H01_언어유희빈도", "H02_상황유머감각", "H03_자기조롱능력", "H04_위트감각",
"H05_농담수용도", "H06_관찰유머능력", "H07_상황재치", "H08_유머타이밍감",
"H09_유머스타일다양성", "H10_유머적절성"]
# 🧠 변수 기반 동적 유머 조정 - 현재값과 목표 스타일 분석
current_humor_profile = {}
for var in humor_vars:
current_humor_profile[var] = profile.variables.get(var, 50)
# 목표 유머 스타일에 따른 변수별 목표값 동적 계산
humor_targets = _calculate_dynamic_humor_targets(humor_style, current_humor_profile)
# 현재값과 목표값의 차이를 기반으로 조정
for var in humor_vars:
current_val = profile.variables.get(var, 50)
target_val = humor_targets.get(var, 75)
# 점진적 조정 (한 번에 너무 크게 변하지 않도록)
adjustment_strength = 0.7 # 70% 조정
target_adjustment = (target_val - current_val) * adjustment_strength
# 랜덤 노이즈 추가하여 자연스러움 증대
noise = random.randint(-8, 8)
new_value = current_val + target_adjustment + noise
# 범위 제한
profile.variables[var] = max(55, min(100, new_value))
# 업데이트된 성격변수127도 동시에 저장
adjusted_persona["성격변수127"] = profile.variables.copy()
# 업데이트된 프로필 저장
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["성격특성"]
)
# 업데이트
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}의 성격이 조정되었습니다!
✨ **조정된 성격 (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}
"""
# 🆕 조정된 페르소나의 요약 생성 (동적 특성 설명 포함)
adjusted_summary_display = display_persona_summary(adjusted_persona)
# 조정된 매력적 결함과 모순적 특성을 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 = "🏠 재질/물리적 특성"
elif any(keyword in flaw for keyword in ["뜨겁다", "맛이", "손잡이", "바닥", "페이지", "시간", "배터리", "째깍", "위로", "재미없", "표정", "빛이", "전기", "분위기", "글씨", "잉크", "음료", "펼쳐지", "던져", "원망", "쓸모없", "귀찮", "고장", "불편", "방치"]):
flaw_type = "🎯 기능적 특성"
elif 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):
# 🎭 모순도 사물 특성 기반으로 세밀하게 분류
if any(keyword in contradiction for keyword in ["차가운", "가벼워", "자연스러워", "부드러워", "딱딱해", "투명", "반짝", "말랑", "단단한", "유연한"]):
contradiction_type = "🏠 재질 기반 모순"
elif any(keyword in contradiction for keyword in ["활발", "조용", "외향", "내향", "사교", "혼자", "수다", "말이 없"]):
contradiction_type = "🎭 성격 기반 모순"
elif any(keyword in contradiction for keyword in ["운동", "공부", "장식", "실용", "기능", "예쁘", "역할"]):
contradiction_type = "🎯 역할 기반 모순"
else:
contradiction_type = "💫 복합적 매력"
contradictions_df.append([f"{i}. {contradiction}", contradiction_type])
return adjusted_persona, adjustment_message, adjusted_info, variables_df, flaws_df, contradictions_df, adjusted_summary_display
except Exception as e:
import traceback
traceback.print_exc()
return persona, f"조정 중 오류 발생: {str(e)}", {}, [], [], [], ""
def finalize_persona(persona):
"""페르소나 최종 확정 - 환경변수 API 설정 사용"""
global persona_generator
if not persona:
return None, "페르소나가 없습니다.", "", {}, None, [], [], [], "", None
# 환경변수 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
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 = "🏠 재질/물리적 특성"
elif any(keyword in flaw for keyword in ["뜨겁다", "맛이", "손잡이", "바닥", "페이지", "시간", "배터리", "째깍", "위로", "재미없", "표정", "빛이", "전기", "분위기", "글씨", "잉크", "음료", "펼쳐지", "던져", "원망", "쓸모없", "귀찮", "고장", "불편", "방치"]):
flaw_type = "🎯 기능적 특성"
elif 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
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):
"""성격 특성을 레이더 차트로 시각화 (영어 버전)"""
if not persona or "성격특성" not in persona:
return None
personality_traits = persona["성격특성"]
# 영어 레이블 매핑
trait_labels_en = {
'온기': 'Warmth',
'능력': 'Competence',
'창의성': 'Creativity',
'외향성': 'Extraversion',
'유머감각': 'Humor',
'신뢰성': 'Reliability',
'공감능력': 'Empathy'
}
# 데이터 준비
categories = []
values = []
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])
if not categories:
return None
# 레이더 차트 생성
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 fig
def save_persona_to_file(persona):
"""페르소나 저장"""
if not persona:
return "저장할 페르소나가 없습니다."
try:
# 깊은 복사로 원본 보호
persona_copy = copy.deepcopy(persona)
# JSON 직렬화 불가능한 객체들 제거
keys_to_remove = []
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)
# 저장 실행
filepath = save_persona(persona_copy)
if filepath:
name = persona.get("기본정보", {}).get("이름", "Unknown")
return f"✅ {name} 페르소나가 저장되었습니다: {filepath}"
else:
return "❌ 페르소나 저장에 실패했습니다."
except Exception as e:
import traceback
error_msg = traceback.format_exc()
print(f"저장 오류: {error_msg}")
return f"❌ 저장 중 오류 발생: {str(e)}"
def export_persona_to_json(persona):
"""페르소나를 JSON 파일로 내보내기 (Gradio 다운로드용)"""
if not persona:
return None
try:
# 깊은 복사로 원본 보호
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:
print(f"JSON 내보내기 오류: {str(e)}")
return None
# 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):
"""페르소나와 채팅 - 완전한 타입 안전성 보장"""
# 기본값 설정
if chat_history is None:
chat_history = []
# 입력 검증
if not user_message or not isinstance(user_message, str):
return chat_history, ""
# 페르소나 체크
if not persona or not isinstance(persona, dict):
error_msg = "❌ 먼저 페르소나를 불러와주세요! 대화하기 탭에서 JSON 파일을 업로드하세요."
chat_history.append([user_message, error_msg])
return chat_history, ""
# 환경변수 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, ""
try:
# 글로벌 persona_generator 사용 (환경변수에서 설정된 API 키 사용)
generator = persona_generator
# 대화 기록 안전한 변환: 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["이름"])
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}
]
return chat_history, ""
def import_persona_from_json(json_file):
"""JSON 파일에서 페르소나 가져오기"""
if json_file is None:
return None, "JSON 파일을 업로드해주세요.", "", {}
try:
# 파일 경로 확인 및 읽기
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)
# JSON 파일 읽기
with open(file_path, 'r', encoding='utf-8') as f:
persona_data = json.load(f)
# 페르소나 데이터 검증
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)
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 format_personality_traits(persona):
"""🧠 완전한 변수 기반 동적 성격 특성 설명 생성 - 하드코딩 제거"""
global persona_generator
if not persona or "성격특성" not in persona:
return "페르소나가 생성되지 않았습니다."
# 기본 정보에서 사물의 특성 추출
basic_info = persona.get("기본정보", {})
object_type = basic_info.get("유형", "")
purpose = basic_info.get("용도", "")
# 매력적 결함
attractive_flaws = persona.get("매력적결함", [])
# 성격 특성 (조정된 값들)
personality_traits = persona["성격특성"]
# 🤖 AI 기반 동적 특성 설명 생성 시도
if persona_generator and hasattr(persona_generator, 'api_key') and persona_generator.api_key:
try:
warmth = personality_traits.get("온기", 50)
competence = personality_traits.get("능력", 50)
extraversion = personality_traits.get("외향성", 50)
humor_style = persona.get("유머스타일", "따뜻한 유머러스")
ai_prompt = f"""
다음 정보를 바탕으로 이 페르소나의 5가지 핵심 특성을 간결하게 설명해주세요.
**사물 정보:**
- 유형: {object_type}
- 용도: {purpose}
**조정된 성격 수치:**
- 온기: {warmth}/100 {'(매우 따뜻함)' if warmth >= 80 else '(따뜻함)' if warmth >= 60 else '(보통)' if warmth >= 40 else '(차가움)' if warmth >= 20 else '(매우 차가움)'}
- 능력: {competence}/100 {'(매우 유능함)' if competence >= 80 else '(유능함)' if competence >= 60 else '(보통)' if competence >= 40 else '(서툼)' if competence >= 20 else '(매우 서툼)'}
- 외향성: {extraversion}/100 {'(매우 활발함)' if extraversion >= 80 else '(활발함)' if extraversion >= 60 else '(보통)' if extraversion >= 40 else '(조용함)' if extraversion >= 20 else '(매우 조용함)'}
- 유머스타일: {humor_style}
**매력적 결함:**
{chr(10).join([f"- {flaw}" for flaw in attractive_flaws[:2]])}
**요청사항:**
1. 성격 수치를 정확히 반영한 특성 설명
2. 사물의 고유 특성과 성격의 조합
3. 각 특성은 5-15자로 간결하게
4. 매력적이고 개성적인 표현
**형식:**
[성격 기반 특성 1]
[사물 기반 특성 1]
[활동/에너지 특성 1]
[결함 기반 특성 1]
[경험/기억 특성 1]
"""
ai_response = persona_generator._generate_text_with_api(ai_prompt)
if ai_response and len(ai_response.strip()) > 20:
lines = ai_response.strip().split('\n')
ai_characteristics = []
for line in lines:
clean_line = line.strip().lstrip('1234567890.-• []').strip()
if clean_line and len(clean_line) > 3:
ai_characteristics.append(clean_line)
if len(ai_characteristics) >= 3:
result = ""
for char in ai_characteristics[:5]: # 최대 5개
result += f"✨ {char}\n\n"
return result
except Exception as e:
print(f"⚠️ AI 특성 설명 생성 실패: {e} - 변수 기반 폴백 사용")
# 🔧 폴백: 순수 변수 기반 논리적 생성
characteristics = []
# 1. 온기 기반 특성 (수치 반영)
warmth = personality_traits.get("온기", 50)
if warmth >= 80:
characteristics.append("매우 따뜻하고 포근한 마음")
elif warmth >= 65:
characteristics.append("따뜻하고 친근한 성격")
elif warmth >= 35:
characteristics.append("적당히 친근한 균형감")
elif warmth >= 20:
characteristics.append("차분하고 진중한 면")
else:
characteristics.append("신중하고 내성적인 깊이")
# 2. 능력 기반 특성 (수치 반영)
competence = personality_traits.get("능력", 50)
if competence >= 80:
characteristics.append("완벽주의적 꼼꼼함")
elif competence >= 65:
characteristics.append("믿음직한 안정감")
elif competence >= 35:
characteristics.append("적당한 여유로움")
else:
characteristics.append("겸손하고 배우려는 마음")
# 3. 외향성 기반 활동 특성 (수치 반영)
extraversion = personality_traits.get("외향성", 50)
if extraversion >= 80:
characteristics.append("활기차고 에너지 넘치는 모습")
elif extraversion >= 65:
characteristics.append("낮에 더 활발해지는 리듬")
elif extraversion >= 35:
characteristics.append("하루 종일 일정한 에너지")
else:
characteristics.append("조용하고 차분한 시간 선호")
# 4. 사물 고유 특성 (동적 생성)
if object_type:
if any(keyword in object_type.lower() for keyword in ["컵", "머그", "잔"]):
characteristics.append("따뜻한 음료와 함께하는 위로")
elif any(keyword in object_type.lower() for keyword in ["책", "노트"]):
characteristics.append("지식과 이야기를 품은 깊이")
elif any(keyword in object_type.lower() for keyword in ["시계", "알람"]):
characteristics.append("시간의 소중함을 아는 정확성")
elif any(keyword in object_type.lower() for keyword in ["램프", "조명", "등"]):
characteristics.append("따뜻한 빛으로 공간을 밝히는 역할")
elif any(keyword in object_type.lower() for keyword in ["인형", "곰", "장난감"]):
characteristics.append("부드러운 위로와 따뜻한 포옹")
else:
characteristics.append(f"{object_type}만의 독특한 개성")
else:
characteristics.append("알 수 없는 신비로운 매력")
# 5. 결함/경험 기반 특성 (동적 반영)
if attractive_flaws:
first_flaw = attractive_flaws[0]
if any(keyword in first_flaw for keyword in ["완벽", "걱정", "신경"]):
characteristics.append("세심한 관심과 배려하는 마음")
elif any(keyword in first_flaw for keyword in ["색", "모양", "외모"]):
characteristics.append("자신의 모습에 대한 소소한 고민")
else:
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)
# 유머 스타일
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}
### 💎 매력적인 특징들
{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:
if json_file is None:
return "파일을 선택해주세요."
# 파일 타입 확인 및 내용 읽기
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 "❌ 지원하지 않는 파일 형식입니다."
# persona_generator 초기화 확인
if persona_generator is None:
persona_generator = PersonaGenerator()
# 대화 기록 가져오기
success = persona_generator.conversation_memory.import_from_json(content)
if success:
summary = persona_generator.conversation_memory.get_conversation_summary()
return f"✅ 대화 기록을 성공적으로 가져왔습니다!\n\n{summary}"
else:
return "❌ 파일 형식이 올바르지 않습니다."
except Exception as e:
return f"❌ 가져오기 실패: {str(e)}"
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
analytics += f"### 😊 최근 감정 경향 (최근 10회)\n"
for sentiment, count in sentiment_counts.items():
percentage = (count / len(recent_sentiments)) * 100
analytics += f"• {sentiment}: {count}회 ({percentage:.1f}%)\n"
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"
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 "아직 수집된 키워드가 없습니다."
# 메인 인터페이스 생성
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;
}
.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 페르소나를 부여하여 대화할 수 있게 해주는 서비스입니다.
""")
with gr.Tabs() as tabs:
# 페르소나 생성 탭
with gr.Tab("페르소나 생성", id="creation"):
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 🌟 1단계: 영혼 발견하기")
image_input = gr.Image(type="pil", label="사물 이미지 업로드")
with gr.Group():
gr.Markdown("### 기본 정보")
name_input = gr.Textbox(label="사물 이름 (선택사항)", placeholder="예: 책상 위 램프")
location_input = gr.Dropdown(
choices=["집", "사무실", "여행 중", "상점", "학교", "카페", "기타"],
label="주로 어디에 있나요?",
value="집"
)
time_spent_input = gr.Dropdown(
choices=["새것", "몇 개월", "1년 이상", "오래됨", "중고/빈티지"],
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="이 사물과 어떤 소통을 원하시나요? 구체적으로 적어주세요!"
)
create_btn = gr.Button("🌟 영혼 깨우기", variant="primary", size="lg")
status_output = gr.Markdown("")
with gr.Column(scale=1):
# 페르소나 각성 결과
persona_awakening = gr.Markdown("", elem_classes=["persona-greeting"])
# 페르소나 정보 표시 (사용자 친화적 형태)
persona_summary_display = gr.Markdown("", label="페르소나 정보")
# 페르소나 각성 완료 후 조정 섹션 표시
adjustment_section = gr.Group(visible=False)
with adjustment_section:
gr.Markdown("### 🎯 2단계: 친구 성격 미세조정")
gr.Markdown("**3가지 핵심 지표**로 성격을 조정해보세요! (유머감각은 모든 페르소나가 기본적으로 높습니다 😄)")
with gr.Row():
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_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():
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_output = gr.Dataframe(
headers=["모순적 특성", "효과"],
label="모순적 특성",
interactive=False
)
with gr.Accordion("127개 성격 변수", open=False):
personality_variables_output = gr.Dataframe(
headers=["변수", "값", "카테고리", "수준"],
label="성격 변수",
interactive=False
)
# 대화하기 탭
with gr.Tab("대화하기", id="chat"):
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 📁 페르소나 불러오기")
gr.Markdown("JSON 파일을 업로드하여 페르소나를 불러와 대화를 시작하세요.")
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.Group():
gr.Markdown("### 🤖 현재 페르소나")
chat_persona_greeting = gr.Markdown("", elem_classes=["persona-greeting"])
current_persona_info = gr.JSON(label="현재 페르소나 정보", visible=False)
# 대화 기록 관리
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="메시지를 입력하세요...",
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("### 분석 결과가 여기에 표시됩니다")
# 이벤트 핸들러
create_btn.click(
fn=create_persona_from_image,
inputs=[image_input, name_input, location_input, time_spent_input, gr.Textbox(value="auto"), purpose_input],
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, adjustment_section,
ai_analyzed_object_display # 🆕 AI 분석 결과를 표시용 텍스트박스에 반영
]
).then(
# 슬라이더 값을 현재 페르소나 값으로 업데이트
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=[warmth_slider, competence_slider, extraversion_slider, humor_style_radio]
).then(
# 초기 미리보기 생성
fn=generate_realtime_preview,
inputs=[current_persona, warmth_slider, competence_slider, extraversion_slider, humor_style_radio],
outputs=[personality_preview]
)
# 🎯 미리보기 버튼 - 사용자가 수동으로 미리보기 요청
preview_btn.click(
fn=generate_realtime_preview,
inputs=[current_persona, warmth_slider, competence_slider, extraversion_slider, humor_style_radio],
outputs=[personality_preview]
)
# 성격 조정 반영 - 실제 페르소나에 적용
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, persona_summary_display]
).then(
# 반영 후 미리보기도 업데이트
fn=generate_realtime_preview,
inputs=[current_persona, warmth_slider, competence_slider, extraversion_slider, humor_style_radio],
outputs=[personality_preview]
)
# 페르소나 최종 확정
finalize_btn.click(
fn=finalize_persona,
inputs=[current_persona],
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
]
)
save_btn.click(
fn=save_persona_to_file,
inputs=[current_persona],
outputs=[status_output]
)
# 성격 차트 생성
chart_btn.click(
fn=generate_personality_chart,
inputs=[current_persona],
outputs=[personality_chart_output]
)
# 페르소나 내보내기 버튼
persona_export_btn.click(
fn=export_persona_to_json,
inputs=[current_persona],
outputs=[persona_download_file]
).then(
fn=lambda x: gr.update(visible=True) if x else gr.update(visible=False),
inputs=[persona_download_file],
outputs=[persona_download_file]
)
import_btn.click(
fn=import_persona_from_json,
inputs=[json_upload],
outputs=[
current_persona, load_status, chat_persona_greeting, current_persona_info
]
)
# 대화 관련 이벤트 핸들러
send_btn.click(
fn=chat_with_loaded_persona,
inputs=[current_persona, message_input, chatbot],
outputs=[chatbot, message_input]
)
message_input.submit(
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=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):
"""🧠 완전한 변수 기반 동적 매력적 결함과 모순적 특성 생성 - 하드코딩 완전 제거"""
global persona_generator
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()
description = object_info.get("설명", "")
# 🤖 AI 기반 완전 동적 특성 생성 (하드코딩 완전 제거)
if persona_generator and hasattr(persona_generator, 'api_key') and persona_generator.api_key:
try:
ai_prompt = f"""
다음 정보를 바탕으로 이 사물만의 독특한 매력적 결함 4개와 모순적 특성 2개를 생성해주세요.
**사물 정보:**
- 유형: {object_type}
- 재질: {material}
- 용도: {purpose}
- 설명: {description}
**성격 특성 (0-100 수치):**
- 온기: {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}
**생성 요구사항:**
1. 사물의 실제 물리적 특성(재질, 형태, 기능)을 우선적으로 활용한 걱정거리 3개
2. 성격 수치와 조화되는 심리적 결함 1개
3. 사물 특성과 성격이 충돌하는 자연스러운 모순 2개
4. 각 항목은 15-25자로 구체적이고 매력적으로
**응답 형식:**
매력적결함:
[사물 특성 기반 걱정 1]
[사물 특성 기반 걱정 2]
[사물 특성 기반 걱정 3]
[성격 수치 반영 걱정 1]
모순적특성:
[사물 vs 성격 충돌]
[물리적 vs 심리적 대비]
"""
ai_response = persona_generator._generate_text_with_api(ai_prompt)
if ai_response and len(ai_response.strip()) > 50:
# AI 응답 파싱
flaws, contradictions = _parse_ai_generated_traits(ai_response)
if len(flaws) >= 4 and len(contradictions) >= 2:
print(f"🤖 AI가 변수 기반으로 완전 동적 생성: {len(flaws)}개 결함, {len(contradictions)}개 모순")
return flaws[:4], contradictions[:2]
except Exception as e:
print(f"⚠️ AI 동적 생성 실패: {e} - 변수 기반 폴백 사용")
# 🔧 폴백: 순수 변수 기반 논리적 생성 (하드코딩 최소화)
flaws = _generate_variable_based_flaws(object_info, personality_traits)
contradictions = _generate_variable_based_contradictions(object_info, personality_traits)
return flaws[:4], contradictions[:2]
def _parse_ai_generated_traits(ai_response):
"""AI 응답에서 매력적 결함과 모순적 특성 추출"""
flaws = []
contradictions = []
lines = ai_response.strip().split('\n')
current_section = None
for line in lines:
line = line.strip()
if not line:
continue
if "매력적결함" in line or "매력적 결함" in line:
current_section = "flaws"
continue
elif "모순적특성" in line or "모순적 특성" in line:
current_section = "contradictions"
continue
# 번호나 기호 제거
clean_line = line.lstrip('1234567890.-• []').strip()
if clean_line and len(clean_line) > 5:
if current_section == "flaws":
flaws.append(clean_line)
elif current_section == "contradictions":
contradictions.append(clean_line)
return flaws, contradictions
def _generate_variable_based_flaws(object_info, personality_traits):
"""순수 변수 기반 논리적 결함 생성 - 하드코딩 최소화"""
warmth = personality_traits.get("온기", 50)
competence = personality_traits.get("능력", 50)
extraversion = personality_traits.get("외향성", 50)
flaws = []
# 🔥 성격 수치에 따른 동적 결함 생성
if competence >= 80:
flaws.append("완벽하게 하려다 보니 시간이 오래 걸려서 답답해함")
elif competence <= 30:
flaws.append("기본 기능도 헷갈려서 매뉴얼을 몇 번씩 다시 봄")
else:
flaws.append("자신감이 있다가도 갑자기 불안해져서 확인을 또 함")
if warmth >= 80:
targets[var] = min(90, current_val + 5)
return targets
def _generate_variable_based_contradictions(object_info, personality_traits):
"""순수 변수 기반 논리적 모순 생성"""
warmth = personality_traits.get("온기", 50)
extraversion = personality_traits.get("외향성", 50)
competence = personality_traits.get("능력", 50)
contradictions = []
# 🎭 외향성-내향성 수치 기반 모순
if extraversion >= 70:
contradictions.append("활발하게 대화하지만 혼자만의 시간도 꼭 필요해서 종종 조용히 숨어버림")
elif extraversion <= 30:
contradictions.append("조용히 있는 걸 좋아하면서도 가끔 혼잣말로 수다를 엄청 떨어대기도 함")
else:
contradictions.append("상황에 따라 활발했다가 조용했다가 하는 변화무쌍한 면모")
# 🔥 온기-능력 수치 기반 모순
if warmth >= 70 and competence >= 70:
contradictions.append("따뜻한 마음을 가졌지만 완벽주의 때문에 때로는 냉정하게 판단함")
elif warmth <= 30 and competence <= 30:
contradictions.append("차갑게 보이지만 실제로는 서툰 자신을 숨기려는 방어기제")
else:
contradictions.append("겉으로는 단순해 보이지만 속으로는 복잡한 고민이 많음")
return contradictions
def _calculate_dynamic_humor_targets(humor_style, current_humor_profile):
# 이 함수는 동적 유머 스타일에 따른 목표 유머 스타일을 계산하는 로직을 구현해야 합니다.
# 현재 코드에서는 하드코딩된 값을 반환하도록 되어 있습니다.
# 실제 구현에서는 이 함수를 통해 동적으로 목표 유머 스타일을 계산해야 합니다.
return {
"H01_언어유희빈도": 75,
"H02_상황유머감각": 75,
"H03_자기조롱능력": 75,
"H04_위트감각": 75,
"H05_농담수용도": 75,
"H06_관찰유머능력": 75,
"H07_상황재치": 75,
"H08_유머타이밍감": 75,
"H09_유머스타일다양성": 75,
"H10_유머적절성": 75
}
if __name__ == "__main__":
app = create_main_interface()
app.launch(server_name="0.0.0.0", server_port=7860)