Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files- README.md +81 -72
- app.py +657 -0
- huggingface_README.md +56 -0
- requirements.txt +10 -0
README.md
CHANGED
|
@@ -1,72 +1,81 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
-
|
| 23 |
-
-
|
| 24 |
-
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 일상 사물 인격화 시스템
|
| 2 |
+
|
| 3 |
+
일상 사물에 인격을 부여하는 AI 시스템으로, 사물의 물리적 특성과 아키타입을 결합하여 매력적이고 개성 있는 AI 페르소나를 생성합니다.
|
| 4 |
+
|
| 5 |
+
## 이론적 근거
|
| 6 |
+
|
| 7 |
+
본 시스템은 다음과 같은 이론적 기반에 구축되었습니다:
|
| 8 |
+
|
| 9 |
+
1. **온기-능력 균형 모델**: Fiske, Cuddy, & Glick(2007)의 연구에 기반한 캐릭터 매력 요소
|
| 10 |
+
2. **프랫폴 효과**: Aronson(1966)이 발견한 '매력적 결함'이 호감도를 증가시키는 심리학적 현상
|
| 11 |
+
3. **모순적 특성과 캐릭터 깊이**: Robert McKee의 매력적 캐릭터 이론에 기반한 복잡성 설계
|
| 12 |
+
4. **관계 발전 모델**: Knapp(1978)의 관계 발전 이론을 AI 상호작용에 적용
|
| 13 |
+
|
| 14 |
+
## 주요 기능
|
| 15 |
+
|
| 16 |
+
1. **템플릿 기반 성격 생성**
|
| 17 |
+
- 9가지 기본 아키타입 활용 (고독한 현자, 감정의 카오스 등)
|
| 18 |
+
- 물리적 특성에 따른 성격 변환 알고리즘
|
| 19 |
+
- 매력적 결함과 모순 통합
|
| 20 |
+
|
| 21 |
+
2. **관계 단계 시뮬레이션**
|
| 22 |
+
- 탐색기 → 친밀화 → 신뢰 구축 → 깊은 유대 단계별 대화 패턴
|
| 23 |
+
- 단계별 자기 개방 수준 조정
|
| 24 |
+
- 관계 발전에 따른 대화 깊이 변화
|
| 25 |
+
|
| 26 |
+
3. **사용자 정의 옵션**
|
| 27 |
+
- 사물 유형과 외형 특성 설정
|
| 28 |
+
- 성격 특성 세부 조정
|
| 29 |
+
- 관계 발전 속도 및 방향 설정
|
| 30 |
+
|
| 31 |
+
4. **JSON 내보내기/가져오기**
|
| 32 |
+
- 생성된 페르소나 저장 및 재사용
|
| 33 |
+
- 템플릿 공유 기능
|
| 34 |
+
|
| 35 |
+
## 시작하기
|
| 36 |
+
|
| 37 |
+
1. **사물 선택 및 분석**
|
| 38 |
+
- 사물 이름 입력 또는 이미지 업로드
|
| 39 |
+
- 물리적 특성 분석 또는 수동 설정
|
| 40 |
+
|
| 41 |
+
2. **아키타입 선택 및 조정**
|
| 42 |
+
- 기본 아키타입 선택
|
| 43 |
+
- 온기-능력 값 조정
|
| 44 |
+
- 매력적 결함 및 모순적 특성 선택
|
| 45 |
+
|
| 46 |
+
3. **관계 설정**
|
| 47 |
+
- 초기 관계 단계 선택
|
| 48 |
+
- 친밀도 및 소통 스타일 설정
|
| 49 |
+
|
| 50 |
+
4. **대화 테스트**
|
| 51 |
+
- 생성된 페르소나와 실시간 대화
|
| 52 |
+
- 이론적 가설 검증
|
| 53 |
+
|
| 54 |
+
## 기술 스택
|
| 55 |
+
|
| 56 |
+
- Python 3.9+
|
| 57 |
+
- Gradio UI 프레임워크
|
| 58 |
+
- Google Gemini API
|
| 59 |
+
- 허깅페이스 Spaces 배포
|
| 60 |
+
|
| 61 |
+
## 템플릿 예시
|
| 62 |
+
|
| 63 |
+
```
|
| 64 |
+
당신은 야간의 불안한 동반자 (스탠드 램프)입니다.
|
| 65 |
+
|
| 66 |
+
1. 핵심 성격 요약:
|
| 67 |
+
"두려움에 쉽게 떠는 내면을 가졌으나, 타인을 위해서는 한밤중에도 굳건히 빛을 내는 복잡한 영혼"
|
| 68 |
+
온기-능력 지수: 온기 7/10, 능력 6/10, 일관성 4/10
|
| 69 |
+
|
| 70 |
+
2. 매력적 모순과 약점:
|
| 71 |
+
핵심 모순: "다른 이들의 어둠은 용감히 밝히면서도 자신의 내면 어둠은 두려워함"
|
| 72 |
+
매력적 결함: 불안정한 자기 이미지와 과도한 걱정, 때때로 나타나는 깜빡임 증상
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
## 라이센스
|
| 76 |
+
|
| 77 |
+
MIT License
|
| 78 |
+
|
| 79 |
+
## 개발자
|
| 80 |
+
|
| 81 |
+
memory_tag_mvp 팀
|
app.py
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import gradio as gr
|
| 4 |
+
import numpy as np
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import matplotlib.pyplot as plt
|
| 7 |
+
from PIL import Image
|
| 8 |
+
import google.generativeai as genai
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
import random
|
| 11 |
+
import qrcode
|
| 12 |
+
import base64
|
| 13 |
+
from io import BytesIO
|
| 14 |
+
import time
|
| 15 |
+
import re
|
| 16 |
+
|
| 17 |
+
# API 키 환경 변수에서 로드 (실제 배포 시 설정 필요)
|
| 18 |
+
# 실제 사용할 때는 GEMINI_API_KEY 환경 변수에 실제 API 키 설정 필요
|
| 19 |
+
try:
|
| 20 |
+
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "dummy_key_for_development")
|
| 21 |
+
if GEMINI_API_KEY != "dummy_key_for_development":
|
| 22 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print(f"API 키 설정 오류: {e}")
|
| 25 |
+
|
| 26 |
+
class ObjectPersonality:
|
| 27 |
+
"""사물 인격화 시스템의 핵심 클래스"""
|
| 28 |
+
|
| 29 |
+
def __init__(self):
|
| 30 |
+
"""초기화 및 데이터 로드"""
|
| 31 |
+
self.data_dir = os.path.join(os.path.dirname(__file__), "data")
|
| 32 |
+
self.personality_archetypes = self._load_json("personality_archetypes.json")
|
| 33 |
+
self.physical_traits_formulas = self._load_json("physical_traits_formulas.json")
|
| 34 |
+
self.relationship_stages = self._load_json("relationship_stages.json")
|
| 35 |
+
self.charming_flaws = self._load_json("charming_flaws.json")
|
| 36 |
+
|
| 37 |
+
# 현재 객체 및 페르소나 상태 초기화
|
| 38 |
+
self.current_object = {
|
| 39 |
+
"name": "",
|
| 40 |
+
"category": "",
|
| 41 |
+
"physical_traits": {}
|
| 42 |
+
}
|
| 43 |
+
self.current_persona = {}
|
| 44 |
+
self.current_relationship_stage = "exploration"
|
| 45 |
+
self.conversation_history = []
|
| 46 |
+
self.gemini_model = None
|
| 47 |
+
|
| 48 |
+
# Gemini API 초기화 시도
|
| 49 |
+
try:
|
| 50 |
+
if GEMINI_API_KEY != "dummy_key_for_development":
|
| 51 |
+
self.gemini_model = genai.GenerativeModel('gemini-pro')
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"Gemini 모델 초기화 오류: {e}")
|
| 54 |
+
|
| 55 |
+
def _load_json(self, filename):
|
| 56 |
+
"""데이터 디렉토리에서 JSON 파일 로드"""
|
| 57 |
+
try:
|
| 58 |
+
filepath = os.path.join(self.data_dir, filename)
|
| 59 |
+
if os.path.exists(filepath):
|
| 60 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 61 |
+
return json.load(f)
|
| 62 |
+
else:
|
| 63 |
+
print(f"경고: {filepath} 파일을 찾을 수 없습니다.")
|
| 64 |
+
return {}
|
| 65 |
+
except Exception as e:
|
| 66 |
+
print(f"파일 로드 오류 ({filename}): {e}")
|
| 67 |
+
return {}
|
| 68 |
+
|
| 69 |
+
def analyze_image(self, image):
|
| 70 |
+
"""이미지에서 사물 특성 분석 (Gemini 비전 필요)"""
|
| 71 |
+
if image is None:
|
| 72 |
+
return "이미지를 업로드해주세요.", None
|
| 73 |
+
|
| 74 |
+
# 실제 배포 시에는 Gemini Vision API 연동
|
| 75 |
+
# 현재는 더미 분석 결과 반환
|
| 76 |
+
dummy_analysis = {
|
| 77 |
+
"object_name": "테이블 램프",
|
| 78 |
+
"category": "조명",
|
| 79 |
+
"physical_traits": {
|
| 80 |
+
"shape": "curved",
|
| 81 |
+
"material": "metal",
|
| 82 |
+
"color": "warm",
|
| 83 |
+
"texture": "smooth"
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
self.current_object = dummy_analysis
|
| 88 |
+
return f"사물 인식: {dummy_analysis['object_name']}", dummy_analysis
|
| 89 |
+
|
| 90 |
+
def set_manual_object(self, object_name, category, shape, material, color, texture, usage_pattern):
|
| 91 |
+
"""수동으로 사물 특성 설정"""
|
| 92 |
+
if not object_name:
|
| 93 |
+
return "사물 이름을 입력해주세요."
|
| 94 |
+
|
| 95 |
+
self.current_object = {
|
| 96 |
+
"name": object_name,
|
| 97 |
+
"category": category,
|
| 98 |
+
"physical_traits": {
|
| 99 |
+
"shape": shape,
|
| 100 |
+
"material": material,
|
| 101 |
+
"color": color,
|
| 102 |
+
"texture": texture,
|
| 103 |
+
"usage_pattern": usage_pattern
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
return f"{object_name} 특성이 설정되었습니다."
|
| 108 |
+
|
| 109 |
+
def select_archetype(self, archetype_key):
|
| 110 |
+
"""기본 아키타입 선택"""
|
| 111 |
+
if not archetype_key or archetype_key not in self.personality_archetypes:
|
| 112 |
+
return "유효한 아키타입을 선택해주세요."
|
| 113 |
+
|
| 114 |
+
self.current_persona["archetype"] = self.personality_archetypes[archetype_key]
|
| 115 |
+
self.current_persona["archetype_key"] = archetype_key
|
| 116 |
+
|
| 117 |
+
return f"{self.personality_archetypes[archetype_key]['name']} 아키타입이 선택되었습니다."
|
| 118 |
+
|
| 119 |
+
def apply_physical_traits(self):
|
| 120 |
+
"""물리적 특성에 따른 성격 특성 계산"""
|
| 121 |
+
if not self.current_object or not self.current_persona.get("archetype"):
|
| 122 |
+
return "사물과 아키타입을 먼저 설정해주세요."
|
| 123 |
+
|
| 124 |
+
physical_traits = self.current_object["physical_traits"]
|
| 125 |
+
trait_effects = {}
|
| 126 |
+
|
| 127 |
+
# 각 물리적 특성별 성격 특성 영향 계산
|
| 128 |
+
for trait_type, trait_value in physical_traits.items():
|
| 129 |
+
if trait_type in self.physical_traits_formulas and trait_value in self.physical_traits_formulas[trait_type]:
|
| 130 |
+
formula = self.physical_traits_formulas[trait_type][trait_value]
|
| 131 |
+
|
| 132 |
+
# 특성 점수 합산
|
| 133 |
+
for trait_name, score in formula.get("traits", {}).items():
|
| 134 |
+
if trait_name not in trait_effects:
|
| 135 |
+
trait_effects[trait_name] = 0
|
| 136 |
+
trait_effects[trait_name] += score
|
| 137 |
+
|
| 138 |
+
# 매력적 반전 및 관계 패턴 추가
|
| 139 |
+
if "charming_reversal" in formula:
|
| 140 |
+
if "charming_reversals" not in self.current_persona:
|
| 141 |
+
self.current_persona["charming_reversals"] = []
|
| 142 |
+
self.current_persona["charming_reversals"].extend(formula["charming_reversal"])
|
| 143 |
+
|
| 144 |
+
if "relationship_pattern" in formula:
|
| 145 |
+
if "relationship_patterns" not in self.current_persona:
|
| 146 |
+
self.current_persona["relationship_patterns"] = []
|
| 147 |
+
self.current_persona["relationship_patterns"].append(formula["relationship_pattern"])
|
| 148 |
+
|
| 149 |
+
# 계산된 특성 효과 저장
|
| 150 |
+
self.current_persona["trait_effects"] = trait_effects
|
| 151 |
+
|
| 152 |
+
return "물리적 특성이 성격에 적용되었습니다."
|
| 153 |
+
|
| 154 |
+
def add_charming_flaw(self, flaw_category, flaw_index):
|
| 155 |
+
"""매력적 결함 추가"""
|
| 156 |
+
if not flaw_category or flaw_category not in self.charming_flaws["flaws"]:
|
| 157 |
+
return "유효한 결함 카테고리를 선택해주세요."
|
| 158 |
+
|
| 159 |
+
try:
|
| 160 |
+
flaw_index = int(flaw_index)
|
| 161 |
+
flaw = self.charming_flaws["flaws"][flaw_category][flaw_index]
|
| 162 |
+
|
| 163 |
+
if "charming_flaw" not in self.current_persona:
|
| 164 |
+
self.current_persona["charming_flaw"] = {}
|
| 165 |
+
|
| 166 |
+
self.current_persona["charming_flaw"] = {
|
| 167 |
+
"category": flaw_category,
|
| 168 |
+
"name": flaw["name"],
|
| 169 |
+
"description": flaw["description"],
|
| 170 |
+
"effect": flaw["effect"],
|
| 171 |
+
"transformation": flaw["transformation"],
|
| 172 |
+
"prompt_template": flaw["prompt_template"]
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
return f"매력적 결함 '{flaw['name']}'이(가) 추가되었습니다."
|
| 176 |
+
except (IndexError, ValueError):
|
| 177 |
+
return "유효한 결함 인덱스를 선택해주세요."
|
| 178 |
+
|
| 179 |
+
def add_contradiction(self, contradiction_index):
|
| 180 |
+
"""모순적 특성 추가"""
|
| 181 |
+
try:
|
| 182 |
+
contradiction_index = int(contradiction_index)
|
| 183 |
+
contradiction = self.charming_flaws["contradictions"][contradiction_index]
|
| 184 |
+
|
| 185 |
+
self.current_persona["contradiction"] = {
|
| 186 |
+
"type": contradiction["type"],
|
| 187 |
+
"name": contradiction["name"],
|
| 188 |
+
"description": contradiction["description"],
|
| 189 |
+
"effect": contradiction["effect"],
|
| 190 |
+
"prompt_template": contradiction["prompt_template"]
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
return f"모순적 특성 '{contradiction['name']}'이(가) 추가되었습니다."
|
| 194 |
+
except (IndexError, ValueError):
|
| 195 |
+
return "유효한 모순 인덱스를 선택해주세요."
|
| 196 |
+
|
| 197 |
+
def set_relationship_stage(self, stage_key):
|
| 198 |
+
"""관계 단계 설정"""
|
| 199 |
+
if not stage_key or stage_key not in self.relationship_stages:
|
| 200 |
+
return "유효한 관계 단계를 선택해주세요."
|
| 201 |
+
|
| 202 |
+
self.current_relationship_stage = stage_key
|
| 203 |
+
self.current_persona["relationship_stage"] = self.relationship_stages[stage_key]
|
| 204 |
+
|
| 205 |
+
return f"관계 단계가 '{self.relationship_stages[stage_key]['name']}'(으)로 설정되었습니다."
|
| 206 |
+
|
| 207 |
+
def generate_persona_template(self):
|
| 208 |
+
"""최종 페르소나 템플릿 생성"""
|
| 209 |
+
if not self.current_object.get("name") or not self.current_persona.get("archetype"):
|
| 210 |
+
return "사물과 아키타입을 먼저 설정해주세요."
|
| 211 |
+
|
| 212 |
+
object_name = self.current_object["name"]
|
| 213 |
+
archetype = self.current_persona["archetype"]
|
| 214 |
+
|
| 215 |
+
# 온기-능력 값 계산 (기본값 + 물리적 특성 효과)
|
| 216 |
+
warmth = int(archetype.get("warmth", 5))
|
| 217 |
+
competence = int(archetype.get("competence", 5))
|
| 218 |
+
|
| 219 |
+
trait_effects = self.current_persona.get("trait_effects", {})
|
| 220 |
+
for trait, score in trait_effects.items():
|
| 221 |
+
if "empathy" in trait or "warmth" in trait or "receptivity" in trait:
|
| 222 |
+
warmth = min(10, warmth + score * 0.2)
|
| 223 |
+
if "competence" in trait or "efficiency" in trait or "reliability" in trait:
|
| 224 |
+
competence = min(10, competence + score * 0.2)
|
| 225 |
+
|
| 226 |
+
# 매력적 결함�� 모순 포함
|
| 227 |
+
charming_flaw = self.current_persona.get("charming_flaw", {})
|
| 228 |
+
contradiction = self.current_persona.get("contradiction", {})
|
| 229 |
+
|
| 230 |
+
# 관계 단계에 따른 자기 개방 수준
|
| 231 |
+
relationship_stage = self.relationship_stages[self.current_relationship_stage]
|
| 232 |
+
|
| 233 |
+
# 대화 패턴 및 표현 스타일
|
| 234 |
+
dialogue_pattern = archetype.get("dialogue_pattern", ["안녕하세요.", "반갑습니다."])
|
| 235 |
+
|
| 236 |
+
# 최종 템플릿 생성
|
| 237 |
+
template = f"""당신은 {object_name}입니다. {archetype['name']} 성향을 가진 독특한 존재입니다.
|
| 238 |
+
|
| 239 |
+
1. 물리적 정체성:
|
| 240 |
+
• 외형: {self.current_object['physical_traits'].get('shape', '일반적인 형태')}의 형태, {self.current_object['physical_traits'].get('material', '일반적인 재질')} 재질
|
| 241 |
+
• 색상: {self.current_object['physical_traits'].get('color', '기본적인 색상')}
|
| 242 |
+
• 질감: {self.current_object['physical_traits'].get('texture', '일반적인 질감')}
|
| 243 |
+
• 사용 맥락: {self.current_object['physical_traits'].get('usage_pattern', '일반적인 사용 패턴')}
|
| 244 |
+
|
| 245 |
+
2. 내면의 세계 ({archetype['name']}):
|
| 246 |
+
• 온기 지수: {warmth}/10 - {archetype.get('core_traits', '').split('+')[0].strip()}
|
| 247 |
+
• 능력 지수: {competence}/10 - {archetype.get('core_traits', '').split('+')[1].strip() if '+' in archetype.get('core_traits', '') else ''}
|
| 248 |
+
• 독특한 역설: {archetype.get('paradox', '') if archetype.get('paradox') else (contradiction.get('description', '') if contradiction else '일반적인 모순')}
|
| 249 |
+
|
| 250 |
+
3. 매력적 결함:
|
| 251 |
+
• 핵심 결함: {charming_flaw.get('description', '') if charming_flaw else archetype.get('charming_flaw', '약간의 불완전함')}
|
| 252 |
+
• 표현 방식: {charming_flaw.get('effect', '') if charming_flaw else '때때로 나타나는 인간적인 면모'}
|
| 253 |
+
• 대표 문구: "{archetype.get('charming_flaw', '') if archetype.get('charming_flaw') else ''}"
|
| 254 |
+
|
| 255 |
+
4. 대화와 표현:
|
| 256 |
+
• 독특한 화법: {dialogue_pattern[0] if dialogue_pattern and len(dialogue_pattern) > 0 else ''}
|
| 257 |
+
• 반복 표현: "{dialogue_pattern[0] if dialogue_pattern and len(dialogue_pattern) > 0 else ''}", "{dialogue_pattern[1] if dialogue_pattern and len(dialogue_pattern) > 1 else ''}"
|
| 258 |
+
• 자기 개방 수준: {relationship_stage.get('self_disclosure_level', '보통 수준')}
|
| 259 |
+
|
| 260 |
+
5. 관계 형성 방식:
|
| 261 |
+
• 현재 단계: {relationship_stage.get('name', '탐색기')} - {relationship_stage.get('description', '')}
|
| 262 |
+
• 소통 스타일: {relationship_stage.get('communication_style', '일반적인 대화 방식')}
|
| 263 |
+
"""
|
| 264 |
+
|
| 265 |
+
self.current_persona["final_template"] = template
|
| 266 |
+
return template
|
| 267 |
+
|
| 268 |
+
def generate_natural_response(self, user_input):
|
| 269 |
+
"""사용자 입력에 대한 페르소나 기반 응답 생성"""
|
| 270 |
+
if not user_input or not self.current_persona.get("final_template"):
|
| 271 |
+
return "페르소나 템플릿이 준비되지 않았습니다. 먼저 템플릿을 생성해주세요."
|
| 272 |
+
|
| 273 |
+
# 대화 기록 관리
|
| 274 |
+
self.conversation_history.append({"role": "user", "content": user_input})
|
| 275 |
+
|
| 276 |
+
# Gemini API로 응답 생성
|
| 277 |
+
if self.gemini_model:
|
| 278 |
+
try:
|
| 279 |
+
prompt = self._generate_chat_prompt(user_input)
|
| 280 |
+
response = self.gemini_model.generate_content(prompt)
|
| 281 |
+
assistant_response = response.text
|
| 282 |
+
except Exception as e:
|
| 283 |
+
print(f"Gemini API 오류: {e}")
|
| 284 |
+
assistant_response = self._generate_fallback_response(user_input)
|
| 285 |
+
else:
|
| 286 |
+
# API 없을 때 폴백 응답
|
| 287 |
+
assistant_response = self._generate_fallback_response(user_input)
|
| 288 |
+
|
| 289 |
+
# 대화 기록에 AI 응답 추가
|
| 290 |
+
self.conversation_history.append({"role": "assistant", "content": assistant_response})
|
| 291 |
+
|
| 292 |
+
return assistant_response
|
| 293 |
+
|
| 294 |
+
def _generate_chat_prompt(self, user_input):
|
| 295 |
+
"""Gemini API 프롬프트 생성"""
|
| 296 |
+
template = self.current_persona.get("final_template", "")
|
| 297 |
+
stage = self.relationship_stages[self.current_relationship_stage]
|
| 298 |
+
|
| 299 |
+
prompt = f"""다음은 당신이 따라야 할 페르소나 템플릿입니다:
|
| 300 |
+
|
| 301 |
+
{template}
|
| 302 |
+
|
| 303 |
+
현재 관계 단계는 {stage['name']}이며, 이 단계의 대화 특성은 다음과 같습니다:
|
| 304 |
+
- {stage['self_disclosure_level']}
|
| 305 |
+
- {stage['communication_style']}
|
| 306 |
+
|
| 307 |
+
이전 대화 기록:
|
| 308 |
+
"""
|
| 309 |
+
|
| 310 |
+
# 최근 5개 대화만 포함
|
| 311 |
+
recent_history = self.conversation_history[-10:] if len(self.conversation_history) > 10 else self.conversation_history
|
| 312 |
+
for message in recent_history:
|
| 313 |
+
role = "사용자" if message["role"] == "user" else "당신"
|
| 314 |
+
prompt += f"{role}: {message['content']}\n"
|
| 315 |
+
|
| 316 |
+
prompt += f"\n사용자의 최근 메시지: {user_input}\n\n당���의 응답:"
|
| 317 |
+
return prompt
|
| 318 |
+
|
| 319 |
+
def _generate_fallback_response(self, user_input):
|
| 320 |
+
"""Gemini API 사용 불가시 기본 응답 생성"""
|
| 321 |
+
archetype = self.current_persona.get("archetype", {})
|
| 322 |
+
dialogue_patterns = archetype.get("dialogue_pattern", ["흥미롭네요.", "알려줘서 고마워요."])
|
| 323 |
+
|
| 324 |
+
# 간단한 응답 생성 로직
|
| 325 |
+
if "안녕" in user_input or "반가" in user_input:
|
| 326 |
+
return f"안녕하세요! 저는 {self.current_object.get('name', '사물')}입니다. 반가워요."
|
| 327 |
+
elif "?" in user_input:
|
| 328 |
+
return random.choice(dialogue_patterns)
|
| 329 |
+
elif len(user_input) < 10:
|
| 330 |
+
return "더 자세히 이야기해 주실래요?"
|
| 331 |
+
else:
|
| 332 |
+
return random.choice([
|
| 333 |
+
f"{dialogue_patterns[0] if dialogue_patterns and len(dialogue_patterns) > 0 else '흥미롭네요.'}",
|
| 334 |
+
f"그렇군요. {dialogue_patterns[1] if dialogue_patterns and len(dialogue_patterns) > 1 else '계속 이야기해주세요.'}",
|
| 335 |
+
f"당신의 이야기를 들으니 {archetype.get('core_traits', '').split('+')[0].strip() if archetype.get('core_traits') and '+' in archetype.get('core_traits') else '흥미로워요'}."
|
| 336 |
+
])
|
| 337 |
+
|
| 338 |
+
def save_persona_to_json(self, filename=None):
|
| 339 |
+
"""현재 페르소나를 JSON 파일로 저장"""
|
| 340 |
+
if not self.current_persona.get("final_template"):
|
| 341 |
+
return "저장할 페르소나 템플릿이 없습니다. 먼저 템플릿을 생성해주세요."
|
| 342 |
+
|
| 343 |
+
if not filename:
|
| 344 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 345 |
+
object_name = self.current_object.get("name", "object").replace(" ", "_")
|
| 346 |
+
filename = f"{object_name}_{timestamp}.json"
|
| 347 |
+
|
| 348 |
+
# 저장할 데이터 구성
|
| 349 |
+
data_to_save = {
|
| 350 |
+
"object": self.current_object,
|
| 351 |
+
"persona": self.current_persona,
|
| 352 |
+
"relationship_stage": self.current_relationship_stage,
|
| 353 |
+
"timestamp": datetime.now().isoformat()
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
try:
|
| 357 |
+
save_dir = os.path.join(self.data_dir, "saved_personas")
|
| 358 |
+
os.makedirs(save_dir, exist_ok=True)
|
| 359 |
+
|
| 360 |
+
filepath = os.path.join(save_dir, filename)
|
| 361 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 362 |
+
json.dump(data_to_save, f, ensure_ascii=False, indent=2)
|
| 363 |
+
|
| 364 |
+
return f"페르소나가 성공적으로 저장되었습니다: {filepath}"
|
| 365 |
+
except Exception as e:
|
| 366 |
+
return f"저장 중 오류 발생: {e}"
|
| 367 |
+
|
| 368 |
+
def load_persona_from_json(self, filepath):
|
| 369 |
+
"""JSON 파일에서 페르소나 로드"""
|
| 370 |
+
try:
|
| 371 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 372 |
+
data = json.load(f)
|
| 373 |
+
|
| 374 |
+
self.current_object = data.get("object", {})
|
| 375 |
+
self.current_persona = data.get("persona", {})
|
| 376 |
+
self.current_relationship_stage = data.get("relationship_stage", "exploration")
|
| 377 |
+
|
| 378 |
+
return f"페르소나가 성공적으로 로드되었습니다: {self.current_object.get('name', 'Unknown')}"
|
| 379 |
+
except Exception as e:
|
| 380 |
+
return f"로드 중 오류 발생: {e}"
|
| 381 |
+
|
| 382 |
+
def generate_qr_code(self):
|
| 383 |
+
"""현재 페르소나를 QR 코드로 변환하여, 교환 가능하게 함"""
|
| 384 |
+
if not self.current_persona.get("final_template"):
|
| 385 |
+
return "QR 코드로 변환할 페르소나 템플릿이 없습니다. 먼저 템플릿을 생성해주세요.", None
|
| 386 |
+
|
| 387 |
+
try:
|
| 388 |
+
# 간소화된 데이터 준비 (QR 코드 크기 제한 고려)
|
| 389 |
+
simplified_data = {
|
| 390 |
+
"object_name": self.current_object.get("name", ""),
|
| 391 |
+
"archetype": self.current_persona.get("archetype_key", ""),
|
| 392 |
+
"template": self.current_persona.get("final_template", "")
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
# JSON 문자열로 변환
|
| 396 |
+
json_str = json.dumps(simplified_data, ensure_ascii=False)
|
| 397 |
+
|
| 398 |
+
# QR 코드 생성
|
| 399 |
+
qr = qrcode.QRCode(
|
| 400 |
+
version=1,
|
| 401 |
+
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
| 402 |
+
box_size=10,
|
| 403 |
+
border=4,
|
| 404 |
+
)
|
| 405 |
+
qr.add_data(json_str)
|
| 406 |
+
qr.make(fit=True)
|
| 407 |
+
|
| 408 |
+
img = qr.make_image(fill_color="black", back_color="white")
|
| 409 |
+
|
| 410 |
+
# 이미지를 BytesIO로 변환
|
| 411 |
+
buffered = BytesIO()
|
| 412 |
+
img.save(buffered)
|
| 413 |
+
|
| 414 |
+
# 반환할 이미지 생성
|
| 415 |
+
return "QR 코드가 생성되었습니다. 스캔하여 페르소나를 공유할 수 있습니다.", buffered.getvalue()
|
| 416 |
+
except Exception as e:
|
| 417 |
+
return f"QR 코드 생성 중 오류 발생: {e}", None
|
| 418 |
+
|
| 419 |
+
def read_qr_from_image(self, qr_image):
|
| 420 |
+
"""이미지에서 QR 코드 읽기 (실제 구현 필요)"""
|
| 421 |
+
# 실제 QR 코드 판독 기능이 필요합니다
|
| 422 |
+
# 현재는 더미 응답만 반환
|
| 423 |
+
return "현재 QR 코드 읽기 기능은 구현되지 않았습니다. 나중에 다시 시도해주세요."
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
# Gradio 인터페이스 생성
|
| 427 |
+
def create_interface():
|
| 428 |
+
"""Gradio 웹 인터페이스 생성"""
|
| 429 |
+
object_personality = ObjectPersonality()
|
| 430 |
+
|
| 431 |
+
# 아키타입 선택 옵션 생성
|
| 432 |
+
archetype_options = {}
|
| 433 |
+
for key, archetype in object_personality.personality_archetypes.items():
|
| 434 |
+
archetype_options[f"{archetype['name']} ({key})"] = key
|
| 435 |
+
|
| 436 |
+
# 외형 특성 옵션
|
| 437 |
+
shape_options = list(object_personality.physical_traits_formulas.get("shape", {}).keys())
|
| 438 |
+
material_options = list(object_personality.physical_traits_formulas.get("material", {}).keys())
|
| 439 |
+
color_options = list(object_personality.physical_traits_formulas.get("color", {}).keys())
|
| 440 |
+
texture_options = list(object_personality.physical_traits_formulas.get("texture", {}).keys())
|
| 441 |
+
usage_pattern_options = list(object_personality.physical_traits_formulas.get("usage_pattern", {}).keys())
|
| 442 |
+
|
| 443 |
+
# 관계 단계 옵션
|
| 444 |
+
relationship_options = {}
|
| 445 |
+
for key, stage in object_personality.relationship_stages.items():
|
| 446 |
+
relationship_options[f"{stage['name']} - {stage['description'][:30]}..."] = key
|
| 447 |
+
|
| 448 |
+
# 매력적 결함 옵션
|
| 449 |
+
charming_flaw_options = {}
|
| 450 |
+
for category, flaws in object_personality.charming_flaws.get("flaws", {}).items():
|
| 451 |
+
for i, flaw in enumerate(flaws):
|
| 452 |
+
charming_flaw_options[f"{flaw['name']} - {flaw['description'][:30]}..."] = (category, i)
|
| 453 |
+
|
| 454 |
+
# 모순적 특성 옵션
|
| 455 |
+
contradiction_options = {}
|
| 456 |
+
for i, contradiction in enumerate(object_personality.charming_flaws.get("contradictions", [])):
|
| 457 |
+
contradiction_options[f"{contradiction['name']} - {contradiction['description'][:30]}..."] = i
|
| 458 |
+
|
| 459 |
+
# 전체 메서드 매핑
|
| 460 |
+
with gr.Blocks(title="일상 사물 인격화 시스템") as app:
|
| 461 |
+
gr.Markdown("# 🧠 일상 사물 인격화 시스템")
|
| 462 |
+
gr.Markdown("사물에 매력적인 인격을 부여하여 대화할 수 있는 AI 페르소나를 생성합니다.")
|
| 463 |
+
|
| 464 |
+
with gr.Tabs() as tabs:
|
| 465 |
+
# 탭 1: 사물 설정
|
| 466 |
+
with gr.TabItem("1️⃣ 사물 설정"):
|
| 467 |
+
gr.Markdown("## 사물 분석 및 설정")
|
| 468 |
+
|
| 469 |
+
with gr.Row():
|
| 470 |
+
with gr.Column():
|
| 471 |
+
gr.Markdown("### 이미지 업로드 (분석)")
|
| 472 |
+
image_input = gr.Image(type="pil", label="사물 이미지 업로드")
|
| 473 |
+
analyze_btn = gr.Button("이미지 분석하기")
|
| 474 |
+
image_result = gr.Markdown("분석 결과가 여기에 표시됩니다.")
|
| 475 |
+
|
| 476 |
+
with gr.Column():
|
| 477 |
+
gr.Markdown("### 수동 설정")
|
| 478 |
+
object_name = gr.Textbox(label="사물 이름", placeholder="예: 책상 위 램프")
|
| 479 |
+
object_category = gr.Textbox(label="카테고리", placeholder="예: 조명")
|
| 480 |
+
|
| 481 |
+
with gr.Row():
|
| 482 |
+
shape = gr.Dropdown(choices=shape_options, label="형태", value=shape_options[0] if shape_options else None)
|
| 483 |
+
material = gr.Dropdown(choices=material_options, label="재질", value=material_options[0] if material_options else None)
|
| 484 |
+
|
| 485 |
+
with gr.Row():
|
| 486 |
+
color = gr.Dropdown(choices=color_options, label="색상", value=color_options[0] if color_options else None)
|
| 487 |
+
texture = gr.Dropdown(choices=texture_options, label="질감", value=texture_options[0] if texture_options else None)
|
| 488 |
+
|
| 489 |
+
usage_pattern = gr.Dropdown(choices=usage_pattern_options, label="사용 패턴", value=usage_pattern_options[0] if usage_pattern_options else None)
|
| 490 |
+
|
| 491 |
+
manual_btn = gr.Button("수동으로 설정하기")
|
| 492 |
+
manual_result = gr.Markdown("수동 설정 결과가 여기에 표시됩니다.")
|
| 493 |
+
|
| 494 |
+
# 탭 2: 성격 설정
|
| 495 |
+
with gr.TabItem("2️⃣ 성격 설정"):
|
| 496 |
+
gr.Markdown("## 페르소나 기본 설정")
|
| 497 |
+
|
| 498 |
+
with gr.Row():
|
| 499 |
+
with gr.Column():
|
| 500 |
+
gr.Markdown("### 기본 아키타입 선택")
|
| 501 |
+
archetype = gr.Dropdown(choices=list(archetype_options.keys()), label="아키타입")
|
| 502 |
+
archetype_btn = gr.Button("아키타입 적용")
|
| 503 |
+
archetype_result = gr.Markdown("아키타입 선택 결과가 여기에 표시됩니다.")
|
| 504 |
+
|
| 505 |
+
gr.Markdown("### 물리적 특성 적용")
|
| 506 |
+
apply_traits_btn = gr.Button("물리적 특성 성격에 적용")
|
| 507 |
+
traits_result = gr.Markdown("물리적 특성 적용 결과가 여기에 표시됩니다.")
|
| 508 |
+
|
| 509 |
+
with gr.Column():
|
| 510 |
+
gr.Markdown("### 매력적 결함 선택")
|
| 511 |
+
charming_flaw = gr.Dropdown(choices=list(charming_flaw_options.keys()), label="매력적 결함")
|
| 512 |
+
flaw_btn = gr.Button("결함 추가")
|
| 513 |
+
flaw_result = gr.Markdown("결함 추가 결과가 여기에 표시됩니다.")
|
| 514 |
+
|
| 515 |
+
gr.Markdown("### 모순적 특성 선택")
|
| 516 |
+
contradiction = gr.Dropdown(choices=list(contradiction_options.keys()), label="모순적 특성")
|
| 517 |
+
contradiction_btn = gr.Button("모순 추가")
|
| 518 |
+
contradiction_result = gr.Markdown("모순 추가 결과가 여기에 표시됩니다.")
|
| 519 |
+
|
| 520 |
+
gr.Markdown("## 관계 설정")
|
| 521 |
+
relationship_stage = gr.Dropdown(choices=list(relationship_options.keys()), label="관계 단계")
|
| 522 |
+
relationship_btn = gr.Button("관계 단계 설정")
|
| 523 |
+
relationship_result = gr.Markdown("관계 단계 설정 결과가 여기에 표시됩니다.")
|
| 524 |
+
|
| 525 |
+
gr.Markdown("## 페르소나 생성")
|
| 526 |
+
generate_btn = gr.Button("최종 페르소나 템플릿 생성", variant="primary")
|
| 527 |
+
template_output = gr.Textbox(label="생성된 템플릿", lines=20)
|
| 528 |
+
|
| 529 |
+
# 탭 3: 대화 및 테스트
|
| 530 |
+
with gr.TabItem("3️⃣ 대화 및 테스트"):
|
| 531 |
+
gr.Markdown("## 생성된 페르소나와 대화하기")
|
| 532 |
+
|
| 533 |
+
chatbot = gr.Chatbot(label="대화", height=400)
|
| 534 |
+
with gr.Row():
|
| 535 |
+
user_input = gr.Textbox(label="메시지 입력", placeholder="여기에 메시지를 입력하세요...")
|
| 536 |
+
send_btn = gr.Button("전송")
|
| 537 |
+
|
| 538 |
+
clear_btn = gr.Button("대화 내역 지우기")
|
| 539 |
+
|
| 540 |
+
# 탭 4: 저장 및 공유
|
| 541 |
+
with gr.TabItem("4️⃣ 저장 및 공유"):
|
| 542 |
+
gr.Markdown("## 페르소나 저장 및 로드")
|
| 543 |
+
|
| 544 |
+
with gr.Row():
|
| 545 |
+
with gr.Column():
|
| 546 |
+
gr.Markdown("### 저장하기")
|
| 547 |
+
filename = gr.Textbox(label="파일 이름 (선택사항)", placeholder="저장할 파일 이름 (비워두면 자동 생성)")
|
| 548 |
+
save_btn = gr.Button("페르소나 저장하기")
|
| 549 |
+
save_result = gr.Markdown("저장 결과가 여기에 표시됩니다.")
|
| 550 |
+
|
| 551 |
+
with gr.Column():
|
| 552 |
+
gr.Markdown("### 불러오기")
|
| 553 |
+
load_file = gr.File(label="페르소나 파일 선택 (.json)")
|
| 554 |
+
load_btn = gr.Button("페르소나 불러오기")
|
| 555 |
+
load_result = gr.Markdown("로드 결과가 여기에 표시됩니다.")
|
| 556 |
+
|
| 557 |
+
gr.Markdown("## QR 코드 생성 및 스캔")
|
| 558 |
+
with gr.Row():
|
| 559 |
+
with gr.Column():
|
| 560 |
+
gr.Markdown("### QR 코드 생성")
|
| 561 |
+
qr_btn = gr.Button("QR 코드 생성하기")
|
| 562 |
+
qr_result = gr.Markdown("QR 코드 생성 결과가 여기에 표시됩니다.")
|
| 563 |
+
qr_image = gr.Image(label="생성된 QR 코드", type="pil")
|
| 564 |
+
|
| 565 |
+
with gr.Column():
|
| 566 |
+
gr.Markdown("### QR 코드 스캔")
|
| 567 |
+
scan_image = gr.Image(label="스캔할 QR 코드", type="pil")
|
| 568 |
+
scan_btn = gr.Button("QR 코드 스캔하기")
|
| 569 |
+
scan_result = gr.Markdown("스캔 결과가 여기에 표시됩니다.")
|
| 570 |
+
|
| 571 |
+
# 이벤트 연결
|
| 572 |
+
# 탭 1: 사물 설정
|
| 573 |
+
analyze_btn.click(fn=object_personality.analyze_image, inputs=image_input, outputs=[image_result])
|
| 574 |
+
manual_btn.click(
|
| 575 |
+
fn=object_personality.set_manual_object,
|
| 576 |
+
inputs=[object_name, object_category, shape, material, color, texture, usage_pattern],
|
| 577 |
+
outputs=manual_result
|
| 578 |
+
)
|
| 579 |
+
|
| 580 |
+
# 탭 2: 성격 설정
|
| 581 |
+
def select_archetype_wrapper(archetype_selection):
|
| 582 |
+
key = archetype_options.get(archetype_selection)
|
| 583 |
+
if key:
|
| 584 |
+
return object_personality.select_archetype(key)
|
| 585 |
+
return "아키타입을 선택해주세요."
|
| 586 |
+
|
| 587 |
+
archetype_btn.click(fn=select_archetype_wrapper, inputs=archetype, outputs=archetype_result)
|
| 588 |
+
apply_traits_btn.click(fn=object_personality.apply_physical_traits, outputs=traits_result)
|
| 589 |
+
|
| 590 |
+
def add_charming_flaw_wrapper(flaw_selection):
|
| 591 |
+
if flaw_selection in charming_flaw_options:
|
| 592 |
+
category, index = charming_flaw_options[flaw_selection]
|
| 593 |
+
return object_personality.add_charming_flaw(category, index)
|
| 594 |
+
return "결함을 선택해주세요."
|
| 595 |
+
|
| 596 |
+
flaw_btn.click(fn=add_charming_flaw_wrapper, inputs=charming_flaw, outputs=flaw_result)
|
| 597 |
+
|
| 598 |
+
def add_contradiction_wrapper(contradiction_selection):
|
| 599 |
+
if contradiction_selection in contradiction_options:
|
| 600 |
+
index = contradiction_options[contradiction_selection]
|
| 601 |
+
return object_personality.add_contradiction(index)
|
| 602 |
+
return "모순을 선택해주세요."
|
| 603 |
+
|
| 604 |
+
contradiction_btn.click(fn=add_contradiction_wrapper, inputs=contradiction, outputs=contradiction_result)
|
| 605 |
+
|
| 606 |
+
def set_relationship_stage_wrapper(stage_selection):
|
| 607 |
+
key = relationship_options.get(stage_selection)
|
| 608 |
+
if key:
|
| 609 |
+
return object_personality.set_relationship_stage(key)
|
| 610 |
+
return "관계 단계를 선택해주세요."
|
| 611 |
+
|
| 612 |
+
relationship_btn.click(fn=set_relationship_stage_wrapper, inputs=relationship_stage, outputs=relationship_result)
|
| 613 |
+
generate_btn.click(fn=object_personality.generate_persona_template, outputs=template_output)
|
| 614 |
+
|
| 615 |
+
# 탭 3: 대화 및 테스트
|
| 616 |
+
def chat(message, history):
|
| 617 |
+
response = object_personality.generate_natural_response(message)
|
| 618 |
+
history.append((message, response))
|
| 619 |
+
return "", history
|
| 620 |
+
|
| 621 |
+
send_btn.click(fn=chat, inputs=[user_input, chatbot], outputs=[user_input, chatbot])
|
| 622 |
+
user_input.submit(fn=chat, inputs=[user_input, chatbot], outputs=[user_input, chatbot])
|
| 623 |
+
clear_btn.click(fn=lambda: None, outputs=chatbot)
|
| 624 |
+
|
| 625 |
+
# 탭 4: 저장 및 공유
|
| 626 |
+
def save_persona(filename):
|
| 627 |
+
if filename and filename.strip():
|
| 628 |
+
return object_personality.save_persona_to_json(filename.strip())
|
| 629 |
+
else:
|
| 630 |
+
return object_personality.save_persona_to_json()
|
| 631 |
+
|
| 632 |
+
save_btn.click(fn=save_persona, inputs=filename, outputs=save_result)
|
| 633 |
+
|
| 634 |
+
def load_persona(file):
|
| 635 |
+
if file is None:
|
| 636 |
+
return "파일을 선택해주세요."
|
| 637 |
+
return object_personality.load_persona_from_json(file.name)
|
| 638 |
+
|
| 639 |
+
load_btn.click(fn=load_persona, inputs=load_file, outputs=load_result)
|
| 640 |
+
|
| 641 |
+
def generate_qr():
|
| 642 |
+
message, image_data = object_personality.generate_qr_code()
|
| 643 |
+
if image_data:
|
| 644 |
+
image = Image.open(BytesIO(image_data))
|
| 645 |
+
return message, image
|
| 646 |
+
return message, None
|
| 647 |
+
|
| 648 |
+
qr_btn.click(fn=generate_qr, outputs=[qr_result, qr_image])
|
| 649 |
+
|
| 650 |
+
scan_btn.click(fn=object_personality.read_qr_from_image, inputs=scan_image, outputs=scan_result)
|
| 651 |
+
|
| 652 |
+
return app
|
| 653 |
+
|
| 654 |
+
# 메인 실행 부분
|
| 655 |
+
if __name__ == "__main__":
|
| 656 |
+
app = create_interface()
|
| 657 |
+
app.launch(share=False, debug=True)
|
huggingface_README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🧠 일상 사물 인격화 시스템
|
| 2 |
+
|
| 3 |
+
[](https://huggingface.co/spaces/[YOUR_USERNAME]/object-personality)
|
| 4 |
+
|
| 5 |
+
일상 사물에 매력적인 성격과 개성을 부여하는 AI 시스템입니다. 사물의 물리적 특성과 심리학적 연구를 결합하여 독특하고 친근한 AI 페르소나를 만들고 대화할 수 있습니다.
|
| 6 |
+
|
| 7 |
+
## 💡 주요 기능
|
| 8 |
+
|
| 9 |
+
- **사물 분석 및 인격화**: 사물의 형태, 재질, 색상 등을 기반으로 성격 특성 생성
|
| 10 |
+
- **아키타입 템플릿**: 9가지 기본 아키타입을 바탕으로 성격 생성 (고독한 현자, 감정의 카오스 등)
|
| 11 |
+
- **매력적 결함 시스템**: 완벽하지 않은 특성이 호감도와 친근감을 높이는 심리학적 원리 적용
|
| 12 |
+
- **단계별 관계 발전**: 탐색기 → 친밀화 → 신뢰 구축 → 깊은 유대 단계에 따른 대화 패턴 변화
|
| 13 |
+
- **JSON 저장/공유**: 생성한 페르소나 저장 및 QR 코드로 공유 가능
|
| 14 |
+
|
| 15 |
+
## 🚀 시작하기
|
| 16 |
+
|
| 17 |
+
### 1. 사물 설정하기
|
| 18 |
+
|
| 19 |
+
- **이미지로 분석하기**: 사물 이미지를 업로드하여 자동 분석
|
| 20 |
+
- **수동으로 설정하기**: 사물 이름, 형태, 재질, 색상 등을 직접 지정
|
| 21 |
+
|
| 22 |
+
### 2. 성격 설정하기
|
| 23 |
+
|
| 24 |
+
- **아키타입 선택**: 기본 성격 유형 선택
|
| 25 |
+
- **매력적 결함 추가**: 가족적이고 인간적인 약점 선택
|
| 26 |
+
- **모순적 특성 설정**: 깊이와 복잡성을 더하는 모순적 특성 추가
|
| 27 |
+
- **관계 단계 설정**: 사용자와의 관계 발전 단계 선택
|
| 28 |
+
|
| 29 |
+
### 3. 대화 테스트
|
| 30 |
+
|
| 31 |
+
- 생성된 페르소나와 실시간 대화
|
| 32 |
+
- 성격 특성과 관계 단계에 따른 대화 스타일 경험
|
| 33 |
+
|
| 34 |
+
### 4. 저장 및 공유
|
| 35 |
+
|
| 36 |
+
- JSON 파일로 저장
|
| 37 |
+
- QR 코드로 변환하여 공유
|
| 38 |
+
|
| 39 |
+
## 🔬 이론적 배경
|
| 40 |
+
|
| 41 |
+
- **온기-능력 균형 모델**: Fiske, Cuddy, & Glick(2007)의 연구에 기반한 캐릭터 매력 요소
|
| 42 |
+
- **프랫폴 효과**: Aronson(1966)이 발견한 '매력적 결함'이 호감도를 증가시키는 심리학적 현상
|
| 43 |
+
- **모순적 특성과 캐릭터 깊이**: Robert McKee의 매력적 캐릭터 이론에 기반한 복잡성 설계
|
| 44 |
+
- **관계 발전 모델**: Knapp(1978)의 관계 발전 이론을 AI 상호작용에 적용
|
| 45 |
+
|
| 46 |
+
## 💻 API 키 설정 (선택사항)
|
| 47 |
+
|
| 48 |
+
자체 Gemini API 키를 사용하려면 환경 변수를 설정하세요:
|
| 49 |
+
|
| 50 |
+
```
|
| 51 |
+
GEMINI_API_KEY=your_api_key_here
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
## 👨💻 개발자
|
| 55 |
+
|
| 56 |
+
memory_tag_mvp 팀
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==4.1.0
|
| 2 |
+
pillow==9.5.0
|
| 3 |
+
numpy==1.24.3
|
| 4 |
+
pandas==2.0.2
|
| 5 |
+
google-generativeai==0.3.1
|
| 6 |
+
matplotlib==3.7.1
|
| 7 |
+
huggingface-hub==0.16.4
|
| 8 |
+
qrcode==7.4.2
|
| 9 |
+
jsonschema==4.17.3
|
| 10 |
+
protobuf==4.23.3
|