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
|