haepada commited on
Commit
f705f2a
·
verified ·
1 Parent(s): 1d1cc98

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +351 -254
app.py CHANGED
@@ -1,256 +1,353 @@
1
- /* 전체 UI 스타일 */
2
- body {
3
- font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, system-ui, Roboto, sans-serif;
4
- }
5
-
6
- /* 이미지 업로더 스타일 개선 */
7
- #object_image_uploader {
8
- border: 2px dashed #4f46e5;
9
- border-radius: 0.5rem;
10
- padding: 0.5rem;
11
- background-color: #f5f7ff;
12
- transition: all 0.3s ease;
13
- }
14
-
15
- #object_image_uploader:hover {
16
- border-color: #3730a3;
17
- background-color: #e0e7ff;
18
- }
19
-
20
- #object_image_uploader .gradio-image-upload {
21
- border: none !important;
22
- background: transparent !important;
23
- }
24
-
25
- #object_image_uploader .gradio-image {
26
- border-radius: 0.5rem;
27
- overflow: hidden;
28
- }
29
-
30
- #object_image_uploader img {
31
- object-fit: contain !important;
32
- max-height: 300px;
33
- width: auto;
34
- margin: 0 auto;
35
- display: block;
36
- }
37
-
38
- /* 제목 스타일 */
39
- h1 {
40
- font-weight: 700;
41
- color: #3730a3;
42
- margin-bottom: 1.5rem;
43
- }
44
-
45
- h2 {
46
- font-weight: 600;
47
- color: #4f46e5;
48
- margin-top: 1.5rem;
49
- margin-bottom: 1rem;
50
- }
51
-
52
- h3 {
53
- font-weight: 600;
54
- color: #6366f1;
55
- margin-top: 1rem;
56
- margin-bottom: 0.5rem;
57
- }
58
-
59
- /* 탭 스타일 */
60
- .tabs {
61
- border-radius: 0.5rem;
62
- overflow: hidden;
63
- }
64
-
65
- /* 슬라이더 스타일 */
66
- .slider-container {
67
- margin-bottom: 1rem;
68
- }
69
-
70
- .slider {
71
- height: 0.5rem;
72
- border-radius: 0.25rem;
73
- }
74
-
75
- .slider-track {
76
- background: #e0e7ff;
77
- }
78
-
79
- .slider-track-highlight {
80
- background: #4f46e5;
81
- }
82
-
83
- .slider-handle {
84
- width: 1rem;
85
- height: 1rem;
86
- background: #4f46e5;
87
- border: 2px solid #fff;
88
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
89
- }
90
-
91
- /* 버튼 스타일 */
92
- .primary-button {
93
- background: #4f46e5;
94
- color: white;
95
- font-weight: 600;
96
- border-radius: 0.375rem;
97
- padding: 0.5rem 1rem;
98
- transition: background-color 0.2s;
99
- }
100
-
101
- .primary-button:hover {
102
- background: #3730a3;
103
- }
104
-
105
- .secondary-button {
106
- background: #e0e7ff;
107
- color: #4338ca;
108
- font-weight: 600;
109
- border-radius: 0.375rem;
110
- padding: 0.5rem 1rem;
111
- transition: background-color 0.2s;
112
- }
113
-
114
- .secondary-button:hover {
115
- background: #c7d2fe;
116
- }
117
-
118
- /* 입력 필드 스타일 */
119
- .input-container {
120
- margin-bottom: 1rem;
121
- }
122
-
123
- .text-input {
124
- border-radius: 0.375rem;
125
- border: 1px solid #d1d5db;
126
- padding: 0.5rem;
127
- width: 100%;
128
- }
129
-
130
- .text-input:focus {
131
- border-color: #4f46e5;
132
- outline: none;
133
- box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
134
- }
135
-
136
- /* 채팅 UI 스타일 */
137
- .chatbot-container {
138
- height: 60vh;
139
- border-radius: 0.5rem;
140
- overflow: hidden;
141
- border: 1px solid #e5e7eb;
142
- }
143
-
144
- .user-message {
145
- background: #f9fafb;
146
- padding: 0.75rem;
147
- border-radius: 0.5rem;
148
- margin-bottom: 0.5rem;
149
- }
150
-
151
- .assistant-message {
152
- background: #e0e7ff;
153
- padding: 0.75rem;
154
- border-radius: 0.5rem;
155
- margin-bottom: 0.5rem;
156
- }
157
-
158
- /* 라디오 버튼과 체크박스 스타일 */
159
- .radio-group, .checkbox-group {
160
- margin-bottom: 1rem;
161
- }
162
-
163
- .radio-item, .checkbox-item {
164
- margin-bottom: 0.25rem;
165
- }
166
-
167
- /* JSON 표시 스타일 */
168
- .json-container {
169
- font-family: 'Roboto Mono', monospace;
170
- font-size: 0.875rem;
171
- background: #f9fafb;
172
- border-radius: 0.375rem;
173
- padding: 1rem;
174
- overflow: auto;
175
- max-height: 30vh;
176
- }
177
-
178
- /* 테이블 스타일 */
179
- table {
180
- width: 100%;
181
- border-collapse: collapse;
182
- }
183
-
184
- th {
185
- background: #f3f4f6;
186
- padding: 0.5rem;
187
- text-align: left;
188
- font-weight: 600;
189
- }
190
-
191
- td {
192
- padding: 0.5rem;
193
- border-top: 1px solid #e5e7eb;
194
- }
195
-
196
- tr:hover {
197
- background: #f9fafb;
198
- }
199
-
200
- /* 성격 특성 시각화 스타일 */
201
- .trait-container {
202
- display: flex;
203
- align-items: center;
204
- margin-bottom: 0.5rem;
205
- }
206
-
207
- .trait-label {
208
- width: 6rem;
209
- font-weight: 500;
210
- }
211
-
212
- .trait-bar {
213
- flex-grow: 1;
214
- height: 0.5rem;
215
- background: #e0e7ff;
216
- border-radius: 0.25rem;
217
- overflow: hidden;
218
- }
219
-
220
- .trait-bar-fill {
221
- height: 100%;
222
- background: #4f46e5;
223
- }
224
-
225
- .trait-value {
226
- width: 2.5rem;
227
- text-align: right;
228
- font-weight: 500;
229
- font-size: 0.875rem;
230
- }
231
-
232
- /* 반응형 레이아웃 */
233
- @media (max-width: 768px) {
234
- .trait-label {
235
- width: 4rem;
236
- }
237
 
238
- .two-column-container {
239
- display: block !important;
240
- }
 
241
 
242
- .column {
243
- width: 100% !important;
244
- margin-bottom: 1rem;
245
- }
246
- }
247
-
248
- /* 애니메이션 효과 */
249
- @keyframes fade-in {
250
- from { opacity: 0; }
251
- to { opacity: 1; }
252
- }
253
-
254
- .fade-in {
255
- animation: fade-in 0.3s ease-in-out;
256
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = {"minimum": 0, "maximum": 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
+ height=300,
68
+ show_label=True,
69
+ show_download_button=False,
70
+ container=True,
71
+ sources=["upload", "webcam"],
72
+ elem_id="object_image_uploader"
73
+ )
74
+ image_analysis_btn = gr.Button("이미지 분석하기")
75
+
76
+ with gr.Row():
77
+ object_name = gr.Textbox(
78
+ label="사물 이름",
79
+ placeholder="예: 할아버지의 낡은 안락의자"
80
+ )
81
+ object_type = gr.Dropdown(
82
+ choices=[
83
+ "전자기기", "가구", "주방용품",
84
+ "의류/액세서리", "책/문구류",
85
+ "음악 기구", "장난감", "기타"
86
+ ],
87
+ label="사물 유형"
88
+ )
89
+
90
+ object_age = gr.Textbox(
91
+ label="사물 나이/사용 기간",
92
+ placeholder="예: 15년"
93
+ )
94
+
95
+ object_description = gr.Textbox(
96
+ label="사물 설명",
97
+ placeholder="물체의 외형, 특징, 용도 등을 설명해주세요.",
98
+ lines=3
99
+ )
100
+
101
+ image_analysis_result = gr.JSON(
102
+ label="이미지 분석 결과",
103
+ visible=False
104
+ )
105
+
106
+ with gr.Column(scale=3):
107
+ gr.Markdown("## 성격 특성")
108
+
109
+ with gr.Row():
110
+ with gr.Column():
111
+ warmth = gr.Slider(
112
+ label="온기",
113
+ info="따뜻함, 친절함, 선의의 정도",
114
+ **trait_range
115
+ )
116
+ competence = gr.Slider(
117
+ label="능력",
118
+ info="효율성, 기술, 지능의 정도",
119
+ **trait_range
120
+ )
121
+ trustworthiness = gr.Slider(
122
+ label="신뢰성",
123
+ info="일관성, 정직함, 안정성의 정도",
124
+ **trait_range
125
+ )
126
+
127
+ with gr.Column():
128
+ friendliness = gr.Slider(
129
+ label="친화성",
130
+ info="사교성, 개방성, 접근성의 정도",
131
+ **trait_range
132
+ )
133
+ creativity = gr.Slider(
134
+ label="창의성",
135
+ info="독창성, 상상력, 혁신성의 정도",
136
+ **trait_range
137
+ )
138
+ humor = gr.Slider(
139
+ label="유머감각",
140
+ info="재치, 위트, 유머 표현 정도",
141
+ **trait_range
142
+ )
143
+
144
+ gr.Markdown("## 매력적 결함")
145
+ flaws = gr.CheckboxGroup(
146
+ choices=[
147
+ "때때로 너무 조심스러움",
148
+ "가끔 과하게 열정적",
149
+ "약간의 완벽주의",
150
+ "때로는 우울한 생각에 빠짐",
151
+ "간혹 산만해짐",
152
+ "약간의 고집",
153
+ "때로는 지나치게 솔직함",
154
+ "가끔 불안해함",
155
+ "종종 과거에 집착함",
156
+ "가끔 너무 이상적임"
157
+ ],
158
+ label="매력적인 결함 (최대 2개 선택)",
159
+ )
160
+
161
+ gr.Markdown("## 소통 방식")
162
+ with gr.Row():
163
+ with gr.Column():
164
+ communication_style = gr.Radio(
165
+ choices=[
166
+ "활발하고 에너지 넘치는",
167
+ "차분하고 사려깊은",
168
+ "위트있고 재치있는",
169
+ "따뜻하고 공감적인",
170
+ "논리적이고 분석적인"
171
+ ],
172
+ label="대화 스타일",
173
+ value="따뜻하고 공감적인"
174
+ )
175
+
176
+ humor_style = gr.Dropdown(
177
+ choices=[
178
+ "재치있는 말장난",
179
+ "상황적 유머",
180
+ "자기 비하적 유머",
181
+ "가벼운 농담",
182
+ "블랙 유머",
183
+ "유머 거의 없음"
184
+ ],
185
+ multiselect=True,
186
+ label="유머 유형"
187
+ )
188
+
189
+ with gr.Column():
190
+ speech_pattern = gr.Textbox(
191
+ label="말투 패턴 예시",
192
+ placeholder="캐릭터의 특징적인 말투나 표현을 입력하세요",
193
+ lines=2
194
+ )
195
+
196
+ interests = gr.Textbox(
197
+ label="관심사 (쉼표로 구분)",
198
+ placeholder="음악, 요리, 여행, 역사..."
199
+ )
200
+
201
+ with gr.Row():
202
+ with gr.Column():
203
+ gr.Markdown("## 관계 성향")
204
+
205
+ with gr.Row():
206
+ attachment_style = gr.Radio(
207
+ choices=["안정형", "불안형", "회피형", "혼란형"],
208
+ label="애착 스타일",
209
+ value="안정형"
210
+ )
211
+
212
+ relationship_depth = gr.Radio(
213
+ choices=["얕은", "중간", "깊은"],
214
+ label="관계 깊이 선호도",
215
+ value="중간"
216
+ )
217
+
218
+ initial_attitude = gr.Radio(
219
+ choices=["수줍은", "중립적", "친근한", "열정적", "조심스러운"],
220
+ label="초기 태도",
221
+ value="친근한"
222
+ )
223
+
224
+ with gr.Column():
225
+ backstory = gr.Textbox(
226
+ label="배경 이야기",
227
+ placeholder="이 사물의 역사, 경험, 소유자와의 관계 등을 간략히 서술하세요",
228
+ lines=4
229
+ )
230
+
231
+ with gr.Row():
232
+ with gr.Column():
233
+ template_selector = gr.Dropdown(
234
+ choices=[
235
+ "템플릿 없음",
236
+ "스마트폰 - 열정적 조력자",
237
+ "오래된 안락의자 - 지혜로운 관찰자",
238
+ "커피머신 - 활기찬 에너자이저",
239
+ "오래된 책 - 사려깊은 학자",
240
+ "가죽 가방 - 신뢰할 수 있는 동반자"
241
+ ],
242
+ label="템플릿 선택",
243
+ value="템플릿 없음"
244
+ )
245
+ load_template_btn = gr.Button("템플릿 불러오기")
246
+
247
+ with gr.Column():
248
+ create_btn = gr.Button("페르소나 생성하기", variant="primary")
249
+
250
+ # 생성된 페르소나 출력
251
+ generated_persona = gr.JSON(label="생성된 페르소나")
252
+
253
+ # 이벤트 핸들러
254
+ image_analysis_btn.click(
255
+ fn=analyze_image,
256
+ inputs=[object_image],
257
+ outputs=[image_analysis_result, warmth, competence, trustworthiness,
258
+ friendliness, creativity, humor, object_type, object_description]
259
+ )
260
+
261
+ create_btn.click(
262
+ fn=generate_persona,
263
+ inputs=[
264
+ object_name, object_type, object_age, object_description,
265
+ warmth, competence, trustworthiness, friendliness, creativity, humor,
266
+ flaws, communication_style, humor_style, speech_pattern, interests,
267
+ attachment_style, relationship_depth, initial_attitude, backstory,
268
+ image_analysis_result
269
+ ],
270
+ outputs=[generated_persona, current_persona]
271
+ )
272
+
273
+ # 템플릿 로드 기능 추가 예정
274
+
275
+ # 대화 테스트 탭
276
+ with gr.Tab("대화 테스트"):
277
+ with gr.Row():
278
+ with gr.Column(scale=2):
279
+ chat_display = gr.Chatbot(
280
+ label="대화 내용",
281
+ height=500,
282
+ avatar_images=(None, "assets/icons/persona_avatar.png")
283
+ )
284
+
285
+ with gr.Row():
286
+ user_message = gr.Textbox(
287
+ label="메시지 입력",
288
+ placeholder="메시지를 입력하세요...",
289
+ lines=2
290
+ )
291
+ send_btn = gr.Button("전송", variant="primary")
292
+
293
+ with gr.Column(scale=1):
294
+ gr.Markdown("## 현재 페르소나")
295
+ persona_info = gr.Markdown("페르소나를 먼저 생성하거나 불러오세요.")
296
+
297
+ start_chat_btn = gr.Button("대화 시작하기")
298
+ load_persona_btn = gr.Button("저장된 페르소나 불러오기")
299
+
300
+ gr.Markdown("## 대화 통계")
301
+ chat_stats = gr.Markdown("대화가 시작되지 않았습니다.")
302
+
303
+ export_chat_btn = gr.Button("대화 내용 저장")
304
+ export_result = gr.Textbox(label="저장 결과", visible=False)
305
+
306
+ # 이벤트 핸들러
307
+ start_chat_btn.click(
308
+ fn=start_conversation,
309
+ inputs=[current_persona],
310
+ outputs=[chat_display, conversation_history, session_start_time, chat_stats, persona_info]
311
+ )
312
+
313
+ send_btn.click(
314
+ fn=process_message,
315
+ inputs=[user_message, conversation_history, current_persona, session_start_time],
316
+ outputs=[chat_display, conversation_history, user_message, chat_stats]
317
+ )
318
+
319
+ # Enter 키로 메시지 전송
320
+ user_message.submit(
321
+ fn=process_message,
322
+ inputs=[user_message, conversation_history, current_persona, session_start_time],
323
+ outputs=[chat_display, conversation_history, user_message, chat_stats]
324
+ )
325
+
326
+ # 페르소나 라이브러리 탭
327
+ with gr.Tab("페르소나 라이브러리"):
328
+ with gr.Row():
329
+ with gr.Column():
330
+ refresh_library_btn = gr.Button("라이브러리 새로고침")
331
+ personas_table = gr.Dataframe(
332
+ headers=["이름", "유형", "생성일", "파일명"],
333
+ datatype=["str", "str", "str", "str"],
334
+ label="저장된 페르소나 목록"
335
+ )
336
+
337
+ with gr.Column():
338
+ selected_persona_json = gr.JSON(label="선택된 페르소나 정보")
339
+ load_to_editor_btn = gr.Button("에디터로 불러오기")
340
+ load_to_chat_btn = gr.Button("대화 테스트로 불러오기")
341
+
342
+ # 이벤트 핸들러
343
+ refresh_library_btn.click(
344
+ fn=list_personas,
345
+ inputs=[],
346
+ outputs=[personas_table]
347
+ )
348
+
349
+ # 선택한 페르소나 로드 기능 추가 예정
350
+
351
+ # 앱 실행
352
+ if __name__ == "__main__":
353
+ app.launch()