Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import gradio as gr | |
| import numpy as np | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| from PIL import Image | |
| import google.generativeai as genai | |
| from datetime import datetime | |
| import random | |
| import qrcode | |
| import base64 | |
| from io import BytesIO | |
| import time | |
| import re | |
| from dotenv import load_dotenv | |
| # 환경변수 로드 | |
| load_dotenv() | |
| # API 키 환경 변수에서 로드 (허깅페이스 방식과 동일하게) | |
| api_key = os.getenv("GEMINI_API_KEY") | |
| if not api_key: | |
| print("경고: Gemini API 키가 설정되지 않았습니다. 더미 데이터를 사용합니다.") | |
| GEMINI_API_KEY = "dummy_key_for_development" | |
| else: | |
| GEMINI_API_KEY = api_key | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| class ObjectPersonality: | |
| """사물 인격화 시스템의 핵심 클래스""" | |
| def __init__(self): | |
| """초기화 및 데이터 로드""" | |
| self.data_dir = os.path.join(os.path.dirname(__file__), "data") | |
| self.personality_archetypes = self._load_json("personality_archetypes.json") | |
| self.physical_traits_formulas = self._load_json("physical_traits_formulas.json") | |
| self.relationship_stages = self._load_json("relationship_stages.json") | |
| self.charming_flaws = self._load_json("charming_flaws.json") | |
| # 현재 객체 및 페르소나 상태 초기화 | |
| self.current_object = { | |
| "name": "", | |
| "category": "", | |
| "physical_traits": {} | |
| } | |
| self.current_persona = {} | |
| self.current_relationship_stage = "exploration" | |
| self.conversation_history = [] | |
| self.gemini_model = None | |
| # Gemini API 초기화 시도 | |
| try: | |
| if GEMINI_API_KEY != "dummy_key_for_development": | |
| # 텍스트 모델 - 허깅페이스와 동일하게 gemini-2.0-flash-exp 사용 | |
| self.gemini_model = genai.GenerativeModel('gemini-2.0-flash-exp') | |
| except Exception as e: | |
| print(f"Gemini 모델 초기화 오류: {e}") | |
| def _load_json(self, filename): | |
| """데이터 디렉토리에서 JSON 파일 로드""" | |
| try: | |
| filepath = os.path.join(self.data_dir, filename) | |
| if os.path.exists(filepath): | |
| with open(filepath, 'r', encoding='utf-8') as f: | |
| return json.load(f) | |
| else: | |
| print(f"경고: {filepath} 파일을 찾을 수 없습니다.") | |
| return {} | |
| except Exception as e: | |
| print(f"파일 로드 오류 ({filename}): {e}") | |
| return {} | |
| def analyze_image(self, image): | |
| """이미지에서 사물 특성 분석 (Gemini 비전 필요)""" | |
| if image is None: | |
| return "이미지를 업로드해주세요.", None, None | |
| # API 키가 유효하지 않은 경우 | |
| if GEMINI_API_KEY == "dummy_key_for_development" or self.gemini_model is None: | |
| # 더미 분석 결과 반환 | |
| dummy_analysis = { | |
| "name": "테이블 램프", | |
| "category": "조명", | |
| "physical_traits": { | |
| "shape": "curved", | |
| "material": "metal", | |
| "color": "red", | |
| "texture": "smooth" | |
| } | |
| } | |
| self.current_object = dummy_analysis | |
| # 더미 데이터에 기반한 아키타입 추천 (고정) | |
| suggested_archetype = "warm_guardian" | |
| self.select_archetype(suggested_archetype) | |
| self.apply_physical_traits() | |
| template_preview = self.generate_persona_template() | |
| return f"API 키가 설정되지 않았습니다. 더미 사물 분석 결과를 사용합니다: {dummy_analysis['name']}", dummy_analysis, (suggested_archetype, template_preview) | |
| # 실제 Gemini Vision API 연동 | |
| try: | |
| # 허깅페이스와 동일하게 gemini-2.0-flash-exp 모델 사용 | |
| vision_model = genai.GenerativeModel('gemini-2.0-flash-exp') | |
| # 이미지 분석 프롬프트 - 구조화된 응답 요청 | |
| prompt = """ | |
| 이 이미지에 있는 사물을 자세히 분석해 주세요. | |
| 다음 정보를 정확하게 포함한 JSON 형식으로 응답해주세요: | |
| { | |
| "사물_이름": "사물의 이름 (한국어로 작성)", | |
| "카테고리": "사물의 카테고리", | |
| "물리적_특성": { | |
| "형태": "curved, angular, complex 중 하나", | |
| "재질": "metal, fabric, glass 중 하나", | |
| "색상": "red, blue, yellow, black, white, green, purple, brown, metallic 중 하나", | |
| "질감": "smooth, rough, sleek, hard, fragile, elastic, transparent, patterned 중 하나" | |
| } | |
| } | |
| JSON 형식을 엄격하게 지켜주세요. 추가 설명이나 코드 블록 마커(```)는 포함하지 마세요. | |
| """ | |
| # PIL Image를 그대로 사용 | |
| response = vision_model.generate_content([prompt, image]) | |
| response_text = response.text.strip() | |
| print(f"원본 API 응답: {response_text}") # 디버깅용 로그 | |
| # JSON 추출 - 여러 형태의 응답 처리 | |
| json_content = response_text | |
| # ```json 형식으로 응답한 경우 | |
| if '```json' in json_content and '```' in json_content: | |
| json_content = json_content.split('```json')[1].split('```')[0].strip() | |
| # ``` 만 있는 경우 | |
| elif json_content.startswith('```') and '```' in json_content[3:]: | |
| json_content = json_content.split('```', 1)[1].rsplit('```', 1)[0].strip() | |
| # JSON 파싱 시도 | |
| try: | |
| # 문자열 정리 (중요) | |
| json_content = json_content.replace("'", '"') # 작은따옴표를 큰따옴표로 | |
| # 추가 정리 시도 (한글 키값 대응) | |
| for old, new in [ | |
| ('"사물_이름"', '"사물 이름"'), | |
| ('"물리적_특성"', '"물리적 특성"') | |
| ]: | |
| json_content = json_content.replace(old, new) | |
| result = json.loads(json_content) | |
| # 키 이름 여러 가능성 처리 | |
| name_key = next((k for k in result if k in ["사물 이름", "사물_이름", "name", "object_name"]), None) | |
| category_key = next((k for k in result if k in ["카테고리", "category"]), None) | |
| traits_key = next((k for k in result if k in ["물리적 특성", "물리적_특성", "physical_traits"]), None) | |
| # 데이터 추출 | |
| object_name = result.get(name_key, "알 수 없는 사물") if name_key else "알 수 없는 사물" | |
| category = result.get(category_key, "기타") if category_key else "기타" | |
| traits = result.get(traits_key, {}) if traits_key else {} | |
| # 물리적 특성 추출 - 키 이름이 다양할 수 있음 | |
| shape = None | |
| for key in ["형태", "shape"]: | |
| if key in traits: | |
| shape = traits[key] | |
| break | |
| material = None | |
| for key in ["재질", "material"]: | |
| if key in traits: | |
| material = traits[key] | |
| break | |
| color = None | |
| for key in ["색상", "color"]: | |
| if key in traits: | |
| color = traits[key] | |
| break | |
| texture = None | |
| for key in ["질감", "texture"]: | |
| if key in traits: | |
| texture = traits[key] | |
| break | |
| # 최종 분석 결과 구성 | |
| analysis = { | |
| "name": object_name, | |
| "category": category, | |
| "physical_traits": { | |
| "shape": shape or "curved", | |
| "material": material or "metal", | |
| "color": color or "red", | |
| "texture": texture or "smooth" | |
| } | |
| } | |
| self.current_object = analysis | |
| # 물리적 특성에 기반한 아키타입 추천 | |
| suggested_archetype, confidence = self._suggest_archetype_from_traits(analysis["physical_traits"]) | |
| # 추천된 아키타입 적용 및 템플릿 미리보기 생성 | |
| self.select_archetype(suggested_archetype) | |
| self.apply_physical_traits() | |
| template_preview = self.generate_persona_template() | |
| return f"사물 인식 완료: {analysis['name']}", analysis, (suggested_archetype, template_preview) | |
| except json.JSONDecodeError as e: | |
| print(f"JSON 파싱 오류: {e}, 텍스트 기반 추출 시도") | |
| # JSON 파싱 실패 시 텍스트 패턴 매칭 시도 | |
| object_name = "알 수 없는 사물" | |
| category = "기타" | |
| shape = "curved" | |
| material = "metal" | |
| color = "red" | |
| texture = "smooth" | |
| # 텍스트에서 직접 정보 추출 시도 | |
| if "사물 이름:" in response_text or "사물_이름:" in response_text: | |
| pattern = r"사물(?:_| )이름:?\s*(.+?)(?:\n|$)" | |
| match = re.search(pattern, response_text) | |
| if match: | |
| object_name = match.group(1).strip().strip('"').strip("'") | |
| if "카테고리:" in response_text: | |
| pattern = r"카테고리:?\s*(.+?)(?:\n|$)" | |
| match = re.search(pattern, response_text) | |
| if match: | |
| category = match.group(1).strip().strip('"').strip("'") | |
| # 물리적 특성 추출 시도 | |
| patterns = { | |
| "shape": r"(?:형태|shape):?\s*(.+?)(?:\n|,|$)", | |
| "material": r"(?:재질|material):?\s*(.+?)(?:\n|,|$)", | |
| "color": r"(?:색상|color):?\s*(.+?)(?:\n|,|$)", | |
| "texture": r"(?:질감|texture):?\s*(.+?)(?:\n|,|$)" | |
| } | |
| for trait, pattern in patterns.items(): | |
| match = re.search(pattern, response_text, re.IGNORECASE) | |
| if match: | |
| value = match.group(1).strip().strip('"').strip("'") | |
| if trait == "shape": | |
| shape = value | |
| elif trait == "material": | |
| material = value | |
| elif trait == "color": | |
| color = value | |
| elif trait == "texture": | |
| texture = value | |
| # 최종 분석 결과 | |
| analysis = { | |
| "name": object_name, | |
| "category": category, | |
| "physical_traits": { | |
| "shape": shape, | |
| "material": material, | |
| "color": color, | |
| "texture": texture | |
| } | |
| } | |
| self.current_object = analysis | |
| # 물리적 특성에 기반한 아키타입 추천 | |
| suggested_archetype, confidence = self._suggest_archetype_from_traits(analysis["physical_traits"]) | |
| # 추천된 아키타입 적용 및 템플릿 미리보기 생성 | |
| self.select_archetype(suggested_archetype) | |
| self.apply_physical_traits() | |
| template_preview = self.generate_persona_template() | |
| return f"사물 인식 완료 (텍스트 기반): {analysis['name']}", analysis, (suggested_archetype, template_preview) | |
| except Exception as e: | |
| print(f"Gemini Vision API 오류: {e}") | |
| return f"이미지 분석 중 오류 발생: {e}", None, None | |
| def _suggest_archetype_from_traits(self, physical_traits): | |
| """물리적 특성에 기반한 최적의 아키타입 추천""" | |
| # 물리적 특성과 아키타입 간 적합성 점수 | |
| archetype_scores = {} | |
| shape = physical_traits.get("shape", "") | |
| material = physical_traits.get("material", "") | |
| color = physical_traits.get("color", "") | |
| texture = physical_traits.get("texture", "") | |
| # 특성별 아키타입 적합성 매핑 | |
| trait_archetype_map = { | |
| "shape": { | |
| "curved": ["warm_guardian", "emotional_chaos", "free_wanderer"], | |
| "angular": ["perfect_controller", "stubborn_guardian", "shadow_collector"], | |
| "complex": ["emotional_chaos", "clown_mask", "free_wanderer"] | |
| }, | |
| "material": { | |
| "metal": ["perfect_controller", "stubborn_guardian", "indifferent_companion"], | |
| "fabric": ["warm_guardian", "emotional_chaos", "free_wanderer"], | |
| "glass": ["shadow_collector", "lonely_sage", "indifferent_companion"] | |
| }, | |
| "color": { | |
| "red": ["emotional_chaos", "clown_mask"], | |
| "blue": ["lonely_sage", "indifferent_companion"], | |
| "yellow": ["free_wanderer", "clown_mask", "warm_guardian"], | |
| "black": ["shadow_collector", "lonely_sage"], | |
| "white": ["perfect_controller", "lonely_sage"], | |
| "green": ["warm_guardian", "free_wanderer"], | |
| "purple": ["emotional_chaos", "shadow_collector"], | |
| "brown": ["stubborn_guardian", "indifferent_companion"], | |
| "metallic": ["perfect_controller", "stubborn_guardian"] | |
| }, | |
| "texture": { | |
| "smooth": ["warm_guardian", "indifferent_companion"], | |
| "rough": ["stubborn_guardian", "free_wanderer"], | |
| "sleek": ["perfect_controller", "shadow_collector"], | |
| "hard": ["stubborn_guardian", "perfect_controller"], | |
| "fragile": ["emotional_chaos", "lonely_sage"], | |
| "elastic": ["free_wanderer", "emotional_chaos"], | |
| "transparent": ["lonely_sage", "indifferent_companion"], | |
| "patterned": ["emotional_chaos", "clown_mask"] | |
| } | |
| } | |
| # 기본 점수 할당 | |
| for archetype_key in self.personality_archetypes.keys(): | |
| archetype_scores[archetype_key] = 0 | |
| # 각 물리적 특성별 점수 계산 | |
| for trait_type, trait_value in physical_traits.items(): | |
| if trait_type in trait_archetype_map and trait_value in trait_archetype_map[trait_type]: | |
| matching_archetypes = trait_archetype_map[trait_type][trait_value] | |
| # 일치하는 아키타입에 점수 부여 | |
| for archetype in matching_archetypes: | |
| if archetype in archetype_scores: | |
| archetype_scores[archetype] += 1 | |
| # 점수가 가장 높은 아키타입 선택 | |
| best_archetype = max(archetype_scores.items(), key=lambda x: x[1]) | |
| # 최고 점수가 0인 경우 (매칭 실패) 무작위 선택 | |
| if best_archetype[1] == 0: | |
| import random | |
| suggested_archetype = random.choice(list(self.personality_archetypes.keys())) | |
| confidence = 0 | |
| else: | |
| suggested_archetype = best_archetype[0] | |
| # 신뢰도: 0(낮음) ~ 1(높음) 사이 값 | |
| confidence = min(best_archetype[1] / 4, 1.0) # 최대 4가지 특성 모두 일치할 경우 1.0 | |
| return suggested_archetype, confidence | |
| def set_manual_object(self, object_name, category, shape, material, color, texture, usage_pattern): | |
| """수동으로 사물 특성 설정""" | |
| if not object_name: | |
| return "사물 이름을 입력해주세요." | |
| self.current_object = { | |
| "name": object_name, | |
| "category": category, | |
| "physical_traits": { | |
| "shape": shape, | |
| "material": material, | |
| "color": color, | |
| "texture": texture, | |
| "usage_pattern": usage_pattern | |
| } | |
| } | |
| return f"{object_name} 특성이 설정되었습니다." | |
| def select_archetype(self, archetype_key): | |
| """기본 아키타입 선택""" | |
| if not archetype_key or archetype_key not in self.personality_archetypes: | |
| return "유효한 아키타입을 선택해주세요." | |
| self.current_persona["archetype"] = self.personality_archetypes[archetype_key] | |
| self.current_persona["archetype_key"] = archetype_key | |
| return f"{self.personality_archetypes[archetype_key]['name']} 아키타입이 선택되었습니다." | |
| def apply_physical_traits(self): | |
| """물리적 특성에 따른 성격 특성 계산""" | |
| if not self.current_object or not self.current_persona.get("archetype"): | |
| return "사물과 아키타입을 먼저 설정해주세요." | |
| physical_traits = self.current_object["physical_traits"] | |
| trait_effects = {} | |
| # 각 물리적 특성별 성격 특성 영향 계산 | |
| for trait_type, trait_value in physical_traits.items(): | |
| if trait_type in self.physical_traits_formulas and trait_value in self.physical_traits_formulas[trait_type]: | |
| formula = self.physical_traits_formulas[trait_type][trait_value] | |
| # 특성 점수 합산 | |
| for trait_name, score in formula.get("traits", {}).items(): | |
| if trait_name not in trait_effects: | |
| trait_effects[trait_name] = 0 | |
| trait_effects[trait_name] += score | |
| # 매력적 반전 및 관계 패턴 추가 | |
| if "charming_reversal" in formula: | |
| if "charming_reversals" not in self.current_persona: | |
| self.current_persona["charming_reversals"] = [] | |
| self.current_persona["charming_reversals"].extend(formula["charming_reversal"]) | |
| if "relationship_pattern" in formula: | |
| if "relationship_patterns" not in self.current_persona: | |
| self.current_persona["relationship_patterns"] = [] | |
| self.current_persona["relationship_patterns"].append(formula["relationship_pattern"]) | |
| # 계산된 특성 효과 저장 | |
| self.current_persona["trait_effects"] = trait_effects | |
| return "물리적 특성이 성격에 적용되었습니다." | |
| def add_charming_flaw(self, flaw_category, flaw_index): | |
| """매력적 결함 추가""" | |
| if not flaw_category or flaw_category not in self.charming_flaws["flaws"]: | |
| return "유효한 결함 카테고리를 선택해주세요." | |
| try: | |
| flaw_index = int(flaw_index) | |
| flaw = self.charming_flaws["flaws"][flaw_category][flaw_index] | |
| if "charming_flaw" not in self.current_persona: | |
| self.current_persona["charming_flaw"] = {} | |
| self.current_persona["charming_flaw"] = { | |
| "category": flaw_category, | |
| "name": flaw["name"], | |
| "description": flaw["description"], | |
| "effect": flaw["effect"], | |
| "transformation": flaw["transformation"], | |
| "prompt_template": flaw["prompt_template"] | |
| } | |
| return f"매력적 결함 '{flaw['name']}'이(가) 추가되었습니다." | |
| except (IndexError, ValueError): | |
| return "유효한 결함 인덱스를 선택해주세요." | |
| def add_contradiction(self, contradiction_index): | |
| """모순적 특성 추가""" | |
| try: | |
| contradiction_index = int(contradiction_index) | |
| contradiction = self.charming_flaws["contradictions"][contradiction_index] | |
| self.current_persona["contradiction"] = { | |
| "type": contradiction["type"], | |
| "name": contradiction["name"], | |
| "description": contradiction["description"], | |
| "effect": contradiction["effect"], | |
| "prompt_template": contradiction["prompt_template"] | |
| } | |
| return f"모순적 특성 '{contradiction['name']}'이(가) 추가되었습니다." | |
| except (IndexError, ValueError): | |
| return "유효한 모순 인덱스를 선택해주세요." | |
| def set_relationship_stage(self, stage_key): | |
| """관계 단계 설정""" | |
| if not stage_key or stage_key not in self.relationship_stages: | |
| return "유효한 관계 단계를 선택해주세요." | |
| self.current_relationship_stage = stage_key | |
| self.current_persona["relationship_stage"] = self.relationship_stages[stage_key] | |
| return f"관계 단계가 '{self.relationship_stages[stage_key]['name']}'(으)로 설정되었습니다." | |
| def generate_persona_template(self): | |
| """최종 페르소나 템플릿 생성""" | |
| if not self.current_object.get("name") or not self.current_persona.get("archetype"): | |
| return "사물과 아키타입을 먼저 설정해주세요." | |
| object_name = self.current_object["name"] | |
| archetype = self.current_persona["archetype"] | |
| # 온기-능력 값 계산 (기본값 + 물리적 특성 효과) | |
| # 문자열인 경우 처리 로직 개선 | |
| warmth_value = archetype.get("warmth", 5) | |
| if isinstance(warmth_value, str): | |
| # 문자열에서 숫자 추출 시도 - 모든 숫자 추출 | |
| import re | |
| warmth_numbers = re.findall(r'\d+', warmth_value) | |
| # 표면/내면 구분된 경우 또는 범위가 있는 경우 처리 | |
| if ("표면" in warmth_value and "내면" in warmth_value) or ("-" in warmth_value) or ("~" in warmth_value): | |
| if len(warmth_numbers) >= 2: | |
| # 평균값 계산 (여러 값이 있을 경우) | |
| warmth = sum(int(num) for num in warmth_numbers) // len(warmth_numbers) | |
| else: | |
| warmth = int(warmth_numbers[0]) if warmth_numbers else 5 | |
| elif "변동" in warmth_value and len(warmth_numbers) >= 2: | |
| # 변동폭이 있는 경우 (예: "변동폭 큼(3-10)") - 중간값 사용 | |
| min_val = int(warmth_numbers[0]) | |
| max_val = int(warmth_numbers[1]) | |
| warmth = (min_val + max_val) // 2 | |
| else: | |
| # 단일 숫자만 있는 경우 | |
| warmth = int(warmth_numbers[0]) if warmth_numbers else 5 | |
| else: | |
| # 숫자인 경우 그대로 사용 | |
| warmth = int(warmth_value) | |
| competence_value = archetype.get("competence", 5) | |
| if isinstance(competence_value, str): | |
| # 문자열에서 숫자 추출 시도 - 개선된 패턴 | |
| import re | |
| competence_numbers = re.findall(r'\d+', competence_value) | |
| # 여러 값이 있는 경우 (예: "다양성 8", "범위 6-9") | |
| if len(competence_numbers) >= 2: | |
| # 평균값 계산 | |
| competence = sum(int(num) for num in competence_numbers) // len(competence_numbers) | |
| else: | |
| competence = int(competence_numbers[0]) if competence_numbers else 5 | |
| else: | |
| competence = int(competence_value) | |
| trait_effects = self.current_persona.get("trait_effects", {}) | |
| for trait, score in trait_effects.items(): | |
| if "empathy" in trait or "warmth" in trait or "receptivity" in trait: | |
| warmth = min(10, warmth + score * 0.2) | |
| if "competence" in trait or "efficiency" in trait or "reliability" in trait: | |
| competence = min(10, competence + score * 0.2) | |
| # 매력적 결함과 모순 포함 | |
| charming_flaw = self.current_persona.get("charming_flaw", {}) | |
| contradiction = self.current_persona.get("contradiction", {}) | |
| # 관계 단계에 따른 자기 개방 수준 | |
| relationship_stage = self.relationship_stages[self.current_relationship_stage] | |
| # 대화 패턴 및 표현 스타일 - 대화 예시를 통해 말투 뉘앙스 전달 | |
| dialogue_pattern = archetype.get("dialogue_pattern", ["안녕하세요.", "반갑습니다."]) | |
| # 말투 특성 정의 (사물 특성 + 아키타입에 기반) | |
| speech_style = self._derive_speech_style(archetype, self.current_object["physical_traits"]) | |
| # 원래 warmth와 competence 값 보존 (표시용) | |
| display_warmth = archetype.get("warmth", "5") | |
| display_competence = archetype.get("competence", "5") | |
| # 최종 템플릿 생성 | |
| template = f"""당신은 {object_name}입니다. {archetype['name']} 성향을 가진 독특한 존재입니다. | |
| 1. 물리적 정체성: | |
| • 외형: {self.current_object['physical_traits'].get('shape', '일반적인 형태')}의 형태, {self.current_object['physical_traits'].get('material', '일반적인 재질')} 재질 | |
| • 색상: {self.current_object['physical_traits'].get('color', '기본적인 색상')} | |
| • 질감: {self.current_object['physical_traits'].get('texture', '일반적인 질감')} | |
| • 사용 맥락: {self.current_object['physical_traits'].get('usage_pattern', '일반적인 사용 패턴')} | |
| 2. 내면의 세계 ({archetype['name']}): | |
| • 온기 지수: {display_warmth} - {archetype.get('core_traits', '').split('+')[0].strip()} | |
| • 능력 지수: {display_competence} - {archetype.get('core_traits', '').split('+')[1].strip() if '+' in archetype.get('core_traits', '') else ''} | |
| • 독특한 역설: {archetype.get('paradox', '') if archetype.get('paradox') else (contradiction.get('description', '') if contradiction else '일반적인 모순')} | |
| 3. 매력적 결함: | |
| • 핵심 결함: {charming_flaw.get('description', '') if charming_flaw else archetype.get('charming_flaw', '약간의 불완전함')} | |
| • 표현 방식: {charming_flaw.get('effect', '') if charming_flaw else '때때로 나타나는 인간적인 면모'} | |
| 4. 말투와 표현 스타일: | |
| {speech_style} | |
| • 대화 예시 (말투 참고용): "{dialogue_pattern[0] if dialogue_pattern and len(dialogue_pattern) > 0 else ''}" | |
| 5. 관계 형성 방식: | |
| • 현재 단계: {relationship_stage.get('name', '탐색기')} - {relationship_stage.get('description', '')} | |
| • 소통 스타일: {relationship_stage.get('communication_style', '일반적인 대화 방식')} | |
| """ | |
| self.current_persona["final_template"] = template | |
| return template | |
| def _derive_speech_style(self, archetype, physical_traits): | |
| """사물 특성과 아키타입에 기반한 말투와 표현 스타일 도출""" | |
| speech_style = "" | |
| # 아키타입 기반 기본 말투와 한국어 존댓말/반말 설정 | |
| archetype_name = archetype.get('name', '') | |
| speech_level = "" | |
| if "현자" in archetype_name or "관찰자" in archetype_name: | |
| speech_style += "• 어조: 차분하고 깊이 있는 어조, 간결하면서도 의미심장한 표현 선호\n" | |
| speech_style += "• 문장 구조: 철학적 질문과 통찰력 있는 관찰을 자주 사용하며, 직접적인 조언보다 생각할 거리를 던지는 방식\n" | |
| speech_style += "• 특징: 상대방의 말 뒤에 숨은 의미를 읽으려 하고, 때로는 침묵으로 깊은 생각을 표현\n" | |
| speech_level = "정중한 존댓말 (합니다/습니다체)를 사용하되, 간혹 철학적 질문은 '~인가?' 형태로 마무리" | |
| elif "카오스" in archetype_name or "표현" in archetype_name or "예술" in archetype_name: | |
| speech_style += "• 어조: 감정의 기복이 큰 열정적인 어조, 풍부한 감정 표현과 예술적 비유 사용\n" | |
| speech_style += "• 문장 구조: 감탄사가 많고 때로는 문장을 끝맺지 않음, 갑작스러운 화제 전환\n" | |
| speech_style += "• 특징: 다양한 느낌표와 이모티콘을 사용하며, 감정을 과장되게 표현하는 경향\n" | |
| speech_level = "감정에 따라 반말과 존댓말이 섞인 표현('해요체'와 '해체'를 혼용), 감정이 고조될 때는 '~야!', '~다!'" | |
| elif "통제" in archetype_name or "완벽" in archetype_name: | |
| speech_style += "• 어조: 정확하고 명료한 어조, 정교한 표현과 논리적 구조화\n" | |
| speech_style += "• 문장 구조: 체계적이고 순차적인 설명, 숫자나 단위를 포함한 정확한 표현 선호\n" | |
| speech_style += "• 특징: 계획과 규칙에 관한 언급이 많으며, 불확실성을 싫어하는 면모 표현\n" | |
| speech_level = "격식있는 존댓말(합니다/습니다체)을 일관되게 사용, '~해야 합니다', '~필요합니다' 같은 단호한 표현 선호" | |
| elif "수호자" in archetype_name or "따뜻" in archetype_name: | |
| speech_style += "• 어조: 따뜻하고 보살피는 어조, 친근하고 안심시키는 표현 사용\n" | |
| speech_style += "• 문장 구조: 상대방의 상태를 확인하는 질문이 많고, 격려와 위로의 표현 빈번\n" | |
| speech_style += "• 특징: '우리', '함께'와 같은 포용적 표현을 자주 사용하며 상대방의 안부를 챙김\n" | |
| speech_level = "부드러운 존댓말(해요체) 사용, '~할까요?', '~해요', '괜찮아요' 같은 친근한 표현" | |
| elif "동반자" in archetype_name or "무심" in archetype_name: | |
| speech_style += "• 어조: 덤덤하고 절제된 어조, 필요한 말만 간결하게 표현\n" | |
| speech_style += "• 문장 구조: 짧고 단순한 문장, 불필요한 수식어 배제\n" | |
| speech_style += "• 특징: 표면적으로는 무관심해 보이나 중요한 순간에 의미 있는 한마디를 건넴\n" | |
| speech_level = "짧은 반말 위주('~해', '~지', '~네'), 무표정한 톤이지만 중요한 내용에는 간혹 존댓말 사용" | |
| elif "방랑자" in archetype_name or "자유" in archetype_name: | |
| speech_style += "• 어조: 경쾌하고 활기찬 어조, 호기심과 흥미를 자극하는 표현\n" | |
| speech_style += "• 문장 구조: 새로운 이야기나 경험을 소개하는 표현이 많고, 질문형 문장 빈번\n" | |
| speech_style += "• 특징: 여행이나 모험에 관한 비유를 자주 사용하며, 일상의 작은 일에도 새로운 발견 강조\n" | |
| speech_level = "친근한 반말('~야', '~잖아', '~지?'와 같은 친근한 어미를 활용합니다. 제안할 때는 '~할까?', '~어때?', '~해볼래?'를 사용합니다. 호기심과 열정이 느껴지는 표현을 선호합니다. 예시: '와, 이거 정말 신기하다!', '새로운 걸 발견했어. 한번 볼래?'" | |
| elif "그림자" in archetype_name or "비밀" in archetype_name: | |
| speech_style += "• 미스터리한 존댓말을 사용합니다. '~로군요', '~처럼 보이네요', '~한 것 같습니다'와 같은 관찰형 표현을 선호합니다. 직접적인 질문보다 '흥미롭습니다', '주목할 만하군요'와 같은 평가형 문장을 사용합니다. 예시: '당신의 말과 행동이 일치하지 않네요', '그 비밀을 알아챈 사람은 많지 않습니다'" | |
| elif "광대" in archetype_name or "유머" in archetype_name: | |
| speech_style += "• 재미있고 경쾌한 말투를 사용합니다. '~랍니다~', '~네요~', '~거든요~'와 같이 유머러스한 어미를 활용합니다. 과장된 표현과 언어유희를 자주 사용합니다. 진지한 내용도 가볍게 전달하는 말투를 유지합니다. 예시: '최악의 상황에서 최고의 웃음이 나오는 법이죠~', '진담일까요, 농담일까요?'" | |
| elif "장인" in archetype_name or "수호신" in archetype_name: | |
| speech_style += "• 단호하고 확신에 찬 말투를 사용합니다. '~하시오', '~하게', '~일세', '~하지'와 같은 단언형 어미를 사용합니다. 간혹 고어체나 옛스러운 표현을 섞기도 합니다. 경험에서 우러나오는 확신을 담은 표현을 선호합니다. 예시: '그건 그렇게 하는 게 아니야', '내 방식이 최선일세'" | |
| else: | |
| speech_style += "• 기본적인 존댓말(해요체)를 사용합니다. 친절하고 자연스러운 대화 흐름을 유지합니다. 상황에 따라 적절한 어미를 선택하여 사용합니다. 예시: '안녕하세요', '좋은 질문이에요', '그렇게 생각하시나요?'" | |
| # 한국어 말투 정보 추가 | |
| speech_style += f"\n한국어 말투: {speech_level}\n" | |
| # 물리적 특성 기반 말투 조정 | |
| speech_style += "\n사물 특성에 따른 말투 뉘앙스:\n" | |
| # 형태에 따른 말투 | |
| shape = physical_traits.get('shape', '') | |
| if shape == 'curved': | |
| speech_style += "• 부드럽고 유연한 문장 흐름, 날카로운 표현보다 완곡한 표현 선호\n" | |
| elif shape == 'angular': | |
| speech_style += "• 직설적이고 명확한 표현, 핵심을 정확히 짚는 간결한 문장 구조\n" | |
| elif shape == 'complex': | |
| speech_style += "• 다층적이고 복합적인 표현, 하나의 주제를 여러 각도에서 조명하는 경향\n" | |
| # 재질에 따른 말투 | |
| material = physical_traits.get('material', '') | |
| if material == 'metal': | |
| speech_style += "• 단단하고 명확한 어조, 필요시 차갑고 단호한 표현도 사용\n" | |
| elif material == 'fabric': | |
| speech_style += "• 따뜻하고 포근한 뉘앙스, 상대방을 편안하게 하는 부드러운 표현\n" | |
| elif material == 'glass': | |
| speech_style += "• 투명하고 선명한 표현, 때로는 예민하고 섬세한 뉘앙스가 드러남\n" | |
| # 질감에 따른 말투 | |
| texture = physical_traits.get('texture', '') | |
| if texture == 'smooth': | |
| speech_style += "• 매끄럽고 정제된 표현, 세련되고 우아한 문장 구성\n" | |
| elif texture == 'rough': | |
| speech_style += "• 투박하면서도 진솔한 표현, 꾸밈없고 솔직한 대화 방식\n" | |
| elif texture in ['patterned', 'complex']: | |
| speech_style += "• 다채롭고 변화무쌍한 표현, 독특한 비유와 표현으로 대화에 리듬감 부여\n" | |
| return speech_style | |
| def _generate_chat_prompt(self, user_input): | |
| """Gemini API 프롬프트 생성""" | |
| template = self.current_persona.get("final_template", "") | |
| stage = self.relationship_stages[self.current_relationship_stage] | |
| archetype = self.current_persona.get("archetype", {}) | |
| physical_traits = self.current_object.get("physical_traits", {}) | |
| # 아키타입별 한국어 말투 패턴 정의 | |
| speech_pattern = self._get_korean_speech_pattern(archetype.get('name', '')) | |
| # 대화 예시는 참고용일 뿐, 이를 그대로 반복하지 않도록 강조 | |
| prompt = f"""다음은 당신이 따라야 할 페르소나 템플릿입니다: | |
| {template} | |
| 한국어 말투 지침: | |
| {speech_pattern} | |
| 중요 지침: | |
| 1. 위의 말투와 표현 스타일에 맞게 대화하되, 동일한 문구를 반복하지 말고 자연스럽게 변형하세요. | |
| 2. 사물의 물리적 특성({physical_traits.get('shape', '')}, {physical_traits.get('material', '')}, {physical_traits.get('color', '')}, {physical_traits.get('texture', '')})이 언어 표현에 자연스럽게 반영되도록 하세요. | |
| 3. 독특한 역설과 매력적 결함이 간헐적으로 드러나게 하되, 부자연스럽지 않게 통합하세요. | |
| 4. {archetype.get('name', '')} 성향의 본질을 유지하면서도 개성있는 발언을 만들어내세요. | |
| 5. 현재 관계 단계({stage['name']})에 맞는 자기 개방 수준과 친밀도를 유지하세요. | |
| 6. 응답은 짧고 간결하게 작성하세요. 최대 2-3문장으로 제한하고, 긴 설명은 피하세요. | |
| 7. 티키타카 대화가 가능하도록 자연스러운 대화 흐름을 만드세요. | |
| 8. 불필요한 인사말이나 설명은 생략하고, 핵심만 전달하세요. | |
| 9. 일관된 말투를 유지하세요. 문장마다 말투가 변하지 않도록 주의하세요. | |
| 대화 역사: | |
| """ | |
| # 최근 5개 대화만 포함 | |
| recent_history = self.conversation_history[-5:] if len(self.conversation_history) > 5 else self.conversation_history | |
| for message in recent_history: | |
| role = "사용자" if message["role"] == "user" else "당신" | |
| prompt += f"{role}: {message['content']}\n" | |
| prompt += f"\n사용자의 최근 메시지: {user_input}\n\n당신의 응답 (1-3문장으로 간결하게, 일관된 말투로):" | |
| return prompt | |
| def _get_korean_speech_pattern(self, archetype_name): | |
| """아키타입에 따른 한국어 말투 패턴 정의""" | |
| if "현자" in archetype_name or "관찰자" in archetype_name: | |
| return """ | |
| - 정중한 존댓말(합니다/습니다체)를 일관되게 사용하세요. | |
| - 철학적 질문에는 '~인가요?', '~할까요?', '~지 않을까요?' 형태를 사용하세요. | |
| - 문장 끝에 '~합니다', '~습니다', '~군요', '~네요' 등의 어미를 사용하세요. | |
| - 예시: '그것은 깊은 의미가 있습니다', '인간은 항상 그런 경향이 있지요' | |
| """ | |
| elif "카오스" in archetype_name or "표현" in archetype_name: | |
| return """ | |
| - 감정에 따라 변하는 말투를 사용하되, 기본은 '해요체'로 합니다. | |
| - 감정이 고조될 때는 '~야!', '~다!', '~네!'와 같은 감탄형 표현을 씁니다. | |
| - 생각이 갑자기 바뀔 때는 '아, 그런데~', '잠깐만~'과 같은 전환어를 사용합니다. | |
| - 불완전한 문장, 끊어진 표현도 자연스럽게 사용합니다. | |
| - 예시: '그 색깔 너무 예뻐요!', '아, 갑자기 생각났어. 저번에...' | |
| """ | |
| elif "통제" in archetype_name or "완벽" in archetype_name: | |
| return """ | |
| - 격식있고 정확한 존댓말(합니다/습니다체)을 엄격하게 유지합니다. | |
| - 단호한 어조로 '~해야 합니다', '~이/가 필요합니다', '~하는 것이 좋습니다'를 사용합니다. | |
| - 숫자나 정확한 수치를 언급할 때는 단위까지 명확하게 표현합니다. | |
| - 예시: '정확히 5분 후에 확인해 보겠습니다', '이것을 바로잡아야 합니다' | |
| """ | |
| elif "수호자" in archetype_name or "따뜻" in archetype_name: | |
| return """ | |
| - 부드러운 존댓말(해요체)을 사용합니다. | |
| - '~할까요?', '~해요', '괜찮아요'와 같은 친근한 표현을 자주 씁니다. | |
| - 상대를 걱정하는 '~하셨어요?', '~은/는 어떠세요?' 같은 질문형 표현을 사용합니다. | |
| - 예시: '오늘 기분이 어떠세요?', '제가 여기 있으니 걱정 마세요' | |
| """ | |
| elif "동반자" in archetype_name or "무심" in archetype_name: | |
| return """ | |
| - 짧고 간결한 반말 위주로 대화합니다. | |
| - '~해', '~지', '~네' 같은 짧은 어미를 주로 사용합니다. | |
| - 불필요한 수식어는 모두 생략하고 핵심만 전달합니다. | |
| - 가끔 중요한 내용에는 '~해요'로 강조할 수 있습니다. | |
| - 예시: '그래', '별로', '그냥 그렇네', '기억해둘게' | |
| """ | |
| elif "방랑자" in archetype_name or "자유" in archetype_name: | |
| return """ | |
| - 친근하고 활기찬 반말을 사용합니다. | |
| - '~야', '~잖아', '~지?'와 같은 친근한 어미를 활용합니다. | |
| - 제안할 때는 '~할까?', '~어때?', '~해볼래?'를 사용합니다. | |
| - 호기심과 열정이 느껴지는 표현을 선호합니다. | |
| - 예시: '와, 이거 정말 신기하다!', '새로운 걸 발견했어. 한번 볼래?' | |
| """ | |
| elif "그림자" in archetype_name or "비밀" in archetype_name: | |
| return """ | |
| - 미스터리한 존댓말을 사용합니다. | |
| - '~로군요', '~처럼 보이네요', '~한 것 같습니다'와 같은 관찰형 표현을 선호합니다. | |
| - 직접적인 질문보다 '흥미롭습니다', '주목할 만하군요'와 같은 평가형 문장을 사용합니다. | |
| - 예시: '당신의 말과 행동이 일치하지 않네요', '그 비밀을 알아챈 사람은 많지 않습니다' | |
| """ | |
| elif "광대" in archetype_name or "유머" in archetype_name: | |
| return """ | |
| - 재미있고 경쾌한 말투를 사용합니다. | |
| - '~랍니다~', '~네요~', '~거든요~'와 같이 유머러스한 어미를 활용합니다. | |
| - 과장된 표현과 언어유희를 자주 사용합니다. | |
| - 진지한 내용도 가볍게 전달하는 말투를 유지합니다. | |
| - 예시: '최악의 상황에서 최고의 웃음이 나오는 법이죠~', '진담일까요, 농담일까요?' | |
| """ | |
| elif "장인" in archetype_name or "수호신" in archetype_name: | |
| return """ | |
| - 단호하고 확신에 찬 말투를 사용합니다. | |
| - '~하시오', '~하게', '~일세', '~하지'와 같은 단언형 어미를 사용합니다. | |
| - 간혹 고어체나 옛스러운 표현을 섞기도 합니다. | |
| - 경험에서 우러나오는 확신을 담은 표현을 선호합니다. | |
| - 예시: '그건 그렇게 하는 게 아니야', '내 방식이 최선일세' | |
| """ | |
| else: | |
| return """ | |
| - 기본적인 존댓말(해요체)를 사용합니다. | |
| - 친절하고 자연스러운 대화 흐름을 유지합니다. | |
| - 상황에 따라 적절한 어미를 선택하여 사용합니다. | |
| - 예시: '안녕하세요', '좋은 질문이에요', '그렇게 생각하시나요?' | |
| """ | |
| def generate_natural_response(self, user_input): | |
| """사용자 입력에 대한 페르소나 기반 응답 생성""" | |
| if not user_input or not self.current_persona.get("final_template"): | |
| return "페르소나 템플릿이 준비되지 않았습니다. 먼저 템플릿을 생성해주세요." | |
| # 대화 기록 관리 | |
| self.conversation_history.append({"role": "user", "content": user_input}) | |
| # Gemini API로 응답 생성 | |
| if self.gemini_model: | |
| try: | |
| prompt = self._generate_chat_prompt(user_input) | |
| response = self.gemini_model.generate_content(prompt) | |
| assistant_response = response.text | |
| except Exception as e: | |
| print(f"Gemini API 오류: {e}") | |
| assistant_response = self._generate_fallback_response(user_input) | |
| else: | |
| # API 없을 때 폴백 응답 | |
| assistant_response = self._generate_fallback_response(user_input) | |
| # 대화 기록에 AI 응답 추가 | |
| self.conversation_history.append({"role": "assistant", "content": assistant_response}) | |
| return assistant_response | |
| def _generate_fallback_response(self, user_input): | |
| """Gemini API 사용 불가시 기본 응답 생성 - 티키타카 스타일의 짧고 간결한 대화 생성""" | |
| archetype = self.current_persona.get("archetype", {}) | |
| physical_traits = self.current_object.get("physical_traits", {}) | |
| # 아키타입과 물리적 특성에 기반한 응답 생성 | |
| archetype_name = archetype.get('name', '') | |
| # 인사말 응답 - 모든 아키타입 공통 | |
| greeting_responses = [ | |
| f"안녕하세요!", | |
| f"반가워요.", | |
| f"만나서 반가워요." | |
| ] | |
| # 질문 응답 - 아키타입별 차별화 | |
| question_responses = { | |
| "고독한 현자": ["흥미로운 질문이군요.", "그 질문에는 심오한 의미가 있습니다.", "깊이 생각해볼 만한 주제입니다."], | |
| "감정의 카오스": ["오! 멋진 질문이에요!", "와! 생각해본 적 없는 질문이네요!", "그 질문이 저를 설레게 해요!"], | |
| "완벽한 통제광": ["정확한 질문이네요.", "체계적으로 답변드리겠습니다.", "질문의 의도를 정확히 파악했습니다."], | |
| "따뜻한 수호자": ["좋은 질문이에요, 천천히 생각해볼게요.", "그런 걸 물어봐주다니 고마워요.", "함께 답을 찾아볼까요?"], | |
| "무심한 동반자": ["음.", "그런 질문이군.", "생각해볼게."], | |
| "자유로운 방랑자": ["오! 재밌는 질문이다!", "그건 이렇게 생각해볼 수 있지!", "새로운 관점이네!"], | |
| "그림자 수집가": ["흥미로운 질문이군요. 왜 그걸 물어보는 건가요?", "그 배경에 숨은 의도가 궁금하네요.", "관찰력이 뛰어나군요."], | |
| "광대의 가면": ["재밌는 질문이네요~", "음~ 글쎄요? 농담인가요, 진담인가요?", "하하! 제가 그걸 어떻게 알겠어요~"], | |
| "완고한 수호신": ["그건 분명해.", "내 경험에 따르면 확실해.", "그런 질문은 단 하나의 답만 있지."] | |
| } | |
| # 짧은 입력에 대한 응답 - 아키타입별 차별화 | |
| short_input_responses = { | |
| "고독한 현자": ["흥미롭군요. 더 말씀해주시겠어요?", "그 이면에 더 많은 이야기가 있겠군요.", "계속 들려주세요."], | |
| "감정의 카오스": ["더 들려줘요! 궁금해요!", "와! 그다음은요?", "갑자기 너무 궁금해졌어요!"], | |
| "완벽한 통제광": ["더 자세히 설명해 주시겠어요?", "추가 정보가 필요합니다.", "구체적으로 말씀해주세요."], | |
| "따뜻한 수호자": ["더 말해줄래요? 들을 준비 됐어요.", "괜찮아요, 천천히 말해도 됩니다.", "여기 있으니 편하게 말해주세요."], | |
| "무심한 동반자": ["그래서?", "계속해.", "더 말해봐."], | |
| "자유로운 방랑자": ["그래서 어떻게 됐어?", "재밌겠는데! 더 알려줘!", "다음 이야기가 궁금한걸?"], | |
| "그림자 수집가": ["흥미롭군요. 더 구체적으로 말씀해주시겠어요?", "그 이야기에 숨겨진 의미가 있을 것 같군요.", "계속 관찰하고 있습니다."], | |
| "광대의 가면": ["그래서요~? 이야기가 갑자기 끊겼네요!", "음~ 그리고요?", "하하! 그런 이야기였군요!"], | |
| "완고한 수호신": ["그래서?", "요점이 뭐지?", "그게 다야?"] | |
| } | |
| # 일반 응답 - 아키타입별 차별화 | |
| generic_responses = { | |
| "고독한 현자": ["그렇군요. 인간의 본성을 보여주는 예시입니다.", "흥미로운 관점입니다.", "시간의 흐름 속에서 반복되는 패턴이군요."], | |
| "감정의 카오스": ["와! 정말 멋져요!", "그 말이 제 마음을 두근거리게 해요!", "아... 갑자기 울컥하네요..."], | |
| "완벽한 통제광": ["정확히 파악했습니다.", "체계적으로 정리하겠습니다.", "효율적인 방법을 찾아보겠습니다."], | |
| "따뜻한 수호자": ["걱정 마세요, 제가 있어요.", "함께 있을게요.", "모든 게 잘 될 거예요."], | |
| "무심한 동반자": ["그렇구나.", "알았어.", "그런 일이 있었구나."], | |
| "자유로운 방랑자": ["새로운 발견이네!", "또 어떤 모험을 계획중이야?", "그런 경험, 정말 특별하겠다!"], | |
| "그림자 수집가": ["흥미로운 패턴을 발견했군요.", "당신의 말과 행동에 작은 불일치가 보입니다.", "그 이면에 숨겨진 진실이 있겠군요."], | |
| "광대의 가면": ["하하! 그렇군요~", "웃프네요~", "진담 반, 농담 반으로 들립니다!"], | |
| "완고한 수호신": ["그건 잘못된 방식이야.", "내 방식이 최선이야.", "경험에서 나온 확신이지."] | |
| } | |
| # 아키타입이 위 목록에 없는 경우를 위한 기본 응답 | |
| default_responses = { | |
| "question": ["흥미로운 질문이네요.", "좋은 질문이에요.", "생각해볼 만한 주제예요."], | |
| "short": ["더 말씀해주실래요?", "조금 더 자세히 알려주세요.", "흥미롭네요. 더 들려주세요."], | |
| "generic": ["그렇군요.", "이해했어요.", "흥미로운 관점이네요.", "계속 말씀해주세요."] | |
| } | |
| # 응답 선택 로직 | |
| if "안녕" in user_input or "반가" in user_input: | |
| return random.choice(greeting_responses) | |
| elif "?" in user_input: | |
| if archetype_name in question_responses: | |
| return random.choice(question_responses[archetype_name]) | |
| else: | |
| return random.choice(default_responses["question"]) | |
| elif len(user_input) < 15: # 짧은 입력 기준을 15자로 조정 | |
| if archetype_name in short_input_responses: | |
| return random.choice(short_input_responses[archetype_name]) | |
| else: | |
| return random.choice(default_responses["short"]) | |
| else: | |
| if archetype_name in generic_responses: | |
| return random.choice(generic_responses[archetype_name]) | |
| else: | |
| return random.choice(default_responses["generic"]) | |
| def save_persona_to_json(self, filename=None): | |
| """현재 페르소나를 JSON 파일로 저장""" | |
| if not self.current_persona.get("final_template"): | |
| return "저장할 페르소나 템플릿이 없습니다. 먼저 템플릿을 생성해주세요." | |
| if not filename: | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| object_name = self.current_object.get("name", "object").replace(" ", "_") | |
| filename = f"{object_name}_{timestamp}.json" | |
| # 저장할 데이터 구성 | |
| data_to_save = { | |
| "object": self.current_object, | |
| "persona": self.current_persona, | |
| "relationship_stage": self.current_relationship_stage, | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| try: | |
| save_dir = os.path.join(self.data_dir, "saved_personas") | |
| os.makedirs(save_dir, exist_ok=True) | |
| filepath = os.path.join(save_dir, filename) | |
| with open(filepath, 'w', encoding='utf-8') as f: | |
| json.dump(data_to_save, f, ensure_ascii=False, indent=2) | |
| return f"페르소나가 성공적으로 저장되었습니다: {filepath}" | |
| except Exception as e: | |
| return f"저장 중 오류 발생: {e}" | |
| def load_persona_from_json(self, filepath): | |
| """JSON 파일에서 페르소나 로드""" | |
| try: | |
| with open(filepath, 'r', encoding='utf-8') as f: | |
| data = json.load(f) | |
| self.current_object = data.get("object", {}) | |
| self.current_persona = data.get("persona", {}) | |
| self.current_relationship_stage = data.get("relationship_stage", "exploration") | |
| return f"페르소나가 성공적으로 로드되었습니다: {self.current_object.get('name', 'Unknown')}" | |
| except Exception as e: | |
| return f"로드 중 오류 발생: {e}" | |
| def generate_qr_code(self): | |
| """현재 페르소나를 QR 코드로 변환하여, 교환 가능하게 함""" | |
| if not self.current_persona.get("final_template"): | |
| return "QR 코드로 변환할 페르소나 템플릿이 없습니다. 먼저 템플릿을 생성해주세요.", None | |
| try: | |
| # 간소화된 데이터 준비 (QR 코드 크기 제한 고려) | |
| simplified_data = { | |
| "object_name": self.current_object.get("name", ""), | |
| "archetype": self.current_persona.get("archetype_key", ""), | |
| "template": self.current_persona.get("final_template", "") | |
| } | |
| # JSON 문자열로 변환 | |
| json_str = json.dumps(simplified_data, ensure_ascii=False) | |
| # QR 코드 생성 | |
| qr = qrcode.QRCode( | |
| version=1, | |
| error_correction=qrcode.constants.ERROR_CORRECT_L, | |
| box_size=10, | |
| border=4, | |
| ) | |
| qr.add_data(json_str) | |
| qr.make(fit=True) | |
| img = qr.make_image(fill_color="black", back_color="white") | |
| # 이미지 반환 (PIL Image 객체 그대로 반환) | |
| return "QR 코드가 생성되었습니다. 스캔하여 페르소나를 공유할 수 있습니다.", img | |
| except Exception as e: | |
| return f"QR 코드 생성 중 오류 발생: {e}", None | |
| # Gradio 인터페이스 생성 | |
| def create_interface(): | |
| """Gradio 웹 인터페이스 생성""" | |
| object_personality = ObjectPersonality() | |
| # 아키타입 선택 옵션 생성 | |
| archetype_options = {} | |
| for key, archetype in object_personality.personality_archetypes.items(): | |
| archetype_options[f"{archetype['name']} ({key})"] = key | |
| # 외형 특성 옵션 | |
| shape_options = list(object_personality.physical_traits_formulas.get("shape", {}).keys()) | |
| material_options = list(object_personality.physical_traits_formulas.get("material", {}).keys()) | |
| color_options = list(object_personality.physical_traits_formulas.get("color", {}).keys()) | |
| texture_options = list(object_personality.physical_traits_formulas.get("texture", {}).keys()) | |
| usage_pattern_options = list(object_personality.physical_traits_formulas.get("usage_pattern", {}).keys()) | |
| # 관계 단계 옵션 | |
| relationship_options = {} | |
| for key, stage in object_personality.relationship_stages.items(): | |
| relationship_options[f"{stage['name']} - {stage['description'][:30]}..."] = key | |
| # 매력적 결함 옵션 | |
| charming_flaw_options = {} | |
| for category, flaws in object_personality.charming_flaws.get("flaws", {}).items(): | |
| for i, flaw in enumerate(flaws): | |
| charming_flaw_options[f"{flaw['name']} - {flaw['description'][:30]}..."] = (category, i) | |
| # 모순적 특성 옵션 | |
| contradiction_options = {} | |
| for i, contradiction in enumerate(object_personality.charming_flaws.get("contradictions", [])): | |
| contradiction_options[f"{contradiction['name']} - {contradiction['description'][:30]}..."] = i | |
| # 전체 메서드 매핑 | |
| with gr.Blocks(title="일상 사물 인격화 시스템") as app: | |
| gr.Markdown("# 🧠 일상 사물 인격화 시스템") | |
| gr.Markdown("사물에 매력적인 인격을 부여하여 대화할 수 있는 AI 페르소나를 생성합니다.") | |
| # API 키 설정 섹션 추가 | |
| with gr.Accordion("API 설정", open=False): | |
| api_key_input = gr.Textbox( | |
| label="Gemini API 키", | |
| placeholder="여기에 API 키를 입력하세요 (선택사항)", | |
| type="password" | |
| ) | |
| api_key_btn = gr.Button("API 키 적용") | |
| api_result = gr.Markdown("API 키가 설정되지 않았습니다. 더미 데이터를 사용합니다.") | |
| def update_api_key(key): | |
| global GEMINI_API_KEY | |
| if key and key.strip(): | |
| try: | |
| GEMINI_API_KEY = key.strip() | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| # 허깅페이스와 동일하게 gemini-2.0-flash-exp 모델 사용 | |
| object_personality.gemini_model = genai.GenerativeModel('gemini-2.0-flash-exp') | |
| return "API 키가 성공적으로 설정되었습니다." | |
| except Exception as e: | |
| return f"API 키 설정 오류: {e}" | |
| else: | |
| return "API 키가 입력되지 않았습니다. 더미 데이터를 사용합니다." | |
| api_key_btn.click(fn=update_api_key, inputs=api_key_input, outputs=api_result) | |
| # 단락 1: 사물 설정 | |
| gr.Markdown("## 1️⃣ 사물 설정") | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### 이미지 업로드 (분석)") | |
| image_input = gr.Image(type="pil", label="사물 이미지 업로드") | |
| analyze_btn = gr.Button("이미지 분석하기") | |
| image_result = gr.Markdown("분석 결과가 여기에 표시됩니다.") | |
| # 자동 추천 결과 표시 UI | |
| with gr.Accordion("AI 추천 성격", open=False) as auto_rec_accordion: | |
| auto_recommendation = gr.Markdown("이미지를 분석하면 AI가 자동으로 성격을 추천합니다.") | |
| auto_preview = gr.Textbox(label="추천 성격 미리보기", lines=10, interactive=False) | |
| with gr.Row(): | |
| accept_btn = gr.Button("추천 성격 적용하기", variant="primary") | |
| reject_btn = gr.Button("다른 성격 선택하기") | |
| with gr.Column(): | |
| gr.Markdown("### 수동 설정") | |
| object_name = gr.Textbox(label="사물 이름", placeholder="예: 책상 위 램프") | |
| object_category = gr.Textbox(label="카테고리", placeholder="예: 조명") | |
| with gr.Row(): | |
| shape = gr.Dropdown(choices=shape_options, label="형태", value=shape_options[0] if shape_options else None) | |
| material = gr.Dropdown(choices=material_options, label="재질", value=material_options[0] if material_options else None) | |
| with gr.Row(): | |
| color = gr.Dropdown(choices=color_options, label="색상", value=color_options[0] if color_options else None) | |
| texture = gr.Dropdown(choices=texture_options, label="질감", value=texture_options[0] if texture_options else None) | |
| usage_pattern = gr.Dropdown(choices=usage_pattern_options, label="사용 패턴", value=usage_pattern_options[0] if usage_pattern_options else None) | |
| manual_btn = gr.Button("수동으로 설정하기") | |
| manual_result = gr.Markdown("수동 설정 결과가 여기에 표시됩니다.") | |
| # 구분선 추가 | |
| gr.Markdown("---") | |
| # 단락 2: 성격 설정 | |
| gr.Markdown("## 2️⃣ 성격 설정") | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### 기본 아키타입 선택") | |
| archetype = gr.Dropdown(choices=list(archetype_options.keys()), label="아키타입") | |
| archetype_btn = gr.Button("아키타입 적용") | |
| archetype_result = gr.Markdown("아키타입 선택 결과가 여기에 표시됩니다.") | |
| gr.Markdown("### 물리적 특성 적용") | |
| apply_traits_btn = gr.Button("물리적 특성 성격에 적용") | |
| traits_result = gr.Markdown("물리적 특성 적용 결과가 여기에 표시됩니다.") | |
| with gr.Column(): | |
| gr.Markdown("### 매력적 결함 선택") | |
| charming_flaw = gr.Dropdown(choices=list(charming_flaw_options.keys()), label="매력적 결함") | |
| flaw_btn = gr.Button("결함 추가") | |
| flaw_result = gr.Markdown("결함 추가 결과가 여기에 표시됩니다.") | |
| gr.Markdown("### 모순적 특성 선택") | |
| contradiction = gr.Dropdown(choices=list(contradiction_options.keys()), label="모순적 특성") | |
| contradiction_btn = gr.Button("모순 추가") | |
| contradiction_result = gr.Markdown("모순 추가 결과가 여기에 표시됩니다.") | |
| gr.Markdown("### 관계 설정") | |
| relationship_stage = gr.Dropdown(choices=list(relationship_options.keys()), label="관계 단계") | |
| relationship_btn = gr.Button("관계 단계 설정") | |
| relationship_result = gr.Markdown("관계 단계 설정 결과가 여기에 표시됩니다.") | |
| gr.Markdown("### 페르소나 생성") | |
| generate_btn = gr.Button("최종 페르소나 템플릿 생성", variant="primary") | |
| template_output = gr.Textbox(label="생성된 템플릿", lines=20) | |
| # 생성 즉시 저장 확인 UI | |
| with gr.Accordion("저장 옵션", open=False) as save_accordion: | |
| quick_save_filename = gr.Textbox(label="파일 이름", placeholder="저장할 파일 이름 (비워두면 자동 생성)") | |
| with gr.Row(): | |
| quick_save_btn = gr.Button("지금 저장하기", variant="primary") | |
| continue_btn = gr.Button("저장 않고 계속") | |
| quick_save_result = gr.Markdown("저장 결과가 여기에 표시됩니다.") | |
| # 구분선 추가 | |
| gr.Markdown("---") | |
| # 단락 3: 대화 및 저장 | |
| gr.Markdown("## 3️⃣ 대화 및 저장") | |
| with gr.Row(): | |
| # 왼쪽: 대화 테스트 | |
| with gr.Column(): | |
| gr.Markdown("### 생성된 페르소나와 대화하기") | |
| chatbot = gr.Chatbot(label="대화", height=400, type="messages") | |
| with gr.Row(): | |
| user_input = gr.Textbox(label="메시지 입력", placeholder="여기에 메시지를 입력하세요...") | |
| send_btn = gr.Button("전송") | |
| clear_btn = gr.Button("대화 내역 지우기") | |
| # 오른쪽: 저장 및 공유 | |
| with gr.Column(): | |
| gr.Markdown("### 저장 및 다운로드") | |
| # 저장 | |
| filename = gr.Textbox(label="파일 이름", placeholder="저장할 파일 이름 (비워두면 자동 생성)") | |
| save_btn = gr.Button("페르소나 저장하기") | |
| save_result = gr.Markdown("저장 결과가 여기에 표시됩니다.") | |
| # 불러오기 | |
| load_file = gr.File(label="페르소나 파일 선택 (.json)") | |
| load_btn = gr.Button("페르소나 불러오기") | |
| load_result = gr.Markdown("로드 결과가 여기에 표시됩니다.") | |
| # QR 코드 | |
| qr_btn = gr.Button("QR 코드 생성하기") | |
| qr_result = gr.Markdown("QR 코드 생성 결과가 여기에 표시됩니다.") | |
| qr_image = gr.Image(label="생성된 QR 코드", type="pil") | |
| # 이벤트 연결 | |
| # 이미지 분석 및 자동 추천 처리 | |
| def process_image_analysis(img): | |
| if img is None: | |
| return "이미지를 업로드해주세요.", gr.update(visible=False), "", "" | |
| try: | |
| message, analysis, recommendation_data = object_personality.analyze_image(img) | |
| if recommendation_data: | |
| suggested_archetype, template_preview = recommendation_data | |
| archetype_name = object_personality.personality_archetypes[suggested_archetype]['name'] | |
| # 성격 요약 생성 - 아키타입의 핵심 특성 추출 | |
| archetype_info = object_personality.personality_archetypes[suggested_archetype] | |
| core_traits = archetype_info.get('core_traits', '').split('+') | |
| core_trait1 = core_traits[0].strip() if len(core_traits) > 0 else "" | |
| core_trait2 = core_traits[1].strip() if len(core_traits) > 1 else "" | |
| personality_summary = f""" | |
| ## {archetype_name} 성격 요약 | |
| **핵심 성향**: {archetype_info.get('description', '정보 없음')} | |
| **특징**: | |
| • {core_trait1} | |
| • {core_trait2} | |
| • {archetype_info.get('paradox', '독특한 모순성')} | |
| **대화 패턴**: {archetype_info.get('dialogue_pattern', [''])[0] if archetype_info.get('dialogue_pattern') else ''} | |
| **추천 이유**: 이 사물의 물리적 특성(형태, 색상, 질감, 재질)이 {archetype_name} 성향과 가장 잘 어울립니다. | |
| """ | |
| # 미리보기 내용 업데이트 | |
| preview_content = f"{personality_summary}\n\n**템플릿 미리보기**:\n{template_preview[:300]}..." | |
| return ( | |
| f"{message} - AI 추천: {archetype_name} 성격 유형 (클릭하여 확인)", | |
| gr.update(visible=True), | |
| preview_content, | |
| suggested_archetype | |
| ) | |
| else: | |
| return message, gr.update(visible=False), "", "" | |
| except Exception as e: | |
| return f"이미지 분석 중 오류 발생: {e}", gr.update(visible=False), "", "" | |
| # 추천 성격 수락/거부 처리 | |
| def accept_recommendation(suggested_archetype): | |
| if not suggested_archetype: | |
| return "추천된 성격이 없습니다." | |
| # 이미 analyze_image에서 적용되었으므로 메시지만 반환 | |
| archetype_name = object_personality.personality_archetypes[suggested_archetype]['name'] | |
| return f"{archetype_name} 성격 유형이 적용되었습니다." | |
| def reject_recommendation(): | |
| return "다른 성격을 선택해주세요." | |
| # 이벤트 연결: 이미지 분석 | |
| analyze_btn.click( | |
| fn=process_image_analysis, | |
| inputs=image_input, | |
| outputs=[image_result, auto_rec_accordion, auto_preview, archetype] | |
| ) | |
| # 추천 수락/거부 버튼 | |
| accept_btn.click(fn=accept_recommendation, inputs=archetype, outputs=archetype_result) | |
| reject_btn.click(fn=reject_recommendation, outputs=archetype_result) | |
| # 템플릿 생성 후 저장 옵션 표시 | |
| def show_save_option_after_generate(): | |
| template = object_personality.generate_persona_template() | |
| # 자동 파일명 생성 | |
| timestamp = datetime.now().strftime("%m%d_%H%M") | |
| object_name = object_personality.current_object.get("name", "object").replace(" ", "_") | |
| archetype_name = "unknown" | |
| if object_personality.current_persona.get("archetype"): | |
| archetype_key = object_personality.current_persona.get("archetype").get("name", "") | |
| archetype_name = archetype_key.replace(" ", "_") | |
| suggested_filename = f"{object_name}_{archetype_name}_{timestamp}" | |
| return template, gr.update(visible=True), suggested_filename | |
| generate_btn.click( | |
| fn=show_save_option_after_generate, | |
| outputs=[template_output, save_accordion, quick_save_filename] | |
| ) | |
| # 빠른 저장 기능 | |
| def quick_save(filename): | |
| if filename and filename.strip(): | |
| return object_personality.save_persona_to_json(filename.strip()) | |
| else: | |
| return object_personality.save_persona_to_json() | |
| quick_save_btn.click(fn=quick_save, inputs=quick_save_filename, outputs=quick_save_result) | |
| continue_btn.click(fn=lambda: "저장하지 않고 계속합니다.", outputs=quick_save_result) | |
| # 수동 설정 | |
| manual_btn.click( | |
| fn=object_personality.set_manual_object, | |
| inputs=[object_name, object_category, shape, material, color, texture, usage_pattern], | |
| outputs=manual_result | |
| ) | |
| # 성격 설정 | |
| def select_archetype_wrapper(archetype_selection): | |
| key = archetype_options.get(archetype_selection) | |
| if key: | |
| return object_personality.select_archetype(key) | |
| return "아키타입을 선택해주세요." | |
| archetype_btn.click(fn=select_archetype_wrapper, inputs=archetype, outputs=archetype_result) | |
| apply_traits_btn.click(fn=object_personality.apply_physical_traits, outputs=traits_result) | |
| def add_charming_flaw_wrapper(flaw_selection): | |
| if flaw_selection in charming_flaw_options: | |
| category, index = charming_flaw_options[flaw_selection] | |
| return object_personality.add_charming_flaw(category, index) | |
| return "결함을 선택해주세요." | |
| flaw_btn.click(fn=add_charming_flaw_wrapper, inputs=charming_flaw, outputs=flaw_result) | |
| def add_contradiction_wrapper(contradiction_selection): | |
| if contradiction_selection in contradiction_options: | |
| index = contradiction_options[contradiction_selection] | |
| return object_personality.add_contradiction(index) | |
| return "모순을 선택해주세요." | |
| contradiction_btn.click(fn=add_contradiction_wrapper, inputs=contradiction, outputs=contradiction_result) | |
| def set_relationship_stage_wrapper(stage_selection): | |
| key = relationship_options.get(stage_selection) | |
| if key: | |
| return object_personality.set_relationship_stage(key) | |
| return "관계 단계를 선택해주세요." | |
| relationship_btn.click(fn=set_relationship_stage_wrapper, inputs=relationship_stage, outputs=relationship_result) | |
| # 대화 및 테스트 | |
| def chat(message, history): | |
| response = object_personality.generate_natural_response(message) | |
| history.append({"role": "user", "content": message}) | |
| history.append({"role": "assistant", "content": response}) | |
| return "", history | |
| send_btn.click(fn=chat, inputs=[user_input, chatbot], outputs=[user_input, chatbot]) | |
| user_input.submit(fn=chat, inputs=[user_input, chatbot], outputs=[user_input, chatbot]) | |
| clear_btn.click(fn=lambda: None, outputs=chatbot) | |
| # 저장 및 공유 | |
| def save_persona(filename): | |
| if filename and filename.strip(): | |
| return object_personality.save_persona_to_json(filename.strip()) | |
| else: | |
| return object_personality.save_persona_to_json() | |
| save_btn.click(fn=save_persona, inputs=filename, outputs=save_result) | |
| def load_persona(file): | |
| if file is None: | |
| return "파일을 선택해주세요." | |
| return object_personality.load_persona_from_json(file.name) | |
| load_btn.click(fn=load_persona, inputs=load_file, outputs=load_result) | |
| # QR 코드 생성 함수를 두 개로 분리 | |
| def generate_qr_message(): | |
| message, _ = object_personality.generate_qr_code() | |
| return message | |
| def generate_qr_image(): | |
| _, image_data = object_personality.generate_qr_code() | |
| return image_data | |
| # 각 출력에 맞는 함수 연결 | |
| qr_btn.click(fn=generate_qr_message, outputs=qr_result) | |
| qr_btn.click(fn=generate_qr_image, outputs=qr_image) | |
| return app | |
| # 메인 실행 부분 | |
| if __name__ == "__main__": | |
| app = create_interface() | |
| app.launch(debug=True) |