Spaces:
Sleeping
Sleeping
| import json | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| from PIL import Image | |
| import PIL.ImageDraw | |
| import os | |
| import time | |
| import random | |
| import pandas as pd | |
| import gradio as gr | |
| import tempfile | |
| import base64 | |
| from datetime import datetime | |
| # 성격 데이터 시각화 함수 | |
| def plot_humor_matrix(humor_data): | |
| if not humor_data: | |
| return None | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| from matplotlib.patches import RegularPolygon | |
| # 데이터 준비 | |
| warmth_vs_wit = humor_data.get("warmth_vs_wit", 50) | |
| self_vs_observational = humor_data.get("self_vs_observational", 50) | |
| subtle_vs_expressive = humor_data.get("subtle_vs_expressive", 50) | |
| # 3차원 데이터 정규화 (0~1 범위) | |
| warmth = warmth_vs_wit / 100 | |
| self_ref = self_vs_observational / 100 | |
| expressive = subtle_vs_expressive / 100 | |
| # 그래프 생성 | |
| fig, ax = plt.subplots(figsize=(7, 6)) | |
| ax.set_aspect('equal') | |
| # 축 설정 | |
| ax.set_xlim(-1.2, 1.2) | |
| ax.set_ylim(-1.2, 1.2) | |
| # 삼각형 그리기 | |
| triangle = RegularPolygon((0, 0), 3, radius=1, orientation=0, edgecolor='gray', facecolor='none') | |
| ax.add_patch(triangle) | |
| # 축 라벨 위치 계산 | |
| angle = np.linspace(0, 2*np.pi, 3, endpoint=False) | |
| x = 1.1 * np.cos(angle) | |
| y = 1.1 * np.sin(angle) | |
| # 축 라벨 추가 | |
| labels = ['따뜻함', '자기참조', '표현적'] | |
| opposite_labels = ['재치', '관찰형', '은은함'] | |
| for i in range(3): | |
| ax.text(x[i], y[i], labels[i], ha='center', va='center', fontsize=12) | |
| ax.text(-x[i]/2, -y[i]/2, opposite_labels[i], ha='center', va='center', fontsize=10, color='gray') | |
| # 내부 가이드라인 그리기 | |
| for j in [0.33, 0.66]: | |
| inner_triangle = RegularPolygon((0, 0), 3, radius=j, orientation=0, edgecolor='lightgray', facecolor='none', linestyle='--') | |
| ax.add_patch(inner_triangle) | |
| # 포인트 계산 | |
| # 삼각좌표계 변환 (barycentric coordinates) | |
| # 각 차원의 값을 삼각형 내부의 점으로 변환 | |
| tx = x[0] * warmth + x[1] * self_ref + x[2] * expressive | |
| ty = y[0] * warmth + y[1] * self_ref + y[2] * expressive | |
| # 포인트 그리기 | |
| ax.scatter(tx, ty, s=150, color='red', zorder=5) | |
| # 축 제거 | |
| ax.axis('off') | |
| # 제목 추가 | |
| plt.title('유머 스타일 매트릭스', fontsize=14) | |
| return fig | |
| # 성격 차트 생성 함수 | |
| def generate_personality_chart(persona): | |
| """Generate a radar chart for personality traits""" | |
| if not persona or "성격특성" not in persona: | |
| # Return empty image with default PIL | |
| img = Image.new('RGB', (400, 400), color='white') | |
| draw = PIL.ImageDraw.Draw(img) | |
| draw.text((150, 180), "데이터 없음", fill='black') | |
| img_path = os.path.join("data", "temp_chart.png") | |
| img.save(img_path) | |
| return img_path | |
| # Get traits | |
| traits = persona["성격특성"] | |
| # Create radar chart | |
| categories = list(traits.keys()) | |
| values = list(traits.values()) | |
| # Add the first value again to close the loop | |
| categories.append(categories[0]) | |
| values.append(values[0]) | |
| # Convert to radians | |
| angles = np.linspace(0, 2*np.pi, len(categories), endpoint=True) | |
| # 한글 폰트 설정 - 기본적으로 사용 가능한 폰트를 먼저 시도 | |
| # Matplotlib에서 지원하는 한글 폰트 목록 | |
| korean_fonts = ['NanumGothic', 'NanumGothicCoding', 'NanumMyeongjo', 'Malgun Gothic', 'Gulim', 'Batang', 'Arial Unicode MS', 'DejaVu Sans'] | |
| # 폰트 설정 | |
| plt.rcParams['font.family'] = 'sans-serif' # 기본 폰트 패밀리 | |
| # 여러 폰트를 시도 | |
| font_found = False | |
| for font in korean_fonts: | |
| try: | |
| plt.rcParams['font.sans-serif'] = [font] + plt.rcParams.get('font.sans-serif', []) | |
| plt.text(0, 0, '테스트', fontfamily=font) | |
| font_found = True | |
| print(f"성공적으로 한글 폰트를 설정했습니다: {font}") | |
| break | |
| except: | |
| continue | |
| if not font_found: | |
| print("한글 지원 폰트를 찾을 수 없습니다. 영문으로 표시합니다.") | |
| # 영어 라벨 매핑 | |
| english_labels = { | |
| "온기": "Warmth", | |
| "능력": "Ability", | |
| "신뢰성": "Trust", | |
| "친화성": "Friendly", | |
| "창의성": "Creative", | |
| "유머감각": "Humor", | |
| "외향성": "Extraversion" | |
| } | |
| categories = [english_labels.get(cat, cat) for cat in categories] | |
| # Create plot with improved aesthetics | |
| fig, ax = plt.subplots(figsize=(7, 7), subplot_kw=dict(polar=True)) | |
| # 배경 스타일 개선 | |
| ax.set_facecolor('#f8f9fa') | |
| fig.patch.set_facecolor('#f8f9fa') | |
| # Grid 스타일 개선 | |
| ax.grid(True, color='#e0e0e0', linestyle='-', linewidth=0.5, alpha=0.7) | |
| # 각도 라벨 위치 및 색상 조정 | |
| ax.set_rlabel_position(90) | |
| ax.tick_params(colors='#6b7280') | |
| # Y축 라벨 제거 및 눈금 표시 | |
| ax.set_yticklabels([]) | |
| ax.set_yticks([20, 40, 60, 80, 100]) | |
| # 범위 설정 | |
| ax.set_ylim(0, 100) | |
| # 차트 그리기 | |
| # 1. 채워진 영역 | |
| ax.fill(angles, values, alpha=0.25, color='#6366f1') | |
| # 2. 테두리 선 | |
| ax.plot(angles, values, 'o-', linewidth=2, color='#6366f1') | |
| # 3. 데이터 포인트 강조 | |
| ax.scatter(angles[:-1], values[:-1], s=100, color='#6366f1', edgecolor='white', zorder=10) | |
| # 4. 각 축 설정 | |
| ax.set_thetagrids(angles[:-1] * 180/np.pi, categories[:-1], fontsize=12) | |
| # 제목 추가 | |
| name = persona.get("기본정보", {}).get("이름", "Unknown") | |
| plt.title(f"{name} 성격 특성", size=16, color='#374151', pad=20, fontweight='bold') | |
| # 저장 | |
| timestamp = int(time.time()) | |
| img_path = os.path.join("data", f"chart_{timestamp}.png") | |
| os.makedirs(os.path.dirname(img_path), exist_ok=True) | |
| plt.savefig(img_path, format='png', bbox_inches='tight', dpi=150, facecolor=fig.get_facecolor()) | |
| plt.close(fig) | |
| return img_path | |
| # 페르소나 저장 함수 | |
| def save_current_persona(current_persona): | |
| """Save current persona to a JSON file""" | |
| if not current_persona: | |
| return "저장할 페르소나가 없습니다." | |
| try: | |
| # 깊은 복사를 통해 원본 데이터를 유지 | |
| import copy | |
| persona_copy = copy.deepcopy(current_persona) | |
| # 저장 불가능한 객체 제거 | |
| keys_to_remove = [] | |
| for key in persona_copy: | |
| if key in ["personality_profile", "humor_matrix", "_state"] or callable(persona_copy[key]): | |
| keys_to_remove.append(key) | |
| for key in keys_to_remove: | |
| persona_copy.pop(key, None) | |
| # 중첩된 딕셔너리와 리스트 내의 비직렬화 가능 객체 제거 | |
| def clean_data(data): | |
| if isinstance(data, dict): | |
| for k in list(data.keys()): | |
| if callable(data[k]): | |
| del data[k] | |
| elif isinstance(data[k], (dict, list)): | |
| data[k] = clean_data(data[k]) | |
| return data | |
| elif isinstance(data, list): | |
| return [clean_data(item) if isinstance(item, (dict, list)) else item for item in data if not callable(item)] | |
| else: | |
| return data | |
| # 데이터 정리 | |
| cleaned_persona = clean_data(persona_copy) | |
| # 최종 검증: JSON 직렬화 가능 여부 확인 | |
| import json | |
| try: | |
| json.dumps(cleaned_persona) | |
| except TypeError as e: | |
| print(f"JSON 직렬화 오류: {str(e)}") | |
| # 기본 정보만 유지하고 나머지는 안전한 데이터만 포함 | |
| basic_info = cleaned_persona.get("기본정보", {}) | |
| 성격특성 = cleaned_persona.get("성격특성", {}) | |
| 매력적결함 = cleaned_persona.get("매력적결함", []) | |
| 모순적특성 = cleaned_persona.get("모순적특성", []) | |
| cleaned_persona = { | |
| "기본정보": basic_info, | |
| "성격특성": 성격특성, | |
| "매력적결함": 매력적결함, | |
| "모순적특성": 모순적특성 | |
| } | |
| # 외부 함수 호출이 필요한 부분 | |
| from modules.data_manager import save_persona | |
| filepath = save_persona(cleaned_persona) | |
| if filepath: | |
| name = current_persona.get("기본정보", {}).get("이름", "Unknown") | |
| return f"{name} 페르소나가 저장되었습니다: {filepath}" | |
| else: | |
| return "페르소나 저장에 실패했습니다." | |
| except Exception as e: | |
| import traceback | |
| error_details = traceback.format_exc() | |
| print(f"저장 오류 상세: {error_details}") | |
| return f"저장 중 오류 발생: {str(e)}" | |
| # 성격 미세조정 함수 | |
| def refine_persona(persona, warmth, competence, creativity, extraversion, humor, trust, humor_style): | |
| """페르소나의 성격을 미세조정하는 함수""" | |
| if not persona: | |
| return persona, "페르소나가 없습니다." | |
| try: | |
| # 복사본 생성 | |
| refined_persona = persona.copy() | |
| # 성격 특성 업데이트 | |
| if "성격특성" in refined_persona: | |
| refined_persona["성격특성"]["온기"] = int(warmth) | |
| refined_persona["성격특성"]["능력"] = int(competence) | |
| refined_persona["성격특성"]["창의성"] = int(creativity) | |
| refined_persona["성격특성"]["외향성"] = int(extraversion) | |
| refined_persona["성격특성"]["유머감각"] = int(humor) | |
| refined_persona["성격특성"]["신뢰성"] = int(trust) | |
| # 유머 스타일 업데이트 | |
| refined_persona["유머스타일"] = humor_style | |
| # 127개 성격 변수가 있으면 업데이트 | |
| if "성격변수127" in refined_persona: | |
| # 온기 관련 변수 업데이트 | |
| for var in ["W01_친절함", "W02_친근함", "W06_공감능력", "W07_포용력"]: | |
| if var in refined_persona["성격변수127"]: | |
| refined_persona["성격변수127"][var] = int(warmth * 0.9 + random.randint(0, 20)) | |
| # 능력 관련 변수 업데이트 | |
| for var in ["C01_효율성", "C02_지능", "C05_정확성", "C09_실행력"]: | |
| if var in refined_persona["성격변수127"]: | |
| refined_persona["성격변수127"][var] = int(competence * 0.9 + random.randint(0, 20)) | |
| # 창의성 관련 변수 업데이트 | |
| for var in ["C04_창의성", "C08_통찰력"]: | |
| if var in refined_persona["성격변수127"]: | |
| refined_persona["성격변수127"][var] = int(creativity * 0.9 + random.randint(0, 20)) | |
| # 외향성 관련 변수 업데이트 | |
| for var in ["E01_사교성", "E02_활동성", "E03_자기주장", "E06_열정성"]: | |
| if var in refined_persona["성격변수127"]: | |
| refined_persona["성격변수127"][var] = int(extraversion * 0.9 + random.randint(0, 20)) | |
| # 유머 관련 변수 업데이트 | |
| if "H01_유머감각" in refined_persona["성격변수127"]: | |
| refined_persona["성격변수127"]["H01_유머감각"] = int(humor * 0.9 + random.randint(0, 20)) | |
| # 신뢰성 관련 변수 업데이트 | |
| if "W04_신뢰성" in refined_persona["성격변수127"]: | |
| refined_persona["성격변수127"]["W04_신뢰성"] = int(trust * 0.9 + random.randint(0, 20)) | |
| # 유머 매트릭스 업데이트 | |
| if "유머매트릭스" in refined_persona: | |
| if humor_style == "위트있는 재치꾼": | |
| refined_persona["유머매트릭스"]["warmth_vs_wit"] = 30 | |
| refined_persona["유머매트릭스"]["self_vs_observational"] = 50 | |
| refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 70 | |
| elif humor_style == "따뜻한 유머러스": | |
| refined_persona["유머매트릭스"]["warmth_vs_wit"] = 80 | |
| refined_persona["유머매트릭스"]["self_vs_observational"] = 60 | |
| refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 60 | |
| elif humor_style == "날카로운 관찰자": | |
| refined_persona["유머매트릭스"]["warmth_vs_wit"] = 40 | |
| refined_persona["유머매트릭스"]["self_vs_observational"] = 20 | |
| refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 50 | |
| elif humor_style == "자기 비하적": | |
| refined_persona["유머매트릭스"]["warmth_vs_wit"] = 60 | |
| refined_persona["유머매트릭스"]["self_vs_observational"] = 85 | |
| refined_persona["유머매트릭스"]["subtle_vs_expressive"] = 40 | |
| return refined_persona, "성격이 성공적으로 미세조정되었습니다." | |
| except Exception as e: | |
| import traceback | |
| error_details = traceback.format_exc() | |
| print(f"성격 미세조정 오류: {error_details}") | |
| return persona, f"성격 미세조정 중 오류가 발생했습니다: {str(e)}" | |
| # 페르소나 리스트 가져오기 함수 | |
| def get_personas_list(): | |
| """Get list of personas for the dataframe""" | |
| from modules.data_manager import list_personas | |
| personas = list_personas() | |
| # Convert to dataframe format | |
| df_data = [] | |
| for i, persona in enumerate(personas): | |
| df_data.append([ | |
| persona["name"], | |
| persona["type"], | |
| persona["created_at"], | |
| persona["filename"] | |
| ]) | |
| return df_data, personas | |
| # 선택한 페르소나 불러오기 함수 | |
| def load_selected_persona(selected_row, personas_list): | |
| """Load persona from the selected row in the dataframe""" | |
| if selected_row is None or len(selected_row) == 0: | |
| return None, "선택된 페르소나가 없습니다.", None, None, None | |
| try: | |
| # Get filepath from selected row | |
| selected_index = selected_row.index[0] if hasattr(selected_row, 'index') else 0 | |
| filepath = personas_list[selected_index]["filepath"] | |
| # Load persona | |
| from modules.data_manager import load_persona, toggle_frontend_backend_view | |
| persona = load_persona(filepath) | |
| if not persona: | |
| return None, "페르소나 로딩에 실패했습니다.", None, None, None | |
| # Generate HTML views | |
| from temp.frontend_view import create_frontend_view_html | |
| from temp.backend_view import create_backend_view_html | |
| frontend_view, backend_view = toggle_frontend_backend_view(persona) | |
| frontend_html = create_frontend_view_html(frontend_view) | |
| backend_html = create_backend_view_html(backend_view) | |
| # Generate personality chart | |
| chart_image_path = generate_personality_chart(frontend_view) | |
| return persona, f"{persona['기본정보']['이름']}을(를) 로드했습니다.", frontend_html, backend_html, chart_image_path | |
| except Exception as e: | |
| return None, f"페르소나 로딩 중 오류 발생: {str(e)}", None, None, None | |
| # 현재 페르소나 정보 표시 함수 | |
| def update_current_persona_info(current_persona): | |
| if not current_persona: | |
| return {}, {}, None, [], [], [] | |
| # 기본 정보 | |
| basic_info = { | |
| "이름": current_persona.get("기본정보", {}).get("이름", "Unknown"), | |
| "유형": current_persona.get("기본정보", {}).get("유형", "Unknown"), | |
| "생성일": current_persona.get("기본정보", {}).get("생성일시", "Unknown"), | |
| "설명": current_persona.get("기본정보", {}).get("설명", "") | |
| } | |
| # 성격 특성 | |
| personality_traits = {} | |
| if "성격특성" in current_persona: | |
| personality_traits = current_persona["성격특성"] | |
| # 성격 요약 정보 | |
| personality_summary = {} | |
| if "성격요약" in current_persona: | |
| personality_summary = current_persona["성격요약"] | |
| elif "성격변수127" in current_persona: | |
| # 직접 성격 요약 계산 | |
| try: | |
| variables = current_persona["성격변수127"] | |
| # 카테고리별 평균 계산 | |
| summary = {} | |
| category_counts = {} | |
| for var_name, value in variables.items(): | |
| category = var_name[0] if var_name and len(var_name) > 0 else "기타" | |
| if category == "W": # 온기 | |
| summary["온기"] = summary.get("온기", 0) + value | |
| category_counts["온기"] = category_counts.get("온기", 0) + 1 | |
| elif category == "C": # 능력 | |
| summary["능력"] = summary.get("능력", 0) + value | |
| category_counts["능력"] = category_counts.get("능력", 0) + 1 | |
| elif category == "E": # 외향성 | |
| summary["외향성"] = summary.get("외향성", 0) + value | |
| category_counts["외향성"] = category_counts.get("외향성", 0) + 1 | |
| elif category == "O": # 개방성 | |
| summary["창의성"] = summary.get("창의성", 0) + value | |
| category_counts["창의성"] = category_counts.get("창의성", 0) + 1 | |
| elif category == "H": # 유머 | |
| summary["유머감각"] = summary.get("유머감각", 0) + value | |
| category_counts["유머감각"] = category_counts.get("유머감각", 0) + 1 | |
| # 평균 계산 | |
| for category in summary: | |
| if category_counts[category] > 0: | |
| summary[category] = summary[category] / category_counts[category] | |
| # 기본값 설정 (데이터가 없는 경우) | |
| if "온기" not in summary: | |
| summary["온기"] = 50 | |
| if "능력" not in summary: | |
| summary["능력"] = 50 | |
| if "외향성" not in summary: | |
| summary["외향성"] = 50 | |
| if "창의성" not in summary: | |
| summary["창의성"] = 50 | |
| if "유머감각" not in summary: | |
| summary["유머감각"] = 50 | |
| personality_summary = summary | |
| except Exception as e: | |
| print(f"성격 요약 계산 오류: {str(e)}") | |
| personality_summary = { | |
| "온기": 50, | |
| "능력": 50, | |
| "외향성": 50, | |
| "창의성": 50, | |
| "유머감각": 50 | |
| } | |
| # 유머 매트릭스 차트 | |
| humor_chart = None | |
| if "유머매트릭스" in current_persona: | |
| humor_chart = plot_humor_matrix(current_persona["유머매트릭스"]) | |
| # 매력적 결함 데이터프레임 | |
| attractive_flaws_df = get_attractive_flaws_df(current_persona) | |
| # 모순적 특성 데이터프레임 | |
| contradictions_df = get_contradictions_df(current_persona) | |
| # 127개 성격 변수 데이터프레임 | |
| personality_variables_df = get_personality_variables_df(current_persona) | |
| return basic_info, personality_traits, humor_chart, attractive_flaws_df, contradictions_df, personality_variables_df | |
| # 성격 변수 데이터프레임 생성 함수 | |
| def get_personality_variables_df(persona): | |
| if not persona or "성격변수127" not in persona: | |
| return [] | |
| variables = persona["성격변수127"] | |
| if isinstance(variables, dict): | |
| rows = [] | |
| for var_name, score in variables.items(): | |
| # 변수 설명은 앱의 메인 파일에서 정의되어 있을 것이므로 일단 빈 문자열로 처리 | |
| description = "" | |
| rows.append([var_name, score, description]) | |
| return rows | |
| return [] | |
| # 매력적 결함 데이터프레임 생성 함수 | |
| def get_attractive_flaws_df(persona): | |
| if not persona or "매력적결함" not in persona: | |
| return [] | |
| flaws = persona["매력적결함"] | |
| effects = [ | |
| "인간적 매력 +25%", | |
| "관계 깊이 +30%", | |
| "공감 유발 +20%" | |
| ] | |
| return [[flaw, effects[i] if i < len(effects) else "매력 증가"] for i, flaw in enumerate(flaws)] | |
| # 모순적 특성 데이터프레임 생성 함수 | |
| def get_contradictions_df(persona): | |
| if not persona or "모순적특성" not in persona: | |
| return [] | |
| contradictions = persona["모순적특성"] | |
| effects = [ | |
| "복잡성 +35%", | |
| "흥미도 +28%" | |
| ] | |
| return [[contradiction, effects[i] if i < len(effects) else "깊이감 증가"] for i, contradiction in enumerate(contradictions)] | |
| def export_persona_json(persona): | |
| """ | |
| 페르소나를 JSON 파일로 내보내는 기능 | |
| """ | |
| if not persona: | |
| return None, "페르소나가 없습니다." | |
| try: | |
| # persona 객체를 JSON으로 직렬화 | |
| persona_dict = persona.copy() | |
| # 복잡한 객체를 딕셔너리로 변환 | |
| if "humor_matrix" in persona_dict and hasattr(persona_dict["humor_matrix"], "to_dict"): | |
| persona_dict["humor_matrix"] = persona_dict["humor_matrix"].to_dict() | |
| if "personality" in persona_dict and hasattr(persona_dict["personality"], "to_dict"): | |
| persona_dict["personality"] = persona_dict["personality"].to_dict() | |
| # 현재 시간을 파일명에 추가 | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| object_name = persona.get("name", "unknown_persona").replace(" ", "_").lower() | |
| filename = f"{object_name}_{timestamp}.json" | |
| # JSON 문자열로 변환 | |
| json_str = json.dumps(persona_dict, ensure_ascii=False, indent=2) | |
| return filename, json_str | |
| except Exception as e: | |
| print(f"페르소나 내보내기 실패: {e}") | |
| return None, f"페르소나 내보내기 실패: {e}" | |
| def import_persona_json(file_obj): | |
| """ | |
| JSON 파일로부터 페르소나를 불러오는 기능 | |
| """ | |
| if not file_obj: | |
| return None, "업로드된 파일이 없습니다." | |
| try: | |
| # JSON 파일을 로드하여 딕셔너리로 변환 | |
| content = file_obj.read().decode('utf-8') | |
| persona_dict = json.loads(content) | |
| # 필수 필드 확인 | |
| required_fields = ["name", "object_type"] | |
| for field in required_fields: | |
| if field not in persona_dict: | |
| return None, f"유효하지 않은 페르소나 파일: {field} 필드가 없습니다." | |
| # 복잡한 객체 재구성 | |
| if "humor_matrix" in persona_dict and isinstance(persona_dict["humor_matrix"], dict): | |
| persona_dict["humor_matrix"] = HumorMatrix.from_dict(persona_dict["humor_matrix"]) | |
| if "personality" in persona_dict and isinstance(persona_dict["personality"], dict): | |
| persona_dict["personality"] = PersonalityProfile.from_dict(persona_dict["personality"]) | |
| return persona_dict, f"{persona_dict['name']} 페르소나를 로드했습니다." | |
| except Exception as e: | |
| print(f"페르소나 불러오기 실패: {e}") | |
| return None, f"페르소나 불러오기 실패: {e}" |