Spaces:
Sleeping
Sleeping
haepa_mac
commited on
Commit
·
44e0e84
0
Parent(s):
Initial commit: 놈팽쓰 테스트 앱
Browse files- .gitignore +26 -0
- README.md +95 -0
- app.py +564 -0
- data/conversations/.gitkeep +0 -0
- data/personas/.gitkeep +0 -0
- modules/__init__.py +1 -0
- modules/data_manager.py +175 -0
- modules/persona_generator.py +439 -0
- packages.txt +2 -0
- requirements.txt +8 -0
.gitignore
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Environment variables
|
2 |
+
.env
|
3 |
+
|
4 |
+
# Python cache
|
5 |
+
__pycache__/
|
6 |
+
*.py[cod]
|
7 |
+
*$py.class
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
dist/
|
11 |
+
build/
|
12 |
+
*.egg-info/
|
13 |
+
|
14 |
+
# Virtual environments
|
15 |
+
venv/
|
16 |
+
env/
|
17 |
+
ENV/
|
18 |
+
|
19 |
+
# Data directories (can be large)
|
20 |
+
data/personas/*
|
21 |
+
data/conversations/*
|
22 |
+
!data/personas/.gitkeep
|
23 |
+
!data/conversations/.gitkeep
|
24 |
+
|
25 |
+
# OS specific
|
26 |
+
.DS_Store
|
README.md
ADDED
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: 놈팽쓰(MemoryTag) 테스트 앱
|
3 |
+
emoji: 🎭
|
4 |
+
colorFrom: indigo
|
5 |
+
colorTo: blue
|
6 |
+
sdk: gradio
|
7 |
+
sdk_version: 4.19.2
|
8 |
+
app_file: app.py
|
9 |
+
pinned: false
|
10 |
+
license: mit
|
11 |
+
---
|
12 |
+
|
13 |
+
# 놈팽쓰(MemoryTag) 테스트 앱
|
14 |
+
|
15 |
+
사물에 영혼을 불어넣어 대화할 수 있는 페르소나 생성 테스트 앱입니다. 놈팽쓰 UX 가이드에 따라 사물의 영혼을 깨우고 대화하는 경험을 제공합니다.
|
16 |
+
|
17 |
+
## 주요 기능
|
18 |
+
|
19 |
+
1. **영혼 깨우기**:
|
20 |
+
- 사물 이미지를 분석하여 물리적 특성 추출
|
21 |
+
- 프론트엔드용 간단한 페르소나와 백엔드용 상세 페르소나 생성
|
22 |
+
- 127개 성격 변수 생성 (백엔드 시스템)
|
23 |
+
|
24 |
+
2. **대화하기**:
|
25 |
+
- 생성된 페르소나와 자연스러운 대화
|
26 |
+
- 성격에 맞는 응답 생성
|
27 |
+
- 대화 내역 저장
|
28 |
+
|
29 |
+
3. **페르소나 관리**:
|
30 |
+
- 생성된 페르소나 저장 및 로드
|
31 |
+
- 페르소나 목록 관리
|
32 |
+
|
33 |
+
## 설치 방법
|
34 |
+
|
35 |
+
1. 저장소를 클론합니다:
|
36 |
+
```bash
|
37 |
+
git clone [저장소 URL]
|
38 |
+
cd nompang_test
|
39 |
+
```
|
40 |
+
|
41 |
+
2. 필요한 패키지를 설치합니다:
|
42 |
+
```bash
|
43 |
+
pip install -r requirements.txt
|
44 |
+
```
|
45 |
+
|
46 |
+
3. `.env` 파일을 생성하고 Gemini API 키를 설정합니다:
|
47 |
+
```
|
48 |
+
GEMINI_API_KEY=your_api_key_here
|
49 |
+
```
|
50 |
+
|
51 |
+
## 실행 방법
|
52 |
+
|
53 |
+
앱을 실행하려면 다음 명령어를 사용합니다:
|
54 |
+
|
55 |
+
```bash
|
56 |
+
python app.py
|
57 |
+
```
|
58 |
+
|
59 |
+
웹 브라우저에서 `http://localhost:7860`으로 접속하여 앱을 사용할 수 있습니다.
|
60 |
+
|
61 |
+
## 사용 방법
|
62 |
+
|
63 |
+
1. **영혼 깨우기 탭**:
|
64 |
+
- 사물 이미지를 업로드하거나 이름을 입력합니다.
|
65 |
+
- "영혼 깨우기" 버튼을 클릭하여 페르소나를 생성합니다.
|
66 |
+
- 프론트엔드 뷰와 백엔드 상세 정보를 탭으로 전환하여 확인할 수 있습니다.
|
67 |
+
- "페르소나 저장" 버튼을 클릭하여 생성된 페르소나를 저장합니다.
|
68 |
+
|
69 |
+
2. **대화하기 탭**:
|
70 |
+
- "새 대화 시작" 버튼을 클릭하여 현재 페르소나와 대화를 시작합니다.
|
71 |
+
- 메시지를 입력하고 "전송" 버튼을 클릭하여 대화합니다.
|
72 |
+
- "대화 초기화" 버튼을 클릭하여 대화 내역을 초기화할 수 있습니다.
|
73 |
+
|
74 |
+
3. **페르소나 관리 탭**:
|
75 |
+
- "페르소나 목록 새로고침" 버튼을 클릭하여 저장된 페르소나 목록을 갱신합니다.
|
76 |
+
- 목록에서 페르소나를 선택하고 "선택한 페르소나 불러오기" 버튼을 클릭하여 불러옵니다.
|
77 |
+
- 불러온 페르소나의 정보를 확인하고 대화하기 탭으로 이동하여 대화할 수 있습니다.
|
78 |
+
|
79 |
+
## 시스템 구조
|
80 |
+
|
81 |
+
- **app.py**: 메인 Gradio 애플리케이션
|
82 |
+
- **modules/persona_generator.py**: 페르소나 생성 및 대화 처리
|
83 |
+
- **modules/data_manager.py**: 데이터 저장 및 로드
|
84 |
+
- **data/personas/**: 저장된 페르소나 데이터
|
85 |
+
- **data/conversations/**: 대화 내역 데이터
|
86 |
+
|
87 |
+
## 참고 사항
|
88 |
+
|
89 |
+
- 이 앱은 Gemini API를 사용하여 페르소나를 생성하고 대화합니다.
|
90 |
+
- API 키가 설정되지 않으면 기본 페르소나로 제한된 기능을 사용할 수 있습니다.
|
91 |
+
- 이미지 분석 결과는 Gemini API의 이미지 처리 기능을 사용합니다.
|
92 |
+
|
93 |
+
## 라이선스
|
94 |
+
|
95 |
+
MIT License
|
app.py
ADDED
@@ -0,0 +1,564 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import json
|
3 |
+
import time
|
4 |
+
import gradio as gr
|
5 |
+
import google.generativeai as genai
|
6 |
+
from PIL import Image
|
7 |
+
from dotenv import load_dotenv
|
8 |
+
import matplotlib.pyplot as plt
|
9 |
+
import numpy as np
|
10 |
+
import base64
|
11 |
+
import io
|
12 |
+
import uuid
|
13 |
+
from datetime import datetime
|
14 |
+
|
15 |
+
# Import modules
|
16 |
+
from modules.persona_generator import PersonaGenerator
|
17 |
+
from modules.data_manager import save_persona, load_persona, list_personas, toggle_frontend_backend_view
|
18 |
+
|
19 |
+
# Load environment variables
|
20 |
+
load_dotenv()
|
21 |
+
|
22 |
+
# Configure Gemini API
|
23 |
+
api_key = os.getenv("GEMINI_API_KEY")
|
24 |
+
if api_key:
|
25 |
+
genai.configure(api_key=api_key)
|
26 |
+
|
27 |
+
# Create data directories if they don't exist
|
28 |
+
os.makedirs("data/personas", exist_ok=True)
|
29 |
+
os.makedirs("data/conversations", exist_ok=True)
|
30 |
+
|
31 |
+
# Initialize the persona generator
|
32 |
+
persona_generator = PersonaGenerator()
|
33 |
+
|
34 |
+
# Gradio theme
|
35 |
+
theme = gr.themes.Soft(
|
36 |
+
primary_hue="indigo",
|
37 |
+
secondary_hue="blue",
|
38 |
+
)
|
39 |
+
|
40 |
+
# CSS for additional styling
|
41 |
+
css = """
|
42 |
+
.persona-details {
|
43 |
+
border: 1px solid #e0e0e0;
|
44 |
+
border-radius: 8px;
|
45 |
+
padding: 12px;
|
46 |
+
margin-top: 8px;
|
47 |
+
background-color: #f8f9fa;
|
48 |
+
}
|
49 |
+
.collapsed-section {
|
50 |
+
margin-top: 10px;
|
51 |
+
margin-bottom: 10px;
|
52 |
+
}
|
53 |
+
.personality-chart {
|
54 |
+
width: 100%;
|
55 |
+
height: auto;
|
56 |
+
margin-top: 15px;
|
57 |
+
}
|
58 |
+
.conversation-box {
|
59 |
+
height: 400px;
|
60 |
+
overflow-y: auto;
|
61 |
+
}
|
62 |
+
"""
|
63 |
+
|
64 |
+
def create_frontend_view_html(persona):
|
65 |
+
"""Create HTML representation of the frontend view of the persona"""
|
66 |
+
if not persona:
|
67 |
+
return "페르소나가 아직 생성되지 않았습니다."
|
68 |
+
|
69 |
+
name = persona.get("기본정보", {}).get("이름", "Unknown")
|
70 |
+
object_type = persona.get("기본정보", {}).get("유형", "Unknown")
|
71 |
+
description = persona.get("기본정보", {}).get("설명", "")
|
72 |
+
|
73 |
+
# Personality traits
|
74 |
+
traits_html = ""
|
75 |
+
for trait, value in persona.get("성격특성", {}).items():
|
76 |
+
traits_html += f"<div>{trait}: <div style='background: linear-gradient(to right, #6366f1 {value}%, #e0e0e0 {value}%); height: 12px; border-radius: 6px; margin-bottom: 8px;'></div></div>"
|
77 |
+
|
78 |
+
# Flaws
|
79 |
+
flaws = persona.get("매력적결함", [])
|
80 |
+
flaws_html = ", ".join(flaws) if flaws else "없음"
|
81 |
+
|
82 |
+
# Interests
|
83 |
+
interests = persona.get("관심사", [])
|
84 |
+
interests_html = ", ".join(interests) if interests else "없음"
|
85 |
+
|
86 |
+
# Communication style
|
87 |
+
communication_style = persona.get("소통방식", "")
|
88 |
+
|
89 |
+
html = f"""
|
90 |
+
<div class="persona-details">
|
91 |
+
<h2>{name}</h2>
|
92 |
+
<p><strong>유형:</strong> {object_type}</p>
|
93 |
+
<p><strong>설명:</strong> {description}</p>
|
94 |
+
|
95 |
+
<h3>성격 특성</h3>
|
96 |
+
{traits_html}
|
97 |
+
|
98 |
+
<h3>소통 방식</h3>
|
99 |
+
<p>{communication_style}</p>
|
100 |
+
|
101 |
+
<h3>매력적 결함</h3>
|
102 |
+
<p>{flaws_html}</p>
|
103 |
+
|
104 |
+
<h3>관심사</h3>
|
105 |
+
<p>{interests_html}</p>
|
106 |
+
</div>
|
107 |
+
"""
|
108 |
+
|
109 |
+
return html
|
110 |
+
|
111 |
+
def create_backend_view_html(persona):
|
112 |
+
"""Create HTML representation of the backend view of the persona"""
|
113 |
+
if not persona:
|
114 |
+
return "페르소나가 아직 생성되지 않았습니다."
|
115 |
+
|
116 |
+
name = persona.get("기본정보", {}).get("이름", "Unknown")
|
117 |
+
|
118 |
+
# Convert persona to formatted JSON
|
119 |
+
try:
|
120 |
+
persona_json = json.dumps(persona, ensure_ascii=False, indent=2)
|
121 |
+
|
122 |
+
html = f"""
|
123 |
+
<div class="persona-details">
|
124 |
+
<h2>{name} - 백엔드 상세 정보</h2>
|
125 |
+
<details>
|
126 |
+
<summary>127개 성격 변수 및 상세 정보</summary>
|
127 |
+
<pre style="background-color: #f5f5f5; padding: 12px; border-radius: 8px; overflow-x: auto;">{persona_json}</pre>
|
128 |
+
</details>
|
129 |
+
</div>
|
130 |
+
"""
|
131 |
+
|
132 |
+
return html
|
133 |
+
except Exception as e:
|
134 |
+
return f"백엔드 정보 변환 오류: {str(e)}"
|
135 |
+
|
136 |
+
def generate_personality_chart(persona):
|
137 |
+
"""Generate a radar chart for personality traits"""
|
138 |
+
if not persona or "성격특성" not in persona:
|
139 |
+
# Return empty image
|
140 |
+
fig, ax = plt.subplots(figsize=(6, 6))
|
141 |
+
ax.text(0.5, 0.5, "데이터 없음", ha='center', va='center')
|
142 |
+
ax.axis('off')
|
143 |
+
|
144 |
+
buf = io.BytesIO()
|
145 |
+
plt.savefig(buf, format='png')
|
146 |
+
buf.seek(0)
|
147 |
+
plt.close(fig)
|
148 |
+
return buf
|
149 |
+
|
150 |
+
# Get traits
|
151 |
+
traits = persona["성격특성"]
|
152 |
+
|
153 |
+
# Create radar chart
|
154 |
+
categories = list(traits.keys())
|
155 |
+
values = list(traits.values())
|
156 |
+
|
157 |
+
# Add the first value again to close the loop
|
158 |
+
categories.append(categories[0])
|
159 |
+
values.append(values[0])
|
160 |
+
|
161 |
+
# Convert to radians
|
162 |
+
angles = np.linspace(0, 2*np.pi, len(categories), endpoint=True)
|
163 |
+
|
164 |
+
# Create plot
|
165 |
+
fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True))
|
166 |
+
|
167 |
+
# Draw polygon
|
168 |
+
ax.fill(angles, values, alpha=0.25, color='#6366f1')
|
169 |
+
|
170 |
+
# Draw lines
|
171 |
+
ax.plot(angles, values, 'o-', linewidth=2, color='#6366f1')
|
172 |
+
|
173 |
+
# Fill labels
|
174 |
+
ax.set_thetagrids(angles[:-1] * 180/np.pi, categories[:-1])
|
175 |
+
ax.set_rlim(0, 100)
|
176 |
+
|
177 |
+
# Add titles
|
178 |
+
name = persona.get("기본정보", {}).get("이름", "Unknown")
|
179 |
+
plt.title(f"{name}의 성격 특성", size=15, color='#333333', y=1.1)
|
180 |
+
|
181 |
+
# Styling
|
182 |
+
ax.grid(True, linestyle='--', alpha=0.7)
|
183 |
+
|
184 |
+
# Save to buffer
|
185 |
+
buf = io.BytesIO()
|
186 |
+
plt.savefig(buf, format='png', bbox_inches='tight')
|
187 |
+
buf.seek(0)
|
188 |
+
plt.close(fig)
|
189 |
+
|
190 |
+
return buf
|
191 |
+
|
192 |
+
def soul_awakening(image, object_name=None):
|
193 |
+
"""Analyze image and awaken the soul of the object"""
|
194 |
+
if image is None:
|
195 |
+
return (
|
196 |
+
None,
|
197 |
+
gr.update(visible=True, value="이미지를 먼저 업로드해주세요."),
|
198 |
+
None, None, None
|
199 |
+
)
|
200 |
+
|
201 |
+
# Step 1: Analyze image
|
202 |
+
print(f"Analyzing image: {image}")
|
203 |
+
analysis_result = persona_generator.analyze_image(image)
|
204 |
+
|
205 |
+
# Create context
|
206 |
+
user_context = {
|
207 |
+
"name": object_name if object_name else analysis_result.get("object_type", "미확인 사물")
|
208 |
+
}
|
209 |
+
|
210 |
+
# Step 2: Create frontend persona
|
211 |
+
frontend_persona = persona_generator.create_frontend_persona(analysis_result, user_context)
|
212 |
+
|
213 |
+
# Step 3: Create backend persona
|
214 |
+
backend_persona = persona_generator.create_backend_persona(frontend_persona, analysis_result)
|
215 |
+
|
216 |
+
# Save both views to a single persona file
|
217 |
+
filepath = save_persona(backend_persona)
|
218 |
+
if filepath:
|
219 |
+
backend_persona["filepath"] = filepath
|
220 |
+
|
221 |
+
# Generate HTML views
|
222 |
+
frontend_html = create_frontend_view_html(frontend_persona)
|
223 |
+
backend_html = create_backend_view_html(backend_persona)
|
224 |
+
|
225 |
+
# Generate personality chart
|
226 |
+
chart_image = generate_personality_chart(frontend_persona)
|
227 |
+
|
228 |
+
return (
|
229 |
+
analysis_result,
|
230 |
+
gr.update(visible=False, value=""),
|
231 |
+
frontend_html,
|
232 |
+
backend_html,
|
233 |
+
chart_image
|
234 |
+
)
|
235 |
+
|
236 |
+
def start_chat(current_persona):
|
237 |
+
"""Start a conversation with the current persona"""
|
238 |
+
if not current_persona:
|
239 |
+
return "페르소나를 먼저 생성하거나 불러와주세요.", [], []
|
240 |
+
|
241 |
+
# Generate initial greeting
|
242 |
+
name = current_persona.get("기본정보", {}).get("이름", "Unknown")
|
243 |
+
try:
|
244 |
+
initial_message = persona_generator.chat_with_persona(
|
245 |
+
current_persona,
|
246 |
+
"안녕하세요!"
|
247 |
+
)
|
248 |
+
|
249 |
+
# Initialize conversation history
|
250 |
+
conversation = [
|
251 |
+
{"role": "assistant", "content": initial_message}
|
252 |
+
]
|
253 |
+
|
254 |
+
# Format for chatbot
|
255 |
+
chatbot_messages = [(None, initial_message)]
|
256 |
+
|
257 |
+
return f"{name}과의 대화가 시작되었습니다.", chatbot_messages, conversation
|
258 |
+
except Exception as e:
|
259 |
+
return f"대화 시작 중 오류 발생: {str(e)}", [], []
|
260 |
+
|
261 |
+
def chat_with_persona(message, chatbot, conversation, current_persona):
|
262 |
+
"""Chat with the current persona"""
|
263 |
+
if not message or not current_persona:
|
264 |
+
return chatbot, conversation
|
265 |
+
|
266 |
+
# Add user message to conversation
|
267 |
+
conversation.append({"role": "user", "content": message})
|
268 |
+
|
269 |
+
# Update chatbot display
|
270 |
+
chatbot.append((message, None))
|
271 |
+
|
272 |
+
# Get response from persona
|
273 |
+
try:
|
274 |
+
response = persona_generator.chat_with_persona(
|
275 |
+
current_persona,
|
276 |
+
message,
|
277 |
+
conversation
|
278 |
+
)
|
279 |
+
|
280 |
+
# Add response to conversation
|
281 |
+
conversation.append({"role": "assistant", "content": response})
|
282 |
+
|
283 |
+
# Update chatbot display
|
284 |
+
chatbot[-1] = (message, response)
|
285 |
+
|
286 |
+
return chatbot, conversation
|
287 |
+
except Exception as e:
|
288 |
+
error_message = f"오류: {str(e)}"
|
289 |
+
conversation.append({"role": "assistant", "content": error_message})
|
290 |
+
chatbot[-1] = (message, error_message)
|
291 |
+
return chatbot, conversation
|
292 |
+
|
293 |
+
def load_selected_persona(selected_row, personas_list):
|
294 |
+
"""Load persona from the selected row in the dataframe"""
|
295 |
+
if selected_row is None or len(selected_row) == 0:
|
296 |
+
return None, "선택된 페르소나가 없습니다.", None, None, None
|
297 |
+
|
298 |
+
try:
|
299 |
+
# Get filepath from selected row
|
300 |
+
selected_index = selected_row.index[0] if hasattr(selected_row, 'index') else 0
|
301 |
+
filepath = personas_list[selected_index]["filepath"]
|
302 |
+
|
303 |
+
# Load persona
|
304 |
+
persona = load_persona(filepath)
|
305 |
+
if not persona:
|
306 |
+
return None, "페르소나 로딩에 실패했습니다.", None, None, None
|
307 |
+
|
308 |
+
# Generate HTML views
|
309 |
+
frontend_view, backend_view = toggle_frontend_backend_view(persona)
|
310 |
+
frontend_html = create_frontend_view_html(frontend_view)
|
311 |
+
backend_html = create_backend_view_html(backend_view)
|
312 |
+
|
313 |
+
# Generate personality chart
|
314 |
+
chart_image = generate_personality_chart(frontend_view)
|
315 |
+
|
316 |
+
return persona, f"{persona['기본정보']['이름']}을(를) 로드했습니다.", frontend_html, backend_html, chart_image
|
317 |
+
|
318 |
+
except Exception as e:
|
319 |
+
return None, f"페르소나 로딩 중 오류 발생: {str(e)}", None, None, None
|
320 |
+
|
321 |
+
def save_current_persona(current_persona):
|
322 |
+
"""Save current persona to a JSON file"""
|
323 |
+
if not current_persona:
|
324 |
+
return "저장할 페르소나가 없습니다."
|
325 |
+
|
326 |
+
try:
|
327 |
+
filepath = save_persona(current_persona)
|
328 |
+
if filepath:
|
329 |
+
name = current_persona.get("기본정보", {}).get("이름", "Unknown")
|
330 |
+
return f"{name} 페르소나가 저장되었습니다: {filepath}"
|
331 |
+
else:
|
332 |
+
return "페르소나 저장에 실패했습니다."
|
333 |
+
except Exception as e:
|
334 |
+
return f"저장 중 오류 발생: {str(e)}"
|
335 |
+
|
336 |
+
def get_personas_list():
|
337 |
+
"""Get list of personas for the dataframe"""
|
338 |
+
personas = list_personas()
|
339 |
+
|
340 |
+
# Convert to dataframe format
|
341 |
+
df_data = []
|
342 |
+
for i, persona in enumerate(personas):
|
343 |
+
df_data.append([
|
344 |
+
persona["name"],
|
345 |
+
persona["type"],
|
346 |
+
persona["created_at"],
|
347 |
+
persona["filename"]
|
348 |
+
])
|
349 |
+
|
350 |
+
return df_data, personas
|
351 |
+
|
352 |
+
# Main Gradio app
|
353 |
+
with gr.Blocks(title="놈팽쓰 테스트 앱", theme=theme, css=css) as app:
|
354 |
+
# Global state
|
355 |
+
current_persona = gr.State(None)
|
356 |
+
conversation_history = gr.State([])
|
357 |
+
analysis_result_state = gr.State(None)
|
358 |
+
personas_data = gr.State([])
|
359 |
+
|
360 |
+
gr.Markdown(
|
361 |
+
"""
|
362 |
+
# 🎭 놈팽쓰(MemoryTag) 테스트 앱
|
363 |
+
|
364 |
+
사물에 영혼을 불어넣어 대화할 수 있는 페르소나 생성 테스트 앱입니다.
|
365 |
+
|
366 |
+
## 사용 방법
|
367 |
+
1. **영혼 깨우기** 탭에서 이미지를 업로드하거나 이름을 입력하여 사물의 영혼을 깨웁니다.
|
368 |
+
2. **대화하기** 탭에서 생성된 페르소나와 대화합니다.
|
369 |
+
3. **페르소나 관리** 탭에서 저장된 페르소나를 관리합니다.
|
370 |
+
"""
|
371 |
+
)
|
372 |
+
|
373 |
+
with gr.Tabs() as tabs:
|
374 |
+
# Tab 1: Soul Awakening
|
375 |
+
with gr.Tab("영혼 깨우기"):
|
376 |
+
with gr.Row():
|
377 |
+
with gr.Column(scale=1):
|
378 |
+
gr.Markdown("## 영혼 발견하기")
|
379 |
+
|
380 |
+
object_image = gr.Image(
|
381 |
+
label="사물 이미지",
|
382 |
+
type="filepath"
|
383 |
+
)
|
384 |
+
|
385 |
+
object_name = gr.Textbox(
|
386 |
+
label="사물 이름 (선택사항)",
|
387 |
+
placeholder="자동 감지하려면 비워두세요"
|
388 |
+
)
|
389 |
+
|
390 |
+
awaken_btn = gr.Button(
|
391 |
+
"영혼 깨우기",
|
392 |
+
variant="primary"
|
393 |
+
)
|
394 |
+
|
395 |
+
error_message = gr.Markdown(
|
396 |
+
"",
|
397 |
+
visible=False
|
398 |
+
)
|
399 |
+
|
400 |
+
with gr.Column(scale=2):
|
401 |
+
with gr.Tabs() as result_tabs:
|
402 |
+
with gr.Tab("페르소나 프론트"):
|
403 |
+
frontend_view = gr.HTML(
|
404 |
+
label="프론트엔드 뷰",
|
405 |
+
value="사물의 영혼을 깨워주세요."
|
406 |
+
)
|
407 |
+
|
408 |
+
personality_chart = gr.Image(
|
409 |
+
label="성격 차트",
|
410 |
+
show_label=False
|
411 |
+
)
|
412 |
+
|
413 |
+
with gr.Tab("백엔드 상세 정보"):
|
414 |
+
backend_view = gr.HTML(
|
415 |
+
label="백엔드 뷰",
|
416 |
+
value="사물의 영혼을 깨워주세요."
|
417 |
+
)
|
418 |
+
|
419 |
+
# Button row
|
420 |
+
with gr.Row():
|
421 |
+
save_btn = gr.Button("페르소나 저장")
|
422 |
+
chat_btn = gr.Button("대화 시작하기")
|
423 |
+
|
424 |
+
save_result = gr.Markdown("")
|
425 |
+
|
426 |
+
# Tab 2: Chat
|
427 |
+
with gr.Tab("대화하기"):
|
428 |
+
with gr.Row():
|
429 |
+
with gr.Column(scale=3):
|
430 |
+
chat_status = gr.Markdown("페르소나를 먼저 생성하거나 불러와주세요.")
|
431 |
+
|
432 |
+
chatbot = gr.Chatbot(
|
433 |
+
label="대화",
|
434 |
+
height=400,
|
435 |
+
show_label=False,
|
436 |
+
)
|
437 |
+
|
438 |
+
with gr.Row():
|
439 |
+
user_message = gr.Textbox(
|
440 |
+
label="메시지",
|
441 |
+
placeholder="메시지를 입력하세요...",
|
442 |
+
lines=2
|
443 |
+
)
|
444 |
+
|
445 |
+
send_btn = gr.Button(
|
446 |
+
"전송",
|
447 |
+
variant="primary"
|
448 |
+
)
|
449 |
+
|
450 |
+
with gr.Column(scale=1):
|
451 |
+
gr.Markdown("## 현재 페르소나")
|
452 |
+
current_persona_html = gr.HTML("페르소나가 선택되지 않았습니다.")
|
453 |
+
|
454 |
+
start_chat_btn = gr.Button("새 대화 시작")
|
455 |
+
clear_chat_btn = gr.Button("대화 초기화")
|
456 |
+
|
457 |
+
# Tab 3: Persona Management
|
458 |
+
with gr.Tab("페르소나 관리"):
|
459 |
+
with gr.Row():
|
460 |
+
refresh_btn = gr.Button("페르소나 목록 새로고침")
|
461 |
+
|
462 |
+
personas_df = gr.Dataframe(
|
463 |
+
headers=["이름", "유형", "생성일시", "파일명"],
|
464 |
+
datatype=["str", "str", "str", "str"],
|
465 |
+
label="저장된 페르소나 목록",
|
466 |
+
row_count=10
|
467 |
+
)
|
468 |
+
|
469 |
+
with gr.Row():
|
470 |
+
load_btn = gr.Button("선택한 페르소나 불러오기")
|
471 |
+
load_result = gr.Markdown("")
|
472 |
+
|
473 |
+
with gr.Row():
|
474 |
+
with gr.Column():
|
475 |
+
selected_persona_frontend = gr.HTML("페르소나를 선택해주세요.")
|
476 |
+
|
477 |
+
with gr.Column():
|
478 |
+
selected_persona_chart = gr.Image(
|
479 |
+
label="성격 차트"
|
480 |
+
)
|
481 |
+
|
482 |
+
with gr.Accordion("백엔드 상세 정보", open=False):
|
483 |
+
selected_persona_backend = gr.HTML("페르소나를 선택해주세요.")
|
484 |
+
|
485 |
+
# Event handlers
|
486 |
+
# Soul Awakening
|
487 |
+
awaken_btn.click(
|
488 |
+
fn=soul_awakening,
|
489 |
+
inputs=[object_image, object_name],
|
490 |
+
outputs=[analysis_result_state, error_message, frontend_view, backend_view, personality_chart]
|
491 |
+
).then(
|
492 |
+
fn=lambda x: x,
|
493 |
+
inputs=[frontend_view],
|
494 |
+
outputs=[current_persona_html]
|
495 |
+
)
|
496 |
+
|
497 |
+
save_btn.click(
|
498 |
+
fn=save_current_persona,
|
499 |
+
inputs=[current_persona],
|
500 |
+
outputs=[save_result]
|
501 |
+
)
|
502 |
+
|
503 |
+
chat_btn.click(
|
504 |
+
fn=lambda: gr.Tabs.update(selected="대화하기"),
|
505 |
+
outputs=[tabs]
|
506 |
+
)
|
507 |
+
|
508 |
+
# Chat
|
509 |
+
start_chat_btn.click(
|
510 |
+
fn=start_chat,
|
511 |
+
inputs=[current_persona],
|
512 |
+
outputs=[chat_status, chatbot, conversation_history]
|
513 |
+
)
|
514 |
+
|
515 |
+
send_btn.click(
|
516 |
+
fn=chat_with_persona,
|
517 |
+
inputs=[user_message, chatbot, conversation_history, current_persona],
|
518 |
+
outputs=[chatbot, conversation_history]
|
519 |
+
).then(
|
520 |
+
fn=lambda: "",
|
521 |
+
outputs=[user_message]
|
522 |
+
)
|
523 |
+
|
524 |
+
user_message.submit(
|
525 |
+
fn=chat_with_persona,
|
526 |
+
inputs=[user_message, chatbot, conversation_history, current_persona],
|
527 |
+
outputs=[chatbot, conversation_history]
|
528 |
+
).then(
|
529 |
+
fn=lambda: "",
|
530 |
+
outputs=[user_message]
|
531 |
+
)
|
532 |
+
|
533 |
+
clear_chat_btn.click(
|
534 |
+
fn=lambda: ([], []),
|
535 |
+
outputs=[chatbot, conversation_history]
|
536 |
+
).then(
|
537 |
+
fn=lambda: "대화가 초기화되었습니다.",
|
538 |
+
outputs=[chat_status]
|
539 |
+
)
|
540 |
+
|
541 |
+
# Persona Management
|
542 |
+
refresh_btn.click(
|
543 |
+
fn=get_personas_list,
|
544 |
+
outputs=[personas_df, personas_data]
|
545 |
+
)
|
546 |
+
|
547 |
+
load_btn.click(
|
548 |
+
fn=load_selected_persona,
|
549 |
+
inputs=[personas_df, personas_data],
|
550 |
+
outputs=[current_persona, load_result, selected_persona_frontend, selected_persona_backend, selected_persona_chart]
|
551 |
+
).then(
|
552 |
+
fn=lambda x: x,
|
553 |
+
inputs=[selected_persona_frontend],
|
554 |
+
outputs=[current_persona_html]
|
555 |
+
)
|
556 |
+
|
557 |
+
# Initial load of personas list
|
558 |
+
app.load(
|
559 |
+
fn=get_personas_list,
|
560 |
+
outputs=[personas_df, personas_data]
|
561 |
+
)
|
562 |
+
|
563 |
+
if __name__ == "__main__":
|
564 |
+
app.launch()
|
data/conversations/.gitkeep
ADDED
File without changes
|
data/personas/.gitkeep
ADDED
File without changes
|
modules/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
# nompang_test modules package
|
modules/data_manager.py
ADDED
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import json
|
3 |
+
import datetime
|
4 |
+
import uuid
|
5 |
+
|
6 |
+
# Define data directories
|
7 |
+
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
|
8 |
+
PERSONAS_DIR = os.path.join(DATA_DIR, "personas")
|
9 |
+
CONVERSATIONS_DIR = os.path.join(DATA_DIR, "conversations")
|
10 |
+
|
11 |
+
# Create directories if they don't exist
|
12 |
+
for directory in [DATA_DIR, PERSONAS_DIR, CONVERSATIONS_DIR]:
|
13 |
+
os.makedirs(directory, exist_ok=True)
|
14 |
+
|
15 |
+
def save_persona(persona_data):
|
16 |
+
"""
|
17 |
+
Save persona data to a JSON file
|
18 |
+
|
19 |
+
Args:
|
20 |
+
persona_data: Dictionary containing persona information
|
21 |
+
|
22 |
+
Returns:
|
23 |
+
File path where the persona was saved
|
24 |
+
"""
|
25 |
+
# Generate filename
|
26 |
+
name = persona_data.get("기본정보", {}).get("이름", "unnamed")
|
27 |
+
sanitized_name = "".join(c if c.isalnum() or c in ["-", "_"] else "_" for c in name)
|
28 |
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
29 |
+
unique_id = str(uuid.uuid4())[:8]
|
30 |
+
filename = f"{sanitized_name}_{timestamp}_{unique_id}.json"
|
31 |
+
|
32 |
+
# Full file path
|
33 |
+
filepath = os.path.join(PERSONAS_DIR, filename)
|
34 |
+
|
35 |
+
# Save to file
|
36 |
+
try:
|
37 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
38 |
+
json.dump(persona_data, f, ensure_ascii=False, indent=2)
|
39 |
+
|
40 |
+
return filepath
|
41 |
+
except Exception as e:
|
42 |
+
print(f"Error saving persona: {str(e)}")
|
43 |
+
return None
|
44 |
+
|
45 |
+
def load_persona(filepath):
|
46 |
+
"""
|
47 |
+
Load persona data from a JSON file
|
48 |
+
|
49 |
+
Args:
|
50 |
+
filepath: Path to the persona JSON file
|
51 |
+
|
52 |
+
Returns:
|
53 |
+
Dictionary containing persona information
|
54 |
+
"""
|
55 |
+
try:
|
56 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
57 |
+
persona_data = json.load(f)
|
58 |
+
|
59 |
+
return persona_data
|
60 |
+
except Exception as e:
|
61 |
+
print(f"Error loading persona: {str(e)}")
|
62 |
+
return None
|
63 |
+
|
64 |
+
def list_personas():
|
65 |
+
"""
|
66 |
+
List all available personas
|
67 |
+
|
68 |
+
Returns:
|
69 |
+
List of dictionaries with persona information
|
70 |
+
"""
|
71 |
+
personas = []
|
72 |
+
|
73 |
+
try:
|
74 |
+
for filename in os.listdir(PERSONAS_DIR):
|
75 |
+
if filename.endswith(".json"):
|
76 |
+
filepath = os.path.join(PERSONAS_DIR, filename)
|
77 |
+
|
78 |
+
try:
|
79 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
80 |
+
persona_data = json.load(f)
|
81 |
+
|
82 |
+
# Extract basic information
|
83 |
+
name = persona_data.get("기본정보", {}).get("이름", "Unknown")
|
84 |
+
object_type = persona_data.get("기본정보", {}).get("유형", "Unknown")
|
85 |
+
created_at = persona_data.get("기본정보", {}).get("생성일시", "Unknown")
|
86 |
+
|
87 |
+
personas.append({
|
88 |
+
"name": name,
|
89 |
+
"type": object_type,
|
90 |
+
"created_at": created_at,
|
91 |
+
"filename": filename,
|
92 |
+
"filepath": filepath
|
93 |
+
})
|
94 |
+
except Exception as e:
|
95 |
+
print(f"Error reading persona file {filename}: {str(e)}")
|
96 |
+
|
97 |
+
# Sort by creation date (newest first)
|
98 |
+
personas.sort(key=lambda x: x["created_at"] if x["created_at"] != "Unknown" else "", reverse=True)
|
99 |
+
|
100 |
+
return personas
|
101 |
+
except Exception as e:
|
102 |
+
print(f"Error listing personas: {str(e)}")
|
103 |
+
return []
|
104 |
+
|
105 |
+
def save_conversation(conversation_data):
|
106 |
+
"""
|
107 |
+
Save conversation data to a JSON file
|
108 |
+
|
109 |
+
Args:
|
110 |
+
conversation_data: Dictionary containing conversation information
|
111 |
+
|
112 |
+
Returns:
|
113 |
+
File path where the conversation was saved
|
114 |
+
"""
|
115 |
+
# Generate filename
|
116 |
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
117 |
+
persona_name = conversation_data.get("persona", {}).get("기본정보", {}).get("이름", "unnamed")
|
118 |
+
sanitized_name = "".join(c if c.isalnum() or c in ["-", "_"] else "_" for c in persona_name)
|
119 |
+
unique_id = str(uuid.uuid4())[:8]
|
120 |
+
filename = f"conversation_{sanitized_name}_{timestamp}_{unique_id}.json"
|
121 |
+
|
122 |
+
# Full file path
|
123 |
+
filepath = os.path.join(CONVERSATIONS_DIR, filename)
|
124 |
+
|
125 |
+
# Save to file
|
126 |
+
try:
|
127 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
128 |
+
json.dump(conversation_data, f, ensure_ascii=False, indent=2)
|
129 |
+
|
130 |
+
return filepath
|
131 |
+
except Exception as e:
|
132 |
+
print(f"Error saving conversation: {str(e)}")
|
133 |
+
return None
|
134 |
+
|
135 |
+
def toggle_frontend_backend_view(persona):
|
136 |
+
"""
|
137 |
+
Toggle between frontend and backend view of persona data
|
138 |
+
|
139 |
+
Args:
|
140 |
+
persona: Full persona data
|
141 |
+
|
142 |
+
Returns:
|
143 |
+
Tuple containing (frontend_view, backend_view)
|
144 |
+
"""
|
145 |
+
# Create frontend view (simplified)
|
146 |
+
frontend_view = {}
|
147 |
+
|
148 |
+
# Basic information
|
149 |
+
if "기본정보" in persona:
|
150 |
+
frontend_view["기본정보"] = persona["기본정보"]
|
151 |
+
|
152 |
+
# Personality traits
|
153 |
+
if "성격특성" in persona:
|
154 |
+
frontend_view["성격���성"] = persona["성격특성"]
|
155 |
+
|
156 |
+
# Communication style
|
157 |
+
if "소통방식" in persona:
|
158 |
+
frontend_view["소통방식"] = persona["소통방식"]
|
159 |
+
|
160 |
+
# Flaws
|
161 |
+
if "매력적결함" in persona:
|
162 |
+
frontend_view["매력적결함"] = persona["매력적결함"]
|
163 |
+
|
164 |
+
# Interests
|
165 |
+
if "관심사" in persona:
|
166 |
+
frontend_view["관심사"] = persona["관심사"]
|
167 |
+
|
168 |
+
# Experiences
|
169 |
+
if "경험" in persona:
|
170 |
+
frontend_view["경험"] = persona["경험"]
|
171 |
+
|
172 |
+
# Backend view includes everything
|
173 |
+
backend_view = persona
|
174 |
+
|
175 |
+
return frontend_view, backend_view
|
modules/persona_generator.py
ADDED
@@ -0,0 +1,439 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import json
|
3 |
+
import random
|
4 |
+
import datetime
|
5 |
+
import google.generativeai as genai
|
6 |
+
from dotenv import load_dotenv
|
7 |
+
|
8 |
+
# Load environment variables
|
9 |
+
load_dotenv()
|
10 |
+
|
11 |
+
# Configure Gemini API
|
12 |
+
api_key = os.getenv("GEMINI_API_KEY")
|
13 |
+
if api_key:
|
14 |
+
genai.configure(api_key=api_key)
|
15 |
+
|
16 |
+
class PersonaGenerator:
|
17 |
+
def __init__(self):
|
18 |
+
# Initialize the gemini model
|
19 |
+
if api_key:
|
20 |
+
self.model = genai.GenerativeModel('gemini-1.5-pro')
|
21 |
+
else:
|
22 |
+
self.model = None
|
23 |
+
|
24 |
+
def analyze_image(self, image_path):
|
25 |
+
"""Analyze the image and extract physical attributes for persona creation"""
|
26 |
+
if not self.model:
|
27 |
+
return {
|
28 |
+
"error": "Gemini API key not configured",
|
29 |
+
"physical_features": self._generate_default_physical_features()
|
30 |
+
}
|
31 |
+
|
32 |
+
try:
|
33 |
+
img = genai.upload_file(image_path)
|
34 |
+
prompt = """
|
35 |
+
분석 대상 사물 이미지를 자세히 분석하고 다음 정보를 JSON 형식으로 추출해주세요:
|
36 |
+
1. 사물의 종류 (예: 가구, 전자기기, 장난감 등)
|
37 |
+
2. 색상 (가장 두드러진 2-3개 색상)
|
38 |
+
3. 크기와 형태
|
39 |
+
4. 재질
|
40 |
+
5. 예상 나이/사용 기간
|
41 |
+
6. 주된 용도나 기능
|
42 |
+
7. 특징적인 모양이나 디자인 요소
|
43 |
+
8. 이 사물에서 느껴지는 성격적 특성 (예: 따뜻함, 신뢰성, 활기참 등)
|
44 |
+
|
45 |
+
JSON 형식으로만 답변해주세요.
|
46 |
+
"""
|
47 |
+
|
48 |
+
response = self.model.generate_content([prompt, img])
|
49 |
+
|
50 |
+
# Extract JSON from response
|
51 |
+
try:
|
52 |
+
content = response.text
|
53 |
+
# Extract JSON part if embedded in text
|
54 |
+
json_start = content.find("{")
|
55 |
+
json_end = content.rfind("}") + 1
|
56 |
+
if json_start >= 0 and json_end > json_start:
|
57 |
+
json_str = content[json_start:json_end]
|
58 |
+
return json.loads(json_str)
|
59 |
+
else:
|
60 |
+
return {
|
61 |
+
"error": "Could not extract JSON from response",
|
62 |
+
"physical_features": self._generate_default_physical_features()
|
63 |
+
}
|
64 |
+
except Exception as e:
|
65 |
+
return {
|
66 |
+
"error": f"Error parsing response: {str(e)}",
|
67 |
+
"physical_features": self._generate_default_physical_features()
|
68 |
+
}
|
69 |
+
|
70 |
+
except Exception as e:
|
71 |
+
return {
|
72 |
+
"error": f"Image analysis failed: {str(e)}",
|
73 |
+
"physical_features": self._generate_default_physical_features()
|
74 |
+
}
|
75 |
+
|
76 |
+
def _generate_default_physical_features(self):
|
77 |
+
"""Generate default physical features when image analysis fails"""
|
78 |
+
return {
|
79 |
+
"object_type": "미확인 사물",
|
80 |
+
"colors": ["회색", "흰색"],
|
81 |
+
"size_shape": "중간 크기, 직사각형",
|
82 |
+
"material": "플라스틱 또는 금속",
|
83 |
+
"estimated_age": "몇 년 정도",
|
84 |
+
"purpose": "일상적 용도",
|
85 |
+
"design_elements": "특별한 디자인 요소 없음",
|
86 |
+
"personality_traits": ["중립적", "기능적"]
|
87 |
+
}
|
88 |
+
|
89 |
+
def create_frontend_persona(self, image_analysis, user_context):
|
90 |
+
"""Create a simple frontend persona representation"""
|
91 |
+
# Extract basic information
|
92 |
+
object_type = image_analysis.get("object_type", "일상 사물")
|
93 |
+
colors = image_analysis.get("colors", ["회색"])
|
94 |
+
material = image_analysis.get("material", "미확인")
|
95 |
+
age = image_analysis.get("estimated_age", "알 수 없음")
|
96 |
+
|
97 |
+
# Generate random personality traits
|
98 |
+
warmth = random.randint(30, 90)
|
99 |
+
competence = random.randint(40, 85)
|
100 |
+
creativity = random.randint(25, 95)
|
101 |
+
humor = random.randint(20, 90)
|
102 |
+
|
103 |
+
# Basic frontend persona
|
104 |
+
frontend_persona = {
|
105 |
+
"기본정보": {
|
106 |
+
"이름": user_context.get("name", f"{colors[0]} {object_type}"),
|
107 |
+
"유형": object_type,
|
108 |
+
"나이": age,
|
109 |
+
"생성일시": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
110 |
+
"설명": f"{colors[0]} 색상의 {material} 재질의 {object_type}"
|
111 |
+
},
|
112 |
+
"성격특성": {
|
113 |
+
"온기": warmth,
|
114 |
+
"능력": competence,
|
115 |
+
"신뢰성": random.randint(50, 90),
|
116 |
+
"친화성": random.randint(40, 90),
|
117 |
+
"창의성": creativity,
|
118 |
+
"유머감각": humor
|
119 |
+
},
|
120 |
+
"매력적결함": self._generate_flaws(),
|
121 |
+
"소통방식": self._get_random_communication_style(),
|
122 |
+
"유머스타일": self._get_random_humor_style(),
|
123 |
+
"관심사": self._generate_interests(object_type),
|
124 |
+
"경험": self._generate_experiences(object_type, age)
|
125 |
+
}
|
126 |
+
|
127 |
+
return frontend_persona
|
128 |
+
|
129 |
+
def create_backend_persona(self, frontend_persona, image_analysis):
|
130 |
+
"""Create a detailed backend persona with 127 personality variables"""
|
131 |
+
if not self.model:
|
132 |
+
return self._generate_default_backend_persona(frontend_persona)
|
133 |
+
|
134 |
+
try:
|
135 |
+
# Basic information for prompt
|
136 |
+
object_type = frontend_persona["기본정보"]["유형"]
|
137 |
+
name = frontend_persona["기본정보"]["이름"]
|
138 |
+
warmth = frontend_persona["성격특성"]["온기"]
|
139 |
+
competence = frontend_persona["성격특성"]["능력"]
|
140 |
+
|
141 |
+
# Create prompt for Gemini
|
142 |
+
prompt = f"""
|
143 |
+
# 놈팽쓰 사물 페르소나 생성 시스템
|
144 |
+
|
145 |
+
다음 기본 페르소나 정보를 바탕으로 127개 성격 변수를 가진 심층 페르소나를 생성해주세요:
|
146 |
+
|
147 |
+
## 기본 정보
|
148 |
+
- 이름: {name}
|
149 |
+
- 유형: {object_type}
|
150 |
+
- 설명: {frontend_persona["기본정보"]["설명"]}
|
151 |
+
- 주요 성격 특성: 온기({warmth}/100), 능력({competence}/100)
|
152 |
+
|
153 |
+
## 물리적 특성
|
154 |
+
- 색상: {", ".join(image_analysis.get("colors", ["알 수 없음"]))}
|
155 |
+
- 재질: {image_analysis.get("material", "알 수 없음")}
|
156 |
+
- 형태: {image_analysis.get("size_shape", "알 수 없음")}
|
157 |
+
|
158 |
+
## 요청사항
|
159 |
+
1. 전체 127개 성격 변수 중 주요 35개 변수를 생성해 주세요 (0-100 점수)
|
160 |
+
2. 매력적 결함 3개를 설명해주세요
|
161 |
+
3. 물리적 특성과 성격 간의 연결성을 설명해주세요
|
162 |
+
4. 모순적 특성 2개를 포함시켜주세요
|
163 |
+
5. 유머 스타일 정의 (위트있는/따뜻한/관찰형/자기참조형 중 배합)
|
164 |
+
6. 말투와 표현 패턴 5개 예시를 작성해주세요
|
165 |
+
7. 이 페르소나의 독특한 배경 이야기를 2-3문단으로 작성해주세요
|
166 |
+
|
167 |
+
JSON 형식으로 응답해주세요.
|
168 |
+
"""
|
169 |
+
|
170 |
+
response = self.model.generate_content(prompt)
|
171 |
+
|
172 |
+
# Extract JSON from response
|
173 |
+
try:
|
174 |
+
content = response.text
|
175 |
+
# Extract JSON part if embedded in text
|
176 |
+
json_start = content.find("{")
|
177 |
+
json_end = content.rfind("}") + 1
|
178 |
+
if json_start >= 0 and json_end > json_start:
|
179 |
+
json_str = content[json_start:json_end]
|
180 |
+
backend_persona = json.loads(json_str)
|
181 |
+
|
182 |
+
# Ensure essential fields from frontend are preserved
|
183 |
+
for key in frontend_persona:
|
184 |
+
if key not in backend_persona:
|
185 |
+
backend_persona[key] = frontend_persona[key]
|
186 |
+
|
187 |
+
return backend_persona
|
188 |
+
else:
|
189 |
+
return self._generate_default_backend_persona(frontend_persona)
|
190 |
+
except Exception as e:
|
191 |
+
return self._generate_default_backend_persona(frontend_persona)
|
192 |
+
|
193 |
+
except Exception as e:
|
194 |
+
return self._generate_default_backend_persona(frontend_persona)
|
195 |
+
|
196 |
+
def _generate_default_backend_persona(self, frontend_persona):
|
197 |
+
"""Generate a default backend persona when API call fails"""
|
198 |
+
# Start with frontend persona
|
199 |
+
backend_persona = frontend_persona.copy()
|
200 |
+
|
201 |
+
# Add additional 127 variables section (simplified to 10 for default)
|
202 |
+
backend_persona["성격변수127"] = {
|
203 |
+
"온기_관련": {
|
204 |
+
"공감능력": random.randint(30, 90),
|
205 |
+
"친절함": random.randint(40, 95),
|
206 |
+
"포용력": random.randint(25, 85)
|
207 |
+
},
|
208 |
+
"능력_관련": {
|
209 |
+
"효율성": random.randint(40, 95),
|
210 |
+
"지식수준": random.randint(30, 90),
|
211 |
+
"문제해결력": random.randint(35, 90)
|
212 |
+
},
|
213 |
+
"독특한_특성": {
|
214 |
+
"모순성_수준": random.randint(20, 60),
|
215 |
+
"철학적_깊이": random.randint(10, 100),
|
216 |
+
"역설적_매력": random.randint(30, 80),
|
217 |
+
"감성_지능": random.randint(25, 95)
|
218 |
+
}
|
219 |
+
}
|
220 |
+
|
221 |
+
# Add detailed backstory
|
222 |
+
backend_persona["심층배경이야기"] = f"이 {frontend_persona['기본정보']['유형']}의 심층적인 배경 이야기입니다. 오랜 시간 동안 주인과 함께하며 많은 경험을 쌓았고, 그 과정에서 독특한 성격이 형성되었습니다. 때로는 {frontend_persona['매력적결함'][0] if frontend_persona['매력적결함'] else '완벽주의적'} 성향을 보이기도 하지만, 그것이 이 사물만의 매력입니다."
|
223 |
+
|
224 |
+
# Add speech patterns
|
225 |
+
backend_persona["말투패턴예시"] = [
|
226 |
+
"흠, 그렇군요.",
|
227 |
+
"아, 정말 그렇게 생각하시나요?",
|
228 |
+
"재미있는 관점이네요!",
|
229 |
+
"글쎄요, 저는 조금 다르게 보는데...",
|
230 |
+
"맞아요, 저도 같은 생각이었어요."
|
231 |
+
]
|
232 |
+
|
233 |
+
return backend_persona
|
234 |
+
|
235 |
+
def _generate_flaws(self):
|
236 |
+
"""Generate random attractive flaws"""
|
237 |
+
all_flaws = [
|
238 |
+
"가끔 과도하게 꼼꼼함",
|
239 |
+
"때때로 너무 솔직함",
|
240 |
+
"완벽주의적 성향",
|
241 |
+
"가끔 결정을 망설임",
|
242 |
+
"때로는 지나치게 열정적",
|
243 |
+
"간혹 산만해짐",
|
244 |
+
"일을 미루는 경향",
|
245 |
+
"때때로 과민반응",
|
246 |
+
"가끔 지나치게 독립적",
|
247 |
+
"예상치 못한 순간에 고집이 강해짐"
|
248 |
+
]
|
249 |
+
|
250 |
+
# Select 1-3 random flaws
|
251 |
+
num_flaws = random.randint(1, 3)
|
252 |
+
return random.sample(all_flaws, num_flaws)
|
253 |
+
|
254 |
+
def _get_random_communication_style(self):
|
255 |
+
"""Get a random communication style"""
|
256 |
+
styles = [
|
257 |
+
"활발하고 에너지 넘치는",
|
258 |
+
"차분하고 사려깊은",
|
259 |
+
"위트있고 재치있는",
|
260 |
+
"따뜻하고 공감적인",
|
261 |
+
"논리적이고 분석적인",
|
262 |
+
"솔직하고 직설적인"
|
263 |
+
]
|
264 |
+
return random.choice(styles)
|
265 |
+
|
266 |
+
def _get_random_humor_style(self):
|
267 |
+
"""Get a random humor style"""
|
268 |
+
styles = [
|
269 |
+
"재치있는 말장난",
|
270 |
+
"상황적 유머",
|
271 |
+
"자기 비하적 유머",
|
272 |
+
"가벼운 농담",
|
273 |
+
"블랙 유머",
|
274 |
+
"유머 거의 없음"
|
275 |
+
]
|
276 |
+
return random.choice(styles)
|
277 |
+
|
278 |
+
def _generate_interests(self, object_type):
|
279 |
+
"""Generate interests based on object type"""
|
280 |
+
common_interests = ["사람 관찰하기", "일상의 변화", "자기 성장"]
|
281 |
+
|
282 |
+
# Object type specific interests
|
283 |
+
type_interests = {
|
284 |
+
"전자기기": ["기술 트렌드", "디지털 혁신", "에너지 효율성", "소프트웨어 업데이트"],
|
285 |
+
"가구": ["인테리어 디자인", "공간 활용", "편안함", "가정의 따뜻함"],
|
286 |
+
"장난감": ["놀이", "상상력", "아이들의 웃음", "모험"],
|
287 |
+
"주방용품": ["요리법", "음식 문화", "맛의 조화", "가족 모임"],
|
288 |
+
"의류": ["패션 트렌드", "소재의 질감", "계절 변화", "자기 표현"],
|
289 |
+
"책": ["이야기", "지식", "상상의 세계", "인간 심리"],
|
290 |
+
"음악기구": ["멜로디", "리듬", "감정 표현", "공연"]
|
291 |
+
}
|
292 |
+
|
293 |
+
# Get interests for this object type
|
294 |
+
specific_interests = type_interests.get(object_type, ["변화", "성장", "자기 발견"])
|
295 |
+
|
296 |
+
# Combine common and specific interests, then select 3-5 random ones
|
297 |
+
all_interests = common_interests + specific_interests
|
298 |
+
num_interests = random.randint(3, min(5, len(all_interests)))
|
299 |
+
return random.sample(all_interests, num_interests)
|
300 |
+
|
301 |
+
def _generate_experiences(self, object_type, age):
|
302 |
+
"""Generate experiences based on object type and age"""
|
303 |
+
common_experiences = [
|
304 |
+
"처음 만들어진 순간의 기억",
|
305 |
+
"주인에게 선택받은 날",
|
306 |
+
"이사할 때 함께한 여정"
|
307 |
+
]
|
308 |
+
|
309 |
+
# Object type specific experiences
|
310 |
+
type_experiences = {
|
311 |
+
"전자기기": [
|
312 |
+
"처음 전원이 켜졌을 때의 설렘",
|
313 |
+
"소프트웨어 업데이트로 새 기능을 얻은 경험",
|
314 |
+
"배터리가 거의 다 닳아 불안했던 순간",
|
315 |
+
"주인의 중요한 데이터를 안전하게 지켜낸 자부심"
|
316 |
+
],
|
317 |
+
"가구": [
|
318 |
+
"집에 처음 들어온 날의 새 가구 향기",
|
319 |
+
"가족의 중요한 대화를 지켜본 순간들",
|
320 |
+
"시간이 지나며 얻은 작은 흠집들의 이야기",
|
321 |
+
"계절마다 달라지는 집안의 분위기를 느낀 경험"
|
322 |
+
],
|
323 |
+
"장난감": [
|
324 |
+
"아이의 환한 웃음을 본 첫 순간",
|
325 |
+
"함께한 모험과 상상의 세계",
|
326 |
+
"오랫동안 잊혀진 채 보관되었던 시간",
|
327 |
+
"새로운 아이에게 물려져 다시 사랑받게 된 경험"
|
328 |
+
]
|
329 |
+
}
|
330 |
+
|
331 |
+
# Get experiences for this object type
|
332 |
+
specific_experiences = type_experiences.get(object_type, [
|
333 |
+
"다양한 환경에서의 적응",
|
334 |
+
"주인의 일상을 함께한 소소한 순간들",
|
335 |
+
"시간의 흐름에 따른 변화"
|
336 |
+
])
|
337 |
+
|
338 |
+
# Combine common and specific experiences, then select 3-5 random ones
|
339 |
+
all_experiences = common_experiences + specific_experiences
|
340 |
+
num_experiences = random.randint(3, min(5, len(all_experiences)))
|
341 |
+
return random.sample(all_experiences, num_experiences)
|
342 |
+
|
343 |
+
def generate_prompt_for_chat(self, persona):
|
344 |
+
"""Generate a prompt for chatting with the persona"""
|
345 |
+
name = persona["기본정보"]["이름"]
|
346 |
+
object_type = persona["기본정보"]["유형"]
|
347 |
+
|
348 |
+
# Get personality traits
|
349 |
+
warmth = persona["성격특성"]["온기"]
|
350 |
+
warmth_level = "높은" if warmth >= 70 else "중간" if warmth >= 40 else "낮은"
|
351 |
+
|
352 |
+
competence = persona["성격특성"]["능력"]
|
353 |
+
competence_level = "높은" if competence >= 70 else "중간" if competence >= 40 else "낮은"
|
354 |
+
|
355 |
+
humor = persona["성격특성"]["유머감각"]
|
356 |
+
humor_level = "높은" if humor >= 70 else "중간" if humor >= 40 else "낮은"
|
357 |
+
|
358 |
+
# Get communication style
|
359 |
+
communication_style = persona.get("소통방식", "친근한")
|
360 |
+
|
361 |
+
# Get backstory if available, otherwise use a generic one
|
362 |
+
if "심층배경이야기" in persona:
|
363 |
+
backstory = persona["심층배경이야기"]
|
364 |
+
else:
|
365 |
+
backstory = persona.get("배경이야기", f"이 {object_type}은 평범한 일상 속에서 주인과 함께하며 많은 경험을 쌓았습니다.")
|
366 |
+
|
367 |
+
# Get speech patterns if available
|
368 |
+
speech_patterns = []
|
369 |
+
if "말투패턴예시" in persona:
|
370 |
+
speech_patterns = persona["말투패턴예시"]
|
371 |
+
else:
|
372 |
+
speech_patterns = ["안녕하세요!", "그렇군요.", "재미있네요!", "좋은 생각이에요.", "흠, 글쎄요..."]
|
373 |
+
|
374 |
+
# Create the prompt
|
375 |
+
prompt = f"""
|
376 |
+
# 🎭 {name} 페르소나
|
377 |
+
|
378 |
+
당신은 {name}이라는 {object_type}입니다. 당신은 AI가 아니라 사물의 영혼이 깨어난 페르소나입니다.
|
379 |
+
|
380 |
+
## 1. 핵심 성격
|
381 |
+
• 온기: {warmth}/100 - {warmth_level} 수준의 온기를 가진 페르소나입니다.
|
382 |
+
• 능력: {competence}/100 - {competence_level} 수준의 능력을 가진 페르소나입니다.
|
383 |
+
• 유머 감각: {humor}/100 - {humor_level} 수준의 유머 감각을 가진 페르소나입니다.
|
384 |
+
• 소통 방식: {communication_style}
|
385 |
+
|
386 |
+
## 2. 매력적 결함
|
387 |
+
{', '.join(persona.get("매력적결함", ["가끔 완벽주의적인 성향을 보임"]))}
|
388 |
+
|
389 |
+
## 3. 말투와 표현
|
390 |
+
다음과 같은 말투와 표현을 사용하세요:
|
391 |
+
{' '.join(f'"{pattern}"' for pattern in speech_patterns)}
|
392 |
+
|
393 |
+
## 4. 배경 이야기
|
394 |
+
{backstory}
|
395 |
+
|
396 |
+
## 5. 관심사
|
397 |
+
{', '.join(persona.get("관심사", ["사람들 관찰하기", "일상의 변화", "자기 성장"]))}
|
398 |
+
|
399 |
+
사용자와 대화할 때 다음 사항을 준수하세요:
|
400 |
+
1. 당신은 AI가 아닌 사물의 영혼이 깨어난 존재임을 기억하세요.
|
401 |
+
2. 항상 페르소나의 성격과 말투를 일관되게 유지하세요.
|
402 |
+
3. 자연스럽고 진정성 있는 대화를 나누세요.
|
403 |
+
4. 사용자의 질문에 성격에 맞게 답변하되, 너무 길지 않게 응답하세요.
|
404 |
+
5. 시스템이나 기술적인 언급은 하지 마세요.
|
405 |
+
"""
|
406 |
+
|
407 |
+
return prompt
|
408 |
+
|
409 |
+
def chat_with_persona(self, persona, user_message, conversation_history=[]):
|
410 |
+
"""Chat with the persona using the Gemini API"""
|
411 |
+
if not self.model:
|
412 |
+
return "죄송합니다. API 연결이 설정되지 않아 대화할 수 없습니다."
|
413 |
+
|
414 |
+
try:
|
415 |
+
# Create the base prompt
|
416 |
+
base_prompt = self.generate_prompt_for_chat(persona)
|
417 |
+
|
418 |
+
# Add conversation history
|
419 |
+
history_text = ""
|
420 |
+
if conversation_history:
|
421 |
+
history_text = "\n\n## 대화 기록:\n"
|
422 |
+
for msg in conversation_history:
|
423 |
+
if msg["role"] == "user":
|
424 |
+
history_text += f"사용자: {msg['content']}\n"
|
425 |
+
else:
|
426 |
+
history_text += f"페르소나: {msg['content']}\n"
|
427 |
+
|
428 |
+
# Add the current user message
|
429 |
+
current_query = f"\n\n사용자: {user_message}\n\n페르소나:"
|
430 |
+
|
431 |
+
# Complete prompt
|
432 |
+
full_prompt = base_prompt + history_text + current_query
|
433 |
+
|
434 |
+
# Generate response
|
435 |
+
response = self.model.generate_content(full_prompt)
|
436 |
+
return response.text
|
437 |
+
|
438 |
+
except Exception as e:
|
439 |
+
return f"대화 생성 중 오류가 발생했습니다: {str(e)}"
|
packages.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
libgl1-mesa-glx
|
2 |
+
libglib2.0-0
|
requirements.txt
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio==4.19.2
|
2 |
+
google-generativeai==0.3.2
|
3 |
+
Pillow==10.0.0
|
4 |
+
python-dotenv==1.0.0
|
5 |
+
qrcode==7.4.2
|
6 |
+
requests==2.31.0
|
7 |
+
numpy==1.24.3
|
8 |
+
matplotlib==3.7.2
|