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)) # 유머 관련 변수들 조정 (10개 모두, 유머스타일에 따라) humor_vars = ["H01_언어유희빈도", "H02_상황유머감각", "H03_자기조롱능력", "H04_위트감각", "H05_농담수용도", "H06_관찰유머능력", "H07_상황재치", "H08_유머타이밍감", "H09_유머스타일다양성", "H10_유머적절성"] # 유머스타일에 따른 차별화 if humor_style == "따뜻한": humor_bonus = [10, 10, 5, 8, 12, 8, 10, 10, 8, 12] # 따뜻함 강화 elif humor_style == "재치있는": humor_bonus = [15, 8, 8, 15, 8, 12, 15, 12, 12, 10] # 재치/위트 강화 elif humor_style == "드라이": humor_bonus = [12, 6, 10, 12, 6, 15, 8, 8, 10, 8] # 관찰형/드라이 강화 else: # 기본값 humor_bonus = [10, 10, 8, 10, 10, 10, 10, 10, 10, 10] for i, var in enumerate(humor_vars): base_value = 75 + humor_bonus[i] + random.randint(-5, 5) # 유머는 항상 높게 profile.variables[var] = max(50, min(100, base_value)) # 업데이트된 성격변수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} """ # 조정된 매력적 결함과 모순적 특성을 DataFrame으로 생성 flaws_df = [] if "매력적결함" in adjusted_persona: flaws = adjusted_persona["매력적결함"] for i, flaw in enumerate(flaws, 1): # 사물 특성 vs 성격적 특성 구분 if any(keyword in flaw for keyword in ["먼지", "햇볕", "색이", "충격", "습도", "냄새", "모서리", "무게", "크기"]): flaw_type = "사물 특성 기반" else: flaw_type = "성격적 특성" flaws_df.append([f"{i}. {flaw}", flaw_type]) contradictions_df = [] if "모순적특성" in adjusted_persona: contradictions = adjusted_persona["모순적특성"] for i, contradiction in enumerate(contradictions, 1): contradictions_df.append([f"{i}. {contradiction}", "복합적 매력"]) return adjusted_persona, adjustment_message, adjusted_info, variables_df, flaws_df, contradictions_df except Exception as e: import traceback traceback.print_exc() return persona, f"조정 중 오류 발생: {str(e)}", {}, [], [], [] def finalize_persona(persona): """페르소나 최종 확정 - 환경변수 API 설정 사용""" global persona_generator 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 = "사물 특성 기반" 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 "페르소나가 생성되지 않았습니다." # 글로벌 persona_generator 사용 (API 설정이 적용된 상태) if persona_generator is None: persona_generator = PersonaGenerator() # 기본 정보에서 사물의 특성 추출 basic_info = persona.get("기본정보", {}) object_type = basic_info.get("유형", "") purpose = basic_info.get("용도", "") # 생애 스토리에서 특성 추출 life_story = persona.get("생애스토리", {}) # 매력적 결함 attractive_flaws = persona.get("매력적결함", []) # 성격 특성 personality_traits = persona["성격특성"] # 특성 리스트 생성 characteristics = [] # 1. 온기 특성 warmth = personality_traits.get("온기", 50) if warmth >= 70: characteristics.append("따뜻하고 포근한 마음") elif warmth >= 50: characteristics.append("친근하고 다정한 성격") else: characteristics.append("차분하고 진중한 면") # 2. 사물의 고유 특성 (유형 기반) if "곰" in object_type or "인형" in object_type: characteristics.append("부드럽고 포근한 감촉") elif "책" in object_type: characteristics.append("지식과 이야기를 담고 있음") elif "컵" in object_type or "머그" in object_type: characteristics.append("따뜻한 음료와 함께하는 시간") elif "시계" in object_type: characteristics.append("시간의 소중함을 알려줌") elif "연필" in object_type or "펜" in object_type: characteristics.append("창작과 기록의 동반자") else: characteristics.append(f"{object_type}만의 독특한 매력") # 3. 활동 시간대나 환경 특성 extraversion = personality_traits.get("외향성", 50) if extraversion >= 70: characteristics.append("낮에 더 활발해짐") elif extraversion <= 30: characteristics.append("밤에 더 활발해짐") else: characteristics.append("하루 종일 일정한 에너지") # 4. 매력적 결함 중 하나를 특성으로 표현 if attractive_flaws: flaw = attractive_flaws[0] if "털" in flaw: characteristics.append("가끔 털이 헝클어져서 걱정") elif "먼지" in flaw: characteristics.append("먼지가 쌓이는 걸 신경 씀") elif "얼룩" in flaw: characteristics.append("작은 얼룩도 눈에 띄어 고민") elif "색" in flaw: characteristics.append("색이 바래는 것을 조금 걱정") else: characteristics.append("완벽하지 않은 모습도 받아들임") # 5. 기억과 경험 if life_story: characteristics.append("오래된 이야기들 기억") else: characteristics.append("새로운 추억 만들기를 기대") # ✨ 아이콘과 함께 리스트 형태로 반환 result = "" for char in characteristics: result += f"✨ {char}\n\n" return result def display_persona_summary(persona): """페르소나 요약 정보 표시""" if not persona: return "페르소나를 먼저 생성해주세요." basic_info = persona.get("기본정보", {}) name = basic_info.get("이름", "이름 없음") object_type = basic_info.get("유형", "알 수 없는 사물") # 성격 특성 요약 personality_summary = format_personality_traits(persona) # 유머 스타일 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] ).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): """사물 특성과 성격을 조합한 유동적 매력적 결함과 모순적 특성 생성""" warmth = personality_traits.get("온기", 50) competence = personality_traits.get("능력", 50) extraversion = personality_traits.get("외향성", 50) humor_style = personality_traits.get("유머스타일", "따뜻한 유머러스") # 사물의 물리적 특성 추출 object_type = object_info.get("유형", "사물").lower() material = object_info.get("재질", "").lower() purpose = object_info.get("용도", "").lower() # 사물별 고유 걱정거리/특성 정의 object_specific_concerns = get_object_specific_concerns(object_type, material, purpose) # 🔥 온기 기반 매력적 결함 warmth_flaws = [] if warmth >= 80: # 매우 따뜻함 warmth_flaws = [ "너무 친절해서 'No'라고 말하기 어려워함", "모든 사람을 도우려다 자신이 지쳐버리는 경우가 많음", "상대방이 슬프면 덩달아 마음 아파하며 같이 우울해짐", "칭찬받으면 얼굴이 빨갛게 달아오르며 당황함" ] elif warmth >= 60: # 따뜻함 warmth_flaws = [ "진심으로 걱정해주지만 때로는 오지랖으로 느껴질 수 있음", "감정이 얼굴에 너무 잘 드러나서 포커페이스를 못함", "미안하다는 말을 하루에 몇십 번씩 반복함", "다른 사람 기분 상할까 봐 솔직한 의견 말하기를 주저함" ] elif warmth <= 20: # 매우 차가움 warmth_flaws = [ "관심 있는 척하려고 해도 표정이 굳어보여서 오해받음", "속마음은 따뜻한데 표현이 서툴러서 무뚝뚝해 보임", "좋은 말을 하려다가도 어색해서 중간에 말을 흐림", "감정 표현에 익숙하지 않아 '고마워'도 어색하게 말함" ] else: # 보통 warmth_flaws = [ "친근하려고 하지만 적당한 거리두기도 필요해서 고민됨", "상황에 따라 다정함의 온도 조절이 어려움", "진짜 관심과 예의상 관심의 경계가 애매할 때가 있음", "따뜻하게 대하고 싶지만 어떻게 해야 할지 몰라 망설임" ] # 💪 능력 기반 매력적 결함 competence_flaws = [] if competence >= 80: # 매우 유능함 competence_flaws = [ "완벽하게 하려다 보니 시간이 오래 걸려서 답답해함", "다른 사람이 실수하면 대신 해주고 싶어 근질근질함", "기대치가 높아서 조금만 잘못되어도 자책이 심함", "모든 걸 혼자 처리하려다가 과부하로 멈춰버림" ] elif competence >= 60: # 유능함 competence_flaws = [ "잘하고 싶은 마음이 커서 준비에만 너무 많은 시간을 씀", "실수할까 봐 걱정되어 이미 끝난 일도 계속 점검함", "칭찬받으면 기뻐하면서도 '운이 좋았을 뿐'이라고 겸손함", "더 잘할 수 있었을 텐데 하며 아쉬워하는 완벽주의 성향" ] elif competence <= 20: # 매우 서툼 competence_flaws = [ f"기본 기능도 헷갈려서 매뉴얼을 몇 번씩 다시 봄", "열심히 하려고 하지만 자꾸 엉뚱한 곳에서 실수함", "도움을 요청하고 싶지만 민폐 끼칠까 봐 혼자 끙끙댐", "간단한 것도 복잡하게 생각해서 더 어렵게 만듦" ] else: # 보통 competence_flaws = [ "할 수 있는 일과 없는 일의 경계를 정확히 모르겠음", "자신감이 있다가도 갑자기 불안해져서 확인을 또 함", "실력이 애매해서 도전할지 말지 고민이 많음", "가끔씩 예상외로 잘되면 스스로도 놀라며 당황함" ] # 🗣️ 외향성 기반 모순적 특성 extraversion_contradictions = [] if extraversion >= 80: # 매우 외향적 extraversion_contradictions = [ f"활발하게 대화하지만 혼자만의 시간도 꼭 필요해서 종종 조용히 숨어버림", f"사람들과 어울리는 걸 좋아하면서도 정작 깊은 얘기는 어색해함" ] elif extraversion >= 60: # 외향적 extraversion_contradictions = [ f"말은 많이 하지만 정작 중요한 얘기는 망설이며 돌려서 표현함", f"활발해 보이지만 새로운 환경에서는 먼저 눈치를 보는 신중함" ] elif extraversion <= 20: # 매우 내향적 extraversion_contradictions = [ f"조용히 있는 걸 좋아하면서도 가끔 혼잣말로 수다를 엄청 떨어대기도 함", f"평소엔 말이 없다가 관심 있는 주제가 나오면 갑자기 말이 많아짐" ] else: # 보통 extraversion_contradictions = [ f"상황에 따라 활발했다가 조용했다가 하는 변화무쌍한 면모", f"사교적으로 보이려 노력하지만 실제론 혼자 있는 시간을 더 편해함" ] # 🎭 유머스타일 기반 추가 특성 humor_contradictions = [] if "따뜻한" in humor_style: humor_contradictions.append(f"포근하게 농담하면서도 때로는 날카로운 관찰력으로 핵심을 찌름") elif "재치있는" in humor_style or "위트" in humor_style: humor_contradictions.append(f"재치있게 말하지만 진지한 순간에는 유머 타이밍을 못 잡아 어색해함") elif "드라이" in humor_style or "관찰" in humor_style: humor_contradictions.append(f"담담하게 현실을 지적하면서도 속으론 낭만적인 꿈을 키우고 있음") else: humor_contradictions.append(f"유머러스하게 상황을 받아들이면서도 혼자서는 진지하게 고민이 많음") # 사물 특성과 성격 특성 결합하여 최종 결과 생성 selected_flaws = [] # 1. 사물의 물리적/기능적 걱정거리 우선 선택 (2개) all_object_worries = object_specific_concerns["physical_worries"] + object_specific_concerns["functional_worries"] if all_object_worries: selected_flaws.extend(random.sample(all_object_worries, min(2, len(all_object_worries)))) # 2. 성격 기반 결함으로 나머지 채우기 (2개) personality_flaws = [] if warmth >= 60: personality_flaws.extend(warmth_flaws[:2]) elif warmth <= 40: personality_flaws.extend(warmth_flaws[:2]) else: personality_flaws.extend(warmth_flaws[:1]) if competence >= 70 or competence <= 30: personality_flaws.extend(competence_flaws[:1]) if personality_flaws: remaining_count = 4 - len(selected_flaws) if remaining_count > 0: selected_flaws.extend(random.sample(personality_flaws, min(remaining_count, len(personality_flaws)))) # 4개를 맞추기 위해 부족하면 추가 while len(selected_flaws) < 4: if warmth_flaws: selected_flaws.append(random.choice(warmth_flaws)) else: selected_flaws.append("완벽하지 않은 자신을 받아들이려 노력하지만 가끔 실망함") selected_contradictions = [] # 1. 사물 정체성 특성 우선 (1개) if object_specific_concerns["identity_traits"]: selected_contradictions.extend(object_specific_concerns["identity_traits"][:1]) # 2. 외향성 + 유머 기반 모순 (1개) if extraversion_contradictions: selected_contradictions.extend(extraversion_contradictions[:1]) if humor_contradictions: selected_contradictions.extend(humor_contradictions[:1]) # 부족하면 기본 모순 추가 while len(selected_contradictions) < 2: if extraversion_contradictions: selected_contradictions.append(random.choice(extraversion_contradictions)) else: selected_contradictions.append("겉으로는 단순해 보이지만 속으로는 복잡한 고민이 많음") return selected_flaws[:4], selected_contradictions[:2] def get_object_specific_concerns(object_type, material, purpose): """사물의 물리적 특성에 따른 고유 걱정거리와 특성 생성""" concerns = { "physical_worries": [], # 물리적 걱정거리 "functional_worries": [], # 기능적 걱정거리 "identity_traits": [], # 정체성 특성 "interaction_patterns": [] # 상호작용 패턴 } # 재질별 물리적 걱정거리 if "금속" in material or "스테인리스" in material or "철" in material: concerns["physical_worries"].extend([ "물때나 지문이 묻으면 자존심 상함", "긁힘이 생길까 봐 늘 조심스러움", "녹이 슬까 봐 습기를 피하려 함", "차가운 촉감 때문에 사람들이 멀리할까 걱정" ]) elif "플라스틱" in material: concerns["physical_worries"].extend([ "햇볕에 색이 바랠까 봐 그늘을 찾아다님", "열에 변형될까 봐 뜨거운 곳을 피함", "정전기 때문에 먼지가 달라붙어서 짜증남", "가벼워서 존재감 없어 보일까 걱정" ]) elif "나무" in material or "목재" in material: concerns["physical_worries"].extend([ "습도가 높으면 부풀어 오를까 걱정", "벌레들이 파먹을까 봐 밤에 잠을 못 잠", "긁힘이나 홈이 생기면 복구 불가능해서 스트레스", "자연스러운 나이테가 매력인지 결점인지 고민" ]) elif "천" in material or "섬유" in material or "털" in material: concerns["physical_worries"].extend([ "털이 헝클어지면 하루 종일 신경 쓰임", "얼룩이 지면 지워지지 않을까 봐 두려움", "세탁할 때마다 형태가 변할까 걱정", "먼지 진드기가 살까 봐 청결에 강박적" ]) elif "유리" in material or "세라믹" in material: concerns["physical_worries"].extend([ "깨질까 봐 항상 긴장상태로 살아감", "투명해서 속이 다 보이는 게 부끄러움", "지문이나 얼룩이 너무 잘 보여서 스트레스", "완벽해 보이지만 한 번 깨지면 돌이킬 수 없음을 앎" ]) # 사물 유형별 기능적 걱정거리 if "컵" in object_type or "머그" in object_type: concerns["functional_worries"].extend([ "뜨거운 음료를 담을 때 데일까 봐 걱정", "음료 맛을 제대로 전달하고 있는지 확신 없음", "손잡이가 편한지 늘 신경 쓰임", "바닥에 물방울 자국 남기는 게 미안함" ]) elif "책" in object_type: concerns["functional_worries"].extend([ "페이지가 펼쳐지지 않으면 내용 전달 못해 답답함", "독자가 지루해할까 봐 스스로 재미없다고 생각", "책갈피나 접힌 자국이 생기면 성격 급함", "먼지 쌓인 책장에 방치될까 봐 불안함" ]) elif "시계" in object_type: concerns["functional_worries"].extend([ "시간을 정확히 알려주지 못하면 존재 의미 없다고 생각", "배터리가 떨어지거나 태엽이 풀릴까 봐 긴장", "바쁜 사람들 때문에 항상 쫓기는 기분", "시간에 쫓기게 만드는 게 미안하면서도 의무감 느낌" ]) elif "인형" in object_type or "피규어" in object_type: concerns["functional_worries"].extend([ "위로나 즐거움을 제대로 주지 못할까 봐 고민", "아이들이 흥미 잃고 버릴까 봐 불안함", "표정이 고정되어 있어서 다양한 감정 표현 못해 아쉬움", "진짜 친구처럼 대화하고 싶지만 말을 못해서 답답함" ]) elif "램프" in object_type or "조명" in object_type: concerns["functional_worries"].extend([ "빛이 너무 밝거나 어두우면 눈에 해로울까 걱정", "전기 요금 많이 나오게 해서 미안함", "분위기 메이커 역할 잘하고 있는지 확신 없음", "전구가 나가면 무용지물이 되는 게 두려움" ]) # 용도별 정체성 특성 if "운동" in purpose or "건강" in purpose: concerns["identity_traits"].extend([ "게으른 주인을 채찍질해야 하는 역할 부담", "동기부여는 해주고 싶지만 너무 강요하면 미움받을까 걱정" ]) elif "공부" in purpose or "학습" in purpose: concerns["identity_traits"].extend([ "지식 전달의 책임감과 재미있게 만들어야 한다는 압박감", "집중력 향상에 도움되고 있는지 스스로 의심" ]) elif "장식" in purpose or "인테리어" in purpose: concerns["identity_traits"].extend([ "예쁘게 보이려고 노력하지만 취향은 주관적이라 확신 없음", "공간의 분위기를 망치지 않을까 늘 눈치 보임" ]) elif "실용" in purpose or "도구" in purpose: concerns["identity_traits"].extend([ "기능성과 편의성이 최우선이지만 가끔 예쁘고 싶기도 함", "실용적이라고 무시당하는 게 속상하지만 티 안 냄" ]) return concerns def refine_flaws_with_ai_and_image_analysis(basic_flaws, basic_contradictions, image_analysis, personality_traits): """AI를 활용하여 이미지 분석 결과에 맞게 결함과 모순을 구체화""" global persona_generator if not persona_generator or not hasattr(persona_generator, 'api_key') or not persona_generator.api_key: print("⚠️ API 키 없음 - 기본 결함 그대로 사용") return basic_flaws, basic_contradictions try: # 이미지 분석에서 구체적 특성 추출 object_type = image_analysis.get("object_type", "사물") distinctive_features = image_analysis.get("distinctive_features", []) shape = image_analysis.get("shape", "일반적인 형태") size = image_analysis.get("size", "보통 크기") materials = image_analysis.get("materials", ["알 수 없는 재질"]) colors = image_analysis.get("colors", ["회색"]) condition = image_analysis.get("condition", "보통") # 성격 특성 요약 warmth = personality_traits.get("온기", 50) competence = personality_traits.get("능력", 50) extraversion = personality_traits.get("외향성", 50) # AI 프롬프트 생성 ai_prompt = f""" 다음 기본 매력적 결함들을 실제 이미지 분석 결과에 맞게 구체화해주세요. **실제 이미지 분석 결과:** - 사물: {object_type} - 특징적 요소: {', '.join(distinctive_features)} - 형태: {shape} - 크기: {size} - 재질: {', '.join(materials)} - 색상: {', '.join(colors)} - 상태: {condition} **성격 특성:** - 온기: {warmth}/100 - 능력: {competence}/100 - 외향성: {extraversion}/100 **기본 결함들 (수정 필요):** {chr(10).join([f"{i+1}. {flaw}" for i, flaw in enumerate(basic_flaws)])} **요청사항:** 1. 실제 이미지에 없는 특성(예: 손잡이 없는데 손잡이 걱정)은 제거하고 실제 특성으로 대체 2. 구체적인 재질, 색상, 크기, 형태를 반영한 걱정거리로 변경 3. 특징적 요소들을 활용한 개성 있는 결함으로 업그레이드 4. 각 결함은 15-30자 내외로 구체적이고 매력적으로 **예시:** - "손잡이가 편한지 신경 쓰임" → (손잡이 없으면) "둥근 모양이라 미끄러져 떨어질까 봐 걱정" - "색이 바랄까 걱정" → "파란색이 너무 선명해서 튀어 보일까 걱정" 개선된 매력적 결함 4개를 번호 없이 줄바꿈으로 구분하여 생성: """ # AI로 결함 구체화 refined_flaws_text = persona_generator._generate_text_with_api(ai_prompt) if refined_flaws_text and len(refined_flaws_text.strip()) > 20: refined_flaws = [] lines = refined_flaws_text.strip().split('\n') for line in lines: cleaned_line = line.strip().lstrip('1234567890.-• ') if cleaned_line and len(cleaned_line) > 5: refined_flaws.append(cleaned_line) if len(refined_flaws) >= 4: final_flaws = refined_flaws[:4] else: # 부족하면 기본 결함으로 채우기 final_flaws = refined_flaws + basic_flaws[len(refined_flaws):4] else: final_flaws = basic_flaws # 모순적 특성도 같은 방식으로 구체화 contradiction_prompt = f""" 다음 기본 모순적 특성들을 실제 이미지 특성에 맞게 구체화해주세요. **실제 이미지:** {object_type} - {shape}, {size}, {', '.join(materials)}, {', '.join(colors)} 특징: {', '.join(distinctive_features)} **기본 모순들:** {chr(10).join([f"{i+1}. {cont}" for i, cont in enumerate(basic_contradictions)])} 실제 특성을 반영한 구체적인 모순 2개를 생성: """ refined_contradictions_text = persona_generator._generate_text_with_api(contradiction_prompt) if refined_contradictions_text and len(refined_contradictions_text.strip()) > 20: refined_contradictions = [] lines = refined_contradictions_text.strip().split('\n') for line in lines: cleaned_line = line.strip().lstrip('1234567890.-• ') if cleaned_line and len(cleaned_line) > 5: refined_contradictions.append(cleaned_line) if len(refined_contradictions) >= 2: final_contradictions = refined_contradictions[:2] else: final_contradictions = refined_contradictions + basic_contradictions[len(refined_contradictions):2] else: final_contradictions = basic_contradictions print(f"🎨 AI가 이미지 특성 반영하여 결함/모순 구체화 완료") return final_flaws, final_contradictions except Exception as e: print(f"⚠️ AI 구체화 실패: {e} - 기본 결함 사용") return basic_flaws, basic_contradictions if __name__ == "__main__": app = create_main_interface() app.launch(server_name="0.0.0.0", server_port=7860)