haepada commited on
Commit
5581923
·
verified ·
1 Parent(s): 78ee288

Upload 12 files

Browse files
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
+ }