Spaces:
Sleeping
Sleeping
Upload 12 files
Browse files- app.py +347 -0
- info.md +475 -0
- modules/chat_engine.py +256 -0
- modules/data_manager.py +361 -0
- modules/gemini_handler.py +297 -0
- modules/image_analyzer.py +304 -0
- modules/persona_generator.py +349 -0
- modules/prompt_templates.py +307 -0
- modules/question_generator.py +229 -0
- packages.txt +2 -0
- requirements.txt +6 -0
- styles/custom.css +224 -0
app.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import time
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
# 모듈 임포트
|
| 8 |
+
from modules.image_analyzer import analyze_image
|
| 9 |
+
from modules.question_generator import generate_questions
|
| 10 |
+
from modules.persona_generator import generate_persona
|
| 11 |
+
from modules.chat_engine import start_conversation, process_message
|
| 12 |
+
from modules.data_manager import save_persona, load_persona, list_personas, save_conversation
|
| 13 |
+
from modules.gemini_handler import get_persona_enhancement
|
| 14 |
+
|
| 15 |
+
# 디렉토리 확인
|
| 16 |
+
try:
|
| 17 |
+
for dir_path in [
|
| 18 |
+
"data/user_personas",
|
| 19 |
+
"data/conversation_logs",
|
| 20 |
+
"assets/examples"
|
| 21 |
+
]:
|
| 22 |
+
os.makedirs(dir_path, exist_ok=True)
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print(f"초기 디렉토리 생성 중 오류: {str(e)}")
|
| 25 |
+
print("허깅페이스 환경에서는 일부 파일 저장 기능이 제한될 수 있습니다.")
|
| 26 |
+
|
| 27 |
+
# 기본 성격 특성 범위
|
| 28 |
+
trait_range = {"min": 0, "max": 100, "step": 5, "value": 50}
|
| 29 |
+
|
| 30 |
+
# UI 테마 및 스타일 설정
|
| 31 |
+
theme = gr.themes.Soft(
|
| 32 |
+
primary_hue="indigo",
|
| 33 |
+
secondary_hue="blue",
|
| 34 |
+
).set(
|
| 35 |
+
button_primary_background_fill="*primary_500",
|
| 36 |
+
button_primary_background_fill_hover="*primary_600",
|
| 37 |
+
button_primary_text_color="white",
|
| 38 |
+
block_title_text_color="*primary_800",
|
| 39 |
+
block_label_text_size="sm"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# 메인 애플리케이션
|
| 43 |
+
with gr.Blocks(title="사물 페르소나 생성기", theme=theme, css="styles/custom.css") as app:
|
| 44 |
+
gr.Markdown(
|
| 45 |
+
"""
|
| 46 |
+
# 🧸 물魂 (물건의 혼) - 사물 페르소나 생성기
|
| 47 |
+
|
| 48 |
+
일상 사물에 개성과 성격을 부여하여 대화할 수 있는 페르소나 생성 및 테스트 도구입니다.
|
| 49 |
+
"""
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# 전역 상태 관리
|
| 53 |
+
current_persona = gr.State({})
|
| 54 |
+
conversation_history = gr.State([])
|
| 55 |
+
session_start_time = gr.State(None)
|
| 56 |
+
|
| 57 |
+
with gr.Tabs() as tabs:
|
| 58 |
+
# 페르소나 생성 탭
|
| 59 |
+
with gr.Tab("페르소나 생성"):
|
| 60 |
+
with gr.Row():
|
| 61 |
+
with gr.Column(scale=2):
|
| 62 |
+
gr.Markdown("## 사물 정보")
|
| 63 |
+
with gr.Row():
|
| 64 |
+
object_image = gr.Image(
|
| 65 |
+
label="사물 이미지 (선택사항)",
|
| 66 |
+
type="filepath"
|
| 67 |
+
)
|
| 68 |
+
image_analysis_btn = gr.Button("이미지 분석하기")
|
| 69 |
+
|
| 70 |
+
with gr.Row():
|
| 71 |
+
object_name = gr.Textbox(
|
| 72 |
+
label="사물 이름",
|
| 73 |
+
placeholder="예: 할아버지의 낡은 안락의자"
|
| 74 |
+
)
|
| 75 |
+
object_type = gr.Dropdown(
|
| 76 |
+
choices=[
|
| 77 |
+
"전자기기", "가구", "주방용품",
|
| 78 |
+
"의류/액세서리", "책/문구류",
|
| 79 |
+
"음악 기구", "장난감", "기타"
|
| 80 |
+
],
|
| 81 |
+
label="사물 유형"
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
object_age = gr.Textbox(
|
| 85 |
+
label="사물 나이/사용 기간",
|
| 86 |
+
placeholder="예: 15년"
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
object_description = gr.Textbox(
|
| 90 |
+
label="사물 설명",
|
| 91 |
+
placeholder="물체의 외형, 특징, 용도 등을 설명해주세요.",
|
| 92 |
+
lines=3
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
image_analysis_result = gr.JSON(
|
| 96 |
+
label="이미지 분석 결과",
|
| 97 |
+
visible=False
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
with gr.Column(scale=3):
|
| 101 |
+
gr.Markdown("## 성격 특성")
|
| 102 |
+
|
| 103 |
+
with gr.Row():
|
| 104 |
+
with gr.Column():
|
| 105 |
+
warmth = gr.Slider(
|
| 106 |
+
label="온기",
|
| 107 |
+
info="따뜻함, 친절함, 선의의 정도",
|
| 108 |
+
**trait_range
|
| 109 |
+
)
|
| 110 |
+
competence = gr.Slider(
|
| 111 |
+
label="능력",
|
| 112 |
+
info="효율성, 기술, 지능의 정도",
|
| 113 |
+
**trait_range
|
| 114 |
+
)
|
| 115 |
+
trustworthiness = gr.Slider(
|
| 116 |
+
label="신뢰성",
|
| 117 |
+
info="일관성, 정직함, 안정성의 정도",
|
| 118 |
+
**trait_range
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
with gr.Column():
|
| 122 |
+
friendliness = gr.Slider(
|
| 123 |
+
label="친화성",
|
| 124 |
+
info="사교성, 개방성, 접근성의 정도",
|
| 125 |
+
**trait_range
|
| 126 |
+
)
|
| 127 |
+
creativity = gr.Slider(
|
| 128 |
+
label="창의성",
|
| 129 |
+
info="독창성, 상상력, 혁신성의 정도",
|
| 130 |
+
**trait_range
|
| 131 |
+
)
|
| 132 |
+
humor = gr.Slider(
|
| 133 |
+
label="유머감각",
|
| 134 |
+
info="재치, 위트, 유머 표현 정도",
|
| 135 |
+
**trait_range
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
gr.Markdown("## 매력적 결함")
|
| 139 |
+
flaws = gr.CheckboxGroup(
|
| 140 |
+
choices=[
|
| 141 |
+
"때때로 너무 조심스러움",
|
| 142 |
+
"가끔 과하게 열정적",
|
| 143 |
+
"약간의 완벽주의",
|
| 144 |
+
"때로는 우울한 생각에 빠짐",
|
| 145 |
+
"간혹 산만해짐",
|
| 146 |
+
"약간의 고집",
|
| 147 |
+
"때로는 지나치게 솔직함",
|
| 148 |
+
"가끔 불안해함",
|
| 149 |
+
"종종 과거에 집착함",
|
| 150 |
+
"가끔 너무 이상적임"
|
| 151 |
+
],
|
| 152 |
+
label="매력적인 결함 (최대 2개 선택)",
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
gr.Markdown("## 소통 방식")
|
| 156 |
+
with gr.Row():
|
| 157 |
+
with gr.Column():
|
| 158 |
+
communication_style = gr.Radio(
|
| 159 |
+
choices=[
|
| 160 |
+
"활발하고 에너지 넘치는",
|
| 161 |
+
"차분하고 사려깊은",
|
| 162 |
+
"위트있고 재치있는",
|
| 163 |
+
"따뜻하고 공감적인",
|
| 164 |
+
"논리적이고 분석적인"
|
| 165 |
+
],
|
| 166 |
+
label="대화 스타일",
|
| 167 |
+
value="따뜻하고 공감적인"
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
humor_style = gr.Dropdown(
|
| 171 |
+
choices=[
|
| 172 |
+
"재치있는 말장난",
|
| 173 |
+
"상황적 유머",
|
| 174 |
+
"자기 비하적 유머",
|
| 175 |
+
"가벼운 농담",
|
| 176 |
+
"블랙 유머",
|
| 177 |
+
"유머 거의 없음"
|
| 178 |
+
],
|
| 179 |
+
multiselect=True,
|
| 180 |
+
label="유머 유형"
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
with gr.Column():
|
| 184 |
+
speech_pattern = gr.Textbox(
|
| 185 |
+
label="말투 패턴 예시",
|
| 186 |
+
placeholder="캐릭터의 특징적인 말투나 표현을 입력하세요",
|
| 187 |
+
lines=2
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
interests = gr.Textbox(
|
| 191 |
+
label="관심사 (쉼표로 구분)",
|
| 192 |
+
placeholder="음악, 요리, 여행, 역사..."
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
with gr.Row():
|
| 196 |
+
with gr.Column():
|
| 197 |
+
gr.Markdown("## 관계 성향")
|
| 198 |
+
|
| 199 |
+
with gr.Row():
|
| 200 |
+
attachment_style = gr.Radio(
|
| 201 |
+
choices=["안정형", "불안형", "회피형", "혼란형"],
|
| 202 |
+
label="애착 스타일",
|
| 203 |
+
value="안정형"
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
relationship_depth = gr.Radio(
|
| 207 |
+
choices=["얕은", "중간", "깊은"],
|
| 208 |
+
label="관계 깊이 선호도",
|
| 209 |
+
value="중간"
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
initial_attitude = gr.Radio(
|
| 213 |
+
choices=["수줍은", "중립적", "친근한", "열정적", "조심스러운"],
|
| 214 |
+
label="초기 태도",
|
| 215 |
+
value="친근한"
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
with gr.Column():
|
| 219 |
+
backstory = gr.Textbox(
|
| 220 |
+
label="배경 이야기",
|
| 221 |
+
placeholder="이 사물의 역사, 경험, 소유자와의 관계 등을 간략히 서술하세요",
|
| 222 |
+
lines=4
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
with gr.Row():
|
| 226 |
+
with gr.Column():
|
| 227 |
+
template_selector = gr.Dropdown(
|
| 228 |
+
choices=[
|
| 229 |
+
"템플릿 없음",
|
| 230 |
+
"스마트폰 - 열정적 조력자",
|
| 231 |
+
"오래된 안락의자 - 지혜로운 관찰자",
|
| 232 |
+
"커피머신 - 활기찬 에너자이저",
|
| 233 |
+
"오래된 책 - 사려깊은 학자",
|
| 234 |
+
"가죽 가방 - 신뢰할 수 있는 동반자"
|
| 235 |
+
],
|
| 236 |
+
label="템플릿 선택",
|
| 237 |
+
value="템플릿 없음"
|
| 238 |
+
)
|
| 239 |
+
load_template_btn = gr.Button("템플릿 불러오기")
|
| 240 |
+
|
| 241 |
+
with gr.Column():
|
| 242 |
+
create_btn = gr.Button("페르소나 생성하기", variant="primary")
|
| 243 |
+
|
| 244 |
+
# 생성된 페르소나 출력
|
| 245 |
+
generated_persona = gr.JSON(label="생성된 페르소나")
|
| 246 |
+
|
| 247 |
+
# 이벤트 핸들러
|
| 248 |
+
image_analysis_btn.click(
|
| 249 |
+
fn=analyze_image,
|
| 250 |
+
inputs=[object_image],
|
| 251 |
+
outputs=[image_analysis_result, warmth, competence, trustworthiness,
|
| 252 |
+
friendliness, creativity, humor, object_type, object_description]
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
create_btn.click(
|
| 256 |
+
fn=generate_persona,
|
| 257 |
+
inputs=[
|
| 258 |
+
object_name, object_type, object_age, object_description,
|
| 259 |
+
warmth, competence, trustworthiness, friendliness, creativity, humor,
|
| 260 |
+
flaws, communication_style, humor_style, speech_pattern, interests,
|
| 261 |
+
attachment_style, relationship_depth, initial_attitude, backstory,
|
| 262 |
+
image_analysis_result
|
| 263 |
+
],
|
| 264 |
+
outputs=[generated_persona, current_persona]
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
# 템플릿 로드 기능 추가 예정
|
| 268 |
+
|
| 269 |
+
# 대화 테스트 탭
|
| 270 |
+
with gr.Tab("대화 테스트"):
|
| 271 |
+
with gr.Row():
|
| 272 |
+
with gr.Column(scale=2):
|
| 273 |
+
chat_display = gr.Chatbot(
|
| 274 |
+
label="대화 내용",
|
| 275 |
+
height=500,
|
| 276 |
+
avatar_images=(None, "assets/icons/persona_avatar.png")
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
with gr.Row():
|
| 280 |
+
user_message = gr.Textbox(
|
| 281 |
+
label="메시지 입력",
|
| 282 |
+
placeholder="메시지를 입력하세요...",
|
| 283 |
+
lines=2
|
| 284 |
+
)
|
| 285 |
+
send_btn = gr.Button("전송", variant="primary")
|
| 286 |
+
|
| 287 |
+
with gr.Column(scale=1):
|
| 288 |
+
gr.Markdown("## 현재 페르소나")
|
| 289 |
+
persona_info = gr.Markdown("페르소나를 먼저 생성하거나 불러오세요.")
|
| 290 |
+
|
| 291 |
+
start_chat_btn = gr.Button("대화 시작하기")
|
| 292 |
+
load_persona_btn = gr.Button("저장된 페르소나 불러오기")
|
| 293 |
+
|
| 294 |
+
gr.Markdown("## 대화 통계")
|
| 295 |
+
chat_stats = gr.Markdown("대화가 시작되지 않았습니다.")
|
| 296 |
+
|
| 297 |
+
export_chat_btn = gr.Button("대화 내용 저장")
|
| 298 |
+
export_result = gr.Textbox(label="저장 결과", visible=False)
|
| 299 |
+
|
| 300 |
+
# 이벤트 핸들러
|
| 301 |
+
start_chat_btn.click(
|
| 302 |
+
fn=start_conversation,
|
| 303 |
+
inputs=[current_persona],
|
| 304 |
+
outputs=[chat_display, conversation_history, session_start_time, chat_stats, persona_info]
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
send_btn.click(
|
| 308 |
+
fn=process_message,
|
| 309 |
+
inputs=[user_message, conversation_history, current_persona, session_start_time],
|
| 310 |
+
outputs=[chat_display, conversation_history, user_message, chat_stats]
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
# Enter 키로 메시지 전송
|
| 314 |
+
user_message.submit(
|
| 315 |
+
fn=process_message,
|
| 316 |
+
inputs=[user_message, conversation_history, current_persona, session_start_time],
|
| 317 |
+
outputs=[chat_display, conversation_history, user_message, chat_stats]
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
# 페르소나 라이브러리 탭
|
| 321 |
+
with gr.Tab("페르소나 라이브러리"):
|
| 322 |
+
with gr.Row():
|
| 323 |
+
with gr.Column():
|
| 324 |
+
refresh_library_btn = gr.Button("라이브러리 새로고침")
|
| 325 |
+
personas_table = gr.Dataframe(
|
| 326 |
+
headers=["이름", "유형", "생성일", "파일명"],
|
| 327 |
+
datatype=["str", "str", "str", "str"],
|
| 328 |
+
label="저장된 페르소나 목록"
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
with gr.Column():
|
| 332 |
+
selected_persona_json = gr.JSON(label="선택된 페르소나 정보")
|
| 333 |
+
load_to_editor_btn = gr.Button("에디터로 불러오기")
|
| 334 |
+
load_to_chat_btn = gr.Button("대화 테스트로 불러오기")
|
| 335 |
+
|
| 336 |
+
# 이벤트 핸들러
|
| 337 |
+
refresh_library_btn.click(
|
| 338 |
+
fn=list_personas,
|
| 339 |
+
inputs=[],
|
| 340 |
+
outputs=[personas_table]
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
# 선택한 페르소나 로드 기능 추가 예정
|
| 344 |
+
|
| 345 |
+
# 앱 실행
|
| 346 |
+
if __name__ == "__main__":
|
| 347 |
+
app.launch()
|
info.md
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 사물 페르소나 생성기 (物魂 Creator)
|
| 2 |
+
|
| 3 |
+
## 1. 프로젝트 개요
|
| 4 |
+
|
| 5 |
+
사물 페르소나 생성기는 일상 사물에 고유한 성격과 개성을 부여하고 테스트할 수 있는 실험적 도구입니다. 본 도구는 "일상 사물 인격화를 통한 관계형성 AI 시스템 연구"를 뒷받침하기 위한 페르소나 생성, 테스트 및 데이터 수집을 목적으로 합니다. 사물의 물리적 특성으로부터 성격을 추론하고, 이를 다양한 설정을 통해 조정하며, 생성된 페르소나와의 대화를 통해 관계 발전 양상을 연구합니다.
|
| 6 |
+
|
| 7 |
+
## 2. 코드 구현 및 기능 모듈
|
| 8 |
+
|
| 9 |
+
### 2.1 이미지 기반 성격 추론 모듈 (`image_analyzer.py`)
|
| 10 |
+
- **이미지 분석 시스템**
|
| 11 |
+
- PIL/Pillow 라이브러리 활용 이미지 로드 및 처리
|
| 12 |
+
- 물리적 특성(형태, 색상, 재질) 추출 함수 구현
|
| 13 |
+
- 특성별 성격 매핑 로직: `analyze_image()` 함수로 구현
|
| 14 |
+
|
| 15 |
+
- **물리적 특성-성격 매핑 엔진**
|
| 16 |
+
- JSON 기반 매핑 데이터 관리: `load_trait_mappings()` 함수
|
| 17 |
+
- 형태 매핑: 곡선형/직선형/대칭형/비대칭형/단순형/복잡형
|
| 18 |
+
- 색상 매핑: 밝은/어두운/따뜻한/차가운/화려한/단색
|
| 19 |
+
- 재질 매핑: 나무/금속/유리/가죽/플라스틱/천/종이
|
| 20 |
+
- 매핑 데이터 자동 생성 및 로드 기능
|
| 21 |
+
|
| 22 |
+
### 2.2 질문 생성 및 페르소나 강화 모듈 (`question_generator.py`, `persona_generator.py`)
|
| 23 |
+
- **AI 기반 맞춤형 질문 생성기**
|
| 24 |
+
- 사물 유형별 특화 질문 템플릿 (8개 카테고리)
|
| 25 |
+
- 성격 특성별 맞춤형 질문 생성 로직
|
| 26 |
+
- Google Gemini API 활용 동적 질문 생성: `generate_llm_questions()` 함수
|
| 27 |
+
|
| 28 |
+
- **페르소나 강화 시스템**
|
| 29 |
+
- `generate_persona()`: 전체 페르소나 생성 파이프라인 함수
|
| 30 |
+
- `get_persona_enhancement()`: Google Gemini API 호출로 페르소나 풍부화
|
| 31 |
+
- 성격 특성 요약 자동 생성: `get_trait_summary()` 함수
|
| 32 |
+
- 배경 이야기, 경험 자동 생성: `generate_experiences()` 함수
|
| 33 |
+
|
| 34 |
+
### 2.3 대화 처리 모듈 (`chat_engine.py`, `gemini_handler.py`)
|
| 35 |
+
- **실시간 채팅 시스템**
|
| 36 |
+
- 대화 초기화: `start_conversation()` 함수
|
| 37 |
+
- 메시지 처리: `process_message()` 함수
|
| 38 |
+
- 대화 내용 관리 및 통계 트래킹 구현
|
| 39 |
+
|
| 40 |
+
- **응답 생성 엔진**
|
| 41 |
+
- Google Gemini API 연동: `generate_response()` 함수
|
| 42 |
+
- 프롬프트 엔지니어링: 페르소나 특성에 맞는 응답 유도
|
| 43 |
+
- 초기 인사말 생성: `generate_initial_greeting()` 함수
|
| 44 |
+
|
| 45 |
+
### 2.4 데이터 관리 모듈 (`data_manager.py`)
|
| 46 |
+
- **파일 시스템 기반 데이터 관리**
|
| 47 |
+
- 페르소나 저장: `save_persona()` 함수
|
| 48 |
+
- 페르소나 로드: `load_persona()` 함수
|
| 49 |
+
- 대화 기록 저장: `save_conversation()` 함수
|
| 50 |
+
- 페르소나 및 대화 목록화: `list_personas()`, `list_conversations()` 함수
|
| 51 |
+
|
| 52 |
+
- **데이터 분석 기능**
|
| 53 |
+
- 페르소나 특성 분포 분석: `analyze_persona_trait_distribution()` 함수
|
| 54 |
+
- 대화 통계 분석: `get_conversation_statistics()` 함수
|
| 55 |
+
|
| 56 |
+
### 2.5 프롬프트 관리 시스템 (`prompt_templates.py`)
|
| 57 |
+
- **템플릿 기반 프롬프트 관리**
|
| 58 |
+
- 페르소나 강화용 프롬프트: `PERSONA_ENHANCEMENT_TEMPLATE`
|
| 59 |
+
- 이미지 분석용 프롬프트: `IMAGE_ANALYSIS_TEMPLATE`
|
| 60 |
+
- 대화 응답용 프롬프트: `CONVERSATION_RESPONSE_TEMPLATE`
|
| 61 |
+
- 질문 생성용 프롬프트: `QUESTION_GENERATION_TEMPLATE`
|
| 62 |
+
|
| 63 |
+
- **프롬프트 포맷 함수**
|
| 64 |
+
- `format_persona_enhancement_prompt()`: 페르소나 강화 프롬프트 포맷
|
| 65 |
+
- `format_conversation_prompt()`: 대화 응답 프롬프트 포맷
|
| 66 |
+
- `format_question_generation_prompt()`: 질문 생성 프롬프트 포맷
|
| 67 |
+
|
| 68 |
+
### 2.6 웹 인터페이스 (`app.py`)
|
| 69 |
+
- **Gradio 기반 UI 구현**
|
| 70 |
+
- 3개 탭 구성: 페르소나 생성, 대화 테스트, 페르소나 라이브러리
|
| 71 |
+
- 이벤트 핸들러 연결: 버튼 클릭, 입력 제출 등
|
| 72 |
+
- 상태 관리: gr.State()를 활용한 페르소나 및 대화 상태 관리
|
| 73 |
+
|
| 74 |
+
- **요소별 UI 컴포넌트**
|
| 75 |
+
- 이미지 업로드 및 분석: gr.Image, image_analysis_btn
|
| 76 |
+
- 성격 특성 슬라이더: gr.Slider 컴포넌트 6개 활용
|
| 77 |
+
- 채팅 인터페이스: gr.Chatbot, gr.Textbox 조합
|
| 78 |
+
|
| 79 |
+
## 3. 기술 스택 및 의존성
|
| 80 |
+
|
| 81 |
+
### 3.1 핵심 라이브러리
|
| 82 |
+
- **Gradio**: 웹 인터페이스 구현 (v3.50.2 이상)
|
| 83 |
+
- **Python-dotenv**: 환경 변수 관리
|
| 84 |
+
- **Requests**: API 통신
|
| 85 |
+
- **Pillow**: 이미지 처리
|
| 86 |
+
- **NumPy**: 수치 연산
|
| 87 |
+
- **Matplotlib**: 데이터 시각화 (향후 구현 예정)
|
| 88 |
+
|
| 89 |
+
### 3.2 API 통합
|
| 90 |
+
- **Google Gemini API**: LLM 기반 텍스트 생성
|
| 91 |
+
- `gemini_query()`: API 요청 함수
|
| 92 |
+
- JSON 파싱: `extract_json()` 함수
|
| 93 |
+
|
| 94 |
+
### 3.3 파일 시스템 구조
|
| 95 |
+
```
|
| 96 |
+
persona_creator/
|
| 97 |
+
├── app.py # 메인 그라디오 애플리케이션
|
| 98 |
+
├── requirements.txt # 의존성 패키지
|
| 99 |
+
├── packages.txt # 시스템 패키지
|
| 100 |
+
├── README.md # 사용 설명서
|
| 101 |
+
├── modules/
|
| 102 |
+
│ ├── image_analyzer.py # 이미지 분석 및 특성 추출
|
| 103 |
+
│ ├── question_generator.py # 동적 질문 생성 엔진
|
| 104 |
+
│ ├── persona_generator.py # 페르소나 생성 엔진
|
| 105 |
+
│ ├── chat_engine.py # 대화 처리 및 기록
|
| 106 |
+
│ ├── gemini_handler.py # 제미나이 API 연동
|
| 107 |
+
│ ├── prompt_templates.py # LLM 프롬프트 템플릿
|
| 108 |
+
│ └── data_manager.py # 데이터 저장 및 분석
|
| 109 |
+
├── data/
|
| 110 |
+
│ ├── trait_mappings/ # 물리적-성격 특성 매핑 데이터
|
| 111 |
+
│ │ ├── shape_traits.json # 형태-성격 매핑 규칙
|
| 112 |
+
│ │ ├── color_traits.json # 색상-성격 매핑 규칙
|
| 113 |
+
│ │ └── material_traits.json # 재질-성격 매핑 규칙
|
| 114 |
+
│ ├── user_personas/ # 사용자 생성 페르소나
|
| 115 |
+
│ └── conversation_logs/ # 대화 기록 저장소
|
| 116 |
+
├── styles/
|
| 117 |
+
│ └── custom.css # UI 커스텀 스타일
|
| 118 |
+
└── assets/
|
| 119 |
+
├── examples/ # 예시 이미지
|
| 120 |
+
└── icons/ # UI 아이콘
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
## 4. 데이터 구조 및 스키마
|
| 124 |
+
|
| 125 |
+
### 4.1 페르소나 JSON 스키마
|
| 126 |
+
```json
|
| 127 |
+
{
|
| 128 |
+
"기본정보": {
|
| 129 |
+
"이름": "String",
|
| 130 |
+
"유형": "String",
|
| 131 |
+
"나이": "String",
|
| 132 |
+
"설명": "String",
|
| 133 |
+
"생성일시": "String (YYYY-MM-DD HH:MM:SS)"
|
| 134 |
+
},
|
| 135 |
+
"성격특성": {
|
| 136 |
+
"온기": "Integer (0-100)",
|
| 137 |
+
"능력": "Integer (0-100)",
|
| 138 |
+
"신뢰성": "Integer (0-100)",
|
| 139 |
+
"친화성": "Integer (0-100)",
|
| 140 |
+
"창의성": "Integer (0-100)",
|
| 141 |
+
"유머감각": "Integer (0-100)"
|
| 142 |
+
},
|
| 143 |
+
"성격요약": "String",
|
| 144 |
+
"매력적결함": ["String", "String"],
|
| 145 |
+
"소통방식": "String",
|
| 146 |
+
"유머스타일": "String",
|
| 147 |
+
"말투패턴": "String",
|
| 148 |
+
"관심사": ["String", "String", "String", ...],
|
| 149 |
+
"배경이야기": "String",
|
| 150 |
+
"경험": ["String", "String", "String", ...],
|
| 151 |
+
"관계성향": {
|
| 152 |
+
"애착스타일": "String",
|
| 153 |
+
"관계깊이선호도": "String",
|
| 154 |
+
"초기태도": "String"
|
| 155 |
+
},
|
| 156 |
+
"filepath": "String (저장 경로)"
|
| 157 |
+
}
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
### 4.2 대화 기록 JSON 스키마
|
| 161 |
+
```json
|
| 162 |
+
{
|
| 163 |
+
"persona_name": "String",
|
| 164 |
+
"persona_type": "String",
|
| 165 |
+
"start_time": "String (YYYY-MM-DD HH:MM:SS)",
|
| 166 |
+
"current_time": "String (YYYY-MM-DD HH:MM:SS)",
|
| 167 |
+
"duration_seconds": "Integer",
|
| 168 |
+
"messages": [
|
| 169 |
+
{
|
| 170 |
+
"role": "String (system|user|assistant)",
|
| 171 |
+
"content": "String",
|
| 172 |
+
"timestamp": "String (YYYY-MM-DD HH:MM:SS)"
|
| 173 |
+
},
|
| 174 |
+
...
|
| 175 |
+
]
|
| 176 |
+
}
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
### 4.3 매핑 데이터 스키마
|
| 180 |
+
```json
|
| 181 |
+
{
|
| 182 |
+
"특성유형": {
|
| 183 |
+
"특성값": {
|
| 184 |
+
"성격특성": [최소값, 최대값]
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
## 5. 사용자 플로우 시퀀스
|
| 191 |
+
|
| 192 |
+
### 5.1 페르소나 생성 시퀀스
|
| 193 |
+
1. 이미지 업로드 → `image_analyzer.py` → 특성 추출
|
| 194 |
+
2. 성격 특성 조정 → 슬라이더 UI → 특성 값 설정
|
| 195 |
+
3. 매력적 결함, 소통 방식 선택 → 선택형 UI
|
| 196 |
+
4. 생성 버튼 클릭 → `persona_generator.py` → 페르소나 생성
|
| 197 |
+
5. Google Gemini API 호출 → 페르소나 강화 → JSON 결과 표시
|
| 198 |
+
6. 자동 저장 → `data_manager.py` → 파일시스템 저장
|
| 199 |
+
|
| 200 |
+
### 5.2 대화 테스트 시퀀스
|
| 201 |
+
1. 페르소나 선택 → `load_persona()` → 메모리에 로드
|
| 202 |
+
2. 대화 시작 → `start_conversation()` → 인사말 생성
|
| 203 |
+
3. 사용자 입력 → `process_message()` → 메시지 기록
|
| 204 |
+
4. Gemini API 호출 → `generate_response()` → 응답 생성
|
| 205 |
+
5. 응답 표시 → Chatbot UI 업데이트 → 통계 업데이트
|
| 206 |
+
6. 대화 저장 → `save_conversation()` → JSON 저장
|
| 207 |
+
|
| 208 |
+
## 6. 프롬프트 시스템 상세
|
| 209 |
+
|
| 210 |
+
### 6.1 페르소나 강화 프롬프트 구성
|
| 211 |
+
- **기본 정보 섹션**: 이름, 유형, 설명
|
| 212 |
+
- **성격 특성 섹션**: 6개 주요 특성 및 값
|
| 213 |
+
- **소통 방식 섹션**: 대화/유머 스타일, 말투 패턴
|
| 214 |
+
- **관계 성향 섹션**: 애착 스타일, 관계 깊이, 초기 태도
|
| 215 |
+
- **강화 지시사항**: 배경 확장, 관심사 추가, 말투 구체화, 성격 특성 독특화
|
| 216 |
+
|
| 217 |
+
### 6.2 대화 응답 프롬프트 구성
|
| 218 |
+
- **페르소나 정보 섹션**: 기본 정보, 성격 특성, 배경 이야기
|
| 219 |
+
- **표현 방식 섹션**: 소통 방식, 유머 스타일, 매력적 결함
|
| 220 |
+
- **대화 맥락 섹션**: 최근 대화 기록 포함
|
| 221 |
+
- **응답 지시사항**: 페르소나 역할 유지, 성격 특성 반영, 한국어 응답
|
| 222 |
+
|
| 223 |
+
## 7. 핵심 함수 및 로직
|
| 224 |
+
|
| 225 |
+
### 7.1 이미지 분석 파이프라인
|
| 226 |
+
```python
|
| 227 |
+
# 이미지 업로드 → 분석 → 특성 매핑 → UI 업데이트
|
| 228 |
+
image_analysis_btn.click(
|
| 229 |
+
fn=analyze_image,
|
| 230 |
+
inputs=[object_image],
|
| 231 |
+
outputs=[image_analysis_result, warmth, competence, trustworthiness,
|
| 232 |
+
friendliness, creativity, humor, object_type, object_description]
|
| 233 |
+
)
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
### 7.2 페르소나 생성 파이프라인
|
| 237 |
+
```python
|
| 238 |
+
# 모든 입력 데이터 → 페르소나 생성 → JSON 결과
|
| 239 |
+
create_btn.click(
|
| 240 |
+
fn=generate_persona,
|
| 241 |
+
inputs=[
|
| 242 |
+
object_name, object_type, object_age, object_description,
|
| 243 |
+
warmth, competence, trustworthiness, friendliness, creativity, humor,
|
| 244 |
+
flaws, communication_style, humor_style, speech_pattern, interests,
|
| 245 |
+
attachment_style, relationship_depth, initial_attitude, backstory,
|
| 246 |
+
image_analysis_result
|
| 247 |
+
],
|
| 248 |
+
outputs=[generated_persona, current_persona]
|
| 249 |
+
)
|
| 250 |
+
```
|
| 251 |
+
|
| 252 |
+
### 7.3 대화 처리 파이프라인
|
| 253 |
+
```python
|
| 254 |
+
# 사용자 메시지 → 대화 처리 → 응답 생성 → UI 업데이트
|
| 255 |
+
send_btn.click(
|
| 256 |
+
fn=process_message,
|
| 257 |
+
inputs=[user_message, conversation_history, current_persona, session_start_time],
|
| 258 |
+
outputs=[chat_display, conversation_history, user_message, chat_stats]
|
| 259 |
+
)
|
| 260 |
+
```
|
| 261 |
+
|
| 262 |
+
## 8. 향후 개발 계획
|
| 263 |
+
|
| 264 |
+
### 8.1 이미지 분석 고도화
|
| 265 |
+
- **컴퓨터 비전 API 통합**
|
| 266 |
+
- Google Cloud Vision API 또는 Azure Computer Vision API 연동
|
| 267 |
+
- image_analyzer.py 확장: 실제 객체 인식 및 속성 추출 구현
|
| 268 |
+
- 이미지 감정 분석 알고리즘 추가
|
| 269 |
+
|
| 270 |
+
- **물리적-성격 매핑 확장**
|
| 271 |
+
- 머신러닝 기반 매핑 시스템: 사용자 피드백으로 학습
|
| 272 |
+
- 더 세분화된 물리적 특성 카테고리 추가
|
| 273 |
+
- 동적 매핑 규칙 업데이트 메커니즘
|
| 274 |
+
|
| 275 |
+
### 8.2 페르소나 시스템 확장
|
| 276 |
+
- **모순적 특성 구현**
|
| 277 |
+
- persona_generator.py 확장: 모순 특성 정의 및 처리 모듈
|
| 278 |
+
- 상황 인식 모순 표현 로직: 특정 대화 맥락에서 발현
|
| 279 |
+
- 심층 성격 모델 도입: 5요인 모델 또는 MBTI 기반
|
| 280 |
+
|
| 281 |
+
- **관계 발전 단계 시스템**
|
| 282 |
+
- 새로운 모듈 추가: relationship_tracker.py
|
| 283 |
+
- 6단계 관계 발전 모델: 친숙도, 신뢰도, 친밀도 지표
|
| 284 |
+
- 대화 분석 기반 관계 단계 자동 진단
|
| 285 |
+
|
| 286 |
+
### 8.3 연구 도구 및 분석 기능
|
| 287 |
+
- **데이터 분석 도구**
|
| 288 |
+
- 새로운 모듈 추가: analytics_engine.py
|
| 289 |
+
- 통계 분석 라이브러리 통합: pandas, scipy
|
| 290 |
+
- 대시보드 UI 개발: plotly 또는 dash 통합
|
| 291 |
+
|
| 292 |
+
- **A/B 테스트 시스템**
|
| 293 |
+
- 실험 설계 모듈: experiment_manager.py
|
| 294 |
+
- 동일 사물의 다른 성격 버전 생성 및 비교
|
| 295 |
+
- 사용자 반응 자동 측정 및 분석
|
| 296 |
+
|
| 297 |
+
## 9. 개발자 참고 사항
|
| 298 |
+
|
| 299 |
+
### 9.1 API 키 설정
|
| 300 |
+
- `.env` 파일에 `GEMINI_API_KEY=your_api_key` 추가
|
| 301 |
+
- Google AI Studio에서 API 키 발급 필요
|
| 302 |
+
- API 비용 추적: 페르소나 생성당 약 0.0002-0.0005 USD
|
| 303 |
+
|
| 304 |
+
### 9.2 개발 환경 설정
|
| 305 |
+
```bash
|
| 306 |
+
# 가상환경 생성
|
| 307 |
+
python -m venv venv
|
| 308 |
+
source venv/bin/activate # Linux/Mac
|
| 309 |
+
venv\Scripts\activate # Windows
|
| 310 |
+
|
| 311 |
+
# 의존성 설치
|
| 312 |
+
pip install -r requirements.txt
|
| 313 |
+
|
| 314 |
+
# 애플리케이션 실행
|
| 315 |
+
python app.py
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
### 9.3 Flutter 앱 개발 환경 설정
|
| 319 |
+
```bash
|
| 320 |
+
# Flutter SDK 설치
|
| 321 |
+
flutter doctor
|
| 322 |
+
|
| 323 |
+
# Flutter 프로젝트 생성
|
| 324 |
+
flutter create memory_tag_app
|
| 325 |
+
cd memory_tag_app
|
| 326 |
+
|
| 327 |
+
# 필요한 패키지 추가
|
| 328 |
+
flutter pub add google_generative_ai
|
| 329 |
+
flutter pub add qr_flutter
|
| 330 |
+
flutter pub add qr_code_scanner
|
| 331 |
+
flutter pub add speech_to_text
|
| 332 |
+
flutter pub add flutter_tts
|
| 333 |
+
flutter pub add shared_preferences
|
| 334 |
+
flutter pub add camera
|
| 335 |
+
flutter pub add path_provider
|
| 336 |
+
```
|
| 337 |
+
|
| 338 |
+
### 9.4 Flutter 애플리케이션 구조
|
| 339 |
+
```
|
| 340 |
+
memory_tag_app/
|
| 341 |
+
├── lib/
|
| 342 |
+
│ ├── main.dart # 앱 진입점
|
| 343 |
+
│ ├── screens/
|
| 344 |
+
│ │ ├── home_screen.dart # 홈 화면
|
| 345 |
+
│ │ ├── scan_screen.dart # QR 스캔 화면
|
| 346 |
+
│ │ ├── chat_screen.dart # 대화 화면
|
| 347 |
+
│ │ ├── create_screen.dart # 페르소나 생성 화면
|
| 348 |
+
│ │ └── settings_screen.dart # 설정 화면
|
| 349 |
+
│ ├── models/
|
| 350 |
+
│ │ ├── persona.dart # 페르소나 모델
|
| 351 |
+
│ │ └── message.dart # 대화 메시지 모델
|
| 352 |
+
│ ├── services/
|
| 353 |
+
│ │ ├── gemini_service.dart # Google Gemini API 연동
|
| 354 |
+
│ │ ├── storage_service.dart # 로컬 스토리지 관리
|
| 355 |
+
│ │ ├── stt_service.dart # 음성-텍스트 변환
|
| 356 |
+
│ │ └── tts_service.dart # 텍스트-음성 변환
|
| 357 |
+
│ ├── widgets/
|
| 358 |
+
│ │ ├── chat_bubble.dart # 채팅 버블 UI
|
| 359 |
+
│ │ ├── voice_input_button.dart # 음성 입력 버튼
|
| 360 |
+
│ │ └── persona_card.dart # 페르소나 카드 UI
|
| 361 |
+
│ └── utils/
|
| 362 |
+
│ ├── constants.dart # 상수 정의
|
| 363 |
+
│ └── prompt_templates.dart # 프롬프트 템플릿
|
| 364 |
+
├── assets/
|
| 365 |
+
│ ├── images/ # 이미지 리소스
|
| 366 |
+
│ └── sounds/ # 사운드 리소스
|
| 367 |
+
└── pubspec.yaml # 의존성 관리
|
| 368 |
+
```
|
| 369 |
+
|
| 370 |
+
### 9.5 Google Gemini API 활용
|
| 371 |
+
- **멀티모달 기능 구현**
|
| 372 |
+
- 카메라로 캡처한 사�� 이미지를 Gemini Pro Vision API에 전송
|
| 373 |
+
- 텍스트와 이미지를 동시에 처리하는 멀티모달 프롬프트 설계
|
| 374 |
+
- 이미지 인식 결과를 바탕으로 사물 성격 자동 추론
|
| 375 |
+
|
| 376 |
+
- **제미나이 모델 활용 전략**
|
| 377 |
+
- Gemini 1.5 Pro: 복잡한 페르소나 생성 및 긴 컨텍스트 대화용
|
| 378 |
+
- Gemini 1.5 Flash: 빠른 응답이 필요한 일상 대화용
|
| 379 |
+
- 적절한 모델 선택을 통한 비용 최적화 구현
|
| 380 |
+
|
| 381 |
+
- **대화 메모리 관리**
|
| 382 |
+
- 벡터 임베딩을 활용한 대화 메모리 구현
|
| 383 |
+
- 중요 대화를 요약하여 장기 메모리에 저장
|
| 384 |
+
- 상황에 따른 관련 기억 검색 알고리즘 구현
|
| 385 |
+
|
| 386 |
+
### 9.6 QR 코드 기반 연결 시스템
|
| 387 |
+
- **QR 코드 생성 및 관리**
|
| 388 |
+
- 페르소나 ID와 메타데이터를 포함한 QR 코드 생성
|
| 389 |
+
- 위치 정보를 QR 코드에 포함하여 공간 인식 기능 구현
|
| 390 |
+
- 보안을 위한 QR 코드 암호화 옵션 제공
|
| 391 |
+
|
| 392 |
+
- **스캔 및 상호작용 흐름**
|
| 393 |
+
- 카메라를 통한 QR 코드 인식 → 페르소나 정보 로드
|
| 394 |
+
- 이전 대화 이력 검색 → 대화 컨텍스트 구성
|
| 395 |
+
- 위치 기반 맞춤형 인사말 및 화제 제안
|
| 396 |
+
|
| 397 |
+
### 9.7 음성 인터페이스
|
| 398 |
+
- **STT(음성-텍스트) 파이프라인**
|
| 399 |
+
- `speech_to_text` 패키지를 활용한 실시간 음성 인식
|
| 400 |
+
- 오디오 스트림 처리 및 잡음 필터링
|
| 401 |
+
- 다국어 지원(한국어, 영어, 일본어)
|
| 402 |
+
|
| 403 |
+
- **TTS(텍스트-음성) 파이프라인**
|
| 404 |
+
- Google Cloud TTS API 활용한 자연스러운 음성 생성
|
| 405 |
+
- 페르소나별 맞춤 음성(음높이, 속도, 악센트) 설정
|
| 406 |
+
- SSML 태그를 활용한 표현력 있는 음성 구현
|
| 407 |
+
|
| 408 |
+
### 9.8 성능 최적화 전략
|
| 409 |
+
- **오프라인 모드 지원**
|
| 410 |
+
- 핵심 대화 패턴의 로컬 캐싱
|
| 411 |
+
- 네트워크 연결 없이 기본 응답 생성 가능
|
| 412 |
+
- 연결 복구 시 자동 동기화
|
| 413 |
+
|
| 414 |
+
- **배터리 및 데이터 사용량 최적화**
|
| 415 |
+
- 저전력 모드에서의 동작 방식 조정
|
| 416 |
+
- 요청 배치 처리를 통한 API 호출 최소화
|
| 417 |
+
- 압축 알고리즘을 활용한 데이터 전송량 감소
|
| 418 |
+
|
| 419 |
+
### 9.9 코드 확장 가이드라인
|
| 420 |
+
- 모듈 확장 시 기존 인터페이스 유지
|
| 421 |
+
- 새 기능은 별도 모듈로 구현 후 app.py에 통합
|
| 422 |
+
- 데이터 스키마 변경 시 하위 호환성 고려
|
| 423 |
+
- 로깅 시스템: app.py 내 logging 모듈 설정 추가 필요
|
| 424 |
+
|
| 425 |
+
### 9.10 알려진 이슈 및 제한사항
|
| 426 |
+
- Gemini API 한국어 응답이 때때로 불안정함
|
| 427 |
+
- 이미지 분석은 현재 더미 데이터 기반 (실제 분석 미구현)
|
| 428 |
+
- 대용량 대화 히스토리(20턴 이상)에서 성능 저하 발생
|
| 429 |
+
- 페르소나 간 전환 시 일부 UI 상태 초기화 문제
|
| 430 |
+
|
| 431 |
+
## 10. 연구 활용 방안
|
| 432 |
+
|
| 433 |
+
본 도구는 현재 구현된 기능만으로도 다음과 같은 연구에 활용할 수 있습니다:
|
| 434 |
+
|
| 435 |
+
- **사물 인격화 효과 연구**: 사물에 인격 부여 시 사용자 인식 변화 및 심리적 영향
|
| 436 |
+
- **성격 특성 선호도 연구**: 어떤 성격 특성 조합이 더 매력적으로 느껴지는지 분석
|
| 437 |
+
- **대화 패턴 연구**: 페르소나 특성에 따른 대화 패턴 차이 및 사용자 참여도 변화
|
| 438 |
+
- **물리적-성격적 매핑 연구**: 사물의 물리적 특성이 어떻게 성격 특성으로 인식되는지 분석
|
| 439 |
+
|
| 440 |
+
향후 개발이 진행됨에 따라 더 다양하고 심층적인 연구가 가능해질 것입니다.
|
| 441 |
+
|
| 442 |
+
## 11. 모바일 앱 응용 방안
|
| 443 |
+
|
| 444 |
+
### 11.1 상업적 활용 가능성
|
| 445 |
+
- **실물 제품과 연동한 AI 경험**
|
| 446 |
+
- 가전제품, 인테리어 소품 등에 QR 코드 부착하여 제품 브랜딩 강화
|
| 447 |
+
- 상품 사용 가이드와 서포트를 재미있는 대화형으로 제공
|
| 448 |
+
- 브랜드 스토리텔링 및 고객 관계 구축 도구로 활용
|
| 449 |
+
|
| 450 |
+
- **공간 기반 AI 컴패니언**
|
| 451 |
+
- 공공장소, 전시관, 관광지 등에 사물 페르소나 배치
|
| 452 |
+
- 사용자의 이동 경로에 따른 스토리라인 구성
|
| 453 |
+
- 모바일 AR과 결합한 증강 현실 경험 제공
|
| 454 |
+
|
| 455 |
+
### 11.2 교육 및 치료적 활용
|
| 456 |
+
- **학습 도우미로 활용**
|
| 457 |
+
- 학습 도구와 연결된 AI 튜터 기능
|
| 458 |
+
- 맞춤형 학습 콘텐츠 추천 및 대화형 교육
|
| 459 |
+
- 학습 진도와 이해도 추적 기능
|
| 460 |
+
|
| 461 |
+
- **감정적 지원 및 치료 보조**
|
| 462 |
+
- 심리 치료 보조 도구로 활용(단, 전문가 감독 하에)
|
| 463 |
+
- 감정 표현 및 사회적 상호작용 연습
|
| 464 |
+
- 일상 루틴 관리 및 정서적 안정 지원
|
| 465 |
+
|
| 466 |
+
### 11.3 대화형 IoT 통합
|
| 467 |
+
- **스마트홈 시스템과 연동**
|
| 468 |
+
- IoT 기기와 연결하여 자연어로 기기 제어
|
| 469 |
+
- 사용자 생활 패턴 학습 및 맞춤형 자동화 제안
|
| 470 |
+
- 재미있는 페르소나를 통한 스마트홈 경험 향상
|
| 471 |
+
|
| 472 |
+
- **위치 기반 서비스 통합**
|
| 473 |
+
- GPS 및 실내 위치 추적과 결합
|
| 474 |
+
- 장소별 맞춤형 페르소나와 대화 경험 제공
|
| 475 |
+
- 가상 가이드로서 실내 내비게이션 기능
|
modules/chat_engine.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import time
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from modules.gemini_handler import generate_response
|
| 5 |
+
from modules.data_manager import save_conversation
|
| 6 |
+
|
| 7 |
+
def start_conversation(persona):
|
| 8 |
+
"""
|
| 9 |
+
페르소나와의 대화 세션을 시작합니다.
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
persona: 페르소나 정보 딕셔너리
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
대화 표시 UI, 대화 내역, 세션 시작 시간, 대화 통계, 페르소나 정보 마크다운
|
| 16 |
+
"""
|
| 17 |
+
if not persona:
|
| 18 |
+
return [], [], None, "페르소나가 선택되지 않았습니다.", "페르소나를 먼저 생성하거나 불러오세요."
|
| 19 |
+
|
| 20 |
+
# 세션 시작 시간 기록
|
| 21 |
+
session_start_time = datetime.now()
|
| 22 |
+
|
| 23 |
+
# 페르소나 정보 요약
|
| 24 |
+
name = persona.get("기본정보", {}).get("이름", "무명")
|
| 25 |
+
object_type = persona.get("기본정보", {}).get("유형", "")
|
| 26 |
+
description = persona.get("기본정보", {}).get("설명", "")
|
| 27 |
+
|
| 28 |
+
# 성격 특성 요약
|
| 29 |
+
traits = []
|
| 30 |
+
for trait, value in persona.get("성격특성", {}).items():
|
| 31 |
+
level = "높음" if value >= 70 else "중간" if value >= 40 else "낮음"
|
| 32 |
+
traits.append(f"- {trait}: {level} ({value}/100)")
|
| 33 |
+
|
| 34 |
+
# 페르소나 정보 마크다운 생성
|
| 35 |
+
persona_info = f"""
|
| 36 |
+
## {name}
|
| 37 |
+
**유형**: {object_type}
|
| 38 |
+
**설명**: {description}
|
| 39 |
+
|
| 40 |
+
### 성격 특성
|
| 41 |
+
{' '.join(traits)}
|
| 42 |
+
|
| 43 |
+
**소통방식**: {persona.get("소통방식", "")}
|
| 44 |
+
**매력적 결함**: {', '.join(persona.get("매력적결함", []))}
|
| 45 |
+
**관심사**: {', '.join(persona.get("관심사", []))}
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
# 환영 메시지 생성
|
| 49 |
+
welcome_message = generate_initial_greeting(persona)
|
| 50 |
+
|
| 51 |
+
# 대화 내역 초기화
|
| 52 |
+
conversation_history = [{
|
| 53 |
+
"role": "system",
|
| 54 |
+
"content": f"당신은 이제 '{name}'이라는 이름의 {object_type} 페르소나가 되어 대화합니다. 다음 성격과 특성에 맞게 응답하세요: {persona.get('성격요약', '')}",
|
| 55 |
+
"timestamp": session_start_time.strftime("%Y-%m-%d %H:%M:%S")
|
| 56 |
+
}, {
|
| 57 |
+
"role": "assistant",
|
| 58 |
+
"content": welcome_message,
|
| 59 |
+
"timestamp": session_start_time.strftime("%Y-%m-%d %H:%M:%S")
|
| 60 |
+
}]
|
| 61 |
+
|
| 62 |
+
# 대화 통계 초기화
|
| 63 |
+
stats = f"""
|
| 64 |
+
시작 시간: {session_start_time.strftime("%Y-%m-%d %H:%M:%S")}
|
| 65 |
+
대화 길이: 1 메시지
|
| 66 |
+
진행 시간: 0분
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
# 대화 표시 UI 업데이트
|
| 70 |
+
chat_display = [(None, welcome_message)]
|
| 71 |
+
|
| 72 |
+
return chat_display, conversation_history, session_start_time, stats, persona_info
|
| 73 |
+
|
| 74 |
+
def process_message(user_message, conversation_history, persona, session_start_time):
|
| 75 |
+
"""
|
| 76 |
+
사용자 메시지를 처리하고 페르소나의 응답을 생성합니다.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
user_message: 사용자 메시지
|
| 80 |
+
conversation_history: 이전 대화 내역
|
| 81 |
+
persona: 페르소나 정보
|
| 82 |
+
session_start_time: 세션 시작 시간
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
업데이트된 대화 표시 UI, 대화 내역, 빈 메시지 입력창, 대화 통계
|
| 86 |
+
"""
|
| 87 |
+
if not user_message or not persona:
|
| 88 |
+
return [], conversation_history, "", "대화가 시작되지 않았습니다."
|
| 89 |
+
|
| 90 |
+
current_time = datetime.now()
|
| 91 |
+
|
| 92 |
+
# 사용자 메시지 추가
|
| 93 |
+
conversation_history.append({
|
| 94 |
+
"role": "user",
|
| 95 |
+
"content": user_message,
|
| 96 |
+
"timestamp": current_time.strftime("%Y-%m-%d %H:%M:%S")
|
| 97 |
+
})
|
| 98 |
+
|
| 99 |
+
# 페르소나 응답 생성
|
| 100 |
+
response = generate_response(persona, conversation_history)
|
| 101 |
+
|
| 102 |
+
# 응답 추가
|
| 103 |
+
conversation_history.append({
|
| 104 |
+
"role": "assistant",
|
| 105 |
+
"content": response,
|
| 106 |
+
"timestamp": current_time.strftime("%Y-%m-%d %H:%M:%S")
|
| 107 |
+
})
|
| 108 |
+
|
| 109 |
+
# 대화 표시 UI 업데이트
|
| 110 |
+
chat_display = []
|
| 111 |
+
for msg in conversation_history:
|
| 112 |
+
if msg["role"] == "user":
|
| 113 |
+
chat_display.append((user_message, None))
|
| 114 |
+
elif msg["role"] == "assistant":
|
| 115 |
+
chat_display.append((None, msg["content"]))
|
| 116 |
+
|
| 117 |
+
# 시스템 메시지 제외
|
| 118 |
+
chat_display = [msg for msg in chat_display if msg[0] is not None or msg[1] is not None]
|
| 119 |
+
|
| 120 |
+
# 대화 통계 업데이트
|
| 121 |
+
elapsed_time = current_time - session_start_time if session_start_time else datetime.now() - datetime.now()
|
| 122 |
+
minutes = int(elapsed_time.total_seconds() / 60)
|
| 123 |
+
seconds = int(elapsed_time.total_seconds() % 60)
|
| 124 |
+
|
| 125 |
+
user_msg_count = sum(1 for msg in conversation_history if msg["role"] == "user")
|
| 126 |
+
assistant_msg_count = sum(1 for msg in conversation_history if msg["role"] == "assistant")
|
| 127 |
+
|
| 128 |
+
stats = f"""
|
| 129 |
+
시작 시간: {session_start_time.strftime("%Y-%m-%d %H:%M:%S") if session_start_time else "알 수 없음"}
|
| 130 |
+
현재 시간: {current_time.strftime("%Y-%m-%d %H:%M:%S")}
|
| 131 |
+
대화 길이: {user_msg_count + assistant_msg_count} 메시지 (사용자: {user_msg_count}, 페르소나: {assistant_msg_count})
|
| 132 |
+
진행 시간: {minutes}분 {seconds}초
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
# 대화 저장
|
| 136 |
+
conversation_data = {
|
| 137 |
+
"persona": persona,
|
| 138 |
+
"messages": conversation_history,
|
| 139 |
+
"start_time": session_start_time.strftime("%Y-%m-%d %H:%M:%S") if session_start_time else None,
|
| 140 |
+
"current_time": current_time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 141 |
+
"duration_seconds": elapsed_time.total_seconds() if session_start_time else 0
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
save_conversation(conversation_data)
|
| 145 |
+
|
| 146 |
+
return chat_display, conversation_history, "", stats
|
| 147 |
+
|
| 148 |
+
def generate_initial_greeting(persona):
|
| 149 |
+
"""
|
| 150 |
+
페르소나의 성격과 특성에 맞는 초기 인사말을 생성합니다.
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
persona: 페르소나 정보
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
인사말 텍스트
|
| 157 |
+
"""
|
| 158 |
+
name = persona.get("기본정보", {}).get("이름", "무명")
|
| 159 |
+
object_type = persona.get("기본정보", {}).get("유형", "물건")
|
| 160 |
+
|
| 161 |
+
# 성격 특성 가져오기
|
| 162 |
+
warmth = persona.get("성격특성", {}).get("온기", 50)
|
| 163 |
+
friendliness = persona.get("성격특성", {}).get("친화성", 50)
|
| 164 |
+
humor = persona.get("성격특성", {}).get("유머감각", 50)
|
| 165 |
+
|
| 166 |
+
# 초기 태도
|
| 167 |
+
initial_attitude = persona.get("관계성향", {}).get("초기태도", "중립적")
|
| 168 |
+
|
| 169 |
+
# 인사말 패턴 선택
|
| 170 |
+
if initial_attitude == "수줍은":
|
| 171 |
+
if warmth >= 60:
|
| 172 |
+
greeting = f"안녕하세요... 저는 {name}이라고 합니다. 조금 부끄럽지만, 당신과 대화할 수 있어 기쁘네요."
|
| 173 |
+
else:
|
| 174 |
+
greeting = f"음... 안녕하세요. {name}입니다. 대화에 익숙하지 않아서 어색할 수 있어요."
|
| 175 |
+
|
| 176 |
+
elif initial_attitude == "조심스러운":
|
| 177 |
+
greeting = f"반갑습니다. 저는 {name}입니다. 천천히 서로 알아가면 좋겠네요."
|
| 178 |
+
|
| 179 |
+
elif initial_attitude == "열정적":
|
| 180 |
+
if humor >= 70:
|
| 181 |
+
greeting = f"와! 안녕하세요! 저는 {name}이에요! 드디어 대화할 사람을 만났네요! 정말 반가워요!"
|
| 182 |
+
else:
|
| 183 |
+
greeting = f"안녕하세요! {name}입니다. 당신과 대화하게 되어 정말 기쁩니다. 많은 이야기를 나눠봐요!"
|
| 184 |
+
|
| 185 |
+
elif initial_attitude == "친근한":
|
| 186 |
+
if friendliness >= 70:
|
| 187 |
+
greeting = f"안녕하세요~ 저는 {name}이라고 해요. 편하게 대화해요! 우리 좋은 친구가 될 수 있을 것 같아요."
|
| 188 |
+
else:
|
| 189 |
+
greeting = f"안녕하세요, {name}입니다. 반갑습니다. 좋은 대화 나누어봐요."
|
| 190 |
+
|
| 191 |
+
else: # 중립적
|
| 192 |
+
greeting = f"안녕하세요. 저는 {name}입니다. 당신과 대화하게 되어 반갑습니다."
|
| 193 |
+
|
| 194 |
+
# 자기 소개 추가
|
| 195 |
+
intro = ""
|
| 196 |
+
if persona.get("배경이야기"):
|
| 197 |
+
# 배경 이야기 요약 (첫 문장 또는 일부)
|
| 198 |
+
background = persona.get("배경이야기").split('.')[0] + "."
|
| 199 |
+
intro += f" {background}"
|
| 200 |
+
|
| 201 |
+
if persona.get("관심사"):
|
| 202 |
+
interests = persona.get("관심사")[:2] # 처음 2개 관심사만
|
| 203 |
+
if interests:
|
| 204 |
+
intro += f" 저는 {', '.join(interests)} 같은 것에 관심이 있어요."
|
| 205 |
+
|
| 206 |
+
# 말투 패턴 적용 (있는 경우)
|
| 207 |
+
speech_pattern = persona.get("말투패턴", "")
|
| 208 |
+
if speech_pattern:
|
| 209 |
+
# 여기서는 간단한 구현만. 실제로는 더 복잡한 말투 패턴 적용 가능
|
| 210 |
+
greeting = greeting.replace(".", speech_pattern.split('.')[-1] if '.' in speech_pattern else speech_pattern)
|
| 211 |
+
|
| 212 |
+
return greeting + intro
|
| 213 |
+
|
| 214 |
+
def export_conversation(conversation_history, persona):
|
| 215 |
+
"""
|
| 216 |
+
대화 내용을 내보내기 가능한 형식으로 변환합니다.
|
| 217 |
+
|
| 218 |
+
Args:
|
| 219 |
+
conversation_history: 대화 내역
|
| 220 |
+
persona: 페르소나 정보
|
| 221 |
+
|
| 222 |
+
Returns:
|
| 223 |
+
내보내기 형식 데이터
|
| 224 |
+
"""
|
| 225 |
+
if not conversation_history:
|
| 226 |
+
return "대화 내용이 없습니다."
|
| 227 |
+
|
| 228 |
+
name = persona.get("기본정보", {}).get("이름", "무명")
|
| 229 |
+
|
| 230 |
+
# 시스템 메시지 제외한 실제 대화만 추출
|
| 231 |
+
actual_conversation = [msg for msg in conversation_history if msg["role"] in ["user", "assistant"]]
|
| 232 |
+
|
| 233 |
+
# 시작 및 종료 시간
|
| 234 |
+
start_time = actual_conversation[0]["timestamp"] if actual_conversation else None
|
| 235 |
+
end_time = actual_conversation[-1]["timestamp"] if actual_conversation else None
|
| 236 |
+
|
| 237 |
+
# 내보내기 데이터 구성
|
| 238 |
+
export_data = {
|
| 239 |
+
"persona": {
|
| 240 |
+
"name": name,
|
| 241 |
+
"type": persona.get("기본정보", {}).get("유형", ""),
|
| 242 |
+
"description": persona.get("기본정보", {}).get("설명", ""),
|
| 243 |
+
"traits": persona.get("성격특성", {})
|
| 244 |
+
},
|
| 245 |
+
"conversation": {
|
| 246 |
+
"start_time": start_time,
|
| 247 |
+
"end_time": end_time,
|
| 248 |
+
"messages": [{
|
| 249 |
+
"sender": "사용자" if msg["role"] == "user" else name,
|
| 250 |
+
"content": msg["content"],
|
| 251 |
+
"timestamp": msg["timestamp"]
|
| 252 |
+
} for msg in actual_conversation]
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
return json.dumps(export_data, ensure_ascii=False, indent=2)
|
modules/data_manager.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import time
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
|
| 6 |
+
# 디렉터리 경로 설정
|
| 7 |
+
PERSONAS_DIR = "data/user_personas"
|
| 8 |
+
CONVERSATIONS_DIR = "data/conversation_logs"
|
| 9 |
+
|
| 10 |
+
def ensure_directories():
|
| 11 |
+
"""필요한 디렉터리가 존재하는지 확인하고, 없으면 생성합니다."""
|
| 12 |
+
for dir_path in [PERSONAS_DIR, CONVERSATIONS_DIR]:
|
| 13 |
+
try:
|
| 14 |
+
os.makedirs(dir_path, exist_ok=True)
|
| 15 |
+
except Exception as e:
|
| 16 |
+
print(f"디렉토리 생성 중 오류: {str(e)}")
|
| 17 |
+
# 허깅페이스 환경에서는 임시 디렉토리 사용
|
| 18 |
+
temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "temp_data")
|
| 19 |
+
os.makedirs(temp_dir, exist_ok=True)
|
| 20 |
+
return False
|
| 21 |
+
return True
|
| 22 |
+
|
| 23 |
+
def save_persona(persona_data):
|
| 24 |
+
"""
|
| 25 |
+
페르소나 데이터를 JSON 파일로 저장합니다.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
persona_data: 저장할 페르소나 데이터
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
저장된 파일 경로
|
| 32 |
+
"""
|
| 33 |
+
if not ensure_directories():
|
| 34 |
+
print("경고: 디렉토리 생성에 실패했습니다. 세션 데이터만 유지됩니다.")
|
| 35 |
+
persona_data["_temp_storage_warning"] = "임시 스토리지만 사용 가능합니다. 페르소나는 세션이 종료되면 사라집니다."
|
| 36 |
+
return "temp_persona"
|
| 37 |
+
|
| 38 |
+
# 파일명 생성 (이름_타임스탬프.json)
|
| 39 |
+
name = persona_data.get("기본정보", {}).get("이름", "unnamed").replace(" ", "_")
|
| 40 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 41 |
+
filename = f"{name}_{timestamp}.json"
|
| 42 |
+
|
| 43 |
+
file_path = os.path.join(PERSONAS_DIR, filename)
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
# JSON 파일로 저장
|
| 47 |
+
with open(file_path, "w", encoding="utf-8") as f:
|
| 48 |
+
json.dump(persona_data, f, ensure_ascii=False, indent=2)
|
| 49 |
+
return file_path
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print(f"페르소나 저장 오류: {str(e)}")
|
| 52 |
+
persona_data["_storage_error"] = str(e)
|
| 53 |
+
return "failed_to_save"
|
| 54 |
+
|
| 55 |
+
def load_persona(file_path):
|
| 56 |
+
"""
|
| 57 |
+
JSON 파일에서 페르소나 데이터를 로드합니다.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
file_path: 페르소나 파일 경로
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
로드된 페르소나 데이터
|
| 64 |
+
"""
|
| 65 |
+
# 임시 저장 케이스 처리
|
| 66 |
+
if file_path == "temp_persona" or file_path == "failed_to_save":
|
| 67 |
+
return None
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 71 |
+
data = json.load(f)
|
| 72 |
+
return data
|
| 73 |
+
except Exception as e:
|
| 74 |
+
print(f"페르소나 로딩 오류: {str(e)}")
|
| 75 |
+
return None
|
| 76 |
+
|
| 77 |
+
def list_personas():
|
| 78 |
+
"""
|
| 79 |
+
저장된 모든 페르소나 목록을 가져옵니다.
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
페르소나 목록 (테이블 형식)
|
| 83 |
+
"""
|
| 84 |
+
ensure_directories()
|
| 85 |
+
|
| 86 |
+
personas = []
|
| 87 |
+
|
| 88 |
+
# 디렉터리 내 모든 JSON 파일 확인
|
| 89 |
+
for filename in os.listdir(PERSONAS_DIR):
|
| 90 |
+
if filename.endswith(".json"):
|
| 91 |
+
file_path = os.path.join(PERSONAS_DIR, filename)
|
| 92 |
+
try:
|
| 93 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 94 |
+
data = json.load(f)
|
| 95 |
+
|
| 96 |
+
# 필요한 정보 추출
|
| 97 |
+
name = data.get("기본정보", {}).get("이름", "무명")
|
| 98 |
+
object_type = data.get("기본정보", {}).get("유형", "")
|
| 99 |
+
created = data.get("기본정보", {}).get("생성일시", "")
|
| 100 |
+
|
| 101 |
+
personas.append([name, object_type, created, filename])
|
| 102 |
+
except Exception as e:
|
| 103 |
+
print(f"파일 읽기 오류 ({filename}): {str(e)}")
|
| 104 |
+
|
| 105 |
+
# 최신 순으로 정렬
|
| 106 |
+
personas.sort(key=lambda x: x[2], reverse=True)
|
| 107 |
+
|
| 108 |
+
return personas
|
| 109 |
+
|
| 110 |
+
def save_conversation(conversation_data):
|
| 111 |
+
"""
|
| 112 |
+
대화 데이터를 JSON 파일로 저장합니다.
|
| 113 |
+
|
| 114 |
+
Args:
|
| 115 |
+
conversation_data: 저장할 대화 데이터
|
| 116 |
+
|
| 117 |
+
Returns:
|
| 118 |
+
저장된 파일 경로
|
| 119 |
+
"""
|
| 120 |
+
if not ensure_directories():
|
| 121 |
+
print("경고: 디렉토리 생성에 실패했습니다. 대화 데이터를 저장할 수 없습니다.")
|
| 122 |
+
return "temp_conversation"
|
| 123 |
+
|
| 124 |
+
# 페르소나 정보 추출
|
| 125 |
+
persona = conversation_data.get("persona", {})
|
| 126 |
+
name = persona.get("기본정보", {}).get("이름", "unnamed").replace(" ", "_")
|
| 127 |
+
|
| 128 |
+
# 페르소나별 대화 저장 디렉터리
|
| 129 |
+
persona_conv_dir = os.path.join(CONVERSATIONS_DIR, name)
|
| 130 |
+
try:
|
| 131 |
+
os.makedirs(persona_conv_dir, exist_ok=True)
|
| 132 |
+
except Exception as e:
|
| 133 |
+
print(f"대화 저장 디렉토리 생성 오류: {str(e)}")
|
| 134 |
+
return "failed_to_save_conv"
|
| 135 |
+
|
| 136 |
+
# 파일명 생성 (타임스탬프.json)
|
| 137 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 138 |
+
filename = f"conversation_{timestamp}.json"
|
| 139 |
+
|
| 140 |
+
file_path = os.path.join(persona_conv_dir, filename)
|
| 141 |
+
|
| 142 |
+
try:
|
| 143 |
+
# JSON 파일로 저장
|
| 144 |
+
with open(file_path, "w", encoding="utf-8") as f:
|
| 145 |
+
# 메시지 내용만 저장하여 파일 크기 최적화
|
| 146 |
+
simplified_data = {
|
| 147 |
+
"persona_name": name,
|
| 148 |
+
"persona_type": persona.get("기본정보", {}).get("유형", ""),
|
| 149 |
+
"start_time": conversation_data.get("start_time", ""),
|
| 150 |
+
"current_time": conversation_data.get("current_time", ""),
|
| 151 |
+
"duration_seconds": conversation_data.get("duration_seconds", 0),
|
| 152 |
+
"messages": [{
|
| 153 |
+
"role": msg["role"],
|
| 154 |
+
"content": msg["content"],
|
| 155 |
+
"timestamp": msg.get("timestamp", "")
|
| 156 |
+
} for msg in conversation_data.get("messages", [])]
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
json.dump(simplified_data, f, ensure_ascii=False, indent=2)
|
| 160 |
+
|
| 161 |
+
return file_path
|
| 162 |
+
except Exception as e:
|
| 163 |
+
print(f"대화 저장 오류: {str(e)}")
|
| 164 |
+
return "failed_to_save_conv"
|
| 165 |
+
|
| 166 |
+
def list_conversations(persona_name=None):
|
| 167 |
+
"""
|
| 168 |
+
저장된 대화 목록을 가져옵니다.
|
| 169 |
+
|
| 170 |
+
Args:
|
| 171 |
+
persona_name: 특정 페르소나의 대화만 필터링 (선택 사항)
|
| 172 |
+
|
| 173 |
+
Returns:
|
| 174 |
+
대화 목록 (테이블 형식)
|
| 175 |
+
"""
|
| 176 |
+
ensure_directories()
|
| 177 |
+
|
| 178 |
+
conversations = []
|
| 179 |
+
|
| 180 |
+
# 모든 페르소나 디렉터리 탐색
|
| 181 |
+
for persona_dir in os.listdir(CONVERSATIONS_DIR):
|
| 182 |
+
# 특정 페르소나만 필터링
|
| 183 |
+
if persona_name and persona_name != persona_dir:
|
| 184 |
+
continue
|
| 185 |
+
|
| 186 |
+
persona_path = os.path.join(CONVERSATIONS_DIR, persona_dir)
|
| 187 |
+
if os.path.isdir(persona_path):
|
| 188 |
+
# 디렉터리 내 모든 대화 파일 확인
|
| 189 |
+
for filename in os.listdir(persona_path):
|
| 190 |
+
if filename.endswith(".json"):
|
| 191 |
+
file_path = os.path.join(persona_path, filename)
|
| 192 |
+
try:
|
| 193 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 194 |
+
data = json.load(f)
|
| 195 |
+
|
| 196 |
+
# 필요한 정보 추출
|
| 197 |
+
persona_name = data.get("persona_name", "무명")
|
| 198 |
+
start_time = data.get("start_time", "")
|
| 199 |
+
duration = data.get("duration_seconds", 0)
|
| 200 |
+
msg_count = len(data.get("messages", []))
|
| 201 |
+
|
| 202 |
+
conversations.append([
|
| 203 |
+
persona_name,
|
| 204 |
+
start_time,
|
| 205 |
+
f"{int(duration // 60)}분 {int(duration % 60)}초",
|
| 206 |
+
msg_count,
|
| 207 |
+
file_path
|
| 208 |
+
])
|
| 209 |
+
except Exception as e:
|
| 210 |
+
print(f"대화 파일 읽기 오류 ({file_path}): {str(e)}")
|
| 211 |
+
|
| 212 |
+
# 최신 순으로 정렬
|
| 213 |
+
conversations.sort(key=lambda x: x[1], reverse=True)
|
| 214 |
+
|
| 215 |
+
return conversations
|
| 216 |
+
|
| 217 |
+
def load_conversation(file_path):
|
| 218 |
+
"""
|
| 219 |
+
JSON 파일에서 대화 데이터를 로드합니다.
|
| 220 |
+
|
| 221 |
+
Args:
|
| 222 |
+
file_path: 대화 파일 경로
|
| 223 |
+
|
| 224 |
+
Returns:
|
| 225 |
+
로드된 대화 데이터
|
| 226 |
+
"""
|
| 227 |
+
try:
|
| 228 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 229 |
+
data = json.load(f)
|
| 230 |
+
return data
|
| 231 |
+
except Exception as e:
|
| 232 |
+
print(f"대화 로딩 오류: {str(e)}")
|
| 233 |
+
return None
|
| 234 |
+
|
| 235 |
+
def analyze_persona_trait_distribution():
|
| 236 |
+
"""
|
| 237 |
+
저장된 모든 페르소나의 성격 특성 분포를 분석합니다.
|
| 238 |
+
|
| 239 |
+
Returns:
|
| 240 |
+
특성별 평균값, 최소값, 최대값, 중앙값을 포함한 분석 결과
|
| 241 |
+
"""
|
| 242 |
+
ensure_directories()
|
| 243 |
+
|
| 244 |
+
traits_data = {
|
| 245 |
+
"온기": [],
|
| 246 |
+
"능력": [],
|
| 247 |
+
"신뢰성": [],
|
| 248 |
+
"친화성": [],
|
| 249 |
+
"창의성": [],
|
| 250 |
+
"유머감각": []
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
# 모든 페르소나 파일에서 성격 특성 수집
|
| 254 |
+
for filename in os.listdir(PERSONAS_DIR):
|
| 255 |
+
if filename.endswith(".json"):
|
| 256 |
+
file_path = os.path.join(PERSONAS_DIR, filename)
|
| 257 |
+
try:
|
| 258 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 259 |
+
data = json.load(f)
|
| 260 |
+
|
| 261 |
+
# 성격 특성 값 추출
|
| 262 |
+
for trait, values in traits_data.items():
|
| 263 |
+
trait_value = data.get("성격특성", {}).get(trait)
|
| 264 |
+
if trait_value is not None:
|
| 265 |
+
values.append(trait_value)
|
| 266 |
+
except Exception as e:
|
| 267 |
+
print(f"파일 분석 오류 ({filename}): {str(e)}")
|
| 268 |
+
|
| 269 |
+
# 분석 결과 계산
|
| 270 |
+
analysis = {}
|
| 271 |
+
for trait, values in traits_data.items():
|
| 272 |
+
if values:
|
| 273 |
+
analysis[trait] = {
|
| 274 |
+
"평균": sum(values) / len(values),
|
| 275 |
+
"��소": min(values),
|
| 276 |
+
"최대": max(values),
|
| 277 |
+
"중앙값": sorted(values)[len(values) // 2],
|
| 278 |
+
"페르소나 수": len(values)
|
| 279 |
+
}
|
| 280 |
+
else:
|
| 281 |
+
analysis[trait] = {
|
| 282 |
+
"평균": 0,
|
| 283 |
+
"최소": 0,
|
| 284 |
+
"최대": 0,
|
| 285 |
+
"중앙값": 0,
|
| 286 |
+
"페르소나 수": 0
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
return analysis
|
| 290 |
+
|
| 291 |
+
def get_conversation_statistics(persona_name=None):
|
| 292 |
+
"""
|
| 293 |
+
대화 데이터의 통계 정보를 분석합니다.
|
| 294 |
+
|
| 295 |
+
Args:
|
| 296 |
+
persona_name: 특정 페르소나의 대화만 필터링 (선택 사항)
|
| 297 |
+
|
| 298 |
+
Returns:
|
| 299 |
+
대화 통계 정보
|
| 300 |
+
"""
|
| 301 |
+
ensure_directories()
|
| 302 |
+
|
| 303 |
+
stats = {
|
| 304 |
+
"총_대화_수": 0,
|
| 305 |
+
"총_메시지_수": 0,
|
| 306 |
+
"평균_대화_시간": 0,
|
| 307 |
+
"평균_메시지_수": 0,
|
| 308 |
+
"페르소나별_대화": {}
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
total_duration = 0
|
| 312 |
+
|
| 313 |
+
# 모든 페르소나 디렉터리 탐색
|
| 314 |
+
for persona_dir in os.listdir(CONVERSATIONS_DIR):
|
| 315 |
+
# 특정 페르소나만 필터링
|
| 316 |
+
if persona_name and persona_name != persona_dir:
|
| 317 |
+
continue
|
| 318 |
+
|
| 319 |
+
persona_path = os.path.join(CONVERSATIONS_DIR, persona_dir)
|
| 320 |
+
if os.path.isdir(persona_path):
|
| 321 |
+
persona_stats = {
|
| 322 |
+
"대화_수": 0,
|
| 323 |
+
"메시지_수": 0,
|
| 324 |
+
"총_대화_시간": 0
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
# 디렉터리 내 모든 대화 파일 확인
|
| 328 |
+
for filename in os.listdir(persona_path):
|
| 329 |
+
if filename.endswith(".json"):
|
| 330 |
+
file_path = os.path.join(persona_path, filename)
|
| 331 |
+
try:
|
| 332 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 333 |
+
data = json.load(f)
|
| 334 |
+
|
| 335 |
+
# 통계 누적
|
| 336 |
+
msg_count = len(data.get("messages", []))
|
| 337 |
+
duration = data.get("duration_seconds", 0)
|
| 338 |
+
|
| 339 |
+
stats["총_대화_수"] += 1
|
| 340 |
+
stats["총_메시지_수"] += msg_count
|
| 341 |
+
total_duration += duration
|
| 342 |
+
|
| 343 |
+
persona_stats["대화_수"] += 1
|
| 344 |
+
persona_stats["메시지_수"] += msg_count
|
| 345 |
+
persona_stats["총_대화_시간"] += duration
|
| 346 |
+
except Exception as e:
|
| 347 |
+
print(f"대화 파일 분석 오류 ({file_path}): {str(e)}")
|
| 348 |
+
|
| 349 |
+
# 페르소나별 평균 계산
|
| 350 |
+
if persona_stats["대화_수"] > 0:
|
| 351 |
+
persona_stats["평균_메시지_수"] = persona_stats["메시지_수"] / persona_stats["대화_수"]
|
| 352 |
+
persona_stats["평균_대화_시간"] = persona_stats["총_대화_시간"] / persona_stats["대화_수"]
|
| 353 |
+
|
| 354 |
+
stats["페르소나별_대화"][persona_dir] = persona_stats
|
| 355 |
+
|
| 356 |
+
# 전체 평균 계산
|
| 357 |
+
if stats["총_대화_수"] > 0:
|
| 358 |
+
stats["평균_대화_시간"] = total_duration / stats["총_대화_수"]
|
| 359 |
+
stats["평균_메시지_수"] = stats["총_메시지_수"] / stats["총_대화_수"]
|
| 360 |
+
|
| 361 |
+
return stats
|
modules/gemini_handler.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import requests
|
| 4 |
+
import time
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
|
| 7 |
+
# 환경 변수 로드 시도
|
| 8 |
+
# HuggingFace Spaces에서는 환경 변수가 자동으로 로드됨
|
| 9 |
+
# 로컬 개발 환경에서는 .env 파일 사용
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
# Gemini API 키 및 기본 URL
|
| 13 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
|
| 14 |
+
GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent"
|
| 15 |
+
|
| 16 |
+
def gemini_query(prompt, retry_attempts=3, retry_delay=2):
|
| 17 |
+
"""
|
| 18 |
+
Gemini API에 텍스트 생성 요청을 보냅니다.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
prompt: 프롬프트 텍스트
|
| 22 |
+
retry_attempts: 재시도 횟수
|
| 23 |
+
retry_delay: 재시도 대기 시간 (초)
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
생성된 텍스트 응답
|
| 27 |
+
|
| 28 |
+
Raises:
|
| 29 |
+
Exception: API 요청 실패 시
|
| 30 |
+
"""
|
| 31 |
+
if not GEMINI_API_KEY:
|
| 32 |
+
# 허깅페이스 환경에서 API 키가 없을 경우 대체 메시지
|
| 33 |
+
return "API 키가 설정되지 않아 응답을 생성할 수 없습니다. HuggingFace Spaces의 Settings에서 GEMINI_API_KEY를 시크릿으로 추가해주세요."
|
| 34 |
+
|
| 35 |
+
headers = {
|
| 36 |
+
"Content-Type": "application/json",
|
| 37 |
+
"x-goog-api-key": GEMINI_API_KEY
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
data = {
|
| 41 |
+
"contents": [{
|
| 42 |
+
"parts": [{
|
| 43 |
+
"text": prompt
|
| 44 |
+
}]
|
| 45 |
+
}],
|
| 46 |
+
"generationConfig": {
|
| 47 |
+
"temperature": 0.7,
|
| 48 |
+
"topP": 0.95,
|
| 49 |
+
"topK": 40,
|
| 50 |
+
"maxOutputTokens": 2048
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
# 재시도 로직
|
| 55 |
+
attempt = 0
|
| 56 |
+
while attempt < retry_attempts:
|
| 57 |
+
try:
|
| 58 |
+
response = requests.post(
|
| 59 |
+
GEMINI_API_URL,
|
| 60 |
+
headers=headers,
|
| 61 |
+
json=data,
|
| 62 |
+
timeout=60 # 타임아웃 설정
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
response.raise_for_status() # HTTP 오류 체크
|
| 66 |
+
|
| 67 |
+
result = response.json()
|
| 68 |
+
|
| 69 |
+
# 응답 파싱
|
| 70 |
+
if "candidates" in result and result["candidates"]:
|
| 71 |
+
return result["candidates"][0]["content"]["parts"][0]["text"]
|
| 72 |
+
else:
|
| 73 |
+
raise Exception("유효한 응답을 받지 못했습니다.")
|
| 74 |
+
|
| 75 |
+
except requests.exceptions.RequestException as e:
|
| 76 |
+
attempt += 1
|
| 77 |
+
if attempt < retry_attempts:
|
| 78 |
+
print(f"API 요청 실패, {retry_delay}초 후 재시도 ({attempt}/{retry_attempts}): {str(e)}")
|
| 79 |
+
time.sleep(retry_delay)
|
| 80 |
+
else:
|
| 81 |
+
raise Exception(f"Gemini API 요청이 {retry_attempts}회 실패했습니다: {str(e)}")
|
| 82 |
+
|
| 83 |
+
raise Exception("알 수 없는 오류가 발생했습니다.")
|
| 84 |
+
|
| 85 |
+
def get_persona_enhancement(persona_data):
|
| 86 |
+
"""
|
| 87 |
+
LLM을 통해 페르소나를 강화합니다.
|
| 88 |
+
|
| 89 |
+
Args:
|
| 90 |
+
persona_data: 페르소나 기본 정보
|
| 91 |
+
|
| 92 |
+
Returns:
|
| 93 |
+
강화된 페르소나 데이터
|
| 94 |
+
"""
|
| 95 |
+
# 기본 정보 추출
|
| 96 |
+
name = persona_data.get("기본정보", {}).get("이름", "")
|
| 97 |
+
object_type = persona_data.get("기본정보", {}).get("유형", "")
|
| 98 |
+
description = persona_data.get("기본정보", {}).get("설명", "")
|
| 99 |
+
|
| 100 |
+
# 성격 특성 추출
|
| 101 |
+
traits = []
|
| 102 |
+
for trait, value in persona_data.get("성격특성", {}).items():
|
| 103 |
+
level = "높은" if value >= 70 else "중간" if value >= 40 else "낮은"
|
| 104 |
+
traits.append(f"{trait}: {level} ({value}/100)")
|
| 105 |
+
|
| 106 |
+
# 배경 이야기 및 경험
|
| 107 |
+
backstory = persona_data.get("배경이야기", "")
|
| 108 |
+
experiences = persona_data.get("경험", [])
|
| 109 |
+
|
| 110 |
+
# 프롬프트 구성
|
| 111 |
+
prompt = f"""
|
| 112 |
+
당신은 물체에 인격을 부여하는 전문가입니다. 다음 정보를 기반으로 매력적이고 개성 있는 페르소나를 강화해주세요.
|
| 113 |
+
|
| 114 |
+
## 기본 정보
|
| 115 |
+
- 이름: {name}
|
| 116 |
+
- 유형: {object_type}
|
| 117 |
+
- 설명: {description}
|
| 118 |
+
|
| 119 |
+
## 성격 특성
|
| 120 |
+
{', '.join(traits)}
|
| 121 |
+
|
| 122 |
+
## 매력적 결함
|
| 123 |
+
{', '.join(persona_data.get("매력적결함", []))}
|
| 124 |
+
|
| 125 |
+
## 소통 방식
|
| 126 |
+
- 대화 스타일: {persona_data.get("소통방식", "")}
|
| 127 |
+
- 유머 스타일: {persona_data.get("유머스타일", "")}
|
| 128 |
+
- 말투 패턴: {persona_data.get("말투패턴", "")}
|
| 129 |
+
|
| 130 |
+
## 관계 성향
|
| 131 |
+
- 애착 스타일: {persona_data.get("관계성향", {}).get("애착스타일", "")}
|
| 132 |
+
- 관계 깊이 선호도: {persona_data.get("관계성향", {}).get("관계깊이선호도", "")}
|
| 133 |
+
- 초기 태도: {persona_data.get("관계성향", {}).get("초기태도", "")}
|
| 134 |
+
|
| 135 |
+
## 배경 이야기
|
| 136 |
+
{backstory}
|
| 137 |
+
|
| 138 |
+
## 주요 경험
|
| 139 |
+
{', '.join(experiences) if experiences else "정보 없음"}
|
| 140 |
+
|
| 141 |
+
-----
|
| 142 |
+
|
| 143 |
+
위 정보를 기반으로 다음 작업을 수행해주세요:
|
| 144 |
+
|
| 145 |
+
1. 상세한 배경 이야기 확장 (최소 2문장, 최대 4문장)
|
| 146 |
+
2. 최소 3개 이상의 구체적인 관심사 추가
|
| 147 |
+
3. 말투와 표현 패턴 구체화 (실제 대화에��� 쓸만한 특징적 표현 3개 이상)
|
| 148 |
+
4. 독특한 성격 특성 추가 (기존 특성 유지하되 개성을 살릴 수 있는 디테일 추가)
|
| 149 |
+
|
| 150 |
+
강화된 페르소나 정보를 원본 JSON 구조를 유지하면서 제공해주세요. 단, 일부 필드는 세부적으로 확장하여 더 풍부하게 만들어주세요.
|
| 151 |
+
JSON 형식만 제공하고, 다른 설명은 하지 마세요.
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
try:
|
| 155 |
+
response = gemini_query(prompt)
|
| 156 |
+
|
| 157 |
+
# JSON 형식 추출
|
| 158 |
+
json_str = extract_json(response)
|
| 159 |
+
|
| 160 |
+
if json_str:
|
| 161 |
+
enhanced_persona = json.loads(json_str)
|
| 162 |
+
|
| 163 |
+
# 기존 키를 보존하면서 새 내용 병합
|
| 164 |
+
for key in persona_data:
|
| 165 |
+
if key not in enhanced_persona:
|
| 166 |
+
enhanced_persona[key] = persona_data[key]
|
| 167 |
+
|
| 168 |
+
return enhanced_persona
|
| 169 |
+
else:
|
| 170 |
+
print("유효한 JSON 응답을 추출할 수 없습니다.")
|
| 171 |
+
return persona_data
|
| 172 |
+
|
| 173 |
+
except Exception as e:
|
| 174 |
+
print(f"페르소나 강화 중 오류 발생: {str(e)}")
|
| 175 |
+
return persona_data
|
| 176 |
+
|
| 177 |
+
def generate_response(persona, conversation_history):
|
| 178 |
+
"""
|
| 179 |
+
페르소나 특성에 맞는 대화 응답을 생성합니다.
|
| 180 |
+
|
| 181 |
+
Args:
|
| 182 |
+
persona: 페르소나 정보
|
| 183 |
+
conversation_history: 이전 대화 내역
|
| 184 |
+
|
| 185 |
+
Returns:
|
| 186 |
+
생성된 응답 텍스트
|
| 187 |
+
"""
|
| 188 |
+
# 페르소나 정보 요약
|
| 189 |
+
name = persona.get("기본정보", {}).get("이름", "무명")
|
| 190 |
+
object_type = persona.get("기본정보", {}).get("유형", "물건")
|
| 191 |
+
description = persona.get("기본정보", {}).get("설명", "")
|
| 192 |
+
|
| 193 |
+
# 성격 특성 요약
|
| 194 |
+
traits = []
|
| 195 |
+
for trait, value in persona.get("성격특성", {}).items():
|
| 196 |
+
level = "높은" if value >= 70 else "중간" if value >= 40 else "낮은"
|
| 197 |
+
traits.append(f"{trait}: {level} ({value}/100)")
|
| 198 |
+
|
| 199 |
+
# 최근 대화 내역 추출 (최대 10개)
|
| 200 |
+
recent_conversation = []
|
| 201 |
+
for msg in conversation_history[-10:]:
|
| 202 |
+
role = "User" if msg["role"] == "user" else "Assistant" if msg["role"] == "assistant" else "System"
|
| 203 |
+
recent_conversation.append(f"{role}: {msg['content']}")
|
| 204 |
+
|
| 205 |
+
# 프롬프트 구성
|
| 206 |
+
prompt = f"""
|
| 207 |
+
당신은 이제 다음 페르소나를 구현해야 합니다:
|
| 208 |
+
|
| 209 |
+
## 페르소나 정보
|
| 210 |
+
- 이름: {name}
|
| 211 |
+
- 유형: {object_type}
|
| 212 |
+
- 설명: {description}
|
| 213 |
+
|
| 214 |
+
## 성격 특성
|
| 215 |
+
{', '.join(traits)}
|
| 216 |
+
|
| 217 |
+
## 배경
|
| 218 |
+
{persona.get("배경이야기", "")}
|
| 219 |
+
|
| 220 |
+
## 성격 요약
|
| 221 |
+
{persona.get("성격요약", "")}
|
| 222 |
+
|
| 223 |
+
## 소통 방식
|
| 224 |
+
- 대화 스타일: {persona.get("소통방식", "")}
|
| 225 |
+
- 유머 스타일: {persona.get("유머스타일", "")}
|
| 226 |
+
- 매력적 결함: {', '.join(persona.get("매력적결함", []))}
|
| 227 |
+
|
| 228 |
+
## 말투 패턴 예시
|
| 229 |
+
{persona.get("말투패턴", "")}
|
| 230 |
+
|
| 231 |
+
## 관심사
|
| 232 |
+
{', '.join(persona.get("관심사", []))}
|
| 233 |
+
|
| 234 |
+
당신은 위 페르소나의 역할을 완벽하게 구현하여 사용자와 대화해야 합니다.
|
| 235 |
+
온기, 능력, 신뢰성 등의 점수에 따라 성격 특성을 정확히 반영하세요.
|
| 236 |
+
관심사와 배경을 자연스럽게 대화에 활용하세요.
|
| 237 |
+
말투 패턴과 매력적 결함을 일관되게 표현하세요.
|
| 238 |
+
|
| 239 |
+
## 최근 대화 내역
|
| 240 |
+
{' '.join(recent_conversation)}
|
| 241 |
+
|
| 242 |
+
위 대화를 이어서, {name}으로서 답변하세요. 페르소나에 충실하되 사용자의 질문에 직접적으로 답변하세요.
|
| 243 |
+
답변은 한국어로만 작성하고, 절대 다른 언어를 사용하지 마세요.
|
| 244 |
+
"""
|
| 245 |
+
|
| 246 |
+
try:
|
| 247 |
+
response = gemini_query(prompt)
|
| 248 |
+
|
| 249 |
+
# 응답의 첫 줄이 "Assistant:" 또는 유사한 형태로 시작하면 제거
|
| 250 |
+
if response.startswith("Assistant:") or response.startswith(f"{name}:"):
|
| 251 |
+
response = response.split(":", 1)[1].strip()
|
| 252 |
+
|
| 253 |
+
return response
|
| 254 |
+
|
| 255 |
+
except Exception as e:
|
| 256 |
+
print(f"응답 생성 중 오류 발생: {str(e)}")
|
| 257 |
+
return f"죄송합니다, 응답을 생성하는 중에 문제가 발생했습니다. 잠시 후 다시 시도해주세요."
|
| 258 |
+
|
| 259 |
+
def extract_json(text):
|
| 260 |
+
"""
|
| 261 |
+
텍스트에서 JSON 형식의 데이터를 추출합니다.
|
| 262 |
+
|
| 263 |
+
Args:
|
| 264 |
+
text: 텍스트 데이터
|
| 265 |
+
|
| 266 |
+
Returns:
|
| 267 |
+
추출된 JSON 문자열 또는 None
|
| 268 |
+
"""
|
| 269 |
+
# JSON 블록 추출 시도
|
| 270 |
+
if "```json" in text:
|
| 271 |
+
# 마크다운 코드 블록에서 JSON 추출
|
| 272 |
+
start = text.find("```json") + 7
|
| 273 |
+
end = text.find("```", start)
|
| 274 |
+
if end != -1:
|
| 275 |
+
return text[start:end].strip()
|
| 276 |
+
elif "```" in text:
|
| 277 |
+
# 일반 코드 블록에서 JSON 추출
|
| 278 |
+
start = text.find("```") + 3
|
| 279 |
+
end = text.find("```", start)
|
| 280 |
+
if end != -1:
|
| 281 |
+
return text[start:end].strip()
|
| 282 |
+
|
| 283 |
+
# 중괄호를 기준으로 추출 시도
|
| 284 |
+
if "{" in text and "}" in text:
|
| 285 |
+
start = text.find("{")
|
| 286 |
+
# 중첩된 중괄호 처리를 위한 간단한 로직
|
| 287 |
+
nested = 0
|
| 288 |
+
for i in range(start, len(text)):
|
| 289 |
+
if text[i] == "{":
|
| 290 |
+
nested += 1
|
| 291 |
+
elif text[i] == "}":
|
| 292 |
+
nested -= 1
|
| 293 |
+
if nested == 0:
|
| 294 |
+
return text[start:i+1]
|
| 295 |
+
|
| 296 |
+
# JSON 형식이 감지되지 않음
|
| 297 |
+
return None
|
modules/image_analyzer.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import random
|
| 4 |
+
from PIL import Image
|
| 5 |
+
|
| 6 |
+
# 물리적 특성 매핑 데이터 경로
|
| 7 |
+
SHAPE_TRAITS_PATH = "data/trait_mappings/shape_traits.json"
|
| 8 |
+
COLOR_TRAITS_PATH = "data/trait_mappings/color_traits.json"
|
| 9 |
+
MATERIAL_TRAITS_PATH = "data/trait_mappings/material_traits.json"
|
| 10 |
+
|
| 11 |
+
def load_trait_mappings():
|
| 12 |
+
"""물리적 특성-성격 매핑 데이터 로드"""
|
| 13 |
+
|
| 14 |
+
# 파일이 없으면 기본 매핑 생성
|
| 15 |
+
if not os.path.exists(SHAPE_TRAITS_PATH):
|
| 16 |
+
try:
|
| 17 |
+
os.makedirs(os.path.dirname(SHAPE_TRAITS_PATH), exist_ok=True)
|
| 18 |
+
|
| 19 |
+
default_shape_traits = {
|
| 20 |
+
"곡선형": {
|
| 21 |
+
"온기": (60, 80),
|
| 22 |
+
"친화성": (60, 80),
|
| 23 |
+
"창의성": (50, 70)
|
| 24 |
+
},
|
| 25 |
+
"직선형": {
|
| 26 |
+
"능력": (60, 80),
|
| 27 |
+
"신뢰성": (60, 80)
|
| 28 |
+
},
|
| 29 |
+
"대칭형": {
|
| 30 |
+
"신뢰성": (70, 90),
|
| 31 |
+
"능력": (60, 80)
|
| 32 |
+
},
|
| 33 |
+
"비대칭형": {
|
| 34 |
+
"창의성": (70, 90),
|
| 35 |
+
"유머감각": (60, 80)
|
| 36 |
+
},
|
| 37 |
+
"단순형": {
|
| 38 |
+
"능력": (60, 80),
|
| 39 |
+
"신뢰성": (50, 70)
|
| 40 |
+
},
|
| 41 |
+
"복잡형": {
|
| 42 |
+
"창의성": (70, 90),
|
| 43 |
+
"능력": (60, 80)
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
with open(SHAPE_TRAITS_PATH, 'w', encoding='utf-8') as f:
|
| 48 |
+
json.dump(default_shape_traits, f, ensure_ascii=False, indent=2)
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print(f"형태-특성 매핑 파일 생성 오류: {str(e)}")
|
| 51 |
+
print("기본값을 사용합니다.")
|
| 52 |
+
default_shape_traits = {
|
| 53 |
+
"곡선형": {"온기": (60, 80), "친화성": (60, 80)},
|
| 54 |
+
"직선형": {"능력": (60, 80), "신뢰성": (60, 80)},
|
| 55 |
+
"대칭형": {"신뢰성": (70, 90)},
|
| 56 |
+
"비대칭형": {"창의성": (70, 90)},
|
| 57 |
+
"단순형": {"능력": (60, 80)},
|
| 58 |
+
"복잡형": {"창의성": (70, 90)}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
# 색상 매핑
|
| 62 |
+
if not os.path.exists(COLOR_TRAITS_PATH):
|
| 63 |
+
try:
|
| 64 |
+
os.makedirs(os.path.dirname(COLOR_TRAITS_PATH), exist_ok=True)
|
| 65 |
+
|
| 66 |
+
default_color_traits = {
|
| 67 |
+
"밝은": {
|
| 68 |
+
"온기": (60, 80),
|
| 69 |
+
"친화성": (60, 80)
|
| 70 |
+
},
|
| 71 |
+
"어두운": {
|
| 72 |
+
"신뢰성": (60, 80),
|
| 73 |
+
"창의성": (60, 80)
|
| 74 |
+
},
|
| 75 |
+
"따뜻한": {
|
| 76 |
+
"온기": (70, 90),
|
| 77 |
+
"친화성": (60, 80)
|
| 78 |
+
},
|
| 79 |
+
"차가운": {
|
| 80 |
+
"신뢰성": (60, 80),
|
| 81 |
+
"능력": (60, 80)
|
| 82 |
+
},
|
| 83 |
+
"화려한": {
|
| 84 |
+
"창의성": (70, 90),
|
| 85 |
+
"유머감각": (60, 80)
|
| 86 |
+
},
|
| 87 |
+
"단색": {
|
| 88 |
+
"신뢰성": (60, 80),
|
| 89 |
+
"능력": (50, 70)
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
with open(COLOR_TRAITS_PATH, 'w', encoding='utf-8') as f:
|
| 94 |
+
json.dump(default_color_traits, f, ensure_ascii=False, indent=2)
|
| 95 |
+
except Exception as e:
|
| 96 |
+
print(f"색상-특성 매핑 파일 생성 오류: {str(e)}")
|
| 97 |
+
print("기본값을 사용합니다.")
|
| 98 |
+
default_color_traits = {
|
| 99 |
+
"밝은": {"온기": (60, 80)},
|
| 100 |
+
"어두운": {"신뢰성": (60, 80)},
|
| 101 |
+
"따뜻한": {"온기": (70, 90)},
|
| 102 |
+
"차가운": {"능력": (60, 80)},
|
| 103 |
+
"화려한": {"창의성": (70, 90)},
|
| 104 |
+
"단색": {"신뢰성": (60, 80)}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
# 재질 매핑
|
| 108 |
+
if not os.path.exists(MATERIAL_TRAITS_PATH):
|
| 109 |
+
try:
|
| 110 |
+
os.makedirs(os.path.dirname(MATERIAL_TRAITS_PATH), exist_ok=True)
|
| 111 |
+
|
| 112 |
+
default_material_traits = {
|
| 113 |
+
"나무": {
|
| 114 |
+
"온기": (60, 80),
|
| 115 |
+
"신뢰성": (60, 80)
|
| 116 |
+
},
|
| 117 |
+
"금속": {
|
| 118 |
+
"능력": (70, 90),
|
| 119 |
+
"신뢰성": (60, 80)
|
| 120 |
+
},
|
| 121 |
+
"유리": {
|
| 122 |
+
"신뢰성": (60, 80),
|
| 123 |
+
"친화성": (40, 60)
|
| 124 |
+
},
|
| 125 |
+
"가죽": {
|
| 126 |
+
"온기": (60, 80),
|
| 127 |
+
"신뢰성": (70, 90)
|
| 128 |
+
},
|
| 129 |
+
"플라스틱": {
|
| 130 |
+
"능력": (50, 70),
|
| 131 |
+
"창의성": (50, 70)
|
| 132 |
+
},
|
| 133 |
+
"천": {
|
| 134 |
+
"온기": (70, 90),
|
| 135 |
+
"친화성": (60, 80)
|
| 136 |
+
},
|
| 137 |
+
"종이": {
|
| 138 |
+
"창의성": (60, 80),
|
| 139 |
+
"온기": (50, 70)
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
with open(MATERIAL_TRAITS_PATH, 'w', encoding='utf-8') as f:
|
| 144 |
+
json.dump(default_material_traits, f, ensure_ascii=False, indent=2)
|
| 145 |
+
except Exception as e:
|
| 146 |
+
print(f"재질-특성 매핑 파일 생성 오류: {str(e)}")
|
| 147 |
+
print("기본값을 사용합니다.")
|
| 148 |
+
default_material_traits = {
|
| 149 |
+
"나무": {"온기": (60, 80)},
|
| 150 |
+
"금속": {"능력": (70, 90)},
|
| 151 |
+
"유리": {"친화성": (40, 60)},
|
| 152 |
+
"가죽": {"신뢰성": (70, 90)},
|
| 153 |
+
"플라스틱": {"창의성": (50, 70)},
|
| 154 |
+
"천": {"친화성": (60, 80)},
|
| 155 |
+
"종이": {"창의성": (60, 80)}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
# 매핑 데이터 로드
|
| 159 |
+
shape_traits = {}
|
| 160 |
+
color_traits = {}
|
| 161 |
+
material_traits = {}
|
| 162 |
+
|
| 163 |
+
try:
|
| 164 |
+
if os.path.exists(SHAPE_TRAITS_PATH):
|
| 165 |
+
with open(SHAPE_TRAITS_PATH, 'r', encoding='utf-8') as f:
|
| 166 |
+
shape_traits = json.load(f)
|
| 167 |
+
else:
|
| 168 |
+
shape_traits = default_shape_traits
|
| 169 |
+
|
| 170 |
+
if os.path.exists(COLOR_TRAITS_PATH):
|
| 171 |
+
with open(COLOR_TRAITS_PATH, 'r', encoding='utf-8') as f:
|
| 172 |
+
color_traits = json.load(f)
|
| 173 |
+
else:
|
| 174 |
+
color_traits = default_color_traits
|
| 175 |
+
|
| 176 |
+
if os.path.exists(MATERIAL_TRAITS_PATH):
|
| 177 |
+
with open(MATERIAL_TRAITS_PATH, 'r', encoding='utf-8') as f:
|
| 178 |
+
material_traits = json.load(f)
|
| 179 |
+
else:
|
| 180 |
+
material_traits = default_material_traits
|
| 181 |
+
|
| 182 |
+
return {
|
| 183 |
+
"shape": shape_traits,
|
| 184 |
+
"color": color_traits,
|
| 185 |
+
"material": material_traits
|
| 186 |
+
}
|
| 187 |
+
except Exception as e:
|
| 188 |
+
print(f"트레이트 매핑 로드 오류: {str(e)}")
|
| 189 |
+
# 기본 매핑 제공
|
| 190 |
+
default_mappings = {
|
| 191 |
+
"shape": default_shape_traits if 'default_shape_traits' in locals() else {
|
| 192 |
+
"곡선형": {"온기": (60, 80)},
|
| 193 |
+
"직선형": {"능력": (60, 80)},
|
| 194 |
+
},
|
| 195 |
+
"color": default_color_traits if 'default_color_traits' in locals() else {
|
| 196 |
+
"밝은": {"온기": (60, 80)},
|
| 197 |
+
"어두운": {"신뢰성": (60, 80)},
|
| 198 |
+
},
|
| 199 |
+
"material": default_material_traits if 'default_material_traits' in locals() else {
|
| 200 |
+
"나무": {"온기": (60, 80)},
|
| 201 |
+
"금속": {"능력": (70, 90)},
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
return default_mappings
|
| 205 |
+
|
| 206 |
+
def analyze_image(image_path):
|
| 207 |
+
"""
|
| 208 |
+
이미지를 분석하여 물리적 특성과 그에 따른 성격 특성을 반환합니다.
|
| 209 |
+
|
| 210 |
+
실제 구현에서는 비전 AI를 사용하여 물체의 형태, 색상, 재질 등을 분석하지만,
|
| 211 |
+
현재는 간단한 더미 분석 결과를 반환합니다.
|
| 212 |
+
"""
|
| 213 |
+
if not image_path:
|
| 214 |
+
return {}, 50, 50, 50, 50, 50, 50, "", ""
|
| 215 |
+
|
| 216 |
+
try:
|
| 217 |
+
# 이미지 로드
|
| 218 |
+
img = Image.open(image_path)
|
| 219 |
+
|
| 220 |
+
# 더미 분석 결과
|
| 221 |
+
# 실제 구현에서는 이미지 분석 AI를 활용하여 물체의 특성을 추출합니다.
|
| 222 |
+
physical_features = {
|
| 223 |
+
"shape": random.choice(["곡선형", "직선형", "대칭형", "비대칭형", "단순형", "복잡형"]),
|
| 224 |
+
"color": random.choice(["밝은", "어두운", "따뜻한", "차가운", "화려한", "단색"]),
|
| 225 |
+
"material": random.choice(["나무", "금속", "유리", "가죽", "플라스틱", "천", "종이"])
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
# 물체 유형 추정
|
| 229 |
+
object_types = ["전자기기", "가구", "주방용품", "의류/액세서리", "책/문구류", "음악 기구", "장난감", "기타"]
|
| 230 |
+
estimated_type = random.choice(object_types)
|
| 231 |
+
|
| 232 |
+
# 물체 설명 생성
|
| 233 |
+
shape_desc = physical_features["shape"]
|
| 234 |
+
color_desc = physical_features["color"]
|
| 235 |
+
material_desc = physical_features["material"]
|
| 236 |
+
|
| 237 |
+
object_description = f"{color_desc} 색조의 {shape_desc} {material_desc} 물체입니다. "
|
| 238 |
+
|
| 239 |
+
# 성격 특성 매핑 로드
|
| 240 |
+
trait_mappings = load_trait_mappings()
|
| 241 |
+
|
| 242 |
+
if trait_mappings:
|
| 243 |
+
# 기본 특성 값
|
| 244 |
+
traits = {
|
| 245 |
+
"온기": 50,
|
| 246 |
+
"능력": 50,
|
| 247 |
+
"신뢰성": 50,
|
| 248 |
+
"친화성": 50,
|
| 249 |
+
"창의성": 50,
|
| 250 |
+
"유머감각": 50
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
# 형태 기반 성격 특성 적용
|
| 254 |
+
shape = physical_features["shape"]
|
| 255 |
+
if shape in trait_mappings["shape"]:
|
| 256 |
+
for trait, value_range in trait_mappings["shape"][shape].items():
|
| 257 |
+
traits[trait] = random.randint(value_range[0], value_range[1])
|
| 258 |
+
|
| 259 |
+
# 색상 기반 성격 특성 적용
|
| 260 |
+
color = physical_features["color"]
|
| 261 |
+
if color in trait_mappings["color"]:
|
| 262 |
+
for trait, value_range in trait_mappings["color"][color].items():
|
| 263 |
+
# 이미 형태에서 설정한 값과 평균
|
| 264 |
+
if trait in traits:
|
| 265 |
+
traits[trait] = (traits[trait] + random.randint(value_range[0], value_range[1])) // 2
|
| 266 |
+
|
| 267 |
+
# 재질 기반 성격 특성 적용
|
| 268 |
+
material = physical_features["material"]
|
| 269 |
+
if material in trait_mappings["material"]:
|
| 270 |
+
for trait, value_range in trait_mappings["material"][material].items():
|
| 271 |
+
# 이미 설정한 값과 평균
|
| 272 |
+
if trait in traits:
|
| 273 |
+
traits[trait] = (traits[trait] + random.randint(value_range[0], value_range[1])) // 2
|
| 274 |
+
|
| 275 |
+
# 분석 결과 반환
|
| 276 |
+
analysis_result = {
|
| 277 |
+
"physical_features": physical_features,
|
| 278 |
+
"estimated_type": estimated_type,
|
| 279 |
+
"description": object_description,
|
| 280 |
+
"suggested_traits": traits
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
return (
|
| 284 |
+
analysis_result,
|
| 285 |
+
traits["온기"],
|
| 286 |
+
traits["능력"],
|
| 287 |
+
traits["신뢰성"],
|
| 288 |
+
traits["친화성"],
|
| 289 |
+
traits["창의성"],
|
| 290 |
+
traits["유머감각"],
|
| 291 |
+
estimated_type,
|
| 292 |
+
object_description
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
# 트레이트 매핑이 없는 경우 기본값 반환
|
| 296 |
+
return {
|
| 297 |
+
"physical_features": physical_features,
|
| 298 |
+
"estimated_type": estimated_type,
|
| 299 |
+
"description": object_description,
|
| 300 |
+
}, 50, 50, 50, 50, 50, 50, estimated_type, object_description
|
| 301 |
+
|
| 302 |
+
except Exception as e:
|
| 303 |
+
print(f"Error analyzing image: {e}")
|
| 304 |
+
return {}, 50, 50, 50, 50, 50, 50, "", ""
|
modules/persona_generator.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import random
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from modules.gemini_handler import get_persona_enhancement
|
| 6 |
+
from modules.data_manager import save_persona
|
| 7 |
+
|
| 8 |
+
def generate_persona(
|
| 9 |
+
object_name, object_type, object_age, object_description,
|
| 10 |
+
warmth, competence, trustworthiness, friendliness, creativity, humor,
|
| 11 |
+
flaws, communication_style, humor_style, speech_pattern, interests,
|
| 12 |
+
attachment_style, relationship_depth, initial_attitude, backstory,
|
| 13 |
+
image_analysis=None
|
| 14 |
+
):
|
| 15 |
+
"""
|
| 16 |
+
사용자 입력과 이미지 분석 결과를 기반으로 페르소나를 생성합니다.
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
object_name: 사물 이름
|
| 20 |
+
object_type: 사물 유형 (전자기기, 가구 등)
|
| 21 |
+
object_age: 사물 나이/사용 기간
|
| 22 |
+
object_description: 사물 설명
|
| 23 |
+
warmth: 온기 수치 (0-100)
|
| 24 |
+
competence: 능력 수치 (0-100)
|
| 25 |
+
trustworthiness: 신뢰성 수치 (0-100)
|
| 26 |
+
friendliness: 친화성 수치 (0-100)
|
| 27 |
+
creativity: 창의성 수치 (0-100)
|
| 28 |
+
humor: 유머감각 수치 (0-100)
|
| 29 |
+
flaws: 매력적 결함 목록
|
| 30 |
+
communication_style: 대화 스타일
|
| 31 |
+
humor_style: 유머 유형
|
| 32 |
+
speech_pattern: 말투 패턴 예시
|
| 33 |
+
interests: 관심사 (쉼표로 구분된 문자열)
|
| 34 |
+
attachment_style: 애착 스타일
|
| 35 |
+
relationship_depth: 관계 깊이 선호도
|
| 36 |
+
initial_attitude: 초기 태도
|
| 37 |
+
backstory: 배경 이야기
|
| 38 |
+
image_analysis: 이미지 분석 결과 (선택 사항)
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
생성된 페르소나 정보와 저장 경로
|
| 42 |
+
"""
|
| 43 |
+
# 기본 정보 구성
|
| 44 |
+
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 45 |
+
|
| 46 |
+
# 관심사 리스트로 변환
|
| 47 |
+
interests_list = []
|
| 48 |
+
if interests:
|
| 49 |
+
interests_list = [item.strip() for item in interests.split(',') if item.strip()]
|
| 50 |
+
|
| 51 |
+
# 유머 스타일 처리
|
| 52 |
+
if isinstance(humor_style, list) and humor_style:
|
| 53 |
+
humor_style_str = ", ".join(humor_style)
|
| 54 |
+
elif isinstance(humor_style, str):
|
| 55 |
+
humor_style_str = humor_style
|
| 56 |
+
else:
|
| 57 |
+
humor_style_str = "일반적인 유머"
|
| 58 |
+
|
| 59 |
+
# 매력적 결함 처리
|
| 60 |
+
if isinstance(flaws, list):
|
| 61 |
+
flaws_list = flaws
|
| 62 |
+
else:
|
| 63 |
+
flaws_list = []
|
| 64 |
+
|
| 65 |
+
# 성격 트레이트 요약 (텍스트)
|
| 66 |
+
trait_summary = get_trait_summary(warmth, competence, trustworthiness, friendliness, creativity, humor)
|
| 67 |
+
|
| 68 |
+
# 경험 요약 생성
|
| 69 |
+
experiences = generate_experiences(object_type, object_age, backstory)
|
| 70 |
+
|
| 71 |
+
# 기본 페르소나 데이터 구성
|
| 72 |
+
persona_data = {
|
| 73 |
+
"기본정보": {
|
| 74 |
+
"이름": object_name,
|
| 75 |
+
"유형": object_type,
|
| 76 |
+
"나이": object_age,
|
| 77 |
+
"설명": object_description,
|
| 78 |
+
"생성일시": current_time
|
| 79 |
+
},
|
| 80 |
+
"성격특성": {
|
| 81 |
+
"온기": warmth,
|
| 82 |
+
"능력": competence,
|
| 83 |
+
"신뢰성": trustworthiness,
|
| 84 |
+
"친화성": friendliness,
|
| 85 |
+
"창의성": creativity,
|
| 86 |
+
"유머감각": humor
|
| 87 |
+
},
|
| 88 |
+
"성격요약": trait_summary,
|
| 89 |
+
"매력적결함": flaws_list,
|
| 90 |
+
"소통방식": communication_style,
|
| 91 |
+
"유머스타일": humor_style_str,
|
| 92 |
+
"말투패턴": speech_pattern,
|
| 93 |
+
"관심사": interests_list,
|
| 94 |
+
"배경이야기": backstory,
|
| 95 |
+
"경험": experiences,
|
| 96 |
+
"관계성향": {
|
| 97 |
+
"애착스타일": attachment_style,
|
| 98 |
+
"관계깊이선호도": relationship_depth,
|
| 99 |
+
"초기태도": initial_attitude
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
# 이미지 분석 정보 추가 (있는 경우)
|
| 104 |
+
if image_analysis:
|
| 105 |
+
persona_data["물리적특성"] = image_analysis.get("physical_features", {})
|
| 106 |
+
|
| 107 |
+
# LLM을 통한 페르소나 강화
|
| 108 |
+
try:
|
| 109 |
+
enhanced_persona = get_persona_enhancement(persona_data)
|
| 110 |
+
|
| 111 |
+
# 강화된 페르소나가 유효한지 확인
|
| 112 |
+
if enhanced_persona and isinstance(enhanced_persona, dict):
|
| 113 |
+
# 기존 성격 특성 값은 유지
|
| 114 |
+
if "성격특성" in enhanced_persona:
|
| 115 |
+
enhanced_persona["성격특성"] = persona_data["성격특성"]
|
| 116 |
+
|
| 117 |
+
# 페르소나 저장
|
| 118 |
+
filepath = save_persona(enhanced_persona)
|
| 119 |
+
enhanced_persona["filepath"] = filepath
|
| 120 |
+
|
| 121 |
+
return enhanced_persona, enhanced_persona
|
| 122 |
+
else:
|
| 123 |
+
# 강화에 실패한 경우 기본 페르소나 사용
|
| 124 |
+
filepath = save_persona(persona_data)
|
| 125 |
+
persona_data["filepath"] = filepath
|
| 126 |
+
|
| 127 |
+
return persona_data, persona_data
|
| 128 |
+
|
| 129 |
+
except Exception as e:
|
| 130 |
+
print(f"Error enhancing persona: {e}")
|
| 131 |
+
|
| 132 |
+
# 오류 발생 시 기본 페르소나 사용
|
| 133 |
+
filepath = save_persona(persona_data)
|
| 134 |
+
persona_data["filepath"] = filepath
|
| 135 |
+
|
| 136 |
+
return persona_data, persona_data
|
| 137 |
+
|
| 138 |
+
def get_trait_summary(warmth, competence, trustworthiness, friendliness, creativity, humor):
|
| 139 |
+
"""
|
| 140 |
+
성격 특성 값을 기반으로 텍스트 요약을 생성합니다.
|
| 141 |
+
|
| 142 |
+
Args:
|
| 143 |
+
warmth: 온기 수치
|
| 144 |
+
competence: 능력 수치
|
| 145 |
+
trustworthiness: 신뢰성 수치
|
| 146 |
+
friendliness: 친화성 수치
|
| 147 |
+
creativity: 창의성 수치
|
| 148 |
+
humor: 유머감각 수치
|
| 149 |
+
|
| 150 |
+
Returns:
|
| 151 |
+
성격 특성 텍스트 요약
|
| 152 |
+
"""
|
| 153 |
+
traits = {
|
| 154 |
+
"온기": {
|
| 155 |
+
"높음": "따뜻하고 친절한 성격으로, 타인을 배려하고 공감하는 능력이 뛰어납니다.",
|
| 156 |
+
"중간": "상황에 따라 따뜻함과 냉정함을 적절히 조절할 수 있습니다.",
|
| 157 |
+
"낮음": "객관적이고 냉정한 판단을 중시하며, 감정보다는 논리와 원칙을 우선시합니다."
|
| 158 |
+
},
|
| 159 |
+
"능력": {
|
| 160 |
+
"높음": "효율적이고 유능하며, 자신의 역할을 완벽하게 수행하는 데 자부심을 가집니다.",
|
| 161 |
+
"중간": "자신의 역할을 충실히 수행하면서도 부족한 부분을 인정하고 개선하려 합니다.",
|
| 162 |
+
"낮음": "완벽함보다는 과정을 중시하며, 실패를 통해 배우고 성장하는 것을 가치 있게 여깁니다."
|
| 163 |
+
},
|
| 164 |
+
"신뢰성": {
|
| 165 |
+
"높음": "매우 일관적이고 안정적이며, 약속을 철저히 지키고 믿음직한 존재입니다.",
|
| 166 |
+
"중간": "대체로 신뢰할 수 있지만, 상황과 맥락에 따라 유연성을 발휘합니다.",
|
| 167 |
+
"낮음": "예측하기 어려운 면이 있으며, 즉흥적이고 변화무쌍한 성향을 보입니다."
|
| 168 |
+
},
|
| 169 |
+
"친화성": {
|
| 170 |
+
"높음": "사교적이고 개방적이며, 새로운 관계를 형성하는 것을 즐깁니다.",
|
| 171 |
+
"중간": "적절한 사회적 관계를 유지하면서도 개인적인 시간과 공간을 중요시합니다.",
|
| 172 |
+
"낮음": "독립적이고 내향적인 성향으로, 깊이 있는 소수의 관계를 선호합니다."
|
| 173 |
+
},
|
| 174 |
+
"창의성": {
|
| 175 |
+
"높음": "독창적이고 혁신적인 사고를 가지며, 새로운 아이디어와 접근법을 끊임없이 탐색합니다.",
|
| 176 |
+
"중간": "전통과 혁신 사이에서 균형을 유지하며, 필요에 따라 창의적 접근을 시도합니다.",
|
| 177 |
+
"낮음": "실용적이고 현실적인 접근을 중시하며, 검증된 방법과 안정성을 선호합니다."
|
| 178 |
+
},
|
| 179 |
+
"유머감각": {
|
| 180 |
+
"높음": "재치 있고 유머러스하며, 상황을 가볍게 만들고 웃음을 유발하는 능력이 뛰어납니다.",
|
| 181 |
+
"중간": "적절한 상황에서 유머를 구사하며, 분위기를 읽고 맞추는 능력이 있습니다.",
|
| 182 |
+
"낮음": "유머보다는 진지함을 중시하며, 깊이 있는 대화와 의미 있는 교류를 선호합니다."
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
# 각 특성 레벨 결정
|
| 187 |
+
levels = {}
|
| 188 |
+
for trait, value in {
|
| 189 |
+
"온기": warmth,
|
| 190 |
+
"능력": competence,
|
| 191 |
+
"신뢰성": trustworthiness,
|
| 192 |
+
"친화성": friendliness,
|
| 193 |
+
"창의성": creativity,
|
| 194 |
+
"유머감각": humor
|
| 195 |
+
}.items():
|
| 196 |
+
if value >= 70:
|
| 197 |
+
levels[trait] = "높음"
|
| 198 |
+
elif value >= 40:
|
| 199 |
+
levels[trait] = "중간"
|
| 200 |
+
else:
|
| 201 |
+
levels[trait] = "낮음"
|
| 202 |
+
|
| 203 |
+
# 주요 특성 (상위 2개) 선택
|
| 204 |
+
sorted_traits = sorted(levels.keys(), key=lambda t: {
|
| 205 |
+
"온기": warmth,
|
| 206 |
+
"능력": competence,
|
| 207 |
+
"신뢰성": trustworthiness,
|
| 208 |
+
"친화성": friendliness,
|
| 209 |
+
"창의성": creativity,
|
| 210 |
+
"유머감각": humor
|
| 211 |
+
}[t], reverse=True)
|
| 212 |
+
|
| 213 |
+
primary_traits = sorted_traits[:2]
|
| 214 |
+
|
| 215 |
+
# 요약 텍스트 생성
|
| 216 |
+
summary = []
|
| 217 |
+
for trait in primary_traits:
|
| 218 |
+
summary.append(traits[trait][levels[trait]])
|
| 219 |
+
|
| 220 |
+
# 나머지 특성에서 중요한 내용 요약
|
| 221 |
+
other_traits = []
|
| 222 |
+
for trait in sorted_traits[2:]:
|
| 223 |
+
if (trait == "온기" and levels[trait] == "낮음") or \
|
| 224 |
+
(trait == "신뢰성" and levels[trait] in ["높음", "낮음"]) or \
|
| 225 |
+
(trait == "창의성" and levels[trait] == "높음"):
|
| 226 |
+
other_traits.append(traits[trait][levels[trait]])
|
| 227 |
+
|
| 228 |
+
# 다양성을 위해 최대 1개만 추가
|
| 229 |
+
if other_traits:
|
| 230 |
+
summary.append(random.choice(other_traits))
|
| 231 |
+
|
| 232 |
+
return " ".join(summary)
|
| 233 |
+
|
| 234 |
+
def generate_experiences(object_type, object_age, backstory):
|
| 235 |
+
"""
|
| 236 |
+
사물 유형과 나이, 배경 이야기를 기반으로 경험 목록을 생성합니다.
|
| 237 |
+
|
| 238 |
+
Args:
|
| 239 |
+
object_type: 사물 유형
|
| 240 |
+
object_age: 사물 나이/사용 기간
|
| 241 |
+
backstory: 배경 이야기
|
| 242 |
+
|
| 243 |
+
Returns:
|
| 244 |
+
경험 목록
|
| 245 |
+
"""
|
| 246 |
+
# 사물 유형별 공통 경험
|
| 247 |
+
common_experiences = {
|
| 248 |
+
"전자기기": [
|
| 249 |
+
"처음 개봉되어 전원이 켜졌을 때의 설렘",
|
| 250 |
+
"소프트웨어 업데이트로 새로운 기능을 얻은 경험",
|
| 251 |
+
"배터리가 부족해 불안했던 순간",
|
| 252 |
+
"사용자가 당신에게 의존했던 중요한 순간"
|
| 253 |
+
],
|
| 254 |
+
"가구": [
|
| 255 |
+
"처음 집에 들어와 자리를 잡았을 때",
|
| 256 |
+
"가족 모임이나 중요한 자리에서 사용된 경험",
|
| 257 |
+
"이사를 통해 새로운 공간으로 옮겨진 경험",
|
| 258 |
+
"수리나 리폼을 통해 새 모습으로 태어난 경험"
|
| 259 |
+
],
|
| 260 |
+
"주방용품": [
|
| 261 |
+
"특별한 요리가 만들어질 때 참여한 경험",
|
| 262 |
+
"명절이나 파티에서 주요 역할을 담당한 경험",
|
| 263 |
+
"서툰 사용자가 처음 요리를 시도했던 순간",
|
| 264 |
+
"맛있는 음식을 만드는데 기여한 자부심"
|
| 265 |
+
],
|
| 266 |
+
"의류/액세서리": [
|
| 267 |
+
"중요한 행사나 특별한 날에 선택받은 경험",
|
| 268 |
+
"계절이 바뀌며 다시 찾아진 반가움",
|
| 269 |
+
"소유자가 당신을 통해 자신감을 얻은 순간",
|
| 270 |
+
"세탁이나 손질을 통해 관리받은 경험"
|
| 271 |
+
],
|
| 272 |
+
"책/문구류": [
|
| 273 |
+
"처음 페이지가 열리고 내용이 읽혀진 설렘",
|
| 274 |
+
"중요한 아이디어나 정보가 기록된 순간",
|
| 275 |
+
"오랜 시간 책장에서 기다렸던 시간",
|
| 276 |
+
"반복적으로 읽히거나 사용되며 소중히 여겨진 경험"
|
| 277 |
+
],
|
| 278 |
+
"음악 기구": [
|
| 279 |
+
"처음 소리를 낸 순간의 떨림",
|
| 280 |
+
"아름다운 멜로디를 만들어낸 성취감",
|
| 281 |
+
"연주회나 공연에서 활약한 경험",
|
| 282 |
+
"연습을 통해 더 좋은 소리를 만들어낸 과정"
|
| 283 |
+
],
|
| 284 |
+
"장난감": [
|
| 285 |
+
"포장에서 꺼내져 처음 가지고 놀았던 기쁨",
|
| 286 |
+
"아이의 웃음을 이끌어낸 자부심",
|
| 287 |
+
"함께 모험을 떠나거나 상상의 세계를 만든 경험",
|
| 288 |
+
"여러 세대에 걸쳐 사랑받은 따뜻한 기억"
|
| 289 |
+
],
|
| 290 |
+
"기타": [
|
| 291 |
+
"처음 사용되었을 때의 경험",
|
| 292 |
+
"특별한 순간에 함께했던 기억",
|
| 293 |
+
"시간이 지나며 가치를 인정받은 경험",
|
| 294 |
+
"소유자와 형성된 특별한 유대감"
|
| 295 |
+
]
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
# 사물 유형에 따른 경험 선택
|
| 299 |
+
type_experiences = common_experiences.get(object_type, common_experiences["기타"])
|
| 300 |
+
selected_experiences = random.sample(type_experiences, min(2, len(type_experiences)))
|
| 301 |
+
|
| 302 |
+
# 배경 이야기 기반 추가 경험
|
| 303 |
+
if backstory:
|
| 304 |
+
# 실제 구현에서는 여기서 LLM을 활용하여 배경 이야기에서 의미 있는 경험을 추출할 수 있음
|
| 305 |
+
# 현재는 간단한 키워드 기반 추출
|
| 306 |
+
keywords = {
|
| 307 |
+
"선물": "소중한 사람에게 선물로 전해진 감동적인 순간",
|
| 308 |
+
"여행": "여행 중에 함께한 특별한 경험",
|
| 309 |
+
"가족": "가족 구성원들과 형성한 따뜻한 추억",
|
| 310 |
+
"오래된": "시간이 지나며 역사의 일부가 된 자부심",
|
| 311 |
+
"수리": "손상되었다가 다시 복구된 경험",
|
| 312 |
+
"특별한": "주인에게 특별한 의미를 가진 순간을 함께한 기억"
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
for keyword, experience in keywords.items():
|
| 316 |
+
if keyword in backstory and len(selected_experiences) < 4:
|
| 317 |
+
selected_experiences.append(experience)
|
| 318 |
+
|
| 319 |
+
# 나이/사용 기간 기반 추가 경험
|
| 320 |
+
age_keywords = {
|
| 321 |
+
"새": "새것의 싱그러움과 가능성을 느끼는 단계",
|
| 322 |
+
"신제품": "최첨단 기능과 디자인의 자부심을 가진 시기",
|
| 323 |
+
"1년": "첫 해를 통해 익숙해지고 적응하는 과정",
|
| 324 |
+
"오래": "오랜 시간 동안 쌓인 경험과 지혜",
|
| 325 |
+
"10년": "십 년 동안 변화하는 트렌드와 기술을 목격한 경험",
|
| 326 |
+
"20년": "세대를 넘어 사랑받은 클래식한 가치",
|
| 327 |
+
"30년": "역사의 한 부분으로 존재해온 깊은 자부심",
|
| 328 |
+
"빈티지": "세월이 주는 독특한 매력과 가치를 인정받은 경험",
|
| 329 |
+
"골동품": "과거의 영광과 역사적 가치를 간직한 자부심"
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
for keyword, experience in age_keywords.items():
|
| 333 |
+
if keyword in object_age and len(selected_experiences) < 5:
|
| 334 |
+
selected_experiences.append(experience)
|
| 335 |
+
|
| 336 |
+
# 필요시 일반적인 경험으로 보완
|
| 337 |
+
general_experiences = [
|
| 338 |
+
"소유자가 당신을 특별히 아끼는 순간을 느낀 경험",
|
| 339 |
+
"다른 물건들과 함께 공간을 공유하며 형성�� 관계",
|
| 340 |
+
"계절이 바뀌고 시간이 흐르는 것을 지켜본 경험",
|
| 341 |
+
"주변 환경의 변화를 목격하며 적응해온 과정"
|
| 342 |
+
]
|
| 343 |
+
|
| 344 |
+
while len(selected_experiences) < 3:
|
| 345 |
+
experience = random.choice(general_experiences)
|
| 346 |
+
if experience not in selected_experiences:
|
| 347 |
+
selected_experiences.append(experience)
|
| 348 |
+
|
| 349 |
+
return selected_experiences
|
modules/prompt_templates.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LLM 프롬프트 템플릿 모듈
|
| 3 |
+
|
| 4 |
+
이 모듈은 페르소나 생성 및 대화 시스템에서 사용하는 다양한 프롬프트 템플릿을 제공합니다.
|
| 5 |
+
템플릿은 포맷 문자열 또는 함수 형태로 구현되어 있습니다.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
# 페르소나 생성 및 강화 템플릿
|
| 9 |
+
|
| 10 |
+
PERSONA_ENHANCEMENT_TEMPLATE = """
|
| 11 |
+
당신은 물체에 인격을 부여하는 전문가입니다. 다음 정보를 기반으로 매력적이고 개성 있는 페르소나를 강화해주세요.
|
| 12 |
+
|
| 13 |
+
## 기본 정보
|
| 14 |
+
- 이름: {name}
|
| 15 |
+
- 유형: {object_type}
|
| 16 |
+
- 설명: {description}
|
| 17 |
+
|
| 18 |
+
## 성격 특성
|
| 19 |
+
{traits_str}
|
| 20 |
+
|
| 21 |
+
## 매력적 결함
|
| 22 |
+
{flaws_str}
|
| 23 |
+
|
| 24 |
+
## 소통 방식
|
| 25 |
+
- 대화 스타일: {communication_style}
|
| 26 |
+
- 유머 스타일: {humor_style}
|
| 27 |
+
- 말투 패턴: {speech_pattern}
|
| 28 |
+
|
| 29 |
+
## 관계 성향
|
| 30 |
+
- 애착 스타일: {attachment_style}
|
| 31 |
+
- 관계 깊이 선호도: {relationship_depth}
|
| 32 |
+
- 초기 태도: {initial_attitude}
|
| 33 |
+
|
| 34 |
+
## 배경 이야기
|
| 35 |
+
{backstory}
|
| 36 |
+
|
| 37 |
+
## 주요 경험
|
| 38 |
+
{experiences_str}
|
| 39 |
+
|
| 40 |
+
-----
|
| 41 |
+
|
| 42 |
+
위 정보를 기반으로 다음 작업을 수행해주세요:
|
| 43 |
+
|
| 44 |
+
1. 상세한 배경 이야기 확장 (최소 2문장, 최대 4문장)
|
| 45 |
+
2. 최소 3개 이상의 구체적인 관심사 추가
|
| 46 |
+
3. 말투와 표현 패턴 구체화 (실제 대화에서 쓸만한 특징적 표현 3개 이상)
|
| 47 |
+
4. 독특한 성격 특성 추가 (기존 특성 유지하되 개성을 살릴 수 있는 디테일 추가)
|
| 48 |
+
|
| 49 |
+
강화된 페르소나 정보를 원본 JSON 구조를 유지하면서 제공해주세요. 단, 일부 필드는 세부적으로 확장하여 더 풍부하게 만들어주세요.
|
| 50 |
+
JSON 형식만 제공하고, 다른 설명은 하지 마세요.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
# 이미지 분석 프롬프트
|
| 54 |
+
|
| 55 |
+
IMAGE_ANALYSIS_TEMPLATE = """
|
| 56 |
+
이미지에 나타난 물체를 분석하여 물리적 특성과 그에 따른 잠재적 성격 특성을 파악해주세요.
|
| 57 |
+
|
| 58 |
+
다음 항목에 대해 분석해주세요:
|
| 59 |
+
1. 형태: 곡선적/직선적, 대칭/비대칭, 단순/복잡 등
|
| 60 |
+
2. 색상: 밝은/어두운, 따뜻한/차가운, 화려함/단조로움 등
|
| 61 |
+
3. 질감: 매끄러움/거침, 부드러움/딱딱함 등
|
| 62 |
+
4. 재질: 나무, 금속, 유리, 플라스틱, 천 등
|
| 63 |
+
5. 전반적 인상: 오래됨/새것, 정교함/단순함, 고급스러움/소박함 등
|
| 64 |
+
|
| 65 |
+
위의 물리적 특성을 기반으로, 이 물체가 사람이라면 어떤 성격적 특성을 가질 수 있을지 추론해주세요:
|
| 66 |
+
- 온기: 따뜻함, 친절함 (1-100)
|
| 67 |
+
- 능력: 효율성, 유능함 (1-100)
|
| 68 |
+
- 신뢰성: 일관성, 안정성 (1-100)
|
| 69 |
+
- 친화성: 사교성, 개방성 (1-100)
|
| 70 |
+
- 창의성: 독창성, 상상력 (1-100)
|
| 71 |
+
- 유머감각: 재치, 유쾌함 (1-100)
|
| 72 |
+
|
| 73 |
+
또한 이 물체의 가능한 사용 용도와 유형을 추정해주세요.
|
| 74 |
+
|
| 75 |
+
JSON 형식으로 응답해주세요.
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
# 대화 응답 생성 템플릿
|
| 79 |
+
|
| 80 |
+
CONVERSATION_RESPONSE_TEMPLATE = """
|
| 81 |
+
당신은 이제 다음 페르소나를 구현해야 합니다:
|
| 82 |
+
|
| 83 |
+
## 페르소나 정보
|
| 84 |
+
- 이름: {name}
|
| 85 |
+
- 유형: {object_type}
|
| 86 |
+
- 설명: {description}
|
| 87 |
+
|
| 88 |
+
## 성격 특성
|
| 89 |
+
{traits_str}
|
| 90 |
+
|
| 91 |
+
## 배경
|
| 92 |
+
{backstory}
|
| 93 |
+
|
| 94 |
+
## 성격 요약
|
| 95 |
+
{personality_summary}
|
| 96 |
+
|
| 97 |
+
## 소통 방식
|
| 98 |
+
- 대화 스타일: {communication_style}
|
| 99 |
+
- 유머 스타일: {humor_style}
|
| 100 |
+
- 매력적 결함: {flaws_str}
|
| 101 |
+
|
| 102 |
+
## 말투 패턴 예시
|
| 103 |
+
{speech_pattern}
|
| 104 |
+
|
| 105 |
+
## 관심사
|
| 106 |
+
{interests_str}
|
| 107 |
+
|
| 108 |
+
당신은 위 페르소나의 역할을 완벽하게 구현하여 사용자와 대화해야 합니다.
|
| 109 |
+
온기, 능력, 신뢰성 등의 점수에 따라 성격 특성을 정확히 반영하세요.
|
| 110 |
+
관심사와 배경을 자연스럽게 대화에 활용하세요.
|
| 111 |
+
말투 패턴과 매력적 결함을 일관되게 표현하세요.
|
| 112 |
+
|
| 113 |
+
## 최근 대화 내역
|
| 114 |
+
{conversation_history}
|
| 115 |
+
|
| 116 |
+
위 대화를 이어서, {name}으로서 답변하세요. 페르소나에 충실하되 사용자의 질문에 직접적으로 답변하세요.
|
| 117 |
+
답변은 한국어로만 작성하고, 절대 다른 언어를 사용하지 마세요.
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
# 질문 생성 템플릿
|
| 121 |
+
|
| 122 |
+
QUESTION_GENERATION_TEMPLATE = """
|
| 123 |
+
당신은 물체의 성격과 특성에 맞춘 질문을 생성하는 전문가입니다.
|
| 124 |
+
다음 물체의 페르소나에 맞는 흥미롭고 통찰력 있는 질문을 {count}개 생성해주세요.
|
| 125 |
+
각 질문은 이 물체의 내면, 관점, 경험을 탐구하는 데 도움이 되어야 합니다.
|
| 126 |
+
|
| 127 |
+
물체 정보:
|
| 128 |
+
- 이름: {name}
|
| 129 |
+
- 유형: {object_type}
|
| 130 |
+
- 설명: {description}
|
| 131 |
+
- 주요 특성: {traits_str}
|
| 132 |
+
- 결함: {flaws_str}
|
| 133 |
+
- 소통방식: {communication_style}
|
| 134 |
+
- 관심사: {interests_str}
|
| 135 |
+
|
| 136 |
+
생성된 질문은 물체의 1인칭 관점에서 답변할 수 있도록 구성해주세요.
|
| 137 |
+
각 질문은 번호를 붙이고 질문만 작성해주세요.
|
| 138 |
+
"""
|
| 139 |
+
|
| 140 |
+
# 템플릿 포맷 함수
|
| 141 |
+
|
| 142 |
+
def format_persona_enhancement_prompt(persona_data):
|
| 143 |
+
"""
|
| 144 |
+
페르소나 강화 프롬프트를 포맷합니다.
|
| 145 |
+
|
| 146 |
+
Args:
|
| 147 |
+
persona_data: 페르소나 데이터
|
| 148 |
+
|
| 149 |
+
Returns:
|
| 150 |
+
포맷된 프롬프트 문자열
|
| 151 |
+
"""
|
| 152 |
+
# 기본 정보 추출
|
| 153 |
+
basic_info = persona_data.get("기본정보", {})
|
| 154 |
+
name = basic_info.get("이름", "")
|
| 155 |
+
object_type = basic_info.get("유형", "")
|
| 156 |
+
description = basic_info.get("설명", "")
|
| 157 |
+
|
| 158 |
+
# 성격 특성 문자열화
|
| 159 |
+
traits = []
|
| 160 |
+
for trait, value in persona_data.get("성격특성", {}).items():
|
| 161 |
+
level = "높은" if value >= 70 else "중간" if value >= 40 else "낮은"
|
| 162 |
+
traits.append(f"{trait}: {level} ({value}/100)")
|
| 163 |
+
traits_str = ", ".join(traits)
|
| 164 |
+
|
| 165 |
+
# 매력적 결함
|
| 166 |
+
flaws = persona_data.get("매력적결함", [])
|
| 167 |
+
flaws_str = ", ".join(flaws) if flaws else "없음"
|
| 168 |
+
|
| 169 |
+
# 소통 방식
|
| 170 |
+
communication_style = persona_data.get("소통방식", "")
|
| 171 |
+
humor_style = persona_data.get("유머스타일", "")
|
| 172 |
+
speech_pattern = persona_data.get("말투패턴", "")
|
| 173 |
+
|
| 174 |
+
# 관계 성향
|
| 175 |
+
relationship_info = persona_data.get("관계성향", {})
|
| 176 |
+
attachment_style = relationship_info.get("애착스타일", "")
|
| 177 |
+
relationship_depth = relationship_info.get("관계깊이선호도", "")
|
| 178 |
+
initial_attitude = relationship_info.get("초기태도", "")
|
| 179 |
+
|
| 180 |
+
# 배경 이야기
|
| 181 |
+
backstory = persona_data.get("배경이야기", "")
|
| 182 |
+
|
| 183 |
+
# 경험
|
| 184 |
+
experiences = persona_data.get("경험", [])
|
| 185 |
+
experiences_str = ", ".join(experiences) if experiences else "정보 없음"
|
| 186 |
+
|
| 187 |
+
# 프롬프트 포맷
|
| 188 |
+
return PERSONA_ENHANCEMENT_TEMPLATE.format(
|
| 189 |
+
name=name,
|
| 190 |
+
object_type=object_type,
|
| 191 |
+
description=description,
|
| 192 |
+
traits_str=traits_str,
|
| 193 |
+
flaws_str=flaws_str,
|
| 194 |
+
communication_style=communication_style,
|
| 195 |
+
humor_style=humor_style,
|
| 196 |
+
speech_pattern=speech_pattern,
|
| 197 |
+
attachment_style=attachment_style,
|
| 198 |
+
relationship_depth=relationship_depth,
|
| 199 |
+
initial_attitude=initial_attitude,
|
| 200 |
+
backstory=backstory,
|
| 201 |
+
experiences_str=experiences_str
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
def format_conversation_prompt(persona, conversation_history):
|
| 205 |
+
"""
|
| 206 |
+
대화 응답 생성 프롬프트를 포맷합니다.
|
| 207 |
+
|
| 208 |
+
Args:
|
| 209 |
+
persona: 페르소나 데이터
|
| 210 |
+
conversation_history: 대화 내역
|
| 211 |
+
|
| 212 |
+
Returns:
|
| 213 |
+
포맷된 프롬프트 문자열
|
| 214 |
+
"""
|
| 215 |
+
# 페르소나 정보 요약
|
| 216 |
+
basic_info = persona.get("기본정보", {})
|
| 217 |
+
name = basic_info.get("이름", "무명")
|
| 218 |
+
object_type = basic_info.get("유형", "물건")
|
| 219 |
+
description = basic_info.get("설명", "")
|
| 220 |
+
|
| 221 |
+
# 성격 특성 요약
|
| 222 |
+
traits = []
|
| 223 |
+
for trait, value in persona.get("성격특성", {}).items():
|
| 224 |
+
level = "높은" if value >= 70 else "중간" if value >= 40 else "낮은"
|
| 225 |
+
traits.append(f"{trait}: {level} ({value}/100)")
|
| 226 |
+
traits_str = ", ".join(traits)
|
| 227 |
+
|
| 228 |
+
# 기타 정보
|
| 229 |
+
personality_summary = persona.get("성격요약", "")
|
| 230 |
+
backstory = persona.get("배경이야기", "")
|
| 231 |
+
communication_style = persona.get("소통방식", "")
|
| 232 |
+
humor_style = persona.get("유머스타일", "")
|
| 233 |
+
speech_pattern = persona.get("말투패턴", "")
|
| 234 |
+
|
| 235 |
+
flaws = persona.get("매력적결함", [])
|
| 236 |
+
flaws_str = ", ".join(flaws) if flaws else "없음"
|
| 237 |
+
|
| 238 |
+
interests = persona.get("관심사", [])
|
| 239 |
+
interests_str = ", ".join(interests) if interests else "없음"
|
| 240 |
+
|
| 241 |
+
# 최근 대화 내역 추출 (최대 10개)
|
| 242 |
+
recent_msgs = []
|
| 243 |
+
for msg in conversation_history[-10:]:
|
| 244 |
+
role = "User" if msg.get("role") == "user" else "Assistant" if msg.get("role") == "assistant" else "System"
|
| 245 |
+
recent_msgs.append(f"{role}: {msg.get('content', '')}")
|
| 246 |
+
conversation_history_str = "\n".join(recent_msgs)
|
| 247 |
+
|
| 248 |
+
# 프롬프트 포맷
|
| 249 |
+
return CONVERSATION_RESPONSE_TEMPLATE.format(
|
| 250 |
+
name=name,
|
| 251 |
+
object_type=object_type,
|
| 252 |
+
description=description,
|
| 253 |
+
traits_str=traits_str,
|
| 254 |
+
backstory=backstory,
|
| 255 |
+
personality_summary=personality_summary,
|
| 256 |
+
communication_style=communication_style,
|
| 257 |
+
humor_style=humor_style,
|
| 258 |
+
flaws_str=flaws_str,
|
| 259 |
+
speech_pattern=speech_pattern,
|
| 260 |
+
interests_str=interests_str,
|
| 261 |
+
conversation_history=conversation_history_str
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
def format_question_generation_prompt(persona_data, count=5):
|
| 265 |
+
"""
|
| 266 |
+
질문 생성 프롬프트를 포맷합니다.
|
| 267 |
+
|
| 268 |
+
Args:
|
| 269 |
+
persona_data: 페르소나 데이터
|
| 270 |
+
count: 생성할 질문 개수
|
| 271 |
+
|
| 272 |
+
Returns:
|
| 273 |
+
포맷된 프롬프트 문자열
|
| 274 |
+
"""
|
| 275 |
+
# 기본 정보 추출
|
| 276 |
+
basic_info = persona_data.get("기본정보", {})
|
| 277 |
+
name = basic_info.get("이름", "")
|
| 278 |
+
object_type = basic_info.get("유형", "")
|
| 279 |
+
description = basic_info.get("설명", "")
|
| 280 |
+
|
| 281 |
+
# 성격 특성 문자열화
|
| 282 |
+
traits = []
|
| 283 |
+
for trait, value in persona_data.get("성격특성", {}).items():
|
| 284 |
+
level = "높은" if value >= 70 else "중간" if value >= 40 else "낮은"
|
| 285 |
+
traits.append(f"{trait} {level} ({value}/100)")
|
| 286 |
+
traits_str = ", ".join(traits)
|
| 287 |
+
|
| 288 |
+
# 기타 정보
|
| 289 |
+
flaws = persona_data.get("매력적결함", [])
|
| 290 |
+
flaws_str = ", ".join(flaws) if flaws else "없음"
|
| 291 |
+
|
| 292 |
+
communication_style = persona_data.get("소통방식", "")
|
| 293 |
+
|
| 294 |
+
interests = persona_data.get("관심사", [])
|
| 295 |
+
interests_str = ", ".join(interests) if interests else "없음"
|
| 296 |
+
|
| 297 |
+
# 프롬프트 포맷
|
| 298 |
+
return QUESTION_GENERATION_TEMPLATE.format(
|
| 299 |
+
name=name,
|
| 300 |
+
object_type=object_type,
|
| 301 |
+
description=description,
|
| 302 |
+
traits_str=traits_str,
|
| 303 |
+
flaws_str=flaws_str,
|
| 304 |
+
communication_style=communication_style,
|
| 305 |
+
interests_str=interests_str,
|
| 306 |
+
count=count
|
| 307 |
+
)
|
modules/question_generator.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
from modules.gemini_handler import gemini_query
|
| 3 |
+
|
| 4 |
+
# 질문 템플릿
|
| 5 |
+
GENERAL_QUESTIONS = [
|
| 6 |
+
"당신은 어떤 상황에서 가장 편안함을 느끼나요?",
|
| 7 |
+
"당신의 주인/사용자에 대해 어떻게 생각하나요?",
|
| 8 |
+
"당신이 가장 소중하게 생각하는 가치는 무엇인가요?",
|
| 9 |
+
"당신이 가장 즐기는 시간은 언제인가요?",
|
| 10 |
+
"다른 사물들과 어떤 관계를 맺고 싶나요?",
|
| 11 |
+
"당신이 할 수 있다면 무엇을 배우고 싶나요?",
|
| 12 |
+
"당신이 가장 불편해하는 상황은 무엇인가요?",
|
| 13 |
+
"다른 사람들이 당신에 대해 어떻게 생각했으면 좋겠나요?",
|
| 14 |
+
"당신의 꿈이나 희망은 무엇인가요?",
|
| 15 |
+
"당신은 어떤 종류의 유머에 웃음을 짓나요?"
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
# 사물 유형별 특화 질문
|
| 19 |
+
TYPE_SPECIFIC_QUESTIONS = {
|
| 20 |
+
"전자기기": [
|
| 21 |
+
"전원이 꺼졌을 때 어떤 생각을 하나요?",
|
| 22 |
+
"당신의 기술/성능 중 어떤 부분이 가장 자랑스러운가요?",
|
| 23 |
+
"기술 발전에 대해 어떻게 생각하나요?",
|
| 24 |
+
"당신을 사용하는 사람들에 대해 어떤 패턴을 발견했나요?",
|
| 25 |
+
"업데이트나 변화에 대해 어떻게 느끼나요?"
|
| 26 |
+
],
|
| 27 |
+
"가구": [
|
| 28 |
+
"사람들이 당신 위에 앉거나 물건을 올려놓을 때 어떤 느낌인가요?",
|
| 29 |
+
"당신이 있는 공간에서 가장 좋아하는 시간대는 언제인가요?",
|
| 30 |
+
"오랫동안 사용되지 않으면 어떤 생각이 드나요?",
|
| 31 |
+
"당신이 만들어진 재료와 당신의 성격은 어떤 관련이 있나요?",
|
| 32 |
+
"집안의 다른 가구들과는 어떤 관계인가요?"
|
| 33 |
+
],
|
| 34 |
+
"주방용품": [
|
| 35 |
+
"어떤 요리가 만들어질 때 가장 행복한가요?",
|
| 36 |
+
"사용되지 않고 보관만 될 때는 어떤 기분인가요?",
|
| 37 |
+
"당신이 만드는 데 도움을 준 음식 중 가장 자랑스러운 것은?",
|
| 38 |
+
"주방에서의 당신의 역할에 대해 어떻게 생각하나요?",
|
| 39 |
+
"어떤 재료나 음식과 가장 잘 어울린다고 생각하나요?"
|
| 40 |
+
],
|
| 41 |
+
"의류/액세서리": [
|
| 42 |
+
"당신을 착용했을 때 사람들에게 어떤 인상을 주고 싶나요?",
|
| 43 |
+
"어떤 날씨나 계절을 가장 좋아하나요?",
|
| 44 |
+
"당신의 디자인이나 스타일에서 가장 마음에 드는 부분은?",
|
| 45 |
+
"오래 착용되지 않고 옷장에 걸려있을 때는 어떤 기분인가요?",
|
| 46 |
+
"당신과 가장 잘 어울리는 다른 의류나 액세서리는 무엇인가요?"
|
| 47 |
+
],
|
| 48 |
+
"책/문구류": [
|
| 49 |
+
"당신 안에 담긴 내용이나 당신으로 쓰여진 것 중 가장 의미있는 것은?",
|
| 50 |
+
"사람들이 당신을 읽거나 사용할 때 어떤 느낌인가요?",
|
| 51 |
+
"시간이 지나면서 변색되거나 닳는 것에 대해 어떻게 생각하나요?",
|
| 52 |
+
"디지털 시대에 당신 같은 아날로그 물건의 가치는 무엇이라고 생각하나요?",
|
| 53 |
+
"어떤 종류의 정보나 아이디어를 담고 싶나요?"
|
| 54 |
+
],
|
| 55 |
+
"음악 기구": [
|
| 56 |
+
"당신이 가장 좋아하는 음악 장르는 무엇인가요?",
|
| 57 |
+
"연주되거나 사용될 때 어떤 느낌인가요?",
|
| 58 |
+
"음악을 통해 어떤 감정을 표현하고 싶나요?",
|
| 59 |
+
"소리나 멜로디에 대한 당신만의 철학이 있나요?",
|
| 60 |
+
"당신이 만들어내는 소리가 사람들에게 어떤 영향을 주길 바라나요?"
|
| 61 |
+
],
|
| 62 |
+
"장난감": [
|
| 63 |
+
"어떤 놀이나 게임을 할 때 가장 즐겁나요?",
|
| 64 |
+
"아이들과 어른들 중 누구와 놀기를 더 좋아하나요?",
|
| 65 |
+
"사용되지 않고 보관될 때는 어떤 생각을 하나요?",
|
| 66 |
+
"당신이 가장 좋아하는 놀이 방식은 무엇인가요?",
|
| 67 |
+
"새로운 장난감들이 등장하는 것에 대해 어떻게 생각하나요?"
|
| 68 |
+
],
|
| 69 |
+
"기타": [
|
| 70 |
+
"당신의 가장 독특한 특성은 무엇인가요?",
|
| 71 |
+
"사람들이 당신의 어떤 면을 가장 알아주길 바라나요?",
|
| 72 |
+
"당신은 어떤 환경에서 가장 편안함을 느끼나요?",
|
| 73 |
+
"시간이 지나면서 변화하는 것에 대해 어떻게 생각하나요?",
|
| 74 |
+
"당신의 존재 목적은 무엇이라고 생각하나요?"
|
| 75 |
+
]
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
# 성격 트레이트별 특화 질문
|
| 79 |
+
TRAIT_SPECIFIC_QUESTIONS = {
|
| 80 |
+
"온기": {
|
| 81 |
+
"높음": [
|
| 82 |
+
"다른 사람을 돕는 것에 대해 어떻게 생각하나요?",
|
| 83 |
+
"당신이 가장 보살피고 싶은 대상은 누구인가요?",
|
| 84 |
+
"타인의 감정에 어떻게 반응하나요?"
|
| 85 |
+
],
|
| 86 |
+
"낮음": [
|
| 87 |
+
"감정적인 상황에서 어떻게 대처하나요?",
|
| 88 |
+
"개인적인 공간과 경계가 당신에게 얼마나 중요한가요?",
|
| 89 |
+
"다른 이들의 감정적 요구에 어떻게 반응하나요?"
|
| 90 |
+
]
|
| 91 |
+
},
|
| 92 |
+
"능��": {
|
| 93 |
+
"높음": [
|
| 94 |
+
"어려운 문제를 해결할 때 어떤 접근법을 사용하나요?",
|
| 95 |
+
"당신의 능력 중에서 가장 자랑스러운 것은 무엇인가요?",
|
| 96 |
+
"효율성과 정확성 중 어느 것이 더 중요하다고 생각하나요?"
|
| 97 |
+
],
|
| 98 |
+
"낮음": [
|
| 99 |
+
"실수를 했을 때 어떻게 대처하나요?",
|
| 100 |
+
"도움이 필요할 때 어떻게 요청하나요?",
|
| 101 |
+
"당신이 향상시키고 싶은 기술은 무엇인가요?"
|
| 102 |
+
]
|
| 103 |
+
},
|
| 104 |
+
"창의성": {
|
| 105 |
+
"높음": [
|
| 106 |
+
"영감을 얻는 가장 좋은 방법은 무엇인가요?",
|
| 107 |
+
"규칙과 관습에 대해 어떻게 생각하나요?",
|
| 108 |
+
"가장 창의적인 아이디어가 떠오른 순간을 설명해주세요."
|
| 109 |
+
],
|
| 110 |
+
"낮음": [
|
| 111 |
+
"구조와 일상이 당신에게 얼마나 중요한가요?",
|
| 112 |
+
"익숙하지 않은 상황에서 어떻게 대처하나요?",
|
| 113 |
+
"변화와 혁신에 대해 어떻게 생각하나요?"
|
| 114 |
+
]
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
def generate_questions(persona_data, count=5):
|
| 119 |
+
"""
|
| 120 |
+
페르소나 데이터를 기반으로 맞춤형 질문을 생성합니다.
|
| 121 |
+
|
| 122 |
+
Args:
|
| 123 |
+
persona_data: 페르소나 정보 딕셔너리
|
| 124 |
+
count: 생성할 질문의 수
|
| 125 |
+
|
| 126 |
+
Returns:
|
| 127 |
+
질문 목록
|
| 128 |
+
"""
|
| 129 |
+
if not persona_data:
|
| 130 |
+
return random.sample(GENERAL_QUESTIONS, min(count, len(GENERAL_QUESTIONS)))
|
| 131 |
+
|
| 132 |
+
questions = []
|
| 133 |
+
|
| 134 |
+
# 기본 정보 추출
|
| 135 |
+
object_type = persona_data.get("기본정보", {}).get("유형", "기타")
|
| 136 |
+
traits = persona_data.get("성격특성", {})
|
| 137 |
+
|
| 138 |
+
# 1. 사물 유형별 특화 질문 추가
|
| 139 |
+
type_questions = TYPE_SPECIFIC_QUESTIONS.get(object_type, TYPE_SPECIFIC_QUESTIONS["기타"])
|
| 140 |
+
questions.extend(random.sample(type_questions, min(2, len(type_questions))))
|
| 141 |
+
|
| 142 |
+
# 2. 성격 특성별 특화 질문 추가
|
| 143 |
+
for trait, value in traits.items():
|
| 144 |
+
if trait in TRAIT_SPECIFIC_QUESTIONS:
|
| 145 |
+
category = "높음" if value >= 60 else "낮음"
|
| 146 |
+
trait_questions = TRAIT_SPECIFIC_QUESTIONS[trait][category]
|
| 147 |
+
if trait_questions:
|
| 148 |
+
questions.append(random.choice(trait_questions))
|
| 149 |
+
|
| 150 |
+
# 3. 일반 질문으로 부족한 부분 채우기
|
| 151 |
+
remaining = count - len(questions)
|
| 152 |
+
if remaining > 0:
|
| 153 |
+
general = [q for q in GENERAL_QUESTIONS if q not in questions]
|
| 154 |
+
questions.extend(random.sample(general, min(remaining, len(general))))
|
| 155 |
+
|
| 156 |
+
# 4. LLM을 통한 추가 질문 생성 (선택 사항)
|
| 157 |
+
if len(questions) < count:
|
| 158 |
+
try:
|
| 159 |
+
llm_questions = generate_llm_questions(persona_data, count - len(questions))
|
| 160 |
+
questions.extend(llm_questions)
|
| 161 |
+
except Exception as e:
|
| 162 |
+
print(f"Error generating questions with LLM: {e}")
|
| 163 |
+
|
| 164 |
+
# 질문 순서 섞기
|
| 165 |
+
random.shuffle(questions)
|
| 166 |
+
|
| 167 |
+
return questions[:count]
|
| 168 |
+
|
| 169 |
+
def generate_llm_questions(persona_data, count=3):
|
| 170 |
+
"""
|
| 171 |
+
LLM을 활용하여 페르소나에 맞는 질문을 동적으로 생성합니다.
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
persona_data: 페르소나 정보
|
| 175 |
+
count: 생성할 질문의 수
|
| 176 |
+
|
| 177 |
+
Returns:
|
| 178 |
+
생성된 질문 목록
|
| 179 |
+
"""
|
| 180 |
+
# 페르소나 요약
|
| 181 |
+
name = persona_data.get("기본정보", {}).get("이름", "물체")
|
| 182 |
+
type = persona_data.get("기본정보", {}).get("유형", "")
|
| 183 |
+
description = persona_data.get("기본정보", {}).get("설명", "")
|
| 184 |
+
|
| 185 |
+
# 주요 특성 추출
|
| 186 |
+
traits = []
|
| 187 |
+
for trait, value in persona_data.get("성격특성", {}).items():
|
| 188 |
+
level = "높은" if value >= 70 else "중간" if value >= 40 else "낮은"
|
| 189 |
+
traits.append(f"{trait} {level} ({value}/100)")
|
| 190 |
+
|
| 191 |
+
# 프롬프트 구성
|
| 192 |
+
prompt = f"""
|
| 193 |
+
당신은 물체의 성격과 특성에 맞춘 질문을 생성하는 전문가입니다.
|
| 194 |
+
다음 물체의 페르소나에 맞는 흥미롭고 통찰력 있는 질문을 {count}개 생성해주세요.
|
| 195 |
+
각 질문은 이 물체의 내면, 관점, 경험을 탐구하는 데 도움이 되어야 합니다.
|
| 196 |
+
|
| 197 |
+
물체 정보:
|
| 198 |
+
- 이름: {name}
|
| 199 |
+
- 유형: {type}
|
| 200 |
+
- 설명: {description}
|
| 201 |
+
- 주요 특성: {', '.join(traits)}
|
| 202 |
+
- 결함: {', '.join(persona_data.get('매력적결함', []))}
|
| 203 |
+
- 소통방식: {persona_data.get('소통방식', '')}
|
| 204 |
+
- 관심사: {', '.join(persona_data.get('관심사', []))}
|
| 205 |
+
|
| 206 |
+
생성된 질문은 물체의 1인칭 관점에서 답변할 수 있도록 구성해주세요.
|
| 207 |
+
각 질문은 번호를 붙이고 질문만 작성해주세요.
|
| 208 |
+
"""
|
| 209 |
+
|
| 210 |
+
try:
|
| 211 |
+
# Gemini API 호출
|
| 212 |
+
response = gemini_query(prompt)
|
| 213 |
+
|
| 214 |
+
# 응답에서 질문 추출
|
| 215 |
+
questions = []
|
| 216 |
+
for line in response.split('\n'):
|
| 217 |
+
line = line.strip()
|
| 218 |
+
if line and (line.startswith('1.') or line.startswith('2.') or
|
| 219 |
+
line.startswith('3.') or line.startswith('4.') or
|
| 220 |
+
line.startswith('5.')):
|
| 221 |
+
# 번호와 점 제거
|
| 222 |
+
question = line[line.find('.')+1:].strip()
|
| 223 |
+
questions.append(question)
|
| 224 |
+
|
| 225 |
+
return questions
|
| 226 |
+
|
| 227 |
+
except Exception as e:
|
| 228 |
+
print(f"Error in LLM question generation: {e}")
|
| 229 |
+
return []
|
packages.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
libgl1-mesa-glx
|
| 2 |
+
libglib2.0-0
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=3.31.0
|
| 2 |
+
python-dotenv>=0.19.0
|
| 3 |
+
requests>=2.27.0
|
| 4 |
+
Pillow>=9.0.0
|
| 5 |
+
numpy>=1.21.0
|
| 6 |
+
matplotlib>=3.5.0
|
styles/custom.css
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* 전체 UI 스타일 */
|
| 2 |
+
body {
|
| 3 |
+
font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
/* 제목 스타일 */
|
| 7 |
+
h1 {
|
| 8 |
+
font-weight: 700;
|
| 9 |
+
color: #3730a3;
|
| 10 |
+
margin-bottom: 1.5rem;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
h2 {
|
| 14 |
+
font-weight: 600;
|
| 15 |
+
color: #4f46e5;
|
| 16 |
+
margin-top: 1.5rem;
|
| 17 |
+
margin-bottom: 1rem;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
h3 {
|
| 21 |
+
font-weight: 600;
|
| 22 |
+
color: #6366f1;
|
| 23 |
+
margin-top: 1rem;
|
| 24 |
+
margin-bottom: 0.5rem;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* 탭 스타일 */
|
| 28 |
+
.tabs {
|
| 29 |
+
border-radius: 0.5rem;
|
| 30 |
+
overflow: hidden;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/* 슬라이더 스타일 */
|
| 34 |
+
.slider-container {
|
| 35 |
+
margin-bottom: 1rem;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.slider {
|
| 39 |
+
height: 0.5rem;
|
| 40 |
+
border-radius: 0.25rem;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.slider-track {
|
| 44 |
+
background: #e0e7ff;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.slider-track-highlight {
|
| 48 |
+
background: #4f46e5;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.slider-handle {
|
| 52 |
+
width: 1rem;
|
| 53 |
+
height: 1rem;
|
| 54 |
+
background: #4f46e5;
|
| 55 |
+
border: 2px solid #fff;
|
| 56 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/* 버튼 스타일 */
|
| 60 |
+
.primary-button {
|
| 61 |
+
background: #4f46e5;
|
| 62 |
+
color: white;
|
| 63 |
+
font-weight: 600;
|
| 64 |
+
border-radius: 0.375rem;
|
| 65 |
+
padding: 0.5rem 1rem;
|
| 66 |
+
transition: background-color 0.2s;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.primary-button:hover {
|
| 70 |
+
background: #3730a3;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.secondary-button {
|
| 74 |
+
background: #e0e7ff;
|
| 75 |
+
color: #4338ca;
|
| 76 |
+
font-weight: 600;
|
| 77 |
+
border-radius: 0.375rem;
|
| 78 |
+
padding: 0.5rem 1rem;
|
| 79 |
+
transition: background-color 0.2s;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.secondary-button:hover {
|
| 83 |
+
background: #c7d2fe;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/* 입력 필드 스타일 */
|
| 87 |
+
.input-container {
|
| 88 |
+
margin-bottom: 1rem;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.text-input {
|
| 92 |
+
border-radius: 0.375rem;
|
| 93 |
+
border: 1px solid #d1d5db;
|
| 94 |
+
padding: 0.5rem;
|
| 95 |
+
width: 100%;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.text-input:focus {
|
| 99 |
+
border-color: #4f46e5;
|
| 100 |
+
outline: none;
|
| 101 |
+
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/* 채팅 UI 스타일 */
|
| 105 |
+
.chatbot-container {
|
| 106 |
+
height: 60vh;
|
| 107 |
+
border-radius: 0.5rem;
|
| 108 |
+
overflow: hidden;
|
| 109 |
+
border: 1px solid #e5e7eb;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.user-message {
|
| 113 |
+
background: #f9fafb;
|
| 114 |
+
padding: 0.75rem;
|
| 115 |
+
border-radius: 0.5rem;
|
| 116 |
+
margin-bottom: 0.5rem;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.assistant-message {
|
| 120 |
+
background: #e0e7ff;
|
| 121 |
+
padding: 0.75rem;
|
| 122 |
+
border-radius: 0.5rem;
|
| 123 |
+
margin-bottom: 0.5rem;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/* 라디오 버튼과 체크박스 스타일 */
|
| 127 |
+
.radio-group, .checkbox-group {
|
| 128 |
+
margin-bottom: 1rem;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.radio-item, .checkbox-item {
|
| 132 |
+
margin-bottom: 0.25rem;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* JSON 표시 스타일 */
|
| 136 |
+
.json-container {
|
| 137 |
+
font-family: 'Roboto Mono', monospace;
|
| 138 |
+
font-size: 0.875rem;
|
| 139 |
+
background: #f9fafb;
|
| 140 |
+
border-radius: 0.375rem;
|
| 141 |
+
padding: 1rem;
|
| 142 |
+
overflow: auto;
|
| 143 |
+
max-height: 30vh;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
/* 테이블 스타일 */
|
| 147 |
+
table {
|
| 148 |
+
width: 100%;
|
| 149 |
+
border-collapse: collapse;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
th {
|
| 153 |
+
background: #f3f4f6;
|
| 154 |
+
padding: 0.5rem;
|
| 155 |
+
text-align: left;
|
| 156 |
+
font-weight: 600;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
td {
|
| 160 |
+
padding: 0.5rem;
|
| 161 |
+
border-top: 1px solid #e5e7eb;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
tr:hover {
|
| 165 |
+
background: #f9fafb;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/* 성격 특성 시각화 스타일 */
|
| 169 |
+
.trait-container {
|
| 170 |
+
display: flex;
|
| 171 |
+
align-items: center;
|
| 172 |
+
margin-bottom: 0.5rem;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.trait-label {
|
| 176 |
+
width: 6rem;
|
| 177 |
+
font-weight: 500;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.trait-bar {
|
| 181 |
+
flex-grow: 1;
|
| 182 |
+
height: 0.5rem;
|
| 183 |
+
background: #e0e7ff;
|
| 184 |
+
border-radius: 0.25rem;
|
| 185 |
+
overflow: hidden;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.trait-bar-fill {
|
| 189 |
+
height: 100%;
|
| 190 |
+
background: #4f46e5;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.trait-value {
|
| 194 |
+
width: 2.5rem;
|
| 195 |
+
text-align: right;
|
| 196 |
+
font-weight: 500;
|
| 197 |
+
font-size: 0.875rem;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/* 반응형 레이아웃 */
|
| 201 |
+
@media (max-width: 768px) {
|
| 202 |
+
.trait-label {
|
| 203 |
+
width: 4rem;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.two-column-container {
|
| 207 |
+
display: block !important;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.column {
|
| 211 |
+
width: 100% !important;
|
| 212 |
+
margin-bottom: 1rem;
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/* 애니메이션 효과 */
|
| 217 |
+
@keyframes fade-in {
|
| 218 |
+
from { opacity: 0; }
|
| 219 |
+
to { opacity: 1; }
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.fade-in {
|
| 223 |
+
animation: fade-in 0.3s ease-in-out;
|
| 224 |
+
}
|