Spaces:
Sleeping
Sleeping
| 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) |