personatest2 / app.py
haepada's picture
Update app.py
6e8c6ba verified
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)